/*▄▄▄ ███▄ ▄███▓ ▄████ ▄▄▄██▀▀▀▓█████▄▄▄█████▓ ▓█████▄ ▓██▒▀█▀ ██▒ ██▒ ▀█▒ ▒██ ▓█ ▀▓ ██▒ ▓▒ ▒██▒ ▄██▓██ ▓██░▒██░▄▄▄░ ░██ ▒███ ▒ ▓██░ ▒░ ▒██░█▀ ▒██ ▒██ ░▓█ ██▓▓██▄██▓ ▒▓█ ▄░ ▓██▓ ░ ░▓█ ▀█▓▒██▒ ░██▒░▒▓███▀▒ ▓███▒ ░▒████▒ ▒██▒ ░ ░▒▓███▀▒░ ▒░ ░ ░ ░▒ ▒ ▒▓▒▒░ ░░ ▒░ ░ ▒ ░░ ▒░▒ ░ ░ ░ ░ ░ ░ ▒ ░▒░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ */ using System; using System.Collections.Generic; using System.Linq; using UnityEngine; using Oxide.Core; using Newtonsoft.Json; using Facepunch; namespace Oxide.Plugins { [Info("NPCApartments", "bmgjet", "1.0.0")] [Description("Spawns NPC players in apartments with loot. Optional safezone/turret/defender control.")] class NPCApartments : RustPlugin { public StoredData dataFile; private Configuration config; private Timer respawnTimer; private bool _respawnDisabled = false; public static NPCApartments plugin; public Dictionary LastUseTime = new Dictionary(); public ApartmentBuilding apartmentBuilding = null; private List botIDs = new List(); public class StoredData { public List NPCDATA = new List(); } public class NPCA { public string RoomNumber; public ulong PlayerNetId; public ulong userid; } private struct DefenderSpawnPoint { public Vector3 LocalOffset; public bool Stationary; } public class ApartmentRoomData { public string RoomNumber; public string Prefab; public Vector3 LocalPosition; public Quaternion LocalRotation; } private class SafezoneStoredData { public List Turrets = new List(); } private class TurretInfo { public string Prefab; public Vector3 Position; public Vector3 RotationEuler; } private static readonly ApartmentRoomData[] DefaultRooms = { new ApartmentRoomData { RoomNumber = "101", Prefab = "medium", LocalPosition = new Vector3(-11f, -7.125f, -7.875f), LocalRotation = new Quaternion(0f, 1f, 0f, 0f) }, new ApartmentRoomData { RoomNumber = "102", Prefab = "medium", LocalPosition = new Vector3(-18f, -7.125f, -7.875f), LocalRotation = new Quaternion(0f, 1f, 0f, 0f) }, new ApartmentRoomData { RoomNumber = "103", Prefab = "medium", LocalPosition = new Vector3(-25f, -7.125f, -7.875f), LocalRotation = new Quaternion(0f, 1f, 0f, 0f) }, new ApartmentRoomData { RoomNumber = "104", Prefab = "medium", LocalPosition = new Vector3(-32f, -7.125f, -7.875f), LocalRotation = new Quaternion(0f, 1f, 0f, 0f) }, new ApartmentRoomData { RoomNumber = "105", Prefab = "medium", LocalPosition = new Vector3(-32f, -7.125f, 5.375f), LocalRotation = new Quaternion(0f, 0f, 0f, -1f) }, new ApartmentRoomData { RoomNumber = "106", Prefab = "medium", LocalPosition = new Vector3(-25f, -7.125f, 5.375f), LocalRotation = new Quaternion(0f, 0f, 0f, -1f) }, new ApartmentRoomData { RoomNumber = "107", Prefab = "medium", LocalPosition = new Vector3(-18f, -7.125f, 5.375f), LocalRotation = new Quaternion(0f, 0f, 0f, -1f) }, new ApartmentRoomData { RoomNumber = "108", Prefab = "medium", LocalPosition = new Vector3(-11f, -7.125f, 5.375f), LocalRotation = new Quaternion(0f, 0f, 0f, -1f) }, new ApartmentRoomData { RoomNumber = "201", Prefab = "medium", LocalPosition = new Vector3(-11f, -3.625f, -7.375f), LocalRotation = new Quaternion(0f, 1f, 0f, 0f) }, new ApartmentRoomData { RoomNumber = "202", Prefab = "medium", LocalPosition = new Vector3(-18f, -3.625f, -7.375f), LocalRotation = new Quaternion(0f, 1f, 0f, 0f) }, new ApartmentRoomData { RoomNumber = "203", Prefab = "medium", LocalPosition = new Vector3(-25f, -3.625f, -7.375f), LocalRotation = new Quaternion(0f, 1f, 0f, 0f) }, new ApartmentRoomData { RoomNumber = "204", Prefab = "medium", LocalPosition = new Vector3(-32f, -3.625f, -7.375f), LocalRotation = new Quaternion(0f, 1f, 0f, 0f) }, new ApartmentRoomData { RoomNumber = "205", Prefab = "medium", LocalPosition = new Vector3(-32f, -3.625f, 4.875f), LocalRotation = new Quaternion(0f, 0f, 0f, -1f) }, new ApartmentRoomData { RoomNumber = "206", Prefab = "medium", LocalPosition = new Vector3(-25f, -3.625f, 4.875f), LocalRotation = new Quaternion(0f, 0f, 0f, -1f) }, new ApartmentRoomData { RoomNumber = "207", Prefab = "medium", LocalPosition = new Vector3(-18f, -3.625f, 4.875f), LocalRotation = new Quaternion(0f, 0f, 0f, -1f) }, new ApartmentRoomData { RoomNumber = "208", Prefab = "medium", LocalPosition = new Vector3(-11f, -3.625f, 4.875f), LocalRotation = new Quaternion(0f, 0f, 0f, -1f) }, new ApartmentRoomData { RoomNumber = "301", Prefab = "medium", LocalPosition = new Vector3(-11f, -0.125f, -7.375f), LocalRotation = new Quaternion(0f, 1f, 0f, 0f) }, new ApartmentRoomData { RoomNumber = "302", Prefab = "medium", LocalPosition = new Vector3(-18f, -0.125f, -7.375f), LocalRotation = new Quaternion(0f, 1f, 0f, 0f) }, new ApartmentRoomData { RoomNumber = "303", Prefab = "medium", LocalPosition = new Vector3(-25f, -0.125f, -7.375f), LocalRotation = new Quaternion(0f, 1f, 0f, 0f) }, new ApartmentRoomData { RoomNumber = "304", Prefab = "medium", LocalPosition = new Vector3(-32f, -0.125f, -7.375f), LocalRotation = new Quaternion(0f, 1f, 0f, 0f) }, new ApartmentRoomData { RoomNumber = "305", Prefab = "medium", LocalPosition = new Vector3(-32f, -0.125f, 4.875f), LocalRotation = new Quaternion(0f, 0f, 0f, -1f) }, new ApartmentRoomData { RoomNumber = "306", Prefab = "medium", LocalPosition = new Vector3(-25f, -0.125f, 4.875f), LocalRotation = new Quaternion(0f, 0f, 0f, -1f) }, new ApartmentRoomData { RoomNumber = "307", Prefab = "medium", LocalPosition = new Vector3(-18f, -0.125f, 4.875f), LocalRotation = new Quaternion(0f, 0f, 0f, -1f) }, new ApartmentRoomData { RoomNumber = "308", Prefab = "medium", LocalPosition = new Vector3(-11f, -0.125f, 4.875f), LocalRotation = new Quaternion(0f, 0f, 0f, -1f) }, new ApartmentRoomData { RoomNumber = "401", Prefab = "medium", LocalPosition = new Vector3(-11f, 3.375f, -7.375f), LocalRotation = new Quaternion(0f, 1f, 0f, 0f) }, new ApartmentRoomData { RoomNumber = "402", Prefab = "medium", LocalPosition = new Vector3(-18f, 3.375f, -7.375f), LocalRotation = new Quaternion(0f, 1f, 0f, 0f) }, new ApartmentRoomData { RoomNumber = "403", Prefab = "medium", LocalPosition = new Vector3(-25f, 3.375f, -7.375f), LocalRotation = new Quaternion(0f, 1f, 0f, 0f) }, new ApartmentRoomData { RoomNumber = "404", Prefab = "medium", LocalPosition = new Vector3(-32f, 3.375f, -7.375f), LocalRotation = new Quaternion(0f, 1f, 0f, 0f) }, new ApartmentRoomData { RoomNumber = "405", Prefab = "medium", LocalPosition = new Vector3(-32f, 3.375f, 4.875f), LocalRotation = new Quaternion(0f, 0f, 0f, -1f) }, new ApartmentRoomData { RoomNumber = "406", Prefab = "medium", LocalPosition = new Vector3(-25f, 3.375f, 4.875f), LocalRotation = new Quaternion(0f, 0f, 0f, -1f) }, new ApartmentRoomData { RoomNumber = "407", Prefab = "medium", LocalPosition = new Vector3(-18f, 3.375f, 4.875f), LocalRotation = new Quaternion(0f, 0f, 0f, -1f) }, new ApartmentRoomData { RoomNumber = "408", Prefab = "medium", LocalPosition = new Vector3(-11f, 3.375f, 4.875f), LocalRotation = new Quaternion(0f, 0f, 0f, -1f) }, new ApartmentRoomData { RoomNumber = "501", Prefab = "medium", LocalPosition = new Vector3(-11f, 6.875f, -7.375f), LocalRotation = new Quaternion(0f, 1f, 0f, 0f) }, new ApartmentRoomData { RoomNumber = "502", Prefab = "medium", LocalPosition = new Vector3(-18f, 6.875f, -7.375f), LocalRotation = new Quaternion(0f, 1f, 0f, 0f) }, new ApartmentRoomData { RoomNumber = "503", Prefab = "medium", LocalPosition = new Vector3(-25f, 6.875f, -7.375f), LocalRotation = new Quaternion(0f, 1f, 0f, 0f) }, new ApartmentRoomData { RoomNumber = "504", Prefab = "medium", LocalPosition = new Vector3(-32f, 6.875f, -7.375f), LocalRotation = new Quaternion(0f, 1f, 0f, 0f) }, new ApartmentRoomData { RoomNumber = "505", Prefab = "medium", LocalPosition = new Vector3(-32f, 6.875f, 4.875f), LocalRotation = new Quaternion(0f, 0f, 0f, -1f) }, new ApartmentRoomData { RoomNumber = "506", Prefab = "medium", LocalPosition = new Vector3(-25f, 6.875f, 4.875f), LocalRotation = new Quaternion(0f, 0f, 0f, -1f) }, new ApartmentRoomData { RoomNumber = "507", Prefab = "medium", LocalPosition = new Vector3(-18f, 6.875f, 4.875f), LocalRotation = new Quaternion(0f, 0f, 0f, -1f) }, new ApartmentRoomData { RoomNumber = "508", Prefab = "medium", LocalPosition = new Vector3(-11f, 6.875f, 4.875f), LocalRotation = new Quaternion(0f, 0f, 0f, -1f) }, new ApartmentRoomData { RoomNumber = "601", Prefab = "large", LocalPosition = new Vector3(-16.7913f, 10.375f, -4.625f), LocalRotation = new Quaternion(0f, 1f, 0f, 0f) }, new ApartmentRoomData { RoomNumber = "602", Prefab = "large", LocalPosition = new Vector3(-7.4587f, 10.375f, 2.125f), LocalRotation = new Quaternion(0f, 0f, 0f, -1f) }, new ApartmentRoomData { RoomNumber = "603", Prefab = "large", LocalPosition = new Vector3(-26.1663f, 10.375f, -4.625f), LocalRotation = new Quaternion(0f, 1f, 0f, 0f) }, new ApartmentRoomData { RoomNumber = "604", Prefab = "large", LocalPosition = new Vector3(-16.8337f, 10.375f, 2.125f), LocalRotation = new Quaternion(0f, 0f, 0f, -1f) }, new ApartmentRoomData { RoomNumber = "605", Prefab = "large", LocalPosition = new Vector3(-35.5413f, 10.375f, -4.625f), LocalRotation = new Quaternion(0f, 1f, 0f, 0f) }, new ApartmentRoomData { RoomNumber = "606", Prefab = "large", LocalPosition = new Vector3(-26.2087f, 10.375f, 2.125f), LocalRotation = new Quaternion(0f, 0f, 0f, -1f) }, new ApartmentRoomData { RoomNumber = "B01", Prefab = "small", LocalPosition = new Vector3(-9.25f, -10.875f, -13.125f), LocalRotation = new Quaternion(0f, 1f, 0f, 0f) }, new ApartmentRoomData { RoomNumber = "B02", Prefab = "small", LocalPosition = new Vector3(-12.75f, -10.875f, -13.125f), LocalRotation = new Quaternion(0f, 1f, 0f, 0f) }, new ApartmentRoomData { RoomNumber = "B03", Prefab = "small", LocalPosition = new Vector3(-16.25f, -10.875f, -13.125f), LocalRotation = new Quaternion(0f, 1f, 0f, 0f) }, new ApartmentRoomData { RoomNumber = "B04", Prefab = "small", LocalPosition = new Vector3(-19.75f, -10.875f, -13.125f), LocalRotation = new Quaternion(0f, 1f, 0f, 0f) }, new ApartmentRoomData { RoomNumber = "B05", Prefab = "small", LocalPosition = new Vector3(-23.25f, -10.875f, -13.125f), LocalRotation = new Quaternion(0f, 1f, 0f, 0f) }, new ApartmentRoomData { RoomNumber = "B06", Prefab = "small", LocalPosition = new Vector3(-26.75f, -10.875f, -13.125f), LocalRotation = new Quaternion(0f, 1f, 0f, 0f) }, new ApartmentRoomData { RoomNumber = "B07", Prefab = "small", LocalPosition = new Vector3(-30.25f, -10.875f, -13.125f), LocalRotation = new Quaternion(0f, 1f, 0f, 0f) }, new ApartmentRoomData { RoomNumber = "B08", Prefab = "small", LocalPosition = new Vector3(-33.75f, -10.875f, -13.125f), LocalRotation = new Quaternion(0f, 1f, 0f, 0f) }, new ApartmentRoomData { RoomNumber = "B09", Prefab = "small", LocalPosition = new Vector3(-33.75f, -10.875f, 10.625f), LocalRotation = new Quaternion(0f, 0f, 0f, -1f) }, new ApartmentRoomData { RoomNumber = "B10", Prefab = "small", LocalPosition = new Vector3(-30.25f, -10.875f, 10.625f), LocalRotation = new Quaternion(0f, 0f, 0f, -1f) }, new ApartmentRoomData { RoomNumber = "B11", Prefab = "small", LocalPosition = new Vector3(-26.75f, -10.875f, 10.625f), LocalRotation = new Quaternion(0f, 0f, 0f, -1f) }, new ApartmentRoomData { RoomNumber = "B12", Prefab = "small", LocalPosition = new Vector3(-23.25f, -10.875f, 10.625f), LocalRotation = new Quaternion(0f, 0f, 0f, -1f) }, new ApartmentRoomData { RoomNumber = "B13", Prefab = "small", LocalPosition = new Vector3(-19.75f, -10.875f, 10.625f), LocalRotation = new Quaternion(0f, 0f, 0f, -1f) }, new ApartmentRoomData { RoomNumber = "B14", Prefab = "small", LocalPosition = new Vector3(-16.25f, -10.875f, 10.625f), LocalRotation = new Quaternion(0f, 0f, 0f, -1f) }, new ApartmentRoomData { RoomNumber = "B15", Prefab = "small", LocalPosition = new Vector3(-12.75f, -10.875f, 10.625f), LocalRotation = new Quaternion(0f, 0f, 0f, -1f) }, new ApartmentRoomData { RoomNumber = "B16", Prefab = "small", LocalPosition = new Vector3(-9.25f, -10.875f, 10.625f), LocalRotation = new Quaternion(0f, 0f, 0f, -1f) }, }; private static readonly DefenderSpawnPoint[] DefenderLocalOffsets = new DefenderSpawnPoint[] { // Ground-floor patrols new DefenderSpawnPoint { LocalOffset = new Vector3( 19.15f, 0.282f, 23.657f), Stationary = false }, new DefenderSpawnPoint { LocalOffset = new Vector3( -5.828f, 0.293f, 23.241f), Stationary = false }, new DefenderSpawnPoint { LocalOffset = new Vector3(-21.968f, 0.218f, 14.032f), Stationary = false }, new DefenderSpawnPoint { LocalOffset = new Vector3(-52.059f, 0.282f, 30.472f), Stationary = false }, new DefenderSpawnPoint { LocalOffset = new Vector3(-19.761f, 0.053f, 39.168f), Stationary = false }, // Mid-floor new DefenderSpawnPoint { LocalOffset = new Vector3(-14.259f, 2.375f, 58.165f), Stationary = true }, new DefenderSpawnPoint { LocalOffset = new Vector3(-13.857f, 1.215f, 75.135f), Stationary = false }, new DefenderSpawnPoint { LocalOffset = new Vector3(-24.427f, 2.375f, 68.884f), Stationary = true }, new DefenderSpawnPoint { LocalOffset = new Vector3(-56.447f, 2.375f, 61.924f), Stationary = true }, // Tower stack (one position per floor) new DefenderSpawnPoint { LocalOffset = new Vector3(-56.835f, 5.875f, 62.411f), Stationary = true }, new DefenderSpawnPoint { LocalOffset = new Vector3(-56.772f, 9.375f, 62.542f), Stationary = true }, new DefenderSpawnPoint { LocalOffset = new Vector3(-56.948f, 12.875f, 62.495f), Stationary = true }, new DefenderSpawnPoint { LocalOffset = new Vector3(-56.421f, 16.375f, 61.670f), Stationary = true }, new DefenderSpawnPoint { LocalOffset = new Vector3(-56.714f, 19.875f, 61.169f), Stationary = true }, new DefenderSpawnPoint { LocalOffset = new Vector3(-56.639f, 19.875f, 62.697f), Stationary = true }, // Top-floor shooters new DefenderSpawnPoint { LocalOffset = new Vector3(-23.966f, 19.874f, 58.905f), Stationary = true }, new DefenderSpawnPoint { LocalOffset = new Vector3(-13.723f, 19.874f, 59.591f), Stationary = true }, new DefenderSpawnPoint { LocalOffset = new Vector3(-23.186f, 19.876f, 74.337f), Stationary = true }, new DefenderSpawnPoint { LocalOffset = new Vector3(-23.888f, 16.376f, 76.118f), Stationary = true }, // Inner rings / patrollers new DefenderSpawnPoint { LocalOffset = new Vector3(-23.986f, 16.374f, 58.663f), Stationary = true }, new DefenderSpawnPoint { LocalOffset = new Vector3(-24.227f, 12.874f, 58.933f), Stationary = true }, new DefenderSpawnPoint { LocalOffset = new Vector3(-23.277f, 12.876f, 75.498f), Stationary = true }, new DefenderSpawnPoint { LocalOffset = new Vector3(-23.080f, 9.376f, 76.133f), Stationary = true }, new DefenderSpawnPoint { LocalOffset = new Vector3(-24.25f, 9.374f, 59.081f), Stationary = true }, new DefenderSpawnPoint { LocalOffset = new Vector3(-24f, 5.874f, 59.214f), Stationary = true }, new DefenderSpawnPoint { LocalOffset = new Vector3(-22.762f, 5.949f, 75.676f), Stationary = true }, new DefenderSpawnPoint { LocalOffset = new Vector3(-24.663f, 2.384f, 56.852f), Stationary = true }, // Sub-floor new DefenderSpawnPoint { LocalOffset = new Vector3(-56.773f, -1.375f, 62.473f), Stationary = true }, new DefenderSpawnPoint { LocalOffset = new Vector3(-14.038f, -1.375f, 62.243f), Stationary = true }, }; private const string DefenderPrefab = "assets/rust.ai/agents/npcplayer/humannpc/scientist/scientistnpc_peacekeeper.prefab"; private const string SafezoneDataFileName = "ApartmentSafezoneRemover"; private SafezoneStoredData safezoneData; private TriggerSafeZone removedSafeZone; private readonly List defenderNpcs = new List(); private readonly HashSet defenderTrackedIds = new HashSet(); private readonly List pendingDefenderRespawns = new List(); private readonly List defenderTrackers = new List(); private bool defenderUnloading; private class DefenderTracker { public ScientistNPC Npc; public Vector3 HomePosition; public bool Stationary; public Timer TickTimer; public BaseEntity CurrentTarget; public float NextTargetCheck; } #region Config private class LootEntry { [JsonProperty("Item Shortname")] public string Shortname = ""; [JsonProperty("Weight")] public int Weight = 1; [JsonProperty("Min Amount")] public int MinAmount = 1; [JsonProperty("Max Amount")] public int MaxAmount = 1; } private class LootProfile { [JsonProperty("Enabled")] public bool Enabled = true; [JsonProperty("Min Rolls")] public int MinRolls = 2; [JsonProperty("Max Rolls")] public int MaxRolls = 4; [JsonProperty("Min Items Per Roll")] public int MinItemsPerRoll = 1; [JsonProperty("Max Items Per Roll")] public int MaxItemsPerRoll = 3; [JsonProperty("Loot Entries")] public List Entries = new List(); } private class SafezoneSettings { [JsonProperty("Remove Apartment SafeZone (and restore on unload)")] public bool RemoveSafezone = true; [JsonProperty("Remove Apartment Turrets (killed on start, restored on unload)")] public bool RemoveTurrets = true; [JsonProperty("Spawn Defender NPCs (peacekeepers) around the building")] public bool SpawnDefenderNpcs = true; [JsonProperty("Defender NPC respawn delay after death (seconds)")] public int DefenderRespawnSeconds = 600; [JsonProperty("Defender NPC prefab path")] public string DefenderPrefab = "assets/rust.ai/agents/npcplayer/humannpc/scientist/scientistnpc_peacekeeper.prefab"; [JsonProperty("Defender NPC health")] public float DefenderHealth = 25f; [JsonProperty("Defender NPC attack range multiplier")] public float DefenderAttackRange = 1f; [JsonProperty("Defender NPC sense range (metres)")] public float DefenderSenseRange = 5f; [JsonProperty("Defender NPC max roam distance from spawn (metres)")] public float DefenderRoamDistance = 15f; [JsonProperty("Defender NPC path tick interval (seconds)")] public float DefenderTickInterval = 15f; [JsonProperty("Defender NPC damage multiplier")] public float DefenderDamageMultiplier = 1f; } private class Configuration { [JsonProperty("Apartment SafeZone Settings")] public SafezoneSettings Safezone = new SafezoneSettings(); [JsonProperty("Limit Master Key")] public bool MasterKeyLimit = true; [JsonProperty("Master Key Cooldown(Secs)")] public int MasterKeyCD = 500; [JsonProperty("Number Of NPC Apartments")] public int NPCApartments = 4; [JsonProperty("Respawn Apartments Delay (Mins)")] public int NPCApartmentsRespawnTimer = 120; // NPC dressing [JsonProperty("Dress NPCs With Clothing")] public bool DressNPCs = true; [JsonProperty("NPC Display Name Format ({0} = room#, used only when RandomUsernames plugin is missing)")] public string NPCDisplayName = "Resident {0}"; [JsonProperty("Use Random Usernames")] public bool UseRandomUsernames = true; [JsonProperty("NPC Min Clothing Items")] public int NPCMinClothing = 2; [JsonProperty("NPC Max Clothing Items")] public int NPCMaxClothing = 3; [JsonProperty("NPC Clothing Items (shortnames, randomly picked)")] public List NPCClothingItems = new List { "hoodie", "jacket.snow", "jacket", "pants", "pants.shorts", "shoes.boots", "hat.cap", "mask.balaclava", "tshirt.long", "shirt.tanktop" }; [JsonProperty("Loot Profiles")] public Dictionary LootProfiles = new Dictionary { ["Generic"] = new LootProfile { MinRolls = 4, MaxRolls = 8, MinItemsPerRoll = 2, MaxItemsPerRoll = 5, Entries = new List { // Resources / scrap new LootEntry { Shortname = "scrap", Weight = 35, MinAmount = 25, MaxAmount = 150 }, new LootEntry { Shortname = "metal.fragments", Weight = 30, MinAmount = 50, MaxAmount = 400 }, new LootEntry { Shortname = "metal.refined", Weight = 8, MinAmount = 5, MaxAmount = 40 }, new LootEntry { Shortname = "hq.metal.ore", Weight = 4, MinAmount = 5, MaxAmount = 25 }, new LootEntry { Shortname = "metal.ore", Weight = 10, MinAmount = 50, MaxAmount = 300 }, new LootEntry { Shortname = "sulfur.ore", Weight = 8, MinAmount = 50, MaxAmount = 200 }, new LootEntry { Shortname = "sulfur", Weight = 6, MinAmount = 20, MaxAmount = 100 }, new LootEntry { Shortname = "stones", Weight = 12, MinAmount = 100, MaxAmount = 500 }, new LootEntry { Shortname = "wood", Weight = 14, MinAmount = 100, MaxAmount = 500 }, new LootEntry { Shortname = "charcoal", Weight = 8, MinAmount = 25, MaxAmount = 150 }, new LootEntry { Shortname = "leather", Weight = 8, MinAmount = 10, MaxAmount = 60 }, new LootEntry { Shortname = "cloth", Weight = 15, MinAmount = 20, MaxAmount = 100 }, new LootEntry { Shortname = "lowgradefuel", Weight = 10, MinAmount = 20, MaxAmount = 80 }, new LootEntry { Shortname = "crude.oil", Weight = 4, MinAmount = 5, MaxAmount = 25 }, new LootEntry { Shortname = "diesel_barrel", Weight = 2, MinAmount = 1, MaxAmount = 2 }, // Components new LootEntry { Shortname = "gears", Weight = 12, MinAmount = 2, MaxAmount = 10 }, new LootEntry { Shortname = "rope", Weight = 14, MinAmount = 5, MaxAmount = 30 }, new LootEntry { Shortname = "sewingkit", Weight = 10, MinAmount = 1, MaxAmount = 4 }, new LootEntry { Shortname = "techparts", Weight = 4, MinAmount = 1, MaxAmount = 3 }, new LootEntry { Shortname = "tech.trash", Weight = 10, MinAmount = 2, MaxAmount = 8 }, new LootEntry { Shortname = "metalspring", Weight = 5, MinAmount = 1, MaxAmount = 4 }, new LootEntry { Shortname = "sheetmetal", Weight = 5, MinAmount = 1, MaxAmount = 5 }, new LootEntry { Shortname = "metalpipe", Weight = 6, MinAmount = 1, MaxAmount = 5 }, new LootEntry { Shortname = "metalblade", Weight = 5, MinAmount = 1, MaxAmount = 4 }, new LootEntry { Shortname = "roadsigns", Weight = 4, MinAmount = 1, MaxAmount = 3 }, new LootEntry { Shortname = "propanetank", Weight = 3, MinAmount = 1, MaxAmount = 2 }, new LootEntry { Shortname = "fuse", Weight = 4, MinAmount = 1, MaxAmount = 3 }, new LootEntry { Shortname = "tarp", Weight = 5, MinAmount = 1, MaxAmount = 3 }, new LootEntry { Shortname = "bone.fragments", Weight = 8, MinAmount = 10, MaxAmount = 50 }, // Medical new LootEntry { Shortname = "bandage", Weight = 8, MinAmount = 2, MaxAmount = 8 }, new LootEntry { Shortname = "syringe.medical", Weight = 5, MinAmount = 1, MaxAmount = 4 }, new LootEntry { Shortname = "largemedkit", Weight = 3, MinAmount = 1, MaxAmount = 2 }, new LootEntry { Shortname = "antiradpills", Weight = 3, MinAmount = 1, MaxAmount = 3 }, // Food / drink new LootEntry { Shortname = "apple", Weight = 6, MinAmount = 2, MaxAmount = 8 }, new LootEntry { Shortname = "can.beans", Weight = 6, MinAmount = 1, MaxAmount = 4 }, new LootEntry { Shortname = "can.tuna", Weight = 5, MinAmount = 1, MaxAmount = 4 }, new LootEntry { Shortname = "smallwaterbottle", Weight = 6, MinAmount = 1, MaxAmount = 3 }, // Ammo (small chance) new LootEntry { Shortname = "ammo.pistol", Weight = 5, MinAmount = 8, MaxAmount = 30 }, new LootEntry { Shortname = "ammo.rifle", Weight = 4, MinAmount = 8, MaxAmount = 30 }, new LootEntry { Shortname = "ammo.shotgun", Weight = 4, MinAmount = 4, MaxAmount = 16 }, new LootEntry { Shortname = "arrow.wooden", Weight = 5, MinAmount = 5, MaxAmount = 20 }, // Attire (small chance) new LootEntry { Shortname = "hoodie", Weight = 4, MinAmount = 1, MaxAmount = 1 }, new LootEntry { Shortname = "pants", Weight = 4, MinAmount = 1, MaxAmount = 1 }, new LootEntry { Shortname = "shoes.boots", Weight = 3, MinAmount = 1, MaxAmount = 1 }, new LootEntry { Shortname = "mask.bandana", Weight = 3, MinAmount = 1, MaxAmount = 1 } } }, ["KitchenCounter"] = new LootProfile { MinRolls = 3, MaxRolls = 6, MinItemsPerRoll = 2, MaxItemsPerRoll = 5, Entries = new List { // Resources / kitchen-y building bits new LootEntry { Shortname = "metal.fragments", Weight = 30, MinAmount = 30, MaxAmount = 200 }, new LootEntry { Shortname = "wood", Weight = 22, MinAmount = 50, MaxAmount = 300 }, new LootEntry { Shortname = "stones", Weight = 12, MinAmount = 50, MaxAmount = 250 }, new LootEntry { Shortname = "scrap", Weight = 18, MinAmount = 5, MaxAmount = 40 }, new LootEntry { Shortname = "cloth", Weight = 15, MinAmount = 10, MaxAmount = 50 }, new LootEntry { Shortname = "rope", Weight = 14, MinAmount = 5, MaxAmount = 25 }, new LootEntry { Shortname = "leather", Weight = 8, MinAmount = 5, MaxAmount = 25 }, new LootEntry { Shortname = "sewingkit", Weight = 10, MinAmount = 1, MaxAmount = 4 }, new LootEntry { Shortname = "tech.trash", Weight = 8, MinAmount = 1, MaxAmount = 5 }, new LootEntry { Shortname = "gears", Weight = 6, MinAmount = 1, MaxAmount = 4 }, new LootEntry { Shortname = "tarp", Weight = 5, MinAmount = 1, MaxAmount = 2 }, new LootEntry { Shortname = "metalpipe", Weight = 4, MinAmount = 1, MaxAmount = 3 }, new LootEntry { Shortname = "sheetmetal", Weight = 4, MinAmount = 1, MaxAmount = 3 }, // Food / drink new LootEntry { Shortname = "can.beans", Weight = 10, MinAmount = 1, MaxAmount = 4 }, new LootEntry { Shortname = "can.tuna", Weight = 8, MinAmount = 1, MaxAmount = 3 }, new LootEntry { Shortname = "apple", Weight = 8, MinAmount = 2, MaxAmount = 6 }, new LootEntry { Shortname = "smallwaterbottle", Weight = 7, MinAmount = 1, MaxAmount = 2 }, new LootEntry { Shortname = "jar.pickle", Weight = 4, MinAmount = 1, MaxAmount = 2 }, new LootEntry { Shortname = "cactusflesh", Weight = 4, MinAmount = 1, MaxAmount = 4 }, new LootEntry { Shortname = "mushroom", Weight = 4, MinAmount = 1, MaxAmount = 4 }, // Misc tools new LootEntry { Shortname = "hammer", Weight = 4, MinAmount = 1, MaxAmount = 1 } } }, ["KitchenCupboard"] = new LootProfile { MinRolls = 5, MaxRolls = 9, MinItemsPerRoll = 3, MaxItemsPerRoll = 6, Entries = new List { // Food pile new LootEntry { Shortname = "meat.boar", Weight = 22, MinAmount = 5, MaxAmount = 25 }, new LootEntry { Shortname = "chicken.raw", Weight = 18, MinAmount = 3, MaxAmount = 12 }, new LootEntry { Shortname = "deermeat.raw", Weight = 16, MinAmount = 3, MaxAmount = 12 }, new LootEntry { Shortname = "bearmeat", Weight = 10, MinAmount = 2, MaxAmount = 8 }, new LootEntry { Shortname = "wolfmeat.raw", Weight = 12, MinAmount = 2, MaxAmount = 8 }, new LootEntry { Shortname = "fish.raw", Weight = 14, MinAmount = 2, MaxAmount = 10 }, new LootEntry { Shortname = "potato", Weight = 14, MinAmount = 4, MaxAmount = 12 }, new LootEntry { Shortname = "corn", Weight = 14, MinAmount = 4, MaxAmount = 15 }, new LootEntry { Shortname = "pumpkin", Weight = 10, MinAmount = 1, MaxAmount = 4 }, new LootEntry { Shortname = "apple", Weight = 14, MinAmount = 3, MaxAmount = 12 }, new LootEntry { Shortname = "black.raspberries", Weight = 8, MinAmount = 2, MaxAmount = 8 }, new LootEntry { Shortname = "blueberries", Weight = 8, MinAmount = 2, MaxAmount = 8 }, new LootEntry { Shortname = "mushroom", Weight = 6, MinAmount = 1, MaxAmount = 5 }, new LootEntry { Shortname = "cactusflesh", Weight = 5, MinAmount = 1, MaxAmount = 4 }, new LootEntry { Shortname = "can.beans", Weight = 16, MinAmount = 1, MaxAmount = 5 }, new LootEntry { Shortname = "can.tuna", Weight = 14, MinAmount = 1, MaxAmount = 5 }, new LootEntry { Shortname = "jar.pickle", Weight = 6, MinAmount = 1, MaxAmount = 3 }, new LootEntry { Shortname = "granolabar", Weight = 8, MinAmount = 1, MaxAmount = 4 }, new LootEntry { Shortname = "chocholate", Weight = 8, MinAmount = 1, MaxAmount = 4 }, new LootEntry { Shortname = "smallwaterbottle", Weight = 14, MinAmount = 1, MaxAmount = 4 }, new LootEntry { Shortname = "waterjug", Weight = 6, MinAmount = 1, MaxAmount = 1 }, // Sprinkled junk new LootEntry { Shortname = "metal.fragments", Weight = 14, MinAmount = 10, MaxAmount = 60 }, new LootEntry { Shortname = "scrap", Weight = 10, MinAmount = 1, MaxAmount = 12 }, new LootEntry { Shortname = "cloth", Weight = 10, MinAmount = 10, MaxAmount = 40 }, new LootEntry { Shortname = "rope", Weight = 8, MinAmount = 3, MaxAmount = 15 }, new LootEntry { Shortname = "sewingkit", Weight = 8, MinAmount = 1, MaxAmount = 2 }, } }, ["Bathroom"] = new LootProfile { MinRolls = 3, MaxRolls = 6, MinItemsPerRoll = 2, MaxItemsPerRoll = 4, Entries = new List { // Medical heavy new LootEntry { Shortname = "bandage", Weight = 30, MinAmount = 3, MaxAmount = 12 }, new LootEntry { Shortname = "syringe.medical", Weight = 22, MinAmount = 1, MaxAmount = 6 }, new LootEntry { Shortname = "largemedkit", Weight = 10, MinAmount = 1, MaxAmount = 2 }, new LootEntry { Shortname = "antiradpills", Weight = 14, MinAmount = 1, MaxAmount = 3 }, // Cleaning / cloth / containers new LootEntry { Shortname = "cloth", Weight = 18, MinAmount = 10, MaxAmount = 50 }, new LootEntry { Shortname = "sewingkit", Weight = 8, MinAmount = 1, MaxAmount = 2 }, new LootEntry { Shortname = "smallwaterbottle", Weight = 10, MinAmount = 1, MaxAmount = 3 }, new LootEntry { Shortname = "waterjug", Weight = 4, MinAmount = 1, MaxAmount = 1 }, new LootEntry { Shortname = "scrap", Weight = 8, MinAmount = 1, MaxAmount = 10 }, new LootEntry { Shortname = "metal.fragments", Weight = 8, MinAmount = 5, MaxAmount = 30 }, new LootEntry { Shortname = "rope", Weight = 6, MinAmount = 2, MaxAmount = 10 }, // Bathroom-ish flair new LootEntry { Shortname = "mask.bandana", Weight = 5, MinAmount = 1, MaxAmount = 1 }, new LootEntry { Shortname = "burlap.gloves", Weight = 4, MinAmount = 1, MaxAmount = 1 }, new LootEntry { Shortname = "burlap.shirt", Weight = 4, MinAmount = 1, MaxAmount = 1 }, new LootEntry { Shortname = "burlap.trousers", Weight = 4, MinAmount = 1, MaxAmount = 1 } } }, ["StorageBox"] = new LootProfile { MinRolls = 7, MaxRolls = 12, MinItemsPerRoll = 3, MaxItemsPerRoll = 7, Entries = new List { // Resources new LootEntry { Shortname = "scrap", Weight = 28, MinAmount = 75, MaxAmount = 250 }, new LootEntry { Shortname = "metal.fragments", Weight = 25, MinAmount = 150, MaxAmount = 700 }, new LootEntry { Shortname = "metal.refined", Weight = 14, MinAmount = 15, MaxAmount = 80 }, new LootEntry { Shortname = "hq.metal.ore", Weight = 8, MinAmount = 10, MaxAmount = 50 }, new LootEntry { Shortname = "metal.ore", Weight = 12, MinAmount = 75, MaxAmount = 400 }, new LootEntry { Shortname = "sulfur.ore", Weight = 10, MinAmount = 50, MaxAmount = 350 }, new LootEntry { Shortname = "sulfur", Weight = 8, MinAmount = 30, MaxAmount = 150 }, new LootEntry { Shortname = "stones", Weight = 10, MinAmount = 100, MaxAmount = 600 }, new LootEntry { Shortname = "wood", Weight = 12, MinAmount = 150, MaxAmount = 800 }, new LootEntry { Shortname = "charcoal", Weight = 8, MinAmount = 30, MaxAmount = 150 }, new LootEntry { Shortname = "leather", Weight = 10, MinAmount = 20, MaxAmount = 100 }, new LootEntry { Shortname = "cloth", Weight = 14, MinAmount = 30, MaxAmount = 150 }, new LootEntry { Shortname = "lowgradefuel", Weight = 10, MinAmount = 30, MaxAmount = 120 }, new LootEntry { Shortname = "crude.oil", Weight = 5, MinAmount = 5, MaxAmount = 30 }, new LootEntry { Shortname = "diesel_barrel", Weight = 2, MinAmount = 1, MaxAmount = 2 }, new LootEntry { Shortname = "bone.fragments", Weight = 8, MinAmount = 20, MaxAmount = 80 }, new LootEntry { Shortname = "fat.animal", Weight = 6, MinAmount = 10, MaxAmount = 40 }, // Components new LootEntry { Shortname = "gears", Weight = 12, MinAmount = 2, MaxAmount = 12 }, new LootEntry { Shortname = "rope", Weight = 14, MinAmount = 10, MaxAmount = 40 }, new LootEntry { Shortname = "sewingkit", Weight = 10, MinAmount = 1, MaxAmount = 5 }, new LootEntry { Shortname = "techparts", Weight = 6, MinAmount = 1, MaxAmount = 4 }, new LootEntry { Shortname = "tech.trash", Weight = 10, MinAmount = 4, MaxAmount = 14 }, new LootEntry { Shortname = "metalspring", Weight = 6, MinAmount = 1, MaxAmount = 5 }, new LootEntry { Shortname = "sheetmetal", Weight = 6, MinAmount = 1, MaxAmount = 6 }, new LootEntry { Shortname = "metalpipe", Weight = 6, MinAmount = 1, MaxAmount = 5 }, new LootEntry { Shortname = "metalblade", Weight = 6, MinAmount = 1, MaxAmount = 5 }, new LootEntry { Shortname = "roadsigns", Weight = 5, MinAmount = 1, MaxAmount = 4 }, new LootEntry { Shortname = "propanetank", Weight = 4, MinAmount = 1, MaxAmount = 2 }, new LootEntry { Shortname = "fuse", Weight = 5, MinAmount = 1, MaxAmount = 3 }, new LootEntry { Shortname = "tarp", Weight = 6, MinAmount = 1, MaxAmount = 3 }, new LootEntry { Shortname = "explosives", Weight = 3, MinAmount = 5, MaxAmount = 25 }, new LootEntry { Shortname = "gunpowder", Weight = 4, MinAmount = 25, MaxAmount = 100 }, // Weapon parts / mods new LootEntry { Shortname = "riflebody", Weight = 4, MinAmount = 1, MaxAmount = 2 }, new LootEntry { Shortname = "smgbody", Weight = 3, MinAmount = 1, MaxAmount = 2 }, new LootEntry { Shortname = "semibody", Weight = 4, MinAmount = 1, MaxAmount = 2 }, new LootEntry { Shortname = "weapon.mod.holosight", Weight = 5, MinAmount = 1, MaxAmount = 1 }, new LootEntry { Shortname = "weapon.mod.silencer", Weight = 4, MinAmount = 1, MaxAmount = 1 }, new LootEntry { Shortname = "weapon.mod.flashlight", Weight = 4, MinAmount = 1, MaxAmount = 1 }, new LootEntry { Shortname = "weapon.mod.lasersight", Weight = 3, MinAmount = 1, MaxAmount = 1 }, new LootEntry { Shortname = "weapon.mod.simplesight", Weight = 4, MinAmount = 1, MaxAmount = 1 }, new LootEntry { Shortname = "weapon.mod.muzzlebrake", Weight = 3, MinAmount = 1, MaxAmount = 1 }, new LootEntry { Shortname = "weapon.mod.small.scope", Weight = 2, MinAmount = 1, MaxAmount = 1 }, // Ammo new LootEntry { Shortname = "ammo.pistol", Weight = 10, MinAmount = 12, MaxAmount = 60 }, new LootEntry { Shortname = "ammo.rifle", Weight = 8, MinAmount = 12, MaxAmount = 60 }, new LootEntry { Shortname = "ammo.shotgun", Weight = 8, MinAmount = 6, MaxAmount = 24 }, new LootEntry { Shortname = "ammo.handmade.shell",Weight = 6, MinAmount = 6, MaxAmount = 24 }, new LootEntry { Shortname = "arrow.wooden", Weight = 6, MinAmount = 8, MaxAmount = 30 }, new LootEntry { Shortname = "arrow.bone", Weight = 4, MinAmount = 4, MaxAmount = 16 }, new LootEntry { Shortname = "arrow.hv", Weight = 3, MinAmount = 2, MaxAmount = 10 }, // Medical / food / water (small chance) new LootEntry { Shortname = "bandage", Weight = 8, MinAmount = 2, MaxAmount = 10 }, new LootEntry { Shortname = "syringe.medical", Weight = 5, MinAmount = 1, MaxAmount = 4 }, new LootEntry { Shortname = "largemedkit", Weight = 3, MinAmount = 1, MaxAmount = 1 }, new LootEntry { Shortname = "apple", Weight = 6, MinAmount = 2, MaxAmount = 8 }, new LootEntry { Shortname = "can.beans", Weight = 6, MinAmount = 1, MaxAmount = 4 }, new LootEntry { Shortname = "can.tuna", Weight = 5, MinAmount = 1, MaxAmount = 4 }, new LootEntry { Shortname = "smallwaterbottle", Weight = 6, MinAmount = 1, MaxAmount = 3 }, // Attire (small chance) new LootEntry { Shortname = "hoodie", Weight = 4, MinAmount = 1, MaxAmount = 1 }, new LootEntry { Shortname = "jacket", Weight = 3, MinAmount = 1, MaxAmount = 1 }, new LootEntry { Shortname = "pants", Weight = 4, MinAmount = 1, MaxAmount = 1 }, new LootEntry { Shortname = "shoes.boots", Weight = 3, MinAmount = 1, MaxAmount = 1 }, new LootEntry { Shortname = "mask.bandana", Weight = 3, MinAmount = 1, MaxAmount = 1 }, new LootEntry { Shortname = "hat.cap", Weight = 3, MinAmount = 1, MaxAmount = 1 } } }, ["Locker"] = new LootProfile { MinRolls = 4, MaxRolls = 8, MinItemsPerRoll = 2, MaxItemsPerRoll = 5, Entries = new List { // Attire heavy new LootEntry { Shortname = "hoodie", Weight = 14, MinAmount = 1, MaxAmount = 1 }, new LootEntry { Shortname = "jacket", Weight = 10, MinAmount = 1, MaxAmount = 1 }, new LootEntry { Shortname = "jacket.snow", Weight = 6, MinAmount = 1, MaxAmount = 1 }, new LootEntry { Shortname = "tshirt.long", Weight = 12, MinAmount = 1, MaxAmount = 1 }, new LootEntry { Shortname = "shirt.tanktop", Weight = 12, MinAmount = 1, MaxAmount = 1 }, new LootEntry { Shortname = "shirt.collared", Weight = 8, MinAmount = 1, MaxAmount = 1 }, new LootEntry { Shortname = "pants", Weight = 14, MinAmount = 1, MaxAmount = 1 }, new LootEntry { Shortname = "pants.shorts", Weight = 10, MinAmount = 1, MaxAmount = 1 }, new LootEntry { Shortname = "shoes.boots", Weight = 10, MinAmount = 1, MaxAmount = 1 }, new LootEntry { Shortname = "burlap.shoes", Weight = 8, MinAmount = 1, MaxAmount = 1 }, new LootEntry { Shortname = "hat.cap", Weight = 10, MinAmount = 1, MaxAmount = 1 }, new LootEntry { Shortname = "hat.beenie", Weight = 8, MinAmount = 1, MaxAmount = 1 }, new LootEntry { Shortname = "hat.boonie", Weight = 6, MinAmount = 1, MaxAmount = 1 }, new LootEntry { Shortname = "mask.bandana", Weight = 10, MinAmount = 1, MaxAmount = 1 }, new LootEntry { Shortname = "mask.balaclava", Weight = 6, MinAmount = 1, MaxAmount = 1 }, new LootEntry { Shortname = "burlap.gloves", Weight = 8, MinAmount = 1, MaxAmount = 1 }, new LootEntry { Shortname = "burlap.shirt", Weight = 10, MinAmount = 1, MaxAmount = 1 }, new LootEntry { Shortname = "burlap.trousers", Weight = 10, MinAmount = 1, MaxAmount = 1 }, new LootEntry { Shortname = "burlap.headwrap", Weight = 6, MinAmount = 1, MaxAmount = 1 }, // Cloth / sewing new LootEntry { Shortname = "cloth", Weight = 18, MinAmount = 30, MaxAmount = 120 }, new LootEntry { Shortname = "leather", Weight = 10, MinAmount = 10, MaxAmount = 50 }, new LootEntry { Shortname = "sewingkit", Weight = 12, MinAmount = 1, MaxAmount = 4 }, new LootEntry { Shortname = "rope", Weight = 12, MinAmount = 5, MaxAmount = 25 }, // Resources / components (small) new LootEntry { Shortname = "scrap", Weight = 14, MinAmount = 10, MaxAmount = 60 }, new LootEntry { Shortname = "metal.fragments", Weight = 12, MinAmount = 20, MaxAmount = 100 }, new LootEntry { Shortname = "gears", Weight = 8, MinAmount = 1, MaxAmount = 5 }, new LootEntry { Shortname = "tech.trash", Weight = 6, MinAmount = 1, MaxAmount = 5 }, new LootEntry { Shortname = "tarp", Weight = 5, MinAmount = 1, MaxAmount = 2 } } }, ["Furnace"] = new LootProfile { MinRolls = 5, MaxRolls = 8, MinItemsPerRoll = 2, MaxItemsPerRoll = 4, Entries = new List { new LootEntry { Shortname = "wood", Weight = 30, MinAmount = 550, MaxAmount = 800 }, new LootEntry { Shortname = "metal.ore", Weight = 30, MinAmount = 550, MaxAmount = 800 }, new LootEntry { Shortname = "sulfur.ore", Weight = 25, MinAmount = 500, MaxAmount = 800 }, new LootEntry { Shortname = "hq.metal.ore", Weight = 16, MinAmount = 250, MaxAmount = 800 }, new LootEntry { Shortname = "stones", Weight = 14, MinAmount = 500, MaxAmount = 800 }, new LootEntry { Shortname = "charcoal", Weight = 14, MinAmount = 100, MaxAmount = 400 }, new LootEntry { Shortname = "scrap", Weight = 14, MinAmount = 25, MaxAmount = 120 }, new LootEntry { Shortname = "crude.oil", Weight = 6, MinAmount = 5, MaxAmount = 25 }, new LootEntry { Shortname = "lowgradefuel", Weight = 10, MinAmount = 20, MaxAmount = 80 }, new LootEntry { Shortname = "metal.fragments", Weight = 8, MinAmount = 50, MaxAmount = 200 }, new LootEntry { Shortname = "sulfur", Weight = 6, MinAmount = 20, MaxAmount = 100 }, new LootEntry { Shortname = "gunpowder", Weight = 4, MinAmount = 25, MaxAmount = 100 } } }, }; public string ToJson() => JsonConvert.SerializeObject(this); public Dictionary ToDictionary() => JsonConvert.DeserializeObject>(ToJson()); } protected override void LoadDefaultConfig() => config = new Configuration(); protected override void LoadConfig() { base.LoadConfig(); try { config = Config.ReadObject(); if (config == null) { throw new JsonException(); } if (config.Safezone == null) { config.Safezone = new SafezoneSettings(); SaveConfig(); } if (!config.ToDictionary().Keys.SequenceEqual(Config.ToDictionary(x => x.Key, x => x.Value).Keys)) { PrintWarning("Configuration appears to be outdated; updating and saving"); SaveConfig(); } } catch { PrintWarning($"Configuration file {Name}.json is invalid; using defaults"); LoadDefaultConfig(); } } protected override void SaveConfig() { PrintWarning($"Configuration changes saved to {Name}.json"); Config.WriteObject(config, true); } #endregion public void UpdateData() { Interface.Oxide.DataFileSystem.WriteObject("NPCApartments", dataFile); } void Init() { plugin = this; dataFile = Interface.Oxide.DataFileSystem.ReadObject("NPCApartments"); if (dataFile == null) { dataFile = new StoredData(); } } private void OnNewSave() { dataFile.NPCDATA.Clear(); UpdateData(); } void Unload() { respawnTimer?.Destroy(); bool safezoneActive = config?.Safezone != null && (config.Safezone.RemoveSafezone || config.Safezone.RemoveTurrets || config.Safezone.SpawnDefenderNpcs); if (safezoneActive) { defenderUnloading = true; if (config.Safezone.SpawnDefenderNpcs) { DestroyAllDefenderTickTimers(); DespawnAllDefenderNpcs(); DestroyPendingDefenderRespawnTimers(); } if (config.Safezone.RemoveTurrets) { RestoreTurrets(); } if (config.Safezone.RemoveSafezone) { RestoreSafeZone(); } ClearSafezoneData(); } plugin = null; } void OnServerInitialized(bool initial) { float baseDelay = !initial ? 1f : 10f; timer.Once(baseDelay, () => { apartmentBuilding = ConVar.ApartmentCommands.GetApartmentBuilding(); InitializeNPCApartments(); bool safezoneActive = config?.Safezone != null && (config.Safezone.RemoveSafezone || config.Safezone.RemoveTurrets || config.Safezone.SpawnDefenderNpcs); if (safezoneActive) { LoadSafezoneData(); if (config.Safezone.RemoveSafezone) { timer.Once(baseDelay + 2f, RemoveSafeZones); } if (config.Safezone.SpawnDefenderNpcs) { timer.Once(baseDelay + 5f, SpawnDefenderNpcs); } } }); if (!plugin.config.MasterKeyLimit) { Unsubscribe(nameof(CanAffordApartmentMasterKey)); Unsubscribe(nameof(OnApartmentMasterKeyPurchased)); } } private void OnEntityDeath(ScientistNPC entity, HitInfo info) { if (entity == null || entity.net == null) { return; } if (config?.Safezone == null || !config.Safezone.SpawnDefenderNpcs) { return; } if (defenderUnloading) { return; } ulong id = entity.net.ID.Value; if (!defenderTrackedIds.Remove(id)) { return; } bool stationary = false; Vector3 pos = entity.transform.position; for (int i = defenderTrackers.Count - 1; i >= 0; i--) { DefenderTracker t = defenderTrackers[i]; if (t == null || t.Npc == entity) { if (t != null && t.Npc == entity) { stationary = t.Stationary; } if (t?.TickTimer is { Destroyed: false } timer) timer.Destroy(); defenderTrackers.RemoveAt(i); pos = t.HomePosition; } } botIDs.Remove(entity.userID); defenderNpcs.Remove(entity); int delay = Mathf.Max(1, config.Safezone.DefenderRespawnSeconds); Timer t2 = timer.Once(delay, () => SpawnDefenderNpcAt(pos, stationary)); pendingDefenderRespawns.Add(t2); } void OnApartmentRoomBreakInCompleted(ApartmentRoom apartmentRoom, BasePlayer player, ApartmentDoor door) { foreach (var d in dataFile.NPCDATA) { if (d.RoomNumber == door.RoomNumber) { BasePlayer bn = BaseNetworkable.serverEntities.Find(new NetworkableId(d.PlayerNetId)) as BasePlayer; if (bn != null) { if (bn.inventory != null) { bn.inventory.Strip(); } bn.transform.position = Vector3.zero; bn.Hurt(new HitInfo(null, bn, Rust.DamageType.Hunger, 9999, Vector3.zero)); } } } } //PVE Over-ride bool CanPurchaseMasterKey() => true; bool CanBribeSecurityGuard() => true; //Allow raid only NPC apartments bool CanPlayerCompleteBreakIn(ApartmentRoom apt, BasePlayer player, ApartmentDoor door) { if (apt.Owners.ElementAt(0).IsSteamId()) { return false; } return true; } object CanAffordApartmentMasterKey(BasePlayer player) { bool flag = MasterKeyChecker(LastUseTime, player); if (flag == false) { return false; } return null; } void OnApartmentMasterKeyPurchased(BasePlayer player) { float now = Time.realtimeSinceStartup; if (!LastUseTime.ContainsKey(player.userID)) { LastUseTime.Add(player.userID, now); } else { LastUseTime[player.userID] = now; } player.SetHostileDuration(config.MasterKeyCD); if (player?.Team?.members?.Count > 0) { foreach (var teammate in player?.Team?.members) { BasePlayer p = BasePlayer.FindByID(teammate); if (p != null) { p?.SetHostileDuration(config.MasterKeyCD); } } } var players = Pool.Get>(); Vis.Entities(player.transform.position, 5, players); if (players?.Count > 0) { foreach (var p in players) { if (p != null) { p.SetHostileDuration(config.MasterKeyCD); } } } Pool.FreeUnmanaged(ref players); } public bool MasterKeyChecker(Dictionary LastUseTime, BasePlayer player) { if (LastUseTime == null) { return true; } float now = Time.realtimeSinceStartup; if (LastUseTime.TryGetValue(player.userID, out float lastTime)) { float remaining = plugin.config.MasterKeyCD - (now - lastTime); if (remaining > 0f) { player.ChatMessage($"You must wait {Mathf.CeilToInt(remaining)} seconds before buying another key."); return false; } } return true; } void SpawnNPCApartments(int SpawnAmount) { if (apartmentBuilding == null) { PrintWarning("Apartment building not found on the map!"); return; } Puts("Found Apartment Building @ " + apartmentBuilding.transform.position + " With " + apartmentBuilding.rooms.Count + " Rooms."); int free = apartmentBuilding.rooms.Count - apartmentBuilding.remainingRooms.Count; int need = SpawnAmount; if (free >= SpawnAmount) { for (int i = need; i > 0; i--) { var apartmentRoom = apartmentBuilding.rooms.GetRandom(); if (apartmentRoom.IsCurrentlyRented()) { foreach (var apartmentRoomlist in apartmentBuilding.rooms) { if (apartmentRoomlist.IsCurrentlyRented()) { continue; } apartmentRoom = apartmentRoomlist; break; } } if (apartmentRoom == null) { break; } Puts("Selected Apartment " + apartmentRoom.RoomNumber); Vector3 anchorPos = apartmentRoom.TeleportAnchor.transform.position; Vector3 spawnPos = new Vector3( anchorPos.x + UnityEngine.Random.Range(-0.5f, 0.5f), anchorPos.y, anchorPos.z + UnityEngine.Random.Range(-0.5f, 0.5f) ); Quaternion spawnRot = Quaternion.Euler(0f, UnityEngine.Random.Range(0f, 360f), 0f); BasePlayer basePlayer = GameManager.server.CreateEntity("assets/prefabs/player/player.prefab", spawnPos, spawnRot, true) as BasePlayer; if (basePlayer == null) { PrintWarning("Failed to spawn NPC BasePlayer!"); continue; } basePlayer.EnableSaving(false); basePlayer.Spawn(); basePlayer.StartSleeping(); basePlayer.enableSaving = false; DressNPC(basePlayer, apartmentRoom.RoomNumber); apartmentBuilding.GiveRoomToPlayer(basePlayer, apartmentRoom); NPCA npc = new NPCA { RoomNumber = apartmentRoom.RoomNumber, PlayerNetId = basePlayer.net.ID.Value, userid = basePlayer.userID }; dataFile.NPCDATA.Add(npc); UpdateData(); timer.Once(5, () => { if (apartmentRoom != null) { foreach (BaseEntity furnitureSpawn in apartmentRoom?.Furniture) { StorageContainer storageContainer = furnitureSpawn as StorageContainer; if (storageContainer != null) { storageContainer?.inventory.Clear(); FillContainerByProfile(storageContainer); storageContainer.SendNetworkUpdate(); } } } }); } } else { Puts("No Free Rooms " + apartmentBuilding.remainingRooms.Count); } ScheduleRespawnTimer(); } private void ScheduleRespawnTimer() { respawnTimer?.Destroy(); respawnTimer = null; if (_respawnDisabled) { return; } respawnTimer = timer.Once(config.NPCApartmentsRespawnTimer * 60, () => { if (_respawnDisabled) { return; } RemoveNPCApartments(); NextFrame(() => { SpawnNPCApartments(config.NPCApartments); }); }); } private void InitializeNPCApartments() { if (apartmentBuilding == null) { PrintWarning("Apartment building not found on the map — NPC apartments will not spawn."); return; } bool hasData = dataFile != null && dataFile.NPCDATA != null && dataFile.NPCDATA.Count > 0; if (hasData) { List validEntries = new List(); int droppedMissing = 0; int droppedUnrented = 0; int droppedDuplicate = 0; foreach (var d in dataFile.NPCDATA) { if (d == null || string.IsNullOrEmpty(d.RoomNumber)) { droppedMissing++; continue; } ApartmentRoom room = apartmentBuilding.FindByRoomNumber(d.RoomNumber); if (room == null) { droppedMissing++; continue; } if (!room.IsCurrentlyRented()) { droppedUnrented++; continue; } if (validEntries.Exists(e => e.RoomNumber == d.RoomNumber)) { droppedDuplicate++; continue; } validEntries.Add(d); } if (validEntries.Count > 0) { int restored = 0; foreach (var d in validEntries) { ApartmentRoom room = apartmentBuilding.FindByRoomNumber(d.RoomNumber); if (room == null) { continue; } BasePlayer existing = null; try { existing = BaseNetworkable.serverEntities.Find(new NetworkableId(d.PlayerNetId)) as BasePlayer; } catch { } if (existing != null && !existing.IsDestroyed) { restored++; continue; } if(existing == null) { existing = BasePlayer.FindByID(d.userid); if (existing != null && !existing.IsDestroyed) { restored++; continue; } } BasePlayer npc = SpawnNPCForRoom(room); if (npc != null) { d.PlayerNetId = npc.net.ID.Value; restored++; } } dataFile.NPCDATA = validEntries; UpdateData(); Puts($"Restored {restored} persistent NPC apartments" + (droppedMissing > 0 ? $" (dropped {droppedMissing} missing)" : "") + (droppedUnrented > 0 ? $" (dropped {droppedUnrented} unrented)" : "") + (droppedDuplicate > 0 ? $" (dropped {droppedDuplicate} duplicate)" : "") + "."); ScheduleRespawnTimer(); return; } PrintWarning($"Data file had {dataFile.NPCDATA.Count} NPC apartment entries but none were valid (missing={droppedMissing}, unrented={droppedUnrented}, duplicate={droppedDuplicate}). Spawning fresh apartments."); dataFile.NPCDATA.Clear(); UpdateData(); } SpawnNPCApartments(config.NPCApartments); } private BasePlayer SpawnNPCForRoom(ApartmentRoom room) { if (apartmentBuilding == null) { return null; } if (room == null || room?.TeleportAnchor == null) { return null; } Vector3 anchorPos = room.TeleportAnchor.transform.position; Vector3 spawnPos = new Vector3(anchorPos.x + UnityEngine.Random.Range(-0.5f, 0.5f), anchorPos.y, anchorPos.z + UnityEngine.Random.Range(-0.5f, 0.5f)); Quaternion spawnRot = Quaternion.Euler(0f, UnityEngine.Random.Range(0f, 360f), 0f); BasePlayer basePlayer = GameManager.server.CreateEntity("assets/prefabs/player/player.prefab", spawnPos, spawnRot, true) as BasePlayer; if (basePlayer == null) { PrintWarning($"Failed to spawn NPC BasePlayer for apartment {room.RoomNumber}!"); return null; } basePlayer.EnableSaving(false); basePlayer.Spawn(); basePlayer.StartSleeping(); basePlayer.enableSaving = false; DressNPC(basePlayer, room.RoomNumber); apartmentBuilding.GiveRoomToPlayer(basePlayer, room); timer.Once(3, () => { if (room == null) { return; } foreach (BaseEntity furnitureSpawn in room.Furniture) { StorageContainer storageContainer = furnitureSpawn as StorageContainer; if (storageContainer != null) { storageContainer?.inventory.Clear(); FillContainerByProfile(storageContainer); storageContainer.SendNetworkUpdate(); } } }); return basePlayer; } void RemoveNPCApartments() { if (dataFile?.NPCDATA == null) { return; } foreach (var d in dataFile.NPCDATA) { BasePlayer bn = BaseNetworkable.serverEntities.Find(new NetworkableId(d.PlayerNetId)) as BasePlayer; if (bn != null) { if (bn.inventory != null) { bn.inventory.Strip(); } bn.transform.position = Vector3.zero; bn.Hurt(new HitInfo(null, bn, Rust.DamageType.Hunger, 9999, Vector3.zero)); } if (apartmentBuilding != null) { ApartmentRoom room = apartmentBuilding.FindByRoomNumber(d.RoomNumber); if (room != null) { room.CancelBreakIn(); if ((room.FrontDoor).IsOpen()) { room.FrontDoor.SetFlagLocal(BaseEntity.Flags.Open, false, false); room.FrontDoor.SendNetworkUpdateImmediate(); } apartmentBuilding.Checkout(room); } } Puts("Un-registered Apartment " + d.RoomNumber); } dataFile.NPCDATA.Clear(); UpdateData(); } private LootProfile GetProfile(string name) { if (string.IsNullOrEmpty(name)) { return null; } if (config?.LootProfiles == null) { return null; } if (config.LootProfiles.TryGetValue(name, out var profile) && profile != null && profile.Enabled) { return profile; } return null; } private LootProfile GetLootProfileForContainer(StorageContainer container) { if (container == null || config?.LootProfiles == null) { return null; } if (container is ApartmentUpkeepTerminal) { return null; } string sp = (container.ShortPrefabName ?? "").ToLower().Replace('_', '.'); string profileName = null; if (sp.Contains("apartment.bathroom")) profileName = "Bathroom"; else if (sp.Contains("apartment.cupboard")) profileName = "KitchenCupboard"; else if (sp.Contains("apartment.counter")) profileName = "KitchenCounter"; else if (sp.Contains("apartment.large.furnace") || sp.Contains("apartment.furnace")) { profileName = "Furnace"; } else if (sp.Contains("furnace")) { profileName = "Furnace"; } else if (sp.Contains("locker")) profileName = "Locker"; else if (sp == "box.wooden.large" || sp.Contains("largewoodbox") || sp.Contains("large_box") || sp.Contains("largebox")) profileName = "StorageBox"; else if (sp.Contains("woodbox") || sp.Contains("wood_box") || sp.Contains("smallbox") || sp.Contains("small_box")) profileName = "StorageBox"; else profileName = "Generic"; if (profileName != null) { var profile = GetProfile(profileName); if (profile != null) { return profile; } } if (config.LootProfiles.TryGetValue("Generic", out var generic) && generic.Enabled) { return generic; } return null; } private LootEntry PickWeightedEntry(List entries) { if (entries == null || entries.Count == 0) { return null; } int totalWeight = 0; foreach (var e in entries) totalWeight += Mathf.Max(1, e.Weight); int roll = UnityEngine.Random.Range(0, totalWeight); int cumulative = 0; foreach (var e in entries) { cumulative += Mathf.Max(1, e.Weight); if (roll < cumulative) { return e; } } return entries[0]; } private void FillContainerByProfile(StorageContainer target) { if (target == null || target.inventory == null) { return; } var profile = GetLootProfileForContainer(target); if (profile == null || profile.Entries == null || profile.Entries.Count == 0) { return; } int minRolls = Mathf.Max(0, profile.MinRolls); int maxRolls = Mathf.Max(minRolls, profile.MaxRolls); int rolls = UnityEngine.Random.Range(minRolls, maxRolls + 1); int minPerRoll = Mathf.Max(0, profile.MinItemsPerRoll); int maxPerRoll = Mathf.Max(minPerRoll, profile.MaxItemsPerRoll); int totalAdded = 0; for (int i = 0; i < rolls; i++) { int itemsThisRoll = UnityEngine.Random.Range(minPerRoll, maxPerRoll + 1); for (int j = 0; j < itemsThisRoll; j++) { var entry = PickWeightedEntry(profile.Entries); if (entry == null || string.IsNullOrEmpty(entry.Shortname)) { continue; } int minAmt = Mathf.Max(1, entry.MinAmount); int maxAmt = Mathf.Max(minAmt, entry.MaxAmount); int amount = UnityEngine.Random.Range(minAmt, maxAmt + 1); Item item = ItemManager.CreateByName(entry.Shortname, amount); if (item == null) { continue; } if (amount != item.amount) { item.amount = amount; } if (item.MoveToContainer(target.inventory)) { totalAdded++; } else { item.Remove(); } } } target.SendNetworkUpdate(); if (target is BaseOven) { timer.Once(3, () => { (target as BaseOven).StartCooking(); }); } } private string ResolveNPCName(string roomNumber) { if (config.UseRandomUsernames) { try { string username = RandomUsernames.Get((ulong)UnityEngine.Random.Range(0, 10000000)); if (!string.IsNullOrWhiteSpace(username)) { return username; } } catch { } } return string.Format(config.NPCDisplayName ?? "Resident {0}", roomNumber); } private const ulong BotIdMin = 514920000; private const ulong BotIdMax = 514922525; private ulong GetFreeBotId() { ulong start = (ulong)UnityEngine.Random.Range((int)BotIdMin, (int)BotIdMax + 1); ulong id = start; do { if (!botIDs.Contains(id)) { return id; } id++; if (id > BotIdMax) { id = BotIdMin; } } while (id != start); return 0; } private void DressNPC(BasePlayer player, string roomNumber) { if (player == null) return; try { string name = ResolveNPCName(roomNumber); player.displayName = name; if (config.DressNPCs && config.NPCClothingItems != null && config.NPCClothingItems.Count > 0) { int minItems = Mathf.Max(1, config.NPCMinClothing); int maxItems = Mathf.Max(minItems, config.NPCMaxClothing); int itemCount = Mathf.Clamp(UnityEngine.Random.Range(minItems, maxItems + 1), 1, config.NPCClothingItems.Count); var picked = config.NPCClothingItems .Where(s => !string.IsNullOrEmpty(s)) .OrderBy(x => UnityEngine.Random.value) .Take(itemCount) .ToList(); foreach (string shortname in picked) { Item item = ItemManager.CreateByName(shortname); if (item == null) { PrintWarning($"Could not create clothing item '{shortname}' — invalid shortname?"); continue; } if (!item.MoveToContainer(player.inventory.containerWear)) { item.Remove(); } } } player.StartSleeping(); player.SendNetworkUpdate(); } catch (Exception ex) { PrintWarning($"Error dressing NPC: {ex.Message}"); } } private void RemoveSafeZones() { if (!config.Safezone.RemoveSafezone) { return; } if (apartmentBuilding == null) { return; } foreach (var safeZone in TriggerSafeZone.allSafeZones.ToArray()) { Vector3 center = safeZone.triggerCollider != null ? safeZone.triggerCollider.bounds.center : safeZone.transform.position; float d = Vector3.Distance(apartmentBuilding.transform.position, center); if (Vector3.Distance(apartmentBuilding.transform.position, center) > 100f) { continue; } Puts($"Removing Apartment SafeZone @ {center}"); removedSafeZone = safeZone; safeZone.enabled = false; if (config.Safezone.RemoveTurrets) { float radius = 2f; if (safeZone.triggerCollider != null) { var b = safeZone.triggerCollider.bounds; radius = Mathf.Max(b.extents.x, b.extents.z); } using var turrets = Pool.Get>(); Vis.Entities(center, radius, turrets); foreach (var turret in turrets) { if (turret == null || turret.IsDestroyed) { continue; } if (safezoneData == null) { safezoneData = new SafezoneStoredData(); } safezoneData.Turrets.Add(new TurretInfo { Prefab = turret.PrefabName, Position = turret.transform.position, RotationEuler = turret.transform.rotation.eulerAngles }); turret.Kill(); } Puts($"Killed {turrets.Count} Apartment Turrets"); SaveSafezoneData(); } break; } } private void RestoreTurrets() { if (!config.Safezone.RemoveTurrets) { return; } if (safezoneData == null || safezoneData.Turrets.Count == 0) { return; } foreach (var t in safezoneData.Turrets) { var entity = GameManager.server.CreateEntity(t.Prefab, t.Position, Quaternion.Euler(t.RotationEuler), true); if (entity == null) { continue; } entity.Spawn(); } Puts($"Restored {safezoneData.Turrets.Count} turrets"); } private void RestoreSafeZone() { if (!config.Safezone.RemoveSafezone) { return; } if (removedSafeZone == null) { return; } removedSafeZone.enabled = true; Puts("Apartment SafeZone restored"); } private void SpawnDefenderNpcs() { if (!config.Safezone.SpawnDefenderNpcs) { return; } if (apartmentBuilding == null) { PrintWarning("ApartmentBuilding not found — skipping defender NPC spawn."); return; } Vector3 pos = Vector3.zero; Vector3 rot = Vector3.zero; foreach (var prefabdata in World.Serialization.world.prefabs) { if (prefabdata.id == 2486362524) { pos = new Vector3(prefabdata.position.x, prefabdata.position.y, prefabdata.position.z); rot = new Vector3(prefabdata.rotation.x, prefabdata.rotation.y, prefabdata.rotation.z); } } string prefab = string.IsNullOrWhiteSpace(config.Safezone.DefenderPrefab) ? DefenderPrefab : config.Safezone.DefenderPrefab; foreach (DefenderSpawnPoint sp in DefenderLocalOffsets) { Vector3 worldPos = pos + (Quaternion.Euler(rot) * sp.LocalOffset); SpawnDefenderNpcAt(worldPos, prefab, sp.Stationary); } Puts($"Spawned {defenderNpcs.Count} apartment defender NPCs"); } private void SpawnDefenderNpcAt(Vector3 worldPos, bool stationary = false) { SpawnDefenderNpcAt(worldPos, config?.Safezone?.DefenderPrefab, stationary); } private void SpawnDefenderNpcAt(Vector3 worldPos, string prefab, bool stationary = false) { if (string.IsNullOrWhiteSpace(prefab)) { prefab = DefenderPrefab; } if (!stationary && Rust.Ai.Gen2.RustNavMesh.SamplePosition(worldPos, out var _navHit, 5f, 25)) worldPos = _navHit.position; BaseEntity baseEntity = GameManager.server.CreateEntity(prefab, worldPos, Quaternion.identity, true); if (baseEntity == null) { Puts($"Failed to spawn defender NPC @ {worldPos}"); return; } ScientistNPC npc = baseEntity as ScientistNPC; if (npc == null) { Puts($"Defender prefab '{prefab}' is not a ScientistNPC — check the path."); baseEntity.Kill(); return; } npc.enableSaving = false; ulong botid = GetFreeBotId(); if (botid == 0) { Puts("Failed to allocate NPC ID - ID range exhausted!"); return; } botIDs.Add(botid); npc.userID = botid; npc.UserIDString = botid.ToString(); ScientistBrain brain = npc.GetComponent(); if (brain != null) { brain.AttackRangeMultiplier = Mathf.Max(0.1f, config.Safezone.DefenderAttackRange); brain.SenseRange = Mathf.Max(1f, config.Safezone.DefenderSenseRange); brain.CheckVisionCone = true; brain.CheckLOS = true; brain.HostileTargetsOnly = true; } npc.Spawn(); npc.InitializeHealth(config.Safezone.DefenderHealth, config.Safezone.DefenderHealth); DefenderTracker tracker = new DefenderTracker { Npc = npc, HomePosition = worldPos, Stationary = stationary, NextTargetCheck = 0f }; defenderTrackers.Add(tracker); defenderNpcs.Add(npc); defenderTrackedIds.Add(npc.net.ID.Value); NextFrame(() => { if (npc == null || npc.IsDestroyed) { return; } try { if (npc.Brain != null) { if (npc.Brain.Events?.Memory != null) { npc.Brain.Events.Memory.Position.Set(npc.transform.position, 4); } BaseNavigator nav = npc.Brain.Navigator; if (nav != null) { float roam = Mathf.Max(1f, config.Safezone.DefenderRoamDistance); nav.MaxRoamDistanceFromHome = roam; nav.BestMovementPointMaxDistance = roam; nav.BestRoamPointMaxDistance = roam; nav.DefaultArea = "Walkable"; nav.MaxWaterDepth = 1f; nav.CanUseNavMesh = true; nav.CanUseAStar = true; nav.CanUseBaseNav = true; nav.Init(npc, nav.Agent); if (stationary) { nav.SetCurrentNavigationType(BaseNavigator.NavigationType.None); } else { nav.SetCurrentNavigationType(BaseNavigator.NavigationType.NavMesh); } } } } catch (Exception ex) { PrintWarning($"Failed to setup defender NPC @ {worldPos}: {ex.Message}"); } }); float initialDelay = Mathf.Max(2f, config.Safezone.DefenderTickInterval); tracker.TickTimer = timer.Once(initialDelay, () => DefenderNPCTick(tracker)); } private void DefenderNPCTick(DefenderTracker tracker) { if (tracker == null) { return; } if (tracker.Npc == null || tracker.Npc.IsDestroyed || tracker.Npc.IsDead()) { return; } if (tracker.Stationary) { return; } tracker.TickTimer = null; try { float senseRange = config.Safezone.DefenderSenseRange; float roamRadius = Mathf.Max(1f, config.Safezone.DefenderRoamDistance); BaseNavigator nav = tracker.Npc.Brain?.Navigator; BaseEntity target = tracker.CurrentTarget; if (target == null || target.IsDestroyed || tracker.Npc.Distance(target) > senseRange * 1.5f) { target = null; } if (target == null && Time.realtimeSinceStartup >= tracker.NextTargetCheck) { tracker.NextTargetCheck = Time.realtimeSinceStartup + 0.5f; if (tracker.Npc.Brain != null && tracker.Npc.Brain.Senses != null) { target = tracker.Npc.Brain.Senses.GetNearestTarget(senseRange); } } tracker.CurrentTarget = target; if (target != null && nav != null) { float dist = tracker.Npc.Distance(target); BaseNavigator.NavigationSpeed speed = dist > 15f ? BaseNavigator.NavigationSpeed.Normal : BaseNavigator.NavigationSpeed.Slow; nav.SetDestination(target.transform.position, speed); } else if (nav != null) { Vector3 npcPos = tracker.Npc.transform.position; float distFromHome = Vector3.Distance(npcPos, tracker.HomePosition); if (distFromHome > roamRadius) { nav.SetDestination(tracker.HomePosition, BaseNavigator.NavigationSpeed.Slow); } else if (!nav.Moving) { Vector3 dest = PickRoamDestination(tracker, roamRadius); if (dest != Vector3.zero) { nav.SetDestination(dest, BaseNavigator.NavigationSpeed.Slow); } } foreach (DefenderTracker other in defenderTrackers) { if (other == tracker || other.Npc == null || other.Npc.IsDestroyed) continue; if (other.Npc.Distance(tracker.Npc) < 3f) { Vector3 nudge = PickRoamDestination(tracker, roamRadius); if (nudge != Vector3.zero) { nav.SetDestination(nudge, BaseNavigator.NavigationSpeed.Slow); } break; } } } } catch (Exception ex) { PrintWarning($"DefenderNPCTick threw for {tracker.Npc?.displayName ?? "?"}: {ex.Message}"); } float interval = Mathf.Max(0.5f, config.Safezone.DefenderTickInterval); tracker.TickTimer = timer.Once(interval, () => DefenderNPCTick(tracker)); } private Vector3 PickRoamDestination(DefenderTracker tracker, float roamRadius) { for (int attempt = 0; attempt < 4; attempt++) { Vector2 offset = UnityEngine.Random.insideUnitCircle * roamRadius; Vector3 dest = tracker.HomePosition + new Vector3(offset.x, 0f, offset.y); dest.y = TerrainMeta.HeightMap.GetHeight(dest); bool crowded = false; foreach (DefenderTracker other in defenderTrackers) { if (other == tracker || other.Npc == null || other.Npc.IsDestroyed) continue; if (Vector3.Distance(dest, other.Npc.transform.position) < 3f) { crowded = true; break; } } if (!crowded) { return dest; } } return tracker.HomePosition; } public Vector3 RandomOffsetXZ(Vector3 original, float radius = 4f) { float angle = UnityEngine.Random.Range(0f, Mathf.PI * 2f); float distance = Mathf.Sqrt(UnityEngine.Random.Range(0f, 1f)) * radius; float x = Mathf.Cos(angle) * distance; float z = Mathf.Sin(angle) * distance; return new Vector3(original.x + x, original.y, original.z + z); } private void DestroyAllDefenderTickTimers() { foreach (DefenderTracker t in defenderTrackers) { if (t?.TickTimer is { Destroyed: false } timer) timer.Destroy(); } defenderTrackers.Clear(); } private void DespawnAllDefenderNpcs() { foreach (BaseCombatEntity entity in defenderNpcs) { if (entity != null && !entity.IsDestroyed) { entity.Kill(); } } defenderNpcs.Clear(); defenderTrackedIds.Clear(); } private void DestroyPendingDefenderRespawnTimers() { foreach (Timer t in pendingDefenderRespawns) { if (t != null && !t.Destroyed) { t.Destroy(); } } pendingDefenderRespawns.Clear(); } private void LoadSafezoneData() { safezoneData = Interface.Oxide.DataFileSystem.ReadObject(SafezoneDataFileName) ?? new SafezoneStoredData(); } private void SaveSafezoneData() { if (safezoneData == null) { safezoneData = new SafezoneStoredData(); } Interface.Oxide.DataFileSystem.WriteObject(SafezoneDataFileName, safezoneData); } private void ClearSafezoneData() { if (safezoneData != null) { safezoneData.Turrets.Clear(); SaveSafezoneData(); } } #region Admin Commands [ChatCommand("npcapt")] private void CmdNpcApartment(BasePlayer player, string command, string[] args) { if (player == null || !player.IsAdmin) { SendReply(player, "You must be an admin to use this command."); return; } if (args == null || args.Length == 0) { SendReply(player, "NPC Apartment commands:"); SendReply(player, "/npcapt restart - Remove all NPC apartments (if any) and spawn a fresh batch"); SendReply(player, "/npcapt stop - Remove all NPC apartments and disable auto-respawn"); return; } switch (args[0].ToLower()) { case "restart": CmdNpcApartmentRestart(player); break; case "stop": CmdNpcApartmentStop(player); break; case "fix": CheckForGlitchedApartments(); break; default: SendReply(player, $"Unknown subcommand: {args[0]}. Use /npcapt for help."); break; } } private void CmdNpcApartmentRestart(BasePlayer player) { LoadConfig(); SendReply(player, "Restarting NPC apartments..."); respawnTimer?.Destroy(); respawnTimer = null; int existing = dataFile?.NPCDATA?.Count ?? 0; if (existing > 0) { RemoveNPCApartments(); } _respawnDisabled = false; NextFrame(() => { SpawnNPCApartments(config.NPCApartments); SendReply(player, $"NPC apartments restarted. Active apartments: {dataFile?.NPCDATA?.Count ?? 0}."); }); } private void CmdNpcApartmentStop(BasePlayer player) { SendReply(player, "Stopping NPC apartments..."); _respawnDisabled = true; respawnTimer?.Destroy(); respawnTimer = null; int existing = dataFile?.NPCDATA?.Count ?? 0; if (existing > 0) { RemoveNPCApartments(); } SendReply(player, "NPC apartments stopped. Use /npcapt restart to bring them back."); } private void CheckForGlitchedApartments() { if (apartmentBuilding == null){return;} Transform buildingTransform = apartmentBuilding.transform; Quaternion buildingRotation = buildingTransform.rotation; Vector3 buildingPosition = buildingTransform.position; foreach (var data in DefaultRooms) { ApartmentRoom existing = apartmentBuilding.FindByRoomNumber(data.RoomNumber); if (existing != null) { continue; } Vector3 worldPos = buildingPosition + (buildingRotation * data.LocalPosition); Quaternion worldRot = buildingRotation * data.LocalRotation; var entity = GameManager.server.CreateEntity("assets/prefabs/apartment/" + data.Prefab + "_apartment.prefab", worldPos, worldRot, true); if (entity == null) { PrintWarning($"Failed to create {data.RoomNumber}"); continue; } entity.Spawn(); var room = entity.GetComponent(); if (room == null) { PrintWarning($"{data.RoomNumber} isn't an ApartmentRoom"); entity.Kill(); continue; } room.RoomNumber = data.RoomNumber; room.transform.SetParent(buildingTransform, true); room.transform.localPosition = data.LocalPosition; room.transform.localRotation = data.LocalRotation; room.SetupFrontDoor(false); apartmentBuilding.rooms.Add(room); Puts($"Respawned glitched apartment {data.RoomNumber} at world {worldPos}"); } } #endregion } }