C#:
// Все права принадлежат дискорд сообществу https://discord.gg/VgNHPpNrz6
//Requires: ZoneManager
using System;
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;
using IEnumerator = System.Collections.IEnumerator;
using Random = UnityEngine.Random;
namespace Oxide.Plugins {
[Info("Dynamic PVP", "HunterZ/CatMeat/Arainrr", "4.5.0", ResourceId = 2728)]
[Description("Creates temporary PvP zones on certain actions/events")]
public class DynamicPVP : RustPlugin {
#region Fields
[PluginReference]
private readonly Plugin 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();
private bool _dataChanged;
private Vector3 _oilRigPosition = Vector3.zero;
private Vector3 _largeOilRigPosition = Vector3.zero;
private Coroutine _createEventsCoroutine;
private bool _useExcludePlayer;
private bool _subscribedCommands;
private bool _subscribedDamage;
private bool _subscribedZones;
public static DynamicPVP Instance {
get;
private set;
}
private sealed class LeftZone : Pool.IPooled {
public string zoneId;
public string eventName;
public Timer zoneTimer;
public void EnterPool() {
zoneId = null;
eventName = null;
zoneTimer?.Destroy();
zoneTimer = null;
}
public void LeavePool() {
}
}
[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
}
#endregion Fields
#region Oxide Hooks
private void Init() {
Instance = this;
LoadData();
permission.RegisterPermission(PermissionAdmin, this);
AddCovalenceCommand(configData.Chat.Command, nameof(CmdDynamicPVP));
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(OnPlayerCommand));
Unsubscribe(nameof(OnServerCommand));
Unsubscribe(nameof(OnSupplyDropLanded));
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
_subscribedCommands = _subscribedDamage = _subscribedZones = false;
}
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));
}
_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;
}
private void OnServerSave() => timer.Once(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) {
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);
}
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);
Pool.Free(ref leftZone);
CheckHooks(HookCheckReasons.DelayRemoved);
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;
}
private void CheckCommandHooks(bool added) {
// optimization: abort if adding a delayzone and already subscribed, or if
// removing a delay/zone and already unsubscribed
if (added == _subscribedCommands) {
return;
}
// also return if subscription status already matches command status
if (HasCommands() == _subscribedCommands) {
return;
}
// subscription status needs to change, so toggle things
if (_subscribedCommands) {
Unsubscribe(nameof(OnPlayerCommand));
Unsubscribe(nameof(OnServerCommand));
}
else {
Subscribe(nameof(OnPlayerCommand));
Subscribe(nameof(OnServerCommand));
}
_subscribedCommands = !_subscribedCommands;
}
private void CheckPvpDelayHooks(bool added) {
// optimization: abort if using TruePVE's ExcludePlayer API
if (_useExcludePlayer) {
//...ensure we're unsubscribed from damage hook, just in case
if (!_subscribedDamage) return;
Unsubscribe(nameof(CanEntityTakeDamage));
_subscribedDamage = false;
return;
}
// optimization: abort if adding a delay and already subscribed, or if
// removing a delay and already unsubscribed
if (added == _subscribedDamage) {
return;
}
// also return if subscription status already matches delay status
if (_pvpDelays.Count > 0 == _subscribedDamage) {
return;
}
// subscription status needs to change, so toggle things
if (_subscribedDamage) {
Unsubscribe(nameof(CanEntityTakeDamage));
}
else {
Subscribe(nameof(CanEntityTakeDamage));
}
_subscribedDamage = !_subscribedDamage;
}
private void CheckZoneHooks(bool added) {
// optimization: abort if adding a zone and already subscribed, or if
// removing a zone and already unsubscribed
if (added == _subscribedZones) {
return;
}
// also return if subscription status already matches zone status
if (_activeDynamicZones.Count > 0 == _subscribedZones) {
return;
}
// subscription status needs to change, so toggle things
if (_subscribedZones) {
Unsubscribe(nameof(OnEnterZone));
Unsubscribe(nameof(OnExitZone));
}
else {
Subscribe(nameof(OnEnterZone));
Subscribe(nameof(OnExitZone));
}
_subscribedZones = !_subscribedZones;
}
private void CheckHooks(HookCheckReasons reasons) {
if (reasons.HasFlag(HookCheckReasons.DelayAdded)) {
CheckPvpDelayHooks(true);
}
else if (reasons.HasFlag(HookCheckReasons.DelayRemoved)) {
CheckPvpDelayHooks(false);
}
if (reasons.HasFlag(HookCheckReasons.ZoneAdded)) {
CheckZoneHooks(true);
}
else if (reasons.HasFlag(HookCheckReasons.ZoneRemoved)) {
CheckZoneHooks(false);
}
if (reasons.HasFlag(HookCheckReasons.DelayAdded) || reasons.HasFlag(HookCheckReasons.ZoneAdded)) {
CheckCommandHooks(true);
}
else if (reasons.HasFlag(HookCheckReasons.DelayRemoved) || reasons.HasFlag(HookCheckReasons.ZoneRemoved)) {
CheckCommandHooks(false);
}
}
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
// coroutine to orchestrate creation of all relevant events on startup
private IEnumerator CreateEvents() {
yield return CreateGeneralEvents();
yield return CreateMonumentEvents();
yield return CreateAutoEvents();
_createEventsCoroutine = null;
}
#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 CoroutineEx.waitForFixedUpdate;
break;
// Excavator Ignition Event
case DieselEngine dieselEngine : StartupDieselEngine(dieselEngine);
yield return CoroutineEx.waitForFixedUpdate;
break;
// Hackable Crate Event
case HackableLockedCrate hackableLockedCrate : StartupHackableLockedCrate(hackableLockedCrate);
yield return CoroutineEx.waitForFixedUpdate;
break;
}
}
}
yield return null;
}
#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, GeneralEventType.HackableCrate.ToString());
}
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, GeneralEventType.HackableCrate.ToString());
}
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();
//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
var eventName = GeneralEventType.HackableCrate.ToString();
if (!CanCreateDynamicPVP(eventName, hackableLockedCrate)) {
return;
}
// NOTE: we are already NextTick() protected here
HandleParentedEntityEvent(eventName, hackableLockedCrate, parentOnCreate : true);
}
private static bool IsOnTheCargoShip(HackableLockedCrate hackableLockedCrate) {
return hackableLockedCrate.GetComponentInParent<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;
}
var 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) {
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.StopWhenKilled) {
return;
}
//When the timer starts, don't stop the event immediately
if (!_eventTimers.ContainsKey(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}");
var eventNameSS = GeneralEventType.SupplySignal.ToString();
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;
}
var eventNameSD = GeneralEventType.SupplyDrop.ToString();
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;
PrintDebug("Found matching a supply signal.");
return entry;
}
PrintDebug("No matches found, probably from a timed event cargo plane");
return null;
}
#endregion SupplyDrop And SupplySignal Event
#region CargoShip Event
// invoke appropriate hook handler for current CargoShip state
// this is only used on startup, to (re)create events for already-existing
// CargoShip entities
private void StartupCargoShip(CargoShip cargoShip) {
if (!cargoShip) {
PrintDebug("CargoShip is null");
return;
}
if (!configData.GeneralEvents.CargoShip.Enabled) {
PrintDebug("Cargo Ship Event is disabled");
return;
}
if (cargoShip.IsShipDocked) {
// docked
PrintDebug("Found docked cargo ship");
OnCargoShipHarborArrived(cargoShip);
}
else if (cargoShip.HasFlag(CargoShip.Egressing)) {
// leaving the map
PrintDebug("Found egressing cargo ship");
OnCargoShipEgress(cargoShip);
}
else if (cargoShip.isDoingHarborApproach && cargoShip.currentHarborApproachNode < cargoShip.harborApproachPath.nodes.Count / 2) {
// approaching a Harbor
PrintDebug("Found cargo ship approaching Harbor");
OnCargoShipHarborApproach(cargoShip, null);
}
else if (cargoShip.HasFlag(CargoShip.HasDocked)) {
// not in any other state, but has previously docked, so this is
// equivalent to having most recently received an
// OnCargoShipHarborLeave hook call
PrintDebug("Found cargo ship leaving Harbor");
OnCargoShipHarborLeave(cargoShip);
}
else {
// not docked, not egressing, not approaching, has never docked
// this implies it has most recently spawned
PrintDebug("Found spawned cargo ship");
OnEntitySpawned(cargoShip);
}
}
// create/update or attempt to delete CargoShip event zone, based on
// specified desired state
private void HandleCargoState(CargoShip cargoShip, bool state) {
if (!cargoShip || null == cargoShip.net) {
return;
}
if (!configData.GeneralEvents.CargoShip.Enabled) {
return;
}
// create/update or attempt to delete zone, based on desired state
var zoneId = cargoShip.net.ID.ToString();
var zoneExists = _activeDynamicZones.ContainsKey(zoneId);
if (zoneExists == state) {
PrintDebug($"CargoShip event {zoneId} is already in desired state={state}");
return;
}
if (state) {
PrintDebug($"Trying to create CargoShip post-spawn event {zoneId}");
if (!CheckEntityOwner(cargoShip)) {
return;
}
var eventName = GeneralEventType.CargoShip.ToString();
// call this here, because otherwise it's difficult to ensure that we
// call it exactly once
if (!CanCreateDynamicPVP(eventName, cargoShip)) {
return;
}
NextTick(() => HandleParentedEntityEvent(eventName, cargoShip, parentOnCreate : true));
}
else {
PrintDebug($"Trying to delete CargoShip post-spawn event {zoneId}");
HandleDeleteDynamicZone(zoneId);
}
}
private void OnEntitySpawned(CargoShip cargoShip) {
if (!cargoShip || null == cargoShip.net) {
// bad entity
return;
}
if (!configData.GeneralEvents.CargoShip.Enabled || !configData.GeneralEvents.CargoShip.SpawnState) {
// not configured to create event on spawn
return;
}
PrintDebug("Trying to create CargoShip spawn event");
if (!CheckEntityOwner(cargoShip)) {
return;
}
var eventName = GeneralEventType.CargoShip.ToString();
// call this here, because otherwise it's
// ... [Code preview truncated due to length. Full source in attached file] ...