I want to add this functionality to my plugin, similar to what I already did for autoturrets:


GunTrap entity = SpawnEntity("assets/prefabs/deployable/single shot trap/guntrap.deployed.prefab", location.Position, location.Rotation) as GunTrap;
and


FlameTurret entity = SpawnEntity("assets/prefabs/npc/flame turret/flameturret.deployed.prefab", location.Position, location.Rotation) as FlameTurret;
I would like the GunTraps and FlameTurrets to be configured the same way as autoturrets, with all the features and settings already implemented for autoturrets. However, I want GunTraps to not require a spawn point on the ground—they will be placed on walls instead. Please provide everything I need to modify or add to my plugin to make this work, including full and complete functions, and ensure that ammo for these new traps is infinite. This configuration does not need to include any comments in the code.

Please resend the entire implementation with these requirements in mind.


functions from other plugins :

private void FillAmmoGunTrap(GunTrap gt)
{
    if (IsUnloading || isAuthorized || gt.IsKilled())
    {
        return;
    }

    gt.ammoType ??= ItemManager.FindItemDefinition("ammo.handmade.shell");

    var ammo = gt.inventory.GetSlot(0);

    if (ammo == null)
    {
        gt.inventory.AddItem(gt.ammoType, config.Weapons.Ammo.GunTrap);
    }
    else ammo.amount = config.Weapons.Ammo.GunTrap;
}
private void SetupGunTrap(GunTrap gt)
{
    if (config.Weapons.Ammo.GunTrap > 0)
    {
        FillAmmoGunTrap(gt);
    }

    if (config.Weapons.InfiniteAmmo.GunTrap)
    {
        gt.inventory.onPreItemRemove += new Action<Item>(OnWeaponItemPreRemove);
    }

    triggers[gt.trigger] = gt;
}
private void OnWeaponItemPreRemove(Item item)
{
    if (isAuthorized || IsUnloading || IsDespawning)
    {
        return;
    }
    else if (!priv.IsKilled() && IsAuthorized())
    {
        isAuthorized = true;
        return;
    }
    else if (privSpawned && priv.IsKilled())
    {
        isAuthorized = true;
        return;
    }

    var weapon = item.parent?.entityOwner;

    if (weapon is AutoTurret turret)
    {
        weapon.Invoke(() => FillAmmoTurret(turret), 0.1f);
    }
    else if (weapon is GunTrap gt)
    {
        weapon.Invoke(() => FillAmmoGunTrap(gt), 0.1f);
    }
    else if (weapon is SamSite ss)
    {
        weapon.Invoke(() => FillAmmoSamSite(ss), 0.1f);
    }
}

private void SpawnGunTraps()
{
    foreach (EntityLocation location in _gunTrapLocations)
    {
        GunTrap entity = SpawnEntity("assets/prefabs/deployable/single shot trap/guntrap.deployed.prefab", location.Position, location.Rotation) as GunTrap;
        entity.SetFlag(BaseEntity.Flags.Locked, true);
        Entities.Add(entity);
        GunTraps.Add(entity);
    }
}

internal void UpdateAmmoGunTrap(int amount)
{
    if (GunTraps.Count == 0)
        return;
    foreach (GunTrap turret in GunTraps)
    {
        for (int i = turret.inventory.itemList.Count - 1; i >= 0; i--)
        {
            Item item = turret.inventory.itemList[i];
            amount += item.amount;
            item.RemoveFromContainer();
            item.Remove();
        }
    }
    if (amount < 1)
        return;
    int amountTurrets = GunTraps.Count;
    int amountInventory = amount % amountTurrets;
    if (amountInventory > 0)
        AddInventory("ammo.handmade.shell", amountInventory, 0, "");
    int amountPerTurret = (amount - amountInventory) / amountTurrets;
    if (amountPerTurret > 0)
        foreach (GunTrap turret in GunTraps)
            turret.inventory.Insert(ItemManager.CreateByName("ammo.handmade.shell", amountPerTurret));
}                        

private void SpawnFlameTurrets()
{
    foreach (EntityLocation location in _flameTurretsLocations)
    {
        FlameTurret entity = SpawnEntity("assets/prefabs/npc/flame turret/flameturret.deployed.prefab", location.Position, location.Rotation) as FlameTurret;
        entity.SetFlag(BaseEntity.Flags.Locked, true);
        Entities.Add(entity);
        FlameTurrets.Add(entity);
    }
}            

internal void UpdateAmmoFlameTurret(int amount)
{
    if (FlameTurrets.Count == 0)
        return;
    foreach (FlameTurret turret in FlameTurrets)
    {
        for (int i = turret.inventory.itemList.Count - 1; i >= 0; i--)
        {
            Item item = turret.inventory.itemList[i];
            amount += item.amount;
            item.RemoveFromContainer();
            item.Remove();
        }
    }
    if (amount < 1)
        return;
    int amountTurrets = FlameTurrets.Count;
    int amountInventory = amount % amountTurrets;
    if (amountInventory > 0)
        AddInventory("lowgradefuel", amountInventory, 0, "");
    int amountPerTurret = (amount - amountInventory) / amountTurrets;
    if (amountPerTurret > 0)
        foreach (FlameTurret turret in FlameTurrets)
            turret.inventory.Insert(ItemManager.CreateByName("lowgradefuel", amountPerTurret));
}            

internal int GetCountAmmoFlameTurrets()
{
    if (FlameTurrets.Count == 0)
        return 0;
    int amount = 0;
    foreach (FlameTurret turret in FlameTurrets)
    foreach (Item item in turret.inventory.itemList)
        amount += item.amount;
    return amount;
}

internal int GetCountAmmoGunTraps()
{
    if (GunTraps.Count == 0)
        return 0;
    int amount = 0;
    foreach (GunTrap turret in GunTraps)
    foreach (Item item in turret.inventory.itemList)
        amount += item.amount;
    return amount;
}            

PLUGIN TO IMPLEMENT:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Facepunch;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core;
using Oxide.Core.Configuration;
using Oxide.Core.Plugins;
using Rust;
using UnityEngine;
using Random = UnityEngine.Random;

namespace Oxide.Plugins
{
    [Info("Dungeon Events", "Marte6", "1.1.5")]
    [Description("Create dungeons populated with NPCs, turrets and loot containers.")]
    public class DungeonEvents : RustPlugin
    {
        [PluginReference]
        Plugin NpcSpawn = null,
            Friends = null,
            Economics = null,
            ServerRewards = null,
            Notify = null;

        private Configuration _config;
        private DynamicConfigFile _dataFile;
        private DungeonData _dungeonData;
        private Timer _autoSpawnTimer;
        private Timer _dungeonCheckTimer;
        private Timer _updateMarkersTimer;
        private Timer _spawnPointScanner;
        private Timer _retryFailedDungeonsTimer;
        private Dictionary<DungeonTierConfig, string> _tierNameMap;
        private List<(Vector3 Position, Quaternion Rotation)> _validSpawnPoints = new();
        private List<DungeonRequest> _failedDungeons = new List<DungeonRequest>();
        private Queue<DungeonRequest> _dungeonQueue = new Queue<DungeonRequest>();
        private VersionNumber PreviousVersion;
        private bool _isCreatingDungeon = false;
        private const float TimeTimers = 60f;
        private const ulong OwnerID = 10203040505;
        private const string PermissionAdmin = "dungeonevents.admin";
        private const string PermissionEnter = "dungeonevents.enter";

        #region Hooks

        private void OnServerInitialized()
        {
            if (!NpcSpawn)
            {
                NextTick(() =>
                {
                    PrintError("You don't have NpcSpawn free codefling.com plugin installed.");
                    Unsubscribe("Unload");
                    Interface.Oxide.UnloadPlugin("DungeonEvents");
                });
                return;
            }

            permission.RegisterPermission(PermissionEnter, this);
            permission.RegisterPermission(PermissionAdmin, this);

            InitializeTierNameMap();
            LoadData();
            RemoveAllDungeons();
            StartSpawnPointScanner();
            StartTimerAutoSpawnDungeons();
            StartTimerRemoveInactiveDungeons();
            StartTimerUpdateMarkers();
            StartRetryFailedDungeonTimer();
            SaveData();
        }

        private void OnServerSave() => SaveData();

        private void Unload()
        {
            if (CoroutineManager.Instance != null)
            {
                UnityEngine.Object.Destroy(CoroutineManager.Instance.gameObject);
            }

            RemoveAllDungeons();
            _autoSpawnTimer?.Destroy();
            _dungeonCheckTimer?.Destroy();
            _updateMarkersTimer?.Destroy();
            _spawnPointScanner?.Destroy();
            _retryFailedDungeonsTimer?.Destroy();
            SaveData();
        }

        object OnTurretTarget(AutoTurret turret, BaseCombatEntity target)
        {
            if (IsDungeonEntity(target))
            {
                return false;
            }

            return null;
        }

        object OnEntityTakeDamage(ScientistNPC scientistNPC, HitInfo info)
        {
            if (scientistNPC == null || info == null || info.Initiator == null)
                return null;

            AutoTurret autoTurret = info.Initiator as AutoTurret;
            if (autoTurret == null)
                return null;
            if (IsDungeonEntity(autoTurret))
                return true;

            return null;
        }

        private object CanEntityTakeDamage(BasePlayer victim, HitInfo info)
        {
            if (info == null || info.Initiator == null || info.Initiator.net == null || !IsPlayer(victim))
                return null;

            if (info.Initiator is ScientistNPC)
            {
                if (IsDungeonEntity(info.Initiator))
                {
                    return true;
                }
            }

            if (info.Initiator is AutoTurret)
            {
                if (IsDungeonEntity(info.Initiator))
                {
                    return true;
                }
            }

            return null;
        }

        object CanEntityTakeDamage(AutoTurret autoTurret, HitInfo hitinfo)
        {
            if (autoTurret == null || hitinfo == null)
                return null;

            if (IsDungeonEntity(autoTurret))
            {
                if (hitinfo.InitiatorPlayer == null || !IsPlayer(hitinfo.Initiator as BasePlayer))
                    return false;
                else
                    return true;
            }

            return null;
        }

        object CanEntityTakeDamage(StorageContainer storageContainer, HitInfo hitinfo)
        {
            if (storageContainer == null || hitinfo == null)
                return null;

            if (IsDungeonEntity(storageContainer))
            {
                if (hitinfo.InitiatorPlayer == null || !IsPlayer(hitinfo.Initiator as BasePlayer))
                    return false;
                else
                    return true;
            }

            return null;
        }

        private object OnPortalUse(BasePlayer player, BasePortal portal)
        {
            ActiveDungeon activeDungeon = GetActiveDungeonByPortal(portal);
            if (activeDungeon == null)
            {
                return null;
            }

            if (_config.LockDungeonToPlayer && !CanEnter(player, activeDungeon))
            {
                NotifyPlayer(player, "NoAccess", 0);
                return false;
            }

            if (!activeDungeon.Spawned)
            {
                NotifyPlayer(player, "DungeonStillSpawningMessage", 0);
                return false;
            }

            return null;
        }

        private void OnCorpsePopulate(ScientistNPC entity, NPCPlayerCorpse corpse)
        {
            if (entity == null)
                return;

            if (IsDungeonEntity(entity))
            {
                NextTick(() =>
                {
                    if (corpse == null)
                        return;

                    Effect.server.Run("assets/prefabs/weapons/flashbang/effects/fx-flashbang-boom.prefab", corpse.transform.position);

                    var container = corpse.containers[0];
                    container.Clear();
                    if (!corpse.IsDestroyed)
                        corpse.Kill();
                });
            }
        }

        #endregion

        #region Chat Commands

        [ChatCommand("buydungeon")]
        private void BuyDungeonCommand(BasePlayer player, string command, string[] args)
        {
            if (args.Length == 0)
            {
                NotifyPlayer(player, "UsageBuyMessage", 0);
                return;
            }

            if (_dungeonData.ActiveDungeons.Any(d => d.OwnerID == player.userID) || _dungeonQueue.Any(r => r.Player != null && r.Player.userID == player.userID))
            {
                NotifyPlayer(player, "AlreadyOwnsDungeonMessage", 0);
                return;
            }

            string tierName = args[0].ToLower();
            var tier = _config.Tiers.GetType().GetProperties().FirstOrDefault(prop => prop.Name.ToLower() == tierName)?.GetValue(_config.Tiers) as DungeonTierConfig;

            if (tier == null)
            {
                NotifyPlayer(player, "InvalidTierMessage", 0, tierName, GetAvailableTiers());
                return;
            }

            int cost = tier.BuyCost;

            if (!CheckBalance(player, cost))
            {
                NotifyPlayer(player, "InsufficientFundsMessage", 0, tierName, cost);
                return;
            }

            if (!WithdrawFunds(player, cost))
            {
                NotifyPlayer(player, "WithdrawFailedMessage", 0, tierName);
                return;
            }

            NotifyPlayer(player, "PurchaseSuccessMessage", 0, tierName);

            LogDebug("Received dungeon creation request.", 0);

            EnqueueDungeonRequest(player, tier, true);
        }

        [ChatCommand("createdungeon")]
        private void CreateDungeonCommand(BasePlayer player, string command, string[] args)
        {
            if (!IsAdmin(player))
            {
                NotifyPlayer(player, "NoAccess", 0);
                return;
            }

            if (args.Length == 0)
            {
                NotifyPlayer(player, "UsageCreateMessage", 0);
                return;
            }

            string tierName = args[0].ToLower();
            var tier = _config.Tiers.GetType().GetProperties().FirstOrDefault(prop => prop.Name.ToLower() == tierName)?.GetValue(_config.Tiers) as DungeonTierConfig;

            if (tier == null)
            {
                NotifyPlayer(player, "InvalidTierMessage", 0, tierName, GetAvailableTiers());
                return;
            }

            LogDebug("Received dungeon creation request.", 1);

            EnqueueDungeonRequest(player, tier, false);
        }

