Иконка ресурса

FREE ManualDoor : 3.7.0 3.7.0

Нет прав для скачивания
C#:
using System;
using System.Collections.Generic;
using System.Linq;
using Oxide.Core;
using Oxide.Core.Configuration;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Cui;
using UnityEngine;

namespace Oxide.Plugins
{
    [Info("ManualDoor", "Gemini", "3.7.0")]
    [Description("Spawns permanent, non-decaying doors with claim timers, eviction, and admin move/rotate GUI. Integrates with HoodWars for gang-based hotel rooms.")]
    public class ManualDoor : RustPlugin
    {
        private const string DoorPrefab = "assets/prefabs/building/door.hinged/door.hinged.metal.prefab";
        private const string DoubleDoorPrefab = "assets/prefabs/building/door.double.hinged/door.double.hinged.metal.prefab";
        private const string CodeLockPrefab = "assets/prefabs/locks/keypad/lock.code.prefab";

        private const string AdminPermission = "manualdoor.admin";

        private const float ClaimDuration = 1800f;  // 30 minutes
        private const float Warn10 = 600f;          // 10 minutes
        private const float Warn5 = 300f;           // 5 minutes

        private StoredData data;
        private DynamicConfigFile dataFile;

        // HoodWars plugin reference for gang integration
        [PluginReference]
        private Plugin HoodWars;

        // doorNetId -> timer
        private readonly Dictionary<ulong, Timer> claimTimers = new Dictionary<ulong, Timer>();

        // playerId -> doorNetId being edited
        private readonly Dictionary<ulong, ulong> editingDoor = new Dictionary<ulong, ulong>();

        // playerId -> move step
        private readonly Dictionary<ulong, float> moveStep = new Dictionary<ulong, float>();

        // playerId -> rotate step
        private readonly Dictionary<ulong, float> rotateStep = new Dictionary<ulong, float>();

        // playerId -> claim UI id
        private readonly Dictionary<ulong, string> claimUIs = new Dictionary<ulong, string>();

        // doorNetId that we intentionally kill during move/rotate
        private readonly HashSet<ulong> intendedKills = new HashSet<ulong>();

        #region Data classes

        private class StoredData
        {
            public Dictionary<ulong, DoorInfo> Doors = new Dictionary<ulong, DoorInfo>();
            public Dictionary<string, DoorLayoutInfo> SavedLayouts = new Dictionary<string, DoorLayoutInfo>();
        }

        private class DoorLayoutInfo
        {
            public List<DoorLayoutEntry> Entries = new List<DoorLayoutEntry>();
            public float SavedOriginX;
            public float SavedOriginY;
            public float SavedOriginZ;
        }

        private class DoorLayoutEntry
        {
            public float OffsetX; // Offset from origin
            public float OffsetY;
            public float OffsetZ;
            public float RotX;
            public float RotY;
            public float RotZ;
            public float RotW;
            public bool IsDoubleDoor;
        }

        private class DoorInfo
        {
            public float PosX;
            public float PosY;
            public float PosZ;

            public float RotX;
            public float RotY;
            public float RotZ;
            public float RotW;

            public ulong OwnerId;
            public ulong ClaimedBy;
            public double ClaimExpiry;

            public List<ulong> EvictedPlayers = new List<ulong>();
            public string LockCode;
            public bool IsDoubleDoor; // true for double door, false for single door

            public Vector3 GetPosition() => new Vector3(PosX, PosY, PosZ);
            public Quaternion GetRotation() => new Quaternion(RotX, RotY, RotZ, RotW);

            public void SetPosition(Vector3 pos)
            {
                PosX = pos.x;
                PosY = pos.y;
                PosZ = pos.z;
            }

            public void SetRotation(Quaternion rot)
            {
                RotX = rot.x;
                RotY = rot.y;
                RotZ = rot.z;
                RotW = rot.w;
            }

            public DoorInfo Clone()
            {
                return new DoorInfo
                {
                    PosX = PosX,
                    PosY = PosY,
                    PosZ = PosZ,
                    RotX = RotX,
                    RotY = RotY,
                    RotZ = RotZ,
                    RotW = RotW,
                    OwnerId = OwnerId,
                    ClaimedBy = ClaimedBy,
                    ClaimExpiry = ClaimExpiry,
                    EvictedPlayers = new List<ulong>(EvictedPlayers),
                    LockCode = LockCode,
                    IsDoubleDoor = IsDoubleDoor
                };
            }
        }

        private class CodeLockState
        {
            public string Code;
            public bool IsLocked;
            public List<ulong> Whitelist = new List<ulong>();
            public List<ulong> Guests = new List<ulong>();
        }

        #endregion

        #region Oxide hooks

        private void Init()
        {
            permission.RegisterPermission(AdminPermission, this);

            dataFile = Interface.Oxide.DataFileSystem.GetFile("ManualDoors");
            try
            {
                data = dataFile.ReadObject<StoredData>() ?? new StoredData();
            }
            catch
            {
                data = new StoredData();
            }

            if (data.Doors == null)
                data.Doors = new Dictionary<ulong, DoorInfo>();
            
            if (data.SavedLayouts == null)
                data.SavedLayouts = new Dictionary<string, DoorLayoutInfo>();
        }

