dFxPhoeniX

AntiOfflineRaid 1.6.0

Sep 23rd, 2026 (edited)
35
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 163.80 KB | None | 0 0
  1. using Newtonsoft.Json;
  2. using Newtonsoft.Json.Linq;
  3. using Oxide.Core;
  4. using Oxide.Core.Libraries.Covalence;
  5. using Oxide.Core.Plugins;
  6. using Oxide.Game.Rust.Cui;
  7. using Rust;
  8. using System;
  9. using System.Collections;
  10. using System.Collections.Generic;
  11. using System.Globalization;
  12. using System.Linq;
  13. using System.IO;
  14. using System.Text;
  15. using UnityEngine;
  16. using Facepunch;
  17.  
  18. //AntiOfflineRaid created with PluginMerge v(1.0.6.0) by MJSU @ https://github.com/dassjosh/Plugin.Merge
  19. namespace Oxide.Plugins
  20. {
  21. [Info("Anti Offline Raid", "Calytic/Shady14u", "1.6.1")]
  22. [Description("Prevents/reduces offline raiding")]
  23. public partial class AntiOfflineRaid : RustPlugin
  24. {
  25. #region 1.AntiOfflineRaid.Config.cs
  26. private static Configuration _config;
  27.  
  28. protected override void LoadConfig()
  29. {
  30. base.LoadConfig();
  31. try
  32. {
  33. var current = Config.ReadObject<JObject>() ?? new JObject();
  34. if (!current.HasValues)
  35. {
  36. _config = Configuration.DefaultConfig();
  37. }
  38. else
  39. {
  40. MigrateLegacyTopLevelSettings(current);
  41.  
  42. if (current["ClansPluginShare"] == null && current["ClanShare"] != null)
  43. current["ClansPluginShare"] = current["ClanShare"].DeepClone();
  44. current.Remove("ClanShare");
  45.  
  46. if (current["GroupSizeUseClansPluginMembers"] == null && current["GroupSizeUseClanMembers"] != null)
  47. current["GroupSizeUseClansPluginMembers"] = current["GroupSizeUseClanMembers"].DeepClone();
  48. current.Remove("GroupSizeUseClanMembers");
  49. current.Remove("GroupSizeUseAuthorizedPlayers");
  50.  
  51. MigrateLegacyShareSettings(current);
  52. MigrateLegacySimpleStatusSettings(current);
  53. MigrateLegacySimpleStatusColors(current);
  54.  
  55. _config = current.ToObject<Configuration>() ?? Configuration.DefaultConfig();
  56. }
  57.  
  58. NormalizeConfig();
  59. SaveConfig();
  60. }
  61. catch (Exception e)
  62. {
  63. Debug.LogException(e);
  64. PrintWarning("Creating new config file.");
  65. LoadDefaultConfig();
  66. NormalizeConfig();
  67. SaveConfig();
  68. }
  69. }
  70.  
  71. private static void MigrateLegacyTopLevelSettings(JObject current)
  72. {
  73. if (current == null) return;
  74.  
  75. MigrateLegacyTopLevelSetting(current, "Activities", "activity");
  76. MigrateLegacyTopLevelSetting(current, "SoundFile", "sound");
  77. MigrateLegacyTopLevelSetting(current, "ClanShare", "clanShare");
  78. MigrateLegacyTopLevelSetting(current, "MinMembers", "minMembers");
  79. current.Remove("ClanFirstOffline");
  80. current.Remove("clanFirstOffline");
  81. }
  82.  
  83. private static void MigrateLegacyTopLevelSetting(JObject current, string currentName, string legacyName)
  84. {
  85. if (current[currentName] == null && current[legacyName] != null) current[currentName] = current[legacyName].DeepClone();
  86. current.Remove(legacyName);
  87. }
  88.  
  89. private static void MigrateLegacyShareSettings(JObject current)
  90. {
  91. if (current == null) return;
  92.  
  93. if (current["MinMembers"] != null)
  94. {
  95. if (current["ClansPluginMinMembers"] == null) current["ClansPluginMinMembers"] = current["MinMembers"].DeepClone();
  96. if (current["RustTeamMinMembers"] == null) current["RustTeamMinMembers"] = current["MinMembers"].DeepClone();
  97. if (current["RustClanMinMembers"] == null) current["RustClanMinMembers"] = current["MinMembers"].DeepClone();
  98. current.Remove("MinMembers");
  99. }
  100.  
  101. current.Remove("ClansPluginFirstOffline");
  102. current.Remove("RustTeamFirstOffline");
  103. current.Remove("RustClanFirstOffline");
  104. }
  105.  
  106. private static void MigrateLegacySimpleStatusSettings(JObject current)
  107. {
  108. if (!(current?["SimpleStatus"] is JObject legacy)) return;
  109.  
  110. MigrateLegacySetting(current, legacy, "SimpleStatusEnabled", "Enabled");
  111. MigrateLegacySetting(current, legacy, "SimpleStatusShowForUnauthorizedOnlyWhenHoldingHammer", "Show outsiders only while holding hammer");
  112. MigrateLegacySetting(current, legacy, "SimpleStatusRefreshIntervalSeconds", "Refresh interval seconds");
  113. MigrateLegacySetting(current, legacy, "SimpleStatusStatusId", "Status id");
  114. MigrateLegacySetting(current, legacy, "SimpleStatusAuthorizedBackground", "Authorized background");
  115. MigrateLegacySetting(current, legacy, "SimpleStatusUnauthorizedBackground", "Unauthorized background");
  116. MigrateLegacySetting(current, legacy, "SimpleStatusAuthorizedIconColor", "Authorized icon color");
  117. MigrateLegacySetting(current, legacy, "SimpleStatusUnauthorizedIconColor", "Unauthorized icon color");
  118. MigrateLegacySetting(current, legacy, "SimpleStatusAuthorizedLabelColor", "Authorized label color");
  119. MigrateLegacySetting(current, legacy, "SimpleStatusUnauthorizedLabelColor", "Unauthorized label color");
  120. MigrateLegacySetting(current, legacy, "SimpleStatusAuthorizedValueColor", "Authorized value color");
  121. MigrateLegacySetting(current, legacy, "SimpleStatusUnauthorizedValueColor", "Unauthorized value color");
  122. MigrateLegacySetting(current, legacy, "SimpleStatusIcon", "Icon (/oxide/data/AntiOfflineRaid/images/<name>.png for local)");
  123. current.Remove("SimpleStatus");
  124. }
  125.  
  126. private static void MigrateLegacySimpleStatusColors(JObject current)
  127. {
  128. if (current == null) return;
  129.  
  130. MigrateKnownSimpleStatusColor(current, "SimpleStatusUnauthorizedBackground", "#773A2F", "#532829", "#C74A37");
  131. MigrateKnownSimpleStatusColor(current, "SimpleStatusUnauthorizedIconColor", "#FF5338", "#FF9482", "#782600");
  132. MigrateKnownSimpleStatusColor(current, "SimpleStatusUnauthorizedLabelColor", "#FF5338", "#FF9482", "#DEDEDE");
  133. MigrateKnownSimpleStatusColor(current, "SimpleStatusUnauthorizedValueColor", "#FF5338", "#FF9482", "#DEDEDE");
  134. }
  135.  
  136. private static void MigrateKnownSimpleStatusColor(JObject current, string propertyName, string replacement, params string[] legacyDefaults)
  137. {
  138. var value = current?[propertyName]?.ToString();
  139. if (string.IsNullOrWhiteSpace(value) || legacyDefaults == null) return;
  140. if (legacyDefaults.Any(x => string.Equals(value.Trim(), x, StringComparison.OrdinalIgnoreCase))) current[propertyName] = replacement;
  141. }
  142.  
  143. private static void MigrateLegacySetting(JObject current, JObject legacy, string currentName, string legacyName)
  144. {
  145. if (current[currentName] == null && legacy[legacyName] != null) current[currentName] = legacy[legacyName].DeepClone();
  146. }
  147.  
  148. protected override void LoadDefaultConfig() => _config = Configuration.DefaultConfig();
  149. protected override void SaveConfig() => Config.WriteObject(_config);
  150.  
  151. public class Configuration
  152. {
  153. [JsonProperty(PropertyName = "AbsoluteTimeScale")]
  154. public Dictionary<string, object> AbsoluteTimeScale = new Dictionary<string, object>();
  155.  
  156. [JsonProperty(PropertyName = "DamageScale")]
  157. public Dictionary<string, object> DamageScale = new Dictionary<string, object>();
  158.  
  159. public List<string> Activities { get; set; }
  160.  
  161. public int AfkMinutes { get; set; }
  162.  
  163. public bool TreatAfkAsOffline { get; set; } = true;
  164.  
  165. public bool ClansPluginShare { get; set; } = false;
  166.  
  167. public int ClansPluginMinMembers { get; set; } = 1;
  168.  
  169. public bool RustTeamShare { get; set; } = false;
  170.  
  171. public int RustTeamMinMembers { get; set; } = 1;
  172.  
  173. public bool RustClanShare { get; set; } = false;
  174.  
  175. public int RustClanMinMembers { get; set; } = 1;
  176.  
  177. public int CooldownMinutes { get; set; }
  178.  
  179. public int MinutesOnlineToQualify { get; set; } = 0;
  180.  
  181. public float InterimDamage { get; set; }
  182.  
  183. public int MinutesSinceLastAttackToProtect { get; set; }
  184.  
  185. public bool PlaySound { get; set; }
  186.  
  187. public List<string> Prefabs { get; set; }
  188.  
  189. public bool ProtectBaseWhenAway { get; set; }
  190.  
  191. public bool ProtectPlayerMadeBoats { get; set; } = true;
  192.  
  193. public bool ProtectTugboats { get; set; } = true;
  194.  
  195. public bool GroupSizeProtectionScaling { get; set; } = true;
  196.  
  197. public bool GroupSizeUseClansPluginMembers { get; set; } = false;
  198.  
  199. public bool GroupSizeUseRustTeams { get; set; } = false;
  200.  
  201. public bool GroupSizeUseRustClans { get; set; } = false;
  202.  
  203. public Dictionary<string, float> GroupSizeProtectionScale { get; set; } = new Dictionary<string, float>();
  204.  
  205. public int ServerTimeOffset { get; set; }
  206.  
  207. public bool ShowMessage { get; set; }
  208.  
  209. public bool SimpleStatusEnabled { get; set; } = false;
  210.  
  211. public bool SimpleStatusShowForUnauthorizedOnlyWhenHoldingHammer { get; set; } = false;
  212.  
  213. public bool SimpleStatusShowAuthorizedProtectionPreview { get; set; } = false;
  214.  
  215. public float SimpleStatusRefreshIntervalSeconds { get; set; } = 1f;
  216.  
  217. public string SimpleStatusStatusId { get; set; } = "antiofflineraid.protection";
  218.  
  219. public string SimpleStatusAuthorizedBackground { get; set; } = "#738D45";
  220.  
  221. public string SimpleStatusUnauthorizedBackground { get; set; } = "#773A2F";
  222.  
  223. public string SimpleStatusAuthorizedIconColor { get; set; } = "#A3FE03";
  224.  
  225. public string SimpleStatusUnauthorizedIconColor { get; set; } = "#FF5338";
  226.  
  227. public string SimpleStatusAuthorizedLabelColor { get; set; } = "#FFFFFF";
  228.  
  229. public string SimpleStatusUnauthorizedLabelColor { get; set; } = "#FF5338";
  230.  
  231. public string SimpleStatusAuthorizedValueColor { get; set; } = "#FFFFFF";
  232.  
  233. public string SimpleStatusUnauthorizedValueColor { get; set; } = "#FF5338";
  234.  
  235. public string SimpleStatusIcon { get; set; } = "https://i.ibb.co/nq6bL11S/protection.png";
  236.  
  237. public string SoundFile { get; set; }
  238. [JsonIgnore] public List<int> DamageScaleKeys { get; set; } = new List<int>();
  239. [JsonIgnore] public List<int> GroupSizeProtectionKeys { get; set; } = new List<int>();
  240.  
  241.  
  242. public static Configuration DefaultConfig()
  243. {
  244. return new Configuration
  245. {
  246. DamageScale = new Dictionary<string, object>
  247. {{"1", 0.2}, {"3", 0.35f}, {"6", 0.5f}, {"12", 0.8f}, {"48", 1}},
  248. AbsoluteTimeScale = new Dictionary<string, object> {{"03", 0.1}},
  249. AfkMinutes = 5,
  250. TreatAfkAsOffline = true,
  251. CooldownMinutes = 10,
  252. MinutesOnlineToQualify = 0,
  253. InterimDamage = 0,
  254. ClansPluginShare = false,
  255. ClansPluginMinMembers = 1,
  256. RustTeamShare = false,
  257. RustTeamMinMembers = 1,
  258. RustClanShare = false,
  259. RustClanMinMembers = 1,
  260. ShowMessage = true,
  261. SimpleStatusEnabled = false,
  262. SimpleStatusShowForUnauthorizedOnlyWhenHoldingHammer = false,
  263. SimpleStatusShowAuthorizedProtectionPreview = false,
  264. SimpleStatusRefreshIntervalSeconds = 1f,
  265. SimpleStatusStatusId = "antiofflineraid.protection",
  266. SimpleStatusAuthorizedBackground = "#738D45",
  267. SimpleStatusUnauthorizedBackground = "#773A2F",
  268. SimpleStatusAuthorizedIconColor = "#A3FE03",
  269. SimpleStatusUnauthorizedIconColor = "#FF5338",
  270. SimpleStatusAuthorizedLabelColor = "#FFFFFF",
  271. SimpleStatusUnauthorizedLabelColor = "#FF5338",
  272. SimpleStatusAuthorizedValueColor = "#FFFFFF",
  273. SimpleStatusUnauthorizedValueColor = "#FF5338",
  274. SimpleStatusIcon = "https://i.ibb.co/nq6bL11S/protection.png",
  275. PlaySound = false,
  276. Prefabs = new List<string>
  277. {
  278. "door.hinged", "door.double.hinged", "window.bars", "floor.ladder.hatch",
  279. "floor.frame", "wall.frame", "shutter", "wall.external", "gates.external", "box", "locker"
  280. },
  281. Activities = GetDefaultActivities(),
  282. ServerTimeOffset = 0,
  283. SoundFile = "assets/prefabs/weapon mods/silencers/effects/silencer_attach.fx.prefab",
  284. ProtectBaseWhenAway = false,
  285. ProtectPlayerMadeBoats = true,
  286. ProtectTugboats = true,
  287. GroupSizeProtectionScaling = true,
  288. GroupSizeUseClansPluginMembers = false,
  289. GroupSizeUseRustTeams = false,
  290. GroupSizeUseRustClans = false,
  291. GroupSizeProtectionScale = new Dictionary<string, float>
  292. {
  293. { "1", 1f },
  294. { "3", 0.90f },
  295. { "5", 0.80f },
  296. { "8", 0.70f },
  297. { "12", 0.60f }
  298. },
  299. MinutesSinceLastAttackToProtect = 10,
  300. DamageScaleKeys = new List<int> { 1, 3, 6, 12, 48 },
  301. GroupSizeProtectionKeys = new List<int> { 1, 3, 5, 8, 12 }
  302. };
  303. }
  304. }
  305.  
  306. private static List<string> GetDefaultActivities()
  307. {
  308. return new List<string> { "input", "loot", "respawn", "chat", "wakeup" };
  309. }
  310.  
  311. private static string NormalizeSimpleStatusIconSetting(string value)
  312. {
  313. if (string.IsNullOrWhiteSpace(value)) return value;
  314.  
  315. var trimmed = value.Trim();
  316. if (trimmed.StartsWith("[", StringComparison.Ordinal) && trimmed.EndsWith(")", StringComparison.Ordinal))
  317. {
  318. var separator = trimmed.IndexOf("](", StringComparison.Ordinal);
  319. if (separator > 0)
  320. {
  321. var linkedValue = trimmed.Substring(separator + 2, trimmed.Length - separator - 3).Trim();
  322. if (!string.IsNullOrWhiteSpace(linkedValue)) return linkedValue;
  323. }
  324. }
  325.  
  326. return trimmed;
  327. }
  328.  
  329. private void NormalizeConfig()
  330. {
  331. var defaults = Configuration.DefaultConfig();
  332. if (_config.AbsoluteTimeScale == null) _config.AbsoluteTimeScale = new Dictionary<string, object>();
  333. if (_config.DamageScale == null || _config.DamageScale.Count == 0) _config.DamageScale = new Dictionary<string, object>(defaults.DamageScale);
  334. if (_config.GroupSizeProtectionScale == null || _config.GroupSizeProtectionScale.Count == 0) _config.GroupSizeProtectionScale = new Dictionary<string, float>(defaults.GroupSizeProtectionScale);
  335. if (_config.Activities == null) _config.Activities = GetDefaultActivities();
  336. if (_config.Prefabs == null) _config.Prefabs = new List<string>(defaults.Prefabs);
  337. if (string.IsNullOrWhiteSpace(_config.SimpleStatusStatusId)) _config.SimpleStatusStatusId = defaults.SimpleStatusStatusId;
  338. if (string.IsNullOrWhiteSpace(_config.SimpleStatusAuthorizedBackground)) _config.SimpleStatusAuthorizedBackground = defaults.SimpleStatusAuthorizedBackground;
  339. if (string.IsNullOrWhiteSpace(_config.SimpleStatusUnauthorizedBackground)) _config.SimpleStatusUnauthorizedBackground = defaults.SimpleStatusUnauthorizedBackground;
  340. if (string.IsNullOrWhiteSpace(_config.SimpleStatusAuthorizedIconColor)) _config.SimpleStatusAuthorizedIconColor = defaults.SimpleStatusAuthorizedIconColor;
  341. if (string.IsNullOrWhiteSpace(_config.SimpleStatusUnauthorizedIconColor)) _config.SimpleStatusUnauthorizedIconColor = defaults.SimpleStatusUnauthorizedIconColor;
  342. if (string.IsNullOrWhiteSpace(_config.SimpleStatusAuthorizedLabelColor)) _config.SimpleStatusAuthorizedLabelColor = defaults.SimpleStatusAuthorizedLabelColor;
  343. if (string.IsNullOrWhiteSpace(_config.SimpleStatusUnauthorizedLabelColor)) _config.SimpleStatusUnauthorizedLabelColor = defaults.SimpleStatusUnauthorizedLabelColor;
  344. if (string.IsNullOrWhiteSpace(_config.SimpleStatusAuthorizedValueColor)) _config.SimpleStatusAuthorizedValueColor = defaults.SimpleStatusAuthorizedValueColor;
  345. if (string.IsNullOrWhiteSpace(_config.SimpleStatusUnauthorizedValueColor)) _config.SimpleStatusUnauthorizedValueColor = defaults.SimpleStatusUnauthorizedValueColor;
  346. if (string.IsNullOrWhiteSpace(_config.SimpleStatusIcon)) _config.SimpleStatusIcon = defaults.SimpleStatusIcon;
  347. _config.SimpleStatusIcon = NormalizeSimpleStatusIconSetting(_config.SimpleStatusIcon);
  348. _config.SimpleStatusRefreshIntervalSeconds = Mathf.Clamp(_config.SimpleStatusRefreshIntervalSeconds, 0.25f, 5f);
  349. if (string.IsNullOrWhiteSpace(_config.SoundFile)) _config.SoundFile = defaults.SoundFile;
  350.  
  351. _config.Activities = _config.Activities.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim().ToLowerInvariant()).Distinct().ToList();
  352. _config.Prefabs = _config.Prefabs.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).Distinct().ToList();
  353. if (_config.AfkMinutes < 1)
  354. {
  355. PrintWarning($"AfkMinutes cannot be lower than 1; clamping configured value {_config.AfkMinutes} to 1.");
  356. _config.AfkMinutes = 1;
  357. }
  358. _config.CooldownMinutes = Math.Max(0, _config.CooldownMinutes);
  359. _config.MinutesOnlineToQualify = Math.Max(0, _config.MinutesOnlineToQualify);
  360. _config.ClansPluginMinMembers = Math.Max(1, _config.ClansPluginMinMembers);
  361. _config.RustTeamMinMembers = Math.Max(1, _config.RustTeamMinMembers);
  362. _config.RustClanMinMembers = Math.Max(1, _config.RustClanMinMembers);
  363. _config.MinutesSinceLastAttackToProtect = Math.Max(-1, _config.MinutesSinceLastAttackToProtect);
  364.  
  365. _config.DamageScaleKeys = ParseThresholdKeys(_config.DamageScale.Keys, "DamageScale");
  366. if (_config.DamageScaleKeys.Count == 0)
  367. {
  368. PrintWarning("DamageScale contains no valid numeric thresholds; restoring defaults.");
  369. _config.DamageScale = new Dictionary<string, object>(defaults.DamageScale);
  370. _config.DamageScaleKeys = new List<int>(defaults.DamageScaleKeys);
  371. }
  372.  
  373. _config.GroupSizeProtectionKeys = ParseThresholdKeys(_config.GroupSizeProtectionScale.Keys, "GroupSizeProtectionScale");
  374. if (_config.GroupSizeProtectionKeys.Count == 0)
  375. {
  376. PrintWarning("GroupSizeProtectionScale contains no valid numeric thresholds; restoring defaults.");
  377. _config.GroupSizeProtectionScale = new Dictionary<string, float>(defaults.GroupSizeProtectionScale);
  378. _config.GroupSizeProtectionKeys = new List<int>(defaults.GroupSizeProtectionKeys);
  379. }
  380. }
  381.  
  382. private List<int> ParseThresholdKeys(IEnumerable<string> keys, string settingName)
  383. {
  384. var result = new List<int>();
  385. foreach (var raw in keys ?? Enumerable.Empty<string>())
  386. {
  387. if (!int.TryParse(raw, out var key) || key < 0)
  388. {
  389. PrintWarning($"Ignoring invalid {settingName} threshold '{raw}'. Thresholds must be non-negative integers.");
  390. continue;
  391. }
  392. if (!result.Contains(key)) result.Add(key);
  393. }
  394. result.Sort();
  395. return result;
  396. }
  397. #endregion
  398.  
  399. #region 2.AntiOfflineRaid.Localization.cs
  400. private static class PluginMessages
  401. {
  402. public const string ProtectionMessage = "Protection Message";
  403. public const string DeniedPermission = "Denied: Permission";
  404. public const string InvalidSyntax = "Invalid Syntax";
  405. public const string PlayerNotFound = "Player Not Found";
  406. public const string HelpTitle = "Help Title";
  407. public const string HelpFirstMinutes = "Help First Minutes";
  408. public const string HelpBetweenMinutesAndHour = "Help Between Minutes And Hour";
  409. public const string HelpFirstHour = "Help First Hour";
  410. public const string HelpAfterDays = "Help After Days";
  411. public const string HelpAfterHours = "Help After Hours";
  412. public const string StatusHeader = "Status Header";
  413. public const string StatusPlayerAfk = "Status Player AFK";
  414. public const string StatusPlayerOffline = "Status Player Offline";
  415. public const string StatusPlayerOnline = "Status Player Online";
  416. public const string StatusAfkMinutes = "Status AFK Minutes";
  417. public const string StatusShare = "Status Share";
  418. public const string StatusShareLastOffline = "Status Share Last Offline";
  419. public const string ProviderClansPlugin = "Provider Clans Plugin";
  420. public const string ProviderRustTeam = "Provider Rust Team";
  421. public const string ProviderRustClan = "Provider Rust Clan";
  422. public const string StatusOnline = "Status Online";
  423. public const string StatusOffline = "Status Offline";
  424. public const string StatusScale = "Status Scale";
  425. public const string OfflineProtectionStatus = "Offline Protection Status";
  426. }
  427.  
  428. protected override void LoadDefaultMessages()
  429. {
  430. lang.RegisterMessages(new Dictionary<string, string>
  431. {
  432. [PluginMessages.ProtectionMessage] = "This building is protected: {amount}%",
  433. [PluginMessages.DeniedPermission] = "You lack permission to do that",
  434. [PluginMessages.InvalidSyntax] = "Invalid syntax. Use: /ao <player name>",
  435. [PluginMessages.PlayerNotFound] = "No player found.",
  436. [PluginMessages.HelpTitle] = "AntiOfflineRaid by <color=#ce422b>Shady14u</color>",
  437. [PluginMessages.HelpFirstMinutes] = "<color=#ffd479>First {0} minutes</color>: 100%",
  438. [PluginMessages.HelpBetweenMinutesAndHour] = "<color=#ffd479>Between {0} minutes and 1 hour</color>: {1}%",
  439. [PluginMessages.HelpFirstHour] = "<color=#ffd479>First hour</color>: {0}%",
  440. [PluginMessages.HelpAfterDays] = "<color=#ffd479>After {0} day(s)</color>: {1}%",
  441. [PluginMessages.HelpAfterHours] = "<color=#ffd479>After {0} hour(s)</color>: {1}%",
  442. [PluginMessages.StatusHeader] = "<color=#F31D07><size=15>AntiOfflineRaid Status</size></color>: {0}",
  443. [PluginMessages.StatusPlayerAfk] = "<color=#56C5D5>Player Status</color>: <color=#E49C63>AFK</color>: {0}",
  444. [PluginMessages.StatusPlayerOffline] = "<color=#56C5D5>Player Status</color>: <color=#F31D07>Offline</color>: {0}",
  445. [PluginMessages.StatusPlayerOnline] = "<color=#56C5D5>Player Status</color>: <color=#3CF348>Online</color>",
  446. [PluginMessages.StatusAfkMinutes] = "<color=#56C5D5>AFK</color>: {0} minutes",
  447. [PluginMessages.StatusShare] = "<color=#56C5D5>{0} Status</color>: {1} ({2} online)",
  448. [PluginMessages.StatusShareLastOffline] = "<color=#56C5D5>{0} Last Offline</color>: {1} - {2}",
  449. [PluginMessages.ProviderClansPlugin] = "Clans Plugin",
  450. [PluginMessages.ProviderRustTeam] = "Rust Team",
  451. [PluginMessages.ProviderRustClan] = "Rust Clan",
  452. [PluginMessages.StatusOnline] = "<color=#3CF348>Online</color>",
  453. [PluginMessages.StatusOffline] = "<color=#F31D07>Offline</color>",
  454. [PluginMessages.StatusScale] = "<color=#56C5D5>Protection</color>: {0}%",
  455. [PluginMessages.OfflineProtectionStatus] = "OFFLINE PROTECTION"
  456. }, this, "en");
  457. }
  458.  
  459. private string GetMsg(string key, object userId = null)
  460. {
  461. return lang.GetMessage(key, this, userId?.ToString());
  462. }
  463.  
  464. void SendMessage(HitInfo hitInfo, int amt = 100)
  465. {
  466. if (!(hitInfo?.Initiator is BasePlayer player)) return;
  467. try
  468. {
  469. ShowMessage(player, amt);
  470. }
  471. catch (Exception ex)
  472. {
  473. PrintWarning($"Unable to show protection message to {player.UserIDString}: {ex.Message}");
  474. }
  475. }
  476. #endregion
  477.  
  478. #region 3.AntiOfflineRaid.Permissions.cs
  479. private static class PluginPermissions
  480. {
  481. public const string Protect = "antiofflineraid.protect";
  482. public const string Check = "antiofflineraid.check";
  483.  
  484.  
  485. }
  486. private void LoadPermissions()
  487. {
  488. permission.RegisterPermission(PluginPermissions.Protect, this);
  489. permission.RegisterPermission(PluginPermissions.Check, this);
  490. }
  491.  
  492. bool HasPerm(BasePlayer p, string pe)
  493. {
  494. return permission.UserHasPermission(p.userID.ToString(), pe);
  495. }
  496.  
  497. bool HasPerm(string userid, string pe)
  498. {
  499. return permission.UserHasPermission(userid, pe);
  500. }
  501. #endregion
  502.  
  503. #region 4.AntiOfflineRaid.Data.cs
  504. private StoredData _storedData;
  505.  
  506. public class StoredData
  507. {
  508. public Dictionary<ulong, LastOnline> LastOnlines { get; set; } = new Dictionary<ulong, LastOnline>();
  509. public Dictionary<ulong, long> LastStructureAttackAt { get; set; } = new Dictionary<ulong, long>(); // Protection-scope net ID -> last attack time
  510. public Dictionary<ulong, double> OnlineQualificationMinutes { get; set; } = new Dictionary<ulong, double>();
  511. public HashSet<ulong> OnlineQualificationActiveSessions { get; set; } = new HashSet<ulong>();
  512. public Dictionary<string, List<string>> MembersCache { get; set; } = new Dictionary<string, List<string>>();
  513. }
  514.  
  515. #region BoilerPlate
  516. private void LoadData()
  517. {
  518. try
  519. {
  520. _storedData = Interface.GetMod().DataFileSystem.ReadObject<StoredData>("AntiOfflineRaid") ?? new StoredData();
  521. }
  522. catch (Exception e)
  523. {
  524. Puts(e.Message);
  525. Puts(e.StackTrace);
  526. _storedData = new StoredData();
  527. }
  528. EnsureDataInitialized();
  529. }
  530.  
  531. private void EnsureDataInitialized()
  532. {
  533. if (_storedData == null) _storedData = new StoredData();
  534. if (_storedData.LastOnlines == null) _storedData.LastOnlines = new Dictionary<ulong, LastOnline>();
  535. if (_storedData.LastStructureAttackAt == null) _storedData.LastStructureAttackAt = new Dictionary<ulong, long>();
  536. if (_storedData.OnlineQualificationMinutes == null) _storedData.OnlineQualificationMinutes = new Dictionary<ulong, double>();
  537. if (_storedData.OnlineQualificationActiveSessions == null) _storedData.OnlineQualificationActiveSessions = new HashSet<ulong>();
  538. if (_storedData.MembersCache == null) _storedData.MembersCache = new Dictionary<string, List<string>>();
  539. }
  540.  
  541. private void SaveData()
  542. {
  543. EnsureDataInitialized();
  544. Interface.GetMod().DataFileSystem.WriteObject("AntiOfflineRaid", _storedData);
  545. }
  546.  
  547. private void ClearWipeScopedData()
  548. {
  549. EnsureDataInitialized();
  550. _storedData.LastStructureAttackAt.Clear();
  551. _storedData.MembersCache.Clear();
  552. _cachedScales.Clear();
  553. }
  554.  
  555. #endregion
  556. #endregion
  557.  
  558. #region 5.AntiOfflineRaid.Hooks.cs
  559. object OnEntityTakeDamage(BaseCombatEntity entity, HitInfo hitInfo)
  560. {
  561. if (hitInfo == null || hitInfo.damageTypes == null || entity == null) return null;
  562. if (hitInfo.damageTypes.Has(DamageType.Decay)) return null;
  563. if (entity is BasePlayer) return null;
  564.  
  565. // Reject unrelated combat entities before doing parent traversal or protection resolution.
  566. var structuralType = entity is BuildingBlock || entity is BoatBuildingBlock || entity is PlayerBoat || entity is Tugboat;
  567. var configuredPrefab = !structuralType && IsConfiguredPrefab(entity);
  568. if (!structuralType && !configuredPrefab) return null;
  569.  
  570. var navalTarget = FindNavalTarget(entity);
  571. if (structuralType && navalTarget != null && entity != navalTarget && !(navalTarget is PlayerBoat && entity is BoatBuildingBlock))
  572. configuredPrefab = IsConfiguredPrefab(entity);
  573. // BoatBuildingBlock forwards damage to its parent PlayerBoat. The child callback already applies protection, so do not scale the forwarded parent callback again
  574. if (entity is PlayerBoat && hitInfo.HitEntity != null && hitInfo.HitEntity != entity && FindNavalTarget(hitInfo.HitEntity) == entity) return null;
  575. return IsBlocked(entity, navalTarget, configuredPrefab) ? OnStructureAttack(entity, hitInfo, navalTarget) : null;
  576. }
  577.  
  578. void OnLootItem(BasePlayer player, Item item)
  579. {
  580. if (player != null) UpdateLastOnline(player);
  581. }
  582.  
  583. void OnLootPlayer(BasePlayer target, BasePlayer looter)
  584. {
  585. if (looter != null) UpdateLastOnline(looter);
  586. }
  587.  
  588. void OnPlayerChat(BasePlayer player, string message, ConVar.Chat.ChatChannel channel)
  589. {
  590. if (player != null) UpdateLastOnline(player);
  591. }
  592.  
  593. void OnPlayerConnected(BasePlayer player)
  594. {
  595. if (player == null) return;
  596. StartOnlineQualificationSession(player.userID, true);
  597. UpdateLastOnline(player);
  598. if (_storedData.LastOnlines.TryGetValue(player.userID, out var state) && state != null) state.LastPosition = player.transform.position;
  599.  
  600. var language = lang.GetLanguage(player.UserIDString);
  601. if (string.IsNullOrEmpty(language)) language = "en";
  602. _lastLangByUser[player.userID] = language;
  603.  
  604. QueueSimpleStatusRefresh(player, false);
  605. }
  606.  
  607. void OnPlayerDisconnected(BasePlayer player, string reason)
  608. {
  609. if (player == null) return;
  610. HideSimpleStatus(player);
  611. _lastInputActivityAt.Remove(player.userID);
  612. _lastLangByUser.Remove(player.userID);
  613.  
  614. LastOnline state = null;
  615. if (_storedData?.LastOnlines != null) _storedData.LastOnlines.TryGetValue(player.userID, out state);
  616. UpdateOnlineQualification(player, state);
  617. EndOnlineQualificationSession(player.userID);
  618.  
  619. var preserveAfkOfflineState = _config?.TreatAfkAsOffline == true && state != null && state.IsAfk();
  620.  
  621. if (preserveAfkOfflineState)
  622. {
  623. // Preserve the moment AFK crossed into offline state instead of restarting the offline timer at disconnect.
  624. state.LastOnlineTime = GetAfkOfflineSince(state);
  625. state.AfkMinutes = Math.Max(state.AfkMinutes, _config.AfkMinutes);
  626. InvalidateDamageScale(player.userID);
  627. }
  628. else
  629. {
  630. UpdateLastOnline(player.userID);
  631. }
  632.  
  633. RefreshAllSimpleStatuses();
  634. }
  635.  
  636. void OnPlayerRespawned(BasePlayer player)
  637. {
  638. if (player == null) return;
  639. UpdateLastOnline(player);
  640. if (_storedData.LastOnlines.TryGetValue(player.userID, out var state) && state != null) state.LastPosition = player.transform.position;
  641. QueueSimpleStatusRefresh(player, false);
  642. }
  643.  
  644. void OnPlayerSleepEnded(BasePlayer player)
  645. {
  646. if (player == null) return;
  647. UpdateLastOnline(player);
  648. if (_storedData.LastOnlines.TryGetValue(player.userID, out var state) && state != null) state.LastPosition = player.transform.position;
  649. QueueSimpleStatusRefresh(player, false);
  650. }
  651.  
  652. void OnPlayerInput(BasePlayer player, InputState input)
  653. {
  654. if (player == null || input?.current == null || input.previous == null) return;
  655.  
  656. var now = Time.realtimeSinceStartup;
  657. if (_lastInputActivityAt.TryGetValue(player.userID, out var last) && now - last < InputActivityThrottleSeconds) return;
  658.  
  659. // Observe the final movement after buttons are released without counting idle input packets as activity
  660. var hasInputActivity = input.current.buttons != 0 && !input.previous.Equals(input.current);
  661. if (!hasInputActivity)
  662. {
  663. if (_storedData?.LastOnlines == null || !_storedData.LastOnlines.TryGetValue(player.userID, out var lastOnline) || lastOnline == null) return;
  664. if (lastOnline.LastPosition.Equals(player.transform.position)) return;
  665. }
  666.  
  667. _lastInputActivityAt[player.userID] = now;
  668. UpdateLastOnline(player);
  669. }
  670.  
  671. private void OnActiveItemChanged(BasePlayer player, Item oldItem, Item newItem)
  672. {
  673. if (_config?.SimpleStatusEnabled != true || !_config.SimpleStatusShowForUnauthorizedOnlyWhenHoldingHammer || player == null) return;
  674. QueueSimpleStatusRefresh(player, false);
  675. }
  676.  
  677. private void OnPlayerLanguageChanged(BasePlayer player, string language)
  678. {
  679. if (player == null || !player.userID.IsSteamId()) return;
  680.  
  681. var current = string.IsNullOrWhiteSpace(language) ? "en" : language.Trim();
  682. _lastLangByUser[player.userID] = current;
  683. _simpleStatusStateKeys.Remove(player.userID);
  684.  
  685. QueueSimpleStatusRefresh(player, false);
  686. }
  687.  
  688. private void RefreshSimpleStatusAuthorizationState(BasePlayer player = null)
  689. {
  690. QueueSimpleStatusRefresh(player);
  691. }
  692.  
  693. private object OnCupboardAssign(BuildingPrivlidge privilege, ulong userId, BasePlayer player)
  694. {
  695. RefreshSimpleStatusAuthorizationState(player);
  696. return null;
  697. }
  698.  
  699. private void OnCupboardAuthorize(BuildingPrivlidge privilege, BasePlayer player)
  700. {
  701. RefreshSimpleStatusAuthorizationState(player);
  702. }
  703.  
  704. private void OnCupboardDeauthorize(BuildingPrivlidge privilege, BasePlayer player)
  705. {
  706. RefreshSimpleStatusAuthorizationState(player);
  707. }
  708.  
  709. private void OnCupboardClearList(BuildingPrivlidge privilege, BasePlayer player)
  710. {
  711. RefreshSimpleStatusAuthorizationState(player);
  712. }
  713.  
  714. private void OnCupboardAuthorize(VehiclePrivilege privilege, BasePlayer player)
  715. {
  716. RefreshSimpleStatusAuthorizationState(player);
  717. }
  718.  
  719. private void OnCupboardDeauthorize(VehiclePrivilege privilege, BasePlayer player)
  720. {
  721. RefreshSimpleStatusAuthorizationState(player);
  722. }
  723.  
  724. private void OnCupboardClearList(VehiclePrivilege privilege, BasePlayer player)
  725. {
  726. RefreshSimpleStatusAuthorizationState(player);
  727. }
  728.  
  729. void OnServerInitialized()
  730. {
  731. LoadPermissions();
  732. LoadData();
  733. if (_newSaveDetected) ClearWipeScopedData();
  734. _storedData.MembersCache.Clear();
  735. _newSaveDetected = false;
  736. _serverInitialized = true;
  737. InitializeOnlineQualificationSessions();
  738.  
  739. ResolveOptionalPluginReferences();
  740. if (HelpText != null) Puts("HelpText detected. /help integration is available when AllowHelpTextFromOtherPlugins is enabled.");
  741. ReportClansPluginAvailability(true);
  742. ReportSimpleStatusDependencies(true);
  743. if (_simpleStatusReady) RefreshSimpleStatusIntegration();
  744. else ScheduleSimpleStatusReadyFallback();
  745.  
  746. if (!_config.Activities.Any())
  747. {
  748. PrintWarning("At least one activity trigger must be configured for this plugin to operate.");
  749. ToggleHooks(new List<string>());
  750. }
  751. else
  752. {
  753. ToggleHooks(_config.Activities);
  754. UpdateLastOnlineAll(false);
  755. _lastOnlineTimer = timer.Repeat(AfkCheckIntervalMinutes * 60, 0, () => UpdateLastOnlineAll());
  756. }
  757. }
  758.  
  759. private void OnServerSave()
  760. {
  761. SaveData();
  762. }
  763.  
  764. private void OnNewSave(string strFilename)
  765. {
  766. _newSaveDetected = true;
  767. if (_storedData == null) return;
  768. ClearWipeScopedData();
  769. SaveData();
  770. }
  771.  
  772. void OnClanCreate(string tag)
  773. {
  774. if (!UsesClansPluginIntegration() || _storedData == null) return;
  775. CacheClansPluginClan(tag);
  776. }
  777.  
  778. void OnClanUpdate(string tag)
  779. {
  780. if (!UsesClansPluginIntegration() || _storedData == null) return;
  781. CacheClansPluginClan(tag);
  782. }
  783.  
  784. void OnClanDestroy(string tag)
  785. {
  786. RemoveCachedClansPluginClan(tag);
  787. }
  788.  
  789. void OnClanDisbanded(string tag, List<ulong> members)
  790. {
  791. RemoveCachedClansPluginClan(tag);
  792. }
  793.  
  794. void OnClanMemberJoined(string tag, ulong joining, List<ulong> members)
  795. {
  796. if (!UsesClansPluginIntegration() || _storedData == null) return;
  797. CacheClansPluginClan(tag);
  798. }
  799.  
  800. void OnClanMemberGone(string tag, ulong leaving, List<ulong> members)
  801. {
  802. if (!UsesClansPluginIntegration() || _storedData == null) return;
  803. CacheClansPluginClan(tag);
  804. }
  805.  
  806. private void RemoveCachedClansPluginClan(string tag)
  807. {
  808. if (!UsesClansPluginIntegration() || _storedData?.MembersCache == null || string.IsNullOrWhiteSpace(tag)) return;
  809. _storedData.MembersCache.Remove(tag);
  810. }
  811.  
  812. void OnPluginLoaded(Plugin plugin)
  813. {
  814. if (plugin == null) return;
  815.  
  816. if (IsClansPlugin(plugin))
  817. {
  818. Clans = plugin;
  819. _storedData?.MembersCache?.Clear();
  820. NextTick(() => ReportClansPluginAvailability());
  821. }
  822.  
  823. if (IsSimpleStatusPlugin(plugin))
  824. {
  825. SimpleStatus = plugin;
  826. if (_config?.SimpleStatusEnabled == true)
  827. {
  828. NextTick(() =>
  829. {
  830. ReportSimpleStatusDependencies();
  831. ScheduleSimpleStatusReadyFallback();
  832. });
  833. }
  834. }
  835.  
  836. if (IsImageLibraryPlugin(plugin))
  837. {
  838. ImageLibrary = plugin;
  839. ResetSimpleStatusIcon();
  840. if (_config?.SimpleStatusEnabled == true)
  841. {
  842. NextTick(() =>
  843. {
  844. ReportSimpleStatusDependencies();
  845. if (_simpleStatusReady) RefreshSimpleStatusIntegration();
  846. });
  847. }
  848. }
  849.  
  850. if (IsHelpTextPlugin(plugin))
  851. {
  852. HelpText = plugin;
  853. Puts("HelpText detected. /help integration is available when AllowHelpTextFromOtherPlugins is enabled.");
  854. }
  855. }
  856.  
  857. void OnPluginUnloaded(Plugin plugin)
  858. {
  859. if (plugin == null) return;
  860.  
  861. if (IsClansPlugin(plugin))
  862. {
  863. Clans = null;
  864. _storedData?.MembersCache?.Clear();
  865. NextTick(() => ReportClansPluginAvailability());
  866. }
  867.  
  868. if (IsSimpleStatusPlugin(plugin))
  869. {
  870. SimpleStatus = null;
  871. _simpleStatusReady = false;
  872. StopSimpleStatusIntegration(false);
  873. if (_config?.SimpleStatusEnabled == true) NextTick(() => ReportSimpleStatusDependencies());
  874. }
  875.  
  876. if (IsImageLibraryPlugin(plugin))
  877. {
  878. ImageLibrary = null;
  879. ResetSimpleStatusIcon();
  880. StopSimpleStatusIntegration(true);
  881. if (_config?.SimpleStatusEnabled == true) NextTick(() => ReportSimpleStatusDependencies());
  882. }
  883.  
  884. if (IsHelpTextPlugin(plugin)) HelpText = null;
  885. }
  886.  
  887. private void OnSimpleStatusReady()
  888. {
  889. _simpleStatusReady = true;
  890. if (!_serverInitialized || _config?.SimpleStatusEnabled != true) return;
  891. ResolveOptionalPluginReferences();
  892. NextTick(() =>
  893. {
  894. ResetSimpleStatusIcon();
  895. ReportSimpleStatusDependencies();
  896. RefreshSimpleStatusIntegration();
  897. });
  898. }
  899.  
  900. protected void ToggleHook(List<string> wakeupTriggers, string trigger, string hook)
  901. {
  902. if (wakeupTriggers.Contains(trigger))
  903. {
  904. Subscribe(hook);
  905. }
  906. else
  907. {
  908. Unsubscribe(hook);
  909. }
  910. }
  911.  
  912. protected void ToggleHooks(List<string> wakeupTriggers)
  913. {
  914. var defaultWakeupTriggers = GetDefaultActivities();
  915.  
  916. foreach (var trigger in defaultWakeupTriggers)
  917. {
  918. switch (trigger)
  919. {
  920. case "input":
  921. ToggleHook(wakeupTriggers, trigger, "OnPlayerInput");
  922. break;
  923. case "loot":
  924. ToggleHook(wakeupTriggers, trigger, "OnLootPlayer");
  925. ToggleHook(wakeupTriggers, trigger, "OnLootItem");
  926. break;
  927. case "respawn":
  928. ToggleHook(wakeupTriggers, trigger, "OnPlayerRespawned");
  929. break;
  930. case "chat":
  931. ToggleHook(wakeupTriggers, trigger, "OnPlayerChat");
  932. break;
  933. case "wakeup":
  934. ToggleHook(wakeupTriggers, trigger, "OnPlayerSleepEnded");
  935. break;
  936. }
  937. }
  938. }
  939.  
  940. private void Unload()
  941. {
  942. _lastOnlineTimer?.Destroy();
  943. _lastOnlineTimer = null;
  944. StopSimpleStatusIntegration(true);
  945. UpdateLastOnlineAll();
  946. SaveData();
  947. _cachedScales.Clear();
  948. _configuredPrefabMatches.Clear();
  949. _simpleStatusColorCache.Clear();
  950. _lastInputActivityAt.Clear();
  951. _onlineQualificationLastUpdateAt.Clear();
  952. _lastLangByUser.Clear();
  953. _lastReportedClansPluginAvailable = null;
  954. _lastReportedSimpleStatusAvailable = null;
  955. _lastReportedImageLibraryAvailable = null;
  956. _serverInitialized = false;
  957. _simpleStatusReady = false;
  958. }
  959. #endregion
  960.  
  961. #region 6.AntiOfflineRaid.Commands.cs
  962. [ConsoleCommand("ao")]
  963. void CheckOfflineStatus(ConsoleSystem.Arg arg)
  964. {
  965. if (arg == null) return;
  966.  
  967. var player = arg.Connection?.player as BasePlayer;
  968. if (player != null && !HasPerm(player, PluginPermissions.Check) && arg.Connection.authLevel < 1)
  969. {
  970. SendReply(arg, GetMsg(PluginMessages.DeniedPermission, arg.Connection.userid));
  971. return;
  972. }
  973.  
  974. var userId = player?.UserIDString;
  975. var playerSearch = arg.Args != null && arg.Args.Length > 0 ? arg.Args[0].ToString() : null;
  976. if (string.IsNullOrWhiteSpace(playerSearch))
  977. {
  978. SendReply(arg, GetMsg(PluginMessages.InvalidSyntax, userId));
  979. return;
  980. }
  981.  
  982. SendReply(arg, SendStatus(playerSearch, userId));
  983. }
  984.  
  985. [ChatCommand("ao")]
  986. void CheckOfflineStatus(BasePlayer player, string command, string[] args)
  987. {
  988. if (player == null) return;
  989. if (!HasPerm(player, PluginPermissions.Check) && (player.net?.connection?.authLevel ?? 0) < 1)
  990. {
  991. SendReply(player, GetMsg(PluginMessages.DeniedPermission, player.UserIDString));
  992. return;
  993. }
  994.  
  995. if (args == null || args.Length < 1 || string.IsNullOrWhiteSpace(args[0]))
  996. {
  997. SendReply(player, GetMsg(PluginMessages.InvalidSyntax, player.UserIDString));
  998. return;
  999. }
  1000.  
  1001. SendReply(player, SendStatus(args[0], player.UserIDString));
  1002. }
  1003.  
  1004. [ChatCommand("ao.fill.onlineTimes")]
  1005. void CmdFillOnlines(BasePlayer player, string command, string[] args)
  1006. {
  1007. if (player == null || !player.IsAdmin) return;
  1008. EnsureDataInitialized();
  1009.  
  1010. var changed = false;
  1011. foreach (var basePlayer in covalence.Players.All)
  1012. {
  1013. if (!ulong.TryParse(basePlayer.Id, out var userId) || !userId.IsSteamId()) continue;
  1014. if (_storedData.LastOnlines.ContainsKey(userId)) continue;
  1015. UpdateLastOnline(userId);
  1016. changed = true;
  1017. }
  1018.  
  1019. if (changed) SaveData();
  1020. }
  1021.  
  1022. void SendHelpText(BasePlayer player)
  1023. {
  1024. if (player == null) return;
  1025. var userId = player.UserIDString;
  1026. var stringBuilder = new StringBuilder(GetMsg(PluginMessages.HelpTitle, userId)).Append('\n');
  1027.  
  1028. if (_config.CooldownMinutes > 0)
  1029. {
  1030. stringBuilder.AppendFormat(GetMsg(PluginMessages.HelpFirstMinutes, userId), _config.CooldownMinutes).Append('\n');
  1031. stringBuilder.AppendFormat(GetMsg(PluginMessages.HelpBetweenMinutesAndHour, userId), _config.CooldownMinutes, Math.Round(_config.InterimDamage * 100f, 0)).Append('\n');
  1032. }
  1033. else
  1034. {
  1035. stringBuilder.AppendFormat(GetMsg(PluginMessages.HelpFirstHour, userId), Math.Round(_config.InterimDamage * 100f, 0)).Append('\n');
  1036. }
  1037.  
  1038. foreach (var key in _config.DamageScaleKeys)
  1039. {
  1040. if (!_config.DamageScale.TryGetValue(key.ToString(), out var rawScale)) continue;
  1041.  
  1042. double scaleValue;
  1043. try
  1044. {
  1045. scaleValue = Convert.ToDouble(rawScale, CultureInfo.InvariantCulture);
  1046. }
  1047. catch
  1048. {
  1049. continue;
  1050. }
  1051.  
  1052. var scale = Math.Round(scaleValue * 100, 0);
  1053. var hours = Math.Round(Convert.ToDouble(key), 1);
  1054. if (hours >= 24)
  1055. {
  1056. var days = Math.Round(hours / 24, 1);
  1057. stringBuilder.AppendFormat(GetMsg(PluginMessages.HelpAfterDays, userId), days, scale).Append('\n');
  1058. }
  1059. else
  1060. {
  1061. stringBuilder.AppendFormat(GetMsg(PluginMessages.HelpAfterHours, userId), hours, scale).Append('\n');
  1062. }
  1063. }
  1064.  
  1065. player.ChatMessage(stringBuilder.ToString());
  1066. }
  1067. #endregion
  1068.  
  1069. #region 7.AntiOfflineRaid.Classes.cs
  1070. public class LastOnline
  1071. {
  1072. [JsonIgnore] public float AfkMinutes = 0;
  1073.  
  1074. public long LastOnlineLong;
  1075.  
  1076. [JsonIgnore] public Vector3 LastPosition;
  1077.  
  1078. public ulong UserId;
  1079.  
  1080. [JsonConstructor]
  1081. public LastOnline(ulong userid, long lastOnlineLong)
  1082. {
  1083. UserId = userid;
  1084. LastOnlineLong = lastOnlineLong;
  1085. }
  1086.  
  1087. public LastOnline(ulong userId, DateTime lastOnline)
  1088. {
  1089. UserId = userId;
  1090. LastOnlineTime = lastOnline;
  1091. }
  1092.  
  1093. [JsonIgnore] public BasePlayer AarPlayer => BasePlayer.FindByID(UserId);
  1094.  
  1095. [JsonIgnore] public double Days => (DateTime.Now - LastOnlineTime).TotalDays;
  1096.  
  1097. [JsonIgnore] public double Hours => (DateTime.Now - LastOnlineTime).TotalHours;
  1098.  
  1099. [JsonIgnore]
  1100. public DateTime LastOnlineTime
  1101. {
  1102. get => DateTime.FromBinary(LastOnlineLong);
  1103.  
  1104. set => LastOnlineLong = value.ToBinary();
  1105. }
  1106.  
  1107. [JsonIgnore] public double Minutes => (DateTime.Now - LastOnlineTime).TotalMinutes;
  1108.  
  1109. public bool HasMoved(Vector3 position)
  1110. {
  1111. var moved = !LastPosition.Equals(position);
  1112. LastPosition = new Vector3(position.x, position.y, position.z);
  1113. return moved;
  1114. }
  1115.  
  1116. public bool IsAfk()
  1117. {
  1118. return Minutes >= _config.AfkMinutes;
  1119. }
  1120.  
  1121. public bool IsConnected()
  1122. {
  1123. var player = AarPlayer;
  1124. return player != null && player.IsConnected;
  1125. }
  1126.  
  1127. public bool IsOffline()
  1128. {
  1129. if (!IsConnected()) return true;
  1130. return _config.TreatAfkAsOffline && IsAfk();
  1131. }
  1132. }
  1133.  
  1134. class ScaleCacheItem
  1135. {
  1136. public DateTime Expires;
  1137. public float Scale;
  1138.  
  1139. public ScaleCacheItem(DateTime expires, float scale)
  1140. {
  1141. Expires = expires;
  1142. Scale = scale;
  1143. }
  1144. }
  1145. private struct SimpleStatusStateKey
  1146. {
  1147. public ulong TargetId;
  1148. public bool PrivilegeBased;
  1149. public bool HasAccess;
  1150. public int ProtectionPercent;
  1151. public string Label;
  1152. public string Background;
  1153. public string IconColor;
  1154. public string LabelColor;
  1155. public string ValueColor;
  1156. public string Icon;
  1157.  
  1158. public SimpleStatusStateKey(ulong targetId, bool privilegeBased, bool hasAccess, int protectionPercent, string label, string background, string iconColor, string labelColor, string valueColor, string icon)
  1159. {
  1160. TargetId = targetId;
  1161. PrivilegeBased = privilegeBased;
  1162. HasAccess = hasAccess;
  1163. ProtectionPercent = protectionPercent;
  1164. Label = label;
  1165. Background = background;
  1166. IconColor = iconColor;
  1167. LabelColor = labelColor;
  1168. ValueColor = valueColor;
  1169. Icon = icon;
  1170. }
  1171.  
  1172. public bool Equals(SimpleStatusStateKey other)
  1173. {
  1174. return TargetId == other.TargetId && PrivilegeBased == other.PrivilegeBased && HasAccess == other.HasAccess && ProtectionPercent == other.ProtectionPercent &&
  1175. string.Equals(Label, other.Label, StringComparison.Ordinal) && string.Equals(Background, other.Background, StringComparison.Ordinal) &&
  1176. string.Equals(IconColor, other.IconColor, StringComparison.Ordinal) && string.Equals(LabelColor, other.LabelColor, StringComparison.Ordinal) &&
  1177. string.Equals(ValueColor, other.ValueColor, StringComparison.Ordinal) && string.Equals(Icon, other.Icon, StringComparison.Ordinal);
  1178. }
  1179. }
  1180. #endregion
  1181.  
  1182. #region 0.AntiOfflineRaid.cs
  1183. [PluginReference] Plugin Clans;
  1184. [PluginReference] Plugin SimpleStatus;
  1185. [PluginReference] Plugin ImageLibrary;
  1186. [PluginReference] Plugin HelpText;
  1187. private const string OfflineProtectionStatusIconKey = "AntiOfflineRaid_Protection_Icon";
  1188. private const string JsonMessage =
  1189. @"[{""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""}]}]";
  1190.  
  1191. private const float AfkCheckIntervalMinutes = 1f;
  1192. private const float NavalPresenceRadius = 30f;
  1193. private const float InputActivityThrottleSeconds = 1f;
  1194. private const float SimpleStatusIconCheckIntervalSeconds = 0.25f;
  1195. private const float SimpleStatusMinBatchDelaySeconds = 0.01f;
  1196. private const int SimpleStatusPlayersPerBatch = 32;
  1197. private const int SimpleStatusIconCheckAttempts = 120;
  1198. private const int ConfiguredPrefabCacheLimit = 2048;
  1199. private const int SimpleStatusColorCacheLimit = 16;
  1200. private readonly Dictionary<ulong, ScaleCacheItem> _cachedScales = new Dictionary<ulong, ScaleCacheItem>();
  1201. private readonly Dictionary<string, bool> _configuredPrefabMatches = new Dictionary<string, bool>(StringComparer.Ordinal);
  1202. private readonly Dictionary<string, string> _simpleStatusColorCache = new Dictionary<string, string>(StringComparer.Ordinal);
  1203. private readonly Dictionary<ulong, float> _lastInputActivityAt = new Dictionary<ulong, float>();
  1204. private readonly Dictionary<ulong, DateTime> _onlineQualificationLastUpdateAt = new Dictionary<ulong, DateTime>();
  1205. private readonly Dictionary<Type, System.Reflection.FieldInfo> _vehiclePrivilegeAuthorizedPlayersFieldByType = new Dictionary<Type, System.Reflection.FieldInfo>();
  1206. private readonly Dictionary<Type, System.Reflection.FieldInfo> _authorizationEntryUserIdFieldByType = new Dictionary<Type, System.Reflection.FieldInfo>();
  1207. private readonly Dictionary<Type, System.Reflection.PropertyInfo> _authorizationEntryUserIdPropertyByType = new Dictionary<Type, System.Reflection.PropertyInfo>();
  1208. private Timer _lastOnlineTimer;
  1209. private Timer _simpleStatusTimer;
  1210. private Timer _languageWatchTimer;
  1211. private readonly List<BasePlayer> _simpleStatusBatchPlayers = new List<BasePlayer>();
  1212. private readonly Queue<BasePlayer> _simpleStatusPriorityPlayers = new Queue<BasePlayer>();
  1213. private readonly HashSet<BasePlayer> _simpleStatusPriorityPlayerSet = new HashSet<BasePlayer>();
  1214. private int _simpleStatusBatchCursor;
  1215. private int _simpleStatusBatchSize;
  1216. private int _simpleStatusSchedulerGeneration;
  1217. private float _simpleStatusBatchDelaySeconds;
  1218. private float _simpleStatusScheduledDelaySeconds;
  1219. private float _simpleStatusNextRunAt;
  1220. private bool _simpleStatusRefreshRequested;
  1221. private bool _simpleStatusRefreshQueued;
  1222. private readonly Dictionary<ulong, string> _lastLangByUser = new Dictionary<ulong, string>();
  1223. private readonly Dictionary<ulong, SimpleStatusStateKey> _simpleStatusStateKeys = new Dictionary<ulong, SimpleStatusStateKey>();
  1224. private readonly HashSet<ulong> _simpleStatusVisiblePlayers = new HashSet<ulong>();
  1225. private bool? _lastReportedClansPluginAvailable;
  1226. private bool? _lastReportedSimpleStatusAvailable;
  1227. private bool? _lastReportedImageLibraryAvailable;
  1228. private bool _serverInitialized;
  1229. private bool _newSaveDetected;
  1230. private bool _simpleStatusReady;
  1231. private bool _simpleStatusCreated;
  1232. private bool _simpleStatusIconRegistered;
  1233. private bool _simpleStatusIconRegistrationPending;
  1234. private string _simpleStatusIconPendingKey;
  1235. private string _resolvedSimpleStatusIcon;
  1236.  
  1237. private bool IsClansPluginAvailable()
  1238. {
  1239. return Clans != null;
  1240. }
  1241.  
  1242. private bool UsesClansPluginIntegration()
  1243. {
  1244. return _config != null && (_config.ClansPluginShare || (_config.GroupSizeProtectionScaling && _config.GroupSizeUseClansPluginMembers));
  1245. }
  1246.  
  1247. private void ReportClansPluginAvailability(bool force = false)
  1248. {
  1249. if (!UsesClansPluginIntegration())
  1250. {
  1251. _lastReportedClansPluginAvailable = null;
  1252. return;
  1253. }
  1254.  
  1255. var available = IsClansPluginAvailable();
  1256. if (!force && _lastReportedClansPluginAvailable.HasValue && _lastReportedClansPluginAvailable.Value == available) return;
  1257. _lastReportedClansPluginAvailable = available;
  1258.  
  1259. if (available) Puts("Clans plugin detected. External clan support is enabled.");
  1260. else PrintWarning("Clans plugin support is enabled, but the Clans plugin is not loaded. External clan checks will be skipped until it becomes available.");
  1261. }
  1262.  
  1263. private static bool IsClansPlugin(Plugin plugin)
  1264. {
  1265. return plugin != null && string.Equals(plugin.Name, "Clans", StringComparison.OrdinalIgnoreCase);
  1266. }
  1267.  
  1268. private static bool IsSimpleStatusPlugin(Plugin plugin)
  1269. {
  1270. return plugin != null && (string.Equals(plugin.Name, "SimpleStatus", StringComparison.OrdinalIgnoreCase) || string.Equals(plugin.Name, "Simple Status", StringComparison.OrdinalIgnoreCase));
  1271. }
  1272.  
  1273. private static bool IsImageLibraryPlugin(Plugin plugin)
  1274. {
  1275. return plugin != null && (string.Equals(plugin.Name, "ImageLibrary", StringComparison.OrdinalIgnoreCase) || string.Equals(plugin.Name, "Image Library", StringComparison.OrdinalIgnoreCase));
  1276. }
  1277.  
  1278. private static bool IsHelpTextPlugin(Plugin plugin)
  1279. {
  1280. return plugin != null && (string.Equals(plugin.Name, "HelpText", StringComparison.OrdinalIgnoreCase) || string.Equals(plugin.Name, "Help Text", StringComparison.OrdinalIgnoreCase));
  1281. }
  1282.  
  1283. private void ResolveOptionalPluginReferences()
  1284. {
  1285. if (Clans == null) Clans = plugins.Find("Clans");
  1286. if (SimpleStatus == null) SimpleStatus = plugins.Find("SimpleStatus") ?? plugins.Find("Simple Status");
  1287. if (ImageLibrary == null) ImageLibrary = plugins.Find("ImageLibrary") ?? plugins.Find("Image Library");
  1288. if (HelpText == null) HelpText = plugins.Find("HelpText") ?? plugins.Find("Help Text");
  1289. }
  1290.  
  1291. private bool IsSimpleStatusIntegrationReady()
  1292. {
  1293. return _serverInitialized && _simpleStatusReady && _config?.SimpleStatusEnabled == true && SimpleStatus != null && ImageLibrary != null;
  1294. }
  1295.  
  1296. private void ScheduleSimpleStatusReadyFallback()
  1297. {
  1298. if (!_serverInitialized || _config?.SimpleStatusEnabled != true || SimpleStatus == null || _simpleStatusReady) return;
  1299. timer.Once(1f, () =>
  1300. {
  1301. if (!_serverInitialized || _config?.SimpleStatusEnabled != true || SimpleStatus == null || _simpleStatusReady) return;
  1302. _simpleStatusReady = true;
  1303. ReportSimpleStatusDependencies();
  1304. RefreshSimpleStatusIntegration();
  1305. });
  1306. }
  1307.  
  1308. private void ReportSimpleStatusDependencies(bool force = false)
  1309. {
  1310. if (_config?.SimpleStatusEnabled != true)
  1311. {
  1312. _lastReportedSimpleStatusAvailable = null;
  1313. _lastReportedImageLibraryAvailable = null;
  1314. return;
  1315. }
  1316.  
  1317. ResolveOptionalPluginReferences();
  1318.  
  1319. var simpleStatusAvailable = SimpleStatus != null;
  1320. if (force || !_lastReportedSimpleStatusAvailable.HasValue || _lastReportedSimpleStatusAvailable.Value != simpleStatusAvailable)
  1321. {
  1322. _lastReportedSimpleStatusAvailable = simpleStatusAvailable;
  1323. if (simpleStatusAvailable) Puts("SimpleStatus detected. Offline protection status integration is enabled.");
  1324. else PrintWarning("SimpleStatus support is enabled, but SimpleStatus is not loaded. Offline protection status display is unavailable.");
  1325. }
  1326.  
  1327. var imageLibraryAvailable = ImageLibrary != null;
  1328. if (force || !_lastReportedImageLibraryAvailable.HasValue || _lastReportedImageLibraryAvailable.Value != imageLibraryAvailable)
  1329. {
  1330. _lastReportedImageLibraryAvailable = imageLibraryAvailable;
  1331. if (imageLibraryAvailable) Puts("ImageLibrary detected. Offline protection status icon integration is enabled.");
  1332. else PrintWarning("SimpleStatus support is enabled, but ImageLibrary is not loaded. Offline protection status display is unavailable.");
  1333. }
  1334. }
  1335.  
  1336. private string GetClansPluginTag(ulong userId)
  1337. {
  1338. if (!userId.IsSteamId() || !IsClansPluginAvailable()) return null;
  1339. try
  1340. {
  1341. return Clans.Call<string>("GetClanOf", userId);
  1342. }
  1343. catch (Exception ex)
  1344. {
  1345. PrintWarning($"Unable to query clan for {userId}: {ex.Message}");
  1346. return null;
  1347. }
  1348. }
  1349.  
  1350. public List<string> CacheClansPluginClan(string tag)
  1351. {
  1352. if (string.IsNullOrWhiteSpace(tag) || !IsClansPluginAvailable()) return new List<string>();
  1353.  
  1354. try
  1355. {
  1356. var clan = Clans.Call<JObject>("GetClan", tag);
  1357. var members = new List<string>();
  1358. if (clan?["members"] != null)
  1359. {
  1360. foreach (var token in clan["members"])
  1361. {
  1362. var memberId = token?.ToString();
  1363. if (!string.IsNullOrWhiteSpace(memberId) && !members.Contains(memberId)) members.Add(memberId);
  1364. }
  1365. }
  1366. _storedData.MembersCache[tag] = members;
  1367. return members;
  1368. }
  1369. catch (Exception ex)
  1370. {
  1371. PrintWarning($"Unable to cache clan '{tag}': {ex.Message}");
  1372. return new List<string>();
  1373. }
  1374. }
  1375.  
  1376. float CacheDamageScale(ulong targetId, float scale, ScaleCacheItem kvp = null)
  1377. {
  1378. if (kvp == null)
  1379. {
  1380. _cachedScales[targetId] = new ScaleCacheItem(DateTime.Now.AddMinutes(1), scale);
  1381. }
  1382. else
  1383. {
  1384. kvp.Expires = DateTime.Now.AddMinutes(1);
  1385. kvp.Scale = scale;
  1386. }
  1387. return scale;
  1388. }
  1389.  
  1390. private void InvalidateDamageScale(ulong userId)
  1391. {
  1392. _cachedScales.Remove(userId);
  1393. }
  1394.  
  1395. private bool UsesOnlineQualification()
  1396. {
  1397. return _config != null && _config.MinutesOnlineToQualify > 0;
  1398. }
  1399.  
  1400. private void InitializeOnlineQualificationSessions()
  1401. {
  1402. EnsureDataInitialized();
  1403. _onlineQualificationLastUpdateAt.Clear();
  1404.  
  1405. if (!UsesOnlineQualification())
  1406. {
  1407. _storedData.OnlineQualificationMinutes.Clear();
  1408. _storedData.OnlineQualificationActiveSessions.Clear();
  1409. return;
  1410. }
  1411.  
  1412. var activeIds = new HashSet<ulong>();
  1413. foreach (var player in BasePlayer.activePlayerList)
  1414. {
  1415. if (player != null && player.IsConnected && player.userID.IsSteamId()) activeIds.Add(player.userID);
  1416. }
  1417.  
  1418. _storedData.OnlineQualificationActiveSessions.RemoveWhere(userId => !activeIds.Contains(userId));
  1419. var now = DateTime.Now;
  1420. foreach (var userId in activeIds)
  1421. {
  1422. if (!_storedData.OnlineQualificationActiveSessions.Contains(userId) || !_storedData.OnlineQualificationMinutes.ContainsKey(userId))
  1423. _storedData.OnlineQualificationMinutes[userId] = 0d;
  1424. _storedData.OnlineQualificationActiveSessions.Add(userId);
  1425. _onlineQualificationLastUpdateAt[userId] = now;
  1426. InvalidateDamageScale(userId);
  1427. }
  1428. }
  1429.  
  1430. private void StartOnlineQualificationSession(ulong userId, bool reset)
  1431. {
  1432. if (!UsesOnlineQualification() || !userId.IsSteamId()) return;
  1433. EnsureDataInitialized();
  1434. if (reset || !_storedData.OnlineQualificationMinutes.ContainsKey(userId)) _storedData.OnlineQualificationMinutes[userId] = 0d;
  1435. _storedData.OnlineQualificationActiveSessions.Add(userId);
  1436. _onlineQualificationLastUpdateAt[userId] = DateTime.Now;
  1437. InvalidateDamageScale(userId);
  1438. }
  1439.  
  1440. private void UpdateOnlineQualification(BasePlayer player, LastOnline lastOnline = null)
  1441. {
  1442. if (!UsesOnlineQualification() || player == null || !player.userID.IsSteamId()) return;
  1443. EnsureDataInitialized();
  1444. if (!_storedData.OnlineQualificationActiveSessions.Contains(player.userID)) return;
  1445.  
  1446. var now = DateTime.Now;
  1447. if (!_onlineQualificationLastUpdateAt.TryGetValue(player.userID, out var lastUpdate))
  1448. {
  1449. _onlineQualificationLastUpdateAt[player.userID] = now;
  1450. return;
  1451. }
  1452.  
  1453. _onlineQualificationLastUpdateAt[player.userID] = now;
  1454. if (now <= lastUpdate) return;
  1455.  
  1456. if (lastOnline == null) _storedData.LastOnlines.TryGetValue(player.userID, out lastOnline);
  1457. var countUntil = now;
  1458. if (_config.TreatAfkAsOffline && lastOnline != null)
  1459. {
  1460. var afkAt = lastOnline.LastOnlineTime.AddMinutes(_config.AfkMinutes);
  1461. if (afkAt < countUntil) countUntil = afkAt;
  1462. }
  1463.  
  1464. if (countUntil <= lastUpdate) return;
  1465. _storedData.OnlineQualificationMinutes.TryGetValue(player.userID, out var previousMinutes);
  1466. var updatedMinutes = previousMinutes + (countUntil - lastUpdate).TotalMinutes;
  1467. _storedData.OnlineQualificationMinutes[player.userID] = Math.Max(0d, updatedMinutes);
  1468. if (previousMinutes < _config.MinutesOnlineToQualify && updatedMinutes >= _config.MinutesOnlineToQualify) InvalidateDamageScale(player.userID);
  1469. }
  1470.  
  1471. private void EndOnlineQualificationSession(ulong userId)
  1472. {
  1473. _onlineQualificationLastUpdateAt.Remove(userId);
  1474. _storedData?.OnlineQualificationActiveSessions?.Remove(userId);
  1475. InvalidateDamageScale(userId);
  1476. }
  1477.  
  1478. private bool IsProtectionQualified(ulong userId)
  1479. {
  1480. if (!UsesOnlineQualification()) return true;
  1481. if (!userId.IsSteamId()) return false;
  1482. EnsureDataInitialized();
  1483. return _storedData.OnlineQualificationMinutes.TryGetValue(userId, out var minutes) && minutes >= _config.MinutesOnlineToQualify;
  1484. }
  1485.  
  1486. protected IPlayer FindPlayerByPartialName(string nameOrIdOrIp)
  1487. {
  1488. return string.IsNullOrWhiteSpace(nameOrIdOrIp) ? null : covalence.Players.FindPlayer(nameOrIdOrIp);
  1489. }
  1490.  
  1491. public List<string> GetClansPluginMembers(string tag)
  1492. {
  1493. if (string.IsNullOrWhiteSpace(tag) || !IsClansPluginAvailable()) return new List<string>();
  1494. if (_storedData.MembersCache.TryGetValue(tag, out var memberList) && memberList != null) return memberList;
  1495. return CacheClansPluginClan(tag);
  1496. }
  1497.  
  1498. private HashSet<ulong> GetRustTeamMembers(ulong targetId)
  1499. {
  1500. var members = new HashSet<ulong>();
  1501. if (!targetId.IsSteamId()) return members;
  1502. members.Add(targetId);
  1503.  
  1504. try
  1505. {
  1506. var team = RelationshipManager.ServerInstance?.FindPlayersTeam(targetId);
  1507. if (team?.members == null) return members;
  1508. foreach (var memberId in team.members)
  1509. {
  1510. if (memberId.IsSteamId()) members.Add(memberId);
  1511. }
  1512. }
  1513. catch
  1514. {
  1515. }
  1516.  
  1517. return members;
  1518. }
  1519.  
  1520. private HashSet<ulong> GetRustClanMembers(ulong targetId)
  1521. {
  1522. var members = new HashSet<ulong>();
  1523. if (!targetId.IsSteamId()) return members;
  1524. members.Add(targetId);
  1525.  
  1526. try
  1527. {
  1528. var player = BasePlayer.FindByID(targetId) ?? BasePlayer.FindSleeping(targetId);
  1529. var clanId = player?.clanId ?? 0L;
  1530. if (clanId == 0L) return members;
  1531.  
  1532. IClan clan;
  1533. if (!(ClanManager.ServerInstance?.Backend?.TryGet(clanId, out clan) ?? false) || clan?.Members == null) return members;
  1534. foreach (var member in clan.Members)
  1535. {
  1536. if (member.SteamId.IsSteamId()) members.Add(member.SteamId);
  1537. }
  1538. }
  1539. catch (Exception ex)
  1540. {
  1541. PrintWarning($"Unable to query Rust native clan for {targetId}: {ex.Message}");
  1542. }
  1543.  
  1544. return members;
  1545. }
  1546.  
  1547. private HashSet<ulong> GetClansPluginMemberIds(ulong targetId)
  1548. {
  1549. var members = new HashSet<ulong>();
  1550. if (!targetId.IsSteamId()) return members;
  1551. members.Add(targetId);
  1552. if (!IsClansPluginAvailable()) return members;
  1553.  
  1554. var tag = GetClansPluginTag(targetId);
  1555. if (string.IsNullOrEmpty(tag)) return members;
  1556. foreach (var memberId in GetClansPluginMembers(tag))
  1557. {
  1558. if (ulong.TryParse(memberId, out var userId) && userId.IsSteamId()) members.Add(userId);
  1559. }
  1560. return members;
  1561. }
  1562.  
  1563. private bool UsesSharedProtectionGroups()
  1564. {
  1565. return _config != null && (_config.ClansPluginShare || _config.RustTeamShare || _config.RustClanShare);
  1566. }
  1567.  
  1568. private HashSet<ulong> GetSharedProtectionMembers(ulong targetId)
  1569. {
  1570. var members = new HashSet<ulong>();
  1571. if (!targetId.IsSteamId()) return members;
  1572. members.Add(targetId);
  1573.  
  1574. if (_config.ClansPluginShare && IsClansPluginAvailable()) members.UnionWith(GetClansPluginMemberIds(targetId));
  1575. if (_config.RustTeamShare) members.UnionWith(GetRustTeamMembers(targetId));
  1576. if (_config.RustClanShare) members.UnionWith(GetRustClanMembers(targetId));
  1577. return members;
  1578. }
  1579.  
  1580. private bool TryEvaluateSharedProtectionProvider(HashSet<ulong> members, int minOnlineMembers, out LastOnline selected, out bool groupOnline)
  1581. {
  1582. selected = null;
  1583. groupOnline = false;
  1584. if (members == null || members.Count == 0) return false;
  1585.  
  1586. minOnlineMembers = Math.Max(1, minOnlineMembers);
  1587. var onlineCount = 0;
  1588. foreach (var userId in members)
  1589. {
  1590. if (IsOffline(userId)) continue;
  1591. onlineCount++;
  1592. if (onlineCount < minOnlineMembers) continue;
  1593. groupOnline = true;
  1594. return true;
  1595. }
  1596.  
  1597. foreach (var userId in members)
  1598. {
  1599. if (!IsProtectionQualified(userId)) continue;
  1600. if (!_storedData.LastOnlines.TryGetValue(userId, out var lastOnline) || lastOnline == null || !lastOnline.IsOffline()) continue;
  1601. if (selected == null || lastOnline.LastOnlineTime > selected.LastOnlineTime) selected = lastOnline;
  1602. }
  1603.  
  1604. // 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.
  1605. return true;
  1606. }
  1607.  
  1608. private void AddSharedProtectionCandidate(HashSet<ulong> members, int minOnlineMembers, List<LastOnline> candidates, ref bool anyGroupOnline)
  1609. {
  1610. if (!TryEvaluateSharedProtectionProvider(members, minOnlineMembers, out var selected, out var groupOnline)) return;
  1611. if (groupOnline)
  1612. {
  1613. anyGroupOnline = true;
  1614. return;
  1615. }
  1616. if (selected != null) candidates.Add(selected);
  1617. }
  1618.  
  1619. private bool TryGetProtectionLastOnline(ulong targetId, out LastOnline protectionLastOnline)
  1620. {
  1621. protectionLastOnline = null;
  1622. if (!_storedData.LastOnlines.TryGetValue(targetId, out var ownerLastOnline) || ownerLastOnline == null) return false;
  1623.  
  1624. if (!UsesSharedProtectionGroups())
  1625. {
  1626. protectionLastOnline = ownerLastOnline;
  1627. return true;
  1628. }
  1629.  
  1630. var candidates = Pool.Get<List<LastOnline>>();
  1631. try
  1632. {
  1633. candidates.Clear();
  1634. var anyGroupOnline = false;
  1635.  
  1636. if (_config.ClansPluginShare && IsClansPluginAvailable())
  1637. AddSharedProtectionCandidate(GetClansPluginMemberIds(targetId), _config.ClansPluginMinMembers, candidates, ref anyGroupOnline);
  1638. if (_config.RustTeamShare)
  1639. AddSharedProtectionCandidate(GetRustTeamMembers(targetId), _config.RustTeamMinMembers, candidates, ref anyGroupOnline);
  1640. if (_config.RustClanShare)
  1641. AddSharedProtectionCandidate(GetRustClanMembers(targetId), _config.RustClanMinMembers, candidates, ref anyGroupOnline);
  1642.  
  1643. if (anyGroupOnline) return false;
  1644. if (candidates.Count == 0)
  1645. {
  1646. protectionLastOnline = ownerLastOnline;
  1647. return true;
  1648. }
  1649.  
  1650. // When multiple share providers apply, use the most recently offline selected member to avoid granting stronger protection than any enabled provider allows.
  1651. protectionLastOnline = candidates[0];
  1652. for (var i = 1; i < candidates.Count; i++)
  1653. {
  1654. if (candidates[i].LastOnlineTime > protectionLastOnline.LastOnlineTime) protectionLastOnline = candidates[i];
  1655. }
  1656. return true;
  1657. }
  1658. finally
  1659. {
  1660. Pool.FreeUnmanaged(ref candidates);
  1661. }
  1662. }
  1663.  
  1664. private HashSet<ulong> GetAuthorizedClansPluginMembers(IEnumerable<ulong> authorizedIds)
  1665. {
  1666. var members = new HashSet<ulong>();
  1667. if (!_config.ClansPluginShare || !IsClansPluginAvailable() || authorizedIds == null) return members;
  1668.  
  1669. foreach (var userId in authorizedIds)
  1670. {
  1671. if (!userId.IsSteamId()) continue;
  1672. members.UnionWith(GetClansPluginMemberIds(userId));
  1673. }
  1674. return members;
  1675. }
  1676.  
  1677. private HashSet<ulong> GetAuthorizedRustTeamMembers(IEnumerable<ulong> authorizedIds)
  1678. {
  1679. var members = new HashSet<ulong>();
  1680. if (!_config.RustTeamShare || authorizedIds == null) return members;
  1681.  
  1682. foreach (var userId in authorizedIds)
  1683. {
  1684. if (!userId.IsSteamId()) continue;
  1685. members.UnionWith(GetRustTeamMembers(userId));
  1686. }
  1687. return members;
  1688. }
  1689.  
  1690. private HashSet<ulong> GetAuthorizedRustClanMembers(IEnumerable<ulong> authorizedIds)
  1691. {
  1692. var members = new HashSet<ulong>();
  1693. if (!_config.RustClanShare || authorizedIds == null) return members;
  1694.  
  1695. foreach (var userId in authorizedIds)
  1696. {
  1697. if (!userId.IsSteamId()) continue;
  1698. members.UnionWith(GetRustClanMembers(userId));
  1699. }
  1700. return members;
  1701. }
  1702.  
  1703. private void AddProtectionLastOnlineCandidates(IEnumerable<ulong> userIds, List<LastOnline> candidates)
  1704. {
  1705. if (userIds == null || candidates == null) return;
  1706.  
  1707. foreach (var userId in userIds)
  1708. {
  1709. if (!userId.IsSteamId() || !HasPerm(userId.ToString(), PluginPermissions.Protect) || !IsProtectionQualified(userId)) continue;
  1710. if (!_storedData.LastOnlines.TryGetValue(userId, out var lastOnline) || lastOnline == null || !lastOnline.IsOffline()) continue;
  1711. var exists = false;
  1712. for (var i = 0; i < candidates.Count; i++)
  1713. {
  1714. if (candidates[i].UserId != userId) continue;
  1715. exists = true;
  1716. break;
  1717. }
  1718. if (!exists) candidates.Add(lastOnline);
  1719. }
  1720. }
  1721.  
  1722. private bool CountOnlineMembersAtLeast(IEnumerable<ulong> members, int minimum)
  1723. {
  1724. if (members == null) return false;
  1725. minimum = Math.Max(1, minimum);
  1726. var count = 0;
  1727. foreach (var userId in members)
  1728. {
  1729. if (IsOffline(userId)) continue;
  1730. count++;
  1731. if (count >= minimum) return true;
  1732. }
  1733. return false;
  1734. }
  1735.  
  1736. private bool TryResolveDirectPrivilegeProtectionSource(List<ulong> authorizedIds, out ulong targetId, out LastOnline protectionLastOnline, out bool groupOnline)
  1737. {
  1738. targetId = 0UL;
  1739. protectionLastOnline = null;
  1740. groupOnline = false;
  1741. if (authorizedIds == null || authorizedIds.Count == 0) return false;
  1742. EnsureDataInitialized();
  1743.  
  1744. LastOnline mostRecentOffline = null;
  1745. var hasQualifiedFallback = false;
  1746. foreach (var userId in authorizedIds)
  1747. {
  1748. if (!userId.IsSteamId()) continue;
  1749. _storedData.LastOnlines.TryGetValue(userId, out var lastOnline);
  1750. var offline = lastOnline != null ? lastOnline.IsOffline() : IsOffline(userId);
  1751. if (!offline) groupOnline = true;
  1752. if (lastOnline == null || !HasPerm(userId.ToString(), PluginPermissions.Protect)) continue;
  1753.  
  1754. var qualified = IsProtectionQualified(userId);
  1755. if (protectionLastOnline == null || (qualified && !hasQualifiedFallback))
  1756. {
  1757. targetId = userId;
  1758. protectionLastOnline = lastOnline;
  1759. hasQualifiedFallback = qualified;
  1760. }
  1761.  
  1762. if (!qualified || !offline) continue;
  1763. if (mostRecentOffline == null || lastOnline.LastOnlineTime > mostRecentOffline.LastOnlineTime) mostRecentOffline = lastOnline;
  1764. }
  1765.  
  1766. if (mostRecentOffline != null)
  1767. {
  1768. protectionLastOnline = mostRecentOffline;
  1769. targetId = mostRecentOffline.UserId;
  1770. }
  1771. return protectionLastOnline != null;
  1772. }
  1773.  
  1774. private bool TryResolvePrivilegeProtectionSource(List<ulong> authorizedIds, out ulong targetId, out LastOnline protectionLastOnline, out bool groupOnline)
  1775. {
  1776. targetId = 0UL;
  1777. protectionLastOnline = null;
  1778. groupOnline = false;
  1779. if (authorizedIds == null || authorizedIds.Count == 0) return false;
  1780. if (!UsesSharedProtectionGroups()) return TryResolveDirectPrivilegeProtectionSource(authorizedIds, out targetId, out protectionLastOnline, out groupOnline);
  1781.  
  1782. var directMembers = new HashSet<ulong>();
  1783. foreach (var userId in authorizedIds)
  1784. {
  1785. if (userId.IsSteamId()) directMembers.Add(userId);
  1786. }
  1787. if (directMembers.Count == 0) return false;
  1788.  
  1789. foreach (var userId in directMembers)
  1790. {
  1791. if (!IsOffline(userId))
  1792. {
  1793. groupOnline = true;
  1794. break;
  1795. }
  1796. }
  1797.  
  1798. var candidates = Pool.Get<List<LastOnline>>();
  1799. try
  1800. {
  1801. candidates.Clear();
  1802. AddProtectionLastOnlineCandidates(directMembers, candidates);
  1803.  
  1804. HashSet<ulong> clansMembers = null;
  1805. if (_config.ClansPluginShare && IsClansPluginAvailable())
  1806. {
  1807. clansMembers = GetAuthorizedClansPluginMembers(directMembers);
  1808. if (CountOnlineMembersAtLeast(clansMembers, _config.ClansPluginMinMembers)) groupOnline = true;
  1809. AddProtectionLastOnlineCandidates(clansMembers, candidates);
  1810. }
  1811.  
  1812. HashSet<ulong> teamMembers = null;
  1813. if (_config.RustTeamShare)
  1814. {
  1815. teamMembers = GetAuthorizedRustTeamMembers(directMembers);
  1816. if (CountOnlineMembersAtLeast(teamMembers, _config.RustTeamMinMembers)) groupOnline = true;
  1817. AddProtectionLastOnlineCandidates(teamMembers, candidates);
  1818. }
  1819.  
  1820. HashSet<ulong> rustClanMembers = null;
  1821. if (_config.RustClanShare)
  1822. {
  1823. rustClanMembers = GetAuthorizedRustClanMembers(directMembers);
  1824. if (CountOnlineMembersAtLeast(rustClanMembers, _config.RustClanMinMembers)) groupOnline = true;
  1825. AddProtectionLastOnlineCandidates(rustClanMembers, candidates);
  1826. }
  1827.  
  1828. if (candidates.Count > 0)
  1829. {
  1830. protectionLastOnline = candidates[0];
  1831. for (var i = 1; i < candidates.Count; i++)
  1832. {
  1833. if (candidates[i].LastOnlineTime > protectionLastOnline.LastOnlineTime) protectionLastOnline = candidates[i];
  1834. }
  1835. targetId = protectionLastOnline.UserId;
  1836. return targetId.IsSteamId();
  1837. }
  1838.  
  1839. var qualificationSources = new HashSet<ulong>(directMembers);
  1840. if (clansMembers != null) qualificationSources.UnionWith(clansMembers);
  1841. if (teamMembers != null) qualificationSources.UnionWith(teamMembers);
  1842. if (rustClanMembers != null) qualificationSources.UnionWith(rustClanMembers);
  1843. foreach (var userId in qualificationSources)
  1844. {
  1845. if (!HasPerm(userId.ToString(), PluginPermissions.Protect) || !IsProtectionQualified(userId)) continue;
  1846. if (!_storedData.LastOnlines.TryGetValue(userId, out var lastOnline) || lastOnline == null) continue;
  1847. targetId = userId;
  1848. protectionLastOnline = lastOnline;
  1849. return true;
  1850. }
  1851.  
  1852. foreach (var userId in directMembers)
  1853. {
  1854. if (!HasPerm(userId.ToString(), PluginPermissions.Protect)) continue;
  1855. if (!_storedData.LastOnlines.TryGetValue(userId, out var lastOnline) || lastOnline == null) continue;
  1856. targetId = userId;
  1857. protectionLastOnline = lastOnline;
  1858. return true;
  1859. }
  1860.  
  1861. return false;
  1862. }
  1863. finally
  1864. {
  1865. Pool.FreeUnmanaged(ref candidates);
  1866. }
  1867. }
  1868.  
  1869. private HashSet<ulong> GetPrivilegeAccessMembers(IEnumerable<ulong> authorizedIds)
  1870. {
  1871. var seeds = new HashSet<ulong>();
  1872. if (authorizedIds == null) return seeds;
  1873.  
  1874. foreach (var userId in authorizedIds)
  1875. if (userId.IsSteamId()) seeds.Add(userId);
  1876.  
  1877. var members = new HashSet<ulong>(seeds);
  1878. if (_config.ClansPluginShare && IsClansPluginAvailable()) members.UnionWith(GetAuthorizedClansPluginMembers(seeds));
  1879. if (_config.RustTeamShare) members.UnionWith(GetAuthorizedRustTeamMembers(seeds));
  1880. if (_config.RustClanShare) members.UnionWith(GetAuthorizedRustClanMembers(seeds));
  1881. return members;
  1882. }
  1883.  
  1884. private bool HasPrivilegeProtectionAccess(ulong viewerId, IEnumerable<ulong> authorizedIds)
  1885. {
  1886. if (!viewerId.IsSteamId() || authorizedIds == null) return false;
  1887. if (!UsesSharedProtectionGroups())
  1888. {
  1889. foreach (var userId in authorizedIds)
  1890. if (userId == viewerId) return true;
  1891. return false;
  1892. }
  1893. return GetPrivilegeAccessMembers(authorizedIds).Contains(viewerId);
  1894. }
  1895.  
  1896. private bool IsPrivilegeGroupInBuildingRange(IEnumerable<ulong> authorizedIds, BuildingPrivlidge privilege)
  1897. {
  1898. if (privilege == null || authorizedIds == null) return false;
  1899. if (!UsesSharedProtectionGroups())
  1900. {
  1901. foreach (var userId in authorizedIds)
  1902. if (userId.IsSteamId() && IsPlayerNearBuildingPrivilege(userId, privilege)) return true;
  1903. return false;
  1904. }
  1905. foreach (var userId in GetPrivilegeAccessMembers(authorizedIds))
  1906. if (IsPlayerNearBuildingPrivilege(userId, privilege)) return true;
  1907. return false;
  1908. }
  1909.  
  1910. private bool IsPrivilegeGroupNearEntity(IEnumerable<ulong> authorizedIds, BaseEntity entity)
  1911. {
  1912. if (entity == null || authorizedIds == null) return false;
  1913. if (!UsesSharedProtectionGroups())
  1914. {
  1915. foreach (var userId in authorizedIds)
  1916. {
  1917. if (!userId.IsSteamId()) continue;
  1918. var player = BasePlayer.FindByID(userId);
  1919. if (player != null && player.IsConnected && Vector3.Distance(player.transform.position, entity.transform.position) <= NavalPresenceRadius) return true;
  1920. }
  1921. return false;
  1922. }
  1923. foreach (var userId in GetPrivilegeAccessMembers(authorizedIds))
  1924. {
  1925. var player = BasePlayer.FindByID(userId);
  1926. if (player != null && player.IsConnected && Vector3.Distance(player.transform.position, entity.transform.position) <= NavalPresenceRadius) return true;
  1927. }
  1928. return false;
  1929. }
  1930.  
  1931. private static ulong GetLockOwnerId(BaseEntity entity)
  1932. {
  1933. var lockEntity = entity?.GetSlot(BaseEntity.Slot.Lock);
  1934. return lockEntity != null && lockEntity.OwnerID.IsSteamId() ? lockEntity.OwnerID : 0UL;
  1935. }
  1936.  
  1937. private bool TryResolveEntityFallbackProtectionSource(BaseEntity entity, out ulong targetId, out LastOnline protectionLastOnline, out bool groupOnline)
  1938. {
  1939. targetId = 0UL;
  1940. protectionLastOnline = null;
  1941. groupOnline = false;
  1942. if (entity == null) return false;
  1943.  
  1944. var lockOwnerId = GetLockOwnerId(entity);
  1945. if (lockOwnerId.IsSteamId() && TryResolveFallbackProtectionSource(lockOwnerId, out targetId, out protectionLastOnline, out groupOnline)) return true;
  1946. if (entity.OwnerID.IsSteamId() && entity.OwnerID != lockOwnerId && TryResolveFallbackProtectionSource(entity.OwnerID, out targetId, out protectionLastOnline, out groupOnline)) return true;
  1947. return false;
  1948. }
  1949.  
  1950. private bool TryResolveNavalFallbackProtectionSource(BaseEntity attackedEntity, BaseEntity navalTarget, VehiclePrivilege privilege, out ulong targetId, out LastOnline protectionLastOnline, out bool groupOnline)
  1951. {
  1952. if (TryResolveEntityFallbackProtectionSource(attackedEntity, out targetId, out protectionLastOnline, out groupOnline)) return true;
  1953. if (privilege != null && privilege.OwnerID.IsSteamId() && TryResolveFallbackProtectionSource(privilege.OwnerID, out targetId, out protectionLastOnline, out groupOnline)) return true;
  1954. if (navalTarget != null && navalTarget.OwnerID.IsSteamId() && TryResolveFallbackProtectionSource(navalTarget.OwnerID, out targetId, out protectionLastOnline, out groupOnline)) return true;
  1955.  
  1956. targetId = 0UL;
  1957. protectionLastOnline = null;
  1958. groupOnline = false;
  1959. return false;
  1960. }
  1961.  
  1962. private bool HasFallbackProtectionAccess(ulong viewerId, ulong targetId)
  1963. {
  1964. if (!viewerId.IsSteamId() || !targetId.IsSteamId()) return false;
  1965. if (viewerId == targetId) return true;
  1966. return UsesSharedProtectionGroups() && GetSharedProtectionMembers(targetId).Contains(viewerId);
  1967. }
  1968.  
  1969. private int GetProtectionGroupSize(ulong targetId, IEnumerable<ulong> authorizedIds = null)
  1970. {
  1971. if (!_config.GroupSizeProtectionScaling) return 1;
  1972.  
  1973. if (!_config.GroupSizeUseRustTeams && !_config.GroupSizeUseClansPluginMembers && !_config.GroupSizeUseRustClans)
  1974. {
  1975. if (authorizedIds == null) return 1;
  1976. var authorizedCount = 0;
  1977. foreach (var userId in authorizedIds)
  1978. {
  1979. if (userId.IsSteamId()) authorizedCount++;
  1980. }
  1981. return Math.Max(1, authorizedCount);
  1982. }
  1983.  
  1984. var relevantUsers = Pool.Get<List<ulong>>();
  1985. var authorizedUsers = Pool.Get<List<ulong>>();
  1986. try
  1987. {
  1988. relevantUsers.Clear();
  1989. authorizedUsers.Clear();
  1990. if (targetId.IsSteamId()) relevantUsers.Add(targetId);
  1991. if (authorizedIds != null)
  1992. {
  1993. foreach (var userId in authorizedIds)
  1994. {
  1995. if (!userId.IsSteamId()) continue;
  1996. if (!authorizedUsers.Contains(userId)) authorizedUsers.Add(userId);
  1997. if (!relevantUsers.Contains(userId)) relevantUsers.Add(userId);
  1998. }
  1999. }
  2000.  
  2001. // TC / vehicle privilege authorization is always the baseline when group-size scaling is enabled.
  2002. var groupSize = Math.Max(1, authorizedUsers.Count);
  2003.  
  2004. if (_config.GroupSizeUseRustTeams)
  2005. {
  2006. foreach (var userId in relevantUsers)
  2007. groupSize = Math.Max(groupSize, GetRustTeamMembers(userId).Count);
  2008. }
  2009.  
  2010. if (_config.GroupSizeUseClansPluginMembers && IsClansPluginAvailable())
  2011. {
  2012. foreach (var userId in relevantUsers)
  2013. groupSize = Math.Max(groupSize, GetClansPluginMemberIds(userId).Count);
  2014. }
  2015.  
  2016. if (_config.GroupSizeUseRustClans)
  2017. {
  2018. foreach (var userId in relevantUsers)
  2019. groupSize = Math.Max(groupSize, GetRustClanMembers(userId).Count);
  2020. }
  2021.  
  2022. return Math.Max(1, groupSize);
  2023. }
  2024. finally
  2025. {
  2026. Pool.FreeUnmanaged(ref authorizedUsers);
  2027. Pool.FreeUnmanaged(ref relevantUsers);
  2028. }
  2029. }
  2030.  
  2031. private float ApplyGroupSizeProtectionScaling(float damageScale, int groupSize)
  2032. {
  2033. if (!_config.GroupSizeProtectionScaling || damageScale < 0f || damageScale >= 1f || groupSize <= 1) return damageScale;
  2034. if (_config.GroupSizeProtectionKeys == null || _config.GroupSizeProtectionKeys.Count == 0) return damageScale;
  2035.  
  2036. var threshold = -1;
  2037. for (var i = 0; i < _config.GroupSizeProtectionKeys.Count; i++)
  2038. {
  2039. var key = _config.GroupSizeProtectionKeys[i];
  2040. if (key > groupSize) break;
  2041. threshold = key;
  2042. }
  2043. if (threshold < 0 || !_config.GroupSizeProtectionScale.TryGetValue(threshold.ToString(), out var protectionFactor)) return damageScale;
  2044. protectionFactor = Mathf.Clamp01(protectionFactor);
  2045.  
  2046. var baseProtection = 1f - damageScale;
  2047. var adjustedProtection = baseProtection * protectionFactor;
  2048. return Mathf.Clamp01(1f - adjustedProtection);
  2049. }
  2050.  
  2051. private void ResetSimpleStatusIcon()
  2052. {
  2053. _simpleStatusIconRegistered = false;
  2054. _simpleStatusIconRegistrationPending = false;
  2055. _simpleStatusIconPendingKey = null;
  2056. _resolvedSimpleStatusIcon = null;
  2057. }
  2058.  
  2059. private string NormalizeSimpleStatusColor(string hex)
  2060. {
  2061. if (string.IsNullOrWhiteSpace(hex)) return "1 1 1 1";
  2062. if (_simpleStatusColorCache.TryGetValue(hex, out var value)) return value;
  2063. var normalized = hex.Trim();
  2064. if (!normalized.StartsWith("#", StringComparison.Ordinal)) normalized = "#" + normalized;
  2065.  
  2066. if (!ColorUtility.TryParseHtmlString(normalized, out var color)) return "1 1 1 1";
  2067. color.a = 1f;
  2068. value = string.Format(CultureInfo.InvariantCulture, "{0:0.###} {1:0.###} {2:0.###} {3:0.###}", color.r, color.g, color.b, color.a);
  2069. if (_simpleStatusColorCache.Count < SimpleStatusColorCacheLimit) _simpleStatusColorCache[hex] = value;
  2070. return value;
  2071. }
  2072.  
  2073. private static string GetSimpleStatusIconKey(string source)
  2074. {
  2075. unchecked
  2076. {
  2077. uint hash = 2166136261;
  2078. foreach (var character in source ?? string.Empty)
  2079. {
  2080. hash ^= character;
  2081. hash *= 16777619;
  2082. }
  2083. return $"{OfflineProtectionStatusIconKey}_{hash:X8}";
  2084. }
  2085. }
  2086.  
  2087. private bool HasImageLibraryImage(string imageKey)
  2088. {
  2089. if (ImageLibrary == null || string.IsNullOrWhiteSpace(imageKey)) return false;
  2090. try
  2091. {
  2092. return ImageLibrary.Call<bool>("HasImage", imageKey, 0UL);
  2093. }
  2094. catch
  2095. {
  2096. return false;
  2097. }
  2098. }
  2099.  
  2100. private void WaitForSimpleStatusIcon(string imageKey, int attemptsRemaining)
  2101. {
  2102. if (!_simpleStatusIconRegistrationPending || !string.Equals(_simpleStatusIconPendingKey, imageKey, StringComparison.Ordinal) || ImageLibrary == null) return;
  2103. if (HasImageLibraryImage(imageKey))
  2104. {
  2105. _simpleStatusIconRegistrationPending = false;
  2106. _simpleStatusIconPendingKey = null;
  2107. _simpleStatusIconRegistered = true;
  2108. _resolvedSimpleStatusIcon = imageKey;
  2109. NextTick(RefreshAllSimpleStatuses);
  2110. return;
  2111. }
  2112.  
  2113. if (attemptsRemaining <= 0)
  2114. {
  2115. _simpleStatusIconRegistrationPending = false;
  2116. _simpleStatusIconPendingKey = null;
  2117. PrintWarning("Unable to register the configured SimpleStatus icon in ImageLibrary.");
  2118. return;
  2119. }
  2120.  
  2121. timer.Once(SimpleStatusIconCheckIntervalSeconds, () => WaitForSimpleStatusIcon(imageKey, attemptsRemaining - 1));
  2122. }
  2123.  
  2124. private void BeginSimpleStatusIconRegistration(string imageKey, Func<bool> register)
  2125. {
  2126. if (string.IsNullOrWhiteSpace(imageKey) || register == null) return;
  2127. _simpleStatusIconRegistrationPending = true;
  2128. _simpleStatusIconPendingKey = imageKey;
  2129.  
  2130. var started = false;
  2131. try
  2132. {
  2133. started = register();
  2134. }
  2135. catch (Exception ex)
  2136. {
  2137. PrintWarning($"Unable to register SimpleStatus icon: {ex.Message}");
  2138. }
  2139.  
  2140. if (!started)
  2141. {
  2142. _simpleStatusIconRegistrationPending = false;
  2143. _simpleStatusIconPendingKey = null;
  2144. PrintWarning("ImageLibrary rejected the configured SimpleStatus icon registration request.");
  2145. return;
  2146. }
  2147.  
  2148. WaitForSimpleStatusIcon(imageKey, SimpleStatusIconCheckAttempts);
  2149. }
  2150.  
  2151. private void EnsureSimpleStatusIcon()
  2152. {
  2153. if (_simpleStatusIconRegistered || _simpleStatusIconRegistrationPending) return;
  2154. _resolvedSimpleStatusIcon = null;
  2155.  
  2156. var configuredIcon = _config?.SimpleStatusIcon;
  2157. if (string.IsNullOrWhiteSpace(configuredIcon)) return;
  2158. var icon = configuredIcon.Trim();
  2159.  
  2160. if (icon.StartsWith("assets/", StringComparison.OrdinalIgnoreCase) || icon.StartsWith("itemid:", StringComparison.OrdinalIgnoreCase) || icon.StartsWith("raw:", StringComparison.OrdinalIgnoreCase))
  2161. {
  2162. _resolvedSimpleStatusIcon = icon;
  2163. _simpleStatusIconRegistered = true;
  2164. return;
  2165. }
  2166.  
  2167. if (ImageLibrary == null) return;
  2168. if (HasImageLibraryImage(icon))
  2169. {
  2170. _resolvedSimpleStatusIcon = icon;
  2171. _simpleStatusIconRegistered = true;
  2172. return;
  2173. }
  2174.  
  2175. if (icon.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || icon.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
  2176. {
  2177. var imageKey = GetSimpleStatusIconKey(icon);
  2178. if (HasImageLibraryImage(imageKey))
  2179. {
  2180. _resolvedSimpleStatusIcon = imageKey;
  2181. _simpleStatusIconRegistered = true;
  2182. return;
  2183. }
  2184.  
  2185. BeginSimpleStatusIconRegistration(imageKey, () => ImageLibrary != null && ImageLibrary.Call<bool>("AddImage", icon, imageKey, 0UL));
  2186. return;
  2187. }
  2188.  
  2189. var dataDir = Interface.Oxide.DataDirectory;
  2190. var baseDir = Path.Combine(dataDir, "AntiOfflineRaid", "images");
  2191. try { Directory.CreateDirectory(baseDir); } catch { }
  2192.  
  2193. string filePath;
  2194. if (Path.IsPathRooted(icon)) filePath = icon;
  2195. else if (icon.Contains("/") || icon.Contains("\\")) filePath = Path.Combine(dataDir, icon.TrimStart('/', '\\'));
  2196. else filePath = Path.Combine(baseDir, icon);
  2197.  
  2198. if (!File.Exists(filePath))
  2199. {
  2200. PrintWarning($"Configured SimpleStatus icon file was not found: {filePath}");
  2201. return;
  2202. }
  2203.  
  2204. try
  2205. {
  2206. var imageKey = GetSimpleStatusIconKey($"{filePath}|{File.GetLastWriteTimeUtc(filePath).Ticks}");
  2207. if (HasImageLibraryImage(imageKey))
  2208. {
  2209. _resolvedSimpleStatusIcon = imageKey;
  2210. _simpleStatusIconRegistered = true;
  2211. return;
  2212. }
  2213.  
  2214. var bytes = File.ReadAllBytes(filePath);
  2215. BeginSimpleStatusIconRegistration(imageKey, () => ImageLibrary != null && ImageLibrary.Call<bool>("AddImageData", imageKey, bytes, 0UL));
  2216. }
  2217. catch (Exception ex)
  2218. {
  2219. PrintWarning($"Unable to register local SimpleStatus icon: {ex.Message}");
  2220. }
  2221. }
  2222.  
  2223. private void RefreshLanguageWatchTimer()
  2224. {
  2225. _languageWatchTimer?.Destroy();
  2226. _languageWatchTimer = null;
  2227. if (_config?.SimpleStatusEnabled != true || SimpleStatus == null || !_simpleStatusCreated) return;
  2228.  
  2229. foreach (var player in BasePlayer.activePlayerList)
  2230. {
  2231. if (player == null || !player.userID.IsSteamId()) continue;
  2232. var language = lang.GetLanguage(player.UserIDString);
  2233. if (string.IsNullOrEmpty(language)) language = "en";
  2234. _lastLangByUser[player.userID] = language;
  2235. }
  2236.  
  2237. _languageWatchTimer = timer.Every(3f, () =>
  2238. {
  2239. if (_config?.SimpleStatusEnabled != true || SimpleStatus == null || !_simpleStatusCreated) return;
  2240.  
  2241. foreach (var player in BasePlayer.activePlayerList)
  2242. {
  2243. if (player == null || !player.userID.IsSteamId()) continue;
  2244.  
  2245. var language = lang.GetLanguage(player.UserIDString);
  2246. if (string.IsNullOrEmpty(language)) language = "en";
  2247. if (_lastLangByUser.TryGetValue(player.userID, out var previous) && string.Equals(previous, language, StringComparison.OrdinalIgnoreCase)) continue;
  2248.  
  2249. _lastLangByUser[player.userID] = language;
  2250. _simpleStatusStateKeys.Remove(player.userID);
  2251. QueueSimpleStatusRefresh(player, false);
  2252. }
  2253. });
  2254. }
  2255.  
  2256. private void RefreshSimpleStatusIntegration()
  2257. {
  2258. StopSimpleStatusIntegration(true);
  2259. if (!IsSimpleStatusIntegrationReady()) return;
  2260.  
  2261. EnsureSimpleStatusIcon();
  2262. var props = new Dictionary<string, object>
  2263. {
  2264. ["color"] = NormalizeSimpleStatusColor(_config.SimpleStatusAuthorizedBackground),
  2265. ["title"] = GetMsg(PluginMessages.OfflineProtectionStatus),
  2266. ["titleColor"] = NormalizeSimpleStatusColor(_config.SimpleStatusAuthorizedLabelColor),
  2267. ["text"] = "0%",
  2268. ["textColor"] = NormalizeSimpleStatusColor(_config.SimpleStatusAuthorizedValueColor),
  2269. ["icon"] = _resolvedSimpleStatusIcon,
  2270. ["iconColor"] = NormalizeSimpleStatusColor(_config.SimpleStatusAuthorizedIconColor)
  2271. };
  2272.  
  2273. try
  2274. {
  2275. SimpleStatus.Call("CreateStatus", this, _config.SimpleStatusStatusId, props);
  2276. _simpleStatusCreated = true;
  2277. }
  2278. catch (Exception ex)
  2279. {
  2280. _simpleStatusCreated = false;
  2281. PrintWarning($"Unable to create SimpleStatus offline protection status: {ex.Message}");
  2282. return;
  2283. }
  2284.  
  2285. _simpleStatusStateKeys.Clear();
  2286. _simpleStatusVisiblePlayers.Clear();
  2287. foreach (var player in BasePlayer.activePlayerList) HideSimpleStatus(player);
  2288.  
  2289. RefreshAllSimpleStatuses();
  2290. }
  2291.  
  2292. private void StopSimpleStatusIntegration(bool hideStatuses)
  2293. {
  2294. StopSimpleStatusScheduler();
  2295.  
  2296. if (hideStatuses && SimpleStatus != null && _simpleStatusCreated)
  2297. {
  2298. foreach (var player in BasePlayer.activePlayerList)
  2299. HideSimpleStatus(player);
  2300. }
  2301.  
  2302. _simpleStatusCreated = false;
  2303. _simpleStatusStateKeys.Clear();
  2304. _simpleStatusVisiblePlayers.Clear();
  2305. }
  2306.  
  2307. private void RefreshAllSimpleStatuses()
  2308. {
  2309. QueueSimpleStatusRefresh();
  2310. }
  2311.  
  2312. private void QueueSimpleStatusRefresh(BasePlayer player = null, bool refreshAll = true)
  2313. {
  2314. if (!IsSimpleStatusIntegrationReady() || !_simpleStatusCreated) return;
  2315. if (player != null && !player.IsDestroyed && player.IsConnected && player.userID.IsSteamId() && _simpleStatusPriorityPlayerSet.Add(player))
  2316. _simpleStatusPriorityPlayers.Enqueue(player);
  2317. if (refreshAll) _simpleStatusRefreshRequested = true;
  2318. if (_simpleStatusRefreshQueued) return;
  2319.  
  2320. _simpleStatusRefreshQueued = true;
  2321. var generation = _simpleStatusSchedulerGeneration;
  2322. NextTick(() =>
  2323. {
  2324. if (generation != _simpleStatusSchedulerGeneration) return;
  2325. _simpleStatusRefreshQueued = false;
  2326. ProcessSimpleStatusRefreshRequest();
  2327. });
  2328. }
  2329.  
  2330. private bool HasConnectedSimpleStatusPlayers()
  2331. {
  2332. foreach (var player in BasePlayer.activePlayerList)
  2333. {
  2334. if (player != null && !player.IsDestroyed && player.IsConnected && player.userID.IsSteamId()) return true;
  2335. }
  2336. return false;
  2337. }
  2338.  
  2339. private void ProcessSimpleStatusRefreshRequest()
  2340. {
  2341. if (!IsSimpleStatusIntegrationReady() || !_simpleStatusCreated) return;
  2342. if (!HasConnectedSimpleStatusPlayers())
  2343. {
  2344. StopSimpleStatusScheduler();
  2345. return;
  2346. }
  2347.  
  2348. var generation = _simpleStatusSchedulerGeneration;
  2349. RefreshSimpleStatusPriorityPlayers();
  2350. if (generation != _simpleStatusSchedulerGeneration || !IsSimpleStatusIntegrationReady() || !_simpleStatusCreated) return;
  2351. if (_languageWatchTimer == null || _languageWatchTimer.Destroyed) RefreshLanguageWatchTimer();
  2352.  
  2353. // A refresh request must not reset an unfinished cycle or postpone an already scheduled batch
  2354. if (_simpleStatusTimer == null || _simpleStatusTimer.Destroyed || _simpleStatusPriorityPlayers.Count > 0 || (_simpleStatusBatchPlayers.Count == 0 && _simpleStatusRefreshRequested))
  2355. ScheduleSimpleStatusBatch(SimpleStatusMinBatchDelaySeconds, true);
  2356. }
  2357.  
  2358. private void RefreshSimpleStatusPriorityPlayers()
  2359. {
  2360. var generation = _simpleStatusSchedulerGeneration;
  2361. var count = Math.Min(SimpleStatusPlayersPerBatch, _simpleStatusPriorityPlayers.Count);
  2362. for (var index = 0; index < count; index++)
  2363. {
  2364. var player = _simpleStatusPriorityPlayers.Dequeue();
  2365. _simpleStatusPriorityPlayerSet.Remove(player);
  2366. if (player != null && !player.IsDestroyed && player.IsConnected) UpdateSimpleStatusFor(player);
  2367. if (generation != _simpleStatusSchedulerGeneration || !IsSimpleStatusIntegrationReady() || !_simpleStatusCreated) return;
  2368. }
  2369. }
  2370.  
  2371. private void ScheduleSimpleStatusBatch(float delay, bool onlyEarlier = false)
  2372. {
  2373. if (!IsSimpleStatusIntegrationReady() || !_simpleStatusCreated) return;
  2374. delay = Mathf.Max(SimpleStatusMinBatchDelaySeconds, delay);
  2375. var now = Time.realtimeSinceStartup;
  2376. var nextRunAt = now + delay;
  2377. if (_simpleStatusTimer != null && !_simpleStatusTimer.Destroyed)
  2378. {
  2379. if (onlyEarlier && _simpleStatusNextRunAt <= nextRunAt) return;
  2380. if (!onlyEarlier && Mathf.Abs(_simpleStatusScheduledDelaySeconds - delay) < 0.0001f && now - _simpleStatusNextRunAt <= Mathf.Max(0.05f, delay))
  2381. {
  2382. _simpleStatusNextRunAt = nextRunAt;
  2383. return;
  2384. }
  2385. _simpleStatusTimer.Destroy();
  2386. }
  2387.  
  2388. // Reuse the repeating timer at steady population; restart it only when cadence changes or it falls behind
  2389. _simpleStatusScheduledDelaySeconds = delay;
  2390. _simpleStatusNextRunAt = nextRunAt;
  2391. _simpleStatusTimer = timer.Every(delay, RefreshSimpleStatusBatch);
  2392. }
  2393.  
  2394. private void StopSimpleStatusScheduler()
  2395. {
  2396. _simpleStatusTimer?.Destroy();
  2397. _simpleStatusTimer = null;
  2398. _languageWatchTimer?.Destroy();
  2399. _languageWatchTimer = null;
  2400. _simpleStatusSchedulerGeneration++;
  2401. _simpleStatusBatchPlayers.Clear();
  2402. _simpleStatusPriorityPlayers.Clear();
  2403. _simpleStatusPriorityPlayerSet.Clear();
  2404. _simpleStatusBatchCursor = 0;
  2405. _simpleStatusBatchSize = 0;
  2406. _simpleStatusBatchDelaySeconds = 0f;
  2407. _simpleStatusScheduledDelaySeconds = 0f;
  2408. _simpleStatusNextRunAt = 0f;
  2409. _simpleStatusRefreshRequested = false;
  2410. _simpleStatusRefreshQueued = false;
  2411. }
  2412.  
  2413. private void RefreshSimpleStatusBatch()
  2414. {
  2415. if (!IsSimpleStatusIntegrationReady() || !_simpleStatusCreated || BasePlayer.activePlayerList.Count == 0)
  2416. {
  2417. StopSimpleStatusScheduler();
  2418. return;
  2419. }
  2420.  
  2421. var generation = _simpleStatusSchedulerGeneration;
  2422. RefreshSimpleStatusPriorityPlayers();
  2423. if (generation != _simpleStatusSchedulerGeneration || !IsSimpleStatusIntegrationReady() || !_simpleStatusCreated) return;
  2424.  
  2425. var refreshInterval = Mathf.Max(0.25f, _config.SimpleStatusRefreshIntervalSeconds);
  2426. if (_simpleStatusBatchPlayers.Count == 0)
  2427. {
  2428. // Snapshot once per cycle so joins, leaves and delayed callbacks cannot move the cursor past players
  2429. foreach (var player in BasePlayer.activePlayerList)
  2430. {
  2431. if (player != null && !player.IsDestroyed && player.IsConnected && player.userID.IsSteamId()) _simpleStatusBatchPlayers.Add(player);
  2432. }
  2433. if (_simpleStatusBatchPlayers.Count == 0)
  2434. {
  2435. StopSimpleStatusScheduler();
  2436. return;
  2437. }
  2438.  
  2439. _simpleStatusBatchCursor = 0;
  2440. _simpleStatusRefreshRequested = false;
  2441. var batchCount = Math.Max(1, Mathf.CeilToInt(_simpleStatusBatchPlayers.Count / (float)SimpleStatusPlayersPerBatch));
  2442. _simpleStatusBatchSize = SimpleStatusPlayersPerBatch;
  2443. _simpleStatusBatchDelaySeconds = Mathf.Max(SimpleStatusMinBatchDelaySeconds, refreshInterval / batchCount);
  2444. }
  2445.  
  2446. var end = Math.Min(_simpleStatusBatchCursor + _simpleStatusBatchSize, _simpleStatusBatchPlayers.Count);
  2447. while (_simpleStatusBatchCursor < end)
  2448. {
  2449. var player = _simpleStatusBatchPlayers[_simpleStatusBatchCursor++];
  2450. if (player != null && !player.IsDestroyed && player.IsConnected) UpdateSimpleStatusFor(player);
  2451. if (generation != _simpleStatusSchedulerGeneration || !IsSimpleStatusIntegrationReady() || !_simpleStatusCreated) return;
  2452. }
  2453.  
  2454. if (_simpleStatusBatchCursor < _simpleStatusBatchPlayers.Count)
  2455. {
  2456. ScheduleSimpleStatusBatch(_simpleStatusBatchDelaySeconds);
  2457. return;
  2458. }
  2459.  
  2460. _simpleStatusBatchPlayers.Clear();
  2461. _simpleStatusBatchCursor = 0;
  2462. ScheduleSimpleStatusBatch(_simpleStatusBatchDelaySeconds);
  2463. }
  2464.  
  2465. private void HideSimpleStatus(BasePlayer player)
  2466. {
  2467. if (player == null) return;
  2468. _simpleStatusStateKeys.Remove(player.userID);
  2469. if (SimpleStatus == null || _config == null)
  2470. {
  2471. _simpleStatusVisiblePlayers.Remove(player.userID);
  2472. return;
  2473. }
  2474.  
  2475. try
  2476. {
  2477. SimpleStatus.Call("SetStatus", player.UserIDString, _config.SimpleStatusStatusId, 0);
  2478. _simpleStatusVisiblePlayers.Remove(player.userID);
  2479. }
  2480. catch
  2481. {
  2482. // Keep the visible marker so the next refresh retries clearing a potentially stale status.
  2483. }
  2484. }
  2485.  
  2486. private void HideSimpleStatusIfVisible(BasePlayer player)
  2487. {
  2488. if (player == null) return;
  2489. if (!_simpleStatusVisiblePlayers.Contains(player.userID) && !_simpleStatusStateKeys.ContainsKey(player.userID)) return;
  2490. HideSimpleStatus(player);
  2491. }
  2492.  
  2493. private bool IsHoldingHammer(BasePlayer player)
  2494. {
  2495. var activeItem = player?.GetActiveItem();
  2496. return activeItem?.info != null && string.Equals(activeItem.info.shortname, "hammer", StringComparison.OrdinalIgnoreCase);
  2497. }
  2498.  
  2499. private void AppendBuildingPrivilegeAuthorizedIds(BuildingPrivlidge privilege, List<ulong> result)
  2500. {
  2501. if (privilege?.authorizedPlayers == null || result == null) return;
  2502. foreach (var entry in privilege.authorizedPlayers)
  2503. {
  2504. var userId = GetAuthorizationEntryUserId(entry);
  2505. if (userId.IsSteamId() && !result.Contains(userId)) result.Add(userId);
  2506. }
  2507. }
  2508.  
  2509. private static ulong GetProtectionScopeId(BaseEntity contextEntity, ulong fallbackTargetId)
  2510. {
  2511. if (contextEntity?.net != null && contextEntity.net.ID.Value != 0UL) return contextEntity.net.ID.Value;
  2512. return fallbackTargetId;
  2513. }
  2514.  
  2515. private DateTime GetAfkOfflineSince(LastOnline lastOnline)
  2516. {
  2517. if (lastOnline == null) return DateTime.Now;
  2518. var offlineSince = lastOnline.LastOnlineTime.AddMinutes(_config.AfkMinutes);
  2519. return offlineSince > DateTime.Now ? DateTime.Now : offlineSince;
  2520. }
  2521.  
  2522. private DateTime GetOfflineSince(LastOnline lastOnline)
  2523. {
  2524. if (lastOnline == null) return DateTime.Now;
  2525. if (lastOnline.IsConnected() && _config.TreatAfkAsOffline && lastOnline.IsAfk()) return GetAfkOfflineSince(lastOnline);
  2526. return lastOnline.LastOnlineTime;
  2527. }
  2528.  
  2529. private bool HasProtectionCooldownElapsed(LastOnline lastOnline)
  2530. {
  2531. if (lastOnline == null || !lastOnline.IsOffline()) return false;
  2532. if (_config.CooldownMinutes <= 0) return true;
  2533. return (DateTime.Now - GetOfflineSince(lastOnline)).TotalMinutes >= _config.CooldownMinutes;
  2534. }
  2535.  
  2536. private float GetCurrentBaseProtectionDamageScale(LastOnline lastOnline)
  2537. {
  2538. if (lastOnline == null || !IsProtectionQualified(lastOnline.UserId) || !HasProtectionCooldownElapsed(lastOnline)) return -1f;
  2539.  
  2540. var offlineSince = GetOfflineSince(lastOnline);
  2541. if (offlineSince <= lastOnline.LastOnlineTime) return ScaleDamageCached(lastOnline);
  2542.  
  2543. return CalculateProtectionDamageScale(offlineSince);
  2544. }
  2545.  
  2546. private bool IsProtectionDelayedForStatus(ulong protectionScopeId, LastOnline protectionLastOnline)
  2547. {
  2548. if (protectionLastOnline == null || !protectionLastOnline.IsOffline()) return false;
  2549. if (!HasProtectionCooldownElapsed(protectionLastOnline)) return true;
  2550.  
  2551. var delayMinutes = _config.MinutesSinceLastAttackToProtect;
  2552. if (delayMinutes <= 0) return false;
  2553. return (DateTime.Now - GetEffectiveProtectionStart(protectionScopeId, protectionLastOnline)).TotalMinutes <= delayMinutes;
  2554. }
  2555.  
  2556. private float GetOfflineProtectionPreviewScale(int groupSize)
  2557. {
  2558. if (_config.DamageScale == null || _config.DamageScaleKeys == null || _config.DamageScaleKeys.Count == 0) return 1f;
  2559.  
  2560. var firstThreshold = _config.DamageScaleKeys[0];
  2561. if (!_config.DamageScale.TryGetValue(firstThreshold.ToString(), out var rawScale)) return 1f;
  2562.  
  2563. try
  2564. {
  2565. var scale = Convert.ToSingle(rawScale, CultureInfo.InvariantCulture);
  2566. return ApplyGroupSizeProtectionScaling(Mathf.Clamp01(scale), groupSize);
  2567. }
  2568. catch
  2569. {
  2570. PrintWarning($"Invalid DamageScale value for preview threshold '{firstThreshold}'.");
  2571. return 1f;
  2572. }
  2573. }
  2574.  
  2575. private static bool IsSameEntity(BaseEntity first, BaseEntity second)
  2576. {
  2577. if (first == null || second == null) return false;
  2578. if (ReferenceEquals(first, second)) return true;
  2579. return first.net != null && second.net != null && first.net.ID.Value == second.net.ID.Value;
  2580. }
  2581.  
  2582. private BaseEntity GetSimpleStatusLookEntity(BasePlayer player)
  2583. {
  2584. if (player == null) return null;
  2585. RaycastHit hit;
  2586. if (!Physics.Raycast(player.eyes.HeadRay(), out hit, 8f)) return null;
  2587. return hit.GetEntity();
  2588. }
  2589.  
  2590. private bool IsEntityInBuildingPrivilege(BaseEntity entity, BuildingPrivlidge privilege)
  2591. {
  2592. if (entity == null) return false;
  2593. if (privilege == null) return entity.GetBuildingPrivilege() == null;
  2594. return IsSameEntity(entity.GetBuildingPrivilege(), privilege);
  2595. }
  2596.  
  2597. private bool IsEntityInNavalTarget(BaseEntity entity, BaseEntity navalTarget)
  2598. {
  2599. if (entity == null || navalTarget == null) return false;
  2600. if (IsSameEntity(entity, navalTarget)) return true;
  2601. return IsSameEntity(FindNavalTarget(entity), navalTarget);
  2602. }
  2603.  
  2604. private bool TryResolveFallbackProtectionSource(ulong baseTargetId, out ulong targetId, out LastOnline protectionLastOnline, out bool groupOnline)
  2605. {
  2606. targetId = 0UL;
  2607. protectionLastOnline = null;
  2608. groupOnline = false;
  2609. if (!baseTargetId.IsSteamId()) return false;
  2610.  
  2611. if (!UsesSharedProtectionGroups())
  2612. {
  2613. groupOnline = !IsOffline(baseTargetId);
  2614. if (!HasPerm(baseTargetId.ToString(), PluginPermissions.Protect)) return false;
  2615. if (!_storedData.LastOnlines.TryGetValue(baseTargetId, out protectionLastOnline) || protectionLastOnline == null) return false;
  2616. targetId = baseTargetId;
  2617. return true;
  2618. }
  2619.  
  2620. var candidates = Pool.Get<List<LastOnline>>();
  2621. try
  2622. {
  2623. candidates.Clear();
  2624. groupOnline = !IsOffline(baseTargetId);
  2625. AddProtectionLastOnlineCandidates(new[] { baseTargetId }, candidates);
  2626.  
  2627. if (_config.ClansPluginShare && IsClansPluginAvailable())
  2628. {
  2629. var members = GetClansPluginMemberIds(baseTargetId);
  2630. if (CountOnlineMembersAtLeast(members, _config.ClansPluginMinMembers)) groupOnline = true;
  2631. AddProtectionLastOnlineCandidates(members, candidates);
  2632. }
  2633.  
  2634. if (_config.RustTeamShare)
  2635. {
  2636. var members = GetRustTeamMembers(baseTargetId);
  2637. if (CountOnlineMembersAtLeast(members, _config.RustTeamMinMembers)) groupOnline = true;
  2638. AddProtectionLastOnlineCandidates(members, candidates);
  2639. }
  2640.  
  2641. if (_config.RustClanShare)
  2642. {
  2643. var members = GetRustClanMembers(baseTargetId);
  2644. if (CountOnlineMembersAtLeast(members, _config.RustClanMinMembers)) groupOnline = true;
  2645. AddProtectionLastOnlineCandidates(members, candidates);
  2646. }
  2647.  
  2648. if (candidates.Count > 0)
  2649. {
  2650. protectionLastOnline = candidates[0];
  2651. for (var i = 1; i < candidates.Count; i++)
  2652. {
  2653. if (candidates[i].LastOnlineTime > protectionLastOnline.LastOnlineTime) protectionLastOnline = candidates[i];
  2654. }
  2655. targetId = protectionLastOnline.UserId;
  2656. return targetId.IsSteamId();
  2657. }
  2658.  
  2659. foreach (var userId in GetSharedProtectionMembers(baseTargetId))
  2660. {
  2661. if (!HasPerm(userId.ToString(), PluginPermissions.Protect) || !IsProtectionQualified(userId)) continue;
  2662. if (!_storedData.LastOnlines.TryGetValue(userId, out var qualifiedLastOnline) || qualifiedLastOnline == null) continue;
  2663. targetId = userId;
  2664. protectionLastOnline = qualifiedLastOnline;
  2665. return true;
  2666. }
  2667.  
  2668. if (!HasPerm(baseTargetId.ToString(), PluginPermissions.Protect)) return false;
  2669. if (!_storedData.LastOnlines.TryGetValue(baseTargetId, out protectionLastOnline) || protectionLastOnline == null) return false;
  2670. targetId = baseTargetId;
  2671. return true;
  2672. }
  2673. finally
  2674. {
  2675. Pool.FreeUnmanaged(ref candidates);
  2676. }
  2677. }
  2678.  
  2679. private bool TryGetCurrentProtectionScale(ulong targetId, LastOnline protectionLastOnline, bool groupOnline, bool groupInRange, int groupSize, ulong protectionScopeId, out float damageScale)
  2680. {
  2681. damageScale = -1f;
  2682. if (!targetId.IsSteamId() || protectionLastOnline == null || !HasPerm(targetId.ToString(), PluginPermissions.Protect) || !IsProtectionQualified(targetId)) return false;
  2683. if (IsProtectionDelayedForStatus(protectionScopeId, protectionLastOnline)) return false;
  2684.  
  2685. if (!groupOnline)
  2686. {
  2687. damageScale = ApplyGroupSizeProtectionScaling(GetProtectionDamageScale(protectionScopeId, protectionLastOnline), groupSize);
  2688. return damageScale >= 0f && damageScale < 1f;
  2689. }
  2690.  
  2691. if (_config.ProtectBaseWhenAway && !groupInRange)
  2692. {
  2693. damageScale = ApplyGroupSizeProtectionScaling(0f, groupSize);
  2694. return damageScale < 1f;
  2695. }
  2696.  
  2697. return false;
  2698. }
  2699.  
  2700. private int DamageScaleToProtectionPercent(float damageScale)
  2701. {
  2702. if (damageScale < 0f) return 0;
  2703. return Mathf.Clamp(Mathf.RoundToInt((1f - Mathf.Clamp01(damageScale)) * 100f), 0, 100);
  2704. }
  2705.  
  2706. private void UpdateSimpleStatusFor(BasePlayer player)
  2707. {
  2708. if (!IsSimpleStatusIntegrationReady() || !_simpleStatusCreated || player == null || !player.IsConnected || !player.userID.IsSteamId()) return;
  2709.  
  2710. var authorizedIds = Pool.Get<List<ulong>>();
  2711. try
  2712. {
  2713. authorizedIds.Clear();
  2714. var hasAccess = false;
  2715. var actualProtectionActive = false;
  2716. var damageScale = -1f;
  2717. var targetId = 0UL;
  2718. var groupSize = 1;
  2719. var privilegeBased = false;
  2720.  
  2721. var navalTarget = FindNavalTarget(player);
  2722. if (navalTarget == null && player.GetMounted() != null) navalTarget = FindNavalTarget(player.GetMounted());
  2723. if (navalTarget != null)
  2724. {
  2725. if (navalTarget is PlayerBoat && !_config.ProtectPlayerMadeBoats)
  2726. {
  2727. HideSimpleStatusIfVisible(player);
  2728. return;
  2729. }
  2730. if (navalTarget is Tugboat && !_config.ProtectTugboats)
  2731. {
  2732. HideSimpleStatusIfVisible(player);
  2733. return;
  2734. }
  2735.  
  2736. var privilege = GetNavalVehiclePrivilege(navalTarget);
  2737. if (privilege != null && AppendVehiclePrivilegeAuthorizedIds(privilege, authorizedIds) && authorizedIds.Count > 0)
  2738. {
  2739. privilegeBased = true;
  2740. if (!TryResolvePrivilegeProtectionSource(authorizedIds, out targetId, out var protectionLastOnline, out var groupOnline))
  2741. {
  2742. HideSimpleStatusIfVisible(player);
  2743. return;
  2744. }
  2745.  
  2746. groupSize = GetProtectionGroupSize(targetId, authorizedIds);
  2747. hasAccess = HasPrivilegeProtectionAccess(player.userID, authorizedIds);
  2748. var groupInRange = !_config.ProtectBaseWhenAway || IsPrivilegeGroupNearEntity(authorizedIds, navalTarget);
  2749. actualProtectionActive = TryGetCurrentProtectionScale(targetId, protectionLastOnline, groupOnline, groupInRange, groupSize, GetProtectionScopeId(privilege, targetId), out damageScale);
  2750. }
  2751. else
  2752. {
  2753. var lookedEntity = GetSimpleStatusLookEntity(player);
  2754. var lookedCombatEntity = lookedEntity as BaseCombatEntity;
  2755. if (lookedCombatEntity == null || !IsBlocked(lookedCombatEntity) || !IsEntityInNavalTarget(lookedEntity, navalTarget))
  2756. {
  2757. HideSimpleStatusIfVisible(player);
  2758. return;
  2759. }
  2760.  
  2761. if (!TryResolveNavalFallbackProtectionSource(lookedEntity, navalTarget, privilege, out targetId, out var protectionLastOnline, out var groupOnline))
  2762. {
  2763. HideSimpleStatusIfVisible(player);
  2764. return;
  2765. }
  2766.  
  2767. groupSize = GetProtectionGroupSize(targetId);
  2768. hasAccess = HasFallbackProtectionAccess(player.userID, targetId);
  2769. var groupInRange = !_config.ProtectBaseWhenAway || IsPlayerOrGroupNearEntity(targetId, navalTarget);
  2770. actualProtectionActive = TryGetCurrentProtectionScale(targetId, protectionLastOnline, groupOnline, groupInRange, groupSize, GetProtectionScopeId(lookedEntity, targetId), out damageScale);
  2771. }
  2772. }
  2773. else
  2774. {
  2775. var lookedEntity = GetSimpleStatusLookEntity(player);
  2776. var lookedCombatEntity = lookedEntity as BaseCombatEntity;
  2777. var lookedEntityValid = lookedCombatEntity != null && IsBlocked(lookedCombatEntity);
  2778.  
  2779. // A valid target without a privilege uses its own ownership fallback, not a nearby player's TC
  2780. var buildingPrivilege = lookedEntityValid ? lookedEntity.GetBuildingPrivilege() : player.GetBuildingPrivilege();
  2781. if (buildingPrivilege != null && buildingPrivilege.IsDestroyed) buildingPrivilege = null;
  2782. if (buildingPrivilege != null) AppendBuildingPrivilegeAuthorizedIds(buildingPrivilege, authorizedIds);
  2783.  
  2784. if (buildingPrivilege != null && authorizedIds.Count > 0)
  2785. {
  2786. privilegeBased = true;
  2787. if (!TryResolvePrivilegeProtectionSource(authorizedIds, out targetId, out var protectionLastOnline, out var groupOnline))
  2788. {
  2789. HideSimpleStatusIfVisible(player);
  2790. return;
  2791. }
  2792.  
  2793. groupSize = GetProtectionGroupSize(targetId, authorizedIds);
  2794. hasAccess = HasPrivilegeProtectionAccess(player.userID, authorizedIds);
  2795. var groupInRange = !_config.ProtectBaseWhenAway || IsPrivilegeGroupInBuildingRange(authorizedIds, buildingPrivilege);
  2796. actualProtectionActive = TryGetCurrentProtectionScale(targetId, protectionLastOnline, groupOnline, groupInRange, groupSize, GetProtectionScopeId(buildingPrivilege, targetId), out damageScale);
  2797. }
  2798. else
  2799. {
  2800. if (!lookedEntityValid || !IsEntityInBuildingPrivilege(lookedEntity, buildingPrivilege))
  2801. {
  2802. HideSimpleStatusIfVisible(player);
  2803. return;
  2804. }
  2805.  
  2806. if (!TryResolveEntityFallbackProtectionSource(lookedEntity, out targetId, out var protectionLastOnline, out var groupOnline))
  2807. {
  2808. HideSimpleStatusIfVisible(player);
  2809. return;
  2810. }
  2811.  
  2812. groupSize = GetProtectionGroupSize(targetId);
  2813. hasAccess = HasFallbackProtectionAccess(player.userID, targetId);
  2814. var groupInRange = !_config.ProtectBaseWhenAway || IsProtectionGroupInRange(targetId, buildingPrivilege);
  2815. actualProtectionActive = TryGetCurrentProtectionScale(targetId, protectionLastOnline, groupOnline, groupInRange, groupSize, GetProtectionScopeId(lookedEntity, targetId), out damageScale);
  2816. }
  2817. }
  2818.  
  2819. if (!hasAccess && _config.SimpleStatusShowForUnauthorizedOnlyWhenHoldingHammer && !IsHoldingHammer(player))
  2820. {
  2821. HideSimpleStatusIfVisible(player);
  2822. return;
  2823. }
  2824.  
  2825. if (hasAccess && !actualProtectionActive)
  2826. {
  2827. // Authorized viewers can preview the first normal protection tier while real protection is inactive.
  2828. damageScale = _config.SimpleStatusShowAuthorizedProtectionPreview ? GetOfflineProtectionPreviewScale(groupSize) : 1f;
  2829. }
  2830. else if (!hasAccess && !actualProtectionActive)
  2831. {
  2832. // Unauthorized viewers keep the red status visible even when protection is currently inactive.
  2833. damageScale = 1f;
  2834. }
  2835.  
  2836. var protectionPercent = DamageScaleToProtectionPercent(damageScale);
  2837.  
  2838. var label = GetMsg(PluginMessages.OfflineProtectionStatus, player.UserIDString);
  2839. var background = hasAccess ? _config.SimpleStatusAuthorizedBackground : _config.SimpleStatusUnauthorizedBackground;
  2840. var iconColor = hasAccess ? _config.SimpleStatusAuthorizedIconColor : _config.SimpleStatusUnauthorizedIconColor;
  2841. var labelColor = hasAccess ? _config.SimpleStatusAuthorizedLabelColor : _config.SimpleStatusUnauthorizedLabelColor;
  2842. var valueColor = hasAccess ? _config.SimpleStatusAuthorizedValueColor : _config.SimpleStatusUnauthorizedValueColor;
  2843. var stateKey = new SimpleStatusStateKey(targetId, privilegeBased, hasAccess, protectionPercent, label, background, iconColor, labelColor, valueColor, _resolvedSimpleStatusIcon);
  2844. if (_simpleStatusStateKeys.TryGetValue(player.userID, out var previousState) && previousState.Equals(stateKey)) return;
  2845. _simpleStatusStateKeys[player.userID] = stateKey;
  2846.  
  2847. SimpleStatus.Call("SetStatus", player.UserIDString, _config.SimpleStatusStatusId, int.MaxValue);
  2848. _simpleStatusVisiblePlayers.Add(player.userID);
  2849. SimpleStatus.Call("SetStatusProperty", player.UserIDString, _config.SimpleStatusStatusId, new Dictionary<string, object>
  2850. {
  2851. ["color"] = NormalizeSimpleStatusColor(background),
  2852. ["title"] = label,
  2853. ["titleColor"] = NormalizeSimpleStatusColor(labelColor),
  2854. ["text"] = protectionPercent + "%",
  2855. ["textColor"] = NormalizeSimpleStatusColor(valueColor),
  2856. ["icon"] = _resolvedSimpleStatusIcon,
  2857. ["iconColor"] = NormalizeSimpleStatusColor(iconColor)
  2858. });
  2859. }
  2860. catch (Exception ex)
  2861. {
  2862. HideSimpleStatusIfVisible(player);
  2863. PrintWarning($"Unable to update SimpleStatus for {player.UserIDString}: {ex.Message}");
  2864. }
  2865. finally
  2866. {
  2867. Pool.FreeUnmanaged(ref authorizedIds);
  2868. }
  2869. }
  2870.  
  2871. void HideMessage(BasePlayer player)
  2872. {
  2873. if (player == null || player.net?.connection == null) return;
  2874. try
  2875. {
  2876. CuiHelper.DestroyUi(player, "AntiOfflineRaidMsg");
  2877. }
  2878. catch (Exception ex)
  2879. {
  2880. PrintWarning($"Unable to hide protection message for {player.UserIDString}: {ex.Message}");
  2881. }
  2882. }
  2883.  
  2884. private BaseEntity FindNavalTarget(BaseEntity entity)
  2885. {
  2886. var current = entity;
  2887. for (var depth = 0; current != null && depth < 16; depth++)
  2888. {
  2889. if (current is PlayerBoat || current is Tugboat) return current;
  2890. current = current.GetParentEntity();
  2891. }
  2892. return null;
  2893. }
  2894.  
  2895. private static bool IsDescendantOfEntity(BaseEntity entity, BaseEntity root)
  2896. {
  2897. if (entity == null || root == null) return false;
  2898. var current = entity;
  2899. for (var depth = 0; current != null && depth < 16; depth++)
  2900. {
  2901. if (current == root) return true;
  2902. current = current.GetParentEntity();
  2903. }
  2904. return false;
  2905. }
  2906.  
  2907. private VehiclePrivilege GetNavalVehiclePrivilege(BaseEntity navalTarget)
  2908. {
  2909. if (navalTarget == null) return null;
  2910. var privilege = navalTarget.GetComponentInChildren<VehiclePrivilege>(true);
  2911. if (privilege != null) return privilege;
  2912.  
  2913. var privileges = Pool.Get<List<VehiclePrivilege>>();
  2914. try
  2915. {
  2916. Vis.Entities(navalTarget.transform.position, 20f, privileges, Rust.Layers.Mask.Deployed);
  2917. foreach (var candidate in privileges)
  2918. {
  2919. if (candidate == null || candidate.IsDestroyed || !IsDescendantOfEntity(candidate, navalTarget)) continue;
  2920. return candidate;
  2921. }
  2922. }
  2923. finally
  2924. {
  2925. Pool.FreeUnmanaged(ref privileges);
  2926. }
  2927.  
  2928. return null;
  2929. }
  2930.  
  2931. private bool AppendVehiclePrivilegeAuthorizedIds(VehiclePrivilege privilege, List<ulong> result)
  2932. {
  2933. if (privilege == null || result == null) return false;
  2934. try
  2935. {
  2936. var privilegeType = privilege.GetType();
  2937. if (!_vehiclePrivilegeAuthorizedPlayersFieldByType.TryGetValue(privilegeType, out var field))
  2938. {
  2939. var type = privilegeType;
  2940. while (type != null && field == null)
  2941. {
  2942. field = type.GetField("authorizedPlayers", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.DeclaredOnly);
  2943. type = type.BaseType;
  2944. }
  2945. _vehiclePrivilegeAuthorizedPlayersFieldByType[privilegeType] = field;
  2946. }
  2947.  
  2948. if (field == null) return false;
  2949. var values = field.GetValue(privilege) as IEnumerable;
  2950. if (values == null) return false;
  2951.  
  2952. foreach (var value in values)
  2953. {
  2954. var userId = GetAuthorizationEntryUserId(value);
  2955. if (userId.IsSteamId() && !result.Contains(userId)) result.Add(userId);
  2956. }
  2957. return true;
  2958. }
  2959. catch (Exception ex)
  2960. {
  2961. PrintWarning($"Unable to read naval privilege authorization: {ex.Message}");
  2962. return false;
  2963. }
  2964. }
  2965.  
  2966. private ulong GetAuthorizationEntryUserId(object value)
  2967. {
  2968. if (value == null) return 0UL;
  2969. if (value is ulong userId) return userId;
  2970.  
  2971. try
  2972. {
  2973. var type = value.GetType();
  2974. const System.Reflection.BindingFlags flags = System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic;
  2975. if (!_authorizationEntryUserIdFieldByType.TryGetValue(type, out var field))
  2976. {
  2977. field = type.GetField("userid", flags) ?? type.GetField("userID", flags) ?? type.GetField("UserID", flags);
  2978. _authorizationEntryUserIdFieldByType[type] = field;
  2979. }
  2980.  
  2981. if (field != null)
  2982. {
  2983. var raw = field.GetValue(value);
  2984. if (raw is ulong fieldUserId) return fieldUserId;
  2985. if (raw != null && ulong.TryParse(raw.ToString(), out fieldUserId)) return fieldUserId;
  2986. }
  2987.  
  2988. if (!_authorizationEntryUserIdPropertyByType.TryGetValue(type, out var property))
  2989. {
  2990. property = type.GetProperty("userid", flags) ?? type.GetProperty("userID", flags) ?? type.GetProperty("UserID", flags);
  2991. _authorizationEntryUserIdPropertyByType[type] = property;
  2992. }
  2993.  
  2994. if (property != null)
  2995. {
  2996. var raw = property.GetValue(value, null);
  2997. if (raw is ulong propertyUserId) return propertyUserId;
  2998. if (raw != null && ulong.TryParse(raw.ToString(), out propertyUserId)) return propertyUserId;
  2999. }
  3000. }
  3001. catch
  3002. {
  3003. }
  3004. return 0UL;
  3005. }
  3006.  
  3007. private BoatBuildingStation FindRelevantBoatBuildingStation(BoatBuildingBlock block)
  3008. {
  3009. if (block == null || block.IsDestroyed) return null;
  3010. var current = block.GetParentEntity();
  3011. for (var depth = 0; current != null && depth < 16; depth++)
  3012. {
  3013. if (current is BoatBuildingStation parentStation) return parentStation;
  3014. current = current.GetParentEntity();
  3015. }
  3016.  
  3017. var stations = Pool.Get<List<BoatBuildingStation>>();
  3018. try
  3019. {
  3020. Vis.Entities(block.transform.position, 30f, stations, Rust.Layers.Mask.Deployed);
  3021. BoatBuildingStation nearest = null;
  3022. var nearestDistanceSqr = float.MaxValue;
  3023. foreach (var station in stations)
  3024. {
  3025. if (station == null || station.IsDestroyed || !station.OwnerID.IsSteamId()) continue;
  3026. if (block.OwnerID.IsSteamId() && station.OwnerID != block.OwnerID) continue;
  3027. var distanceSqr = (station.transform.position - block.transform.position).sqrMagnitude;
  3028. if (distanceSqr >= nearestDistanceSqr) continue;
  3029. nearestDistanceSqr = distanceSqr;
  3030. nearest = station;
  3031. }
  3032. return nearest;
  3033. }
  3034. finally
  3035. {
  3036. Pool.FreeUnmanaged(ref stations);
  3037. }
  3038. }
  3039.  
  3040. private bool IsPlayerNearEntity(ulong playerId, BaseEntity entity)
  3041. {
  3042. if (entity == null) return false;
  3043. var player = BasePlayer.FindByID(playerId);
  3044. if (player == null || !player.IsConnected) return false;
  3045. if (IsDescendantOfEntity(player, entity)) return true;
  3046. return (player.transform.position - entity.transform.position).sqrMagnitude <= NavalPresenceRadius * NavalPresenceRadius;
  3047. }
  3048.  
  3049. private bool IsPlayerOrGroupNearEntity(ulong targetId, BaseEntity entity)
  3050. {
  3051. foreach (var userId in GetSharedProtectionMembers(targetId))
  3052. {
  3053. if (IsPlayerNearEntity(userId, entity)) return true;
  3054. }
  3055. return false;
  3056. }
  3057.  
  3058. private bool IsConfiguredPrefab(BaseCombatEntity entity)
  3059. {
  3060. var prefabName = entity?.ShortPrefabName;
  3061. if (string.IsNullOrEmpty(prefabName) || _config?.Prefabs == null) return false;
  3062. if (_configuredPrefabMatches.TryGetValue(prefabName, out var matched)) return matched;
  3063.  
  3064. foreach (var configuredPrefab in _config.Prefabs)
  3065. {
  3066. if (!prefabName.Contains(configuredPrefab)) continue;
  3067. matched = true;
  3068. break;
  3069. }
  3070. if (_configuredPrefabMatches.Count < ConfiguredPrefabCacheLimit) _configuredPrefabMatches[prefabName] = matched;
  3071. return matched;
  3072. }
  3073.  
  3074. public bool IsBlocked(BaseCombatEntity entity)
  3075. {
  3076. if (entity == null) return false;
  3077. var structuralType = entity is BuildingBlock || entity is BoatBuildingBlock || entity is PlayerBoat || entity is Tugboat;
  3078. var configuredPrefab = !structuralType && IsConfiguredPrefab(entity);
  3079. var navalTarget = FindNavalTarget(entity);
  3080. if (structuralType && navalTarget != null && entity != navalTarget && !(navalTarget is PlayerBoat && entity is BoatBuildingBlock))
  3081. configuredPrefab = IsConfiguredPrefab(entity);
  3082. return IsBlocked(entity, navalTarget, configuredPrefab);
  3083. }
  3084.  
  3085. private bool IsBlocked(BaseCombatEntity entity, BaseEntity navalTarget, bool configuredPrefab)
  3086. {
  3087. if (entity == null) return false;
  3088. if (navalTarget is PlayerBoat) return _config.ProtectPlayerMadeBoats && (entity == navalTarget || entity is BoatBuildingBlock || configuredPrefab);
  3089. if (navalTarget is Tugboat) return _config.ProtectTugboats && (entity == navalTarget || configuredPrefab);
  3090. if (entity is BoatBuildingBlock) return _config.ProtectPlayerMadeBoats;
  3091. if (entity is BuildingBlock) return true;
  3092. return configuredPrefab;
  3093. }
  3094.  
  3095. private bool IsProtectionGroupInRange(ulong targetId, BuildingPrivlidge privilege = null)
  3096. {
  3097. foreach (var userId in GetSharedProtectionMembers(targetId))
  3098. {
  3099. if (privilege != null)
  3100. {
  3101. if (IsPlayerNearBuildingPrivilege(userId, privilege)) return true;
  3102. }
  3103. else if (IsPlayerInRange(userId))
  3104. {
  3105. return true;
  3106. }
  3107. }
  3108. return false;
  3109. }
  3110.  
  3111. private static bool IsPlayerNearBuildingPrivilege(ulong targetId, BuildingPrivlidge privilege)
  3112. {
  3113. if (privilege == null || privilege.IsDestroyed) return false;
  3114. var player = BasePlayer.FindByID(targetId);
  3115. if (player == null || !player.IsConnected) return false;
  3116.  
  3117. var currentPrivilege = player.GetBuildingPrivilege();
  3118. if (currentPrivilege == null || currentPrivilege.IsDestroyed) return false;
  3119. if (ReferenceEquals(currentPrivilege, privilege)) return true;
  3120. if (currentPrivilege.net == null || privilege.net == null) return false;
  3121. return currentPrivilege.net.ID.Value == privilege.net.ID.Value;
  3122. }
  3123.  
  3124. public bool IsOffline(ulong playerId)
  3125. {
  3126. EnsureDataInitialized();
  3127. if (_storedData.LastOnlines.TryGetValue(playerId, out var lastOnlinePlayer) && lastOnlinePlayer != null)
  3128. return lastOnlinePlayer.IsOffline();
  3129.  
  3130. var player = covalence.Players.FindPlayerById(playerId.ToString());
  3131. return player == null || !player.IsConnected;
  3132. }
  3133.  
  3134. private static bool IsPlayerInRange(ulong targetId)
  3135. {
  3136. var player = BasePlayer.FindByID(targetId);
  3137. return player != null && player.IsBuildingAuthed();
  3138. }
  3139.  
  3140. private void PlayProtectionSound(HitInfo hitInfo, bool isFire)
  3141. {
  3142. if (!_config.PlaySound || isFire || !(hitInfo?.Initiator is BasePlayer player)) return;
  3143. try
  3144. {
  3145. Effect.server.Run(_config.SoundFile, player.transform.position);
  3146. }
  3147. catch (Exception ex)
  3148. {
  3149. PrintWarning($"Unable to play protection sound: {ex.Message}");
  3150. }
  3151. }
  3152.  
  3153. public object MitigateDamage(HitInfo hitInfo, float scale)
  3154. {
  3155. if (hitInfo?.damageTypes == null || scale < 0f) return null;
  3156. var isFire = hitInfo.damageTypes.GetMajorityDamageType() == DamageType.Heat;
  3157.  
  3158. if (scale <= 0f)
  3159. {
  3160. if (_config.ShowMessage && ((isFire && hitInfo.WeaponPrefab != null) || !isFire)) SendMessage(hitInfo);
  3161. PlayProtectionSound(hitInfo, isFire);
  3162. return true;
  3163. }
  3164.  
  3165. hitInfo.damageTypes.ScaleAll(scale);
  3166. if (scale >= 1f) return null;
  3167.  
  3168. if (_config.ShowMessage && ((isFire && hitInfo.WeaponPrefab != null) || !isFire)) SendMessage(hitInfo, DamageScaleToProtectionPercent(scale));
  3169. PlayProtectionSound(hitInfo, isFire);
  3170. return null;
  3171. }
  3172.  
  3173. object OnStructureAttack(BaseEntity entity, HitInfo hitInfo)
  3174. {
  3175. return OnStructureAttack(entity, hitInfo, FindNavalTarget(entity));
  3176. }
  3177.  
  3178. private object OnStructureAttack(BaseEntity entity, HitInfo hitInfo, BaseEntity navalTarget)
  3179. {
  3180. if (entity == null || hitInfo == null) return null;
  3181. if (navalTarget is PlayerBoat && _config.ProtectPlayerMadeBoats) return OnNavalStructureAttack(navalTarget, entity, hitInfo);
  3182. if (navalTarget is Tugboat && _config.ProtectTugboats) return OnNavalStructureAttack(navalTarget, entity, hitInfo);
  3183.  
  3184. if (entity is BoatBuildingBlock boatBlock)
  3185. {
  3186. if (!_config.ProtectPlayerMadeBoats) return null;
  3187. var station = FindRelevantBoatBuildingStation(boatBlock);
  3188. if (station == null) return null;
  3189.  
  3190. ulong targetId;
  3191. LastOnline protectionLastOnline;
  3192. bool groupOnline;
  3193. if (!TryResolveEntityFallbackProtectionSource(boatBlock, out targetId, out protectionLastOnline, out groupOnline))
  3194. {
  3195. if (!station.OwnerID.IsSteamId() || !TryResolveFallbackProtectionSource(station.OwnerID, out targetId, out protectionLastOnline, out groupOnline)) return null;
  3196. }
  3197.  
  3198. if (groupOnline && !_config.ProtectBaseWhenAway) return null;
  3199. var groupSize = GetProtectionGroupSize(targetId);
  3200. var groupInRange = !_config.ProtectBaseWhenAway || IsPlayerOrGroupNearEntity(targetId, station);
  3201. var protectionScopeId = GetProtectionScopeId(boatBlock, targetId);
  3202. return ApplyProtection(hitInfo, targetId, protectionLastOnline, groupOnline, groupInRange, groupSize, protectionScopeId);
  3203. }
  3204.  
  3205. var buildBlock = entity as BuildingBlock;
  3206. if (buildBlock != null && buildBlock.grade == BuildingGrade.Enum.Twigs) return null;
  3207.  
  3208. var buildingPrivilege = entity.GetBuildingPrivilege();
  3209. var authorizedIds = Pool.Get<List<ulong>>();
  3210. try
  3211. {
  3212. authorizedIds.Clear();
  3213. if (buildingPrivilege != null) AppendBuildingPrivilegeAuthorizedIds(buildingPrivilege, authorizedIds);
  3214. if (buildingPrivilege != null && authorizedIds.Count > 0)
  3215. {
  3216. if (!TryResolvePrivilegeProtectionSource(authorizedIds, out var targetId, out var protectionLastOnline, out var groupOnline)) return null;
  3217. if (groupOnline && !_config.ProtectBaseWhenAway) return null;
  3218. var groupSize = GetProtectionGroupSize(targetId, authorizedIds);
  3219. var groupInRange = !_config.ProtectBaseWhenAway || IsPrivilegeGroupInBuildingRange(authorizedIds, buildingPrivilege);
  3220. return ApplyProtection(hitInfo, targetId, protectionLastOnline, groupOnline, groupInRange, groupSize, GetProtectionScopeId(buildingPrivilege, targetId));
  3221. }
  3222.  
  3223. if (!TryResolveEntityFallbackProtectionSource(entity, out var fallbackResolvedId, out var fallbackLastOnline, out var fallbackGroupOnline)) return null;
  3224. if (fallbackGroupOnline && !_config.ProtectBaseWhenAway) return null;
  3225. var fallbackGroupSize = GetProtectionGroupSize(fallbackResolvedId);
  3226. var fallbackGroupInRange = !_config.ProtectBaseWhenAway || IsProtectionGroupInRange(fallbackResolvedId, buildingPrivilege);
  3227. return ApplyProtection(hitInfo, fallbackResolvedId, fallbackLastOnline, fallbackGroupOnline, fallbackGroupInRange, fallbackGroupSize, GetProtectionScopeId(entity, fallbackResolvedId));
  3228. }
  3229. finally
  3230. {
  3231. Pool.FreeUnmanaged(ref authorizedIds);
  3232. }
  3233. }
  3234.  
  3235. private object OnNavalStructureAttack(BaseEntity navalTarget, BaseEntity attackedEntity, HitInfo hitInfo)
  3236. {
  3237. if (navalTarget == null || attackedEntity == null || hitInfo == null) return null;
  3238. var privilege = GetNavalVehiclePrivilege(navalTarget);
  3239. var authorizedIds = Pool.Get<List<ulong>>();
  3240. try
  3241. {
  3242. authorizedIds.Clear();
  3243. if (privilege != null && AppendVehiclePrivilegeAuthorizedIds(privilege, authorizedIds) && authorizedIds.Count > 0)
  3244. {
  3245. if (!TryResolvePrivilegeProtectionSource(authorizedIds, out var targetId, out var protectionLastOnline, out var groupOnline)) return null;
  3246. if (groupOnline && !_config.ProtectBaseWhenAway) return null;
  3247. var groupSize = GetProtectionGroupSize(targetId, authorizedIds);
  3248. var groupInRange = !_config.ProtectBaseWhenAway || IsPrivilegeGroupNearEntity(authorizedIds, navalTarget);
  3249. return ApplyProtection(hitInfo, targetId, protectionLastOnline, groupOnline, groupInRange, groupSize, GetProtectionScopeId(privilege, targetId));
  3250. }
  3251.  
  3252. if (!TryResolveNavalFallbackProtectionSource(attackedEntity, navalTarget, privilege, out var fallbackTargetId, out var fallbackLastOnline, out var fallbackGroupOnline)) return null;
  3253. if (fallbackGroupOnline && !_config.ProtectBaseWhenAway) return null;
  3254. var fallbackGroupSize = GetProtectionGroupSize(fallbackTargetId);
  3255. var fallbackGroupInRange = !_config.ProtectBaseWhenAway || IsPlayerOrGroupNearEntity(fallbackTargetId, navalTarget);
  3256. return ApplyProtection(hitInfo, fallbackTargetId, fallbackLastOnline, fallbackGroupOnline, fallbackGroupInRange, fallbackGroupSize, GetProtectionScopeId(attackedEntity, fallbackTargetId));
  3257. }
  3258. finally
  3259. {
  3260. Pool.FreeUnmanaged(ref authorizedIds);
  3261. }
  3262. }
  3263.  
  3264. private DateTime GetEffectiveProtectionStart(ulong protectionScopeId, LastOnline protectionLastOnline)
  3265. {
  3266. var start = protectionLastOnline != null ? GetOfflineSince(protectionLastOnline) : DateTime.Now;
  3267. if (!_storedData.LastStructureAttackAt.TryGetValue(protectionScopeId, out var lastAttackLong) || lastAttackLong == 0) return start;
  3268.  
  3269. try
  3270. {
  3271. var lastAttack = DateTime.FromBinary(lastAttackLong);
  3272. return lastAttack > start ? lastAttack : start;
  3273. }
  3274. catch
  3275. {
  3276. _storedData.LastStructureAttackAt.Remove(protectionScopeId);
  3277. return start;
  3278. }
  3279. }
  3280.  
  3281. private bool ShouldDelayProtectionForRecentAttack(ulong protectionScopeId, LastOnline protectionLastOnline)
  3282. {
  3283. if (protectionLastOnline == null || !protectionLastOnline.IsOffline()) return false;
  3284. var cooldownElapsed = HasProtectionCooldownElapsed(protectionLastOnline);
  3285. var delayMinutes = _config.MinutesSinceLastAttackToProtect;
  3286. if (delayMinutes <= 0) return !cooldownElapsed;
  3287.  
  3288. var now = DateTime.Now;
  3289. var effectiveStart = GetEffectiveProtectionStart(protectionScopeId, protectionLastOnline);
  3290. if (cooldownElapsed && (now - effectiveStart).TotalMinutes > delayMinutes) return false;
  3291.  
  3292. // Hits during the initial cooldown must also extend the pending raid delay for this scope
  3293. _storedData.LastStructureAttackAt[protectionScopeId] = now.ToBinary();
  3294. InvalidateDamageScale(protectionLastOnline.UserId);
  3295. return true;
  3296. }
  3297.  
  3298. private float GetProtectionDamageScale(ulong protectionScopeId, LastOnline protectionLastOnline)
  3299. {
  3300. if (protectionLastOnline == null || !protectionLastOnline.IsOffline() || !IsProtectionQualified(protectionLastOnline.UserId)) return -1f;
  3301. var effectiveStart = GetEffectiveProtectionStart(protectionScopeId, protectionLastOnline);
  3302. if (effectiveStart <= protectionLastOnline.LastOnlineTime) return ScaleDamageCached(protectionLastOnline);
  3303. return CalculateProtectionDamageScale(effectiveStart);
  3304. }
  3305.  
  3306. private object ApplyProtection(HitInfo hitInfo, ulong targetId, LastOnline protectionLastOnline, bool groupOnline, bool groupInRange, int groupSize, ulong protectionScopeId)
  3307. {
  3308. if (hitInfo == null || !targetId.IsSteamId() || !HasPerm(targetId.ToString(), PluginPermissions.Protect) || !IsProtectionQualified(targetId)) return null;
  3309.  
  3310. if (!groupOnline)
  3311. {
  3312. if (protectionLastOnline == null || ShouldDelayProtectionForRecentAttack(protectionScopeId, protectionLastOnline)) return null;
  3313. var scale = ApplyGroupSizeProtectionScaling(GetProtectionDamageScale(protectionScopeId, protectionLastOnline), groupSize);
  3314. return MitigateDamage(hitInfo, scale);
  3315. }
  3316.  
  3317. if (_config.ProtectBaseWhenAway && !groupInRange)
  3318. {
  3319. var awayScale = ApplyGroupSizeProtectionScaling(0f, groupSize);
  3320. return MitigateDamage(hitInfo, awayScale);
  3321. }
  3322.  
  3323. return null;
  3324. }
  3325.  
  3326. public float ScaleDamage(LastOnline lastOnline)
  3327. {
  3328. if (lastOnline == null || !lastOnline.IsOffline() || !IsProtectionQualified(lastOnline.UserId)) return -1f;
  3329. return CalculateProtectionDamageScale(lastOnline.LastOnlineTime);
  3330. }
  3331.  
  3332. private float CalculateProtectionDamageScale(DateTime protectionStart)
  3333. {
  3334. // Eligibility is checked on the real player state, not on a shifted offline or raid-delay timestamp
  3335. var now = DateTime.Now;
  3336.  
  3337. // Absolute time scale intentionally overrides relative offline-time scaling
  3338. if (_config.AbsoluteTimeScale.Count > 0)
  3339. {
  3340. var hour = now.ToString("HH", DateTimeFormatInfo.InvariantInfo);
  3341. if (_config.AbsoluteTimeScale.TryGetValue(hour, out var scaleObj))
  3342. {
  3343. try
  3344. {
  3345. return Convert.ToSingle(scaleObj, CultureInfo.InvariantCulture);
  3346. }
  3347. catch
  3348. {
  3349. PrintWarning($"Invalid AbsoluteTimeScale value for hour '{hour}'.");
  3350. }
  3351. }
  3352. }
  3353.  
  3354. var offlineTime = now - protectionStart;
  3355. if (offlineTime.TotalMinutes < 60) return _config.InterimDamage;
  3356. var scaleKey = -1;
  3357. for (var i = 0; i < _config.DamageScaleKeys.Count; i++)
  3358. {
  3359. var key = _config.DamageScaleKeys[i];
  3360. if (key > offlineTime.TotalHours) break;
  3361. scaleKey = key;
  3362. }
  3363. if (scaleKey < 0 || !_config.DamageScale.TryGetValue(scaleKey.ToString(), out var scaleValue)) return -1f;
  3364.  
  3365. try
  3366. {
  3367. return Convert.ToSingle(scaleValue, CultureInfo.InvariantCulture);
  3368. }
  3369. catch
  3370. {
  3371. PrintWarning($"Invalid DamageScale value for threshold '{scaleKey}'.");
  3372. return -1f;
  3373. }
  3374. }
  3375.  
  3376. float ScaleDamageCached(LastOnline lastOnline)
  3377. {
  3378. if (lastOnline == null) return -1f;
  3379. if (_cachedScales.TryGetValue(lastOnline.UserId, out var kvp) && kvp != null && DateTime.Now < kvp.Expires) return kvp.Scale;
  3380. return CacheDamageScale(lastOnline.UserId, ScaleDamage(lastOnline), kvp);
  3381. }
  3382.  
  3383. private void AppendShareStatus(StringBuilder stringBuilder, string userId, string providerName, HashSet<ulong> members, int minOnlineMembers)
  3384. {
  3385. if (stringBuilder == null || members == null || members.Count == 0) return;
  3386.  
  3387. var onlineCount = members.Count(memberId => !IsOffline(memberId));
  3388. var groupOnline = onlineCount >= Math.Max(1, minOnlineMembers);
  3389. var groupStatus = GetMsg(groupOnline ? PluginMessages.StatusOnline : PluginMessages.StatusOffline, userId);
  3390. stringBuilder.AppendLine(string.Format(GetMsg(PluginMessages.StatusShare, userId), providerName, groupStatus, onlineCount));
  3391.  
  3392. if (groupOnline || !TryEvaluateSharedProtectionProvider(members, minOnlineMembers, out var selected, out _) || selected == null) return;
  3393. var selectedPlayer = covalence.Players.FindPlayerById(selected.UserId.ToString());
  3394. var selectedName = selectedPlayer?.Name ?? selected.UserId.ToString();
  3395. stringBuilder.AppendLine(string.Format(GetMsg(PluginMessages.StatusShareLastOffline, userId), providerName, selectedName, selected.LastOnlineTime.AddHours(_config.ServerTimeOffset)));
  3396. }
  3397.  
  3398. string SendStatus(string playerSearch, string userId = null)
  3399. {
  3400. EnsureDataInitialized();
  3401. if (string.IsNullOrWhiteSpace(playerSearch)) return GetMsg(PluginMessages.InvalidSyntax, userId);
  3402. var target = FindPlayerByPartialName(playerSearch);
  3403. if (target == null || !ulong.TryParse(target.Id, out var targetId) || !_storedData.LastOnlines.TryGetValue(targetId, out var lo) || lo == null)
  3404. return GetMsg(PluginMessages.PlayerNotFound, userId);
  3405.  
  3406. var stringBuilder = new StringBuilder();
  3407. stringBuilder.AppendLine(string.Format(GetMsg(PluginMessages.StatusHeader, userId), target.Name));
  3408. if (IsOffline(targetId))
  3409. {
  3410. var statusTime = GetOfflineSince(lo).AddHours(_config.ServerTimeOffset);
  3411. stringBuilder.AppendLine(target.IsConnected
  3412. ? string.Format(GetMsg(PluginMessages.StatusPlayerAfk, userId), statusTime)
  3413. : string.Format(GetMsg(PluginMessages.StatusPlayerOffline, userId), statusTime));
  3414. }
  3415. else
  3416. {
  3417. stringBuilder.AppendLine(GetMsg(PluginMessages.StatusPlayerOnline, userId));
  3418. }
  3419.  
  3420. stringBuilder.AppendLine(string.Format(GetMsg(PluginMessages.StatusAfkMinutes, userId), Math.Round(lo.AfkMinutes, 1)));
  3421. if (_config.ClansPluginShare && IsClansPluginAvailable())
  3422. AppendShareStatus(stringBuilder, userId, GetMsg(PluginMessages.ProviderClansPlugin, userId), GetClansPluginMemberIds(targetId), _config.ClansPluginMinMembers);
  3423. if (_config.RustTeamShare)
  3424. AppendShareStatus(stringBuilder, userId, GetMsg(PluginMessages.ProviderRustTeam, userId), GetRustTeamMembers(targetId), _config.RustTeamMinMembers);
  3425. if (_config.RustClanShare)
  3426. AppendShareStatus(stringBuilder, userId, GetMsg(PluginMessages.ProviderRustClan, userId), GetRustClanMembers(targetId), _config.RustClanMinMembers);
  3427.  
  3428. if (TryGetProtectionLastOnline(targetId, out var protectionLastOnline))
  3429. {
  3430. var scale = GetCurrentBaseProtectionDamageScale(protectionLastOnline);
  3431. if (scale > -1f) stringBuilder.AppendLine(string.Format(GetMsg(PluginMessages.StatusScale, userId), DamageScaleToProtectionPercent(scale)));
  3432. }
  3433. return stringBuilder.ToString();
  3434. }
  3435.  
  3436. void ShowMessage(BasePlayer player, int amount = 100)
  3437. {
  3438. if (player == null || !player.IsConnected || player.net?.connection == null) return;
  3439. HideMessage(player);
  3440.  
  3441. var sb = new StringBuilder(JsonMessage);
  3442. sb.Replace("{1}", Core.Random.Range(1, 99999).ToString());
  3443. sb.Replace("{protection_message}", GetMsg(PluginMessages.ProtectionMessage, player.UserIDString));
  3444. sb.Replace("{amount}", amount.ToString());
  3445. CuiHelper.AddUi(player, sb.ToString());
  3446. timer.In(3f, () =>
  3447. {
  3448. if (player != null && player.IsConnected && player.net?.connection != null) HideMessage(player);
  3449. });
  3450. }
  3451.  
  3452. void UpdateLastOnline(BasePlayer player)
  3453. {
  3454. if (player == null) return;
  3455. UpdateLastOnline(player.userID, player);
  3456. }
  3457.  
  3458. void UpdateLastOnline(ulong playerId)
  3459. {
  3460. if (!playerId.IsSteamId()) return;
  3461. UpdateLastOnline(playerId, BasePlayer.FindByID(playerId));
  3462. }
  3463.  
  3464. private void UpdateLastOnline(ulong playerId, BasePlayer activePlayer)
  3465. {
  3466. if (!playerId.IsSteamId()) return;
  3467. EnsureDataInitialized();
  3468. _storedData.LastOnlines.TryGetValue(playerId, out var lastOnlinePlayer);
  3469. if (activePlayer != null) UpdateOnlineQualification(activePlayer, lastOnlinePlayer);
  3470. if (lastOnlinePlayer == null)
  3471. {
  3472. lastOnlinePlayer = new LastOnline(playerId, DateTime.Now);
  3473. _storedData.LastOnlines[playerId] = lastOnlinePlayer;
  3474. }
  3475. else
  3476. {
  3477. lastOnlinePlayer.LastOnlineTime = DateTime.Now;
  3478. lastOnlinePlayer.AfkMinutes = 0;
  3479. }
  3480.  
  3481. // Keep the movement baseline aligned with activity already recorded by the enabled hooks
  3482. if (activePlayer != null && !activePlayer.IsDestroyed) lastOnlinePlayer.LastPosition = activePlayer.transform.position;
  3483. InvalidateDamageScale(playerId);
  3484. }
  3485.  
  3486. private void InitializeLastOnline(BasePlayer player)
  3487. {
  3488. if (player == null || !player.IsConnected || !player.userID.IsSteamId()) return;
  3489. EnsureDataInitialized();
  3490. var now = DateTime.Now;
  3491. _storedData.LastOnlines.TryGetValue(player.userID, out var lastOnline);
  3492. var restoreLastOnline = false;
  3493.  
  3494. // Preserve hot-reload activity only when the stored timestamp belongs to the current connection
  3495. if (lastOnline != null && lastOnline.UserId == player.userID && lastOnline.LastOnlineLong != 0 && player.net?.connection != null)
  3496. {
  3497. try
  3498. {
  3499. var lastActivity = lastOnline.LastOnlineTime;
  3500. var secondsConnected = player.secondsConnected;
  3501. // The connection age is reported in whole seconds
  3502. restoreLastOnline = secondsConnected > 0 && lastActivity <= now && (now - lastActivity).TotalSeconds <= (double)secondsConnected + 1d;
  3503. }
  3504. catch (ArgumentException)
  3505. {
  3506. // Invalid saved timestamps are initialized as a fresh session
  3507. }
  3508. }
  3509.  
  3510. if (!restoreLastOnline)
  3511. {
  3512. StartOnlineQualificationSession(player.userID, true);
  3513. lastOnline = new LastOnline(player.userID, now);
  3514. _storedData.LastOnlines[player.userID] = lastOnline;
  3515. InvalidateDamageScale(player.userID);
  3516. }
  3517.  
  3518. lastOnline.LastPosition = player.transform.position;
  3519. lastOnline.AfkMinutes = (float)Math.Max(0d, (now - lastOnline.LastOnlineTime).TotalMinutes);
  3520. }
  3521.  
  3522. void UpdateLastOnlineAll(bool afkCheck = true)
  3523. {
  3524. EnsureDataInitialized();
  3525. foreach (var player in BasePlayer.activePlayerList)
  3526. {
  3527. if (player == null || !player.IsConnected) continue;
  3528.  
  3529. if (!afkCheck)
  3530. {
  3531. InitializeLastOnline(player);
  3532. continue;
  3533. }
  3534.  
  3535. if (!_storedData.LastOnlines.TryGetValue(player.userID, out var lastOnlinePlayer) || lastOnlinePlayer == null)
  3536. {
  3537. UpdateLastOnline(player);
  3538. if (_storedData.LastOnlines.TryGetValue(player.userID, out lastOnlinePlayer) && lastOnlinePlayer != null)
  3539. lastOnlinePlayer.LastPosition = player.transform.position;
  3540. continue;
  3541. }
  3542.  
  3543. if (lastOnlinePlayer.HasMoved(player.transform.position))
  3544. {
  3545. UpdateLastOnline(player);
  3546. continue;
  3547. }
  3548.  
  3549. UpdateOnlineQualification(player, lastOnlinePlayer);
  3550. lastOnlinePlayer.AfkMinutes = (float)Math.Max(0d, lastOnlinePlayer.Minutes);
  3551. }
  3552. }
  3553. #endregion
  3554.  
  3555. }
  3556.  
  3557. }
Advertisement
Add Comment
Please, Sign In to add comment