        [ChatCommand("removeinactivedungeons")]
        private void RemoveInactiveDungeonsCommand(BasePlayer player, string command, string[] args)
        {
            if (!IsAdmin(player))
                return;

            var removedDungeons = RemoveInactiveDungeons();
            NotifyPlayerOnRemoval(player, removedDungeons);
        }

        [ChatCommand("removealldungeons")]
        private void RemoveAllDungeonsCommand(BasePlayer player, string command, string[] args)
        {
            if (!IsAdmin(player))
                return;

            var removedDungeons = RemoveAllDungeons();
            NotifyPlayerOnRemoval(player, removedDungeons);
        }

        [ChatCommand("forceremovealldungeons")]
        private void ForceRemoveAllDungeonsCommand(BasePlayer player, string command, string[] args)
        {
            if (!IsAdmin(player))
                return;

            CleanupExistingEntities();
            NotifyPlayer(player, "CleanedUp", 0);
        }

        #endregion

        #region Dungeon Management

        private class DungeonRequest
        {
            public BasePlayer Player { get; set; }
            public DungeonTierConfig Tier { get; set; }
            public bool LockToPlayer { get; set; }
        }

        private void StartRetryFailedDungeonTimer()
        {
            _retryFailedDungeonsTimer = timer.Every(
                30,
                () =>
                {
                    if (_failedDungeons.Count > 0)
                    {
                        LogDebug("Retrying failed dungeons...", 2);

                        for (int i = _failedDungeons.Count - 1; i >= 0; i--)
                        {
                            var request = _failedDungeons[i];
                            _failedDungeons.RemoveAt(i);

                            EnqueueDungeonRequest(request.Player, request.Tier);
                        }
                    }
                }
            );
        }

        private void StartSpawnPointScanner()
        {
            _spawnPointScanner = timer.Every(
                TimeTimers,
                () =>
                {
                    ScanSpawnPoints();
                }
            );
        }

        private void StartTimerAutoSpawnDungeons()
        {
            if (_config.DungeonSpawn.EnableAutoSpawn)
            {
                _autoSpawnTimer = timer.Every(
                    TimeTimers,
                    () =>
                    {
                        AutoSpawnDungeon();
                    }
                );
            }
        }

        private void StartTimerRemoveInactiveDungeons()
        {
            _dungeonCheckTimer = timer.Every(
                TimeTimers,
                () =>
                {
                    var removedDungeons = RemoveInactiveDungeons();
                    if (removedDungeons.Count > 0)
                    {
                        foreach (var dungeon in removedDungeons)
                        {
                            LogDebug($"Removed inactive dungeon of tier {dungeon.TierName} at grid {dungeon.Grid}.", 1);
                        }
                    }
                }
            );
        }

        private void StartTimerUpdateMarkers()
        {
            _updateMarkersTimer = timer.Every(TimeTimers, () => UpdateMarkers());
        }

        private void ScanSpawnPoints()
        {
            int pointsScanned = 0;
            int maxPointsToFind = 1000;

            int initialCount = _validSpawnPoints.Count;

            while (pointsScanned < 100 && _validSpawnPoints.Count < maxPointsToFind)
            {
                if (TryFindDungeonSpawnPoint(out var position, out var rotation))
                {
                    if (IsSafeFromOtherDungeons(position) && !IsCloseToMonuments(position) && IsFarFromExistingSpawnPoints(position, _config.DungeonSpawn.MinDistanceBetweenDungeons))
                    {
                        _validSpawnPoints.Add((position, rotation));
                    }
                }

                pointsScanned++;
            }

            int addedPoints = _validSpawnPoints.Count - initialCount;
            if (addedPoints > 0)
            {
                LogDebug($"Added {addedPoints} dungeon spawn points. Total now: {_validSpawnPoints.Count}", 1);
            }
        }

        private bool IsFarFromExistingSpawnPoints(Vector3 position, float minDistance)
        {
            foreach (var existingPoint in _validSpawnPoints)
            {
                if (Vector3.Distance(existingPoint.Position, position) < minDistance)
                {
                    return false;
                }
            }
            return true;
        }

        private (Vector3 Position, Quaternion Rotation)? GetNextSpawnPoint()
        {
            if (_validSpawnPoints.Count == 0)
            {
                return null;
            }

            var nextPoint = _validSpawnPoints.First();
            _validSpawnPoints.RemoveAt(0);
            return nextPoint;
        }

        private void RecycleUsedSpawnPoints()
        {
            if (_validSpawnPoints.Count == 0)
            {
                LogDebug("Recycling used spawn points.");
                ScanSpawnPoints();
            }
        }

        private void UpdateMarkers()
        {
            var activeDungeons = new List<ActiveDungeon>(_dungeonData.ActiveDungeons);
            foreach (var activeDungeon in activeDungeons)
            {
                foreach (var marker in _dungeonData.ActiveDungeonMarkers.Where(m => m.Position == activeDungeon.Position).ToList())
                {
                    var vendingMarker = BaseNetworkable.serverEntities.Find(new NetworkableId(marker.VendingMarker)) as VendingMachineMapMarker;

                    if (activeDungeon?.OwnerName != string.Empty)
                    {
                        vendingMarker.markerShopName = $"{activeDungeon.TierConfig.MapName} ({activeDungeon.OwnerName})";
                        vendingMarker.SendNetworkUpdate();
                    }

                    var radiusMarker = BaseNetworkable.serverEntities.Find(new NetworkableId(marker.RadiusMarker)) as MapMarkerGenericRadius;
                    radiusMarker?.SendUpdate();
                }
            }
        }

        private void AutoSpawnDungeon()
        {
            var tier = GetDifficultyWithLeastActiveDungeons();
            EnqueueDungeonRequest(null, tier);
        }

        private DungeonTierConfig GetDifficultyWithLeastActiveDungeons()
        {
            var activeDungeonsByDifficulty = new Dictionary<string, int>
            {
                { "easy", 0 },
                { "normal", 0 },
                { "medium", 0 },
                { "hard", 0 },
                { "nightmare", 0 },
            };

            foreach (var dungeon in _dungeonData.ActiveDungeons)
            {
                var tierName = dungeon.TierName.ToLower();
                if (activeDungeonsByDifficulty.ContainsKey(tierName))
                {
                    activeDungeonsByDifficulty[tierName]++;
                }
            }

            var sortedDifficulties = activeDungeonsByDifficulty.OrderBy(d => d.Value).ThenBy(d => Array.IndexOf(new[] { "easy", "normal", "medium", "hard", "nightmare" }, d.Key)).ToList();

            return _config.Tiers.GetType().GetProperties().FirstOrDefault(prop => prop.Name.ToLower() == sortedDifficulties.First().Key)?.GetValue(_config.Tiers) as DungeonTierConfig;
        }

        private void EnqueueDungeonRequest(BasePlayer player, DungeonTierConfig tier, bool lockToPlayer = false)
        {
            var request = new DungeonRequest
            {
                Player = player,
                Tier = tier,
                LockToPlayer = lockToPlayer,
            };

            _dungeonQueue.Enqueue(request);

            if (player != null)
            {
                NotifyPlayer(player, "ReceivedRequest", 0);
            }

            ProcessDungeonQueue();
        }

        private void ProcessDungeonQueue()
        {
            if (_isCreatingDungeon || _dungeonQueue.Count == 0)
                return;

            var request = _dungeonQueue.Dequeue();

            if (_dungeonData.ActiveDungeons.Count >= _config.DungeonSpawn.MaxTotalActiveDungeons)
            {
                LogDebug(lang.GetMessage("MaxActiveDungeonsReached", this), 1);
                NotifyPlayer(request.Player, "MaxActiveDungeonsReached", 0);
                return;
            }

            var spawnPoint = GetNextSpawnPoint();
            if (spawnPoint == null)
            {
                LogDebug("No spawn points available. Rescanning for new points.", 1);
                ScanSpawnPoints();

                spawnPoint = GetNextSpawnPoint();
                if (spawnPoint == null)
                {
                    LogDebug("Still no spawn points available after rescanning.", 1);
                    NotifyPlayer(request.Player, "DungeonSpawnFailed", 0);
                    _failedDungeons.Add(request);
                    return;
                }
            }

            var (position, rotation) = spawnPoint.Value;

            if (!IsSafeFromOtherDungeons(position))
            {
                LogDebug($"The point {position} is not safe for creation.");
                RecycleUsedSpawnPoints();

                NotifyPlayer(request.Player, "DungeonSpawnFailed", 0);
                _failedDungeons.Add(request);
                return;
            }

            LogDebug($"Using spawn point: Position = {position}, Rotation = {rotation}");

            _isCreatingDungeon = true;

            CoroutineManager.Instance.StartCoroutine(
                TryCreateDungeon(
                    position,
                    rotation,
                    request.Tier,
                    request.Player,
                    request.LockToPlayer,
                    () =>
                    {
                        _isCreatingDungeon = false;
                        ProcessDungeonQueue();
                    }
                )
            );
        }

        private void PlaceEntryBarrier(ActiveDungeon activeDungeon, ProceduralDynamicDungeon dungeon)
        {
            Vector3 entrancePosition = dungeon.entranceHack.transform.position;
            Vector3 entranceForward = dungeon.entranceHack.transform.forward;
            float offsetDistance = 3f;
            Vector3 barrierPosition = entrancePosition + (entranceForward * offsetDistance);

            string prefabPath = "assets/prefabs/deployable/barricades/barricade.cover.wood.prefab";
            var barrier = GameManager.server.CreateEntity(prefabPath, barrierPosition, Quaternion.identity);
            barrier.OwnerID = OwnerID;

            LogDebug($"Placing barrier at the dungeon entrance: {barrierPosition}", 1);
            barrier.Spawn();
            activeDungeon.EntityIds.Add(barrier.net.ID.Value);
        }

        private IEnumerator TryCreateDungeon(Vector3 position, Quaternion rotation, DungeonTierConfig tierConfig, BasePlayer player, bool lockToPlayer, Action onComplete)
        {
            bool success = false;
            yield return CreateDungeon(position, rotation, tierConfig, player, lockToPlayer, result => success = result);

            if (!success)
            {
                LogDebug("Failed to create the dungeon. We will try again shortly.", 1);
                NotifyPlayer(player, "DungeonSpawnFailed", 0);

                EnqueueDungeonRequest(player, tierConfig, lockToPlayer);
            }

            onComplete?.Invoke();
        }

        private IEnumerator CreateDungeon(
            Vector3 position,
            Quaternion rotation,
            DungeonTierConfig tierConfig = null,
            BasePlayer player = null,
            bool lockDungeonToPlayer = false,
            Action<bool> callback = null
        )
        {
            tierConfig ??= GetDifficultyWithLeastActiveDungeons();

            string prefabPath = GetRandomDungeonPrefab();
            if (!ValidatePrefabPath(prefabPath))
            {
                LogDebug("Invalid prefab path for the dungeon.", 2);
                callback?.Invoke(false);
                yield break;
            }

            if (_config.DungeonSpawn.MaxDungeonLifetime > 0)
            {
                XmasDungeon.xmaslifetime = _config.DungeonSpawn.MaxDungeonLifetime;
                HalloweenDungeon.lifetime = _config.DungeonSpawn.MaxDungeonLifetime;
            }
            else
            {
                XmasDungeon.xmaslifetime = 3600;
                HalloweenDungeon.lifetime = 3600;
            }

            var dungeonPortal = GameManager.server.CreateEntity(prefabPath, position, rotation) as BasePortal;
            if (dungeonPortal == null)
            {
                LogDebug("Failed to create the dungeon portal entity.", 2);
                callback?.Invoke(false);
                yield break;
            }

            dungeonPortal.OwnerID = OwnerID;
            dungeonPortal.Spawn();

            var grid = MapHelper.PositionToString(position);
            LogDebug($"Trying to spawn Dungeon at grid: {grid}.");

            ProceduralDynamicDungeon dungeon = GetProceduralDungeon(dungeonPortal);
            if (!ValidateProceduralDungeon(dungeon, dungeonPortal, grid))
            {
                dungeonPortal?.Kill(BaseNetworkable.DestroyMode.None);
                callback?.Invoke(false);
                yield break;
            }

            yield return new WaitForSeconds(3f);
            yield return CoroutineManager.Instance.StartCoroutine(RemoveOriginalEntities(dungeon));
            yield return new WaitForSeconds(3f);

            LogDebug($"SEED: {dungeon.baseseed} {dungeonPortal.GetType().Name}");

            var spawnPoints = GetAllSpawnPoints(dungeon, dungeonPortal.GetType().Name, tierConfig);
            if (spawnPoints.Count == 0)
            {
                LogDebug("Dungeon spawn failed! Not enough spawn points inside for entities.", 1);
                dungeonPortal?.Kill(BaseNetworkable.DestroyMode.None);
                callback?.Invoke(false);
                yield break;
            }

            LogDebug($"Selected dungeon tier: {_tierNameMap[tierConfig]} at grid: {grid}.");

            if (!tierConfig.IsValid())
            {
                LogDebug("Invalid tier configuration: Percentages must sum to 100%.", 2);
                dungeonPortal?.Kill(BaseNetworkable.DestroyMode.None);
                callback?.Invoke(false);
                yield break;
            }

            if (dungeonPortal == null)
            {
                LogDebug("Dungeon portal not found.", 2);
                callback?.Invoke(false);
                yield break;
            }

            var activeDungeon = RegisterActiveDungeon(dungeonPortal, dungeon, position, grid, tierConfig, player, lockDungeonToPlayer);

            int totalPoints = spawnPoints.Count;
            int turretCount = (int)(totalPoints * (tierConfig.TurretPercentage / 100f));
            int boxCount = (int)(totalPoints * (tierConfig.LootBoxPercentage / 100f));
            int npcCount = totalPoints - turretCount - boxCount;

            AssignTurretsNearBoxes(spawnPoints, turretCount, boxCount, npcCount, out var turretSpawns, out var boxSpawns, out var npcSpawns);

            LogDebug($"Spawn points found: {totalPoints}, Turrets: {turretCount}, Boxes: {boxCount}, NPCs: {npcCount}");

            SpawnEntities(turretSpawns, "AutoTurret", activeDungeon, dungeon);
            SpawnEntities(boxSpawns, "StorageContainer", activeDungeon, dungeon);
            SpawnEntities(npcSpawns, "NPC", activeDungeon, dungeon);

            LogDebug($"Finished placing entities. Total placed: {activeDungeon.EntityIds.Count}/{spawnPoints.Count}.");

            CreateDungeonMarkers(activeDungeon);
            NotifyPlayersOfNewDungeon(activeDungeon);

            LogDebug($"{_tierNameMap[tierConfig]} Dungeon spawned successfully at grid: {activeDungeon.Grid}.", 1);

            activeDungeon.Spawned = true;

            PlaceEntryBarrier(activeDungeon, dungeon);

            NotifyPlayer(player, "DungeonSpawnedSuccess", 0, activeDungeon.Grid);

            callback?.Invoke(true);
            yield return null;
        }

