Skip to content
RustPluginGenerator

How to make a Rust server HUD plugin (CUI)

Builds a small persistent CUI panel with the server name, online player count and in-game clock, updated from an Oxide timer for every connected player. Visibility is saved per Steam ID with OnPlayerConnected and OnPlayerDisconnected plus the /hud toggle.

A server HUD is the quickest way to show your branding and live information without spamming chat. The catch is that CUI panels do not persist: every respawn or reconnect wipes your layer, and the in-game clock drifts if you refresh it rarely. This plugin builds one small CuiElementContainer and re-sends it to each awake player on a timer, so the panel stays visible and the time stays accurate.

Server name comes from ConVar.Server.hostname, the player number from BasePlayer.activePlayerList.Count, and the clock from TOD_Sky.Instance.Cycle.Hour. Visibility is stored per Steam ID in oxide/data, so a player who hides the HUD keeps it hidden after reconnecting. OnPlayerConnected pushes the panel on join, OnPlayerDisconnected writes the data file, and /hud toggles it. The timer reference is destroyed in Unload so a reload does not stack updates.

The plugin code

ServerHud.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("Server HUD", "RustPluginGenerator", "1.0.0")]
    [Description("Shows a persistent CUI panel with server name, online count and in-game time.")]
    public class ServerHud : RustPlugin
    {
        private const string PanelName = "ServerHud.Panel";

        private PluginConfig config;
        private StoredData stored;
        private Timer refreshTimer;

        private class PluginConfig
        {
            [JsonProperty("Refresh Interval (seconds)")]
            public float RefreshInterval = 3f;

            [JsonProperty("Show Clock")]
            public bool ShowClock = true;

            [JsonProperty("Show Player Count")]
            public bool ShowPlayerCount = true;

            [JsonProperty("Anchor Min")]
            public string AnchorMin = "0.01 0.94";

            [JsonProperty("Anchor Max")]
            public string AnchorMax = "0.16 0.99";

            [JsonProperty("Panel Color")]
            public string PanelColor = "0.05 0.05 0.05 0.75";

            [JsonProperty("Text Color")]
            public string TextColor = "1 1 1 1";

            [JsonProperty("Font Size")]
            public int FontSize = 12;

            [JsonProperty("Hidden Steam IDs")]
            public List<ulong> Hidden = new List<ulong>();
        }

        private class StoredData
        {
            [JsonProperty("Hidden Steam IDs")]
            public HashSet<ulong> Hidden = 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();
            }
            if (config.Hidden == null)
            {
                config.Hidden = new List<ulong>();
            }
            SaveConfig();
        }

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

        protected override void LoadDefaultMessages()
        {
            lang.RegisterMessages(new Dictionary<string, string>
            {
                ["HudEnabled"] = "HUD enabled.",
                ["HudDisabled"] = "HUD disabled."
            }, this);
        }

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

        private void OnServerInitialized()
        {
            LoadData();

            float interval = config.RefreshInterval;
            if (interval < 1f) interval = 1f;
            refreshTimer = timer.Every(interval, RefreshAll);
        }

        private void Unload()
        {
            if (refreshTimer != null)
            {
                refreshTimer.Destroy();
                refreshTimer = null;
            }

            SaveData();

            foreach (var player in BasePlayer.activePlayerList)
            {
                if (player != null)
                {
                    CuiHelper.DestroyUi(player, PanelName);
                }
            }
        }

        private void LoadData()
        {
            try
            {
                stored = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(Name);
            }
            catch
            {
                stored = null;
            }

            if (stored == null)
            {
                stored = new StoredData();
            }
            if (stored.Hidden == null)
            {
                stored.Hidden = new HashSet<ulong>();
            }
        }

        private void SaveData()
        {
            if (stored != null)
            {
                Interface.Oxide.DataFileSystem.WriteObject(Name, stored);
            }
        }

        private void RefreshAll()
        {
            foreach (var player in BasePlayer.activePlayerList)
            {
                if (player == null || player.IsNpc || !player.IsConnected)
                {
                    continue;
                }
                if (stored.Hidden.Contains(player.userID.Get()))
                {
                    continue;
                }
                ShowHud(player);
            }
        }

        private void ShowHud(BasePlayer player)
        {
            if (player == null || !player.IsConnected)
            {
                return;
            }

            string text = config.ShowPlayerCount
                ? $"{BasePlayer.activePlayerList.Count} online"
                : null;

            if (config.ShowClock)
            {
                string clock = GetGameTime();
                text = string.IsNullOrEmpty(text) ? clock : text + "  |  " + clock;
            }

            if (string.IsNullOrEmpty(text))
            {
                text = " ";
            }

            var container = new CuiElementContainer();
            string panel = container.Add(new CuiPanel
            {
                Image = { Color = config.PanelColor },
                RectTransform = { AnchorMin = config.AnchorMin, AnchorMax = config.AnchorMax },
                CursorEnabled = false
            }, "Overlay", PanelName);

            container.Add(new CuiLabel
            {
                Text =
                {
                    Text = ConVar.Server.hostname ?? "Rust Server",
                    FontSize = config.FontSize,
                    Align = TextAnchor.MiddleCenter,
                    Color = config.TextColor
                },
                RectTransform = { AnchorMin = "0 0.5", AnchorMax = "1 1" }
            }, panel);

            container.Add(new CuiLabel
            {
                Text =
                {
                    Text = text,
                    FontSize = config.FontSize,
                    Align = TextAnchor.MiddleCenter,
                    Color = config.TextColor
                },
                RectTransform = { AnchorMin = "0 0", AnchorMax = "1 0.5" }
            }, panel);

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

        private string GetGameTime()
        {
            var sky = TOD_Sky.Instance;
            if (sky == null)
            {
                return "--:--";
            }

            float hour = sky.Cycle.Hour;
            int h = Mathf.FloorToInt(hour);
            int m = Mathf.FloorToInt((hour - h) * 60f);
            return string.Format("{0:00}:{1:00}", h, m);
        }

        private void OnPlayerConnected(BasePlayer player)
        {
            if (player == null || player.IsNpc)
            {
                return;
            }
            if (stored.Hidden.Contains(player.userID.Get()))
            {
                return;
            }

            timer.Once(1f, () =>
            {
                if (player != null && player.IsConnected && !stored.Hidden.Contains(player.userID.Get()))
                {
                    ShowHud(player);
                }
            });
        }

        private void OnPlayerDisconnected(BasePlayer player, string reason)
        {
            SaveData();
        }

        [ChatCommand("hud")]
        private void CmdHud(BasePlayer player, string command, string[] args)
        {
            if (player == null)
            {
                return;
            }

            ulong id = player.userID.Get();
            if (stored.Hidden.Contains(id))
            {
                stored.Hidden.Remove(id);
                ShowHud(player);
                SendReply(player, Msg("HudEnabled", player.UserIDString));
            }
            else
            {
                stored.Hidden.Add(id);
                CuiHelper.DestroyUi(player, PanelName);
                SendReply(player, Msg("HudDisabled", player.UserIDString));
            }

            SaveData();
        }
    }
}

