Skip to content
RustPluginGenerator

How to make a Rust admin menu plugin (CUI panel)

Builds a CUI admin panel that opens with /am and renders one button per action defined in the config, each button running a console command through a plugin console command. No gameplay hooks are required.

Admins on a busy server end up typing F1 console commands just to switch time of day or clear the sky. A single permission-gated CUI panel keeps those actions one click away and lets you change which actions exist without recompiling anything.

This plugin registers an adminmenu.use permission, reads a list of actions from its config (label, console command, optional confirm flag), and draws one button per action plus a close button. Clicking a button calls the plugin console command adminmenu.run <index>, which is dispatched on behalf of the admin. Because the console command requires arg.Player() and rechecks the permission, a modified client cannot queue those commands from the server console or bypass the check. Actions marked Confirm in the config require a second click on the same button within ten seconds before the command runs.

The plugin code

AdminMenu.cs
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Cui;
using UnityEngine;

namespace Oxide.Plugins
{
    [Info("Admin Menu", "RustPluginGenerator", "1.0.0")]
    [Description("A permission-gated CUI admin panel whose buttons run config-defined console commands.")]
    public class AdminMenu : RustPlugin
    {
        private const string PermUse = "adminmenu.use";
        private const string PanelName = "AdminMenu.Panel";
        private const float ConfirmWindow = 10f;

        private PluginConfig config;
        private readonly Dictionary<ulong, Pending> pending = new Dictionary<ulong, Pending>();

        private class MenuAction
        {
            [JsonProperty("Label")]
            public string Label { get; set; } = "";

            [JsonProperty("Command")]
            public string Command { get; set; } = "";

            [JsonProperty("Confirm")]
            public bool Confirm { get; set; }
        }

        private class PluginConfig
        {
            [JsonProperty("Panel Title")]
            public string Title { get; set; } = "ADMIN MENU";

            [JsonProperty("Actions")]
            public List<MenuAction> Actions { get; set; } = new List<MenuAction>
            {
                new MenuAction { Label = "Set Time: Day", Command = "env.time 12", Confirm = false },
                new MenuAction { Label = "Set Time: Night", Command = "env.time 0", Confirm = false },
                new MenuAction { Label = "Kill All Patrol Helicopters", Command = "killall patrolhelicopter", Confirm = true }
            };
        }

        private class Pending
        {
            public int Index;
            public float Time;
        }

        protected override void LoadDefaultConfig() => config = new PluginConfig();

        protected override void LoadConfig()
        {
            base.LoadConfig();
            try
            {
                config = Config.ReadObject<PluginConfig>();
                if (config == null) throw new Exception();
                if (config.Actions == null) config.Actions = new List<MenuAction>();
            }
            catch
            {
                PrintWarning("Configuration file is invalid, using defaults");
                LoadDefaultConfig();
            }
            SaveConfig();
        }

        protected override void SaveConfig() => Config.WriteObject(config);

        protected override void LoadDefaultMessages()
        {
            lang.RegisterMessages(new Dictionary<string, string>
            {
                ["NoPermission"] = "You do not have permission to use the admin menu.",
                ["NoAction"] = "That action does not exist.",
                ["NoCommand"] = "This action has no command configured.",
                ["ConfirmPrompt"] = "Click the same button again within {0} seconds to confirm: {1}",
                ["Ran"] = "Executed: {0}"
            }, this);
        }

        private string Msg(string key, string userId = null, params object[] args) =>
            string.Format(lang.GetMessage(key, this, userId), args);

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

        private void Unload()
        {
            pending.Clear();
            foreach (var player in BasePlayer.activePlayerList)
            {
                if (player == null) continue;
                CuiHelper.DestroyUi(player, PanelName);
            }
        }

        [ChatCommand("am")]
        private void CmdAm(BasePlayer player, string command, string[] args)
        {
            if (player == null) return;
            if (!permission.UserHasPermission(player.UserIDString, PermUse))
            {
                SendReply(player, Msg("NoPermission", player.UserIDString));
                return;
            }
            DrawMenu(player);
        }

        [ConsoleCommand("adminmenu.close")]
        private void CcmdClose(ConsoleSystem.Arg arg)
        {
            var player = arg.Player();
            if (player == null) return;
            if (!permission.UserHasPermission(player.UserIDString, PermUse)) return;
            pending.Remove(player.userID.Get());
            CuiHelper.DestroyUi(player, PanelName);
        }

