#if CARBON using Carbon.Extensions; using Carbon.Plugins.OfflineRaidProtectionEx; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Oxide.Core; using Oxide.Core.Libraries; using Oxide.Core.Plugins; using System.Collections.Generic; using System.Linq; using System.Text; namespace Carbon.Plugins { [Info("Offline Raid Protection", "realedwin", "1.1.15"), Description("Prevents/reduces offline raids by other players")] public sealed class OfflineRaidProtection : CarbonPlugin { #region Fields [PluginReference] private Plugin Clans; private static OfflineRaidProtection Instance { get; set; } private readonly PlayerManager _playerManager = new(); private readonly Dictionary _scaleCache = new(); private readonly Dictionary> _clanMemberCache = new(); private readonly Dictionary _clanTagCache = new(); private readonly Dictionary _prefabCache = new(); private readonly List _damageScaleKeys = new(); private readonly List _absolutTimeScaleKeys = new(); #pragma warning disable IDE0044 private System.TimeZoneInfo _timeZone = System.TimeZoneInfo.Utc; // Default timezone #pragma warning restore IDE0044 #region Temp private readonly List _tmpList = new(); private readonly HashSet _tmpHashSet = new(); private readonly HashSet _tmpHashSet2 = new(); private readonly StringBuilder _sb = new(); private readonly TextTable _textTable = new(); #endregion Temp #region Constants private const string COMMAND_HIDEGAMETIP = "gametip.hidegametip", COMMAND_SHOWGAMETIP = "gametip.showgametip", LANG_MESSAGE_AMOUNT = "{amount}", LANG_MESSAGE_COLOR = "{color}", LANG_PROTECTION_MESSAGE_BUILDING = "Protection Message Building", LANG_PROTECTION_MESSAGE_VEHICLE = "Protection Message Vehicle", MESSAGE_INVALID_SYNTAX = "Invalid Syntax", MESSAGE_PLAYER_NOT_FOUND = "No player found", MESSAGE_TITLE_SIZE = "15", TEXT_CLAN_MEMBER = "Clan Members", TEXT_TEAM_MEMBER = "Team Members"; #region Colors private const string COLOR_AQUA = "#1ABC9C", COLOR_BLUE = "#3498DB", COLOR_DARK_GREEN = "#1F8B4C", COLOR_GREEN = "#57F287", COLOR_ORANGE = "#E67E22", COLOR_RED = "#ED4245", COLOR_WHITE = "white", COLOR_YELLOW = "#FFFF00"; #endregion Colors #endregion Constants #endregion Fields #region Classes private sealed class ConfigData { [JsonProperty(PropertyName = "Raid Protection Options")] public RaidProtectionOptions RaidProtection { get; set; } [JsonProperty(PropertyName = "Team Options")] public TeamOptions Team { get; set; } [JsonProperty(PropertyName = "Command Options")] public CommandOptions Command { get; set; } [JsonProperty(PropertyName = "Permission Options")] public PermissionOptions Permission { get; set; } [JsonProperty(PropertyName = "Other Options")] public OtherOptions Other { get; set; } [JsonProperty(PropertyName = "Timezone Options")] public TimeZoneOptions TimeZone { get; set; } public VersionNumber Version { get; set; } public class RaidProtectionOptions { [JsonProperty(PropertyName = "Only mitigate damage caused by players")] public bool OnlyPlayerDamage { get; set; } [JsonProperty(PropertyName = "Protect players that are online")] public bool OnlineRaidProtection { get; set; } [JsonProperty(PropertyName = "Scale of damage depending on the current hour of the real day")] public Dictionary AbsoluteTimeScale { get; set; } [JsonProperty(PropertyName = "Scale of damage depending on the offline time in hours")] public Dictionary DamageScale { get; set; } [JsonProperty(PropertyName = "Cooldown in minutes")] public int CooldownMinutes { get; set; } [JsonProperty(PropertyName = "Online time to qualify for offline raid protection in minutes")] public int CooldownQualifyMinutes { get; set; } [JsonProperty(PropertyName = "Scale of damage between the cooldown and the first configured time")] public float InterimDamage { get; set; } [JsonProperty(PropertyName = "Protect all prefabs")] public bool ProtectAll { get; set; } [JsonProperty(PropertyName = "Protect AI (animals, NPCs, Bradley and attack helicopters etc.) if 'Protect all Prefabs' is enabled")] public bool ProtectAi { get; set; } [JsonProperty(PropertyName = "Protect vehicles")] public bool ProtectVehicles { get; set; } [JsonProperty(PropertyName = "Protect twigs")] public bool ProtectTwigs { get; set; } [JsonProperty(PropertyName = "Prefabs to protect")] public HashSet Prefabs { get; set; } [JsonProperty(PropertyName = "Prefabs blacklist")] public HashSet PrefabsBlacklist { get; set; } } public class TeamOptions { [JsonProperty(PropertyName = "Enable team offline protection sharing")] public bool TeamShare { get; set; } [JsonProperty(PropertyName = "Mitigate damage by the team-mate who was offline the longest")] public bool TeamFirstOffline { get; set; } [JsonProperty(PropertyName = "Include players that are whitelisted on Codelocks")] public bool IncludeWhitelistPlayers { get; set; } [JsonProperty(PropertyName = "Prevent players from leaving or disbanding their team if at least one team member is offline")] public bool TeamAvoidAbuse { get; set; } [JsonProperty(PropertyName = "Enable offline raid protection penalty for leaving or disbanding a team")] public bool TeamEnablePenalty { get; set; } [JsonProperty(PropertyName = "Penalty duration in hours")] public float TeamPenaltyDuration { get; set; } } public class CommandOptions { [JsonProperty(PropertyName = "Commands to check offline protection status")] public string[] Commands { get; set; } [JsonProperty(PropertyName = "Command to display offline raid protection information")] public string CommandHelp { get; set; } [JsonProperty(PropertyName = "Command to fill the offline times of all players")] public string CommandFillOnlineTimes { get; set; } [JsonProperty(PropertyName = "Command to update the permission status for all players.")] public string CommandUpdatePermissions { get; set; } [JsonProperty(PropertyName = "Command to change a player's offline time")] public string CommandTestOffline { get; set; } [JsonProperty(PropertyName = "Command to change a player's offline time to the current time")] public string CommandTestOnline { get; set; } [JsonProperty(PropertyName = "Command to change a player's penalty duration")] public string CommandTestPenalty { get; set; } [JsonProperty(PropertyName = "Command to update the Prefabs to protect list")] public string CommandUpdatePrefabList { get; set; } [JsonProperty(PropertyName = "Command to dump the Prefabs to protect list")] public string CommandDumpPrefabList { get; set; } [JsonIgnore] private int _cooldown; [JsonProperty(PropertyName = "Command cooldown in seconds")] public int CommandCooldown { get => _cooldown; set => _cooldown = System.Math.Max(0, value); } internal void RegisterCommands(Plugin plugin, OfflineRaidProtection offlineRaidProtection) { RegisterChatCommands(Commands, plugin, offlineRaidProtection.cmdStatus, Configuration.Permission.Check); RegisterChatCommands(new[] { CommandHelp }, plugin, offlineRaidProtection.cmdHelp, Configuration.Permission.Protect); RegisterChatCommands(new[] { CommandFillOnlineTimes }, plugin, offlineRaidProtection.cmdFillOnlineTimes, Configuration.Permission.Admin); RegisterChatCommands(new[] { CommandTestOffline }, plugin, offlineRaidProtection.cmdTestOffline, Configuration.Permission.Admin); RegisterChatCommands(new[] { CommandTestOnline }, plugin, offlineRaidProtection.cmdTestOnline, Configuration.Permission.Admin); RegisterChatCommands(new[] { CommandTestPenalty }, plugin, offlineRaidProtection.cmdTestPenalty, Configuration.Permission.Admin); RegisterConsoleCommands(new[] { CommandFillOnlineTimes }, plugin, nameof(Instance.ccFillOnlineTimes), Configuration.Permission.Admin); RegisterConsoleCommands(new[] { CommandUpdatePermissions }, plugin, nameof(Instance.ccUpdatePermissions), Configuration.Permission.Admin); RegisterConsoleCommands(new[] { CommandUpdatePrefabList }, plugin, nameof(Instance.ccUpdatePrefabList), Configuration.Permission.Admin); RegisterConsoleCommands(new[] { CommandDumpPrefabList }, plugin, nameof(Instance.ccDumpPrefabList), Configuration.Permission.Admin); } private void RegisterChatCommands(string[] commands, Plugin plugin, System.Action callback, string permission) { foreach (var command in commands) Community.Runtime.Core.cmd.AddChatCommand(command, plugin, callback, cooldown: CommandCooldown * 1000, permissions: new[] { permission }); } private void RegisterConsoleCommands(string[] commands, Plugin plugin, string callback, string permission) { foreach (var command in commands) Community.Runtime.Core.cmd.AddConsoleCommand(command, plugin, callback, cooldown: CommandCooldown * 1000, permissions: new[] { permission }); } } public class PermissionOptions { [JsonProperty(PropertyName = "Permission required to enable offline protection")] public string Protect { get; set; } [JsonProperty(PropertyName = "Permission required to check offline protection status")] public string Check { get; set; } [JsonProperty(PropertyName = "Permission required to use admin functions")] public string Admin { get; set; } internal void RegisterPermissions(Permission permission, Plugin plugin) { string[] permissions = [Protect, Check, Admin]; foreach (var perm in permissions) permission.RegisterPermission(perm, plugin); } } public class OtherOptions { [JsonProperty(PropertyName = "Play sound when damage is mitigated")] public bool PlaySound { get; set; } [JsonProperty(PropertyName = "Asset path of the sound to be played")] public string SoundPath { get; set; } [JsonProperty(PropertyName = "Display a game tip message when a prefab is protected")] public bool ShowMessage { get; set; } [JsonProperty(PropertyName = "Game tip message shows remaining protection time")] public bool ShowRemainingTime { get; set; } [JsonProperty(PropertyName = "Message duration in seconds")] public float MessageDuration { get; set; } } public class TimeZoneOptions { [JsonProperty(PropertyName = "Timezone for Windows")] public string WinTimeZone { get; set; } [JsonProperty(PropertyName = "Timezone for Linux")] public string UnixTimeZone { get; set; } } } [method: JsonConstructor] private sealed class LastOnlineData(in ulong userid, in string userName, in long lastOnline, in long lastConnect) { [JsonProperty(PropertyName = "User ID")] public ulong UserID { get; set; } = userid; [JsonProperty(PropertyName = "User Name")] public string UserName { get; set; } = userName; [JsonProperty(PropertyName = "Last Online")] public long LastOnline { get; set; } = lastOnline; [JsonProperty(PropertyName = "End of Penalty")] public long PenaltyEnd { get; set; } [JsonProperty(PropertyName = "Last Connect")] public long LastConnect { get; set; } = lastConnect; [JsonIgnore] public System.DateTime LastOnlineDT { get => System.DateTime.FromBinary(LastOnline); set => LastOnline = value.ToBinary(); } [JsonIgnore] public System.DateTime PenaltyEndDT { get => new(PenaltyEnd); private set => PenaltyEnd = value.Ticks; } [JsonIgnore] public System.DateTime LastConnectDT { get => System.DateTime.FromBinary(LastConnect); set => LastConnect = value.ToBinary(); } public LastOnlineData(in BasePlayer player, in System.DateTime currentTime, in bool connected = false) : this(player.userID.Get(), player.displayName, 0, 0) { LastOnlineDT = currentTime; LastConnectDT = connected ? currentTime : LastConnectDT; } // [JsonIgnore] public float Days => (float)TimeSpanSinceLastOnline.TotalDays; [JsonIgnore] public float Minutes => (float)TimeSpanSinceLastOnline.TotalMinutes; [JsonIgnore] public float Hours => (float)TimeSpanSinceLastOnline.TotalHours; [JsonIgnore] private System.TimeSpan TimeSpanSinceLastOnline => System.DateTime.UtcNow - (!IsOnline ? LastOnlineDT : System.DateTime.UtcNow); [JsonIgnore] private BasePlayer Player => Instance._playerManager.GetPlayer(UserID); [JsonIgnore] public bool IsOnline => Player is not null && Player.IsConnected; [JsonIgnore] public bool IsOffline => !IsOnline && Minutes >= Configuration.RaidProtection.CooldownMinutes; public void EnablePenalty(in float duration) => PenaltyEndDT = System.DateTime.UtcNow.AddHours(duration); public void DisablePenalty() => PenaltyEnd = 0L; } private sealed class PlayerScaleCache { public float Scale { get; set; } public long Expires { get; private set; } public bool ActiveGameTipMessage { get; set; } public System.TimeSpan RemainingTime { get; set; } public bool HasPermission { get; set; } public System.Action Action { get; private set; } public PlayerScaleCache(System.DateTime expires, float scale, bool hasPermission) { ExpiresDT = expires; Scale = scale; ActiveGameTipMessage = false; HasPermission = hasPermission; Action = null; } public System.DateTime ExpiresDT { // get => new(Expires); set => Expires = value.Ticks; } public void CacheAction(BasePlayer player) => Action = GetAction(player, this); private void ClearAction() => Action = null; private static System.Action GetAction(BasePlayer player, PlayerScaleCache playerScaleCache) { return () => { playerScaleCache.ActiveGameTipMessage = false; if (player is not null) player.SendConsoleCommand(COMMAND_HIDEGAMETIP); else playerScaleCache.ClearAction(); }; } } private sealed class PlayerManager { private readonly Dictionary _playersByUserID = new(); private readonly Dictionary _playersByName = new(); public void AddPlayer(in BasePlayer player) { _playersByUserID[player.userID.Get()] = player; _playersByName[player.displayName] = player; } // public void RemovePlayer(in BasePlayer player) // { // _playersByUserID.Remove(player.userID.Get()); // _playersByName.Remove(player.displayName); // } public BasePlayer GetPlayer(in ulong userID) => _playersByUserID.GetValueOrDefault(userID, null); public BasePlayer GetPlayer(in string displayName) { if (_playersByName.TryGetValue(displayName, out var player)) return player; if (ulong.TryParse(displayName, out var userID) && _playersByUserID.TryGetValue(userID, out player)) return player; return null; } public void Clear() { _playersByUserID.Clear(); _playersByName.Clear(); } } #endregion Classes #region Data private Dictionary _lastOnline = new(); private void SaveData() => Interface.Oxide.DataFileSystem.WriteObject($"{Name}/{nameof(LastOnlineData)}", _lastOnline); private void Save() { UpdateLastOnlineAll(); SaveData(); } private void LoadData() { try { _lastOnline = Interface.Oxide.DataFileSystem.ReadObject>($"{Name}/{nameof(LastOnlineData)}"); } catch (System.Exception ex) { PrintError(ex.ToString()); } _lastOnline ??= new(); } #endregion Data #region Config private static ConfigData Configuration { get; set; } protected override void LoadDefaultConfig() => Configuration = GetBaseConfig(); protected override void SaveConfig() => Config.WriteObject(Configuration, true); protected override void LoadConfig() { base.LoadConfig(); try { Configuration = Config.ReadObject(); if (Configuration.Version < Version) UpdateConfigValues(); Config.WriteObject(Configuration, true); SetTimeZone(); } catch (System.Exception ex) { PrintError($"There is an error in your configuration file. Using default settings\n{ex}"); LoadDefaultConfig(); } } private void UpdateConfigValues() { PrintWarning("Config update detected! Update config values..."); var baseConfig = GetBaseConfig(); if (Configuration.Version < new VersionNumber(1, 1, 8)) Configuration.Command.CommandUpdatePermissions = baseConfig.Command.CommandUpdatePermissions; if (Configuration.Version < new VersionNumber(1, 1, 15)) { Configuration.Command.CommandUpdatePrefabList = baseConfig.Command.CommandUpdatePrefabList; Configuration.Command.CommandDumpPrefabList = baseConfig.Command.CommandDumpPrefabList; Configuration.RaidProtection.CooldownQualifyMinutes = baseConfig.RaidProtection.CooldownQualifyMinutes; } Configuration.Version = Version; SaveConfig(); PrintWarning("Config update has been completed!"); } private void SetTimeZone() { #if WIN _timeZone = !string.IsNullOrEmpty(Configuration.TimeZone.WinTimeZone) ? GetTimeZoneByID(Configuration.TimeZone.WinTimeZone) : _timeZone; #endif #if UNIX _timeZone = !string.IsNullOrEmpty(Configuration.TimeZone.UnixTimeZone) ? GetTimeZoneByID(Configuration.TimeZone.UnixTimeZone) : _timeZone; #endif } private System.TimeZoneInfo GetTimeZoneByID(string id) => System.TimeZoneInfo.GetSystemTimeZones().FirstOrDefault(tz => tz.Id == id) ?? System.TimeZoneInfo.Utc; private ConfigData GetBaseConfig() { return new ConfigData { RaidProtection = new() { OnlyPlayerDamage = false, OnlineRaidProtection = false, AbsoluteTimeScale = new(), CooldownMinutes = 10, CooldownQualifyMinutes = 0, DamageScale = new() { { 12f, 0.25f }, { 24f, 0.5f }, { 48f, 1f }, }, InterimDamage = 0f, ProtectAll = false, ProtectAi = false, ProtectVehicles = true, ProtectTwigs = false, Prefabs = GetPrefabNames(), PrefabsBlacklist = new() }, Team = new() { TeamShare = true, TeamFirstOffline = false, IncludeWhitelistPlayers = false, TeamAvoidAbuse = false, TeamEnablePenalty = false, TeamPenaltyDuration = 24f }, Command = new() { Commands = new[] { "ao", "orp" }, CommandHelp = "raidprot", CommandFillOnlineTimes = "orp.fill.onlinetimes", CommandUpdatePermissions = "orp.update.permissions", CommandTestOffline = "orp.test.offline", CommandTestOnline = "orp.test.online", CommandTestPenalty = "orp.test.penalty", CommandUpdatePrefabList = "orp.update.prefabs", CommandDumpPrefabList = "orp.dump.prefabs", CommandCooldown = 1 }, Permission = new() { Protect = "offlineraidprotection.protect", Check = "offlineraidprotection.check", Admin = "offlineraidprotection.admin" }, Other = new() { PlaySound = false, SoundPath = "assets/prefabs/locks/keypad/effects/lock.code.denied.prefab", ShowMessage = true, ShowRemainingTime = false, MessageDuration = 3f }, TimeZone = new() { WinTimeZone = "W. Europe Standard Time", UnixTimeZone = "Europe/Berlin" }, Version = Version }; } private HashSet GetPrefabNames() { var prefabNames = new HashSet( ItemManager.GetItemDefinitions() .Select(itemDefinition => itemDefinition.GetComponent()) .Where(itemModDeployable => itemModDeployable is not null) .Select(itemModDeployable => System.IO.Path.GetFileNameWithoutExtension(itemModDeployable.entityPrefab.resourcePath)) .OrderBy(name => name)); var manifest = GameManifest.Current; prefabNames.UnionWith(manifest.entities .Select(entity => GameManager.server.FindPrefab(entity).GetComponent()) .Where(vehicle => vehicle is not null) .Select(vehicle => vehicle.ShortPrefabName) .OrderBy(name => name)); return prefabNames; } #endregion Config #region Hooks private void Loaded() { Instance ??= this; Configuration.Permission.RegisterPermissions(permission, this); Configuration.Command.RegisterCommands(this, this); LoadData(); UnsubscribeHooks(); } private void OnServerInitialized() => CacheData(); private void OnNewSave(string _filename) { _lastOnline.Clear(); SaveData(); } private void OnServerSave() => ServerMgr.Instance.Invoke(Instance.Save, 10f); private void OnServerShutdown() => Save(); private void Unload() { Save(); Configuration = null; Instance = null; Clans = null; _prefabCache.Clear(); _scaleCache.Clear(); _lastOnline.Clear(); _damageScaleKeys.Clear(); _absolutTimeScaleKeys.Clear(); _tmpList.Clear(); _tmpHashSet.Clear(); _tmpHashSet2.Clear(); _sb.Clear(); _textTable.Clear(); FreeAllClanPoolLists(); _clanMemberCache.Clear(); _clanTagCache.Clear(); _playerManager.Clear(); foreach (var player in BasePlayer.activePlayerList) { if (player is null) return; player.SendConsoleCommand(COMMAND_HIDEGAMETIP); } } private void OnPlayerConnected(BasePlayer player) { if (player is null) return; var currentTime = System.DateTime.UtcNow; UpdateLastOnline(player, currentTime); UpdateLastConnect(player, currentTime); _playerManager.AddPlayer(player); if (!_scaleCache.TryGetValue(player.userID.Get(), out var scaleCache)) { scaleCache = new PlayerScaleCache(currentTime, -1f, player.userID.Get().HasPermission(Configuration.Permission.Protect)); _scaleCache[player.userID.Get()] = scaleCache; } else scaleCache.CacheAction(player); } private void OnPlayerDisconnected(BasePlayer player) { if (player is null) return; var currentTime = System.DateTime.UtcNow; UpdateLastOnline(player, currentTime); } private void OnUserNameUpdated(string id, string _oldName, string newName) { if (_lastOnline.TryGetValue(ulong.Parse(id), out var lastOnline)) lastOnline.UserName = newName; } private object OnEntityTakeDamage(BaseCombatEntity entity, HitInfo hitInfo) { if (hitInfo is null || entity is null || (Configuration.RaidProtection.OnlyPlayerDamage && hitInfo.InitiatorPlayer is null)) return null; if (!hitInfo.damageTypes.Has(Rust.DamageType.Decay) && IsProtected(entity)) return OnStructureAttack(entity, ref hitInfo); return null; } #endregion Hooks #region Hook Subscribtion private void UnsubscribeHooks() { if (Configuration.Team.TeamAvoidAbuse || Configuration.Team.TeamEnablePenalty) return; Unsubscribe(nameof(OnTeamDisband)); Unsubscribe(nameof(OnTeamKick)); Unsubscribe(nameof(OnTeamLeave)); } #endregion Hook Subscribtion #region Cache Methods private void CacheData() { CachePrefabs(); CacheAllClans(); CacheDamageScaleKeys(); CacheAllPlayerScale(); CacheAllPlayers(); } private void CachePrefabs() { foreach (var itemDefinition in ItemManager.GetItemDefinitions()) { var itemModDeployable = itemDefinition.GetComponent(); if (itemModDeployable is null) continue; var shortName = System.IO.Path.GetFileNameWithoutExtension(itemModDeployable.entityPrefab.resourcePath); _prefabCache[itemModDeployable.entityPrefab.GetEntity().prefabID] = IsEntityProtected(shortName); } var manifest = GameManifest.Current; foreach (var entity in manifest.entities) { var prefab = GameManager.server.FindPrefab(entity); if (prefab is null) continue; UnityEngine.Component activeComponent = null; var componentTypes = new[] { typeof(BaseNpc), typeof(NPCPlayer), typeof(BradleyAPC), typeof(AttackHelicopter), typeof(CH47Helicopter), typeof(BaseVehicle), typeof(BasePlayer) }; foreach (var type in componentTypes) { activeComponent = prefab.GetComponent(type); if (activeComponent is not null) break; } if (activeComponent is null) continue; var baseEntity = (BaseEntity)activeComponent; var prefabId = baseEntity.prefabID; var shortName = baseEntity.ShortPrefabName; var isAi = activeComponent is BaseNpc or NPCPlayer or BradleyAPC or AttackHelicopter or CH47Helicopter or BasePlayer; var isVehicle = activeComponent is BaseVehicle && !isAi; _prefabCache[prefabId] = IsEntityProtected(shortName, isVehicle, isAi); } return; static bool IsEntityProtected(in string shortName, in bool isVehicle = false, in bool isAi = false) { return !Configuration.RaidProtection.PrefabsBlacklist.Contains(shortName) && ((Configuration.RaidProtection.ProtectVehicles && isVehicle) || (Configuration.RaidProtection.ProtectAll && !(isAi && !Configuration.RaidProtection.ProtectAi)) || (!Configuration.RaidProtection.ProtectAll && Configuration.RaidProtection.Prefabs.Contains(shortName))); } } private void CacheAllClans() { // Call the "GetAllClans" method and retrieve all clan tags var clans = Clans?.Call("GetAllClans"); if (clans is null || clans.Count == 0) return; foreach (var tag in clans) { var clanTag = tag.ToString(); if (!string.IsNullOrEmpty(clanTag)) CacheClan(clanTag); } } private List CacheClan(in string tag) { if (string.IsNullOrEmpty(tag)) return null; // Call the "GetClan" method and retrieve the clan data var clan = Clans?.Call("GetClan", tag); if (clan?["members"] is null) return null; if (!_clanMemberCache.TryGetValue(tag, out var clanMemberList)) { clanMemberList = Facepunch.Pool.Get>(); _clanMemberCache[tag] = clanMemberList; } else clanMemberList.Clear(); foreach (var memberToken in clan["members"]) { if (!ulong.TryParse(memberToken.ToString(), out var memberID) || memberID == 0) continue; clanMemberList.Add(memberID); _clanTagCache[memberID] = tag; } clan.RemoveAll(); return clanMemberList; } private void CacheDamageScaleKeys() { _damageScaleKeys.Clear(); _damageScaleKeys.AddRange(Configuration.RaidProtection.DamageScale.Keys.ToList()); _damageScaleKeys.Sort(); _absolutTimeScaleKeys.Clear(); _absolutTimeScaleKeys.AddRange(Configuration.RaidProtection.AbsoluteTimeScale.Keys.ToList()); _absolutTimeScaleKeys.Sort(); } private void CacheAllPlayerScale() { foreach (var lastOnline in _lastOnline) CacheDamageScale(lastOnline.Value.UserID, -1f, true); } private float CacheDamageScale(in ulong targetID, in float scale, in bool @default) { var currentTime = System.DateTime.UtcNow; // default var expiration = currentTime.AddMinutes(1); if (_scaleCache.TryGetValue(targetID, out var scaleCache)) { scaleCache.ExpiresDT = @default ? currentTime : expiration; scaleCache.Scale = scale; } else _scaleCache[targetID] = new PlayerScaleCache(@default ? currentTime : expiration, scale, targetID.HasPermission(Configuration.Permission.Protect)); return scale; } private void CacheAllPlayers() { foreach (var player in BasePlayer.allPlayerList) _playerManager.AddPlayer(player); } #endregion Cache Methods #region Core Methods private void UpdateLastOnlineAll() { var currentTime = System.DateTime.UtcNow; foreach (var player in BasePlayer.activePlayerList) { if (player.IsConnected) UpdateLastOnline(player, currentTime); } } private void UpdateLastOnline(in BasePlayer player, in System.DateTime currentTime) { if (_lastOnline.TryGetValue(player.userID.Get(), out var lastOnline)) { lastOnline.LastOnlineDT = currentTime; lastOnline.UserName = player.displayName ?? lastOnline.UserName; } else _lastOnline[player.userID.Get()] = new LastOnlineData(player, currentTime); } private void UpdateLastConnect(in BasePlayer player, in System.DateTime currentTime) { if (_lastOnline.TryGetValue(player.userID.Get(), out var lastOnline)) lastOnline.LastConnectDT = currentTime; else _lastOnline[player.userID.Get()] = new LastOnlineData(player, currentTime, true); } private bool IsProtected(in BaseCombatEntity entity) { // If the entity is a BuildingBlock, it's protected if (entity is BuildingBlock buildingBlock && (Configuration.RaidProtection.ProtectTwigs || buildingBlock.grade != BuildingGrade.Enum.Twigs)) return true; // If ProtectAll is enabled, only check the blacklist if (Configuration.RaidProtection.ProtectAll) return !_prefabCache.TryGetValue(entity.prefabID, out var isNotProtected) || isNotProtected; // If the entity's ID is in the cache, return it's protection status if (_prefabCache.TryGetValue(entity.prefabID, out var isProtected)) return isProtected; // If none of the above conditions are met var result = Configuration.RaidProtection.Prefabs.Contains(entity.ShortPrefabName); _prefabCache[entity.prefabID] = result; return result; } private object OnStructureAttack(in BaseCombatEntity entity, ref HitInfo hitInfo) { // Get authorized players for the entity var authorizedPlayers = GetAuthorizedPlayers(entity); if (authorizedPlayers is null || authorizedPlayers.Count == 0) return null; // Check if the TC is player-owned or NPC-owned foreach (var id in authorizedPlayers) { if (!id.IsSteamId()) return null; break; } // Determine targetID (either the entity's owner or an authorized player) var targetID = entity.OwnerID; if (entity.OwnerID == 0UL || !authorizedPlayers.Contains(entity.OwnerID)) { using var enumerator = authorizedPlayers.GetEnumerator(); enumerator.MoveNext(); targetID = enumerator.Current; } // Check if InitiatorPlayer is an authorized player if (hitInfo.InitiatorPlayer is not null && authorizedPlayers.Contains(hitInfo.InitiatorPlayer.userID.Get())) return null; // Get the most recent team member based on the configuration setting targetID = GetRecentActiveMemberAll(targetID, authorizedPlayers); if (!_lastOnline.TryGetValue(targetID, out var recentTarget) || (!Configuration.RaidProtection.OnlineRaidProtection && recentTarget.IsOnline) || recentTarget.PenaltyEnd >= System.DateTime.UtcNow.Ticks) return null; if (!Configuration.RaidProtection.OnlineRaidProtection && AnyPlayersOnline(authorizedPlayers)) return null; // No authorized players are online, mitigate the damage based on the recent member's last offline time _isVehicle = entity is BaseVehicle || entity.GetParentEntity() is BaseVehicle; return MitigateDamage(ref hitInfo, GetCachedDamageScale(targetID), targetID); } private HashSet GetAuthorizedPlayers(in BaseCombatEntity entity) { // Prioritize vehicle authorization if (Configuration.RaidProtection.ProtectVehicles) { _tmpHashSet.Clear(); if (entity is ModularCar modularCar) { foreach (var whitelistPlayer in modularCar.CarLock.WhitelistPlayers) _tmpHashSet.Add(whitelistPlayer); if (_tmpHashSet.Count > 0) return _tmpHashSet; } else { var parentEntity = entity.GetParentEntity(); if (entity is BaseVehicle || parentEntity is BaseVehicle) { GetAuthorizedPlayersVehicle(entity); if (_tmpHashSet.Count > 0) return _tmpHashSet; } } } // TC authorization var privilege = entity.GetBuildingPrivilege(); return privilege is not null ? GetAuthorizedPlayerIdsFromList(privilege.authorizedPlayers, buildingPrivlidge: privilege) : null; } private HashSet GetAuthorizedPlayersVehicle(in BaseCombatEntity entity) { var vehiclePrivilege = GetVehiclePrivilege(entity.children) ?? GetVehiclePrivilege(entity.GetParentEntity()?.children); return vehiclePrivilege is not null ? GetAuthorizedPlayerIdsFromList(vehiclePrivilege.authorizedPlayers, vehiclePrivilege: vehiclePrivilege) : null; static VehiclePrivilege GetVehiclePrivilege(in List entities) { if (entities is null) return null; foreach (var child in entities) { if (child is not VehiclePrivilege vehiclePrivilege) continue; return vehiclePrivilege; } return null; } } private HashSet GetAuthorizedPlayerIdsFromList(in ICollection authorizedPlayers, in BuildingPrivlidge buildingPrivlidge = null, in VehiclePrivilege vehiclePrivilege = null) { if (authorizedPlayers is null || authorizedPlayers.Count == 0) return null; _tmpHashSet.Clear(); foreach (var authPlayer in authorizedPlayers) _tmpHashSet.Add(authPlayer.userid); if (!Configuration.Team.IncludeWhitelistPlayers) return _tmpHashSet; if (buildingPrivlidge is not null) AddWhitelistPlayers(buildingPrivlidge.GetBuilding()?.decayEntities); if (vehiclePrivilege is not null) AddWhitelistPlayers(vehiclePrivilege.GetParentEntity()?.children); return _tmpHashSet; } private void AddWhitelistPlayers(in IEnumerable entities) { if (entities is null) return; foreach (var entity in entities) { if (entity is not Door door) continue; foreach (var doorChild in door.children) { if (doorChild is not CodeLock codeLock) continue; foreach (var whitelistPlayer in codeLock.whitelistPlayers) _tmpHashSet.Add(whitelistPlayer); } } } private ulong GetRecentActiveMemberAll(in ulong targetID, in HashSet players = null) { if (!Configuration.Team.TeamShare) return targetID; if (players is null || players.Count == 0) return GetRecentActiveMember(targetID); _tmpHashSet2.Clear(); _tmpHashSet2.Add(targetID); if (Clans is not null) { foreach (var player in players) { var tag = GetClanTag(player); if (string.IsNullOrEmpty(tag)) continue; var clanMembers = GetClanMembers(tag); _tmpHashSet2.UnionWith(clanMembers); } return GetOfflineMember(_tmpHashSet2); } foreach (var player in players) { var teamMembers = GetTeamMembers(player); if(teamMembers is null) continue; _tmpHashSet2.UnionWith(teamMembers); } return GetOfflineMember(_tmpHashSet2); } private ulong GetRecentActiveMember(in ulong targetID) { if (Clans is not null) { var tag = GetClanTag(targetID); if (string.IsNullOrEmpty(tag)) return targetID; var clanMembers = GetClanMembers(tag); return clanMembers is not null && clanMembers.Count > 0 ? GetOfflineMember(clanMembers) : targetID; } var teamMembers = GetTeamMembers(targetID); return teamMembers is not null && teamMembers.Count > 0 ? GetOfflineMember(teamMembers) : targetID; } private bool AnyPlayersOffline(in List playerIDs) { foreach (var player in playerIDs) { if (IsOffline(player)) return true; } return false; } private bool AnyPlayersOnline(in HashSet playerIDs) { foreach (var player in playerIDs) { if (IsOnline(player)) return true; } return false; } private bool IsOffline(in ulong playerID) { if (_lastOnline.TryGetValue(playerID, out var lastOnlinePlayer)) return lastOnlinePlayer.IsOffline; var player = _playerManager.GetPlayer(playerID); return player is null || !player.IsConnected; } private bool IsOnline(in ulong playerID) { if (_lastOnline.TryGetValue(playerID, out var lastOnlinePlayer)) return lastOnlinePlayer.IsOnline; var player = _playerManager.GetPlayer(playerID); return player is not null && player.IsConnected; } private float GetCachedDamageScale(in ulong targetID) { if (!_scaleCache.TryGetValue(targetID, out var scaleCache) || System.DateTime.UtcNow.Ticks > scaleCache.Expires) return CacheDamageScale(targetID, scaleCache?.HasPermission == true ? GetDamageScale(targetID, scaleCache) : -1f, false); if (!Configuration.Other.ShowRemainingTime) return scaleCache.Scale; if (_lastOnline.TryGetValue(targetID, out var lastOnline)) scaleCache.RemainingTime = System.TimeSpan.FromHours(_damageScaleKeys.Count > 0 ? _damageScaleKeys[^1] - lastOnline.Hours : 0d); return scaleCache.Scale; } private float GetDamageScale(in ulong targetID, in PlayerScaleCache scaleCache = null) { if (!_lastOnline.TryGetValue(targetID, out var lastOnline) || (!Configuration.RaidProtection.OnlineRaidProtection && !lastOnline.IsOffline)) return -1f; UpdateRemainingTime(scaleCache); if (Configuration.RaidProtection.AbsoluteTimeScale.Count > 0 && _absolutTimeScaleKeys.Count > 0) { var absoluteTimeScale = GetAbsoluteTimeScale(); if (absoluteTimeScale is not -1f) return absoluteTimeScale; } if (Configuration.RaidProtection.DamageScale.Count > 0 && _damageScaleKeys.Count > 0) return GetOfflineTimeScale(); return -1f; void UpdateRemainingTime(in PlayerScaleCache scaleCache = null) { if (Configuration.Other.ShowRemainingTime && scaleCache is not null) scaleCache.RemainingTime = System.TimeSpan.FromHours(_damageScaleKeys.Count > 0 ? _damageScaleKeys[^1] - lastOnline.Hours : 0d); } float GetOfflineTimeScale() { if (!lastOnline.IsOffline) return -1f; var minutes = System.DateTime.FromBinary(lastOnline.LastOnline - lastOnline.LastConnect).Minute; if (minutes < Configuration.RaidProtection.CooldownQualifyMinutes && lastOnline.LastConnect <= 0L) return -1f; if (lastOnline.Hours < _damageScaleKeys[0]) return Configuration.RaidProtection.InterimDamage; var lastValidScale = Configuration.RaidProtection.DamageScale[_damageScaleKeys[0]]; foreach (var key in _damageScaleKeys) { if (lastOnline.Hours >= key) lastValidScale = Configuration.RaidProtection.DamageScale[key]; } return lastValidScale; } float GetAbsoluteTimeScale() { var currentHour = System.TimeZoneInfo.ConvertTimeFromUtc(System.DateTime.UtcNow, _timeZone).Hour; return Configuration.RaidProtection.AbsoluteTimeScale.GetValueOrDefault(currentHour, -1f); } } private object MitigateDamage(ref HitInfo hitInfo, in float scale, in ulong targetID) { if (scale is <= -1f or 1f) return null; var isFire = hitInfo.damageTypes.GetMajorityDamageType() is Rust.DamageType.Heat or Rust.DamageType.Fun_Water; var showMessage = Configuration.Other.ShowMessage && ((isFire && hitInfo.WeaponPrefab is not null) || !isFire); var playSound = Configuration.Other.PlaySound && !isFire; if (scale == 0f) { if (showMessage) SendMessage(hitInfo, targetID); PlaySound(ref hitInfo); return true; } hitInfo.damageTypes.ScaleAll(scale); if (!(scale < 1)) return null; if (showMessage) SendMessage(hitInfo, targetID, scale.ToPercent()); PlaySound(ref hitInfo); return null; void PlaySound(ref HitInfo hitInfo) { if (playSound && hitInfo.InitiatorPlayer is not null) Effect.server.Run(Configuration.Other.SoundPath, hitInfo.InitiatorPlayer.transform.position, UnityEngine.Vector3.zero); } } #endregion Core Methods #region Game Tip Message private bool _isVehicle; private void SendMessage(in HitInfo hitInfo, in ulong targetID, in float amount = 100f) { if (hitInfo.InitiatorPlayer is null) return; var initiator = hitInfo.InitiatorPlayer; if (!_scaleCache.TryGetValue(initiator.userID.Get(), out var playerScaleCache)) { playerScaleCache = new PlayerScaleCache(System.DateTime.UtcNow, -1f, targetID.HasPermission(Configuration.Permission.Protect)); _scaleCache[initiator.userID.Get()] = playerScaleCache; } if (playerScaleCache.ActiveGameTipMessage) return; ShowMessageTip(initiator, targetID, amount); playerScaleCache.ActiveGameTipMessage = true; if (playerScaleCache.Action is null) playerScaleCache.CacheAction(initiator); ServerMgr.Instance.Invoke(playerScaleCache.Action, Configuration.Other.MessageDuration); } private void ShowMessageTip(in BasePlayer player, in ulong targetID, in float amount = 100f) { _sb.Clear(); _sb.Append(Msg(!_isVehicle ? LANG_PROTECTION_MESSAGE_BUILDING : LANG_PROTECTION_MESSAGE_VEHICLE, player.UserIDString)); if (Configuration.Other.ShowRemainingTime && _scaleCache.TryGetValue(targetID, out var playerScaleCache)) { var remainingTime = playerScaleCache.RemainingTime; _sb.Append(" (") .Append(remainingTime.Days).Append("d:") .Append(remainingTime.Hours).Append("h:") .Append(remainingTime.Minutes).Append("m)"); } _sb.Replace(LANG_MESSAGE_AMOUNT, $"{amount}"); _sb.Replace(LANG_MESSAGE_COLOR, GetColor(amount)); player.SendConsoleCommand(COMMAND_SHOWGAMETIP, _sb.ToString()); } private string GetColor(in float amount) { return amount switch { 100f => COLOR_RED, > 50f and < 100f => COLOR_ORANGE, > 25f and <= 50f => COLOR_YELLOW, > 0f and <= 25f => COLOR_AQUA, 0f => COLOR_GREEN, _ => COLOR_WHITE }; } #endregion Game Tip Message #region Clans/Teams Integration private string GetClanTag(in ulong userID) { if (_clanTagCache.TryGetValue(userID, out var tag)) return tag; var team = GetTeam(userID); if (!(team?.members.Count > 0)) return null; tag = Clans?.Call("GetClanOf", userID); _clanTagCache[userID] = tag; return tag; } private List GetClanMembers(in string tag) => string.IsNullOrEmpty(tag) ? null : _clanMemberCache.TryGetValue(tag, out var members) ? members : CacheClan(tag); private RelationshipManager.PlayerTeam GetTeam(in ulong userID) => RelationshipManager.ServerInstance.FindPlayersTeam(userID); private List GetTeamMembers(in ulong userID) { _tmpList.Clear(); var team = GetTeam(userID); if (team?.members.Count > 0) _tmpList.AddRange(team.members); return _tmpList.Count > 0 ? _tmpList : null; } private ulong GetOfflineMember(in ICollection members) { if (members is null || members.Count == 0) return 0UL; var result = 0UL; var comparisonValue = Configuration.Team.TeamFirstOffline ? float.MinValue : float.MaxValue; foreach (var memberID in members) { if (!_lastOnline.TryGetValue(memberID, out var lastOnlineMember)) continue; var memberMinutes = lastOnlineMember.Minutes; // If ClanFirstOffline is true, find the member who has been offline the longest // Else, find the member who has been offline the shortest if ((!Configuration.Team.TeamFirstOffline || !(memberMinutes > comparisonValue)) && (Configuration.Team.TeamFirstOffline || !(memberMinutes < comparisonValue))) continue; comparisonValue = memberMinutes; result = memberID; } return result; } private void FreeClanPoolList(in string tag) { if (_clanMemberCache.TryGetValue(tag, out var list)) Facepunch.Pool.FreeUnmanaged(ref list); } private void FreeAllClanPoolLists() { foreach (var list in _clanMemberCache.Values) { var tmpList = list; Facepunch.Pool.FreeUnmanaged(ref tmpList); } } #region Clans Hooks private void OnPluginLoaded(Plugin plugin) { if (plugin.Name == nameof(Clans)) Clans = plugin; } private void OnPluginUnloaded(Plugin plugin) { if (plugin.Name == nameof(Clans)) Clans = null; } private void OnClanCreate(string tag) => CacheClan(tag); private void OnClanUpdate(string tag) => CacheClan(tag); private void OnClanMemberJoined(string userID, string tag) { if (_clanMemberCache.TryGetValue(tag, out var clan)) clan.Add(ulong.Parse(userID)); else CacheClan(tag); } private void OnClanMemberGone(string userID, string tag) { if (_clanMemberCache.TryGetValue(tag, out var clan)) clan.Remove(ulong.Parse(userID)); else CacheClan(tag); } private void OnClanDisbanded(string tag, List _memberUserIDs) { FreeClanPoolList(tag); _ = _clanMemberCache.Remove(tag); } private void OnClanDestroy(string tag) { FreeClanPoolList(tag); _ = _clanMemberCache.Remove(tag); } #endregion Clans Hooks #region Team Hooks private object OnTeamDisband(RelationshipManager.PlayerTeam team) { if (team is null || team.members.Count == 0) return null; if (Configuration.Team.TeamAvoidAbuse && AnyPlayersOffline(team.members)) return true; if (!Configuration.Team.TeamEnablePenalty) return null; foreach (var memberID in team.members) { if (_lastOnline.TryGetValue(memberID, out var member)) member.EnablePenalty(Configuration.Team.TeamPenaltyDuration); } return null; } private object OnTeamKick(RelationshipManager.PlayerTeam team, BasePlayer _player, ulong _target) { if (team is null || team.members.Count == 0) return null; if (Configuration.Team.TeamAvoidAbuse && AnyPlayersOffline(team.members)) return true; if (!Configuration.Team.TeamEnablePenalty) return null; foreach (var memberID in team.members) { if (_lastOnline.TryGetValue(memberID, out var member)) member.EnablePenalty(Configuration.Team.TeamPenaltyDuration); } return null; } private object OnTeamLeave(RelationshipManager.PlayerTeam team, BasePlayer _player) { if (team is null || team.members.Count == 0) return null; if (Configuration.Team.TeamAvoidAbuse && AnyPlayersOffline(team.members)) return true; if (!Configuration.Team.TeamEnablePenalty) return null; foreach (var memberID in team.members) { if (_lastOnline.TryGetValue(memberID, out var member)) member.EnablePenalty(Configuration.Team.TeamPenaltyDuration); } return null; } #endregion Team Hooks #endregion Clans/Teams Integration #region Commands #region ChatCommands private static readonly UnityEngine.RaycastHit[] RaycastHits = new UnityEngine.RaycastHit[1]; private static UnityEngine.Ray _ray; private void cmdStatus(BasePlayer player, string _command, string[] args) { if (player is null) return; if (args is null || args.Length != 0) { player.ChatMessage(GetStatusText(args)); return; } _ray.origin = player.eyes.position; _ray.direction = player.eyes.HeadForward(); var hitCount = UnityEngine.Physics.RaycastNonAlloc(_ray, RaycastHits, 50f, Rust.Layers.Solid); if (hitCount > 0) { var entity = RaycastHits[0].GetEntity(); if (entity is null || !IsProtected((BaseCombatEntity)entity)) { player.ChatMessage("Not a protected player structure."); return; } var authorizedPlayers = GetAuthorizedPlayers((BaseCombatEntity)entity); if (authorizedPlayers is null || authorizedPlayers.Count == 0) { player.ChatMessage("Ownerless structure."); return; } foreach (var id in authorizedPlayers) { if (!id.IsSteamId()) { player.ChatMessage("Ownerless structure."); return; } break; } var targetID = entity.OwnerID; if (entity.OwnerID == 0UL || !authorizedPlayers.Contains(entity.OwnerID)) { using var enumerator = authorizedPlayers.GetEnumerator(); enumerator.MoveNext(); targetID = enumerator.Current; } targetID = GetRecentActiveMemberAll(targetID, authorizedPlayers); player.ChatMessage(GetStatusText(new[] { targetID.ToString() })); } else player.ChatMessage("You are looking at nothing or you are too far away."); } private void cmdHelp(BasePlayer player, string _command, string[] _args) { if (player is null) return; player.ChatMessage(GetHelpText(player.userID.Get())); } private void cmdFillOnlineTimes(BasePlayer player, string command, string[] args) { var currentTime = System.DateTime.UtcNow; var playerCount = 0; foreach (var currentPlayer in BasePlayer.allPlayerList) { UpdateLastOnline(currentPlayer, currentTime); CacheDamageScale(currentPlayer.userID.Get(), -1f, true); playerCount++; } SaveData(); if (player is null) return; var msg = $"Updated the {nameof(LastOnlineData)}.json file for {playerCount} players."; player.ChatMessage(msg); } private void cmdTestOffline(BasePlayer player, string _command, string[] args) { if (player is null || args is null || args.Length == 0 || args.Length > 2) { if (player is not null) player.ChatMessage(MESSAGE_INVALID_SYNTAX); return; } var userID = player.userID.Get(); if (args.Length == 2) { userID = _playerManager.GetPlayer(args[0])?.userID.Get() ?? 0UL; if (userID == 0UL && !ulong.TryParse(args[0], out userID)) { player.ChatMessage(MESSAGE_PLAYER_NOT_FOUND); return; } } if (!double.TryParse(args[^1], out var hours)) { player.ChatMessage(MESSAGE_INVALID_SYNTAX); return; } if (_lastOnline.TryGetValue(userID, out var target)) { target.LastOnlineDT = target.LastOnlineDT.Subtract(System.TimeSpan.FromHours(hours)); player.ChatMessage($"{target.UserName} | {System.TimeZoneInfo.ConvertTimeFromUtc(target.LastOnlineDT, _timeZone)}"); } else { player.ChatMessage(MESSAGE_PLAYER_NOT_FOUND); return; } CacheDamageScale(userID, -1f, true); } private void cmdTestOnline(BasePlayer player, string _command, string[] args) { if (player is null || args is null || args.Length == 0 || args.Length > 1) { if (player is not null) player.ChatMessage(MESSAGE_INVALID_SYNTAX); return; } var userID = player.userID.Get(); if (args.Length == 1) { userID = _playerManager.GetPlayer(args[0])?.userID.Get() ?? 0UL; if (userID == 0UL && !ulong.TryParse(args[0], out userID)) { player.ChatMessage(MESSAGE_PLAYER_NOT_FOUND); return; } } if (_lastOnline.TryGetValue(userID, out var target)) { target.LastOnlineDT = System.DateTime.UtcNow; player.ChatMessage($"{target.UserName} | {System.TimeZoneInfo.ConvertTimeFromUtc(target.LastOnlineDT, _timeZone)}"); } else { player.ChatMessage(MESSAGE_PLAYER_NOT_FOUND); return; } CacheDamageScale(userID, -1f, true); } private void cmdTestPenalty(BasePlayer player, string _command, string[] args) { if (player is null || args is null || args.Length == 0 || args.Length > 2) { if (player is not null) player.ChatMessage(MESSAGE_INVALID_SYNTAX); return; } var userID = player.userID.Get(); if (args.Length == 2) { userID = _playerManager.GetPlayer(args[0])?.userID.Get() ?? 0UL; if (userID == 0UL && !ulong.TryParse(args[0], out userID)) { player.ChatMessage(MESSAGE_PLAYER_NOT_FOUND); return; } } if (!float.TryParse(args[^1], out var duration)) { player.ChatMessage(MESSAGE_INVALID_SYNTAX); return; } if (_lastOnline.TryGetValue(userID, out var target)) { if (duration > 0f) { target.EnablePenalty(duration); player.ChatMessage($"{target.UserName} | Penalty until {System.TimeZoneInfo.ConvertTimeFromUtc(target.PenaltyEndDT, _timeZone)}"); } else { target.DisablePenalty(); player.ChatMessage($"{target.UserName} | Penalty disabled"); } } else { player.ChatMessage(MESSAGE_PLAYER_NOT_FOUND); return; } CacheDamageScale(userID, -1f, true); } #endregion ChatCommands #region ConsoleCommands private void ccFillOnlineTimes(ConsoleSystem.Arg arg) { var currentTime = System.DateTime.UtcNow; var playerCount = 0; foreach (var currentPlayer in BasePlayer.allPlayerList) { UpdateLastOnline(currentPlayer, currentTime); CacheDamageScale(currentPlayer.userID.Get(), -1f, true); playerCount++; } SaveData(); var msg = $"Updated the {nameof(LastOnlineData)}.json file for {playerCount} players."; SendReply(arg, msg); } private void ccUpdatePermissions(ConsoleSystem.Arg arg) { foreach (var key in _scaleCache.Keys) _scaleCache[key].HasPermission = key.HasPermission(Configuration.Permission.Protect); SendReply(arg, "Updated the permission status for all players."); } private void ccUpdatePrefabList(ConsoleSystem.Arg arg) { var count = Configuration.RaidProtection.Prefabs.Count; if (arg.Args.Length == 1 && arg.Args[0] == "true") Configuration.RaidProtection.Prefabs = GetPrefabNames(); Configuration.RaidProtection.Prefabs.UnionWith(GetPrefabNames()); count = Configuration.RaidProtection.Prefabs.Count - count; CachePrefabs(); SaveConfig(); SendReply(arg, $"Updated the Prefabs to protect list in the configuration. {(count >= 0 ? $"Added {count}" : $"Removed {-count}")} Prefab(s)"); } private void ccDumpPrefabList(ConsoleSystem.Arg arg) { Configuration.RaidProtection.Prefabs.Clear(); CachePrefabs(); SaveConfig(); SendReply(arg, $"Cleared the Prefabs to protect list in the configuration."); } #endregion ConsoleCommands #endregion Commands #region Lang protected override void LoadDefaultMessages() => LoadMessages(); private void LoadMessages() { lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"This building is protected: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"This vehicle is protected: {LANG_MESSAGE_AMOUNT}%" } }, this); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Hierdie gebou is beskerm: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"Hierdie voertuig is beskerm: {LANG_MESSAGE_AMOUNT}%" } }, this, "af"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"{LANG_MESSAGE_AMOUNT}% :هذا المبنى محمي" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"{LANG_MESSAGE_AMOUNT}% :هذه السيارة محمية" } }, this, "ar"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Aquest edifici està protegit: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"Aquest vehicle està protegit: {LANG_MESSAGE_AMOUNT}%" } }, this, "ca"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Tato budova je chráněna: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"Toto vozidlo je chráněno: {LANG_MESSAGE_AMOUNT}%" } }, this, "cs"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Denne bygning er beskyttet: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"Dette køretøj er beskyttet: {LANG_MESSAGE_AMOUNT}%" } }, this, "da"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Dieses Gebäude ist geschützt: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"Dieses Fahrzeug ist geschützt: {LANG_MESSAGE_AMOUNT}%" } }, this, "de"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"{LANG_MESSAGE_AMOUNT}% :הבניין הזה מוגן" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"{LANG_MESSAGE_AMOUNT}%: הרכב הזה מוגן" } }, this, "he"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Ez az épület védett: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"Ez a jármű védett: {LANG_MESSAGE_AMOUNT}%" } }, this, "hu"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Αυτό το κτίριο είναι προστατευμένο: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"Αυτό το όχημα είναι προστατευμένο: {LANG_MESSAGE_AMOUNT}%" } }, this, "el"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Este edificio está protegido: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"Este vehículo está protegido: {LANG_MESSAGE_AMOUNT}%" } }, this, "es-ES"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Tämä rakennus on suojattu: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"Tämä ajoneuvo on suojattu: {LANG_MESSAGE_AMOUNT}%" } }, this, "fi"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Ce bâtiment est protégé : {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"Ce véhicule est protégé : {LANG_MESSAGE_AMOUNT}%" } }, this, "fr"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Questo edificio è protetto: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"Questo veicolo è protetto: {LANG_MESSAGE_AMOUNT}%" } }, this, "it"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"この建物は保護されています: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"この車両は保護されています: {LANG_MESSAGE_AMOUNT}%" } }, this, "ja"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"이 건물은 보호되고 있습니다: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"이 차량은 보호되고 있습니다: {LANG_MESSAGE_AMOUNT}%" } }, this, "ko"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Dit gebouw is beschermd: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"Dit voertuig is beschermd: {LANG_MESSAGE_AMOUNT}%" } }, this, "nl"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Denne bygningen er beskyttet: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"Dette kjøretøyet er beskyttet: {LANG_MESSAGE_AMOUNT}%" } }, this, "no"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Ten budynek jest chroniony: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"To pojazd jest chroniony: {LANG_MESSAGE_AMOUNT}%" } }, this, "pl"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Este edifício está protegido: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"Este veículo está protegido: {LANG_MESSAGE_AMOUNT}%" } }, this, "pt"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Această clădire este protejată: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"Acest vehicul este protejat: {LANG_MESSAGE_AMOUNT}%" } }, this, "ro"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Ова зграда је заштићена: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"Ово возило је заштићено: {LANG_MESSAGE_AMOUNT}%" } }, this, "sr"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Denna byggnad är skyddad: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"Detta fordon är skyddat: {LANG_MESSAGE_AMOUNT}%" } }, this, "sv-SE"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Ця будівля захищена: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"Цей транспортний засіб захищено: {LANG_MESSAGE_AMOUNT}%" } }, this, "uk"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Tòa nhà này được bảo vệ: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"Phương tiện này được bảo vệ: {LANG_MESSAGE_AMOUNT}%" } }, this, "vi"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"该建筑受到保护: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"该车辆受到保护: {LANG_MESSAGE_AMOUNT}%" } }, this, "zh-CN"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"該建築受到保護: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"該車輛受到保護: {LANG_MESSAGE_AMOUNT}%" } }, this, "zh-TW"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Arrr! This here stronghold be fortified: {LANG_MESSAGE_AMOUNT}%, matey!" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"Yo ho ho! This here ship be secured: {LANG_MESSAGE_AMOUNT}%, aye!" } }, this, "en-PT"); } #endregion Lang #region Texts private string GetStatusText(in string[] args) { if (args is null || args.Length != 1) return MESSAGE_INVALID_SYNTAX; var userID = _playerManager.GetPlayer(args[0])?.userID.Get() ?? 0UL; if (userID == 0UL && !ulong.TryParse(args[0], out userID)) return MESSAGE_PLAYER_NOT_FOUND; if (!_lastOnline.TryGetValue(userID, out var lastOnline)) return MESSAGE_PLAYER_NOT_FOUND; var isOnline = lastOnline.IsOnline; var onlineColor = isOnline ? COLOR_GREEN : COLOR_RED; _sb.Clear(); _sb.AppendLine($"Offline Raid Protection Status {lastOnline.UserName}"); _sb.AppendLine($"Player Status {(isOnline ? "Online" : $"Offline {System.TimeZoneInfo.ConvertTimeFromUtc(lastOnline.LastOnlineDT, _timeZone)}")}"); AppendTeamOrClanMembersStatus(userID); var penaltyEnabled = lastOnline.PenaltyEnd >= System.DateTime.UtcNow.Ticks; if (Configuration.Team.TeamEnablePenalty) _sb.AppendLine($"Penalty Status {(penaltyEnabled ? $"Enabled {System.TimeZoneInfo.ConvertTimeFromUtc(lastOnline.PenaltyEndDT, _timeZone)}" : $"Disabled")}"); if (penaltyEnabled) return _sb.ToString(); var scale = GetDamageScale(GetRecentActiveMemberAll(userID)); var prot = scale.ToPercent(); if (scale is not -1) _sb.AppendLine($"Scale {scale} ({(prot >= 0f ? $"{prot}% Protection" : $"+{-prot}% Damage")})"); return _sb.ToString(); } private void AppendTeamOrClanMembersStatus(in ulong userID) { if (!Configuration.Team.TeamShare) return; var tag = Clans is not null ? GetClanTag(userID) : null; var members = !string.IsNullOrEmpty(tag) ? GetClanMembers(tag) : GetTeamMembers(userID); if (!(members?.Count > 1)) return; _textTable.Clear(); _textTable.AddColumn($"{(Clans is not null ? TEXT_CLAN_MEMBER : TEXT_TEAM_MEMBER)}"); foreach (var member in members) { if (userID == member) continue; if (!_lastOnline.TryGetValue(member, out var m)) continue; var memberOnline = m.IsOnline; var newRow = $"{m.UserName} | {(memberOnline ? $"Online" : $"Offline | {System.TimeZoneInfo.ConvertTimeFromUtc(m.LastOnlineDT, _timeZone)}")}"; _textTable.AddRow(newRow); } _sb.AppendLine(_textTable.ToString()); } private string GetHelpText(in ulong userID) { _sb.Clear(); _sb.AppendLine($"Offline Raid Protection Info {System.TimeZoneInfo.ConvertTimeFromUtc(System.DateTime.UtcNow, _timeZone):HH:mm:ss} {_timeZone.DisplayName.Split(' ')[0]}"); if (Configuration.RaidProtection.AbsoluteTimeScale.Keys.Count > 0) { foreach (var key in _absolutTimeScaleKeys) { var scalePercent = $"{Configuration.RaidProtection.AbsoluteTimeScale[key].ToPercent()}"; var hours = key.ToString(); _sb.AppendLine($"At {hours} o'clock: {(scalePercent.ToFloat() >= 0f ? $"{scalePercent}% Protection" : $"+{-scalePercent.ToFloat()}% Damage")}"); } } if (Configuration.RaidProtection.DamageScale.Keys.Count > 0) { var interimDamageScalePercent = Configuration.RaidProtection.InterimDamage.ToPercent(); if (Configuration.RaidProtection.CooldownMinutes > 0) { _sb.AppendLine($"First {Configuration.RaidProtection.CooldownMinutes} minutes: 0% Protection") .AppendLine($"Between {Configuration.RaidProtection.CooldownMinutes} minutes and {_damageScaleKeys[0]} hours: {interimDamageScalePercent}% Protection"); } else _sb.AppendLine($"First {_damageScaleKeys[0]} hour(s): {interimDamageScalePercent}% Protection"); foreach (var key in _damageScaleKeys) { var scalePercent = $"{Configuration.RaidProtection.DamageScale[key].ToPercent()}"; _sb.AppendLine($"After {key} hours: {(scalePercent.ToFloat() >= 0f ? $"{scalePercent}% Protection" : $"+{-scalePercent.ToFloat()}% Damage")}"); } } if (!Configuration.Team.TeamEnablePenalty || !_lastOnline.TryGetValue(userID, out var lastOnline)) return _sb.ToString(); var penaltyEnabled = lastOnline.PenaltyEnd >= System.DateTime.UtcNow.Ticks; _sb.AppendLine($"Penalty Status {(penaltyEnabled ? $"Enabled {System.TimeZoneInfo.ConvertTimeFromUtc(lastOnline.PenaltyEndDT, _timeZone):HH:mm:ss}" : $"Disabled")}"); return _sb.ToString(); } #endregion Texts #region Helper Methods private string Msg(in string key, in string userID = null) => lang.GetMessage(key, this, userID); #endregion Helper Methods } } #region Extension Methods namespace Carbon.Plugins.OfflineRaidProtectionEx { public static class ExtensionMethods { private static readonly Permission P; static ExtensionMethods() => P = Interface.Oxide.GetLibrary(); private static bool HasPermission(this string userID, string permission) => !string.IsNullOrEmpty(userID) && P.UserHasPermission(userID, permission); public static bool HasPermission(this BasePlayer player, string permission) => player.UserIDString.HasPermission(permission); public static bool HasPermission(this ulong userID, string permission) => userID.ToString().HasPermission(permission); public static float ToPercent(this float value) => 100f - (value * 100f); } } #endregion Extension Methods #else using Facepunch.Extend; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Oxide.Core; using Oxide.Core.Libraries; using Oxide.Core.Plugins; using Oxide.Plugins.OfflineRaidProtectionEx; using System.Collections.Generic; using System.Linq; using System.Text; namespace Oxide.Plugins { [Info("Offline Raid Protection", "realedwin", "1.1.15"), Description("Prevents/reduces offline raids by other players")] public sealed class OfflineRaidProtection : RustPlugin { #region Fields [PluginReference] private Plugin Clans; private static OfflineRaidProtection Instance { get; set; } private readonly PlayerManager _playerManager = new(); private readonly Dictionary _scaleCache = new(); private readonly Dictionary> _clanMemberCache = new(); private readonly Dictionary _clanTagCache = new(); private readonly Dictionary _prefabCache = new(); private readonly List _damageScaleKeys = new(); private readonly List _absolutTimeScaleKeys = new(); private System.TimeZoneInfo _timeZone = System.TimeZoneInfo.Utc; // Default timezone #region Temp private readonly List _tmpList = new(); private readonly HashSet _tmpHashSet = new(); private readonly HashSet _tmpHashSet2 = new(); private readonly StringBuilder _sb = new(); private readonly TextTable _textTable = new(); #endregion Temp #region Constants private const string COMMAND_HIDEGAMETIP = "gametip.hidegametip", COMMAND_SHOWGAMETIP = "gametip.showgametip", LANG_MESSAGE_AMOUNT = "{amount}", LANG_MESSAGE_COLOR = "{color}", LANG_MESSAGE_NOPERMISSION = "You don't have the permission to use this command", LANG_PROTECTION_MESSAGE_BUILDING = "Protection Message Building", LANG_PROTECTION_MESSAGE_VEHICLE = "Protection Message Vehicle", MESSAGE_INVALID_SYNTAX = "Invalid Syntax", MESSAGE_PLAYER_NOT_FOUND = "No player found", MESSAGE_TITLE_SIZE = "15", TEXT_CLAN_MEMBER = "Clan Members", TEXT_TEAM_MEMBER = "Team Members"; #region Colors private const string COLOR_AQUA = "#1ABC9C", COLOR_BLUE = "#3498DB", COLOR_DARK_GREEN = "#1F8B4C", COLOR_GREEN = "#57F287", COLOR_ORANGE = "#E67E22", COLOR_RED = "#ED4245", COLOR_WHITE = "white", COLOR_YELLOW = "#FFFF00"; #endregion Colors #endregion Constants #endregion Fields #region Classes private sealed class ConfigData { [JsonProperty(PropertyName = "Raid Protection Options")] public RaidProtectionOptions RaidProtection { get; set; } [JsonProperty(PropertyName = "Team Options")] public TeamOptions Team { get; set; } [JsonProperty(PropertyName = "Command Options")] public CommandOptions Command { get; set; } [JsonProperty(PropertyName = "Permission Options")] public PermissionOptions Permission { get; set; } [JsonProperty(PropertyName = "Other Options")] public OtherOptions Other { get; set; } [JsonProperty(PropertyName = "Timezone Options")] public TimeZoneOptions TimeZone { get; set; } public VersionNumber Version { get; set; } public class RaidProtectionOptions { [JsonProperty(PropertyName = "Only mitigate damage caused by players")] public bool OnlyPlayerDamage { get; set; } [JsonProperty(PropertyName = "Protect players that are online")] public bool OnlineRaidProtection { get; set; } [JsonProperty(PropertyName = "Scale of damage depending on the current hour of the real day")] public Dictionary AbsoluteTimeScale { get; set; } [JsonProperty(PropertyName = "Scale of damage depending on the offline time in hours")] public Dictionary DamageScale { get; set; } [JsonProperty(PropertyName = "Cooldown in minutes")] public int CooldownMinutes { get; set; } [JsonProperty(PropertyName = "Online time to qualify for offline raid protection in minutes")] public int CooldownQualifyMinutes { get; set; } [JsonProperty(PropertyName = "Scale of damage between the cooldown and the first configured time")] public float InterimDamage { get; set; } [JsonProperty(PropertyName = "Protect all prefabs")] public bool ProtectAll { get; set; } [JsonProperty(PropertyName = "Protect AI (animals, NPCs, Bradley and attack helicopters etc.) if 'Protect all Prefabs' is enabled")] public bool ProtectAi { get; set; } [JsonProperty(PropertyName = "Protect vehicles")] public bool ProtectVehicles { get; set; } [JsonProperty(PropertyName = "Protect twigs")] public bool ProtectTwigs { get; set; } [JsonProperty(PropertyName = "Prefabs to protect")] public HashSet Prefabs { get; set; } [JsonProperty(PropertyName = "Prefabs blacklist")] public HashSet PrefabsBlacklist { get; set; } } public class TeamOptions { [JsonProperty(PropertyName = "Enable team offline protection sharing")] public bool TeamShare { get; set; } [JsonProperty(PropertyName = "Mitigate damage by the team-mate who was offline the longest")] public bool TeamFirstOffline { get; set; } [JsonProperty(PropertyName = "Include players that are whitelisted on Codelocks")] public bool IncludeWhitelistPlayers { get; set; } [JsonProperty(PropertyName = "Prevent players from leaving or disbanding their team if at least one team member is offline")] public bool TeamAvoidAbuse { get; set; } [JsonProperty(PropertyName = "Enable offline raid protection penalty for leaving or disbanding a team")] public bool TeamEnablePenalty { get; set; } [JsonProperty(PropertyName = "Penalty duration in hours")] public float TeamPenaltyDuration { get; set; } } public class CommandOptions { [JsonProperty(PropertyName = "Commands to check offline protection status")] public string[] Commands { get; set; } [JsonProperty(PropertyName = "Command to display offline raid protection information")] public string CommandHelp { get; set; } [JsonProperty(PropertyName = "Command to fill the offline times of all players")] public string CommandFillOnlineTimes { get; set; } [JsonProperty(PropertyName = "Command to update the permission status for all players.")] public string CommandUpdatePermissions { get; set; } [JsonProperty(PropertyName = "Command to change a player's offline time")] public string CommandTestOffline { get; set; } [JsonProperty(PropertyName = "Command to change a player's offline time to the current time")] public string CommandTestOnline { get; set; } [JsonProperty(PropertyName = "Command to change a player's penalty duration")] public string CommandTestPenalty { get; set; } [JsonProperty(PropertyName = "Command to update the Prefabs to protect list")] public string CommandUpdatePrefabList { get; set; } [JsonProperty(PropertyName = "Command to dump the Prefabs to protect list")] public string CommandDumpPrefabList { get; set; } internal void RegisterCommands(Plugin plugin, OfflineRaidProtection offlineRaidProtection) { RegisterChatCommands(Commands, plugin, offlineRaidProtection.cmdStatus); RegisterChatCommands(new[] { CommandHelp }, plugin, offlineRaidProtection.cmdHelp); RegisterChatCommands(new[] { CommandFillOnlineTimes }, plugin, offlineRaidProtection.cmdFillOnlineTimes); RegisterChatCommands(new[] { CommandTestOffline }, plugin, offlineRaidProtection.cmdTestOffline); RegisterChatCommands(new[] { CommandTestOnline }, plugin, offlineRaidProtection.cmdTestOnline); RegisterChatCommands(new[] { CommandTestPenalty }, plugin, offlineRaidProtection.cmdTestPenalty); RegisterConsoleCommands(new[] { CommandFillOnlineTimes }, plugin, nameof(Instance.ccFillOnlineTimes)); RegisterConsoleCommands(new[] { CommandUpdatePermissions }, plugin, nameof(Instance.ccUpdatePermissions)); RegisterConsoleCommands(new[] { CommandUpdatePrefabList }, plugin, nameof(Instance.ccUpdatePrefabList)); RegisterConsoleCommands(new[] { CommandDumpPrefabList }, plugin, nameof(Instance.ccDumpPrefabList)); } private void RegisterChatCommands(string[] commands, Plugin plugin, System.Action callback) { foreach (var command in commands) Instance.cmd.AddChatCommand(command, plugin, callback); } private void RegisterConsoleCommands(string[] commands, Plugin plugin, string callback) { foreach (var command in commands) Instance.cmd.AddConsoleCommand(command, plugin, callback); } } public class PermissionOptions { [JsonProperty(PropertyName = "Permission required to enable offline protection")] public string Protect { get; set; } [JsonProperty(PropertyName = "Permission required to check offline protection status")] public string Check { get; set; } [JsonProperty(PropertyName = "Permission required to use admin functions")] public string Admin { get; set; } internal void RegisterPermissions(Permission permission, Plugin plugin) { string[] permissions = { Protect, Check, Admin }; foreach (var perm in permissions) permission.RegisterPermission(perm, plugin); } } public class OtherOptions { [JsonProperty(PropertyName = "Play sound when damage is mitigated")] public bool PlaySound { get; set; } [JsonProperty(PropertyName = "Asset path of the sound to be played")] public string SoundPath { get; set; } [JsonProperty(PropertyName = "Display a game tip message when a prefab is protected")] public bool ShowMessage { get; set; } [JsonProperty(PropertyName = "Game tip message shows remaining protection time")] public bool ShowRemainingTime { get; set; } [JsonProperty(PropertyName = "Message duration in seconds")] public float MessageDuration { get; set; } } public class TimeZoneOptions { [JsonProperty(PropertyName = "Timezone")] public string TimeZone { get; set; } } } private sealed class LastOnlineData { [JsonProperty(PropertyName = "User ID")] public ulong UserID { get; set; } [JsonProperty(PropertyName = "User Name")] public string UserName { get; set; } [JsonProperty(PropertyName = "Last Online")] public long LastOnline { get; set; } [JsonProperty(PropertyName = "End of Penalty")] public long PenaltyEnd { get; set; } [JsonProperty(PropertyName = "Last Connect")] public long LastConnect { get; set; } [JsonIgnore] public System.DateTime LastOnlineDT { get => System.DateTime.FromBinary(LastOnline); set => LastOnline = value.ToBinary(); } [JsonIgnore] public System.DateTime PenaltyEndDT { get => new(PenaltyEnd); private set => PenaltyEnd = value.Ticks; } [JsonIgnore] public System.DateTime LastConnectDT { get => System.DateTime.FromBinary(LastConnect); set => LastConnect = value.ToBinary(); } [JsonConstructor] public LastOnlineData(in ulong userid, in string userName, in long lastOnline, in long lastConnect) { UserID = userid; UserName = userName; LastOnline = lastOnline; LastConnect = lastConnect; } public LastOnlineData(in BasePlayer player, in System.DateTime currentTime, in bool connected = false) { UserID = player.userID.Get(); UserName = player.displayName; LastOnlineDT = currentTime; LastConnectDT = connected ? currentTime : LastConnectDT; } // [JsonIgnore] public float Days => (float)TimeSpanSinceLastOnline.TotalDays; [JsonIgnore] public float Minutes => (float)TimeSpanSinceLastOnline.TotalMinutes; [JsonIgnore] public float Hours => (float)TimeSpanSinceLastOnline.TotalHours; [JsonIgnore] private System.TimeSpan TimeSpanSinceLastOnline => System.DateTime.UtcNow - (!IsOnline ? LastOnlineDT : System.DateTime.UtcNow); [JsonIgnore] private BasePlayer Player => Instance._playerManager.GetPlayer(UserID); [JsonIgnore] public bool IsOnline => Player is not null && Player.IsConnected; [JsonIgnore] public bool IsOffline => !IsOnline && Minutes >= Configuration.RaidProtection.CooldownMinutes; public void EnablePenalty(in float duration) => PenaltyEndDT = System.DateTime.UtcNow.AddHours(duration); public void DisablePenalty() => PenaltyEnd = 0L; } private sealed class PlayerScaleCache { public float Scale { get; set; } public long Expires { get; private set; } public bool ActiveGameTipMessage { get; set; } public System.TimeSpan RemainingTime { get; set; } public bool HasPermission { get; set; } public System.Action Action { get; private set; } public PlayerScaleCache(System.DateTime expires, float scale, bool hasPermission) { ExpiresDT = expires; Scale = scale; ActiveGameTipMessage = false; HasPermission = hasPermission; Action = null; } public System.DateTime ExpiresDT { // get => new(Expires); set => Expires = value.Ticks; } public void CacheAction(BasePlayer player) => Action = GetAction(player, this); private void ClearAction() => Action = null; private static System.Action GetAction(BasePlayer player, PlayerScaleCache playerScaleCache) { return () => { playerScaleCache.ActiveGameTipMessage = false; if (player is not null) player.SendConsoleCommand(COMMAND_HIDEGAMETIP); else playerScaleCache.ClearAction(); }; } } private sealed class PlayerManager { private readonly Dictionary _playersByUserID = new(); private readonly Dictionary _playersByName = new(); public void AddPlayer(in BasePlayer player) { _playersByUserID[player.userID.Get()] = player; _playersByName[player.displayName] = player; } // public void RemovePlayer(in BasePlayer player) // { // _playersByUserID.Remove(player.userID.Get()); // _playersByName.Remove(player.displayName); // } public BasePlayer GetPlayer(in ulong userID) => _playersByUserID.TryGetValue(userID, out var player) ? player : null; public BasePlayer GetPlayer(in string displayName) { if (_playersByName.TryGetValue(displayName, out var player)) return player; if (ulong.TryParse(displayName, out var userID) && _playersByUserID.TryGetValue(userID, out player)) return player; return null; } public void Clear() { _playersByUserID.Clear(); _playersByName.Clear(); } } #endregion Classes #region Data private Dictionary _lastOnline = new(); private void SaveData() => Interface.Oxide.DataFileSystem.WriteObject($"{Name}/{nameof(LastOnlineData)}", _lastOnline); private void Save() { UpdateLastOnlineAll(); SaveData(); } private void LoadData() { try { _lastOnline = Interface.Oxide.DataFileSystem.ReadObject>($"{Name}/{nameof(LastOnlineData)}"); } catch (System.Exception ex) { PrintError(ex.ToString()); } _lastOnline ??= new(); } #endregion Data #region Config private static ConfigData Configuration { get; set; } protected override void LoadDefaultConfig() => Configuration = GetBaseConfig(); protected override void SaveConfig() => Config.WriteObject(Configuration, true); protected override void LoadConfig() { base.LoadConfig(); try { Configuration = Config.ReadObject(); if (Configuration.Version < Version) UpdateConfigValues(); Config.WriteObject(Configuration, true); SetTimeZone(); } catch (System.Exception ex) { PrintError($"There is an error in your configuration file. Using default settings\n{ex}"); LoadDefaultConfig(); } } private void UpdateConfigValues() { PrintWarning("Config update detected! Update config values..."); var baseConfig = GetBaseConfig(); if (Configuration.Version < new VersionNumber(1, 1, 8)) Configuration.Command.CommandUpdatePermissions = baseConfig.Command.CommandUpdatePermissions; if (Configuration.Version < new VersionNumber(1, 1, 15)) { Configuration.Command.CommandUpdatePrefabList = baseConfig.Command.CommandUpdatePrefabList; Configuration.Command.CommandDumpPrefabList = baseConfig.Command.CommandDumpPrefabList; Configuration.RaidProtection.CooldownQualifyMinutes = baseConfig.RaidProtection.CooldownQualifyMinutes; } Configuration.Version = Version; SaveConfig(); PrintWarning("Config update has been completed!"); } private void SetTimeZone() => _timeZone = !string.IsNullOrEmpty(Configuration.TimeZone.TimeZone) ? GetTimeZoneByID(Configuration.TimeZone.TimeZone) : _timeZone; private System.TimeZoneInfo GetTimeZoneByID(string id) => System.TimeZoneInfo.GetSystemTimeZones().FirstOrDefault(tz => tz.Id == id) ?? System.TimeZoneInfo.Utc; private ConfigData GetBaseConfig() { return new ConfigData { RaidProtection = new() { OnlyPlayerDamage = false, OnlineRaidProtection = false, AbsoluteTimeScale = new(), CooldownMinutes = 10, CooldownQualifyMinutes = 0, DamageScale = new() { { 12f, 0.25f }, { 24f, 0.5f }, { 48f, 1f }, }, InterimDamage = 0f, ProtectAll = false, ProtectAi = false, ProtectVehicles = true, ProtectTwigs = false, Prefabs = GetPrefabNames(), PrefabsBlacklist = new() }, Team = new() { TeamShare = true, TeamFirstOffline = false, IncludeWhitelistPlayers = false, TeamAvoidAbuse = false, TeamEnablePenalty = false, TeamPenaltyDuration = 24f }, Command = new() { Commands = new[] { "ao", "orp" }, CommandHelp = "raidprot", CommandFillOnlineTimes = "orp.fill.onlinetimes", CommandUpdatePermissions = "orp.update.permissions", CommandTestOffline = "orp.test.offline", CommandTestOnline = "orp.test.online", CommandTestPenalty = "orp.test.penalty", CommandUpdatePrefabList = "orp.update.prefabs", CommandDumpPrefabList = "orp.dump.prefabs", }, Permission = new() { Protect = "offlineraidprotection.protect", Check = "offlineraidprotection.check", Admin = "offlineraidprotection.admin" }, Other = new() { PlaySound = false, SoundPath = "assets/prefabs/locks/keypad/effects/lock.code.denied.prefab", ShowMessage = true, ShowRemainingTime = false, MessageDuration = 3f }, TimeZone = new() { TimeZone = "" }, Version = Version }; } private HashSet GetPrefabNames() { var prefabNames = new HashSet( ItemManager.GetItemDefinitions() .Select(itemDefinition => itemDefinition.GetComponent()) .Where(itemModDeployable => itemModDeployable is not null) .Select(itemModDeployable => System.IO.Path.GetFileNameWithoutExtension(itemModDeployable.entityPrefab.resourcePath)) .OrderBy(name => name)); var manifest = GameManifest.Current; prefabNames.UnionWith(manifest.entities .Select(entity => GameManager.server.FindPrefab(entity).GetComponent()) .Where(vehicle => vehicle is not null) .Select(vehicle => vehicle.ShortPrefabName) .OrderBy(name => name)); return prefabNames; } #endregion Config #region Hooks private void Loaded() { Instance ??= this; Configuration.Permission.RegisterPermissions(permission, this); Configuration.Command.RegisterCommands(this, this); LoadData(); UnsubscribeHooks(); } private void OnServerInitialized() => CacheData(); private void OnNewSave(string _filename) { _lastOnline.Clear(); SaveData(); } private void OnServerSave() => ServerMgr.Instance.Invoke(Instance.Save, 10f); private void OnServerShutdown() => Save(); private void Unload() { Save(); Configuration = null; Instance = null; Clans = null; _prefabCache.Clear(); _scaleCache.Clear(); _lastOnline.Clear(); _damageScaleKeys.Clear(); _absolutTimeScaleKeys.Clear(); _tmpList.Clear(); _tmpHashSet.Clear(); _tmpHashSet2.Clear(); _sb.Clear(); _textTable.Clear(); FreeAllClanPoolLists(); _clanMemberCache.Clear(); _clanTagCache.Clear(); _playerManager.Clear(); foreach (var player in BasePlayer.activePlayerList) { if (player is null) return; player.SendConsoleCommand(COMMAND_HIDEGAMETIP); } } private void OnPlayerConnected(BasePlayer player) { if (player is null) return; var currentTime = System.DateTime.UtcNow; UpdateLastOnline(player, currentTime); UpdateLastConnect(player, currentTime); _playerManager.AddPlayer(player); if (!_scaleCache.TryGetValue(player.userID.Get(), out var scaleCache)) { scaleCache = new PlayerScaleCache(currentTime, -1f, player.userID.Get().HasPermission(Configuration.Permission.Protect)); _scaleCache[player.userID.Get()] = scaleCache; } else scaleCache.CacheAction(player); } private void OnPlayerDisconnected(BasePlayer player) { if (player is null) return; var currentTime = System.DateTime.UtcNow; UpdateLastOnline(player, currentTime); } private void OnUserNameUpdated(string id, string _oldName, string newName) { if (_lastOnline.TryGetValue(ulong.Parse(id), out var lastOnline)) lastOnline.UserName = newName; } private object OnEntityTakeDamage(BaseCombatEntity entity, HitInfo hitInfo) { if (hitInfo is null || entity is null || (Configuration.RaidProtection.OnlyPlayerDamage && hitInfo.InitiatorPlayer is null)) return null; if (!hitInfo.damageTypes.Has(Rust.DamageType.Decay) && IsProtected(entity)) return OnStructureAttack(entity, ref hitInfo); return null; } #endregion Hooks #region Hook Subscribtion private void UnsubscribeHooks() { if (Configuration.Team.TeamAvoidAbuse || Configuration.Team.TeamEnablePenalty) return; Unsubscribe(nameof(OnTeamDisband)); Unsubscribe(nameof(OnTeamKick)); Unsubscribe(nameof(OnTeamLeave)); } #endregion Hook Subscribtion #region Cache Methods private void CacheData() { CachePrefabs(); CacheAllClans(); CacheDamageScaleKeys(); CacheAllPlayerScale(); CacheAllPlayers(); } private void CachePrefabs() { foreach (var itemDefinition in ItemManager.GetItemDefinitions()) { var itemModDeployable = itemDefinition.GetComponent(); if (itemModDeployable is null) continue; var shortName = System.IO.Path.GetFileNameWithoutExtension(itemModDeployable.entityPrefab.resourcePath); _prefabCache[itemModDeployable.entityPrefab.GetEntity().prefabID] = IsEntityProtected(shortName); } var manifest = GameManifest.Current; foreach (var entity in manifest.entities) { var prefab = GameManager.server.FindPrefab(entity); if (prefab is null) continue; UnityEngine.Component activeComponent = null; var componentTypes = new[] { typeof(BaseNpc), typeof(NPCPlayer), typeof(BradleyAPC), typeof(AttackHelicopter), typeof(CH47Helicopter), typeof(BaseVehicle), typeof(BasePlayer) }; foreach (var type in componentTypes) { activeComponent = prefab.GetComponent(type); if (activeComponent is not null) break; } if (activeComponent is null) continue; var baseEntity = (BaseEntity)activeComponent; var prefabId = baseEntity.prefabID; var shortName = baseEntity.ShortPrefabName; var isAi = activeComponent is BaseNpc or NPCPlayer or BradleyAPC or AttackHelicopter or CH47Helicopter or BasePlayer; var isVehicle = activeComponent is BaseVehicle && !isAi; _prefabCache[prefabId] = IsEntityProtected(shortName, isVehicle, isAi); } return; static bool IsEntityProtected(in string shortName, in bool isVehicle = false, in bool isAi = false) { return !Configuration.RaidProtection.PrefabsBlacklist.Contains(shortName) && ((Configuration.RaidProtection.ProtectVehicles && isVehicle) || (Configuration.RaidProtection.ProtectAll && !(isAi && !Configuration.RaidProtection.ProtectAi)) || (!Configuration.RaidProtection.ProtectAll && Configuration.RaidProtection.Prefabs.Contains(shortName))); } } private void CacheAllClans() { // Call the "GetAllClans" method and retrieve all clan tags var clans = Clans?.Call("GetAllClans"); if (clans is null || clans.Count == 0) return; foreach (var tag in clans) { var clanTag = tag.ToString(); if (!string.IsNullOrEmpty(clanTag)) CacheClan(clanTag); } } private List CacheClan(in string tag) { if (string.IsNullOrEmpty(tag)) return null; // Call the "GetClan" method and retrieve the clan data var clan = Clans?.Call("GetClan", tag); if (clan?["members"] is null) return null; if (!_clanMemberCache.TryGetValue(tag, out var clanMemberList)) { clanMemberList = Facepunch.Pool.Get>(); _clanMemberCache[tag] = clanMemberList; } else clanMemberList.Clear(); foreach (var memberToken in clan["members"]) { if (!ulong.TryParse(memberToken.ToString(), out var memberID) || memberID == 0) continue; clanMemberList.Add(memberID); _clanTagCache[memberID] = tag; } clan.RemoveAll(); return clanMemberList; } private void CacheDamageScaleKeys() { _damageScaleKeys.Clear(); _damageScaleKeys.AddRange(Configuration.RaidProtection.DamageScale.Keys.ToList()); _damageScaleKeys.Sort(); _absolutTimeScaleKeys.Clear(); _absolutTimeScaleKeys.AddRange(Configuration.RaidProtection.AbsoluteTimeScale.Keys.ToList()); _absolutTimeScaleKeys.Sort(); } private void CacheAllPlayerScale() { foreach (var lastOnline in _lastOnline) CacheDamageScale(lastOnline.Value.UserID, -1f, true); } private float CacheDamageScale(in ulong targetID, in float scale, in bool @default) { var currentTime = System.DateTime.UtcNow; // default var expiration = currentTime.AddMinutes(1); if (_scaleCache.TryGetValue(targetID, out var scaleCache)) { scaleCache.ExpiresDT = @default ? currentTime : expiration; scaleCache.Scale = scale; } else _scaleCache[targetID] = new PlayerScaleCache(@default ? currentTime : expiration, scale, targetID.HasPermission(Configuration.Permission.Protect)); return scale; } private void CacheAllPlayers() { foreach (var player in BasePlayer.allPlayerList) _playerManager.AddPlayer(player); } #endregion Cache Methods #region Core Methods private void UpdateLastOnlineAll() { var currentTime = System.DateTime.UtcNow; foreach (var player in BasePlayer.activePlayerList) { if (player.IsConnected) UpdateLastOnline(player, currentTime); } } private void UpdateLastOnline(in BasePlayer player, in System.DateTime currentTime) { if (_lastOnline.TryGetValue(player.userID.Get(), out var lastOnline)) { lastOnline.LastOnlineDT = currentTime; lastOnline.UserName = player.displayName ?? lastOnline.UserName; } else _lastOnline[player.userID.Get()] = new LastOnlineData(player, currentTime); } private void UpdateLastConnect(in BasePlayer player, in System.DateTime currentTime) { if (_lastOnline.TryGetValue(player.userID.Get(), out var lastOnline)) lastOnline.LastConnectDT = currentTime; else _lastOnline[player.userID.Get()] = new LastOnlineData(player, currentTime, true); } private bool IsProtected(in BaseCombatEntity entity) { // If the entity is a BuildingBlock, it's protected if (entity is BuildingBlock buildingBlock && (Configuration.RaidProtection.ProtectTwigs || buildingBlock.grade != BuildingGrade.Enum.Twigs)) return true; // If ProtectAll is enabled, only check the blacklist if (Configuration.RaidProtection.ProtectAll) return !_prefabCache.TryGetValue(entity.prefabID, out var isNotProtected) || isNotProtected; // If the entity's ID is in the cache, return it's protection status if (_prefabCache.TryGetValue(entity.prefabID, out var isProtected)) return isProtected; // If none of the above conditions are met var result = Configuration.RaidProtection.Prefabs.Contains(entity.ShortPrefabName); _prefabCache[entity.prefabID] = result; return result; } private object OnStructureAttack(in BaseCombatEntity entity, ref HitInfo hitInfo) { // Get authorized players for the entity var authorizedPlayers = GetAuthorizedPlayers(entity); if (authorizedPlayers is null || authorizedPlayers.Count == 0) return null; // Check if the TC is player-owned or NPC-owned foreach (var id in authorizedPlayers) { if (!id.IsSteamId()) return null; break; } // Determine targetID (either the entity's owner or an authorized player) var targetID = entity.OwnerID; if (entity.OwnerID == 0UL || !authorizedPlayers.Contains(entity.OwnerID)) { using var enumerator = authorizedPlayers.GetEnumerator(); enumerator.MoveNext(); targetID = enumerator.Current; } // Check if InitiatorPlayer is an authorized player if (hitInfo.InitiatorPlayer is not null && authorizedPlayers.Contains(hitInfo.InitiatorPlayer.userID.Get())) return null; // Get the most recent team member based on the configuration setting targetID = GetRecentActiveMemberAll(targetID, authorizedPlayers); if (!_lastOnline.TryGetValue(targetID, out var recentTarget) || (!Configuration.RaidProtection.OnlineRaidProtection && recentTarget.IsOnline) || recentTarget.PenaltyEnd >= System.DateTime.UtcNow.Ticks) return null; if (!Configuration.RaidProtection.OnlineRaidProtection && AnyPlayersOnline(authorizedPlayers)) return null; // No authorized players are online, mitigate the damage based on the recent member's last offline time _isVehicle = entity is BaseVehicle || entity.GetParentEntity() is BaseVehicle; return MitigateDamage(ref hitInfo, GetCachedDamageScale(targetID), targetID); } private HashSet GetAuthorizedPlayers(in BaseCombatEntity entity) { // Prioritize vehicle authorization if (Configuration.RaidProtection.ProtectVehicles) { _tmpHashSet.Clear(); if (entity is ModularCar modularCar) { foreach (var whitelistPlayer in modularCar.CarLock.WhitelistPlayers) _tmpHashSet.Add(whitelistPlayer); if (_tmpHashSet.Count > 0) return _tmpHashSet; } else { var parentEntity = entity.GetParentEntity(); if (entity is BaseVehicle || parentEntity is BaseVehicle) { GetAuthorizedPlayersVehicle(entity); if (_tmpHashSet.Count > 0) return _tmpHashSet; } } } // TC authorization var privilege = entity.GetBuildingPrivilege(); return privilege is not null ? GetAuthorizedPlayerIdsFromList(privilege.authorizedPlayers, buildingPrivlidge: privilege) : null; } private HashSet GetAuthorizedPlayersVehicle(in BaseCombatEntity entity) { var vehiclePrivilege = GetVehiclePrivilege(entity.children) ?? GetVehiclePrivilege(entity.GetParentEntity()?.children); return vehiclePrivilege is not null ? GetAuthorizedPlayerIdsFromList(vehiclePrivilege.authorizedPlayers, vehiclePrivilege: vehiclePrivilege) : null; static VehiclePrivilege GetVehiclePrivilege(in List entities) { if (entities is null) return null; foreach (var child in entities) { if (child is not VehiclePrivilege vehiclePrivilege) continue; return vehiclePrivilege; } return null; } } private HashSet GetAuthorizedPlayerIdsFromList(in ICollection authorizedPlayers, in BuildingPrivlidge buildingPrivlidge = null, in VehiclePrivilege vehiclePrivilege = null) { if (authorizedPlayers is null || authorizedPlayers.Count == 0) return null; _tmpHashSet.Clear(); foreach (var authPlayer in authorizedPlayers) _tmpHashSet.Add(authPlayer.userid); if (!Configuration.Team.IncludeWhitelistPlayers) return _tmpHashSet; if (buildingPrivlidge is not null) AddWhitelistPlayers(buildingPrivlidge.GetBuilding()?.decayEntities); if (vehiclePrivilege is not null) AddWhitelistPlayers(vehiclePrivilege.GetParentEntity()?.children); return _tmpHashSet; } private void AddWhitelistPlayers(in IEnumerable entities) { if (entities is null) return; foreach (var entity in entities) { if (entity is not Door door) continue; foreach (var doorChild in door.children) { if (doorChild is not CodeLock codeLock) continue; foreach (var whitelistPlayer in codeLock.whitelistPlayers) _tmpHashSet.Add(whitelistPlayer); } } } private ulong GetRecentActiveMemberAll(in ulong targetID, in HashSet players = null) { if (!Configuration.Team.TeamShare) return targetID; if (players is null || players.Count == 0) return GetRecentActiveMember(targetID); _tmpHashSet2.Clear(); _tmpHashSet2.Add(targetID); if (Clans is not null) { foreach (var player in players) { var tag = GetClanTag(player); if (string.IsNullOrEmpty(tag)) continue; var clanMembers = GetClanMembers(tag); _tmpHashSet2.UnionWith(clanMembers); } return GetOfflineMember(_tmpHashSet2); } foreach (var player in players) { var teamMembers = GetTeamMembers(player); if(teamMembers is null) continue; _tmpHashSet2.UnionWith(teamMembers); } return GetOfflineMember(_tmpHashSet2); } private ulong GetRecentActiveMember(in ulong targetID) { if (Clans is not null) { var tag = GetClanTag(targetID); if (string.IsNullOrEmpty(tag)) return targetID; var clanMembers = GetClanMembers(tag); return clanMembers is not null && clanMembers.Count > 0 ? GetOfflineMember(clanMembers) : targetID; } var teamMembers = GetTeamMembers(targetID); return teamMembers is not null && teamMembers.Count > 0 ? GetOfflineMember(teamMembers) : targetID; } private bool AnyPlayersOffline(in List playerIDs) { foreach (var player in playerIDs) { if (IsOffline(player)) return true; } return false; } private bool AnyPlayersOnline(in HashSet playerIDs) { foreach (var player in playerIDs) { if (IsOnline(player)) return true; } return false; } private bool IsOffline(in ulong playerID) { if (_lastOnline.TryGetValue(playerID, out var lastOnlinePlayer)) return lastOnlinePlayer.IsOffline; var player = _playerManager.GetPlayer(playerID); return player is null || !player.IsConnected; } private bool IsOnline(in ulong playerID) { if (_lastOnline.TryGetValue(playerID, out var lastOnlinePlayer)) return lastOnlinePlayer.IsOnline; var player = _playerManager.GetPlayer(playerID); return player is not null && player.IsConnected; } private float GetCachedDamageScale(in ulong targetID) { if (!_scaleCache.TryGetValue(targetID, out var scaleCache) || System.DateTime.UtcNow.Ticks > scaleCache.Expires) return CacheDamageScale(targetID, scaleCache?.HasPermission == true ? GetDamageScale(targetID, scaleCache) : -1f, false); if (!Configuration.Other.ShowRemainingTime) return scaleCache.Scale; if (_lastOnline.TryGetValue(targetID, out var lastOnline)) scaleCache.RemainingTime = System.TimeSpan.FromHours(_damageScaleKeys.Count > 0 ? _damageScaleKeys[^1] - lastOnline.Hours : 0d); return scaleCache.Scale; } private float GetDamageScale(in ulong targetID, in PlayerScaleCache scaleCache = null) { if (!_lastOnline.TryGetValue(targetID, out var lastOnline) || (!Configuration.RaidProtection.OnlineRaidProtection && !lastOnline.IsOffline)) return -1f; UpdateRemainingTime(scaleCache); if (Configuration.RaidProtection.AbsoluteTimeScale.Count > 0 && _absolutTimeScaleKeys.Count > 0) { var absoluteTimeScale = GetAbsoluteTimeScale(); if (absoluteTimeScale is not -1f) return absoluteTimeScale; } if (Configuration.RaidProtection.DamageScale.Count > 0 && _damageScaleKeys.Count > 0) return GetOfflineTimeScale(); return -1f; void UpdateRemainingTime(in PlayerScaleCache scaleCache = null) { if (Configuration.Other.ShowRemainingTime && scaleCache is not null) scaleCache.RemainingTime = System.TimeSpan.FromHours(_damageScaleKeys.Count > 0 ? _damageScaleKeys[^1] - lastOnline.Hours : 0d); } float GetOfflineTimeScale() { if (!lastOnline.IsOffline) return -1f; var minutes = System.DateTime.FromBinary(lastOnline.LastOnline - lastOnline.LastConnect).Minute; if (minutes < Configuration.RaidProtection.CooldownQualifyMinutes && lastOnline.LastConnect <= 0L) return -1f; if (lastOnline.Hours < _damageScaleKeys[0]) return Configuration.RaidProtection.InterimDamage; var lastValidScale = Configuration.RaidProtection.DamageScale[_damageScaleKeys[0]]; foreach (var key in _damageScaleKeys) { if (lastOnline.Hours >= key) lastValidScale = Configuration.RaidProtection.DamageScale[key]; } return lastValidScale; } float GetAbsoluteTimeScale() { var currentHour = System.TimeZoneInfo.ConvertTimeFromUtc(System.DateTime.UtcNow, _timeZone).Hour; return Configuration.RaidProtection.AbsoluteTimeScale.TryGetValue(currentHour, out var scale) ? scale : -1f; } } private object MitigateDamage(ref HitInfo hitInfo, in float scale, in ulong targetID) { if (scale is <= -1f or 1f) return null; var isFire = hitInfo.damageTypes.GetMajorityDamageType() is Rust.DamageType.Heat or Rust.DamageType.Fun_Water; var showMessage = Configuration.Other.ShowMessage && ((isFire && hitInfo.WeaponPrefab is not null) || !isFire); var playSound = Configuration.Other.PlaySound && !isFire; if (scale == 0f) { if (showMessage) SendMessage(hitInfo, targetID); PlaySound(ref hitInfo); return true; } hitInfo.damageTypes.ScaleAll(scale); if (!(scale < 1)) return null; if (showMessage) SendMessage(hitInfo, targetID, scale.ToPercent()); PlaySound(ref hitInfo); return null; void PlaySound(ref HitInfo hitInfo) { if (playSound && hitInfo.InitiatorPlayer is not null) Effect.server.Run(Configuration.Other.SoundPath, hitInfo.InitiatorPlayer.transform.position, UnityEngine.Vector3.zero); } } #endregion Core Methods #region Game Tip Message private bool _isVehicle; private void SendMessage(in HitInfo hitInfo, in ulong targetID, in float amount = 100f) { if (hitInfo.InitiatorPlayer is null) return; var initiator = hitInfo.InitiatorPlayer; if (!_scaleCache.TryGetValue(initiator.userID.Get(), out var playerScaleCache)) { playerScaleCache = new PlayerScaleCache(System.DateTime.UtcNow, -1f, targetID.HasPermission(Configuration.Permission.Protect)); _scaleCache[initiator.userID.Get()] = playerScaleCache; } if (playerScaleCache.ActiveGameTipMessage) return; ShowMessageTip(initiator, targetID, amount); playerScaleCache.ActiveGameTipMessage = true; if (playerScaleCache.Action is null) playerScaleCache.CacheAction(initiator); ServerMgr.Instance.Invoke(playerScaleCache.Action, Configuration.Other.MessageDuration); } private void ShowMessageTip(in BasePlayer player, in ulong targetID, in float amount = 100f) { _sb.Clear(); _sb.Append(Msg(!_isVehicle ? LANG_PROTECTION_MESSAGE_BUILDING : LANG_PROTECTION_MESSAGE_VEHICLE, player.UserIDString)); if (Configuration.Other.ShowRemainingTime && _scaleCache.TryGetValue(targetID, out var playerScaleCache)) { var remainingTime = playerScaleCache.RemainingTime; _sb.Append(" (") .Append(remainingTime.Days).Append("d:") .Append(remainingTime.Hours).Append("h:") .Append(remainingTime.Minutes).Append("m)"); } _sb.Replace(LANG_MESSAGE_AMOUNT, $"{amount}"); _sb.Replace(LANG_MESSAGE_COLOR, GetColor(amount)); player.SendConsoleCommand(COMMAND_SHOWGAMETIP, _sb.ToString()); } private string GetColor(in float amount) { return amount switch { 100f => COLOR_RED, > 50f and < 100f => COLOR_ORANGE, > 25f and <= 50f => COLOR_YELLOW, > 0f and <= 25f => COLOR_AQUA, 0f => COLOR_GREEN, _ => COLOR_WHITE }; } #endregion Game Tip Message #region Clans/Teams Integration private string GetClanTag(in ulong userID) { if (_clanTagCache.TryGetValue(userID, out var tag)) return tag; var team = GetTeam(userID); if (!(team?.members.Count > 0)) return null; tag = Clans?.Call("GetClanOf", userID); _clanTagCache[userID] = tag; return tag; } private List GetClanMembers(in string tag) => string.IsNullOrEmpty(tag) ? null : _clanMemberCache.TryGetValue(tag, out var members) ? members : CacheClan(tag); private RelationshipManager.PlayerTeam GetTeam(in ulong userID) => RelationshipManager.ServerInstance.FindPlayersTeam(userID); private List GetTeamMembers(in ulong userID) { _tmpList.Clear(); var team = GetTeam(userID); if (team?.members.Count > 0) _tmpList.AddRange(team.members); return _tmpList.Count > 0 ? _tmpList : null; } private ulong GetOfflineMember(in ICollection members) { if (members is null || members.Count == 0) return 0UL; var result = 0UL; var comparisonValue = Configuration.Team.TeamFirstOffline ? float.MinValue : float.MaxValue; foreach (var memberID in members) { if (!_lastOnline.TryGetValue(memberID, out var lastOnlineMember)) continue; var memberMinutes = lastOnlineMember.Minutes; // If ClanFirstOffline is true, find the member who has been offline the longest // Else, find the member who has been offline the shortest if ((!Configuration.Team.TeamFirstOffline || !(memberMinutes > comparisonValue)) && (Configuration.Team.TeamFirstOffline || !(memberMinutes < comparisonValue))) continue; comparisonValue = memberMinutes; result = memberID; } return result; } private void FreeClanPoolList(in string tag) { if (_clanMemberCache.TryGetValue(tag, out var list)) Facepunch.Pool.FreeUnmanaged(ref list); } private void FreeAllClanPoolLists() { foreach (var list in _clanMemberCache.Values) { var tmpList = list; Facepunch.Pool.FreeUnmanaged(ref tmpList); } } #region Clans Hooks private void OnPluginLoaded(Plugin plugin) { if (plugin.Name == nameof(Clans)) Clans = plugin; } private void OnPluginUnloaded(Plugin plugin) { if (plugin.Name == nameof(Clans)) Clans = null; } private void OnClanCreate(string tag) => CacheClan(tag); private void OnClanUpdate(string tag) => CacheClan(tag); private void OnClanMemberJoined(string userID, string tag) { if (_clanMemberCache.TryGetValue(tag, out var clan)) clan.Add(ulong.Parse(userID)); else CacheClan(tag); } private void OnClanMemberGone(string userID, string tag) { if (_clanMemberCache.TryGetValue(tag, out var clan)) clan.Remove(ulong.Parse(userID)); else CacheClan(tag); } private void OnClanDisbanded(string tag, List _memberUserIDs) { FreeClanPoolList(tag); _ = _clanMemberCache.Remove(tag); } private void OnClanDestroy(string tag) { FreeClanPoolList(tag); _ = _clanMemberCache.Remove(tag); } #endregion Clans Hooks #region Team Hooks private object OnTeamDisband(RelationshipManager.PlayerTeam team) { if (team is null || team.members.Count == 0) return null; if (Configuration.Team.TeamAvoidAbuse && AnyPlayersOffline(team.members)) return true; if (!Configuration.Team.TeamEnablePenalty) return null; foreach (var memberID in team.members) { if (_lastOnline.TryGetValue(memberID, out var member)) member.EnablePenalty(Configuration.Team.TeamPenaltyDuration); } return null; } private object OnTeamKick(RelationshipManager.PlayerTeam team, BasePlayer _player, ulong _target) { if (team is null || team.members.Count == 0) return null; if (Configuration.Team.TeamAvoidAbuse && AnyPlayersOffline(team.members)) return true; if (!Configuration.Team.TeamEnablePenalty) return null; foreach (var memberID in team.members) { if (_lastOnline.TryGetValue(memberID, out var member)) member.EnablePenalty(Configuration.Team.TeamPenaltyDuration); } return null; } private object OnTeamLeave(RelationshipManager.PlayerTeam team, BasePlayer _player) { if (team is null || team.members.Count == 0) return null; if (Configuration.Team.TeamAvoidAbuse && AnyPlayersOffline(team.members)) return true; if (!Configuration.Team.TeamEnablePenalty) return null; foreach (var memberID in team.members) { if (_lastOnline.TryGetValue(memberID, out var member)) member.EnablePenalty(Configuration.Team.TeamPenaltyDuration); } return null; } #endregion Team Hooks #endregion Clans/Teams Integration #region Commands #region ChatCommands private static readonly UnityEngine.RaycastHit[] RaycastHits = new UnityEngine.RaycastHit[1]; private static UnityEngine.Ray _ray; private void cmdStatus(BasePlayer player, string _command, string[] args) { if (player is null) return; if (args is null || args.Length != 0) { player.ChatMessage(player.HasPermission(Configuration.Permission.Check) ? GetStatusText(args) : LANG_MESSAGE_NOPERMISSION); return; } else if (!player.HasPermission(Configuration.Permission.Check)) { player.ChatMessage(LANG_MESSAGE_NOPERMISSION); } _ray.origin = player.eyes.position; _ray.direction = player.eyes.HeadForward(); var hitCount = UnityEngine.Physics.RaycastNonAlloc(_ray, RaycastHits, 50f, Rust.Layers.Solid); if (hitCount > 0) { var entity = RaycastHits[0].GetEntity(); if (entity is null || !IsProtected((BaseCombatEntity)entity)) { player.ChatMessage("Not a protected player structure."); return; } var authorizedPlayers = GetAuthorizedPlayers((BaseCombatEntity)entity); if (authorizedPlayers is null || authorizedPlayers.Count == 0) { player.ChatMessage("Ownerless structure."); return; } foreach (var id in authorizedPlayers) { if (!id.IsSteamId()) { player.ChatMessage("Ownerless structure."); return; } break; } var targetID = entity.OwnerID; if (entity.OwnerID == 0UL || !authorizedPlayers.Contains(entity.OwnerID)) { using var enumerator = authorizedPlayers.GetEnumerator(); enumerator.MoveNext(); targetID = enumerator.Current; } targetID = GetRecentActiveMemberAll(targetID, authorizedPlayers); player.ChatMessage(GetStatusText(new[] { targetID.ToString() })); } else player.ChatMessage("You are looking at nothing or you are too far away."); } private void cmdHelp(BasePlayer player, string _command, string[] _args) { if (player is null) return; player.ChatMessage(player.HasPermission(Configuration.Permission.Protect) ? GetHelpText(player.userID.Get()) : LANG_MESSAGE_NOPERMISSION); } private void cmdFillOnlineTimes(BasePlayer player, string command, string[] args) { if (player is null || !player.HasPermission(Configuration.Permission.Admin)) { if (player is not null) player.ChatMessage(LANG_MESSAGE_NOPERMISSION); return; } var currentTime = System.DateTime.UtcNow; var playerCount = 0; foreach (var currentPlayer in BasePlayer.allPlayerList) { UpdateLastOnline(currentPlayer, currentTime); CacheDamageScale(currentPlayer.userID.Get(), -1f, true); playerCount++; } SaveData(); var msg = $"Updated the {nameof(LastOnlineData)}.json file for {playerCount} players."; player.ChatMessage(msg); } private void cmdTestOffline(BasePlayer player, string _command, string[] args) { if (player is null || !player.HasPermission(Configuration.Permission.Admin)) { if (player is not null) player.ChatMessage(LANG_MESSAGE_NOPERMISSION); return; } if (args is null || args.Length == 0 || args.Length > 2) { player.ChatMessage(MESSAGE_INVALID_SYNTAX); return; } var userID = player.userID.Get(); if (args.Length == 2) { userID = _playerManager.GetPlayer(args[0])?.userID.Get() ?? 0UL; if (userID == 0UL && !ulong.TryParse(args[0], out userID)) { player.ChatMessage(MESSAGE_PLAYER_NOT_FOUND); return; } } if (!double.TryParse(args[^1], out var hours)) { player.ChatMessage(MESSAGE_INVALID_SYNTAX); return; } if (_lastOnline.TryGetValue(userID, out var target)) { target.LastOnlineDT = target.LastOnlineDT.Subtract(System.TimeSpan.FromHours(hours)); player.ChatMessage($"{target.UserName} | {System.TimeZoneInfo.ConvertTimeFromUtc(target.LastOnlineDT, _timeZone)}"); } else { player.ChatMessage(MESSAGE_PLAYER_NOT_FOUND); return; } CacheDamageScale(userID, -1f, true); } private void cmdTestOnline(BasePlayer player, string _command, string[] args) { if (player is null || !player.HasPermission(Configuration.Permission.Admin)) { if (player is not null) player.ChatMessage(LANG_MESSAGE_NOPERMISSION); return; } if (args is null || args.Length == 0 || args.Length > 1) { player.ChatMessage(MESSAGE_INVALID_SYNTAX); return; } var userID = player.userID.Get(); if (args.Length == 1) { userID = _playerManager.GetPlayer(args[0])?.userID.Get() ?? 0UL; if (userID == 0UL && !ulong.TryParse(args[0], out userID)) { player.ChatMessage(MESSAGE_PLAYER_NOT_FOUND); return; } } if (_lastOnline.TryGetValue(userID, out var target)) { target.LastOnlineDT = System.DateTime.UtcNow; player.ChatMessage($"{target.UserName} | {System.TimeZoneInfo.ConvertTimeFromUtc(target.LastOnlineDT, _timeZone)}"); } else { player.ChatMessage(MESSAGE_PLAYER_NOT_FOUND); return; } CacheDamageScale(userID, -1f, true); } private void cmdTestPenalty(BasePlayer player, string _command, string[] args) { if (player is null || !player.HasPermission(Configuration.Permission.Admin)) { if (player is not null) player.ChatMessage(LANG_MESSAGE_NOPERMISSION); return; } if (args is null || args.Length == 0 || args.Length > 2) { player.ChatMessage(MESSAGE_INVALID_SYNTAX); return; } var userID = player.userID.Get(); if (args.Length == 2) { userID = _playerManager.GetPlayer(args[0])?.userID.Get() ?? 0UL; if (userID == 0UL && !ulong.TryParse(args[0], out userID)) { player.ChatMessage(MESSAGE_PLAYER_NOT_FOUND); return; } } if (!float.TryParse(args[^1], out var duration)) { player.ChatMessage(MESSAGE_INVALID_SYNTAX); return; } if (_lastOnline.TryGetValue(userID, out var target)) { if (duration > 0f) { target.EnablePenalty(duration); player.ChatMessage($"{target.UserName} | Penalty until {System.TimeZoneInfo.ConvertTimeFromUtc(target.PenaltyEndDT, _timeZone)}"); } else { target.DisablePenalty(); player.ChatMessage($"{target.UserName} | Penalty disabled"); } } else { player.ChatMessage(MESSAGE_PLAYER_NOT_FOUND); return; } CacheDamageScale(userID, -1f, true); } #endregion ChatCommands #region ConsoleCommands private void ccFillOnlineTimes(ConsoleSystem.Arg arg) { if (arg is null || arg.Connection is null || !arg.Connection.userid.HasPermission(Configuration.Permission.Admin)) { SendReply(arg, LANG_MESSAGE_NOPERMISSION); return; } var currentTime = System.DateTime.UtcNow; var playerCount = 0; foreach (var currentPlayer in BasePlayer.allPlayerList) { UpdateLastOnline(currentPlayer, currentTime); CacheDamageScale(currentPlayer.userID.Get(), -1f, true); playerCount++; } SaveData(); var msg = $"Updated the {nameof(LastOnlineData)}.json file for {playerCount} players."; SendReply(arg, msg); } private void ccUpdatePermissions(ConsoleSystem.Arg arg) { if (arg is null || arg.Connection is null || !arg.Connection.userid.HasPermission(Configuration.Permission.Admin)) { SendReply(arg, LANG_MESSAGE_NOPERMISSION); return; } foreach (var key in _scaleCache.Keys) _scaleCache[key].HasPermission = key.HasPermission(Configuration.Permission.Protect); SendReply(arg, "Updated the permission status for all players."); } private void ccUpdatePrefabList(ConsoleSystem.Arg arg) { if (arg is null || arg.Connection is null || !arg.Connection.userid.HasPermission(Configuration.Permission.Admin)) { SendReply(arg, LANG_MESSAGE_NOPERMISSION); return; } var count = Configuration.RaidProtection.Prefabs.Count; if (arg.Args.Length == 1 && arg.Args[0] == "true") Configuration.RaidProtection.Prefabs = GetPrefabNames(); Configuration.RaidProtection.Prefabs.UnionWith(GetPrefabNames()); count = Configuration.RaidProtection.Prefabs.Count - count; CachePrefabs(); SaveConfig(); SendReply(arg, $"Updated the Prefabs to protect list in the configuration. {(count >= 0 ? $"Added {count}" : $"Removed {-count}")} Prefab(s)"); } private void ccDumpPrefabList(ConsoleSystem.Arg arg) { if (arg is null || arg.Connection is null || !arg.Connection.userid.HasPermission(Configuration.Permission.Admin)) { SendReply(arg, LANG_MESSAGE_NOPERMISSION); return; } Configuration.RaidProtection.Prefabs.Clear(); CachePrefabs(); SaveConfig(); SendReply(arg, $"Cleared the Prefabs to protect list in the configuration."); } #endregion ConsoleCommands #endregion Commands #region Lang protected override void LoadDefaultMessages() => LoadMessages(); private void LoadMessages() { lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"This building is protected: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"This vehicle is protected: {LANG_MESSAGE_AMOUNT}%" } }, this); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Hierdie gebou is beskerm: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"Hierdie voertuig is beskerm: {LANG_MESSAGE_AMOUNT}%" } }, this, "af"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"{LANG_MESSAGE_AMOUNT}% :هذا المبنى محمي" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"{LANG_MESSAGE_AMOUNT}% :هذه السيارة محمية" } }, this, "ar"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Aquest edifici està protegit: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"Aquest vehicle està protegit: {LANG_MESSAGE_AMOUNT}%" } }, this, "ca"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Tato budova je chráněna: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"Toto vozidlo je chráněno: {LANG_MESSAGE_AMOUNT}%" } }, this, "cs"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Denne bygning er beskyttet: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"Dette køretøj er beskyttet: {LANG_MESSAGE_AMOUNT}%" } }, this, "da"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Dieses Gebäude ist geschützt: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"Dieses Fahrzeug ist geschützt: {LANG_MESSAGE_AMOUNT}%" } }, this, "de"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"{LANG_MESSAGE_AMOUNT}% :הבניין הזה מוגן" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"{LANG_MESSAGE_AMOUNT}%: הרכב הזה מוגן" } }, this, "he"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Ez az épület védett: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"Ez a jármű védett: {LANG_MESSAGE_AMOUNT}%" } }, this, "hu"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Αυτό το κτίριο είναι προστατευμένο: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"Αυτό το όχημα είναι προστατευμένο: {LANG_MESSAGE_AMOUNT}%" } }, this, "el"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Este edificio está protegido: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"Este vehículo está protegido: {LANG_MESSAGE_AMOUNT}%" } }, this, "es-ES"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Tämä rakennus on suojattu: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"Tämä ajoneuvo on suojattu: {LANG_MESSAGE_AMOUNT}%" } }, this, "fi"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Ce bâtiment est protégé : {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"Ce véhicule est protégé : {LANG_MESSAGE_AMOUNT}%" } }, this, "fr"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Questo edificio è protetto: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"Questo veicolo è protetto: {LANG_MESSAGE_AMOUNT}%" } }, this, "it"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"この建物は保護されています: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"この車両は保護されています: {LANG_MESSAGE_AMOUNT}%" } }, this, "ja"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"이 건물은 보호되고 있습니다: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"이 차량은 보호되고 있습니다: {LANG_MESSAGE_AMOUNT}%" } }, this, "ko"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Dit gebouw is beschermd: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"Dit voertuig is beschermd: {LANG_MESSAGE_AMOUNT}%" } }, this, "nl"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Denne bygningen er beskyttet: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"Dette kjøretøyet er beskyttet: {LANG_MESSAGE_AMOUNT}%" } }, this, "no"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Ten budynek jest chroniony: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"To pojazd jest chroniony: {LANG_MESSAGE_AMOUNT}%" } }, this, "pl"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Este edifício está protegido: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"Este veículo está protegido: {LANG_MESSAGE_AMOUNT}%" } }, this, "pt"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Această clădire este protejată: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"Acest vehicul este protejat: {LANG_MESSAGE_AMOUNT}%" } }, this, "ro"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Ова зграда је заштићена: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"Ово возило је заштићено: {LANG_MESSAGE_AMOUNT}%" } }, this, "sr"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Denna byggnad är skyddad: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"Detta fordon är skyddat: {LANG_MESSAGE_AMOUNT}%" } }, this, "sv-SE"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Ця будівля захищена: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"Цей транспортний засіб захищено: {LANG_MESSAGE_AMOUNT}%" } }, this, "uk"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Tòa nhà này được bảo vệ: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"Phương tiện này được bảo vệ: {LANG_MESSAGE_AMOUNT}%" } }, this, "vi"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"该建筑受到保护: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"该车辆受到保护: {LANG_MESSAGE_AMOUNT}%" } }, this, "zh-CN"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"該建築受到保護: {LANG_MESSAGE_AMOUNT}%" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"該車輛受到保護: {LANG_MESSAGE_AMOUNT}%" } }, this, "zh-TW"); lang.RegisterMessages(new Dictionary { { LANG_PROTECTION_MESSAGE_BUILDING, $"Arrr! This here stronghold be fortified: {LANG_MESSAGE_AMOUNT}%, matey!" }, { LANG_PROTECTION_MESSAGE_VEHICLE, $"Yo ho ho! This here ship be secured: {LANG_MESSAGE_AMOUNT}%, aye!" } }, this, "en-PT"); } #endregion Lang #region Texts private string GetStatusText(in string[] args) { if (args is null || args.Length != 1) return MESSAGE_INVALID_SYNTAX; var userID = _playerManager.GetPlayer(args[0])?.userID.Get() ?? 0UL; if (userID == 0UL && !ulong.TryParse(args[0], out userID)) return MESSAGE_PLAYER_NOT_FOUND; if (!_lastOnline.TryGetValue(userID, out var lastOnline)) return MESSAGE_PLAYER_NOT_FOUND; var isOnline = lastOnline.IsOnline; var onlineColor = isOnline ? COLOR_GREEN : COLOR_RED; _sb.Clear(); _sb.AppendLine($"Offline Raid Protection Status {lastOnline.UserName}"); _sb.AppendLine($"Player Status {(isOnline ? "Online" : $"Offline {System.TimeZoneInfo.ConvertTimeFromUtc(lastOnline.LastOnlineDT, _timeZone)}")}"); AppendTeamOrClanMembersStatus(userID); var penaltyEnabled = lastOnline.PenaltyEnd >= System.DateTime.UtcNow.Ticks; if (Configuration.Team.TeamEnablePenalty) _sb.AppendLine($"Penalty Status {(penaltyEnabled ? $"Enabled {System.TimeZoneInfo.ConvertTimeFromUtc(lastOnline.PenaltyEndDT, _timeZone)}" : $"Disabled")}"); if (penaltyEnabled) return _sb.ToString(); var scale = GetDamageScale(GetRecentActiveMemberAll(userID)); var prot = scale.ToPercent(); if (scale is not -1) _sb.AppendLine($"Scale {scale} ({(prot >= 0f ? $"{prot}% Protection" : $"+{-prot}% Damage")})"); return _sb.ToString(); } private void AppendTeamOrClanMembersStatus(in ulong userID) { if (!Configuration.Team.TeamShare) return; var tag = Clans is not null ? GetClanTag(userID) : null; var members = !string.IsNullOrEmpty(tag) ? GetClanMembers(tag) : GetTeamMembers(userID); if (!(members?.Count > 1)) return; _textTable.Clear(); _textTable.AddColumn($"{(Clans is not null ? TEXT_CLAN_MEMBER : TEXT_TEAM_MEMBER)}"); foreach (var member in members) { if (userID == member) continue; if (!_lastOnline.TryGetValue(member, out var m)) continue; var memberOnline = m.IsOnline; var newRow = $"{m.UserName} | {(memberOnline ? $"Online" : $"Offline | {System.TimeZoneInfo.ConvertTimeFromUtc(m.LastOnlineDT, _timeZone)}")}"; _textTable.AddRow(newRow); } _sb.AppendLine(_textTable.ToString()); } private string GetHelpText(in ulong userID) { _sb.Clear(); _sb.AppendLine($"Offline Raid Protection Info {System.TimeZoneInfo.ConvertTimeFromUtc(System.DateTime.UtcNow, _timeZone):HH:mm:ss} {_timeZone.DisplayName.Split(' ')[0]}"); if (Configuration.RaidProtection.AbsoluteTimeScale.Keys.Count > 0) { foreach (var key in _absolutTimeScaleKeys) { var scalePercent = $"{Configuration.RaidProtection.AbsoluteTimeScale[key].ToPercent()}"; var hours = key.ToString(); _sb.AppendLine($"At {hours} o'clock: {(scalePercent.ToFloat() >= 0f ? $"{scalePercent}% Protection" : $"+{-scalePercent.ToFloat()}% Damage")}"); } } if (Configuration.RaidProtection.DamageScale.Keys.Count > 0) { var interimDamageScalePercent = Configuration.RaidProtection.InterimDamage.ToPercent(); if (Configuration.RaidProtection.CooldownMinutes > 0) { _sb.AppendLine($"First {Configuration.RaidProtection.CooldownMinutes} minutes: 0% Protection") .AppendLine($"Between {Configuration.RaidProtection.CooldownMinutes} minutes and {_damageScaleKeys[0]} hours: {interimDamageScalePercent}% Protection"); } else _sb.AppendLine($"First {_damageScaleKeys[0]} hour(s): {interimDamageScalePercent}% Protection"); foreach (var key in _damageScaleKeys) { var scalePercent = $"{Configuration.RaidProtection.DamageScale[key].ToPercent()}"; _sb.AppendLine($"After {key} hours: {(scalePercent.ToFloat() >= 0f ? $"{scalePercent}% Protection" : $"+{-scalePercent.ToFloat()}% Damage")}"); } } if (!Configuration.Team.TeamEnablePenalty || !_lastOnline.TryGetValue(userID, out var lastOnline)) return _sb.ToString(); var penaltyEnabled = lastOnline.PenaltyEnd >= System.DateTime.UtcNow.Ticks; _sb.AppendLine($"Penalty Status {(penaltyEnabled ? $"Enabled {System.TimeZoneInfo.ConvertTimeFromUtc(lastOnline.PenaltyEndDT, _timeZone):HH:mm:ss}" : $"Disabled")}"); return _sb.ToString(); } #endregion Texts #region Helper Methods private string Msg(in string key, in string userID = null) => lang.GetMessage(key, this, userID); #endregion Helper Methods } } #region Extension Methods namespace Oxide.Plugins.OfflineRaidProtectionEx { public static class ExtensionMethods { private static readonly Permission P; static ExtensionMethods() => P = Interface.Oxide.GetLibrary(); private static bool HasPermission(this string userID, string permission) => !string.IsNullOrEmpty(userID) && P.UserHasPermission(userID, permission); public static bool HasPermission(this BasePlayer player, string permission) => player.UserIDString.HasPermission(permission); public static bool HasPermission(this ulong userID, string permission) => userID.ToString().HasPermission(permission); public static float ToPercent(this float value) => 100f - (value * 100f); } } #endregion Extension Methods #endif