Hooks used: OnServerInitialized, OnPlayerConnected, OnPlayerDisconnected

Config file

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

ServerHud.json
{
  "Refresh Interval (seconds)": 3,
  "Show Clock": true,
  "Show Player Count": true,
  "Anchor Min": "0.01 0.94",
  "Anchor Max": "0.16 0.99",
  "Panel Color": "0.05 0.05 0.05 0.75",
  "Text Color": "1 1 1 1",
  "Font Size": 12,
  "Hidden Steam IDs": []
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/hudToggles the HUD panel on or off for the calling player. The choice is saved and restored on reconnect.Everyone

How the code works

  1. Config and stored visibility

    The nested PluginConfig holds Refresh Interval (seconds), Show Clock, Show Player Count, the anchor strings Anchor Min/Anchor Max, Panel Color, Text Color and Font Size, all read through Config.ReadObject after LoadConfig. Player preferences live in a separate StoredData class with a HashSet<ulong> named Hidden, loaded in LoadData through Interface.Oxide.DataFileSystem.ReadObject<StoredData>(Name). LoadData also recreates both objects when the file is missing or empty.

  2. Building the CuiElementContainer

    The method ShowHud(BasePlayer player) creates a CuiElementContainer, adds a parent CuiPanel with CursorEnabled false and the configured anchors, then two CuiLabels: the hostname on the upper half, and a combined line on the lower half built from the online count and the clock. Before CuiHelper.AddUi the method calls CuiHelper.DestroyUi(player, PanelName), so repeated sends replace the old panel instead of stacking rectangles. The constant PanelName is ServerHud.Panel.

  3. Timer refresh loop

    In OnServerInitialized, the stored Timer field refreshTimer is assigned from timer.Every(interval, RefreshAll), where interval is config.RefreshInterval clamped to a minimum of 1 second. RefreshAll loops BasePlayer.activePlayerList, skips null, NPC and disconnected entries, skips Steam IDs present in stored.Hidden, and calls ShowHud. A short interval makes the clock tick; a longer one cuts RPC volume.

  4. Reading the in-game clock

    GetGameTime reads TOD_Sky.Instance.Cycle.Hour and returns --:-- when TOD_Sky.Instance is null. Otherwise Mathf.FloorToInt gives the hour and the fractional part multiplied by 60 and floored gives the minutes, formatted with string.Format("{0:00}:{1:00}", h, m). The panel shows that string only when Show Clock is true; the online count line is controlled separately by Show Player Count.

  5. Connecting and disconnecting

    OnPlayerConnected returns early for NPCs and for players already present in stored.Hidden. Otherwise it schedules timer.Once(1f, ...) so the client is ready before the first draw; the callback re-checks that the player is still connected and still visible. OnPlayerDisconnected only calls SaveData, which writes the current stored object to the plugin's data file so a toggle made just before logout survives.

  6. The /hud toggle and cleanup

    [ChatCommand("hud")] CmdHud checks membership in stored.Hidden: if present it removes the ID, calls ShowHud and replies HudEnabled; otherwise it adds the ID, calls CuiHelper.DestroyUi(player, PanelName) and replies HudDisabled. Both branches finish with SaveData. Unload destroys refreshTimer, saves the data file and destroys the panel for every player in BasePlayer.activePlayerList so no stale layer survives a reload.

Install it on your server

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

Variations

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

  • Corner minimap-style panel

    Anchor the HUD to the top-left with a compass heading derived from the player's eyes forward vector.

    Generate this version
  • Wipe countdown line

    Add a third line showing days and hours until the next scheduled wipe from config.

    Generate this version
  • Server event alerts

    Push a temporary line into the HUD when a patrol helicopter or airdrop spawns.

    Generate this version

FAQ

Why does the HUD disappear after a respawn or map change?

Rust clients rebuild their CUI layers on respawn and world load, and only your plugin can restore them. Re-sending the panel from the refresh timer handles this automatically. If you refresh only on connect, the panel vanishes after a death and never returns until the player reconnects.

How much network traffic does a CUI timer add?

Each CuiHelper.AddUi call sends one RPC per player. On a busy server, a 1-second refresh for 200 players is 200 small RPCs every second on top of normal game traffic. A 3 to 5 second interval is a good compromise: the clock still looks live and the cost stays low.

Can I restrict /hud to a group?

Yes. Add private const string PermUse = "serverhud.use"; and register it in private void Init(). Then check if (!permission.UserHasPermission(player.UserIDString, PermUse)) at the top of CmdHud and return with a lang message. Grant it with oxide.grant group default serverhud.use.

Does this work on Carbon?

Yes. Carbon loads Oxide plugins without changes, and CuiHelper, lang, Interface.Oxide.DataFileSystem and the standard hooks behave the same. Copy the .cs file into oxide/plugins (or Carbon's plugin folder), let it compile, then edit the generated config under oxide/config.

How do I turn the clock off?

Set Show Clock to false in the config and reload the plugin. The panel then shows only the server name and the online count. The player count line is controlled separately by Show Player Count, so you can show any combination of the three fields.

Customize it for your server

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

Prompt: Create a Rust Oxide server HUD plugin: persistent CUI panel showing server name, online count and in-game time from TOD_Sky, refreshed by one timer, /hud toggle saved per Steam ID in oxide/data. No permissions.
Customize for my server

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

Last updated Oct 26, 2026.

Customize custom hud

Customize