//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.1.0", 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();
private readonly Dictionary<string, BaseEvent> _activeDynamicZones =
new();
private readonly HashSet<string> _potentialDeepSeaZones = new();
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
}
public enum GeneralEventType
{
Bradley,
Helicopter,
Satellite,
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
}
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) } }
};
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>();
}
_useExcludePlayer = _configData.Global.UseExcludePlayer;
if (!_useExcludePlayer &&
null != TruePVE &&
TruePVE.Version >= new VersionNumber(2, 2, 3))
{
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
{
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");
}
}
}
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!");
}
if (_configData.GeneralEvents.SupplySignal.Enabled ||
_configData.GeneralEvents.TimedSupply.Enabled)
{
Subscribe(nameof(OnCargoPlaneSignaled));
Subscribe(nameof(OnSupplyDropDropped));
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.SatelliteCrash.Enabled ||
(_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)");
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();
}
var leftZones = Pool.Get<List<LeftZone>>();
leftZones.AddRange(_pvpDelays.Values);
for (var i = leftZones.Count - 1; i >= 0; --i)
{
var value = leftZones[i];
Pool.Free(ref value);
}
Pool.FreeUnmanaged(ref leftZones);
_pvpDelays.Clear();
Pool.Directory.TryRemove(typeof(LeftZone), out _);
var spheres = Pool.Get<List<List<SphereEntity>>>();
spheres.AddRange(_zoneSpheres.Values);
for (var i = _zoneSpheres.Count - 1; i >= 0; --i)
{
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)
{
var apZoneI = apZone;
Pool.FreeUnmanaged(ref apZoneI);
}
_activePluginZones.Clear();
DomeEvent.DomeEventsToCheck?.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
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.Get();
if (!_pvpDelays.Remove(playerId, out var leftZone)) return false;
Interface.CallHook("OnPlayerRemovedFromPVPDelay",
playerId, leftZone.zoneId, player);
CheckHooks(HookCheckReasons.DelayRemoved, null);
Pool.Free(ref leftZone);
return true;
}
private bool CheckEntityOwner(BaseEntity baseEntity)
{
if (!_configData.Global.CheckEntityOwner ||
!baseEntity.OwnerID.IsSteamId() ||
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()
{
var checkedEvents = Pool.Get<HashSet<BaseEvent>>();
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)
{
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;
}
private void UpdateDynamicHook(
bool needSubscription, HookCategory hookCategory)
{
if (!_subscriptionsByCategory.TryGetValue(
hookCategory, out var haveSubscription) ||
needSubscription == haveSubscription ||
!_hooksByCategory.TryGetValue(hookCategory, out var hooks))
{
return;
}
foreach (var hook in hooks)
{
if (needSubscription)
{
Subscribe(hook);
}
else
{
Unsubscribe(hook);
}
}
_subscriptionsByCategory[hookCategory] = needSubscription;
}
private void CheckCommandHooks(bool added)
{
if (added &&
_subscriptionsByCategory.TryGetValue(
HookCategory.Command, out var subscribed) &&
subscribed)
{
return;
}
UpdateDynamicHook(HasCommands(), HookCategory.Command);
}
private void CheckPluginHooks(BaseEvent baseEvent)
{
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);
private void CheckHooks(HookCheckReasons reasons, BaseEvent baseEvent)
{
if (reasons.HasFlag(HookCheckReasons.DelayAdded) ||
reasons.HasFlag(HookCheckReasons.ZoneAdded))
{
CheckCommandHooks(true);
}
else if (reasons.HasFlag(HookCheckReasons.DelayRemoved) ||
reasons.HasFlag(HookCheckReasons.ZoneRemoved))
{
CheckCommandHooks(false);
}
if (reasons.HasFlag(HookCheckReasons.DelayAdded) ||
reasons.HasFlag(HookCheckReasons.DelayRemoved))
{
CheckPvpDelayHooks();
}
if (reasons.HasFlag(HookCheckReasons.ZoneAdded) ||
reasons.HasFlag(HookCheckReasons.ZoneRemoved))
{
CheckPluginHooks(baseEvent);
CheckZoneHooks();
}
}
#endregion Methods
#region Events
#region Startup
private YieldInstruction DynamicYield(bool pause = false)
{
if (_targetFps <= 0) _targetFps = Mathf.Min(ConVar.FPS.limit, 30);
return
pause && _configData.Global.DebugEnabled ? _pauseYield :
Performance.report.frameRate >= _targetFps ? _fastYield :
_throttleYield;
}
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;
}
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
private IEnumerator CreateGeneralEvents()
{
var checkGeneralEvents = false;
checkGeneralEvents |= _configData.GeneralEvents.CargoShip.Enabled;
checkGeneralEvents |=
_configData.GeneralEvents.HackableCrate.Enabled &&
_configData.GeneralEvents.HackableCrate.StopWhenKilled;
checkGeneralEvents |= _configData.GeneralEvents.ExcavatorIgnition.Enabled;
if (checkGeneralEvents)
{
foreach (var serverEntity in BaseNetworkable.serverEntities)
{
switch (serverEntity)
{
case CargoShip cargoShip:
StartupCargoShip(cargoShip);
yield return DynamicYield();
break;
case DieselEngine dieselEngine:
StartupDieselEngine(dieselEngine);
yield return DynamicYield();
break;
case HackableLockedCrate hackableLockedCrate:
StartupHackableLockedCrate(hackableLockedCrate);
yield return DynamicYield();
break;
}
}
}
yield return DynamicYield(true);
}
#region ExcavatorIgnition Event
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
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)
{
PrintDebug(
"StartupHackableLockedCrate(): Found looted hackable locked crate, and TimerStartWhenLooted set; ignoring because elapsed time unknown");
}
else if (
hackableLockedCrate.HasFlag(HackableLockedCrate.Flag_FullyHacked) &&
baseEvent.TimerStartWhenUnlocked)
{
PrintDebug(
"StartupHackableLockedCrate(): Found unlocked hackable locked crate and TimerStartWhenUnlocked set; ignoring because elapsed time unknown");
}
else if (hackableLockedCrate.HasFlag(HackableLockedCrate.Flag_Hacking) &&
!baseEvent.StartWhenSpawned)
{
PrintDebug("StartupHackableLockedCrate(): Found hacking hackable locked crate and StartWhenSpawned NOT set; triggering OnCrateHack()");
OnCrateHack(hackableLockedCrate);
}
else if (baseEvent.StartWhenSpawned)
{
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)
{
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)
{
PrintDebug($"OnEntityKill(HackableLockedCrate): Unknown HackableLockedCrate eventName={baseEvent.GetName()} for zoneId={zoneId}", DebugLevel.Warning);
return;
}
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;
}
if (!CanCreateDynamicPVP(baseEvent.GetName(), hackableLockedCrate))
{
return;
}
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)
{
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)
{
var baseEvent = _configData.GeneralEvents.PatrolHelicopter;
if (!baseEvent.Enabled)
{
return;
}
if (!patrolHelicopter || null == patrolHelicopter.net)
{
return;
}
PrintDebug("OnEntityDeath(PatrolHelicopter): Trying to create Patrol Helicopter killed event.");
if (!CheckEntityOwner(patrolHelicopter))
{
return;
}
HandleGeneralEvent(baseEvent, patrolHelicopter, useEntityId: false);
}
private void OnEntityDeath(BradleyAPC bradleyApc, HitInfo info)
{
var baseEvent = _configData.GeneralEvents.BradleyApc;
if (!baseEvent.Enabled)
{
return;
}
if (!bradleyApc || null == bradleyApc.net)
{
return;
}
PrintDebug("OnEntityDeath(BradleyAPC): Trying to create Bradley APC killed event.");
if (!CheckEntityOwner(bradleyApc))
{
return;
}
HandleGeneralEvent(baseEvent, bradleyApc, useEntityId: false);
}
#endregion PatrolHelicopter And BradleyAPC Event
#region Satellite Crash Event
private void OnEntitySpawned(BaseEntity entity)
{
// Проверяем, что это обломки спутника (в префабе есть "satellite_debris")
if (entity == null || !entity.PrefabName.Contains("satellite_debris"))
return;
var baseEvent = _configData.GeneralEvents.SatelliteCrash;
if (!baseEvent.Enabled) return;
if (!entity || null == entity.net) return;
PrintDebug("OnEntitySpawned(SatelliteDebris): Trying to create Satellite Crash spawned event.");
HandleGeneralEvent(baseEvent, entity, useEntityId: false);
}
#endregion Satellite Crash Event
#region SupplyDrop And SupplySignal Event
private readonly HashSet<NetworkableId> _activeSignaledPlanesAndDrops = new();
private void OnCargoPlaneSignaled(
CargoPlane plane, SupplySignal signal) => NextTick(() =>
{
if (!plane || !signal)
{
return;
}
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;
}
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
{
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))
{
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))
{
return;
}
if (baseEvent is not SupplyDropEvent supplyDropEvent)
{
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)
{
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)
{
PrintDebug($"OnEntityKill(SupplyDrop): Unknown SupplyDrop eventName={baseEvent.GetName()} for zoneId={zoneId}", DebugLevel.Warning);
return;
}
ZM_GetZoneByID(zoneId)?.transform.SetParent(null, true);
ParentDome(zoneId, Vector3.zero);
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)
{
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))
{
return;
}
var supplyDropID = supplyDrop.net.ID;
var isSignaled = _activeSignaledPlanesAndDrops.Contains(supplyDropID);
if (isSignaled)
{
PrintDebug($"HandleSupplyDropEvent(): Supply Drop {supplyDropID} is from supply signal");
HandleSupplyDropEvent(
_configData.GeneralEvents.SupplySignal, isLanded, supplyDrop);
}
else
{
PrintDebug($"HandleSupplyDropEvent(): Supply Drop {supplyDropID} is from timed event");
HandleSupplyDropEvent(
_configData.GeneralEvents.TimedSupply, isLanded, supplyDrop);
}
}
private void HandleSupplyDropEvent(
SupplyDropEvent supplyDropEvent, bool isLanded, SupplyDrop supplyDrop)
{
if (!supplyDropEvent.Enabled)
{
PrintDebug($"HandleSupplyDropEvent(): Skipping event creation because supply drop event type is disabled: {supplyDropEvent}");
return;
}
if (isLanded == supplyDropEvent.StartWhenSpawned)
{
PrintDebug($"HandleSupplyDropEvent(): Skipping event creation because supply drop event state is disabled: {GetSupplyDropStateName(isLanded)}");
return;
}
if (!CanCreateDynamicPVP(supplyDropEvent.GetName(), supplyDrop))
{
return;
}
HandleParentedEntityEvent(
supplyDropEvent, supplyDrop, parentOnCreate: isLanded);
}
#endregion SupplyDrop And SupplySignal Event
#region CargoShip Event
private bool IsLeavingHarbor(CargoShip cargoShip)
{
if (
!cargoShip || !cargoShip.isDoingHarborApproach || cargoShip.IsShipDocked)
{
return false;
}
if (null == cargoShip.harborApproachPath?.nodes)
{
PrintError($"IsLeavingHarbor(): ERROR: Cargo ship at {cargoShip.transform.position} is on harbor docking sequence, but the path data is null");
return false;
}
for (var i = cargoShip.currentHarborApproachNode;
i < cargoShip.harborApproachPath.nodes.Count;
++i)
{
if (cargoShip.harborApproachPath.nodes[i].maxVelocityOnApproach == 0.0f)
{
return false;
}
}
return true;
}
private void StartupCargoShip(CargoShip cargoShip)
{
if (!cargoShip)
{
PrintDebug("StartupCargoShip(): CargoShip is null");
return;
}
if (!_configData.GeneralEvents.CargoShip.Enabled)
{
PrintDebug("StartupCargoShip(): Cargo Ship Event is disabled");
return;
}
if (cargoShip.HasFlag(CargoShip.Egressing))
{
PrintDebug("StartupCargoShip(): Found cargo ship in egress from the map");
OnCargoShipEgress(cargoShip);
}
else if (cargoShip.isDoingHarborApproach)
{
if (cargoShip.IsShipDocked)
{
PrintDebug("StartupCargoShip(): Found cargo ship docked at a Harbor");
OnCargoShipHarborArrived(cargoShip);
}
else if (IsLeavingHarbor(cargoShip))
{
PrintDebug("StartupCargoShip(): Found cargo ship leaving a Harbor");
OnCargoShipHarborLeave(cargoShip);
}
else
{
PrintDebug("StartupCargoShip(): Found cargo ship approaching a Harbor");
OnCargoShipHarborApproach(cargoShip);
}
}
else if (cargoShip.HasFlag(CargoShip.HasDocked))
{
PrintDebug("StartupCargoShip(): Found cargo ship that has previously docked at a Harbor");
OnCargoShipHarborLeave(cargoShip);
}
else
{
PrintDebug("StartupCargoShip(): Found cargo ship that has not approached a Harbor");
OnEntitySpawned(cargoShip);
}
}
private void HandleCargoState(CargoShip cargoShip, bool state)
{
if (!cargoShip || null == cargoShip.net)
{
return;
}
var baseEvent = _configData.GeneralEvents.CargoShip;
if (!baseEvent.Enabled)
{
return;
}
var zoneId = cargoShip.net.ID.ToString();
var zoneExists = _activeDynamicZones.ContainsKey(zoneId);
if (zoneExists == state)
{
PrintDebug($"HandleCargoState(): CargoShip event {zoneId} is already in desired state={state}");
return;
}
if (state)
{
PrintDebug($"HandleCargoState(): Trying to create CargoShip post-spawn event {zoneId}");
if (!CheckEntityOwner(cargoShip))
{
return;
}
if (!CanCreateDynamicPVP(baseEvent.GetName(), cargoShip))
{
return;
}
NextTick(() =>
HandleParentedEntityEvent(baseEvent, cargoShip, parentOnCreate: true));
}
else
{
PrintDebug($"HandleCargoState(): Trying to delete CargoShip post-spawn event {zoneId}");
HandleDeleteDynamicZone(zoneId);
}
}
private void OnEntitySpawned(CargoShip cargoShip)
{
var baseEvent = _configData.GeneralEvents.CargoShip;
if (!baseEvent.Enabled)
{
return;
}
if (!cargoShip || null == cargoShip.net)
{
PrintDebug("OnEntitySpawned(CargoShip): ERROR: CargoShip or Net is null", DebugLevel.Error);
return;
}
if (!baseEvent.SpawnState)
{
PrintDebug("OnEntitySpawned(CargoShip): Ignoring due to start-when-spawned false");
return;
}
if (!CheckEntityOwner(cargoShip))
{
PrintDebug("OnEntitySpawned(CargoShip): Ignoring due to entity owner");
return;
}
if (!CanCreateDynamicPVP(baseEvent.GetName(), cargoShip))
{
return;
}
PrintDebug("OnEntitySpawned(CargoShip): Trying to create CargoShip spawn event");
NextTick(() =>
HandleParentedEntityEvent(baseEvent, cargoShip, parentOnCreate: true));
}
private void OnEntityKill(CargoShip cargoShip)
{
if (!_configData.GeneralEvents.CargoShip.Enabled)
{
PrintDebug("OnEntityKill(CargoShip): Ignoring due to event disabled");
return;
}
if (!cargoShip || null == cargoShip.net)
{
PrintDebug("OnEntityKill(CargoShip): ERROR: Crate or Net is null", DebugLevel.Error);
return;
}
HandleDeleteDynamicZone(cargoShip.net.ID.ToString());
}
private void OnCargoShipEgress(CargoShip cargoShip) =>
HandleCargoState(
cargoShip, _configData.GeneralEvents.CargoShip.EgressState);
private void OnCargoShipHarborApproach(CargoShip cargoShip) =>
HandleCargoState(
cargoShip, _configData.GeneralEvents.CargoShip.ApproachState);
private void OnCargoShipHarborArrived(CargoShip cargoShip) =>
HandleCargoState(
cargoShip, _configData.GeneralEvents.CargoShip.DockState);
private void OnCargoShipHarborLeave(CargoShip cargoShip) =>
HandleCargoState(
cargoShip, _configData.GeneralEvents.CargoShip.DepartState);
#endregion CargoShip Event
#endregion General Events
#region Deep Sea Events
private bool AnySeepSeaEventEnabled() =>
true == _configData?.DeepSeaEvents.GhostShip.Enabled ||
true == _configData?.DeepSeaEvents.DeepSeaIsland.Enabled ||
true == _configData?.DeepSeaEvents.IslandCannon.Enabled;
private static bool IsDeepSeaEvent(GeneralEventType eventType) => eventType is
GeneralEventType.GhostShip or
GeneralEventType.DeepSeaIsland or
GeneralEventType.IslandCannon;
private static bool IsDeepSeaOpen(DeepSeaManager deepSeaManager) =>
true == deepSeaManager?.IsOpen();
private void OnDeepSeaOpened(DeepSeaManager deepSeaManager) => NextTick(() =>
{
TryStartCoroutine(CoroutineTypes.CreateDeepSeaEvents, deepSeaManager);
});
private void OnDeepSeaClosed(DeepSeaManager deepSeaManager) => NextTick(() =>
{
TryStartCoroutine(CoroutineTypes.DeleteDeepSeaEvents, deepSeaManager);
});
private IEnumerator CreateDeepSeaEvents(
DeepSeaManager deepSeaManager, bool delay, bool init)
{
var startTime = DateTime.UtcNow;
Puts("Creating Deep Sea Events");
var total = 0;
var sinceSleep = 0;
var deepSeaEntities = Pool.Get<HashSet<BaseEntity>>();
deepSeaManager.GetAllDeepSeaEntities(deepSeaEntities);
yield return DynamicYield();
foreach (var entity in deepSeaEntities)
{
switch (entity)
{
case Cannon cannon:
CreateCannonEvent(cannon, delay);
yield return DynamicYield();
break;
case GhostShip ghostShip:
CreateGhostShipEvent(ghostShip, delay);
yield return DynamicYield();
break;
case DeepSeaIsland deepSeaIsland:
CreateDeepSeaIslandEvent(deepSeaIsland, delay);
yield return DynamicYield();
break;
}
if (++sinceSleep < 250) continue;
total += sinceSleep;
sinceSleep = 0;
Puts($"...{total}/{deepSeaEntities.Count} Deep Sea entities scanned...");
yield return DynamicYield();
}
total += sinceSleep;
Puts($"...Finished scanning {total}/{deepSeaEntities.Count} Deep Sea entities");
Pool.FreeUnmanaged(ref deepSeaEntities);
if (init) yield break;
Puts($"Deep Sea Events creation completed in {(DateTime.UtcNow - startTime).TotalSeconds} seconds");
_coroutine = null;
}
private IEnumerator DeleteDeepSeaEvents()
{
var startTime = DateTime.UtcNow;
Puts("Deleting Deep Sea Events");
foreach (var zoneID in _potentialDeepSeaZones)
{
HandleDeleteDynamicZone(zoneID);
yield return DynamicYield();
}
_potentialDeepSeaZones.Clear();
Puts($"Deep Sea Events deletion completed in {(DateTime.UtcNow - startTime).TotalSeconds} seconds");
_coroutine = null;
}
#region GhostShip Event
private void CreateGhostShipEvent(GhostShip ghostShip, bool delay)
{
if (!ghostShip || null == ghostShip.net)
{
PrintDebug("CreateGhostShipEvent(): ERROR: GhostShip or Net is null", DebugLevel.Error);
return;
}
var baseEvent = _configData.DeepSeaEvents.GhostShip;
if (!baseEvent.Enabled)
{
PrintDebug("CreateGhostShipEvent(): Ignoring due to event disabled");
return;
}
PrintDebug("CreateGhostShipEvent(): Trying to create Ghost Ship event");
if (!CheckEntityOwner(ghostShip))
{
return;
}
HandleGeneralEvent(
baseEvent, ghostShip, useEntityId: true, delay, EventHeightStrategy.Zero);
}
#endregion GhostShip Event
#region DeepSeaIsland Event
private void CreateDeepSeaIslandEvent(DeepSeaIsland deepSeaIsland, bool delay)
{
if (!deepSeaIsland || null == deepSeaIsland.net)
{
PrintDebug("CreateDeepSeaIslandEvent(): ERROR: DeepSeaIsland or Net is null", DebugLevel.Error);
return;
}
var baseEvent = _configData.DeepSeaEvents.DeepSeaIsland;
if (!baseEvent.Enabled)
{
PrintDebug("CreateDeepSeaIslandEvent(): Ignoring due to event disabled");
return;
}
PrintDebug("CreateDeepSeaIslandEvent(): Trying to create Deep Sea Island event");
if (!CheckEntityOwner(deepSeaIsland))
{
return;
}
var eventName = baseEvent.GetName();
var zoneId = deepSeaIsland.net?.ID.ToString();
if (string.IsNullOrEmpty(zoneId))
{
PrintDebug($"CreateDeepSeaIslandEvent(): Aborting creation of eventName={eventName}, because entity is null", DebugLevel.Warning);
return;
}
if (_activeDynamicZones.ContainsKey(zoneId))
{
PrintDebug($"CreateDeepSeaIslandEvent(): Aborting creation of redundant eventName={eventName} for DeepSeaIsland={deepSeaIsland} with baseEntity.net.ID={zoneId}", DebugLevel.Warning);
return;
}
if (!CanCreateDynamicPVP(eventName, deepSeaIsland))
{
return;
}
var (transform, position, radius) = GetIslandGeometry(deepSeaIsland);
var dynamicZone = baseEvent.GetDynamicZone();
if (dynamicZone is SphereCubeAutoGeoDynamicZone { DoAutoGeo: true } geo)
{
geo.Radius = radius;
geo.Size = Vector3.zero; // disable cuboid zone
}
if (!_potentialDeepSeaZones.Add(zoneId))
{
return;
}
CreateDynamicZone(
baseEvent, position, zoneId, dynamicZone.ZoneSettings(transform), delay);
}
#endregion DeepSeaIsland Event
#region IslandCannon Event
private void CreateCannonEvent(Cannon cannon, bool delay)
{
if (!cannon || null == cannon.net)
{
PrintDebug("CreateCannonEvent(): ERROR: Cannon or Net is null", DebugLevel.Error);
return;
}
var baseEvent = _configData.DeepSeaEvents.IslandCannon;
if (!baseEvent.Enabled)
{
PrintDebug("CreateCannonEvent(): Ignoring due to event disabled");
return;
}
var cannonPrefab = cannon.ShortPrefabName;
if ("cannon.land.static" != cannonPrefab)
{
if ("cannon.deployed" != cannonPrefab)
{
PrintDebug($"CreateCannonEvent(): Ignoring unsupported Cannon prefab '{cannonPrefab}'");
}
return;
}
PrintDebug("CreateCannonEvent(): Trying to create Island Cannon event");
if (!CheckEntityOwner(cannon))
{
return;
}
HandleGeneralEvent(baseEvent, cannon, true, delay);
}
#endregion IslandCannon Event
#endregion Deep Sea Events
#region Monument Events
#region Automatic Geometry
private static bool IsCustomPreventBuildingPrefab(string name) =>
name is "assets/bundled/prefabs/modding/volumes_and_triggers/prevent_building_cube.prefab"
or "assets/bundled/prefabs/modding/volumes_and_triggers/prevent_building_sphere.prefab";
private Collider GetPreventBuildingCollider(Vector3 position)
{
var position2D = position.XZ2D();
var count = Physics.OverlapSphereNonAlloc(
position, 1, _colliderBuffer, Rust.Layers.Mask.Prevent_Building,
QueryTriggerInteraction.Ignore);
var mDist = float.MaxValue;
var mSize = 0.0f;
var mIndex = -1;
for (var i = 0; i < count; ++i)
{
var collider = _colliderBuffer[i];
if (collider is not BoxCollider && collider is not SphereCollider)
{
continue;
}
if (!IsCustomPreventBuildingPrefab(collider.name)) continue;
var center2D = GetCenter(collider).XZ2D();
var dist = Vector2.Distance(position2D, center2D);
var size = GetSize(collider);
if (dist < mDist)
{
mDist = dist;
mSize = size;
mIndex = i;
continue;
}
if (dist > mDist) continue;
if (size <= mSize) continue;
mDist = dist;
mSize = size;
mIndex = i;
}
return mIndex >= 0 ? _colliderBuffer[mIndex] : null;
}
private static Vector3 GetCenter(Collider collider)
{
return collider switch
{
BoxCollider b => collider.transform.TransformPoint(b.center),
SphereCollider s => collider.transform.TransformPoint(s.center),
_ => collider.transform.position
};
}
private static float GetSize(Collider collider)
{
return collider switch
{
BoxCollider b => b.size.magnitude,
SphereCollider s => s.radius,
_ => 0f
};
}
private static Collider GetPreventBuildingCollider(Transform transform)
{
if (!transform) return null;
var list = Pool.Get<List<Collider>>();
if (transform.TryGetComponent<MonumentInfo>(out var monumentInfo))
{
Vis.Colliders(
monumentInfo.obbBounds, list, Rust.Layers.Mask.Prevent_Building);
}
else
{
var tSize = transform.GetBounds().size.Max();
if (tSize <= 0.0f)
{
tSize = 5.0f;
}
Vis.Colliders(
transform.position, tSize, list, Rust.Layers.Mask.Prevent_Building);
}
Collider mCollider = null;
var mSize = 0.0f;
var mDist = float.MaxValue;
foreach (var collider in list)
{
if (!collider.transform.IsChildOf(transform)) continue;
var cSize = GetSize(collider);
if (cSize <= 0) continue;
if (!mCollider || cSize > mSize)
{
mCollider = collider;
mSize = cSize;
mDist = Vector3.Distance(transform.position, GetCenter(collider));
continue;
}
if (cSize < mSize) continue;
var cCenter = GetCenter(collider);
var cDist = Vector3.Distance(transform.position, cCenter);
if (cDist >= mDist) continue;
mCollider = collider;
mSize = cSize;
mDist = cDist;
}
Pool.FreeUnmanaged(ref list);
return mCollider;
}
private Collider GetPreventBuildingCollider(
Transform transform, bool customMonument)
{
if (!transform) return null;
return customMonument ?
GetPreventBuildingCollider(transform.position) :
GetPreventBuildingCollider(transform);
}
private static (Vector3 size, float rotation, Vector3 offset)
ApplyColliderData(
Collider collider, Vector3 size, float rotation, Vector3 offset)
{
if (!collider) return (Vector3.zero, 0.0f, Vector3.zero);
switch (collider)
{
case BoxCollider b:
{
size.Scale(b.size);
offset += b.center;
break;
}
case SphereCollider s:
{
size *= s.radius;
offset += s.center;
break;
}
default:
{
return (Vector3.zero, 0.0f, Vector3.zero);
}
}
return (size, rotation, offset);
}
private static (Vector3 size, float rotation, Vector3 offset)
GetPreventBuildingParams(Collider collider, Transform tMonument, bool global)
{
if (!collider || !tMonument)
{
return (Vector3.zero, 0.0f, Vector3.zero);
}
var tList = Pool.Get<List<Transform>>();
for (var transform = collider.transform;
transform && transform != tMonument;
transform = transform.parent)
{
tList.Add(transform);
if (global) break;
}
var offset = Vector3.zero;
var rotation = Quaternion.identity;
var size = Vector3.one;
for (var i = tList.Count - 1; i >= 0; --i)
{
var transform = tList[i];
offset += rotation * transform.localPosition;
rotation *= transform.localRotation;
size.Scale(transform.localScale);
}
Pool.FreeUnmanaged(ref tList);
if (global)
{
offset -= tMonument.position;
}
return ApplyColliderData(collider, size, rotation.eulerAngles.y, offset);
}
private (float radius, Vector3 size, float rotation, Vector3 offset)
GetPreventBuildingParams(Transform monumentTransform, bool customMonument)
{
var pbCollider = GetPreventBuildingCollider(
monumentTransform, customMonument);
if (!pbCollider) return (0.0f, Vector3.zero, 0.0f, Vector3.zero);
var (size, rotation, offset) =
GetPreventBuildingParams(pbCollider, monumentTransform, customMonument);
if (Vector3.zero == size)
{
return (0.0f, Vector3.zero, 0.0f, Vector3.zero);
}
return pbCollider switch
{
BoxCollider => (0.0f, size, rotation, offset),
SphereCollider => (size.Max(), Vector3.zero, rotation, offset),
_ => (0.0f, Vector3.zero, 0.0f, Vector3.zero)
};
}
private (float radius, Vector3 size, float rotation, Vector3 offset)
GetLabLinkParams(Transform transform)
{
DungeonBaseInfo dungeonBaseInfo = TerrainMeta.Path.FindClosest(
TerrainMeta.Path.DungeonBaseEntrances, transform.position);
Bounds bounds = new();
var first = true;
foreach (DungeonBaseLink linkI in TerrainMeta.Path.DungeonBaseLinks)
{
DungeonBaseInfo linkDungeon = TerrainMeta.Path.FindClosest(
TerrainMeta.Path.DungeonBaseEntrances, linkI.transform.position);
if (dungeonBaseInfo != linkDungeon) continue;
var child0 = linkI.transform.childCount > 0 ?
linkI.transform.GetChild(0) : linkI.transform;
var collider = GetPreventBuildingCollider(child0, false);
if (!collider) continue;
var cBounds = collider.bounds;
if (first)
{
bounds.center = cBounds.center;
first = false;
}
bounds.Encapsulate(cBounds);
}
return (0.0f, bounds.size, 0.0f, bounds.center - transform.position);
}
private bool IsApplicableTunnelLink(
DungeonGridInfo dungeon, GameObject link, bool stairwell)
{
if (!link.name.StartsWith(
"assets/bundled/prefabs/autospawn/tunnel-link/link-"))
{
return false;
}
var linkDist =
Vector3.Distance(dungeon.transform.position, link.transform.position);
if (linkDist >= 300.0f)
{
if (!_brokenTunnels)
{
PrintWarning("Corrupt Train Tunnel stairwell/dwelling linkage(s) detected. Related monument events may not work as expected. This is a map/RustEdit issue, not a DynamicPVP issue.");
_brokenTunnels = true;
}
return false;
}
if (!link.TryGetComponent<DungeonGridLink>(out var dungeonGridLink))
{
return false;
}
var isStairwell = DungeonGridLinkType.Elevator == dungeonGridLink.UpType;
return isStairwell == stairwell;
}
private (float radius, Vector3 size, float rotation, Vector3 offset)
GetTunnelLinkParams(Transform linkTransform, bool stairwell)
{
DungeonGridInfo dungeonGridInfo = TerrainMeta.Path.FindClosest(
TerrainMeta.Path.DungeonGridEntrances, linkTransform.position);
if (!dungeonGridInfo) return (0.0f, Vector3.zero, 0.0f, Vector3.zero);
Bounds bounds = new();
var first = true;
foreach (GameObject link in dungeonGridInfo.Links)
{
if (!IsApplicableTunnelLink(dungeonGridInfo, link, stairwell))
{
continue;
}
var collider = GetPreventBuildingCollider(link.transform, false);
if (!collider)
{
continue;
}
var cBounds = collider.bounds;
if (first)
{
bounds = cBounds;
first = false;
continue;
}
bounds.Encapsulate(cBounds);
}
return (0.0f, bounds.size, 0.0f, bounds.center - linkTransform.position);
}
private (float radius, Vector3 size, float rotation, Vector3 offset)
GetMonumentGeometry(Transform monumentTransform, MonumentEventType type) =>
type switch
{
MonumentEventType.Default =>
GetPreventBuildingParams(monumentTransform, false),
MonumentEventType.Custom =>
GetPreventBuildingParams(monumentTransform, true),
MonumentEventType.TunnelEntrance =>
GetTunnelLinkParams(monumentTransform, true),
MonumentEventType.TunnelLink =>
GetTunnelLinkParams(monumentTransform, false),
MonumentEventType.TunnelSection =>
GetPreventBuildingParams(monumentTransform, false),
MonumentEventType.UnderwaterLabs =>
GetLabLinkParams(monumentTransform),
_ =>
throw new ArgumentOutOfRangeException(nameof(type), type, null)
};
private static void UpdateGeometry(
MonumentEvent monumentEvent, bool first, float newRadius, Vector3 newSize,
float newRotation, Vector3 newOffset)
{
var updateRadius = newRadius > monumentEvent.DynamicZone.Radius;
var updateSizeX = newSize.x > monumentEvent.DynamicZone.Size.x;
var updateSizeY = newSize.y > monumentEvent.DynamicZone.Size.y;
var updateSizeZ = newSize.z > monumentEvent.DynamicZone.Size.z;
if (!first && !updateRadius && !updateSizeX && !updateSizeY &&
!updateSizeZ)
{
return;
}
if (updateRadius) monumentEvent.DynamicZone.Radius = newRadius;
var tempSize = monumentEvent.DynamicZone.Size;
if (updateSizeX) tempSize.x = newSize.x;
if (updateSizeY) tempSize.y = newSize.y;
if (updateSizeZ) tempSize.z = newSize.z;
monumentEvent.DynamicZone.Size = tempSize;
monumentEvent.DynamicZone.Rotation = newRotation;
monumentEvent.TransformPosition = newOffset;
}
private void CheckMonumentGeometry(
string monumentName, Transform transform,
MonumentEventType monumentEventType, bool first,
MonumentEvent monumentEvent)
{
if (first)
{
_originalMonumentGeometries.TryAdd(monumentName,
new OriginalMonumentGeometry
{
Radius = monumentEvent.DynamicZone.Radius,
Rotation = monumentEvent.DynamicZone.Rotation,
Size = monumentEvent.DynamicZone.Size,
FixedRotation = monumentEvent.DynamicZone.FixedRotation,
TransformPosition = monumentEvent.TransformPosition
});
monumentEvent.DynamicZone.Radius = 0.0f;
monumentEvent.DynamicZone.Rotation = 0.0f;
monumentEvent.DynamicZone.Size = Vector3.zero;
monumentEvent.DynamicZone.FixedRotation = monumentEventType switch
{
MonumentEventType.Default => false,
MonumentEventType.Custom => true,
MonumentEventType.TunnelEntrance => true,
MonumentEventType.TunnelLink => true,
MonumentEventType.TunnelSection => false,
MonumentEventType.UnderwaterLabs => true,
_ => false
};
monumentEvent.TransformPosition = Vector3.zero;
}
var (radius, size, rotation, offset) =
GetMonumentGeometry(transform, monumentEventType);
UpdateGeometry(monumentEvent, first, radius, size, rotation, offset);
}
private bool ShouldSaveGeometry()
{
var changed = false;
foreach (var (name, oGeo) in _originalMonumentGeometries)
{
if (!_configData.MonumentEvents.TryGetValue(name, out var monumentEvent))
{
continue;
}
if (changed || oGeo.HasSameData(monumentEvent)) continue;
PrintDebug($"ShouldSaveGeometry(): Geometry changed for monument event {name}; subsequent changes may not be reported");
changed = true;
}
_originalMonumentGeometries.Clear();
return changed;
}
private struct OriginalMonumentGeometry
{
public float Radius = 0f;
public float Rotation = 0f;
public Vector3 Size = Vector3.zero;
public bool FixedRotation = false;
public Vector3 TransformPosition = Vector3.zero;
public OriginalMonumentGeometry()
{
}
public bool HasSameData(MonumentEvent monumentEvent) =>
Mathf.Approximately(Radius, monumentEvent.DynamicZone.Radius) &&
Mathf.Approximately(Rotation, monumentEvent.DynamicZone.Rotation) &&
Size == monumentEvent.DynamicZone.Size &&
FixedRotation == monumentEvent.DynamicZone.FixedRotation &&
TransformPosition == monumentEvent.TransformPosition;
}
#endregion Automatic Geometry
private IEnumerator CreateMonumentEvent(
string monumentName, Transform transform, MonumentEventType type,
HashSet<string> addedEvents, List<string> createdEvents)
{
if (!_configData.MonumentEvents.TryGetValue(
monumentName, out var monumentEvent))
{
monumentEvent = new MonumentEvent { EventName = monumentName };
_configData.MonumentEvents.Add(monumentName, monumentEvent);
addedEvents.Add(monumentName);
}
if (monumentEvent.Enabled)
{
if (monumentEvent.DynamicZone.DoAutoGeo)
{
PrintDebug($"CreateMonumentEvent(): Calculating geometry for monument event {monumentName} with type {type} at location {transform.position}");
CheckMonumentGeometry(
monumentName, transform, type,
first: !createdEvents.Contains(monumentName), monumentEvent);
}
if (HandleMonumentEvent(monumentEvent, transform))
{
createdEvents.Add(monumentName);
}
}
yield return DynamicYield();
}
private static (string monumentName, bool custom) GetLandmarkName(
LandmarkInfo landmarkInfo)
{
if (!landmarkInfo.name.Contains("monument_marker.prefab"))
{
return (landmarkInfo.displayPhrase?.english?.Trim(), false);
}
var obj = landmarkInfo.transform.root.gameObject;
foreach (var (prefabName, objectSet) in World.SpawnedPrefabs)
{
if (objectSet.Contains(obj)) return (prefabName, true);
}
return (landmarkInfo.transform.root.name, true);
}
private IEnumerator CreateLandmarkMonumentEvents(
HashSet<string> addedEvents, List<string> createdEvents)
{
foreach (LandmarkInfo landmarkInfo in TerrainMeta.Path.Landmarks)
{
if (landmarkInfo is DungeonGridInfo or DungeonBaseLandmarkInfo)
{
continue;
}
var (monumentName, custom) = GetLandmarkName(landmarkInfo);
if (string.IsNullOrEmpty(monumentName) &&
landmarkInfo.shouldDisplayOnMap)
{
PrintDebug($"CreateLandmarkMonumentEvents(): Skipping visible landmark because it has no map title: {landmarkInfo}");
continue;
}
if (landmarkInfo is MonumentInfo && "Train Tunnel" == monumentName)
{
monumentName = "Train Tunnel Bunker";
}
else if (!landmarkInfo.shouldDisplayOnMap)
{
PrintDebug($"CreateLandmarkMonumentEvents(): Skipping unlabeled landmark {monumentName} of type {landmarkInfo}");
continue;
}
switch (landmarkInfo.name)
{
case "assets/bundled/prefabs/autospawn/monument/harbor/harbor_1.prefab":
monumentName += " A";
break;
case "assets/bundled/prefabs/autospawn/monument/harbor/harbor_2.prefab":
monumentName += " B";
break;
case PrefabLargeOilRig:
_largeOilRigPosition = landmarkInfo.transform.position;
break;
case PrefabOilRig:
_oilRigPosition = landmarkInfo.transform.position;
break;
}
var type =
custom ? MonumentEventType.Custom :
"Underwater Lab" == monumentName ? MonumentEventType.UnderwaterLabs :
MonumentEventType.Default;
yield return CreateMonumentEvent(
monumentName, landmarkInfo.transform, type, addedEvents, createdEvents);
}
yield return DynamicYield(true);
}
private string ToTunnelSectionEventName(string cellName)
{
if (!cellName.StartsWith("assets/bundled/prefabs/autospawn/tunnel"))
{
PrintDebug($"ToTunnelSectionEventName(): Skipping unsupported DungeonGridCell type due to non-tunnel: {cellName}");
return "";
}
var stringBuilder = Pool.Get<StringBuilder>();
stringBuilder.Clear().Append("Train Tunnel");
var slashSplit = cellName.Split("/");
if (slashSplit.Length < 6)
{
PrintDebug($"ToTunnelSectionEventName(): Skipping unsupported DungeonGridCell type due to path components: {cellName}");
Pool.FreeUnmanaged(ref stringBuilder);
return "";
}
var upwards = "tunnel-upwards" == slashSplit[4];
var intersection = "intersection.prefab" == slashSplit[5];
var dashSplit = slashSplit[5].Split("-");
if (dashSplit.Length < 2 && !intersection)
{
PrintDebug($"ToTunnelSectionEventName(): Skipp
For carbonNot An Issue
Please delete. I do primary development in carbon, so it definitely works there without changes. Probably you got bit by Carbon's rollout issues today.
Locked