C#:
/*
* Copyright (c) 2025 Bazz3l
*
* Guarded Crate cannot be copied, edited and/or (re)distributed without the express permission of Bazz3l.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
using System.Collections.Generic;
using System.Globalization;
using System.Collections;
using System.Text;
using System.Linq;
using System;
using System.Diagnostics;
using Oxide.Core.Plugins;
using Oxide.Core;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json;
using GuardedCrateExt;
using UnityEngine;
using Rust;
namespace Oxide.Plugins {
[Info("Guarded Crate", "Bazz3l", "2.0.13")]
[Description("Spawn custom locked crate events guarded by scientists, eliminate the threats to gain high value loot before the time runs out.")]
internal class GuardedCrate : RustPlugin {
[PluginReference]
private Plugin NpcSpawn, Clans, ZoneManager, HackableLock, GUIAnnouncements = null;
#region Fields
private const string PERM_USE = "guardedcrate.use";
private const string MARKER_PREFAB = "assets/prefabs/tools/map/genericradiusmarker.prefab";
private const string CRATE_PREFAB = "assets/prefabs/deployable/chinooklockedcrate/codelockedhackablecrate.prefab";
private const string PLANE_PREFAB = "assets/prefabs/npc/cargo plane/cargo_plane.prefab";
private readonly object _objFalse = false;
private readonly object _objTrue = true;
private StoredData _storedData;
private static ConfigData _configData;
private static GuardedCrate _plugin;
private static Func<string, string, string> _getMessage = null;
private static Func<ulong, string> _tryFetchTag = null;
private static Func<Vector3, JObject, ScientistNPC> _trySpawnNpc = null;
private static Func<BasePlayer, HackableLockedCrate, object> _tryLockHackable = null;
private static Func<string, string, string, BasePlayer, float, object> _sendGuiNotification = null;
#endregion
#region Config
protected override void LoadDefaultConfig() {
_configData = ConfigData.DefaultConfig();
_configData.Version = Version;
}
protected override void LoadConfig() {
base.LoadConfig();
try {
_configData = Config.ReadObject<ConfigData>();
if (_configData == null) throw new JsonException();
bool hasChanged = false;
if (string.IsNullOrEmpty(_configData.CommandName)) {
_configData.CommandName = "gcrate";
_configData.Version = Version;
hasChanged = true;
}
if (_configData.AutoStartDuration <= 0.0f) {
_configData.AutoStartDuration = 1800f;
_configData.Version = Version;
hasChanged = true;
}
if (_configData.MessageSettings == null) {
_configData.MessageSettings = ConfigData.GetDefaultMessageSettings();
_configData.Version = Version;
hasChanged = true;
}
if (_configData.ZoneManagerSettings == null) {
_configData.ZoneManagerSettings = ConfigData.GetDefaultZoneManagerSettings();
_configData.Version = Version;
hasChanged = true;
}
if (_configData.Events == null || _configData.Events.Count == 0) {
_configData.Events = ConfigData.GetDefaultEvents();
_configData.Version = Version;
hasChanged = true;
}
if (_configData.BlockedTopologies == null) {
_configData.BlockedTopologies = ConfigData.GetDefaultTopologies();
_configData.Version = Version;
hasChanged = true;
}
if (_configData.EventSelectionSettings == null) {
_configData.EventSelectionSettings = ConfigData.GetDefaultEventSelectionSettings();
_configData.Version = Version;
hasChanged = true;
}
_configData.EventSelectionSettings ??= ConfigData.GetDefaultEventSelectionSettings();
_configData.Events ??= ConfigData.GetDefaultEvents();
_configData.EventSelectionSettings.EnsureEntries(_configData.Events.Select(x => x.EventName));
if (hasChanged) {
SaveConfig();
PrintWarning("Config has been updated.");
}
}
catch (Exception e) {
LoadDefaultConfig();
PrintWarning("Loaded default config.");
}
}
protected override void SaveConfig() => Config.WriteObject(_configData, true);
private class ConfigData {
[JsonProperty("CommandName: used to manage and setup events.")]
public string CommandName;
[JsonProperty("EnableAutoStart: enabling auto start will spawn events at a specified duration.")]
public bool AutoStartEnabled;
[JsonProperty("AutoStartDuration: auto start duration specify in seconds.")]
public float AutoStartDuration;
[JsonProperty("MessageSettings: message notification settings")]
public MessageSettings MessageSettings;
[JsonProperty("ZoneManagerSettings: zone manager settings")]
public ZoneMangerSettings ZoneManagerSettings;
[JsonConverter(typeof(EnumCollectionStringConverter<TerrainBiome.Enum>))]
[JsonProperty("BlockedTopologies: specifying topologies will prevent events spawning in these areas")]
public List<TerrainBiome.Enum> BlockedTopologies;
[JsonProperty("EventSelectionSettings: controls which event tiers can be auto-selected and how often")]
public EventSelectionSettings EventSelectionSettings;
[JsonProperty("Events: full event profiles and guard tuning")]
public List<EventEntry> Events;
[JsonProperty("VersionNumber: current version of the plugin, do not change this!!!")]
public VersionNumber? Version;
public static ConfigData DefaultConfig() {
return new ConfigData {
CommandName = "gcrate", AutoStartEnabled = true, AutoStartDuration = 3600, MessageSettings = ConfigData.GetDefaultMessageSettings(), ZoneManagerSettings = ConfigData.GetDefaultZoneManagerSettings(), BlockedTopologies = ConfigData.GetDefaultTopologies(), EventSelectionSettings = ConfigData.GetDefaultEventSelectionSettings(), Events = ConfigData.GetDefaultEvents(),
}
;
}
public static MessageSettings GetDefaultMessageSettings() {
return new MessageSettings {
EnableToast = false, EnableChat = true, EnableChatPrefix = true, ChatIcon = 76561199542824781, EnableGuiAnnouncements = false, GuiAnnouncementsBgColor = "Purple", GuiAnnouncementsTextColor = "White"
}
;
}
public static ZoneMangerSettings GetDefaultZoneManagerSettings() {
return new ZoneMangerSettings {
EnabledIgnoredZones = false, IgnoredZones = new List<string>()
}
;
}
public static List<TerrainBiome.Enum> GetDefaultTopologies() {
return new List<TerrainBiome.Enum> {
TerrainBiome.Enum.Arctic, TerrainBiome.Enum.Jungle
}
;
}
public static EventSelectionSettings GetDefaultEventSelectionSettings() {
return new EventSelectionSettings {
UseWeightedRandomSelection = true, EventWeights = new List<EventWeightEntry> {
new() {
EventName = "Easy", Weight = 50f, Enabled = true
}
, new() {
EventName = "Medium", Weight = 30f, Enabled = true
}
, new() {
EventName = "Hard", Weight = 15f, Enabled = true
}
, new() {
EventName = "Elite", Weight = 5f, Enabled = true
}
,
}
}
;
}
public static List<EventEntry> GetDefaultEvents() {
return DefaultEvents() .Select(defaultEvent => CreateDefaultEvent(defaultEvent.Name, defaultEvent.HackSeconds, defaultEvent.MarkerColor, defaultEvent.Guards, defaultEvent.Weapon, defaultEvent.Health)) .ToList();
}
}
private class EventSelectionSettings {
[JsonProperty("use weighted random selection for automatic event picks")]
public bool UseWeightedRandomSelection;
[JsonProperty("event weight entries")]
public List<EventWeightEntry> EventWeights;
[JsonIgnore]
public bool HasValidWeights => EventWeights != null && EventWeights.Count > 0 && EventWeights.Any(x => x != null && x.Enabled && x.Weight > 0f);
public void EnsureEntries(IEnumerable<string> eventNames) {
EventWeights ??= new List<EventWeightEntry>();
foreach (string eventName in eventNames) {
if (string.IsNullOrWhiteSpace(eventName)) continue;
if (EventWeights.Any(x => x != null && x.EventName.Equals(eventName, StringComparison.OrdinalIgnoreCase))) continue;
EventWeights.Add(new EventWeightEntry {
EventName = eventName, Weight = 1f, Enabled = true
}
);
}
}
}
private class EventWeightEntry {
[JsonProperty("event name")]
public string EventName;
[JsonProperty("selection weight")]
public float Weight;
[JsonProperty("enabled")]
public bool Enabled;
}
private class GuardSpawnSettings {
[JsonProperty("use random guard amount between min and max")]
public bool UseRandomAmount;
[JsonProperty("minimum guard amount")]
public int MinAmount;
[JsonProperty("maximum guard amount")]
public int MaxAmount;
public void Normalize(int fallbackAmount = 1) {
if (fallbackAmount < 1) fallbackAmount = 1;
if (MinAmount <= 0 && MaxAmount <= 0) {
MinAmount = fallbackAmount;
MaxAmount = fallbackAmount;
}
if (MinAmount <= 0) MinAmount = MaxAmount > 0 ? MaxAmount : fallbackAmount;
if (MaxAmount <= 0) MaxAmount = MinAmount > 0 ? MinAmount : fallbackAmount;
if (MinAmount > MaxAmount) (MinAmount, MaxAmount) = (MaxAmount, MinAmount);
if (MinAmount < 1) MinAmount = 1;
if (MaxAmount < 1) MaxAmount = 1;
}
public int GetSpawnAmount() {
Normalize();
return UseRandomAmount ? UnityEngine.Random.Range(MinAmount, MaxAmount + 1) : MaxAmount;
}
public static GuardSpawnSettings CreateFixed(int amount) {
amount = Mathf.Max(1, amount);
return new GuardSpawnSettings {
UseRandomAmount = false, MinAmount = amount, MaxAmount = amount
}
;
}
}
private class MessageSettings {
[JsonProperty("enable toast message")]
public bool EnableToast;
[JsonProperty("enable chat message")]
public bool EnableChat;
[JsonProperty("enable chat prefix")]
public bool EnableChatPrefix;
[JsonProperty("custom chat message icon (steam64)")]
public ulong ChatIcon;
[JsonProperty("enable gui announcements plugin from umod.org")]
public bool EnableGuiAnnouncements;
[JsonProperty("gui announcements text color")]
public string GuiAnnouncementsTextColor;
[JsonProperty("gui announcements background color")]
public string GuiAnnouncementsBgColor;
}
private class ZoneMangerSettings {
[JsonProperty("enable ignored zones")]
public bool EnabledIgnoredZones;
[JsonProperty("ignore these zone ids, leave empty to exclude all zones")]
public List<string> IgnoredZones;
}
private static List<EventEntry> CloneEvents(IEnumerable<EventEntry> source) {
return source?.Select(CloneEventEntry).Where(x => x != null).ToList() ?? new List<EventEntry>();
}
private static EventEntry CloneEventEntry(EventEntry source) {
if (source == null) return null;
return new EventEntry {
EventName = source.EventName, EventDuration = source.EventDuration, EnableLockToPlayer = source.EnableLockToPlayer, EnableClanTag = source.EnableClanTag, EnableAutoHack = source.EnableAutoHack, HackableCrateHackSeconds = source.HackableCrateHackSeconds, HackableCrateFallDrag = source.HackableCrateFallDrag, EnableMarker = source.EnableMarker, MapMarkerColor1 = source.MapMarkerColor1, MapMarkerColor2 = source.MapMarkerColor2, MapMarkerRadius = source.MapMarkerRadius, MapMarkerOpacity = source.MapMarkerOpacity, EnableLootTable = source.EnableLootTable, LootMinAmount = source.LootMinAmount, LootMaxAmount = source.LootMaxAmount, EnableEliminateGuards = source.EnableEliminateGuards, GuardAmount = source.GuardAmount, GuardSpawnSettings = CloneGuardSpawnSettings(source.GuardSpawnSettings, source.GuardAmount), GuardConfig = CloneGuardConfig(source.GuardConfig), LootTable = source.LootTable?.Select(CloneItemEntry).ToList() ?? new List<ItemEntry>()
}
;
}
private static ItemEntry CloneItemEntry(ItemEntry source) {
if (source == null) return null;
return new ItemEntry {
DisplayName = source.DisplayName, Shortname = source.Shortname, SkinID = source.SkinID, MinAmount = source.MinAmount, MaxAmount = source.MaxAmount
}
;
}
private static GuardSpawnSettings CloneGuardSpawnSettings(GuardSpawnSettings source, int fallbackAmount) {
GuardSpawnSettings clone = source != null ? new GuardSpawnSettings {
UseRandomAmount = source.UseRandomAmount, MinAmount = source.MinAmount, MaxAmount = source.MaxAmount
}
: GuardSpawnSettings.CreateFixed(fallbackAmount);
clone.Normalize(fallbackAmount);
return clone;
}
private static GuardConfig CloneGuardConfig(GuardConfig source) {
if (source == null) return null;
GuardConfig clone = new GuardConfig {
Name = source.Name, WearItems = source.WearItems?.Select(x => new GuardConfig.WearEntry {
ShortName = x.ShortName, SkinID = x.SkinID
}
).ToList() ?? new List<GuardConfig.WearEntry>(), BeltItems = source.BeltItems?.Select(x => new GuardConfig.BeltEntry {
ShortName = x.ShortName, SkinID = x.SkinID, Amount = x.Amount, Ammo = x.Ammo, Mods = x.Mods != null ? new List<string>(x.Mods) : new List<string>()
}
).ToList() ?? new List<GuardConfig.BeltEntry>(), Kit = source.Kit, Health = source.Health, RoamRange = source.RoamRange, ChaseRange = source.ChaseRange, SenseRange = source.SenseRange, AttackRangeMultiplier = source.AttackRangeMultiplier, CheckVisionCone = source.CheckVisionCone, VisionCone = source.VisionCone, DamageScale = source.DamageScale, TurretDamageScale = source.TurretDamageScale, AimConeScale = source.AimConeScale, Speed = source.Speed, SleepDistance = source.SleepDistance, MemoryDuration = source.MemoryDuration, HostileTargetsOnly = source.HostileTargetsOnly, DisableRadio = source.DisableRadio, CanRunAwayWater = source.CanRunAwayWater, CanSleep = source.CanSleep, CanRaid = source.CanRaid, Stationary = source.Stationary, AboveOrUnderGround = source.AboveOrUnderGround
}
;
if (source.Parsed != null) clone.CacheConfig();
return clone;
}
private void SyncConfiguredEventsToData(bool save = true) {
if (_configData?.Events == null || _configData.Events.Count == 0) return;
_storedData ??= new StoredData();
_storedData.CrateEventEntries = CloneEvents(_configData.Events);
if (_configData.EventSelectionSettings != null) {
_configData.EventSelectionSettings.EnsureEntries(_storedData.EventNames);
if (save) SaveConfig();
}
if (save) SaveData();
}
private void SyncEventToConfig(EventEntry sourceEvent, bool save = true) {
if (sourceEvent == null || _configData?.Events == null) return;
int index = _configData.Events.FindIndex(x => x != null && x.EventName.Equals(sourceEvent.EventName, StringComparison.OrdinalIgnoreCase));
if (index < 0) {
_configData.Events.Add(CloneEventEntry(sourceEvent));
}
else {
_configData.Events[index] = CloneEventEntry(sourceEvent);
}
if (_configData.EventSelectionSettings != null) _configData.EventSelectionSettings.EnsureEntries(_configData.Events.Select(x => x.EventName));
if (save) SaveConfig();
}
#endregion
#region Storage
private static (string Name, float HackSeconds, string MarkerColor, int Guards, string Weapon, float Health)[] DefaultEvents() {
return new (string Name, float HackSeconds, string MarkerColor, int Guards, string Weapon, float Health)[] {
("Easy", 60f, "#32A844", 8, "smg.mp5", 200f), ("Medium", 120f, "#EDDF45", 10, "smg.mp5", 250f), ("Hard", 180f, "#3060D9", 12, "rifle.ak", 300f), ("Elite", 180f, "#3060D9", 20, "rifle.ak", 300f)
}
;
}
private void LoadDefaultData() {
_storedData = new StoredData();
foreach (var defaultEvent in DefaultEvents()) {
_storedData.CrateEventEntries.Add(CreateDefaultEvent(defaultEvent.Name, defaultEvent.HackSeconds, defaultEvent.MarkerColor, defaultEvent.Guards, defaultEvent.Weapon, defaultEvent.Health) );
}
_configData?.EventSelectionSettings?.EnsureEntries(_storedData.EventNames);
SaveConfig();
SaveData();
}
private void LoadData() {
try {
_storedData = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(Name);
if (_storedData is not {
IsValid : true
}
) throw new Exception();
foreach (EventEntry eventEntry in _storedData.CrateEventEntries) eventEntry?.Normalize();
}
catch {
_storedData = new StoredData();
PrintWarning("Loaded default data.");
}
SyncConfiguredEventsToData();
}
private void SaveData() {
if (_storedData is not {
IsValid : true
}
) return;
Interface.Oxide.DataFileSystem.WriteObject(Name, _storedData);
}
private class StoredData {
public List<EventEntry> CrateEventEntries = new();
[JsonIgnore]
private string[] _eventNames;
[JsonIgnore]
public string[] EventNames {
get {
return _eventNames ??= CrateEventEntries .Select(x => x.EventName) .ToArray();
}
}
[JsonIgnore]
public bool IsValid => CrateEventEntries != null && CrateEventEntries.Count > 0;
public EventEntry FindEventByName(string eventName) {
return CrateEventEntries.Find(x => x.EventName.Equals(eventName, StringComparison.OrdinalIgnoreCase));
}
}
private class EventEntry {
[JsonProperty("event display name)")]
public string EventName;
[JsonProperty("event duration")]
public float EventDuration;
[JsonProperty("enable lock to player when completing the event")]
public bool EnableLockToPlayer;
[JsonProperty("enable clan tag")]
public bool EnableClanTag;
[JsonProperty("enable auto hacking of crate when an event is finished")]
public bool EnableAutoHack;
[JsonProperty("hackable locked crate")]
public float HackableCrateHackSeconds;
[JsonProperty("hackable crate fall drag")]
public float HackableCrateFallDrag;
[JsonProperty("enable marker")]
public bool EnableMarker;
[JsonProperty("marker color 1")]
public string MapMarkerColor1;
[JsonProperty("marker color 2")]
public string MapMarkerColor2;
[JsonProperty("marker radius")]
public float MapMarkerRadius;
[JsonProperty("marker opacity")]
public float MapMarkerOpacity;
[JsonProperty("enable loot table")]
public bool EnableLootTable;
[JsonProperty("min loot items")]
public int LootMinAmount;
[JsonProperty("max loot items")]
public int LootMaxAmount;
[JsonProperty("enable eliminate all guards before looting")]
public bool EnableEliminateGuards;
[JsonProperty("guard spawn amount")]
public int GuardAmount;
[JsonProperty("guard spawn settings")]
public GuardSpawnSettings GuardSpawnSettings;
[JsonProperty("guard spawn config")]
public GuardConfig GuardConfig;
public void Normalize() {
if (GuardAmount < 1) GuardAmount = 1;
GuardSpawnSettings ??= GuardedCrate.GuardSpawnSettings.CreateFixed(GuardAmount);
GuardSpawnSettings.Normalize(GuardAmount);
GuardAmount = GuardSpawnSettings.MaxAmount;
}
[JsonProperty("create loot items")]
public List<ItemEntry> LootTable;
}
private class ItemEntry {
public string DisplayName;
public string Shortname;
public ulong SkinID = 0UL;
public int MinAmount;
public int MaxAmount;
public static List<ItemEntry> SaveItems(ItemContainer container) {
List<ItemEntry> items = new List<ItemEntry>();
foreach (Item item in container.itemList) {
items.Add(new ItemEntry {
DisplayName = item.name, Shortname = item.info.shortname, SkinID = item.skin, MinAmount = item.amount, MaxAmount = item.amount,
}
);
}
return items;
}
public Item CreateItem() {
Item item = ItemManager.CreateByName(Shortname, UnityEngine.Random.Range(MinAmount, MaxAmount), SkinID);
item.name = DisplayName;
item.MarkDirty();
return item;
}
}
private class GuardConfig {
public string Name;
public List<WearEntry> WearItems;
public List<BeltEntry> BeltItems;
public string Kit;
public float Health;
public float RoamRange;
public float ChaseRange;
public float SenseRange;
public float AttackRangeMultiplier;
public bool CheckVisionCone;
public float VisionCone;
public float DamageScale;
public float TurretDamageScale;
public float AimConeScale;
public float Speed;
public float SleepDistance;
public float MemoryDuration;
public bool HostileTargetsOnly;
public bool DisableRadio;
public bool CanRunAwayWater;
public bool CanSleep;
public bool CanRaid;
public bool Stationary;
public bool AboveOrUnderGround;
[JsonIgnore]
public string[] Kits;
[JsonIgnore]
public JObject Parsed;
public class BeltEntry {
public string ShortName;
public ulong SkinID;
public int Amount;
public string Ammo;
public List<string> Mods;
public static List<BeltEntry> SaveItems(ItemContainer container) {
List<BeltEntry> items = new List<BeltEntry>();
foreach (Item item in container.itemList) {
BeltEntry beltEntry = new BeltEntry {
ShortName = item.info.shortname, SkinID = item.skin, Amount = item.amount, Mods = new List<string>()
}
;
if (item.GetHeldEntity() is BaseProjectile projectile && projectile?.primaryMagazine != null && projectile.primaryMagazine.ammoType != null) beltEntry.Ammo = projectile.primaryMagazine.ammoType.shortname;
if (item?.contents?.itemList != null) {
foreach (Item itemContent in item.contents.itemList) beltEntry.Mods.Add(itemContent.info.shortname);
}
items.Add(beltEntry);
}
return items;
}
}
public class WearEntry {
public string ShortName;
public ulong SkinID;
public static List<WearEntry> SaveItems(ItemContainer container) {
List<WearEntry> items = new List<WearEntry>();
foreach (Item item in container.itemList) {
items.Add(new WearEntry {
ShortName = item.info.shortname, SkinID = item.skin
}
);
}
return items;
}
}
public void CacheConfig() {
Kits = Kit?.Split(',') ?? Array.Empty<string>();
Parsed = new JObject {
["Name"]
= Name, ["WearItems"] = new JArray {
WearItems.Select(x => new JObject {
["ShortName"]
= x.ShortName, ["SkinID"] = x.SkinID
}
)
}
, ["BeltItems"] = new JArray {
BeltItems.Select(x => new JObject {
["ShortName"]
= x.ShortName, ["Amount"] = x.Amount, ["SkinID"] = x.SkinID, ["Mods"] = new JArray {
x.Mods
}
, ["Ammo"] = x.Ammo
}
)
}
, ["Kit"] = Kit, ["Health"] = Health, ["RoamRange"] = RoamRange, ["ChaseRange"] = ChaseRange, ["SenseRange"] = SenseRange, ["ListenRange"] = SenseRange / 2f, ["AttackRangeMultiplier"] = AttackRangeMultiplier, ["CheckVisionCone"] = CheckVisionCone, ["VisionCone"] = VisionCone, ["HostileTargetsOnly"] = HostileTargetsOnly, ["DamageScale"] = DamageScale, ["TurretDamageScale"] = TurretDamageScale, ["AimConeScale"] = AimConeScale, ["DisableRadio"] = DisableRadio, ["CanRunAwayWater"] = CanRunAwayWater, ["CanSleep"] = CanSleep, ["SleepDistance"] = SleepDistance, ["Speed"] = Speed, ["AreaMask"] = !AboveOrUnderGround ? 1 : 25, ["AgentTypeID"] = !AboveOrUnderGround ? -1372625422 : 0, ["HomePosition"] = string.Empty, ["MemoryDuration"] = MemoryDuration, ["States"] = new JArray {
Stationary ? new HashSet<string> {
"IdleState", "CombatStationaryState"
}
: CanRaid ? new HashSet<string> {
"RoamState", "CombatState", "ChaseState", "RaidState"
}
: new HashSet<string> {
"RoamState", "CombatState", "ChaseState"
}
}
}
;
}
}
private static EventEntry CreateDefaultEvent(string name, float hackSeconds, string markerColor, int guardAmount, string weaponShortName, float guardHealth) {
return new EventEntry {
EventName = name, EventDuration = 1800f, EnableAutoHack = true, HackableCrateHackSeconds = hackSeconds, HackableCrateFallDrag = 1f, EnableLockToPlayer = true, EnableClanTag = true, EnableMarker = true, MapMarkerColor1 = markerColor, MapMarkerColor2 = "#000000", MapMarkerOpacity = 0.6f, MapMarkerRadius = 0.7f, EnableLootTable = false, LootMinAmount = 6, LootMaxAmount = 10, LootTable = new List<ItemEntry>(), EnableEliminateGuards = true, GuardAmount = guardAmount, GuardSpawnSettings = GuardSpawnSettings.CreateFixed(guardAmount), GuardConfig = CreateDefaultGuard($"{name} Guard", weaponShortName, guardHealth)
}
;
}
private static GuardConfig CreateDefaultGuard(string name, string shortname, float health) {
return new GuardConfig {
Name = name, WearItems = new List<GuardConfig.WearEntry> {
new() {
ShortName = "hazmatsuit_scientist_peacekeeper", SkinID = 0UL
}
}
, BeltItems = new List<GuardConfig.BeltEntry> {
new() {
ShortName = shortname, Amount = 1, SkinID = 0UL, Mods = new List<string>()
}
, new() {
ShortName = "syringe.medical", Amount = 10, SkinID = 0UL, Mods = new List<string>()
}
}
, Kit = "", Health = health, RoamRange = 5f, ChaseRange = 40f, SenseRange = 80f, AttackRangeMultiplier = 8f, VisionCone = 180f, CheckVisionCone = false, DamageScale = 1f, TurretDamageScale = 0.25f, AimConeScale = shortname == "rifle.ak" ? 0.15f : 0.25f, SleepDistance = 100f, Speed = 8.5f, MemoryDuration = 30f
}
;
}
#endregion
#region Lang
protected override void LoadDefaultMessages() {
lang.RegisterMessages(new Dictionary<string, string> {
{
LangKeys.NoPermission, "Sorry you don't have permission to do that."
}
, {
LangKeys.Prefix, "<color=#8a916f>Guarded Crate</color>:\n"
}
, {
LangKeys.FailedToStartEvent, "Failed to start event."
}
, {
LangKeys.StartEvent, "Event starting."
}
, {
LangKeys.ClearEvents, "Cleaning up all running events."
}
, {
LangKeys.EventStart, "Special delivery is on its way to <color=#e7cf85>{0}</color> watch out it is heavily contested by guards, severity level <color=#e7cf85>{1}</color>.\nBe fast before the event ends in <color=#e7cf85>{2}</color>."
}
, {
LangKeys.EventCompleted, "<color=#e7cf85>{1}</color> has cleared the event at <color=#e7cf85>{0}</color>."
}
, {
LangKeys.EventEnded, "Event ended at <color=#e7cf85>{0}</color>; You were not fast enough; better luck next time!"
}
, {
LangKeys.EventNotFound, "Event not found, please make sure you have typed the correct name."
}
, {
LangKeys.EventIntersecting, "Another event is intersecting this position."
}
, {
LangKeys.EventPositionInvalid, "Event position invalid."
}
, {
LangKeys.EliminateGuards, "The crate is still contested eliminate all guards to gain access to high-valued loot."
}
, {
LangKeys.EventUpdated, "Event updated, please reload the plugin to take effect."
}
, {
LangKeys.InvalidGuardAmount, "Invalid guard amount must be between {0} - {1}."
}
, {
LangKeys.HelpStartEvent, "<color=#e7cf85>/{0}</color> start \"<color=#e7cf85><{1}></color>\", start an event of a specified type."
}
, {
LangKeys.HelpStopEvent, "<color=#e7cf85>/{0}</color> stop, stop all currently running events.\n\n"
}
, {
LangKeys.HelpHereEvent, "<color=#e7cf85>/{0}</color> here \"<color=#e7cf85><event-name></color>\", start an event at your position\n\n"
}
, {
LangKeys.HelpPositionEvent, "<color=#e7cf85>/{0}</color> position \"<color=#e7cf85><event-name></color>\" \"<color=#e7cf85>x y z</color>\", start an event at a specified position.\n\n"
}
, {
LangKeys.HelpLootEvent, "<color=#e7cf85>/{0}</color> loot \"<color=#e7cf85><event-name></color>\", create loot items that you wish to spawn in the crate, add the items to your inventory and run the command.\n\n"
}
, {
LangKeys.HelpDragEvent, "<color=#e7cf85>/{0}</color> drag \"<color=#e7cf85><event-name></color>\", specify the amount of drag the crate should have while falling.\n\n"
}
, {
LangKeys.HelpGuardAmount, "<color=#e7cf85>/{0}</color> amount \"<color=#e7cf85><event-name></color>\", specify the guard amount to spawn.\n\n"
}
, {
LangKeys.HelpGuardLoadout, "<color=#e7cf85>/{0}</color> loadout \"<color=#e7cf85><event-name></color>\", set guard loadout using items in your inventory."
}
}
, this);
}
private static class LangKeys {
public const string Prefix = "Prefix";
public const string NoPermission = "NoPermission";
public const string FailedToStartEvent = "FailedToStartEvent";
public const string ClearEvents = "ClearEvents";
public const string StartEvent = "StartEvent";
public const string EventCompleted = "EventCompleted";
public const string EventStart = "EventStart";
public const string EventEnded = "EventClear";
public const string EventNotFound = "EventNotFound";
public const string EventPositionInvalid = "EventPosInvalid";
public const string EventIntersecting = "EventNearby";
public const string EventUpdated = "EventUpdated";
public const string EliminateGuards = "EliminateGuards";
public const string InvalidGuardAmount = "InvalidGuardAmount";
public const string HelpStartEvent = "HelpStartEvent";
public const string HelpStopEvent = "HelpStopEvent";
public const string HelpHereEvent = "HelpHereEvent";
public const string HelpPositionEvent = "HelpPositionEvent";
public const string HelpLootEvent = "HelpLootEvent";
public const string HelpDragEvent = "HelpDragEvent";
public const string HelpGuardAmount = "HelpGuardAmount";
public const string HelpGuardLoadout = "HelpGuardLoadout";
}
#endregion
#region Oxide Hooks
private void OnServerInitialized() {
SpawnManager.Instance.FindSpawnPoints();
if (!string.IsNullOrEmpty(_configData.CommandName)) {
cmd.AddConsoleCommand(_configData.CommandName, this, nameof(EventConsoleCommands));
cmd.AddChatCommand(_configData.CommandName, this, nameof(EventCommandCommands));
}
if (_configData.AutoStartEnabled && _configData.AutoStartDuration > 0) timer.Every(_configData.AutoStartDuration, () => TryStartEvent(null));
}
private void Init() {
permission.RegisterPermission(PERM_USE, this);
_plugin = this;
_getMessage = (key, userId) => lang.GetMessage(key, this, userId);
_tryFetchTag = TryFetchTag;
_trySpawnNpc = TrySpawnNpc;
_tryLockHackable = TryLockHackable;
_sendGuiNotification = SendGuiNotification;
LoadData();
SpawnManager.Initialize(_configData.BlockedTopologies);
}
private void Unload() {
try {
EventManager.OnUnload();
SpawnManager.OnUnload();
EntityCache.OnUnload();
}
finally {
_plugin = null;
_configData = null;
}
}
private void OnEntityDeath(ScientistNPC scientist, HitInfo info) {
NetworkableId id = scientist.net?.ID ?? default;
EntityCache.FindEntityEventInstance(id)?.OnGuardKilled(scientist, info?.InitiatorPlayer);
EntityCache.RemoveEntity(id);
}
private void OnEntityKill(ScientistNPC scientist) {
NetworkableId id = scientist.net?.ID ?? default;
EntityCache.FindEntityEventInstance(id)?.OnGuardKilled(scientist, null);
EntityCache.RemoveEntity(id);
}
private void OnEntityKill(LootContainer container) {
NetworkableId id = container.net?.ID ?? default;
EntityCache.FindEntityEventInstance(id)?.OnCrateKilled(container);
EntityCache.RemoveEntity(id);
}
private void OnEntityKill(CargoPlane plane) {
NetworkableId id = plane.net?.ID ?? default;
EntityCache.FindEntityEventInstance(id)?.OnPlaneKilled(plane);
EntityCache.RemoveEntity(id);
}
private object CanHackCrate(BasePlayer player, HackableLockedCrate crate) {
NetworkableId id = crate.net?.ID ?? default;
return EntityCache.FindEntityEventInstance(id)?.CanHackCrate(player);
}
#endregion
#region Spawn Manager
private class SpawnManager : IDisposable {
public static SpawnManager Instance {
get;
private set;
}
private const int BLOCKED_TERRAIN_TOPOLOGIES = (int)(TerrainTopology.Enum.Cliff | TerrainTopology.Enum.Cliffside | TerrainTopology.Enum.Beach | TerrainTopology.Enum.Beachside | TerrainTopology.Enum.Ocean | TerrainTopology.Enum.Oceanside | TerrainTopology.Enum.Monument | TerrainTopology.Enum.Building | TerrainTopology.Enum.River | TerrainTopology.Enum.Riverside | TerrainTopology.Enum.Lake | TerrainTopology.Enum.Lakeside);
private readonly int _blockedLayerMasks = LayerMask.GetMask("Tree", "Construction", "Prevent_Building", "Vehicle_World", "Vehicle_Large", "Vehicle_Detailed");
private readonly List<ZoneInfo> _ignoreZones = new();
private readonly Queue<Vector3> _spawnPoints = new();
private readonly Stopwatch _stopwatch = new();
private TerrainBiome.Enum _blockedTerrainBiomes = 0;
private bool _hasBlockedTerrainBiomes;
private Coroutine _routine;
private int _spawnPointAttempts;
private int _spawnPointCount;
public static void Initialize(List<TerrainBiome.Enum> blockedTerrainBiomes) {
Instance = new SpawnManager();
Instance._hasBlockedTerrainBiomes = blockedTerrainBiomes.Count > 0;
foreach (TerrainBiome.Enum blockedTopology in blockedTerrainBiomes) Instance._blockedTerrainBiomes |= blockedTopology;
Instance.GetIgnoredZones();
}
public static void OnUnload() {
Instance.Dispose();
Instance = null;
}
public void Dispose() {
CleanupSpawnsRoutine();
_ignoreZones.Clear();
_spawnPoints.Clear();
}
private IEnumerator GenerateSpawnsRoutine(int maxSpawnPoints = 100, int maxSpawnAttempts = 5000, float frameBudget = 0.10f) {
yield return CoroutineEx.waitForEndOfFrame;
Interface.Oxide.LogDebug("GuardedCrate: generating spawn points.");
float mapSizeX = TerrainMeta.Size.x / 2;
float mapSizeZ = TerrainMeta.Size.z / 2;
Vector3 spawnPoint = Vector3.zero;
_spawnPointAttempts = 0;
_spawnPointCount = 0;
while (_spawnPointCount < maxSpawnPoints) {
_stopwatch.Reset();
_stopwatch.Start();
for (int index = _spawnPointCount; index < maxSpawnPoints; ++index) {
spawnPoint.x = UnityEngine.Random.Range(-mapSizeX, mapSizeX);
spawnPoint.z = UnityEngine.Random.Range(-mapSizeZ, mapSizeZ);
spawnPoint.y = WaterLevel.GetWaterOrTerrainSurface(spawnPoint, false, false);
++_spawnPointAttempts;
if (TestSpawnPoint(ref spawnPoint)) {
_spawnPoints.Enqueue(spawnPoint);
++_spawnPointCount;
}
if (_stopwatch.Elapsed.TotalMilliseconds >= frameBudget) break;
}
if (_spawnPointAttempts >= maxSpawnAttempts) break;
yield return CoroutineEx.waitForEndOfFrame;
yield return CoroutineEx.waitForEndOfFrame;
}
_stopwatch.Stop();
Interface.Oxide.LogDebug("GuardedCrate: successfully found {0} spawn points.", _spawnPoints.Count);
_routine = null;
}
private void CleanupSpawnsRoutine() {
if (_routine == null) return;
ServerMgr.Instance.StopCoroutine(_routine);
_routine = null;
}
public void FindSpawnPoints() {
if (_routine != null) return;
_routine = ServerMgr.Instance.StartCoroutine(GenerateSpawnsRoutine());
}
public Vector3 GetSpawnPoint() {
Vector3 spawnPoint = Vector3.zero;
while (_spawnPoints.Count > 0) {
spawnPoint = _spawnPoints.Dequeue();
if (IsSpawnPointValid(spawnPoint)) break;
}
if (_routine == null && _spawnPoints.Count <= 2) {
FindSpawnPoints();
spawnPoint = Vector3.zero;
}
return spawnPoint;
}
private void GetIgnoredZones() {
if (!_configData.ZoneManagerSettings.EnabledIgnoredZones) return;
if (_plugin?.ZoneManager == null) return;
ZoneMangerSettings zoneMangerSettings = _configData?.ZoneManagerSettings;
if (!(zoneMangerSettings?.IgnoredZones?.Count > 0)) return;
if (_plugin.ZoneManager.Call("GetZoneIDs") is not string[] zoneIds || zoneIds.Length == 0) return;
foreach (string zoneId in zoneIds) {
if (!zoneMangerSettings.IgnoredZones.Contains(zoneId)) continue;
if (_plugin.ZoneManager.Call("GetZoneLocation", zoneId) is not Vector3 position) continue;
if (_plugin.ZoneManager.Call("GetZoneRadius", zoneId) is not float radius) continue;
_ignoreZones.Add(new ZoneInfo(position, radius));
}
}
private bool TestSpawnPoint(ref Vector3 spawnPoint) {
if (!Physics.Raycast(spawnPoint.WithY(spawnPoint.y + 100f), Vector3.down, out RaycastHit hit, 100f, 1218652417, QueryTriggerInteraction.Ignore)) return false;
spawnPoint.y = hit.point.y;
if (AntiHack.TestInsideTerrain(spawnPoint)) return false;
if (AntiHack.IsInsideMesh(spawnPoint)) return false;
if (IsInterceptingPosition(spawnPoint)) return false;
if (IsInterceptingIgnored(spawnPoint)) return false;
if (IsBlockedTerrainTopology(spawnPoint)) return false;
if (IsBlockedTerrainBiome(spawnPoint)) return false;
return IsSpawnPointValid(spawnPoint);
}
private bool IsSpawnPointValid(Vector3 spawnPoint) {
if (WaterLevel.Test(spawnPoint, false, false)) return false;
if (!IsValidCollider(spawnPoint)) return false;
if (HasPlayersNearby(spawnPoint)) return false;
return !HasBuildingNearby(spawnPoint);
}
private bool IsBlockedTerrainTopology(Vector3 spawnPoint) {
return (TerrainMeta.TopologyMap.GetTopology(spawnPoint) & (int)BLOCKED_TERRAIN_TOPOLOGIES) != 0;
}
private bool IsBlockedTerrainBiome(Vector3 spawnPoint) {
return _hasBlockedTerrainBiomes && TerrainMeta.BiomeMap.GetBiome(spawnPoint, (int)_blockedTerrainBiomes) != 0.0f;
}
private bool IsInterceptingIgnored(Vector3 spawnPoint) {
foreach (ZoneInfo zoneInfo in _ignoreZones) {
if (zoneInfo.IsInBounds(spawnPoint)) return true;
}
return false;
}
private bool IsInterceptingPosition(Vector3 spawnPoint) {
const float distance = 100f;
foreach (Vector3 pos in _spawnPoints) {
if (Vector3.Distance(pos, spawnPoint) <= distance) return true;
}
return false;
}
private bool IsValidCollider(Vector3 spawnPoint) {
// ... [Code preview truncated due to length. Full source in attached file] ...