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

FREE SuperPVxInfo : 1.14.0 1.14.0

Нет прав для скачивания
C#:
using Facepunch;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Oxide.Core;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Cui;
using System;
using System.Collections.Generic;
using System.Globalization;
using UnityEngine;

namespace Oxide.Plugins;

[Info("Super PVx Info", "HunterZ", "1.14.0")]
[Description("Displays PvE/PvP/etc. status on player's HUD")]
public class SuperPVxInfo : RustPlugin
{
  #region Plugin Data

  // list of plugins whose PVP delay statuses are tracked
  private enum PvpDelayType
  {
    AbandonedBases,
    DynamicPvp,
    PlayerBasePvpZones,
    RaidableBases,
    TruePve
  }

  // primary status types tracked by this plugin
  public enum PVxType { PVE, PVP, PVPDelay, SafeZone }

  // PVP statuses that are managed via listening to player enter/exit hooks
  [Flags]
  private enum PvpBubbleTypes
  {
    None            = 0,
    ArmoredTrain    = 1 << 0, // Adem
    Caravan         = 1 << 1, // Adem
    CargoTrainEvent = 1 << 2, // Nikedemos - PVP only
    Convoy          = 1 << 3, // Adem
    DefendableHomes = 1 << 4, // KpucTaJl
    Shipwreck       = 1 << 5, // Adem
    Space           = 1 << 6, // Adem
    Sputnik         = 1 << 7, // Adem
    Triangulation   = 1 << 8  // KpucTaJl
  }

  private readonly Dictionary<PvpBubbleTypes, List<string>>
    _pvpBubbleEventHooks = new()
  {
    { PvpBubbleTypes.ArmoredTrain, new List<string>
      { nameof(OnPlayerEnterArmoredTrain), nameof(OnPlayerExitArmoredTrain),
        nameof(OnArmoredTrainStartMoving), nameof(OnArmoredTrainEventStop)
      }
    },
    { PvpBubbleTypes.Caravan, new List<string>
      { nameof(OnPlayerEnterCaravan), nameof(OnPlayerExitCaravan),
        nameof(OnCaravanStop)
      }
    },
    { PvpBubbleTypes.Convoy, new List<string>
      { nameof(OnPlayerEnterConvoy), nameof(OnPlayerExitConvoy),
        nameof(OnConvoyStop)
      }
    },
    { PvpBubbleTypes.DefendableHomes, new List<string>
      { nameof(OnPlayerEnterDefendableHomes),
        nameof(OnPlayerExitDefendableHomes)
      }
    },
    { PvpBubbleTypes.Shipwreck, new List<string>
      { nameof(OnPlayerEnterShipwreck), nameof(OnPlayerExitShipwreck),
        nameof(OnShipwreckStop)
      }
    },
    { PvpBubbleTypes.Space, new List<string>
      { nameof(OnPlayerEnterSpace), nameof(OnPlayerExitSpace),
        nameof(OnSpaceEventStop)
      }
    },
    { PvpBubbleTypes.Sputnik, new List<string>
      { nameof(OnPlayerEnterSputnik), nameof(OnPlayerExitSputnik)
      }
    },
    { PvpBubbleTypes.Triangulation, new List<string>
      { nameof(OnPlayerEnterTriangulation), nameof(OnPlayerExitTriangulation),
        nameof(OnTriangulationEnd)
      }
    }
  };

  // PVP events that are managed via listening to start/stop hooks that provide
  //  a PVP area definition
  private enum PvpLocationEventType
  {
    // KpucTaJl plugins
    Air,
    ArcticBase,
    FerryTerminal,
    GasStation,
    Harbor,
    Junkyard,
    PowerPlant,
    SatelliteDish,
    Supermarket,
    Water
  }

  private readonly Dictionary<PvpLocationEventType, List<string>>
    _pvpLocationEventHooks = new()
  { { PvpLocationEventType.Air, new List<string>
      { nameof(OnAirEventStart), nameof(OnAirEventEnd)
      }
    },
    { PvpLocationEventType.ArcticBase, new List<string>
      { nameof(OnArcticBaseEventStart), nameof(OnArcticBaseEventEnd)
      }
    },
    { PvpLocationEventType.FerryTerminal, new List<string>
      { nameof(OnFerryTerminalEventStart), nameof(OnFerryTerminalEventEnd)
      }
    },
    { PvpLocationEventType.GasStation, new List<string>
      { nameof(OnGasStationEventStart), nameof(OnGasStationEventEnd)
      }
    },
    { PvpLocationEventType.Harbor, new List<string>
      { nameof(OnHarborEventStart), nameof(OnHarborEventEnd)
      }
    },
    { PvpLocationEventType.Junkyard, new List<string>
      { nameof(OnJunkyardEventStart), nameof(OnJunkyardEventEnd)
      }
    },
    { PvpLocationEventType.PowerPlant, new List<string>
      { nameof(OnPowerPlantEventStart), nameof(OnPowerPlantEventEnd)
      }
    },
    { PvpLocationEventType.SatelliteDish, new List<string>
      { nameof(OnSatDishEventStart), nameof(OnSatDishEventEnd)
      }
    },
    { PvpLocationEventType.Supermarket, new List<string>
      { nameof(OnSupermarketEventStart), nameof(OnSupermarketEventEnd)
      }
    },
    { PvpLocationEventType.Water, new List<string>
      { nameof(OnWaterEventStart), nameof(OnWaterEventEnd)
      }
    }
  };


  [PluginReference] Plugin
    AbandonedBases, DynamicPVP, DangerousTreasures, PlayerBasePvpZones,
    PopupNotifications, RaidableBases, SimpleStatus, TruePVE, ZoneManager;

  private ConfigData _configData;

  // active TruePVE PVP delay timers by plugin name by player ID
  private readonly Dictionary<ulong, Dictionary<string, Timer>>
    _excludedPlayers = new();

  // NOTE: this is not to be used directly for sending messages, but rather for
  //  populating the default language dictionary, and for enumerating which
  //  messages exist
  private readonly Dictionary<string, string> _notifyMessages = new()
  {
    ["Unexpected Exit From Abandoned Or Raidable Base"] =
      "{0}Left Abandoned/Raidable Base Zone",
    ["Unexpected Exit From Dangerous Treasures Event"] =
      "{0}Left Dangerous Treasures Zone",
    ["Safe Zone Entry"] =
      "{0}Entering Safe Zone",
    ["Safe Zone Exit"] =
      "{0}Leaving Safe Zone",
    ["PVP Height Entry"] =
      "{0}WARNING: Entering Sky/Portal PVP Zone",
    ["PVP Height Exit"] =
      "{0}Leaving Sky/Portal PVP Zone",
    ["PVP Depth Entry"] =
      "{0}WARNING: Entering Train Tunnels PVP Zone",
    ["PVP Depth Exit"] =
      "{0}Leaving Train Tunnels PVP Zone",
    ["PVP Apartment Entry"] =
      "{0}WARNING: Entering Apartment PVP Zone",
    ["PVP Apartment Exit"] =
      "{0}Leaving Apartment PVP Zone",
    ["PVP Deep Sea Entry"] =
      "{0}WARNING: Entering Deep Sea PVP Zone",
    ["PVP Deep Sea Exit"] =
      "{0}Leaving Deep Sea PVP Zone"
  };