        private void OnServerInitialized()
        {
            var removeIds = new List<ulong>();

            foreach (var kvp in data.Doors.ToList())
            {
                var id = kvp.Key;
                var info = kvp.Value;

                var existingDoor = BaseNetworkable.serverEntities
                    .OfType<BaseEntity>()
                    .FirstOrDefault(e => e.net != null && e.net.ID.Value == id);

                if (existingDoor == null)
                {
                    var spawned = SpawnDoorFromInfo(info);
                    if (spawned != null)
                    {
                        data.Doors.Remove(id);
                        data.Doors[spawned.net.ID.Value] = info;
                    }
                    else
                    {
                        removeIds.Add(id);
                    }
                }
                else
                {
                    RestoreClaimTimer(existingDoor, info);
                }
            }

            foreach (var id in removeIds)
                data.Doors.Remove(id);

            SaveData();

            timer.Every(1f, UpdateAllClaimUIs);
        }

        private void Unload()
        {
            foreach (var player in BasePlayer.activePlayerList)
            {
                DestroyClaimUI(player);
                DestroyAdminUI(player);
            }

            foreach (var t in claimTimers.Values)
                t?.Destroy();

            claimTimers.Clear();
            SaveData();
        }

        private void OnPlayerDisconnected(BasePlayer player)
        {
            DestroyClaimUI(player);
            DestroyAdminUI(player);

            editingDoor.Remove(player.userID);
            moveStep.Remove(player.userID);
            rotateStep.Remove(player.userID);
        }

        private object OnEntityTakeDamage(BaseCombatEntity entity, HitInfo info)
        {
            if (entity == null || info == null || entity.net == null)
                return null;

            // no decay on our doors
            if (data.Doors.ContainsKey(entity.net.ID.Value) &&
                info.damageTypes.Has(Rust.DamageType.Decay))
            {
                return false;
            }

            return null;
        }

        private void OnEntityKill(BaseNetworkable entity)
        {
            if (entity == null || entity.net == null)
                return;

            var id = entity.net.ID.Value;

            if (intendedKills.Contains(id))
            {
                intendedKills.Remove(id);
                return;
            }

            if (data.Doors.ContainsKey(id))
            {
                data.Doors.Remove(id);

                if (claimTimers.TryGetValue(id, out var t))
                {
                    t?.Destroy();
                    claimTimers.Remove(id);
                }

                // if any admin was editing this door, close their UI
                foreach (var kvp in editingDoor.ToList())
                {
                    if (kvp.Value == id)
                    {
                        var player = BasePlayer.FindByID(kvp.Key);
                        if (player != null)
                        {
                            DestroyAdminUI(player);
                            SendReply(player, "<color=#ff6666>The door you were editing was destroyed.</color>");
                        }

                        editingDoor.Remove(kvp.Key);
                    }
                }

                SaveData();
            }
        }

        /// <summary>
        /// Prevent players from removing our doors or their code locks via pickup (E + hammer).
        /// </summary>
        private object CanPickupEntity(BasePlayer player, BaseEntity entity)
        {
            if (player == null || entity == null)
                return null;

            // If it's a code lock with a ManualDoor parent, block
            if (entity is CodeLock codeLock)
            {
                var parent = codeLock.GetParentEntity();
                if (parent != null && parent.net != null && data.Doors.ContainsKey(parent.net.ID.Value))
                {
                    SendReply(player, "<color=#ff6666>You cannot remove this lock.</color>");
                    return false;
                }

                return null;
            }

            // If it's a ManualDoor itself, block pickup (must use /removedoor)
            if (entity.net != null && data.Doors.ContainsKey(entity.net.ID.Value))
            {
                SendReply(player, "<color=#ff6666>You cannot pick up this door. Use /removedoor as admin.</color>");
                return false;
            }

            return null;
        }

        /// <summary>
        /// Block enemy players from deploying code locks on ManualDoors in enemy HQ.
        /// This hook fires when a player tries to deploy an item on an entity.
        /// </summary>
        private object CanDeployItem(BasePlayer player, Deployer deployer, uint entityId)
        {
            if (player == null || deployer == null)
                return null;

            // Check if deploying a code lock
            var item = deployer.GetItem();
            if (item == null || item.info.shortname != "lock.code")
                return null;

            // Get the entity being deployed on
            var entity = BaseNetworkable.serverEntities.Find(new NetworkableId(entityId)) as BaseEntity;
            if (entity == null || entity.net == null)
                return null;

            // Check if it's a ManualDoor
            if (!data.Doors.ContainsKey(entity.net.ID.Value))
                return null;

            // Check with HoodWars if player can deploy locks in this location
            if (HoodWars == null || !HoodWars.IsLoaded)
                return null; // HoodWars not loaded, allow

            var result = HoodWars.Call("API_CanPlayerClaimInHQ", player, entity.transform.position);
            if (result is bool canClaim && !canClaim)
            {
                var gangName = HoodWars.Call("API_GetHQGangName", entity.transform.position);
                SendReply(player, $"<color=#ff6666>You cannot place locks on doors in {gangName} HQ. Only gang members can lock hotel doors in their own HQ.</color>");
                return false;
            }

            return null;
        }

