Иконка ресурса

FREE PlayerBasePvpZones : 1.2.0 1.2.0

Нет прав для скачивания
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] ...

Автор
15FPS
Скачиваний
0
Просмотры
1
Первый выпуск
Обновление
Оценка
0.00 звёзд 0 оценок
Сверху