  private Timer _saveDataTimer;

  private StoredData _storedData;

  private const string UIName = "SuperPVxInfoUI";

  #endregion Plugin Data

  #region Utility Methods

  private bool IsEventEnabled(PvpBubbleTypes type) => type switch
  {
    PvpBubbleTypes.None
      => true, // return true to avoid a _pvpBubbleEventHooks dictionary lookup
    PvpBubbleTypes.ArmoredTrain
      => _configData?.PvpPlugins?.ArmoredTrain is true,
    PvpBubbleTypes.Caravan
      => _configData?.PvpPlugins?.Caravan is true,
    PvpBubbleTypes.CargoTrainEvent
      => true, // always enabled because it provides PVP specific hooks
    PvpBubbleTypes.Convoy
      => _configData?.PvpPlugins?.Convoy is true,
    PvpBubbleTypes.DefendableHomes
      => _configData?.PvpPlugins?.DefendableHomes is true,
    PvpBubbleTypes.Shipwreck
      => _configData?.PvpPlugins?.Shipwreck is true,
    PvpBubbleTypes.Space
      => _configData?.PvpPlugins?.Space is true,
    PvpBubbleTypes.Sputnik
      => _configData?.PvpPlugins?.Sputnik is true,
    PvpBubbleTypes.Triangulation
      => _configData?.PvpPlugins?.Triangulation is true
  };

  private bool IsEventEnabled(PvpLocationEventType type) => type switch
  {
    PvpLocationEventType.Air
      => _configData?.PvpPlugins?.AirEvent is true,
    PvpLocationEventType.ArcticBase
      => _configData?.PvpPlugins?.ArcticBaseEvent is true,
    PvpLocationEventType.FerryTerminal
      => _configData?.PvpPlugins?.FerryTerminalEvent is true,
    PvpLocationEventType.GasStation
      => _configData?.PvpPlugins?.GasStationEvent is true,
    PvpLocationEventType.Harbor
      => _configData?.PvpPlugins?.HarborEvent is true,
    PvpLocationEventType.Junkyard
      => _configData?.PvpPlugins?.JunkyardEvent is true,
    PvpLocationEventType.PowerPlant
      => _configData?.PvpPlugins?.PowerPlantEvent is true,
    PvpLocationEventType.SatelliteDish
      => _configData?.PvpPlugins?.SatelliteDishEvent is true,
    PvpLocationEventType.Supermarket
      => _configData?.PvpPlugins?.SupermarketEvent is true,
    PvpLocationEventType.Water
      => _configData?.PvpPlugins?.WaterEvent is true
  };

  private bool IsEventEnabled<T>(T type) => type switch
  {
    PvpBubbleTypes pvpBubbleTypes
      => IsEventEnabled(pvpBubbleTypes),
    PvpLocationEventType pvpLocationEventType
      => IsEventEnabled(pvpLocationEventType),
    _
      => true
  };

  private void InitEventHooks<T>(Dictionary<T, List<string>> hookDictionary)
    where T : Enum
  {
    // var nameOfT = typeof(T).Name;
    foreach (
      T eventType in Enum.GetValues(typeof(T)))
    {
      if (IsEventEnabled(eventType))
      {
        // Puts($"{nameOfT} event {eventType} support enabled");
        continue;
      }
      // Puts($"Disabling {nameOfT} event {eventType} support:");
      if (!hookDictionary.TryGetValue(eventType, out var eventHooks))
      {
        // PrintWarning($" No hooks defined for {nameOfT} event {eventType}");
        continue;
      }
      foreach (var eventHook in eventHooks)
      {
        Unsubscribe(eventHook);
        // Puts($" Unsubscribed hook {eventHook} for {nameOfT} event {eventType}");
      }
    }
  }

  private int ScrubEventData()
  {
    var removeCount = 0;
    foreach (PvpLocationEventType eventType
             in Enum.GetValues(typeof(PvpLocationEventType)))
    {
      if (IsEventEnabled(eventType)) continue;
      if (!_storedData.PvpEvents.Remove(eventType)) continue;
      ++removeCount;
    }
    return removeCount;
  }

  private static bool IsValidPlayer(BasePlayer player, bool checkConnected) =>
    player &&
    !player.IsNpc &&
    player.userID.IsSteamId() &&
    (!checkConnected || player.IsConnected);

  private static PlayerWatcher GetPlayerWatcher(BasePlayer player) =>
    IsValidPlayer(player, true) ? player.GetComponent<PlayerWatcher>() : null;

  private void SendCannedMessage(BasePlayer player, string key)
  {
    if (null == _configData ||
        !_configData.NotifySettings.Enabled.TryGetValue(
          key, out var enabled) ||
        !enabled)
    {
      return;
    }
    var message = lang.GetMessage(key, this, player.UserIDString);
    if (null == message) return;
    if (_configData.NotifySettings.ChatEnabled)
    {
      SendReply(
        player, string.Format(message, _configData.NotifySettings.ChatPrefix));
    }
    if (_configData.NotifySettings.PopupNotificationsEnabled &&
        null != PopupNotifications)
    {
      PopupNotifications.Call(
        "CreatePopupNotification",
        string.Format(
          message, _configData.NotifySettings.PopupNotificationsPrefix),
        player);
    }
  }

  private void ExcludePlayerRemove(ulong userid, string pluginName)
  {
    // get timers-by-plugin for player
    if (!_excludedPlayers.TryGetValue(userid, out var excludeTimers) ||
        null == excludeTimers)
    {
      return;
    }

    // remove timer entry if present, and destroy it if needed
    if (excludeTimers.Remove(pluginName, out var removedTimer))
    {
      DestroyTimer(removedTimer);
    }

    // abort if timers-by-plugin is still not empty for this player
    if (excludeTimers.Count > 0) return;

    // timers-by-plugin is empty - remove PVP delay status
    var player = BasePlayer.FindByID(userid);
    if (player)
    {
      SetPvpDelay(player, PvpDelayType.TruePve, false);
    }
  }

