Skip to content
RustPluginGenerator

How to make a Rust server info panel plugin (CUI tabs)

Builds a tabbed CUI window opened with /info that shows rules, commands and wipe information sourced from the config, with an optional one-time auto-open when a player first connects. Uses OnPlayerConnected plus a chat command and console commands for tab switching.

Every server has information players need but won't read: rules, the command list, wipe schedule. Pasting it into chat scrolls away in seconds, and sign walls are static. A proper info panel gives players a window they can open any time, with tabs so the content stays readable.

This plugin builds the window with Oxide's CUI (CuiElementContainer) instead of a client-side menu, so you control every label and button. The text for each tab lives in the config file, which means you edit oxide/config/InfoPanel.json and reload the plugin instead of recompiling. Tab buttons carry console commands that rebuild the panel with a different content block; a close button destroys the layer.

OnPlayerConnected optionally opens the panel once per player, tracked in oxide/data. The player still opens it later with /info, which is permission-gated.

The plugin code

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

namespace Oxide.Plugins
{
    [Info("InfoPanel", "RustPluginGenerator", "1.0.0")]
    [Description("Shows a configurable tabbed server information window built with CUI.")]
    public class InfoPanel : RustPlugin
    {
        private const string PermUse = "infopanel.use";
        private const string PanelName = "InfoPanel.Panel";

        private PluginConfig config;
        private readonly Dictionary<ulong, string> activeTab = new Dictionary<ulong, string>();
        private StoredData stored;

        private class PluginConfig
        {
            [JsonProperty("Panel title")]
            public string Title { get; set; } = "Server Information";

            [JsonProperty("Tab label - Rules")]
            public string TabRules { get; set; } = "Rules";

            [JsonProperty("Tab label - Commands")]
            public string TabCommands { get; set; } = "Commands";

            [JsonProperty("Tab label - Wipe")]
            public string TabWipe { get; set; } = "Wipe info";

            [JsonProperty("Close button label")]
            public string CloseButton { get; set; } = "Close";

            [JsonProperty("Rules text")]
            public string RulesText { get; set; } = "1. No cheating or exploits.\n2. No raiding before 24h after wipe.\n3. Respect other players.\n4. Do not block monuments.";

            [JsonProperty("Commands text")]
            public string CommandsText { get; set; } = "/info - open this panel\n/help - server help\n/kit - list your kits\n/report <player> - report a rule break";

            [JsonProperty("Wipe text")]
            public string WipeText { get; set; } = "Wipe: every first Thursday of the month.\nMap wipes with server wipe, blueprints wipe too.\nCheck the Discord for the exact time.";

            [JsonProperty("Auto open on first connect")]
            public bool AutoOpenOnFirstConnect { get; set; } = true;
        }

        private class StoredData
        {
            [JsonProperty("Players that have seen the panel")]
            public HashSet<ulong> Seen { get; set; } = new HashSet<ulong>();
        }

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

        protected override void LoadConfig()
        {
            base.LoadConfig();
            try
            {
                config = Config.ReadObject<PluginConfig>();
                if (config == null) throw new Exception();
            }
            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 this command."
            }, 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 OnServerInitialized()
        {
            stored = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(Name) ?? new StoredData();
        }

        private void OnPlayerConnected(BasePlayer player)
        {
            if (player == null || !config.AutoOpenOnFirstConnect) return;
            if (stored == null) stored = new StoredData();
            if (stored.Seen.Contains(player.userID.Get())) return;

            stored.Seen.Add(player.userID.Get());
            Interface.Oxide.DataFileSystem.WriteObject(Name, stored);

            timer.Once(2f, () =>
            {
                if (player != null && player.IsConnected)
                    OpenPanel(player, "rules");
            });
        }

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

        [ConsoleCommand("infopanel.close")]
        private void CcmdClose(ConsoleSystem.Arg arg)
        {
            var player = arg.Player();
            if (player == null) return;
            activeTab.Remove(player.userID.Get());
            CuiHelper.DestroyUi(player, PanelName);
        }

        [ConsoleCommand("infopanel.tab")]
        private void CcmdTab(ConsoleSystem.Arg arg)
        {
            var player = arg.Player();
            if (player == null) return;
            if (!permission.UserHasPermission(player.UserIDString, PermUse))
            {
                SendReply(player, Msg("NoPermission", player.UserIDString));
                return;
            }
            string tab = arg.GetString(0, "rules");
            if (tab != "rules" && tab != "commands" && tab != "wipe") tab = "rules";
            OpenPanel(player, tab);
        }