        /// <summary>
        /// Secondary hook - fires when an item (like code lock) is deployed on a door.
        /// If player is in enemy territory, immediately remove the lock.
        /// </summary>
        private void OnItemDeployed(Deployer deployer, BaseEntity entity, BaseEntity deployedEntity)
        {
            if (deployer == null || entity == null || deployedEntity == null)
                return;

            // Check if the deployed entity is a code lock
            if (!(deployedEntity is CodeLock codeLock))
                return;

            // Check if it's on a ManualDoor
            if (entity.net == null || !data.Doors.ContainsKey(entity.net.ID.Value))
                return;

            var player = deployer.GetOwnerPlayer();
            if (player == null)
                return;

            // Check with HoodWars if player can deploy locks in this location
            if (HoodWars == null || !HoodWars.IsLoaded)
                return; // HoodWars not loaded, allow

            var result = HoodWars.Call("API_CanPlayerClaimInHQ", player, entity.transform.position);
            if (result is bool canClaim && !canClaim)
            {
                var gangName = HoodWars.Call("API_GetHQGangName", entity.transform.position);
                
                // Remove the code lock and refund it
                codeLock.Kill();
                
                // Give back the code lock
                var lockItem = ItemManager.CreateByName("lock.code", 1);
                if (lockItem != null)
                    player.GiveItem(lockItem);
                
                SendReply(player, $"<color=#ff6666>You cannot place locks on doors in {gangName} HQ. Only gang members can lock hotel doors in their own HQ.</color>");
            }
        }

        /// <summary>
        /// Prevent enemies from setting codes on existing locks on ManualDoors in enemy HQ.
        /// </summary>
        private object CanChangeCode(BasePlayer player, CodeLock codeLock, string newCode, bool isGuestCode)
        {
            if (player == null || codeLock == null)
                return null;

            var parent = codeLock.GetParentEntity();
            if (parent == null || parent.net == null)
                return null;

            // Check if it's a ManualDoor
            if (!data.Doors.ContainsKey(parent.net.ID.Value))
                return null;

            // Check with HoodWars if player can use locks in this location
            if (HoodWars == null || !HoodWars.IsLoaded)
                return null; // HoodWars not loaded, allow

            var result = HoodWars.Call("API_CanPlayerClaimInHQ", player, parent.transform.position);
            if (result is bool canClaim && !canClaim)
            {
                var gangName = HoodWars.Call("API_GetHQGangName", parent.transform.position);
                SendReply(player, $"<color=#ff6666>You cannot set codes on doors in {gangName} HQ. Only gang members can lock hotel doors in their own HQ.</color>");
                return false;
            }

            return null;
        }

        private object CanUseLockedEntity(BasePlayer player, CodeLock codeLock)
        {
            if (player == null || codeLock == null)
                return null;

            var parent = codeLock.GetParentEntity();
            if (parent == null || parent.net == null)
                return null;

            if (!data.Doors.TryGetValue(parent.net.ID.Value, out var info))
                return null;

            if (info.ClaimedBy == 0 || GetTimeRemaining(info) <= 0)
                return null; // unclaimed/expired, allow normal behaviour

            // authorized/whitelisted are fine
            if (info.ClaimedBy == player.userID)
                return true;
            if (codeLock.whitelistPlayers.Contains(player.userID))
                return true;
            if (codeLock.guestPlayers.Contains(player.userID))
                return true;

            return null;
        }

        /// <summary>
        /// OPTIONAL: for Oxide versions that fire this hook when a code is set.
        /// If your server doesn't call it, it will just sit unused (you still have /claimdoor).
        /// </summary>
        private void OnCodeLockCodeEntered(CodeLock codeLock, BasePlayer player, string code)
        {
            TryClaimFromCode(codeLock, player, code);
        }

        private void TryClaimFromCode(CodeLock codeLock, BasePlayer player, string code)
        {
            if (player == null || codeLock == null)
                return;

            var parent = codeLock.GetParentEntity();
            if (parent == null || parent.net == null)
                return;

            if (!data.Doors.TryGetValue(parent.net.ID.Value, out var info))
                return; // not our door

            var remaining = GetTimeRemaining(info);
            var unclaimedOrExpired = info.ClaimedBy == 0 || remaining <= 0;

            if (!unclaimedOrExpired)
                return; // DO NOT reset timer or code on already claimed doors

            if (info.EvictedPlayers.Contains(player.userID))
            {
                SendReply(player, "<color=#ff6666>You were recently evicted from this door and cannot reclaim it.</color>");
                return;
            }

            // Check HoodWars gang restrictions if plugin is loaded
            if (!CanPlayerClaimDoor(player, parent))
            {
                return; // HoodWars blocked the claim
            }

            ClaimDoor(parent, info, player, code);
        }

        // Check with HoodWars if player can claim this door based on gang affiliation
        private bool CanPlayerClaimDoor(BasePlayer player, BaseEntity door)
        {
            if (HoodWars == null || !HoodWars.IsLoaded)
                return true; // HoodWars not loaded, allow claim

            // Call HoodWars API to check if player can claim in this HQ
            var result = HoodWars.Call("API_CanPlayerClaimInHQ", player, door.transform.position);
            if (result is bool canClaim)
            {
                if (!canClaim)
                {
                    // Get the gang name for a more helpful message
                    var gangName = HoodWars.Call("API_GetHQGangName", door.transform.position);
                    SendReply(player, $"<color=#ff6666>You cannot claim doors in {gangName} HQ. Only gang members can claim hotel rooms in their own HQ.</color>");
                    return false;
                }
            }

            return true;
        }

        #endregion

        #region Chat commands