  private static void DestroyTimer(Timer t)
  {
    if (TimerValid(t)) t.Destroy();
  }

  private static bool TimerValid(Timer t) => t is { Destroyed: false };

  #endregion Utility Methods

  #region Oxide Methods

  protected override void LoadDefaultMessages() =>
    lang.RegisterMessages(_notifyMessages, this);

  private void Init()
  {
    LoadData();
    PlayerWatcher.AllowForceUpdate = _configData?.ForceUpdates is not false;
    PlayerWatcher.Instance = this;
    var deepSeaBounds = DeepSeaManager.DeepSeaBounds;
    PlayerWatcher.DeepSeaMin = deepSeaBounds.center - deepSeaBounds.extents;
    PlayerWatcher.DeepSeaMax = deepSeaBounds.center + deepSeaBounds.extents;
    PlayerWatcher.UpdateIntervalSeconds =
      _configData?.UpdateIntervalSeconds ?? 1.0f;
    if (!string.IsNullOrEmpty(_configData?.ToggleCommand))
    {
      AddCovalenceCommand(_configData?.ToggleCommand, nameof(ToggleUI));
    }
    // only listen to hooks from event plugins that are configured as PVP
    InitEventHooks(_pvpBubbleEventHooks);
    InitEventHooks(_pvpLocationEventHooks);
  }

  private void OnServerInitialized(bool isStartup)
  {
    if (Convert.ToBoolean(DynamicPVP?.Call("IsUsingExcludePlayer")))
    {
      Puts("OnServerInitialized(): Detected DynamicPVP support for TruePVE PVP delays");
      Unsubscribe(nameof(OnPlayerAddedToPVPDelay));
      Unsubscribe(nameof(OnPlayerRemovedFromPVPDelay));
    }
    if (Convert.ToBoolean(PlayerBasePvpZones?.Call("IsUsingExcludePlayer")))
    {
      Puts("OnServerInitialized(): Detected PlayerBasePvpZones support for TruePVE PVP delays");
      Unsubscribe(nameof(OnPlayerBasePvpDelayStart));
      Unsubscribe(nameof(OnPlayerBasePvpDelayStop));
    }

    if (null == ZoneManager ||
        ZoneManager.Version < new VersionNumber(3, 1, 10))
    {
      PrintWarning("ZoneManager is outdated or not running; this plugin may not work properly");
    }

    var toDict = Pool.Get<Dictionary<string, object>>();
    if (true != _configData?.SyncAssumptions)
    {
      Puts("TruePVE config query is disabled - skipping");
    }
    else if (true != TruePVE?.IsLoaded)
    {
      Puts("TruePVE not loaded - skipping config query");
    }
    else if (TruePVE?.Call("GetOptionsNoAlloc", toDict) is not true)
    {
      Puts("TruePVE config query failed");
    }
    else
    {
      Puts("Got TruePVE config:");
      _configData.PvpAboveHeight = (float)toDict["world_above"];
      Puts($"- PVP above height: {_configData.PvpAboveHeight}");
      _configData.PvpBelowHeight = (float)toDict["world_under"];
      Puts($"- PVP below height: {_configData.PvpBelowHeight}");
      _configData.PvpApartments = (bool)toDict["apartment_pvp"];
      Puts($"- PVP apartments: {_configData.PvpApartments}");
      _configData.PvpDeepSea = (bool)toDict["deepsea_pvp"];
      Puts($"- PVP deep sea: {_configData.PvpDeepSea}");
    }
    Pool.FreeUnmanaged(ref toDict);
    PlayerWatcher.PvpAboveHeight = _configData?.PvpAboveHeight ?? 1000.0f;
    PlayerWatcher.PvpBelowHeight = _configData?.PvpBelowHeight ?? -500.0f;
    PlayerWatcher.PvpApartments  = _configData?.PvpApartments  ?? false;
    PlayerWatcher.PvpDeepSea     = _configData?.PvpDeepSea     ?? false;

    var saveData = false;
    if (null != _storedData)
    {
      // sync with TruePVE mappings in case of reload
      TP_GetMappingsToStoredData();
      Puts($"OnServerInitialized(): mappings count after TruePVE sync: {_storedData.Mappings.Count}");

      // purge any mappings that Zone Manager doesn't recognize
      var activeZoneIds = Pool.Get<List<string>>();
      ZM_GetZoneIDsNoAlloc(activeZoneIds);
      Puts($"OnServerInitialized(): ZoneManager active zone count: {activeZoneIds.Count}");
      var deadZoneIds = Pool.Get<List<string>>();
      foreach (var zoneId in _storedData.Mappings.Keys)
      {
        if ("default" == zoneId || activeZoneIds.Contains(zoneId)) continue;
        deadZoneIds.Add(zoneId);
      }
      Pool.FreeUnmanaged(ref activeZoneIds);
      foreach (var deadZoneId in deadZoneIds)
      {
        _storedData.Mappings.Remove(deadZoneId);
      }
      if (deadZoneIds.Count > 0)
      {
        PrintWarning($"OnServerInitialized(): Purged {deadZoneIds.Count} unknown/obsolete zoneId(s) from database");
        saveData = true;
      }
      Pool.FreeUnmanaged(ref deadZoneIds);

      // migrate or clear old PVP events
      var oldEventsCount = _storedData.PendingPvpEvents.Count;
      if (oldEventsCount > 0)
      {
        if (isStartup)
        {
          // don't use events from a previous server run
          Puts($"OnServerInitialized(): Purging {oldEventsCount} obsolete saved PVP event record(s)");
          saveData = true;
        }
        else
        {
          // migrate events that haven't already been re-added by another
          //  plugin's OnServerInitialized()
          var migratedCount = 0;
          foreach (var (eventType, eventData) in _storedData.PendingPvpEvents)
          {
            if (!IsEventEnabled(eventType)) continue;
            if (!_storedData.PvpEvents.TryAdd(eventType, eventData)) continue;
            ++migratedCount;
          }
          Puts($"OnServerInitialized(): Migrated {migratedCount}/{oldEventsCount} saved PVP event record(s)");
          // schedule a save if we didn't just retain everything
          saveData |= migratedCount != _storedData.PendingPvpEvents.Count ||
                      migratedCount != _storedData.PvpEvents.Count;
        }
        // purge pending list regardless, as we're done with it
        _storedData.PendingPvpEvents.Clear();
      }

      // be paranoid and scrub any disabled event data, in case it somehow got
      //  added before we disabled hooks
      var removeCount = ScrubEventData();
      if (removeCount > 0)
      {
        Puts($"OnServerInitialized(): Purged {removeCount} saved PVP event record(s) for disabled events");
        saveData = true;
      }
    }
    if (saveData)
    {
      SaveData();
    }

    // setup SimpleStatus integration if appropriate
    SS_CreateStatuses();

    foreach (var player in BasePlayer.activePlayerList)
    {
      OnPlayerConnected(player);
    }

    if (true ==_configData?.NotifySettings.PopupNotificationsEnabled &&
        null == PopupNotifications)
    {
      PrintWarning("Notify via PopupNotifications enabled, but required plugin is missing");
    }
  }

