dFxPhoeniX

AntiOfflineRaid 1.6.1

Sep 16th, 2026 (edited)
30
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 134.23 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.0")]
  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 float InterimDamage { get; set; }
  180.  
  181. public int MinutesSinceLastAttackToProtect { get; set; }
  182.  
  183. public bool PlaySound { get; set; }
  184.  
  185. public List<string> Prefabs { get; set; }
  186.  
  187. public bool ProtectBaseWhenAway { get; set; }
  188.  
  189. public bool ProtectPlayerMadeBoats { get; set; } = true;
  190.  
  191. public bool ProtectTugboats { get; set; } = true;
  192.  
  193. public bool GroupSizeProtectionScaling { get; set; } = true;
  194.  
  195. public bool GroupSizeUseClansPluginMembers { get; set; } = false;
  196.  
  197. public bool GroupSizeUseRustTeams { get; set; } = false;
  198.  
  199. public bool GroupSizeUseRustClans { get; set; } = false;
  200.  
  201. public Dictionary<string, float> GroupSizeProtectionScale { get; set; } = new Dictionary<string, float>();
  202.  
  203. public int ServerTimeOffset { get; set; }
  204.  
  205. public bool ShowMessage { get; set; }
  206.  
  207. public bool SimpleStatusEnabled { get; set; } = false;
  208.  
  209. public bool SimpleStatusShowForUnauthorizedOnlyWhenHoldingHammer { get; set; } = false;
  210.  
  211. public bool SimpleStatusShowAuthorizedProtectionPreview { get; set; } = false;
  212.  
  213. public float SimpleStatusRefreshIntervalSeconds { get; set; } = 1f;
  214.  
  215. public string SimpleStatusStatusId { get; set; } = "antiofflineraid.protection";
  216.  
  217. public string SimpleStatusAuthorizedBackground { get; set; } = "#738D45";
  218.  
  219. public string SimpleStatusUnauthorizedBackground { get; set; } = "#773A2F";
  220.  
  221. public string SimpleStatusAuthorizedIconColor { get; set; } = "#A3FE03";
  222.  
  223. public string SimpleStatusUnauthorizedIconColor { get; set; } = "#FF5338";
  224.  
  225. public string SimpleStatusAuthorizedLabelColor { get; set; } = "#FFFFFF";
  226.  
  227. public string SimpleStatusUnauthorizedLabelColor { get; set; } = "#FF5338";
  228.  
  229. public string SimpleStatusAuthorizedValueColor { get; set; } = "#FFFFFF";
  230.  
  231. public string SimpleStatusUnauthorizedValueColor { get; set; } = "#FF5338";
  232.  
  233. public string SimpleStatusIcon { get; set; } = "https://i.ibb.co/nq6bL11S/protection.png";
  234.  
  235. public string SoundFile { get; set; }
  236. [JsonIgnore] public List<int> DamageScaleKeys { get; set; } = new List<int>();
  237. [JsonIgnore] public List<int> GroupSizeProtectionKeys { get; set; } = new List<int>();
  238.  
  239.  
  240. public static Configuration DefaultConfig()
  241. {
  242. return new Configuration
  243. {
  244. DamageScale = new Dictionary<string, object>
  245. {{"1", 0.2}, {"3", 0.35f}, {"6", 0.5f}, {"12", 0.8f}, {"48", 1}},
  246. AbsoluteTimeScale = new Dictionary<string, object> {{"03", 0.1}},
  247. AfkMinutes = 5,
  248. TreatAfkAsOffline = true,
  249. CooldownMinutes = 10,
  250. InterimDamage = 0,
  251. ClansPluginShare = false,
  252. ClansPluginMinMembers = 1,
  253. RustTeamShare = false,
  254. RustTeamMinMembers = 1,
  255. RustClanShare = false,
  256. RustClanMinMembers = 1,
  257. ShowMessage = true,
  258. SimpleStatusEnabled = false,
  259. SimpleStatusShowForUnauthorizedOnlyWhenHoldingHammer = false,
  260. SimpleStatusShowAuthorizedProtectionPreview = false,
  261. SimpleStatusRefreshIntervalSeconds = 1f,
  262. SimpleStatusStatusId = "antiofflineraid.protection",
  263. SimpleStatusAuthorizedBackground = "#738D45",
  264. SimpleStatusUnauthorizedBackground = "#773A2F",
  265. SimpleStatusAuthorizedIconColor = "#A3FE03",
  266. SimpleStatusUnauthorizedIconColor = "#FF5338",
  267. SimpleStatusAuthorizedLabelColor = "#FFFFFF",
  268. SimpleStatusUnauthorizedLabelColor = "#FF5338",
  269. SimpleStatusAuthorizedValueColor = "#FFFFFF",
  270. SimpleStatusUnauthorizedValueColor = "#FF5338",
  271. SimpleStatusIcon = "https://i.ibb.co/nq6bL11S/protection.png",
  272. PlaySound = false,
  273. Prefabs = new List<string>
  274. {
  275. "door.hinged", "door.double.hinged", "window.bars", "floor.ladder.hatch",
  276. "floor.frame", "wall.frame", "shutter", "wall.external", "gates.external", "box", "locker"
  277. },
  278. Activities = GetDefaultActivities(),
  279. ServerTimeOffset = 0,
  280. SoundFile = "assets/prefabs/weapon mods/silencers/effects/silencer_attach.fx.prefab",
  281. ProtectBaseWhenAway = false,
  282. ProtectPlayerMadeBoats = true,
  283. ProtectTugboats = true,
  284. GroupSizeProtectionScaling = true,
  285. GroupSizeUseClansPluginMembers = false,
  286. GroupSizeUseRustTeams = false,
  287. GroupSizeUseRustClans = false,
  288. GroupSizeProtectionScale = new Dictionary<string, float>
  289. {
  290. { "1", 1f },
  291. { "3", 0.90f },
  292. { "5", 0.80f },
  293. { "8", 0.70f },
  294. { "12", 0.60f }
  295. },
  296. MinutesSinceLastAttackToProtect = 10,
  297. DamageScaleKeys = new List<int> { 1, 3, 6, 12, 48 },
  298. GroupSizeProtectionKeys = new List<int> { 1, 3, 5, 8, 12 }
  299. };
  300. }
  301. }
  302.  
  303. private static List<string> GetDefaultActivities()
  304. {
  305. return new List<string> { "input", "loot", "respawn", "chat", "wakeup" };
  306. }
  307.  
  308. private static string NormalizeSimpleStatusIconSetting(string value)
  309. {
  310. if (string.IsNullOrWhiteSpace(value)) return value;
  311.  
  312. var trimmed = value.Trim();
  313. if (trimmed.StartsWith("[", StringComparison.Ordinal) && trimmed.EndsWith(")", StringComparison.Ordinal))
  314. {
  315. var separator = trimmed.IndexOf("](", StringComparison.Ordinal);
  316. if (separator > 0)
  317. {
  318. var linkedValue = trimmed.Substring(separator + 2, trimmed.Length - separator - 3).Trim();
  319. if (!string.IsNullOrWhiteSpace(linkedValue)) return linkedValue;
  320. }
  321. }
  322.  
  323. return trimmed;
  324. }
  325.  
  326. private void NormalizeConfig()
  327. {
  328. var defaults = Configuration.DefaultConfig();
  329. if (_config.AbsoluteTimeScale == null) _config.AbsoluteTimeScale = new Dictionary<string, object>();
  330. if (_config.DamageScale == null || _config.DamageScale.Count == 0) _config.DamageScale = new Dictionary<string, object>(defaults.DamageScale);
  331. if (_config.GroupSizeProtectionScale == null || _config.GroupSizeProtectionScale.Count == 0) _config.GroupSizeProtectionScale = new Dictionary<string, float>(defaults.GroupSizeProtectionScale);
  332. if (_config.Activities == null) _config.Activities = GetDefaultActivities();
  333. if (_config.Prefabs == null) _config.Prefabs = new List<string>(defaults.Prefabs);
  334. if (string.IsNullOrWhiteSpace(_config.SimpleStatusStatusId)) _config.SimpleStatusStatusId = defaults.SimpleStatusStatusId;
  335. if (string.IsNullOrWhiteSpace(_config.SimpleStatusAuthorizedBackground)) _config.SimpleStatusAuthorizedBackground = defaults.SimpleStatusAuthorizedBackground;
  336. if (string.IsNullOrWhiteSpace(_config.SimpleStatusUnauthorizedBackground)) _config.SimpleStatusUnauthorizedBackground = defaults.SimpleStatusUnauthorizedBackground;
  337. if (string.IsNullOrWhiteSpace(_config.SimpleStatusAuthorizedIconColor)) _config.SimpleStatusAuthorizedIconColor = defaults.SimpleStatusAuthorizedIconColor;
  338. if (string.IsNullOrWhiteSpace(_config.SimpleStatusUnauthorizedIconColor)) _config.SimpleStatusUnauthorizedIconColor = defaults.SimpleStatusUnauthorizedIconColor;
  339. if (string.IsNullOrWhiteSpace(_config.SimpleStatusAuthorizedLabelColor)) _config.SimpleStatusAuthorizedLabelColor = defaults.SimpleStatusAuthorizedLabelColor;
  340. if (string.IsNullOrWhiteSpace(_config.SimpleStatusUnauthorizedLabelColor)) _config.SimpleStatusUnauthorizedLabelColor = defaults.SimpleStatusUnauthorizedLabelColor;
  341. if (string.IsNullOrWhiteSpace(_config.SimpleStatusAuthorizedValueColor)) _config.SimpleStatusAuthorizedValueColor = defaults.SimpleStatusAuthorizedValueColor;
  342. if (string.IsNullOrWhiteSpace(_config.SimpleStatusUnauthorizedValueColor)) _config.SimpleStatusUnauthorizedValueColor = defaults.SimpleStatusUnauthorizedValueColor;
  343. if (string.IsNullOrWhiteSpace(_config.SimpleStatusIcon)) _config.SimpleStatusIcon = defaults.SimpleStatusIcon;
  344. _config.SimpleStatusIcon = NormalizeSimpleStatusIconSetting(_config.SimpleStatusIcon);
  345. _config.SimpleStatusRefreshIntervalSeconds = Mathf.Clamp(_config.SimpleStatusRefreshIntervalSeconds, 0.25f, 5f);
  346. if (string.IsNullOrWhiteSpace(_config.SoundFile)) _config.SoundFile = defaults.SoundFile;
  347.  
  348. _config.Activities = _config.Activities.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim().ToLowerInvariant()).Distinct().ToList();
  349. _config.Prefabs = _config.Prefabs.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).Distinct().ToList();
  350. if (_config.AfkMinutes < 1)
  351. {
  352. PrintWarning($"AfkMinutes cannot be lower than 1; clamping configured value {_config.AfkMinutes} to 1.");
  353. _config.AfkMinutes = 1;
  354. }
  355. _config.CooldownMinutes = Math.Max(0, _config.CooldownMinutes);
  356. _config.ClansPluginMinMembers = Math.Max(1, _config.ClansPluginMinMembers);
  357. _config.RustTeamMinMembers = Math.Max(1, _config.RustTeamMinMembers);
  358. _config.RustClanMinMembers = Math.Max(1, _config.RustClanMinMembers);
  359. _config.MinutesSinceLastAttackToProtect = Math.Max(-1, _config.MinutesSinceLastAttackToProtect);
  360.  
  361. _config.DamageScaleKeys = ParseThresholdKeys(_config.DamageScale.Keys, "DamageScale");
  362. if (_config.DamageScaleKeys.Count == 0)
  363. {
  364. PrintWarning("DamageScale contains no valid numeric thresholds; restoring defaults.");
  365. _config.DamageScale = new Dictionary<string, object>(defaults.DamageScale);
  366. _config.DamageScaleKeys = new List<int>(defaults.DamageScaleKeys);
  367. }
  368.  
  369. _config.GroupSizeProtectionKeys = ParseThresholdKeys(_config.GroupSizeProtectionScale.Keys, "GroupSizeProtectionScale");
  370. if (_config.GroupSizeProtectionKeys.Count == 0)
  371. {
  372. PrintWarning("GroupSizeProtectionScale contains no valid numeric thresholds; restoring defaults.");
  373. _config.GroupSizeProtectionScale = new Dictionary<string, float>(defaults.GroupSizeProtectionScale);
  374. _config.GroupSizeProtectionKeys = new List<int>(defaults.GroupSizeProtectionKeys);
  375. }
  376. }
  377.  
  378. private List<int> ParseThresholdKeys(IEnumerable<string> keys, string settingName)
  379. {
  380. var result = new List<int>();
  381. foreach (var raw in keys ?? Enumerable.Empty<string>())
  382. {
  383. if (!int.TryParse(raw, out var key) || key < 0)
  384. {
  385. PrintWarning($"Ignoring invalid {settingName} threshold '{raw}'. Thresholds must be non-negative integers.");
  386. continue;
  387. }
  388. if (!result.Contains(key)) result.Add(key);
  389. }
  390. result.Sort();
  391. return result;
  392. }
  393. #endregion
  394.  
  395. #region 2.AntiOfflineRaid.Localization.cs
  396. private static class PluginMessages
  397. {
  398. public const string ProtectionMessage = "Protection Message";
  399. public const string DeniedPermission = "Denied: Permission";
  400. public const string InvalidSyntax = "Invalid Syntax";
  401. public const string PlayerNotFound = "Player Not Found";
  402. public const string HelpTitle = "Help Title";
  403. public const string HelpFirstMinutes = "Help First Minutes";
  404. public const string HelpBetweenMinutesAndHour = "Help Between Minutes And Hour";
  405. public const string HelpFirstHour = "Help First Hour";
  406. public const string HelpAfterDays = "Help After Days";
  407. public const string HelpAfterHours = "Help After Hours";
  408. public const string StatusHeader = "Status Header";
  409. public const string StatusPlayerAfk = "Status Player AFK";
  410. public const string StatusPlayerOffline = "Status Player Offline";
  411. public const string StatusPlayerOnline = "Status Player Online";
  412. public const string StatusAfkMinutes = "Status AFK Minutes";
  413. public const string StatusShare = "Status Share";
  414. public const string StatusShareLastOffline = "Status Share Last Offline";
  415. public const string ProviderClansPlugin = "Provider Clans Plugin";
  416. public const string ProviderRustTeam = "Provider Rust Team";
  417. public const string ProviderRustClan = "Provider Rust Clan";
  418. public const string StatusOnline = "Status Online";
  419. public const string StatusOffline = "Status Offline";
  420. public const string StatusScale = "Status Scale";
  421. public const string OfflineProtectionStatus = "Offline Protection Status";
  422. }
  423.  
  424. protected override void LoadDefaultMessages()
  425. {
  426. lang.RegisterMessages(new Dictionary<string, string>
  427. {
  428. [PluginMessages.ProtectionMessage] = "This building is protected: {amount}%",
  429. [PluginMessages.DeniedPermission] = "You lack permission to do that",
  430. [PluginMessages.InvalidSyntax] = "Invalid syntax. Use: /ao <player name>",
  431. [PluginMessages.PlayerNotFound] = "No player found.",
  432. [PluginMessages.HelpTitle] = "AntiOfflineRaid by <color=#ce422b>Shady14u</color>",
  433. [PluginMessages.HelpFirstMinutes] = "<color=#ffd479>First {0} minutes</color>: 100%",
  434. [PluginMessages.HelpBetweenMinutesAndHour] = "<color=#ffd479>Between {0} minutes and 1 hour</color>: {1}%",
  435. [PluginMessages.HelpFirstHour] = "<color=#ffd479>First hour</color>: {0}%",
  436. [PluginMessages.HelpAfterDays] = "<color=#ffd479>After {0} day(s)</color>: {1}%",
  437. [PluginMessages.HelpAfterHours] = "<color=#ffd479>After {0} hour(s)</color>: {1}%",
  438. [PluginMessages.StatusHeader] = "<color=#F31D07><size=15>AntiOfflineRaid Status</size></color>: {0}",
  439. [PluginMessages.StatusPlayerAfk] = "<color=#56C5D5>Player Status</color>: <color=#E49C63>AFK</color>: {0}",
  440. [PluginMessages.StatusPlayerOffline] = "<color=#56C5D5>Player Status</color>: <color=#F31D07>Offline</color>: {0}",
  441. [PluginMessages.StatusPlayerOnline] = "<color=#56C5D5>Player Status</color>: <color=#3CF348>Online</color>",
  442. [PluginMessages.StatusAfkMinutes] = "<color=#56C5D5>AFK</color>: {0} minutes",
  443. [PluginMessages.StatusShare] = "<color=#56C5D5>{0} Status</color>: {1} ({2} online)",
  444. [PluginMessages.StatusShareLastOffline] = "<color=#56C5D5>{0} Last Offline</color>: {1} - {2}",
  445. [PluginMessages.ProviderClansPlugin] = "Clans Plugin",
  446. [PluginMessages.ProviderRustTeam] = "Rust Team",
  447. [PluginMessages.ProviderRustClan] = "Rust Clan",
  448. [PluginMessages.StatusOnline] = "<color=#3CF348>Online</color>",
  449. [PluginMessages.StatusOffline] = "<color=#F31D07>Offline</color>",
  450. [PluginMessages.StatusScale] = "<color=#56C5D5>Protection</color>: {0}%",
  451. [PluginMessages.OfflineProtectionStatus] = "OFFLINE PROTECTION"
  452. }, this, "en");
  453. }
  454.  
  455. private string GetMsg(string key, object userId = null)
  456. {
  457. return lang.GetMessage(key, this, userId?.ToString());
  458. }
  459.  
  460. void SendMessage(HitInfo hitInfo, int amt = 100)
  461. {
  462. if (!(hitInfo?.Initiator is BasePlayer player)) return;
  463. try
  464. {
  465. ShowMessage(player, amt);
  466. }
  467. catch (Exception ex)
  468. {
  469. PrintWarning($"Unable to show protection message to {player.UserIDString}: {ex.Message}");
  470. }
  471. }
  472. #endregion
  473.  
  474. #region 3.AntiOfflineRaid.Permissions.cs
  475. private static class PluginPermissions
  476. {
  477. public const string Protect = "antiofflineraid.protect";
  478. public const string Check = "antiofflineraid.check";
  479.  
  480.  
  481. }
  482. private void LoadPermissions()
  483. {
  484. permission.RegisterPermission(PluginPermissions.Protect, this);
  485. permission.RegisterPermission(PluginPermissions.Check, this);
  486. }
  487.  
  488. bool HasPerm(BasePlayer p, string pe)
  489. {
  490. return permission.UserHasPermission(p.userID.ToString(), pe);
  491. }
  492.  
  493. bool HasPerm(string userid, string pe)
  494. {
  495. return permission.UserHasPermission(userid, pe);
  496. }
  497. #endregion
  498.  
  499. #region 4.AntiOfflineRaid.Data.cs
  500. private StoredData _storedData;
  501.  
  502. public class StoredData
  503. {
  504. public Dictionary<ulong, LastOnline> LastOnlines { get; set; } = new Dictionary<ulong, LastOnline>();
  505. public Dictionary<ulong, long> LastStructureAttackAt { get; set; } = new Dictionary<ulong, long>(); // Protection-scope net ID -> last attack time
  506. public Dictionary<string, List<string>> MembersCache { get; set; } = new Dictionary<string, List<string>>();
  507. }
  508.  
  509. #region BoilerPlate
  510. private void LoadData()
  511. {
  512. try
  513. {
  514. _storedData = Interface.GetMod().DataFileSystem.ReadObject<StoredData>("AntiOfflineRaid") ?? new StoredData();
  515. }
  516. catch (Exception e)
  517. {
  518. Puts(e.Message);
  519. Puts(e.StackTrace);
  520. _storedData = new StoredData();
  521. }
  522. EnsureDataInitialized();
  523. }
  524.  
  525. private void EnsureDataInitialized()
  526. {
  527. if (_storedData == null) _storedData = new StoredData();
  528. if (_storedData.LastOnlines == null) _storedData.LastOnlines = new Dictionary<ulong, LastOnline>();
  529. if (_storedData.LastStructureAttackAt == null) _storedData.LastStructureAttackAt = new Dictionary<ulong, long>();
  530. if (_storedData.MembersCache == null) _storedData.MembersCache = new Dictionary<string, List<string>>();
  531. }
  532.  
  533. private void SaveData()
  534. {
  535. EnsureDataInitialized();
  536. Interface.GetMod().DataFileSystem.WriteObject("AntiOfflineRaid", _storedData);
  537. }
  538.  
  539. private void ClearWipeScopedData()
  540. {
  541. EnsureDataInitialized();
  542. _storedData.LastStructureAttackAt.Clear();
  543. _storedData.MembersCache.Clear();
  544. _cachedScales.Clear();
  545. }
  546.  
  547. #endregion
  548. #endregion
  549.  
  550. #region 5.AntiOfflineRaid.Hooks.cs
  551. object OnEntityTakeDamage(BaseCombatEntity entity, HitInfo hitInfo)
  552. {
  553. if (hitInfo == null || hitInfo.damageTypes == null || entity == null) return null;
  554. if (hitInfo.damageTypes.Has(DamageType.Decay)) return null;
  555.  
  556. // BoatBuildingBlock forwards damage to its parent PlayerBoat. The child callback already applies protection, so do not scale the forwarded parent callback again
  557. if (entity is PlayerBoat && hitInfo.HitEntity != null && hitInfo.HitEntity != entity && FindNavalTarget(hitInfo.HitEntity) == entity) return null;
  558. return IsBlocked(entity) ? OnStructureAttack(entity, hitInfo) : null;
  559. }
  560.  
  561. void OnLootItem(BasePlayer player, Item item)
  562. {
  563. if (player != null) UpdateLastOnline(player.userID);
  564. }
  565.  
  566. void OnLootPlayer(BasePlayer target, BasePlayer looter)
  567. {
  568. if (looter != null) UpdateLastOnline(looter.userID);
  569. }
  570.  
  571. void OnPlayerChat(BasePlayer player, string message, ConVar.Chat.ChatChannel channel)
  572. {
  573. if (player != null) UpdateLastOnline(player.userID);
  574. }
  575.  
  576. void OnPlayerConnected(BasePlayer player)
  577. {
  578. if (player == null) return;
  579. UpdateLastOnline(player.userID);
  580. if (_storedData.LastOnlines.TryGetValue(player.userID, out var state) && state != null) state.LastPosition = player.transform.position;
  581.  
  582. var language = lang.GetLanguage(player.UserIDString);
  583. if (string.IsNullOrEmpty(language)) language = "en";
  584. _lastLangByUser[player.userID] = language;
  585.  
  586. if (_config?.SimpleStatusEnabled == true) NextTick(() => UpdateSimpleStatusFor(player));
  587. }
  588.  
  589. void OnPlayerDisconnected(BasePlayer player, string reason)
  590. {
  591. if (player == null) return;
  592. HideSimpleStatus(player);
  593. _lastInputActivityAt.Remove(player.userID);
  594. _lastLangByUser.Remove(player.userID);
  595.  
  596. LastOnline state = null;
  597. var preserveAfkOfflineState = _config?.TreatAfkAsOffline == true && _storedData?.LastOnlines != null &&
  598. _storedData.LastOnlines.TryGetValue(player.userID, out state) && state != null && state.IsAfk();
  599.  
  600. if (preserveAfkOfflineState)
  601. {
  602. // Preserve the moment AFK crossed into offline state instead of restarting the offline timer at disconnect.
  603. state.LastOnlineTime = GetAfkOfflineSince(state);
  604. state.AfkMinutes = Math.Max(state.AfkMinutes, _config.AfkMinutes);
  605. InvalidateDamageScale(player.userID);
  606. }
  607. else
  608. {
  609. UpdateLastOnline(player.userID);
  610. }
  611. }
  612.  
  613. void OnPlayerRespawned(BasePlayer player)
  614. {
  615. if (player == null) return;
  616. UpdateLastOnline(player.userID);
  617. if (_storedData.LastOnlines.TryGetValue(player.userID, out var state) && state != null) state.LastPosition = player.transform.position;
  618. if (_config?.SimpleStatusEnabled == true) NextTick(() => UpdateSimpleStatusFor(player));
  619. }
  620.  
  621. void OnPlayerSleepEnded(BasePlayer player)
  622. {
  623. if (player == null) return;
  624. UpdateLastOnline(player.userID);
  625. if (_storedData.LastOnlines.TryGetValue(player.userID, out var state) && state != null) state.LastPosition = player.transform.position;
  626. if (_config?.SimpleStatusEnabled == true) NextTick(() => UpdateSimpleStatusFor(player));
  627. }
  628.  
  629. void OnPlayerInput(BasePlayer player, InputState input)
  630. {
  631. if (player == null || input?.current == null || input.previous == null) return;
  632. if (input.current.buttons == 0 || input.previous.Equals(input.current)) return;
  633.  
  634. var now = Time.realtimeSinceStartup;
  635. if (_lastInputActivityAt.TryGetValue(player.userID, out var last) && now - last < InputActivityThrottleSeconds) return;
  636. _lastInputActivityAt[player.userID] = now;
  637. UpdateLastOnline(player.userID);
  638. }
  639.  
  640. private void OnActiveItemChanged(BasePlayer player, Item oldItem, Item newItem)
  641. {
  642. if (_config?.SimpleStatusEnabled != true || !_config.SimpleStatusShowForUnauthorizedOnlyWhenHoldingHammer || player == null) return;
  643. NextTick(() => UpdateSimpleStatusFor(player));
  644. }
  645.  
  646. private void OnPlayerLanguageChanged(BasePlayer player, string language)
  647. {
  648. if (player == null || !player.userID.IsSteamId()) return;
  649.  
  650. var current = string.IsNullOrWhiteSpace(language) ? "en" : language.Trim();
  651. _lastLangByUser[player.userID] = current;
  652. _simpleStatusStateKeys.Remove(player.userID);
  653.  
  654. if (_config?.SimpleStatusEnabled == true && SimpleStatus != null)
  655. {
  656. NextTick(() =>
  657. {
  658. if (player != null && !player.IsDestroyed && player.IsConnected) UpdateSimpleStatusFor(player);
  659. });
  660. }
  661. }
  662.  
  663. private void RefreshSimpleStatusAuthorizationState()
  664. {
  665. if (_config?.SimpleStatusEnabled != true) return;
  666. _simpleStatusStateKeys.Clear();
  667. NextTick(RefreshAllSimpleStatuses);
  668. }
  669.  
  670. private object OnCupboardAssign(BuildingPrivlidge privilege, ulong userId, BasePlayer player)
  671. {
  672. RefreshSimpleStatusAuthorizationState();
  673. return null;
  674. }
  675.  
  676. private void OnCupboardAuthorize(BuildingPrivlidge privilege, BasePlayer player)
  677. {
  678. RefreshSimpleStatusAuthorizationState();
  679. }
  680.  
  681. private void OnCupboardDeauthorize(BuildingPrivlidge privilege, BasePlayer player)
  682. {
  683. RefreshSimpleStatusAuthorizationState();
  684. }
  685.  
  686. private void OnCupboardClearList(BuildingPrivlidge privilege, BasePlayer player)
  687. {
  688. RefreshSimpleStatusAuthorizationState();
  689. }
  690.  
  691. private void OnCupboardAuthorize(VehiclePrivilege privilege, BasePlayer player)
  692. {
  693. RefreshSimpleStatusAuthorizationState();
  694. }
  695.  
  696. private void OnCupboardDeauthorize(VehiclePrivilege privilege, BasePlayer player)
  697. {
  698. RefreshSimpleStatusAuthorizationState();
  699. }
  700.  
  701. private void OnCupboardClearList(VehiclePrivilege privilege, BasePlayer player)
  702. {
  703. RefreshSimpleStatusAuthorizationState();
  704. }
  705.  
  706. void OnServerInitialized()
  707. {
  708. LoadPermissions();
  709. LoadData();
  710. if (_newSaveDetected) ClearWipeScopedData();
  711. _storedData.MembersCache.Clear();
  712. _newSaveDetected = false;
  713. _serverInitialized = true;
  714.  
  715. ResolveOptionalPluginReferences();
  716. if (HelpText != null) Puts("HelpText detected. /help integration is available when AllowHelpTextFromOtherPlugins is enabled.");
  717. ReportClansPluginAvailability(true);
  718. ReportSimpleStatusDependencies(true);
  719. if (_simpleStatusReady) RefreshSimpleStatusIntegration();
  720. else ScheduleSimpleStatusReadyFallback();
  721.  
  722. if (!_config.Activities.Any())
  723. {
  724. PrintWarning("At least one activity trigger must be configured for this plugin to operate.");
  725. ToggleHooks(new List<string>());
  726. }
  727. else
  728. {
  729. ToggleHooks(_config.Activities);
  730. UpdateLastOnlineAll(false);
  731. _lastOnlineTimer = timer.Repeat(AfkCheckIntervalMinutes * 60, 0, () => UpdateLastOnlineAll());
  732. }
  733. }
  734.  
  735. private void OnServerSave()
  736. {
  737. SaveData();
  738. }
  739.  
  740. private void OnNewSave(string strFilename)
  741. {
  742. _newSaveDetected = true;
  743. if (_storedData == null) return;
  744. ClearWipeScopedData();
  745. SaveData();
  746. }
  747.  
  748. void OnClanCreate(string tag)
  749. {
  750. if (!UsesClansPluginIntegration() || _storedData == null) return;
  751. CacheClansPluginClan(tag);
  752. }
  753.  
  754. void OnClanUpdate(string tag)
  755. {
  756. if (!UsesClansPluginIntegration() || _storedData == null) return;
  757. CacheClansPluginClan(tag);
  758. }
  759.  
  760. void OnClanDestroy(string tag)
  761. {
  762. RemoveCachedClansPluginClan(tag);
  763. }
  764.  
  765. void OnClanDisbanded(string tag, List<ulong> members)
  766. {
  767. RemoveCachedClansPluginClan(tag);
  768. }
  769.  
  770. void OnClanMemberJoined(string tag, ulong joining, List<ulong> members)
  771. {
  772. if (!UsesClansPluginIntegration() || _storedData == null) return;
  773. CacheClansPluginClan(tag);
  774. }
  775.  
  776. void OnClanMemberGone(string tag, ulong leaving, List<ulong> members)
  777. {
  778. if (!UsesClansPluginIntegration() || _storedData == null) return;
  779. CacheClansPluginClan(tag);
  780. }
  781.  
  782. private void RemoveCachedClansPluginClan(string tag)
  783. {
  784. if (!UsesClansPluginIntegration() || _storedData?.MembersCache == null || string.IsNullOrWhiteSpace(tag)) return;
  785. _storedData.MembersCache.Remove(tag);
  786. }
  787.  
  788. void OnPluginLoaded(Plugin plugin)
  789. {
  790. if (plugin == null) return;
  791.  
  792. if (IsClansPlugin(plugin))
  793. {
  794. Clans = plugin;
  795. _storedData?.MembersCache?.Clear();
  796. NextTick(() => ReportClansPluginAvailability());
  797. }
  798.  
  799. if (IsSimpleStatusPlugin(plugin))
  800. {
  801. SimpleStatus = plugin;
  802. if (_config?.SimpleStatusEnabled == true)
  803. {
  804. NextTick(() =>
  805. {
  806. ReportSimpleStatusDependencies();
  807. ScheduleSimpleStatusReadyFallback();
  808. });
  809. }
  810. }
  811.  
  812. if (IsImageLibraryPlugin(plugin))
  813. {
  814. ImageLibrary = plugin;
  815. ResetSimpleStatusIcon();
  816. if (_config?.SimpleStatusEnabled == true)
  817. {
  818. NextTick(() =>
  819. {
  820. ReportSimpleStatusDependencies();
  821. if (_simpleStatusReady) RefreshSimpleStatusIntegration();
  822. });
  823. }
  824. }
  825.  
  826. if (IsHelpTextPlugin(plugin))
  827. {
  828. HelpText = plugin;
  829. Puts("HelpText detected. /help integration is available when AllowHelpTextFromOtherPlugins is enabled.");
  830. }
  831. }
  832.  
  833. void OnPluginUnloaded(Plugin plugin)
  834. {
  835. if (plugin == null) return;
  836.  
  837. if (IsClansPlugin(plugin))
  838. {
  839. Clans = null;
  840. _storedData?.MembersCache?.Clear();
  841. NextTick(() => ReportClansPluginAvailability());
  842. }
  843.  
  844. if (IsSimpleStatusPlugin(plugin))
  845. {
  846. SimpleStatus = null;
  847. _simpleStatusReady = false;
  848. StopSimpleStatusIntegration(false);
  849. if (_config?.SimpleStatusEnabled == true) NextTick(() => ReportSimpleStatusDependencies());
  850. }
  851.  
  852. if (IsImageLibraryPlugin(plugin))
  853. {
  854. ImageLibrary = null;
  855. ResetSimpleStatusIcon();
  856. StopSimpleStatusIntegration(true);
  857. if (_config?.SimpleStatusEnabled == true) NextTick(() => ReportSimpleStatusDependencies());
  858. }
  859.  
  860. if (IsHelpTextPlugin(plugin)) HelpText = null;
  861. }
  862.  
  863. private void OnSimpleStatusReady()
  864. {
  865. _simpleStatusReady = true;
  866. if (!_serverInitialized || _config?.SimpleStatusEnabled != true) return;
  867. ResolveOptionalPluginReferences();
  868. NextTick(() =>
  869. {
  870. ResetSimpleStatusIcon();
  871. ReportSimpleStatusDependencies();
  872. RefreshSimpleStatusIntegration();
  873. });
  874. }
  875.  
  876. protected void ToggleHook(List<string> wakeupTriggers, string trigger, string hook)
  877. {
  878. if (wakeupTriggers.Contains(trigger))
  879. {
  880. Subscribe(hook);
  881. }
  882. else
  883. {
  884. Unsubscribe(hook);
  885. }
  886. }
  887.  
  888. protected void ToggleHooks(List<string> wakeupTriggers)
  889. {
  890. var defaultWakeupTriggers = GetDefaultActivities();
  891.  
  892. foreach (var trigger in defaultWakeupTriggers)
  893. {
  894. switch (trigger)
  895. {
  896. case "input":
  897. ToggleHook(wakeupTriggers, trigger, "OnPlayerInput");
  898. break;
  899. case "loot":
  900. ToggleHook(wakeupTriggers, trigger, "OnLootPlayer");
  901. ToggleHook(wakeupTriggers, trigger, "OnLootItem");
  902. break;
  903. case "respawn":
  904. ToggleHook(wakeupTriggers, trigger, "OnPlayerRespawned");
  905. break;
  906. case "chat":
  907. ToggleHook(wakeupTriggers, trigger, "OnPlayerChat");
  908. break;
  909. case "wakeup":
  910. ToggleHook(wakeupTriggers, trigger, "OnPlayerSleepEnded");
  911. break;
  912. }
  913. }
  914. }
  915.  
  916. private void Unload()
  917. {
  918. _lastOnlineTimer?.Destroy();
  919. _lastOnlineTimer = null;
  920. StopSimpleStatusIntegration(true);
  921. UpdateLastOnlineAll();
  922. SaveData();
  923. _cachedScales.Clear();
  924. _lastInputActivityAt.Clear();
  925. _lastLangByUser.Clear();
  926. _lastReportedClansPluginAvailable = null;
  927. _lastReportedSimpleStatusAvailable = null;
  928. _lastReportedImageLibraryAvailable = null;
  929. _serverInitialized = false;
  930. _simpleStatusReady = false;
  931. }
  932. #endregion
  933.  
  934. #region 6.AntiOfflineRaid.Commands.cs
  935. [ConsoleCommand("ao")]
  936. void CheckOfflineStatus(ConsoleSystem.Arg arg)
  937. {
  938. if (arg == null) return;
  939.  
  940. var player = arg.Connection?.player as BasePlayer;
  941. if (player != null && !HasPerm(player, PluginPermissions.Check) && arg.Connection.authLevel < 1)
  942. {
  943. SendReply(arg, GetMsg(PluginMessages.DeniedPermission, arg.Connection.userid));
  944. return;
  945. }
  946.  
  947. var userId = player?.UserIDString;
  948. var playerSearch = arg.Args != null && arg.Args.Length > 0 ? arg.Args[0].ToString() : null;
  949. if (string.IsNullOrWhiteSpace(playerSearch))
  950. {
  951. SendReply(arg, GetMsg(PluginMessages.InvalidSyntax, userId));
  952. return;
  953. }
  954.  
  955. SendReply(arg, SendStatus(playerSearch, userId));
  956. }
  957.  
  958. [ChatCommand("ao")]
  959. void CheckOfflineStatus(BasePlayer player, string command, string[] args)
  960. {
  961. if (player == null) return;
  962. if (!HasPerm(player, PluginPermissions.Check) && (player.net?.connection?.authLevel ?? 0) < 1)
  963. {
  964. SendReply(player, GetMsg(PluginMessages.DeniedPermission, player.UserIDString));
  965. return;
  966. }
  967.  
  968. if (args == null || args.Length < 1 || string.IsNullOrWhiteSpace(args[0]))
  969. {
  970. SendReply(player, GetMsg(PluginMessages.InvalidSyntax, player.UserIDString));
  971. return;
  972. }
  973.  
  974. SendReply(player, SendStatus(args[0], player.UserIDString));
  975. }
  976.  
  977. [ChatCommand("ao.fill.onlineTimes")]
  978. void CmdFillOnlines(BasePlayer player, string command, string[] args)
  979. {
  980. if (player == null || !player.IsAdmin) return;
  981. EnsureDataInitialized();
  982.  
  983. var changed = false;
  984. foreach (var basePlayer in covalence.Players.All)
  985. {
  986. if (!ulong.TryParse(basePlayer.Id, out var userId) || !userId.IsSteamId()) continue;
  987. if (_storedData.LastOnlines.ContainsKey(userId)) continue;
  988. UpdateLastOnline(userId);
  989. changed = true;
  990. }
  991.  
  992. if (changed) SaveData();
  993. }
  994.  
  995. void SendHelpText(BasePlayer player)
  996. {
  997. if (player == null) return;
  998. var userId = player.UserIDString;
  999. var stringBuilder = new StringBuilder(GetMsg(PluginMessages.HelpTitle, userId)).Append('\n');
  1000.  
  1001. if (_config.CooldownMinutes > 0)
  1002. {
  1003. stringBuilder.AppendFormat(GetMsg(PluginMessages.HelpFirstMinutes, userId), _config.CooldownMinutes).Append('\n');
  1004. stringBuilder.AppendFormat(GetMsg(PluginMessages.HelpBetweenMinutesAndHour, userId), _config.CooldownMinutes, Math.Round(_config.InterimDamage * 100f, 0)).Append('\n');
  1005. }
  1006. else
  1007. {
  1008. stringBuilder.AppendFormat(GetMsg(PluginMessages.HelpFirstHour, userId), Math.Round(_config.InterimDamage * 100f, 0)).Append('\n');
  1009. }
  1010.  
  1011. foreach (var key in _config.DamageScaleKeys)
  1012. {
  1013. if (!_config.DamageScale.TryGetValue(key.ToString(), out var rawScale)) continue;
  1014.  
  1015. double scaleValue;
  1016. try
  1017. {
  1018. scaleValue = Convert.ToDouble(rawScale, CultureInfo.InvariantCulture);
  1019. }
  1020. catch
  1021. {
  1022. continue;
  1023. }
  1024.  
  1025. var scale = Math.Round(scaleValue * 100, 0);
  1026. var hours = Math.Round(Convert.ToDouble(key), 1);
  1027. if (hours >= 24)
  1028. {
  1029. var days = Math.Round(hours / 24, 1);
  1030. stringBuilder.AppendFormat(GetMsg(PluginMessages.HelpAfterDays, userId), days, scale).Append('\n');
  1031. }
  1032. else
  1033. {
  1034. stringBuilder.AppendFormat(GetMsg(PluginMessages.HelpAfterHours, userId), hours, scale).Append('\n');
  1035. }
  1036. }
  1037.  
  1038. player.ChatMessage(stringBuilder.ToString());
  1039. }
  1040. #endregion
  1041.  
  1042. #region 7.AntiOfflineRaid.Classes.cs
  1043. public class LastOnline
  1044. {
  1045. [JsonIgnore] public float AfkMinutes = 0;
  1046.  
  1047. public long LastOnlineLong;
  1048.  
  1049. [JsonIgnore] public Vector3 LastPosition;
  1050.  
  1051. public ulong UserId;
  1052.  
  1053. [JsonConstructor]
  1054. public LastOnline(ulong userid, long lastOnlineLong)
  1055. {
  1056. UserId = userid;
  1057. LastOnlineLong = lastOnlineLong;
  1058. }
  1059.  
  1060. public LastOnline(ulong userId, DateTime lastOnline)
  1061. {
  1062. UserId = userId;
  1063. LastOnlineTime = lastOnline;
  1064. }
  1065.  
  1066. [JsonIgnore] public BasePlayer AarPlayer => BasePlayer.FindByID(UserId);
  1067.  
  1068. [JsonIgnore] public double Days => (DateTime.Now - LastOnlineTime).TotalDays;
  1069.  
  1070. [JsonIgnore] public double Hours => (DateTime.Now - LastOnlineTime).TotalHours;
  1071.  
  1072. [JsonIgnore]
  1073. public DateTime LastOnlineTime
  1074. {
  1075. get => DateTime.FromBinary(LastOnlineLong);
  1076.  
  1077. set => LastOnlineLong = value.ToBinary();
  1078. }
  1079.  
  1080. [JsonIgnore] public double Minutes => (DateTime.Now - LastOnlineTime).TotalMinutes;
  1081.  
  1082. public bool HasMoved(Vector3 position)
  1083. {
  1084. var moved = !LastPosition.Equals(position);
  1085. LastPosition = new Vector3(position.x, position.y, position.z);
  1086. return moved;
  1087. }
  1088.  
  1089. public bool IsAfk()
  1090. {
  1091. return Minutes >= _config.AfkMinutes;
  1092. }
  1093.  
  1094. public bool IsConnected()
  1095. {
  1096. var player = AarPlayer;
  1097. return player != null && player.IsConnected;
  1098. }
  1099.  
  1100. public bool IsOffline()
  1101. {
  1102. if (!IsConnected()) return true;
  1103. return _config.TreatAfkAsOffline && IsAfk();
  1104. }
  1105. }
  1106.  
  1107. class ScaleCacheItem
  1108. {
  1109. public DateTime Expires;
  1110. public float Scale;
  1111.  
  1112. public ScaleCacheItem(DateTime expires, float scale)
  1113. {
  1114. Expires = expires;
  1115. Scale = scale;
  1116. }
  1117. }
  1118. #endregion
  1119.  
  1120. #region 0.AntiOfflineRaid.cs
  1121. [PluginReference] Plugin Clans;
  1122. [PluginReference] Plugin SimpleStatus;
  1123. [PluginReference] Plugin ImageLibrary;
  1124. [PluginReference] Plugin HelpText;
  1125. private const string OfflineProtectionStatusIconKey = "AntiOfflineRaid_Protection_Icon";
  1126. private const string JsonMessage =
  1127. @"[{""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""}]}]";
  1128.  
  1129. private const float AfkCheckIntervalMinutes = 1f;
  1130. private const float NavalPresenceRadius = 30f;
  1131. private const float InputActivityThrottleSeconds = 1f;
  1132. private const float SimpleStatusIconCheckIntervalSeconds = 0.25f;
  1133. private const int SimpleStatusIconCheckAttempts = 120;
  1134. private readonly Dictionary<ulong, ScaleCacheItem> _cachedScales = new Dictionary<ulong, ScaleCacheItem>();
  1135. private readonly Dictionary<ulong, float> _lastInputActivityAt = new Dictionary<ulong, float>();
  1136. private readonly Dictionary<Type, System.Reflection.FieldInfo> _vehiclePrivilegeAuthorizedPlayersFieldByType = new Dictionary<Type, System.Reflection.FieldInfo>();
  1137. private Timer _lastOnlineTimer;
  1138. private Timer _simpleStatusTimer;
  1139. private Timer _languageWatchTimer;
  1140. private readonly Dictionary<ulong, string> _lastLangByUser = new Dictionary<ulong, string>();
  1141. private readonly Dictionary<ulong, string> _simpleStatusStateKeys = new Dictionary<ulong, string>();
  1142. private readonly HashSet<ulong> _simpleStatusVisiblePlayers = new HashSet<ulong>();
  1143. private bool? _lastReportedClansPluginAvailable;
  1144. private bool? _lastReportedSimpleStatusAvailable;
  1145. private bool? _lastReportedImageLibraryAvailable;
  1146. private bool _serverInitialized;
  1147. private bool _newSaveDetected;
  1148. private bool _simpleStatusReady;
  1149. private bool _simpleStatusCreated;
  1150. private bool _simpleStatusIconRegistered;
  1151. private bool _simpleStatusIconRegistrationPending;
  1152. private string _simpleStatusIconPendingKey;
  1153. private string _resolvedSimpleStatusIcon;
  1154.  
  1155. private bool IsClansPluginAvailable()
  1156. {
  1157. return Clans != null;
  1158. }
  1159.  
  1160. private bool UsesClansPluginIntegration()
  1161. {
  1162. return _config != null && (_config.ClansPluginShare || (_config.GroupSizeProtectionScaling && _config.GroupSizeUseClansPluginMembers));
  1163. }
  1164.  
  1165. private void ReportClansPluginAvailability(bool force = false)
  1166. {
  1167. if (!UsesClansPluginIntegration())
  1168. {
  1169. _lastReportedClansPluginAvailable = null;
  1170. return;
  1171. }
  1172.  
  1173. var available = IsClansPluginAvailable();
  1174. if (!force && _lastReportedClansPluginAvailable.HasValue && _lastReportedClansPluginAvailable.Value == available) return;
  1175. _lastReportedClansPluginAvailable = available;
  1176.  
  1177. if (available) Puts("Clans plugin detected. External clan support is enabled.");
  1178. else PrintWarning("Clans plugin support is enabled, but the Clans plugin is not loaded. External clan checks will be skipped until it becomes available.");
  1179. }
  1180.  
  1181. private static bool IsClansPlugin(Plugin plugin)
  1182. {
  1183. return plugin != null && string.Equals(plugin.Name, "Clans", StringComparison.OrdinalIgnoreCase);
  1184. }
  1185.  
  1186. private static bool IsSimpleStatusPlugin(Plugin plugin)
  1187. {
  1188. return plugin != null && (string.Equals(plugin.Name, "SimpleStatus", StringComparison.OrdinalIgnoreCase) || string.Equals(plugin.Name, "Simple Status", StringComparison.OrdinalIgnoreCase));
  1189. }
  1190.  
  1191. private static bool IsImageLibraryPlugin(Plugin plugin)
  1192. {
  1193. return plugin != null && (string.Equals(plugin.Name, "ImageLibrary", StringComparison.OrdinalIgnoreCase) || string.Equals(plugin.Name, "Image Library", StringComparison.OrdinalIgnoreCase));
  1194. }
  1195.  
  1196. private static bool IsHelpTextPlugin(Plugin plugin)
  1197. {
  1198. return plugin != null && (string.Equals(plugin.Name, "HelpText", StringComparison.OrdinalIgnoreCase) || string.Equals(plugin.Name, "Help Text", StringComparison.OrdinalIgnoreCase));
  1199. }
  1200.  
  1201. private void ResolveOptionalPluginReferences()
  1202. {
  1203. if (Clans == null) Clans = plugins.Find("Clans");
  1204. if (SimpleStatus == null) SimpleStatus = plugins.Find("SimpleStatus") ?? plugins.Find("Simple Status");
  1205. if (ImageLibrary == null) ImageLibrary = plugins.Find("ImageLibrary") ?? plugins.Find("Image Library");
  1206. if (HelpText == null) HelpText = plugins.Find("HelpText") ?? plugins.Find("Help Text");
  1207. }
  1208.  
  1209. private bool IsSimpleStatusIntegrationReady()
  1210. {
  1211. return _serverInitialized && _simpleStatusReady && _config?.SimpleStatusEnabled == true && SimpleStatus != null && ImageLibrary != null;
  1212. }
  1213.  
  1214. private void ScheduleSimpleStatusReadyFallback()
  1215. {
  1216. if (!_serverInitialized || _config?.SimpleStatusEnabled != true || SimpleStatus == null || _simpleStatusReady) return;
  1217. timer.Once(1f, () =>
  1218. {
  1219. if (!_serverInitialized || _config?.SimpleStatusEnabled != true || SimpleStatus == null || _simpleStatusReady) return;
  1220. _simpleStatusReady = true;
  1221. ReportSimpleStatusDependencies();
  1222. RefreshSimpleStatusIntegration();
  1223. });
  1224. }
  1225.  
  1226. private void ReportSimpleStatusDependencies(bool force = false)
  1227. {
  1228. if (_config?.SimpleStatusEnabled != true)
  1229. {
  1230. _lastReportedSimpleStatusAvailable = null;
  1231. _lastReportedImageLibraryAvailable = null;
  1232. return;
  1233. }
  1234.  
  1235. ResolveOptionalPluginReferences();
  1236.  
  1237. var simpleStatusAvailable = SimpleStatus != null;
  1238. if (force || !_lastReportedSimpleStatusAvailable.HasValue || _lastReportedSimpleStatusAvailable.Value != simpleStatusAvailable)
  1239. {
  1240. _lastReportedSimpleStatusAvailable = simpleStatusAvailable;
  1241. if (simpleStatusAvailable) Puts("SimpleStatus detected. Offline protection status integration is enabled.");
  1242. else PrintWarning("SimpleStatus support is enabled, but SimpleStatus is not loaded. Offline protection status display is unavailable.");
  1243. }
  1244.  
  1245. var imageLibraryAvailable = ImageLibrary != null;
  1246. if (force || !_lastReportedImageLibraryAvailable.HasValue || _lastReportedImageLibraryAvailable.Value != imageLibraryAvailable)
  1247. {
  1248. _lastReportedImageLibraryAvailable = imageLibraryAvailable;
  1249. if (imageLibraryAvailable) Puts("ImageLibrary detected. Offline protection status icon integration is enabled.");
  1250. else PrintWarning("SimpleStatus support is enabled, but ImageLibrary is not loaded. Offline protection status display is unavailable.");
  1251. }
  1252. }
  1253.  
  1254. private string GetClansPluginTag(ulong userId)
  1255. {
  1256. if (!userId.IsSteamId() || !IsClansPluginAvailable()) return null;
  1257. try
  1258. {
  1259. return Clans.Call<string>("GetClanOf", userId);
  1260. }
  1261. catch (Exception ex)
  1262. {
  1263. PrintWarning($"Unable to query clan for {userId}: {ex.Message}");
  1264. return null;
  1265. }
  1266. }
  1267.  
  1268. public List<string> CacheClansPluginClan(string tag)
  1269. {
  1270. if (string.IsNullOrWhiteSpace(tag) || !IsClansPluginAvailable()) return new List<string>();
  1271.  
  1272. try
  1273. {
  1274. var clan = Clans.Call<JObject>("GetClan", tag);
  1275. var members = new List<string>();
  1276. if (clan?["members"] != null)
  1277. {
  1278. foreach (var token in clan["members"])
  1279. {
  1280. var memberId = token?.ToString();
  1281. if (!string.IsNullOrWhiteSpace(memberId) && !members.Contains(memberId)) members.Add(memberId);
  1282. }
  1283. }
  1284. _storedData.MembersCache[tag] = members;
  1285. return members;
  1286. }
  1287. catch (Exception ex)
  1288. {
  1289. PrintWarning($"Unable to cache clan '{tag}': {ex.Message}");
  1290. return new List<string>();
  1291. }
  1292. }
  1293.  
  1294. float CacheDamageScale(ulong targetId, float scale, ScaleCacheItem kvp = null)
  1295. {
  1296. if (kvp == null)
  1297. {
  1298. _cachedScales[targetId] = new ScaleCacheItem(DateTime.Now.AddMinutes(1), scale);
  1299. }
  1300. else
  1301. {
  1302. kvp.Expires = DateTime.Now.AddMinutes(1);
  1303. kvp.Scale = scale;
  1304. }
  1305. return scale;
  1306. }
  1307.  
  1308. private void InvalidateDamageScale(ulong userId)
  1309. {
  1310. _cachedScales.Remove(userId);
  1311. }
  1312.  
  1313. protected IPlayer FindPlayerByPartialName(string nameOrIdOrIp)
  1314. {
  1315. return string.IsNullOrWhiteSpace(nameOrIdOrIp) ? null : covalence.Players.FindPlayer(nameOrIdOrIp);
  1316. }
  1317.  
  1318. public List<string> GetClansPluginMembers(string tag)
  1319. {
  1320. if (string.IsNullOrWhiteSpace(tag) || !IsClansPluginAvailable()) return new List<string>();
  1321. if (_storedData.MembersCache.TryGetValue(tag, out var memberList) && memberList != null) return memberList;
  1322. return CacheClansPluginClan(tag);
  1323. }
  1324.  
  1325. private HashSet<ulong> GetRustTeamMembers(ulong targetId)
  1326. {
  1327. var members = new HashSet<ulong>();
  1328. if (!targetId.IsSteamId()) return members;
  1329. members.Add(targetId);
  1330.  
  1331. try
  1332. {
  1333. var team = RelationshipManager.ServerInstance?.FindPlayersTeam(targetId);
  1334. if (team?.members == null) return members;
  1335. foreach (var memberId in team.members)
  1336. {
  1337. if (memberId.IsSteamId()) members.Add(memberId);
  1338. }
  1339. }
  1340. catch
  1341. {
  1342. }
  1343.  
  1344. return members;
  1345. }
  1346.  
  1347. private HashSet<ulong> GetRustClanMembers(ulong targetId)
  1348. {
  1349. var members = new HashSet<ulong>();
  1350. if (!targetId.IsSteamId()) return members;
  1351. members.Add(targetId);
  1352.  
  1353. try
  1354. {
  1355. var player = BasePlayer.FindByID(targetId) ?? BasePlayer.FindSleeping(targetId);
  1356. var clanId = player?.clanId ?? 0L;
  1357. if (clanId == 0L) return members;
  1358.  
  1359. IClan clan;
  1360. if (!(ClanManager.ServerInstance?.Backend?.TryGet(clanId, out clan) ?? false) || clan?.Members == null) return members;
  1361. foreach (var member in clan.Members)
  1362. {
  1363. if (member.SteamId.IsSteamId()) members.Add(member.SteamId);
  1364. }
  1365. }
  1366. catch (Exception ex)
  1367. {
  1368. PrintWarning($"Unable to query Rust native clan for {targetId}: {ex.Message}");
  1369. }
  1370.  
  1371. return members;
  1372. }
  1373.  
  1374. private HashSet<ulong> GetClansPluginMemberIds(ulong targetId)
  1375. {
  1376. var members = new HashSet<ulong>();
  1377. if (!targetId.IsSteamId()) return members;
  1378. members.Add(targetId);
  1379. if (!IsClansPluginAvailable()) return members;
  1380.  
  1381. var tag = GetClansPluginTag(targetId);
  1382. if (string.IsNullOrEmpty(tag)) return members;
  1383. foreach (var memberId in GetClansPluginMembers(tag))
  1384. {
  1385. if (ulong.TryParse(memberId, out var userId) && userId.IsSteamId()) members.Add(userId);
  1386. }
  1387. return members;
  1388. }
  1389.  
  1390. private bool UsesSharedProtectionGroups()
  1391. {
  1392. return _config != null && (_config.ClansPluginShare || _config.RustTeamShare || _config.RustClanShare);
  1393. }
  1394.  
  1395. private HashSet<ulong> GetSharedProtectionMembers(ulong targetId)
  1396. {
  1397. var members = new HashSet<ulong>();
  1398. if (!targetId.IsSteamId()) return members;
  1399. members.Add(targetId);
  1400.  
  1401. if (_config.ClansPluginShare && IsClansPluginAvailable()) members.UnionWith(GetClansPluginMemberIds(targetId));
  1402. if (_config.RustTeamShare) members.UnionWith(GetRustTeamMembers(targetId));
  1403. if (_config.RustClanShare) members.UnionWith(GetRustClanMembers(targetId));
  1404. return members;
  1405. }
  1406.  
  1407. private bool TryEvaluateSharedProtectionProvider(HashSet<ulong> members, int minOnlineMembers, out LastOnline selected, out bool groupOnline)
  1408. {
  1409. selected = null;
  1410. groupOnline = false;
  1411. if (members == null || members.Count == 0) return false;
  1412.  
  1413. minOnlineMembers = Math.Max(1, minOnlineMembers);
  1414. var onlineCount = members.Count(userId => !IsOffline(userId));
  1415. if (onlineCount >= minOnlineMembers)
  1416. {
  1417. groupOnline = true;
  1418. return true;
  1419. }
  1420.  
  1421. var candidates = new List<LastOnline>();
  1422. foreach (var userId in members)
  1423. {
  1424. if (_storedData.LastOnlines.TryGetValue(userId, out var lastOnline) && lastOnline != null && lastOnline.IsOffline()) candidates.Add(lastOnline);
  1425. }
  1426. if (candidates.Count == 0) return true;
  1427.  
  1428. // 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.
  1429. selected = candidates.OrderBy(x => x.Minutes).First();
  1430. return true;
  1431. }
  1432.  
  1433. private void AddSharedProtectionCandidate(HashSet<ulong> members, int minOnlineMembers, List<LastOnline> candidates, ref bool anyGroupOnline)
  1434. {
  1435. if (!TryEvaluateSharedProtectionProvider(members, minOnlineMembers, out var selected, out var groupOnline)) return;
  1436. if (groupOnline)
  1437. {
  1438. anyGroupOnline = true;
  1439. return;
  1440. }
  1441. if (selected != null) candidates.Add(selected);
  1442. }
  1443.  
  1444. private bool TryGetProtectionLastOnline(ulong targetId, out LastOnline protectionLastOnline)
  1445. {
  1446. protectionLastOnline = null;
  1447. if (!_storedData.LastOnlines.TryGetValue(targetId, out var ownerLastOnline) || ownerLastOnline == null) return false;
  1448.  
  1449. if (!UsesSharedProtectionGroups())
  1450. {
  1451. protectionLastOnline = ownerLastOnline;
  1452. return true;
  1453. }
  1454.  
  1455. var candidates = new List<LastOnline>();
  1456. var anyGroupOnline = false;
  1457.  
  1458. if (_config.ClansPluginShare && IsClansPluginAvailable())
  1459. AddSharedProtectionCandidate(GetClansPluginMemberIds(targetId), _config.ClansPluginMinMembers, candidates, ref anyGroupOnline);
  1460. if (_config.RustTeamShare)
  1461. AddSharedProtectionCandidate(GetRustTeamMembers(targetId), _config.RustTeamMinMembers, candidates, ref anyGroupOnline);
  1462. if (_config.RustClanShare)
  1463. AddSharedProtectionCandidate(GetRustClanMembers(targetId), _config.RustClanMinMembers, candidates, ref anyGroupOnline);
  1464.  
  1465. if (anyGroupOnline) return false;
  1466. if (candidates.Count == 0)
  1467. {
  1468. protectionLastOnline = ownerLastOnline;
  1469. return true;
  1470. }
  1471.  
  1472. // When multiple share providers apply, use the most recently offline selected member to avoid granting stronger protection than any enabled provider allows.
  1473. protectionLastOnline = candidates.OrderBy(x => x.Minutes).First();
  1474. return true;
  1475. }
  1476.  
  1477. private HashSet<ulong> GetAuthorizedClansPluginMembers(IEnumerable<ulong> authorizedIds)
  1478. {
  1479. var members = new HashSet<ulong>();
  1480. if (!_config.ClansPluginShare || !IsClansPluginAvailable() || authorizedIds == null) return members;
  1481.  
  1482. foreach (var userId in authorizedIds)
  1483. {
  1484. if (!userId.IsSteamId()) continue;
  1485. members.UnionWith(GetClansPluginMemberIds(userId));
  1486. }
  1487. return members;
  1488. }
  1489.  
  1490. private HashSet<ulong> GetAuthorizedRustTeamMembers(IEnumerable<ulong> authorizedIds)
  1491. {
  1492. var members = new HashSet<ulong>();
  1493. if (!_config.RustTeamShare || authorizedIds == null) return members;
  1494.  
  1495. foreach (var userId in authorizedIds)
  1496. {
  1497. if (!userId.IsSteamId()) continue;
  1498. members.UnionWith(GetRustTeamMembers(userId));
  1499. }
  1500. return members;
  1501. }
  1502.  
  1503. private HashSet<ulong> GetAuthorizedRustClanMembers(IEnumerable<ulong> authorizedIds)
  1504. {
  1505. var members = new HashSet<ulong>();
  1506. if (!_config.RustClanShare || authorizedIds == null) return members;
  1507.  
  1508. foreach (var userId in authorizedIds)
  1509. {
  1510. if (!userId.IsSteamId()) continue;
  1511. members.UnionWith(GetRustClanMembers(userId));
  1512. }
  1513. return members;
  1514. }
  1515.  
  1516. private void AddProtectionLastOnlineCandidates(IEnumerable<ulong> userIds, List<LastOnline> candidates)
  1517. {
  1518. if (userIds == null || candidates == null) return;
  1519.  
  1520. foreach (var userId in userIds)
  1521. {
  1522. if (!userId.IsSteamId() || !HasPerm(userId.ToString(), PluginPermissions.Protect)) continue;
  1523. if (!_storedData.LastOnlines.TryGetValue(userId, out var lastOnline) || lastOnline == null || !lastOnline.IsOffline()) continue;
  1524. if (candidates.All(x => x.UserId != userId)) candidates.Add(lastOnline);
  1525. }
  1526. }
  1527.  
  1528. private bool TryResolvePrivilegeProtectionSource(List<ulong> authorizedIds, out ulong targetId, out LastOnline protectionLastOnline, out bool groupOnline)
  1529. {
  1530. targetId = 0UL;
  1531. protectionLastOnline = null;
  1532. groupOnline = false;
  1533. if (authorizedIds == null || authorizedIds.Count == 0) return false;
  1534.  
  1535. var directMembers = new HashSet<ulong>(authorizedIds.Where(x => x.IsSteamId()));
  1536. if (directMembers.Count == 0) return false;
  1537.  
  1538. if (directMembers.Any(userId => !IsOffline(userId))) groupOnline = true;
  1539.  
  1540. var candidates = new List<LastOnline>();
  1541. AddProtectionLastOnlineCandidates(directMembers, candidates);
  1542.  
  1543. var clansMembers = GetAuthorizedClansPluginMembers(directMembers);
  1544. if (clansMembers.Count > 0)
  1545. {
  1546. if (clansMembers.Count(userId => !IsOffline(userId)) >= _config.ClansPluginMinMembers) groupOnline = true;
  1547. AddProtectionLastOnlineCandidates(clansMembers, candidates);
  1548. }
  1549.  
  1550. var teamMembers = GetAuthorizedRustTeamMembers(directMembers);
  1551. if (teamMembers.Count > 0)
  1552. {
  1553. if (teamMembers.Count(userId => !IsOffline(userId)) >= _config.RustTeamMinMembers) groupOnline = true;
  1554. AddProtectionLastOnlineCandidates(teamMembers, candidates);
  1555. }
  1556.  
  1557. var rustClanMembers = GetAuthorizedRustClanMembers(directMembers);
  1558. if (rustClanMembers.Count > 0)
  1559. {
  1560. if (rustClanMembers.Count(userId => !IsOffline(userId)) >= _config.RustClanMinMembers) groupOnline = true;
  1561. AddProtectionLastOnlineCandidates(rustClanMembers, candidates);
  1562. }
  1563.  
  1564. if (candidates.Count > 0)
  1565. {
  1566. protectionLastOnline = candidates.OrderBy(x => x.Minutes).First();
  1567. targetId = protectionLastOnline.UserId;
  1568. return targetId.IsSteamId();
  1569. }
  1570.  
  1571. foreach (var userId in directMembers)
  1572. {
  1573. if (!HasPerm(userId.ToString(), PluginPermissions.Protect)) continue;
  1574. if (!_storedData.LastOnlines.TryGetValue(userId, out var lastOnline) || lastOnline == null) continue;
  1575. targetId = userId;
  1576. protectionLastOnline = lastOnline;
  1577. return true;
  1578. }
  1579.  
  1580. return false;
  1581. }
  1582.  
  1583. private HashSet<ulong> GetPrivilegeAccessMembers(IEnumerable<ulong> authorizedIds)
  1584. {
  1585. var seeds = new HashSet<ulong>();
  1586. if (authorizedIds == null) return seeds;
  1587.  
  1588. foreach (var userId in authorizedIds)
  1589. if (userId.IsSteamId()) seeds.Add(userId);
  1590.  
  1591. var members = new HashSet<ulong>(seeds);
  1592. members.UnionWith(GetAuthorizedClansPluginMembers(seeds));
  1593. members.UnionWith(GetAuthorizedRustTeamMembers(seeds));
  1594. members.UnionWith(GetAuthorizedRustClanMembers(seeds));
  1595. return members;
  1596. }
  1597.  
  1598. private bool HasPrivilegeProtectionAccess(ulong viewerId, IEnumerable<ulong> authorizedIds)
  1599. {
  1600. return viewerId.IsSteamId() && GetPrivilegeAccessMembers(authorizedIds).Contains(viewerId);
  1601. }
  1602.  
  1603. private bool IsPrivilegeGroupInBuildingRange(IEnumerable<ulong> authorizedIds, BuildingPrivlidge privilege)
  1604. {
  1605. if (privilege == null) return false;
  1606. foreach (var userId in GetPrivilegeAccessMembers(authorizedIds))
  1607. if (IsPlayerNearBuildingPrivilege(userId, privilege)) return true;
  1608. return false;
  1609. }
  1610.  
  1611. private bool IsPrivilegeGroupNearEntity(IEnumerable<ulong> authorizedIds, BaseEntity entity)
  1612. {
  1613. if (entity == null) return false;
  1614. foreach (var userId in GetPrivilegeAccessMembers(authorizedIds))
  1615. {
  1616. var player = BasePlayer.FindByID(userId);
  1617. if (player != null && player.IsConnected && Vector3.Distance(player.transform.position, entity.transform.position) <= NavalPresenceRadius) return true;
  1618. }
  1619. return false;
  1620. }
  1621.  
  1622. private static ulong GetLockOwnerId(BaseEntity entity)
  1623. {
  1624. var lockEntity = entity?.GetSlot(BaseEntity.Slot.Lock);
  1625. return lockEntity != null && lockEntity.OwnerID.IsSteamId() ? lockEntity.OwnerID : 0UL;
  1626. }
  1627.  
  1628. private bool TryResolveEntityFallbackProtectionSource(BaseEntity entity, out ulong targetId, out LastOnline protectionLastOnline, out bool groupOnline)
  1629. {
  1630. targetId = 0UL;
  1631. protectionLastOnline = null;
  1632. groupOnline = false;
  1633. if (entity == null) return false;
  1634.  
  1635. var lockOwnerId = GetLockOwnerId(entity);
  1636. if (lockOwnerId.IsSteamId() && TryResolveFallbackProtectionSource(lockOwnerId, out targetId, out protectionLastOnline, out groupOnline)) return true;
  1637. if (entity.OwnerID.IsSteamId() && entity.OwnerID != lockOwnerId && TryResolveFallbackProtectionSource(entity.OwnerID, out targetId, out protectionLastOnline, out groupOnline)) return true;
  1638. return false;
  1639. }
  1640.  
  1641. private bool TryResolveNavalFallbackProtectionSource(BaseEntity attackedEntity, BaseEntity navalTarget, VehiclePrivilege privilege, out ulong targetId, out LastOnline protectionLastOnline, out bool groupOnline)
  1642. {
  1643. if (TryResolveEntityFallbackProtectionSource(attackedEntity, out targetId, out protectionLastOnline, out groupOnline)) return true;
  1644. if (privilege != null && privilege.OwnerID.IsSteamId() && TryResolveFallbackProtectionSource(privilege.OwnerID, out targetId, out protectionLastOnline, out groupOnline)) return true;
  1645. if (navalTarget != null && navalTarget.OwnerID.IsSteamId() && TryResolveFallbackProtectionSource(navalTarget.OwnerID, out targetId, out protectionLastOnline, out groupOnline)) return true;
  1646.  
  1647. targetId = 0UL;
  1648. protectionLastOnline = null;
  1649. groupOnline = false;
  1650. return false;
  1651. }
  1652.  
  1653. private bool HasFallbackProtectionAccess(ulong viewerId, ulong targetId)
  1654. {
  1655. if (!viewerId.IsSteamId() || !targetId.IsSteamId()) return false;
  1656. if (viewerId == targetId) return true;
  1657. return UsesSharedProtectionGroups() && GetSharedProtectionMembers(targetId).Contains(viewerId);
  1658. }
  1659.  
  1660. private int GetProtectionGroupSize(ulong targetId, IEnumerable<ulong> authorizedIds = null)
  1661. {
  1662. if (!_config.GroupSizeProtectionScaling) return 1;
  1663.  
  1664. var relevantUsers = new HashSet<ulong>();
  1665. var authorizedUsers = new HashSet<ulong>();
  1666. if (targetId.IsSteamId()) relevantUsers.Add(targetId);
  1667. if (authorizedIds != null)
  1668. {
  1669. foreach (var userId in authorizedIds)
  1670. {
  1671. if (!userId.IsSteamId()) continue;
  1672. authorizedUsers.Add(userId);
  1673. relevantUsers.Add(userId);
  1674. }
  1675. }
  1676.  
  1677. // TC / vehicle privilege authorization is always the baseline when group-size scaling is enabled.
  1678. var groupSize = Math.Max(1, authorizedUsers.Count);
  1679.  
  1680. if (_config.GroupSizeUseRustTeams)
  1681. {
  1682. foreach (var userId in relevantUsers)
  1683. groupSize = Math.Max(groupSize, GetRustTeamMembers(userId).Count);
  1684. }
  1685.  
  1686. if (_config.GroupSizeUseClansPluginMembers && IsClansPluginAvailable())
  1687. {
  1688. foreach (var userId in relevantUsers)
  1689. groupSize = Math.Max(groupSize, GetClansPluginMemberIds(userId).Count);
  1690. }
  1691.  
  1692. if (_config.GroupSizeUseRustClans)
  1693. {
  1694. foreach (var userId in relevantUsers)
  1695. groupSize = Math.Max(groupSize, GetRustClanMembers(userId).Count);
  1696. }
  1697.  
  1698. return Math.Max(1, groupSize);
  1699. }
  1700.  
  1701. private float ApplyGroupSizeProtectionScaling(float damageScale, int groupSize)
  1702. {
  1703. if (!_config.GroupSizeProtectionScaling || damageScale < 0f || damageScale >= 1f || groupSize <= 1) return damageScale;
  1704. if (_config.GroupSizeProtectionKeys == null || _config.GroupSizeProtectionKeys.Count == 0) return damageScale;
  1705.  
  1706. var eligibleThresholds = _config.GroupSizeProtectionKeys.Where(x => x <= groupSize).ToList();
  1707. if (eligibleThresholds.Count == 0) return damageScale;
  1708. var threshold = eligibleThresholds[eligibleThresholds.Count - 1];
  1709. if (!_config.GroupSizeProtectionScale.TryGetValue(threshold.ToString(), out var protectionFactor)) return damageScale;
  1710. protectionFactor = Mathf.Clamp01(protectionFactor);
  1711.  
  1712. var baseProtection = 1f - damageScale;
  1713. var adjustedProtection = baseProtection * protectionFactor;
  1714. return Mathf.Clamp01(1f - adjustedProtection);
  1715. }
  1716.  
  1717. private void ResetSimpleStatusIcon()
  1718. {
  1719. _simpleStatusIconRegistered = false;
  1720. _simpleStatusIconRegistrationPending = false;
  1721. _simpleStatusIconPendingKey = null;
  1722. _resolvedSimpleStatusIcon = null;
  1723. }
  1724.  
  1725. private string NormalizeSimpleStatusColor(string hex)
  1726. {
  1727. if (string.IsNullOrWhiteSpace(hex)) return "1 1 1 1";
  1728. var normalized = hex.Trim();
  1729. if (!normalized.StartsWith("#", StringComparison.Ordinal)) normalized = "#" + normalized;
  1730.  
  1731. if (!ColorUtility.TryParseHtmlString(normalized, out var color)) return "1 1 1 1";
  1732. color.a = 1f;
  1733. return string.Format(CultureInfo.InvariantCulture, "{0:0.###} {1:0.###} {2:0.###} {3:0.###}", color.r, color.g, color.b, color.a);
  1734. }
  1735.  
  1736. private static string GetSimpleStatusIconKey(string source)
  1737. {
  1738. unchecked
  1739. {
  1740. uint hash = 2166136261;
  1741. foreach (var character in source ?? string.Empty)
  1742. {
  1743. hash ^= character;
  1744. hash *= 16777619;
  1745. }
  1746. return $"{OfflineProtectionStatusIconKey}_{hash:X8}";
  1747. }
  1748. }
  1749.  
  1750. private bool HasImageLibraryImage(string imageKey)
  1751. {
  1752. if (ImageLibrary == null || string.IsNullOrWhiteSpace(imageKey)) return false;
  1753. try
  1754. {
  1755. return ImageLibrary.Call<bool>("HasImage", imageKey, 0UL);
  1756. }
  1757. catch
  1758. {
  1759. return false;
  1760. }
  1761. }
  1762.  
  1763. private void WaitForSimpleStatusIcon(string imageKey, int attemptsRemaining)
  1764. {
  1765. if (!_simpleStatusIconRegistrationPending || !string.Equals(_simpleStatusIconPendingKey, imageKey, StringComparison.Ordinal) || ImageLibrary == null) return;
  1766. if (HasImageLibraryImage(imageKey))
  1767. {
  1768. _simpleStatusIconRegistrationPending = false;
  1769. _simpleStatusIconPendingKey = null;
  1770. _simpleStatusIconRegistered = true;
  1771. _resolvedSimpleStatusIcon = imageKey;
  1772. NextTick(RefreshAllSimpleStatuses);
  1773. return;
  1774. }
  1775.  
  1776. if (attemptsRemaining <= 0)
  1777. {
  1778. _simpleStatusIconRegistrationPending = false;
  1779. _simpleStatusIconPendingKey = null;
  1780. PrintWarning("Unable to register the configured SimpleStatus icon in ImageLibrary.");
  1781. return;
  1782. }
  1783.  
  1784. timer.Once(SimpleStatusIconCheckIntervalSeconds, () => WaitForSimpleStatusIcon(imageKey, attemptsRemaining - 1));
  1785. }
  1786.  
  1787. private void BeginSimpleStatusIconRegistration(string imageKey, Func<bool> register)
  1788. {
  1789. if (string.IsNullOrWhiteSpace(imageKey) || register == null) return;
  1790. _simpleStatusIconRegistrationPending = true;
  1791. _simpleStatusIconPendingKey = imageKey;
  1792.  
  1793. var started = false;
  1794. try
  1795. {
  1796. started = register();
  1797. }
  1798. catch (Exception ex)
  1799. {
  1800. PrintWarning($"Unable to register SimpleStatus icon: {ex.Message}");
  1801. }
  1802.  
  1803. if (!started)
  1804. {
  1805. _simpleStatusIconRegistrationPending = false;
  1806. _simpleStatusIconPendingKey = null;
  1807. PrintWarning("ImageLibrary rejected the configured SimpleStatus icon registration request.");
  1808. return;
  1809. }
  1810.  
  1811. WaitForSimpleStatusIcon(imageKey, SimpleStatusIconCheckAttempts);
  1812. }
  1813.  
  1814. private void EnsureSimpleStatusIcon()
  1815. {
  1816. if (_simpleStatusIconRegistered || _simpleStatusIconRegistrationPending) return;
  1817. _resolvedSimpleStatusIcon = null;
  1818.  
  1819. var configuredIcon = _config?.SimpleStatusIcon;
  1820. if (string.IsNullOrWhiteSpace(configuredIcon)) return;
  1821. var icon = configuredIcon.Trim();
  1822.  
  1823. if (icon.StartsWith("assets/", StringComparison.OrdinalIgnoreCase) || icon.StartsWith("itemid:", StringComparison.OrdinalIgnoreCase) || icon.StartsWith("raw:", StringComparison.OrdinalIgnoreCase))
  1824. {
  1825. _resolvedSimpleStatusIcon = icon;
  1826. _simpleStatusIconRegistered = true;
  1827. return;
  1828. }
  1829.  
  1830. if (ImageLibrary == null) return;
  1831. if (HasImageLibraryImage(icon))
  1832. {
  1833. _resolvedSimpleStatusIcon = icon;
  1834. _simpleStatusIconRegistered = true;
  1835. return;
  1836. }
  1837.  
  1838. if (icon.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || icon.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
  1839. {
  1840. var imageKey = GetSimpleStatusIconKey(icon);
  1841. if (HasImageLibraryImage(imageKey))
  1842. {
  1843. _resolvedSimpleStatusIcon = imageKey;
  1844. _simpleStatusIconRegistered = true;
  1845. return;
  1846. }
  1847.  
  1848. BeginSimpleStatusIconRegistration(imageKey, () => ImageLibrary != null && ImageLibrary.Call<bool>("AddImage", icon, imageKey, 0UL));
  1849. return;
  1850. }
  1851.  
  1852. var dataDir = Interface.Oxide.DataDirectory;
  1853. var baseDir = Path.Combine(dataDir, "AntiOfflineRaid", "images");
  1854. try { Directory.CreateDirectory(baseDir); } catch { }
  1855.  
  1856. string filePath;
  1857. if (Path.IsPathRooted(icon)) filePath = icon;
  1858. else if (icon.Contains("/") || icon.Contains("\\")) filePath = Path.Combine(dataDir, icon.TrimStart('/', '\\'));
  1859. else filePath = Path.Combine(baseDir, icon);
  1860.  
  1861. if (!File.Exists(filePath))
  1862. {
  1863. PrintWarning($"Configured SimpleStatus icon file was not found: {filePath}");
  1864. return;
  1865. }
  1866.  
  1867. try
  1868. {
  1869. var imageKey = GetSimpleStatusIconKey($"{filePath}|{File.GetLastWriteTimeUtc(filePath).Ticks}");
  1870. if (HasImageLibraryImage(imageKey))
  1871. {
  1872. _resolvedSimpleStatusIcon = imageKey;
  1873. _simpleStatusIconRegistered = true;
  1874. return;
  1875. }
  1876.  
  1877. var bytes = File.ReadAllBytes(filePath);
  1878. BeginSimpleStatusIconRegistration(imageKey, () => ImageLibrary != null && ImageLibrary.Call<bool>("AddImageData", imageKey, bytes, 0UL));
  1879. }
  1880. catch (Exception ex)
  1881. {
  1882. PrintWarning($"Unable to register local SimpleStatus icon: {ex.Message}");
  1883. }
  1884. }
  1885.  
  1886. private void RefreshLanguageWatchTimer()
  1887. {
  1888. _languageWatchTimer?.Destroy();
  1889. _languageWatchTimer = null;
  1890. if (_config?.SimpleStatusEnabled != true || SimpleStatus == null || !_simpleStatusCreated) return;
  1891.  
  1892. foreach (var player in BasePlayer.activePlayerList)
  1893. {
  1894. if (player == null || !player.userID.IsSteamId()) continue;
  1895. var language = lang.GetLanguage(player.UserIDString);
  1896. if (string.IsNullOrEmpty(language)) language = "en";
  1897. _lastLangByUser[player.userID] = language;
  1898. }
  1899.  
  1900. _languageWatchTimer = timer.Every(3f, () =>
  1901. {
  1902. if (_config?.SimpleStatusEnabled != true || SimpleStatus == null || !_simpleStatusCreated) return;
  1903.  
  1904. foreach (var player in BasePlayer.activePlayerList)
  1905. {
  1906. if (player == null || !player.userID.IsSteamId()) continue;
  1907.  
  1908. var language = lang.GetLanguage(player.UserIDString);
  1909. if (string.IsNullOrEmpty(language)) language = "en";
  1910. if (_lastLangByUser.TryGetValue(player.userID, out var previous) && string.Equals(previous, language, StringComparison.OrdinalIgnoreCase)) continue;
  1911.  
  1912. _lastLangByUser[player.userID] = language;
  1913. _simpleStatusStateKeys.Remove(player.userID);
  1914. UpdateSimpleStatusFor(player);
  1915. }
  1916. });
  1917. }
  1918.  
  1919. private void RefreshSimpleStatusIntegration()
  1920. {
  1921. StopSimpleStatusIntegration(true);
  1922. if (!IsSimpleStatusIntegrationReady()) return;
  1923.  
  1924. EnsureSimpleStatusIcon();
  1925. var props = new Dictionary<string, object>
  1926. {
  1927. ["color"] = NormalizeSimpleStatusColor(_config.SimpleStatusAuthorizedBackground),
  1928. ["title"] = GetMsg(PluginMessages.OfflineProtectionStatus),
  1929. ["titleColor"] = NormalizeSimpleStatusColor(_config.SimpleStatusAuthorizedLabelColor),
  1930. ["text"] = "0%",
  1931. ["textColor"] = NormalizeSimpleStatusColor(_config.SimpleStatusAuthorizedValueColor),
  1932. ["icon"] = _resolvedSimpleStatusIcon,
  1933. ["iconColor"] = NormalizeSimpleStatusColor(_config.SimpleStatusAuthorizedIconColor)
  1934. };
  1935.  
  1936. try
  1937. {
  1938. SimpleStatus.Call("CreateStatus", this, _config.SimpleStatusStatusId, props);
  1939. _simpleStatusCreated = true;
  1940. }
  1941. catch (Exception ex)
  1942. {
  1943. _simpleStatusCreated = false;
  1944. PrintWarning($"Unable to create SimpleStatus offline protection status: {ex.Message}");
  1945. return;
  1946. }
  1947.  
  1948. _simpleStatusStateKeys.Clear();
  1949. _simpleStatusVisiblePlayers.Clear();
  1950. foreach (var player in BasePlayer.activePlayerList) HideSimpleStatus(player);
  1951.  
  1952. RefreshAllSimpleStatuses();
  1953. _simpleStatusTimer = timer.Every(_config.SimpleStatusRefreshIntervalSeconds, RefreshAllSimpleStatuses);
  1954. RefreshLanguageWatchTimer();
  1955. }
  1956.  
  1957. private void StopSimpleStatusIntegration(bool hideStatuses)
  1958. {
  1959. _simpleStatusTimer?.Destroy();
  1960. _simpleStatusTimer = null;
  1961. _languageWatchTimer?.Destroy();
  1962. _languageWatchTimer = null;
  1963.  
  1964. if (hideStatuses && SimpleStatus != null && _simpleStatusCreated)
  1965. {
  1966. foreach (var player in BasePlayer.activePlayerList)
  1967. HideSimpleStatus(player);
  1968. }
  1969.  
  1970. _simpleStatusCreated = false;
  1971. _simpleStatusStateKeys.Clear();
  1972. _simpleStatusVisiblePlayers.Clear();
  1973. }
  1974.  
  1975. private void RefreshAllSimpleStatuses()
  1976. {
  1977. if (!IsSimpleStatusIntegrationReady() || !_simpleStatusCreated) return;
  1978. foreach (var player in BasePlayer.activePlayerList)
  1979. UpdateSimpleStatusFor(player);
  1980. }
  1981.  
  1982. private void HideSimpleStatus(BasePlayer player)
  1983. {
  1984. if (player == null) return;
  1985. _simpleStatusStateKeys.Remove(player.userID);
  1986. if (SimpleStatus == null || _config == null)
  1987. {
  1988. _simpleStatusVisiblePlayers.Remove(player.userID);
  1989. return;
  1990. }
  1991.  
  1992. try
  1993. {
  1994. SimpleStatus.Call("SetStatus", player.UserIDString, _config.SimpleStatusStatusId, 0);
  1995. _simpleStatusVisiblePlayers.Remove(player.userID);
  1996. }
  1997. catch
  1998. {
  1999. // Keep the visible marker so the next refresh retries clearing a potentially stale status.
  2000. }
  2001. }
  2002.  
  2003. private void HideSimpleStatusIfVisible(BasePlayer player)
  2004. {
  2005. if (player == null) return;
  2006. if (!_simpleStatusVisiblePlayers.Contains(player.userID) && !_simpleStatusStateKeys.ContainsKey(player.userID)) return;
  2007. HideSimpleStatus(player);
  2008. }
  2009.  
  2010. private bool IsHoldingHammer(BasePlayer player)
  2011. {
  2012. var activeItem = player?.GetActiveItem();
  2013. return activeItem?.info != null && string.Equals(activeItem.info.shortname, "hammer", StringComparison.OrdinalIgnoreCase);
  2014. }
  2015.  
  2016. private void AppendBuildingPrivilegeAuthorizedIds(BuildingPrivlidge privilege, List<ulong> result)
  2017. {
  2018. if (privilege?.authorizedPlayers == null || result == null) return;
  2019. foreach (var entry in privilege.authorizedPlayers)
  2020. {
  2021. var userId = GetAuthorizationEntryUserId(entry);
  2022. if (userId.IsSteamId() && !result.Contains(userId)) result.Add(userId);
  2023. }
  2024. }
  2025.  
  2026. private static ulong GetProtectionScopeId(BaseEntity contextEntity, ulong fallbackTargetId)
  2027. {
  2028. if (contextEntity?.net != null && contextEntity.net.ID.Value != 0UL) return contextEntity.net.ID.Value;
  2029. return fallbackTargetId;
  2030. }
  2031.  
  2032. private DateTime GetAfkOfflineSince(LastOnline lastOnline)
  2033. {
  2034. if (lastOnline == null) return DateTime.Now;
  2035. var offlineSince = lastOnline.LastOnlineTime.AddMinutes(_config.AfkMinutes);
  2036. return offlineSince > DateTime.Now ? DateTime.Now : offlineSince;
  2037. }
  2038.  
  2039. private DateTime GetOfflineSince(LastOnline lastOnline)
  2040. {
  2041. if (lastOnline == null) return DateTime.Now;
  2042. if (lastOnline.IsConnected() && _config.TreatAfkAsOffline && lastOnline.IsAfk()) return GetAfkOfflineSince(lastOnline);
  2043. return lastOnline.LastOnlineTime;
  2044. }
  2045.  
  2046. private bool HasProtectionCooldownElapsed(LastOnline lastOnline)
  2047. {
  2048. if (lastOnline == null || !lastOnline.IsOffline()) return false;
  2049. if (_config.CooldownMinutes <= 0) return true;
  2050. return (DateTime.Now - GetOfflineSince(lastOnline)).TotalMinutes >= _config.CooldownMinutes;
  2051. }
  2052.  
  2053. private float GetCurrentBaseProtectionDamageScale(LastOnline lastOnline)
  2054. {
  2055. if (!HasProtectionCooldownElapsed(lastOnline)) return -1f;
  2056.  
  2057. var offlineSince = GetOfflineSince(lastOnline);
  2058. if (offlineSince <= lastOnline.LastOnlineTime) return ScaleDamageCached(lastOnline);
  2059.  
  2060. var effectiveState = new LastOnline(lastOnline.UserId, offlineSince)
  2061. {
  2062. AfkMinutes = lastOnline.AfkMinutes,
  2063. LastPosition = lastOnline.LastPosition
  2064. };
  2065. return ScaleDamage(effectiveState);
  2066. }
  2067.  
  2068. private bool IsProtectionDelayedForStatus(ulong protectionScopeId, LastOnline protectionLastOnline)
  2069. {
  2070. if (protectionLastOnline == null || !protectionLastOnline.IsOffline()) return false;
  2071. if (!HasProtectionCooldownElapsed(protectionLastOnline)) return true;
  2072.  
  2073. var delayMinutes = _config.MinutesSinceLastAttackToProtect;
  2074. if (delayMinutes <= 0) return false;
  2075. return (DateTime.Now - GetEffectiveProtectionStart(protectionScopeId, protectionLastOnline)).TotalMinutes <= delayMinutes;
  2076. }
  2077.  
  2078. private float GetOfflineProtectionPreviewScale(int groupSize)
  2079. {
  2080. if (_config.DamageScale == null || _config.DamageScaleKeys == null || _config.DamageScaleKeys.Count == 0) return 1f;
  2081.  
  2082. var firstThreshold = _config.DamageScaleKeys[0];
  2083. if (!_config.DamageScale.TryGetValue(firstThreshold.ToString(), out var rawScale)) return 1f;
  2084.  
  2085. try
  2086. {
  2087. var scale = Convert.ToSingle(rawScale, CultureInfo.InvariantCulture);
  2088. return ApplyGroupSizeProtectionScaling(Mathf.Clamp01(scale), groupSize);
  2089. }
  2090. catch
  2091. {
  2092. PrintWarning($"Invalid DamageScale value for preview threshold '{firstThreshold}'.");
  2093. return 1f;
  2094. }
  2095. }
  2096.  
  2097. private static bool IsSameEntity(BaseEntity first, BaseEntity second)
  2098. {
  2099. if (first == null || second == null) return false;
  2100. if (ReferenceEquals(first, second)) return true;
  2101. return first.net != null && second.net != null && first.net.ID.Value == second.net.ID.Value;
  2102. }
  2103.  
  2104. private BaseEntity GetSimpleStatusLookEntity(BasePlayer player)
  2105. {
  2106. if (player == null) return null;
  2107. RaycastHit hit;
  2108. if (!Physics.Raycast(player.eyes.HeadRay(), out hit, 8f)) return null;
  2109. return hit.GetEntity();
  2110. }
  2111.  
  2112. private bool IsEntityInBuildingPrivilege(BaseEntity entity, BuildingPrivlidge privilege)
  2113. {
  2114. if (entity == null) return false;
  2115. if (privilege == null) return entity.GetBuildingPrivilege() == null;
  2116. return IsSameEntity(entity.GetBuildingPrivilege(), privilege);
  2117. }
  2118.  
  2119. private bool IsEntityInNavalTarget(BaseEntity entity, BaseEntity navalTarget)
  2120. {
  2121. if (entity == null || navalTarget == null) return false;
  2122. if (IsSameEntity(entity, navalTarget)) return true;
  2123. return IsSameEntity(FindNavalTarget(entity), navalTarget);
  2124. }
  2125.  
  2126. private bool TryResolveFallbackProtectionSource(ulong baseTargetId, out ulong targetId, out LastOnline protectionLastOnline, out bool groupOnline)
  2127. {
  2128. targetId = 0UL;
  2129. protectionLastOnline = null;
  2130. groupOnline = false;
  2131. if (!baseTargetId.IsSteamId()) return false;
  2132.  
  2133. var candidates = new List<LastOnline>();
  2134. groupOnline = !IsOffline(baseTargetId);
  2135. AddProtectionLastOnlineCandidates(new[] { baseTargetId }, candidates);
  2136.  
  2137. if (_config.ClansPluginShare && IsClansPluginAvailable())
  2138. {
  2139. var members = GetClansPluginMemberIds(baseTargetId);
  2140. if (members.Count(userId => !IsOffline(userId)) >= _config.ClansPluginMinMembers) groupOnline = true;
  2141. AddProtectionLastOnlineCandidates(members, candidates);
  2142. }
  2143.  
  2144. if (_config.RustTeamShare)
  2145. {
  2146. var members = GetRustTeamMembers(baseTargetId);
  2147. if (members.Count(userId => !IsOffline(userId)) >= _config.RustTeamMinMembers) groupOnline = true;
  2148. AddProtectionLastOnlineCandidates(members, candidates);
  2149. }
  2150.  
  2151. if (_config.RustClanShare)
  2152. {
  2153. var members = GetRustClanMembers(baseTargetId);
  2154. if (members.Count(userId => !IsOffline(userId)) >= _config.RustClanMinMembers) groupOnline = true;
  2155. AddProtectionLastOnlineCandidates(members, candidates);
  2156. }
  2157.  
  2158. if (candidates.Count > 0)
  2159. {
  2160. protectionLastOnline = candidates.OrderBy(x => x.Minutes).First();
  2161. targetId = protectionLastOnline.UserId;
  2162. return targetId.IsSteamId();
  2163. }
  2164.  
  2165. if (!HasPerm(baseTargetId.ToString(), PluginPermissions.Protect)) return false;
  2166. if (!_storedData.LastOnlines.TryGetValue(baseTargetId, out protectionLastOnline) || protectionLastOnline == null) return false;
  2167. targetId = baseTargetId;
  2168. return true;
  2169. }
  2170.  
  2171. private bool TryGetCurrentProtectionScale(ulong targetId, LastOnline protectionLastOnline, bool groupOnline, bool groupInRange, int groupSize, ulong protectionScopeId, out float damageScale)
  2172. {
  2173. damageScale = -1f;
  2174. if (!targetId.IsSteamId() || protectionLastOnline == null || !HasPerm(targetId.ToString(), PluginPermissions.Protect)) return false;
  2175. if (IsProtectionDelayedForStatus(protectionScopeId, protectionLastOnline)) return false;
  2176.  
  2177. if (!groupOnline)
  2178. {
  2179. damageScale = ApplyGroupSizeProtectionScaling(GetProtectionDamageScale(protectionScopeId, protectionLastOnline), groupSize);
  2180. return damageScale >= 0f && damageScale < 1f;
  2181. }
  2182.  
  2183. if (_config.ProtectBaseWhenAway && !groupInRange)
  2184. {
  2185. damageScale = ApplyGroupSizeProtectionScaling(0f, groupSize);
  2186. return damageScale < 1f;
  2187. }
  2188.  
  2189. return false;
  2190. }
  2191.  
  2192. private int DamageScaleToProtectionPercent(float damageScale)
  2193. {
  2194. if (damageScale < 0f) return 0;
  2195. return Mathf.Clamp(Mathf.RoundToInt((1f - Mathf.Clamp01(damageScale)) * 100f), 0, 100);
  2196. }
  2197.  
  2198. private void UpdateSimpleStatusFor(BasePlayer player)
  2199. {
  2200. if (!IsSimpleStatusIntegrationReady() || !_simpleStatusCreated || player == null || !player.IsConnected || !player.userID.IsSteamId()) return;
  2201.  
  2202. var authorizedIds = Pool.Get<List<ulong>>();
  2203. try
  2204. {
  2205. authorizedIds.Clear();
  2206. var hasAccess = false;
  2207. var actualProtectionActive = false;
  2208. var damageScale = -1f;
  2209. var targetId = 0UL;
  2210. var groupSize = 1;
  2211. var privilegeBased = false;
  2212.  
  2213. var navalTarget = FindNavalTarget(player);
  2214. if (navalTarget == null && player.GetMounted() != null) navalTarget = FindNavalTarget(player.GetMounted());
  2215. if (navalTarget != null)
  2216. {
  2217. if (navalTarget is PlayerBoat && !_config.ProtectPlayerMadeBoats)
  2218. {
  2219. HideSimpleStatusIfVisible(player);
  2220. return;
  2221. }
  2222. if (navalTarget is Tugboat && !_config.ProtectTugboats)
  2223. {
  2224. HideSimpleStatusIfVisible(player);
  2225. return;
  2226. }
  2227.  
  2228. var privilege = GetNavalVehiclePrivilege(navalTarget);
  2229. if (privilege != null && AppendVehiclePrivilegeAuthorizedIds(privilege, authorizedIds) && authorizedIds.Count > 0)
  2230. {
  2231. privilegeBased = true;
  2232. if (!TryResolvePrivilegeProtectionSource(authorizedIds, out targetId, out var protectionLastOnline, out var groupOnline))
  2233. {
  2234. HideSimpleStatusIfVisible(player);
  2235. return;
  2236. }
  2237.  
  2238. groupSize = GetProtectionGroupSize(targetId, authorizedIds);
  2239. hasAccess = HasPrivilegeProtectionAccess(player.userID, authorizedIds);
  2240. var groupInRange = !_config.ProtectBaseWhenAway || IsPrivilegeGroupNearEntity(authorizedIds, navalTarget);
  2241. actualProtectionActive = TryGetCurrentProtectionScale(targetId, protectionLastOnline, groupOnline, groupInRange, groupSize, GetProtectionScopeId(privilege, targetId), out damageScale);
  2242. }
  2243. else
  2244. {
  2245. var lookedEntity = GetSimpleStatusLookEntity(player);
  2246. var lookedCombatEntity = lookedEntity as BaseCombatEntity;
  2247. if (lookedCombatEntity == null || !IsBlocked(lookedCombatEntity) || !IsEntityInNavalTarget(lookedEntity, navalTarget))
  2248. {
  2249. HideSimpleStatusIfVisible(player);
  2250. return;
  2251. }
  2252.  
  2253. if (!TryResolveNavalFallbackProtectionSource(lookedEntity, navalTarget, privilege, out targetId, out var protectionLastOnline, out var groupOnline))
  2254. {
  2255. HideSimpleStatusIfVisible(player);
  2256. return;
  2257. }
  2258.  
  2259. groupSize = GetProtectionGroupSize(targetId);
  2260. hasAccess = HasFallbackProtectionAccess(player.userID, targetId);
  2261. var groupInRange = !_config.ProtectBaseWhenAway || IsPlayerOrGroupNearEntity(targetId, navalTarget);
  2262. actualProtectionActive = TryGetCurrentProtectionScale(targetId, protectionLastOnline, groupOnline, groupInRange, groupSize, GetProtectionScopeId(lookedEntity, targetId), out damageScale);
  2263. }
  2264. }
  2265. else
  2266. {
  2267. var lookedEntity = GetSimpleStatusLookEntity(player);
  2268. var lookedCombatEntity = lookedEntity as BaseCombatEntity;
  2269. var lookedEntityValid = lookedCombatEntity != null && IsBlocked(lookedCombatEntity);
  2270.  
  2271. var lookedPrivilege = lookedEntityValid ? lookedEntity.GetBuildingPrivilege() : null;
  2272. if (lookedPrivilege != null && lookedPrivilege.IsDestroyed) lookedPrivilege = null;
  2273.  
  2274. var playerPrivilege = player.GetBuildingPrivilege();
  2275. if (playerPrivilege != null && playerPrivilege.IsDestroyed) playerPrivilege = null;
  2276.  
  2277. // Prefer the privilege of the protected entity being looked at, then fall back to the player's current privilege.
  2278. var buildingPrivilege = lookedPrivilege ?? playerPrivilege;
  2279. if (buildingPrivilege != null) AppendBuildingPrivilegeAuthorizedIds(buildingPrivilege, authorizedIds);
  2280.  
  2281. if (buildingPrivilege != null && authorizedIds.Count > 0)
  2282. {
  2283. privilegeBased = true;
  2284. if (!TryResolvePrivilegeProtectionSource(authorizedIds, out targetId, out var protectionLastOnline, out var groupOnline))
  2285. {
  2286. HideSimpleStatusIfVisible(player);
  2287. return;
  2288. }
  2289.  
  2290. groupSize = GetProtectionGroupSize(targetId, authorizedIds);
  2291. hasAccess = HasPrivilegeProtectionAccess(player.userID, authorizedIds);
  2292. var groupInRange = !_config.ProtectBaseWhenAway || IsPrivilegeGroupInBuildingRange(authorizedIds, buildingPrivilege);
  2293. actualProtectionActive = TryGetCurrentProtectionScale(targetId, protectionLastOnline, groupOnline, groupInRange, groupSize, GetProtectionScopeId(buildingPrivilege, targetId), out damageScale);
  2294. }
  2295. else
  2296. {
  2297. if (!lookedEntityValid || !IsEntityInBuildingPrivilege(lookedEntity, buildingPrivilege))
  2298. {
  2299. HideSimpleStatusIfVisible(player);
  2300. return;
  2301. }
  2302.  
  2303. if (!TryResolveEntityFallbackProtectionSource(lookedEntity, out targetId, out var protectionLastOnline, out var groupOnline))
  2304. {
  2305. HideSimpleStatusIfVisible(player);
  2306. return;
  2307. }
  2308.  
  2309. groupSize = GetProtectionGroupSize(targetId);
  2310. hasAccess = HasFallbackProtectionAccess(player.userID, targetId);
  2311. var groupInRange = !_config.ProtectBaseWhenAway || IsProtectionGroupInRange(targetId, buildingPrivilege);
  2312. actualProtectionActive = TryGetCurrentProtectionScale(targetId, protectionLastOnline, groupOnline, groupInRange, groupSize, GetProtectionScopeId(lookedEntity, targetId), out damageScale);
  2313. }
  2314. }
  2315.  
  2316. if (!hasAccess && _config.SimpleStatusShowForUnauthorizedOnlyWhenHoldingHammer && !IsHoldingHammer(player))
  2317. {
  2318. HideSimpleStatusIfVisible(player);
  2319. return;
  2320. }
  2321.  
  2322. if (hasAccess && !actualProtectionActive)
  2323. {
  2324. // Authorized viewers can preview the first normal protection tier while real protection is inactive.
  2325. damageScale = _config.SimpleStatusShowAuthorizedProtectionPreview ? GetOfflineProtectionPreviewScale(groupSize) : 1f;
  2326. }
  2327. else if (!hasAccess && !actualProtectionActive)
  2328. {
  2329. // Unauthorized viewers keep the red status visible even when protection is currently inactive.
  2330. damageScale = 1f;
  2331. }
  2332.  
  2333. var protectionPercent = DamageScaleToProtectionPercent(damageScale);
  2334.  
  2335. var label = GetMsg(PluginMessages.OfflineProtectionStatus, player.UserIDString);
  2336. var background = hasAccess ? _config.SimpleStatusAuthorizedBackground : _config.SimpleStatusUnauthorizedBackground;
  2337. var iconColor = hasAccess ? _config.SimpleStatusAuthorizedIconColor : _config.SimpleStatusUnauthorizedIconColor;
  2338. var labelColor = hasAccess ? _config.SimpleStatusAuthorizedLabelColor : _config.SimpleStatusUnauthorizedLabelColor;
  2339. var valueColor = hasAccess ? _config.SimpleStatusAuthorizedValueColor : _config.SimpleStatusUnauthorizedValueColor;
  2340. var stateKey = $"{targetId}:{privilegeBased}:{hasAccess}:{protectionPercent}:{label}:{background}:{iconColor}:{labelColor}:{valueColor}:{_resolvedSimpleStatusIcon}";
  2341. if (_simpleStatusStateKeys.TryGetValue(player.userID, out var previousState) && previousState == stateKey) return;
  2342. _simpleStatusStateKeys[player.userID] = stateKey;
  2343.  
  2344. SimpleStatus.Call("SetStatus", player.UserIDString, _config.SimpleStatusStatusId, int.MaxValue);
  2345. _simpleStatusVisiblePlayers.Add(player.userID);
  2346. SimpleStatus.Call("SetStatusProperty", player.UserIDString, _config.SimpleStatusStatusId, new Dictionary<string, object>
  2347. {
  2348. ["color"] = NormalizeSimpleStatusColor(background),
  2349. ["title"] = label,
  2350. ["titleColor"] = NormalizeSimpleStatusColor(labelColor),
  2351. ["text"] = protectionPercent + "%",
  2352. ["textColor"] = NormalizeSimpleStatusColor(valueColor),
  2353. ["icon"] = _resolvedSimpleStatusIcon,
  2354. ["iconColor"] = NormalizeSimpleStatusColor(iconColor)
  2355. });
  2356. }
  2357. catch (Exception ex)
  2358. {
  2359. HideSimpleStatusIfVisible(player);
  2360. PrintWarning($"Unable to update SimpleStatus for {player.UserIDString}: {ex.Message}");
  2361. }
  2362. finally
  2363. {
  2364. Pool.FreeUnmanaged(ref authorizedIds);
  2365. }
  2366. }
  2367.  
  2368. void HideMessage(BasePlayer player)
  2369. {
  2370. if (player == null || player.net?.connection == null) return;
  2371. try
  2372. {
  2373. CuiHelper.DestroyUi(player, "AntiOfflineRaidMsg");
  2374. }
  2375. catch (Exception ex)
  2376. {
  2377. PrintWarning($"Unable to hide protection message for {player.UserIDString}: {ex.Message}");
  2378. }
  2379. }
  2380.  
  2381. private List<ulong> GetBuildingAuthorizedIds(BaseEntity entity)
  2382. {
  2383. var result = new List<ulong>();
  2384. var privilege = entity?.GetBuildingPrivilege();
  2385. if (privilege?.authorizedPlayers == null) return result;
  2386. foreach (var entry in privilege.authorizedPlayers)
  2387. {
  2388. var userId = GetAuthorizationEntryUserId(entry);
  2389. if (userId.IsSteamId() && !result.Contains(userId)) result.Add(userId);
  2390. }
  2391. return result;
  2392. }
  2393.  
  2394. private BaseEntity FindNavalTarget(BaseEntity entity)
  2395. {
  2396. var current = entity;
  2397. for (var depth = 0; current != null && depth < 16; depth++)
  2398. {
  2399. if (current is PlayerBoat || current is Tugboat) return current;
  2400. current = current.GetParentEntity();
  2401. }
  2402. return null;
  2403. }
  2404.  
  2405. private static bool IsDescendantOfEntity(BaseEntity entity, BaseEntity root)
  2406. {
  2407. if (entity == null || root == null) return false;
  2408. var current = entity;
  2409. for (var depth = 0; current != null && depth < 16; depth++)
  2410. {
  2411. if (current == root) return true;
  2412. current = current.GetParentEntity();
  2413. }
  2414. return false;
  2415. }
  2416.  
  2417. private VehiclePrivilege GetNavalVehiclePrivilege(BaseEntity navalTarget)
  2418. {
  2419. if (navalTarget == null) return null;
  2420. var privilege = navalTarget.GetComponentInChildren<VehiclePrivilege>(true);
  2421. if (privilege != null) return privilege;
  2422.  
  2423. var privileges = Pool.Get<List<VehiclePrivilege>>();
  2424. try
  2425. {
  2426. Vis.Entities(navalTarget.transform.position, 20f, privileges, Rust.Layers.Mask.Deployed);
  2427. foreach (var candidate in privileges)
  2428. {
  2429. if (candidate == null || candidate.IsDestroyed || !IsDescendantOfEntity(candidate, navalTarget)) continue;
  2430. return candidate;
  2431. }
  2432. }
  2433. finally
  2434. {
  2435. Pool.FreeUnmanaged(ref privileges);
  2436. }
  2437.  
  2438. return null;
  2439. }
  2440.  
  2441. private bool AppendVehiclePrivilegeAuthorizedIds(VehiclePrivilege privilege, List<ulong> result)
  2442. {
  2443. if (privilege == null || result == null) return false;
  2444. try
  2445. {
  2446. var privilegeType = privilege.GetType();
  2447. if (!_vehiclePrivilegeAuthorizedPlayersFieldByType.TryGetValue(privilegeType, out var field))
  2448. {
  2449. var type = privilegeType;
  2450. while (type != null && field == null)
  2451. {
  2452. field = type.GetField("authorizedPlayers", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.DeclaredOnly);
  2453. type = type.BaseType;
  2454. }
  2455. _vehiclePrivilegeAuthorizedPlayersFieldByType[privilegeType] = field;
  2456. }
  2457.  
  2458. if (field == null) return false;
  2459. var values = field.GetValue(privilege) as IEnumerable;
  2460. if (values == null) return false;
  2461.  
  2462. foreach (var value in values)
  2463. {
  2464. var userId = GetAuthorizationEntryUserId(value);
  2465. if (userId.IsSteamId() && !result.Contains(userId)) result.Add(userId);
  2466. }
  2467. return true;
  2468. }
  2469. catch (Exception ex)
  2470. {
  2471. PrintWarning($"Unable to read naval privilege authorization: {ex.Message}");
  2472. return false;
  2473. }
  2474. }
  2475.  
  2476. private static ulong GetAuthorizationEntryUserId(object value)
  2477. {
  2478. if (value == null) return 0UL;
  2479. if (value is ulong userId) return userId;
  2480.  
  2481. try
  2482. {
  2483. var type = value.GetType();
  2484. const System.Reflection.BindingFlags flags = System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic;
  2485. var field = type.GetField("userid", flags) ?? type.GetField("userID", flags) ?? type.GetField("UserID", flags);
  2486. if (field != null)
  2487. {
  2488. var raw = field.GetValue(value);
  2489. if (raw is ulong fieldUserId) return fieldUserId;
  2490. if (raw != null && ulong.TryParse(raw.ToString(), out fieldUserId)) return fieldUserId;
  2491. }
  2492.  
  2493. var property = type.GetProperty("userid", flags) ?? type.GetProperty("userID", flags) ?? type.GetProperty("UserID", flags);
  2494. if (property != null)
  2495. {
  2496. var raw = property.GetValue(value, null);
  2497. if (raw is ulong propertyUserId) return propertyUserId;
  2498. if (raw != null && ulong.TryParse(raw.ToString(), out propertyUserId)) return propertyUserId;
  2499. }
  2500. }
  2501. catch
  2502. {
  2503. }
  2504. return 0UL;
  2505. }
  2506.  
  2507. private BoatBuildingStation FindRelevantBoatBuildingStation(BoatBuildingBlock block)
  2508. {
  2509. if (block == null || block.IsDestroyed) return null;
  2510. var current = block.GetParentEntity();
  2511. for (var depth = 0; current != null && depth < 16; depth++)
  2512. {
  2513. if (current is BoatBuildingStation parentStation) return parentStation;
  2514. current = current.GetParentEntity();
  2515. }
  2516.  
  2517. var stations = Pool.Get<List<BoatBuildingStation>>();
  2518. try
  2519. {
  2520. Vis.Entities(block.transform.position, 30f, stations, Rust.Layers.Mask.Deployed);
  2521. BoatBuildingStation nearest = null;
  2522. var nearestDistanceSqr = float.MaxValue;
  2523. foreach (var station in stations)
  2524. {
  2525. if (station == null || station.IsDestroyed || !station.OwnerID.IsSteamId()) continue;
  2526. if (block.OwnerID.IsSteamId() && station.OwnerID != block.OwnerID) continue;
  2527. var distanceSqr = (station.transform.position - block.transform.position).sqrMagnitude;
  2528. if (distanceSqr >= nearestDistanceSqr) continue;
  2529. nearestDistanceSqr = distanceSqr;
  2530. nearest = station;
  2531. }
  2532. return nearest;
  2533. }
  2534. finally
  2535. {
  2536. Pool.FreeUnmanaged(ref stations);
  2537. }
  2538. }
  2539.  
  2540. private bool IsPlayerNearEntity(ulong playerId, BaseEntity entity)
  2541. {
  2542. if (entity == null) return false;
  2543. var player = BasePlayer.FindByID(playerId);
  2544. if (player == null || !player.IsConnected) return false;
  2545. if (IsDescendantOfEntity(player, entity)) return true;
  2546. return (player.transform.position - entity.transform.position).sqrMagnitude <= NavalPresenceRadius * NavalPresenceRadius;
  2547. }
  2548.  
  2549. private bool IsPlayerOrGroupNearEntity(ulong targetId, BaseEntity entity)
  2550. {
  2551. foreach (var userId in GetSharedProtectionMembers(targetId))
  2552. {
  2553. if (IsPlayerNearEntity(userId, entity)) return true;
  2554. }
  2555. return false;
  2556. }
  2557.  
  2558. private bool IsConfiguredPrefab(BaseCombatEntity entity)
  2559. {
  2560. var prefabName = entity?.ShortPrefabName;
  2561. return !string.IsNullOrEmpty(prefabName) && _config.Prefabs.Any(x => prefabName.Contains(x));
  2562. }
  2563.  
  2564. public bool IsBlocked(BaseCombatEntity entity)
  2565. {
  2566. if (entity == null) return false;
  2567. var navalTarget = FindNavalTarget(entity);
  2568. if (navalTarget is PlayerBoat) return _config.ProtectPlayerMadeBoats && (entity == navalTarget || entity is BoatBuildingBlock || IsConfiguredPrefab(entity));
  2569. if (navalTarget is Tugboat) return _config.ProtectTugboats && (entity == navalTarget || IsConfiguredPrefab(entity));
  2570. if (entity is BoatBuildingBlock) return _config.ProtectPlayerMadeBoats;
  2571. if (entity is BuildingBlock) return true;
  2572. return IsConfiguredPrefab(entity);
  2573. }
  2574.  
  2575. private bool IsProtectionGroupInRange(ulong targetId, BuildingPrivlidge privilege = null)
  2576. {
  2577. foreach (var userId in GetSharedProtectionMembers(targetId))
  2578. {
  2579. if (privilege != null)
  2580. {
  2581. if (IsPlayerNearBuildingPrivilege(userId, privilege)) return true;
  2582. }
  2583. else if (IsPlayerInRange(userId))
  2584. {
  2585. return true;
  2586. }
  2587. }
  2588. return false;
  2589. }
  2590.  
  2591. private static bool IsPlayerNearBuildingPrivilege(ulong targetId, BuildingPrivlidge privilege)
  2592. {
  2593. if (privilege == null || privilege.IsDestroyed) return false;
  2594. var player = BasePlayer.FindByID(targetId);
  2595. if (player == null || !player.IsConnected) return false;
  2596.  
  2597. var currentPrivilege = player.GetBuildingPrivilege();
  2598. if (currentPrivilege == null || currentPrivilege.IsDestroyed) return false;
  2599. if (ReferenceEquals(currentPrivilege, privilege)) return true;
  2600. if (currentPrivilege.net == null || privilege.net == null) return false;
  2601. return currentPrivilege.net.ID.Value == privilege.net.ID.Value;
  2602. }
  2603.  
  2604. public bool IsOffline(ulong playerId)
  2605. {
  2606. EnsureDataInitialized();
  2607. if (_storedData.LastOnlines.TryGetValue(playerId, out var lastOnlinePlayer) && lastOnlinePlayer != null)
  2608. return lastOnlinePlayer.IsOffline();
  2609.  
  2610. var player = covalence.Players.FindPlayerById(playerId.ToString());
  2611. return player == null || !player.IsConnected;
  2612. }
  2613.  
  2614. private static bool IsPlayerInRange(ulong targetId)
  2615. {
  2616. var player = BasePlayer.FindByID(targetId);
  2617. return player != null && player.IsBuildingAuthed();
  2618. }
  2619.  
  2620. private void PlayProtectionSound(HitInfo hitInfo, bool isFire)
  2621. {
  2622. if (!_config.PlaySound || isFire || !(hitInfo?.Initiator is BasePlayer player)) return;
  2623. try
  2624. {
  2625. Effect.server.Run(_config.SoundFile, player.transform.position);
  2626. }
  2627. catch (Exception ex)
  2628. {
  2629. PrintWarning($"Unable to play protection sound: {ex.Message}");
  2630. }
  2631. }
  2632.  
  2633. public object MitigateDamage(HitInfo hitInfo, float scale)
  2634. {
  2635. if (hitInfo?.damageTypes == null || scale < 0f) return null;
  2636. var isFire = hitInfo.damageTypes.GetMajorityDamageType() == DamageType.Heat;
  2637.  
  2638. if (scale <= 0f)
  2639. {
  2640. if (_config.ShowMessage && ((isFire && hitInfo.WeaponPrefab != null) || !isFire)) SendMessage(hitInfo);
  2641. PlayProtectionSound(hitInfo, isFire);
  2642. return true;
  2643. }
  2644.  
  2645. hitInfo.damageTypes.ScaleAll(scale);
  2646. if (scale >= 1f) return null;
  2647.  
  2648. if (_config.ShowMessage && ((isFire && hitInfo.WeaponPrefab != null) || !isFire)) SendMessage(hitInfo, DamageScaleToProtectionPercent(scale));
  2649. PlayProtectionSound(hitInfo, isFire);
  2650. return null;
  2651. }
  2652.  
  2653. object OnStructureAttack(BaseEntity entity, HitInfo hitInfo)
  2654. {
  2655. if (entity == null || hitInfo == null) return null;
  2656. var navalTarget = FindNavalTarget(entity);
  2657. if (navalTarget is PlayerBoat && _config.ProtectPlayerMadeBoats) return OnNavalStructureAttack(navalTarget, entity, hitInfo);
  2658. if (navalTarget is Tugboat && _config.ProtectTugboats) return OnNavalStructureAttack(navalTarget, entity, hitInfo);
  2659.  
  2660. if (entity is BoatBuildingBlock boatBlock)
  2661. {
  2662. if (!_config.ProtectPlayerMadeBoats) return null;
  2663. var station = FindRelevantBoatBuildingStation(boatBlock);
  2664. if (station == null) return null;
  2665.  
  2666. ulong targetId;
  2667. LastOnline protectionLastOnline;
  2668. bool groupOnline;
  2669. if (!TryResolveEntityFallbackProtectionSource(boatBlock, out targetId, out protectionLastOnline, out groupOnline))
  2670. {
  2671. if (!station.OwnerID.IsSteamId() || !TryResolveFallbackProtectionSource(station.OwnerID, out targetId, out protectionLastOnline, out groupOnline)) return null;
  2672. }
  2673.  
  2674. var groupSize = GetProtectionGroupSize(targetId);
  2675. var groupInRange = !_config.ProtectBaseWhenAway || IsPlayerOrGroupNearEntity(targetId, station);
  2676. var protectionScopeId = GetProtectionScopeId(boatBlock, targetId);
  2677. return ApplyProtection(hitInfo, targetId, protectionLastOnline, groupOnline, groupInRange, groupSize, protectionScopeId);
  2678. }
  2679.  
  2680. var buildBlock = entity as BuildingBlock;
  2681. if (buildBlock != null && buildBlock.grade == BuildingGrade.Enum.Twigs) return null;
  2682.  
  2683. var buildingPrivilege = entity.GetBuildingPrivilege();
  2684. var authorizedIds = GetBuildingAuthorizedIds(entity);
  2685. if (buildingPrivilege != null && authorizedIds.Count > 0)
  2686. {
  2687. if (!TryResolvePrivilegeProtectionSource(authorizedIds, out var targetId, out var protectionLastOnline, out var groupOnline)) return null;
  2688. var groupSize = GetProtectionGroupSize(targetId, authorizedIds);
  2689. var groupInRange = !_config.ProtectBaseWhenAway || IsPrivilegeGroupInBuildingRange(authorizedIds, buildingPrivilege);
  2690. return ApplyProtection(hitInfo, targetId, protectionLastOnline, groupOnline, groupInRange, groupSize, GetProtectionScopeId(buildingPrivilege, targetId));
  2691. }
  2692.  
  2693. if (!TryResolveEntityFallbackProtectionSource(entity, out var fallbackResolvedId, out var fallbackLastOnline, out var fallbackGroupOnline)) return null;
  2694. var fallbackGroupSize = GetProtectionGroupSize(fallbackResolvedId);
  2695. var fallbackGroupInRange = !_config.ProtectBaseWhenAway || IsProtectionGroupInRange(fallbackResolvedId, buildingPrivilege);
  2696. return ApplyProtection(hitInfo, fallbackResolvedId, fallbackLastOnline, fallbackGroupOnline, fallbackGroupInRange, fallbackGroupSize, GetProtectionScopeId(entity, fallbackResolvedId));
  2697. }
  2698.  
  2699. private object OnNavalStructureAttack(BaseEntity navalTarget, BaseEntity attackedEntity, HitInfo hitInfo)
  2700. {
  2701. if (navalTarget == null || attackedEntity == null || hitInfo == null) return null;
  2702. var privilege = GetNavalVehiclePrivilege(navalTarget);
  2703. var authorizedIds = Pool.Get<List<ulong>>();
  2704. try
  2705. {
  2706. authorizedIds.Clear();
  2707. if (privilege != null && AppendVehiclePrivilegeAuthorizedIds(privilege, authorizedIds) && authorizedIds.Count > 0)
  2708. {
  2709. if (!TryResolvePrivilegeProtectionSource(authorizedIds, out var targetId, out var protectionLastOnline, out var groupOnline)) return null;
  2710. var groupSize = GetProtectionGroupSize(targetId, authorizedIds);
  2711. var groupInRange = !_config.ProtectBaseWhenAway || IsPrivilegeGroupNearEntity(authorizedIds, navalTarget);
  2712. return ApplyProtection(hitInfo, targetId, protectionLastOnline, groupOnline, groupInRange, groupSize, GetProtectionScopeId(privilege, targetId));
  2713. }
  2714.  
  2715. if (!TryResolveNavalFallbackProtectionSource(attackedEntity, navalTarget, privilege, out var fallbackTargetId, out var fallbackLastOnline, out var fallbackGroupOnline)) return null;
  2716. var fallbackGroupSize = GetProtectionGroupSize(fallbackTargetId);
  2717. var fallbackGroupInRange = !_config.ProtectBaseWhenAway || IsPlayerOrGroupNearEntity(fallbackTargetId, navalTarget);
  2718. return ApplyProtection(hitInfo, fallbackTargetId, fallbackLastOnline, fallbackGroupOnline, fallbackGroupInRange, fallbackGroupSize, GetProtectionScopeId(attackedEntity, fallbackTargetId));
  2719. }
  2720. finally
  2721. {
  2722. Pool.FreeUnmanaged(ref authorizedIds);
  2723. }
  2724. }
  2725.  
  2726. private DateTime GetEffectiveProtectionStart(ulong protectionScopeId, LastOnline protectionLastOnline)
  2727. {
  2728. var start = protectionLastOnline != null ? GetOfflineSince(protectionLastOnline) : DateTime.Now;
  2729. if (!_storedData.LastStructureAttackAt.TryGetValue(protectionScopeId, out var lastAttackLong) || lastAttackLong == 0) return start;
  2730.  
  2731. try
  2732. {
  2733. var lastAttack = DateTime.FromBinary(lastAttackLong);
  2734. return lastAttack > start ? lastAttack : start;
  2735. }
  2736. catch
  2737. {
  2738. _storedData.LastStructureAttackAt.Remove(protectionScopeId);
  2739. return start;
  2740. }
  2741. }
  2742.  
  2743. private bool ShouldDelayProtectionForRecentAttack(ulong protectionScopeId, LastOnline protectionLastOnline)
  2744. {
  2745. if (protectionLastOnline == null || !protectionLastOnline.IsOffline()) return false;
  2746. if (!HasProtectionCooldownElapsed(protectionLastOnline)) return true;
  2747.  
  2748. var delayMinutes = _config.MinutesSinceLastAttackToProtect;
  2749. if (delayMinutes <= 0) return false;
  2750.  
  2751. var now = DateTime.Now;
  2752. var effectiveStart = GetEffectiveProtectionStart(protectionScopeId, protectionLastOnline);
  2753. if ((now - effectiveStart).TotalMinutes > delayMinutes) return false;
  2754.  
  2755. _storedData.LastStructureAttackAt[protectionScopeId] = now.ToBinary();
  2756. InvalidateDamageScale(protectionLastOnline.UserId);
  2757. return true;
  2758. }
  2759.  
  2760. private float GetProtectionDamageScale(ulong protectionScopeId, LastOnline protectionLastOnline)
  2761. {
  2762. if (protectionLastOnline == null) return -1f;
  2763. var effectiveStart = GetEffectiveProtectionStart(protectionScopeId, protectionLastOnline);
  2764. if (effectiveStart <= protectionLastOnline.LastOnlineTime) return ScaleDamageCached(protectionLastOnline);
  2765.  
  2766. var effectiveState = new LastOnline(protectionLastOnline.UserId, effectiveStart)
  2767. {
  2768. AfkMinutes = protectionLastOnline.AfkMinutes,
  2769. LastPosition = protectionLastOnline.LastPosition
  2770. };
  2771. return ScaleDamage(effectiveState);
  2772. }
  2773.  
  2774. private object ApplyProtection(HitInfo hitInfo, ulong targetId, LastOnline protectionLastOnline, bool groupOnline, bool groupInRange, int groupSize, ulong protectionScopeId)
  2775. {
  2776. if (hitInfo == null || !targetId.IsSteamId() || !HasPerm(targetId.ToString(), PluginPermissions.Protect)) return null;
  2777.  
  2778. if (!groupOnline)
  2779. {
  2780. if (protectionLastOnline == null || ShouldDelayProtectionForRecentAttack(protectionScopeId, protectionLastOnline)) return null;
  2781. var scale = ApplyGroupSizeProtectionScaling(GetProtectionDamageScale(protectionScopeId, protectionLastOnline), groupSize);
  2782. return MitigateDamage(hitInfo, scale);
  2783. }
  2784.  
  2785. if (_config.ProtectBaseWhenAway && !groupInRange)
  2786. {
  2787. var awayScale = ApplyGroupSizeProtectionScaling(0f, groupSize);
  2788. return MitigateDamage(hitInfo, awayScale);
  2789. }
  2790.  
  2791. return null;
  2792. }
  2793.  
  2794. public float ScaleDamage(LastOnline lastOnline)
  2795. {
  2796. if (lastOnline == null || !lastOnline.IsOffline()) return -1f;
  2797.  
  2798. // Absolute time scale intentionally overrides relative offline-time scaling
  2799. if (_config.AbsoluteTimeScale.Count > 0)
  2800. {
  2801. var hour = DateTime.Now.ToString("HH", DateTimeFormatInfo.InvariantInfo);
  2802. if (_config.AbsoluteTimeScale.TryGetValue(hour, out var scaleObj))
  2803. {
  2804. try
  2805. {
  2806. return Convert.ToSingle(scaleObj, CultureInfo.InvariantCulture);
  2807. }
  2808. catch
  2809. {
  2810. PrintWarning($"Invalid AbsoluteTimeScale value for hour '{hour}'.");
  2811. }
  2812. }
  2813. }
  2814.  
  2815. if (lastOnline.Minutes < 60) return _config.InterimDamage;
  2816. var eligibleThresholds = _config.DamageScaleKeys.Where(x => x <= lastOnline.Hours).ToList();
  2817. if (eligibleThresholds.Count == 0) return -1f;
  2818. var scaleKey = eligibleThresholds[eligibleThresholds.Count - 1];
  2819. if (!_config.DamageScale.TryGetValue(scaleKey.ToString(), out var scaleValue)) return -1f;
  2820.  
  2821. try
  2822. {
  2823. return Convert.ToSingle(scaleValue, CultureInfo.InvariantCulture);
  2824. }
  2825. catch
  2826. {
  2827. PrintWarning($"Invalid DamageScale value for threshold '{scaleKey}'.");
  2828. return -1f;
  2829. }
  2830. }
  2831.  
  2832. float ScaleDamageCached(LastOnline lastOnline)
  2833. {
  2834. if (lastOnline == null) return -1f;
  2835. if (_cachedScales.TryGetValue(lastOnline.UserId, out var kvp) && kvp != null && DateTime.Now < kvp.Expires) return kvp.Scale;
  2836. return CacheDamageScale(lastOnline.UserId, ScaleDamage(lastOnline), kvp);
  2837. }
  2838.  
  2839. private void AppendShareStatus(StringBuilder stringBuilder, string userId, string providerName, HashSet<ulong> members, int minOnlineMembers)
  2840. {
  2841. if (stringBuilder == null || members == null || members.Count == 0) return;
  2842.  
  2843. var onlineCount = members.Count(memberId => !IsOffline(memberId));
  2844. var groupOnline = onlineCount >= Math.Max(1, minOnlineMembers);
  2845. var groupStatus = GetMsg(groupOnline ? PluginMessages.StatusOnline : PluginMessages.StatusOffline, userId);
  2846. stringBuilder.AppendLine(string.Format(GetMsg(PluginMessages.StatusShare, userId), providerName, groupStatus, onlineCount));
  2847.  
  2848. if (groupOnline || !TryEvaluateSharedProtectionProvider(members, minOnlineMembers, out var selected, out _) || selected == null) return;
  2849. var selectedPlayer = covalence.Players.FindPlayerById(selected.UserId.ToString());
  2850. var selectedName = selectedPlayer?.Name ?? selected.UserId.ToString();
  2851. stringBuilder.AppendLine(string.Format(GetMsg(PluginMessages.StatusShareLastOffline, userId), providerName, selectedName, selected.LastOnlineTime.AddHours(_config.ServerTimeOffset)));
  2852. }
  2853.  
  2854. string SendStatus(string playerSearch, string userId = null)
  2855. {
  2856. EnsureDataInitialized();
  2857. if (string.IsNullOrWhiteSpace(playerSearch)) return GetMsg(PluginMessages.InvalidSyntax, userId);
  2858. var target = FindPlayerByPartialName(playerSearch);
  2859. if (target == null || !ulong.TryParse(target.Id, out var targetId) || !_storedData.LastOnlines.TryGetValue(targetId, out var lo) || lo == null)
  2860. return GetMsg(PluginMessages.PlayerNotFound, userId);
  2861.  
  2862. var stringBuilder = new StringBuilder();
  2863. stringBuilder.AppendLine(string.Format(GetMsg(PluginMessages.StatusHeader, userId), target.Name));
  2864. if (IsOffline(targetId))
  2865. {
  2866. var statusTime = GetOfflineSince(lo).AddHours(_config.ServerTimeOffset);
  2867. stringBuilder.AppendLine(target.IsConnected
  2868. ? string.Format(GetMsg(PluginMessages.StatusPlayerAfk, userId), statusTime)
  2869. : string.Format(GetMsg(PluginMessages.StatusPlayerOffline, userId), statusTime));
  2870. }
  2871. else
  2872. {
  2873. stringBuilder.AppendLine(GetMsg(PluginMessages.StatusPlayerOnline, userId));
  2874. }
  2875.  
  2876. stringBuilder.AppendLine(string.Format(GetMsg(PluginMessages.StatusAfkMinutes, userId), Math.Round(lo.AfkMinutes, 1)));
  2877. if (_config.ClansPluginShare && IsClansPluginAvailable())
  2878. AppendShareStatus(stringBuilder, userId, GetMsg(PluginMessages.ProviderClansPlugin, userId), GetClansPluginMemberIds(targetId), _config.ClansPluginMinMembers);
  2879. if (_config.RustTeamShare)
  2880. AppendShareStatus(stringBuilder, userId, GetMsg(PluginMessages.ProviderRustTeam, userId), GetRustTeamMembers(targetId), _config.RustTeamMinMembers);
  2881. if (_config.RustClanShare)
  2882. AppendShareStatus(stringBuilder, userId, GetMsg(PluginMessages.ProviderRustClan, userId), GetRustClanMembers(targetId), _config.RustClanMinMembers);
  2883.  
  2884. if (TryGetProtectionLastOnline(targetId, out var protectionLastOnline))
  2885. {
  2886. var scale = GetCurrentBaseProtectionDamageScale(protectionLastOnline);
  2887. if (scale > -1f) stringBuilder.AppendLine(string.Format(GetMsg(PluginMessages.StatusScale, userId), DamageScaleToProtectionPercent(scale)));
  2888. }
  2889. return stringBuilder.ToString();
  2890. }
  2891.  
  2892. void ShowMessage(BasePlayer player, int amount = 100)
  2893. {
  2894. if (player == null || !player.IsConnected || player.net?.connection == null) return;
  2895. HideMessage(player);
  2896.  
  2897. var sb = new StringBuilder(JsonMessage);
  2898. sb.Replace("{1}", Core.Random.Range(1, 99999).ToString());
  2899. sb.Replace("{protection_message}", GetMsg(PluginMessages.ProtectionMessage, player.UserIDString));
  2900. sb.Replace("{amount}", amount.ToString());
  2901. CuiHelper.AddUi(player, sb.ToString());
  2902. timer.In(3f, () =>
  2903. {
  2904. if (player != null && player.IsConnected && player.net?.connection != null) HideMessage(player);
  2905. });
  2906. }
  2907.  
  2908. void UpdateLastOnline(ulong playerId)
  2909. {
  2910. if (!playerId.IsSteamId()) return;
  2911. EnsureDataInitialized();
  2912. if (!_storedData.LastOnlines.TryGetValue(playerId, out var lastOnlinePlayer) || lastOnlinePlayer == null)
  2913. {
  2914. _storedData.LastOnlines[playerId] = new LastOnline(playerId, DateTime.Now);
  2915. }
  2916. else
  2917. {
  2918. lastOnlinePlayer.LastOnlineTime = DateTime.Now;
  2919. lastOnlinePlayer.AfkMinutes = 0;
  2920. }
  2921. InvalidateDamageScale(playerId);
  2922. }
  2923.  
  2924. void UpdateLastOnlineAll(bool afkCheck = true)
  2925. {
  2926. EnsureDataInitialized();
  2927. foreach (var player in BasePlayer.activePlayerList)
  2928. {
  2929. if (player == null || !player.IsConnected) continue;
  2930.  
  2931. if (!afkCheck)
  2932. {
  2933. UpdateLastOnline(player.userID);
  2934. if (_storedData.LastOnlines.TryGetValue(player.userID, out var initialState) && initialState != null)
  2935. initialState.LastPosition = player.transform.position;
  2936. continue;
  2937. }
  2938.  
  2939. if (!_storedData.LastOnlines.TryGetValue(player.userID, out var lastOnlinePlayer) || lastOnlinePlayer == null)
  2940. {
  2941. UpdateLastOnline(player.userID);
  2942. if (_storedData.LastOnlines.TryGetValue(player.userID, out lastOnlinePlayer) && lastOnlinePlayer != null)
  2943. lastOnlinePlayer.LastPosition = player.transform.position;
  2944. continue;
  2945. }
  2946.  
  2947. if (lastOnlinePlayer.HasMoved(player.transform.position))
  2948. {
  2949. UpdateLastOnline(player.userID);
  2950. continue;
  2951. }
  2952.  
  2953. lastOnlinePlayer.AfkMinutes = (float)Math.Max(0d, lastOnlinePlayer.Minutes);
  2954. }
  2955. }
  2956. #endregion
  2957.  
  2958. }
  2959.  
  2960. }
Add Comment
Please, Sign In to add comment