        private void OpenPanel(BasePlayer player, string tab)
        {
            activeTab[player.userID.Get()] = tab;
            CuiHelper.DestroyUi(player, PanelName);

            var container = new CuiElementContainer();
            container.Add(new CuiPanel
            {
                Image = { Color = "0.08 0.08 0.08 0.94" },
                RectTransform = { AnchorMin = "0.3 0.25", AnchorMax = "0.7 0.75" },
                CursorEnabled = true
            }, "Overlay", PanelName);

            container.Add(new CuiLabel
            {
                Text = { Text = config.Title, FontSize = 22, Align = TextAnchor.MiddleCenter, Color = "0.95 0.85 0.5 1" },
                RectTransform = { AnchorMin = "0 0.86", AnchorMax = "1 1" }
            }, PanelName);

            AddTab(container, "rules", config.TabRules, 0.02f, 0.2f, tab == "rules");
            AddTab(container, "commands", config.TabCommands, 0.23f, 0.41f, tab == "commands");
            AddTab(container, "wipe", config.TabWipe, 0.44f, 0.62f, tab == "wipe");

            string body;
            switch (tab)
            {
                case "commands": body = config.CommandsText; break;
                case "wipe": body = config.WipeText; break;
                default: body = config.RulesText; break;
            }

            container.Add(new CuiLabel
            {
                Text = { Text = body, FontSize = 13, Align = TextAnchor.UpperLeft, Color = "0.9 0.9 0.9 1" },
                RectTransform = { AnchorMin = "0.04 0.16", AnchorMax = "0.76 0.82" }
            }, PanelName);

            container.Add(new CuiButton
            {
                Button = { Command = "infopanel.close", Color = "0.55 0.15 0.15 1" },
                RectTransform = { AnchorMin = "0.8 0.03", AnchorMax = "0.96 0.13" },
                Text = { Text = config.CloseButton, FontSize = 14, Align = TextAnchor.MiddleCenter, Color = "1 1 1 1" }
            }, PanelName);

            CuiHelper.AddUi(player, container);
        }

        private void AddTab(CuiElementContainer container, string key, string label, float minX, float maxX, bool selected)
        {
            container.Add(new CuiButton
            {
                Button =
                {
                    Command = "infopanel.tab " + key,
                    Color = selected ? "0.25 0.45 0.25 1" : "0.18 0.18 0.18 1"
                },
                RectTransform =
                {
                    AnchorMin = minX.ToString("0.00", System.Globalization.CultureInfo.InvariantCulture) + " 0.74",
                    AnchorMax = maxX.ToString("0.00", System.Globalization.CultureInfo.InvariantCulture) + " 0.84"
                },
                Text = { Text = label, FontSize = 13, Align = TextAnchor.MiddleCenter, Color = "1 1 1 1" }
            }, PanelName);
        }

        private void Unload()
        {
            foreach (var player in BasePlayer.activePlayerList)
            {
                if (player != null)
                    CuiHelper.DestroyUi(player, PanelName);
            }
            if (stored != null)
                Interface.Oxide.DataFileSystem.WriteObject(Name, stored);
        }
    }
}

Hooks used: OnServerInitialized, OnPlayerConnected

Config file

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