        private void AssignTurretsNearBoxes(
            List<Vector3> spawnPoints,
            int turretCount,
            int boxCount,
            int npcCount,
            out List<Vector3> turretSpawns,
            out List<Vector3> boxSpawns,
            out List<Vector3> npcSpawns
        )
        {
            turretSpawns = new List<Vector3>();
            boxSpawns = new List<Vector3>();
            npcSpawns = new List<Vector3>();

            var usedSpawnPoints = new HashSet<Vector3>();
            float maxDistance = 10f;

            for (int i = 0; i < boxCount && spawnPoints.Count > 0; i++)
            {
                var boxPoint = spawnPoints.FirstOrDefault(p => !usedSpawnPoints.Contains(p));
                if (boxPoint == default)
                    break;

                boxSpawns.Add(boxPoint);
                usedSpawnPoints.Add(boxPoint);

                var turretPoint = spawnPoints.FirstOrDefault(p => !usedSpawnPoints.Contains(p) && Vector3.Distance(boxPoint, p) <= maxDistance);

                if (turretPoint != default && turretSpawns.Count < turretCount)
                {
                    turretSpawns.Add(turretPoint);
                    usedSpawnPoints.Add(turretPoint);
                }
            }

            foreach (var point in spawnPoints)
            {
                if (turretSpawns.Count >= turretCount)
                    break;

                if (!usedSpawnPoints.Contains(point))
                {
                    turretSpawns.Add(point);
                    usedSpawnPoints.Add(point);
                }
            }

            foreach (var point in spawnPoints.Except(usedSpawnPoints).ToList())
            {
                if (boxSpawns.Count < boxCount)
                {
                    boxSpawns.Add(point);
                    usedSpawnPoints.Add(point);
                }
                else if (npcSpawns.Count < npcCount)
                {
                    npcSpawns.Add(point);
                    usedSpawnPoints.Add(point);
                }
            }

            if (turretSpawns.Count < turretCount)
            {
                LogDebug($"Only {turretSpawns.Count}/{turretCount} turrets could be placed due to insufficient spawn points.", 1);
            }

            if (boxSpawns.Count < boxCount)
            {
                LogDebug($"Only {boxSpawns.Count}/{boxCount} loot boxes could be placed due to insufficient spawn points.", 1);
            }

            if (npcSpawns.Count < npcCount)
            {
                LogDebug($"Only {npcSpawns.Count}/{npcCount} NPCs could be placed due to insufficient spawn points.", 1);
            }
        }

        private ProceduralDynamicDungeon GetProceduralDungeon(BasePortal dungeonPortal)
        {
            return (dungeonPortal as XmasDungeon)?.dungeonInstance.Get(true) ?? (dungeonPortal as HalloweenDungeon)?.dungeonInstance.Get(true);
        }

        private List<Vector3> GetAllSpawnPoints(ProceduralDynamicDungeon dungeon, string dungeonType, DungeonTierConfig tierConfig)
        {
            var basePoints = dungeon
                .spawnedCells.SelectMany(cell => cell.spawnGroups)
                .SelectMany(group => group.spawnPoints)
                .Select(point =>
                {
                    point.GetLocation(out var position, out _);
                    return position;
                })
                .ToList();

            if (
                (tierConfig.MinSpawnPoints > 20 && basePoints.Count < 8)
                || (tierConfig.MinSpawnPoints > 15 && basePoints.Count < 6)
                || (tierConfig.MinSpawnPoints > 5 && basePoints.Count < 3)
                || (basePoints.Count <= 2)
            )
            {
                LogDebug("Not enough spawn points inside for entities. Spawn points: " + basePoints.Count);
                return new List<Vector3>();
            }

            if (tierConfig.MinSpawnPoints <= basePoints.Count)
            {
                LogDebug($"Total valid spawn points: {basePoints.Count}", 1);

                var selectedPoints = basePoints.OrderBy(x => Random.value).Take(tierConfig.MinSpawnPoints).ToList();

                LogDebug($"Selected {tierConfig.MinSpawnPoints} random spawn points.");
                return selectedPoints;
            }

            if (tierConfig.MinSpawnPoints <= basePoints.Count)
            {
                LogDebug($"Total valid spawn points: {basePoints.Count}");
                return basePoints;
            }

            var existingSpawnPoints = new List<Vector3>(basePoints);
            while (existingSpawnPoints.Count < tierConfig.MinSpawnPoints)
            {
                var originalPoint = basePoints[Random.Range(0, basePoints.Count)];

                var nearbyPoint = GenerateNearbyPoint(originalPoint, existingSpawnPoints, dungeonType);
                if (nearbyPoint != originalPoint)
                {
                    LogDebug($"Generated nearby point: {nearbyPoint}");
                    existingSpawnPoints.Add(nearbyPoint);
                }
            }

            LogDebug($"Total valid spawn points: {existingSpawnPoints.Count}");
            return existingSpawnPoints;
        }

        private Vector3 GenerateNearbyPoint(Vector3 originalPosition, List<Vector3> existingPoints, string dungeonType)
        {
            float maxDistance = 30f;
            float minDistance = 3f;
            int maxAttempts = 50;

            for (int attempt = 0; attempt < maxAttempts; attempt++)
            {
                Vector3 nearbyPoint = GenerateRandomOffset(originalPosition, maxDistance);

                if (IsValidNearbyPoint(nearbyPoint, minDistance, existingPoints, dungeonType))
                {
                    return nearbyPoint;
                }
            }

            return originalPosition;
        }

        private Vector3 GenerateRandomOffset(Vector3 originalPosition, float maxDistance)
        {
            float offsetX = Random.Range(-maxDistance, maxDistance);
            float offsetZ = Random.Range(-maxDistance, maxDistance);

            return new Vector3(originalPosition.x + offsetX, originalPosition.y, originalPosition.z + offsetZ);
        }

        private bool IsValidNearbyPoint(Vector3 point, float minDistance, List<Vector3> existingPoints, string dungeonType)
        {
            if (!IsFarFromExistingPoints(point, minDistance, existingPoints))
                return false;

            if (LocationCheck.IsFlying(point, 0.5f))
                return false;

            if (LocationCheck.IsClipped(point, 0.5f))
                return false;

            if (LocationCheck.IsInsideObject(point, 0.5f))
                return false;

            if (!IsSurroundedByWalls(point, 10.0f))
                return false;

            return true;
        }

        public bool IsSurroundedByWalls(Vector3 position, float checkDistance)
        {
            Vector3[] directions = { Vector3.up, Vector3.down, Vector3.forward, Vector3.back, Vector3.right, Vector3.left };

            foreach (var direction in directions)
            {
                if (!Physics.Raycast(position, direction, checkDistance, Layers.Mask.World, QueryTriggerInteraction.Ignore))
                {
                    return false;
                }
            }

            return true;
        }

        private bool IsFarFromExistingPoints(Vector3 position, float minDistance, List<Vector3> existingPoints)
        {
            foreach (var existingPoint in existingPoints)
            {
                if (Vector3.Distance(existingPoint, position) < minDistance)
                {
                    return false;
                }
            }
            return true;
        }

        private bool TryPlaceTurretOnCeiling(Vector3 basePosition, out Vector3 ceilingPosition, out Quaternion ceilingRotation)
        {
            float maxCeilingDistance = 7f;

            RaycastHit hitInfo;
            if (Physics.Raycast(basePosition, Vector3.up, out hitInfo, maxCeilingDistance))
            {
                if (hitInfo.collider != null && hitInfo.collider.gameObject != null)
                {
                    ceilingPosition = hitInfo.point - (Vector3.up * 0.5f);
                    ceilingRotation = Quaternion.Euler(0f, 0f, 180f);
                    return true;
                }
            }

            ceilingPosition = Vector3.zero;
            ceilingRotation = Quaternion.identity;
            return false;
        }

        private JObject NpcSpawnObjectConfig(ActiveDungeon activeDungeon)
        {
            var npcConfig = activeDungeon.TierConfig.NpcConfig;
            var areaMask = activeDungeon.DungeonType == "XmasDungeon" ? -1 : 1;
            var agentTypeID = activeDungeon.DungeonType == "XmasDungeon" ? 0 : -1372625422;

            return new JObject
            {
                ["Name"] = npcConfig.Name,
                ["HostileTargetsOnly"] = false,
                ["DamageScale"] = npcConfig.DamageScale,
                ["States"] = new JArray { "RoamState", "ChaseState", "CombatState" },
                ["AttackRangeMultiplier"] = 1f,
                ["SenseRange"] = 60f,
                ["MemoryDuration"] = 30f,
                ["CheckVisionCone"] = false,
                ["VisionCone"] = 135f,
                ["Health"] = npcConfig.Health,
                ["RoamRange"] = npcConfig.RoamRange,
                ["ChaseRange"] = npcConfig.ChaseRange,
                ["WearItems"] = new JArray(npcConfig.WearItems.Items.Select(item => new JObject { ["ShortName"] = item.ShortName, ["SkinID"] = item.SkinID })),
                ["BeltItems"] = new JArray(
                    npcConfig.BeltItems.Items.Select(item => new JObject
                    {
                        ["ShortName"] = item.ShortName,
                        ["Amount"] = item.Amount,
                        ["SkinID"] = item.SkinID,
                        ["Mods"] = new JArray(item.Mods ?? new List<string>()),
                        ["Ammo"] = "",
                    })
                ),
                ["TurretDamageScale"] = 1f,
                ["AimConeScale"] = 1.5f,
                ["DisableRadio"] = false,
                ["Stationary"] = false,
                ["CanUseWeaponMounted"] = true,
                ["CanRunAwayWater"] = false,
                ["Speed"] = 5f,
                ["AreaMask"] = areaMask,
                ["AgentTypeID"] = agentTypeID,
                ["HomePosition"] = string.Empty,
            };
        }

        private void SpawnEntities(List<Vector3> points, string entityType, ActiveDungeon activeDungeon, ProceduralDynamicDungeon dungeon)
        {
            foreach (var point in points)
            {
                if (entityType == "NPC")
                {
                    var npcConfig = NpcSpawnObjectConfig(activeDungeon);
                    ScientistNPC entity = (ScientistNPC)NpcSpawn.Call("SpawnNpc", point, npcConfig);
                    entity.OwnerID = OwnerID;

                    if (entity != null && !entity.IsDestroyed)
                    {
                        LogDebug($"Successfully spawned NPC at {point} with entity ID: {entity.net.ID}");
                        InitializeEntity(entity, activeDungeon);
                    }
                }
                else
                {
                    var prefabPath = entityType switch
                    {
                        "AutoTurret" => "assets/prefabs/npc/autoturret/autoturret_deployed.prefab",
                        "StorageContainer" => "assets/prefabs/deployable/woodenbox/woodbox_deployed.prefab",
                        _ => throw new ArgumentException($"Unknown entity type: {entityType}"),
                    };

                    Vector3 spawnPosition = point;
                    Quaternion spawnRotation = Quaternion.identity;

                    if (entityType == "AutoTurret")
                    {
                        bool shouldPlaceOnCeiling = Random.value < 0.8f;

                        if (shouldPlaceOnCeiling && TryPlaceTurretOnCeiling(point, out var ceilingPosition, out var ceilingRotation))
                        {
                            spawnPosition = ceilingPosition;
                            spawnRotation = ceilingRotation;
                            LogDebug($"Placing turret on ceiling at {ceilingPosition} with rotation {ceilingRotation.eulerAngles}");
                        }
                        else
                        {
                            LogDebug($"Placing turret on floor at {spawnPosition}");
                        }
                    }

                    var entity = GameManager.server.CreateEntity(prefabPath, spawnPosition, spawnRotation);
                    if (entity != null)
                    {
                        entity.OwnerID = OwnerID;
                        entity.SetParent(dungeon, true, true);
                        entity.Spawn();

                        LogDebug($"Successfully spawned {entityType} at {spawnPosition} with entity ID: {entity.net.ID}");
                        InitializeEntity(entity, activeDungeon);
                    }
                    else
                    {
                        LogDebug($"Failed to create {entityType} at position {spawnPosition}. Prefab: {prefabPath}");
                    }
                }
            }
        }

        private bool ValidatePrefabPath(string prefabPath)
        {
            if (!string.IsNullOrEmpty(prefabPath))
                return true;

            LogDebug("No prefab path available for dungeon spawning.");
            return false;
        }