        [ConsoleCommand("adminmenu.run")]
        private void CcmdRun(ConsoleSystem.Arg arg)
        {
            var player = arg.Player();
            if (player == null) return;
            if (!permission.UserHasPermission(player.UserIDString, PermUse))
            {
                SendReply(player, Msg("NoPermission", player.UserIDString));
                return;
            }

            int index = arg.GetInt(0, -1);
            if (index < 0 || index >= config.Actions.Count)
            {
                SendReply(player, Msg("NoAction", player.UserIDString));
                return;
            }

            var action = config.Actions[index];
            if (action == null || string.IsNullOrEmpty(action.Command))
            {
                SendReply(player, Msg("NoCommand", player.UserIDString));
                return;
            }

            ulong id = player.userID.Get();

            if (action.Confirm)
            {
                float now = Time.realtimeSinceStartup;
                Pending existing;
                if (pending.TryGetValue(id, out existing) && existing.Index == index && now - existing.Time <= ConfirmWindow)
                {
                    pending.Remove(id);
                    Execute(player, action);
                    return;
                }

                pending[id] = new Pending { Index = index, Time = now };
                SendReply(player, Msg("ConfirmPrompt", player.UserIDString, (int)ConfirmWindow, action.Label));
                return;
            }

            Execute(player, action);
        }

        private void Execute(BasePlayer player, MenuAction action)
        {
            pending.Remove(player.userID.Get());
            Server.Command(action.Command);
            SendReply(player, Msg("Ran", player.UserIDString, action.Label));
            CuiHelper.DestroyUi(player, PanelName);
        }

        private void DrawMenu(BasePlayer player)
        {
            CuiHelper.DestroyUi(player, PanelName);

            var container = new CuiElementContainer();
            const float width = 380f;
            const float rowHeight = 32f;
            const float padding = 10f;
            int count = Mathf.Max(config.Actions.Count, 1);
            float height = 44f + count * (rowHeight + padding) + padding + rowHeight + padding;

            string anchorMin = "0.5 " + (0.5f - height / 1080f * 0.5f);
            string anchorMax = "0.5 " + (0.5f + height / 1080f * 0.5f);

            container.Add(new CuiPanel
            {
                Image = { Color = "0.08 0.08 0.08 0.95" },
                RectTransform = { AnchorMin = anchorMin, AnchorMax = anchorMax, OffsetMin = (-width / 2) + " 0", OffsetMax = (width / 2) + " 0" },
                CursorEnabled = true
            }, "Overlay", PanelName);

            container.Add(new CuiLabel
            {
                Text = { Text = config.Title ?? "ADMIN MENU", FontSize = 18, Align = TextAnchor.MiddleCenter, Color = "1 1 1 1" },
                RectTransform = { AnchorMin = "0 1", AnchorMax = "1 1", OffsetMin = "0 -44", OffsetMax = "0 -8" }
            }, PanelName);

            float top = -44f - padding;
            for (int i = 0; i < config.Actions.Count; i++)
            {
                var action = config.Actions[i];
                string label = action != null ? action.Label : "";
                container.Add(new CuiButton
                {
                    Button = { Command = "adminmenu.run " + i, Color = "0.25 0.35 0.55 0.95" },
                    RectTransform = { AnchorMin = "0 1", AnchorMax = "1 1", OffsetMin = padding + " " + (top - rowHeight), OffsetMax = (-padding) + " " + top },
                    Text = { Text = label, FontSize = 14, Align = TextAnchor.MiddleCenter, Color = "1 1 1 1" }
                }, PanelName);
                top -= rowHeight + padding;
            }

            container.Add(new CuiButton
            {
                Button = { Command = "adminmenu.close", Color = "0.55 0.2 0.2 0.95" },
                RectTransform = { AnchorMin = "0 0", AnchorMax = "1 0", OffsetMin = padding + " " + padding, OffsetMax = (-padding) + " " + (padding + rowHeight) },
                Text = { Text = "CLOSE", FontSize = 14, Align = TextAnchor.MiddleCenter, Color = "1 1 1 1" }
            }, PanelName);

            CuiHelper.AddUi(player, container);
        }
    }
}

Config file

Created automatically on first load at oxide/config/AdminMenu.json.

