C#:
// Все права принадлежат дискорд сообществу https://discord.gg/VgNHPpNrz6
// Requires: ZoneManager
using Facepunch;
using Newtonsoft.Json;
using Oxide.Core;
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.2.0")]
[Description("Maintains Zone Manager / TruePVE exclusion zones around player bases")]
public class PlayerBasePvpZones : RustPlugin
{
#region Fields
[PluginReference] private readonly Plugin TruePVE;
// user-defined plugin config data
private ConfigData _configData = new();
// 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();
// 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();
// 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();
// 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:";
#endregion Fields
#region Core Methods
// generate a current 3D bounding box around a base
private Bounds CalculateBuildingBounds(BuildingPrivlidge toolCupboard)
{
// start with a box around the TC, whose space diagonal is the minimum
// total building radius
var buildingBounds = new Bounds(
toolCupboard.CenterPoint(),
Vector3.one *
_configData.Building.MinimumBuildingRadius / Mathf.Sqrt(3f));
// get building, aborting if not found
var building = toolCupboard.GetBuilding();
if (null == building) return buildingBounds;
// precalculate extents for a cube whose space diagonal is the radius
var minimumBlockExtents =
Vector3.one * _configData.Building.MinimumBlockRadius / Mathf.Sqrt(3f);
// precalculate square of minimum block magnitude for comparison purposes
var minimumBlockSqrMagnitude =
_configData.Building.MinimumBlockRadius *
_configData.Building.MinimumBlockRadius;
// grow buildingBounds volume to contain all building blocks
foreach (var entity in building.buildingBlocks)
{
// get the block's bounds and center those on its world coordinates
var entityBounds = entity.bounds;
entityBounds.center = entity.CenterPoint();
// try to be efficient by checking the largest dimension followed by
// the squared magnitude
// this will probably always return true, unless minimumBlockRadius is
// set to a very small value
if (entityBounds.extents.Max() < _configData.Building.MinimumBlockRadius
&& entityBounds.extents.sqrMagnitude < minimumBlockSqrMagnitude)
{
// block is smaller than the minimum, so substitute in the minimum
entityBounds.extents = minimumBlockExtents;
}
// grow volume to contain this block
buildingBounds.Encapsulate(entityBounds);
}
return buildingBounds;
}
// derive a radius from building bounding box
private static float CalculateBuildingRadius(Bounds buildingBounds) =>
buildingBounds.extents.magnitude;
// 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;
cTimer.Destroy();
return true;
}
// BuildingBlock wrapper for GetToolCupboard()
private static BuildingPrivlidge GetToolCupboard(
BuildingBlock buildingBlock) =>
GetToolCupboard(buildingBlock.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)
{
// check the easy stuff first
if (null == building ||
!building.HasBuildingBlocks() ||
!building.HasBuildingPrivileges())
{
return null;
}
// since the building is in *some* TC range, see if any of those are
// physically attached (credit: Kulltero for this more efficient method)
foreach (var toolCupboard in building.buildingPrivileges)
{
if (!building.decayEntities.Contains(toolCupboard)) continue;
// only one TC can be connected, so return it - or null if it's not
// player-owned
return IsPlayerOwned(toolCupboard) ? toolCupboard : null;
}
// no attached player-owned TC found
return 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;
private bool IsKnownPlayerBaseId(NetworkableId networkableId) =>
_buildingData.ContainsKey(networkableId) ||
_buildingCreateTimers.ContainsKey(networkableId) ||
_shelterData.ContainsKey(networkableId) ||
_shelterCreateTimers.ContainsKey(networkableId) ||
_tugboatData.ContainsKey(networkableId);
private static ulong? GetOwnerID(EntityPrivilege legacyShelter)
{
if (!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.userid.IsSteamId()) return playerNameID.userid;
}
return null;
}
private static VehiclePrivilege GetVehiclePrivilege(BaseVehicle vehicle)
{
foreach (var child in vehicle.children)
{
if (child is VehiclePrivilege privilege) return privilege;
}
return null;
}
// OwnerID is zero for shelters for some reason, so check auth list
private static bool IsPlayerOwned(EntityPrivilege legacyShelter) =>
null != GetOwnerID(legacyShelter);
private static bool IsPlayerOwned(DecayEntity decayEntity) =>
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, _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 buildingBounds = CalculateBuildingBounds(toolCupboard);
var radius = CalculateBuildingRadius(buildingBounds);
// abort if footprint basically unchanged
if (buildingData.Location == buildingBounds.center &&
Mathf.Approximately(buildingData.Radius, radius))
{
return;
}
// update existing building data record
buildingData.Update(buildingBounds.center, 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;
// get initial building footprint data
var buildingBounds = CalculateBuildingBounds(toolCupboard);
var radius = CalculateBuildingRadius(buildingBounds);
// create + record new building data record
var buildingData = Pool.Get<BuildingData>();
if (null == buildingData)
{
PrintError("CreateBuildingData(): Failed to allocate BuildingData from pool");
return;
}
buildingData.Init(toolCupboard, buildingBounds.center, 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
ZM_EraseZone(toolCupboardID);
}
else
{
//...by adding to given bulk delete list
bulkDeleteList.Add(GetZoneID(toolCupboardID));
}
// destroy mapping
TP_RemoveMapping(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 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;
// create + record new shelter data record
var shelterData = Pool.Get<ShelterData>();
if (null == shelterData)
{
PrintError("CreateShelterData(): Failed to allocate ShelterData from pool");
return;
}
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
ZM_EraseZone(legacyShelterID);
}
else
{
//...by adding to given bulk delete list
bulkDeleteList.Add(GetZoneID(legacyShelterID));
}
// destroy mapping
TP_RemoveMapping(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
if (!IsValid(tugboat)) return;
var tugboatID = GetNetworkableID(tugboat);
// if a tugboat already exists, then this call is probably redundant
if (_tugboatData.ContainsKey(tugboatID)) return;
// create + record new tugboat data record
var tugboatData = Pool.Get<TugboatData>();
if (null == tugboatData)
{
PrintError("CreateTugboatData(): Failed to allocate TugboatData from pool");
return;
}
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
ZM_EraseZone(tugboatID);
}
else
{
//...by adding to given bulk delete list
bulkDeleteList.Add(GetZoneID(tugboatID));
}
// destroy mapping
TP_RemoveMapping(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
delayData.Item1.Destroy();
// 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);
}
}
// purge any dangling zones + mappings from previous runs
// this shouldn't be needed, but DynamicPVP does it, so I figured it
// wouldn't hurt
private void PurgeOldZones()
{
// get list of all active Zone Manager zone IDs
var zoneIds = Pool.Get<List<string>>();
ZM_GetZoneIDs(zoneIds);
if (zoneIds.Count <= 0)
{
Pool.FreeUnmanaged(ref zoneIds);
return;
}
var mappingCount = 0U;
var eraseList = Pool.Get<List<string>>();
foreach (var zoneId in zoneIds)
{
if (!zoneId.Contains(ZoneIdPrefix)) continue;
// this is one of ours - add to delete request list
eraseList.Add(zoneId);
//...and delete TruePVE mapping
if (TP_RemoveMapping(zoneId)) ++mappingCount;
}
Pool.FreeUnmanaged(ref zoneIds);
// bulk delete zones
var eraseResults = Pool.Get<List<bool>>();
ZM_EraseZones(eraseList, eraseResults);
Pool.FreeUnmanaged(ref eraseList);
// count how many zones actually got deleted
var zoneCount = 0U;
foreach (var result in eraseResults) if (result) ++zoneCount;
Pool.FreeUnmanaged(ref eraseResults);
Puts($"OnServerInitialized(): Deleted {zoneCount} dangling zone(s)...");
Puts($"OnServerInitialized(): Deleted {mappingCount} dangling mapping(s)...");
}
// 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)
{
var toolCupboard = GetToolCupboard(building);
if (!IsValid(toolCupboard) || !IsPlayerOwned(toolCupboard)) 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)
{
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 zones...");
// create zones for all existing tugboats
foreach (var serverEntity in BaseNetworkable.serverEntities)
{
if (serverEntity is not VehiclePrivilege tugboat || !IsValid(tugboat))
{
continue;
}
CreateTugboatData(tugboat);
yield return DynamicYield();
}
Puts($"CreateData(): Created {_tugboatData.Count} tugboat zones...");
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"
}, this);
}
private void Init()
{
if (null == _configData) return;
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));
}
// purge any dangling zones
PurgeOldZones();
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)
{
networkableIds.Add(networkableId);
}
foreach(var networkableId in networkableIds)
{
deleter(networkableId, bulkDeleteList);
}
dict.Clear();
Pool.FreeUnmanaged(ref networkableIds);
}
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) dTimer?.Destroy();
}
private void Unload()
{
if (null != _createDataCoroutine)
{
ServerMgr.Instance.StopCoroutine(_createDataCoroutine);
_createDataCoroutine = 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 (_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) 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)
{
timerData.Item1.Destroy();
}
_buildingDeleteTimers.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.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);
// called when a tugboat reaches zero health and sinks
// NOTE: this only fires for Tugboat and not VehiclePrivilege
private void OnEntityDeath(Tugboat tugboat)
{
// cache tugboat ID
var tugboatPrivilege = GetVehiclePrivilege(tugboat);
if (!tugboatPrivilege) return;
var tugboatID = GetNetworkableID(tugboatPrivilege);
// if we have a record on this tugboat, clear its entity reference ASAP to
// minimize side effects
// NOTE: we can abort here if we have no record, because tugboats have no
// creation delay
if (!_tugboatData.TryGetValue(tugboatID, out var tugboatData))
{
return;
}
// release entity reference immediately to minimize side effects
tugboatData.ClearEntity();
// also untether ZoneManager zone if present, so that it doesn't disappear
UntetherZone(GetZoneID(tugboatID), tugboat);
// schedule deletion of the dropped base
NextTick(() => ScheduleDeleteTugboatData(tugboatID));
}
// called when a building block is destroyed
private void OnEntityKill(BuildingBlock buildingBlock)
{
// abort if this isn't a player-owned building block
if (!IsPlayerOwned(buildingBlock)) return;
// attempt to find an attached TC
var toolCupboard = GetToolCupboard(buildingBlock);
// abort if no TC found, since there's nothing we can do without one
if (!toolCupboard) return;
// cache TC ID
var toolCupboardID = GetNetworkableID(toolCupboard);
// schedule a check of the updated player base
NextTick(() => ScheduleCheckBuildingData(toolCupboardID));
}
// called when a TC is destroyed
private void OnEntityKill(BuildingPrivlidge toolCupboard)
{
// abort if this isn't a player-owned building TC
if (!IsPlayerOwned(toolCupboard)) return;
// cache TC ID, because it's going away
var toolCupboardID = GetNetworkableID(toolCupboard);
// owner player ID too
var ownerID = toolCupboard.OwnerID;
// if we have a record on this base, clear its TC 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 base that is pending creation
if (_buildingData.TryGetValue(toolCupboardID, out var buildingData))
{
// release TC reference immediately to minimize side effects
buildingData.ClearEntity();
}
// schedule deletion of the dropped base
NextTick(() => ScheduleDeleteBuildingData(toolCupboardID, ownerID));
}
// called when a legacy shelter is destroyed
private void OnEntityKill(EntityPrivilege legacyShelter)
{
// abort if this isn't a player-owned shelter
if (!IsPlayerOwned(legacyShelter)) return;
// cache shelter ID, because it's going away
var legacyShelterID = GetNetworkableID(legacyShelter);
// owner player ID too
var ownerID = GetOwnerID(legacyShelter);
// if we have a record on this shelter, 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 base that is pending creation
if (_shelterData.TryGetValue(legacyShelterID, out var shelterData))
{
// release entity reference immediately to minimize side effects
shelterData.ClearEntity();
}
// schedule deletion of the dropped base
NextTick(() => ScheduleDeleteShelterData(legacyShelterID, ownerID));
}
// called when a tugboat despawns
// NOTE: using Tugboat instead of VehiclePrivilege since we've tethered a
// bunch of stuff to that
private void OnEntityKill(Tugboat tugboat) => OnEntityDeath(tugboat);
// called when a building block is spawned
// this is preferred over OnEntityBuilt both because it's more
// straightforward, and because it catches things like CopyPaste spawning
// with player owner set
private void OnEntitySpawned(BuildingBlock buildingBlock) =>
OnEntityKill(buildingBlock);
// called when a TC is spawned
// this is preferred over OnEntityBuilt both because it's more
// straightforward, and because it catches things like CopyPaste spawning
// with player owner set
private void OnEntitySpawned(BuildingPrivlidge toolCupboard)
{
// abort if this isn't a player-owned TC
if (!IsPlayerOwned(toolCupboard)) return;
// schedule creation of new player base record + zone
NextTick(() => ScheduleCreateBuildingData(toolCupboard));
}
// called when a legacy shelter is spawned
// this is preferred over OnEntityBuilt both because it's more
// straightforward, and because it catches things like CopyPaste spawning
// with player owner set
private void OnEntitySpawned(EntityPrivilege legacyShelter)
{
NextTick(() =>
{
// abort if this isn't a player-owned shelter
// this needs to be checked inside of NextTick() because Raidable
// Shelters doesn't clear auth until after spawning
if (!IsPlayerOwned(legacyShelter)) return;
// schedule creation of new player base record + zone
ScheduleCreateShelterData(legacyShelter);
});
}
// called when a tugboat is spawned
// this is preferred over OnEntityBuilt both because it's more
// straightforward, and because it catches things like CopyPaste spawning
// with player owner set
private void OnEntitySpawned(VehiclePrivilege tugboat)
{
// immediately create new player base record + zone
// (tugboats have no creation delay because players can't normally
// (de)spawn them at will)
NextTick(() => CreateTugboatData(tugboat));
}
// custom hook defined by this plugin to return originating zoneID if player
// has an active PvP delay, or an empty string if not
private string OnPlayerBasePvpDelayQuery(ulong playerID) =>
_pvpDelayTimers.TryGetValue(playerID, out var delayData) ?
delayData.Item2 : string.Empty;
private bool IsUsingExcludePlayer() => _useExcludePlayer;
#endregion Oxide/RustPlugin API/Hooks
#region ZoneManager Integration
#region ZoneManager Helpers
// return NetworkableId from a given ZoneManager zoneID string, or null if
// not found
private static NetworkableId? GetNetworkableID(string zoneID) =>
zoneID.StartsWith(ZoneIdPrefix) && ulong.TryParse(
zoneID.Substring(ZoneIdPrefix.Length), out var value) ?
new NetworkableId(value) : null;
// synthesize ZoneManager zoneID string from networkableID
private static string GetZoneID(NetworkableId networkableID) =>
ZoneIdPrefix + networkableID;
// get options array for ZoneManager zone creation
private string[] GetZoneOptions(string zoneName, float radius) =>
_configData.ZoneMessages ?
new[]
{
"name", zoneName,
"radius", radius.ToString(CultureInfo.InvariantCulture),
"enter_message", lang.GetMessage("MessageZoneEnter", this),
"leave_message", lang.GetMessage("MessageZoneExit", this)
} :
new[] { "radius", radius.ToString(CultureInfo.InvariantCulture) };
// tether a ZoneManager zone to a transform
// credit: CatMeat & Arainrr for examples via DynamicPVP
private static void TetherZone(string zoneID, Transform parentTransform)
{
var zoneTransform = ZM_GetZoneByID(zoneID)?.transform;
if (!zoneTransform) return;
zoneTransform.SetParent(parentTransform);
zoneTransform.rotation = parentTransform.rotation;
zoneTransform.position = parentTransform.position;
}
// untether a ZoneManager zone from its parent transform (if any)
private static void UntetherZone(string zoneID, BaseEntity parentEntity) =>
ZM_GetZoneByID(zoneID)?.transform.SetParent(null, true);
// create or update a ZoneManager zone
private static bool ZM_CreateOrUpdateZone(
string zoneID, string[] zoneOptions, Vector3 location) =>
Convert.ToBoolean(Interface.CallHook(
"CreateOrUpdateZone", zoneID, zoneOptions, location));
// create or update a ZoneManager zone for given building record
// optionally takes TC ID for performance reasons
private bool ZM_CreateOrUpdateZone(
BuildingData buildingData, NetworkableId? toolCupboardID = null)
{
if (null == toolCupboardID && buildingData.ToolCupboard)
{
toolCupboardID = GetNetworkableID(buildingData.ToolCupboard);
}
if (null == toolCupboardID) return false;
return ZM_CreateOrUpdateZone(
GetZoneID((NetworkableId)toolCupboardID),
GetZoneOptions(ZoneIdPrefix + "building", buildingData.Radius),
buildingData.Location);
}
// create or update a ZoneManager zone for given shelter record
// optionally takes shelter ID for performance reasons
private bool ZM_CreateOrUpdateZone(
ShelterData shelterData, NetworkableId? legacyShelterID = null)
{
if (null == legacyShelterID && shelterData.LegacyShelter)
{
legacyShelterID = GetNetworkableID(shelterData.LegacyShelter);
}
if (null == legacyShelterID) return false;
return ZM_CreateOrUpdateZone(
GetZoneID((NetworkableId)legacyShelterID),
GetZoneOptions(ZoneIdPrefix + "shelter", shelterData.Radius),
shelterData.Location);
}
// create or update a ZoneManager zone for given tugboat record
// optionally takes tugboat ID for performance reasons
private bool ZM_CreateOrUpdateZone(
TugboatData tugboatData, NetworkableId? tugboatID = null)
{
if (null == tugboatID && tugboatData.Tugboat)
{
tugboatID = GetNetworkableID(tugboatData.Tugboat);
}
if (null == tugboatID) return false;
var zoneID = GetZoneID((NetworkableId)tugboatID);
if (!ZM_CreateOrUpdateZone(
zoneID,
GetZoneOptions(ZoneIdPrefix + "tugboat", tugboatData.Radius),
tugboatData.Location))
{
return false;
}
// tether ZoneManager zone to tugboat
var tugboatTransform = tugboatData.Tugboat?.GetParentEntity()?.transform;
if (tugboatTransform) TetherZone(zoneID, tugboatTransform);
return true;
}
// delete a ZoneManager zone by Zone ID
private static bool ZM_EraseZone(string zoneID) =>
Convert.ToBoolean(Interface.CallHook("EraseZone", zoneID));
// delete a batch of ZoneManager zones by Zone ID
private static void ZM_EraseZones(
List<string> zoneIDs, List<bool> results = null) =>
Interface.CallHook("EraseZones", zoneIDs, results);
// delete a ZoneManager zone by networkable ID
private static void ZM_EraseZone(NetworkableId networkableId) =>
ZM_EraseZone(GetZoneID(networkableId));
// get list of players in the given ZoneManager zone, if any
private static void ZM_GetPlayersInZone(
string zoneID, List<BasePlayer> list) =>
Interface.CallHook("GetPlayersInZoneNoAlloc", zoneID, list);
// get ZoneManager's actual data object for a given zoneID
// credit: CatMeat & Arainrr for examples via DynamicPVP
private static ZoneManager.Zone ZM_GetZoneByID(string zoneID) =>
Interface.CallHook("GetZoneByID", zoneID) as ZoneManager.Zone;
// get the list of all active ZoneManager zone IDs
private static void ZM_GetZoneIDs(List<string> list) =>
Interface.CallHook("GetZoneIDSNoAlloc", list);
#endregion ZoneManager Helpers
#region ZoneManager Hooks
// called when a player enters any ZoneManager zone
private void OnEnterZone(string zoneID, BasePlayer player)
{
var playerID = player.userID.Get();
// abort if not a real player
if (!playerID.IsSteamId()) return;
// abort if not one of ours
var networkableId = GetNetworkableID(zoneID);
if (null == networkableId ||
!IsKnownPlayerBaseId((NetworkableId)networkableId))
// ... [Code preview truncated due to length. Full source in attached file] ...