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

FREE DynamicPVP : 4.7.1 4.7.1

Нет прав для скачивания
C#:
// Все права принадлежат дискорд сообществу https://discord.gg/VgNHPpNrz6
 //Requires: ZoneManager
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using Facepunch;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Oxide.Core;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core.Plugins;
using UnityEngine;
namespace Oxide.Plugins {
    [Info("Dynamic PVP", "HunterZ/CatMeat/Arainrr", "4.7.1", ResourceId = 2728)]
    [Description("Creates temporary PvP zones on certain actions/events")]
    public class DynamicPVP : RustPlugin {
#region Fields
        [PluginReference]
        private readonly Plugin Backpacks, BotReSpawn, TruePVE, ZoneManager;
        private const string PermissionAdmin = "dynamicpvp.admin";
        private const string PrefabLargeOilRig = "assets/bundled/prefabs/autospawn/monument/offshore/oilrig_1.prefab";
        private const string PrefabOilRig = "assets/bundled/prefabs/autospawn/monument/offshore/oilrig_2.prefab";
        private const string PrefabSphere = "assets/prefabs/visualization/sphere.prefab";
        private const string ZoneName = "DynamicPVP";
        private readonly Dictionary<string, Timer> _eventTimers = new();
        private readonly Dictionary<ulong, LeftZone> _pvpDelays = new();
        //ID -> EventName
        private readonly Dictionary<string, string> _activeDynamicZones = new();
        // plugin integration zone tracking - used for managing hook subscriptions
        //  and for faster lookups
        private enum PluginZoneCategory {
            BackpacksForce, BackpacksPrevent, LootDefender, RestoreUponDeath
        }
        private readonly Dictionary<PluginZoneCategory, HashSet<string>> _activePluginZones = new();
        private Vector3 _oilRigPosition = Vector3.zero;
        private Vector3 _largeOilRigPosition = Vector3.zero;
        private bool _useExcludePlayer;
        private bool _brokenTunnels;
        private bool _dataChanged;
        private Coroutine _createEventsCoroutine;
        private readonly YieldInstruction _fastYield = null;
        private readonly YieldInstruction _throttleYield = CoroutineEx.waitForSeconds(0.1f);
        private readonly YieldInstruction _pauseYield = CoroutineEx.waitForSeconds(0.5f);
        private float _targetFps = -1.0f;
        public static DynamicPVP Instance {
            get;
            private set;
        }
        private sealed class LeftZone : Pool.IPooled {
            public string zoneId;
            public string eventName;
            public Timer zoneTimer;
            private void Reset() {
                zoneId = null;
                eventName = null;
                zoneTimer?.Destroy();
                zoneTimer = null;
            }
            public void EnterPool() => Reset();
            public void LeavePool() => Reset();
        }
        [Flags]
        [JsonConverter(typeof(StringEnumConverter))]
        private enum PvpDelayTypes {
            None = 0, ZonePlayersCanDamageDelayedPlayers = 1, DelayedPlayersCanDamageZonePlayers = 1 << 1, DelayedPlayersCanDamageDelayedPlayers = 1 << 2
        }
        private enum GeneralEventType {
            Bradley, Helicopter, SupplyDrop, SupplySignal, CargoShip, HackableCrate, ExcavatorIgnition
        }
        [Flags]
        private enum HookCheckReasons {
            None = 0, DelayAdded = 1 << 0, DelayRemoved = 1 << 1, ZoneAdded = 1 << 2, ZoneRemoved = 1 << 3
        }
        private enum HookCategory {
            Command, PluginBackpacksForce, PluginBackpacksPrevent, PluginLootDefender, PluginRestoreUponDeath, PvpDelay, Zone,
        }
        // hook names by hook category
        private readonly Dictionary<HookCategory, List<string>> _hooksByCategory = new() {
            {
                HookCategory.Command, new List<string> {
                    nameof(OnPlayerCommand), nameof(OnServerCommand)
                }
            }
            , {
                HookCategory.PluginBackpacksForce, new List<string> {
                    nameof(OnPlayerDeath)
                }
            }
            , {
                HookCategory.PluginBackpacksPrevent, new List<string> {
                    nameof(CanDropBackpack)
                }
            }
            , {
                HookCategory.PluginLootDefender, new List<string> {
                    nameof(OnLootLockedEntity)
                }
            }
            , {
                HookCategory.PluginRestoreUponDeath, new List<string> {
                    nameof(OnRestoreUponDeath)
                }
            }
            , {
                HookCategory.PvpDelay, new List<string> {
                    nameof(CanEntityTakeDamage)
                }
            }
            , {
                HookCategory.Zone, new List<string> {
                    nameof(OnEnterZone), nameof(OnExitZone)
                }
            }
        }
        ;
        // current hook subscription state by hook category
        private readonly Dictionary<HookCategory, bool> _subscriptionsByCategory = new();
        private readonly Collider[] _colliderBuffer = new Collider[8];
        private enum MonumentEventType {
            Default, Custom, TunnelEntrance, TunnelLink, TunnelSection, UnderwaterLabs
        }
        private readonly Dictionary<string, OriginalMonumentGeometry> _originalMonumentGeometries = new();
#endregion Fields
#region Oxide Hooks
        private void Init() {
            foreach (PluginZoneCategory pzCat in Enum.GetValues(typeof(PluginZoneCategory))) {
                _activePluginZones[pzCat] = new HashSet<string>();
            }
            _brokenTunnels = false;
            Instance = this;
            LoadData();
            permission.RegisterPermission(PermissionAdmin, this);
            AddCovalenceCommand(configData.Chat.Command, nameof(CmdDynamicPVP));
            Unsubscribe(nameof(CanDropBackpack));
            Unsubscribe(nameof(CanEntityTakeDamage));
            Unsubscribe(nameof(OnCargoPlaneSignaled));
            Unsubscribe(nameof(OnCargoShipEgress));
            Unsubscribe(nameof(OnCargoShipHarborApproach));
            Unsubscribe(nameof(OnCargoShipHarborArrived));
            Unsubscribe(nameof(OnCargoShipHarborLeave));
            Unsubscribe(nameof(OnCrateHack));
            Unsubscribe(nameof(OnCrateHackEnd));
            Unsubscribe(nameof(OnDieselEngineToggled));
            Unsubscribe(nameof(OnEnterZone));
            Unsubscribe(nameof(OnEntityDeath));
            Unsubscribe(nameof(OnEntityKill));
            Unsubscribe(nameof(OnEntitySpawned));
            Unsubscribe(nameof(OnExitZone));
            Unsubscribe(nameof(OnLootEntity));
            Unsubscribe(nameof(OnLootLockedEntity));
            Unsubscribe(nameof(OnPlayerCommand));
            Unsubscribe(nameof(OnPlayerDeath));
            Unsubscribe(nameof(OnRestoreUponDeath));
            Unsubscribe(nameof(OnServerCommand));
            Unsubscribe(nameof(OnSupplyDropLanded));
            foreach (var (category, _) in _hooksByCategory) {
                _subscriptionsByCategory[category] = false;
            }
            if (configData.Global.LogToFile) {
                _debugStringBuilder = Pool.Get<StringBuilder>();
            }
            // setup new TruePVE "ExcludePlayer" support
            _useExcludePlayer = configData.Global.UseExcludePlayer;
            // if ExcludePlayer is disabled in config but is supported...if (!_useExcludePlayer &&
            null != TruePVE && TruePVE.Version >= new VersionNumber(2, 2, 3)) {
                //...and all PVP delays are enabled, auto-enable internally and warn
                if ((PvpDelayTypes.ZonePlayersCanDamageDelayedPlayers | PvpDelayTypes.DelayedPlayersCanDamageZonePlayers | PvpDelayTypes.DelayedPlayersCanDamageDelayedPlayers) == configData.Global.PvpDelayFlags) {
                    _useExcludePlayer = true;
                    Puts("All PVP delay flags active and TruePVE 2.2.3+ detected, so TruePVE PVP delays will be used for performance and cross-plugin support; please consider enabling TruePVE PVP Delay API in the config file to skip this check");
                }
                // else just nag, since settings are not compatible
                else {
                    Puts("Some/all PVP delay flags NOT active, but TruePVE 2.2.3+ detected; please consider switching to TruePVE PVP Delay API in the config file for performance and cross-plugin support");
                }
            }
            // else ExcludePlayer is already enabled, or TruePVE 2.2.3+ not running
        }
        private void OnServerInitialized() {
            DeleteOldDynamicZones();
            if (configData.GeneralEvents.ExcavatorIgnition.Enabled) {
                Subscribe(nameof(OnDieselEngineToggled));
            }
            if (configData.GeneralEvents.PatrolHelicopter.Enabled || configData.GeneralEvents.BradleyApc.Enabled) {
                Subscribe(nameof(OnEntityDeath));
            }
            if (configData.GeneralEvents.SupplySignal.Enabled || configData.GeneralEvents.TimedSupply.Enabled) {
                Subscribe(nameof(OnCargoPlaneSignaled));
                // this is now subscribed regardless of start on spawn-vs-landing, as we
                //  need to tether the zone to the drop on landing in both cases
                Subscribe(nameof(OnSupplyDropLanded));
            }
            if (configData.GeneralEvents.HackableCrate.Enabled && configData.GeneralEvents.HackableCrate.TimerStartWhenUnlocked) {
                Subscribe(nameof(OnCrateHackEnd));
            }
            if ((configData.GeneralEvents.TimedSupply.Enabled && configData.GeneralEvents.TimedSupply.TimerStartWhenLooted) || (configData.GeneralEvents.SupplySignal.Enabled && configData.GeneralEvents.SupplySignal.TimerStartWhenLooted) || (configData.GeneralEvents.HackableCrate.Enabled && configData.GeneralEvents.HackableCrate.TimerStartWhenLooted)) {
                Subscribe(nameof(OnLootEntity));
            }
            if (configData.GeneralEvents.HackableCrate.Enabled && !configData.GeneralEvents.HackableCrate.StartWhenSpawned) {
                Subscribe(nameof(OnCrateHack));
            }
            if ((configData.GeneralEvents.TimedSupply.Enabled && configData.GeneralEvents.TimedSupply.StartWhenSpawned) || (configData.GeneralEvents.SupplySignal.Enabled && configData.GeneralEvents.SupplySignal.StartWhenSpawned) || (configData.GeneralEvents.HackableCrate.Enabled && configData.GeneralEvents.HackableCrate.StartWhenSpawned)) {
                Subscribe(nameof(OnEntitySpawned));
            }
            if ((configData.GeneralEvents.TimedSupply.Enabled && configData.GeneralEvents.TimedSupply.StopWhenKilled) || (configData.GeneralEvents.SupplySignal.Enabled && configData.GeneralEvents.SupplySignal.StopWhenKilled) || (configData.GeneralEvents.HackableCrate.Enabled && configData.GeneralEvents.HackableCrate.StopWhenKilled)) {
                Subscribe(nameof(OnEntityKill));
            }
            if (configData.GeneralEvents.CargoShip.Enabled) {
                Subscribe(nameof(OnCargoShipEgress));
                Subscribe(nameof(OnCargoShipHarborApproach));
                Subscribe(nameof(OnCargoShipHarborArrived));
                Subscribe(nameof(OnCargoShipHarborLeave));
                Subscribe(nameof(OnEntityKill));
                Subscribe(nameof(OnEntitySpawned));
            }
            NextTick(() => {
                _createEventsCoroutine = ServerMgr.Instance.StartCoroutine(CreateEvents());
            }
            );
        }
        private void Unload() {
            if (_createEventsCoroutine != null) {
                ServerMgr.Instance.StopCoroutine(_createEventsCoroutine);
            }
            if (_activeDynamicZones.Count > 0) {
                PrintDebug($"Deleting {_activeDynamicZones.Count} active zones");
                foreach (var entry in _activeDynamicZones.ToArray()) {
                    DeleteDynamicZone(entry.Key);
                }
            }
            var leftZones = _pvpDelays.Values.ToArray();
            for (var i = leftZones.Length - 1; i >= 0; i--) {
                var value = leftZones[i];
                Pool.Free(ref value);
            }
            var spheres = _zoneSpheres.Values.ToArray();
            for (var i = spheres.Length - 1; i >= 0; i--) {
                var sphereEntities = spheres[i];
                foreach (var sphereEntity in sphereEntities) {
                    if (sphereEntity && !sphereEntity.IsDestroyed) {
                        sphereEntity.KillMessage();
                    }
                }
                Pool.FreeUnmanaged(ref sphereEntities);
            }
            SaveData();
            SaveDebug();
            Pool.Directory.TryRemove(typeof(LeftZone), out _);
            Instance = null;
            if (null == _debugStringBuilder) return;
            Pool.FreeUnmanaged(ref _debugStringBuilder);
            _debugStringBuilder = null;
            _originalMonumentGeometries.Clear();
            _activePluginZones.Clear();
        }
        private void OnServerSave() => timer.Once(UnityEngine.Random.Range(0f, 60f), () => {
            SaveDebug();
            if (!_dataChanged) return;
            SaveData();
            _dataChanged = false;
        }
        );
        private void OnPlayerRespawned(BasePlayer player) {
            if (!player || !player.userID.IsSteamId()) {
                PrintDebug("OnPlayerRespawned(): Ignoring respawn of null/NPC player");
                return;
            }
            TryRemovePVPDelay(player);
        }
#endregion Oxide Hooks
#region Methods
        private void TryRemoveEventTimer(string zoneId) {
            if (_eventTimers.Remove(zoneId, out var value)) {
                value?.Destroy();
            }
        }
        private LeftZone GetOrAddPVPDelay(BasePlayer player, string zoneId, string eventName, BaseEvent baseEvent) {
            PrintDebug($"Adding {player.displayName} to pvp delay");
            var added = false;
            if (_pvpDelays.TryGetValue(player.userID, out var leftZone)) {
                leftZone.zoneTimer?.Destroy();
            }
            else {
                added = true;
                leftZone = Pool.Get<LeftZone>();
                _pvpDelays.Add(player.userID, leftZone);
            }
            leftZone.zoneId = zoneId;
            leftZone.eventName = eventName;
            if (added) {
                CheckHooks(HookCheckReasons.DelayAdded, baseEvent);
            }
            return leftZone;
        }
        private bool TryRemovePVPDelay(BasePlayer player) {
            PrintDebug($"Removing {player.displayName} from pvp delay");
            var playerId = player.userID.Get();
            if (!_pvpDelays.Remove(playerId, out var leftZone)) return false;
            Interface.CallHook("OnPlayerRemovedFromPVPDelay", playerId, leftZone.zoneId, player);
            CheckHooks(HookCheckReasons.DelayRemoved, null);
            // baseEvent not needed
            Pool.Free(ref leftZone);
            return true;
        }
        private bool CheckEntityOwner(BaseEntity baseEntity) {
            if (!configData.Global.CheckEntityOwner || !baseEntity.OwnerID.IsSteamId() ||  // HeliSignals and BradleyDrops exception
            baseEntity.skinID != 0) {
                return true;
            }
            PrintDebug($"Skipping event creation because baseEntity={baseEntity} is owned by player={baseEntity.OwnerID}");
            return false;
        }
        private bool CanCreateDynamicPVP(string eventName, BaseEntity entity) {
            if (Interface.CallHook("OnCreateDynamicPVP", eventName, entity) == null) {
                return true;
            }
            PrintDebug($"Skipping event creation for eventName={eventName} due to OnCreateDynamicPVP hook result");
            return false;
        }
        private bool HasCommands() {
            // track which events we've checked, to avoid redundant calls to
            //  GetBaseEvent(); note that use of pool API means we need to free this
            //  on every return
            var checkedEvents = Pool.Get<HashSet<string>>();
            // check for command-containing zones referenced by PVP delays, which
            //  either work when PVP delayed, or are an active zone
            // HZ: I guess this is really trying to catch the corner case of players
            //  in PVP delay because a zone expired?
            foreach (var leftZone in _pvpDelays.Values) {
                var baseEvent = GetBaseEvent(leftZone.eventName);
                if (baseEvent == null || baseEvent.CommandList.Count <= 0) {
                    continue;
                }
                if (baseEvent.CommandWorksForPVPDelay || _activeDynamicZones.ContainsValue(leftZone.eventName)) {
                    Pool.FreeUnmanaged(ref checkedEvents);
                    return true;
                }
                checkedEvents.Add(leftZone.eventName);
            }
            foreach (var eventName in _activeDynamicZones.Values) {
                // optimization: skip if we've already checked this in the other loop
                if (checkedEvents.Contains(eventName)) {
                    continue;
                }
                var baseEvent = GetBaseEvent(eventName);
                if (null == baseEvent || baseEvent.CommandList.Count <= 0) continue;
                Pool.FreeUnmanaged(ref checkedEvents);
                return true;
            }
            Pool.FreeUnmanaged(ref checkedEvents);
            return false;
        }
        /// toggle dynamic hook subscription(s) based on need
        private void UpdateDynamicHook(bool needSubscription, HookCategory hookCategory) {
            // abort if subscription tracking undefined, or subscription need already
            //  met, or hooks not defined
            if (!_subscriptionsByCategory.TryGetValue(hookCategory, out var haveSubscription) || needSubscription == haveSubscription || !_hooksByCategory.TryGetValue(hookCategory, out var hooks)) {
                return;
            }
            // (un)subscribe per subscription need
            foreach (var hook in hooks) {
                if (needSubscription) {
                    Subscribe(hook);
                }
                else {
                    Unsubscribe(hook);
                }
            }
            // record that we've achieved desired subscription state
            _subscriptionsByCategory[hookCategory] = needSubscription;
        }
        private void CheckCommandHooks(bool added) {
            // optimization: avoid calling HasCommands() if added + already subscribed
            if (added && _subscriptionsByCategory.TryGetValue(HookCategory.Command, out var subscribed) && subscribed) {
                return;
            }
            UpdateDynamicHook(HasCommands(), HookCategory.Command);
        }
        /// update plugin integration tracking/subscriptions as appropriate
        private void CheckPluginHooks(BaseEvent baseEvent) {
            // this currently only supports checks when baseEvent is provided
            if (null == baseEvent) return;
            foreach (PluginZoneCategory pzCat in Enum.GetValues(typeof(PluginZoneCategory))) {
                if (HasPluginZoneCategory(baseEvent, pzCat)) {
                    UpdateDynamicHook(_activePluginZones[pzCat].Count > 0, ToHookCategory(pzCat));
                }
            }
        }
        private void CheckPvpDelayHooks() => UpdateDynamicHook(!_useExcludePlayer && _pvpDelays.Count > 0, HookCategory.PvpDelay);
        private void CheckZoneHooks() => UpdateDynamicHook(_activeDynamicZones.Count > 0, HookCategory.Zone);
        /// check whether hook subscription changes are warranted
        //
        // baseEvent is used as an optimization to only check plugin integration
        //  hook subscriptions when relevant zones are added/removed
        private void CheckHooks(HookCheckReasons reasons, BaseEvent baseEvent) {
            // update command hooks based on PVP delay or zone changes
            if (reasons.HasFlag(HookCheckReasons.DelayAdded) || reasons.HasFlag(HookCheckReasons.ZoneAdded)) {
                CheckCommandHooks(true);
            }
            else if (reasons.HasFlag(HookCheckReasons.DelayRemoved) || reasons.HasFlag(HookCheckReasons.ZoneRemoved)) {
                CheckCommandHooks(false);
            }
            // update PVP delay hooks based on PVP delay changes
            if (reasons.HasFlag(HookCheckReasons.DelayAdded) || reasons.HasFlag(HookCheckReasons.DelayRemoved)) {
                CheckPvpDelayHooks();
            }
            // update plugin and zone hooks based on zone changes
            if (reasons.HasFlag(HookCheckReasons.ZoneAdded) || reasons.HasFlag(HookCheckReasons.ZoneRemoved)) {
                CheckPluginHooks(baseEvent);
                CheckZoneHooks();
            }
        }
        private BaseEvent GetBaseEvent(string eventName) {
            if (string.IsNullOrEmpty(eventName)) {
                throw new ArgumentNullException(nameof(eventName));
            }
            if (Interface.CallHook("OnGetBaseEvent", eventName) is BaseEvent externalEvent) {
                return externalEvent;
            }
            if (Enum.IsDefined(typeof(GeneralEventType), eventName) && Enum.TryParse(eventName, true, out GeneralEventType generalEventType)) {
                switch (generalEventType) {
                    case GeneralEventType.Bradley : return configData.GeneralEvents.BradleyApc;
                    case GeneralEventType.HackableCrate : return configData.GeneralEvents.HackableCrate;
                    case GeneralEventType.Helicopter : return configData.GeneralEvents.PatrolHelicopter;
                    case GeneralEventType.SupplyDrop : return configData.GeneralEvents.TimedSupply;
                    case GeneralEventType.SupplySignal : return configData.GeneralEvents.SupplySignal;
                    case GeneralEventType.ExcavatorIgnition : return configData.GeneralEvents.ExcavatorIgnition;
                    case GeneralEventType.CargoShip : return configData.GeneralEvents.CargoShip;
                    default : PrintDebug($"ERROR: Missing BaseEvent lookup for generalEventType={generalEventType} for eventName={eventName}.", DebugLevel.Error);
                    return null;
                }
            }
            if (storedData.autoEvents.TryGetValue(eventName, out var autoEvent)) {
                return autoEvent;
            }
            if (storedData.timedEvents.TryGetValue(eventName, out var timedEvent)) {
                return timedEvent;
            }
            if (configData.MonumentEvents.TryGetValue(eventName, out var monumentEvent)) {
                return monumentEvent;
            }
            PrintDebug($"ERROR: Failed to get base event settings for {eventName}", DebugLevel.Error);
            return null;
        }
#endregion Methods
#region Events
#region Startup
        // 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(bool pause = false)
        {
            // perform one-time caching of target FPS
            if (_targetFps <= 0) _targetFps = Mathf.Min(ConVar.FPS.limit, 30);
            return pause && configData.Global.DebugEnabled ? _pauseYield : Performance.report.frameRate >= _targetFps ? _fastYield : _throttleYield;
        }
        // coroutine to orchestrate creation of all relevant events on startup
        private IEnumerator CreateEvents() {
            var startTime = DateTime.UtcNow;
            Puts("Creating General Events");
            yield return CreateGeneralEvents();
            // this will get logged at a lower level
            yield return CreateMonumentEvents();
            Puts("Creating Auto Events");
            yield return CreateAutoEvents();
            _createEventsCoroutine = null;
            Puts($"Startup event creation completed in {(DateTime.UtcNow - startTime).TotalSeconds} seconds");
        }
#endregion Startup
#region General Event
        // coroutine to determine whether any General Events should be created based
        //  on currently existing entities of interest
        // this is expected to only be called on startup
        private IEnumerator CreateGeneralEvents() {
            // determine up-front whether there are any general events to create,
            //  because iterating over all net entities is not cheap
            var checkGeneralEvents = false;
            // TODO: Bradley, Patrol Helicopter, Supply Drop, Timed Supply
            checkGeneralEvents |= configData.GeneralEvents.CargoShip.Enabled;
            // NOTE: StopWhenKilled is checked because we don't want to start events
            //  whose end is determined by a timer, as we don't know elapsed times
            checkGeneralEvents |= configData.GeneralEvents.HackableCrate.Enabled && configData.GeneralEvents.HackableCrate.StopWhenKilled;
            checkGeneralEvents |= configData.GeneralEvents.ExcavatorIgnition.Enabled;
            if (checkGeneralEvents) {
                foreach (var serverEntity in BaseNetworkable.serverEntities) {
                    switch (serverEntity) {
                        // Cargo Ship Event
                        case CargoShip cargoShip : StartupCargoShip(cargoShip);
                        yield return DynamicYield();
                        break;
                        // Excavator Ignition Event
                        case DieselEngine dieselEngine : StartupDieselEngine(dieselEngine);
                        yield return DynamicYield();
                        break;
                        // Hackable Crate Event
                        case HackableLockedCrate hackableLockedCrate : StartupHackableLockedCrate(hackableLockedCrate);
                        yield return DynamicYield();
                        break;
                    }
                }
            }
            yield return DynamicYield(true);
        }
#region ExcavatorIgnition Event
        // invoke appropriate hook handler for current DieselEngine state
        // this is only used on startup, to (re)create events for already-existing
        //  DieselEngine entities
        private void StartupDieselEngine(DieselEngine dieselEngine) {
            if (!dieselEngine) {
                PrintDebug("DieselEngine is null");
                return;
            }
            if (!configData.GeneralEvents.ExcavatorIgnition.Enabled) {
                PrintDebug("Excavator Ignition Event is disabled");
                return;
            }
            if (!dieselEngine.IsOn()) {
                PrintDebug("DieselEngine is off");
                return;
            }
            PrintDebug("Found activated Giant Excavator");
            OnDieselEngineToggled(dieselEngine);
        }
        private void OnDieselEngineToggled(DieselEngine dieselEngine) {
            if (!dieselEngine || null == dieselEngine.net) {
                PrintDebug("ERROR: OnDieselEngineToggled(): Engine or Net is null", DebugLevel.Error);
                return;
            }
            var zoneId = dieselEngine.net.ID.ToString();
            if (dieselEngine.IsOn()) {
                PrintDebug($"OnDieselEngineToggled(): Requesting 'just-in-case' delete of zoneId={zoneId} due to excavator enable");
                DeleteDynamicZone(zoneId);
                HandleGeneralEvent(GeneralEventType.ExcavatorIgnition, dieselEngine, true);
            }
            else {
                PrintDebug($"OnDieselEngineToggled(): Scheduling delete of zoneId={zoneId} due to excavator disable");
                HandleDeleteDynamicZone(zoneId);
            }
        }
#endregion ExcavatorIgnition Event
#region HackableLockedCrate Event
        // invoke appropriate hook handler for current HackableLockedCrate state
        // this is only used on startup, to (re)create events for already-existing
        //  HackableLockedCrate entities
        private void StartupHackableLockedCrate(HackableLockedCrate hackableLockedCrate) {
            if (!hackableLockedCrate) {
                PrintDebug("HackableLockedCrate is null");
                return;
            }
            if (!configData.GeneralEvents.HackableCrate.Enabled) {
                PrintDebug("Hackable Crate Event is disabled");
                return;
            }
            if (!configData.GeneralEvents.HackableCrate.StopWhenKilled) {
                PrintDebug("Hackable Crate Event doesn't stop when killed");
                return;
            }
            if (0 != hackableLockedCrate.FirstLooterId && configData.GeneralEvents.HackableCrate.TimerStartWhenLooted) {
                // looted and stop after time since loot enabled
                // we don't know elapsed time, so err on the side of assuming the event
                //  has already ended
                PrintDebug("Found looted hackable locked crate, and TimerStartWhenLooted set; ignoring because elapsed time unknown");
            }
            else if (hackableLockedCrate.HasFlag(HackableLockedCrate.Flag_FullyHacked) && configData.GeneralEvents.HackableCrate.TimerStartWhenUnlocked) {
                // unlocked and stop after time since unlock enabled
                // we don't know elapsed time, so err on the side of assuming the event
                //  has already ended
                PrintDebug("Found unlocked hackable locked crate and TimerStartWhenUnlocked set; ignoring because elapsed time unknown");
            }
            else if (hackableLockedCrate.HasFlag(HackableLockedCrate.Flag_Hacking) && !configData.GeneralEvents.HackableCrate.StartWhenSpawned) {
                // hacking and start on hacking enabled
                PrintDebug("Found hacking hackable locked crate and StartWhenSpawned NOT set; triggering OnCrateHack()");
                OnCrateHack(hackableLockedCrate);
            }
            else if (configData.GeneralEvents.HackableCrate.StartWhenSpawned) {
                // any other state and start on spawn + stop when killed enabled
                PrintDebug("Found hackable locked crate, and StartWhenSpawned set; triggering OnEntitySpawned()");
                OnEntitySpawned(hackableLockedCrate);
            }
            else {
                PrintDebug("Found hackable locked crate, but ignoring because of either start on hack, or stop on timer with elapsed time unknown");
            }
        }
        private void OnEntitySpawned(HackableLockedCrate hackableLockedCrate) {
            if (!hackableLockedCrate || null == hackableLockedCrate.net) {
                PrintDebug("ERROR: OnEntitySpawned(HackableLockedCrate): Crate or Net is null", DebugLevel.Error);
                return;
            }
            if (!configData.GeneralEvents.HackableCrate.Enabled || !configData.GeneralEvents.HackableCrate.StartWhenSpawned) {
                PrintDebug("OnEntitySpawned(HackableLockedCrate): Ignoring due to event or spawn start disabled");
                return;
            }
            PrintDebug("Trying to create hackable crate spawn event");
            NextTick(() => LockedCrateEvent(hackableLockedCrate));
        }
        private void OnCrateHack(HackableLockedCrate hackableLockedCrate) {
            if (!hackableLockedCrate || null == hackableLockedCrate.net) {
                PrintDebug("ERROR: OnCrateHack(): Crate or Net is null", DebugLevel.Error);
                return;
            }
            PrintDebug("OnCrateHack(): Trying to create hackable crate hack event");
            NextTick(() => LockedCrateEvent(hackableLockedCrate));
        }
        private void OnCrateHackEnd(HackableLockedCrate hackableLockedCrate) {
            if (!hackableLockedCrate || null == hackableLockedCrate.net) {
                PrintDebug("ERROR: OnCrateHackEnd(): Crate or Net is null", DebugLevel.Error);
                return;
            }
            var zoneId = hackableLockedCrate.net.ID.ToString();
            PrintDebug($"OnCrateHackEnd(): Scheduling delete of zoneId={zoneId} in {configData.GeneralEvents.HackableCrate.Duration}s");
            HandleDeleteDynamicZone(zoneId, configData.GeneralEvents.HackableCrate.Duration, nameof(GeneralEventType.HackableCrate));
        }
        private void OnLootEntity(BasePlayer player, HackableLockedCrate hackableLockedCrate) {
            if (!hackableLockedCrate || null == hackableLockedCrate.net) {
                PrintDebug("ERROR: OnLootEntity(HackableLockedCrate): Crate or Net is null", DebugLevel.Error);
                return;
            }
            if (!configData.GeneralEvents.HackableCrate.Enabled || !configData.GeneralEvents.HackableCrate.TimerStartWhenLooted) {
                PrintDebug("OnLootEntity(HackableLockedCrate): Ignoring due to event or loot delay disabled");
                return;
            }
            var zoneId = hackableLockedCrate.net.ID.ToString();
            PrintDebug($"OnLootEntity(HackableLockedCrate): Scheduling delete of zoneId={zoneId} in {configData.GeneralEvents.HackableCrate.Duration}s");
            HandleDeleteDynamicZone(zoneId, configData.GeneralEvents.HackableCrate.Duration, nameof(GeneralEventType.HackableCrate));
        }
        private void OnEntityKill(HackableLockedCrate hackableLockedCrate) {
            if (!hackableLockedCrate || null == hackableLockedCrate.net) {
                PrintDebug("ERROR: OnEntityKill(HackableLockedCrate): Crate or Net is null", DebugLevel.Error);
                return;
            }
            if (!configData.GeneralEvents.HackableCrate.Enabled || !configData.GeneralEvents.HackableCrate.StopWhenKilled) {
                PrintDebug("OnEntityKill(HackableLockedCrate): Ignoring due to event or kill stop disabled");
                return;
            }
            var zoneId = hackableLockedCrate.net.ID.ToString();
            if (!_activeDynamicZones.ContainsKey(zoneId)) {
                // no active zone for this hackable locked crate
                return;
            }
            // untether everything that we may have parented to the
            //  HackableLockedCrate so that they don't get killed along with it
            ZM_GetZoneByID(zoneId)?.transform.SetParent(null, true);
            ParentDome(zoneId, Vector3.zero);
            //When the timer starts, don't stop the event immediately
            if (_eventTimers.ContainsKey(zoneId)) {
                PrintDebug($"OnEntityKill(HackableLockedCrate): Ignoring due to event timer already active for zoneId={zoneId}");
                return;
            }
            PrintDebug($"OnEntityKill(HackableLockedCrate): Scheduling delete of zoneId={zoneId}");
            HandleDeleteDynamicZone(zoneId);
        }
        private void LockedCrateEvent(HackableLockedCrate hackableLockedCrate) {
            if (!CheckEntityOwner(hackableLockedCrate)) {
                return;
            }
            if (configData.GeneralEvents.HackableCrate.ExcludeOilRig && IsOnTheOilRig(hackableLockedCrate)) {
                PrintDebug("The hackable locked crate is on an oil rig.Skipping event creation.");
                return;
            }
            if (configData.GeneralEvents.HackableCrate.ExcludeCargoShip && IsOnTheCargoShip(hackableLockedCrate)) {
                PrintDebug("The hackable locked crate is on a cargo ship.Skipping event creation.");
                return;
            }
            // call this here, because otherwise it's difficult to ensure that we call
            //  it exactly once
            const string eventName = nameof(GeneralEventType.HackableCrate);
            if (!CanCreateDynamicPVP(eventName, hackableLockedCrate)) {
                return;
            }
            // NOTE: we are already NextTick() protected here
            HandleParentedEntityEvent(eventName, hackableLockedCrate, parentOnCreate : true);
        }
        private static bool IsOnTheCargoShip(HackableLockedCrate hackableLockedCrate) {
            var crateParent = hackableLockedCrate?.transform.parent;
            return crateParent && crateParent.HasComponent<CargoShip>();
        }
        private bool IsOnTheOilRig(HackableLockedCrate hackableLockedCrate) {
            // this may now get called before monument event creation if hackable
            //  crates exist on startup, so populate oilrig positions here if needed
            if (Vector3.zero == _oilRigPosition || Vector3.zero == _largeOilRigPosition) {
                foreach (LandmarkInfo landmarkInfo in TerrainMeta.Path.Landmarks) {
                    switch (landmarkInfo.name) {
                        case PrefabLargeOilRig : _largeOilRigPosition = landmarkInfo.transform.position;
                        break;
                        case PrefabOilRig : _oilRigPosition = landmarkInfo.transform.position;
                        break;
                    }
                    if (Vector3.zero != _oilRigPosition && Vector3.zero != _largeOilRigPosition) {
                        break;
                    }
                }
            }
            if (_oilRigPosition != Vector3.zero && Vector3Ex.Distance2D(hackableLockedCrate.transform.position, _oilRigPosition) < 50f) {
                return true;
            }
            if (_largeOilRigPosition != Vector3.zero && Vector3Ex.Distance2D(hackableLockedCrate.transform.position, _largeOilRigPosition) < 50f) {
                return true;
            }
            return false;
        }
#endregion HackableLockedCrate Event
#region PatrolHelicopter And BradleyAPC Event
        private void OnEntityDeath(PatrolHelicopter patrolHelicopter, HitInfo info) {
            if (!patrolHelicopter || null == patrolHelicopter.net) {
                return;
            }
            PatrolHelicopterEvent(patrolHelicopter);
        }
        private void OnEntityDeath(BradleyAPC bradleyApc, HitInfo info) {
            if (!bradleyApc || null == bradleyApc.net) {
                return;
            }
            BradleyApcEvent(bradleyApc);
        }
        private void PatrolHelicopterEvent(PatrolHelicopter patrolHelicopter) {
            if (!configData.GeneralEvents.PatrolHelicopter.Enabled) {
                return;
            }
            PrintDebug("Trying to create Patrol Helicopter killed event.");
            if (!CheckEntityOwner(patrolHelicopter)) {
                return;
            }
            HandleGeneralEvent(GeneralEventType.Helicopter, patrolHelicopter, false);
        }
        private void BradleyApcEvent(BradleyAPC bradleyAPC) {
            if (!configData.GeneralEvents.BradleyApc.Enabled) {
                return;
            }
            PrintDebug("Trying to create Bradley APC killed event.");
            if (!CheckEntityOwner(bradleyAPC)) {
                return;
            }
            HandleGeneralEvent(GeneralEventType.Bradley, bradleyAPC, false);
        }
#endregion PatrolHelicopter And BradleyAPC Event
#region SupplyDrop And SupplySignal Event
        // TODO: seems dodgy that Vector3 is being used as a key, because comparing
        //  floats is fraught; consider using network ID or something instead, and
        //  storing the location as data if needed
        private readonly Dictionary<Vector3, Timer> activeSupplySignals = new();
        private void OnCargoPlaneSignaled(CargoPlane cargoPlane, SupplySignal supplySignal) => NextTick(() => {
            if (!supplySignal || !cargoPlane) {
                return;
            }
            Vector3 dropPosition = cargoPlane.dropPosition;
            if (activeSupplySignals.ContainsKey(dropPosition)) {
                return;
            }
            // TODO: why is this a hard-coded 15-minute delay?
            activeSupplySignals.Add(dropPosition, timer.Once(900f, () => activeSupplySignals.Remove(dropPosition)));
            PrintDebug($"A supply signal is thrown at {dropPosition}");
        }
        );
        private void OnEntitySpawned(SupplyDrop supplyDrop) => NextTick(() => OnSupplyDropEvent(supplyDrop, false));
        private void OnSupplyDropLanded(SupplyDrop supplyDrop) {
            if (!supplyDrop || null == supplyDrop.net) {
                return;
            }
            var zoneId = supplyDrop.net.ID.ToString();
            if (_activeDynamicZones.TryGetValue(zoneId, out var eventName)) {
                // event was already created on spawn; parent the event to the entity,
                //  so that they move together
                // NOTES:
                // - don't delete on failure, because leaving the existing zone on the
                //    ground is better than deleting it
                // - no need to delay parenting, as the zone presumably has already
                //    existed for a bit
                ParentEventToEntity(zoneId, GetBaseEvent(eventName), supplyDrop, deleteOnFailure : false, delay : false);
                return;
            }
            NextTick(() => OnSupplyDropEvent(supplyDrop, true));
        }
        private void OnLootEntity(BasePlayer _, SupplyDrop supplyDrop) {
            if (!supplyDrop || null == supplyDrop.net) {
                return;
            }
            var zoneId = supplyDrop.net.ID.ToString();
            if (!_activeDynamicZones.TryGetValue(zoneId, out var eventName)) {
                // no active zone for this supply drop
                return;
            }
            var eventConfig = eventName switch {
                nameof(GeneralEventType.SupplySignal) => configData.GeneralEvents.SupplySignal, nameof(GeneralEventType.SupplyDrop) => configData.GeneralEvents.TimedSupply, _ => null
            }
            ;
            if (null == eventConfig) {
                // pathological
                PrintDebug($"Unknown SupplyDrop eventName={eventName} for zoneId={zoneId}", DebugLevel.Warning);
                return;
            }
            if (!eventConfig.Enabled || !eventConfig.TimerStartWhenLooted) {
                return;
            }
            HandleDeleteDynamicZone(zoneId, eventConfig.Duration, eventName);
        }
        private void OnEntityKill(SupplyDrop supplyDrop) {
            if (!supplyDrop || null == supplyDrop.net) {
                PrintDebug("ERROR: OnEntityKill(SupplyDrop): Drop or Net is null", DebugLevel.Error);
                return;
            }
            var zoneId = supplyDrop.net.ID.ToString();
            if (!_activeDynamicZones.TryGetValue(zoneId, out var eventName)) {
                // no active zone for this supply drop
                return;
            }
            // untether everything that we may have parented to the SupplyDrop so that
            //  they don't get killed along with it
            ZM_GetZoneByID(zoneId)?.transform.SetParent(null, true);
            ParentDome(zoneId, Vector3.zero);
            var eventConfig = eventName switch {
                nameof(GeneralEventType.SupplySignal) => configData.GeneralEvents.SupplySignal, nameof(GeneralEventType.SupplyDrop) => configData.GeneralEvents.TimedSupply, _ => null
            }
            ;
            if (eventConfig is not {
                Enabled : true
            }
            || !eventConfig.StopWhenKilled) {
                return;
            }
            //When the timer starts, don't stop the event immediately
            if (_eventTimers.ContainsKey(zoneId)) {
                PrintDebug($"OnEntityKill(SupplyDrop): Ignoring due to event timer already active for zoneId={zoneId}");
                return;
            }
            PrintDebug($"OnEntityKill(SupplyDrop): Scheduling delete of zoneId={zoneId}");
            HandleDeleteDynamicZone(zoneId);
        }
        private static string GetSupplyDropStateName(bool isLanded) => isLanded ? "Landed" : "Spawned";
        private void OnSupplyDropEvent(SupplyDrop supplyDrop, bool isLanded) {
            if (!supplyDrop || null == supplyDrop.net) {
                return;
            }
            PrintDebug($"Trying to create supply drop {GetSupplyDropStateName(isLanded)} event at {supplyDrop.transform.position}.");
            if (!CheckEntityOwner(supplyDrop)) {
                return;
            }
            var supplySignal = GetSupplySignalNear(supplyDrop.transform.position);
            if (null != supplySignal) {
                PrintDebug("Supply drop is probably from supply signal");
                if (!configData.GeneralEvents.SupplySignal.Enabled) {
                    PrintDebug("Event for supply signals disabled.Skipping event creation.");
                    return;
                }
                if (isLanded == configData.GeneralEvents.SupplySignal.StartWhenSpawned) {
                    PrintDebug($"{GetSupplyDropStateName(isLanded)} for supply signals disabled.");
                    return;
                }
                var entry = supplySignal.Value;
                entry.Value?.Destroy();
                activeSupplySignals.Remove(entry.Key);
                PrintDebug($"Removing Supply signal from active list.Active supply signals remaining: {activeSupplySignals.Count}");
                const string eventNameSS = nameof(GeneralEventType.SupplySignal);
                if (!CanCreateDynamicPVP(eventNameSS, supplyDrop)) {
                    return;
                }
                HandleParentedEntityEvent(eventNameSS, supplyDrop, parentOnCreate : isLanded);
                return;
            }
            PrintDebug("Supply drop is probably NOT from supply signal");
            if (!configData.GeneralEvents.TimedSupply.Enabled) {
                PrintDebug("Event for timed supply disabled.Skipping event creation.");
                return;
            }
            if (isLanded == configData.GeneralEvents.TimedSupply.StartWhenSpawned) {
                PrintDebug($"{GetSupplyDropStateName(isLanded)} for timed supply disabled.");
                return;
            }
            const string eventNameSD = nameof(GeneralEventType.SupplyDrop);
            if (!CanCreateDynamicPVP(eventNameSD, supplyDrop)) {
                return;
            }
            HandleParentedEntityEvent(eventNameSD, supplyDrop, parentOnCreate : isLanded);
        }
        private KeyValuePair<Vector3, Timer>? GetSupplySignalNear(Vector3 position) {
            PrintDebug($"Checking {activeSupplySignals.Count} active supply signals");
            if (activeSupplySignals.Count <= 0) {
                PrintDebug("No active signals, must be from a timed event cargo plane");
                return null;
            }
            foreach (var entry in activeSupplySignals) {
                var distance = Vector3Ex.Distance2D(entry.Key, position);
                PrintDebug($"Found a supply signal at {entry.Key} located {distance}m away.");
                if (distance > configData.Global.CompareRadius) continue;
// ... [Code preview truncated due to length. Full source in attached file] ...

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