        [ChatCommand("spawndoor")]
        private void CmdSpawnDoor(BasePlayer player, string cmd, string[] args)
        {
            if (!permission.UserHasPermission(player.UserIDString, AdminPermission))
            {
                SendReply(player, "<color=#ff6666>Permission denied.</color>");
                return;
            }

            if (!Physics.Raycast(player.eyes.HeadRay(), out var hit, 10f))
            {
                SendReply(player, "<color=#ffcc00>Look at the ground to place the door.</color>");
                return;
            }

            var rot = Quaternion.Euler(0f, player.viewAngles.y + 180f, 0f);
            var door = SpawnPermanentDoor(hit.point, rot, player.userID, false);

            if (door != null)
                SendReply(player, "<color=#66ff66>Single door spawned. Use /dooredit while looking at it to adjust.</color>");
        }

        [ChatCommand("spawndoubledoor")]
        private void CmdSpawnDoubleDoor(BasePlayer player, string cmd, string[] args)
        {
            if (!permission.UserHasPermission(player.UserIDString, AdminPermission))
            {
                SendReply(player, "<color=#ff6666>Permission denied.</color>");
                return;
            }

            if (!Physics.Raycast(player.eyes.HeadRay(), out var hit, 10f))
            {
                SendReply(player, "<color=#ffcc00>Look at the ground to place the door.</color>");
                return;
            }

            var rot = Quaternion.Euler(0f, player.viewAngles.y + 180f, 0f);
            var door = SpawnPermanentDoor(hit.point, rot, player.userID, true);

            if (door != null)
                SendReply(player, "<color=#66ff66>Double door spawned. Use /dooredit while looking at it to adjust.</color>");
        }

        [ChatCommand("removedoor")]
        private void CmdRemoveDoor(BasePlayer player, string cmd, string[] args)
        {
            if (!permission.UserHasPermission(player.UserIDString, AdminPermission))
            {
                SendReply(player, "<color=#ff6666>Permission denied.</color>");
                return;
            }

            if (!Physics.Raycast(player.eyes.HeadRay(), out var hit, 5f))
            {
                SendReply(player, "<color=#ffcc00>Look at a ManualDoor.</color>");
                return;
            }

            var ent = hit.GetEntity();
            if (ent is CodeLock)
                ent = ent.GetParentEntity();

            if (ent == null || ent.net == null || !data.Doors.ContainsKey(ent.net.ID.Value))
            {
                SendReply(player, "<color=#ffcc00>That is not a ManualDoor.</color>");
                return;
            }

            data.Doors.Remove(ent.net.ID.Value);
            SaveData();
            ent.Kill();

            SendReply(player, "<color=#66ff66>ManualDoor removed.</color>");
        }

        /// <summary>
        /// Manual claim command:
        /// - If unclaimed or expired: claim + NEW random 4-digit code + start timer + UI.
        /// - If already claimed: NO reset, just message.
        /// - Also creates CodeLock if missing.
        /// </summary>
        [ChatCommand("claimdoor")]
        private void CmdClaimDoor(BasePlayer player, string cmd, string[] args)
        {
            if (!Physics.Raycast(player.eyes.HeadRay(), out var hit, 4f))
            {
                SendReply(player, "<color=#ffcc00>Look at a ManualDoor to claim it.</color>");
                return;
            }

            var ent = hit.GetEntity();
            if (ent is CodeLock)
                ent = ent.GetParentEntity();

            if (ent == null || ent.net == null || !data.Doors.TryGetValue(ent.net.ID.Value, out var info))
            {
                SendReply(player, "<color=#ffcc00>That is not a ManualDoor.</color>");
                return;
            }

            var remaining = GetTimeRemaining(info);
            var unclaimedOrExpired = info.ClaimedBy == 0 || remaining <= 0;

            if (!unclaimedOrExpired)
            {
                SendReply(player, "<color=#ff6666>This door is already claimed. Timer will not be reset.</color>");
                return;
            }

            if (info.EvictedPlayers.Contains(player.userID))
            {
                SendReply(player, "<color=#ff6666>You were recently evicted from this door and cannot reclaim it.</color>");
                return;
            }

            // Check HoodWars gang restrictions
            if (!CanPlayerClaimDoor(player, ent))
            {
                return; // HoodWars blocked the claim
            }

            // Attach lock if missing
            var codeLock = ent.GetSlot(BaseEntity.Slot.Lock) as CodeLock;
            if (codeLock == null)
            {
                var lockEnt = GameManager.server.CreateEntity(CodeLockPrefab, Vector3.zero, Quaternion.identity);
                if (lockEnt == null)
                {
                    SendReply(player, "<color=#ff6666>Failed to create code lock.</color>");
                    return;
                }

                lockEnt.SetParent(ent, "lock");
                lockEnt.OwnerID = info.OwnerId != 0 ? info.OwnerId : player.userID;
                lockEnt.Spawn();
                codeLock = lockEnt as CodeLock;
            }

            // Always generate a new random code for /claimdoor
            var code = UnityEngine.Random.Range(1000, 9999).ToString();

            ClaimDoor(ent, info, player, code);
            SendReply(player, $"<color=#66ff66>Door claimed. Your code is <b>{code}</b>.</color>");
        }

