C#:
// Requires: ZoneManager
using Facepunch;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core.Plugins;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using UnityEngine;
namespace Oxide.Plugins;
[Info("Player Base PvP Zones", "HunterZ", "1.6.0")]
[Description("Maintains Zone Manager / TruePVE exclusion zones around player bases")]
public class PlayerBasePvpZones : RustPlugin
{
#region Fields
[PluginReference] Plugin TruePVE;
// user-defined plugin config data
private ConfigData _configData = new();
// persistent data
private PersistData _persistData = new();
// _persistData disk write delay timer
private Timer _saveDataTimer;
// active building zones by TC ID
private Dictionary<NetworkableId, BuildingData> _buildingData = new();
// active building zone check delay timers by TC ID
private Dictionary<NetworkableId, Timer> _buildingCheckTimers = new();
// building zone creation delay timers by TC ID
private Dictionary<NetworkableId, Timer> _buildingCreateTimers = new();
// building zone deletion delay timers by TC ID
private Dictionary<NetworkableId, Timer> _buildingDeleteTimers = new();
// active modular boat zones by TC ID
private Dictionary<NetworkableId, ModularBoatData> _modularBoatData = new();
// modular boat zone creation delay timers by TC ID
private Dictionary<NetworkableId, Timer> _modularBoatCreateTimers = new();
// modular boat zone deletion delay timers by TC ID
private Dictionary<NetworkableId, Timer> _modularBoatDeleteTimers = new();
// active legacy shelter zones by shelter ID
private Dictionary<NetworkableId, ShelterData> _shelterData = new();
// legacy shelter zone creation delay timers by shelter ID
private Dictionary<NetworkableId, Timer> _shelterCreateTimers = new();
// legacy shelter zone deletion delay timers by shelter ID
private Dictionary<NetworkableId, Timer> _shelterDeleteTimers = new();
// active tugboat zones by tugboat ID
private Dictionary<NetworkableId, TugboatData> _tugboatData = new();
// tugboat zone deletion delay timers by tugboat ID
private Dictionary<NetworkableId, Timer> _tugboatDeleteTimers = new();
// set of containing base zone net IDs by player ID
// NOTE: to avoid heap churn, the HashSet's are pooled, and are only freed in
// Unload(). This means that empty sets may exist when a player is not in any
// base zones!
private readonly Dictionary<ulong, HashSet<NetworkableId>> _playerZones =
new();
// active pvp delay timer + zoneID by player ID
private readonly Dictionary<ulong, (Timer, string)> _pvpDelayTimers = new();
// true if TruePVE 2.2.3+ ExcludePlayer() should be used for PVP delays, false
// if CanEntityTakeDamage() hook handler should be used
private bool _useExcludePlayer;
private Coroutine _createDataCoroutine;
private readonly YieldInstruction _fastYield = null;
private readonly YieldInstruction _throttleYield =
CoroutineEx.waitForSeconds(0.1f);
private float _targetFps = -1.0f;
// ZoneManager zone ID prefix
private const string ZoneIdPrefix = "PlayerBasePVP:";
// zone toggle data
// layer mask for tool cupboard raycast searches
private readonly int _buildingLayerMask = LayerMask.GetMask("Deployed");
// layer mask for legacy shelter raycast searches
private readonly int _shelterLayerMask =
LayerMask.GetMask("Deployed", "Construction");
// maximum distance from tool cupboard at which toggling allowed; this is
// currently meant to roughly match regular interaction distance
private const float BuildingToggleRadius = 3.0f;
// shelters use player being in building privilege as a requirement
// tugboats use player being mounted at the wheel as a requirement, in lieu
// of raycast/distance checks
private const string CommandRoot = "pbpz";
private const string CommandHelp = "help";
private const string CommandToggle = "toggle";
private const string TogglePermission = "toggleZones";
private readonly List<DecayEntityData> _decayEntityData = new(1000);
#endregion Fields
#region Core Methods
private string GetPermission(string suffix) => $"{Name}.{suffix}";
// calculate a sphere around the building to which the given TC is attached
private (Vector3, float) CalculateBuildingSphere(
BuildingPrivlidge toolCupboard)
{
// lower-bound the radius at the configured value
var radius = _configData.Building.MinimumBuildingRadius;
// get building, aborting if not found
var building = toolCupboard?.GetBuilding();
if (building is null)
{
PrintError($"Tool cupboard {toolCupboard} has no attached building; aborting");
return (toolCupboard?.CenterPoint() ?? Vector3.zero, radius);
}
// get an overall building center by averaging all relevant building
// entities' center points
_decayEntityData.Clear();
var positionSum = Vector3.zero;
foreach (var buildingBlock in building.buildingBlocks)
{
var obb = buildingBlock.WorldSpaceBounds();
var position = obb.position;
positionSum += position;
var blockRadius = Mathf.Max(
obb.extents.magnitude, _configData.Building.MinimumBlockRadius);
_decayEntityData.Add(
new DecayEntityData(position, blockRadius));
}
if (_configData.Building.IncludeTurretRadius)
{
foreach (var entity in building.decayEntities)
{
if (entity is not AutoTurret turret) continue;
var position = turret.CenterPoint();
positionSum += position;
_decayEntityData.Add(
new DecayEntityData(position, turret.sightRange));
}
}
// protect from division by zero
if (_decayEntityData.IsEmpty())
{
PrintError($"Tool cupboard {toolCupboard} has no usable entities; aborting");
return (toolCupboard.CenterPoint(), radius);
}
var center = positionSum / _decayEntityData.Count;
// get an overall building size by finding the longest distance from the
// building center to the most distant point on a relevant entity's sphere
foreach (var data in _decayEntityData)
{
var dist = Vector3.Distance(center, data.Position) + data.Radius;
if (dist > radius) radius = dist;
}
return (center, radius);
}
// remove+destroy timer stored in the given dictionary under the given key
private static bool CancelDictionaryTimer<T>(
ref Dictionary<T, Timer> dictionary, T key)
{
if (!dictionary.Remove(key, out var cTimer)) return false;
DestroyTimer(cTimer);
return true;
}
// Return TC to which given decay entity is physically attached via a
// building, or null if none found
//
// Note that this will return null for entities deployed on bare ground; this
// is intentional for now
private static BuildingPrivlidge GetToolCupboard(
DecayEntity decayEntity) => GetToolCupboard(decayEntity?.GetBuilding());
// try to find and return a physically-attached TC for the given building, or
// null if no suitable result found
// only supports player-owned TCs (NOT to be confused with player-authed!)
// this is our differentiator of whether a building should have a zone
private static BuildingPrivlidge GetToolCupboard(
BuildingManager.Building building) =>
building?.GetDominatingBuildingPrivilege() is { } toolCupboard &&
IsPlayerOwned(toolCupboard) ? toolCupboard : null;
// get unique ID for any BaseNetworkable object
// credit: Karuza for suggesting this as the best unique ID approach
private static NetworkableId GetNetworkableID(
BaseNetworkable baseNetworkable) => baseNetworkable.net.ID;
// construct NetworkableId from the given unsigned long integer value
private static NetworkableId GetNetworkableID(ulong value) => new(value);
private bool IsKnownPlayerBaseId(NetworkableId networkableId) =>
_buildingData.ContainsKey(networkableId) ||
_buildingCreateTimers.ContainsKey(networkableId) ||
_modularBoatData.ContainsKey(networkableId) ||
_modularBoatCreateTimers.ContainsKey(networkableId) ||
_shelterData.ContainsKey(networkableId) ||
_shelterCreateTimers.ContainsKey(networkableId) ||
_tugboatData.ContainsKey(networkableId);
private static ulong? GetOwnerID(PlayerBoat modularBoat)
{
// OwnerID is not set for modular boats for some reason, so find the first
// player who owns a steering wheel (if any)
if (!IsValid(modularBoat)) return null;
// first check cached wheels
// these won't be cached on boat spawn, but is needed on boat EOL because
// children will already be gone for some reason
var wheels = modularBoat.SteeringWheels?.Cached;
if (null != wheels)
{
foreach (var wheel in wheels)
{
if (!IsValid(wheel)) continue;
var ownerID = wheel.OwnerID;
if (ownerID.IsSteamId()) return ownerID;
}
}
// fall back to checking children
// caches won't be populated on boat spawn, so we check these for an owner
foreach (var child in modularBoat.children)
{
if (!IsValid(child)) continue;
var ownerID = child.OwnerID;
if (ownerID.IsSteamId()) return ownerID;
}
return null;
}
private static ulong? GetOwnerID(EntityPrivilege legacyShelter)
{
if (!IsValid(legacyShelter)) return null;
// OwnerID is zero for shelters for some reason, so find the first
// authorized player (if any)
//
// only one player can normally auth to a shelter, but we also want to
// account for the possibility of plugin-spawned shelters that are authed
// to non-player(s)
foreach (var playerNameID in legacyShelter.authorizedPlayers)
{
if (playerNameID.IsSteamId()) return playerNameID;
}
return null;
}
// get building privilege for a given legacy shelter
private static EntityPrivilege GetShelterPrivilege(BaseEntity entity) =>
entity is LegacyShelter shelter && IsValid(shelter) ?
shelter.GetEntityBuildingPrivilege() : null;
// get building privilege for a given vehicle (tugboat)
private static VehiclePrivilege GetVehiclePrivilege(BaseVehicle vehicle)
{
if (!IsValid(vehicle)) return null;
foreach (var child in vehicle.children)
{
if (child is VehiclePrivilege privilege) return privilege;
}
return null;
}
// OwnerID is zero for modular boats for some reason, so check owner of
// steering wheel(s)
private static bool IsPlayerOwned(PlayerBoat modularBoat) =>
GetOwnerID(modularBoat) is not null;
// OwnerID is zero for shelters for some reason, so check auth list instead
private static bool IsPlayerOwned(EntityPrivilege legacyShelter) =>
IsValid(legacyShelter) && null != GetOwnerID(legacyShelter);
private static bool IsPlayerOwned(DecayEntity decayEntity) =>
IsValid(decayEntity) && decayEntity.OwnerID.IsSteamId();
private static bool IsValid(BaseNetworkable baseNetworkable) =>
baseNetworkable &&
!baseNetworkable.IsDestroyed &&
null != baseNetworkable.net &&
baseNetworkable.transform;
private void NotifyOwnerAbort(ulong ownerID)
{
var player = BasePlayer.FindByID(ownerID);
if (!player || !player.userID.IsSteamId()) return;
var message = lang.GetMessage(
"NotifyOwnerAbort", this, player.UserIDString);
SendReply(player, string.Format(message, _configData.PrefixNotify));
}
private void NotifyOwnerCreate(ulong ownerID)
{
var player = BasePlayer.FindByID(ownerID);
if (!player || !player.userID.IsSteamId()) return;
var message = lang.GetMessage(
"NotifyOwnerCreate", this, player.UserIDString);
SendReply(player, string.Format(
message, _configData.PrefixNotify,
Mathf.CeilToInt(_configData.CreateDelaySeconds)));
}
private void NotifyZoneDelete(NetworkableId baseID)
{
var playersInZone = Pool.Get<List<BasePlayer>>();
ZM_GetPlayersInZone(GetZoneID(baseID), playersInZone);
foreach (var player in playersInZone)
{
if (!player || !player.userID.IsSteamId()) continue;
var message =
lang.GetMessage("NotifyZoneDelete", this, player.UserIDString);
SendReply(player, string.Format(
message, _configData.PrefixNotify, _configData.DeleteDelaySeconds));
}
Pool.FreeUnmanaged(ref playersInZone);
}
// detect and handle any updates to physical footprint of given TC ID's
// building
private void CheckBuildingData(NetworkableId toolCupboardID)
{
// clean up any check timer that may have invoked this
CancelDictionaryTimer(ref _buildingCheckTimers, toolCupboardID);
// abort if this building is pending creation or deletion
if (_buildingCreateTimers.ContainsKey(toolCupboardID) ||
_buildingDeleteTimers.ContainsKey(toolCupboardID))
{
return;
}
// get building record, aborting if unknown or no TC reference
// NOTE: lacking a TC reference is pathological, as it should only occur
// when a building is pending deletion, which we've already checked
if (!_buildingData.TryGetValue(toolCupboardID, out var buildingData) ||
null == buildingData ||
!IsValid(buildingData.ToolCupboard))
{
return;
}
// get cached TC
var toolCupboard = buildingData.ToolCupboard;
// get building record, aborting if not found
var building = toolCupboard.GetBuilding();
if (null == building) return;
// get updated building footprint data
var (position, radius) = CalculateBuildingSphere(toolCupboard);
// abort if footprint basically unchanged
if (buildingData.Location == position &&
Mathf.Approximately(buildingData.Radius, radius))
{
return;
}
// update existing building data record
buildingData.Update(position, radius);
// update zone
ZM_CreateOrUpdateZone(buildingData, toolCupboardID);
// schedule a follow-up in case more changes are occuring
ScheduleCheckBuildingData(toolCupboardID);
}
// create new building record + zone for given TC
private void CreateBuildingData(BuildingPrivlidge toolCupboard)
{
// abort if TC object is destroyed
if (!IsValid(toolCupboard)) return;
var toolCupboardID = GetNetworkableID(toolCupboard);
// clean up any creation timer that may have invoked this
CancelDictionaryTimer(ref _buildingCreateTimers, toolCupboardID);
// if a building already exists, then this call is probably redundant
if (_buildingData.ContainsKey(toolCupboardID)) return;
// abort if zone toggled off
if (!GetToggleStates(_persistData.BuildingToggleData, toolCupboardID).Item1)
{
return;
}
// get initial building footprint data
var (position, radius) = CalculateBuildingSphere(toolCupboard);
// create + record new building data record
var buildingData = Pool.Get<BuildingData>();
buildingData.Init(toolCupboard, position, radius);
_buildingData.Add(toolCupboardID, buildingData);
// create zone
if (ZM_CreateOrUpdateZone(buildingData, toolCupboardID))
{
// ...and mapping
TP_AddOrUpdateMapping(toolCupboardID);
}
// schedule a follow-up in case more changes are occuring
ScheduleCheckBuildingData(toolCupboardID);
}
// delete building record + zone for given TC ID
private void DeleteBuildingData(
NetworkableId toolCupboardID, List<string> bulkDeleteList = null)
{
// clean up any deletion timer that may have invoked this
CancelDictionaryTimer(ref _buildingDeleteTimers, toolCupboardID);
// remove building record to local variable, aborting if not found
if (!_buildingData.Remove(toolCupboardID, out var buildingData)) return;
// drop building record
Pool.Free(ref buildingData);
// destroy building zone
if (null == bulkDeleteList)
{
// ...immediately
TP_RemoveMapping(toolCupboardID);
ZM_EraseZone(toolCupboardID);
}
else
{
// ...by adding to given bulk delete list
bulkDeleteList.Add(GetZoneID(toolCupboardID));
}
}
// schedule a delayed building update check
// this should be used where practical to reduce recalculation of base
// footprint when lots of building blocks are being spawned/destroyed around
// the same time
// also provides various sanity checks
private void ScheduleCheckBuildingData(NetworkableId toolCupboardID)
{
// abort if base is unknown, or if any timers are already running
if (!_buildingData.ContainsKey(toolCupboardID) ||
_buildingCheckTimers.ContainsKey(toolCupboardID) ||
_buildingCreateTimers.ContainsKey(toolCupboardID) ||
_buildingDeleteTimers.ContainsKey(toolCupboardID))
{
return;
}
// schedule call
_buildingCheckTimers.Add(toolCupboardID, timer.Once(
_configData.Building.CheckDelaySeconds,
() => CheckBuildingData(toolCupboardID)));
}
// schedule a delayed building creation
// this should be used during normal operations, in order to apply delays
// and/or notifications per plugin configuration
private void ScheduleCreateBuildingData(BuildingPrivlidge toolCupboard)
{
var toolCupboardID = GetNetworkableID(toolCupboard);
// abort if building is already known, or if any timers are already running
if (_buildingData.ContainsKey(toolCupboardID) ||
_buildingCheckTimers.ContainsKey(toolCupboardID) ||
_buildingCreateTimers.ContainsKey(toolCupboardID) ||
_buildingDeleteTimers.ContainsKey(toolCupboardID))
{
return;
}
// schedule call
_buildingCreateTimers.Add(toolCupboardID, timer.Once(
_configData.CreateDelaySeconds, () => CreateBuildingData(toolCupboard)));
// notify player
if (_configData.CreateNotify) NotifyOwnerCreate(toolCupboard.OwnerID);
}
// schedule a delayed building deletion
// this should be used during normal operations, in order to apply delays
// and/or notifications per plugin configuration
private void ScheduleDeleteBuildingData(
NetworkableId toolCupboardID, ulong ownerID)
{
var notifyZone = _configData.DeleteNotify;
// notify owner if this was pending creation
if (CancelDictionaryTimer(ref _buildingCreateTimers, toolCupboardID) &&
_configData.CreateNotify)
{
NotifyOwnerAbort(ownerID);
// suppress zone notify because there isn't one yet
notifyZone = false;
}
// also cancel any check timer
CancelDictionaryTimer(ref _buildingCheckTimers, toolCupboardID);
// abort if building is unknown, or if deletion timer is already running
if (!_buildingData.ContainsKey(toolCupboardID) ||
_buildingDeleteTimers.ContainsKey(toolCupboardID))
{
return;
}
// schedule call
_buildingDeleteTimers.Add(toolCupboardID, timer.Once(
_configData.DeleteDelaySeconds,
() => DeleteBuildingData(toolCupboardID)));
// notify players
if (notifyZone) NotifyZoneDelete(toolCupboardID);
}
// create new modular boat record + zone for given modular boat
private void CreateModularBoatData(PlayerBoat modularBoat)
{
// abort if boat object is destroyed or not player owned
var ownerID = GetOwnerID(modularBoat);
if (null == ownerID) return;
var modularBoatID = GetNetworkableID(modularBoat);
// clean up any creation timer that may have invoked this
CancelDictionaryTimer(ref _modularBoatCreateTimers, modularBoatID);
// if data already exists, then this call is probably redundant
if (_modularBoatData.ContainsKey(modularBoatID)) return;
// abort if zone toggled off
if (!GetToggleStates(
_persistData.ModularBoatToggleData, modularBoatID).Item1)
{
return;
}
// create + record new modular boat data record
var modularBoatData = Pool.Get<ModularBoatData>();
modularBoatData.Init(modularBoat, _configData.ModularBoat.Radius);
_modularBoatData.Add(modularBoatID, modularBoatData);
// create zone
if (ZM_CreateOrUpdateZone(modularBoatData, modularBoatID))
{
// ...and mapping
TP_AddOrUpdateMapping(modularBoatID);
}
// if sunken, schedule delete
if (modularBoat.HasFlag(BaseEntity.Flags.Broken))
{
ScheduleDeleteModularBoatData(modularBoatID, (ulong)ownerID);
}
}
// delete modular boat record + zone for given modular boat ID
private void DeleteModularBoatData(
NetworkableId modularBoatID, List<string> bulkDeleteList = null)
{
// clean up any deletion timer that may have invoked this
CancelDictionaryTimer(ref _modularBoatDeleteTimers, modularBoatID);
// remove modular boat record to local variable, aborting if not found
if (!_modularBoatData.Remove(modularBoatID, out var modularBoatData)) return;
// drop modular boat record
Pool.Free(ref modularBoatData);
// destroy modular boat zone
if (null == bulkDeleteList)
{
// ...immediately
TP_RemoveMapping(modularBoatID);
ZM_EraseZone(modularBoatID);
}
else
{
// ...by adding to given bulk delete list
bulkDeleteList.Add(GetZoneID(modularBoatID));
}
}
// schedule a delayed modular boat creation
private void ScheduleCreateModularBoatData(PlayerBoat modularBoat)
{
var modularBoatID = GetNetworkableID(modularBoat);
// abort if modular boat is already known, or if any timers are already
// running
if (_modularBoatData.ContainsKey(modularBoatID) ||
_modularBoatCreateTimers.ContainsKey(modularBoatID) ||
_modularBoatDeleteTimers.ContainsKey(modularBoatID))
{
return;
}
// schedule call
_modularBoatCreateTimers.Add(modularBoatID, timer.Once(
_configData.CreateDelaySeconds, () => CreateModularBoatData(modularBoat)));
// notify player
var ownerID = GetOwnerID(modularBoat);
if (ownerID is null) return;
if (_configData.CreateNotify) NotifyOwnerCreate((ulong)ownerID);
}
// schedule a delayed modular boat deletion
private void ScheduleDeleteModularBoatData(
NetworkableId modularBoatID, ulong ownerID)
{
var notifyZone = _configData.DeleteNotify;
// notify owner if this was pending creation
if (CancelDictionaryTimer(ref _modularBoatCreateTimers, modularBoatID) &&
_configData.CreateNotify)
{
NotifyOwnerAbort(ownerID);
// suppress zone notify because there isn't one yet
notifyZone = false;
}
// abort if building is unknown, or if deletion timer is already running
if (!_modularBoatData.ContainsKey(modularBoatID) ||
_modularBoatDeleteTimers.ContainsKey(modularBoatID))
{
return;
}
// schedule call
_modularBoatDeleteTimers.Add(modularBoatID, timer.Once(
_configData.DeleteDelaySeconds,
() => DeleteModularBoatData(modularBoatID)));
// notify players
if (notifyZone) NotifyZoneDelete(modularBoatID);
}
// create a new shelter record + zone for given legacy shelter
private void CreateShelterData(EntityPrivilege legacyShelter)
{
// abort if shelter object is destroyed
if (!IsValid(legacyShelter)) return;
var legacyShelterID = GetNetworkableID(legacyShelter);
// clean up any creation timer that may have invoked this
CancelDictionaryTimer(ref _shelterCreateTimers, legacyShelterID);
// if a shelter already exists, then this call is probably redundant
if (_shelterData.ContainsKey(legacyShelterID)) return;
// abort if zone toggled off
if (!GetToggleStates(_persistData.ShelterToggleData, legacyShelterID).Item1)
{
return;
}
// create + record new shelter data record
var shelterData = Pool.Get<ShelterData>();
shelterData.Init(legacyShelter, _configData.Shelter.Radius);
_shelterData.Add(legacyShelterID, shelterData);
// create zone
if (ZM_CreateOrUpdateZone(shelterData, legacyShelterID))
{
// ...and mapping
TP_AddOrUpdateMapping(legacyShelterID);
}
}
// delete shelter record + zone for given LS ID
private void DeleteShelterData(
NetworkableId legacyShelterID, List<string> bulkDeleteList = null)
{
// clean up any deletion timer that may have invoked this
CancelDictionaryTimer(ref _shelterDeleteTimers, legacyShelterID);
// remove shelter record to local variable, aborting if not found
if (!_shelterData.Remove(legacyShelterID, out var shelterData)) return;
// drop shelter record
Pool.Free(ref shelterData);
// destroy shelter zone
if (null == bulkDeleteList)
{
// ...immediately
TP_RemoveMapping(legacyShelterID);
ZM_EraseZone(legacyShelterID);
}
else
{
// ...by adding to given bulk delete list
bulkDeleteList.Add(GetZoneID(legacyShelterID));
}
}
// schedule a delayed shelter creation
// this should be used during normal operations, in order to apply delays
// and/or notifications per plugin configuration
private void ScheduleCreateShelterData(EntityPrivilege legacyShelter)
{
var legacyShelterID = GetNetworkableID(legacyShelter);
// abort if shelter is already known, or if any timers are already running
if (_shelterData.ContainsKey(legacyShelterID) ||
_shelterCreateTimers.ContainsKey(legacyShelterID) ||
_shelterDeleteTimers.ContainsKey(legacyShelterID))
{
return;
}
// schedule call
_shelterCreateTimers.Add(legacyShelterID, timer.Once(
_configData.CreateDelaySeconds, () => CreateShelterData(legacyShelter)));
// notify players
if (!_configData.CreateNotify) return;
var ownerID = GetOwnerID(legacyShelter);
if (null != ownerID) NotifyOwnerCreate((ulong)ownerID);
}
// schedule a delayed shelter deletion
// this should be used during normal operations, in order to apply delays
// and/or notifications per plugin configuration
private void ScheduleDeleteShelterData(
NetworkableId legacyShelterID, ulong? ownerID)
{
var notifyZone = _configData.DeleteNotify;
// notify owner if this was pending creation
if (CancelDictionaryTimer(ref _shelterCreateTimers, legacyShelterID) &&
_configData.CreateNotify)
{
if (null != ownerID) NotifyOwnerAbort((ulong)ownerID);
// suppress zone notify because there isn't one yet
notifyZone = false;
}
// abort if shelter is unknown, or if deletion timer is already running
if (!_shelterData.ContainsKey(legacyShelterID) ||
_shelterDeleteTimers.ContainsKey(legacyShelterID))
{
return;
}
// schedule call
_shelterDeleteTimers.Add(legacyShelterID, timer.Once(
_configData.DeleteDelaySeconds,
() => DeleteShelterData(legacyShelterID)));
// notify players in zone
if (notifyZone) NotifyZoneDelete(legacyShelterID);
}
// create a new tugboat record + zone for given tugboat
private void CreateTugboatData(VehiclePrivilege tugboat)
{
// abort if tugboat object is destroyed or on a modular boat
if (!IsValid(tugboat) || tugboat is PlayerBoatPrivilege) return;
var tugboatID = GetNetworkableID(tugboat);
// if a tugboat already exists, then this call is probably redundant
if (_tugboatData.ContainsKey(tugboatID)) return;
// abort if zone toggled off
if (!GetToggleStates(_persistData.TugboatToggleData, tugboatID).Item1)
{
return;
}
// create + record new tugboat data record
var tugboatData = Pool.Get<TugboatData>();
tugboatData.Init(
tugboat, _configData.Tugboat.Radius,
_configData.Tugboat.ForceNetworking, _configData.Tugboat.ForceBuoyancy);
_tugboatData.Add(tugboatID, tugboatData);
// create zone
if (ZM_CreateOrUpdateZone(tugboatData, tugboatID))
{
// ...and mapping
TP_AddOrUpdateMapping(tugboatID);
}
}
// delete tugboat record + zone for given tugboat ID
private void DeleteTugboatData(
NetworkableId tugboatID, List<string> bulkDeleteList = null)
{
// clean up any deletion timer that may have invoked this
CancelDictionaryTimer(ref _tugboatDeleteTimers, tugboatID);
// remove tugboat record to local variable, aborting if not found
if (!_tugboatData.Remove(tugboatID, out var tugboatData)) return;
// drop tugboat record
Pool.Free(ref tugboatData);
// destroy tugboat zone
if (null == bulkDeleteList)
{
// ...immediately
TP_RemoveMapping(tugboatID);
ZM_EraseZone(tugboatID);
}
else
{
// ...by adding to given bulk delete list
bulkDeleteList.Add(GetZoneID(tugboatID));
}
}
// schedule a delayed shelter deletion
// this should be used during normal operations, in order to apply delays
// and/or notifications per plugin configuration
private void ScheduleDeleteTugboatData(NetworkableId tugboatID)
{
// abort if tugboat is unknown, or if deletion timer is already running
if (!_tugboatData.ContainsKey(tugboatID) ||
_tugboatDeleteTimers.ContainsKey(tugboatID))
{
return;
}
// schedule call
_tugboatDeleteTimers.Add(tugboatID, timer.Once(
_configData.DeleteDelaySeconds, () => DeleteTugboatData(tugboatID)));
// notify players in zone
if (_configData.DeleteNotify) NotifyZoneDelete(tugboatID);
}
// activate/restart PvP delay for given player and send notification
// does nothing if no PvP delay configured
// notifies if delay not already active
// restarts delay if already active
// sends stop notification for old zoneID, plus start notification for new
// one, if zoneID changes with an already-active delay
private void PlayerBasePvpDelayStart(ulong playerID, string zoneID)
{
// abort if no delay configured
if (_configData.PvpDelaySeconds <= 0.0f) return;
if (_pvpDelayTimers.TryGetValue(playerID, out var delayData))
{
// delay already active - reset timer
delayData.Item1.Reset(_configData.PvpDelaySeconds);
// handle zone change
if (zoneID != delayData.Item2)
{
// fire notification hook for old zone
Interface.CallHook("OnPlayerBasePvpDelayStop",
playerID, delayData.Item2);
// record new zone
delayData.Item2 = zoneID;
}
}
else
{
// no delay active - start+record one
_pvpDelayTimers.Add(playerID, (timer.Once(
_configData.PvpDelaySeconds, () => PlayerBasePvpDelayStop(
playerID)), zoneID));
}
// fire notification hook
Interface.CallHook("OnPlayerBasePvpDelayStart", playerID, zoneID);
// also notify TruePVE if we're doing that
if (_useExcludePlayer)
{
Interface.CallHook("ExcludePlayer",
playerID, _configData.PvpDelaySeconds, this);
}
}
// stop/end PvP delay for given player and send notification
private void PlayerBasePvpDelayStop(ulong playerID)
{
// abort if no delay record exists
if (!_pvpDelayTimers.Remove(playerID, out var delayData)) return;
// clean up timer
DestroyTimer(delayData.Item1);
// fire notification hook
Interface.CallHook("OnPlayerBasePvpDelayStop", playerID, delayData.Item2);
// also notify TruePVE if we're doing that
if (_useExcludePlayer)
{
Interface.CallHook("ExcludePlayer", playerID, 0.0f, this);
}
}
// utility method to return an appropriate yield instruction based on whether
// this is a long pause for debug logging to catch up, whether current server
// framerate is too low, etc.
private YieldInstruction DynamicYield()
{
// perform one-time caching of target FPS
if (_targetFps <= 0) _targetFps = Mathf.Min(ConVar.FPS.limit, 30);
return
Performance.report.frameRate >= _targetFps ? _fastYield : _throttleYield;
}
// coroutine method to asynchronously create zones for all existing bases
private IEnumerator CreateData()
{
var startTime = DateTime.UtcNow;
Puts("CreateData(): Starting zone creation...");
// create zones for all existing player-owned bases
foreach (var building in BuildingManager.server.buildingDictionary.Values)
{
var toolCupboard = GetToolCupboard(building);
if (!IsPlayerOwned(toolCupboard)) continue;
CreateBuildingData(toolCupboard);
yield return DynamicYield();
}
Puts($"CreateData(): Created {_buildingData.Count} building zone(s)...");
// create zones for all existing modular boats and tugboats
// done together so only one pass over the global entity list is needed
foreach (var serverEntity in BaseNetworkable.serverEntities)
{
if (serverEntity is PlayerBoat modularBoat)
{
CreateModularBoatData(modularBoat);
yield return DynamicYield();
}
else if (
// modular boat build privileges are also VehiclePrivilege, so we have
// to ignore those
serverEntity is VehiclePrivilege tugboat and not PlayerBoatPrivilege)
{
CreateTugboatData(tugboat);
yield return DynamicYield();
}
}
Puts($"CreateData(): Created {_modularBoatData.Count} modular boat zone(s)...");
Puts($"CreateData(): Created {_tugboatData.Count} tugboat zone(s)...");
// create zones for all existing player-owned legacy shelters
foreach (var shelterList in LegacyShelter.SheltersPerPlayer.Values)
{
foreach (var shelter in shelterList)
{
if (!IsValid(shelter) ||
!shelter.entityPrivilege.TryGet(true, out var legacyShelter) ||
!IsPlayerOwned(legacyShelter))
{
continue;
}
CreateShelterData(legacyShelter);
yield return DynamicYield();
}
}
Puts($"CreateData(): Created {_shelterData.Count} shelter zone(s)...");
Puts($"CreateData(): ...Startup completed in {(DateTime.UtcNow - startTime).TotalSeconds} seconds.");
_createDataCoroutine = null;
}
#endregion Core Methods
#region Oxide/RustPlugin API/Hooks
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary<string, string>
{
["NotifyOwnerAbort"] =
"{0}Player Base PvP Zone creation canceled",
["NotifyOwnerCreate"] =
"{0}Player Base PvP Zone will be created here in {1} second(s)",
["NotifyZoneDelete"] =
"{0}Player Base PvP Zone will be deleted here in {1} second(s)",
["MessageZoneEnter"] =
"WARNING: Entering Player Base PVP Zone",
["MessageZoneExit"] =
"Leaving Player Base PVP Zone",
["ToggleLockoutDamage"] =
"Player Base PvP Zone cannot be toggled for another {0} second(s) due to recent damage",
["ToggleLockoutToggle"] =
"Player Base PvP Zone cannot be toggled for another {0} second(s) due to recent toggle",
["ToggleCancelCreate"] =
"Player Base PvP Zone creation canceled due to toggle command",
["ToggleCancelDelete"] =
"Player Base PvP Zone deletion canceled due to toggle command",
["ToggleDelete"] =
"Player Base PvP Zone deletion triggered due to toggle command",
["ToggleCreate"] =
"Player Base PvP Zone creation triggered due to toggle command",
["ToggleFail"] =
"No togglable Player Base PvP Zone here",
["HelpToggle"] =
" /{0} {1} - toggle Player Base PvP Zone for authorized nearby shelter, looked-at TC, or mounted tugboat",
["HelpNoPerms"] =
"You do not have permission to run any Player Base PvP Zone commands",
["CmdNoParams"] =
"Valid parameter(s) required; type the following chat command for help: /{0} {1}",
["CmdNoPerm"] =
"You do not have permission to run this Player Base PvP Zone command",
["CmdUnknown"] =
"Unknown Player Base PvP Zone command"
}, this);
}
private void Init()
{
LoadData();
// unsubscribe from OnEntitySpawned() hook calls under OnServerInitialized()
// PBPZ is not dependent on OnEntitySpawned() during server startup, because
// it scans all relevant entities instead
Unsubscribe(nameof(OnEntitySpawned));
// also initially unsubscribe from damage hooks, because they're expensive
Unsubscribe(nameof(OnEntityTakeDamage));
AddCovalenceCommand(CommandRoot, nameof(HandleCommand));
permission.RegisterPermission(GetPermission(TogglePermission), this);
if (null == _configData) return;
_decayEntityData.Clear();
BaseData.SphereDarkness = _configData.SphereDarkness;
}
private void OnServerInitialized()
{
_useExcludePlayer =
null != TruePVE && TruePVE.Version >= new VersionNumber(2, 2, 3);
if (_useExcludePlayer)
{
Puts("OnServerInitialized(): TruePVE 2.2.3+ detected! TruePVE PVP delays will be used");
Unsubscribe(nameof(CanEntityTakeDamage));
}
// resubscribe OnEntitySpawned() hook, as it's now safe to handle this
Subscribe(nameof(OnEntitySpawned));
// subscribe OnEntityTakeDamage() if toggle lockout on damage is requested
// via config file
if (_configData.Toggle.DamageLockoutSeconds > 0.0f)
{
Subscribe(nameof(OnEntityTakeDamage));
}
// clean up toggle data dictionaries, and save data file if anything changed
// NOTE: this mainly handles the case that a wipe happened between sessions
var saveData = false;
if (CleanupToggleDict(_persistData.BuildingToggleData) is
var defunctBuildings and > 0)
{
Puts($"Removed {defunctBuildings} defunct building zone toggle data entries from data file");
saveData = true;
}
if (CleanupToggleDict(_persistData.ModularBoatToggleData) is
var defunctModularBoats and > 0)
{
Puts($"Removed {defunctModularBoats} defunct modular boat zone toggle data entries from data file");
saveData = true;
}
if (CleanupToggleDict(_persistData.ShelterToggleData) is
var defunctShelters and > 0)
{
Puts($"Removed {defunctShelters} defunct shelter zone toggle data entries from data file");
saveData = true;
}
if (CleanupToggleDict(_persistData.TugboatToggleData) is
var defunctTugboats and > 0)
{
Puts($"Removed {defunctTugboats} defunct tugboat zone toggle data entries from data file");
saveData = true;
}
if (saveData)
{
SaveData();
}
NextTick(() =>
{
_createDataCoroutine = ServerMgr.Instance.StartCoroutine(CreateData());
});
}
private static void DestroyBaseDataDictionary<T>(
ref Dictionary<NetworkableId, T> dict,
Action<NetworkableId, List<string>> deleter,
List<string> bulkDeleteList)
{
var networkableIds = Pool.Get<List<NetworkableId>>();
foreach (var networkableId in dict.Keys)
{
networkableIds.Add(networkableId);
}
foreach(var networkableId in networkableIds)
{
deleter(networkableId, bulkDeleteList);
}
dict.Clear();
Pool.FreeUnmanaged(ref networkableIds);
}
private static void DestroyTimer(Timer t)
{
if (TimerValid(t)) t.Destroy();
}
private static bool TimerValid(Timer t) => false == t?.Destroyed;
private void DestroyTimerDictionary<T>(
ref Dictionary<T, Timer> dict, string desc)
{
if (dict.Count <= 0) return;
Puts($"Unload(): Destroying {dict.Count} {desc} timer(s)...");
foreach (var dTimer in dict.Values)
{
DestroyTimer(dTimer);
}
}
private void Unload()
{
if (null != _createDataCoroutine)
{
ServerMgr.Instance.StopCoroutine(_createDataCoroutine);
_createDataCoroutine = null;
}
// if save timer active, force immediate write
if (TimerValid(_saveDataTimer))
{
Puts("Unload(): Forcing data file write");
WriteData();
}
_saveDataTimer = null;
Puts("Unload(): Cleaning up...");
// cleanup base zones
var bulkDeleteList = Pool.Get<List<string>>();
if (_buildingData.Count > 0)
{
Puts($"Unload(): Destroying {_buildingData.Count} building zone records...");
DestroyBaseDataDictionary(
ref _buildingData, DeleteBuildingData, bulkDeleteList);
}
DestroyTimerDictionary(ref _buildingCheckTimers, "building check");
DestroyTimerDictionary(ref _buildingCreateTimers, "building creation");
DestroyTimerDictionary(ref _buildingDeleteTimers, "building deletion");
if (_modularBoatData.Count > 0)
{
Puts($"Unload(): Destroying {_modularBoatData.Count} modular boat zone records...");
DestroyBaseDataDictionary(
ref _modularBoatData, DeleteModularBoatData, bulkDeleteList);
}
DestroyTimerDictionary(ref _modularBoatCreateTimers, "modular boat creation");
DestroyTimerDictionary(ref _modularBoatDeleteTimers, "modular boat deletion");
if (_shelterData.Count > 0)
{
Puts($"Unload(): Destroying {_shelterData.Count} shelter zone records...");
DestroyBaseDataDictionary(
ref _shelterData, DeleteShelterData, bulkDeleteList);
}
DestroyTimerDictionary(ref _shelterCreateTimers, "shelter creation");
DestroyTimerDictionary(ref _shelterDeleteTimers, "shelter deletion");
if (_tugboatData.Count > 0)
{
Puts($"Unload(): Destroying {_tugboatData.Count} tugboat zone records...");
DestroyBaseDataDictionary(
ref _tugboatData, DeleteTugboatData, bulkDeleteList);
}
DestroyTimerDictionary(ref _tugboatDeleteTimers, "tugboat deletion");
if (bulkDeleteList.Count > 0)
{
TP_RemoveMappings(bulkDeleteList);
ZM_EraseZones(bulkDeleteList);
}
Pool.FreeUnmanaged(ref bulkDeleteList);
// cleanup player records
if (_playerZones.Count > 0)
{
Puts($"Unload(): Destroying {_playerZones.Count} player-in-zones records...");
foreach (var playerZoneData in _playerZones)
{
var zones = playerZoneData.Value;
if (null == zones) continue;
Pool.FreeUnmanaged(ref zones);
}
_playerZones.Clear();
}
if (_pvpDelayTimers.Count > 0)
{
Puts($"Unload(): Destroying {_pvpDelayTimers.Count} player PvP delay timers...");
foreach (var timerData in _pvpDelayTimers.Values)
{
DestroyTimer(timerData.Item1);
}
_pvpDelayTimers.Clear();
}
_decayEntityData.Clear();
Puts("Unload(): ...Cleanup complete.");
}
private void ClampToZero(ref float value, string name)
{
if (value >= 0) return;
PrintWarning($"Illegal {name}={value}; clamping to zero");
value = 0.0f;
}
protected override void LoadConfig()
{
base.LoadConfig();
try
{
_configData = Config.ReadObject<ConfigData>();
if (null == _configData)
{
LoadDefaultConfig();
}
else
{
ClampToZero(ref _configData.CreateDelaySeconds, "createDelaySeconds");
ClampToZero(ref _configData.DeleteDelaySeconds, "deleteDelaySeconds");
ClampToZero(ref _configData.PvpDelaySeconds, "pvpDelaySeconds");
if (_configData.SphereDarkness > 10)
{
PrintWarning($"Illegal sphereDarkness={_configData.SphereDarkness} value; clamping to 10");
_configData.SphereDarkness = 10;
}
if (string.IsNullOrEmpty(_configData.RulesetName))
{
PrintWarning($"Illegal truePveRuleset={_configData.RulesetName} value; resetting to \"exclude\"");
_configData.RulesetName = "exclude";
}
ClampToZero(ref _configData.Building.CheckDelaySeconds, "building.checkDelaySeconds");
ClampToZero(ref _configData.Building.MinimumBuildingRadius, "building.minimumBuildingRadius");
ClampToZero(ref _configData.Building.MinimumBlockRadius, "building.minimumBlockRadius");
ClampToZero(ref _configData.ModularBoat.CheckDelaySeconds, "modularBoat.checkDelaySeconds");
ClampToZero(ref _configData.ModularBoat.Radius, "modularBoat.radius");
ClampToZero(ref _configData.Shelter.Radius, "shelter.radius");
ClampToZero(ref _configData.Tugboat.Radius, "tugboat.radius");
}
}
catch (Exception ex)
{
PrintError($"Exception while loading configuration file:\n{ex}");
LoadDefaultConfig();
}
SaveConfig();
}
protected override void LoadDefaultConfig()
{
PrintWarning("Creating a new configuration file");
_configData = new ConfigData();
}
protected override void SaveConfig() => Config.WriteObject(_configData);
private void LoadData()
{
try
{
_persistData =
Interface.Oxide.DataFileSystem.ReadObject<PersistData>(Name);
}
catch (Exception ex)
{
Puts($"LoadData(): Error reading data file: {ex}");
_persistData = null;
}
if (_persistData == null) ClearData();
}
private void ClearData()
{
_persistData = new PersistData();
Puts("ClearData(): Scheduling write of new data file");
SaveData();
}
// this is a frontend to WriteData() that enforces a minimum delay between
// data file writes
private void SaveData()
{
// start a save timer unless already running
if (TimerValid(_saveDataTimer)) return;
// TODO: make save delay configurable?
_saveDataTimer = timer.Once(60.0f, WriteData);
}
// do the actual data file write
private void WriteData()
{
DestroyTimer(_saveDataTimer);
Interface.Oxide.DataFileSystem.WriteObject(Name, _persistData);
Puts("WriteData(): Wrote data file");
}
// common logic for modular boat zone end-of-life scenarios
// untether controls whether sphere(s) and/or zone should be disconnected from
// the modular boat, which is desirable when it's sunk/killed but not when
// the zone is being toggled off by a player
private void HandleModularBoatEol(
PlayerBoat modularBoat, bool disconnect = true)
{
// abort if this isn't a player-owned modular boat
if (!IsValid(modularBoat)) return;
// cache modular boat ID
var modularBoatID = GetNetworkableID(modularBoat);
// owner player ID too
var ownerID = GetOwnerID(modularBoat);
if (ownerID is null) return;
// if we have a record on this modular boat, clear its entity reference ASAP
// to minimize side effects
// NOTE: don't abort here if we have no record, as we need to also handle
// the case of a modular boat that is pending creation
if (disconnect &&
_modularBoatData.TryGetValue(modularBoatID, out var modularBoatData))
{
// release entity reference and disconnect spheres immediately to
// minimize side effects
modularBoatData.ClearEntity();
// also untether ZoneManager zone if present, so that it doesn't
// ... [Code preview truncated due to length. Full source in attached file] ...