Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- using Newtonsoft.Json;
- using Newtonsoft.Json.Linq;
- using Oxide.Core;
- using Oxide.Core.Libraries.Covalence;
- using Oxide.Core.Plugins;
- using Oxide.Game.Rust.Cui;
- using Rust;
- using System;
- using System.Collections;
- using System.Collections.Generic;
- using System.Globalization;
- using System.Linq;
- using System.IO;
- using System.Text;
- using UnityEngine;
- using Facepunch;
- //AntiOfflineRaid created with PluginMerge v(1.0.6.0) by MJSU @ https://github.com/dassjosh/Plugin.Merge
- namespace Oxide.Plugins
- {
- [Info("Anti Offline Raid", "Calytic/Shady14u", "1.6.1")]
- [Description("Prevents/reduces offline raiding")]
- public partial class AntiOfflineRaid : RustPlugin
- {
- #region 1.AntiOfflineRaid.Config.cs
- private static Configuration _config;
- protected override void LoadConfig()
- {
- base.LoadConfig();
- try
- {
- var current = Config.ReadObject<JObject>() ?? new JObject();
- if (!current.HasValues)
- {
- _config = Configuration.DefaultConfig();
- }
- else
- {
- MigrateLegacyTopLevelSettings(current);
- if (current["ClansPluginShare"] == null && current["ClanShare"] != null)
- current["ClansPluginShare"] = current["ClanShare"].DeepClone();
- current.Remove("ClanShare");
- if (current["GroupSizeUseClansPluginMembers"] == null && current["GroupSizeUseClanMembers"] != null)
- current["GroupSizeUseClansPluginMembers"] = current["GroupSizeUseClanMembers"].DeepClone();
- current.Remove("GroupSizeUseClanMembers");
- current.Remove("GroupSizeUseAuthorizedPlayers");
- MigrateLegacyShareSettings(current);
- MigrateLegacySimpleStatusSettings(current);
- MigrateLegacySimpleStatusColors(current);
- _config = current.ToObject<Configuration>() ?? Configuration.DefaultConfig();
- }
- NormalizeConfig();
- SaveConfig();
- }
- catch (Exception e)
- {
- Debug.LogException(e);
- PrintWarning("Creating new config file.");
- LoadDefaultConfig();
- NormalizeConfig();
- SaveConfig();
- }
- }
- private static void MigrateLegacyTopLevelSettings(JObject current)
- {
- if (current == null) return;
- MigrateLegacyTopLevelSetting(current, "Activities", "activity");
- MigrateLegacyTopLevelSetting(current, "SoundFile", "sound");
- MigrateLegacyTopLevelSetting(current, "ClanShare", "clanShare");
- MigrateLegacyTopLevelSetting(current, "MinMembers", "minMembers");
- current.Remove("ClanFirstOffline");
- current.Remove("clanFirstOffline");
- }
- private static void MigrateLegacyTopLevelSetting(JObject current, string currentName, string legacyName)
- {
- if (current[currentName] == null && current[legacyName] != null) current[currentName] = current[legacyName].DeepClone();
- current.Remove(legacyName);
- }
- private static void MigrateLegacyShareSettings(JObject current)
- {
- if (current == null) return;
- if (current["MinMembers"] != null)
- {
- if (current["ClansPluginMinMembers"] == null) current["ClansPluginMinMembers"] = current["MinMembers"].DeepClone();
- if (current["RustTeamMinMembers"] == null) current["RustTeamMinMembers"] = current["MinMembers"].DeepClone();
- if (current["RustClanMinMembers"] == null) current["RustClanMinMembers"] = current["MinMembers"].DeepClone();
- current.Remove("MinMembers");
- }
- current.Remove("ClansPluginFirstOffline");
- current.Remove("RustTeamFirstOffline");
- current.Remove("RustClanFirstOffline");
- }
- private static void MigrateLegacySimpleStatusSettings(JObject current)
- {
- if (!(current?["SimpleStatus"] is JObject legacy)) return;
- MigrateLegacySetting(current, legacy, "SimpleStatusEnabled", "Enabled");
- MigrateLegacySetting(current, legacy, "SimpleStatusShowForUnauthorizedOnlyWhenHoldingHammer", "Show outsiders only while holding hammer");
- MigrateLegacySetting(current, legacy, "SimpleStatusRefreshIntervalSeconds", "Refresh interval seconds");
- MigrateLegacySetting(current, legacy, "SimpleStatusStatusId", "Status id");
- MigrateLegacySetting(current, legacy, "SimpleStatusAuthorizedBackground", "Authorized background");
- MigrateLegacySetting(current, legacy, "SimpleStatusUnauthorizedBackground", "Unauthorized background");
- MigrateLegacySetting(current, legacy, "SimpleStatusAuthorizedIconColor", "Authorized icon color");
- MigrateLegacySetting(current, legacy, "SimpleStatusUnauthorizedIconColor", "Unauthorized icon color");
- MigrateLegacySetting(current, legacy, "SimpleStatusAuthorizedLabelColor", "Authorized label color");
- MigrateLegacySetting(current, legacy, "SimpleStatusUnauthorizedLabelColor", "Unauthorized label color");
- MigrateLegacySetting(current, legacy, "SimpleStatusAuthorizedValueColor", "Authorized value color");
- MigrateLegacySetting(current, legacy, "SimpleStatusUnauthorizedValueColor", "Unauthorized value color");
- MigrateLegacySetting(current, legacy, "SimpleStatusIcon", "Icon (/oxide/data/AntiOfflineRaid/images/<name>.png for local)");
- current.Remove("SimpleStatus");
- }
- private static void MigrateLegacySimpleStatusColors(JObject current)
- {
- if (current == null) return;
- MigrateKnownSimpleStatusColor(current, "SimpleStatusUnauthorizedBackground", "#773A2F", "#532829", "#C74A37");
- MigrateKnownSimpleStatusColor(current, "SimpleStatusUnauthorizedIconColor", "#FF5338", "#FF9482", "#782600");
- MigrateKnownSimpleStatusColor(current, "SimpleStatusUnauthorizedLabelColor", "#FF5338", "#FF9482", "#DEDEDE");
- MigrateKnownSimpleStatusColor(current, "SimpleStatusUnauthorizedValueColor", "#FF5338", "#FF9482", "#DEDEDE");
- }
- private static void MigrateKnownSimpleStatusColor(JObject current, string propertyName, string replacement, params string[] legacyDefaults)
- {
- var value = current?[propertyName]?.ToString();
- if (string.IsNullOrWhiteSpace(value) || legacyDefaults == null) return;
- if (legacyDefaults.Any(x => string.Equals(value.Trim(), x, StringComparison.OrdinalIgnoreCase))) current[propertyName] = replacement;
- }
- private static void MigrateLegacySetting(JObject current, JObject legacy, string currentName, string legacyName)
- {
- if (current[currentName] == null && legacy[legacyName] != null) current[currentName] = legacy[legacyName].DeepClone();
- }
- protected override void LoadDefaultConfig() => _config = Configuration.DefaultConfig();
- protected override void SaveConfig() => Config.WriteObject(_config);
- public class Configuration
- {
- [JsonProperty(PropertyName = "AbsoluteTimeScale")]
- public Dictionary<string, object> AbsoluteTimeScale = new Dictionary<string, object>();
- [JsonProperty(PropertyName = "DamageScale")]
- public Dictionary<string, object> DamageScale = new Dictionary<string, object>();
- public List<string> Activities { get; set; }
- public int AfkMinutes { get; set; }
- public bool TreatAfkAsOffline { get; set; } = true;
- public bool ClansPluginShare { get; set; } = false;
- public int ClansPluginMinMembers { get; set; } = 1;
- public bool RustTeamShare { get; set; } = false;
- public int RustTeamMinMembers { get; set; } = 1;
- public bool RustClanShare { get; set; } = false;
- public int RustClanMinMembers { get; set; } = 1;
- public int CooldownMinutes { get; set; }
- public int MinutesOnlineToQualify { get; set; } = 0;
- public float InterimDamage { get; set; }
- public int MinutesSinceLastAttackToProtect { get; set; }
- public bool PlaySound { get; set; }
- public List<string> Prefabs { get; set; }
- public bool ProtectBaseWhenAway { get; set; }
- public bool ProtectPlayerMadeBoats { get; set; } = true;
- public bool ProtectTugboats { get; set; } = true;
- public bool GroupSizeProtectionScaling { get; set; } = true;
- public bool GroupSizeUseClansPluginMembers { get; set; } = false;
- public bool GroupSizeUseRustTeams { get; set; } = false;
- public bool GroupSizeUseRustClans { get; set; } = false;
- public Dictionary<string, float> GroupSizeProtectionScale { get; set; } = new Dictionary<string, float>();
- public int ServerTimeOffset { get; set; }
- public bool ShowMessage { get; set; }
- public bool SimpleStatusEnabled { get; set; } = false;
- public bool SimpleStatusShowForUnauthorizedOnlyWhenHoldingHammer { get; set; } = false;
- public bool SimpleStatusShowAuthorizedProtectionPreview { get; set; } = false;
- public float SimpleStatusRefreshIntervalSeconds { get; set; } = 1f;
- public string SimpleStatusStatusId { get; set; } = "antiofflineraid.protection";
- public string SimpleStatusAuthorizedBackground { get; set; } = "#738D45";
- public string SimpleStatusUnauthorizedBackground { get; set; } = "#773A2F";
- public string SimpleStatusAuthorizedIconColor { get; set; } = "#A3FE03";
- public string SimpleStatusUnauthorizedIconColor { get; set; } = "#FF5338";
- public string SimpleStatusAuthorizedLabelColor { get; set; } = "#FFFFFF";
- public string SimpleStatusUnauthorizedLabelColor { get; set; } = "#FF5338";
- public string SimpleStatusAuthorizedValueColor { get; set; } = "#FFFFFF";
- public string SimpleStatusUnauthorizedValueColor { get; set; } = "#FF5338";
- public string SimpleStatusIcon { get; set; } = "https://i.ibb.co/nq6bL11S/protection.png";
- public string SoundFile { get; set; }
- [JsonIgnore] public List<int> DamageScaleKeys { get; set; } = new List<int>();
- [JsonIgnore] public List<int> GroupSizeProtectionKeys { get; set; } = new List<int>();
- public static Configuration DefaultConfig()
- {
- return new Configuration
- {
- DamageScale = new Dictionary<string, object>
- {{"1", 0.2}, {"3", 0.35f}, {"6", 0.5f}, {"12", 0.8f}, {"48", 1}},
- AbsoluteTimeScale = new Dictionary<string, object> {{"03", 0.1}},
- AfkMinutes = 5,
- TreatAfkAsOffline = true,
- CooldownMinutes = 10,
- MinutesOnlineToQualify = 0,
- InterimDamage = 0,
- ClansPluginShare = false,
- ClansPluginMinMembers = 1,
- RustTeamShare = false,
- RustTeamMinMembers = 1,
- RustClanShare = false,
- RustClanMinMembers = 1,
- ShowMessage = true,
- SimpleStatusEnabled = false,
- SimpleStatusShowForUnauthorizedOnlyWhenHoldingHammer = false,
- SimpleStatusShowAuthorizedProtectionPreview = false,
- SimpleStatusRefreshIntervalSeconds = 1f,
- SimpleStatusStatusId = "antiofflineraid.protection",
- SimpleStatusAuthorizedBackground = "#738D45",
- SimpleStatusUnauthorizedBackground = "#773A2F",
- SimpleStatusAuthorizedIconColor = "#A3FE03",
- SimpleStatusUnauthorizedIconColor = "#FF5338",
- SimpleStatusAuthorizedLabelColor = "#FFFFFF",
- SimpleStatusUnauthorizedLabelColor = "#FF5338",
- SimpleStatusAuthorizedValueColor = "#FFFFFF",
- SimpleStatusUnauthorizedValueColor = "#FF5338",
- SimpleStatusIcon = "https://i.ibb.co/nq6bL11S/protection.png",
- PlaySound = false,
- Prefabs = new List<string>
- {
- "door.hinged", "door.double.hinged", "window.bars", "floor.ladder.hatch",
- "floor.frame", "wall.frame", "shutter", "wall.external", "gates.external", "box", "locker"
- },
- Activities = GetDefaultActivities(),
- ServerTimeOffset = 0,
- SoundFile = "assets/prefabs/weapon mods/silencers/effects/silencer_attach.fx.prefab",
- ProtectBaseWhenAway = false,
- ProtectPlayerMadeBoats = true,
- ProtectTugboats = true,
- GroupSizeProtectionScaling = true,
- GroupSizeUseClansPluginMembers = false,
- GroupSizeUseRustTeams = false,
- GroupSizeUseRustClans = false,
- GroupSizeProtectionScale = new Dictionary<string, float>
- {
- { "1", 1f },
- { "3", 0.90f },
- { "5", 0.80f },
- { "8", 0.70f },
- { "12", 0.60f }
- },
- MinutesSinceLastAttackToProtect = 10,
- DamageScaleKeys = new List<int> { 1, 3, 6, 12, 48 },
- GroupSizeProtectionKeys = new List<int> { 1, 3, 5, 8, 12 }
- };
- }
- }
- private static List<string> GetDefaultActivities()
- {
- return new List<string> { "input", "loot", "respawn", "chat", "wakeup" };
- }
- private static string NormalizeSimpleStatusIconSetting(string value)
- {
- if (string.IsNullOrWhiteSpace(value)) return value;
- var trimmed = value.Trim();
- if (trimmed.StartsWith("[", StringComparison.Ordinal) && trimmed.EndsWith(")", StringComparison.Ordinal))
- {
- var separator = trimmed.IndexOf("](", StringComparison.Ordinal);
- if (separator > 0)
- {
- var linkedValue = trimmed.Substring(separator + 2, trimmed.Length - separator - 3).Trim();
- if (!string.IsNullOrWhiteSpace(linkedValue)) return linkedValue;
- }
- }
- return trimmed;
- }
- private void NormalizeConfig()
- {
- var defaults = Configuration.DefaultConfig();
- if (_config.AbsoluteTimeScale == null) _config.AbsoluteTimeScale = new Dictionary<string, object>();
- if (_config.DamageScale == null || _config.DamageScale.Count == 0) _config.DamageScale = new Dictionary<string, object>(defaults.DamageScale);
- if (_config.GroupSizeProtectionScale == null || _config.GroupSizeProtectionScale.Count == 0) _config.GroupSizeProtectionScale = new Dictionary<string, float>(defaults.GroupSizeProtectionScale);
- if (_config.Activities == null) _config.Activities = GetDefaultActivities();
- if (_config.Prefabs == null) _config.Prefabs = new List<string>(defaults.Prefabs);
- if (string.IsNullOrWhiteSpace(_config.SimpleStatusStatusId)) _config.SimpleStatusStatusId = defaults.SimpleStatusStatusId;
- if (string.IsNullOrWhiteSpace(_config.SimpleStatusAuthorizedBackground)) _config.SimpleStatusAuthorizedBackground = defaults.SimpleStatusAuthorizedBackground;
- if (string.IsNullOrWhiteSpace(_config.SimpleStatusUnauthorizedBackground)) _config.SimpleStatusUnauthorizedBackground = defaults.SimpleStatusUnauthorizedBackground;
- if (string.IsNullOrWhiteSpace(_config.SimpleStatusAuthorizedIconColor)) _config.SimpleStatusAuthorizedIconColor = defaults.SimpleStatusAuthorizedIconColor;
- if (string.IsNullOrWhiteSpace(_config.SimpleStatusUnauthorizedIconColor)) _config.SimpleStatusUnauthorizedIconColor = defaults.SimpleStatusUnauthorizedIconColor;
- if (string.IsNullOrWhiteSpace(_config.SimpleStatusAuthorizedLabelColor)) _config.SimpleStatusAuthorizedLabelColor = defaults.SimpleStatusAuthorizedLabelColor;
- if (string.IsNullOrWhiteSpace(_config.SimpleStatusUnauthorizedLabelColor)) _config.SimpleStatusUnauthorizedLabelColor = defaults.SimpleStatusUnauthorizedLabelColor;
- if (string.IsNullOrWhiteSpace(_config.SimpleStatusAuthorizedValueColor)) _config.SimpleStatusAuthorizedValueColor = defaults.SimpleStatusAuthorizedValueColor;
- if (string.IsNullOrWhiteSpace(_config.SimpleStatusUnauthorizedValueColor)) _config.SimpleStatusUnauthorizedValueColor = defaults.SimpleStatusUnauthorizedValueColor;
- if (string.IsNullOrWhiteSpace(_config.SimpleStatusIcon)) _config.SimpleStatusIcon = defaults.SimpleStatusIcon;
- _config.SimpleStatusIcon = NormalizeSimpleStatusIconSetting(_config.SimpleStatusIcon);
- _config.SimpleStatusRefreshIntervalSeconds = Mathf.Clamp(_config.SimpleStatusRefreshIntervalSeconds, 0.25f, 5f);
- if (string.IsNullOrWhiteSpace(_config.SoundFile)) _config.SoundFile = defaults.SoundFile;
- _config.Activities = _config.Activities.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim().ToLowerInvariant()).Distinct().ToList();
- _config.Prefabs = _config.Prefabs.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).Distinct().ToList();
- if (_config.AfkMinutes < 1)
- {
- PrintWarning($"AfkMinutes cannot be lower than 1; clamping configured value {_config.AfkMinutes} to 1.");
- _config.AfkMinutes = 1;
- }
- _config.CooldownMinutes = Math.Max(0, _config.CooldownMinutes);
- _config.MinutesOnlineToQualify = Math.Max(0, _config.MinutesOnlineToQualify);
- _config.ClansPluginMinMembers = Math.Max(1, _config.ClansPluginMinMembers);
- _config.RustTeamMinMembers = Math.Max(1, _config.RustTeamMinMembers);
- _config.RustClanMinMembers = Math.Max(1, _config.RustClanMinMembers);
- _config.MinutesSinceLastAttackToProtect = Math.Max(-1, _config.MinutesSinceLastAttackToProtect);
- _config.DamageScaleKeys = ParseThresholdKeys(_config.DamageScale.Keys, "DamageScale");
- if (_config.DamageScaleKeys.Count == 0)
- {
- PrintWarning("DamageScale contains no valid numeric thresholds; restoring defaults.");
- _config.DamageScale = new Dictionary<string, object>(defaults.DamageScale);
- _config.DamageScaleKeys = new List<int>(defaults.DamageScaleKeys);
- }
- _config.GroupSizeProtectionKeys = ParseThresholdKeys(_config.GroupSizeProtectionScale.Keys, "GroupSizeProtectionScale");
- if (_config.GroupSizeProtectionKeys.Count == 0)
- {
- PrintWarning("GroupSizeProtectionScale contains no valid numeric thresholds; restoring defaults.");
- _config.GroupSizeProtectionScale = new Dictionary<string, float>(defaults.GroupSizeProtectionScale);
- _config.GroupSizeProtectionKeys = new List<int>(defaults.GroupSizeProtectionKeys);
- }
- }
- private List<int> ParseThresholdKeys(IEnumerable<string> keys, string settingName)
- {
- var result = new List<int>();
- foreach (var raw in keys ?? Enumerable.Empty<string>())
- {
- if (!int.TryParse(raw, out var key) || key < 0)
- {
- PrintWarning($"Ignoring invalid {settingName} threshold '{raw}'. Thresholds must be non-negative integers.");
- continue;
- }
- if (!result.Contains(key)) result.Add(key);
- }
- result.Sort();
- return result;
- }
- #endregion
- #region 2.AntiOfflineRaid.Localization.cs
- private static class PluginMessages
- {
- public const string ProtectionMessage = "Protection Message";
- public const string DeniedPermission = "Denied: Permission";
- public const string InvalidSyntax = "Invalid Syntax";
- public const string PlayerNotFound = "Player Not Found";
- public const string HelpTitle = "Help Title";
- public const string HelpFirstMinutes = "Help First Minutes";
- public const string HelpBetweenMinutesAndHour = "Help Between Minutes And Hour";
- public const string HelpFirstHour = "Help First Hour";
- public const string HelpAfterDays = "Help After Days";
- public const string HelpAfterHours = "Help After Hours";
- public const string StatusHeader = "Status Header";
- public const string StatusPlayerAfk = "Status Player AFK";
- public const string StatusPlayerOffline = "Status Player Offline";
- public const string StatusPlayerOnline = "Status Player Online";
- public const string StatusAfkMinutes = "Status AFK Minutes";
- public const string StatusShare = "Status Share";
- public const string StatusShareLastOffline = "Status Share Last Offline";
- public const string ProviderClansPlugin = "Provider Clans Plugin";
- public const string ProviderRustTeam = "Provider Rust Team";
- public const string ProviderRustClan = "Provider Rust Clan";
- public const string StatusOnline = "Status Online";
- public const string StatusOffline = "Status Offline";
- public const string StatusScale = "Status Scale";
- public const string OfflineProtectionStatus = "Offline Protection Status";
- }
- protected override void LoadDefaultMessages()
- {
- lang.RegisterMessages(new Dictionary<string, string>
- {
- [PluginMessages.ProtectionMessage] = "This building is protected: {amount}%",
- [PluginMessages.DeniedPermission] = "You lack permission to do that",
- [PluginMessages.InvalidSyntax] = "Invalid syntax. Use: /ao <player name>",
- [PluginMessages.PlayerNotFound] = "No player found.",
- [PluginMessages.HelpTitle] = "AntiOfflineRaid by <color=#ce422b>Shady14u</color>",
- [PluginMessages.HelpFirstMinutes] = "<color=#ffd479>First {0} minutes</color>: 100%",
- [PluginMessages.HelpBetweenMinutesAndHour] = "<color=#ffd479>Between {0} minutes and 1 hour</color>: {1}%",
- [PluginMessages.HelpFirstHour] = "<color=#ffd479>First hour</color>: {0}%",
- [PluginMessages.HelpAfterDays] = "<color=#ffd479>After {0} day(s)</color>: {1}%",
- [PluginMessages.HelpAfterHours] = "<color=#ffd479>After {0} hour(s)</color>: {1}%",
- [PluginMessages.StatusHeader] = "<color=#F31D07><size=15>AntiOfflineRaid Status</size></color>: {0}",
- [PluginMessages.StatusPlayerAfk] = "<color=#56C5D5>Player Status</color>: <color=#E49C63>AFK</color>: {0}",
- [PluginMessages.StatusPlayerOffline] = "<color=#56C5D5>Player Status</color>: <color=#F31D07>Offline</color>: {0}",
- [PluginMessages.StatusPlayerOnline] = "<color=#56C5D5>Player Status</color>: <color=#3CF348>Online</color>",
- [PluginMessages.StatusAfkMinutes] = "<color=#56C5D5>AFK</color>: {0} minutes",
- [PluginMessages.StatusShare] = "<color=#56C5D5>{0} Status</color>: {1} ({2} online)",
- [PluginMessages.StatusShareLastOffline] = "<color=#56C5D5>{0} Last Offline</color>: {1} - {2}",
- [PluginMessages.ProviderClansPlugin] = "Clans Plugin",
- [PluginMessages.ProviderRustTeam] = "Rust Team",
- [PluginMessages.ProviderRustClan] = "Rust Clan",
- [PluginMessages.StatusOnline] = "<color=#3CF348>Online</color>",
- [PluginMessages.StatusOffline] = "<color=#F31D07>Offline</color>",
- [PluginMessages.StatusScale] = "<color=#56C5D5>Protection</color>: {0}%",
- [PluginMessages.OfflineProtectionStatus] = "OFFLINE PROTECTION"
- }, this, "en");
- }
- private string GetMsg(string key, object userId = null)
- {
- return lang.GetMessage(key, this, userId?.ToString());
- }
- void SendMessage(HitInfo hitInfo, int amt = 100)
- {
- if (!(hitInfo?.Initiator is BasePlayer player)) return;
- try
- {
- ShowMessage(player, amt);
- }
- catch (Exception ex)
- {
- PrintWarning($"Unable to show protection message to {player.UserIDString}: {ex.Message}");
- }
- }
- #endregion
- #region 3.AntiOfflineRaid.Permissions.cs
- private static class PluginPermissions
- {
- public const string Protect = "antiofflineraid.protect";
- public const string Check = "antiofflineraid.check";
- }
- private void LoadPermissions()
- {
- permission.RegisterPermission(PluginPermissions.Protect, this);
- permission.RegisterPermission(PluginPermissions.Check, this);
- }
- bool HasPerm(BasePlayer p, string pe)
- {
- return permission.UserHasPermission(p.userID.ToString(), pe);
- }
- bool HasPerm(string userid, string pe)
- {
- return permission.UserHasPermission(userid, pe);
- }
- #endregion
- #region 4.AntiOfflineRaid.Data.cs
- private StoredData _storedData;
- public class StoredData
- {
- public Dictionary<ulong, LastOnline> LastOnlines { get; set; } = new Dictionary<ulong, LastOnline>();
- public Dictionary<ulong, long> LastStructureAttackAt { get; set; } = new Dictionary<ulong, long>(); // Protection-scope net ID -> last attack time
- public Dictionary<ulong, double> OnlineQualificationMinutes { get; set; } = new Dictionary<ulong, double>();
- public HashSet<ulong> OnlineQualificationActiveSessions { get; set; } = new HashSet<ulong>();
- public Dictionary<string, List<string>> MembersCache { get; set; } = new Dictionary<string, List<string>>();
- }
- #region BoilerPlate
- private void LoadData()
- {
- try
- {
- _storedData = Interface.GetMod().DataFileSystem.ReadObject<StoredData>("AntiOfflineRaid") ?? new StoredData();
- }
- catch (Exception e)
- {
- Puts(e.Message);
- Puts(e.StackTrace);
- _storedData = new StoredData();
- }
- EnsureDataInitialized();
- }
- private void EnsureDataInitialized()
- {
- if (_storedData == null) _storedData = new StoredData();
- if (_storedData.LastOnlines == null) _storedData.LastOnlines = new Dictionary<ulong, LastOnline>();
- if (_storedData.LastStructureAttackAt == null) _storedData.LastStructureAttackAt = new Dictionary<ulong, long>();
- if (_storedData.OnlineQualificationMinutes == null) _storedData.OnlineQualificationMinutes = new Dictionary<ulong, double>();
- if (_storedData.OnlineQualificationActiveSessions == null) _storedData.OnlineQualificationActiveSessions = new HashSet<ulong>();
- if (_storedData.MembersCache == null) _storedData.MembersCache = new Dictionary<string, List<string>>();
- }
- private void SaveData()
- {
- EnsureDataInitialized();
- Interface.GetMod().DataFileSystem.WriteObject("AntiOfflineRaid", _storedData);
- }
- private void ClearWipeScopedData()
- {
- EnsureDataInitialized();
- _storedData.LastStructureAttackAt.Clear();
- _storedData.MembersCache.Clear();
- _cachedScales.Clear();
- }
- #endregion
- #endregion
- #region 5.AntiOfflineRaid.Hooks.cs
- object OnEntityTakeDamage(BaseCombatEntity entity, HitInfo hitInfo)
- {
- if (hitInfo == null || hitInfo.damageTypes == null || entity == null) return null;
- if (hitInfo.damageTypes.Has(DamageType.Decay)) return null;
- if (entity is BasePlayer) return null;
- // Reject unrelated combat entities before doing parent traversal or protection resolution.
- var structuralType = entity is BuildingBlock || entity is BoatBuildingBlock || entity is PlayerBoat || entity is Tugboat;
- var configuredPrefab = !structuralType && IsConfiguredPrefab(entity);
- if (!structuralType && !configuredPrefab) return null;
- var navalTarget = FindNavalTarget(entity);
- if (structuralType && navalTarget != null && entity != navalTarget && !(navalTarget is PlayerBoat && entity is BoatBuildingBlock))
- configuredPrefab = IsConfiguredPrefab(entity);
- // BoatBuildingBlock forwards damage to its parent PlayerBoat. The child callback already applies protection, so do not scale the forwarded parent callback again
- if (entity is PlayerBoat && hitInfo.HitEntity != null && hitInfo.HitEntity != entity && FindNavalTarget(hitInfo.HitEntity) == entity) return null;
- return IsBlocked(entity, navalTarget, configuredPrefab) ? OnStructureAttack(entity, hitInfo, navalTarget) : null;
- }
- void OnLootItem(BasePlayer player, Item item)
- {
- if (player != null) UpdateLastOnline(player);
- }
- void OnLootPlayer(BasePlayer target, BasePlayer looter)
- {
- if (looter != null) UpdateLastOnline(looter);
- }
- void OnPlayerChat(BasePlayer player, string message, ConVar.Chat.ChatChannel channel)
- {
- if (player != null) UpdateLastOnline(player);
- }
- void OnPlayerConnected(BasePlayer player)
- {
- if (player == null) return;
- StartOnlineQualificationSession(player.userID, true);
- UpdateLastOnline(player);
- if (_storedData.LastOnlines.TryGetValue(player.userID, out var state) && state != null) state.LastPosition = player.transform.position;
- var language = lang.GetLanguage(player.UserIDString);
- if (string.IsNullOrEmpty(language)) language = "en";
- _lastLangByUser[player.userID] = language;
- QueueSimpleStatusRefresh(player, false);
- }
- void OnPlayerDisconnected(BasePlayer player, string reason)
- {
- if (player == null) return;
- HideSimpleStatus(player);
- _lastInputActivityAt.Remove(player.userID);
- _lastLangByUser.Remove(player.userID);
- LastOnline state = null;
- if (_storedData?.LastOnlines != null) _storedData.LastOnlines.TryGetValue(player.userID, out state);
- UpdateOnlineQualification(player, state);
- EndOnlineQualificationSession(player.userID);
- var preserveAfkOfflineState = _config?.TreatAfkAsOffline == true && state != null && state.IsAfk();
- if (preserveAfkOfflineState)
- {
- // Preserve the moment AFK crossed into offline state instead of restarting the offline timer at disconnect.
- state.LastOnlineTime = GetAfkOfflineSince(state);
- state.AfkMinutes = Math.Max(state.AfkMinutes, _config.AfkMinutes);
- InvalidateDamageScale(player.userID);
- }
- else
- {
- UpdateLastOnline(player.userID);
- }
- RefreshAllSimpleStatuses();
- }
- void OnPlayerRespawned(BasePlayer player)
- {
- if (player == null) return;
- UpdateLastOnline(player);
- if (_storedData.LastOnlines.TryGetValue(player.userID, out var state) && state != null) state.LastPosition = player.transform.position;
- QueueSimpleStatusRefresh(player, false);
- }
- void OnPlayerSleepEnded(BasePlayer player)
- {
- if (player == null) return;
- UpdateLastOnline(player);
- if (_storedData.LastOnlines.TryGetValue(player.userID, out var state) && state != null) state.LastPosition = player.transform.position;
- QueueSimpleStatusRefresh(player, false);
- }
- void OnPlayerInput(BasePlayer player, InputState input)
- {
- if (player == null || input?.current == null || input.previous == null) return;
- var now = Time.realtimeSinceStartup;
- if (_lastInputActivityAt.TryGetValue(player.userID, out var last) && now - last < InputActivityThrottleSeconds) return;
- // Observe the final movement after buttons are released without counting idle input packets as activity
- var hasInputActivity = input.current.buttons != 0 && !input.previous.Equals(input.current);
- if (!hasInputActivity)
- {
- if (_storedData?.LastOnlines == null || !_storedData.LastOnlines.TryGetValue(player.userID, out var lastOnline) || lastOnline == null) return;
- if (lastOnline.LastPosition.Equals(player.transform.position)) return;
- }
- _lastInputActivityAt[player.userID] = now;
- UpdateLastOnline(player);
- }
- private void OnActiveItemChanged(BasePlayer player, Item oldItem, Item newItem)
- {
- if (_config?.SimpleStatusEnabled != true || !_config.SimpleStatusShowForUnauthorizedOnlyWhenHoldingHammer || player == null) return;
- QueueSimpleStatusRefresh(player, false);
- }
- private void OnPlayerLanguageChanged(BasePlayer player, string language)
- {
- if (player == null || !player.userID.IsSteamId()) return;
- var current = string.IsNullOrWhiteSpace(language) ? "en" : language.Trim();
- _lastLangByUser[player.userID] = current;
- _simpleStatusStateKeys.Remove(player.userID);
- QueueSimpleStatusRefresh(player, false);
- }
- private void RefreshSimpleStatusAuthorizationState(BasePlayer player = null)
- {
- QueueSimpleStatusRefresh(player);
- }
- private object OnCupboardAssign(BuildingPrivlidge privilege, ulong userId, BasePlayer player)
- {
- RefreshSimpleStatusAuthorizationState(player);
- return null;
- }
- private void OnCupboardAuthorize(BuildingPrivlidge privilege, BasePlayer player)
- {
- RefreshSimpleStatusAuthorizationState(player);
- }
- private void OnCupboardDeauthorize(BuildingPrivlidge privilege, BasePlayer player)
- {
- RefreshSimpleStatusAuthorizationState(player);
- }
- private void OnCupboardClearList(BuildingPrivlidge privilege, BasePlayer player)
- {
- RefreshSimpleStatusAuthorizationState(player);
- }
- private void OnCupboardAuthorize(VehiclePrivilege privilege, BasePlayer player)
- {
- RefreshSimpleStatusAuthorizationState(player);
- }
- private void OnCupboardDeauthorize(VehiclePrivilege privilege, BasePlayer player)
- {
- RefreshSimpleStatusAuthorizationState(player);
- }
- private void OnCupboardClearList(VehiclePrivilege privilege, BasePlayer player)
- {
- RefreshSimpleStatusAuthorizationState(player);
- }
- void OnServerInitialized()
- {
- LoadPermissions();
- LoadData();
- if (_newSaveDetected) ClearWipeScopedData();
- _storedData.MembersCache.Clear();
- _newSaveDetected = false;
- _serverInitialized = true;
- InitializeOnlineQualificationSessions();
- ResolveOptionalPluginReferences();
- if (HelpText != null) Puts("HelpText detected. /help integration is available when AllowHelpTextFromOtherPlugins is enabled.");
- ReportClansPluginAvailability(true);
- ReportSimpleStatusDependencies(true);
- if (_simpleStatusReady) RefreshSimpleStatusIntegration();
- else ScheduleSimpleStatusReadyFallback();
- if (!_config.Activities.Any())
- {
- PrintWarning("At least one activity trigger must be configured for this plugin to operate.");
- ToggleHooks(new List<string>());
- }
- else
- {
- ToggleHooks(_config.Activities);
- UpdateLastOnlineAll(false);
- _lastOnlineTimer = timer.Repeat(AfkCheckIntervalMinutes * 60, 0, () => UpdateLastOnlineAll());
- }
- }
- private void OnServerSave()
- {
- SaveData();
- }
- private void OnNewSave(string strFilename)
- {
- _newSaveDetected = true;
- if (_storedData == null) return;
- ClearWipeScopedData();
- SaveData();
- }
- void OnClanCreate(string tag)
- {
- if (!UsesClansPluginIntegration() || _storedData == null) return;
- CacheClansPluginClan(tag);
- }
- void OnClanUpdate(string tag)
- {
- if (!UsesClansPluginIntegration() || _storedData == null) return;
- CacheClansPluginClan(tag);
- }
- void OnClanDestroy(string tag)
- {
- RemoveCachedClansPluginClan(tag);
- }
- void OnClanDisbanded(string tag, List<ulong> members)
- {
- RemoveCachedClansPluginClan(tag);
- }
- void OnClanMemberJoined(string tag, ulong joining, List<ulong> members)
- {
- if (!UsesClansPluginIntegration() || _storedData == null) return;
- CacheClansPluginClan(tag);
- }
- void OnClanMemberGone(string tag, ulong leaving, List<ulong> members)
- {
- if (!UsesClansPluginIntegration() || _storedData == null) return;
- CacheClansPluginClan(tag);
- }
- private void RemoveCachedClansPluginClan(string tag)
- {
- if (!UsesClansPluginIntegration() || _storedData?.MembersCache == null || string.IsNullOrWhiteSpace(tag)) return;
- _storedData.MembersCache.Remove(tag);
- }
- void OnPluginLoaded(Plugin plugin)
- {
- if (plugin == null) return;
- if (IsClansPlugin(plugin))
- {
- Clans = plugin;
- _storedData?.MembersCache?.Clear();
- NextTick(() => ReportClansPluginAvailability());
- }
- if (IsSimpleStatusPlugin(plugin))
- {
- SimpleStatus = plugin;
- if (_config?.SimpleStatusEnabled == true)
- {
- NextTick(() =>
- {
- ReportSimpleStatusDependencies();
- ScheduleSimpleStatusReadyFallback();
- });
- }
- }
- if (IsImageLibraryPlugin(plugin))
- {
- ImageLibrary = plugin;
- ResetSimpleStatusIcon();
- if (_config?.SimpleStatusEnabled == true)
- {
- NextTick(() =>
- {
- ReportSimpleStatusDependencies();
- if (_simpleStatusReady) RefreshSimpleStatusIntegration();
- });
- }
- }
- if (IsHelpTextPlugin(plugin))
- {
- HelpText = plugin;
- Puts("HelpText detected. /help integration is available when AllowHelpTextFromOtherPlugins is enabled.");
- }
- }
- void OnPluginUnloaded(Plugin plugin)
- {
- if (plugin == null) return;
- if (IsClansPlugin(plugin))
- {
- Clans = null;
- _storedData?.MembersCache?.Clear();
- NextTick(() => ReportClansPluginAvailability());
- }
- if (IsSimpleStatusPlugin(plugin))
- {
- SimpleStatus = null;
- _simpleStatusReady = false;
- StopSimpleStatusIntegration(false);
- if (_config?.SimpleStatusEnabled == true) NextTick(() => ReportSimpleStatusDependencies());
- }
- if (IsImageLibraryPlugin(plugin))
- {
- ImageLibrary = null;
- ResetSimpleStatusIcon();
- StopSimpleStatusIntegration(true);
- if (_config?.SimpleStatusEnabled == true) NextTick(() => ReportSimpleStatusDependencies());
- }
- if (IsHelpTextPlugin(plugin)) HelpText = null;
- }
- private void OnSimpleStatusReady()
- {
- _simpleStatusReady = true;
- if (!_serverInitialized || _config?.SimpleStatusEnabled != true) return;
- ResolveOptionalPluginReferences();
- NextTick(() =>
- {
- ResetSimpleStatusIcon();
- ReportSimpleStatusDependencies();
- RefreshSimpleStatusIntegration();
- });
- }
- protected void ToggleHook(List<string> wakeupTriggers, string trigger, string hook)
- {
- if (wakeupTriggers.Contains(trigger))
- {
- Subscribe(hook);
- }
- else
- {
- Unsubscribe(hook);
- }
- }
- protected void ToggleHooks(List<string> wakeupTriggers)
- {
- var defaultWakeupTriggers = GetDefaultActivities();
- foreach (var trigger in defaultWakeupTriggers)
- {
- switch (trigger)
- {
- case "input":
- ToggleHook(wakeupTriggers, trigger, "OnPlayerInput");
- break;
- case "loot":
- ToggleHook(wakeupTriggers, trigger, "OnLootPlayer");
- ToggleHook(wakeupTriggers, trigger, "OnLootItem");
- break;
- case "respawn":
- ToggleHook(wakeupTriggers, trigger, "OnPlayerRespawned");
- break;
- case "chat":
- ToggleHook(wakeupTriggers, trigger, "OnPlayerChat");
- break;
- case "wakeup":
- ToggleHook(wakeupTriggers, trigger, "OnPlayerSleepEnded");
- break;
- }
- }
- }
- private void Unload()
- {
- _lastOnlineTimer?.Destroy();
- _lastOnlineTimer = null;
- StopSimpleStatusIntegration(true);
- UpdateLastOnlineAll();
- SaveData();
- _cachedScales.Clear();
- _configuredPrefabMatches.Clear();
- _simpleStatusColorCache.Clear();
- _lastInputActivityAt.Clear();
- _onlineQualificationLastUpdateAt.Clear();
- _lastLangByUser.Clear();
- _lastReportedClansPluginAvailable = null;
- _lastReportedSimpleStatusAvailable = null;
- _lastReportedImageLibraryAvailable = null;
- _serverInitialized = false;
- _simpleStatusReady = false;
- }
- #endregion
- #region 6.AntiOfflineRaid.Commands.cs
- [ConsoleCommand("ao")]
- void CheckOfflineStatus(ConsoleSystem.Arg arg)
- {
- if (arg == null) return;
- var player = arg.Connection?.player as BasePlayer;
- if (player != null && !HasPerm(player, PluginPermissions.Check) && arg.Connection.authLevel < 1)
- {
- SendReply(arg, GetMsg(PluginMessages.DeniedPermission, arg.Connection.userid));
- return;
- }
- var userId = player?.UserIDString;
- var playerSearch = arg.Args != null && arg.Args.Length > 0 ? arg.Args[0].ToString() : null;
- if (string.IsNullOrWhiteSpace(playerSearch))
- {
- SendReply(arg, GetMsg(PluginMessages.InvalidSyntax, userId));
- return;
- }
- SendReply(arg, SendStatus(playerSearch, userId));
- }
- [ChatCommand("ao")]
- void CheckOfflineStatus(BasePlayer player, string command, string[] args)
- {
- if (player == null) return;
- if (!HasPerm(player, PluginPermissions.Check) && (player.net?.connection?.authLevel ?? 0) < 1)
- {
- SendReply(player, GetMsg(PluginMessages.DeniedPermission, player.UserIDString));
- return;
- }
- if (args == null || args.Length < 1 || string.IsNullOrWhiteSpace(args[0]))
- {
- SendReply(player, GetMsg(PluginMessages.InvalidSyntax, player.UserIDString));
- return;
- }
- SendReply(player, SendStatus(args[0], player.UserIDString));
- }
- [ChatCommand("ao.fill.onlineTimes")]
- void CmdFillOnlines(BasePlayer player, string command, string[] args)
- {
- if (player == null || !player.IsAdmin) return;
- EnsureDataInitialized();
- var changed = false;
- foreach (var basePlayer in covalence.Players.All)
- {
- if (!ulong.TryParse(basePlayer.Id, out var userId) || !userId.IsSteamId()) continue;
- if (_storedData.LastOnlines.ContainsKey(userId)) continue;
- UpdateLastOnline(userId);
- changed = true;
- }
- if (changed) SaveData();
- }
- void SendHelpText(BasePlayer player)
- {
- if (player == null) return;
- var userId = player.UserIDString;
- var stringBuilder = new StringBuilder(GetMsg(PluginMessages.HelpTitle, userId)).Append('\n');
- if (_config.CooldownMinutes > 0)
- {
- stringBuilder.AppendFormat(GetMsg(PluginMessages.HelpFirstMinutes, userId), _config.CooldownMinutes).Append('\n');
- stringBuilder.AppendFormat(GetMsg(PluginMessages.HelpBetweenMinutesAndHour, userId), _config.CooldownMinutes, Math.Round(_config.InterimDamage * 100f, 0)).Append('\n');
- }
- else
- {
- stringBuilder.AppendFormat(GetMsg(PluginMessages.HelpFirstHour, userId), Math.Round(_config.InterimDamage * 100f, 0)).Append('\n');
- }
- foreach (var key in _config.DamageScaleKeys)
- {
- if (!_config.DamageScale.TryGetValue(key.ToString(), out var rawScale)) continue;
- double scaleValue;
- try
- {
- scaleValue = Convert.ToDouble(rawScale, CultureInfo.InvariantCulture);
- }
- catch
- {
- continue;
- }
- var scale = Math.Round(scaleValue * 100, 0);
- var hours = Math.Round(Convert.ToDouble(key), 1);
- if (hours >= 24)
- {
- var days = Math.Round(hours / 24, 1);
- stringBuilder.AppendFormat(GetMsg(PluginMessages.HelpAfterDays, userId), days, scale).Append('\n');
- }
- else
- {
- stringBuilder.AppendFormat(GetMsg(PluginMessages.HelpAfterHours, userId), hours, scale).Append('\n');
- }
- }
- player.ChatMessage(stringBuilder.ToString());
- }
- #endregion
- #region 7.AntiOfflineRaid.Classes.cs
- public class LastOnline
- {
- [JsonIgnore] public float AfkMinutes = 0;
- public long LastOnlineLong;
- [JsonIgnore] public Vector3 LastPosition;
- public ulong UserId;
- [JsonConstructor]
- public LastOnline(ulong userid, long lastOnlineLong)
- {
- UserId = userid;
- LastOnlineLong = lastOnlineLong;
- }
- public LastOnline(ulong userId, DateTime lastOnline)
- {
- UserId = userId;
- LastOnlineTime = lastOnline;
- }
- [JsonIgnore] public BasePlayer AarPlayer => BasePlayer.FindByID(UserId);
- [JsonIgnore] public double Days => (DateTime.Now - LastOnlineTime).TotalDays;
- [JsonIgnore] public double Hours => (DateTime.Now - LastOnlineTime).TotalHours;
- [JsonIgnore]
- public DateTime LastOnlineTime
- {
- get => DateTime.FromBinary(LastOnlineLong);
- set => LastOnlineLong = value.ToBinary();
- }
- [JsonIgnore] public double Minutes => (DateTime.Now - LastOnlineTime).TotalMinutes;
- public bool HasMoved(Vector3 position)
- {
- var moved = !LastPosition.Equals(position);
- LastPosition = new Vector3(position.x, position.y, position.z);
- return moved;
- }
- public bool IsAfk()
- {
- return Minutes >= _config.AfkMinutes;
- }
- public bool IsConnected()
- {
- var player = AarPlayer;
- return player != null && player.IsConnected;
- }
- public bool IsOffline()
- {
- if (!IsConnected()) return true;
- return _config.TreatAfkAsOffline && IsAfk();
- }
- }
- class ScaleCacheItem
- {
- public DateTime Expires;
- public float Scale;
- public ScaleCacheItem(DateTime expires, float scale)
- {
- Expires = expires;
- Scale = scale;
- }
- }
- private struct SimpleStatusStateKey
- {
- public ulong TargetId;
- public bool PrivilegeBased;
- public bool HasAccess;
- public int ProtectionPercent;
- public string Label;
- public string Background;
- public string IconColor;
- public string LabelColor;
- public string ValueColor;
- public string Icon;
- public SimpleStatusStateKey(ulong targetId, bool privilegeBased, bool hasAccess, int protectionPercent, string label, string background, string iconColor, string labelColor, string valueColor, string icon)
- {
- TargetId = targetId;
- PrivilegeBased = privilegeBased;
- HasAccess = hasAccess;
- ProtectionPercent = protectionPercent;
- Label = label;
- Background = background;
- IconColor = iconColor;
- LabelColor = labelColor;
- ValueColor = valueColor;
- Icon = icon;
- }
- public bool Equals(SimpleStatusStateKey other)
- {
- return TargetId == other.TargetId && PrivilegeBased == other.PrivilegeBased && HasAccess == other.HasAccess && ProtectionPercent == other.ProtectionPercent &&
- string.Equals(Label, other.Label, StringComparison.Ordinal) && string.Equals(Background, other.Background, StringComparison.Ordinal) &&
- string.Equals(IconColor, other.IconColor, StringComparison.Ordinal) && string.Equals(LabelColor, other.LabelColor, StringComparison.Ordinal) &&
- string.Equals(ValueColor, other.ValueColor, StringComparison.Ordinal) && string.Equals(Icon, other.Icon, StringComparison.Ordinal);
- }
- }
- #endregion
- #region 0.AntiOfflineRaid.cs
- [PluginReference] Plugin Clans;
- [PluginReference] Plugin SimpleStatus;
- [PluginReference] Plugin ImageLibrary;
- [PluginReference] Plugin HelpText;
- private const string OfflineProtectionStatusIconKey = "AntiOfflineRaid_Protection_Icon";
- private const string JsonMessage =
- @"[{""name"":""AntiOfflineRaidMsg"",""parent"":""Overlay"",""components"":[{""type"":""UnityEngine.UI.Image"",""color"":""0 0 0 0.78""},{""type"":""RectTransform"",""anchormax"":""0.64 0.88"",""anchormin"":""0.38 0.79""}]},{""name"":""MessageLabel{1}"",""parent"":""AntiOfflineRaidMsg"",""components"":[{""type"":""UnityEngine.UI.Text"",""align"":""MiddleCenter"",""fontSize"":""19"",""text"":""{protection_message}""},{""type"":""RectTransform"",""anchormax"":""1 1"",""anchormin"":""0 0""}]}]";
- private const float AfkCheckIntervalMinutes = 1f;
- private const float NavalPresenceRadius = 30f;
- private const float InputActivityThrottleSeconds = 1f;
- private const float SimpleStatusIconCheckIntervalSeconds = 0.25f;
- private const float SimpleStatusMinBatchDelaySeconds = 0.01f;
- private const int SimpleStatusPlayersPerBatch = 32;
- private const int SimpleStatusIconCheckAttempts = 120;
- private const int ConfiguredPrefabCacheLimit = 2048;
- private const int SimpleStatusColorCacheLimit = 16;
- private readonly Dictionary<ulong, ScaleCacheItem> _cachedScales = new Dictionary<ulong, ScaleCacheItem>();
- private readonly Dictionary<string, bool> _configuredPrefabMatches = new Dictionary<string, bool>(StringComparer.Ordinal);
- private readonly Dictionary<string, string> _simpleStatusColorCache = new Dictionary<string, string>(StringComparer.Ordinal);
- private readonly Dictionary<ulong, float> _lastInputActivityAt = new Dictionary<ulong, float>();
- private readonly Dictionary<ulong, DateTime> _onlineQualificationLastUpdateAt = new Dictionary<ulong, DateTime>();
- private readonly Dictionary<Type, System.Reflection.FieldInfo> _vehiclePrivilegeAuthorizedPlayersFieldByType = new Dictionary<Type, System.Reflection.FieldInfo>();
- private readonly Dictionary<Type, System.Reflection.FieldInfo> _authorizationEntryUserIdFieldByType = new Dictionary<Type, System.Reflection.FieldInfo>();
- private readonly Dictionary<Type, System.Reflection.PropertyInfo> _authorizationEntryUserIdPropertyByType = new Dictionary<Type, System.Reflection.PropertyInfo>();
- private Timer _lastOnlineTimer;
- private Timer _simpleStatusTimer;
- private Timer _languageWatchTimer;
- private readonly List<BasePlayer> _simpleStatusBatchPlayers = new List<BasePlayer>();
- private readonly Queue<BasePlayer> _simpleStatusPriorityPlayers = new Queue<BasePlayer>();
- private readonly HashSet<BasePlayer> _simpleStatusPriorityPlayerSet = new HashSet<BasePlayer>();
- private int _simpleStatusBatchCursor;
- private int _simpleStatusBatchSize;
- private int _simpleStatusSchedulerGeneration;
- private float _simpleStatusBatchDelaySeconds;
- private float _simpleStatusScheduledDelaySeconds;
- private float _simpleStatusNextRunAt;
- private bool _simpleStatusRefreshRequested;
- private bool _simpleStatusRefreshQueued;
- private readonly Dictionary<ulong, string> _lastLangByUser = new Dictionary<ulong, string>();
- private readonly Dictionary<ulong, SimpleStatusStateKey> _simpleStatusStateKeys = new Dictionary<ulong, SimpleStatusStateKey>();
- private readonly HashSet<ulong> _simpleStatusVisiblePlayers = new HashSet<ulong>();
- private bool? _lastReportedClansPluginAvailable;
- private bool? _lastReportedSimpleStatusAvailable;
- private bool? _lastReportedImageLibraryAvailable;
- private bool _serverInitialized;
- private bool _newSaveDetected;
- private bool _simpleStatusReady;
- private bool _simpleStatusCreated;
- private bool _simpleStatusIconRegistered;
- private bool _simpleStatusIconRegistrationPending;
- private string _simpleStatusIconPendingKey;
- private string _resolvedSimpleStatusIcon;
- private bool IsClansPluginAvailable()
- {
- return Clans != null;
- }
- private bool UsesClansPluginIntegration()
- {
- return _config != null && (_config.ClansPluginShare || (_config.GroupSizeProtectionScaling && _config.GroupSizeUseClansPluginMembers));
- }
- private void ReportClansPluginAvailability(bool force = false)
- {
- if (!UsesClansPluginIntegration())
- {
- _lastReportedClansPluginAvailable = null;
- return;
- }
- var available = IsClansPluginAvailable();
- if (!force && _lastReportedClansPluginAvailable.HasValue && _lastReportedClansPluginAvailable.Value == available) return;
- _lastReportedClansPluginAvailable = available;
- if (available) Puts("Clans plugin detected. External clan support is enabled.");
- else PrintWarning("Clans plugin support is enabled, but the Clans plugin is not loaded. External clan checks will be skipped until it becomes available.");
- }
- private static bool IsClansPlugin(Plugin plugin)
- {
- return plugin != null && string.Equals(plugin.Name, "Clans", StringComparison.OrdinalIgnoreCase);
- }
- private static bool IsSimpleStatusPlugin(Plugin plugin)
- {
- return plugin != null && (string.Equals(plugin.Name, "SimpleStatus", StringComparison.OrdinalIgnoreCase) || string.Equals(plugin.Name, "Simple Status", StringComparison.OrdinalIgnoreCase));
- }
- private static bool IsImageLibraryPlugin(Plugin plugin)
- {
- return plugin != null && (string.Equals(plugin.Name, "ImageLibrary", StringComparison.OrdinalIgnoreCase) || string.Equals(plugin.Name, "Image Library", StringComparison.OrdinalIgnoreCase));
- }
- private static bool IsHelpTextPlugin(Plugin plugin)
- {
- return plugin != null && (string.Equals(plugin.Name, "HelpText", StringComparison.OrdinalIgnoreCase) || string.Equals(plugin.Name, "Help Text", StringComparison.OrdinalIgnoreCase));
- }
- private void ResolveOptionalPluginReferences()
- {
- if (Clans == null) Clans = plugins.Find("Clans");
- if (SimpleStatus == null) SimpleStatus = plugins.Find("SimpleStatus") ?? plugins.Find("Simple Status");
- if (ImageLibrary == null) ImageLibrary = plugins.Find("ImageLibrary") ?? plugins.Find("Image Library");
- if (HelpText == null) HelpText = plugins.Find("HelpText") ?? plugins.Find("Help Text");
- }
- private bool IsSimpleStatusIntegrationReady()
- {
- return _serverInitialized && _simpleStatusReady && _config?.SimpleStatusEnabled == true && SimpleStatus != null && ImageLibrary != null;
- }
- private void ScheduleSimpleStatusReadyFallback()
- {
- if (!_serverInitialized || _config?.SimpleStatusEnabled != true || SimpleStatus == null || _simpleStatusReady) return;
- timer.Once(1f, () =>
- {
- if (!_serverInitialized || _config?.SimpleStatusEnabled != true || SimpleStatus == null || _simpleStatusReady) return;
- _simpleStatusReady = true;
- ReportSimpleStatusDependencies();
- RefreshSimpleStatusIntegration();
- });
- }
- private void ReportSimpleStatusDependencies(bool force = false)
- {
- if (_config?.SimpleStatusEnabled != true)
- {
- _lastReportedSimpleStatusAvailable = null;
- _lastReportedImageLibraryAvailable = null;
- return;
- }
- ResolveOptionalPluginReferences();
- var simpleStatusAvailable = SimpleStatus != null;
- if (force || !_lastReportedSimpleStatusAvailable.HasValue || _lastReportedSimpleStatusAvailable.Value != simpleStatusAvailable)
- {
- _lastReportedSimpleStatusAvailable = simpleStatusAvailable;
- if (simpleStatusAvailable) Puts("SimpleStatus detected. Offline protection status integration is enabled.");
- else PrintWarning("SimpleStatus support is enabled, but SimpleStatus is not loaded. Offline protection status display is unavailable.");
- }
- var imageLibraryAvailable = ImageLibrary != null;
- if (force || !_lastReportedImageLibraryAvailable.HasValue || _lastReportedImageLibraryAvailable.Value != imageLibraryAvailable)
- {
- _lastReportedImageLibraryAvailable = imageLibraryAvailable;
- if (imageLibraryAvailable) Puts("ImageLibrary detected. Offline protection status icon integration is enabled.");
- else PrintWarning("SimpleStatus support is enabled, but ImageLibrary is not loaded. Offline protection status display is unavailable.");
- }
- }
- private string GetClansPluginTag(ulong userId)
- {
- if (!userId.IsSteamId() || !IsClansPluginAvailable()) return null;
- try
- {
- return Clans.Call<string>("GetClanOf", userId);
- }
- catch (Exception ex)
- {
- PrintWarning($"Unable to query clan for {userId}: {ex.Message}");
- return null;
- }
- }
- public List<string> CacheClansPluginClan(string tag)
- {
- if (string.IsNullOrWhiteSpace(tag) || !IsClansPluginAvailable()) return new List<string>();
- try
- {
- var clan = Clans.Call<JObject>("GetClan", tag);
- var members = new List<string>();
- if (clan?["members"] != null)
- {
- foreach (var token in clan["members"])
- {
- var memberId = token?.ToString();
- if (!string.IsNullOrWhiteSpace(memberId) && !members.Contains(memberId)) members.Add(memberId);
- }
- }
- _storedData.MembersCache[tag] = members;
- return members;
- }
- catch (Exception ex)
- {
- PrintWarning($"Unable to cache clan '{tag}': {ex.Message}");
- return new List<string>();
- }
- }
- float CacheDamageScale(ulong targetId, float scale, ScaleCacheItem kvp = null)
- {
- if (kvp == null)
- {
- _cachedScales[targetId] = new ScaleCacheItem(DateTime.Now.AddMinutes(1), scale);
- }
- else
- {
- kvp.Expires = DateTime.Now.AddMinutes(1);
- kvp.Scale = scale;
- }
- return scale;
- }
- private void InvalidateDamageScale(ulong userId)
- {
- _cachedScales.Remove(userId);
- }
- private bool UsesOnlineQualification()
- {
- return _config != null && _config.MinutesOnlineToQualify > 0;
- }
- private void InitializeOnlineQualificationSessions()
- {
- EnsureDataInitialized();
- _onlineQualificationLastUpdateAt.Clear();
- if (!UsesOnlineQualification())
- {
- _storedData.OnlineQualificationMinutes.Clear();
- _storedData.OnlineQualificationActiveSessions.Clear();
- return;
- }
- var activeIds = new HashSet<ulong>();
- foreach (var player in BasePlayer.activePlayerList)
- {
- if (player != null && player.IsConnected && player.userID.IsSteamId()) activeIds.Add(player.userID);
- }
- _storedData.OnlineQualificationActiveSessions.RemoveWhere(userId => !activeIds.Contains(userId));
- var now = DateTime.Now;
- foreach (var userId in activeIds)
- {
- if (!_storedData.OnlineQualificationActiveSessions.Contains(userId) || !_storedData.OnlineQualificationMinutes.ContainsKey(userId))
- _storedData.OnlineQualificationMinutes[userId] = 0d;
- _storedData.OnlineQualificationActiveSessions.Add(userId);
- _onlineQualificationLastUpdateAt[userId] = now;
- InvalidateDamageScale(userId);
- }
- }
- private void StartOnlineQualificationSession(ulong userId, bool reset)
- {
- if (!UsesOnlineQualification() || !userId.IsSteamId()) return;
- EnsureDataInitialized();
- if (reset || !_storedData.OnlineQualificationMinutes.ContainsKey(userId)) _storedData.OnlineQualificationMinutes[userId] = 0d;
- _storedData.OnlineQualificationActiveSessions.Add(userId);
- _onlineQualificationLastUpdateAt[userId] = DateTime.Now;
- InvalidateDamageScale(userId);
- }
- private void UpdateOnlineQualification(BasePlayer player, LastOnline lastOnline = null)
- {
- if (!UsesOnlineQualification() || player == null || !player.userID.IsSteamId()) return;
- EnsureDataInitialized();
- if (!_storedData.OnlineQualificationActiveSessions.Contains(player.userID)) return;
- var now = DateTime.Now;
- if (!_onlineQualificationLastUpdateAt.TryGetValue(player.userID, out var lastUpdate))
- {
- _onlineQualificationLastUpdateAt[player.userID] = now;
- return;
- }
- _onlineQualificationLastUpdateAt[player.userID] = now;
- if (now <= lastUpdate) return;
- if (lastOnline == null) _storedData.LastOnlines.TryGetValue(player.userID, out lastOnline);
- var countUntil = now;
- if (_config.TreatAfkAsOffline && lastOnline != null)
- {
- var afkAt = lastOnline.LastOnlineTime.AddMinutes(_config.AfkMinutes);
- if (afkAt < countUntil) countUntil = afkAt;
- }
- if (countUntil <= lastUpdate) return;
- _storedData.OnlineQualificationMinutes.TryGetValue(player.userID, out var previousMinutes);
- var updatedMinutes = previousMinutes + (countUntil - lastUpdate).TotalMinutes;
- _storedData.OnlineQualificationMinutes[player.userID] = Math.Max(0d, updatedMinutes);
- if (previousMinutes < _config.MinutesOnlineToQualify && updatedMinutes >= _config.MinutesOnlineToQualify) InvalidateDamageScale(player.userID);
- }
- private void EndOnlineQualificationSession(ulong userId)
- {
- _onlineQualificationLastUpdateAt.Remove(userId);
- _storedData?.OnlineQualificationActiveSessions?.Remove(userId);
- InvalidateDamageScale(userId);
- }
- private bool IsProtectionQualified(ulong userId)
- {
- if (!UsesOnlineQualification()) return true;
- if (!userId.IsSteamId()) return false;
- EnsureDataInitialized();
- return _storedData.OnlineQualificationMinutes.TryGetValue(userId, out var minutes) && minutes >= _config.MinutesOnlineToQualify;
- }
- protected IPlayer FindPlayerByPartialName(string nameOrIdOrIp)
- {
- return string.IsNullOrWhiteSpace(nameOrIdOrIp) ? null : covalence.Players.FindPlayer(nameOrIdOrIp);
- }
- public List<string> GetClansPluginMembers(string tag)
- {
- if (string.IsNullOrWhiteSpace(tag) || !IsClansPluginAvailable()) return new List<string>();
- if (_storedData.MembersCache.TryGetValue(tag, out var memberList) && memberList != null) return memberList;
- return CacheClansPluginClan(tag);
- }
- private HashSet<ulong> GetRustTeamMembers(ulong targetId)
- {
- var members = new HashSet<ulong>();
- if (!targetId.IsSteamId()) return members;
- members.Add(targetId);
- try
- {
- var team = RelationshipManager.ServerInstance?.FindPlayersTeam(targetId);
- if (team?.members == null) return members;
- foreach (var memberId in team.members)
- {
- if (memberId.IsSteamId()) members.Add(memberId);
- }
- }
- catch
- {
- }
- return members;
- }
- private HashSet<ulong> GetRustClanMembers(ulong targetId)
- {
- var members = new HashSet<ulong>();
- if (!targetId.IsSteamId()) return members;
- members.Add(targetId);
- try
- {
- var player = BasePlayer.FindByID(targetId) ?? BasePlayer.FindSleeping(targetId);
- var clanId = player?.clanId ?? 0L;
- if (clanId == 0L) return members;
- IClan clan;
- if (!(ClanManager.ServerInstance?.Backend?.TryGet(clanId, out clan) ?? false) || clan?.Members == null) return members;
- foreach (var member in clan.Members)
- {
- if (member.SteamId.IsSteamId()) members.Add(member.SteamId);
- }
- }
- catch (Exception ex)
- {
- PrintWarning($"Unable to query Rust native clan for {targetId}: {ex.Message}");
- }
- return members;
- }
- private HashSet<ulong> GetClansPluginMemberIds(ulong targetId)
- {
- var members = new HashSet<ulong>();
- if (!targetId.IsSteamId()) return members;
- members.Add(targetId);
- if (!IsClansPluginAvailable()) return members;
- var tag = GetClansPluginTag(targetId);
- if (string.IsNullOrEmpty(tag)) return members;
- foreach (var memberId in GetClansPluginMembers(tag))
- {
- if (ulong.TryParse(memberId, out var userId) && userId.IsSteamId()) members.Add(userId);
- }
- return members;
- }
- private bool UsesSharedProtectionGroups()
- {
- return _config != null && (_config.ClansPluginShare || _config.RustTeamShare || _config.RustClanShare);
- }
- private HashSet<ulong> GetSharedProtectionMembers(ulong targetId)
- {
- var members = new HashSet<ulong>();
- if (!targetId.IsSteamId()) return members;
- members.Add(targetId);
- if (_config.ClansPluginShare && IsClansPluginAvailable()) members.UnionWith(GetClansPluginMemberIds(targetId));
- if (_config.RustTeamShare) members.UnionWith(GetRustTeamMembers(targetId));
- if (_config.RustClanShare) members.UnionWith(GetRustClanMembers(targetId));
- return members;
- }
- private bool TryEvaluateSharedProtectionProvider(HashSet<ulong> members, int minOnlineMembers, out LastOnline selected, out bool groupOnline)
- {
- selected = null;
- groupOnline = false;
- if (members == null || members.Count == 0) return false;
- minOnlineMembers = Math.Max(1, minOnlineMembers);
- var onlineCount = 0;
- foreach (var userId in members)
- {
- if (IsOffline(userId)) continue;
- onlineCount++;
- if (onlineCount < minOnlineMembers) continue;
- groupOnline = true;
- return true;
- }
- foreach (var userId in members)
- {
- if (!IsProtectionQualified(userId)) continue;
- if (!_storedData.LastOnlines.TryGetValue(userId, out var lastOnline) || lastOnline == null || !lastOnline.IsOffline()) continue;
- if (selected == null || lastOnline.LastOnlineTime > selected.LastOnlineTime) selected = lastOnline;
- }
- // Protection always follows the member who was active most recently. This keeps group protection conservative and avoids aging a base from an older offline member.
- return true;
- }
- private void AddSharedProtectionCandidate(HashSet<ulong> members, int minOnlineMembers, List<LastOnline> candidates, ref bool anyGroupOnline)
- {
- if (!TryEvaluateSharedProtectionProvider(members, minOnlineMembers, out var selected, out var groupOnline)) return;
- if (groupOnline)
- {
- anyGroupOnline = true;
- return;
- }
- if (selected != null) candidates.Add(selected);
- }
- private bool TryGetProtectionLastOnline(ulong targetId, out LastOnline protectionLastOnline)
- {
- protectionLastOnline = null;
- if (!_storedData.LastOnlines.TryGetValue(targetId, out var ownerLastOnline) || ownerLastOnline == null) return false;
- if (!UsesSharedProtectionGroups())
- {
- protectionLastOnline = ownerLastOnline;
- return true;
- }
- var candidates = Pool.Get<List<LastOnline>>();
- try
- {
- candidates.Clear();
- var anyGroupOnline = false;
- if (_config.ClansPluginShare && IsClansPluginAvailable())
- AddSharedProtectionCandidate(GetClansPluginMemberIds(targetId), _config.ClansPluginMinMembers, candidates, ref anyGroupOnline);
- if (_config.RustTeamShare)
- AddSharedProtectionCandidate(GetRustTeamMembers(targetId), _config.RustTeamMinMembers, candidates, ref anyGroupOnline);
- if (_config.RustClanShare)
- AddSharedProtectionCandidate(GetRustClanMembers(targetId), _config.RustClanMinMembers, candidates, ref anyGroupOnline);
- if (anyGroupOnline) return false;
- if (candidates.Count == 0)
- {
- protectionLastOnline = ownerLastOnline;
- return true;
- }
- // When multiple share providers apply, use the most recently offline selected member to avoid granting stronger protection than any enabled provider allows.
- protectionLastOnline = candidates[0];
- for (var i = 1; i < candidates.Count; i++)
- {
- if (candidates[i].LastOnlineTime > protectionLastOnline.LastOnlineTime) protectionLastOnline = candidates[i];
- }
- return true;
- }
- finally
- {
- Pool.FreeUnmanaged(ref candidates);
- }
- }
- private HashSet<ulong> GetAuthorizedClansPluginMembers(IEnumerable<ulong> authorizedIds)
- {
- var members = new HashSet<ulong>();
- if (!_config.ClansPluginShare || !IsClansPluginAvailable() || authorizedIds == null) return members;
- foreach (var userId in authorizedIds)
- {
- if (!userId.IsSteamId()) continue;
- members.UnionWith(GetClansPluginMemberIds(userId));
- }
- return members;
- }
- private HashSet<ulong> GetAuthorizedRustTeamMembers(IEnumerable<ulong> authorizedIds)
- {
- var members = new HashSet<ulong>();
- if (!_config.RustTeamShare || authorizedIds == null) return members;
- foreach (var userId in authorizedIds)
- {
- if (!userId.IsSteamId()) continue;
- members.UnionWith(GetRustTeamMembers(userId));
- }
- return members;
- }
- private HashSet<ulong> GetAuthorizedRustClanMembers(IEnumerable<ulong> authorizedIds)
- {
- var members = new HashSet<ulong>();
- if (!_config.RustClanShare || authorizedIds == null) return members;
- foreach (var userId in authorizedIds)
- {
- if (!userId.IsSteamId()) continue;
- members.UnionWith(GetRustClanMembers(userId));
- }
- return members;
- }
- private void AddProtectionLastOnlineCandidates(IEnumerable<ulong> userIds, List<LastOnline> candidates)
- {
- if (userIds == null || candidates == null) return;
- foreach (var userId in userIds)
- {
- if (!userId.IsSteamId() || !HasPerm(userId.ToString(), PluginPermissions.Protect) || !IsProtectionQualified(userId)) continue;
- if (!_storedData.LastOnlines.TryGetValue(userId, out var lastOnline) || lastOnline == null || !lastOnline.IsOffline()) continue;
- var exists = false;
- for (var i = 0; i < candidates.Count; i++)
- {
- if (candidates[i].UserId != userId) continue;
- exists = true;
- break;
- }
- if (!exists) candidates.Add(lastOnline);
- }
- }
- private bool CountOnlineMembersAtLeast(IEnumerable<ulong> members, int minimum)
- {
- if (members == null) return false;
- minimum = Math.Max(1, minimum);
- var count = 0;
- foreach (var userId in members)
- {
- if (IsOffline(userId)) continue;
- count++;
- if (count >= minimum) return true;
- }
- return false;
- }
- private bool TryResolveDirectPrivilegeProtectionSource(List<ulong> authorizedIds, out ulong targetId, out LastOnline protectionLastOnline, out bool groupOnline)
- {
- targetId = 0UL;
- protectionLastOnline = null;
- groupOnline = false;
- if (authorizedIds == null || authorizedIds.Count == 0) return false;
- EnsureDataInitialized();
- LastOnline mostRecentOffline = null;
- var hasQualifiedFallback = false;
- foreach (var userId in authorizedIds)
- {
- if (!userId.IsSteamId()) continue;
- _storedData.LastOnlines.TryGetValue(userId, out var lastOnline);
- var offline = lastOnline != null ? lastOnline.IsOffline() : IsOffline(userId);
- if (!offline) groupOnline = true;
- if (lastOnline == null || !HasPerm(userId.ToString(), PluginPermissions.Protect)) continue;
- var qualified = IsProtectionQualified(userId);
- if (protectionLastOnline == null || (qualified && !hasQualifiedFallback))
- {
- targetId = userId;
- protectionLastOnline = lastOnline;
- hasQualifiedFallback = qualified;
- }
- if (!qualified || !offline) continue;
- if (mostRecentOffline == null || lastOnline.LastOnlineTime > mostRecentOffline.LastOnlineTime) mostRecentOffline = lastOnline;
- }
- if (mostRecentOffline != null)
- {
- protectionLastOnline = mostRecentOffline;
- targetId = mostRecentOffline.UserId;
- }
- return protectionLastOnline != null;
- }
- private bool TryResolvePrivilegeProtectionSource(List<ulong> authorizedIds, out ulong targetId, out LastOnline protectionLastOnline, out bool groupOnline)
- {
- targetId = 0UL;
- protectionLastOnline = null;
- groupOnline = false;
- if (authorizedIds == null || authorizedIds.Count == 0) return false;
- if (!UsesSharedProtectionGroups()) return TryResolveDirectPrivilegeProtectionSource(authorizedIds, out targetId, out protectionLastOnline, out groupOnline);
- var directMembers = new HashSet<ulong>();
- foreach (var userId in authorizedIds)
- {
- if (userId.IsSteamId()) directMembers.Add(userId);
- }
- if (directMembers.Count == 0) return false;
- foreach (var userId in directMembers)
- {
- if (!IsOffline(userId))
- {
- groupOnline = true;
- break;
- }
- }
- var candidates = Pool.Get<List<LastOnline>>();
- try
- {
- candidates.Clear();
- AddProtectionLastOnlineCandidates(directMembers, candidates);
- HashSet<ulong> clansMembers = null;
- if (_config.ClansPluginShare && IsClansPluginAvailable())
- {
- clansMembers = GetAuthorizedClansPluginMembers(directMembers);
- if (CountOnlineMembersAtLeast(clansMembers, _config.ClansPluginMinMembers)) groupOnline = true;
- AddProtectionLastOnlineCandidates(clansMembers, candidates);
- }
- HashSet<ulong> teamMembers = null;
- if (_config.RustTeamShare)
- {
- teamMembers = GetAuthorizedRustTeamMembers(directMembers);
- if (CountOnlineMembersAtLeast(teamMembers, _config.RustTeamMinMembers)) groupOnline = true;
- AddProtectionLastOnlineCandidates(teamMembers, candidates);
- }
- HashSet<ulong> rustClanMembers = null;
- if (_config.RustClanShare)
- {
- rustClanMembers = GetAuthorizedRustClanMembers(directMembers);
- if (CountOnlineMembersAtLeast(rustClanMembers, _config.RustClanMinMembers)) groupOnline = true;
- AddProtectionLastOnlineCandidates(rustClanMembers, candidates);
- }
- if (candidates.Count > 0)
- {
- protectionLastOnline = candidates[0];
- for (var i = 1; i < candidates.Count; i++)
- {
- if (candidates[i].LastOnlineTime > protectionLastOnline.LastOnlineTime) protectionLastOnline = candidates[i];
- }
- targetId = protectionLastOnline.UserId;
- return targetId.IsSteamId();
- }
- var qualificationSources = new HashSet<ulong>(directMembers);
- if (clansMembers != null) qualificationSources.UnionWith(clansMembers);
- if (teamMembers != null) qualificationSources.UnionWith(teamMembers);
- if (rustClanMembers != null) qualificationSources.UnionWith(rustClanMembers);
- foreach (var userId in qualificationSources)
- {
- if (!HasPerm(userId.ToString(), PluginPermissions.Protect) || !IsProtectionQualified(userId)) continue;
- if (!_storedData.LastOnlines.TryGetValue(userId, out var lastOnline) || lastOnline == null) continue;
- targetId = userId;
- protectionLastOnline = lastOnline;
- return true;
- }
- foreach (var userId in directMembers)
- {
- if (!HasPerm(userId.ToString(), PluginPermissions.Protect)) continue;
- if (!_storedData.LastOnlines.TryGetValue(userId, out var lastOnline) || lastOnline == null) continue;
- targetId = userId;
- protectionLastOnline = lastOnline;
- return true;
- }
- return false;
- }
- finally
- {
- Pool.FreeUnmanaged(ref candidates);
- }
- }
- private HashSet<ulong> GetPrivilegeAccessMembers(IEnumerable<ulong> authorizedIds)
- {
- var seeds = new HashSet<ulong>();
- if (authorizedIds == null) return seeds;
- foreach (var userId in authorizedIds)
- if (userId.IsSteamId()) seeds.Add(userId);
- var members = new HashSet<ulong>(seeds);
- if (_config.ClansPluginShare && IsClansPluginAvailable()) members.UnionWith(GetAuthorizedClansPluginMembers(seeds));
- if (_config.RustTeamShare) members.UnionWith(GetAuthorizedRustTeamMembers(seeds));
- if (_config.RustClanShare) members.UnionWith(GetAuthorizedRustClanMembers(seeds));
- return members;
- }
- private bool HasPrivilegeProtectionAccess(ulong viewerId, IEnumerable<ulong> authorizedIds)
- {
- if (!viewerId.IsSteamId() || authorizedIds == null) return false;
- if (!UsesSharedProtectionGroups())
- {
- foreach (var userId in authorizedIds)
- if (userId == viewerId) return true;
- return false;
- }
- return GetPrivilegeAccessMembers(authorizedIds).Contains(viewerId);
- }
- private bool IsPrivilegeGroupInBuildingRange(IEnumerable<ulong> authorizedIds, BuildingPrivlidge privilege)
- {
- if (privilege == null || authorizedIds == null) return false;
- if (!UsesSharedProtectionGroups())
- {
- foreach (var userId in authorizedIds)
- if (userId.IsSteamId() && IsPlayerNearBuildingPrivilege(userId, privilege)) return true;
- return false;
- }
- foreach (var userId in GetPrivilegeAccessMembers(authorizedIds))
- if (IsPlayerNearBuildingPrivilege(userId, privilege)) return true;
- return false;
- }
- private bool IsPrivilegeGroupNearEntity(IEnumerable<ulong> authorizedIds, BaseEntity entity)
- {
- if (entity == null || authorizedIds == null) return false;
- if (!UsesSharedProtectionGroups())
- {
- foreach (var userId in authorizedIds)
- {
- if (!userId.IsSteamId()) continue;
- var player = BasePlayer.FindByID(userId);
- if (player != null && player.IsConnected && Vector3.Distance(player.transform.position, entity.transform.position) <= NavalPresenceRadius) return true;
- }
- return false;
- }
- foreach (var userId in GetPrivilegeAccessMembers(authorizedIds))
- {
- var player = BasePlayer.FindByID(userId);
- if (player != null && player.IsConnected && Vector3.Distance(player.transform.position, entity.transform.position) <= NavalPresenceRadius) return true;
- }
- return false;
- }
- private static ulong GetLockOwnerId(BaseEntity entity)
- {
- var lockEntity = entity?.GetSlot(BaseEntity.Slot.Lock);
- return lockEntity != null && lockEntity.OwnerID.IsSteamId() ? lockEntity.OwnerID : 0UL;
- }
- private bool TryResolveEntityFallbackProtectionSource(BaseEntity entity, out ulong targetId, out LastOnline protectionLastOnline, out bool groupOnline)
- {
- targetId = 0UL;
- protectionLastOnline = null;
- groupOnline = false;
- if (entity == null) return false;
- var lockOwnerId = GetLockOwnerId(entity);
- if (lockOwnerId.IsSteamId() && TryResolveFallbackProtectionSource(lockOwnerId, out targetId, out protectionLastOnline, out groupOnline)) return true;
- if (entity.OwnerID.IsSteamId() && entity.OwnerID != lockOwnerId && TryResolveFallbackProtectionSource(entity.OwnerID, out targetId, out protectionLastOnline, out groupOnline)) return true;
- return false;
- }
- private bool TryResolveNavalFallbackProtectionSource(BaseEntity attackedEntity, BaseEntity navalTarget, VehiclePrivilege privilege, out ulong targetId, out LastOnline protectionLastOnline, out bool groupOnline)
- {
- if (TryResolveEntityFallbackProtectionSource(attackedEntity, out targetId, out protectionLastOnline, out groupOnline)) return true;
- if (privilege != null && privilege.OwnerID.IsSteamId() && TryResolveFallbackProtectionSource(privilege.OwnerID, out targetId, out protectionLastOnline, out groupOnline)) return true;
- if (navalTarget != null && navalTarget.OwnerID.IsSteamId() && TryResolveFallbackProtectionSource(navalTarget.OwnerID, out targetId, out protectionLastOnline, out groupOnline)) return true;
- targetId = 0UL;
- protectionLastOnline = null;
- groupOnline = false;
- return false;
- }
- private bool HasFallbackProtectionAccess(ulong viewerId, ulong targetId)
- {
- if (!viewerId.IsSteamId() || !targetId.IsSteamId()) return false;
- if (viewerId == targetId) return true;
- return UsesSharedProtectionGroups() && GetSharedProtectionMembers(targetId).Contains(viewerId);
- }
- private int GetProtectionGroupSize(ulong targetId, IEnumerable<ulong> authorizedIds = null)
- {
- if (!_config.GroupSizeProtectionScaling) return 1;
- if (!_config.GroupSizeUseRustTeams && !_config.GroupSizeUseClansPluginMembers && !_config.GroupSizeUseRustClans)
- {
- if (authorizedIds == null) return 1;
- var authorizedCount = 0;
- foreach (var userId in authorizedIds)
- {
- if (userId.IsSteamId()) authorizedCount++;
- }
- return Math.Max(1, authorizedCount);
- }
- var relevantUsers = Pool.Get<List<ulong>>();
- var authorizedUsers = Pool.Get<List<ulong>>();
- try
- {
- relevantUsers.Clear();
- authorizedUsers.Clear();
- if (targetId.IsSteamId()) relevantUsers.Add(targetId);
- if (authorizedIds != null)
- {
- foreach (var userId in authorizedIds)
- {
- if (!userId.IsSteamId()) continue;
- if (!authorizedUsers.Contains(userId)) authorizedUsers.Add(userId);
- if (!relevantUsers.Contains(userId)) relevantUsers.Add(userId);
- }
- }
- // TC / vehicle privilege authorization is always the baseline when group-size scaling is enabled.
- var groupSize = Math.Max(1, authorizedUsers.Count);
- if (_config.GroupSizeUseRustTeams)
- {
- foreach (var userId in relevantUsers)
- groupSize = Math.Max(groupSize, GetRustTeamMembers(userId).Count);
- }
- if (_config.GroupSizeUseClansPluginMembers && IsClansPluginAvailable())
- {
- foreach (var userId in relevantUsers)
- groupSize = Math.Max(groupSize, GetClansPluginMemberIds(userId).Count);
- }
- if (_config.GroupSizeUseRustClans)
- {
- foreach (var userId in relevantUsers)
- groupSize = Math.Max(groupSize, GetRustClanMembers(userId).Count);
- }
- return Math.Max(1, groupSize);
- }
- finally
- {
- Pool.FreeUnmanaged(ref authorizedUsers);
- Pool.FreeUnmanaged(ref relevantUsers);
- }
- }
- private float ApplyGroupSizeProtectionScaling(float damageScale, int groupSize)
- {
- if (!_config.GroupSizeProtectionScaling || damageScale < 0f || damageScale >= 1f || groupSize <= 1) return damageScale;
- if (_config.GroupSizeProtectionKeys == null || _config.GroupSizeProtectionKeys.Count == 0) return damageScale;
- var threshold = -1;
- for (var i = 0; i < _config.GroupSizeProtectionKeys.Count; i++)
- {
- var key = _config.GroupSizeProtectionKeys[i];
- if (key > groupSize) break;
- threshold = key;
- }
- if (threshold < 0 || !_config.GroupSizeProtectionScale.TryGetValue(threshold.ToString(), out var protectionFactor)) return damageScale;
- protectionFactor = Mathf.Clamp01(protectionFactor);
- var baseProtection = 1f - damageScale;
- var adjustedProtection = baseProtection * protectionFactor;
- return Mathf.Clamp01(1f - adjustedProtection);
- }
- private void ResetSimpleStatusIcon()
- {
- _simpleStatusIconRegistered = false;
- _simpleStatusIconRegistrationPending = false;
- _simpleStatusIconPendingKey = null;
- _resolvedSimpleStatusIcon = null;
- }
- private string NormalizeSimpleStatusColor(string hex)
- {
- if (string.IsNullOrWhiteSpace(hex)) return "1 1 1 1";
- if (_simpleStatusColorCache.TryGetValue(hex, out var value)) return value;
- var normalized = hex.Trim();
- if (!normalized.StartsWith("#", StringComparison.Ordinal)) normalized = "#" + normalized;
- if (!ColorUtility.TryParseHtmlString(normalized, out var color)) return "1 1 1 1";
- color.a = 1f;
- value = string.Format(CultureInfo.InvariantCulture, "{0:0.###} {1:0.###} {2:0.###} {3:0.###}", color.r, color.g, color.b, color.a);
- if (_simpleStatusColorCache.Count < SimpleStatusColorCacheLimit) _simpleStatusColorCache[hex] = value;
- return value;
- }
- private static string GetSimpleStatusIconKey(string source)
- {
- unchecked
- {
- uint hash = 2166136261;
- foreach (var character in source ?? string.Empty)
- {
- hash ^= character;
- hash *= 16777619;
- }
- return $"{OfflineProtectionStatusIconKey}_{hash:X8}";
- }
- }
- private bool HasImageLibraryImage(string imageKey)
- {
- if (ImageLibrary == null || string.IsNullOrWhiteSpace(imageKey)) return false;
- try
- {
- return ImageLibrary.Call<bool>("HasImage", imageKey, 0UL);
- }
- catch
- {
- return false;
- }
- }
- private void WaitForSimpleStatusIcon(string imageKey, int attemptsRemaining)
- {
- if (!_simpleStatusIconRegistrationPending || !string.Equals(_simpleStatusIconPendingKey, imageKey, StringComparison.Ordinal) || ImageLibrary == null) return;
- if (HasImageLibraryImage(imageKey))
- {
- _simpleStatusIconRegistrationPending = false;
- _simpleStatusIconPendingKey = null;
- _simpleStatusIconRegistered = true;
- _resolvedSimpleStatusIcon = imageKey;
- NextTick(RefreshAllSimpleStatuses);
- return;
- }
- if (attemptsRemaining <= 0)
- {
- _simpleStatusIconRegistrationPending = false;
- _simpleStatusIconPendingKey = null;
- PrintWarning("Unable to register the configured SimpleStatus icon in ImageLibrary.");
- return;
- }
- timer.Once(SimpleStatusIconCheckIntervalSeconds, () => WaitForSimpleStatusIcon(imageKey, attemptsRemaining - 1));
- }
- private void BeginSimpleStatusIconRegistration(string imageKey, Func<bool> register)
- {
- if (string.IsNullOrWhiteSpace(imageKey) || register == null) return;
- _simpleStatusIconRegistrationPending = true;
- _simpleStatusIconPendingKey = imageKey;
- var started = false;
- try
- {
- started = register();
- }
- catch (Exception ex)
- {
- PrintWarning($"Unable to register SimpleStatus icon: {ex.Message}");
- }
- if (!started)
- {
- _simpleStatusIconRegistrationPending = false;
- _simpleStatusIconPendingKey = null;
- PrintWarning("ImageLibrary rejected the configured SimpleStatus icon registration request.");
- return;
- }
- WaitForSimpleStatusIcon(imageKey, SimpleStatusIconCheckAttempts);
- }
- private void EnsureSimpleStatusIcon()
- {
- if (_simpleStatusIconRegistered || _simpleStatusIconRegistrationPending) return;
- _resolvedSimpleStatusIcon = null;
- var configuredIcon = _config?.SimpleStatusIcon;
- if (string.IsNullOrWhiteSpace(configuredIcon)) return;
- var icon = configuredIcon.Trim();
- if (icon.StartsWith("assets/", StringComparison.OrdinalIgnoreCase) || icon.StartsWith("itemid:", StringComparison.OrdinalIgnoreCase) || icon.StartsWith("raw:", StringComparison.OrdinalIgnoreCase))
- {
- _resolvedSimpleStatusIcon = icon;
- _simpleStatusIconRegistered = true;
- return;
- }
- if (ImageLibrary == null) return;
- if (HasImageLibraryImage(icon))
- {
- _resolvedSimpleStatusIcon = icon;
- _simpleStatusIconRegistered = true;
- return;
- }
- if (icon.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || icon.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
- {
- var imageKey = GetSimpleStatusIconKey(icon);
- if (HasImageLibraryImage(imageKey))
- {
- _resolvedSimpleStatusIcon = imageKey;
- _simpleStatusIconRegistered = true;
- return;
- }
- BeginSimpleStatusIconRegistration(imageKey, () => ImageLibrary != null && ImageLibrary.Call<bool>("AddImage", icon, imageKey, 0UL));
- return;
- }
- var dataDir = Interface.Oxide.DataDirectory;
- var baseDir = Path.Combine(dataDir, "AntiOfflineRaid", "images");
- try { Directory.CreateDirectory(baseDir); } catch { }
- string filePath;
- if (Path.IsPathRooted(icon)) filePath = icon;
- else if (icon.Contains("/") || icon.Contains("\\")) filePath = Path.Combine(dataDir, icon.TrimStart('/', '\\'));
- else filePath = Path.Combine(baseDir, icon);
- if (!File.Exists(filePath))
- {
- PrintWarning($"Configured SimpleStatus icon file was not found: {filePath}");
- return;
- }
- try
- {
- var imageKey = GetSimpleStatusIconKey($"{filePath}|{File.GetLastWriteTimeUtc(filePath).Ticks}");
- if (HasImageLibraryImage(imageKey))
- {
- _resolvedSimpleStatusIcon = imageKey;
- _simpleStatusIconRegistered = true;
- return;
- }
- var bytes = File.ReadAllBytes(filePath);
- BeginSimpleStatusIconRegistration(imageKey, () => ImageLibrary != null && ImageLibrary.Call<bool>("AddImageData", imageKey, bytes, 0UL));
- }
- catch (Exception ex)
- {
- PrintWarning($"Unable to register local SimpleStatus icon: {ex.Message}");
- }
- }
- private void RefreshLanguageWatchTimer()
- {
- _languageWatchTimer?.Destroy();
- _languageWatchTimer = null;
- if (_config?.SimpleStatusEnabled != true || SimpleStatus == null || !_simpleStatusCreated) return;
- foreach (var player in BasePlayer.activePlayerList)
- {
- if (player == null || !player.userID.IsSteamId()) continue;
- var language = lang.GetLanguage(player.UserIDString);
- if (string.IsNullOrEmpty(language)) language = "en";
- _lastLangByUser[player.userID] = language;
- }
- _languageWatchTimer = timer.Every(3f, () =>
- {
- if (_config?.SimpleStatusEnabled != true || SimpleStatus == null || !_simpleStatusCreated) return;
- foreach (var player in BasePlayer.activePlayerList)
- {
- if (player == null || !player.userID.IsSteamId()) continue;
- var language = lang.GetLanguage(player.UserIDString);
- if (string.IsNullOrEmpty(language)) language = "en";
- if (_lastLangByUser.TryGetValue(player.userID, out var previous) && string.Equals(previous, language, StringComparison.OrdinalIgnoreCase)) continue;
- _lastLangByUser[player.userID] = language;
- _simpleStatusStateKeys.Remove(player.userID);
- QueueSimpleStatusRefresh(player, false);
- }
- });
- }
- private void RefreshSimpleStatusIntegration()
- {
- StopSimpleStatusIntegration(true);
- if (!IsSimpleStatusIntegrationReady()) return;
- EnsureSimpleStatusIcon();
- var props = new Dictionary<string, object>
- {
- ["color"] = NormalizeSimpleStatusColor(_config.SimpleStatusAuthorizedBackground),
- ["title"] = GetMsg(PluginMessages.OfflineProtectionStatus),
- ["titleColor"] = NormalizeSimpleStatusColor(_config.SimpleStatusAuthorizedLabelColor),
- ["text"] = "0%",
- ["textColor"] = NormalizeSimpleStatusColor(_config.SimpleStatusAuthorizedValueColor),
- ["icon"] = _resolvedSimpleStatusIcon,
- ["iconColor"] = NormalizeSimpleStatusColor(_config.SimpleStatusAuthorizedIconColor)
- };
- try
- {
- SimpleStatus.Call("CreateStatus", this, _config.SimpleStatusStatusId, props);
- _simpleStatusCreated = true;
- }
- catch (Exception ex)
- {
- _simpleStatusCreated = false;
- PrintWarning($"Unable to create SimpleStatus offline protection status: {ex.Message}");
- return;
- }
- _simpleStatusStateKeys.Clear();
- _simpleStatusVisiblePlayers.Clear();
- foreach (var player in BasePlayer.activePlayerList) HideSimpleStatus(player);
- RefreshAllSimpleStatuses();
- }
- private void StopSimpleStatusIntegration(bool hideStatuses)
- {
- StopSimpleStatusScheduler();
- if (hideStatuses && SimpleStatus != null && _simpleStatusCreated)
- {
- foreach (var player in BasePlayer.activePlayerList)
- HideSimpleStatus(player);
- }
- _simpleStatusCreated = false;
- _simpleStatusStateKeys.Clear();
- _simpleStatusVisiblePlayers.Clear();
- }
- private void RefreshAllSimpleStatuses()
- {
- QueueSimpleStatusRefresh();
- }
- private void QueueSimpleStatusRefresh(BasePlayer player = null, bool refreshAll = true)
- {
- if (!IsSimpleStatusIntegrationReady() || !_simpleStatusCreated) return;
- if (player != null && !player.IsDestroyed && player.IsConnected && player.userID.IsSteamId() && _simpleStatusPriorityPlayerSet.Add(player))
- _simpleStatusPriorityPlayers.Enqueue(player);
- if (refreshAll) _simpleStatusRefreshRequested = true;
- if (_simpleStatusRefreshQueued) return;
- _simpleStatusRefreshQueued = true;
- var generation = _simpleStatusSchedulerGeneration;
- NextTick(() =>
- {
- if (generation != _simpleStatusSchedulerGeneration) return;
- _simpleStatusRefreshQueued = false;
- ProcessSimpleStatusRefreshRequest();
- });
- }
- private bool HasConnectedSimpleStatusPlayers()
- {
- foreach (var player in BasePlayer.activePlayerList)
- {
- if (player != null && !player.IsDestroyed && player.IsConnected && player.userID.IsSteamId()) return true;
- }
- return false;
- }
- private void ProcessSimpleStatusRefreshRequest()
- {
- if (!IsSimpleStatusIntegrationReady() || !_simpleStatusCreated) return;
- if (!HasConnectedSimpleStatusPlayers())
- {
- StopSimpleStatusScheduler();
- return;
- }
- var generation = _simpleStatusSchedulerGeneration;
- RefreshSimpleStatusPriorityPlayers();
- if (generation != _simpleStatusSchedulerGeneration || !IsSimpleStatusIntegrationReady() || !_simpleStatusCreated) return;
- if (_languageWatchTimer == null || _languageWatchTimer.Destroyed) RefreshLanguageWatchTimer();
- // A refresh request must not reset an unfinished cycle or postpone an already scheduled batch
- if (_simpleStatusTimer == null || _simpleStatusTimer.Destroyed || _simpleStatusPriorityPlayers.Count > 0 || (_simpleStatusBatchPlayers.Count == 0 && _simpleStatusRefreshRequested))
- ScheduleSimpleStatusBatch(SimpleStatusMinBatchDelaySeconds, true);
- }
- private void RefreshSimpleStatusPriorityPlayers()
- {
- var generation = _simpleStatusSchedulerGeneration;
- var count = Math.Min(SimpleStatusPlayersPerBatch, _simpleStatusPriorityPlayers.Count);
- for (var index = 0; index < count; index++)
- {
- var player = _simpleStatusPriorityPlayers.Dequeue();
- _simpleStatusPriorityPlayerSet.Remove(player);
- if (player != null && !player.IsDestroyed && player.IsConnected) UpdateSimpleStatusFor(player);
- if (generation != _simpleStatusSchedulerGeneration || !IsSimpleStatusIntegrationReady() || !_simpleStatusCreated) return;
- }
- }
- private void ScheduleSimpleStatusBatch(float delay, bool onlyEarlier = false)
- {
- if (!IsSimpleStatusIntegrationReady() || !_simpleStatusCreated) return;
- delay = Mathf.Max(SimpleStatusMinBatchDelaySeconds, delay);
- var now = Time.realtimeSinceStartup;
- var nextRunAt = now + delay;
- if (_simpleStatusTimer != null && !_simpleStatusTimer.Destroyed)
- {
- if (onlyEarlier && _simpleStatusNextRunAt <= nextRunAt) return;
- if (!onlyEarlier && Mathf.Abs(_simpleStatusScheduledDelaySeconds - delay) < 0.0001f && now - _simpleStatusNextRunAt <= Mathf.Max(0.05f, delay))
- {
- _simpleStatusNextRunAt = nextRunAt;
- return;
- }
- _simpleStatusTimer.Destroy();
- }
- // Reuse the repeating timer at steady population; restart it only when cadence changes or it falls behind
- _simpleStatusScheduledDelaySeconds = delay;
- _simpleStatusNextRunAt = nextRunAt;
- _simpleStatusTimer = timer.Every(delay, RefreshSimpleStatusBatch);
- }
- private void StopSimpleStatusScheduler()
- {
- _simpleStatusTimer?.Destroy();
- _simpleStatusTimer = null;
- _languageWatchTimer?.Destroy();
- _languageWatchTimer = null;
- _simpleStatusSchedulerGeneration++;
- _simpleStatusBatchPlayers.Clear();
- _simpleStatusPriorityPlayers.Clear();
- _simpleStatusPriorityPlayerSet.Clear();
- _simpleStatusBatchCursor = 0;
- _simpleStatusBatchSize = 0;
- _simpleStatusBatchDelaySeconds = 0f;
- _simpleStatusScheduledDelaySeconds = 0f;
- _simpleStatusNextRunAt = 0f;
- _simpleStatusRefreshRequested = false;
- _simpleStatusRefreshQueued = false;
- }
- private void RefreshSimpleStatusBatch()
- {
- if (!IsSimpleStatusIntegrationReady() || !_simpleStatusCreated || BasePlayer.activePlayerList.Count == 0)
- {
- StopSimpleStatusScheduler();
- return;
- }
- var generation = _simpleStatusSchedulerGeneration;
- RefreshSimpleStatusPriorityPlayers();
- if (generation != _simpleStatusSchedulerGeneration || !IsSimpleStatusIntegrationReady() || !_simpleStatusCreated) return;
- var refreshInterval = Mathf.Max(0.25f, _config.SimpleStatusRefreshIntervalSeconds);
- if (_simpleStatusBatchPlayers.Count == 0)
- {
- // Snapshot once per cycle so joins, leaves and delayed callbacks cannot move the cursor past players
- foreach (var player in BasePlayer.activePlayerList)
- {
- if (player != null && !player.IsDestroyed && player.IsConnected && player.userID.IsSteamId()) _simpleStatusBatchPlayers.Add(player);
- }
- if (_simpleStatusBatchPlayers.Count == 0)
- {
- StopSimpleStatusScheduler();
- return;
- }
- _simpleStatusBatchCursor = 0;
- _simpleStatusRefreshRequested = false;
- var batchCount = Math.Max(1, Mathf.CeilToInt(_simpleStatusBatchPlayers.Count / (float)SimpleStatusPlayersPerBatch));
- _simpleStatusBatchSize = SimpleStatusPlayersPerBatch;
- _simpleStatusBatchDelaySeconds = Mathf.Max(SimpleStatusMinBatchDelaySeconds, refreshInterval / batchCount);
- }
- var end = Math.Min(_simpleStatusBatchCursor + _simpleStatusBatchSize, _simpleStatusBatchPlayers.Count);
- while (_simpleStatusBatchCursor < end)
- {
- var player = _simpleStatusBatchPlayers[_simpleStatusBatchCursor++];
- if (player != null && !player.IsDestroyed && player.IsConnected) UpdateSimpleStatusFor(player);
- if (generation != _simpleStatusSchedulerGeneration || !IsSimpleStatusIntegrationReady() || !_simpleStatusCreated) return;
- }
- if (_simpleStatusBatchCursor < _simpleStatusBatchPlayers.Count)
- {
- ScheduleSimpleStatusBatch(_simpleStatusBatchDelaySeconds);
- return;
- }
- _simpleStatusBatchPlayers.Clear();
- _simpleStatusBatchCursor = 0;
- ScheduleSimpleStatusBatch(_simpleStatusBatchDelaySeconds);
- }
- private void HideSimpleStatus(BasePlayer player)
- {
- if (player == null) return;
- _simpleStatusStateKeys.Remove(player.userID);
- if (SimpleStatus == null || _config == null)
- {
- _simpleStatusVisiblePlayers.Remove(player.userID);
- return;
- }
- try
- {
- SimpleStatus.Call("SetStatus", player.UserIDString, _config.SimpleStatusStatusId, 0);
- _simpleStatusVisiblePlayers.Remove(player.userID);
- }
- catch
- {
- // Keep the visible marker so the next refresh retries clearing a potentially stale status.
- }
- }
- private void HideSimpleStatusIfVisible(BasePlayer player)
- {
- if (player == null) return;
- if (!_simpleStatusVisiblePlayers.Contains(player.userID) && !_simpleStatusStateKeys.ContainsKey(player.userID)) return;
- HideSimpleStatus(player);
- }
- private bool IsHoldingHammer(BasePlayer player)
- {
- var activeItem = player?.GetActiveItem();
- return activeItem?.info != null && string.Equals(activeItem.info.shortname, "hammer", StringComparison.OrdinalIgnoreCase);
- }
- private void AppendBuildingPrivilegeAuthorizedIds(BuildingPrivlidge privilege, List<ulong> result)
- {
- if (privilege?.authorizedPlayers == null || result == null) return;
- foreach (var entry in privilege.authorizedPlayers)
- {
- var userId = GetAuthorizationEntryUserId(entry);
- if (userId.IsSteamId() && !result.Contains(userId)) result.Add(userId);
- }
- }
- private static ulong GetProtectionScopeId(BaseEntity contextEntity, ulong fallbackTargetId)
- {
- if (contextEntity?.net != null && contextEntity.net.ID.Value != 0UL) return contextEntity.net.ID.Value;
- return fallbackTargetId;
- }
- private DateTime GetAfkOfflineSince(LastOnline lastOnline)
- {
- if (lastOnline == null) return DateTime.Now;
- var offlineSince = lastOnline.LastOnlineTime.AddMinutes(_config.AfkMinutes);
- return offlineSince > DateTime.Now ? DateTime.Now : offlineSince;
- }
- private DateTime GetOfflineSince(LastOnline lastOnline)
- {
- if (lastOnline == null) return DateTime.Now;
- if (lastOnline.IsConnected() && _config.TreatAfkAsOffline && lastOnline.IsAfk()) return GetAfkOfflineSince(lastOnline);
- return lastOnline.LastOnlineTime;
- }
- private bool HasProtectionCooldownElapsed(LastOnline lastOnline)
- {
- if (lastOnline == null || !lastOnline.IsOffline()) return false;
- if (_config.CooldownMinutes <= 0) return true;
- return (DateTime.Now - GetOfflineSince(lastOnline)).TotalMinutes >= _config.CooldownMinutes;
- }
- private float GetCurrentBaseProtectionDamageScale(LastOnline lastOnline)
- {
- if (lastOnline == null || !IsProtectionQualified(lastOnline.UserId) || !HasProtectionCooldownElapsed(lastOnline)) return -1f;
- var offlineSince = GetOfflineSince(lastOnline);
- if (offlineSince <= lastOnline.LastOnlineTime) return ScaleDamageCached(lastOnline);
- return CalculateProtectionDamageScale(offlineSince);
- }
- private bool IsProtectionDelayedForStatus(ulong protectionScopeId, LastOnline protectionLastOnline)
- {
- if (protectionLastOnline == null || !protectionLastOnline.IsOffline()) return false;
- if (!HasProtectionCooldownElapsed(protectionLastOnline)) return true;
- var delayMinutes = _config.MinutesSinceLastAttackToProtect;
- if (delayMinutes <= 0) return false;
- return (DateTime.Now - GetEffectiveProtectionStart(protectionScopeId, protectionLastOnline)).TotalMinutes <= delayMinutes;
- }
- private float GetOfflineProtectionPreviewScale(int groupSize)
- {
- if (_config.DamageScale == null || _config.DamageScaleKeys == null || _config.DamageScaleKeys.Count == 0) return 1f;
- var firstThreshold = _config.DamageScaleKeys[0];
- if (!_config.DamageScale.TryGetValue(firstThreshold.ToString(), out var rawScale)) return 1f;
- try
- {
- var scale = Convert.ToSingle(rawScale, CultureInfo.InvariantCulture);
- return ApplyGroupSizeProtectionScaling(Mathf.Clamp01(scale), groupSize);
- }
- catch
- {
- PrintWarning($"Invalid DamageScale value for preview threshold '{firstThreshold}'.");
- return 1f;
- }
- }
- private static bool IsSameEntity(BaseEntity first, BaseEntity second)
- {
- if (first == null || second == null) return false;
- if (ReferenceEquals(first, second)) return true;
- return first.net != null && second.net != null && first.net.ID.Value == second.net.ID.Value;
- }
- private BaseEntity GetSimpleStatusLookEntity(BasePlayer player)
- {
- if (player == null) return null;
- RaycastHit hit;
- if (!Physics.Raycast(player.eyes.HeadRay(), out hit, 8f)) return null;
- return hit.GetEntity();
- }
- private bool IsEntityInBuildingPrivilege(BaseEntity entity, BuildingPrivlidge privilege)
- {
- if (entity == null) return false;
- if (privilege == null) return entity.GetBuildingPrivilege() == null;
- return IsSameEntity(entity.GetBuildingPrivilege(), privilege);
- }
- private bool IsEntityInNavalTarget(BaseEntity entity, BaseEntity navalTarget)
- {
- if (entity == null || navalTarget == null) return false;
- if (IsSameEntity(entity, navalTarget)) return true;
- return IsSameEntity(FindNavalTarget(entity), navalTarget);
- }
- private bool TryResolveFallbackProtectionSource(ulong baseTargetId, out ulong targetId, out LastOnline protectionLastOnline, out bool groupOnline)
- {
- targetId = 0UL;
- protectionLastOnline = null;
- groupOnline = false;
- if (!baseTargetId.IsSteamId()) return false;
- if (!UsesSharedProtectionGroups())
- {
- groupOnline = !IsOffline(baseTargetId);
- if (!HasPerm(baseTargetId.ToString(), PluginPermissions.Protect)) return false;
- if (!_storedData.LastOnlines.TryGetValue(baseTargetId, out protectionLastOnline) || protectionLastOnline == null) return false;
- targetId = baseTargetId;
- return true;
- }
- var candidates = Pool.Get<List<LastOnline>>();
- try
- {
- candidates.Clear();
- groupOnline = !IsOffline(baseTargetId);
- AddProtectionLastOnlineCandidates(new[] { baseTargetId }, candidates);
- if (_config.ClansPluginShare && IsClansPluginAvailable())
- {
- var members = GetClansPluginMemberIds(baseTargetId);
- if (CountOnlineMembersAtLeast(members, _config.ClansPluginMinMembers)) groupOnline = true;
- AddProtectionLastOnlineCandidates(members, candidates);
- }
- if (_config.RustTeamShare)
- {
- var members = GetRustTeamMembers(baseTargetId);
- if (CountOnlineMembersAtLeast(members, _config.RustTeamMinMembers)) groupOnline = true;
- AddProtectionLastOnlineCandidates(members, candidates);
- }
- if (_config.RustClanShare)
- {
- var members = GetRustClanMembers(baseTargetId);
- if (CountOnlineMembersAtLeast(members, _config.RustClanMinMembers)) groupOnline = true;
- AddProtectionLastOnlineCandidates(members, candidates);
- }
- if (candidates.Count > 0)
- {
- protectionLastOnline = candidates[0];
- for (var i = 1; i < candidates.Count; i++)
- {
- if (candidates[i].LastOnlineTime > protectionLastOnline.LastOnlineTime) protectionLastOnline = candidates[i];
- }
- targetId = protectionLastOnline.UserId;
- return targetId.IsSteamId();
- }
- foreach (var userId in GetSharedProtectionMembers(baseTargetId))
- {
- if (!HasPerm(userId.ToString(), PluginPermissions.Protect) || !IsProtectionQualified(userId)) continue;
- if (!_storedData.LastOnlines.TryGetValue(userId, out var qualifiedLastOnline) || qualifiedLastOnline == null) continue;
- targetId = userId;
- protectionLastOnline = qualifiedLastOnline;
- return true;
- }
- if (!HasPerm(baseTargetId.ToString(), PluginPermissions.Protect)) return false;
- if (!_storedData.LastOnlines.TryGetValue(baseTargetId, out protectionLastOnline) || protectionLastOnline == null) return false;
- targetId = baseTargetId;
- return true;
- }
- finally
- {
- Pool.FreeUnmanaged(ref candidates);
- }
- }
- private bool TryGetCurrentProtectionScale(ulong targetId, LastOnline protectionLastOnline, bool groupOnline, bool groupInRange, int groupSize, ulong protectionScopeId, out float damageScale)
- {
- damageScale = -1f;
- if (!targetId.IsSteamId() || protectionLastOnline == null || !HasPerm(targetId.ToString(), PluginPermissions.Protect) || !IsProtectionQualified(targetId)) return false;
- if (IsProtectionDelayedForStatus(protectionScopeId, protectionLastOnline)) return false;
- if (!groupOnline)
- {
- damageScale = ApplyGroupSizeProtectionScaling(GetProtectionDamageScale(protectionScopeId, protectionLastOnline), groupSize);
- return damageScale >= 0f && damageScale < 1f;
- }
- if (_config.ProtectBaseWhenAway && !groupInRange)
- {
- damageScale = ApplyGroupSizeProtectionScaling(0f, groupSize);
- return damageScale < 1f;
- }
- return false;
- }
- private int DamageScaleToProtectionPercent(float damageScale)
- {
- if (damageScale < 0f) return 0;
- return Mathf.Clamp(Mathf.RoundToInt((1f - Mathf.Clamp01(damageScale)) * 100f), 0, 100);
- }
- private void UpdateSimpleStatusFor(BasePlayer player)
- {
- if (!IsSimpleStatusIntegrationReady() || !_simpleStatusCreated || player == null || !player.IsConnected || !player.userID.IsSteamId()) return;
- var authorizedIds = Pool.Get<List<ulong>>();
- try
- {
- authorizedIds.Clear();
- var hasAccess = false;
- var actualProtectionActive = false;
- var damageScale = -1f;
- var targetId = 0UL;
- var groupSize = 1;
- var privilegeBased = false;
- var navalTarget = FindNavalTarget(player);
- if (navalTarget == null && player.GetMounted() != null) navalTarget = FindNavalTarget(player.GetMounted());
- if (navalTarget != null)
- {
- if (navalTarget is PlayerBoat && !_config.ProtectPlayerMadeBoats)
- {
- HideSimpleStatusIfVisible(player);
- return;
- }
- if (navalTarget is Tugboat && !_config.ProtectTugboats)
- {
- HideSimpleStatusIfVisible(player);
- return;
- }
- var privilege = GetNavalVehiclePrivilege(navalTarget);
- if (privilege != null && AppendVehiclePrivilegeAuthorizedIds(privilege, authorizedIds) && authorizedIds.Count > 0)
- {
- privilegeBased = true;
- if (!TryResolvePrivilegeProtectionSource(authorizedIds, out targetId, out var protectionLastOnline, out var groupOnline))
- {
- HideSimpleStatusIfVisible(player);
- return;
- }
- groupSize = GetProtectionGroupSize(targetId, authorizedIds);
- hasAccess = HasPrivilegeProtectionAccess(player.userID, authorizedIds);
- var groupInRange = !_config.ProtectBaseWhenAway || IsPrivilegeGroupNearEntity(authorizedIds, navalTarget);
- actualProtectionActive = TryGetCurrentProtectionScale(targetId, protectionLastOnline, groupOnline, groupInRange, groupSize, GetProtectionScopeId(privilege, targetId), out damageScale);
- }
- else
- {
- var lookedEntity = GetSimpleStatusLookEntity(player);
- var lookedCombatEntity = lookedEntity as BaseCombatEntity;
- if (lookedCombatEntity == null || !IsBlocked(lookedCombatEntity) || !IsEntityInNavalTarget(lookedEntity, navalTarget))
- {
- HideSimpleStatusIfVisible(player);
- return;
- }
- if (!TryResolveNavalFallbackProtectionSource(lookedEntity, navalTarget, privilege, out targetId, out var protectionLastOnline, out var groupOnline))
- {
- HideSimpleStatusIfVisible(player);
- return;
- }
- groupSize = GetProtectionGroupSize(targetId);
- hasAccess = HasFallbackProtectionAccess(player.userID, targetId);
- var groupInRange = !_config.ProtectBaseWhenAway || IsPlayerOrGroupNearEntity(targetId, navalTarget);
- actualProtectionActive = TryGetCurrentProtectionScale(targetId, protectionLastOnline, groupOnline, groupInRange, groupSize, GetProtectionScopeId(lookedEntity, targetId), out damageScale);
- }
- }
- else
- {
- var lookedEntity = GetSimpleStatusLookEntity(player);
- var lookedCombatEntity = lookedEntity as BaseCombatEntity;
- var lookedEntityValid = lookedCombatEntity != null && IsBlocked(lookedCombatEntity);
- // A valid target without a privilege uses its own ownership fallback, not a nearby player's TC
- var buildingPrivilege = lookedEntityValid ? lookedEntity.GetBuildingPrivilege() : player.GetBuildingPrivilege();
- if (buildingPrivilege != null && buildingPrivilege.IsDestroyed) buildingPrivilege = null;
- if (buildingPrivilege != null) AppendBuildingPrivilegeAuthorizedIds(buildingPrivilege, authorizedIds);
- if (buildingPrivilege != null && authorizedIds.Count > 0)
- {
- privilegeBased = true;
- if (!TryResolvePrivilegeProtectionSource(authorizedIds, out targetId, out var protectionLastOnline, out var groupOnline))
- {
- HideSimpleStatusIfVisible(player);
- return;
- }
- groupSize = GetProtectionGroupSize(targetId, authorizedIds);
- hasAccess = HasPrivilegeProtectionAccess(player.userID, authorizedIds);
- var groupInRange = !_config.ProtectBaseWhenAway || IsPrivilegeGroupInBuildingRange(authorizedIds, buildingPrivilege);
- actualProtectionActive = TryGetCurrentProtectionScale(targetId, protectionLastOnline, groupOnline, groupInRange, groupSize, GetProtectionScopeId(buildingPrivilege, targetId), out damageScale);
- }
- else
- {
- if (!lookedEntityValid || !IsEntityInBuildingPrivilege(lookedEntity, buildingPrivilege))
- {
- HideSimpleStatusIfVisible(player);
- return;
- }
- if (!TryResolveEntityFallbackProtectionSource(lookedEntity, out targetId, out var protectionLastOnline, out var groupOnline))
- {
- HideSimpleStatusIfVisible(player);
- return;
- }
- groupSize = GetProtectionGroupSize(targetId);
- hasAccess = HasFallbackProtectionAccess(player.userID, targetId);
- var groupInRange = !_config.ProtectBaseWhenAway || IsProtectionGroupInRange(targetId, buildingPrivilege);
- actualProtectionActive = TryGetCurrentProtectionScale(targetId, protectionLastOnline, groupOnline, groupInRange, groupSize, GetProtectionScopeId(lookedEntity, targetId), out damageScale);
- }
- }
- if (!hasAccess && _config.SimpleStatusShowForUnauthorizedOnlyWhenHoldingHammer && !IsHoldingHammer(player))
- {
- HideSimpleStatusIfVisible(player);
- return;
- }
- if (hasAccess && !actualProtectionActive)
- {
- // Authorized viewers can preview the first normal protection tier while real protection is inactive.
- damageScale = _config.SimpleStatusShowAuthorizedProtectionPreview ? GetOfflineProtectionPreviewScale(groupSize) : 1f;
- }
- else if (!hasAccess && !actualProtectionActive)
- {
- // Unauthorized viewers keep the red status visible even when protection is currently inactive.
- damageScale = 1f;
- }
- var protectionPercent = DamageScaleToProtectionPercent(damageScale);
- var label = GetMsg(PluginMessages.OfflineProtectionStatus, player.UserIDString);
- var background = hasAccess ? _config.SimpleStatusAuthorizedBackground : _config.SimpleStatusUnauthorizedBackground;
- var iconColor = hasAccess ? _config.SimpleStatusAuthorizedIconColor : _config.SimpleStatusUnauthorizedIconColor;
- var labelColor = hasAccess ? _config.SimpleStatusAuthorizedLabelColor : _config.SimpleStatusUnauthorizedLabelColor;
- var valueColor = hasAccess ? _config.SimpleStatusAuthorizedValueColor : _config.SimpleStatusUnauthorizedValueColor;
- var stateKey = new SimpleStatusStateKey(targetId, privilegeBased, hasAccess, protectionPercent, label, background, iconColor, labelColor, valueColor, _resolvedSimpleStatusIcon);
- if (_simpleStatusStateKeys.TryGetValue(player.userID, out var previousState) && previousState.Equals(stateKey)) return;
- _simpleStatusStateKeys[player.userID] = stateKey;
- SimpleStatus.Call("SetStatus", player.UserIDString, _config.SimpleStatusStatusId, int.MaxValue);
- _simpleStatusVisiblePlayers.Add(player.userID);
- SimpleStatus.Call("SetStatusProperty", player.UserIDString, _config.SimpleStatusStatusId, new Dictionary<string, object>
- {
- ["color"] = NormalizeSimpleStatusColor(background),
- ["title"] = label,
- ["titleColor"] = NormalizeSimpleStatusColor(labelColor),
- ["text"] = protectionPercent + "%",
- ["textColor"] = NormalizeSimpleStatusColor(valueColor),
- ["icon"] = _resolvedSimpleStatusIcon,
- ["iconColor"] = NormalizeSimpleStatusColor(iconColor)
- });
- }
- catch (Exception ex)
- {
- HideSimpleStatusIfVisible(player);
- PrintWarning($"Unable to update SimpleStatus for {player.UserIDString}: {ex.Message}");
- }
- finally
- {
- Pool.FreeUnmanaged(ref authorizedIds);
- }
- }
- void HideMessage(BasePlayer player)
- {
- if (player == null || player.net?.connection == null) return;
- try
- {
- CuiHelper.DestroyUi(player, "AntiOfflineRaidMsg");
- }
- catch (Exception ex)
- {
- PrintWarning($"Unable to hide protection message for {player.UserIDString}: {ex.Message}");
- }
- }
- private BaseEntity FindNavalTarget(BaseEntity entity)
- {
- var current = entity;
- for (var depth = 0; current != null && depth < 16; depth++)
- {
- if (current is PlayerBoat || current is Tugboat) return current;
- current = current.GetParentEntity();
- }
- return null;
- }
- private static bool IsDescendantOfEntity(BaseEntity entity, BaseEntity root)
- {
- if (entity == null || root == null) return false;
- var current = entity;
- for (var depth = 0; current != null && depth < 16; depth++)
- {
- if (current == root) return true;
- current = current.GetParentEntity();
- }
- return false;
- }
- private VehiclePrivilege GetNavalVehiclePrivilege(BaseEntity navalTarget)
- {
- if (navalTarget == null) return null;
- var privilege = navalTarget.GetComponentInChildren<VehiclePrivilege>(true);
- if (privilege != null) return privilege;
- var privileges = Pool.Get<List<VehiclePrivilege>>();
- try
- {
- Vis.Entities(navalTarget.transform.position, 20f, privileges, Rust.Layers.Mask.Deployed);
- foreach (var candidate in privileges)
- {
- if (candidate == null || candidate.IsDestroyed || !IsDescendantOfEntity(candidate, navalTarget)) continue;
- return candidate;
- }
- }
- finally
- {
- Pool.FreeUnmanaged(ref privileges);
- }
- return null;
- }
- private bool AppendVehiclePrivilegeAuthorizedIds(VehiclePrivilege privilege, List<ulong> result)
- {
- if (privilege == null || result == null) return false;
- try
- {
- var privilegeType = privilege.GetType();
- if (!_vehiclePrivilegeAuthorizedPlayersFieldByType.TryGetValue(privilegeType, out var field))
- {
- var type = privilegeType;
- while (type != null && field == null)
- {
- field = type.GetField("authorizedPlayers", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.DeclaredOnly);
- type = type.BaseType;
- }
- _vehiclePrivilegeAuthorizedPlayersFieldByType[privilegeType] = field;
- }
- if (field == null) return false;
- var values = field.GetValue(privilege) as IEnumerable;
- if (values == null) return false;
- foreach (var value in values)
- {
- var userId = GetAuthorizationEntryUserId(value);
- if (userId.IsSteamId() && !result.Contains(userId)) result.Add(userId);
- }
- return true;
- }
- catch (Exception ex)
- {
- PrintWarning($"Unable to read naval privilege authorization: {ex.Message}");
- return false;
- }
- }
- private ulong GetAuthorizationEntryUserId(object value)
- {
- if (value == null) return 0UL;
- if (value is ulong userId) return userId;
- try
- {
- var type = value.GetType();
- const System.Reflection.BindingFlags flags = System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic;
- if (!_authorizationEntryUserIdFieldByType.TryGetValue(type, out var field))
- {
- field = type.GetField("userid", flags) ?? type.GetField("userID", flags) ?? type.GetField("UserID", flags);
- _authorizationEntryUserIdFieldByType[type] = field;
- }
- if (field != null)
- {
- var raw = field.GetValue(value);
- if (raw is ulong fieldUserId) return fieldUserId;
- if (raw != null && ulong.TryParse(raw.ToString(), out fieldUserId)) return fieldUserId;
- }
- if (!_authorizationEntryUserIdPropertyByType.TryGetValue(type, out var property))
- {
- property = type.GetProperty("userid", flags) ?? type.GetProperty("userID", flags) ?? type.GetProperty("UserID", flags);
- _authorizationEntryUserIdPropertyByType[type] = property;
- }
- if (property != null)
- {
- var raw = property.GetValue(value, null);
- if (raw is ulong propertyUserId) return propertyUserId;
- if (raw != null && ulong.TryParse(raw.ToString(), out propertyUserId)) return propertyUserId;
- }
- }
- catch
- {
- }
- return 0UL;
- }
- private BoatBuildingStation FindRelevantBoatBuildingStation(BoatBuildingBlock block)
- {
- if (block == null || block.IsDestroyed) return null;
- var current = block.GetParentEntity();
- for (var depth = 0; current != null && depth < 16; depth++)
- {
- if (current is BoatBuildingStation parentStation) return parentStation;
- current = current.GetParentEntity();
- }
- var stations = Pool.Get<List<BoatBuildingStation>>();
- try
- {
- Vis.Entities(block.transform.position, 30f, stations, Rust.Layers.Mask.Deployed);
- BoatBuildingStation nearest = null;
- var nearestDistanceSqr = float.MaxValue;
- foreach (var station in stations)
- {
- if (station == null || station.IsDestroyed || !station.OwnerID.IsSteamId()) continue;
- if (block.OwnerID.IsSteamId() && station.OwnerID != block.OwnerID) continue;
- var distanceSqr = (station.transform.position - block.transform.position).sqrMagnitude;
- if (distanceSqr >= nearestDistanceSqr) continue;
- nearestDistanceSqr = distanceSqr;
- nearest = station;
- }
- return nearest;
- }
- finally
- {
- Pool.FreeUnmanaged(ref stations);
- }
- }
- private bool IsPlayerNearEntity(ulong playerId, BaseEntity entity)
- {
- if (entity == null) return false;
- var player = BasePlayer.FindByID(playerId);
- if (player == null || !player.IsConnected) return false;
- if (IsDescendantOfEntity(player, entity)) return true;
- return (player.transform.position - entity.transform.position).sqrMagnitude <= NavalPresenceRadius * NavalPresenceRadius;
- }
- private bool IsPlayerOrGroupNearEntity(ulong targetId, BaseEntity entity)
- {
- foreach (var userId in GetSharedProtectionMembers(targetId))
- {
- if (IsPlayerNearEntity(userId, entity)) return true;
- }
- return false;
- }
- private bool IsConfiguredPrefab(BaseCombatEntity entity)
- {
- var prefabName = entity?.ShortPrefabName;
- if (string.IsNullOrEmpty(prefabName) || _config?.Prefabs == null) return false;
- if (_configuredPrefabMatches.TryGetValue(prefabName, out var matched)) return matched;
- foreach (var configuredPrefab in _config.Prefabs)
- {
- if (!prefabName.Contains(configuredPrefab)) continue;
- matched = true;
- break;
- }
- if (_configuredPrefabMatches.Count < ConfiguredPrefabCacheLimit) _configuredPrefabMatches[prefabName] = matched;
- return matched;
- }
- public bool IsBlocked(BaseCombatEntity entity)
- {
- if (entity == null) return false;
- var structuralType = entity is BuildingBlock || entity is BoatBuildingBlock || entity is PlayerBoat || entity is Tugboat;
- var configuredPrefab = !structuralType && IsConfiguredPrefab(entity);
- var navalTarget = FindNavalTarget(entity);
- if (structuralType && navalTarget != null && entity != navalTarget && !(navalTarget is PlayerBoat && entity is BoatBuildingBlock))
- configuredPrefab = IsConfiguredPrefab(entity);
- return IsBlocked(entity, navalTarget, configuredPrefab);
- }
- private bool IsBlocked(BaseCombatEntity entity, BaseEntity navalTarget, bool configuredPrefab)
- {
- if (entity == null) return false;
- if (navalTarget is PlayerBoat) return _config.ProtectPlayerMadeBoats && (entity == navalTarget || entity is BoatBuildingBlock || configuredPrefab);
- if (navalTarget is Tugboat) return _config.ProtectTugboats && (entity == navalTarget || configuredPrefab);
- if (entity is BoatBuildingBlock) return _config.ProtectPlayerMadeBoats;
- if (entity is BuildingBlock) return true;
- return configuredPrefab;
- }
- private bool IsProtectionGroupInRange(ulong targetId, BuildingPrivlidge privilege = null)
- {
- foreach (var userId in GetSharedProtectionMembers(targetId))
- {
- if (privilege != null)
- {
- if (IsPlayerNearBuildingPrivilege(userId, privilege)) return true;
- }
- else if (IsPlayerInRange(userId))
- {
- return true;
- }
- }
- return false;
- }
- private static bool IsPlayerNearBuildingPrivilege(ulong targetId, BuildingPrivlidge privilege)
- {
- if (privilege == null || privilege.IsDestroyed) return false;
- var player = BasePlayer.FindByID(targetId);
- if (player == null || !player.IsConnected) return false;
- var currentPrivilege = player.GetBuildingPrivilege();
- if (currentPrivilege == null || currentPrivilege.IsDestroyed) return false;
- if (ReferenceEquals(currentPrivilege, privilege)) return true;
- if (currentPrivilege.net == null || privilege.net == null) return false;
- return currentPrivilege.net.ID.Value == privilege.net.ID.Value;
- }
- public bool IsOffline(ulong playerId)
- {
- EnsureDataInitialized();
- if (_storedData.LastOnlines.TryGetValue(playerId, out var lastOnlinePlayer) && lastOnlinePlayer != null)
- return lastOnlinePlayer.IsOffline();
- var player = covalence.Players.FindPlayerById(playerId.ToString());
- return player == null || !player.IsConnected;
- }
- private static bool IsPlayerInRange(ulong targetId)
- {
- var player = BasePlayer.FindByID(targetId);
- return player != null && player.IsBuildingAuthed();
- }
- private void PlayProtectionSound(HitInfo hitInfo, bool isFire)
- {
- if (!_config.PlaySound || isFire || !(hitInfo?.Initiator is BasePlayer player)) return;
- try
- {
- Effect.server.Run(_config.SoundFile, player.transform.position);
- }
- catch (Exception ex)
- {
- PrintWarning($"Unable to play protection sound: {ex.Message}");
- }
- }
- public object MitigateDamage(HitInfo hitInfo, float scale)
- {
- if (hitInfo?.damageTypes == null || scale < 0f) return null;
- var isFire = hitInfo.damageTypes.GetMajorityDamageType() == DamageType.Heat;
- if (scale <= 0f)
- {
- if (_config.ShowMessage && ((isFire && hitInfo.WeaponPrefab != null) || !isFire)) SendMessage(hitInfo);
- PlayProtectionSound(hitInfo, isFire);
- return true;
- }
- hitInfo.damageTypes.ScaleAll(scale);
- if (scale >= 1f) return null;
- if (_config.ShowMessage && ((isFire && hitInfo.WeaponPrefab != null) || !isFire)) SendMessage(hitInfo, DamageScaleToProtectionPercent(scale));
- PlayProtectionSound(hitInfo, isFire);
- return null;
- }
- object OnStructureAttack(BaseEntity entity, HitInfo hitInfo)
- {
- return OnStructureAttack(entity, hitInfo, FindNavalTarget(entity));
- }
- private object OnStructureAttack(BaseEntity entity, HitInfo hitInfo, BaseEntity navalTarget)
- {
- if (entity == null || hitInfo == null) return null;
- if (navalTarget is PlayerBoat && _config.ProtectPlayerMadeBoats) return OnNavalStructureAttack(navalTarget, entity, hitInfo);
- if (navalTarget is Tugboat && _config.ProtectTugboats) return OnNavalStructureAttack(navalTarget, entity, hitInfo);
- if (entity is BoatBuildingBlock boatBlock)
- {
- if (!_config.ProtectPlayerMadeBoats) return null;
- var station = FindRelevantBoatBuildingStation(boatBlock);
- if (station == null) return null;
- ulong targetId;
- LastOnline protectionLastOnline;
- bool groupOnline;
- if (!TryResolveEntityFallbackProtectionSource(boatBlock, out targetId, out protectionLastOnline, out groupOnline))
- {
- if (!station.OwnerID.IsSteamId() || !TryResolveFallbackProtectionSource(station.OwnerID, out targetId, out protectionLastOnline, out groupOnline)) return null;
- }
- if (groupOnline && !_config.ProtectBaseWhenAway) return null;
- var groupSize = GetProtectionGroupSize(targetId);
- var groupInRange = !_config.ProtectBaseWhenAway || IsPlayerOrGroupNearEntity(targetId, station);
- var protectionScopeId = GetProtectionScopeId(boatBlock, targetId);
- return ApplyProtection(hitInfo, targetId, protectionLastOnline, groupOnline, groupInRange, groupSize, protectionScopeId);
- }
- var buildBlock = entity as BuildingBlock;
- if (buildBlock != null && buildBlock.grade == BuildingGrade.Enum.Twigs) return null;
- var buildingPrivilege = entity.GetBuildingPrivilege();
- var authorizedIds = Pool.Get<List<ulong>>();
- try
- {
- authorizedIds.Clear();
- if (buildingPrivilege != null) AppendBuildingPrivilegeAuthorizedIds(buildingPrivilege, authorizedIds);
- if (buildingPrivilege != null && authorizedIds.Count > 0)
- {
- if (!TryResolvePrivilegeProtectionSource(authorizedIds, out var targetId, out var protectionLastOnline, out var groupOnline)) return null;
- if (groupOnline && !_config.ProtectBaseWhenAway) return null;
- var groupSize = GetProtectionGroupSize(targetId, authorizedIds);
- var groupInRange = !_config.ProtectBaseWhenAway || IsPrivilegeGroupInBuildingRange(authorizedIds, buildingPrivilege);
- return ApplyProtection(hitInfo, targetId, protectionLastOnline, groupOnline, groupInRange, groupSize, GetProtectionScopeId(buildingPrivilege, targetId));
- }
- if (!TryResolveEntityFallbackProtectionSource(entity, out var fallbackResolvedId, out var fallbackLastOnline, out var fallbackGroupOnline)) return null;
- if (fallbackGroupOnline && !_config.ProtectBaseWhenAway) return null;
- var fallbackGroupSize = GetProtectionGroupSize(fallbackResolvedId);
- var fallbackGroupInRange = !_config.ProtectBaseWhenAway || IsProtectionGroupInRange(fallbackResolvedId, buildingPrivilege);
- return ApplyProtection(hitInfo, fallbackResolvedId, fallbackLastOnline, fallbackGroupOnline, fallbackGroupInRange, fallbackGroupSize, GetProtectionScopeId(entity, fallbackResolvedId));
- }
- finally
- {
- Pool.FreeUnmanaged(ref authorizedIds);
- }
- }
- private object OnNavalStructureAttack(BaseEntity navalTarget, BaseEntity attackedEntity, HitInfo hitInfo)
- {
- if (navalTarget == null || attackedEntity == null || hitInfo == null) return null;
- var privilege = GetNavalVehiclePrivilege(navalTarget);
- var authorizedIds = Pool.Get<List<ulong>>();
- try
- {
- authorizedIds.Clear();
- if (privilege != null && AppendVehiclePrivilegeAuthorizedIds(privilege, authorizedIds) && authorizedIds.Count > 0)
- {
- if (!TryResolvePrivilegeProtectionSource(authorizedIds, out var targetId, out var protectionLastOnline, out var groupOnline)) return null;
- if (groupOnline && !_config.ProtectBaseWhenAway) return null;
- var groupSize = GetProtectionGroupSize(targetId, authorizedIds);
- var groupInRange = !_config.ProtectBaseWhenAway || IsPrivilegeGroupNearEntity(authorizedIds, navalTarget);
- return ApplyProtection(hitInfo, targetId, protectionLastOnline, groupOnline, groupInRange, groupSize, GetProtectionScopeId(privilege, targetId));
- }
- if (!TryResolveNavalFallbackProtectionSource(attackedEntity, navalTarget, privilege, out var fallbackTargetId, out var fallbackLastOnline, out var fallbackGroupOnline)) return null;
- if (fallbackGroupOnline && !_config.ProtectBaseWhenAway) return null;
- var fallbackGroupSize = GetProtectionGroupSize(fallbackTargetId);
- var fallbackGroupInRange = !_config.ProtectBaseWhenAway || IsPlayerOrGroupNearEntity(fallbackTargetId, navalTarget);
- return ApplyProtection(hitInfo, fallbackTargetId, fallbackLastOnline, fallbackGroupOnline, fallbackGroupInRange, fallbackGroupSize, GetProtectionScopeId(attackedEntity, fallbackTargetId));
- }
- finally
- {
- Pool.FreeUnmanaged(ref authorizedIds);
- }
- }
- private DateTime GetEffectiveProtectionStart(ulong protectionScopeId, LastOnline protectionLastOnline)
- {
- var start = protectionLastOnline != null ? GetOfflineSince(protectionLastOnline) : DateTime.Now;
- if (!_storedData.LastStructureAttackAt.TryGetValue(protectionScopeId, out var lastAttackLong) || lastAttackLong == 0) return start;
- try
- {
- var lastAttack = DateTime.FromBinary(lastAttackLong);
- return lastAttack > start ? lastAttack : start;
- }
- catch
- {
- _storedData.LastStructureAttackAt.Remove(protectionScopeId);
- return start;
- }
- }
- private bool ShouldDelayProtectionForRecentAttack(ulong protectionScopeId, LastOnline protectionLastOnline)
- {
- if (protectionLastOnline == null || !protectionLastOnline.IsOffline()) return false;
- var cooldownElapsed = HasProtectionCooldownElapsed(protectionLastOnline);
- var delayMinutes = _config.MinutesSinceLastAttackToProtect;
- if (delayMinutes <= 0) return !cooldownElapsed;
- var now = DateTime.Now;
- var effectiveStart = GetEffectiveProtectionStart(protectionScopeId, protectionLastOnline);
- if (cooldownElapsed && (now - effectiveStart).TotalMinutes > delayMinutes) return false;
- // Hits during the initial cooldown must also extend the pending raid delay for this scope
- _storedData.LastStructureAttackAt[protectionScopeId] = now.ToBinary();
- InvalidateDamageScale(protectionLastOnline.UserId);
- return true;
- }
- private float GetProtectionDamageScale(ulong protectionScopeId, LastOnline protectionLastOnline)
- {
- if (protectionLastOnline == null || !protectionLastOnline.IsOffline() || !IsProtectionQualified(protectionLastOnline.UserId)) return -1f;
- var effectiveStart = GetEffectiveProtectionStart(protectionScopeId, protectionLastOnline);
- if (effectiveStart <= protectionLastOnline.LastOnlineTime) return ScaleDamageCached(protectionLastOnline);
- return CalculateProtectionDamageScale(effectiveStart);
- }
- private object ApplyProtection(HitInfo hitInfo, ulong targetId, LastOnline protectionLastOnline, bool groupOnline, bool groupInRange, int groupSize, ulong protectionScopeId)
- {
- if (hitInfo == null || !targetId.IsSteamId() || !HasPerm(targetId.ToString(), PluginPermissions.Protect) || !IsProtectionQualified(targetId)) return null;
- if (!groupOnline)
- {
- if (protectionLastOnline == null || ShouldDelayProtectionForRecentAttack(protectionScopeId, protectionLastOnline)) return null;
- var scale = ApplyGroupSizeProtectionScaling(GetProtectionDamageScale(protectionScopeId, protectionLastOnline), groupSize);
- return MitigateDamage(hitInfo, scale);
- }
- if (_config.ProtectBaseWhenAway && !groupInRange)
- {
- var awayScale = ApplyGroupSizeProtectionScaling(0f, groupSize);
- return MitigateDamage(hitInfo, awayScale);
- }
- return null;
- }
- public float ScaleDamage(LastOnline lastOnline)
- {
- if (lastOnline == null || !lastOnline.IsOffline() || !IsProtectionQualified(lastOnline.UserId)) return -1f;
- return CalculateProtectionDamageScale(lastOnline.LastOnlineTime);
- }
- private float CalculateProtectionDamageScale(DateTime protectionStart)
- {
- // Eligibility is checked on the real player state, not on a shifted offline or raid-delay timestamp
- var now = DateTime.Now;
- // Absolute time scale intentionally overrides relative offline-time scaling
- if (_config.AbsoluteTimeScale.Count > 0)
- {
- var hour = now.ToString("HH", DateTimeFormatInfo.InvariantInfo);
- if (_config.AbsoluteTimeScale.TryGetValue(hour, out var scaleObj))
- {
- try
- {
- return Convert.ToSingle(scaleObj, CultureInfo.InvariantCulture);
- }
- catch
- {
- PrintWarning($"Invalid AbsoluteTimeScale value for hour '{hour}'.");
- }
- }
- }
- var offlineTime = now - protectionStart;
- if (offlineTime.TotalMinutes < 60) return _config.InterimDamage;
- var scaleKey = -1;
- for (var i = 0; i < _config.DamageScaleKeys.Count; i++)
- {
- var key = _config.DamageScaleKeys[i];
- if (key > offlineTime.TotalHours) break;
- scaleKey = key;
- }
- if (scaleKey < 0 || !_config.DamageScale.TryGetValue(scaleKey.ToString(), out var scaleValue)) return -1f;
- try
- {
- return Convert.ToSingle(scaleValue, CultureInfo.InvariantCulture);
- }
- catch
- {
- PrintWarning($"Invalid DamageScale value for threshold '{scaleKey}'.");
- return -1f;
- }
- }
- float ScaleDamageCached(LastOnline lastOnline)
- {
- if (lastOnline == null) return -1f;
- if (_cachedScales.TryGetValue(lastOnline.UserId, out var kvp) && kvp != null && DateTime.Now < kvp.Expires) return kvp.Scale;
- return CacheDamageScale(lastOnline.UserId, ScaleDamage(lastOnline), kvp);
- }
- private void AppendShareStatus(StringBuilder stringBuilder, string userId, string providerName, HashSet<ulong> members, int minOnlineMembers)
- {
- if (stringBuilder == null || members == null || members.Count == 0) return;
- var onlineCount = members.Count(memberId => !IsOffline(memberId));
- var groupOnline = onlineCount >= Math.Max(1, minOnlineMembers);
- var groupStatus = GetMsg(groupOnline ? PluginMessages.StatusOnline : PluginMessages.StatusOffline, userId);
- stringBuilder.AppendLine(string.Format(GetMsg(PluginMessages.StatusShare, userId), providerName, groupStatus, onlineCount));
- if (groupOnline || !TryEvaluateSharedProtectionProvider(members, minOnlineMembers, out var selected, out _) || selected == null) return;
- var selectedPlayer = covalence.Players.FindPlayerById(selected.UserId.ToString());
- var selectedName = selectedPlayer?.Name ?? selected.UserId.ToString();
- stringBuilder.AppendLine(string.Format(GetMsg(PluginMessages.StatusShareLastOffline, userId), providerName, selectedName, selected.LastOnlineTime.AddHours(_config.ServerTimeOffset)));
- }
- string SendStatus(string playerSearch, string userId = null)
- {
- EnsureDataInitialized();
- if (string.IsNullOrWhiteSpace(playerSearch)) return GetMsg(PluginMessages.InvalidSyntax, userId);
- var target = FindPlayerByPartialName(playerSearch);
- if (target == null || !ulong.TryParse(target.Id, out var targetId) || !_storedData.LastOnlines.TryGetValue(targetId, out var lo) || lo == null)
- return GetMsg(PluginMessages.PlayerNotFound, userId);
- var stringBuilder = new StringBuilder();
- stringBuilder.AppendLine(string.Format(GetMsg(PluginMessages.StatusHeader, userId), target.Name));
- if (IsOffline(targetId))
- {
- var statusTime = GetOfflineSince(lo).AddHours(_config.ServerTimeOffset);
- stringBuilder.AppendLine(target.IsConnected
- ? string.Format(GetMsg(PluginMessages.StatusPlayerAfk, userId), statusTime)
- : string.Format(GetMsg(PluginMessages.StatusPlayerOffline, userId), statusTime));
- }
- else
- {
- stringBuilder.AppendLine(GetMsg(PluginMessages.StatusPlayerOnline, userId));
- }
- stringBuilder.AppendLine(string.Format(GetMsg(PluginMessages.StatusAfkMinutes, userId), Math.Round(lo.AfkMinutes, 1)));
- if (_config.ClansPluginShare && IsClansPluginAvailable())
- AppendShareStatus(stringBuilder, userId, GetMsg(PluginMessages.ProviderClansPlugin, userId), GetClansPluginMemberIds(targetId), _config.ClansPluginMinMembers);
- if (_config.RustTeamShare)
- AppendShareStatus(stringBuilder, userId, GetMsg(PluginMessages.ProviderRustTeam, userId), GetRustTeamMembers(targetId), _config.RustTeamMinMembers);
- if (_config.RustClanShare)
- AppendShareStatus(stringBuilder, userId, GetMsg(PluginMessages.ProviderRustClan, userId), GetRustClanMembers(targetId), _config.RustClanMinMembers);
- if (TryGetProtectionLastOnline(targetId, out var protectionLastOnline))
- {
- var scale = GetCurrentBaseProtectionDamageScale(protectionLastOnline);
- if (scale > -1f) stringBuilder.AppendLine(string.Format(GetMsg(PluginMessages.StatusScale, userId), DamageScaleToProtectionPercent(scale)));
- }
- return stringBuilder.ToString();
- }
- void ShowMessage(BasePlayer player, int amount = 100)
- {
- if (player == null || !player.IsConnected || player.net?.connection == null) return;
- HideMessage(player);
- var sb = new StringBuilder(JsonMessage);
- sb.Replace("{1}", Core.Random.Range(1, 99999).ToString());
- sb.Replace("{protection_message}", GetMsg(PluginMessages.ProtectionMessage, player.UserIDString));
- sb.Replace("{amount}", amount.ToString());
- CuiHelper.AddUi(player, sb.ToString());
- timer.In(3f, () =>
- {
- if (player != null && player.IsConnected && player.net?.connection != null) HideMessage(player);
- });
- }
- void UpdateLastOnline(BasePlayer player)
- {
- if (player == null) return;
- UpdateLastOnline(player.userID, player);
- }
- void UpdateLastOnline(ulong playerId)
- {
- if (!playerId.IsSteamId()) return;
- UpdateLastOnline(playerId, BasePlayer.FindByID(playerId));
- }
- private void UpdateLastOnline(ulong playerId, BasePlayer activePlayer)
- {
- if (!playerId.IsSteamId()) return;
- EnsureDataInitialized();
- _storedData.LastOnlines.TryGetValue(playerId, out var lastOnlinePlayer);
- if (activePlayer != null) UpdateOnlineQualification(activePlayer, lastOnlinePlayer);
- if (lastOnlinePlayer == null)
- {
- lastOnlinePlayer = new LastOnline(playerId, DateTime.Now);
- _storedData.LastOnlines[playerId] = lastOnlinePlayer;
- }
- else
- {
- lastOnlinePlayer.LastOnlineTime = DateTime.Now;
- lastOnlinePlayer.AfkMinutes = 0;
- }
- // Keep the movement baseline aligned with activity already recorded by the enabled hooks
- if (activePlayer != null && !activePlayer.IsDestroyed) lastOnlinePlayer.LastPosition = activePlayer.transform.position;
- InvalidateDamageScale(playerId);
- }
- private void InitializeLastOnline(BasePlayer player)
- {
- if (player == null || !player.IsConnected || !player.userID.IsSteamId()) return;
- EnsureDataInitialized();
- var now = DateTime.Now;
- _storedData.LastOnlines.TryGetValue(player.userID, out var lastOnline);
- var restoreLastOnline = false;
- // Preserve hot-reload activity only when the stored timestamp belongs to the current connection
- if (lastOnline != null && lastOnline.UserId == player.userID && lastOnline.LastOnlineLong != 0 && player.net?.connection != null)
- {
- try
- {
- var lastActivity = lastOnline.LastOnlineTime;
- var secondsConnected = player.secondsConnected;
- // The connection age is reported in whole seconds
- restoreLastOnline = secondsConnected > 0 && lastActivity <= now && (now - lastActivity).TotalSeconds <= (double)secondsConnected + 1d;
- }
- catch (ArgumentException)
- {
- // Invalid saved timestamps are initialized as a fresh session
- }
- }
- if (!restoreLastOnline)
- {
- StartOnlineQualificationSession(player.userID, true);
- lastOnline = new LastOnline(player.userID, now);
- _storedData.LastOnlines[player.userID] = lastOnline;
- InvalidateDamageScale(player.userID);
- }
- lastOnline.LastPosition = player.transform.position;
- lastOnline.AfkMinutes = (float)Math.Max(0d, (now - lastOnline.LastOnlineTime).TotalMinutes);
- }
- void UpdateLastOnlineAll(bool afkCheck = true)
- {
- EnsureDataInitialized();
- foreach (var player in BasePlayer.activePlayerList)
- {
- if (player == null || !player.IsConnected) continue;
- if (!afkCheck)
- {
- InitializeLastOnline(player);
- continue;
- }
- if (!_storedData.LastOnlines.TryGetValue(player.userID, out var lastOnlinePlayer) || lastOnlinePlayer == null)
- {
- UpdateLastOnline(player);
- if (_storedData.LastOnlines.TryGetValue(player.userID, out lastOnlinePlayer) && lastOnlinePlayer != null)
- lastOnlinePlayer.LastPosition = player.transform.position;
- continue;
- }
- if (lastOnlinePlayer.HasMoved(player.transform.position))
- {
- UpdateLastOnline(player);
- continue;
- }
- UpdateOnlineQualification(player, lastOnlinePlayer);
- lastOnlinePlayer.AfkMinutes = (float)Math.Max(0d, lastOnlinePlayer.Minutes);
- }
- }
- #endregion
- }
- }
Advertisement
Add Comment
Please, Sign In to add comment