        private bool ValidateProceduralDungeon(ProceduralDynamicDungeon proceduralDungeon, BasePortal dungeonPortal, string grid)
        {
            if (proceduralDungeon == null)
            {
                LogDebug($"ProceduralDynamicDungeon not found, destroying the Dungeon at grid: {grid}.");
                dungeonPortal.Kill(BaseNetworkable.DestroyMode.None);
                return false;
            }

            if (proceduralDungeon.spawnedCells.Count < 1)
            {
                LogDebug($"No cells found, destroying the Dungeon at grid: {grid}.");
                dungeonPortal.Kill(BaseNetworkable.DestroyMode.None);
                return false;
            }

            return true;
        }

        private ActiveDungeon RegisterActiveDungeon(
            BasePortal dungeonPortal,
            ProceduralDynamicDungeon proceduralDungeon,
            Vector3 position,
            string grid,
            DungeonTierConfig selectedTier,
            BasePlayer player,
            bool lockToPlayer
        )
        {
            var activeDungeon = new ActiveDungeon
            {
                PortalId = dungeonPortal.net.ID.Value,
                DynamicDungeonId = proceduralDungeon.net.ID.Value,
                Position = position,
                Grid = grid,
                TierName = _tierNameMap[selectedTier],
                DungeonType = dungeonPortal.GetType().Name,
                TierConfig = selectedTier,
                EntityIds = new List<ulong>(),
                Spawned = false,
            };

            if (lockToPlayer && player != null)
            {
                activeDungeon.OwnerID = player.userID;
                activeDungeon.OwnerName = player.displayName;
            }

            _dungeonData.ActiveDungeons.Add(activeDungeon);

            return activeDungeon;
        }

        private List<(Vector3 Position, Quaternion Rotation)> GetAvailableSpawnLocations(ProceduralDynamicDungeon proceduralDungeon, string prefabPath)
        {
            GameObject prefab = GameManager.server.FindPrefab(prefabPath);
            if (prefab == null)
            {
                Puts($"Prefab not found: {prefabPath}");
                return new List<(Vector3, Quaternion)>();
            }

            BaseEntity prefabEntity = prefab.GetComponent<BaseEntity>();
            if (prefabEntity == null)
            {
                Puts($"Prefab does not have a BaseEntity: {prefabPath}");
                return new List<(Vector3, Quaternion)>();
            }

            return proceduralDungeon
                .spawnedCells.Where(cell => cell != null)
                .SelectMany(cell => cell.spawnGroups)
                .Where(group => group != null)
                .SelectMany(group => group.spawnPoints)
                .Where(spawnPoint => spawnPoint != null && spawnPoint.IsAvailableTo(prefab))
                .Select(spawnPoint =>
                {
                    spawnPoint.GetLocation(out var position, out var rotation);
                    return (position, rotation);
                })
                .ToList();
        }

        private void InitializeEntity(BaseEntity entity, ActiveDungeon activeDungeon)
        {
            if (entity != null)
            {
                activeDungeon.EntityIds.Add(entity.net.ID.Value);

                if (entity is NPCPlayer)
                {
                    activeDungeon.NpcIds.Add(entity.net.ID.Value);
                }
                else if (entity is AutoTurret turret)
                {
                    ConfigureTurret(turret, activeDungeon.TierConfig.AutoTurretConfig);
                }
                else if (entity is StorageContainer storage)
                {
                    ConfigureStorage(storage, activeDungeon.TierConfig);
                }
            }
        }

        private IEnumerator RemoveOriginalEntities(ProceduralDynamicDungeon proceduralDungeon)
        {
            foreach (var cell in proceduralDungeon.spawnedCells)
            {
                foreach (var group in cell.spawnGroups)
                {
                    for (int i = group.spawnInstances.Count - 1; i >= 0; i--)
                    {
                        SpawnPointInstance spawnPointInstance = group.spawnInstances[i];
                        BaseEntity entity = spawnPointInstance?.gameObject?.ToBaseEntity();

                        if (entity != null && !IsDungeonEntity(entity) && (entity is NPCDwelling || entity is ScarecrowNPC || entity is LootContainer))
                        {
                            LogDebug($"Removing original entity: {entity.ShortPrefabName} at {entity.transform.position}");
                            entity.Kill();
                        }
                    }
                }
            }

            yield return null;
        }

        private void ConfigureTurret(AutoTurret turret, TurretConfig turretConfig)
        {
            turret.InitializeHealth(turretConfig.Health, turretConfig.Health);

            var weapon = ItemManager.CreateByName(turretConfig.WeaponShortName);
            weapon?.MoveToContainer(turret.inventory, 0);

            var laserSight = ItemManager.CreateByName("weapon.mod.lasersight");
            laserSight?.MoveToContainer(weapon.contents);

            turret.UpdateAttachedWeapon();
            turret.gameObject.AddComponent<TurretMonoBehaviour>();
        }

        public class TurretMonoBehaviour : MonoBehaviour
        {
            private AutoTurret _turret;

            private void Awake()
            {
                _turret = GetComponent<AutoTurret>();

                _turret.SetPeacekeepermode(false);
                _turret.InitiateStartup();
                _turret.isLootable = false;
                _turret.dropFloats = false;
                _turret.dropsLoot = false;

                InvokeRepeating(nameof(RefillAmmo), 2f, 30f);
            }

            private void RefillAmmo()
            {
                int maxReserveAmmo = 1000;

                if (_turret.AttachedWeapon is not BaseProjectile baseProjectile || baseProjectile.primaryMagazine?.ammoType == null)
                    return;

                var ammoType = baseProjectile.primaryMagazine.ammoType;
                int currentAmmoCount = _turret.inventory.itemList.Where(item => item.info == ammoType).Sum(item => item.amount);

                if (currentAmmoCount < maxReserveAmmo)
                {
                    int ammoNeeded = maxReserveAmmo - currentAmmoCount;
                    Item ammoItem = ItemManager.Create(ammoType, ammoNeeded);
                    ammoItem?.MoveToContainer(_turret.inventory);

                    _turret.UpdateTotalAmmo();
                    _turret.EnsureReloaded();
                    _turret.SendNetworkUpdateImmediate();
                }
            }

            private void OnDestroy()
            {
                CancelInvoke(nameof(RefillAmmo));
            }
        }

        private void ConfigureStorage(StorageContainer storage, DungeonTierConfig tierConfig)
        {
            storage.skinID = tierConfig.SmallWoodBoxSkinID;
            PopulateStorage(storage.inventory, tierConfig, _config.LootBoxConfig.LootItems);
            AddLock(storage);
        }

        private void AddLock(StorageContainer storage)
        {
            var prefabName = "assets/prefabs/locks/keylock/lock.key.prefab";
            var keyLock = GameManager.server.CreateEntity(prefabName) as KeyLock;
            if (keyLock == null)
                return;

            keyLock.gameObject.Identity();
            keyLock.SetParent(storage, storage.GetSlotAnchorName(BaseEntity.Slot.Lock));
            keyLock.Spawn();
            storage.SetSlot(BaseEntity.Slot.Lock, keyLock);
            keyLock.keyCode = Random.Range(1000, 9999);
            keyLock.OwnerID = 0;
            keyLock.firstKeyCreated = true;
            keyLock.SetFlag(BaseEntity.Flags.Locked, true);
        }

        private void PopulateStorage(ItemContainer container, DungeonTierConfig tierConfig, List<ItemConfig> lootItems)
        {
            var shuffledItems = lootItems.OrderBy(_ => Random.value).ToList();
            var differentItemsCount = 0;

            foreach (var itemConfig in shuffledItems)
            {
                if (differentItemsCount >= tierConfig.MaxItemsPerBox)
                {
                    break;
                }

                if (Random.value * 100f <= itemConfig.InclusionChancePercentage)
                {
                    var itemDefinition = ItemManager.FindItemDefinition(itemConfig.ShortName);
                    if (itemDefinition != null)
                    {
                        var amount = Random.Range(itemConfig.MinimumAmount, itemConfig.MaximumAmount + 1);
                        if (container.itemList.Count < container.capacity)
                        {
                            var item = ItemManager.Create(itemDefinition, amount, itemConfig.SkinID);

                            if (!string.IsNullOrEmpty(itemConfig.CustomName))
                                item.name = itemConfig.CustomName;

                            item.MoveToContainer(container);
                            differentItemsCount++;
                        }
                    }
                }
            }
        }

        private List<ActiveDungeon> RemoveInactiveDungeons()
        {
            var removedDungeons = new List<ActiveDungeon>();

            foreach (var dungeon in _dungeonData.ActiveDungeons.ToList())
            {
                if (CanBeRemoved(dungeon))
                {
                    removedDungeons.Add(dungeon);
                    DestroyDungeon(dungeon);
                }
            }

            SaveData();

            return removedDungeons;
        }

        private void DestroyDungeon(ActiveDungeon dungeon)
        {
            var proceduralDynamicDungeon = BaseNetworkable.serverEntities.Find(new NetworkableId(dungeon.DynamicDungeonId)) as ProceduralDynamicDungeon;
            if (proceduralDynamicDungeon != null)
            {
                RemoveLootableCorpse(proceduralDynamicDungeon);
                RemoveItemDrop(proceduralDynamicDungeon);
                RemoveDroppedItemContainer(proceduralDynamicDungeon);
            }

            var dungeonEntity = BaseNetworkable.serverEntities.Find(new NetworkableId(dungeon.PortalId)) as HalloweenDungeon;
            if (dungeonEntity != null)
            {
                dungeonEntity?.Kill(BaseNetworkable.DestroyMode.None);
            }

            DestroyEntities(dungeon);
            RemoveDungeonMarkers(dungeon);
            _dungeonData.ActiveDungeons.Remove(dungeon);
        }

        private void RemoveDroppedItemContainer(ProceduralDynamicDungeon dungeon)
        {
            List<DroppedItemContainer> obj = Pool.Get<List<DroppedItemContainer>>();
            Vis.Entities(dungeon.transform.position, 80f, obj);
            foreach (DroppedItemContainer item in obj)
            {
                if (item.IsValid() && !item.IsDestroyed)
                {
                    item?.Kill();
                }
            }

            Pool.FreeUnmanaged(ref obj);
        }

        private void RemoveItemDrop(ProceduralDynamicDungeon dungeon)
        {
            List<DroppedItem> obj = Pool.Get<List<DroppedItem>>();
            Vis.Entities(dungeon.transform.position, 80f, obj);
            foreach (DroppedItem item in obj)
            {
                if (item.IsValid() && !item.IsDestroyed)
                {
                    item?.Kill();
                }
            }

            Pool.FreeUnmanaged(ref obj);
        }

        private void RemoveLootableCorpse(ProceduralDynamicDungeon dungeon)
        {
            List<LootableCorpse> obj = Pool.Get<List<LootableCorpse>>();
            Vis.Entities(dungeon.transform.position, 80f, obj);
            foreach (LootableCorpse item in obj)
            {
                if (item.IsValid() && !item.IsDestroyed)
                {
                    item?.Kill();
                }
            }

            Pool.FreeUnmanaged(ref obj);
        }

        private void DestroyEntities(ActiveDungeon dungeon)
        {
            foreach (var entityId in dungeon.EntityIds)
            {
                var entity = BaseNetworkable.serverEntities.Find(new NetworkableId(entityId)) as BaseEntity;
                entity?.Kill(BaseNetworkable.DestroyMode.None);
            }

            dungeon.EntityIds.Clear();
        }

        private void RemoveDungeonMarkers(ActiveDungeon dungeon)
        {
            foreach (var marker in _dungeonData.ActiveDungeonMarkers.Where(m => m.Position == dungeon.Position).ToList())
            {
                var vendingMarker = BaseNetworkable.serverEntities.Find(new NetworkableId(marker.VendingMarker)) as BaseEntity;
                var radiusMarker = BaseNetworkable.serverEntities.Find(new NetworkableId(marker.RadiusMarker)) as BaseEntity;

                vendingMarker?.Kill(BaseNetworkable.DestroyMode.None);
                radiusMarker?.Kill(BaseNetworkable.DestroyMode.None);

                _dungeonData.ActiveDungeonMarkers.Remove(marker);
            }
        }

        private IEnumerable<ActiveDungeon> RemoveAllDungeons()
        {
            var removedDungeons = new List<ActiveDungeon>(_dungeonData.ActiveDungeons);
            foreach (var dungeon in removedDungeons)
            {
                DestroyDungeon(dungeon);
            }
            _dungeonData = new DungeonData();
            SaveData();
            return removedDungeons;
        }

        #endregion

        #region Utility Methods

        private string GetAvailableTiers()
        {
            return string.Join(", ", _config.Tiers.GetType().GetProperties().Select(prop => prop.Name));
        }

        private bool CheckBalance(BasePlayer player, int cost)
        {
            if (_config.EconomyPlugin == 1)
            {
                return CheckEconomicsBalance(player, cost);
            }
            else if (_config.EconomyPlugin == 2)
            {
                return CheckServerRewardsBalance(player, cost);
            }

            return false;
        }

        private bool WithdrawFunds(BasePlayer player, int cost)
        {
            if (_config.EconomyPlugin == 1)
            {
                return WithdrawEconomicsFunds(player, cost);
            }
            else if (_config.EconomyPlugin == 2)
            {
                return WithdrawServerRewardsFunds(player, cost);
            }

            return false;
        }

        private bool CheckEconomicsBalance(BasePlayer player, int cost)
        {
            if (Economics == null)
            {
                PrintWarning("Economics plugin is not loaded.");
                return false;
            }

            double balance = Convert.ToDouble(Economics?.CallHook("Balance", player.userID));
            return balance >= cost;
        }

        private bool CheckServerRewardsBalance(BasePlayer player, int cost)
        {
            if (ServerRewards == null)
            {
                PrintWarning("ServerRewards plugin is not loaded.");
                return false;
            }

            int balance = Convert.ToInt32(ServerRewards?.CallHook("CheckPoints", player.userID));
            return balance >= cost;
        }