  private void Unload()
  {
    // clear out any active TruePVE PVP delay timers
    foreach (var excludeTimers in _excludedPlayers.Values)
    {
      foreach (var excludeTimer in excludeTimers.Values)
      {
        DestroyTimer(excludeTimer);
      }
      excludeTimers.Clear();
    }
    _excludedPlayers.Clear();
    // destroy GUIs for all active players
    foreach (var player in BasePlayer.activePlayerList)
    {
      OnPlayerDisconnected(player);
    }
    PlayerWatcher.Instance = null;
    // if save timer active, force immediate write
    if (TimerValid(_saveDataTimer))
    {
      WriteData();
    }

    _saveDataTimer = null;
  }

  private void OnPlayerConnected(BasePlayer player)
  {
    if (!IsValidPlayer(player, true)) return;

    // abort if an active watcher is already attached
    // isActiveAndEnabled is needed because sometimes reloading the plugin
    //  causes it to catch a watcher that is still in the process of being
    //  destroyed
    var watcher = player.GetComponent<PlayerWatcher>();
    if (watcher && watcher.isActiveAndEnabled) return;

    watcher = player.gameObject.AddComponent<PlayerWatcher>();
    HashSet<PvpDelayType> pvpDelays = new();
    GetPlayerPvpDelays(pvpDelays, player.userID.Get());
    watcher.Init(
      IsPlayerInBase(player), pvpDelays, GetPlayerZoneType(player), player);
    watcher.StartWatching();
  }

  private void OnPlayerDisconnected(BasePlayer player)
  {
    if (IsValidPlayer(player, false))
    {
      player.gameObject.GetComponent<PlayerWatcher>()?.OnDestroy();
    }
    DestroyUI(player);
  }

  private void OnPlayerRespawned(BasePlayer player) =>
    NextTick(() =>
    {
      if (!IsValidPlayer(player, true)) return;
      var watcher = GetPlayerWatcher(player);
      if (!watcher) return;

      // check everything because the player could be anywhere now
      if (watcher.InBaseType != null) watcher.SetCheckBase(true);
      watcher.SetCheckPVxEvent(true);
      watcher.SetCheckZone(true);
      watcher.SetCheckPvpDelay(true);
      watcher.Force();
    });

  #endregion Oxide Methods

  #region TruePVE Integration

  #region TruePVE Utilities

  // use Interface.CallHook() instead of explicitly calling TruePVE, in case
  //  other PVE plugins implement support for this
  private void TP_GetMappingsToStoredData() =>
    Interface.CallHook("GetMappingsDictionaryNoAlloc", _storedData.Mappings);

  #endregion TruePVE Utilities

  #region TruePVE Hook Handlers

  // called when a plugin maps a Zone Manager zone to a TruePVE ruleset
  private void AddOrUpdateMapping(string zoneId, string ruleset)
  {
    if (null == _storedData ||
        string.IsNullOrEmpty(zoneId) || string.IsNullOrEmpty(ruleset))
    {
      return;
    }

    _storedData.Mappings[zoneId] = ruleset;
    SaveData();
  }

  // called when a plugin deletes a mapping
  private void RemoveMapping(string zoneId)
  {
    if (null == _storedData ||
        string.IsNullOrEmpty(zoneId) ||
        !_storedData.Mappings.Remove(zoneId))
    {
      return;
    }

    SaveData();
  }

  // called when a plugin does a mappings bulk remove
  private void RemoveMappings(List<string> keys, List<string> _ = null)
  {
    if (null == _storedData) return;

    var dataChanged = false;
    foreach (var key in keys)
    {
      if (!string.IsNullOrEmpty(key) && _storedData.Mappings.Remove(key))
      {
        dataChanged = true;
      }
    }
    if (dataChanged)
    {
      SaveData();
    }
  }

  // called when a plugin requests a timed rule exclusion (PVP exit delay)
  private void ExcludePlayer(ulong userid, float maxDelayLength, Plugin plugin)
  {
    if (null == plugin || !userid.IsSteamId()) return;
    var pluginName = plugin.Name;

    NextTick(() =>
    {
      // if delay is non-positive, just try to remove any existing record
      if (maxDelayLength <= 0.0f)
      {
        ExcludePlayerRemove(userid, pluginName);
        return;
      }

      // handle the case of updating an existing record
      var hasTimers =
        _excludedPlayers.TryGetValue(userid, out var excludeTimers);
      if (hasTimers &&
          excludeTimers?.TryGetValue(pluginName, out var excludeTimer) is true)
      {
        if (TimerValid(excludeTimer))
        {
          excludeTimer.Reset(maxDelayLength);
          return;
        }
        // pathological: remove defunct entry; we'll create a new one below
        excludeTimers.Remove(pluginName);
      }

      // handle the case that no timers have ever been recorded for player
      // (just create an empty timers-by-plugin sub-dictionary)
      if (null == excludeTimers)
      {
        excludeTimers = new Dictionary<string, Timer>();
        _excludedPlayers.Add(userid, excludeTimers);
      }

      // add a timer to the dictionary that simply removes itself on fire
      // existence of a dictionary entry then represents an active PVP delay
      excludeTimers.Add(pluginName, timer.Once(
        maxDelayLength, () => { ExcludePlayerRemove(userid, pluginName); }));

      var player = BasePlayer.FindByID(userid);
      if (player)
      {
        SetPvpDelay(player, PvpDelayType.TruePve, true);
      }
    });
  }

  #endregion TruePVE Hook Handlers

  #endregion TruePVE Integration

  #region ZoneManager Integration

  #region ZoneManager Utilities

  private void ZM_GetZoneIDsNoAlloc(List<string> list) =>
    ZoneManager?.Call("GetZoneIDsNoAlloc", list);

  private void ZM_GetPlayerZoneIDsNoAlloc(
    BasePlayer player, List<string> list) =>
    ZoneManager?.Call("GetPlayerZoneIDsNoAlloc", player, list);

