From bef0b10aed4bd1a091cf57091ae39ca811bed8b6 Mon Sep 17 00:00:00 2001 From: HunterZ <108939+HunterZ@users.noreply.github.com> Date: Thu, 13 Nov 2025 20:11:13 -0800 Subject: [PATCH 1/4] PBPZ: port stinkyPumpkin contrib to 1.3.1 baseline --- PlayerBasePvpZones.cs | 512 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 503 insertions(+), 9 deletions(-) diff --git a/PlayerBasePvpZones.cs b/PlayerBasePvpZones.cs index aaf7c8a..8385a64 100644 --- a/PlayerBasePvpZones.cs +++ b/PlayerBasePvpZones.cs @@ -8,11 +8,12 @@ using System.Collections; using System.Collections.Generic; using System.Globalization; +using System.Linq; using UnityEngine; namespace Oxide.Plugins; -[Info("Player Base PvP Zones", "HunterZ", "1.3.1")] +[Info("Player Base PvP Zones", "HunterZ", "1.4.0")] [Description("Maintains Zone Manager / TruePVE exclusion zones around player bases")] public class PlayerBasePvpZones : RustPlugin { @@ -20,6 +21,18 @@ public class PlayerBasePvpZones : RustPlugin [PluginReference] Plugin TruePVE; + // permission for zone toggle command + private const string PermissionToggle = "playerbasepvpzones.toggle"; + + // tracks which players have zones enabled (stored by OwnerID) + private HashSet _playersWithZonesEnabled = new HashSet(); + + // tracks pending zone enable/disable requests by player ID + private Dictionary _toggleTimers = new Dictionary(); + + // data file name for persistence + private const string DataFileName = "PlayerBasePvpZones_EnabledPlayers"; + // user-defined plugin config data private ConfigData _configData = new(); @@ -77,6 +90,45 @@ public class PlayerBasePvpZones : RustPlugin #region Core Methods + // Collect zone IDs for a player before deletion + private List CollectPlayerZoneIds(ulong playerID) + { + var zoneIds = Pool.Get>(); + + // Collect building zone IDs + foreach (var (tcID, buildingData) in _buildingData) + { + if (buildingData.ToolCupboard && + buildingData.ToolCupboard.OwnerID == playerID) + { + zoneIds.Add(GetZoneID(tcID)); + } + } + + // Collect shelter zone IDs + foreach (var (shelterID, shelterData) in _shelterData) + { + if (shelterData.LegacyShelter && + GetOwnerID(shelterData.LegacyShelter) == playerID) + { + zoneIds.Add(GetZoneID(shelterID)); + } + } + + // Collect tugboat zone IDs + foreach (var (tugboatID, tugboatData) in _tugboatData) + { + if (tugboatData.Tugboat && + tugboatData.Tugboat.authorizedPlayers.Exists( + x => x.userid == playerID)) + { + zoneIds.Add(GetZoneID(tugboatID)); + } + } + + return zoneIds; + } + // generate a current 3D bounding box around a base private Bounds CalculateBuildingBounds(BuildingPrivlidge toolCupboard) { @@ -219,6 +271,11 @@ private static bool IsValid(BaseNetworkable baseNetworkable) => null != baseNetworkable.net && baseNetworkable.transform; + private bool IsPlayerZonesEnabled(ulong playerID) + { + return _playersWithZonesEnabled.Contains(playerID); + } + private void NotifyOwnerAbort(ulong ownerID) { var player = BasePlayer.FindByID(ownerID); @@ -397,6 +454,9 @@ private void ScheduleCheckBuildingData(NetworkableId toolCupboardID) // and/or notifications per plugin configuration private void ScheduleCreateBuildingData(BuildingPrivlidge toolCupboard) { + // Check if this player has zones enabled + if (!IsPlayerZonesEnabled(toolCupboard.OwnerID)) return; + var toolCupboardID = GetNetworkableID(toolCupboard); // abort if building is already known, or if any timers are already running @@ -516,6 +576,12 @@ private void DeleteShelterData( // and/or notifications per plugin configuration private void ScheduleCreateShelterData(EntityPrivilege legacyShelter) { + var ownerID = GetOwnerID(legacyShelter); + if (null == ownerID) return; + + // Check if this player has zones enabled + if (!IsPlayerZonesEnabled((ulong)ownerID)) return; + var legacyShelterID = GetNetworkableID(legacyShelter); // abort if shelter is already known, or if any timers are already running @@ -532,9 +598,7 @@ private void ScheduleCreateShelterData(EntityPrivilege legacyShelter) () => CreateShelterData(legacyShelter))); // notify players - if (!_configData.CreateNotify) return; - var ownerID = GetOwnerID(legacyShelter); - if (null != ownerID) NotifyOwnerCreate((ulong)ownerID); + if (_configData.CreateNotify) NotifyOwnerCreate((ulong)ownerID); } // schedule a delayed shelter deletion @@ -573,6 +637,18 @@ private void ScheduleDeleteShelterData( // create a new tugboat record + zone for given tugboat private void CreateTugboatData(VehiclePrivilege tugboat) { + // Check if any authorized player has zones enabled + bool hasEnabledPlayer = false; + foreach (var auth in tugboat.authorizedPlayers) + { + if (IsPlayerZonesEnabled(auth.userid)) + { + hasEnabledPlayer = true; + break; + } + } + if (!hasEnabledPlayer) return; + // abort if tugboat object is destroyed if (!IsValid(tugboat)) return; @@ -726,6 +802,7 @@ private YieldInstruction DynamicYield() } // coroutine method to asynchronously create zones for all existing bases + // coroutine method to asynchronously create zones for enabled players only private IEnumerator CreateData() { var startTime = DateTime.UtcNow; @@ -733,18 +810,24 @@ private IEnumerator CreateData() Puts("CreateData(): Starting zone creation..."); // create zones for all existing player-owned bases + // create zones for enabled players' bases foreach (var building in BuildingManager.server.buildingDictionary.Values) { var toolCupboard = GetToolCupboard(building); if (!IsValid(toolCupboard) || !IsPlayerOwned(toolCupboard)) continue; + if (!IsPlayerZonesEnabled(toolCupboard.OwnerID)) continue; + CreateBuildingData(toolCupboard); yield return DynamicYield(); } Puts($"CreateData(): Created {_buildingData.Count} building zones..."); // create zones for all existing player-owned legacy shelters - foreach (var shelterList in LegacyShelter.SheltersPerPlayer.Values) + // create zones for enabled players' shelters + foreach (var (playerID, shelterList) in LegacyShelter.SheltersPerPlayer) { + if (!IsPlayerZonesEnabled(playerID)) continue; + foreach (var shelter in shelterList) { if (!IsValid(shelter) || @@ -760,12 +843,26 @@ private IEnumerator CreateData() Puts($"CreateData(): Created {_shelterData.Count} shelter zones..."); // create zones for all existing tugboats + // create zones for tugboats (check authorization) foreach (var serverEntity in BaseNetworkable.serverEntities) { if (serverEntity is not VehiclePrivilege tugboat || !IsValid(tugboat)) { continue; } + + // Check if any authorized player has zones enabled + bool hasEnabledPlayer = false; + foreach (var auth in tugboat.authorizedPlayers) + { + if (IsPlayerZonesEnabled(auth.userid)) + { + hasEnabledPlayer = true; + break; + } + } + if (!hasEnabledPlayer) continue; + CreateTugboatData(tugboat); yield return DynamicYield(); } @@ -776,6 +873,203 @@ private IEnumerator CreateData() _createDataCoroutine = null; } + // Method to create zones for a specific player + private int CreatePlayerZones(ulong playerID) + { + Puts($"CreatePlayerZones(): Creating zones for player {playerID}..."); + var count = 0; + + // Create building zones + foreach (var building in BuildingManager.server.buildingDictionary.Values) + { + var toolCupboard = GetToolCupboard(building); + if (!IsValid(toolCupboard) || !IsPlayerOwned(toolCupboard)) continue; + if (toolCupboard.OwnerID != playerID) continue; + + CreateBuildingData(toolCupboard); + count++; + } + + // Create shelter zones + if (LegacyShelter.SheltersPerPlayer.TryGetValue(playerID, out var shelterList)) + { + foreach (var shelter in shelterList) + { + if (!IsValid(shelter) || + !shelter.entityPrivilege.TryGet(true, out var legacyShelter) || + !IsPlayerOwned(legacyShelter)) + { + continue; + } + CreateShelterData(legacyShelter); + count++; + } + } + + // Create tugboat zones - tugboats don't have direct owner tracking + // so we need to check each one + foreach (var serverEntity in BaseNetworkable.serverEntities) + { + if (serverEntity is not VehiclePrivilege tugboat || !IsValid(tugboat)) + { + continue; + } + + // Check if this player is authorized on the tugboat + if (!tugboat.authorizedPlayers.Exists(x => x.userid == playerID)) + { + continue; + } + + CreateTugboatData(tugboat); + count++; + } + + Puts($"CreatePlayerZones(): Created {count} zone(s) for player {playerID}"); + return count; + } + + // Method to remove zones for a specific player + private void RemovePlayerZones(ulong playerID) + { + Puts($"RemovePlayerZones(): Removing zones for player {playerID}..."); + var bulkDeleteList = Pool.Get>(); + var count = 0; + + // Remove building zones + var buildingIDsToRemove = Pool.Get>(); + foreach (var (tcID, buildingData) in _buildingData) + { + if (buildingData.ToolCupboard && buildingData.ToolCupboard.OwnerID == playerID) + { + buildingIDsToRemove.Add(tcID); + } + } + foreach (var tcID in buildingIDsToRemove) + { + DeleteBuildingData(tcID, bulkDeleteList); + count++; + } + Pool.FreeUnmanaged(ref buildingIDsToRemove); + + // Cancel any pending building timers + CancelPlayerTimers(playerID, ref _buildingCheckTimers); + CancelPlayerTimers(playerID, ref _buildingCreateTimers); + CancelPlayerTimers(playerID, ref _buildingDeleteTimers); + + // Remove shelter zones + var shelterIDsToRemove = Pool.Get>(); + foreach (var (shelterID, shelterData) in _shelterData) + { + if (shelterData.LegacyShelter) + { + var ownerID = GetOwnerID(shelterData.LegacyShelter); + if (ownerID == playerID) + { + shelterIDsToRemove.Add(shelterID); + } + } + } + foreach (var shelterID in shelterIDsToRemove) + { + DeleteShelterData(shelterID, bulkDeleteList); + count++; + } + Pool.FreeUnmanaged(ref shelterIDsToRemove); + + // Cancel any pending shelter timers + CancelPlayerTimers(playerID, ref _shelterCreateTimers); + CancelPlayerTimers(playerID, ref _shelterDeleteTimers); + + // Remove tugboat zones - check authorization + var tugboatIDsToRemove = Pool.Get>(); + foreach (var (tugboatID, tugboatData) in _tugboatData) + { + if (tugboatData.Tugboat && + tugboatData.Tugboat.authorizedPlayers.Exists(x => x.userid == playerID)) + { + tugboatIDsToRemove.Add(tugboatID); + } + } + foreach (var tugboatID in tugboatIDsToRemove) + { + DeleteTugboatData(tugboatID, bulkDeleteList); + count++; + } + Pool.FreeUnmanaged(ref tugboatIDsToRemove); + + // Cancel any pending tugboat timers + CancelPlayerTimers(playerID, ref _tugboatDeleteTimers); + + // Bulk delete zones + if (bulkDeleteList.Count > 0) + { + ZM_EraseZones(bulkDeleteList); + } + Pool.FreeUnmanaged(ref bulkDeleteList); + + Puts($"RemovePlayerZones(): Removed {count} zone(s) for player {playerID}"); + } + + // Helper method to cancel timers for a specific player + private void CancelPlayerTimers(ulong playerID, ref Dictionary timerDict) + { + var toRemove = Pool.Get>(); + + foreach (var (key, timer) in timerDict) + { + // Check if this is a building/shelter/tugboat owned by the player + if (key is NetworkableId netID) + { + bool shouldRemove = false; + + // Check building data + if (_buildingData.TryGetValue(netID, out var buildingData)) + { + if (buildingData.ToolCupboard && buildingData.ToolCupboard.OwnerID == playerID) + { + shouldRemove = true; + } + } + + // Check shelter data + if (_shelterData.TryGetValue(netID, out var shelterData)) + { + if (shelterData.LegacyShelter) + { + var ownerID = GetOwnerID(shelterData.LegacyShelter); + if (ownerID == playerID) + { + shouldRemove = true; + } + } + } + + // Check tugboat data + if (_tugboatData.TryGetValue(netID, out var tugboatData)) + { + if (tugboatData.Tugboat && + tugboatData.Tugboat.authorizedPlayers.Exists(x => x.userid == playerID)) + { + shouldRemove = true; + } + } + + if (shouldRemove) + { + toRemove.Add(key); + } + } + } + + foreach (var key in toRemove) + { + CancelDictionaryTimer(ref timerDict, key); + } + + Pool.FreeUnmanaged(ref toRemove); + } + #endregion Core Methods #region Oxide/RustPlugin API/Hooks @@ -793,10 +1087,53 @@ protected override void LoadDefaultMessages() ["MessageZoneEnter"] = "WARNING: Entering Player Base PVP Zone", ["MessageZoneExit"] = - "Leaving Player Base PVP Zone" + "Leaving Player Base PVP Zone", + ["NoPermission"] = + "You don't have permission to use this command.", + ["ZonesEnabled"] = + "[PBPZ] Your base PvP zones are now ENABLED.", + ["ZonesDisabled"] = + "[PBPZ] Your base PvP zones are now DISABLED.", + ["ToggleAlreadyPending"] = + "[PBPZ] You already have a zone toggle in progress. Please wait.", + ["ToggleEnableStarted"] = + "[PBPZ] Your base PvP zones will be ENABLED in {0} second(s).", + ["ToggleDisableStarted"] = + "[PBPZ] Your base PvP zones will be DISABLED in {0} second(s).", + ["ToggleEnableBroadcast"] = + "[PBPZ] {0} is enabling their base PvP zones in {1} second(s).", + ["ToggleDisableBroadcast"] = + "[PBPZ] {0} is disabling their base PvP zones in {1} second(s).", + ["ToggleEnableComplete"] = + "[PBPZ] {0}'s base PvP zones are now ACTIVE.", + ["ToggleDisableComplete"] = + "[PBPZ] {0}'s base PvP zones are now INACTIVE." }, this); } + private void LoadData() + { + try + { + _playersWithZonesEnabled = Interface.Oxide.DataFileSystem.ReadObject>(DataFileName); + if (_playersWithZonesEnabled == null) + { + _playersWithZonesEnabled = new HashSet(); + } + Puts($"Loaded {_playersWithZonesEnabled.Count} player(s) with zones enabled"); + } + catch + { + _playersWithZonesEnabled = new HashSet(); + Puts("Created new enabled players data file"); + } + } + + private void SaveData() + { + Interface.Oxide.DataFileSystem.WriteObject(DataFileName, _playersWithZonesEnabled); + } + private void Init() { // unsubscribe from OnEntitySpawned() hook calls under OnServerInitialized() @@ -807,6 +1144,12 @@ private void Init() Unsubscribe(nameof(OnEntitySpawned)); if (null == _configData) return; BaseData.SphereDarkness = _configData.SphereDarkness; + + // Register permission + permission.RegisterPermission(PermissionToggle, this); + + // Load enabled players data + LoadData(); } private void OnServerInitialized() @@ -822,12 +1165,129 @@ private void OnServerInitialized() // resubscribe OnEntitySpawned() hook, as it's now safe to handle this Subscribe(nameof(OnEntitySpawned)); + // Create zones only for players who have them enabled NextTick(() => { _createDataCoroutine = ServerMgr.Instance.StartCoroutine(CreateData()); }); } + [ChatCommand("pbpz")] + private void CommandTogglePvpZones(BasePlayer player, string command, string[] args) + { + // Check permission + if (!permission.UserHasPermission(player.UserIDString, PermissionToggle)) + { + SendReply(player, lang.GetMessage("NoPermission", this, player.UserIDString)); + return; + } + + var playerID = player.userID.Get(); + + // Check if player already has a pending toggle + if (_toggleTimers.ContainsKey(playerID)) + { + SendReply(player, lang.GetMessage("ToggleAlreadyPending", this, player.UserIDString)); + return; + } + + // Determine if enabling or disabling + bool isEnabling = !_playersWithZonesEnabled.Contains(playerID); + float delay = isEnabling ? _configData.ToggleEnableDelaySeconds : _configData.ToggleDisableDelaySeconds; + + // Schedule the toggle + _toggleTimers.Add(playerID, timer.Once(delay, () => ExecuteToggle(playerID, isEnabling))); + + // Notify the player + if (isEnabling) + { + var message = lang.GetMessage("ToggleEnableStarted", this, player.UserIDString); + SendReply(player, string.Format(message, delay)); + + // Broadcast to server if configured + if (_configData.ToggleEnableBroadcast) + { + var broadcastMsg = lang.GetMessage("ToggleEnableBroadcast", this); + Server.Broadcast(string.Format(broadcastMsg, player.displayName, delay)); + } + } + else + { + var message = lang.GetMessage("ToggleDisableStarted", this, player.UserIDString); + SendReply(player, string.Format(message, delay)); + + // Broadcast to server if configured + if (_configData.ToggleDisableBroadcast) + { + var broadcastMsg = lang.GetMessage("ToggleDisableBroadcast", this); + Server.Broadcast(string.Format(broadcastMsg, player.displayName, delay)); + } + } + } + + private void ExecuteToggle(ulong playerID, bool isEnabling) + { + // Remove timer + _toggleTimers.Remove(playerID); + + // Get player for notifications + var player = BasePlayer.FindByID(playerID); + var playerName = player?.displayName ?? "Unknown Player"; + + if (isEnabling) + { + // Enable zones for this player + _playersWithZonesEnabled.Add(playerID); + SaveData(); + + if (player != null) + { + SendReply(player, lang.GetMessage("ZonesEnabled", this, player.UserIDString)); + } + + // Broadcast completion if configured + if (_configData.ToggleEnableCompleteBroadcast) + { + var broadcastMsg = lang.GetMessage("ToggleEnableComplete", this); + Server.Broadcast(string.Format(broadcastMsg, playerName)); + } + + // Create zones for this player's bases + var zoneCount = CreatePlayerZones(playerID); + + // Call hook to notify other plugins (like ZoneMarkerSync) + Interface.CallHook("OnPlayerBasePvpZonesEnabled", playerID, zoneCount); + } + else + { + // Disable zones for this player + var removedZoneIds = CollectPlayerZoneIds(playerID); + + _playersWithZonesEnabled.Remove(playerID); + SaveData(); + + if (player != null) + { + SendReply(player, lang.GetMessage("ZonesDisabled", this, player.UserIDString)); + } + + // Broadcast completion if configured + if (_configData.ToggleDisableCompleteBroadcast) + { + var broadcastMsg = lang.GetMessage("ToggleDisableComplete", this); + Server.Broadcast(string.Format(broadcastMsg, playerName)); + } + + // Remove all zones owned by this player + RemovePlayerZones(playerID); + // Call hook to notify other plugins (like ZoneMarkerSync) + Interface.CallHook("OnPlayerBasePvpZonesDisabled", playerID, removedZoneIds); + + // Free the list + Pool.FreeUnmanaged(ref removedZoneIds); + } + } + private static void DestroyBaseDataDictionary( ref Dictionary dict, Action> deleter, @@ -856,12 +1316,26 @@ private void DestroyTimerDictionary( private void Unload() { + // Save enabled players data + SaveData(); + if (null != _createDataCoroutine) { ServerMgr.Instance.StopCoroutine(_createDataCoroutine); _createDataCoroutine = null; } + // Clean up toggle timers + if (_toggleTimers.Count > 0) + { + Puts($"Unload(): Destroying {_toggleTimers.Count} toggle timer(s)..."); + foreach (var (_, toggleTimer) in _toggleTimers) + { + toggleTimer?.Destroy(); + } + _toggleTimers.Clear(); + } + Puts("Unload(): Cleaning up..."); // cleanup base zones var bulkDeleteList = Pool.Get>(); @@ -914,7 +1388,7 @@ private void Unload() { timerData.Item1.Destroy(); } - _buildingDeleteTimers.Clear(); + _pvpDelayTimers.Clear(); } Puts("Unload(): ...Cleanup complete."); } @@ -941,6 +1415,8 @@ protected override void LoadConfig() ClampToZero(ref _configData.CreateDelaySeconds, "createDelaySeconds"); ClampToZero(ref _configData.DeleteDelaySeconds, "deleteDelaySeconds"); ClampToZero(ref _configData.PvpDelaySeconds, "pvpDelaySeconds"); + ClampToZero(ref _configData.ToggleEnableDelaySeconds, "toggleEnableDelaySeconds"); + ClampToZero(ref _configData.ToggleDisableDelaySeconds, "toggleDisableDelaySeconds"); if (_configData.SphereDarkness > 10) { PrintWarning($"Illegal sphereDarkness={_configData.SphereDarkness} value; clamping to 10"); @@ -1629,6 +2105,24 @@ private sealed class ConfigData [JsonProperty(PropertyName = "Zone TruePVE mappings ruleset name")] public string RulesetName = "exclude"; + [JsonProperty(PropertyName = "Toggle enable delay in seconds")] + public float ToggleEnableDelaySeconds = 300.0f; + + [JsonProperty(PropertyName = "Toggle disable delay in seconds")] + public float ToggleDisableDelaySeconds = 60.0f; + + [JsonProperty(PropertyName = "Broadcast when player starts enabling zones")] + public bool ToggleEnableBroadcast = true; + + [JsonProperty(PropertyName = "Broadcast when player starts disabling zones")] + public bool ToggleDisableBroadcast = true; + + [JsonProperty(PropertyName = "Broadcast when player zones are enabled (complete)")] + public bool ToggleEnableCompleteBroadcast = true; + + [JsonProperty(PropertyName = "Broadcast when player zones are disabled (complete)")] + public bool ToggleDisableCompleteBroadcast = true; + [JsonProperty(PropertyName = "Building settings")] public BuildingConfigData Building = new(); @@ -1668,6 +2162,6 @@ private sealed class TugboatConfigData [JsonProperty(PropertyName = "Tugboat zone radius")] public float Radius = 32.0f; } -} -#endregion Internal Classes + #endregion Internal Classes +} From 7096eb83698e365aec40d5c1526101ca8ed33917 Mon Sep 17 00:00:00 2001 From: HunterZ <108939+HunterZ@users.noreply.github.com> Date: Thu, 13 Nov 2025 20:32:45 -0800 Subject: [PATCH 2/4] stinkyPumpkin cleanup round 1 --- PlayerBasePvpZones.cs | 88 +++++++++++++++---------------------------- 1 file changed, 30 insertions(+), 58 deletions(-) diff --git a/PlayerBasePvpZones.cs b/PlayerBasePvpZones.cs index 8385a64..353b710 100644 --- a/PlayerBasePvpZones.cs +++ b/PlayerBasePvpZones.cs @@ -8,7 +8,6 @@ using System.Collections; using System.Collections.Generic; using System.Globalization; -using System.Linq; using UnityEngine; namespace Oxide.Plugins; @@ -119,8 +118,7 @@ private List CollectPlayerZoneIds(ulong playerID) foreach (var (tugboatID, tugboatData) in _tugboatData) { if (tugboatData.Tugboat && - tugboatData.Tugboat.authorizedPlayers.Exists( - x => x.userid == playerID)) + tugboatData.Tugboat.authorizedPlayers.Contains(playerID)) { zoneIds.Add(GetZoneID(tugboatID)); } @@ -638,14 +636,12 @@ private void ScheduleDeleteShelterData( private void CreateTugboatData(VehiclePrivilege tugboat) { // Check if any authorized player has zones enabled - bool hasEnabledPlayer = false; + var hasEnabledPlayer = false; foreach (var auth in tugboat.authorizedPlayers) { - if (IsPlayerZonesEnabled(auth.userid)) - { - hasEnabledPlayer = true; - break; - } + if (!IsPlayerZonesEnabled(auth)) continue; + hasEnabledPlayer = true; + break; } if (!hasEnabledPlayer) return; @@ -852,14 +848,12 @@ private IEnumerator CreateData() } // Check if any authorized player has zones enabled - bool hasEnabledPlayer = false; + var hasEnabledPlayer = false; foreach (var auth in tugboat.authorizedPlayers) { - if (IsPlayerZonesEnabled(auth.userid)) - { - hasEnabledPlayer = true; - break; - } + if (!IsPlayerZonesEnabled(auth)) continue; + hasEnabledPlayer = true; + break; } if (!hasEnabledPlayer) continue; @@ -916,10 +910,7 @@ private int CreatePlayerZones(ulong playerID) } // Check if this player is authorized on the tugboat - if (!tugboat.authorizedPlayers.Exists(x => x.userid == playerID)) - { - continue; - } + if (!tugboat.authorizedPlayers.Contains(playerID)) continue; CreateTugboatData(tugboat); count++; @@ -986,7 +977,7 @@ private void RemovePlayerZones(ulong playerID) foreach (var (tugboatID, tugboatData) in _tugboatData) { if (tugboatData.Tugboat && - tugboatData.Tugboat.authorizedPlayers.Exists(x => x.userid == playerID)) + tugboatData.Tugboat.authorizedPlayers.Contains(playerID)) { tugboatIDsToRemove.Add(tugboatID); } @@ -1016,49 +1007,27 @@ private void CancelPlayerTimers(ulong playerID, ref Dictionary time { var toRemove = Pool.Get>(); - foreach (var (key, timer) in timerDict) + foreach (var key in timerDict.Keys) { // Check if this is a building/shelter/tugboat owned by the player - if (key is NetworkableId netID) - { - bool shouldRemove = false; + if (key is not NetworkableId netID) continue; + if ( // Check building data - if (_buildingData.TryGetValue(netID, out var buildingData)) - { - if (buildingData.ToolCupboard && buildingData.ToolCupboard.OwnerID == playerID) - { - shouldRemove = true; - } - } - + (_buildingData.TryGetValue(netID, out var buildingData) && + buildingData.ToolCupboard && + buildingData.ToolCupboard.OwnerID == playerID) || // Check shelter data - if (_shelterData.TryGetValue(netID, out var shelterData)) - { - if (shelterData.LegacyShelter) - { - var ownerID = GetOwnerID(shelterData.LegacyShelter); - if (ownerID == playerID) - { - shouldRemove = true; - } - } - } - + (_shelterData.TryGetValue(netID, out var shelterData) && + shelterData.LegacyShelter && + GetOwnerID(shelterData.LegacyShelter) == playerID) || // Check tugboat data - if (_tugboatData.TryGetValue(netID, out var tugboatData)) - { - if (tugboatData.Tugboat && - tugboatData.Tugboat.authorizedPlayers.Exists(x => x.userid == playerID)) - { - shouldRemove = true; - } - } - - if (shouldRemove) - { - toRemove.Add(key); - } + _tugboatData.TryGetValue(netID, out var tugboatData) && + tugboatData.Tugboat && + tugboatData.Tugboat.authorizedPlayers.Contains(playerID) + ) + { + toRemove.Add(key); } } @@ -1329,7 +1298,7 @@ private void Unload() if (_toggleTimers.Count > 0) { Puts($"Unload(): Destroying {_toggleTimers.Count} toggle timer(s)..."); - foreach (var (_, toggleTimer) in _toggleTimers) + foreach (var toggleTimer in _toggleTimers.Values) { toggleTimer?.Destroy(); } @@ -1375,6 +1344,7 @@ private void Unload() Puts($"Unload(): Destroying {_playerZones.Count} player-in-zones records..."); foreach (var playerZoneData in _playerZones) { + // this dance is required to avoid modifying the loop variable var zones = playerZoneData.Value; if (null == zones) continue; Pool.FreeUnmanaged(ref zones); @@ -2043,6 +2013,7 @@ public void Init( if (null == SphereList) return; foreach (var sphere in SphereList) { + if (!IsValid(sphere)) continue; sphere.ServerPosition = Vector3.zero; sphere.SetParent(tugboatParent); // match networking with parent (avoids need to force global tugboats) @@ -2069,6 +2040,7 @@ public override void ClearEntity(bool destroying = false) // un-tether any spheres from the tugboat foreach (var sphere in SphereList) { + if (!IsValid(sphere)) continue; sphere.SetParent(null, true, true); sphere.ServerPosition = Location; sphere.LerpRadiusTo(Radius * 2.0f, Radius / 2.0f); From dce2f095e371c540120cfb1c84dd3a45197ebb89 Mon Sep 17 00:00:00 2001 From: HunterZ <108939+HunterZ@users.noreply.github.com> Date: Thu, 13 Nov 2025 21:42:57 -0800 Subject: [PATCH 3/4] stinkyPumpkin refactor: - don't uniquely name data file - don't split CollectPlayerZoneIDs() list lifecycle management - log data file load exception - implement simple data file save delay management - use common facility for cleaning up toggle timers - cleanup --- PlayerBasePvpZones.cs | 141 +++++++++++++++++++++++------------------- 1 file changed, 78 insertions(+), 63 deletions(-) diff --git a/PlayerBasePvpZones.cs b/PlayerBasePvpZones.cs index 353b710..b1655d9 100644 --- a/PlayerBasePvpZones.cs +++ b/PlayerBasePvpZones.cs @@ -24,13 +24,10 @@ public class PlayerBasePvpZones : RustPlugin private const string PermissionToggle = "playerbasepvpzones.toggle"; // tracks which players have zones enabled (stored by OwnerID) - private HashSet _playersWithZonesEnabled = new HashSet(); + private HashSet _playersWithZonesEnabled = new(); // tracks pending zone enable/disable requests by player ID - private Dictionary _toggleTimers = new Dictionary(); - - // data file name for persistence - private const string DataFileName = "PlayerBasePvpZones_EnabledPlayers"; + private Dictionary _toggleTimers = new(); // user-defined plugin config data private ConfigData _configData = new(); @@ -90,17 +87,15 @@ public class PlayerBasePvpZones : RustPlugin #region Core Methods // Collect zone IDs for a player before deletion - private List CollectPlayerZoneIds(ulong playerID) + private void CollectPlayerZoneIDs(ulong playerID, List zoneIDs) { - var zoneIds = Pool.Get>(); - // Collect building zone IDs foreach (var (tcID, buildingData) in _buildingData) { if (buildingData.ToolCupboard && buildingData.ToolCupboard.OwnerID == playerID) { - zoneIds.Add(GetZoneID(tcID)); + zoneIDs.Add(GetZoneID(tcID)); } } @@ -110,7 +105,7 @@ private List CollectPlayerZoneIds(ulong playerID) if (shelterData.LegacyShelter && GetOwnerID(shelterData.LegacyShelter) == playerID) { - zoneIds.Add(GetZoneID(shelterID)); + zoneIDs.Add(GetZoneID(shelterID)); } } @@ -120,11 +115,9 @@ private List CollectPlayerZoneIds(ulong playerID) if (tugboatData.Tugboat && tugboatData.Tugboat.authorizedPlayers.Contains(playerID)) { - zoneIds.Add(GetZoneID(tugboatID)); + zoneIDs.Add(GetZoneID(tugboatID)); } } - - return zoneIds; } // generate a current 3D bounding box around a base @@ -1084,23 +1077,49 @@ private void LoadData() { try { - _playersWithZonesEnabled = Interface.Oxide.DataFileSystem.ReadObject>(DataFileName); - if (_playersWithZonesEnabled == null) - { - _playersWithZonesEnabled = new HashSet(); - } - Puts($"Loaded {_playersWithZonesEnabled.Count} player(s) with zones enabled"); + _playersWithZonesEnabled = + Interface.Oxide.DataFileSystem.ReadObject>(Name); + } + catch (Exception ex) + { + PrintError($"Exception while loading data file:\n{ex}"); + _playersWithZonesEnabled = null; } - catch + + if (null == _playersWithZonesEnabled) { _playersWithZonesEnabled = new HashSet(); + SaveData(false); Puts("Created new enabled players data file"); } + else + { + Puts($"Loaded {_playersWithZonesEnabled.Count} player(s) with zones enabled"); + } } - private void SaveData() + private Timer _saveDataTimer; + private static bool TimerValid(Timer t) => false == t?.Destroyed; + + // write data to file + // if delay is true, save will be delayed by 5 seconds, and all redundant + // requests during that time will be ignored + // if delay is false, save will occur immediately + private void SaveData(bool delay = true) { - Interface.Oxide.DataFileSystem.WriteObject(DataFileName, _playersWithZonesEnabled); + if (delay) + { + // abort if timer is already running + if (TimerValid(_saveDataTimer)) return; + // schedule a save 5 seconds from now + _saveDataTimer = timer.Once(5.0f, () => SaveData(false)); + return; + } + // else save immediately + Interface.Oxide.DataFileSystem.WriteObject(Name, _playersWithZonesEnabled); + // ...and also clean up save timer + _saveDataTimer.Destroy(); + _saveDataTimer = null; } private void Init() @@ -1142,12 +1161,14 @@ private void OnServerInitialized() } [ChatCommand("pbpz")] - private void CommandTogglePvpZones(BasePlayer player, string command, string[] args) + private void CommandTogglePvpZones( + BasePlayer player, string command, string[] args) { // Check permission if (!permission.UserHasPermission(player.UserIDString, PermissionToggle)) { - SendReply(player, lang.GetMessage("NoPermission", this, player.UserIDString)); + SendReply( + player, lang.GetMessage("NoPermission", this, player.UserIDString)); return; } @@ -1156,29 +1177,31 @@ private void CommandTogglePvpZones(BasePlayer player, string command, string[] a // Check if player already has a pending toggle if (_toggleTimers.ContainsKey(playerID)) { - SendReply(player, lang.GetMessage("ToggleAlreadyPending", this, player.UserIDString)); + SendReply(player, + lang.GetMessage("ToggleAlreadyPending", this, player.UserIDString)); return; } // Determine if enabling or disabling - bool isEnabling = !_playersWithZonesEnabled.Contains(playerID); - float delay = isEnabling ? _configData.ToggleEnableDelaySeconds : _configData.ToggleDisableDelaySeconds; + var enable = !_playersWithZonesEnabled.Contains(playerID); + var delay = enable ? + _configData.ToggleEnableDelaySeconds : + _configData.ToggleDisableDelaySeconds; // Schedule the toggle - _toggleTimers.Add(playerID, timer.Once(delay, () => ExecuteToggle(playerID, isEnabling))); + _toggleTimers.Add( + playerID, timer.Once(delay, () => TogglePlayerZones(playerID, enable))); // Notify the player - if (isEnabling) + if (enable) { var message = lang.GetMessage("ToggleEnableStarted", this, player.UserIDString); SendReply(player, string.Format(message, delay)); // Broadcast to server if configured - if (_configData.ToggleEnableBroadcast) - { - var broadcastMsg = lang.GetMessage("ToggleEnableBroadcast", this); - Server.Broadcast(string.Format(broadcastMsg, player.displayName, delay)); - } + if (!_configData.ToggleEnableBroadcast) return; + var broadcastMsg = lang.GetMessage("ToggleEnableBroadcast", this); + Server.Broadcast(string.Format(broadcastMsg, player.displayName, delay)); } else { @@ -1186,32 +1209,31 @@ private void CommandTogglePvpZones(BasePlayer player, string command, string[] a SendReply(player, string.Format(message, delay)); // Broadcast to server if configured - if (_configData.ToggleDisableBroadcast) - { - var broadcastMsg = lang.GetMessage("ToggleDisableBroadcast", this); - Server.Broadcast(string.Format(broadcastMsg, player.displayName, delay)); - } + if (!_configData.ToggleDisableBroadcast) return; + var broadcastMsg = lang.GetMessage("ToggleDisableBroadcast", this); + Server.Broadcast(string.Format(broadcastMsg, player.displayName, delay)); } } - private void ExecuteToggle(ulong playerID, bool isEnabling) + private void TogglePlayerZones(ulong playerID, bool enable) { // Remove timer - _toggleTimers.Remove(playerID); + CancelDictionaryTimer(ref _toggleTimers, playerID); // Get player for notifications var player = BasePlayer.FindByID(playerID); var playerName = player?.displayName ?? "Unknown Player"; - if (isEnabling) + if (enable) { // Enable zones for this player _playersWithZonesEnabled.Add(playerID); SaveData(); - if (player != null) + if (player) { - SendReply(player, lang.GetMessage("ZonesEnabled", this, player.UserIDString)); + SendReply( + player, lang.GetMessage("ZonesEnabled", this, player.UserIDString)); } // Broadcast completion if configured @@ -1230,14 +1252,15 @@ private void ExecuteToggle(ulong playerID, bool isEnabling) else { // Disable zones for this player - var removedZoneIds = CollectPlayerZoneIds(playerID); - + var removedZoneIDs = Pool.Get>(); + CollectPlayerZoneIDs(playerID, removedZoneIDs); _playersWithZonesEnabled.Remove(playerID); SaveData(); - if (player != null) + if (player) { - SendReply(player, lang.GetMessage("ZonesDisabled", this, player.UserIDString)); + SendReply( + player, lang.GetMessage("ZonesDisabled", this, player.UserIDString)); } // Broadcast completion if configured @@ -1249,11 +1272,12 @@ private void ExecuteToggle(ulong playerID, bool isEnabling) // Remove all zones owned by this player RemovePlayerZones(playerID); + // Call hook to notify other plugins (like ZoneMarkerSync) - Interface.CallHook("OnPlayerBasePvpZonesDisabled", playerID, removedZoneIds); + Interface.CallHook("OnPlayerBasePvpZonesDisabled", playerID, removedZoneIDs); // Free the list - Pool.FreeUnmanaged(ref removedZoneIds); + Pool.FreeUnmanaged(ref removedZoneIDs); } } @@ -1285,8 +1309,8 @@ private void DestroyTimerDictionary( private void Unload() { - // Save enabled players data - SaveData(); + // if delayed save pending, force it now + if (TimerValid(_saveDataTimer)) SaveData(false); if (null != _createDataCoroutine) { @@ -1294,18 +1318,9 @@ private void Unload() _createDataCoroutine = null; } - // Clean up toggle timers - if (_toggleTimers.Count > 0) - { - Puts($"Unload(): Destroying {_toggleTimers.Count} toggle timer(s)..."); - foreach (var toggleTimer in _toggleTimers.Values) - { - toggleTimer?.Destroy(); - } - _toggleTimers.Clear(); - } - Puts("Unload(): Cleaning up..."); + // Clean up toggle timers + DestroyTimerDictionary(ref _toggleTimers, "toggle"); // cleanup base zones var bulkDeleteList = Pool.Get>(); if (_buildingData.Count > 0) From 37ed63c706a5e7d46946e5dc0098402b93d91290 Mon Sep 17 00:00:00 2001 From: HunterZ <108939+HunterZ@users.noreply.github.com> Date: Thu, 13 Nov 2025 23:18:39 -0800 Subject: [PATCH 4/4] PBPZ stinkyPumpkin tweaks: - add HasDecayEntities() check to GetToolCupboard() - use new TruePVE bulk mappings removal API when bulk removing player zones - fix my own typo in previous CancelPlayerTimers() cleanup - cleanup --- PlayerBasePvpZones.cs | 41 ++++++++++++++++++----------------------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/PlayerBasePvpZones.cs b/PlayerBasePvpZones.cs index b1655d9..04a90d5 100644 --- a/PlayerBasePvpZones.cs +++ b/PlayerBasePvpZones.cs @@ -192,6 +192,7 @@ private static BuildingPrivlidge GetToolCupboard( { // check the easy stuff first if (null == building || + !building.HasDecayEntities() || !building.HasBuildingBlocks() || !building.HasBuildingPrivileges()) { @@ -790,16 +791,14 @@ private YieldInstruction DynamicYield() Performance.report.frameRate >= _targetFps ? _fastYield : _throttleYield; } - // coroutine method to asynchronously create zones for all existing bases - // coroutine method to asynchronously create zones for enabled players only + // coroutine to asynchronously perform bulk creation of appropriate base zones private IEnumerator CreateData() { var startTime = DateTime.UtcNow; Puts("CreateData(): Starting zone creation..."); - // create zones for all existing player-owned bases - // create zones for enabled players' bases + // create zones for all existing bases owned by PVP players foreach (var building in BuildingManager.server.buildingDictionary.Values) { var toolCupboard = GetToolCupboard(building); @@ -811,8 +810,7 @@ private IEnumerator CreateData() } Puts($"CreateData(): Created {_buildingData.Count} building zones..."); - // create zones for all existing player-owned legacy shelters - // create zones for enabled players' shelters + // create zones for all existing legacy shelters owned by PVP players foreach (var (playerID, shelterList) in LegacyShelter.SheltersPerPlayer) { if (!IsPlayerZonesEnabled(playerID)) continue; @@ -831,8 +829,7 @@ private IEnumerator CreateData() } Puts($"CreateData(): Created {_shelterData.Count} shelter zones..."); - // create zones for all existing tugboats - // create zones for tugboats (check authorization) + // create zones for all existing tugboats owned by PVP players foreach (var serverEntity in BaseNetworkable.serverEntities) { if (serverEntity is not VehiclePrivilege tugboat || !IsValid(tugboat)) @@ -917,6 +914,7 @@ private int CreatePlayerZones(ulong playerID) private void RemovePlayerZones(ulong playerID) { Puts($"RemovePlayerZones(): Removing zones for player {playerID}..."); + var bulkDeleteList = Pool.Get>(); var count = 0; @@ -924,7 +922,8 @@ private void RemovePlayerZones(ulong playerID) var buildingIDsToRemove = Pool.Get>(); foreach (var (tcID, buildingData) in _buildingData) { - if (buildingData.ToolCupboard && buildingData.ToolCupboard.OwnerID == playerID) + if (buildingData.ToolCupboard && + buildingData.ToolCupboard.OwnerID == playerID) { buildingIDsToRemove.Add(tcID); } @@ -945,13 +944,10 @@ private void RemovePlayerZones(ulong playerID) var shelterIDsToRemove = Pool.Get>(); foreach (var (shelterID, shelterData) in _shelterData) { - if (shelterData.LegacyShelter) + if (shelterData.LegacyShelter && + GetOwnerID(shelterData.LegacyShelter) == playerID) { - var ownerID = GetOwnerID(shelterData.LegacyShelter); - if (ownerID == playerID) - { - shelterIDsToRemove.Add(shelterID); - } + shelterIDsToRemove.Add(shelterID); } } foreach (var shelterID in shelterIDsToRemove) @@ -988,6 +984,7 @@ private void RemovePlayerZones(ulong playerID) // Bulk delete zones if (bulkDeleteList.Count > 0) { + TP_RemoveMappings(bulkDeleteList); ZM_EraseZones(bulkDeleteList); } Pool.FreeUnmanaged(ref bulkDeleteList); @@ -996,7 +993,8 @@ private void RemovePlayerZones(ulong playerID) } // Helper method to cancel timers for a specific player - private void CancelPlayerTimers(ulong playerID, ref Dictionary timerDict) + private void CancelPlayerTimers( + ulong playerID, ref Dictionary timerDict) { var toRemove = Pool.Get>(); @@ -1015,19 +1013,16 @@ private void CancelPlayerTimers(ulong playerID, ref Dictionary time shelterData.LegacyShelter && GetOwnerID(shelterData.LegacyShelter) == playerID) || // Check tugboat data - _tugboatData.TryGetValue(netID, out var tugboatData) && - tugboatData.Tugboat && - tugboatData.Tugboat.authorizedPlayers.Contains(playerID) + (_tugboatData.TryGetValue(netID, out var tugboatData) && + tugboatData.Tugboat && + tugboatData.Tugboat.authorizedPlayers.Contains(playerID)) ) { toRemove.Add(key); } } - foreach (var key in toRemove) - { - CancelDictionaryTimer(ref timerDict, key); - } + foreach (var key in toRemove) CancelDictionaryTimer(ref timerDict, key); Pool.FreeUnmanaged(ref toRemove); }