C#:
using Newtonsoft.Json;
using System;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core;
using ProtoBuf;
using Facepunch;
using Oxide.Plugins.SimpleStatusExtensionMethods;
using System.Linq;
using System.Globalization;
using System.Reflection;
using Oxide.Game.Rust.Cui;
using Oxide.Core.Plugins;
using System.Collections.Generic;
using Network;
using UnityEngine;
namespace Oxide.Plugins {
[Info("SimpleStatus", "mr01sam & dFxPhoeniX", "1.3.1")]
[Description("Allows plugins to add custom status displays for the UI.")]
partial class SimpleStatus : CovalencePlugin {
/* // Changelog
[v1.2.0]
-SimpleStatus now works together with AdvancedStatus. You can now have both installed on your server and they wont overlap with each other. Might be a few bugs with this integration, just report them if you see them.
-Added progress bars, which mimic the style of the health/thirst/hunger/vehicle durability bars
-Added new hook for OnSimpleStatusReady that is called after the plugin has finished loading and is called when a plugin that uses SimpleStatus is loaded, ideal for initializing statuses
-Added a rank property that can determine the priority of listing custom statuses
-Updated some API endpoints, old ones still exist but the new ones are recommended to use
-Fixed issue where statuses wouldnt account for horse stamina
-Updated rendering so it should be quicker to redraw
[v1.2.1]
-Fixed issue where statuses would be invisible after dying
-Fixed issue where holding a planner would mess up the statuses
[v1.2.2]
-Fixed issue where the SetColor api wouldnt work
-Fixed issue where statuses would appear at the wrong index if the color was changed
-Fixed issue where the title/text woutuld not be translated if changed
[v1.2.3]
-Fixed issue where statuses would not load properly if you were dead and tried to reconnect
-Statuses now disappear visually when you die and sleep
[v1.2.4]
-Fixed null reference issue on PlayerSleepEnded hook
-Fixed issue where hiding the status would cause it to not come back
[v1.2.5]
-Fixed issue where some statuses would not redraw after being removed once
[v1.2.6]
-Fixed issue where some statuses would not show up after respawning in a bed/sleeping bag
-Removed OnSimpleStatusReady hook, I recommend just doing this logic in the ServerInitialized like before
[v1.2.7]
-Brought OnSimpleStatusReady back to not break plugins
-Fixed issue where hooks were not being called, causing statuses not to show up
[v1.2.8]
-Fixed issue where OnCollectiblePickup would sometimes throw an error
-Fixed issue where statuses would not realign properly when using a stamina based vehicle, like bikes
-Fixed issue of statuses overlapping when you build
-Fixed bug with AdvancedStatus integration where SetText would not properly update the SubText color property
[v1.2.9]
-Fix for 3/6/2025 Rust update
[v1.2.10]
-Fixed integration with AdvancedStatus
-Adjusted status height to match new vanilla height
-Fixed overlapping status when holding shield
[v1.2.11]
-Fixed issue where background color would be white when calling CreateStatus for carbon users
[v1.2.12]
-Improved AdvancedStatus integration
-Fixed several issues with status syncing when AdvancedStatus is installed
-Fixed background, icon, and icon color not updating correctly with AdvancedStatus
-Fixed statuses not properly hiding/showing with the toggle command when AdvancedStatus is installed
-Fixed status refresh issues after sleep/wake
[v1.2.19]
-Fixed custom statuses briefly overlapping native Rust HUD rows
-Added immediate two-stage layout refreshes for crafting, item notifications, building actions, active-item changes, mounting, and dismounting
-Updated crafting tracking for current Rust versions without using the removed ItemCraftTask.owner field
-Added exact note.inv tracking for native Rust inventory notification rows
-Updated standalone native status counting for poison, pet, buffs, debuffs, mounts, metabolism, workbench, crafting, safe zone, shield, and building privilege
-Fixed cumulative spacing when multiple vanilla statuses are visible
-Added one consistent 2px gap between the vanilla status group and the SimpleStatus group
-Separated the 28px vanilla row step from the 30px SimpleStatus row step
-Aligned note.inv tracking to the next server tick and reused the existing expiry polling
-Fixed hidden status preferences not being restored after reload
-Fixed /ts changes not being saved immediately
-Fixed status position updates not respecting Rank order
-Fixed standalone localization using the real player user ID
-Fixed culture-dependent progress, alpha, and UI offset formatting
-Fixed delayed AdvancedStatus initialization and runtime mode migration
-Fixed finite-duration status tracking while AdvancedStatus is active
-Fixed stale AdvancedStatus bars and standalone UI remaining after plugin unload
-Fixed expired saved statuses modifying their collection while being restored
-Fixed stale saved status entries when their owning plugin is no longer loaded
-Fixed malformed JSON when status titles or text contain quotes, backslashes, or line breaks
-Clamped standalone progress values to a valid 0-1 range
-Improved API null safety, inactive-status updates, corrupted-data recovery, color parsing, and native layout refresh consistency
[v1.3.0]
-Added native CustomVitals support for SimpleStatus statuses
-Added internal CustomVitals wrapper using Facepunch SendCustomVitals without requiring Carbon or CarbonAliases
-Added optional Carbon and CarbonAliases CustomVitalManager support when the internal wrapper is disabled
-Added batched CustomVitals rendering so multiple active SimpleStatus statuses are sent together in Rank order
-Added automatic Legacy CUI fallback for progress bars and unsupported CustomVitals icon types
-Added direct image URL support for SimpleStatus icons through ImageLibrary with automatic CustomVitals refresh after download
-Added one-time compatibility warnings identifying the plugin, status, and unsupported feature that causes Legacy CUI fallback
-Added configuration options "UseCustomVitals", "UseInternalCustomVitalsWrapper", "ForceCustomVitalsForUnsupportedStatuses", and "IgnoreAdvancedStatus"
-Added runtime backend switching between AdvancedStatus and standalone CustomVitals or Legacy CUI
-Added informational AdvancedStatus detection messages and warnings for unsupported or unavailable CustomVitals backend configurations
-Added public CustomVitals API with plugin ownership, cleanup, Rank ordering, and timer-safe batching through the internal SimpleStatus wrapper
-Fixed SimpleStatus initialization after load * when ImageLibrary loads after SimpleStatus, removing the need for a manual SimpleStatus reload
-Fixed transient plugin unload/load cycles preserving and restoring registered statuses instead of deleting them immediately
-Added ImageLibrary load detection messages and clarified CustomStatusFramework conflict detection to match the existing AdvancedStatus integration messages
-Removed the unused "warning" localization key
[v1.3.1]
-Fixed AdvancedStatus Rank integration by mapping SimpleStatus Rank to AdvancedStatus Order
-Fixed AdvancedStatus progress initialization, text transparency, and full redraws for properties that require layout or style recreation
-Fixed AdvancedStatus raw ImageLibrary icons, item ID icon resolution, and empty icon handling
-Added ImageLibrary reload refreshes so active status icons recover without reloading SimpleStatus
-Added runtime CustomStatusFramework conflict detection when it is loaded after SimpleStatus
-Fixed AdvancedStatus per-player override semantics for empty text and icon values to match standalone and CustomVitals rendering
-Fixed culture-dependent AdvancedStatus progress overrides
-Removed unused legacy status type metadata and stale commented code
*/
public static SimpleStatus PLUGIN;
[PluginReference]
private readonly Plugin ImageLibrary, CustomStatusFramework, AdvancedStatus;
private readonly bool Debugging = false;
// If you are a developer, you can enable this to get console logs.
private readonly int[] DebugCodes = new int[] {
}
;
// Add codes here for specific debug statements you want to see, empty array will show all.
private const string NoteInventoryCommand = "note.inv";
private const float LayoutVerificationDelay = 0.25f;
private const float NativeNotificationDuration = 3.7f;
private const float ImageUrlLoadCheckInterval = 1f;
private const int ImageUrlLoadMaxChecks = 120;
private const float PluginDataCleanupDelay = 300f;
private readonly HashSet<ulong> _pendingLayoutRefresh = new HashSet<ulong>();
private readonly Dictionary<ItemCraftTask, ulong> _craftTaskPlayers = new Dictionary<ItemCraftTask, ulong>();
private long _nativeNotificationSequence;
private CustomVitalsBridge _customVitalsBridge;
private bool _customVitalsAvailable;
private readonly Dictionary<ulong, Dictionary<string, object>> _customVitalHandles = new Dictionary<ulong, Dictionary<string, object>>();
private readonly HashSet<string> _customVitalsWarnings = new HashSet<string>();
private readonly HashSet<string> _pendingImageUrls = new HashSet<string>(StringComparer.Ordinal);
private readonly HashSet<ulong> _legacyCustomVitalsFallbackPlayers = new HashSet<ulong>();
private readonly Dictionary<ulong, Dictionary<string, InternalCustomVitalRegistration>> _internalCustomVitalRegistrations = new Dictionary<ulong, Dictionary<string, InternalCustomVitalRegistration>>();
private readonly Dictionary<string, long> _pendingPluginDataCleanup = new Dictionary<string, long>(StringComparer.OrdinalIgnoreCase);
private long _internalCustomVitalSequence;
private long _pluginDataCleanupSequence;
private bool _simpleStatusReady;
private bool _dataLoaded;
private bool _imageLibraryMissingWarningShown;
private bool _customStatusFrameworkWarningShown;
private bool _advancedStatusDetectedMessageShown;
private bool _advancedStatusIgnoredWarningShown;
private bool _internalCustomVitalsAdvancedStatusWarningShown;
private bool UsingCustomVitals => config?.UseCustomVitals == true && _customVitalsAvailable && !UsingAdvancedStatus;
private bool InternalCustomVitalsWrapperAvailable => config?.UseCustomVitals == true && config?.UseInternalCustomVitalsWrapper == true && _customVitalsAvailable && _customVitalsBridge?.UsesInternalWrapper == true;
#region Init
private void Init() {
UnsubscribeAll(nameof(OnPlayerSleepEnded), nameof(OnPlayerSleep), nameof(OnPlayerDeath), nameof(CanPickupEntity), nameof(OnItemPickup), nameof(OnStructureUpgrade), nameof(OnStructureRepair), nameof(OnCupboardAuthorize), nameof(OnCupboardDeauthorize), nameof(OnDispenserGather), nameof(OnCollectiblePickup), nameof(OnPlayerRespawn), nameof(OnItemCraft), nameof(OnItemCraftFinished), nameof(OnItemCraftCancelled), nameof(OnActiveItemChanged), nameof(OnEntityMounted), nameof(OnEntityDismounted), nameof(OnSendCommand) );
PLUGIN = this;
}
private void OnServerInitialized() {
TryInitializeSimpleStatus();
}
private bool TryInitializeSimpleStatus() {
if (_simpleStatusReady) {
return true;
}
if (!ImageLibrary?.IsLoaded ?? true) {
if (!_imageLibraryMissingWarningShown) {
_imageLibraryMissingWarningShown = true;
PrintWarning("ImageLibrary is required for SimpleStatus. Waiting for ImageLibrary to load before initializing.");
}
return false;
}
if (_imageLibraryMissingWarningShown) {
Puts("ImageLibrary loaded. Continuing SimpleStatus initialization.");
}
else {
Puts("ImageLibrary detected.");
}
_imageLibraryMissingWarningShown = false;
if (CustomStatusFramework?.IsLoaded ?? false) {
if (!_customStatusFrameworkWarningShown) {
_customStatusFrameworkWarningShown = true;
PrintError("CustomStatusFramework detected. SimpleStatus cannot initialize while CustomStatusFramework is loaded because both plugins provide the same status UI functionality. Please unload CustomStatusFramework.");
}
return false;
}
_customStatusFrameworkWarningShown = false;
AddCovalenceCommand(config.ToggleStatusCommand, nameof(CmdToggleStatus));
InitializeCustomVitals();
LoadData();
_dataLoaded = true;
LogAdvancedStatusState();
SubscribeAll();
foreach (var player in BasePlayer.activePlayerList) {
OnPlayerConnected(player);
OnPlayerSleepEnded(player);
}
_simpleStatusReady = true;
Interface.CallHook("OnSimpleStatusReady");
ScheduleUnloadedPluginDataCleanup();
return true;
}
private bool IsAdvancedStatusReady() {
if (!UsingAdvancedStatus) {
return false;
}
var result = AdvancedStatus?.Call("IsReady");
return result is bool && (bool)result;
}
private void OnAdvancedStatusLoaded(VersionNumber version) {
LogAdvancedStatusState();
if (!UsingAdvancedStatus) {
return;
}
foreach (var player in BasePlayer.activePlayerList) {
if (player == null) {
continue;
}
RemoveCustomVitals(player);
_legacyCustomVitalsFallbackPlayers.Remove(player.userID);
BehaviorManager.HideBehavior(player);
Behaviours .GetValueOrDefault(player.UserIDString) ?.UpdateAdvancedTrackingState();
if (!Data.PlayersHiding.Contains(player.UserIDString) && !player.IsDead() && !player.IsSleeping()) {
AS_ShowAllBars(player);
}
}
}
private void Unload() {
_simpleStatusReady = false;
if (_dataLoaded) {
SaveData();
}
_internalCustomVitalRegistrations.Clear();
var advancedStatusReady = IsAdvancedStatusReady();
foreach (var player in BasePlayer.activePlayerList.ToArray()) {
if (player == null) {
continue;
}
if (advancedStatusReady) {
AS_HideAllBars(player);
}
RemoveCustomVitals(player);
CuiHelper.DestroyUi(player, UI_Base_ID);
BehaviorManager.RemoveBehavior(player.UserIDString);
}
_pendingLayoutRefresh.Clear();
_craftTaskPlayers.Clear();
_customVitalHandles.Clear();
_customVitalsWarnings.Clear();
_pendingImageUrls.Clear();
_legacyCustomVitalsFallbackPlayers.Clear();
_pendingPluginDataCleanup.Clear();
_internalCustomVitalSequence = 0;
_pluginDataCleanupSequence = 0;
}
private void OnPluginLoaded(Plugin plugin) {
if (plugin == null || plugin.Name == Name) {
return;
}
var name = plugin.Name;
_pendingPluginDataCleanup.Remove(name);
if (!_simpleStatusReady) {
NextTick(() => TryInitializeSimpleStatus());
return;
}
if (name.Equals(nameof(CustomStatusFramework), StringComparison.OrdinalIgnoreCase)) {
if (!_customStatusFrameworkWarningShown) {
_customStatusFrameworkWarningShown = true;
PrintError("CustomStatusFramework was loaded while SimpleStatus is already active. Both plugins provide overlapping status UI functionality and should not run together. Please unload CustomStatusFramework or SimpleStatus.");
}
return;
}
if (name.Equals(nameof(ImageLibrary), StringComparison.OrdinalIgnoreCase)) {
_imageLibraryMissingWarningShown = false;
_pendingImageUrls.Clear();
NextTick(RefreshAllStatusRenderers);
return;
}
NextTick(() => {
if (!_simpleStatusReady || !plugin.IsLoaded) {
return;
}
plugin.Call("OnSimpleStatusReady");
RefreshPluginStatuses(name);
}
);
}
private void OnPluginUnloaded(Plugin plugin) {
if (plugin == null) {
return;
}
var name = plugin.Name;
if (name == Name) {
return;
}
if (name.Equals(nameof(AdvancedStatus), StringComparison.OrdinalIgnoreCase)) {
RemoveAllInternalCustomVitals(name);
_advancedStatusDetectedMessageShown = false;
_advancedStatusIgnoredWarningShown = false;
if (config?.IgnoreAdvancedStatus != true) {
NextTick(RestoreStandaloneStatuses);
}
return;
}
RemoveAllInternalCustomVitals(name);
if (_dataLoaded && Data.Statuses.Values.Any(x => x != null && x.PluginName == name)) {
SuspendPluginStatuses(name);
SchedulePluginDataCleanup(name);
}
if (name.Equals(nameof(CustomStatusFramework), StringComparison.OrdinalIgnoreCase)) {
_customStatusFrameworkWarningShown = false;
if (!_simpleStatusReady) {
NextTick(() => TryInitializeSimpleStatus());
}
}
}
private void ScheduleUnloadedPluginDataCleanup() {
foreach (var pluginName in Data.Statuses.Values .Where(status => status != null && !string.IsNullOrWhiteSpace(status.PluginName) && !status.PluginIsLoaded) .Select(status => status.PluginName) .Distinct(StringComparer.OrdinalIgnoreCase)) {
SchedulePluginDataCleanup(pluginName);
}
}
private void SchedulePluginDataCleanup(string pluginName) {
if (!_simpleStatusReady || string.IsNullOrWhiteSpace(pluginName)) {
return;
}
var sequence = ++_pluginDataCleanupSequence;
_pendingPluginDataCleanup[pluginName] = sequence;
timer.Once(PluginDataCleanupDelay, () => {
long currentSequence;
if (!_simpleStatusReady || !_pendingPluginDataCleanup.TryGetValue(pluginName, out currentSequence) || currentSequence != sequence) {
return;
}
_pendingPluginDataCleanup.Remove(pluginName);
var plugin = Interface.uMod.RootPluginManager.GetPlugin(pluginName);
if (plugin?.IsLoaded ?? false) {
return;
}
if (Data.Statuses.Values.Any(status => status != null && status.PluginName == pluginName)) {
RemovePluginData(pluginName);
}
}
);
}
private void SuspendPluginStatuses(string pluginName) {
var statusIds = new HashSet<string>(Data.Statuses .Where(entry => entry.Value != null && entry.Value.PluginName == pluginName) .Select(entry => entry.Key));
if (statusIds.Count == 0) {
return;
}
foreach (var basePlayer in BasePlayer.activePlayerList.ToArray()) {
if (basePlayer == null) {
continue;
}
var playerData = Data.Player.GetValueOrDefault(basePlayer.UserIDString);
if (playerData == null || !statusIds.Any(playerData.ContainsKey)) {
continue;
}
foreach (var statusId in statusIds) {
var data = playerData.GetValueOrDefault(statusId);
if (data != null) {
data.Drawn = false;
}
}
if (UsingAdvancedStatus) {
foreach (var statusId in statusIds) {
var status = Data.Statuses.GetValueOrDefault(statusId);
if (status != null) {
AS_DeleteBar(basePlayer.userID, status);
}
}
Behaviours.GetValueOrDefault(basePlayer.UserIDString)?.UpdateAdvancedTrackingState();
continue;
}
if (UsingCustomVitals) {
RefreshCustomVitals(basePlayer);
continue;
}
foreach (var statusId in statusIds) {
CuiHelper.DestroyUi(basePlayer, string.Format(UI_Status_ID, statusId));
}
var behaviour = Behaviours.GetValueOrDefault(basePlayer.UserIDString);
if (behaviour != null) {
behaviour.rowsNeedUpdate = true;
behaviour.forceCheckMods = true;
behaviour.ForceLayoutRefresh();
}
}
}
private void RefreshPluginStatuses(string pluginName) {
var statusIds = new HashSet<string>(Data.Statuses .Where(entry => entry.Value != null && entry.Value.PluginName == pluginName && entry.Value.PluginIsLoaded) .Select(entry => entry.Key));
if (statusIds.Count == 0) {
return;
}
foreach (var basePlayer in BasePlayer.activePlayerList.ToArray()) {
if (basePlayer == null) {
continue;
}
var playerData = Data.Player.GetValueOrDefault(basePlayer.UserIDString);
if (playerData == null || !statusIds.Any(playerData.ContainsKey)) {
continue;
}
foreach (var statusId in statusIds) {
var data = playerData.GetValueOrDefault(statusId);
if (data != null) {
data.Drawn = false;
}
}
if (Data.PlayersHiding.Contains(basePlayer.UserIDString) || basePlayer.IsDead() || basePlayer.IsSleeping()) {
continue;
}
var behaviour = BehaviorManager.AddBehavior(basePlayer);
if (UsingAdvancedStatus) {
AS_ShowAllBars(basePlayer);
behaviour?.UpdateAdvancedTrackingState();
continue;
}
BehaviorManager.ShowBehavior(basePlayer);
behaviour?.ResumeStandalone();
}
}
private void RefreshAllStatusRenderers() {
if (!_simpleStatusReady || !(ImageLibrary?.IsLoaded ?? false)) {
return;
}
foreach (var player in BasePlayer.activePlayerList.ToArray()) {
if (player == null || !player.IsConnected) {
continue;
}
if (Data.PlayersHiding.Contains(player.UserIDString) || player.IsDead() || player.IsSleeping()) {
continue;
}
var behaviour = BehaviorManager.AddBehavior(player);
if (UsingAdvancedStatus) {
AS_HideAllBars(player);
AS_ShowAllBars(player);
behaviour?.UpdateAdvancedTrackingState();
continue;
}
if (UsingCustomVitals) {
RefreshCustomVitals(player);
continue;
}
ResetStandaloneDrawState(player);
CuiHelper.DestroyUi(player, UI_Base_ID);
BehaviorManager.ShowBehavior(player);
behaviour?.ResumeStandalone();
}
}
private void RestoreStandaloneStatuses() {
foreach (var player in BasePlayer.activePlayerList) {
if (player == null || player.IsDead() || player.IsSleeping() || Data.PlayersHiding.Contains(player.UserIDString)) {
continue;
}
var behaviour = BehaviorManager.AddBehavior(player);
BehaviorManager.ShowBehavior(player);
behaviour?.ResumeStandalone();
}
}
#endregion
#region Subscribing
public List<string> SubscribedHooks = new List<string>();
private void SubscribeAll() {
foreach (var hook in SubscribedHooks) {
Subscribe(hook);
}
}
private void UnsubscribeAll(params string[] hooks) {
foreach (var hook in hooks) {
SubscribedHooks.Add(hook);
Unsubscribe(hook);
}
}
#endregion
#region Oxide Hooks
private void OnPlayerConnected(BasePlayer basePlayer) {
if (basePlayer == null) {
return;
}
BehaviorManager.AddBehavior(basePlayer);
if (InternalCustomVitalsWrapperAvailable && HasInternalCustomVitalRegistrations(basePlayer.userID)) {
var userId = basePlayer.userID;
NextTick(() => RefreshInternalCustomVitals(userId));
}
}
private void OnPlayerDisconnected(BasePlayer basePlayer) {
if (basePlayer == null) {
return;
}
RemoveCustomVitals(basePlayer, basePlayer.IsConnected);
CuiHelper.DestroyUi(basePlayer, UI_Base_ID);
_pendingLayoutRefresh.Remove(basePlayer.userID);
_legacyCustomVitalsFallbackPlayers.Remove(basePlayer.userID);
foreach (var task in _craftTaskPlayers .Where(entry => entry.Value == basePlayer.userID) .Select(entry => entry.Key) .ToArray()) {
_craftTaskPlayers.Remove(task);
}
NextTick(() => {
BehaviorManager.RemoveBehavior(basePlayer.UserIDString);
}
);
Debug($"Disconnecting {basePlayer.displayName} has {(Data.Player.ContainsKey(basePlayer.UserIDString) ? Data.Player[basePlayer.UserIDString].Count : 0)} statuses: {Data.Player.GetValueOrDefault(basePlayer.UserIDString)?.Keys.ToSentence()}");
}
private void OnPlayerSleepEnded(BasePlayer basePlayer) {
if (basePlayer == null) {
return;
}
if (!Data.PlayersHiding.Contains(basePlayer.UserIDString)) {
if (UsingAdvancedStatus) {
if (IsAdvancedStatusReady()) {
AS_ShowAllBars(basePlayer);
}
}
else {
BehaviorManager.ShowBehavior(basePlayer);
}
}
}
private void OnPlayerSleep(BasePlayer basePlayer) {
if (basePlayer == null) {
return;
}
if (UsingAdvancedStatus) AS_HideAllBars(basePlayer);
else BehaviorManager.HideBehavior(basePlayer);
}
private void OnPlayerRespawn(BasePlayer basePlayer, BasePlayer.SpawnPoint spawnPoint) {
if (basePlayer == null) {
return;
}
OnPlayerConnected(basePlayer);
}
private void OnPlayerRespawn(BasePlayer basePlayer, SleepingBag sleepingBag2) {
if (basePlayer == null) {
return;
}
OnPlayerConnected(basePlayer);
}
private void OnPlayerDeath(BasePlayer basePlayer, HitInfo info) => OnPlayerDisconnected(basePlayer);
private void OnSendCommand(Connection connection, string command, object[] args) {
if (UsingAdvancedStatus || connection == null || string.IsNullOrEmpty(command)) {
return;
}
var isExactCommand = command.Equals(NoteInventoryCommand, StringComparison.OrdinalIgnoreCase);
if (!isExactCommand && !command.StartsWith(NoteInventoryCommand + " ", StringComparison.OrdinalIgnoreCase)) {
return;
}
int itemId;
if (!TryGetInventoryNotificationItemId(command, args, out itemId)) {
return;
}
var basePlayer = BasePlayer.FindByID(connection.userid);
if (basePlayer == null || !basePlayer.IsConnected || UsesCustomVitalsForPlayer(basePlayer)) {
return;
}
var behaviour = Behaviours.GetValueOrDefault(basePlayer.UserIDString);
if (behaviour == null) {
return;
}
var itemDefinition = ItemManager.FindItemDefinition(itemId);
var isStackable = itemDefinition != null && itemDefinition.stackable > 1;
var key = isStackable ? $"note:{itemId}" : $"note:{itemId}:{++_nativeNotificationSequence}";
var userId = basePlayer.userID;
NextTick(() => {
var currentPlayer = BasePlayer.FindByID(userId);
if (currentPlayer == null || !currentPlayer.IsConnected) {
return;
}
var currentBehaviour = Behaviours.GetValueOrDefault(currentPlayer.UserIDString);
if (currentBehaviour == null) {
return;
}
var visibleUntil = Time.realtimeSinceStartup + NativeNotificationDuration;
float existingVisibleUntil;
if (!currentBehaviour.itemStatuses.TryGetValue(key, out existingVisibleUntil) || visibleUntil > existingVisibleUntil) {
currentBehaviour.itemStatuses[key] = visibleUntil;
}
currentBehaviour.ForceLayoutRefresh();
}
);
}
private static bool TryGetInventoryNotificationItemId(string command, object[] args, out int itemId) {
itemId = 0;
if (command.Equals(NoteInventoryCommand, StringComparison.OrdinalIgnoreCase)) {
if (args == null || args.Length == 0) {
return false;
}
return int.TryParse(Convert.ToString(args[0], CultureInfo.InvariantCulture), NumberStyles.Integer, CultureInfo.InvariantCulture, out itemId);
}
var parts = command.Split(new[] {
' '
}
, StringSplitOptions.RemoveEmptyEntries);
return parts.Length > 1 && int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out itemId);
}
private bool? OnItemCraft(ItemCraftTask task, BasePlayer basePlayer, Item item) {
if (UsingAdvancedStatus || UsesCustomVitalsForPlayer(basePlayer)) {
return null;
}
if (task != null && basePlayer != null) {
_craftTaskPlayers[task] = basePlayer.userID;
}
ScheduleStandaloneLayoutRefresh(basePlayer);
return null;
}
private void OnItemCraftFinished(ItemCraftTask task, Item item) {
var basePlayer = FindCraftTaskPlayer(task);
if (UsingAdvancedStatus || UsesCustomVitalsForPlayer(basePlayer)) {
return;
}
ScheduleStandaloneLayoutRefresh(basePlayer);
ScheduleCraftTaskCleanup(task, basePlayer);
}
private void OnItemCraftCancelled(ItemCraftTask task) {
var basePlayer = FindCraftTaskPlayer(task);
if (UsingAdvancedStatus || UsesCustomVitalsForPlayer(basePlayer)) {
return;
}
ScheduleStandaloneLayoutRefresh(basePlayer);
if (task != null) {
_craftTaskPlayers.Remove(task);
}
}
private void OnActiveItemChanged(BasePlayer basePlayer, Item oldItem, Item newItem) {
ScheduleStandaloneLayoutRefresh(basePlayer);
}
private void OnEntityMounted(BaseMountable entity, BasePlayer basePlayer) {
ScheduleStandaloneLayoutRefresh(basePlayer);
}
private void OnEntityDismounted(BaseMountable entity, BasePlayer basePlayer) {
ScheduleStandaloneLayoutRefresh(basePlayer);
}
private BasePlayer FindCraftTaskPlayer(ItemCraftTask task) {
if (task == null) {
return null;
}
ulong userId;
if (_craftTaskPlayers.TryGetValue(task, out userId)) {
return BasePlayer.FindByID(userId);
}
foreach (var basePlayer in BasePlayer.activePlayerList) {
var queue = basePlayer?.inventory?.crafting?.queue;
if (queue != null && queue.Any(queuedTask => ReferenceEquals(queuedTask, task))) {
_craftTaskPlayers[task] = basePlayer.userID;
return basePlayer;
}
}
return null;
}
private void ScheduleCraftTaskCleanup(ItemCraftTask task, BasePlayer basePlayer) {
if (task == null) {
return;
}
var userId = basePlayer?.userID ?? 0UL;
timer.Once(1f, () => {
var currentPlayer = userId == 0UL ? null : BasePlayer.FindByID(userId);
var queue = currentPlayer?.inventory?.crafting?.queue;
var stillQueued = queue != null && queue.Any(queuedTask => ReferenceEquals(queuedTask, task));
if (!stillQueued) {
_craftTaskPlayers.Remove(task);
}
}
);
}
private void ScheduleStandaloneLayoutRefresh(BasePlayer basePlayer) {
if (basePlayer == null || UsingAdvancedStatus || UsesCustomVitalsForPlayer(basePlayer)) {
return;
}
var userId = basePlayer.userID;
if (!_pendingLayoutRefresh.Add(userId)) {
return;
}
NextTick(() => RefreshStandaloneLayout(userId));
timer.Once(LayoutVerificationDelay, () => {
_pendingLayoutRefresh.Remove(userId);
RefreshStandaloneLayout(userId);
}
);
}
private void RefreshStandaloneLayout(ulong userId) {
var basePlayer = BasePlayer.FindByID(userId);
if (basePlayer == null || !basePlayer.IsConnected) {
return;
}
var behaviour = Behaviours.GetValueOrDefault(basePlayer.UserIDString);
behaviour?.ForceLayoutRefresh();
}
private void CanPickupEntity(BasePlayer basePlayer, BaseEntity entity) {
if (basePlayer == null || entity == null) {
return;
}
NextTick(() => ScheduleStandaloneLayoutRefresh(basePlayer));
}
private void OnEntityBuilt(Planner plan, GameObject go) {
if (plan == null || go == null) {
return;
}
var basePlayer = plan.GetOwnerPlayer();
if (basePlayer == null) {
return;
}
var buildingBlock = go.ToBaseEntity() as BuildingBlock;
if (buildingBlock == null) {
return;
}
ScheduleStandaloneLayoutRefresh(basePlayer);
}
private void OnItemPickup(Item item, BasePlayer basePlayer) {
if (basePlayer == null || item == null) {
return;
}
ScheduleStandaloneLayoutRefresh(basePlayer);
}
private void OnStructureUpgrade(BaseCombatEntity entity, BasePlayer basePlayer, BuildingGrade.Enum grade) {
if (basePlayer == null) {
return;
}
ScheduleStandaloneLayoutRefresh(basePlayer);
}
private void OnStructureRepair(BuildingBlock entity, BasePlayer basePlayer) {
NextTick(() => {
if (entity != null && basePlayer != null) {
ScheduleStandaloneLayoutRefresh(basePlayer);
}
}
);
}
private void OnCupboardAuthorize(BuildingPrivlidge privilege, BasePlayer basePlayer) {
ScheduleStandaloneLayoutRefresh(basePlayer);
}
private void OnCupboardDeauthorize(BuildingPrivlidge privilege, BasePlayer basePlayer) {
ScheduleStandaloneLayoutRefresh(basePlayer);
}
private void OnDispenserGather(ResourceDispenser dispenser, BasePlayer basePlayer, Item item) {
if (basePlayer == null || item == null) {
return;
}
ScheduleStandaloneLayoutRefresh(basePlayer);
}
private void OnCollectiblePickup(CollectibleEntity collectible, BasePlayer basePlayer) {
if (basePlayer == null || collectible == null) {
return;
}
ScheduleStandaloneLayoutRefresh(basePlayer);
}
#endregion
#region Status Info
protected class SavedData {
public Dictionary<string, StatusInfo> Statuses = new Dictionary<string, StatusInfo>();
public HashSet<string> PlayersHiding = new HashSet<string>();
public Dictionary<string, Dictionary<string, PlayerStatusInfo>> Player = new Dictionary<string, Dictionary<string, PlayerStatusInfo>>();
}
private static string GetImageData(string path) {
if (string.IsNullOrWhiteSpace(path)) {
return string.Empty;
}
else if (path.IsAssetPath()) {
return path;
}
else if (path.IsItemId()) {
return path.AsItemId();
}
else if (path.IsRawImage()) {
return PLUGIN.ImageLibrary?.Call<string>("GetImage", path.AsRawImage()) ?? string.Empty;
}
else if (path.IsImageUrl()) {
return PLUGIN.GetImageUrlData(path);
}
return PLUGIN.ImageLibrary?.Call<string>("GetImage", path) ?? string.Empty;
}
protected class PlayerStatusInfo {
public int Duration;
public string Color = null;
public string Title = null;
public string TitleColor = null;
public string Text = null;
public string TextColor = null;
public string ImagePathOrUrl = null;
public string IconColor = null;
public string Progress = null;
public string ProgressColor = null;
public string UserId = "0";
[JsonIgnore]
public DateTime? EndTime = null;
[JsonIgnore]
public bool IsPastEndTime => EndTime.HasValue && EndTime.Value < DateTime.Now;
[JsonIgnore]
public int DurationUntilEndTime => !EndTime.HasValue ? Duration : (int)Math.Floor((EndTime.Value.Subtract(DateTime.Now)).TotalSeconds);
[JsonIgnore]
public bool Drawn = false;
}
private SavedData _data = new SavedData();
protected static SavedData Data => PLUGIN._data;
private void RemovePluginData(string pluginName) {
Debug($"Removing {pluginName} because its no longer loaded");
var statuses = Data.Statuses .Where(entry => entry.Value != null && entry.Value.PluginName == pluginName) .Select(entry => entry.Key) .ToArray();
foreach (var status in statuses) {
Data.Statuses.Remove(status);
}
var userIdsToUpdate = new HashSet<string>();
foreach (var playerData in Data.Player.ToArray()) {
var userId = playerData.Key;
if (playerData.Value == null) {
Data.Player.Remove(userId);
continue;
}
foreach (var key in playerData.Value.Keys.ToArray()) {
if (statuses.Contains(key)) {
playerData.Value.Remove(key);
}
}
if (playerData.Value.Count == 0) {
Data.Player.Remove(userId);
}
userIdsToUpdate.Add(userId);
}
foreach (var userId in userIdsToUpdate) {
var behavior = Behaviours.GetValueOrDefault(userId);
if (behavior == null) {
continue;
}
behavior.rowsNeedUpdate = true;
behavior.UpdateAdvancedTrackingState();
behavior.UpdateCustomVitalsTrackingState();
var basePlayer = BasePlayer.Find(userId);
if (basePlayer != null) {
if (UsingCustomVitals) {
RefreshCustomVitals(basePlayer);
}
else {
foreach(var status in statuses) {
CuiHelper.DestroyUi(basePlayer, string.Format(UI_Status_ID, status));
}
}
}
}
}
private void SaveData() {
Interface.Oxide.DataFileSystem.WriteObject($"{Name}/data", Data);
}
private void LoadData() {
Debug("Load data called");
SavedData data;
try {
data = Interface.Oxide.DataFileSystem .ReadObject<SavedData>($"{Name}/data") ?? new SavedData();
}
catch (Exception exception) {
PrintError($"Could not read the data file. Starting with clean data: " + exception.Message);
data = new SavedData();
}
if (data.Statuses == null) {
data.Statuses = new Dictionary<string, StatusInfo>();
}
if (data.Player == null) {
data.Player = new Dictionary<string, Dictionary<string, PlayerStatusInfo>>();
}
if (data.PlayersHiding == null) {
data.PlayersHiding = new HashSet<string>();
}
_data.Player = data.Player;
_data.PlayersHiding = data.PlayersHiding;
foreach (var status in data.Statuses.ToArray()) {
if (status.Value == null || string.IsNullOrWhiteSpace(status.Value.PluginName)) {
foreach (var playerData in _data.Player.ToArray()) {
playerData.Value?.Remove(status.Key);
if (playerData.Value == null || playerData.Value.Count == 0) {
_data.Player.Remove(playerData.Key);
}
}
continue;
}
if (!_data.Statuses.ContainsKey(status.Key)) {
Debug("Assigned new status from load");
status.Value.Id = status.Key;
_data.Statuses[status.Key] = status.Value;
}
}
}
private static string NormalizeOverride(string value) {
return value == RESET_KEYWORD ? null : value;
}
private void SyncStatusDataForAdvanced(PlayerStatusInfo data, Dictionary<string, object> properties) {
foreach (var key in properties.Keys) {
switch (key) {
case "color" : data.Color = NormalizeOverride(properties.GetProperty<string>("color"));
break;
case "title" : data.Title = NormalizeOverride(properties.GetProperty<string>("title"));
break;
case "titleColor" : data.TitleColor = NormalizeOverride(properties.GetProperty<string>("titleColor"));
break;
case "text" : data.Text = NormalizeOverride(properties.GetProperty<string>("text"));
break;
case "textColor" : data.TextColor = NormalizeOverride(properties.GetProperty<string>("textColor"));
break;
case "icon" : data.ImagePathOrUrl = NormalizeOverride(properties.GetProperty<string>("icon"));
break;
case "iconColor" : data.IconColor = NormalizeOverride(properties.GetProperty<string>("iconColor"));
break;
case "progress" : {
var progressObj = properties.GetValueOrDefault("progress", null);
var progressStr = progressObj == null ? null : Convert.ToString(progressObj, CultureInfo.InvariantCulture);
data.Progress = NormalizeOverride(progressStr);
break;
}
case "progressColor" : data.ProgressColor = NormalizeOverride(properties.GetProperty<string>("progressColor"));
break;
}
}
}
private void NotifyStatusUpdates(string userId, string statusId, Dictionary<string, object> properties) {
if (properties == null) {
return;
}
foreach (var property in properties) {
switch (property.Key) {
case "color" : case "title" : case "titleColor" : case "text" : case "textColor" : case "icon" : case "iconColor" : case "progress" : case "progressColor" : Interface.CallHook("OnStatusUpdate", userId, statusId, property.Key, Convert.ToString(property.Value, CultureInfo.InvariantCulture) ?? string.Empty);
break;
}
}
}
private static void Debug(string message) {
if (PLUGIN == null || !PLUGIN.Debugging) {
return;
}
var code = message.Split(" ").First().GetHashCode();
if (PLUGIN.DebugCodes != null && PLUGIN.DebugCodes.Length > 0 && !PLUGIN.DebugCodes.Contains(code)) {
return;
}
PLUGIN?.Puts($"DEBUG({code}): {message}");
}
protected class StatusInfo {
[JsonIgnore]
public Plugin Plugin => Interface.uMod.RootPluginManager.GetPlugin(PluginName);
[JsonIgnore]
public bool PluginIsLoaded => Plugin?.IsLoaded ?? false;
public string PluginName;
public string Id;
public string Color;
public string Title;
public string TitleColor;
public string Text = null;
public string TextColor;
public string ImageLibraryNameOrAssetPath;
// ... [Code preview truncated due to length. Full source in attached file] ...