        [ChatCommand("dooredit")]
        private void CmdDoorEdit(BasePlayer player, string cmd, string[] args)
        {
            if (!permission.UserHasPermission(player.UserIDString, AdminPermission))
            {
                SendReply(player, "<color=#ff6666>Permission denied.</color>");
                return;
            }

            if (!Physics.Raycast(player.eyes.HeadRay(), out var hit, 5f))
            {
                SendReply(player, "<color=#ffcc00>Look at a ManualDoor.</color>");
                return;
            }

            var ent = hit.GetEntity();
            if (ent is CodeLock)
                ent = ent.GetParentEntity();

            if (ent == null || ent.net == null || !data.Doors.ContainsKey(ent.net.ID.Value))
            {
                SendReply(player, "<color=#ffcc00>That is not a ManualDoor.</color>");
                return;
            }

            editingDoor[player.userID] = ent.net.ID.Value;
            moveStep[player.userID] = 0.1f;
            rotateStep[player.userID] = 15f;

            CreateAdminUI(player);
            SendReply(player, "<color=#66ff66>Door edit mode active. Use the GUI to move/rotate.</color>");
        }

        [ChatCommand("doorinfo")]
        private void CmdDoorInfo(BasePlayer player, string cmd, string[] args)
        {
            if (!permission.UserHasPermission(player.UserIDString, AdminPermission))
            {
                SendReply(player, "<color=#ff6666>Permission denied.</color>");
                return;
            }

            if (!Physics.Raycast(player.eyes.HeadRay(), out var hit, 5f))
            {
                SendReply(player, "<color=#ffcc00>Look at a ManualDoor.</color>");
                return;
            }

            var ent = hit.GetEntity();
            if (ent is CodeLock)
                ent = ent.GetParentEntity();

            if (ent == null || ent.net == null || !data.Doors.TryGetValue(ent.net.ID.Value, out var info))
            {
                SendReply(player, "<color=#ffcc00>That is not a ManualDoor.</color>");
                return;
            }

            string claimant = "None";
            if (info.ClaimedBy != 0)
            {
                var p = BasePlayer.FindByID(info.ClaimedBy);
                claimant = p != null ? p.displayName : info.ClaimedBy.ToString();
            }

            var remaining = GetTimeRemaining(info);
            var timeStr = remaining > 0 ? FormatTime((int)remaining) : "Expired / Unclaimed";

            SendReply(player, "<color=#66ccff>=== ManualDoor Info ===</color>");
            SendReply(player, $"<color=#ffffff>ID: {ent.net.ID.Value}</color>");
            SendReply(player, $"<color=#ffffff>Claimed By: {claimant}</color>");
            SendReply(player, $"<color=#ffffff>Time Remaining: {timeStr}</color>");
            SendReply(player, $"<color=#ffffff>Evicted Players: {info.EvictedPlayers.Count}</color>");
        }

        [ChatCommand("resetdoor")]
        private void CmdResetDoor(BasePlayer player, string cmd, string[] args)
        {
            if (!permission.UserHasPermission(player.UserIDString, AdminPermission))
            {
                SendReply(player, "<color=#ff6666>Permission denied.</color>");
                return;
            }

            if (!Physics.Raycast(player.eyes.HeadRay(), out var hit, 5f))
            {
                SendReply(player, "<color=#ffcc00>Look at a ManualDoor.</color>");
                return;
            }

            var ent = hit.GetEntity();
            if (ent is CodeLock)
                ent = ent.GetParentEntity();

            if (ent == null || ent.net == null || !data.Doors.TryGetValue(ent.net.ID.Value, out var info))
            {
                SendReply(player, "<color=#ffcc00>That is not a ManualDoor.</color>");
                return;
            }

            info.ClaimedBy = 0;
            info.ClaimExpiry = 0;
            info.EvictedPlayers.Clear();
            info.LockCode = null;

            var cl = ent.GetSlot(BaseEntity.Slot.Lock) as CodeLock;
            if (cl != null)
            {
                cl.code = "";
                cl.SetFlag(BaseEntity.Flags.Locked, false);
                cl.whitelistPlayers.Clear();
                cl.guestPlayers.Clear();
                cl.SendNetworkUpdate();
            }

            if (claimTimers.TryGetValue(ent.net.ID.Value, out var t))
            {
                t?.Destroy();
                claimTimers.Remove(ent.net.ID.Value);
            }

            SaveData();
            SendReply(player, "<color=#66ff66>Door claim reset.</color>");
        }