        private bool WithdrawEconomicsFunds(BasePlayer player, int cost)
        {
            if (Economics == null)
            {
                PrintWarning("Economics plugin is not loaded.");
                return false;
            }

            return Convert.ToBoolean(Economics?.CallHook("Withdraw", player.userID, (double)cost));
        }

        private bool WithdrawServerRewardsFunds(BasePlayer player, int cost)
        {
            if (ServerRewards == null)
            {
                PrintWarning("ServerRewards plugin is not loaded.");
                return false;
            }

            return Convert.ToBoolean(ServerRewards?.CallHook("TakePoints", player.userID, cost));
        }

        private bool CanEnter(BasePlayer player, ActiveDungeon activeDungeon)
        {
            if (!permission.UserHasPermission(player.UserIDString, PermissionEnter) && !permission.UserHasPermission(player.UserIDString, PermissionAdmin))
                return false;

            if (activeDungeon.OwnerID == 0UL)
            {
                activeDungeon.OwnerID = player.userID;
                activeDungeon.OwnerName = player.displayName;
                return true;
            }

            if (activeDungeon.OwnerID == player.userID)
                return true;

            if (_config.AllowTeam && player.currentTeam != 0UL)
            {
                var team = RelationshipManager.ServerInstance.FindTeam(player.currentTeam);
                if (team != null && team.members.Contains(activeDungeon.OwnerID))
                    return true;
            }

            if (_config.AllowFriends && Friends != null)
            {
                bool areFriends = (bool)(Friends.CallHook("AreFriends", activeDungeon.OwnerID, player.userID) ?? false);
                if (areFriends)
                    return true;
            }

            return permission.UserHasPermission(player.UserIDString, PermissionAdmin);
        }

        private ActiveDungeon GetActiveDungeonByPortal(BasePortal portal)
        {
            return _dungeonData.ActiveDungeons.FirstOrDefault(d => d.PortalId == portal.net.ID.Value);
        }

        private void LogDebug(string message, int level = 0)
        {
            if (string.IsNullOrEmpty(message))
                return;

            string formattedMessage = $"[{Name}] {message}";

            switch (level)
            {
                case 1:
                    if (_config.EnableWarningLogs)
                        Debug.LogWarning(formattedMessage);
                    break;
                case 2:
                    if (_config.EnableWarningLogs)
                        Debug.LogError(formattedMessage);
                    break;
                default:
                    if (_config.EnableDebugLogs)
                        Debug.Log(formattedMessage);
                    break;
            }
        }

        private void CleanupExistingEntities()
        {
            foreach (
                var entityType in new Type[]
                {
                    typeof(VendingMachineMapMarker),
                    typeof(MapMarkerGenericRadius),
                    typeof(BasePortal),
                    typeof(ProceduralDynamicDungeon),
                    typeof(AutoTurret),
                    typeof(NPCPlayer),
                    typeof(StorageContainer),
                }
            )
            {
                foreach (var obj in UnityEngine.Object.FindObjectsOfType(entityType))
                {
                    if (obj is BaseEntity baseEntity && baseEntity.OwnerID == OwnerID)
                    {
                        baseEntity.Kill(BaseNetworkable.DestroyMode.None);
                    }
                }
            }

            _dungeonData = new DungeonData();
        }

        private void InitializeTierNameMap()
        {
            _tierNameMap = new Dictionary<DungeonTierConfig, string>
            {
                { _config.Tiers.Easy, "Easy" },
                { _config.Tiers.Normal, "Normal" },
                { _config.Tiers.Medium, "Medium" },
                { _config.Tiers.Hard, "Hard" },
                { _config.Tiers.Nightmare, "Nightmare" },
            };
        }

        private void LoadData()
        {
            _dataFile = Interface.Oxide.DataFileSystem.GetFile($"{Name}_Data");
            _dungeonData = _dataFile.ReadObject<DungeonData>() ?? new DungeonData();
        }

        private void SaveData()
        {
            _dataFile.WriteObject(_dungeonData);
        }

        private static bool IsPlayer(BasePlayer targetPlayer) => targetPlayer != null && targetPlayer.userID.IsSteamId();

        private void NotifyPlayersOfNewDungeon(ActiveDungeon activeDungeon)
        {
            foreach (var player in BasePlayer.activePlayerList)
            {
                NotifyPlayer(player, "DungeonAppeared", 0, activeDungeon.TierConfig.MapName, activeDungeon.Grid);

                if (_config.EnableShowToast)
                {
                    var message = Msg(player.UserIDString, "DungeonAppeared", activeDungeon.TierConfig.MapName, activeDungeon.Grid);
                    player.ShowToast(GameTip.Styles.Blue_Normal, message);
                }
            }
        }

        private void NotifyPlayerOnRemoval(BasePlayer player, IEnumerable<ActiveDungeon> removedDungeons)
        {
            if (!removedDungeons.Any())
            {
                NotifyPlayer(player, "NoInactiveDungeonsToRemove", 0);
                return;
            }

            foreach (var dungeon in removedDungeons)
            {
                NotifyPlayer(player, "DungeonRemovedDetail", 0, dungeon.TierName, dungeon.Grid);
            }
        }

        private bool IsAdmin(BasePlayer player) => player != null && player.IsAdmin;

        private bool IsDungeonEntity(BaseEntity entity) => entity != null && _dungeonData.ActiveDungeons.Any(d => d.EntityIds.Contains(entity.net.ID.Value));

        private string GetRandomDungeonPrefab()
        {
            var prefabs = new List<string>();
            if (_config.DungeonSpawn.EnableXmasDungeon && false)
                prefabs.Add("assets/prefabs/missions/portal/xmasportalentry.prefab");
            if (_config.DungeonSpawn.EnableHalloweenDungeon)
                prefabs.Add("assets/prefabs/missions/portal/halloweenportalentry.prefab");
            return prefabs.Count > 0 ? prefabs[Random.Range(0, prefabs.Count)] : null;
        }

        private static bool IsValidPosition(Vector3 position, out Vector3 finalPosition, out Quaternion finalRotation)
        {
            finalPosition = Vector3.zero;
            finalRotation = Quaternion.identity;

            if (!LocationCheck.TerrainDetection(position, out var hitInfo))
                return false;
            if (LocationCheck.IsInsideRock(position, 10f) || LocationCheck.InWater(position) || LocationCheck.IsOnRoad(position) || LocationCheck.InNoBuildZone(position, 10f))
                return false;

            if (LocationCheck.HasCollider(position, 10f))
                return false;
            if (LocationCheck.IsInMonument(position))
                return false;

            return SetSuitablePositionAndRotation(position, hitInfo, out finalPosition, out finalRotation);
        }

        private static bool SetSuitablePositionAndRotation(Vector3 position, RaycastHit hitInfo, out Vector3 suitablePosition, out Quaternion suitableRotation)
        {
            suitablePosition = hitInfo.point;
            suitableRotation = Quaternion.FromToRotation(Vector3.up, hitInfo.normal);
            return true;
        }

        private void CreateDungeonMarkers(ActiveDungeon activeDungeon)
        {
            var prefabVendingMarker = "assets/prefabs/deployable/vendingmachine/vending_mapmarker.prefab";
            var vendingMarker = CreateMapMarker<VendingMachineMapMarker>(prefabVendingMarker, activeDungeon.Position);
            vendingMarker.OwnerID = OwnerID;
            vendingMarker.markerShopName = activeDungeon.TierConfig.MapName;
            vendingMarker.SendNetworkUpdate();

            var prefabRadiusMarker = "assets/prefabs/tools/map/genericradiusmarker.prefab";
            var radiusMarker = CreateMapMarker<MapMarkerGenericRadius>(prefabRadiusMarker, activeDungeon.Position);
            radiusMarker.OwnerID = OwnerID;
            radiusMarker.alpha = 0.75f;
            radiusMarker.radius = 0.5f;
            radiusMarker.color2 = GetMarkerColorByTier(activeDungeon.TierName);
            radiusMarker.SendUpdate();
            radiusMarker.SendNetworkUpdate();

            _dungeonData.ActiveDungeonMarkers.Add(
                new DungeonMarker
                {
                    VendingMarker = vendingMarker.net.ID.Value,
                    RadiusMarker = radiusMarker.net.ID.Value,
                    Position = activeDungeon.Position,
                    Tier = activeDungeon.TierName,
                }
            );
        }

        private T CreateMapMarker<T>(string prefab, Vector3 position)
            where T : BaseEntity
        {
            var entity = GameManager.server.CreateEntity(prefab, position) as T;
            entity?.Spawn();
            return entity;
        }

        private static Color GetMarkerColorByTier(string tierName)
        {
            return tierName switch
            {
                "Easy" => new Color(0.2f, 0.8f, 0.2f, 1f),
                "Normal" => new Color(1f, 0.8f, 0f, 1f),
                "Medium" => new Color(1f, 0.5f, 0f, 1f),
                "Hard" => new Color(0.8f, 0.1f, 0.1f, 1f),
                "Nightmare" => new Color(0.6f, 0.3f, 1f, 1f),
                _ => new Color(0.5f, 0.5f, 0.5f, 1f),
            };
        }

        private bool CanBeRemoved(ActiveDungeon dungeon)
        {
            if (_config.DungeonSpawn.MaxDungeonLifetime > 0 && HasDungeonExceededMaxLifetime(dungeon))
            {
                return NoPlayersInside(dungeon) && dungeon.Spawned;
            }

            if (AreAllNpcsGone(dungeon))
            {
                return NoPlayersInside(dungeon) && dungeon.Spawned;
            }

            return false;
        }

        private bool HasDungeonExceededMaxLifetime(ActiveDungeon dungeon)
        {
            var timeElapsed = DateTime.UtcNow - dungeon.CreationTime;
            return timeElapsed.TotalSeconds >= _config.DungeonSpawn.MaxDungeonLifetime;
        }

        private bool AreAllNpcsGone(ActiveDungeon dungeon)
        {
            return dungeon.NpcIds.All(npcId =>
            {
                var npc = BaseNetworkable.serverEntities.Find(new NetworkableId(npcId)) as BaseEntity;
                return npc == null || npc.IsDestroyed;
            });
        }

        private bool NoPlayersInside(ActiveDungeon dungeon)
        {
            var dynamicDungeon = BaseNetworkable.serverEntities.Find(new NetworkableId(dungeon.DynamicDungeonId)) as ProceduralDynamicDungeon;
            return dynamicDungeon == null || !dynamicDungeon.ContainsAnyPlayers();
        }

        private bool TryFindDungeonSpawnPoint(out Vector3 position, out Quaternion rotation)
        {
            var mapSize = World.Size;
            for (var attempt = 0; attempt < 1000; attempt++)
            {
                var candidatePosition = new Vector3(Random.Range(-mapSize / 2, mapSize / 2), 0, Random.Range(-mapSize / 2, mapSize / 2));

                if (IsValidPosition(candidatePosition, out position, out rotation) && IsSafeFromOtherDungeons(position) && !IsCloseToMonuments(position))
                {
                    return true;
                }
            }

            position = Vector3.zero;
            rotation = Quaternion.identity;
            return false;
        }

        private bool IsSafeFromOtherDungeons(Vector3 position)
        {
            return _dungeonData.ActiveDungeons.All(dungeon => Vector3.Distance(position, dungeon.Position) >= _config.DungeonSpawn.MinDistanceBetweenDungeons);
        }

        private bool IsCloseToMonuments(Vector3 position)
        {
            return TerrainMeta.Path.Monuments.Any(mon => Vector3.Distance(position, mon.transform.position) < _config.DungeonSpawn.MinDistanceFromMonuments);
        }

        private DungeonTierConfig GetDungeonTier(int spawnPointCount)
        {
            if (spawnPointCount < _config.Tiers.Normal.MinSpawnPoints)
                return _config.Tiers.Easy;
            if (spawnPointCount < _config.Tiers.Medium.MinSpawnPoints)
                return _config.Tiers.Normal;
            if (spawnPointCount < _config.Tiers.Hard.MinSpawnPoints)
                return _config.Tiers.Medium;
            if (spawnPointCount < _config.Tiers.Nightmare.MinSpawnPoints)
                return _config.Tiers.Hard;

            return _config.Tiers.Nightmare;
        }

        #endregion

        #region Configuration

        private class Configuration
        {
            [JsonProperty(PropertyName = "Use Notify Plugin")]
            public bool UseNotify { get; set; } = true;

            [JsonProperty(PropertyName = "Enable Debug Logs")]
            public bool EnableDebugLogs { get; set; } = false;

            [JsonProperty(PropertyName = "Enable Warning Logs")]
            public bool EnableWarningLogs { get; set; } = true;

            [JsonProperty(PropertyName = "Enable Toast Messages")]
            public bool EnableShowToast { get; set; } = true;

            [JsonProperty(PropertyName = "Lock dungeon for first player")]
            public bool LockDungeonToPlayer { get; set; } = true;

            [JsonProperty(PropertyName = "Economy Plugin (1 - Economics, 2 - ServerRewards)")]
            public int EconomyPlugin { get; set; } = 1;

            [JsonProperty(PropertyName = "Allow team members")]
            public bool AllowTeam { get; set; } = true;

            [JsonProperty(PropertyName = "Allow friends")]
            public bool AllowFriends { get; set; } = true;

            [JsonProperty(PropertyName = "Dungeon Settings")]
            public DungeonSpawnSettings DungeonSpawn { get; set; } = new DungeonSpawnSettings();

            [JsonProperty(PropertyName = "Tiers")]
            public DungeonTiers Tiers { get; set; } = new DungeonTiers();

