C#:
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Cui;
using Rust;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Vehicle Mines", "Form23 Guy", "1.0.23")]
[Description("Loot-only anti-vehicle mines, sea mines, and depth charges with custom placement, triggering, salvage, and crate injection.")]
public class VehicleMines : RustPlugin
{
[PluginReference]
private Plugin AlphaLoot, BetterLoot, MagicLoot, ImageLibrary;
private const string PermissionAdmin = "vehiclemines.admin";
private const string AntiVehiclePrefab = "assets/prefabs/deployable/landmine/landmine.prefab";
private const string DepthChargeUiName = "VehicleMines.DepthCharge";
private const string MineDataFileName = "VehicleMines/Mines";
private const string AntiVehicleIconImageName = "vehiclemines.icon.antivehicle";
private const string SeaMineIconImageName = "vehiclemines.icon.sea";
private const string DepthChargeIconImageName = "vehiclemines.icon.depthcharge";
private const float DefaultAutomaticMineDecaySeconds = 8f * 60f * 60f;
private PluginConfig _config;
private MineDatabase _mineDatabase = new MineDatabase();
private readonly HashSet<MineController> _controllers = new HashSet<MineController>();
private readonly List<MineController> _controllerSchedule = new List<MineController>();
private readonly Dictionary<ulong, MineController> _controllersByNetworkId = new Dictionary<ulong, MineController>();
private readonly Dictionary<ulong, MineRecord> _mineRecordsByNetworkId = new Dictionary<ulong, MineRecord>();
private readonly Dictionary<ulong, ulong> _mineOwnersByNetworkId = new Dictionary<ulong, ulong>();
private readonly List<ulong> _mineDatabaseRecordIdBuffer = new List<ulong>();
private readonly List<ulong> _mineDatabaseCleanupIdBuffer = new List<ulong>();
private readonly List<ulong> _mineDatabaseCleanupOwnerBuffer = new List<ulong>();
private readonly List<ulong> _mineDatabaseEmptyOwnerBuffer = new List<ulong>();
private readonly List<BaseEntity> _mineRemovalEntityBuffer = new List<BaseEntity>();
private readonly HashSet<ulong> _mineRemovalNetworkIdBuffer = new HashSet<ulong>();
private readonly Dictionary<ulong, float> _placementCooldowns = new Dictionary<ulong, float>();
private readonly Dictionary<ulong, float> _adminCommandCooldowns = new Dictionary<ulong, float>();
private readonly Dictionary<ulong, PendingDepthChargePlacement> _pendingDepthChargePlacements = new Dictionary<ulong, PendingDepthChargePlacement>();
private readonly Dictionary<ulong, int> _lootInjectionSequences = new Dictionary<ulong, int>();
private readonly HashSet<ulong> _lootSpawnObservedForRescan = new HashSet<ulong>();
private readonly HashSet<BaseCombatEntity> _damagedCharacterBuffer = new HashSet<BaseCombatEntity>();
private readonly List<BaseEntity> _resolveEntityChain = new List<BaseEntity>(32);
private readonly HashSet<BaseEntity> _resolveVisited = new HashSet<BaseEntity>();
private readonly List<BaseEntity> _resolveTransformParents = new List<BaseEntity>(16);
private readonly List<BaseEntity> _resolveChildren = new List<BaseEntity>(32);
private readonly List<BasePlayer> _mountedPlayerBuffer = new List<BasePlayer>(8);
private readonly Dictionary<uint, string> _entityMatchTextCache = new Dictionary<uint, string>();
private Collider[] _triggerColliderBuffer = new Collider[128];
private Collider[] _characterColliderBuffer = new Collider[128];
private Timer _mineScanTimer;
private Timer _mineDecayTimer;
private Timer _mineDataSaveTimer;
private Timer _mineDatabaseIntegrityTimer;
private Timer _queuedMineDataSaveTimer;
private int _nextControllerScanIndex;
private int _nextLootInjectionSequence;
private int _nextDepthChargePlacementSequence;
private int _lootModRescanSequence;
private bool _trackLootSpawnsForRescan;
private bool _mineDatabaseDirty;
private bool _mineDatabaseIntegrityCheckRunning;
private bool _mineRuntimeHooksSubscribed;
private bool _isUnloading;
private bool _isRemovingAllMines;
private bool _triggerColliderBufferWarningIssued;
private bool _characterColliderBufferWarningIssued;
private bool _reflectionFreeConfigWarningIssued;
private string[] _antiVehicleTokens = new string[0];
private string[] _seaVehicleTokens = new string[0];
private string[] _depthChargeTokens = new string[0];
private string[] _npcVehicleTokens = new string[0];
private ItemDefinition _antiVehicleItemDefinition;
private ItemDefinition _seaItemDefinition;
private ItemDefinition _depthChargeItemDefinition;
private enum MineKind
{
AntiVehicle,
Sea,
DepthCharge
}
private enum WaterBodyKind
{
Unknown,
Ocean,
Inland
}
private class PendingDepthChargePlacement
{
public int Sequence;
public Vector3 SurfacePoint;
public Vector3 PlayerStartPosition;
public float WaterSurfaceY;
public float SeabedY;
public float WaterDepth;
public float MinimumDepth;
public float MaximumDepth;
public float SelectedDepth;
public float YawDegrees;
public float ExpiresAt;
}
private class SafetySettings
{
[JsonProperty("VMGive Maximum Amount Per Command")]
public int MaximumGiveAmount = 100;
[JsonProperty("VMGive Command Cooldown Seconds")]
public float GiveCommandCooldownSeconds = 1f;
[JsonProperty("Maximum Active Mines Server Wide - 0 Disables Limit")]
public int MaximumActiveMines = 500;
[JsonProperty("Maximum Active Mines Per Player - 0 Disables Limit")]
public int MaximumMinesPerPlayer = 50;
[JsonProperty("Maximum Mine Quantity Injected Into One Crate")]
public int MaximumLootQuantityPerCrate = 10;
[JsonProperty("Minimum Allowed Mine Scan Interval Seconds")]
public float MinimumScanIntervalSeconds = 0.2f;
[JsonProperty("Maximum Allowed Mine Trigger Radius Metres")]
public float MaximumTriggerRadius = 8f;
[JsonProperty("Maximum Allowed Character Damage Radius Metres")]
public float MaximumCharacterDamageRadius = 10f;
}
private class PerformanceSettings
{
[JsonProperty("Central Mine Scheduler Tick Seconds")]
public float SchedulerTickSeconds = 0.1f;
[JsonProperty("Maximum Mine Scans Per Scheduler Tick")]
public int MaximumMineScansPerTick = 64;
[JsonProperty("Trigger Collider Buffer Size")]
public int TriggerColliderBufferSize = 128;
[JsonProperty("Character Damage Collider Buffer Size")]
public int CharacterColliderBufferSize = 128;
}
private class ImageLibraryIconSettings
{
[JsonProperty("Enabled - Optional ImageLibrary Integration")]
public bool Enabled = true;
[JsonProperty("Scope Note")]
public string ScopeNote = "ImageLibrary icons are used only in Vehicle Mines custom CUI. They cannot replace icons inside Rust's native inventory grid; native inventory artwork still requires a valid Workshop skin ID.";
[JsonProperty("Anti-Vehicle Mine Custom Icon URL - Blank Uses Available Workshop Skin Image")]
public string AntiVehicleIconUrl = string.Empty;
[JsonProperty("Sea Mine Custom Icon URL - Blank Uses Available Workshop Skin Image")]
public string SeaMineIconUrl = string.Empty;
[JsonProperty("Depth Charge Custom Icon URL - Blank Uses Available Workshop Skin Image")]
public string DepthChargeIconUrl = string.Empty;
[JsonProperty("Show Depth Charge Icon In Deployment UI")]
public bool ShowDepthChargeIconInDeploymentUi = true;
}
private class PersistenceSettings
{
[JsonProperty("Use Mine Location Database In oxide/data Instead Of Full Server Entity Scan")]
public bool UseMineLocationDatabase = true;
[JsonProperty("Performance Warning")]
public string PerformanceWarning = "Setting the database option to false forces a full BaseNetworkable.serverEntities scan on every plugin/server load. This is supported but is NOT PERFORMANT on large servers.";
[JsonProperty("Run One-Time Legacy Mine Scan When Database Has Never Been Built")]
public bool RunOneTimeLegacyMigration = true;
[JsonProperty("Remove All Placed Mines On Plugin Load, Reload, And Unload")]
public bool RemoveAllMinesOnLoadReloadAndUnload = true;
[JsonProperty("Automatic Mine Decay Seconds - 0 Disables")]
public float AutomaticDecaySeconds = VehicleMines.DefaultAutomaticMineDecaySeconds;
[JsonProperty("Mine Decay Check Interval Seconds")]
public float DecayCheckIntervalSeconds = 30f;
[JsonProperty("Mine Database Save Interval Seconds")]
public float DatabaseSaveIntervalSeconds = 60f;
[JsonProperty("Check Mine Database After Every Plant, Detonation, Decay, Salvage, Or Destruction")]
public bool CheckAfterMineStateChange = true;
[JsonProperty("Mine Database Integrity Check Interval Seconds - 0 Disables")]
public float DatabaseIntegrityCheckIntervalSeconds = 60f;
}
private class MineDatabase
{
[JsonProperty("Schema Version")]
public int SchemaVersion = 2;
[JsonProperty("Initial Legacy Scan Completed")]
public bool InitialLegacyScanCompleted;
[JsonProperty("Remove-All Startup Migration Completed")]
public bool RemoveAllStartupMigrationCompleted;
[JsonProperty("Last Updated UTC")]
public string LastUpdatedUtc = string.Empty;
[JsonProperty("Players")]
public Dictionary<ulong, PlayerMineRecords> Players = new Dictionary<ulong, PlayerMineRecords>();
}
private class PlayerMineRecords
{
[JsonProperty("Mines")]
public Dictionary<ulong, MineRecord> Mines = new Dictionary<ulong, MineRecord>();
}
private class MineRecord
{
[JsonProperty("Entity Network ID")]
public ulong NetworkId;
[JsonProperty("Mine Type")]
public string MineType = string.Empty;
[JsonProperty("Prefab")]
public string Prefab = string.Empty;
[JsonProperty("Position")]
public StoredPosition Position = new StoredPosition();
[JsonProperty("Placed At UTC Unix Seconds")]
public long PlacedAtUtc;
[JsonProperty("Expires At UTC Unix Seconds - 0 Means No Decay")]
public long ExpiresAtUtc;
[JsonProperty("Seconds Until Death Or Decay")]
public long SecondsUntilDecay;
}
private class StoredPosition
{
[JsonProperty("X")]
public float X;
[JsonProperty("Y")]
public float Y;
[JsonProperty("Z")]
public float Z;
public StoredPosition()
{
}
public StoredPosition(Vector3 position)
{
X = position.x;
Y = position.y;
Z = position.z;
}
}
#region Configuration
private class PluginConfig
{
[JsonProperty("Items")]
public ItemSettings Items = new ItemSettings();
[JsonProperty("Placement")]
public PlacementSettings Placement = new PlacementSettings();
[JsonProperty("Administration And Safety Limits")]
public SafetySettings Safety = new SafetySettings();
[JsonProperty("Performance")]
public PerformanceSettings Performance = new PerformanceSettings();
[JsonProperty("Mine Database And Automatic Decay")]
public PersistenceSettings Persistence = new PersistenceSettings();
[JsonProperty("ImageLibrary Custom UI Icons")]
public ImageLibraryIconSettings ImageLibraryIcons = new ImageLibraryIconSettings();
[JsonProperty("PvE And NPC Vehicle Compatibility")]
public CompatibilitySettings Compatibility = new CompatibilitySettings();
[JsonProperty("Anti-Vehicle Mine")]
public MineSettings AntiVehicle = MineSettings.CreateAntiVehicle();
[JsonProperty("Sea Mine")]
public MineSettings Sea = MineSettings.CreateSea();
[JsonProperty("Depth Charge")]
public MineSettings DepthCharge = MineSettings.CreateDepthCharge();
[JsonProperty("Salvage")]
public SalvageSettings Salvage = new SalvageSettings();
[JsonProperty("Loot Settings")]
public LootSettings Loot = new LootSettings();
}
private class ItemSettings
{
[JsonProperty("Anti-Vehicle Mine Base Item Shortname")]
public string AntiVehicleBaseShortname = "trap.landmine";
[JsonProperty("Anti-Vehicle Mine Display Name")]
public string AntiVehicleDisplayName = "Anti-Vehicle Mine";
[JsonProperty("Anti-Vehicle Mine Skin ID")]
public ulong AntiVehicleSkinId = 9100000000001UL;
[JsonProperty("Anti-Vehicle Craft Skin Inventory ID - 0 Disabled")]
public int AntiVehicleCraftSkinInventoryId = 0;
[JsonProperty("Sea Mine Base Item Shortname")]
public string SeaBaseShortname = "trap.landmine";
[JsonProperty("Sea Mine Display Name")]
public string SeaDisplayName = "Sea Mine";
[JsonProperty("Sea Mine Skin ID")]
public ulong SeaSkinId = 9100000000002UL;
[JsonProperty("Sea Mine Craft Skin Inventory ID - 0 Disabled")]
public int SeaCraftSkinInventoryId = 0;
[JsonProperty("Depth Charge Base Item Shortname")]
public string DepthChargeBaseShortname = "trap.landmine";
[JsonProperty("Depth Charge Display Name")]
public string DepthChargeDisplayName = "Depth Charge";
[JsonProperty("Depth Charge Skin ID")]
public ulong DepthChargeSkinId = 9100000000003UL;
[JsonProperty("Depth Charge Craft Skin Inventory ID - 0 Disabled")]
public int DepthChargeCraftSkinInventoryId = 0;
[JsonProperty("Note About Icons")]
public string IconNote = "Workshop skin IDs control native inventory artwork. Optional ImageLibrary URLs are used only in Vehicle Mines custom CUI.";
}
private class PlacementSettings
{
[JsonProperty("Use Primary Fire To Place Custom Mines")]
public bool UsePrimaryFire = true;
[JsonProperty("Placement Distance")]
public float Distance = 6f;
[JsonProperty("Placement Cooldown Seconds")]
public float CooldownSeconds = 0.35f;
[JsonProperty("Anti-Vehicle Mine Surface Offset")]
public float AntiVehicleSurfaceOffset = 0.025f;
[JsonProperty("Sea Mine Waterline Offset")]
public float SeaWaterlineOffset = -0.2f;
[JsonProperty("Sea Mine Barrel Pitch Degrees")]
public float SeaBarrelPitchDegrees = 90f;
[JsonProperty("Sea Mine Roll Degrees", NullValueHandling = NullValueHandling.Ignore)]
public float? LegacySeaBarrelRollDegrees;
[JsonProperty("Sea Mine Minimum Water Depth")]
public float SeaMinimumWaterDepth = 0.5f;
[JsonProperty("Use Fixed Ocean Sea Level For Sea Mines And Depth Charges")]
public bool UseFixedOceanSeaLevel = true;
[JsonProperty("Fixed Ocean Sea Level World Y")]
public float FixedOceanSeaLevelY = 0f;
[JsonProperty("Water Placement Note")]
public string WaterPlacementNote = "Ocean, lake, river, and supported custom water volumes are allowed when the measured water column meets the configured minimum depth.";
[JsonProperty("Depth Charge Minimum Water Column Metres")]
public float DepthChargeMinimumWaterDepth = 10f;
[JsonProperty("Depth Charge Default Deployment Depth Metres")]
public float DepthChargeDefaultDepth = 10f;
[JsonProperty("Depth Charge Minimum Selectable Depth Metres")]
public float DepthChargeMinimumSelectableDepth = 5f;
[JsonProperty("Depth Charge Seabed Clearance Metres")]
public float DepthChargeSeabedClearance = 0.25f;
[JsonProperty("Depth Charge Placement Confirmation Timeout Seconds")]
public float DepthChargePlacementTimeoutSeconds = 20f;
[JsonProperty("Depth Charge Maximum Player Movement Before Confirmation Metres")]
public float DepthChargeMaximumPlayerMovement = 3f;
[JsonProperty("Depth Charge Maximum Seabed Raycast Distance Metres")]
public float DepthChargeMaximumRaycastDepth = 500f;
[JsonProperty("Dry Surface Water Tolerance")]
public float DrySurfaceWaterTolerance = 0.05f;
}
private class CompatibilitySettings
{
[JsonProperty("Override TruePVE Vehicle Damage Blocking")]
public bool OverrideTruePVEVehicleDamage = true;
[JsonProperty("NPC Vehicle Override - Allow Triggering And Forced Damage")]
public bool NpcVehicleOverride = true;
[JsonProperty("NPC Vehicle Match Tokens", ObjectCreationHandling = ObjectCreationHandling.Replace)]
public List<string> NpcVehicleTokens = new List<string>
{
"bradley",
"scientistboat",
"scientist_boat",
"npcboat",
"npc_boat",
"patrolboat",
"patrol_boat",
"ptboat",
"pt_boat"
};
}
private class MineSettings
{
[JsonProperty("Enabled")]
public bool Enabled = true;
[JsonProperty("Deployed Prefab")]
public string DeployedPrefab;
[JsonProperty("Arm Delay Seconds")]
public float ArmDelaySeconds;
[JsonProperty("Scan Interval Seconds")]
public float ScanIntervalSeconds;
[JsonProperty("Trigger Radius")]
public float TriggerRadius;
[JsonProperty("Direct Vehicle Damage")]
public float DirectVehicleDamage;
[JsonProperty("Bradley Direct Damage")]
public float BradleyDamage;
[JsonProperty("Use Native Explosive Damage On Triggered Vehicle")]
public bool UseNativeExplosiveDamage = false;
[JsonProperty("Native Explosive Profile - C4 or Satchel")]
public string NativeExplosiveProfile = "C4";
[JsonProperty("Native Explosions Applied To Triggered Vehicle")]
public int NativeExplosions = 0;
[JsonProperty("Use Server-Owned Damage To Bypass PvE Vehicle Protection")]
public bool UseServerOwnedVehicleDamage = true;
[JsonProperty("Force Health Reduction If Damage Hooks Block Vehicle Damage")]
public bool ForceHealthReductionIfBlocked = true;
[JsonProperty("Vehicle Damage Verification Delay Seconds")]
public float DamageVerificationDelaySeconds = 0.1f;
[JsonProperty("Log Vehicle Damage Diagnostics")]
public bool LogVehicleDamageDiagnostics = false;
[JsonProperty("Use Native C4 Damage On Triggered Vehicle", NullValueHandling = NullValueHandling.Ignore)]
public bool? LegacyUseNativeC4Damage;
[JsonProperty("Native C4 Explosions Applied To Triggered Vehicle", NullValueHandling = NullValueHandling.Ignore)]
public int? LegacyNativeC4Explosions;
[JsonProperty("Occupant And Nearby Character Damage")]
public float CharacterDamage;
[JsonProperty("Character Damage Radius")]
public float CharacterDamageRadius;
[JsonProperty("Explosion Effect Prefab")]
public string ExplosionEffect;
[JsonProperty("Sympathetic Detonation From Explosive Damage")]
public bool SympatheticDetonation = true;
[JsonProperty("Destroy From Ordinary Damage Without Salvage")]
public bool DestroyFromOrdinaryDamage = true;
[JsonProperty("Vehicle Match Tokens", ObjectCreationHandling = ObjectCreationHandling.Replace)]
public List<string> VehicleTokens = new List<string>();
public static MineSettings CreateAntiVehicle()
{
return new MineSettings
{
Enabled = true,
DeployedPrefab = AntiVehiclePrefab,
ArmDelaySeconds = 3f,
ScanIntervalSeconds = 0.2f,
TriggerRadius = 2.25f,
DirectVehicleDamage = 750f,
BradleyDamage = 300f,
UseNativeExplosiveDamage = false,
NativeExplosiveProfile = "C4",
NativeExplosions = 0,
UseServerOwnedVehicleDamage = true,
ForceHealthReductionIfBlocked = true,
DamageVerificationDelaySeconds = 0.1f,
LogVehicleDamageDiagnostics = false,
CharacterDamage = 40f,
CharacterDamageRadius = 4f,
ExplosionEffect = "assets/prefabs/tools/c4/effects/c4_explosion.prefab",
SympatheticDetonation = true,
DestroyFromOrdinaryDamage = true,
VehicleTokens = new List<string>
{
"bradleyapc",
"modularcar",
"snowmobile",
"motorbike",
"motorcycle",
"pedalbike",
"bicycle",
"workcart",
"locomotive",
"traincar"
}
};
}
public static MineSettings CreateSea()
{
return new MineSettings
{
Enabled = true,
DeployedPrefab = "assets/bundled/prefabs/radtown/oil_barrel.prefab",
ArmDelaySeconds = 3f,
ScanIntervalSeconds = 0.2f,
TriggerRadius = 3f,
DirectVehicleDamage = 900f,
BradleyDamage = 0f,
UseNativeExplosiveDamage = false,
NativeExplosiveProfile = "Satchel",
NativeExplosions = 0,
UseServerOwnedVehicleDamage = true,
ForceHealthReductionIfBlocked = true,
DamageVerificationDelaySeconds = 0.1f,
LogVehicleDamageDiagnostics = false,
CharacterDamage = 45f,
CharacterDamageRadius = 5f,
ExplosionEffect = "assets/prefabs/tools/c4/effects/c4_explosion.prefab",
SympatheticDetonation = true,
DestroyFromOrdinaryDamage = true,
VehicleTokens = new List<string>
{
"rowboat",
"rhib",
"ptboat",
"pt_boat",
"patrolboat",
"patrol_boat",
"tugboat",
"submarine",
"submarineduo",
"submarinesolo",
"kayak"
}
};
}
public static MineSettings CreateDepthCharge()
{
return new MineSettings
{
Enabled = true,
DeployedPrefab = "assets/bundled/prefabs/radtown/oil_barrel.prefab",
ArmDelaySeconds = 3f,
ScanIntervalSeconds = 0.2f,
TriggerRadius = 4f,
DirectVehicleDamage = 1200f,
BradleyDamage = 0f,
UseNativeExplosiveDamage = false,
NativeExplosiveProfile = "C4",
NativeExplosions = 0,
UseServerOwnedVehicleDamage = true,
ForceHealthReductionIfBlocked = true,
DamageVerificationDelaySeconds = 0.1f,
LogVehicleDamageDiagnostics = false,
CharacterDamage = 55f,
CharacterDamageRadius = 5f,
ExplosionEffect = "assets/prefabs/tools/c4/effects/c4_explosion.prefab",
SympatheticDetonation = true,
DestroyFromOrdinaryDamage = true,
VehicleTokens = new List<string>
{
"submarine",
"submarineduo",
"submarinesolo"
}
};
}
}
private class SalvageSettings
{
[JsonProperty("Required Tool Shortname")]
public string RequiredToolShortname = "hammer.salvaged";
[JsonProperty("Required Hammer Hits")]
public int RequiredHits = 1;
[JsonProperty("Drop Resources Into World")]
public bool DropIntoWorld = true;
[JsonProperty("Anti-Vehicle Mine Drops", ObjectCreationHandling = ObjectCreationHandling.Replace)]
public List<ResourceDrop> AntiVehicleDrops = new List<ResourceDrop>
{
new ResourceDrop("explosives", 2),
new ResourceDrop("lowgradefuel", 20),
new ResourceDrop("metal.refined", 30)
};
[JsonProperty("Sea Mine Drops", ObjectCreationHandling = ObjectCreationHandling.Replace)]
public List<ResourceDrop> SeaDrops = new List<ResourceDrop>
{
new ResourceDrop("explosives", 4),
new ResourceDrop("lowgradefuel", 50),
new ResourceDrop("metal.fragments", 200),
new ResourceDrop("metal.refined", 20)
};
[JsonProperty("Depth Charge Drops", ObjectCreationHandling = ObjectCreationHandling.Replace)]
public List<ResourceDrop> DepthChargeDrops = new List<ResourceDrop>
{
new ResourceDrop("explosives", 5),
new ResourceDrop("lowgradefuel", 60),
new ResourceDrop("metal.fragments", 250),
new ResourceDrop("metal.refined", 25)
};
}
private class ResourceDrop
{
[JsonProperty("Item Shortname")]
public string Shortname;
[JsonProperty("Amount")]
public int Amount;
public ResourceDrop()
{
}
public ResourceDrop(string shortname, int amount)
{
Shortname = shortname;
Amount = amount;
}
}
private class LootSettings
{
[JsonProperty("Enabled")]
public bool Enabled = true;
[JsonProperty("Prevent Duplicate Injection")]
public bool PreventDuplicateInjection = true;
[JsonProperty("Drop Overflow Beside Crate")]
public bool DropOverflowBesideCrate = true;
[JsonProperty("Loot Mod Compatibility")]
public LootModCompatibilitySettings ModCompatibility = new LootModCompatibilitySettings();
[JsonProperty("Crates")]
public Dictionary<string, CrateSettings> Crates = CreateDefaults();
private static Dictionary<string, CrateSettings> CreateDefaults()
{
return new Dictionary<string, CrateSettings>
{
["Locked Crate"] = new CrateSettings
{
Enabled = true,
PrefabMatchTokens = new List<string> { "codelockedhackablecrate", "hackablelockedcrate" },
SpawnChancePercent = 15f,
MinimumAmount = 1,
MaximumAmount = 3,
RecommendedMaximum = 3,
QuantityOverride = 0,
AntiVehicleMineWeight = 70f,
SeaMineWeight = 30f,
DepthChargeWeight = 15f
},
["Elite Crate"] = new CrateSettings
{
Enabled = true,
PrefabMatchTokens = new List<string> { "crate_elite" },
SpawnChancePercent = 10f,
MinimumAmount = 1,
MaximumAmount = 5,
RecommendedMaximum = 5,
QuantityOverride = 0,
AntiVehicleMineWeight = 70f,
SeaMineWeight = 30f,
DepthChargeWeight = 15f
},
["Green Military Crate"] = new CrateSettings
{
Enabled = true,
PrefabMatchTokens = new List<string> { "crate_normal_2" },
SpawnChancePercent = 5f,
MinimumAmount = 1,
MaximumAmount = 1,
RecommendedMaximum = 1,
QuantityOverride = 0,
AntiVehicleMineWeight = 80f,
SeaMineWeight = 20f,
DepthChargeWeight = 15f
},
["Tool Crate"] = new CrateSettings
{
Enabled = true,
PrefabMatchTokens = new List<string> { "crate_tools" },
SpawnChancePercent = 3f,
MinimumAmount = 1,
MaximumAmount = 1,
RecommendedMaximum = 1,
QuantityOverride = 0,
AntiVehicleMineWeight = 85f,
SeaMineWeight = 15f,
DepthChargeWeight = 15f
}
};
}
}
private class LootModCompatibilitySettings
{
[JsonProperty("Enabled")]
public bool Enabled = true;
[JsonProperty("Enable MagicLoot Compatibility")]
public bool MagicLoot = true;
[JsonProperty("Enable BetterLoot Compatibility")]
public bool BetterLoot = true;
[JsonProperty("Enable AlphaLoot Compatibility")]
public bool AlphaLoot = true;
[JsonProperty("Post-Population Injection Delay Seconds")]
public float InjectionDelaySeconds = 0.1f;
[JsonProperty("Inventory Ready Retry Count")]
public int InventoryReadyRetryCount = 8;
[JsonProperty("Inventory Ready Retry Interval Seconds")]
public float InventoryReadyRetryIntervalSeconds = 0.25f;
[JsonProperty("Reinject Existing Containers When A Supported Loot Mod Loads")]
public bool ReinjectOnLootModLoad = true;
[JsonProperty("MagicLoot Load Rescan Delay Seconds")]
public float MagicLootRescanDelaySeconds = 2f;
[JsonProperty("BetterLoot Load Rescan Delay Seconds")]
public float BetterLootRescanDelaySeconds = 2f;
[JsonProperty("AlphaLoot Load Rescan Delay Seconds")]
public float AlphaLootRescanDelaySeconds = 22f;
[JsonProperty("Startup Rescan For Direct-Population Loot Mods")]
public bool StartupRescan = true;
[JsonProperty("Startup Rescan Delay Seconds")]
public float StartupRescanDelaySeconds = 3f;
[JsonProperty("Expand Full Loot Containers For Mine Stacks")]
public bool ExpandContainerCapacity = true;
[JsonProperty("Maximum Container Capacity")]
public int MaximumContainerCapacity = 36;
[JsonProperty("Log Compatibility Activity")]
public bool LogActivity = false;
}
private class CrateSettings
{
[JsonProperty("Enabled")]
public bool Enabled = true;
[JsonProperty("Prefab Name Match Tokens", ObjectCreationHandling = ObjectCreationHandling.Replace)]
public List<string> PrefabMatchTokens = new List<string>();
[JsonProperty("Spawn Chance Percent")]
public float SpawnChancePercent;
[JsonProperty("Minimum Combined Mine Quantity")]
public int MinimumAmount = 1;
[JsonProperty("Maximum Combined Mine Quantity")]
public int MaximumAmount = 1;
[JsonProperty("Recommended Maximum")]
public int RecommendedMaximum = 1;
[JsonProperty("Quantity Override - 0 Uses Min Max")]
public int QuantityOverride;
[JsonProperty("Anti-Vehicle Mine Weight")]
public float AntiVehicleMineWeight = 50f;
[JsonProperty("Sea Mine Weight")]
public float SeaMineWeight = 50f;
[JsonProperty("Depth Charge Weight")]
public float DepthChargeWeight = 15f;
}
protected override void LoadDefaultConfig()
{
_config = new PluginConfig();
ValidateConfig();
SaveConfig();
}
protected override void LoadConfig()
{
base.LoadConfig();
try
{
_config = Config.ReadObject<PluginConfig>();
if (_config == null)
{
throw new JsonException("Configuration deserialized as null.");
}
}
catch (Exception ex)
{
PrintError($"Configuration could not be read: {ex.Message}. A fresh configuration will be created.");
_config = new PluginConfig();
}
ValidateConfig();
SaveConfig();
}
protected override void SaveConfig()
{
Config.WriteObject(_config, true);
}
private void ValidateConfig()
{
if (_config.Items == null) _config.Items = new ItemSettings();
MigrateLegacyBaseItemShortnames();
if (_config.Placement == null) _config.Placement = new PlacementSettings();
if (_config.Safety == null) _config.Safety = new SafetySettings();
if (_config.Performance == null) _config.Performance = new PerformanceSettings();
if (_config.Persistence == null) _config.Persistence = new PersistenceSettings();
if (_config.ImageLibraryIcons == null) _config.ImageLibraryIcons = new ImageLibraryIconSettings();
if (_config.Compatibility == null) _config.Compatibility = new CompatibilitySettings();
if (_config.Compatibility.NpcVehicleTokens == null) _config.Compatibility.NpcVehicleTokens = new CompatibilitySettings().NpcVehicleTokens;
if (_config.AntiVehicle == null) _config.AntiVehicle = MineSettings.CreateAntiVehicle();
if (_config.Sea == null) _config.Sea = MineSettings.CreateSea();
if (_config.DepthCharge == null) _config.DepthCharge = MineSettings.CreateDepthCharge();
if (_config.Salvage == null) _config.Salvage = new SalvageSettings();
if (_config.Loot == null) _config.Loot = new LootSettings();
if (_config.Loot.ModCompatibility == null) _config.Loot.ModCompatibility = new LootModCompatibilitySettings();
if (_config.Loot.Crates == null) _config.Loot.Crates = new LootSettings().Crates;
LootModCompatibilitySettings lootCompatibility = _config.Loot.ModCompatibility;
lootCompatibility.InjectionDelaySeconds = Mathf.Clamp(lootCompatibility.InjectionDelaySeconds, 0f, 5f);
lootCompatibility.InventoryReadyRetryCount = Mathf.Clamp(lootCompatibility.InventoryReadyRetryCount, 0, 20);
lootCompatibility.InventoryReadyRetryIntervalSeconds = Mathf.Clamp(lootCompatibility.InventoryReadyRetryIntervalSeconds, 0.05f, 5f);
lootCompatibility.MagicLootRescanDelaySeconds = Mathf.Clamp(lootCompatibility.MagicLootRescanDelaySeconds, 0f, 30f);
lootCompatibility.BetterLootRescanDelaySeconds = Mathf.Clamp(lootCompatibility.BetterLootRescanDelaySeconds, 0f, 30f);
lootCompatibility.AlphaLootRescanDelaySeconds = Mathf.Clamp(lootCompatibility.AlphaLootRescanDelaySeconds, 0f, 60f);
lootCompatibility.StartupRescanDelaySeconds = Mathf.Clamp(lootCompatibility.StartupRescanDelaySeconds, 0f, 30f);
lootCompatibility.MaximumContainerCapacity = Mathf.Clamp(lootCompatibility.MaximumContainerCapacity, 1, 36);
_config.Safety.MaximumGiveAmount = Mathf.Clamp(_config.Safety.MaximumGiveAmount, 1, 1000);
_config.Safety.GiveCommandCooldownSeconds = Mathf.Clamp(_config.Safety.GiveCommandCooldownSeconds, 0.1f, 60f);
_config.Safety.MaximumActiveMines = Mathf.Clamp(_config.Safety.MaximumActiveMines, 0, 10000);
_config.Safety.MaximumMinesPerPlayer = Mathf.Clamp(_config.Safety.MaximumMinesPerPlayer, 0, 1000);
_config.Safety.MaximumLootQuantityPerCrate = Mathf.Clamp(_config.Safety.MaximumLootQuantityPerCrate, 1, 100);
_config.Safety.MinimumScanIntervalSeconds = Mathf.Clamp(_config.Safety.MinimumScanIntervalSeconds, 0.1f, 5f);
_config.Safety.MaximumTriggerRadius = Mathf.Clamp(_config.Safety.MaximumTriggerRadius, 1f, 20f);
_config.Safety.MaximumCharacterDamageRadius = Mathf.Clamp(_config.Safety.MaximumCharacterDamageRadius, 1f, 20f);
_config.Performance.SchedulerTickSeconds = Mathf.Clamp(_config.Performance.SchedulerTickSeconds, 0.05f, 1f);
_config.Performance.MaximumMineScansPerTick = Mathf.Clamp(_config.Performance.MaximumMineScansPerTick, 1, 1000);
_config.Performance.TriggerColliderBufferSize = Mathf.Clamp(_config.Performance.TriggerColliderBufferSize, 16, 1024);
_config.Performance.CharacterColliderBufferSize = Mathf.Clamp(_config.Performance.CharacterColliderBufferSize, 16, 1024);
if (_config.Persistence.PerformanceWarning == null)
{
_config.Persistence.PerformanceWarning = new PersistenceSettings().PerformanceWarning;
}
if (float.IsNaN(_config.Persistence.AutomaticDecaySeconds) || float.IsInfinity(_config.Persistence.AutomaticDecaySeconds))
{
_config.Persistence.AutomaticDecaySeconds = DefaultAutomaticMineDecaySeconds;
}
_config.Persistence.AutomaticDecaySeconds = Mathf.Clamp(_config.Persistence.AutomaticDecaySeconds, 0f, 31536000f);
_config.Persistence.DecayCheckIntervalSeconds = Mathf.Clamp(_config.Persistence.DecayCheckIntervalSeconds, 5f, 600f);
_config.Persistence.DatabaseSaveIntervalSeconds = Mathf.Clamp(_config.Persistence.DatabaseSaveIntervalSeconds, 5f, 600f);
_config.Persistence.DatabaseIntegrityCheckIntervalSeconds = Mathf.Clamp(_config.Persistence.DatabaseIntegrityCheckIntervalSeconds, 0f, 3600f);
if (_config.Persistence.DatabaseIntegrityCheckIntervalSeconds > 0f && _config.Persistence.DatabaseIntegrityCheckIntervalSeconds < 10f)
{
_config.Persistence.DatabaseIntegrityCheckIntervalSeconds = 10f;
}
if (!_config.Persistence.UseMineLocationDatabase)
{
PrintWarning("Mine location database is disabled. Vehicle Mines will scan BaseNetworkable.serverEntities on every load; this mode is supported but NOT PERFORMANT on large servers.");
}
if (_config.Persistence.RemoveAllMinesOnLoadReloadAndUnload)
{
PrintWarning("Remove-all lifecycle mode is enabled. Deployed Vehicle Mines and their database records are purged on plugin load, reload, and unload instead of being restored.");
}
_config.ImageLibraryIcons.ScopeNote = string.IsNullOrWhiteSpace(_config.ImageLibraryIcons.ScopeNote)
? new ImageLibraryIconSettings().ScopeNote
: _config.ImageLibraryIcons.ScopeNote.Trim();
_config.ImageLibraryIcons.AntiVehicleIconUrl = (_config.ImageLibraryIcons.AntiVehicleIconUrl ?? string.Empty).Trim();
_config.ImageLibraryIcons.SeaMineIconUrl = (_config.ImageLibraryIcons.SeaMineIconUrl ?? string.Empty).Trim();
_config.ImageLibraryIcons.DepthChargeIconUrl = (_config.ImageLibraryIcons.DepthChargeIconUrl ?? string.Empty).Trim();
if (_config.Items.AntiVehicleSkinId == 0UL || _config.Items.SeaSkinId == 0UL || _config.Items.DepthChargeSkinId == 0UL)
{
PrintWarning("All custom mine items should use non-zero, unique skin IDs. Placeholder IDs are acceptable for identity, but published Workshop skin IDs are required for custom inventory artwork.");
}
if (_config.Items.AntiVehicleSkinId == 0UL)
{
_config.Items.AntiVehicleSkinId = 9100000000001UL;
}
HashSet<ulong> configuredMineSkins = new HashSet<ulong> { _config.Items.AntiVehicleSkinId };
if (_config.Items.SeaSkinId == 0UL || !configuredMineSkins.Add(_config.Items.SeaSkinId))
{
PrintError("The Sea Mine skin ID was zero or duplicated another mine type. Resetting it to a unique placeholder.");
_config.Items.SeaSkinId = 9100000000002UL;
while (!configuredMineSkins.Add(_config.Items.SeaSkinId))
{
_config.Items.SeaSkinId++;
}
}
if (_config.Items.DepthChargeSkinId == 0UL || !configuredMineSkins.Add(_config.Items.DepthChargeSkinId))
{
PrintError("The Depth Charge skin ID was zero or duplicated another mine type. Resetting it to a unique placeholder.");
_config.Items.DepthChargeSkinId = 9100000000003UL;
while (!configuredMineSkins.Add(_config.Items.DepthChargeSkinId))
{
_config.Items.DepthChargeSkinId++;
}
}
_config.Placement.Distance = Mathf.Clamp(_config.Placement.Distance, 1f, 20f);
_config.Placement.CooldownSeconds = Mathf.Max(0.1f, _config.Placement.CooldownSeconds);
if (_config.Placement.LegacySeaBarrelRollDegrees.HasValue)
{
_config.Placement.SeaBarrelPitchDegrees = _config.Placement.LegacySeaBarrelRollDegrees.Value;
_config.Placement.LegacySeaBarrelRollDegrees = null;
}
_config.Placement.SeaBarrelPitchDegrees = Mathf.Repeat(_config.Placement.SeaBarrelPitchDegrees + 180f, 360f) - 180f;
_config.Placement.SeaMinimumWaterDepth = Mathf.Max(0.05f, _config.Placement.SeaMinimumWaterDepth);
if (float.IsNaN(_config.Placement.FixedOceanSeaLevelY) || float.IsInfinity(_config.Placement.FixedOceanSeaLevelY))
{
_config.Placement.FixedOceanSeaLevelY = 0f;
}
_config.Placement.FixedOceanSeaLevelY = Mathf.Clamp(_config.Placement.FixedOceanSeaLevelY, -1000f, 1000f);
_config.Placement.DepthChargeMinimumWaterDepth = Mathf.Max(10f, _config.Placement.DepthChargeMinimumWaterDepth);
_config.Placement.DepthChargeMinimumSelectableDepth = Mathf.Clamp(_config.Placement.DepthChargeMinimumSelectableDepth, 5f, _config.Placement.DepthChargeMinimumWaterDepth);
_config.Placement.DepthChargeDefaultDepth = Mathf.Max(_config.Placement.DepthChargeMinimumSelectableDepth, _config.Placement.DepthChargeDefaultDepth);
_config.Placement.DepthChargeSeabedClearance = Mathf.Clamp(_config.Placement.DepthChargeSeabedClearance, 0f, 2f);
_config.Placement.DepthChargePlacementTimeoutSeconds = Mathf.Clamp(_config.Placement.DepthChargePlacementTimeoutSeconds, 5f, 120f);
_config.Placement.DepthChargeMaximumPlayerMovement = Mathf.Clamp(_config.Placement.DepthChargeMaximumPlayerMovement, 0.5f, 20f);
_config.Placement.DepthChargeMaximumRaycastDepth = Mathf.Clamp(_config.Placement.DepthChargeMaximumRaycastDepth, 25f, 2000f);
_config.Placement.DrySurfaceWaterTolerance = Mathf.Clamp(_config.Placement.DrySurfaceWaterTolerance, 0f, 0.25f);
_config.Salvage.RequiredHits = Mathf.Max(1, _config.Salvage.RequiredHits);
DeduplicateStrings(_config.Compatibility.NpcVehicleTokens);
DeduplicateStrings(_config.AntiVehicle.VehicleTokens);
DeduplicateStrings(_config.Sea.VehicleTokens);
DeduplicateStrings(_config.DepthCharge.VehicleTokens);
DeduplicateDrops(_config.Salvage.AntiVehicleDrops);
DeduplicateDrops(_config.Salvage.SeaDrops);
DeduplicateDrops(_config.Salvage.DepthChargeDrops);
ValidateMineSettings(_config.AntiVehicle, "Anti-Vehicle Mine");
ValidateMineSettings(_config.Sea, "Sea Mine");
ValidateMineSettings(_config.DepthCharge, "Depth Charge");
ValidateDrops(_config.Salvage.AntiVehicleDrops, "Anti-Vehicle Mine");
ValidateDrops(_config.Salvage.SeaDrops, "Sea Mine");
ValidateDrops(_config.Salvage.DepthChargeDrops, "Depth Charge");
// Iterate over a snapshot of the keys. Assigning a value through the dictionary
// indexer changes the dictionary vers, even when replacing an existing key,
// which invalidates a live Dictionary enum.
foreach (string crateKey in _config.Loot.Crates.Keys.ToList())
{
CrateSettings crate;
if (!_config.Loot.Crates.TryGetValue(crateKey, out crate) || crate == null)
{
crate = new CrateSettings();
_config.Loot.Crates[crateKey] = crate;
}
if (crate.PrefabMatchTokens == null) crate.PrefabMatchTokens = new List<string>();
crate.SpawnChancePercent = Mathf.Clamp(crate.SpawnChancePercent, 0f, 100f);
crate.MinimumAmount = Mathf.Clamp(crate.MinimumAmount, 1, _config.Safety.MaximumLootQuantityPerCrate);
crate.MaximumAmount = Mathf.Clamp(crate.MaximumAmount, crate.MinimumAmount, _config.Safety.MaximumLootQuantityPerCrate);
// ... [Code preview truncated due to length. Full source in attached file] ...