        /// <summary>
        /// Save all current door positions as a layout.
        /// Usage: /savelayout <name>
        /// </summary>
        [ChatCommand("savelayout")]
        private void CmdSaveLayout(BasePlayer player, string cmd, string[] args)
        {
            if (!permission.UserHasPermission(player.UserIDString, AdminPermission))
            {
                SendReply(player, "<color=#ff6666>Permission denied.</color>");
                return;
            }

            if (args.Length < 1)
            {
                SendReply(player, "<color=#ffcc00>Usage: /savelayout <name></color>");
                return;
            }

            string layoutName = args[0].ToLower();

            if (data.Doors.Count == 0)
            {
                SendReply(player, "<color=#ff6666>No ManualDoors exist to save.</color>");
                return;
            }

            // Calculate centroid of all doors as the origin
            float originX = 0, originY = 0, originZ = 0;
            foreach (var door in data.Doors.Values)
            {
                originX += door.PosX;
                originY += door.PosY;
                originZ += door.PosZ;
            }
            originX /= data.Doors.Count;
            originY /= data.Doors.Count;
            originZ /= data.Doors.Count;

            var layout = new DoorLayoutInfo
            {
                SavedOriginX = originX,
                SavedOriginY = originY,
                SavedOriginZ = originZ
            };

            foreach (var door in data.Doors.Values)
            {
                layout.Entries.Add(new DoorLayoutEntry
                {
                    OffsetX = door.PosX - originX,
                    OffsetY = door.PosY - originY,
                    OffsetZ = door.PosZ - originZ,
                    RotX = door.RotX,
                    RotY = door.RotY,
                    RotZ = door.RotZ,
                    RotW = door.RotW,
                    IsDoubleDoor = door.IsDoubleDoor
                });
            }

            if (data.SavedLayouts == null)
                data.SavedLayouts = new Dictionary<string, DoorLayoutInfo>();

            data.SavedLayouts[layoutName] = layout;
            SaveData();

            SendReply(player, $"<color=#66ff66>Layout '{layoutName}' saved with {layout.Entries.Count} doors. Origin at ({originX:F1}, {originY:F1}, {originZ:F1})</color>");
        }

        /// <summary>
        /// List all saved layouts.
        /// Usage: /listlayouts
        /// </summary>
        [ChatCommand("listlayouts")]
        private void CmdListLayouts(BasePlayer player, string cmd, string[] args)
        {
            if (!permission.UserHasPermission(player.UserIDString, AdminPermission))
            {
                SendReply(player, "<color=#ff6666>Permission denied.</color>");
                return;
            }

            if (data.SavedLayouts == null || data.SavedLayouts.Count == 0)
            {
                SendReply(player, "<color=#ffcc00>No saved layouts found.</color>");
                return;
            }

            SendReply(player, "<color=#66ccff>=== Saved Layouts ===</color>");
            foreach (var kvp in data.SavedLayouts)
            {
                SendReply(player, $"<color=#ffffff>• {kvp.Key}: {kvp.Value.Entries.Count} doors</color>");
            }
        }

        /// <summary>
        /// Spawn doors from a saved layout at a new position.
        /// Usage: /spawnlayout <name>
        /// </summary>
        [ChatCommand("spawnlayout")]
        private void CmdSpawnLayout(BasePlayer player, string cmd, string[] args)
        {
            if (!permission.UserHasPermission(player.UserIDString, AdminPermission))
            {
                SendReply(player, "<color=#ff6666>Permission denied.</color>");
                return;
            }

            if (args.Length < 1)
            {
                SendReply(player, "<color=#ffcc00>Usage: /spawnlayout <name></color>");
                return;
            }

            string layoutName = args[0].ToLower();

            if (data.SavedLayouts == null || !data.SavedLayouts.TryGetValue(layoutName, out var layout))
            {
                SendReply(player, $"<color=#ff6666>Layout '{layoutName}' not found.</color>");
                return;
            }

            if (!Physics.Raycast(player.eyes.HeadRay(), out var hit, 50f))
            {
                SendReply(player, "<color=#ffcc00>Look at the ground to place the layout center.</color>");
                return;
            }

            Vector3 newOrigin = hit.point;
            int spawned = 0;

            foreach (var entry in layout.Entries)
            {
                var pos = new Vector3(
                    newOrigin.x + entry.OffsetX,
                    newOrigin.y + entry.OffsetY,
                    newOrigin.z + entry.OffsetZ
                );
                var rot = new Quaternion(entry.RotX, entry.RotY, entry.RotZ, entry.RotW);

                var door = SpawnPermanentDoor(pos, rot, player.userID, entry.IsDoubleDoor);
                if (door != null)
                    spawned++;
            }

            SendReply(player, $"<color=#66ff66>Spawned {spawned}/{layout.Entries.Count} doors from layout '{layoutName}' at ({newOrigin.x:F1}, {newOrigin.y:F1}, {newOrigin.z:F1})</color>");
        }

        /// <summary>
        /// Delete a saved layout.
        /// Usage: /deletelayout <name>
        /// </summary>
        [ChatCommand("deletelayout")]
        private void CmdDeleteLayout(BasePlayer player, string cmd, string[] args)
        {
            if (!permission.UserHasPermission(player.UserIDString, AdminPermission))
            {
                SendReply(player, "<color=#ff6666>Permission denied.</color>");
                return;
            }

            if (args.Length < 1)
            {
                SendReply(player, "<color=#ffcc00>Usage: /deletelayout <name></color>");
                return;
            }

            string layoutName = args[0].ToLower();

            if (data.SavedLayouts == null || !data.SavedLayouts.ContainsKey(layoutName))
            {
                SendReply(player, $"<color=#ff6666>Layout '{layoutName}' not found.</color>");
                return;
            }

            data.SavedLayouts.Remove(layoutName);
            SaveData();
            SendReply(player, $"<color=#66ff66>Layout '{layoutName}' deleted.</color>");
        }