            [JsonProperty(PropertyName = "Loot Box Config")]
            public LootBoxConfig LootBoxConfig { get; set; } = new LootBoxConfig();

            [JsonProperty(PropertyName = "Version")]
            public VersionNumber Version { get; set; }
        }

        private class DungeonTiers
        {
            [JsonProperty(PropertyName = "Easy")]
            public DungeonTierConfig Easy { get; set; } =
                new DungeonTierConfig
                {
                    MinSpawnPoints = 5,
                    MapName = "Dungeon: Easy",
                    BuyCost = 300,
                    LootBoxPercentage = 30,
                    TurretPercentage = 20,
                    NpcPercentage = 50,
                    AutoTurretConfig = new TurretConfig { Health = 300, WeaponShortName = "pistol.revolver" },
                    SmallWoodBoxSkinID = 2998755525,
                    MaxItemsPerBox = 3,
                    NpcConfig = new NpcConfig
                    {
                        Name = "Wild Scavenger",
                        Health = 100,
                        DamageScale = 0.5f,
                        RoamRange = 15f,
                        ChaseRange = 15f,
                        WearItems = new WearConfig
                        {
                            Items = new List<WearItemConfig>
                            {
                                new() { ShortName = "tshirt.long", SkinID = 10118 },
                                new() { ShortName = "burlap.gloves", SkinID = 949616124 },
                                new() { ShortName = "attire.snowman.helmet", SkinID = 0 },
                                new() { ShortName = "pants", SkinID = 2352962213 },
                                new() { ShortName = "shoes.boots", SkinID = 2380731293 },
                            },
                        },
                        BeltItems = new BeltConfig
                        {
                            Items = new List<BeltItemConfig>
                            {
                                new()
                                {
                                    ShortName = "pistol.revolver",
                                    Amount = 1,
                                    SkinID = 3140577175,
                                },
                            },
                        },
                    },
                };

            [JsonProperty(PropertyName = "Normal")]
            public DungeonTierConfig Normal { get; set; } =
                new DungeonTierConfig
                {
                    MinSpawnPoints = 7,
                    MapName = "Dungeon: Normal",
                    BuyCost = 500,
                    LootBoxPercentage = 30,
                    TurretPercentage = 25,
                    NpcPercentage = 45,
                    AutoTurretConfig = new TurretConfig { Health = 400, WeaponShortName = "pistol.revolver" },
                    SmallWoodBoxSkinID = 2998755525,
                    MaxItemsPerBox = 4,
                    NpcConfig = new NpcConfig
                    {
                        Name = "Battle Forager",
                        Health = 150,
                        DamageScale = 0.8f,
                        RoamRange = 15f,
                        ChaseRange = 15f,
                        WearItems = new WearConfig
                        {
                            Items = new List<WearItemConfig>
                            {
                                new() { ShortName = "bucket.helmet", SkinID = 1073904216 },
                                new() { ShortName = "jacket", SkinID = 2350426469 },
                                new() { ShortName = "tshirt", SkinID = 10039 },
                                new() { ShortName = "burlap.gloves.new", SkinID = 0 },
                                new() { ShortName = "pants", SkinID = 1441311938 },
                                new() { ShortName = "shoes.boots", SkinID = 2075527039 },
                            },
                        },
                        BeltItems = new BeltConfig
                        {
                            Items = new List<BeltItemConfig>
                            {
                                new()
                                {
                                    ShortName = "smg.2",
                                    Amount = 1,
                                    SkinID = 2386688842,
                                    Mods = new List<string> { "weapon.mod.flashlight" },
                                },
                            },
                        },
                    },
                };

            [JsonProperty(PropertyName = "Medium")]
            public DungeonTierConfig Medium { get; set; } =
                new DungeonTierConfig
                {
                    MinSpawnPoints = 10,
                    MapName = "Dungeon: Medium",
                    BuyCost = 700,
                    LootBoxPercentage = 25,
                    TurretPercentage = 30,
                    NpcPercentage = 45,
                    AutoTurretConfig = new TurretConfig { Health = 600, WeaponShortName = "rifle.ak" },
                    SmallWoodBoxSkinID = 2998755525,
                    MaxItemsPerBox = 5,
                    NpcConfig = new NpcConfig
                    {
                        Name = "Crimson Raider",
                        Health = 200,
                        DamageScale = 1.1f,
                        RoamRange = 15f,
                        ChaseRange = 15f,
                        WearItems = new WearConfig
                        {
                            Items = new List<WearItemConfig>
                            {
                                new() { ShortName = "coffeecan.helmet", SkinID = 1727561127 },
                                new() { ShortName = "roadsign.jacket", SkinID = 1727562915 },
                                new() { ShortName = "roadsign.gloves", SkinID = 2799639349 },
                                new() { ShortName = "pants", SkinID = 2814837980 },
                                new() { ShortName = "shoes.boots", SkinID = 1839313604 },
                                new() { ShortName = "tshirt.long", SkinID = 566893368 },
                            },
                        },
                        BeltItems = new BeltConfig
                        {
                            Items = new List<BeltItemConfig>
                            {
                                new()
                                {
                                    ShortName = "rifle.semiauto",
                                    Amount = 1,
                                    SkinID = 2617680693,
                                    Mods = new List<string> { "weapon.mod.flashlight" },
                                },
                                new()
                                {
                                    ShortName = "syringe.medical",
                                    Amount = 10,
                                    SkinID = 0,
                                },
                            },
                        },
                    },
                };

            [JsonProperty(PropertyName = "Hard")]
            public DungeonTierConfig Hard { get; set; } =
                new DungeonTierConfig
                {
                    MinSpawnPoints = 15,
                    MapName = "Dungeon: Hard",
                    BuyCost = 1000,
                    LootBoxPercentage = 20,
                    TurretPercentage = 35,
                    NpcPercentage = 45,
                    AutoTurretConfig = new TurretConfig { Health = 800, WeaponShortName = "rifle.bolt" },
                    SmallWoodBoxSkinID = 2998755525,
                    MaxItemsPerBox = 7,
                    NpcConfig = new NpcConfig
                    {
                        Name = "Elite Outlaw",
                        Health = 250,
                        DamageScale = 1.3f,
                        RoamRange = 15f,
                        ChaseRange = 15f,
                        WearItems = new WearConfig
                        {
                            Items = new List<WearItemConfig>
                            {
                                new() { ShortName = "metal.facemask", SkinID = 3284864766 },
                                new() { ShortName = "metal.plate.torso", SkinID = 2105505757 },
                                new() { ShortName = "burlap.gloves", SkinID = 2090790324 },
                                new() { ShortName = "hoodie", SkinID = 2080975449 },
                                new() { ShortName = "shoes.boots", SkinID = 10023 },
                                new() { ShortName = "pants", SkinID = 2080977144 },
                                new() { ShortName = "roadsign.kilt", SkinID = 2120628865 },
                            },
                        },
                        BeltItems = new BeltConfig
                        {
                            Items = new List<BeltItemConfig>
                            {
                                new()
                                {
                                    ShortName = "rifle.ak",
                                    Amount = 1,
                                    SkinID = 3190379864,
                                    Mods = new List<string> { "weapon.mod.flashlight" },
                                },
                                new()
                                {
                                    ShortName = "syringe.medical",
                                    Amount = 20,
                                    SkinID = 0,
                                },
                            },
                        },
                    },
                };

            [JsonProperty(PropertyName = "Nightmare")]
            public DungeonTierConfig Nightmare { get; set; } =
                new DungeonTierConfig
                {
                    MinSpawnPoints = 20,
                    MapName = "Dungeon: Nightmare",
                    BuyCost = 1500,
                    LootBoxPercentage = 30,
                    TurretPercentage = 30,
                    NpcPercentage = 40,
                    AutoTurretConfig = new TurretConfig { Health = 1000, WeaponShortName = "rifle.bolt" },
                    SmallWoodBoxSkinID = 2998755525,
                    MaxItemsPerBox = 10,
                    NpcConfig = new NpcConfig
                    {
                        Name = "Nightmare Hunter",
                        Health = 300,
                        DamageScale = 1.5f,
                        RoamRange = 15f,
                        ChaseRange = 15f,
                        WearItems = new WearConfig
                        {
                            Items = new List<WearItemConfig>
                            {
                                new() { ShortName = "metal.facemask", SkinID = 3343860599 },
                                new() { ShortName = "metal.plate.torso", SkinID = 3343861569 },
                                new() { ShortName = "hoodie", SkinID = 1369835131 },
                                new() { ShortName = "roadsign.kilt", SkinID = 1727564168 },
                                new() { ShortName = "burlap.gloves", SkinID = 1210780157 },
                                new() { ShortName = "pants", SkinID = 810745264 },
                                new() { ShortName = "shoes.boots", SkinID = 1210771348 },
                            },
                        },
                        BeltItems = new BeltConfig
                        {
                            Items = new List<BeltItemConfig>
                            {
                                new()
                                {
                                    ShortName = "lmg.m249",
                                    Amount = 1,
                                    SkinID = 1883947256,
                                    Mods = new List<string> { "weapon.mod.flashlight" },
                                },
                                new()
                                {
                                    ShortName = "syringe.medical",
                                    Amount = 50,
                                    SkinID = 0,
                                },
                            },
                        },
                    },
                };
        }

        private class DungeonTierConfig
        {
            [JsonProperty(PropertyName = "Minimum Entity Count for Dungeon Tier")]
            public int MinSpawnPoints { get; set; }

            [JsonProperty(PropertyName = "Map Name")]
            public string MapName { get; set; }

            [JsonProperty(PropertyName = "Buy Cost")]
            public int BuyCost { get; set; }

            [JsonProperty(PropertyName = "Loot Box Percentage")]
            public int LootBoxPercentage { get; set; }

            [JsonProperty(PropertyName = "Turret Percentage")]
            public int TurretPercentage { get; set; }

            [JsonProperty(PropertyName = "NPC Percentage")]
            public int NpcPercentage { get; set; }

            [JsonProperty(PropertyName = "NPC Config")]
            public NpcConfig NpcConfig { get; set; } = new NpcConfig();

            [JsonProperty(PropertyName = "Small Wood Box Skin ID")]
            public ulong SmallWoodBoxSkinID { get; set; } = 2998755525;

            [JsonProperty(PropertyName = "Max Items Per Box")]
            public int MaxItemsPerBox { get; set; } = 6;

            [JsonProperty(PropertyName = "Auto Turret Config")]
            public TurretConfig AutoTurretConfig { get; set; } = new TurretConfig();

            public bool IsValid() => LootBoxPercentage + TurretPercentage + NpcPercentage == 100;
        }

        public class NpcConfig
        {
            [JsonProperty(PropertyName = "NPC Name")]
            public string Name { get; set; }

            [JsonProperty(PropertyName = "NPC Health")]
            public int Health { get; set; }

            [JsonProperty(PropertyName = "Damage Scale")]
            public float DamageScale { get; set; }

            [JsonProperty(PropertyName = "NPC Roam Range")]
            public float RoamRange { get; set; }

            [JsonProperty(PropertyName = "NPC Chase Range")]
            public float ChaseRange { get; set; }

            [JsonProperty(PropertyName = "Wear Items")]
            public WearConfig WearItems { get; set; } = new WearConfig();

            [JsonProperty(PropertyName = "Belt Items")]
            public BeltConfig BeltItems { get; set; } = new BeltConfig();
        }

