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] ...