C#:
// PATCHED by penguin-rust-base: fixed removed Oxide APIs (FindByID→FindAwakeOrSleeping, net.connection→Connection) + ulong→string conversion (line 3459) + removed .Get() on userID (lines 535, 3620)
//Requires: ZoneManager
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
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 Prefabs.Misc;
using UnityEngine;
namespace Oxide.Plugins;
[Info("Dynamic PVP", "HunterZ/CatMeat/Arainrr", "5.0.2", ResourceId = 2728)]
[Description("Creates temporary PvP zones on certain actions/events")]
public class DynamicPVP : RustPlugin {
#region Fields
[PluginReference]
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 PrefabSphereDome = "assets/prefabs/visualization/sphere.prefab";
private const string PrefabSphereRedRing = "assets/bundled/prefabs/modding/events/twitch/br_sphere_red.prefab";
private const string PrefabSphereGreenRing = "assets/bundled/prefabs/modding/events/twitch/br_sphere_green.prefab";
private const string PrefabSphereBlueRing = "assets/bundled/prefabs/modding/events/twitch/br_sphere.prefab";
private const string PrefabSpherePurpleRing = "assets/bundled/prefabs/modding/events/twitch/br_sphere_purple.prefab";
private const string ZoneName = "DynamicPVP";
private readonly Dictionary<string, Timer> _eventDeleteTimers = new();
private readonly Dictionary<ulong, LeftZone> _pvpDelays = new();
// Map of event names + base events by ZoneID
private readonly Dictionary<string, BaseEvent> _activeDynamicZones = new();
// ZoneID/NetID of Deep Sea zones that we may have tried to create
private readonly HashSet<string> _potentialDeepSeaZones = 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 _coroutine;
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;
private sealed class LeftZone : Pool.IPooled {
public string zoneId;
public BaseEvent baseEvent;
public Timer zoneTimer;
private void Reset() {
zoneId = null;
baseEvent = null;
zoneTimer?.Destroy();
zoneTimer = null;
}
public void EnterPool() => Reset();
public void LeavePool() => Reset();
}
private enum CoroutineTypes {
CreateStartupEvents, CreateDeepSeaEvents, DeleteDeepSeaEvents
}
[Flags]
[JsonConverter(typeof(StringEnumConverter))]
private enum PvpDelayTypes {
None = 0, ZonePlayersCanDamageDelayedPlayers = 1, DelayedPlayersCanDamageZonePlayers = 1 << 1, DelayedPlayersCanDamageDelayedPlayers = 1 << 2
}
// general and deep sea event types
// these are managed as a single enum to prevent name collisions
public enum GeneralEventType {
Bradley, Helicopter, TimedSupply, SupplySignal, CargoShip, HackableCrate, ExcavatorIgnition, GhostShip, DeepSeaIsland, IslandCannon
}
[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] = Pool.Get<HashSet<string>>();
}
_brokenTunnels = false;
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(OnDeepSeaClosed));
Unsubscribe(nameof(OnDeepSeaOpened));
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.Keys) {
_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() {
if (null == ZoneManager || ZoneManager.Version < new VersionNumber(3, 1, 10)) {
PrintError("Zone Manager missing or outdated; please update for proper function of this plugin!");
}
// resubscribe to any hooks that are conditional due to config options
// code is grouped alphabetically by first hook name, to make it easier t
// see what gets subscribed and why
if (_configData.GeneralEvents.SupplySignal.Enabled || _configData.GeneralEvents.TimedSupply.Enabled) {
// this is subscribed if either drop event is enabled, because we need to
// differentiate either way
Subscribe(nameof(OnCargoPlaneSignaled));
Subscribe(nameof(OnSupplyDropDropped));
// 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.CargoShip.Enabled) {
Subscribe(nameof(OnCargoShipEgress));
Subscribe(nameof(OnCargoShipHarborApproach));
Subscribe(nameof(OnCargoShipHarborArrived));
Subscribe(nameof(OnCargoShipHarborLeave));
}
if (_configData.GeneralEvents.HackableCrate.Enabled && !_configData.GeneralEvents.HackableCrate.StartWhenSpawned) {
Subscribe(nameof(OnCrateHack));
}
if (_configData.GeneralEvents.HackableCrate.Enabled && _configData.GeneralEvents.HackableCrate.TimerStartWhenUnlocked) {
Subscribe(nameof(OnCrateHackEnd));
}
if (AnySeepSeaEventEnabled()) {
Subscribe(nameof(OnDeepSeaClosed));
Subscribe(nameof(OnDeepSeaOpened));
}
if (_configData.GeneralEvents.ExcavatorIgnition.Enabled) {
Subscribe(nameof(OnDieselEngineToggled));
}
if (_configData.GeneralEvents.PatrolHelicopter.Enabled || _configData.GeneralEvents.BradleyApc.Enabled) {
Subscribe(nameof(OnEntityDeath));
}
if (_configData.GeneralEvents.TimedSupply.Enabled || _configData.GeneralEvents.SupplySignal.Enabled || _configData.GeneralEvents.HackableCrate.Enabled || _configData.GeneralEvents.CargoShip.Enabled) {
Subscribe(nameof(OnEntityKill));
}
if ((_configData.GeneralEvents.HackableCrate.Enabled && _configData.GeneralEvents.HackableCrate.StartWhenSpawned) || _configData.GeneralEvents.CargoShip.Enabled) {
Subscribe(nameof(OnEntitySpawned));
}
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));
}
NextTick(() => TryStartCoroutine(CoroutineTypes.CreateStartupEvents));
}
private void Unload() {
if (_coroutine != null) {
ServerMgr.Instance.StopCoroutine(_coroutine);
}
if (_activeDynamicZones.Count > 0) {
PrintDebug($"Unload(): Deleting {_activeDynamicZones.Count} active zone(s)");
// copy zone keys to a temporary list, because each deletion will modify
// _activeDynamicZones
var zoneKeys = Pool.Get<List<string>>();
zoneKeys.AddRange(_activeDynamicZones.Keys);
foreach (var key in zoneKeys) {
DeleteDynamicZone(key);
}
Pool.FreeUnmanaged(ref zoneKeys);
_activeDynamicZones.Clear();
_potentialDeepSeaZones.Clear();
}
// copy LeftZone records to a temporary list to that we can reverse iterate
var leftZones = Pool.Get<List<LeftZone>>();
leftZones.AddRange(_pvpDelays.Values);
for (var i = leftZones.Count - 1; i >= 0; --i) {
// this is cheating because it leaves leftZones[i] in a dangling state,
// but it's okay because we're going to free/clear everything anyway
var value = leftZones[i];
Pool.Free(ref value);
}
Pool.FreeUnmanaged(ref leftZones);
_pvpDelays.Clear();
// also remove LeftZone class from pool framework, in case it changes on
// plugin reload
Pool.Directory.TryRemove(typeof(LeftZone), out _);
// copy sphere lists to a temporary list so that we can reverse iterate
var spheres = Pool.Get<List<List<SphereEntity>>>();
spheres.AddRange(_zoneSpheres.Values);
for (var i = _zoneSpheres.Count - 1; i >= 0; --i) {
// this is cheating because it leaves spheres[i] in a dangling state, but
// it's okay because we're going to free/clear everything anyway
var sphereEntities = spheres[i];
foreach (var sphereEntity in sphereEntities) {
if (!sphereEntity || sphereEntity.IsDestroyed) continue;
sphereEntity.KillMessage();
}
Pool.FreeUnmanaged(ref sphereEntities);
}
Pool.FreeUnmanaged(ref spheres);
_zoneSpheres.Clear();
SaveData();
SaveDebug();
if (null == _debugStringBuilder) return;
Pool.FreeUnmanaged(ref _debugStringBuilder);
_debugStringBuilder = null;
_originalMonumentGeometries.Clear();
foreach (var apZone in _activePluginZones.Values) {
// this is cheating because it leaves apZone in a dangling state, but it's
// okay because we're going to clear the list just after this
var apZoneI = apZone;
Pool.FreeUnmanaged(ref apZoneI);
}
_activePluginZones.Clear();
DomeEvent._domeEventsToCheck = null;
_activeSignaledPlanesAndDrops.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
// try to start event creation coroutine
// startup: true => CreateStartupEvents, false => CreateDeepSeaEvents
// if coroutine already running, waits for it to stop and then starts it
private void TryStartCoroutine(CoroutineTypes type, DeepSeaManager deepSeaManager = null) {
if (null != _coroutine) {
Puts("Waiting for current event creation coroutine to finish...");
timer.Once(1.0f, () => TryStartCoroutine(type, deepSeaManager));
return;
}
if (!deepSeaManager) {
deepSeaManager = DeepSeaManager.ServerInstance;
}
switch (type) {
case CoroutineTypes.CreateStartupEvents : _coroutine = ServerMgr.Instance.StartCoroutine(CreateStartupEvents());
return;
case CoroutineTypes.CreateDeepSeaEvents : if (deepSeaManager) {
_coroutine = ServerMgr.Instance.StartCoroutine(CreateDeepSeaEvents(deepSeaManager, true, false));
}
else {
Puts("ERROR: Can't start Deep Sea Events creation coroutine because DeepSeaManager is null");
}
return;
case CoroutineTypes.DeleteDeepSeaEvents : _coroutine = ServerMgr.Instance.StartCoroutine(DeleteDeepSeaEvents());
return;
default : throw new ArgumentOutOfRangeException(nameof(type), type, null);
}
}
private void TryRemoveEventTimer(string zoneId) {
if (_eventDeleteTimers.Remove(zoneId, out var value)) {
value?.Destroy();
}
}
private LeftZone GetOrAddPVPDelay(BasePlayer player, string zoneId, BaseEvent baseEvent) {
PrintDebug($"GetOrAddPVPDelay(): 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.baseEvent = baseEvent;
if (added) {
CheckHooks(HookCheckReasons.DelayAdded, baseEvent);
}
return leftZone;
}
private bool TryRemovePVPDelay(BasePlayer player) {
PrintDebug($"TryRemovePVPDelay(): Removing {player.displayName} from PVP delay");
var playerId = player.userID;
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($"CheckEntityOwner(): 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($"CanCreateDynamicPVP(): 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<BaseEvent>>();
// 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) {
if (leftZone.baseEvent == null || leftZone.baseEvent.CommandList.Count <= 0) {
continue;
}
if (leftZone.baseEvent.CommandWorksForPVPDelay || _activeDynamicZones.ContainsValue(leftZone.baseEvent)) {
Pool.FreeUnmanaged(ref checkedEvents);
return true;
}
checkedEvents.Add(leftZone.baseEvent);
}
foreach (var baseEvent in _activeDynamicZones.Values) {
// optimization: skip if we've already checked this in the other loop
if (checkedEvents.Contains(baseEvent)) {
continue;
}
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();
}
}
#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 CreateStartupEvents() {
var startTime = DateTime.UtcNow;
Puts("Creating General Events");
yield return CreateGeneralEvents();
var deepSeaManager = DeepSeaManager.ServerInstance;
switch (AnySeepSeaEventEnabled()) {
case true when IsDeepSeaOpen(deepSeaManager) : // this will get logged at a lower level
yield return CreateDeepSeaEvents(deepSeaManager, delay : false, init : true);
break;
case true when deepSeaManager : Puts("Skipping Deep Sea Events (Deep Sea closed)");
break;
case true : Puts("Skipping Deep Sea Events (Deep Sea disabled)");
break;
default : Puts("Skipping Deep Sea Events (no events enabled)");
break;
}
// this will get logged at a lower level
yield return CreateMonumentEvents();
Puts("Creating Auto Events");
yield return CreateAutoEvents();
Puts($"Startup event creation completed in {(DateTime.UtcNow - startTime).TotalSeconds} seconds");
_coroutine = null;
}
#endregion Startup
#region General Events
// 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("StartupDieselEngine(): DieselEngine is null");
return;
}
if (!_configData.GeneralEvents.ExcavatorIgnition.Enabled) {
PrintDebug("StartupDieselEngine(): Excavator Ignition Event is disabled");
return;
}
if (!dieselEngine.IsOn()) {
PrintDebug("StartupDieselEngine(): DieselEngine is off");
return;
}
PrintDebug("StartupDieselEngine(): Found activated Giant Excavator");
OnDieselEngineToggled(dieselEngine);
}
private void OnDieselEngineToggled(DieselEngine dieselEngine) {
if (!dieselEngine || null == dieselEngine.net) {
PrintDebug("OnDieselEngineToggled(): ERROR: 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(_configData.GeneralEvents.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("StartupHackableLockedCrate(): HackableLockedCrate is null");
return;
}
var baseEvent = _configData.GeneralEvents.HackableCrate;
if (!baseEvent.Enabled) {
PrintDebug("StartupHackableLockedCrate(): Hackable Crate Event is disabled");
return;
}
if (!baseEvent.StopWhenKilled) {
PrintDebug("StartupHackableLockedCrate(): Hackable Crate Event doesn't stop when killed");
return;
}
if (0 != hackableLockedCrate.FirstLooterId && baseEvent.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("StartupHackableLockedCrate(): Found looted hackable locked crate, and TimerStartWhenLooted set; ignoring because elapsed time unknown");
}
else if (hackableLockedCrate.HasFlag(HackableLockedCrate.Flag_FullyHacked) && baseEvent.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("StartupHackableLockedCrate(): Found unlocked hackable locked crate and TimerStartWhenUnlocked set; ignoring because elapsed time unknown");
}
else if (hackableLockedCrate.HasFlag(HackableLockedCrate.Flag_Hacking) && !baseEvent.StartWhenSpawned) {
// hacking and start on hacking enabled
PrintDebug("StartupHackableLockedCrate(): Found hacking hackable locked crate and StartWhenSpawned NOT set; triggering OnCrateHack()");
OnCrateHack(hackableLockedCrate);
}
else if (baseEvent.StartWhenSpawned) {
// any other state and start on spawn + stop when killed enabled
PrintDebug("StartupHackableLockedCrate(): Found hackable locked crate, and StartWhenSpawned set; triggering OnEntitySpawned()");
OnEntitySpawned(hackableLockedCrate);
}
else {
PrintDebug("StartupHackableLockedCrate(): 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) {
var baseEvent = _configData.GeneralEvents.HackableCrate;
if (!baseEvent.Enabled) {
return;
}
if (!hackableLockedCrate || null == hackableLockedCrate.net) {
PrintDebug("OnEntitySpawned(): ERROR: HackableLockedCrate or Net is null", DebugLevel.Error);
return;
}
if (!baseEvent.StartWhenSpawned) {
PrintDebug("OnEntitySpawned(HackableLockedCrate): Ignoring due to start-when-spawned false");
return;
}
PrintDebug("OnEntitySpawned(): Trying to create HackableCrate spawn event");
NextTick(() => LockedCrateEvent(hackableLockedCrate));
}
private void OnCrateHack(HackableLockedCrate hackableLockedCrate) {
if (!hackableLockedCrate || null == hackableLockedCrate.net) {
PrintDebug("OnCrateHack(): ERROR: 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("OnCrateHackEnd(): ERROR: Crate or Net is null", DebugLevel.Error);
return;
}
var zoneId = hackableLockedCrate.net.ID.ToString();
var duration = _configData.GeneralEvents.HackableCrate.Duration;
PrintDebug($"OnCrateHackEnd(): Scheduling delete of zoneId={zoneId} in {duration} seconds");
HandleDeleteDynamicZone(zoneId, duration);
}
private void OnLootEntity(BasePlayer player, HackableLockedCrate hackableLockedCrate) {
if (!hackableLockedCrate || null == hackableLockedCrate.net) {
PrintDebug("OnLootEntity(HackableLockedCrate): ERROR: Crate or Net is null", DebugLevel.Error);
return;
}
var baseEvent = _configData.GeneralEvents.HackableCrate;
if (!baseEvent.Enabled || !baseEvent.TimerStartWhenLooted) {
PrintDebug("OnLootEntity(HackableLockedCrate): Ignoring due to event or loot delay disabled");
return;
}
var zoneId = hackableLockedCrate.net.ID.ToString();
var duration = baseEvent.Duration;
PrintDebug($"OnLootEntity(HackableLockedCrate): Scheduling delete of zoneId={zoneId} in {duration} seconds");
HandleDeleteDynamicZone(zoneId, duration);
}
private void OnEntityKill(HackableLockedCrate hackableLockedCrate) {
// all overloads of this hook are called if any of them are needed, so abort
// quickly for performance
if (!_configData.GeneralEvents.HackableCrate.Enabled) {
PrintDebug("OnEntityKill(HackableLockedCrate): Ignoring due to event disabled");
return;
}
if (!hackableLockedCrate || null == hackableLockedCrate.net) {
PrintDebug("OnEntityKill(HackableLockedCrate): ERROR: Crate or Net is null", DebugLevel.Error);
return;
}
var zoneId = hackableLockedCrate.net.ID.ToString();
if (!_activeDynamicZones.TryGetValue(zoneId, out var baseEvent)) {
PrintDebug($"OnEntityKill(HackableLockedCrate): Ignoring due to no event for ID={zoneId}");
return;
}
if (baseEvent is not HackableCrateEvent hackableCrateEvent) {
// pathological
PrintDebug($"OnEntityKill(HackableLockedCrate): Unknown HackableLockedCrate eventName={baseEvent.GetName()} for zoneId={zoneId}", DebugLevel.Warning);
return;
}
// untether everything parented to the crate so that it doesn't get killed
// along with it
ZM_GetZoneByID(zoneId)?.transform.SetParent(null, true);
ParentDome(zoneId, Vector3.zero);
if (!hackableCrateEvent.StopWhenKilled) {
PrintDebug($"OnEntityKill(HackableLockedCrate): Not requesting delete of zoneId={zoneId} because stop-when-killed is false");
return;
}
if (_eventDeleteTimers.ContainsKey(zoneId)) {
PrintDebug($"OnEntityKill(HackableLockedCrate): Not requesting delete of zoneId={zoneId} because a delete timer is already active");
return;
}
HandleDeleteDynamicZone(zoneId);
}
private void LockedCrateEvent(HackableLockedCrate hackableLockedCrate) {
if (!CheckEntityOwner(hackableLockedCrate)) {
return;
}
var baseEvent = _configData.GeneralEvents.HackableCrate;
if (baseEvent.ExcludeOilRig && IsOnTheOilRig(hackableLockedCrate)) {
PrintDebug("LockedCrateEvent(): The hackable locked crate is on an oil rig. Skipping event creation.");
return;
}
if (baseEvent.ExcludeCargoShip && IsOnTheCargoShip(hackableLockedCrate)) {
PrintDebug("LockedCrateEvent(): The hackable locked crate is on a cargo ship. Skipping event creation.");
return;
}
if (baseEvent.ExcludeGhostShip && IsOnGhostShip(hackableLockedCrate)) {
PrintDebug("LockedCrateEvent(): The hackable locked crate is on a ghost ship. Skipping event creation.");
return;
}
// call this here, because otherwise it's difficult to ensure that we call
// it exactly once
if (!CanCreateDynamicPVP(baseEvent.GetName(), hackableLockedCrate)) {
return;
}
// NOTE: we are already NextTick() protected here
HandleParentedEntityEvent(baseEvent, hackableLockedCrate, parentOnCreate : true);
}
private static bool IsOnTheCargoShip(HackableLockedCrate hackableLockedCrate) => true == hackableLockedCrate?.transform.parent?.HasComponent<CargoShip>();
private static bool IsOnGhostShip(HackableLockedCrate hackableLockedCrate) => true == hackableLockedCrate?.transform.parent?.HasComponent<GhostShip>();
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 (!_configData.GeneralEvents.PatrolHelicopter.Enabled) {
return;
}
if (!patrolHelicopter || null == patrolHelicopter.net) {
return;
}
PatrolHelicopterEvent(patrolHelicopter);
}
private void OnEntityDeath(BradleyAPC bradleyApc, HitInfo info) {
if (!_configData.GeneralEvents.BradleyApc.Enabled) {
return;
}
if (!bradleyApc || null == bradleyApc.net) {
return;
}
BradleyApcEvent(bradleyApc);
}
private void PatrolHelicopterEvent(PatrolHelicopter patrolHelicopter) {
var baseEvent = _configData.GeneralEvents.PatrolHelicopter;
if (!baseEvent.Enabled) {
return;
}
PrintDebug("PatrolHelicopterEvent(): Trying to create Patrol Helicopter killed event.");
if (!CheckEntityOwner(patrolHelicopter)) {
return;
}
HandleGeneralEvent(baseEvent, patrolHelicopter, useEntityId : false);
}
private void BradleyApcEvent(BradleyAPC bradleyApc) {
var baseEvent = _configData.GeneralEvents.BradleyApc;
if (!baseEvent.Enabled) {
return;
}
PrintDebug("BradleyApcEvent(): Trying to create Bradley APC killed event.");
if (!CheckEntityOwner(bradleyApc)) {
return;
}
HandleGeneralEvent(baseEvent, bradleyApc, useEntityId : false);
}
#endregion PatrolHelicopter And BradleyAPC Event
#region SupplyDrop And SupplySignal Event
// stores NetIDs of active CargoPlane and SupplyDrop instances spawned by a
// SupplySignal
private readonly HashSet<NetworkableId> _activeSignaledPlanesAndDrops = new();
private void OnCargoPlaneSignaled(CargoPlane plane, SupplySignal signal) => NextTick(() => {
if (!plane || !signal) {
return;
}
// record that plane was signaled
var planeID = plane.net.ID;
if (!_activeSignaledPlanesAndDrops.Add(planeID)) {
return;
}
PrintDebug($"OnCargoPlaneSignaled(): Supply Signal {signal.net.ID} spawned Cargo Plane {planeID} with drop position {plane.dropPosition}");
}
);
private void OnSupplyDropDropped(BaseEntity entity, CargoPlane plane) => NextTick(() => {
var supplyDrop = entity as SupplyDrop;
if (!supplyDrop || null == supplyDrop.net || !plane || null == plane.net) {
return;
}
// if plane was signaled, record drop as signaled too
var planeID = plane.net.ID;
var supplyDropID = supplyDrop.net.ID;
if (_activeSignaledPlanesAndDrops.Contains(planeID)) {
_activeSignaledPlanesAndDrops.Add(supplyDropID);
PrintDebug($"OnSupplyDropDropped(): Signaled Cargo Plane {planeID} spawned Supply Drop {supplyDropID} at position {supplyDrop.transform.position}");
}
else {
// must be a timed drop rather than a signaled one
PrintDebug($"OnSupplyDropDropped(): Timed Cargo Plane {planeID} spawned Supply Drop {supplyDropID} at position {supplyDrop.transform.position}");
}
HandleSupplyDropEvent(supplyDrop, false);
}
);
private void OnSupplyDropLanded(SupplyDrop supplyDrop) => NextTick(() => {
if (!supplyDrop || null == supplyDrop.net) {
return;
}
var zoneId = supplyDrop.net.ID.ToString();
if (_activeDynamicZones.TryGetValue(zoneId, out var baseEvent)) {
// 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, baseEvent, supplyDrop, deleteOnFailure : false, delay : false);
return;
}
HandleSupplyDropEvent(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 baseEvent)) {
// no active zone for this supply drop
return;
}
if (baseEvent is not SupplyDropEvent supplyDropEvent) {
// pathological
PrintDebug($"OnLootEntity(): Unknown SupplyDrop eventName={baseEvent.GetName()} for zoneId={zoneId}", DebugLevel.Warning);
return;
}
if (!supplyDropEvent.Enabled || !supplyDropEvent.TimerStartWhenLooted) {
return;
}
HandleDeleteDynamicZone(zoneId, supplyDropEvent.Duration);
}
private void OnEntityKill(SupplyDrop supplyDrop) {
// all overloads of this hook are called if any of them are needed, so abort
// quickly for performance
if (!_configData.GeneralEvents.SupplySignal.Enabled && !_configData.GeneralEvents.TimedSupply.Enabled) {
PrintDebug("OnEntityKill(SupplyDrop): Ignoring due to both SupplyDrop events disabled");
return;
}
if (!supplyDrop || null == supplyDrop.net) {
PrintDebug("OnEntityKill(SupplyDrop): ERROR: Drop or Net is null", DebugLevel.Error);
return;
}
var supplyDropID = supplyDrop.net.ID;
_activeSignaledPlanesAndDrops.Remove(supplyDropID);
var zoneId = supplyDropID.ToString();
if (!_activeDynamicZones.TryGetValue(zoneId, out var baseEvent)) {
PrintDebug($"OnEntityKill(SupplyDrop): Ignoring due to no event for ID={zoneId}");
return;
}
if (baseEvent is not SupplyDropEvent supplyDropEvent) {
// pathological
PrintDebug($"OnEntityKill(SupplyDrop): Unknown SupplyDrop eventName={baseEvent.GetName()} for zoneId={zoneId}", DebugLevel.Warning);
return;
}
// untether everything parented to the drop so that it doesn't get killed
// along with it
ZM_GetZoneByID(zoneId)?.transform.SetParent(null, true);
ParentDome(zoneId, Vector3.zero);
// abort if specific drop event flavor is disabled, or not configured to
// stop on kill
if (supplyDropEvent is not {
Enabled : true
}
|| !supplyDropEvent.StopWhenKilled) {
PrintDebug($"OnEntityKill(SupplyDrop): Not requesting delete of zoneId={zoneId} because event {supplyDropEvent.GetName()} is disabled, or stop-when-killed is false");
return;
}
if (_eventDeleteTimers.ContainsKey(zoneId)) {
PrintDebug($"OnEntityKill(SupplyDrop): Not requesting delete of zoneId={zoneId} because a delete timer is already active");
return;
}
HandleDeleteDynamicZone(zoneId);
}
private void OnEntityKill(CargoPlane plane) {
// all overloads of this hook are called if any of them are needed, so abort
// quickly for performance
if (!_configData.GeneralEvents.SupplySignal.Enabled && !_configData.GeneralEvents.TimedSupply.Enabled) {
return;
}
if (!plane || null == plane.net) {
return;
}
_activeSignaledPlanesAndDrops.Remove(plane.net.ID);
}
private static string GetSupplyDropStateName(bool isLanded) => isLanded ? "Landed" : "Spawned";
private void HandleSupplyDropEvent(SupplyDrop supplyDrop, bool isLanded) {
if (!supplyDrop || null == supplyDrop.net) {
return;
}
PrintDebug($"HandleSupplyDropEvent(): Trying to create supply drop {GetSupplyDropStateName(isLanded)} event at {supplyDrop.transform.position}.");
if (!CheckEntityOwner(supplyDrop
// ... [Code preview truncated due to length. Full source in attached file] ...