        /// <summary>
        /// Move all existing doors by an offset.
        /// Usage: /movedoors <x> <y> <z>
        /// </summary>
        [ChatCommand("movedoors")]
        private void CmdMoveAllDoors(BasePlayer player, string cmd, string[] args)
        {
            if (!permission.UserHasPermission(player.UserIDString, AdminPermission))
            {
                SendReply(player, "<color=#ff6666>Permission denied.</color>");
                return;
            }

            if (args.Length < 3)
            {
                SendReply(player, "<color=#ffcc00>Usage: /movedoors <x> <y> <z></color>");
                return;
            }

            if (!float.TryParse(args[0], out float x) || !float.TryParse(args[1], out float y) || !float.TryParse(args[2], out float z))
            {
                SendReply(player, "<color=#ff6666>Invalid offset values. Use numbers.</color>");
                return;
            }

            var offset = new Vector3(x, y, z);
            var doorIds = data.Doors.Keys.ToList();
            int moved = 0;

            foreach (var doorId in doorIds)
            {
                if (!data.Doors.TryGetValue(doorId, out var info))
                    continue;

                var state = GetCodeLockState(doorId);
                var newPos = info.GetPosition() + offset;
                var rot = info.GetRotation();
                var newInfo = info.Clone();
                newInfo.SetPosition(newPos);

                intendedKills.Add(doorId);
                KillDoorById(doorId);
                data.Doors.Remove(doorId);

                if (claimTimers.TryGetValue(doorId, out var t))
                {
                    t?.Destroy();
                    claimTimers.Remove(doorId);
                }

                var newDoor = SpawnDoorWithState(newPos, rot, newInfo, state);
                if (newDoor != null)
                {
                    var newId = newDoor.net.ID.Value;
                    data.Doors[newId] = newInfo;

                    if (newInfo.ClaimedBy != 0 && GetTimeRemaining(newInfo) > 0)
                        StartClaimTimer(newDoor, newInfo);

                    moved++;
                }
            }

            SaveData();
            SendReply(player, $"<color=#66ff66>Moved {moved} doors by offset ({x:F2}, {y:F2}, {z:F2})</color>");
        }

        #endregion

        #region Console commands (admin GUI)

        [ConsoleCommand("mdoor.moveup")]
        private void CmdMoveUp(ConsoleSystem.Arg arg) => HandleMove(arg, "up");

        [ConsoleCommand("mdoor.movedown")]
        private void CmdMoveDown(ConsoleSystem.Arg arg) => HandleMove(arg, "down");

        [ConsoleCommand("mdoor.moveforward")]
        private void CmdMoveForward(ConsoleSystem.Arg arg) => HandleMove(arg, "forward");

        [ConsoleCommand("mdoor.moveback")]
        private void CmdMoveBack(ConsoleSystem.Arg arg) => HandleMove(arg, "back");

        [ConsoleCommand("mdoor.moveleft")]
        private void CmdMoveLeft(ConsoleSystem.Arg arg) => HandleMove(arg, "left");

        [ConsoleCommand("mdoor.moveright")]
        private void CmdMoveRight(ConsoleSystem.Arg arg) => HandleMove(arg, "right");

        [ConsoleCommand("mdoor.rotateleft")]
        private void CmdRotateLeft(ConsoleSystem.Arg arg) => HandleRotate(arg, "left");

        [ConsoleCommand("mdoor.rotateright")]
        private void CmdRotateRight(ConsoleSystem.Arg arg) => HandleRotate(arg, "right");

        [ConsoleCommand("mdoor.step1")]
        private void CmdStep1(ConsoleSystem.Arg arg) => HandleMoveStep(arg, 0.01f);

        [ConsoleCommand("mdoor.step2")]
        private void CmdStep2(ConsoleSystem.Arg arg) => HandleMoveStep(arg, 0.1f);

        [ConsoleCommand("mdoor.step3")]
        private void CmdStep3(ConsoleSystem.Arg arg) => HandleMoveStep(arg, 0.5f);

        [ConsoleCommand("mdoor.step4")]
        private void CmdStep4(ConsoleSystem.Arg arg) => HandleMoveStep(arg, 1.0f);

        [ConsoleCommand("mdoor.rot1")]
        private void CmdRot1(ConsoleSystem.Arg arg) => HandleRotateStep(arg, 5f);

        [ConsoleCommand("mdoor.rot2")]
        private void CmdRot2(ConsoleSystem.Arg arg) => HandleRotateStep(arg, 15f);

        [ConsoleCommand("mdoor.rot3")]
        private void CmdRot3(ConsoleSystem.Arg arg) => HandleRotateStep(arg, 45f);

        [ConsoleCommand("mdoor.rot4")]
        private void CmdRot4(ConsoleSystem.Arg arg) => HandleRotateStep(arg, 90f);

        [ConsoleCommand("mdoor.close")]
        private void CmdCloseUI(ConsoleSystem.Arg arg)
        {
            var player = arg.Player();
            if (player == null)
                return;

            DestroyAdminUI(player);
            editingDoor.Remove(player.userID);
            moveStep.Remove(player.userID);
            rotateStep.Remove(player.userID);

            SendReply(player, "<color=#66ff66>Door edit mode closed.</color>");
        }

        private void HandleMove(ConsoleSystem.Arg arg, string dir)
        {
            var player = arg.Player();
            if (player == null) return;
            if (!permission.UserHasPermission(player.UserIDString, AdminPermission)) return;

            if (!editingDoor.TryGetValue(player.userID, out var doorId))
            {
                SendReply(player, "<color=#ff6666>No door selected. Use /dooredit first.</color>");
                return;
            }

            var step = moveStep.TryGetValue(player.userID, out var s) ? s : 0.1f;

            var fwd = player.eyes.HeadForward();
            fwd.y = 0f;
            fwd.Normalize();
            var right = Vector3.Cross(Vector3.up, fwd);
            right.Normalize();

            var offset = Vector3.zero;

            switch (dir)
            {
                case "up": offset = Vector3.up * step; break;
                case "down": offset = Vector3.down * step; break;
                case "forward": offset = fwd * step; break;
                case "back": offset = -fwd * step; break;
                case "left": offset = -right * step; break;
                case "right": offset = right * step; break;
            }

            MoveDoor(player, doorId, offset);
        }