        public class WearConfig
        {
            [JsonProperty(PropertyName = "Items", ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public List<WearItemConfig> Items { get; set; } = new List<WearItemConfig>();
        }

        public class BeltConfig
        {
            [JsonProperty(PropertyName = "Items", ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public List<BeltItemConfig> Items { get; set; } = new List<BeltItemConfig>();
        }

        public class BeltItemConfig
        {
            [JsonProperty(PropertyName = "ShortName")]
            public string ShortName { get; set; }

            [JsonProperty(PropertyName = "Amount")]
            public int Amount { get; set; }

            [JsonProperty(PropertyName = "SkinID")]
            public ulong SkinID { get; set; }

            [JsonProperty(PropertyName = "Mods")]
            public List<string> Mods { get; set; } = new List<string>();
        }

        public class WearItemConfig
        {
            [JsonProperty(PropertyName = "ShortName")]
            public string ShortName { get; set; }

            [JsonProperty(PropertyName = "SkinID")]
            public ulong SkinID { get; set; }
        }

        public class LootBoxConfig
        {
            [JsonProperty(PropertyName = "Loot Items", ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public List<ItemConfig> LootItems { get; set; } =
                new List<ItemConfig>
                {
                    new()
                    {
                        ShortName = "ammo.rifle",
                        InclusionChancePercentage = 15,
                        MinimumAmount = 100,
                        MaximumAmount = 300,
                    },
                    new()
                    {
                        ShortName = "sulfur.ore",
                        InclusionChancePercentage = 15,
                        MinimumAmount = 100,
                        MaximumAmount = 300,
                    },
                    new()
                    {
                        ShortName = "scrap",
                        InclusionChancePercentage = 70,
                        MinimumAmount = 500,
                        MaximumAmount = 2000,
                    },
                    new()
                    {
                        ShortName = "rifle.ak",
                        InclusionChancePercentage = 5,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "rocket.launcher.dragon",
                        InclusionChancePercentage = 5,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "grenade.f1",
                        InclusionChancePercentage = 10,
                        MinimumAmount = 1,
                        MaximumAmount = 5,
                    },
                    new()
                    {
                        ShortName = "crude.oil",
                        InclusionChancePercentage = 40,
                        MinimumAmount = 10,
                        MaximumAmount = 100,
                    },
                    new()
                    {
                        ShortName = "diesel_barrel",
                        InclusionChancePercentage = 30,
                        MinimumAmount = 1,
                        MaximumAmount = 5,
                    },
                    new()
                    {
                        ShortName = "cctv.camera",
                        InclusionChancePercentage = 20,
                        MinimumAmount = 5,
                        MaximumAmount = 20,
                    },
                    new()
                    {
                        ShortName = "gingerbreadsuit",
                        InclusionChancePercentage = 5,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "gears",
                        InclusionChancePercentage = 20,
                        MinimumAmount = 1,
                        MaximumAmount = 20,
                    },
                    new()
                    {
                        ShortName = "metal.refined",
                        InclusionChancePercentage = 20,
                        MinimumAmount = 10,
                        MaximumAmount = 80,
                    },
                    new()
                    {
                        ShortName = "electric.furnace",
                        InclusionChancePercentage = 20,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "fuse",
                        InclusionChancePercentage = 20,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "xmas.door.garland",
                        InclusionChancePercentage = 10,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "gunpowder",
                        InclusionChancePercentage = 50,
                        MinimumAmount = 50,
                        MaximumAmount = 150,
                    },
                    new()
                    {
                        ShortName = "handcuffs",
                        InclusionChancePercentage = 10,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "hazmatsuit",
                        InclusionChancePercentage = 10,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "ammo.rocket.hv",
                        InclusionChancePercentage = 10,
                        MinimumAmount = 1,
                        MaximumAmount = 20,
                    },
                    new()
                    {
                        ShortName = "ammo.rifle.hv",
                        InclusionChancePercentage = 10,
                        MinimumAmount = 100,
                        MaximumAmount = 300,
                    },
                    new()
                    {
                        ShortName = "metal.facemask",
                        InclusionChancePercentage = 25,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "ammo.rocket.fire",
                        InclusionChancePercentage = 10,
                        MinimumAmount = 5,
                        MaximumAmount = 10,
                    },
                    new()
                    {
                        ShortName = "jackhammer",
                        InclusionChancePercentage = 30,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "rifle.l96",
                        InclusionChancePercentage = 5,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "furnace.large",
                        InclusionChancePercentage = 50,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "largemedkit",
                        InclusionChancePercentage = 50,
                        MinimumAmount = 1,
                        MaximumAmount = 10,
                    },
                    new()
                    {
                        ShortName = "xmas.present.large",
                        InclusionChancePercentage = 40,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "locker",
                        InclusionChancePercentage = 40,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "lowgradefuel",
                        InclusionChancePercentage = 70,
                        MinimumAmount = 50,
                        MaximumAmount = 200,
                    },
                    new()
                    {
                        ShortName = "rifle.lr300",
                        InclusionChancePercentage = 5,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "rifle.m39",
                        InclusionChancePercentage = 5,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "pistol.m92",
                        InclusionChancePercentage = 10,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "lmg.m249",
                        InclusionChancePercentage = 2,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "syringe.medical",
                        InclusionChancePercentage = 70,
                        MinimumAmount = 10,
                        MaximumAmount = 30,
                    },
                    new()
                    {
                        ShortName = "metal.ore",
                        InclusionChancePercentage = 80,
                        MinimumAmount = 100,
                        MaximumAmount = 300,
                    },
                    new()
                    {
                        ShortName = "metalpipe",
                        InclusionChancePercentage = 60,
                        MinimumAmount = 1,
                        MaximumAmount = 5,
                    },
                    new()
                    {
                        ShortName = "metalspring",
                        InclusionChancePercentage = 60,
                        MinimumAmount = 1,
                        MaximumAmount = 5,
                    },
                    new()
                    {
                        ShortName = "minigun",
                        InclusionChancePercentage = 2,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "grenade.molotov",
                        InclusionChancePercentage = 10,
                        MinimumAmount = 1,
                        MaximumAmount = 5,
                    },
                    new()
                    {
                        ShortName = "smg.mp5",
                        InclusionChancePercentage = 4,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "multiplegrenadelauncher",
                        InclusionChancePercentage = 15,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "mummymask",
                        InclusionChancePercentage = 20,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "mushroom",
                        InclusionChancePercentage = 80,
                        MinimumAmount = 20,
                        MaximumAmount = 40,
                    },
                    new()
                    {
                        ShortName = "weapon.mod.muzzleboost",
                        InclusionChancePercentage = 35,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "weapon.mod.muzzlebrake",
                        InclusionChancePercentage = 35,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "nightvisiongoggles",
                        InclusionChancePercentage = 20,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "newyeargong",
                        InclusionChancePercentage = 25,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "hazmatsuit.nomadsuit",
                        InclusionChancePercentage = 20,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "firework.boomer.pattern",
                        InclusionChancePercentage = 40,
                        MinimumAmount = 1,
                        MaximumAmount = 5,
                    },
                    new()
                    {
                        ShortName = "jar.pickle",
                        InclusionChancePercentage = 60,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "ammo.pistol",
                        InclusionChancePercentage = 70,
                        MinimumAmount = 100,
                        MaximumAmount = 500,
                    },
                    new()
                    {
                        ShortName = "pookie.bear",
                        InclusionChancePercentage = 60,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "potato",
                        InclusionChancePercentage = 70,
                        MinimumAmount = 100,
                        MaximumAmount = 100,
                    },
                    new()
                    {
                        ShortName = "pistol.revolver",
                        InclusionChancePercentage = 50,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "riflebody",
                        InclusionChancePercentage = 60,
                        MinimumAmount = 5,
                        MaximumAmount = 10,
                    },
                    new()
                    {
                        ShortName = "roadsign.gloves",
                        InclusionChancePercentage = 40,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "roadsign.kilt",
                        InclusionChancePercentage = 40,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "ammo.rocket.basic",
                        InclusionChancePercentage = 30,
                        MinimumAmount = 1,
                        MaximumAmount = 5,
                    },
                    new()
                    {
                        ShortName = "rocket.launcher",
                        InclusionChancePercentage = 5,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "rope",
                        InclusionChancePercentage = 60,
                        MinimumAmount = 5,
                        MaximumAmount = 10,
                    },
                    new()
                    {
                        ShortName = "icepick.salvaged",
                        InclusionChancePercentage = 30,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "explosive.satchel",
                        InclusionChancePercentage = 25,
                        MinimumAmount = 1,
                        MaximumAmount = 5,
                    },
                    new()
                    {
                        ShortName = "electric.seismicsensor",
                        InclusionChancePercentage = 20,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "semibody",
                        InclusionChancePercentage = 60,
                        MinimumAmount = 5,
                        MaximumAmount = 10,
                    },
                    new()
                    {
                        ShortName = "pistol.semiauto",
                        InclusionChancePercentage = 10,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "rifle.semiauto",
                        InclusionChancePercentage = 5,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "sheetmetal",
                        InclusionChancePercentage = 70,
                        MinimumAmount = 1,
                        MaximumAmount = 9,
                    },
                    new()
                    {
                        ShortName = "sewingkit",
                        InclusionChancePercentage = 65,
                        MinimumAmount = 1,
                        MaximumAmount = 10,
                    },
                    new()
                    {
                        ShortName = "weapon.mod.silencer",
                        InclusionChancePercentage = 10,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "weapon.mod.simplesight",
                        InclusionChancePercentage = 50,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "knife.skinning",
                        InclusionChancePercentage = 50,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "rifle.sks",
                        InclusionChancePercentage = 7,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "xmas.present.small",
                        InclusionChancePercentage = 40,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "smgbody",
                        InclusionChancePercentage = 60,
                        MinimumAmount = 5,
                        MaximumAmount = 9,
                    },
                    new()
                    {
                        ShortName = "sofa",
                        InclusionChancePercentage = 30,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "hazmatsuit.spacesuit",
                        InclusionChancePercentage = 10,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "spookyspeaker",
                        InclusionChancePercentage = 25,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "stones",
                        InclusionChancePercentage = 80,
                        MinimumAmount = 100,
                        MaximumAmount = 600,
                    },
                    new()
                    {
                        ShortName = "sulfur",
                        InclusionChancePercentage = 80,
                        MinimumAmount = 100,
                        MaximumAmount = 600,
                    },
                    new()
                    {
                        ShortName = "tactical.gloves",
                        InclusionChancePercentage = 35,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "tarp",
                        InclusionChancePercentage = 60,
                        MinimumAmount = 1,
                        MaximumAmount = 5,
                    },
                    new()
                    {
                        ShortName = "vampire.stake",
                        InclusionChancePercentage = 20,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "vending.machine",
                        InclusionChancePercentage = 15,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "bottle.vodka",
                        InclusionChancePercentage = 9,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "gun.water",
                        InclusionChancePercentage = 9,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "piano",
                        InclusionChancePercentage = 9,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "firework.volcano",
                        InclusionChancePercentage = 30,
                        MinimumAmount = 1,
                        MaximumAmount = 5,
                    },
                    new()
                    {
                        ShortName = "hat.wolf",
                        InclusionChancePercentage = 50,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "worm",
                        InclusionChancePercentage = 70,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                };
        }

        private class TurretConfig
        {
            [JsonProperty(PropertyName = "Health")]
            public float Health { get; set; }

            [JsonProperty(PropertyName = "Weapon Short Name")]
            public string WeaponShortName { get; set; }
        }

        private class DungeonSpawnSettings
        {
            [JsonProperty(PropertyName = "Enable Auto Spawn")]
            public bool EnableAutoSpawn { get; set; } = false;

            [JsonProperty(PropertyName = "Enable Halloween Dungeon")]
            public bool EnableHalloweenDungeon { get; set; } = true;

            [JsonProperty(PropertyName = "Enable Xmas Dungeon")]
            public bool EnableXmasDungeon { get; set; } = false;

            [JsonProperty(PropertyName = "Max Total Active Dungeons")]
            public int MaxTotalActiveDungeons { get; set; } = 5;

            [JsonProperty(PropertyName = "Min Distance From Monuments")]
            public float MinDistanceFromMonuments { get; set; } = 100f;

            [JsonProperty(PropertyName = "Min Distance Between Dungeons")]
            public float MinDistanceBetweenDungeons { get; set; } = 100f;

            [JsonProperty(PropertyName = "Remove Dungeon if All NPCs Are Dead")]
            public bool RemoveIfAllNpcsDead { get; set; } = true;

            [JsonProperty(PropertyName = "Max Time (in seconds) to Keep Dungeon Alive")]
            public int MaxDungeonLifetime { get; set; } = 1800;
        }

        public class ItemConfig
        {
            [JsonProperty(PropertyName = "ShortName")]
            public string ShortName { get; set; }

            [JsonProperty(PropertyName = "Inclusion Chance Percentage")]
            public float InclusionChancePercentage { get; set; }

            [JsonProperty(PropertyName = "Minimum Amount")]
            public int MinimumAmount { get; set; }

            [JsonProperty(PropertyName = "Maximum Amount")]
            public int MaximumAmount { get; set; }

            [JsonProperty(PropertyName = "CustomName")]
            public string CustomName { get; set; } = string.Empty;

            [JsonProperty(PropertyName = "SkinID")]
            public ulong SkinID { get; set; } = 0;
        }

        protected override void LoadConfig()
        {
            PreviousVersion = Version;
            base.LoadConfig();
            try
            {
                _config = Config.ReadObject<Configuration>();
                bool configChanged = false;

                if (_config == null)
                    LoadDefaultConfig();

                if (_config.Version == null || _config.Version < Version)
                {
                    PreviousVersion = _config.Version;
                    PrintWarning("Updating configuration file to new version.");
                    _config.Version = Version;
                    configChanged = true;
                }

                if (configChanged)
                {
                    PrintWarning("Configuration changes detected, saving changes.");
                    SaveConfig();
                }
            }
            catch (Exception ex)
            {
                PrintError($"Error reading config: {ex.Message}");
                LoadDefaultConfig();
            }
            SaveConfig();
        }

        protected override void LoadDefaultConfig()
        {
            PrintWarning("Creating a new configuration file");
            _config = new Configuration();
            _config.Version = Version;
        }

        protected override void SaveConfig()
        {
            Config.WriteObject(_config, true);
        }

        #endregion

        #region Localization

        protected override void LoadDefaultMessages()
        {
            if (PreviousVersion < Version)
            {
                lang.RegisterMessages(new Dictionary<string, string> { }, this);
                lang.RegisterMessages(new Dictionary<string, string> { }, this, "pt-BR");
                lang.RegisterMessages(new Dictionary<string, string> { }, this, "de");
                lang.RegisterMessages(new Dictionary<string, string> { }, this, "es");
                lang.RegisterMessages(new Dictionary<string, string> { }, this, "ru");
            }

            lang.RegisterMessages(
                new Dictionary<string, string>
                {
                    ["AlreadyOwnsDungeonMessage"] = "You already own an active dungeon. You cannot purchase another one until it expires or is removed.",
                    ["DungeonAppeared"] = "A {0} has appeared at grid {1}!",
                    ["DungeonRemovedDetail"] = "Removed dungeon of tier {0} at grid {1}.",
                    ["DungeonSpawnedSuccess"] = "Dungeon spawned successfully at grid {0}!",
                    ["DungeonSpawnFailed"] = "Failed to create the dungeon. We will try again shortly.",
                    ["DungeonStillSpawningMessage"] = "The dungeon is still spawning. Please wait until it's ready before entering.",
                    ["InsufficientFundsMessage"] = "You don't have enough funds to buy the {0} tier. Required: {1}.",
                    ["InvalidTierMessage"] = "Invalid tier name '{0}'. Available tiers: {1}.",
                    ["MaxActiveDungeonsReached"] = "Cannot spawn: Maximum number of active dungeons reached.",
                    ["NoAccess"] = "You don't have permission to do this.",
                    ["PurchaseSuccessMessage"] = "You have successfully purchased the {0} tier!",
                    ["ReceivedRequest"] = "Your request has been added to the queue. Please wait.",
                    ["UsageBuyMessage"] = "Usage: /buydungeon <tier_name>",
                    ["UsageCreateMessage"] = "Usage: /createdungeon <tier_name>",
                    ["CleanedUp"] = "All entities related to this plugin have been cleaned up.",
                },
                this
            );

            lang.RegisterMessages(
                new Dictionary<string, string>
                {
                    ["AlreadyOwnsDungeonMessage"] = "Você já possui uma masmorra ativa. Não é possível comprar outra antes que ela expire ou seja removida.",
                    ["DungeonAppeared"] = "Uma {0} apareceu na grade {1}!",
                    ["DungeonRemovedDetail"] = "Masmorra de nível {0} removida na grade {1}.",
                    ["DungeonSpawnedSuccess"] = "Masmorra gerada com sucesso na grade {0}!",
                    ["DungeonSpawnFailed"] = "Falha ao criar a masmorra. Tentaremos novamente em breve.",
                    ["DungeonStillSpawningMessage"] = "A masmorra ainda está sendo gerada. Por favor, aguarde até que esteja pronta antes de entrar.",
                    ["InsufficientFundsMessage"] = "Você não tem fundos suficientes para comprar o nível {0}. Requerido: {1}.",
                    ["InvalidTierMessage"] = "Nome de nível inválido '{0}'. Níveis disponíveis: {1}.",
                    ["MaxActiveDungeonsReached"] = "Não é possível gerar: número máximo de masmorras ativas alcançado.",
                    ["NoAccess"] = "Você não tem permissão para fazer isso.",
                    ["PurchaseSuccessMessage"] = "Você comprou com sucesso o nível {0}!",
                    ["ReceivedRequest"] = "Seu pedido foi adicionado à fila. Por favor, aguarde.",
                    ["UsageBuyMessage"] = "Uso: /buydungeon <nome_do_nível>",
                    ["UsageCreateMessage"] = "Uso: /createdungeon <nome_do_nível>",
                    ["CleanedUp"] = "Todos os objetos relacionados a este plugin foram removidos.",
                },
                this,
                "pt-BR"
            );

            lang.RegisterMessages(
                new Dictionary<string, string>
                {
                    ["AlreadyOwnsDungeonMessage"] = "Du besitzt bereits ein aktives Verlies. Du kannst kein weiteres kaufen, bevor es abläuft oder entfernt wird.",
                    ["DungeonAppeared"] = "Ein {0} ist bei Gitter {1} erschienen!",
                    ["DungeonRemovedDetail"] = "Verlies der Stufe {0} bei Gitter {1} wurde entfernt.",
                    ["DungeonSpawnedSuccess"] = "Verlies erfolgreich bei Gitter {0} erstellt!",
                    ["DungeonSpawnFailed"] = "Fehler beim Erstellen des Verlieses. Wir versuchen es bald erneut.",
                    ["DungeonStillSpawningMessage"] = "Das Verlies wird noch erstellt. Bitte warte, bis es bereit ist, bevor du es betrittst.",
                    ["InsufficientFundsMessage"] = "Du hast nicht genug Geld, um die Stufe {0} zu kaufen. Erforderlich: {1}.",
                    ["InvalidTierMessage"] = "Ungültiger Stufenname '{0}'. Verfügbare Stufen: {1}.",
                    ["MaxActiveDungeonsReached"] = "Kann nicht erstellt werden: Maximale Anzahl aktiver Verliese erreicht.",
                    ["NoAccess"] = "Du hast keine Berechtigung, dies zu tun.",
                    ["PurchaseSuccessMessage"] = "Du hast die Stufe {0} erfolgreich gekauft!",
                    ["ReceivedRequest"] = "Deine Anfrage wurde der Warteschlange hinzugefügt. Bitte warte.",
                    ["UsageBuyMessage"] = "Verwendung: /buydungeon <stufen_name>",
                    ["UsageCreateMessage"] = "Verwendung: /createdungeon <stufen_name>",
                    ["CleanedUp"] = "Alle mit diesem Plugin verbundenen Objekte wurden entfernt.",
                },
                this,
                "de"
            );

            lang.RegisterMessages(
                new Dictionary<string, string>
                {
                    ["AlreadyOwnsDungeonMessage"] = "Ya posees una mazmorra activa. No puedes comprar otra hasta que expire o se elimine.",
                    ["DungeonAppeared"] = "¡Una {0} ha aparecido en la cuadrícula {1}!",
                    ["DungeonRemovedDetail"] = "Mazmorra de nivel {0} eliminada en la cuadrícula {1}.",
                    ["DungeonSpawnedSuccess"] = "Mazmorra creada exitosamente en la cuadrícula {0}!",
                    ["DungeonSpawnFailed"] = "Error al crear la mazmorra. Intentaremos de nuevo pronto.",
                    ["DungeonStillSpawningMessage"] = "La mazmorra aún se está generando. Por favor, espera hasta que esté lista antes de entrar.",
                    ["InsufficientFundsMessage"] = "No tienes suficientes fondos para comprar el nivel {0}. Requerido: {1}.",
                    ["InvalidTierMessage"] = "Nombre de nivel inválido '{0}'. Niveles disponibles: {1}.",
                    ["MaxActiveDungeonsReached"] = "No se puede generar: se alcanzó el número máximo de mazmorras activas.",
                    ["NoAccess"] = "No tienes permiso para hacer esto.",
                    ["PurchaseSuccessMessage"] = "¡Completaste exitosamente la compra del nivel {0}!",
                    ["ReceivedRequest"] = "Tu solicitud se ha añadido a la cola. Por favor espera.",
                    ["UsageBuyMessage"] = "Uso: /buydungeon <nombre_del_nivel>",
                    ["UsageCreateMessage"] = "Uso: /createdungeon <nombre_del_nivel>",
                    ["CleanedUp"] = "Todos los objetos relacionados con este complemento han sido eliminados.",
                },
                this,
                "es"
            );

            lang.RegisterMessages(
                new Dictionary<string, string>
                {
                    ["AlreadyOwnsDungeonMessage"] = "Вы уже владеете активным подземельем. Вы не можете приобрести другое, пока оно не истечет или не будет удалено.",
                    ["DungeonAppeared"] = "Подземелье {0} появилось в сетке {1}!",
                    ["DungeonRemovedDetail"] = "Удалено подземелье уровня {0} в сетке {1}.",
                    ["DungeonSpawnedSuccess"] = "Подземелье успешно создано в сетке {0}!",
                    ["DungeonSpawnFailed"] = "Не удалось создать подземелье. Мы скоро попробуем снова.",
                    ["DungeonStillSpawningMessage"] = "Подземелье все еще создается. Пожалуйста, подождите, пока оно не будет готово перед входом.",
                    ["InsufficientFundsMessage"] = "У вас недостаточно средств для покупки уровня {0}. Требуется: {1}.",
                    ["InvalidTierMessage"] = "Недопустимое имя уровня '{0}'. Доступные уровни: {1}.",
                    ["MaxActiveDungeonsReached"] = "Невозможно создать: достигнуто максимальное количество активных подземелий.",
                    ["NoAccess"] = "У вас нет разрешения для этого.",
                    ["PurchaseSuccessMessage"] = "Вы успешно приобрели уровень {0}!",
                    ["ReceivedRequest"] = "Ваш запрос добавлен в очередь. Пожалуйста, подождите.",
                    ["UsageBuyMessage"] = "Использование: /buydungeon <имя_уровня>",
                    ["UsageCreateMessage"] = "Использование: /createdungeon <имя_уровня>",
                    ["CleanedUp"] = "Все объекты, связанные с этим плагином, были удалены.",
                },
                this,
                "ru"
            );
        }

        private string Msg(string userId, string key, params object[] obj)
        {
            return string.Format(lang.GetMessage(key, this, userId), obj);
        }

        private void NotifyPlayer(BasePlayer player, string key, int type, params object[] obj)
        {
            if (player != null && player.IsConnected)
            {
                var userIdString = player.UserIDString;
                if (_config.UseNotify && Notify != null)
                    Interface.Oxide.CallHook("SendNotify", userIdString, type, Msg(userIdString, key, obj));
                else
                    player.ChatMessage(Msg(userIdString, key, obj));
            }
        }

        #endregion

        #region Data Classes

        private class DungeonData
        {
            public List<ActiveDungeon> ActiveDungeons { get; set; } = new List<ActiveDungeon>();
            public List<DungeonMarker> ActiveDungeonMarkers { get; set; } = new List<DungeonMarker>();
        }

        private class ActiveDungeon
        {
            public ulong PortalId { get; set; }
            public ulong DynamicDungeonId { get; set; }
            public ulong OwnerID { get; set; } = 0UL;
            public string OwnerName { get; set; } = string.Empty;
            public Vector3 Position { get; set; }
            public string Grid { get; set; }
            public string TierName { get; set; }
            public string DungeonType { get; set; }
            public DungeonTierConfig TierConfig { get; set; }
            public List<ulong> EntityIds { get; set; } = new List<ulong>();
            public List<ulong> NpcIds { get; set; } = new List<ulong>();
            public bool Spawned { get; set; } = false;
            public DateTime CreationTime { get; set; } = DateTime.UtcNow;
        }

        private class DungeonMarker
        {
            public ulong VendingMarker { get; set; }
            public ulong RadiusMarker { get; set; }
            public Vector3 Position { get; set; }
            public string Tier { get; set; }
        }

        #endregion

        #region Collider Checks

        public static class LocationCheck
        {
            public static bool IsInsideRock(Vector3 position, float radius)
            {
                List<Collider> colliders = Pool.GetList<Collider>();
                Vis.Colliders(position, radius, colliders, Layers.Mask.World, QueryTriggerInteraction.Ignore);

                bool result = false;
                foreach (Collider collider in colliders)
                {
                    if (
                        collider.name.Contains("rock", CompareOptions.OrdinalIgnoreCase)
                        || collider.name.Contains("cliff", CompareOptions.OrdinalIgnoreCase)
                        || collider.name.Contains("formation", CompareOptions.OrdinalIgnoreCase)
                    )
                    {
                        result = true;
                        break;
                    }
                }

                Pool.FreeUnmanaged(ref colliders);
                return result;
            }

            public static bool IsFlying(Vector3 position, float minHeight)
            {
                RaycastHit hitInfo;
                return !Physics.Raycast(position, Vector3.down, out hitInfo, minHeight, -5);
            }

            public static bool IsInsideObject(Vector3 position, float radius)
            {
                return GamePhysics.CheckSphere(
                    position,
                    radius,
                    Layers.Mask.Player_Server | Layers.Server.Deployed | Layers.Mask.Prevent_Movement | Layers.Mask.Trigger,
                    QueryTriggerInteraction.Ignore
                );
            }

            public static bool IsClipped(Vector3 position, float radius, float margin = -0.30f)
            {
                var colliders = Pool.Get<List<Collider>>();
                Vis.Colliders(position, radius, colliders, Layers.Mask.World);

                string[] ignoredColliders = { "prevent_building", "UndergroundTrigger", "heatSource", "radiation", "comfortSource" };

                bool isClipped = false;

                foreach (var collider in colliders)
                {
                    if (ignoredColliders.Contains(collider.name))
                    {
                        continue;
                    }

                    var bounds = collider.bounds;

                    bool isInside =
                        position.x >= (bounds.min.x - margin)
                        && position.x <= (bounds.max.x + margin)
                        && position.z >= (bounds.min.z - margin)
                        && position.z <= (bounds.max.z + margin)
                        && position.y >= (bounds.min.y - margin)
                        && position.y <= (bounds.max.y + margin);

                    if (isInside)
                    {
                        isClipped = true;
                        break;
                    }
                }

                colliders.Clear();
                Pool.FreeUnmanaged(ref colliders);

                return isClipped;
            }

            public static bool TerrainDetection(Vector3 position, out RaycastHit hitInfo)
            {
                return Physics.Linecast(position + Vector3.up, position - Vector3.up, out hitInfo, Layers.Mask.Terrain);
            }

            public static bool InWater(Vector3 position) => WaterLevel.Test(position, false, false);

            public static bool IsOnRoad(Vector3 position)
            {
                return (TerrainMeta.TopologyMap.GetTopology(position) & (int)TerrainTopology.Enum.Road) != 0;
            }

            public static bool InNoBuildZone(Vector3 position, float radius)
            {
                return Physics.CheckSphere(position, radius, Layers.Mask.Prevent_Building);
            }

            public static bool IsInMonument(Vector3 position)
            {
                return TerrainMeta.Path.Monuments.Any(monument => monument.IsInBounds(position) && monument.shouldDisplayOnMap) || OnSpecificTopology(position, TerrainTopology.Enum.Monument);
            }

            public static bool HasCollider(Vector3 position, float radius)
            {
                List<Collider> hitColliders = Pool.Get<List<Collider>>();
                GamePhysics.OverlapSphere(position, radius, hitColliders);

                bool hasColliderNearby = false;
                foreach (Collider collider in hitColliders)
                {
                    BaseEntity entity = collider.gameObject.ToBaseEntity();
                    if (entity != null)
                    {
                        hasColliderNearby = true;
                        break;
                    }
                }

                Pool.FreeUnmanaged(ref hitColliders);
                return hasColliderNearby;
            }

            private static bool OnSpecificTopology(Vector3 position, TerrainTopology.Enum topology)
            {
                return (TerrainMeta.TopologyMap.GetTopology(position) & (int)topology) != 0;
            }
        }

        #endregion

        public class CoroutineManager : MonoBehaviour
        {
            private static CoroutineManager _instance;

            public static CoroutineManager Instance
            {
                get
                {
                    if (_instance == null)
                    {
                        var obj = new GameObject("CoroutineManager");
                        _instance = obj.AddComponent<CoroutineManager>();
                        GameObject.DontDestroyOnLoad(obj);
                    }
                    return _instance;
                }
            }
        }
    }
}