AdminMenu.json
{
  "Panel Title": "ADMIN MENU",
  "Actions": [
    {
      "Label": "Set Time: Day",
      "Command": "env.time 12",
      "Confirm": false
    },
    {
      "Label": "Set Time: Night",
      "Command": "env.time 0",
      "Confirm": false
    },
    {
      "Label": "Kill All Patrol Helicopters",
      "Command": "killall patrolhelicopter",
      "Confirm": true
    }
  ]
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/amOpens the admin panel.adminmenu.use
adminmenu.run <index>Runs the configured action at the given index for the calling player. Called by the CUI buttons.Everyone
adminmenu.closeCloses the panel for the calling player. Called by the close button.Everyone
Permissions
PermissionGrants
adminmenu.useGrants access to /am and the action buttons.

How the code works

  1. Config drives the buttons

    The nested PluginConfig class holds a Panel Title string and a List<MenuAction> named Actions. Each MenuAction has Label, Command, and Confirm. On load, LoadDefaultConfig fills in three actions: set time to day (env.time 12), set time to night (env.time 0), and kill all Patrol Helicopters via killall patrolhelicopter. Editing oxide/config/AdminMenu.json changes the panel, so new actions do not need code changes. LoadConfig falls back to defaults if the file cannot be parsed.

  2. Rendering the panel

    DrawMenu(BasePlayer player) builds a CuiElementContainer, adds a centred CuiPanel, then loops over config.Actions. Each button's Command is adminmenu.run <i>, which Oxide dispatches to the console command below. CuiHelper.DestroyUi(player, PanelName) runs before CuiHelper.AddUi so reopening the panel does not stack duplicate elements. The label uses TextAnchor.MiddleCenter and a fixed row height constant so the panel size matches the action count.

  3. Running the action

    The [ConsoleCommand("adminmenu.run")] method reads arg.Player(); any invocation from the server console or RCON returns without executing, so panel actions cannot be queued from outside. It rechecks adminmenu.use, reads the index via arg.GetInt(0, -1), and validates it against config.Actions.Count. If the entry has Confirm set, it stores the player's Steam ID and a timestamp in pending and sends a prompt instead of running the command.

  4. Confirmations without a modal

    When a Confirm action is clicked, the plugin saves Time.realtimeSinceStartup under the player's Steam ID in the pending dictionary and calls SendReply with the ConfirmPrompt message. A second click on the same index within ten seconds (ConfirmWindow) passes the check and calls Execute. Clicking any other action replaces the stored entry, so a slow second click cannot confirm the wrong action.

  5. Executing on the server

    Execute(BasePlayer player, MenuAction action) clears the player's pending entry, calls Server.Command(action.Command), sends the Ran message via SendReply, and destroys the panel with CuiHelper.DestroyUi. The command runs with no player context, exactly like typing it in the F1 console. CuiHelper.DestroyUi is also called by the adminmenu.close console command wired to the CLOSE button.

  6. Cleanup

    Unload iterates BasePlayer.activePlayerList and calls CuiHelper.DestroyUi(player, PanelName) for each, so a plugin reload does not leave orphan panels on screen. The pending dictionary is cleared on unload, and nothing is written to oxide/data. The config is saved on load, so any admin editing Actions sees changes after /reload AdminMenu.

Install it on your server

  1. Save AdminMenu.cs

    Keep the file name exactly as the class name. Oxide matches them.
  2. Upload it to the plugins folder

    Oxide: oxide/plugins/. Carbon: carbon/plugins/. The server compiles and loads it within a few seconds, no restart needed.
  3. Edit the config if you want

    On first load the default config is written to oxide/config/AdminMenu.json (Carbon: carbon/configs/AdminMenu.json). After editing, reload it:oxide.reload AdminMenu c.reload AdminMenu # Carbon
  4. Grant the permission

    Give it to every player, or swap default for a group such as vip:oxide.grant group default adminmenu.use c.grant group default adminmenu.use # Carbon

Variations

Each one opens the generator with a ready prompt you can tweak.

FAQ

Why does clicking a button do nothing?

Check the server console for a permission error. The player needs adminmenu.use, and the console command requires a player context, so a call arriving without one is refused by design. Also confirm the button's index exists in Actions.

Can I add my own actions?

Yes. Add an object to Actions in oxide/config/AdminMenu.json with Label, Command, and Confirm. The command is passed to Server.Command, so use the same text you would type in F1. Reload the plugin so the CUI reads the updated list.

Does this plugin work on Carbon?

Yes. Carbon loads Oxide C# plugins and handles the same CuiHelper, [ChatCommand], [ConsoleCommand], permission, lang, and Server.Command APIs. Drop the same .cs file into carbon/plugins and reload it from the Carbon console.

How do I install it?

Drop AdminMenu.cs into oxide/plugins (or carbon/plugins), wait for the file to compile, then grant the permission with oxide.grant user <steamid> adminmenu.use. /reload AdminMenu re-reads the config after you edit Actions.

Why does the panel close after each action?

Actions execute on the server with no player context, and Rust often closes CUI after a server-side state change. The plugin deliberately closes the panel after running an action so the player is not stuck behind a stale overlay.

Customize it for your server

Change the reward, add a cooldown, restrict it to VIPs. Start from this prompt and edit anything.

Prompt: Generate a Rust Oxide plugin called AdminMenu with a configurable CUI panel opened by /am behind the adminmenu.use permission; each config action has Label, Command, and Confirm fields and runs via the adminmenu.run console command.
Customize for my server

Edit the prompt on the next screen. 2 free plugins a month, no card.

Last updated Oct 26, 2026.

Customize admin menu

Customize