  // if player is in a zone, return its type if possible, else return null
  public PVxType? GetPlayerZoneType(BasePlayer player)
  {
    if (null == _configData || !IsValidPlayer(player, true)) return null;

    // get current zone (if any)
    var (zoneId, zoneName) = GetSmallestZoneIdAndName(player);

    // go by zone name first
    if (!string.IsNullOrEmpty(zoneName))
    {
      foreach (var pveZoneName in _configData.PveZoneManagerNames)
      {
        if (zoneName.Contains(pveZoneName, CompareOptions.IgnoreCase))
        {
          return PVxType.PVE;
        }
      }
      foreach (var pvpZoneName in _configData.PvpZoneManagerNames)
      {
        if (zoneName.Contains(pvpZoneName, CompareOptions.IgnoreCase))
        {
          return PVxType.PVP;
        }
      }
    }

    if (!string.IsNullOrEmpty(zoneId))
    {
      // return PVP if this is a TruePVE/NextGenPVE exclusion zone
      // (needed for e.g. ZoneManagerAutoZones which doesn't put "PVP" in its
      //  zone names)
      if (IsExcludeZone(zoneId))
      {
        return PVxType.PVP;
      }

      // check Zone Manager flags
      if (ZM_GetZoneFlag(zoneId, "pvpgod"))
      {
        return ZM_GetZoneFlag(zoneId, "pvegod") ?
          // no-PvP *and* no-PvE => treat as safe zone
          PVxType.SafeZone :
          // no-PvP only => treat as PvE zone
          PVxType.PVE;
      }
    }

    // give up
    return null;
  }

  private (string, string) GetSmallestZoneIdAndName(BasePlayer player)
  {
    if (ZoneManager == null) return (null, null);
    var smallestRadius = float.MaxValue;
    string smallestId = null;
    string smallestName = null;
    var zoneIDs = Pool.Get<List<string>>();
    ZM_GetPlayerZoneIDsNoAlloc(player, zoneIDs);
    foreach (var zoneId in zoneIDs)
    {
      if (string.IsNullOrEmpty(zoneId)) continue;
      var zoneName = ZM_GetZoneName(zoneId);

      // get whichever of 2D zone size or radius is greater than zero
      var zoneMagnitude2D = ZM_GetZoneSize(zoneId).Magnitude2D();
      var zoneRadius = zoneMagnitude2D < float.Epsilon ?
        ZM_GetZoneRadius(zoneId) : zoneMagnitude2D;
      if (zoneRadius < float.Epsilon) continue;
      if (zoneRadius >= smallestRadius) continue;
      // zone is the smallest we've seen; record it as such
      smallestRadius = zoneRadius;
      smallestId = zoneId;
      smallestName = zoneName;
    }
    Pool.FreeUnmanaged(ref zoneIDs);

    return (smallestId, smallestName);
  }

  private bool ZM_GetZoneFlag(string zoneId, string zoneFlag) =>
    Convert.ToBoolean(ZoneManager?.Call("HasFlag", zoneId, zoneFlag));

  private string ZM_GetZoneName(string zoneId) =>
    Convert.ToString(ZoneManager?.Call("GetZoneName", zoneId));

  private float ZM_GetZoneRadius(string zoneId) =>
    Convert.ToSingle(ZoneManager?.Call("GetZoneRadius", zoneId));

  private Vector3 ZM_GetZoneSize(string zoneId) =>
    ZoneManager?.Call("GetZoneSize", zoneId) is Vector3 zoneSize ?
      zoneSize : Vector3.zero;

  private bool IsExcludeZone(string zoneId)
  {
    if (null == _storedData || null == _configData ||
        !_storedData.Mappings.TryGetValue(zoneId, out var ruleset))
    {
      return false;
    }

    foreach (var name in _configData.PveExclusionNames)
    {
      if (ruleset.Contains(name, CompareOptions.IgnoreCase)) return true;
    }

    return false;
  }

  // common logic for setting watcher's zone check request flag
  private void CheckZone(BasePlayer player) =>
    NextTick(() =>
    {
      if (!IsValidPlayer(player, true)) return;
      var watcher = GetPlayerWatcher(player);
      if (!watcher) return;
      watcher.SetCheckZone(true);
      watcher.Force();
    });

  #endregion ZoneManager Utilities

  #region ZoneManager Hook Handlers

  private void OnEnterZone(string zoneId, BasePlayer player) =>
    CheckZone(player);

  private void OnExitZone(string zoneId, BasePlayer player) =>
    // check if player is exiting from a smaller zone into a larger one
    CheckZone(player);

  #endregion ZoneManager Hook Handlers

  #endregion ZoneManager Integration

  #region PVP Plugin Integrations

  #region PVP Plugin Utilities

  // create or update an event record with the given data
  private void CreateOrUpdatePvpLocationEvent(
    PvpLocationEventType type, Vector3 location, float radius)
  {
    // Puts($"PVP location event create/update: type={type}, location={location}, radius={radius}");
    if (null == _storedData) return;
    _storedData.PvpEvents[type] = new PvpEventData(location, radius);
    SaveData();
  }

  // delete an event record with the given data, if any
  private void DeleteLocationPvpEvent(PvpLocationEventType type)
  {
    // Puts($"PVP location event delete: type={type}");
    if (null == _storedData || !_storedData.PvpEvents.Remove(type)) return;
    SaveData();
  }

  // check whether player is in any Raidable Base
  // TODO: add Abandoned Bases support?
  // this is expensive, and should only be called if state is totally unknown
  // (e.g. on connect)
  private PVxType? IsPlayerInBase(BasePlayer player)
  {
    // get list of all active Raidable Bases
    if (RaidableBases?.Call("GetAllEvents") is not List<(
          Vector3, string, int, bool allowPvp, string, float, float, float,
          ulong, BasePlayer, List<BasePlayer>, List<BasePlayer> intruders,
          HashSet<BaseEntity>, string, DateTime, DateTime, float, int)>
        rbEvents)
    {
      PrintError("IsPlayerInBase failure - Raidable Bases GetAllEvents API mismatch");
      return null;
    }

    // look for a base that the player is in
    foreach (
      var
        (_, _, _, allowPvp, _, _, _, _, _, _, _, intruders, _, _, _, _, _, _)
      in rbEvents)
    {
      if (intruders.Contains(player))
      {
        // base found; return its type
        return allowPvp ? PVxType.PVP : PVxType.PVE;
      }
    }

    // player not in any bases
    return null;
  }

