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

FREE PlayerBasePvpZones : 1.6.0 1.6.0

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

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