C#:
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Libraries.Covalence;
using Oxide.Game.Rust.Cui;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Cupboard Inspector", "Kaktus", "1.0.0")]
[Description("Admin visual audit tool for group-limited servers. Scans cupboards over the authorization limit, draws world markers, provides UI with teleport and pagination.")]
public class CupboardInspector : RustPlugin
{
private const string PermUse = "cupboardinspector.use";
private const string UiLayer = "CupboardInspector.UI";
private const int ItemsPerPage = 4;
private Configuration _config;
private Dictionary<ulong, List<ViolationInfo>> _playerViolations = new();
private Dictionary<ulong, int> _playerPages = new();
#region Config
private class Configuration
{
[JsonProperty("Max authorized players allowed in cupboard")]
public int MaxAuthorized = 2;
[JsonProperty("Marker duration (seconds)")]
public float MarkerDuration = 30f;
[JsonProperty("Marker height offset")]
public float MarkerHeight = 2.0f;
[JsonProperty("Marker line height")]
public float MarkerLineHeight = 25f;
[JsonProperty("Draw arrows in world")]
public bool DrawArrows = true;
[JsonProperty("Draw text in world")]
public bool DrawText = true;
[JsonProperty("UI Anchor Min")]
public string UiAnchorMin = "0.25 0.22";
[JsonProperty("UI Anchor Max")]
public string UiAnchorMax = "0.75 0.78";
[JsonProperty("Panel Color")]
public string PanelColor = "0.10 0.10 0.10 0.97";
[JsonProperty("Header Color")]
public string HeaderColor = "0.46 0.73 0.00 1.0";
[JsonProperty("Row Color")]
public string RowColor = "0.14 0.14 0.14 0.90";
[JsonProperty("Row Alt Color")]
public string RowAltColor = "0.16 0.16 0.16 0.90";
[JsonProperty("Button Color")]
public string ButtonColor = "0.20 0.20 0.20 1.0";
[JsonProperty("TP Button Color")]
public string TpButtonColor = "0.20 0.55 0.80 1.0";
[JsonProperty("Danger Button Color")]
public string DangerButtonColor = "0.91 0.30 0.24 1.0";
[JsonProperty("Text Color")]
public string TextColor = "1 1 1 1";
[JsonProperty("Muted Text Color")]
public string MutedTextColor = "0.60 0.60 0.60 1.0";
}
protected override void LoadDefaultConfig()
{
_config = new Configuration();
PrintWarning("Creating new config file.");
}
private void LoadConfigValues()
{
try
{
_config = Config.ReadObject<Configuration>();
if (_config == null) throw new JsonException();
}
catch
{
PrintWarning("Config is invalid, generating a new one.");
LoadDefaultConfig();
}
SaveConfig();
}
protected override void SaveConfig() => Config.WriteObject(_config, true);
#endregion
#region Models
private class ViolationInfo
{
public Vector3 Position;
public int Count;
public string OwnerName;
public List<string> Names = new List<string>();
}
#endregion
#region Hooks
private void Init()
{
permission.RegisterPermission(PermUse, this);
LoadConfigValues();
}
private void Unload()
{
foreach (var player in BasePlayer.activePlayerList)
DestroyUi(player);
_playerViolations.Clear();
_playerPages.Clear();
}
private void OnPlayerDisconnected(BasePlayer player, string reason)
{
DestroyUi(player);
_playerViolations.Remove(player.userID);
_playerPages.Remove(player.userID);
}
#endregion
#region Chat Commands
[ChatCommand("cupcheck")]
private void CmdCupCheck(BasePlayer player, string command, string[] args)
{
if (!HasAccess(player))
{
SendReply(player, "You do not have permission to use this command.");
return;
}
var violations = FindViolations();
_playerViolations[player.userID] = violations;
_playerPages[player.userID] = 0;
DrawUi(player);
if (violations.Count == 0)
{
SendReply(player, "<color=#76B900>[Cupboard Inspector]</color> No violations found.");
return;
}
DrawWorldMarkers(player, violations);
SendReply(player,
$"<color=#76B900>[Cupboard Inspector]</color> Found " +
$"<color=#FFB800>{violations.Count}</color> violation(s). " +
$"Markers drawn for {_config.MarkerDuration:0} sec.");
}
#endregion
#region Console Commands
[ConsoleCommand("cupinspect.close")]
private void CCmdClose(ConsoleSystem.Arg arg)
{
var player = arg.Player();
if (player == null) return;
DestroyUi(player);
_playerViolations.Remove(player.userID);
_playerPages.Remove(player.userID);
}
[ConsoleCommand("cupinspect.rescan")]
private void CCmdRescan(ConsoleSystem.Arg arg)
{
var player = arg.Player();
if (player == null || !HasAccess(player)) return;
var violations = FindViolations();
_playerViolations[player.userID] = violations;
_playerPages[player.userID] = 0;
DrawUi(player);
DrawWorldMarkers(player, violations);
}
[ConsoleCommand("cupinspect.page")]
private void CCmdPage(ConsoleSystem.Arg arg)
{
var player = arg.Player();
if (player == null || !HasAccess(player)) return;
var pageStr = arg.GetString(0, "0");
if (!int.TryParse(pageStr, out var page)) return;
_playerPages[player.userID] = page;
DrawUi(player);
}
[ConsoleCommand("cupinspect.tp")]
private void CCmdTeleport(ConsoleSystem.Arg arg)
{
var player = arg.Player();
if (player == null || !HasAccess(player)) return;
var indexStr = arg.GetString(0, "-1");
if (!int.TryParse(indexStr, out var index)) return;
if (!_playerViolations.TryGetValue(player.userID, out var violations))
return;
if (index < 0 || index >= violations.Count)
return;
var target = violations[index].Position + new Vector3(0f, 2f, 0f);
player.Teleport(target);
SendReply(player,
$"<color=#76B900>[Cupboard Inspector]</color> Teleported to violation " +
$"<color=#FFB800>#{index + 1}</color> " +
$"(Owner: <color=#76B900>{violations[index].OwnerName}</color>)");
}
#endregion
#region Core Logic
private bool HasAccess(BasePlayer player)
{
if (player == null) return false;
return player.IsAdmin ||
permission.UserHasPermission(player.UserIDString, PermUse);
}
private List<ViolationInfo> FindViolations()
{
var result = new List<ViolationInfo>();
foreach (var entity in BaseNetworkable.serverEntities)
{
var cp = entity as BuildingPrivlidge;
if (cp == null || cp.IsDestroyed)
continue;
var authList = cp.authorizedPlayers;
if (authList == null || authList.Count <= _config.MaxAuthorized)
continue;
var info = new ViolationInfo
{
Position = cp.transform.position,
Count = authList.Count,
OwnerName = "Unknown",
Names = new List<string>()
};
foreach (var steamId in authList)
{
var name = GetPlayerName(steamId);
info.Names.Add(name);
}
if (info.Names.Count > 0)
info.OwnerName = info.Names[0];
result.Add(info);
}
return result
.OrderByDescending(v => v.Count)
.ThenBy(v => v.Position.x)
.ToList();
}
private string GetPlayerName(ulong steamId)
{
var online = BasePlayer.activePlayerList
.FirstOrDefault(p => p.userID == steamId);
if (online != null) return online.displayName;
var sleeping = BasePlayer.sleepingPlayerList
.FirstOrDefault(p => p.userID == steamId);
if (sleeping != null) return sleeping.displayName;
return steamId.ToString();
}
#endregion
#region World Markers
private void DrawWorldMarkers(BasePlayer admin, List<ViolationInfo> violations)
{
if (admin == null || violations == null || violations.Count == 0)
return;
foreach (var v in violations)
{
var target = v.Position + new Vector3(0f, _config.MarkerHeight, 0f);
if (_config.DrawArrows)
{
admin.SendConsoleCommand(
"ddraw.line",
_config.MarkerDuration,
Color.green,
target,
target + new Vector3(0f, _config.MarkerLineHeight, 0f));
admin.SendConsoleCommand(
"ddraw.arrow",
_config.MarkerDuration,
Color.yellow,
target + new Vector3(0f, 3f, 0f),
target,
0.25f);
}
if (_config.DrawText)
{
var authNames = string.Join(", ", v.Names);
var markerText =
$"<size=10>" +
$"VIOLATION ({v.Count})\n" +
$"Owner: {v.OwnerName}\n" +
$"Auth: {authNames}\n" +
$"Pos: {v.Position.x:0}, {v.Position.y:0}, {v.Position.z:0}" +
$"</size>";
admin.SendConsoleCommand(
"ddraw.text",
_config.MarkerDuration,
Color.green,
target + new Vector3(0f, 0.5f, 0f),
markerText);
}
admin.SendConsoleCommand(
"ddraw.sphere",
_config.MarkerDuration,
Color.red,
target,
0.35f);
}
}
#endregion
#region UI
private void DrawUi(BasePlayer player)
{
DestroyUi(player);
if (!_playerViolations.TryGetValue(player.userID, out var violations))
violations = new List<ViolationInfo>();
if (!_playerPages.TryGetValue(player.userID, out var page))
page = 0;
var totalPages = Math.Max(1,
(int)Math.Ceiling((double)violations.Count / ItemsPerPage));
if (page >= totalPages) page = totalPages - 1;
if (page < 0) page = 0;
var pageItems = violations
.Skip(page * ItemsPerPage)
.Take(ItemsPerPage)
.ToList();
var container = new CuiElementContainer();
container.Add(new CuiPanel
{
Image = { Color = _config.PanelColor },
RectTransform =
{
AnchorMin = _config.UiAnchorMin,
AnchorMax = _config.UiAnchorMax
},
CursorEnabled = true
}, "Overlay", UiLayer);
container.Add(new CuiPanel
{
Image = { Color = _config.HeaderColor },
RectTransform =
{
AnchorMin = "0 0.90",
AnchorMax = "1 1"
}
}, UiLayer, UiLayer + ".Header");
container.Add(new CuiLabel
{
Text =
{
Text = "CUPBOARD INSPECTOR",
FontSize = 14,
Align = TextAnchor.MiddleCenter,
Color = _config.TextColor
},
RectTransform =
{
AnchorMin = "0 0",
AnchorMax = "0.85 1"
}
}, UiLayer + ".Header");
container.Add(new CuiButton
{
Button =
{
Color = "0.91 0.30 0.24 1.0",
Command = "cupinspect.close"
},
RectTransform =
{
AnchorMin = "0.90 0.15",
AnchorMax = "0.99 0.85"
},
Text =
{
Text = "✕",
FontSize = 14,
Align = TextAnchor.MiddleCenter,
Color = _config.TextColor
}
}, UiLayer + ".Header");
var summaryText = violations.Count == 0
? "No violations found. Authorization limits are clean."
: $"Found {violations.Count} violation(s) | Page {page + 1} / {totalPages}";
var summaryColor = violations.Count == 0
? "0.46 0.73 0 1"
: "1.0 0.72 0 1";
container.Add(new CuiLabel
{
Text =
{
Text = summaryText,
FontSize = 11,
Align = TextAnchor.MiddleCenter,
Color = summaryColor
},
RectTransform =
{
AnchorMin = "0.05 0.82",
AnchorMax = "0.95 0.89"
}
}, UiLayer);
var yTop = 0.80f;
var rowH = 0.145f;
var gap = 0.01f;
for (int i = 0; i < pageItems.Count; i++)
{
var item = pageItems[i];
var globalIndex = page * ItemsPerPage + i;
var rowTop = yTop - (i * (rowH + gap));
var rowBot = rowTop - rowH;
var rowColor = i % 2 == 0
? _config.RowColor
: _config.RowAltColor;
var rowId = UiLayer + $".Row{i}";
container.Add(new CuiPanel
{
Image = { Color = rowColor },
RectTransform =
{
AnchorMin = $"0.03 {rowBot}",
AnchorMax = $"0.97 {rowTop}"
}
}, UiLayer, rowId);
container.Add(new CuiLabel
{
Text =
{
Text = $"#{globalIndex + 1}",
FontSize = 14,
Align = TextAnchor.MiddleCenter,
Color = _config.HeaderColor
},
RectTransform =
{
AnchorMin = "0.0 0.0",
AnchorMax = "0.06 1.0"
}
}, rowId);
var authNames = string.Join(", ", item.Names.Take(4));
if (item.Names.Count > 4)
authNames += $" (+{item.Names.Count - 4})";
var infoText =
$"Auth: <color=#FFB800>{item.Count}</color> | " +
$"Owner: <color=#76B900>{item.OwnerName}</color>\n" +
$"Players: {authNames}\n" +
$"Pos: {item.Position.x:0}, {item.Position.y:0}, {item.Position.z:0}";
container.Add(new CuiLabel
{
Text =
{
Text = infoText,
FontSize = 10,
Align = TextAnchor.MiddleLeft,
Color = _config.TextColor
},
RectTransform =
{
AnchorMin = "0.07 0.05",
AnchorMax = "0.82 0.95"
}
}, rowId);
container.Add(new CuiButton
{
Button =
{
Color = _config.TpButtonColor,
Command = $"cupinspect.tp {globalIndex}"
},
RectTransform =
{
AnchorMin = "0.84 0.20",
AnchorMax = "0.98 0.80"
},
Text =
{
Text = "TP",
FontSize = 12,
Align = TextAnchor.MiddleCenter,
Color = _config.TextColor
}
}, rowId);
}
if (violations.Count == 0)
{
container.Add(new CuiLabel
{
Text =
{
Text = "All clear! No authorization violations detected.",
FontSize = 13,
Align = TextAnchor.MiddleCenter,
Color = _config.TextColor
},
RectTransform =
{
AnchorMin = "0.10 0.40",
AnchorMax = "0.90 0.60"
}
}, UiLayer);
}
container.Add(new CuiButton
{
Button =
{
Color = _config.ButtonColor,
Command = "cupinspect.rescan"
},
RectTransform =
{
AnchorMin = "0.03 0.02",
AnchorMax = "0.20 0.10"
},
Text =
{
Text = "RESCAN",
FontSize = 11,
Align = TextAnchor.MiddleCenter,
Color = _config.TextColor
}
}, UiLayer);
if (page > 0)
{
container.Add(new CuiButton
{
Button =
{
Color = _config.ButtonColor,
Command = $"cupinspect.page {page - 1}"
},
RectTransform =
{
AnchorMin = "0.35 0.02",
AnchorMax = "0.48 0.10"
},
Text =
{
Text = "◀ PREV",
FontSize = 11,
Align = TextAnchor.MiddleCenter,
Color = _config.TextColor
}
}, UiLayer);
}
container.Add(new CuiLabel
{
Text =
{
Text = $"{page + 1} / {totalPages}",
FontSize = 11,
Align = TextAnchor.MiddleCenter,
Color = _config.MutedTextColor
},
RectTransform =
{
AnchorMin = "0.48 0.02",
AnchorMax = "0.52 0.10"
}
}, UiLayer);
if (page < totalPages - 1)
{
container.Add(new CuiButton
{
Button =
{
Color = _config.ButtonColor,
Command = $"cupinspect.page {page + 1}"
},
RectTransform =
{
AnchorMin = "0.52 0.02",
AnchorMax = "0.65 0.10"
},
Text =
{
Text = "NEXT ▶",
FontSize = 11,
Align = TextAnchor.MiddleCenter,
Color = _config.TextColor
}
}, UiLayer);
}
container.Add(new CuiButton
{
Button =
{
Color = _config.DangerButtonColor,
Command = "cupinspect.close"
},
RectTransform =
{
AnchorMin = "0.80 0.02",
AnchorMax = "0.97 0.10"
},
Text =
{
Text = "CLOSE",
FontSize = 11,
Align = TextAnchor.MiddleCenter,
Color = _config.TextColor
}
}, UiLayer);
CuiHelper.AddUi(player, container);
}
private void DestroyUi(BasePlayer player)
{
CuiHelper.DestroyUi(player, UiLayer);
}
#endregion
}
}