  // check whether player is in a PVP event that only provides event
  // start/stop hooks with location, requiring active polling of player
  // position
  private bool IsPlayerInPvpEvent(BasePlayer player)
  {
    if (null == _storedData) return false;
    foreach (var eventData in _storedData.PvpEvents.Values)
    {
      if (Vector3.Distance(eventData.Location, player.transform.position) <=
          eventData.Radius)
      {
        return true;
      }
    }
    return false;
  }

  // check whether player is in an event that can be PvE or PvP
  // note that this is only useful for unexpected exits (e.g. respawning)
  //  because we don't get a PvP-versus-PvE indication from this
  private bool IsPlayerInPVxEvent(BasePlayer player) =>
    null != DangerousTreasures && Convert.ToBoolean(DangerousTreasures.Call(
      "EventTerritory", player.transform.position));

  // check if player has any PVP delays active
  // this should only be called when hook-reported states don't exist yet, or
  //  can't be relied upon for some reason
  private void GetPlayerPvpDelays(
    HashSet<PvpDelayType> pvpDelays, ulong playerID)
  {
    pvpDelays.Clear();

    if (AbandonedBases != null && Convert.ToBoolean(
          AbandonedBases.Call("HasPVPDelay", playerID)))
    {
      pvpDelays.Add(PvpDelayType.AbandonedBases);
    }

    if (DynamicPVP != null && Convert.ToBoolean(
          DynamicPVP.Call("IsPlayerInPVPDelay", playerID)))
    {
      pvpDelays.Add(PvpDelayType.DynamicPvp);
    }

    if (PlayerBasePvpZones != null && !string.IsNullOrEmpty(Convert.ToString(
          PlayerBasePvpZones.Call("OnPlayerBasePvpDelayQuery", playerID))))
    {
      pvpDelays.Add(PvpDelayType.PlayerBasePvpZones);
    }

    if (RaidableBases != null && Convert.ToBoolean(
          RaidableBases.Call("HasPVPDelay", playerID)))
    {
      pvpDelays.Add(PvpDelayType.RaidableBases);
    }

    if (_excludedPlayers.TryGetValue(playerID, out var excludeTimers) &&
        excludeTimers.Count > 0)
    {
      pvpDelays.Add(PvpDelayType.TruePve);
    }
  }

  // common logic for Abandoned/Raidable Base entry hooks
  private static void EnteredBase(
    BasePlayer player, PVxType baseType,
    Vector3 baseLocation, float baseRadius)
  {
    if (!IsValidPlayer(player, true)) return;
    var watcher = GetPlayerWatcher(player);
    if (!watcher) return;
    watcher.BaseLocation = baseLocation;
    watcher.BaseRadius = baseRadius;
    watcher.InBaseType = baseType;
    watcher.Force();
  }

  // common logic for Abandoned/Raidable Base exit hooks
  private static void ExitedBase(
    BasePlayer player, bool checkPlayerValid = true)
  {
    if (checkPlayerValid && !IsValidPlayer(player, true)) return;
    var watcher = GetPlayerWatcher(player);
    if (!watcher) return;
    watcher.SetCheckZone(true);
    watcher.InBaseType = null;
    watcher.Force();
  }

  // common logic for "in PVP bubble" hooks to set/clear a player's state
  private static void SetPvpBubble(
    BasePlayer player, PvpBubbleTypes type, bool state)
  {
    // PlayerWatcher.Instance?.Puts($"PVP bubble event update: player={player}, type={type}, state={state}");
    if (!IsValidPlayer(player, true)) return;
    var watcher = GetPlayerWatcher(player);
    if (!watcher) return;
    var oldState = watcher.InPvpBubbleTypes;
    if (state)
    {
      watcher.InPvpBubbleTypes |= type;
    }
    else
    {
      watcher.InPvpBubbleTypes &= ~type;
      if (PvpBubbleTypes.None == watcher.InPvpBubbleTypes)
      {
        watcher.SetCheckZone(true);
      }
    }
    if (watcher.InPvpBubbleTypes != oldState)
    {
      watcher.Force();
    }
  }

  // common logic for "in PVP bubble" hooks to clear all players' states
  private static void EndPvpBubble(PvpBubbleTypes type)
  {
    foreach (var player in BasePlayer.activePlayerList)
    {
      SetPvpBubble(player, type, false);
    }
  }

  // common logic for PVP Delay hooks
  private static void SetPvpDelay(
    BasePlayer player, PvpDelayType type, bool state)
  {
    if (!IsValidPlayer(player, true)) return;
    var watcher = GetPlayerWatcher(player);
    if (!watcher) return;
    if (state)
    {
      watcher.AddPvpDelay(type);
    }
    else if (watcher.ClearPvpDelay(type) <= 0)
    {
      watcher.SetCheckBase(true);
      watcher.SetCheckZone(true);
    }
    watcher.Force();
  }

  // common logic for PVx event hooks
  private static void SetPvxEvent(
    BasePlayer player, PVxType eventType, bool state)
  {
    if (!IsValidPlayer(player, true)) return;
    var watcher = GetPlayerWatcher(player);
    if (!watcher) return;
    if (state)
    {
      watcher.SetInPVxEventType(eventType);
    }
    else
    {
      watcher.SetCheckZone(true);
      watcher.SetInPVxEventType(null);
    }
    watcher.Force();
  }

  #endregion PVP Plugin Utilities

  #region Nivex Hook Handlers

  #region RaidableBases Hook Handlers

  // NOTE: Only need to include up to the last parameter that we actually use;
  //  Oxide/Carbon will call the closest-matching hook signature

  // temporary shim for people too lazy to upgrade :p
  private void OnPlayerEnteredRaidableBase(
    BasePlayer player, Vector3 location, bool allowPVP, int mode, string id,
    float _, float __, double loadTime, ulong ownerId, string baseName,
    DateTime spawnTime, DateTime despawnTime, float radius) =>
    OnPlayerEnteredRaidableBase(
      player, location, allowPVP, mode, id, _, __, (float)loadTime, ownerId,
      baseName, spawnTime, despawnTime, radius);

  private void OnPlayerEnteredRaidableBase(
    BasePlayer player, Vector3 location, bool allowPVP, int mode, string id,
    float _, float __, float loadTime, ulong ownerId, string baseName,
    DateTime spawnTime, DateTime despawnTime, float radius) =>
    NextTick(() => EnteredBase(
      player, allowPVP ? PVxType.PVP : PVxType.PVE, location, radius));

  private void OnPlayerExitedRaidableBase(BasePlayer player) =>
    NextTick(() => ExitedBase(player));