InfoPanel.json
{
  "Panel title": "Server Information",
  "Tab label - Rules": "Rules",
  "Tab label - Commands": "Commands",
  "Tab label - Wipe": "Wipe info",
  "Close button label": "Close",
  "Rules text": "1. No cheating or exploits.\n2. No raiding before 24h after wipe.\n3. Respect other players.\n4. Do not block monuments.",
  "Commands text": "/info - open this panel\n/help - server help\n/kit - list your kits\n/report <player> - report a rule break",
  "Wipe text": "Wipe: every first Thursday of the month.\nMap wipes with server wipe, blueprints wipe too.\nCheck the Discord for the exact time.",
  "Auto open on first connect": true,
  "Players that have seen the panel": []
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/infoOpens the server info window for the calling player.infopanel.use
infopanel.closeConsole command fired by the close button; destroys the panel layer.infopanel.use
infopanel.tab <rules|commands|wipe>Console command fired by the tab buttons; rebuilds the panel showing the requested tab.infopanel.use
Permissions
PermissionGrants
infopanel.useAllows the player to open the info panel and switch tabs.

How the code works

  1. Panel state and CUI layer

    The plugin keeps a Dictionary<ulong, string> of each player's currently selected tab. OpenPanel(player, tab) calls CuiHelper.DestroyUi first so repeated opens never stack layers, then builds a fresh CuiElementContainer with one CuiPanel anchored to the middle of the screen. The returned panel name is used as the parent for every label and button added afterwards. Cursor is enabled on the panel so players can click the buttons.

  2. Rendering tabs from config

    Each tab is a CuiButton in a top row. Its Button.Command is infopanel.tab <key>, which the [ConsoleCommand("infopanel.tab")] handler parses with arg.GetString(0, ""). The content area is one CuiLabel whose Text.Text comes from the matching config field, so switching tabs is just a rebuild with a different string. RectTransform.AnchorMin/AnchorMax define the layout; keep the content label tall enough for your longest text.

  3. The /info chat command

    [ChatCommand("info")] checks permission.UserHasPermission(player.UserIDString, PermUse) and calls OpenPanel(player, "rules"). If the player lacks the permission, the reply comes from lang (NoPermission). The chat command is the only way to open the panel from the player's side; the tab and close buttons use console commands because CUI buttons can only send console or chat commands, not CUI events directly.

  4. Handling close

    The close button in the bottom-right sends infopanel.close. Its handler looks up the calling player with arg.Player() (which is null when run from RCON, so guard it), removes them from the tab dictionary, and calls CuiHelper.DestroyUi(player, PanelName). The same destroy call is used in Unload across BasePlayer.activePlayerList so no player is left with a stuck window after a reload.

  5. Auto-open on first connect

    OnPlayerConnected(BasePlayer player) checks config.AutoOpenOnFirstConnect. If enabled, it looks up player.userID in the stored HashSet<ulong> Seen. Players not in the set get a timer.Once(2f, ...) open so the client is fully spawned, then are added and the data file is saved. Players already in the set get nothing, matching the "first connect" wording. Deleting oxide/data/InfoPanel.json resets the list so everyone sees it again.

  6. Config-driven text and lang

    PluginConfig holds a RulesText, CommandsText and WipeText string plus AutoOpenOnFirstConnect (default true) and Title (default Server Information). These are the only text sources for the panel body. Fixed strings like the tab labels and the close button also live in config (TabRules, TabCommands, TabWipe, CloseButton) so admins can translate them without touching code. Player warnings and replies (NoPermission) go through lang.

Install it on your server

  1. Save InfoPanel.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/InfoPanel.json (Carbon: carbon/configs/InfoPanel.json). After editing, reload it:oxide.reload InfoPanel c.reload InfoPanel # Carbon
  4. Grant the permission

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

Variations

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

  • Add a live wipe countdown

    Compute the time until the next wipe from a scheduled timestamp and show it inside the Wipe tab instead of static text.

    Generate this version
  • Hide tabs from non-vips

    Gate individual tabs behind separate permissions so VIP-only commands are hidden from regular players.

    Generate this version
  • Add clickable links

    Include Discord and website links as buttons that open the player's browser via client.connect.

    Generate this version

FAQ

Why does the panel not open when a player types /info?

The player almost certainly lacks infopanel.use. Grant it with oxide.grant user <id> infopanel.use or through a group. Admins are not automatically granted plugin permissions, so test with a non-admin account too.

How do I reset the first-connect auto-open?

Delete oxide/data/InfoPanel.json while the plugin is unloaded, or run oxide.reload InfoPanel after deleting it; the plugin recreates an empty set and every player is treated as new on their next connect. Set AutoOpenOnFirstConnect to false to disable entirely.

Does this plugin work on Carbon?

Yes. The file is a standard Oxide plugin and Carbon loads it directly; CUI and the OnPlayerConnected hook behave the same on both frameworks. Nothing in the code uses framework-specific APIs beyond CuiHelper and Interface.Oxide.DataFileSystem.

How do I edit the text once the server has run the plugin?

Edit oxide/config/InfoPanel.json while the server is stopped or after unloading the plugin, then load it again. Line breaks inside JSON strings must be written as \n; a literal multi-line string breaks deserialization and the plugin falls back to defaults with a warning.

Does the panel interfere with other CUI plugins?

Each plugin owns its own CUI layer names, so InfoPanel will not destroy or overlap another plugin's windows unless two plugins both use the layer name InfoPanel.Panel. If you fork the code, keep the layer name distinct. The panel destroys only its own layer on close and unload.

Customize it for your server

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

Prompt: Create an InfoPanel Oxide plugin: /info opens a CUI window with Rules, Commands and Wipe tabs from config, tab and close buttons via console commands, and optional auto-open on first connect.
Customize for my server

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

Last updated Oct 26, 2026.

Customize server info panel

Customize