C#:
// ============================================================================
// AdminButtons (c) Fullwiped - https://fullwiped.com
// ----------------------------------------------------------------------------
// Free to use and modify on your own servers.
// Redistribution, resale, or re-publishing this file (original or modified)
// is prohibited without written permission from Fullwiped.
// Support and updates are provided at Fullwiped's discretion.
// ============================================================================
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Cui;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEngine;
using UnityEngine.Networking;
namespace Oxide.Plugins
{
[Info("AdminButtons", "Fullwiped", "1.0.1")]
[Description("Admin quick-action HUD tray.")]
public class AdminButtons : RustPlugin
{
// perm: adminbuttons.admin
// cmd: /ab
// images: oxide/data/Fullwiped/Images/AdminButtons/<IconKey>.png (128x128 PNG)
#region Constants
private const string Perm = "adminbuttons.admin";
private const string PanelName = "AdminButtons.Root";
private const string DataSubdir = "Fullwiped/Images/AdminButtons";
private const int MaxButtons = 8;
// Base dimensions at 100% scale
private const float CellW = 44f;
private const float CellH = 48f;
private const float CellGap = 3f;
private const float PadX = 7f;
private const float PadY = 4f;
private const float CloseSize = 18f;
private const float CloseGap = 7f;
private const float ScreenMargin = 18f;
private const int LabelFontSize = 7;
private const int CloseFontSize = 13;
private const string ClickSfx = "assets/bundled/prefabs/fx/notice/item.select.fx.prefab";
// client-side console cmds
private static readonly HashSet<string> ClientOnlyCommands = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"noclip", "global.noclip",
"respawn", "global.respawn",
"respawn_sleepingbag", "global.respawn_sleepingbag",
"suicide", "global.suicide",
"hud", "global.hud",
"combatlog", "global.combatlog",
"debugcamera", "global.debugcamera",
"consoletoggle", "global.consoletoggle",
};
#endregion
#region Runtime state
private readonly HashSet<ulong> _closedThisSession = new HashSet<ulong>();
private readonly Dictionary<string, string> _iconCrc
= new Dictionary<string, string>();
private readonly Dictionary<ulong, Dictionary<int, Timer>> _flashTimers
= new Dictionary<ulong, Dictionary<int, Timer>>();
#endregion
#region Config
private Configuration _cfg;
private class ButtonConfig
{
[JsonProperty("Label")]
public string Label = "";
[JsonProperty("Icon")]
public string Icon = "";
[JsonProperty("Commands")]
public List<string> Commands = new List<string>();
}
private class Configuration
{
[JsonProperty("Chat command")]
public string ChatCommand = "ab";
[JsonProperty("Panel position (0 = BottomLeft, 1 = TopLeft, 2 = TopRight, 3 = BottomRight)")]
public int PanelPosition = 0;
[JsonProperty("UI Scale (%) — 100 = default, 75 = -25%, 50 = -50%, 150 = +50%")]
public int Scale = 100;
[JsonProperty("Auto-open on connect")]
public bool AutoOpenOnConnect = true;
[JsonProperty("Play click sound")]
public bool PlayClickSound = true;
[JsonProperty("Use local images (false = use Web URLs below)")]
public bool UseLocalImages = true;
[JsonProperty("Web image URLs (recommended 128x128 transparent PNG, used only when 'Use local images' = false)")]
public Dictionary<string, string> WebImages = new Dictionary<string, string>();
[JsonProperty("Buttons (left to right, max 8)")]
public List<ButtonConfig> Buttons = new List<ButtonConfig>();
}
protected override void LoadDefaultConfig()
{
_cfg = new Configuration
{
Buttons = new List<ButtonConfig>
{
new ButtonConfig { Label = "ADMIN", Icon = "ADMIN", Commands = new List<string> { "chat.say \"/admin\"" } },
new ButtonConfig { Label = "RADAR", Icon = "RADAR", Commands = new List<string> { "chat.say \"/radar\"" } },
new ButtonConfig { Label = "NOCLIP", Icon = "NOCLIP", Commands = new List<string> { "noclip" } },
new ButtonConfig { Label = "VANISH", Icon = "VANISH", Commands = new List<string> { "chat.say \"/vanish\"" } },
new ButtonConfig { Label = "OUTPOST", Icon = "OUTPOST", Commands = new List<string> { "outpost" } },
new ButtonConfig { Label = "BANDIT", Icon = "BANDIT", Commands = new List<string> { "bandit" } },
new ButtonConfig { Label = "TPMARKER", Icon = "TPMARKER", Commands = new List<string> { "teleport2marker" } },
new ButtonConfig { Label = "HEAL", Icon = "HEAL", Commands = new List<string> { "heal 100" } },
},
WebImages = new Dictionary<string, string>
{
["ADMIN"] = "https://i.ibb.co/TBBS73X2/ADMIN.png",
["RADAR"] = "https://i.ibb.co/213xSyb1/RADAR.png",
["NOCLIP"] = "https://i.ibb.co/Q7TvJZTP/NOCLIP.png",
["VANISH"] = "https://i.ibb.co/qFyV9P6g/VANISH.png",
["OUTPOST"] = "https://i.ibb.co/4gjrwZ25/OUTPOST.png",
["BANDIT"] = "https://i.ibb.co/84NZhjkp/BANDIT.png",
["TPMARKER"] = "https://i.ibb.co/F4JF9NP2/TPMARKER.png",
["HEAL"] = "https://i.ibb.co/vvwVb83C/HEAL.png",
["CLOSE"] = "https://i.ibb.co/ZRG7JtKr/CLOSE.png",
}
};
}
protected override void LoadConfig()
{
base.LoadConfig();
try { _cfg = Config.ReadObject<Configuration>(); }
catch { _cfg = null; }
if (_cfg == null) LoadDefaultConfig();
SaveConfig();
}
protected override void SaveConfig() => Config.WriteObject(_cfg);
// Returns scale factor clamped to [25, 400] so nobody breaks the UI accidentally.
private float ScaleFactor() => Mathf.Clamp(_cfg.Scale <= 0 ? 100 : _cfg.Scale, 25, 400) / 100f;
#endregion
#region Lifecycle
private void Init()
{
permission.RegisterPermission(Perm, this);
string chatCmd = string.IsNullOrWhiteSpace(_cfg.ChatCommand) ? "ab" : _cfg.ChatCommand;
cmd.AddChatCommand(chatCmd, this, nameof(CmdToggle));
}
private void OnServerInitialized()
{
LoadAllIcons();
if (!_cfg.AutoOpenOnConnect) return;
foreach (var player in BasePlayer.activePlayerList)
if (HasPerm(player)) DrawPanel(player);
}
private void Unload()
{
foreach (var player in BasePlayer.activePlayerList)
CuiHelper.DestroyUi(player, PanelName);
foreach (var inner in _flashTimers.Values)
foreach (var t in inner.Values) t?.Destroy();
_flashTimers.Clear();
}
private void OnPlayerConnected(BasePlayer player)
{
if (player == null || !HasPerm(player)) return;
if (!_cfg.AutoOpenOnConnect) return;
timer.Once(2f, () =>
{
if (player != null && player.IsConnected && !_closedThisSession.Contains(player.userID))
DrawPanel(player);
});
}
private void OnPlayerDisconnected(BasePlayer player, string reason)
{
if (player == null) return;
_closedThisSession.Remove(player.userID);
CancelAllFlashes(player.userID);
CuiHelper.DestroyUi(player, PanelName);
}
#endregion
#region Icon loading
private void LoadAllIcons()
{
_iconCrc.Clear();
var keys = new HashSet<string>();
foreach (var b in _cfg.Buttons.Take(MaxButtons))
if (!string.IsNullOrEmpty(b.Icon)) keys.Add(b.Icon);
keys.Add("CLOSE");
foreach (var key in keys)
{
if (_cfg.UseLocalImages) LoadLocal(key);
else LoadWeb(key);
}
}
private void LoadLocal(string key)
{
string path = Path.Combine(Interface.Oxide.DataDirectory, DataSubdir, key + ".png");
if (!File.Exists(path))
{
PrintWarning($"[AdminButtons] Missing local image: {path}");
return;
}
try { StoreImage(key, File.ReadAllBytes(path)); }
catch (Exception e) { PrintWarning($"[AdminButtons] Read failed {path}: {e.Message}"); }
}
private void LoadWeb(string key)
{
string url;
if (!_cfg.WebImages.TryGetValue(key, out url) || string.IsNullOrWhiteSpace(url))
{
PrintWarning($"[AdminButtons] Missing WebImages URL for icon '{key}'");
return;
}
ServerMgr.Instance.StartCoroutine(DownloadAndStore(key, url));
}
private System.Collections.IEnumerator DownloadAndStore(string key, string url)
{
using (var req = UnityWebRequest.Get(url))
{
req.downloadHandler = new DownloadHandlerBuffer();
yield return req.SendWebRequest();
if (req.isNetworkError || req.isHttpError)
{
PrintWarning($"[AdminButtons] Download failed {url}: {req.error}");
yield break;
}
StoreImage(key, req.downloadHandler.data);
}
}
private void StoreImage(string key, byte[] data)
{
try
{
uint crc = FileStorage.server.Store(data, FileStorage.Type.png, CommunityEntity.ServerInstance.net.ID);
_iconCrc[key] = crc.ToString();
}
catch (Exception e) { PrintWarning($"[AdminButtons] FileStorage error {key}: {e.Message}"); }
}
#endregion
#region UI
private float ComputePanelWidth(float s)
{
int count = Math.Min(_cfg.Buttons.Count, MaxButtons);
if (count <= 0) count = 1;
float gridW = count * CellW * s + (count - 1) * CellGap * s;
return PadX * s + gridW + CloseGap * s + CloseSize * s + PadX * s;
}
private float PanelHeight(float s) => (PadY + CellH + PadY) * s;
private void DrawPanel(BasePlayer player)
{
CuiHelper.DestroyUi(player, PanelName);
CancelAllFlashes(player.userID);
float s = ScaleFactor();
float panelW = ComputePanelWidth(s);
float panelH = PanelHeight(s);
var e = new CuiElementContainer();
string aMin, aMax, oMin, oMax;
GetPanelAnchors(_cfg.PanelPosition, panelW, panelH, s, out aMin, out aMax, out oMin, out oMax);
e.Add(new CuiPanel
{
Image = { Color = "0.05 0.06 0.08 0.78", Material = "assets/content/ui/uibackgroundblur.mat" },
RectTransform = { AnchorMin = aMin, AnchorMax = aMax, OffsetMin = oMin, OffsetMax = oMax },
CursorEnabled = false,
}, "Overlay", PanelName);
// top accent
e.Add(new CuiPanel
{
Image = { Color = "1 0.78 0.28 0.7" },
RectTransform = { AnchorMin = "0 1", AnchorMax = "1 1", OffsetMin = "0 -1", OffsetMax = "0 0" }
}, PanelName);
// bottom line
e.Add(new CuiPanel
{
Image = { Color = "0 0 0 0.5" },
RectTransform = { AnchorMin = "0 0", AnchorMax = "1 0", OffsetMin = "0 0", OffsetMax = "0 1" }
}, PanelName);
var btns = _cfg.Buttons.Take(MaxButtons).ToList();
for (int i = 0; i < btns.Count; i++)
{
float x0 = PadX * s + i * (CellW + CellGap) * s;
float x1 = x0 + CellW * s;
float y0 = PadY * s;
float y1 = y0 + CellH * s;
DrawCell(e, btns[i], i, player, x0, y0, x1, y1, flashOverride: false, s: s);
}
float closeX1 = panelW - PadX * s;
float closeX0 = closeX1 - CloseSize * s;
float closeY0 = (panelH - CloseSize * s) / 2f;
float closeY1 = closeY0 + CloseSize * s;
DrawCloseButton(e, closeX0, closeY0, closeX1, closeY1, s);
CuiHelper.AddUi(player, e);
}
private void DrawCloseButton(CuiElementContainer e, float x0, float y0, float x1, float y1, float s)
{
string name = PanelName + ".Close";
e.Add(new CuiPanel
{
Image = { Color = "0 0 0 0.0" },
RectTransform = {
AnchorMin = "0 0", AnchorMax = "0 0",
OffsetMin = $"{x0} {y0}", OffsetMax = $"{x1} {y1}"
}
}, PanelName, name);
string crc;
if (_iconCrc.TryGetValue("CLOSE", out crc) && !string.IsNullOrEmpty(crc))
{
e.Add(new CuiElement
{
Parent = name,
Components =
{
new CuiRawImageComponent { Png = crc, Color = "1 0.78 0.28 0.95" },
new CuiRectTransformComponent { AnchorMin = "0.18 0.18", AnchorMax = "0.82 0.82" }
}
});
}
else
{
e.Add(new CuiLabel
{
Text = { Text = "X", FontSize = Mathf.Max(1, Mathf.RoundToInt(CloseFontSize * s)), Align = TextAnchor.MiddleCenter, Color = "1 0.78 0.28 1", Font = "robotocondensed-bold.ttf" },
RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" }
}, name);
}
e.Add(new CuiButton
{
Button = { Color = "1 1 1 0.01", Command = "adminbuttons.close" },
RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" },
Text = { Text = "" }
}, name, name + ".Hit");
}
private void DrawCell(CuiElementContainer e, ButtonConfig btn, int idx, BasePlayer player,
float x0, float y0, float x1, float y1, bool flashOverride, float s)
{
string cellName = $"{PanelName}.Btn{idx}";
string bgColor = flashOverride ? "1 0.78 0.28 0.55" : "0.085 0.10 0.13 0.78";
e.Add(new CuiPanel
{
Image = { Color = bgColor },
RectTransform = {
AnchorMin = "0 0", AnchorMax = "0 0",
OffsetMin = $"{x0} {y0}", OffsetMax = $"{x1} {y1}"
}
}, PanelName, cellName);
e.Add(new CuiPanel
{
Image = { Color = flashOverride ? "1 0.78 0.28 0.95" : "0.22 0.26 0.32 0.45" },
RectTransform = { AnchorMin = "0 1", AnchorMax = "1 1", OffsetMin = "0 -1", OffsetMax = "0 0" }
}, cellName);
string crc;
if (!string.IsNullOrEmpty(btn.Icon) && _iconCrc.TryGetValue(btn.Icon, out crc) && !string.IsNullOrEmpty(crc))
{
string iconColor = flashOverride ? "1 1 1 1" : "0.92 0.92 0.92 0.92";
bool hasLabel = !string.IsNullOrEmpty(btn.Label);
string iconAnchorMin = hasLabel ? "0.12 0.32" : "0.08 0.08";
string iconAnchorMax = hasLabel ? "0.88 0.96" : "0.92 0.92";
e.Add(new CuiElement
{
Parent = cellName,
Components =
{
new CuiRawImageComponent { Png = crc, Color = iconColor },
new CuiRectTransformComponent { AnchorMin = iconAnchorMin, AnchorMax = iconAnchorMax }
}
});
}
if (!string.IsNullOrEmpty(btn.Label))
{
string labelColor = flashOverride ? "1 1 1 1" : "0.78 0.82 0.86 0.92";
e.Add(new CuiLabel
{
Text = {
Text = btn.Label.ToUpperInvariant(),
FontSize = Mathf.Max(1, Mathf.RoundToInt(LabelFontSize * s)),
Align = TextAnchor.MiddleCenter,
Font = "robotocondensed-bold.ttf",
Color = labelColor
},
RectTransform = { AnchorMin = "0 0.04", AnchorMax = "1 0.30" }
}, cellName);
}
e.Add(new CuiButton
{
Button = { Color = "1 1 1 0.01", Command = $"adminbuttons.click {idx}" },
RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" },
Text = { Text = "" }
}, cellName, cellName + ".Hit");
}
private void RedrawCell(BasePlayer player, int idx, bool flash)
{
if (player == null || !player.IsConnected) return;
if (idx < 0 || idx >= _cfg.Buttons.Count || idx >= MaxButtons) return;
string cellName = $"{PanelName}.Btn{idx}";
CuiHelper.DestroyUi(player, cellName);
float s = ScaleFactor();
float x0 = PadX * s + idx * (CellW + CellGap) * s;
float x1 = x0 + CellW * s;
float y0 = PadY * s;
float y1 = y0 + CellH * s;
var e = new CuiElementContainer();
DrawCell(e, _cfg.Buttons[idx], idx, player, x0, y0, x1, y1, flashOverride: flash, s: s);
CuiHelper.AddUi(player, e);
}
private void GetPanelAnchors(int pos, float w, float h, float s, out string aMin, out string aMax, out string oMin, out string oMax)
{
float m = ScreenMargin * s;
switch (pos)
{
case 1: aMin = "0 1"; aMax = "0 1"; oMin = $"{m} {-h - m}"; oMax = $"{m + w} {-m}"; break;
case 2: aMin = "1 1"; aMax = "1 1"; oMin = $"{-w - m} {-h - m}"; oMax = $"{-m} {-m}"; break;
case 3: aMin = "1 0"; aMax = "1 0"; oMin = $"{-w - m} {m}"; oMax = $"{-m} {m + h}"; break;
default:aMin = "0 0"; aMax = "0 0"; oMin = $"{m} {m}"; oMax = $"{m + w} {m + h}"; break;
}
}
#endregion
#region Click feedback
private void PlayClick(BasePlayer player)
{
if (!_cfg.PlayClickSound) return;
try
{
var effect = new Effect(ClickSfx, player.transform.position, Vector3.zero);
EffectNetwork.Send(effect, player.net.connection);
}
catch (Exception e) { PrintWarning($"[AdminButtons] Sound error: {e.Message}"); }
}
private void FlashCell(BasePlayer player, int idx)
{
if (player == null) return;
Dictionary<int, Timer> inner;
if (_flashTimers.TryGetValue(player.userID, out inner))
{
Timer existing;
if (inner.TryGetValue(idx, out existing)) { existing?.Destroy(); inner.Remove(idx); }
}
RedrawCell(player, idx, flash: true);
var t = timer.Once(0.22f, () =>
{
Dictionary<int, Timer> map2;
if (_flashTimers.TryGetValue(player.userID, out map2)) map2.Remove(idx);
if (player != null && player.IsConnected && !_closedThisSession.Contains(player.userID))
RedrawCell(player, idx, flash: false);
});
if (!_flashTimers.TryGetValue(player.userID, out inner))
{
inner = new Dictionary<int, Timer>();
_flashTimers[player.userID] = inner;
}
inner[idx] = t;
}
private void CancelAllFlashes(ulong userId)
{
Dictionary<int, Timer> inner;
if (!_flashTimers.TryGetValue(userId, out inner)) return;
foreach (var t in inner.Values) t?.Destroy();
_flashTimers.Remove(userId);
}
#endregion
#region Commands
private void CmdToggle(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (!HasPerm(player))
{
player.ChatMessage("You don't have permission.");
return;
}
if (_closedThisSession.Contains(player.userID))
{
_closedThisSession.Remove(player.userID);
DrawPanel(player);
}
else
{
CuiHelper.DestroyUi(player, PanelName);
_closedThisSession.Add(player.userID);
}
}
[ConsoleCommand("adminbuttons.close")]
private void CmdClose(ConsoleSystem.Arg arg)
{
var player = arg.Player();
if (player == null || !HasPerm(player)) return;
CuiHelper.DestroyUi(player, PanelName);
CancelAllFlashes(player.userID);
_closedThisSession.Add(player.userID);
}
[ConsoleCommand("adminbuttons.click")]
private void CmdClick(ConsoleSystem.Arg arg)
{
var player = arg.Player();
if (player == null) return;
if (!HasPerm(player))
{
player.ChatMessage("You don't have permission.");
return;
}
if (!arg.HasArgs(1)) return;
int idx;
if (!int.TryParse(arg.Args[0], out idx)) return;
if (idx < 0 || idx >= _cfg.Buttons.Count || idx >= MaxButtons) return;
var btn = _cfg.Buttons[idx];
PlayClick(player);
FlashCell(player, idx);
if (btn.Commands == null) return;
foreach (var raw in btn.Commands)
{
if (string.IsNullOrWhiteSpace(raw)) continue;
string cmd = raw
.Replace("{steamid}", player.UserIDString)
.Replace("{name}", player.displayName ?? "");
// kill via API (console can be no-op under god/protection)
string trimmed = cmd.Trim().ToLowerInvariant();
if (trimmed == "kill" || trimmed == "global.kill")
{
try { player.Die(); }
catch (Exception e) { PrintWarning($"[AdminButtons] Die() error: {e.Message}"); }
continue;
}
try
{
string firstToken = cmd.TrimStart().Split(new[] { ' ', '\t' }, 2)[0];
if (ClientOnlyCommands.Contains(firstToken))
player.SendConsoleCommand(cmd);
else
ConsoleSystem.Run(
ConsoleSystem.Option.Server.FromConnection(player.net.connection),
cmd);
}
catch (Exception ex)
{
PrintWarning($"[AdminButtons] Run('{cmd}') failed: {ex.Message}");
}
}
}
[ConsoleCommand("adminbuttons.reload")]
private void CmdReloadImages(ConsoleSystem.Arg arg)
{
var player = arg.Player();
if (player != null && !HasPerm(player)) return;
LoadAllIcons();
foreach (var p in BasePlayer.activePlayerList)
if (HasPerm(p) && !_closedThisSession.Contains(p.userID))
DrawPanel(p);
PrintWarning("[AdminButtons] Icons reloaded.");
}
#endregion
#region Helpers
private bool HasPerm(BasePlayer player)
=> player != null && permission.UserHasPermission(player.UserIDString, Perm);
#endregion
}
}