  // temporary shim for people too lazy to upgrade :p
  private void OnRaidableBaseEnded(
    Vector3 location, int mode, bool allowPvP, string id, float _,
    float __, double loadTime, ulong ownerId, BasePlayer owner,
    List<BasePlayer> raiders, List<BasePlayer> intruders,
    List<BaseEntity> entities, string baseName, DateTime spawnDateTime,
    DateTime despawnDateTime, float protectionRadius) =>
    OnRaidableBaseEnded(
      location, mode, allowPvP, id, _, __, (float)loadTime, ownerId, owner,
      raiders, intruders, entities, baseName, spawnDateTime, despawnDateTime,
      protectionRadius);

  private void OnRaidableBaseEnded(
    Vector3 location, int mode, bool allowPvP, string id, float _,
    float __, float loadTime, ulong ownerId, BasePlayer owner,
    List<BasePlayer> raiders, List<BasePlayer> intruders,
    List<BaseEntity> entities, string baseName, DateTime spawnDateTime,
    DateTime despawnDateTime, float protectionRadius) =>
    NextTick(() =>
    {
      // set zone check flag for any players in base radius
      foreach (var player in intruders)
      {
        if (!IsValidPlayer(player, true)) continue;
        // skip player if not within radius of raidable base
        if (Vector3.Distance(location, player.transform.position) >
            protectionRadius)
        {
          continue;
        }
        ExitedBase(player, false);
      }
    });

  #endregion RaidableBases Hook Handlers

  #region AbandonedBases Hook Handlers

  private void OnPlayerEnteredAbandonedBase(
    BasePlayer player, Vector3 eventPos, float radius, bool allowPVP) =>
    NextTick(() => EnteredBase(
      player, allowPVP ? PVxType.PVP : PVxType.PVE, eventPos, radius));

  private void OnPlayerExitAbandonedBase(BasePlayer player) =>
    NextTick(() => ExitedBase(player));

  private void OnAbandonedBaseEnded(
    Vector3 eventPos, float radius, bool allowPVP,
    List<BasePlayer> participants) =>
    NextTick(() =>
    {
      foreach (var player in participants)
      {
        if (!IsValidPlayer(player, true)) continue;
        if (Vector3.Distance(eventPos, player.transform.position) > radius)
        {
          continue;
        }
        ExitedBase(player, false);
      }
    });

  #endregion AbandonedBases Hook Handlers

  // NOTE: The same hook handlers can now be used for both Abandoned Bases and
  //  Raidable Bases

  private void OnPlayerPvpDelayStart(
    BasePlayer player, ulong userId, string pluginName) => NextTick(() =>
  {
    if (!Enum.TryParse(pluginName, out PvpDelayType type)) return;
    SetPvpDelay(player, type, true);
  });

  private void OnPlayerPvpDelayReset(
    BasePlayer player, ulong userId, string pluginName) =>
    OnPlayerPvpDelayStart(player, userId, pluginName);

  private void OnPlayerPvpDelayExpired(
    BasePlayer player, ulong userId, string pluginName) => NextTick(() =>
  {
    if (!Enum.TryParse(pluginName, out PvpDelayType type)) return;
    SetPvpDelay(player, type, false);
  });

  #region DangerousTreasures Hook Handlers

  private static void OnPlayerEnteredDangerousEvent(
    BasePlayer player, Vector3 eventPos, bool allowPVP) =>
    SetPvxEvent(player, allowPVP ? PVxType.PVP : PVxType.PVE, true);

  private static void OnPlayerExitedDangerousEvent(
    BasePlayer player, Vector3 eventPos, bool allowPVP) =>
    SetPvxEvent(player, allowPVP ? PVxType.PVP : PVxType.PVE, false);

  #endregion DangerousTreasures Hook Handlers

  #endregion Nivex Hook Handlers

  #region PVP Bubble Hook Handlers

  #region Adem Hook Handlers

  private void OnPlayerEnterArmoredTrain(BasePlayer player) =>
    NextTick(() => SetPvpBubble(
      player, PvpBubbleTypes.ArmoredTrain, true));

  private void OnPlayerExitArmoredTrain(BasePlayer player) =>
    NextTick(() => SetPvpBubble(
      player, PvpBubbleTypes.ArmoredTrain, false));

  private void OnArmoredTrainStartMoving(Vector3 position) =>
    NextTick(static () => EndPvpBubble(PvpBubbleTypes.ArmoredTrain));

  private void OnArmoredTrainEventStop() =>
    NextTick(static () => EndPvpBubble(PvpBubbleTypes.ArmoredTrain));

  private void OnPlayerEnterCaravan(BasePlayer player) =>
    NextTick(() => SetPvpBubble(player, PvpBubbleTypes.Caravan, true));

  private void OnPlayerExitCaravan(BasePlayer player) =>
    NextTick(() => SetPvpBubble(player, PvpBubbleTypes.Caravan, false));

  private void OnCaravanStop() =>
    NextTick(static () => EndPvpBubble(PvpBubbleTypes.Caravan));

  private void OnPlayerEnterConvoy(BasePlayer player) =>
    NextTick(() => SetPvpBubble(player, PvpBubbleTypes.Convoy, true));

  private void OnPlayerExitConvoy(BasePlayer player) =>
    NextTick(() => SetPvpBubble(player, PvpBubbleTypes.Convoy, false));

  private void OnConvoyStop() =>
    NextTick(static () => EndPvpBubble(PvpBubbleTypes.Convoy));

  private void OnPlayerEnterShipwreck(BasePlayer player) =>
    NextTick(() => SetPvpBubble(player, PvpBubbleTypes.Shipwreck, true));

  private void OnPlayerExitShipwreck(BasePlayer player) =>
    NextTick(() => SetPvpBubble(player, PvpBubbleTypes.Shipwreck, false));

  private void OnShipwreckStop() =>
    NextTick(static () => EndPvpBubble(PvpBubbleTypes.Shipwreck));

  private void OnPlayerEnterSpace(BasePlayer player) =>
    NextTick(() => SetPvpBubble(player, PvpBubbleTypes.Space, true));

  private void OnPlayerExitSpace(BasePlayer player) =>
    NextTick(() => SetPvpBubble(player, PvpBubbleTypes.Space, false));

  private void OnSpaceEventStop() =>
    NextTick(static () => EndPvpBubble(PvpBubbleTypes.Space));

  private void OnPlayerEnterSputnik(BasePlayer player) =>
    NextTick(() => SetPvpBubble(player, PvpBubbleTypes.Sputnik, true));

  private void OnPlayerExitSputnik(BasePlayer player) =>
    NextTick(() => SetPvpBubble(player, PvpBubbleTypes.Sputnik, false));

  #endregion Adem Hook Handlers

  #region KpucTaJl Hook Handlers