        private void HandleRotate(ConsoleSystem.Arg arg, string dir)
        {
            var player = arg.Player();
            if (player == null) return;
            if (!permission.UserHasPermission(player.UserIDString, AdminPermission)) return;

            if (!editingDoor.TryGetValue(player.userID, out var doorId))
            {
                SendReply(player, "<color=#ff6666>No door selected. Use /dooredit first.</color>");
                return;
            }

            var step = rotateStep.TryGetValue(player.userID, out var s) ? s : 15f;
            var angle = dir == "left" ? -step : step;

            RotateDoor(player, doorId, angle);
        }

        private void HandleMoveStep(ConsoleSystem.Arg arg, float step)
        {
            var player = arg.Player();
            if (player == null) return;
            if (!permission.UserHasPermission(player.UserIDString, AdminPermission)) return;

            moveStep[player.userID] = Mathf.Clamp(step, 0.01f, 5f);
            CreateAdminUI(player);
        }

        private void HandleRotateStep(ConsoleSystem.Arg arg, float step)
        {
            var player = arg.Player();
            if (player == null) return;
            if (!permission.UserHasPermission(player.UserIDString, AdminPermission)) return;

            rotateStep[player.userID] = Mathf.Clamp(step, 1f, 180f);
            CreateAdminUI(player);
        }

        #endregion

        #region Door manipulation

        private void MoveDoor(BasePlayer player, ulong doorId, Vector3 offset)
        {
            if (!data.Doors.TryGetValue(doorId, out var info))
            {
                SendReply(player, "<color=#ff6666>Door no longer exists.</color>");
                DestroyAdminUI(player);
                editingDoor.Remove(player.userID);
                return;
            }

            var state = GetCodeLockState(doorId);
            var newPos = info.GetPosition() + offset;
            var rot = info.GetRotation();
            var newInfo = info.Clone();
            newInfo.SetPosition(newPos);

            intendedKills.Add(doorId);
            KillDoorById(doorId);
            data.Doors.Remove(doorId);

            if (claimTimers.TryGetValue(doorId, out var t))
            {
                t?.Destroy();
                claimTimers.Remove(doorId);
            }

            var newDoor = SpawnDoorWithState(newPos, rot, newInfo, state);
            if (newDoor == null)
                return;

            var newId = newDoor.net.ID.Value;
            data.Doors[newId] = newInfo;
            editingDoor[player.userID] = newId;

            if (newInfo.ClaimedBy != 0 && GetTimeRemaining(newInfo) > 0)
                StartClaimTimer(newDoor, newInfo);

            SaveData();
        }

        private void RotateDoor(BasePlayer player, ulong doorId, float angle)
        {
            if (!data.Doors.TryGetValue(doorId, out var info))
            {
                SendReply(player, "<color=#ff6666>Door no longer exists.</color>");
                DestroyAdminUI(player);
                editingDoor.Remove(player.userID);
                return;
            }

            var state = GetCodeLockState(doorId);
            var pos = info.GetPosition();
            var newRot = info.GetRotation() * Quaternion.Euler(0f, angle, 0f);
            var newInfo = info.Clone();
            newInfo.SetRotation(newRot);

            intendedKills.Add(doorId);
            KillDoorById(doorId);
            data.Doors.Remove(doorId);

            if (claimTimers.TryGetValue(doorId, out var t))
            {
                t?.Destroy();
                claimTimers.Remove(doorId);
            }

            var newDoor = SpawnDoorWithState(pos, newRot, newInfo, state);
            if (newDoor == null)
                return;

            var newId = newDoor.net.ID.Value;
            data.Doors[newId] = newInfo;
            editingDoor[player.userID] = newId;

            if (newInfo.ClaimedBy != 0 && GetTimeRemaining(newInfo) > 0)
                StartClaimTimer(newDoor, newInfo);

            SaveData();
        }

        private CodeLockState GetCodeLockState(ulong doorId)
        {
            var door = BaseNetworkable.serverEntities
                .OfType<BaseEntity>()
                .FirstOrDefault(e => e.net != null && e.net.ID.Value == doorId);

            if (door == null)
                return null;

            var cl = door.GetSlot(BaseEntity.Slot.Lock) as CodeLock;
            if (cl == null)
                return null;

            return new CodeLockState
            {
                Code = cl.code,
                IsLocked = cl.IsLocked(),
                Whitelist = cl.whitelistPlayers.ToList(),
                Guests = cl.guestPlayers.ToList()
            };
        }

        private void KillDoorById(ulong doorId)
        {
            var door = BaseNetworkable.serverEntities
                .OfType<BaseEntity>()
                .FirstOrDefault(e => e.net != null && e
// ... [Code preview truncated due to length. Full source in attached file] ...

Автор
15FPS
Скачиваний
0
Просмотры
1
Первый выпуск
Обновление
Оценка
0.00 звёзд 0 оценок
Сверху