  private void OnPlayerEnterDefendableHomes(BasePlayer player) =>
    NextTick(() => SetPvpBubble(player, PvpBubbleTypes.DefendableHomes, true));

  private void OnPlayerExitDefendableHomes(BasePlayer player) =>
    NextTick(() => SetPvpBubble(
      player, PvpBubbleTypes.DefendableHomes, false));

  private void OnPlayerEnterTriangulation(BasePlayer player) =>
    NextTick(() => SetPvpBubble(player, PvpBubbleTypes.Triangulation, true));

  private void OnPlayerExitTriangulation(BasePlayer player) =>
    NextTick(() => SetPvpBubble(player, PvpBubbleTypes.Triangulation, false));

  private void OnTriangulationEnd() =>
    NextTick(static () => EndPvpBubble(PvpBubbleTypes.Triangulation));

  #endregion KpucTaJl Hook Handlers

  #region Nikedemos Hook Handlers

  private void OnPlayerEnterPVPBubble(
    TrainEngine trainEngine, BasePlayer player) =>
    NextTick(() => SetPvpBubble(
      player, PvpBubbleTypes.CargoTrainEvent, true));

  private void OnPlayerExitPVPBubble(
    TrainEngine trainEngine, BasePlayer player) =>
    NextTick(() => SetPvpBubble(
      player, PvpBubbleTypes.CargoTrainEvent, false));

  private void OnTrainEventEnded(TrainEngine trainEngine) =>
    NextTick(static () => EndPvpBubble(PvpBubbleTypes.CargoTrainEvent));

  #endregion Nikedemos Handlers

  #endregion PVP Bubble Hook Handlers

  #region PlayerBasePvpZones Hook Handlers

  private void OnPlayerBasePvpDelayStart(ulong playerId, string zoneId)
  {
    NextTick(() =>
    {
      var player = BasePlayer.FindByID(playerId);
      SetPvpDelay(player, PvpDelayType.PlayerBasePvpZones, true);
    });
  }

  private void OnPlayerBasePvpDelayStop(ulong playerId, string zoneId)
  {
    NextTick(() =>
    {
      var player = BasePlayer.FindByID(playerId);
      SetPvpDelay(player, PvpDelayType.PlayerBasePvpZones, false);
    });
  }

  #endregion PlayerBasePvpZones Hook Handlers

  #region DynamicPVP Hook Handlers

  private void OnPlayerAddedToPVPDelay(
    ulong playerId, string zoneId, float pvpDelayTime)
  {
    NextTick(() =>
    {
      var player = BasePlayer.FindByID(playerId);
      SetPvpDelay(player, PvpDelayType.DynamicPvp, true);
    });
  }

  private void OnPlayerRemovedFromPVPDelay(ulong playerId, string zoneId)
  {
    NextTick(() =>
    {
      var player = BasePlayer.FindByID(playerId);
      SetPvpDelay(player, PvpDelayType.DynamicPvp, false);
    });
  }

  #endregion DynamicPVP Hook Handlers

  #region PVP Location Hook Handlers

  #region KpucTaJl Hook Handlers

  private void OnAirEventStart(
    HashSet<BaseEntity> entities, Vector3 pos, float radius) =>
    NextTick(() => CreateOrUpdatePvpLocationEvent(
      PvpLocationEventType.Air, pos, radius));

  private void OnAirEventEnd() =>
    NextTick(() => DeleteLocationPvpEvent(
      PvpLocationEventType.Air));

  private void OnArcticBaseEventStart(Vector3 pos, float radius) =>
    NextTick(() => CreateOrUpdatePvpLocationEvent(
      PvpLocationEventType.ArcticBase, pos, radius));

  private void OnArcticBaseEventEnd() =>
    NextTick(() => DeleteLocationPvpEvent(
      PvpLocationEventType.ArcticBase));

  private void OnFerryTerminalEventStart(Vector3 pos, float radius) =>
    NextTick(() => CreateOrUpdatePvpLocationEvent(
      PvpLocationEventType.FerryTerminal, pos, radius));

  private void OnFerryTerminalEventEnd() =>
    NextTick(() => DeleteLocationPvpEvent(
      PvpLocationEventType.FerryTerminal));

  private void OnGasStationEventStart(Vector3 pos, float radius) =>
    NextTick(() => CreateOrUpdatePvpLocationEvent(
      PvpLocationEventType.GasStation, pos, radius));

  private void OnGasStationEventEnd() =>
    NextTick(() => DeleteLocationPvpEvent(
      PvpLocationEventType.GasStation));

  private void OnHarborEventStart(Vector3 pos, float radius) =>
    NextTick(() => CreateOrUpdatePvpLocationEvent(
      PvpLocationEventType.Harbor, pos, radius));

  private void OnHarborEventEnd() =>
    NextTick(() => DeleteLocationPvpEvent(
      PvpLocationEventType.Harbor));

  private void OnJunkyardEventStart(Vector3 pos, float radius) =>
    NextTick(() => CreateOrUpdatePvpLocationEvent(
      PvpLocationEventType.Junkyard, pos, radius));

  private void OnJunkyardEventEnd() =>
    NextTick(() => DeleteLocationPvpEvent(
      PvpLocationEventType.Junkyard));

  private void OnPowerPlantEventStart(Vector3 pos, float radius) =>
    NextTick(() => CreateOrUpdatePvpLocationEvent(
      PvpLocationEventType.PowerPlant, pos, radius));

  private void OnPowerPlantEventEnd() =>
    NextTick(() => DeleteLocationPvpEvent(
      PvpLocationEventType.PowerPlant));

  private void OnSatDishEventStart(Vector3 pos, float radius) =>
    NextTick(() => CreateOrUpdatePvpLocationEvent(
      PvpLocationEventType.SatelliteDish, pos, radius));

  private void OnSatDishEventEnd() =>
    NextTick(() => DeleteLocationPvpEvent(
      PvpLocationEventType.SatelliteDish));

  private void OnSupermarketEventStart(Vector3 pos, float radius) =>
    NextTick(() => CreateOrUpdatePvpLocationEvent(
      PvpLocationEventType.Supermarket, pos, radius));

  private void OnSupermarketEventEnd() =>
    NextTick(() => DeleteLocationPvpEvent(
      PvpLocationEventType.Supermarket));

  private void OnWaterEventStart(
    HashSet<BaseEntity> entities, Vector3 pos, float radius) =>
    NextTick(() => CreateOrUpdatePvpLocationEvent(
      PvpLocationEventType.Water, pos, radius));

  private void OnWaterEventEnd() =>
    NextTick(() => DeleteLocationP
// ... [Code preview truncated due to length. Full source in attached file] ...

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