Skip to content
RustPluginGenerator

How to make a Rust welcome panel plugin (CUI on spawn)

Shows a configurable CUI welcome panel with a title, message lines and server links when a player wakes up, closing on a button click or after a timeout. Uses OnPlayerSleepEnded and OnPlayerDisconnected.

New and returning players often land in the world without knowing the server rules, wipe schedule or Discord address. A chat broadcast works, but it scrolls away fast and players miss it. A CUI panel drawn at the moment the player finishes waking up is seen every session and can be dismissed with one click.

This plugin hooks OnPlayerSleepEnded, which fires when a player leaves the sleeping state after connecting or respawning, and builds a CuiElementContainer anchored to the screen centre. Title, body lines, a links block and the close button label all come from the config. The panel auto-closes after a configurable number of seconds through a tracked timer.Once, and pressing the close button destroys it immediately. A session set prevents the panel appearing twice in the same connection, and OnPlayerDisconnected clears that state so it shows again next time.

The plugin code

WelcomePanel.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("Welcome Panel", "RustPluginGenerator", "1.0.0")]
    [Description("Shows a configurable CUI welcome panel when a player wakes up.")]
    public class WelcomePanel : RustPlugin
    {
        private const string PanelName = "WelcomePanelUI";
        private const string PermUse = "welcomepanel.use";

        private PluginConfig config;
        private readonly HashSet<ulong> shown = new HashSet<ulong>();
        private readonly Dictionary<ulong, Timer> closeTimers = new Dictionary<ulong, Timer>();

        private class PluginConfig
        {
            [JsonProperty("Title")]
            public string Title = "WELCOME TO OUR SERVER";

            [JsonProperty("Lines")]
            public List<string> Lines = new List<string>
            {
                "Wipe schedule: first Thursday of every month",
                "Group limit: 4 players",
                "No cheating, no exploit abuse"
            };

            [JsonProperty("Links")]
            public List<string> Links = new List<string>
            {
                "Discord: discord.gg/example",
                "Website: example.com"
            };

            [JsonProperty("Auto Close Seconds")]
            public float AutoCloseSeconds = 15f;

            [JsonProperty("Cursor Enabled")]
            public bool CursorEnabled = true;

            [JsonProperty("Panel Anchor Min")]
            public string AnchorMin = "0.28 0.30";

            [JsonProperty("Panel Anchor Max")]
            public string AnchorMax = "0.72 0.70";

            [JsonProperty("Background Color")]
            public string BackgroundColor = "0.10 0.10 0.10 0.92";

            [JsonProperty("Title Color")]
            public string TitleColor = "1.00 0.78 0.30 1.00";

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

            [JsonProperty("Title Font Size")]
            public int TitleFontSize = 22;

            [JsonProperty("Text Font Size")]
            public int TextFontSize = 14;
        }

        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 OnPlayerSleepEnded(BasePlayer player)
        {
            if (player == null || player.IsNpc || !player.IsConnected) return;
            ulong id = player.userID.Get();
            if (!shown.Add(id)) return;
            ShowPanel(player);
        }

        private void OnPlayerDisconnected(BasePlayer player, string reason)
        {
            if (player == null) return;
            ulong id = player.userID.Get();
            shown.Remove(id);
            DestroyTimer(id);
            CuiHelper.DestroyUi(player, PanelName);
        }

        private void ShowPanel(BasePlayer player)
        {
            DestroyTimer(player.userID.Get());
            CuiHelper.DestroyUi(player, PanelName);

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

            c.Add(new CuiLabel
            {
                Text = { Text = config.Title, FontSize = config.TitleFontSize, Color = config.TitleColor, Align = TextAnchor.MiddleCenter },
                RectTransform = { AnchorMin = "0.02 0.82", AnchorMax = "0.98 0.97" }
            }, panel);

            string body = config.Lines != null ? string.Join("\n", config.Lines.ToArray()) : "";
            c.Add(new CuiLabel
            {
                Text = { Text = body, FontSize = config.TextFontSize, Color = config.TextColor, Align = TextAnchor.UpperLeft },
                RectTransform = { AnchorMin = "0.05 0.45", AnchorMax = "0.95 0.80" }
            }, panel);

            string links = config.Links != null ? string.Join("\n", config.Links.ToArray()) : "";
            c.Add(new CuiLabel
            {
                Text = { Text = links, FontSize = config.TextFontSize, Color = config.TextColor, Align = TextAnchor.UpperLeft },
                RectTransform = { AnchorMin = "0.05 0.22", AnchorMax = "0.95 0.43" }
            }, panel);

            c.Add(new CuiButton
            {
                Button = { Command = "welcomepanel.close", Color = "0.70 0.20 0.20 1.00" },
                RectTransform = { AnchorMin = "0.38 0.05", AnchorMax = "0.62 0.18" },
                Text = { Text = "CLOSE", FontSize = 16, Align = TextAnchor.MiddleCenter }
            }, panel);

            CuiHelper.AddUi(player, c);

            if (config.AutoCloseSeconds > 0f)
            {
                ulong id = player.userID.Get();
                closeTimers[id] = timer.Once(config.AutoCloseSeconds, () =>
                {
                    closeTimers.Remove(id);
                    if (player != null && player.IsConnected)
                        CuiHelper.DestroyUi(player, PanelName);
                });
            }
        }

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

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

        private void DestroyTimer(ulong id)
        {
            Timer existing;
            if (closeTimers.TryGetValue(id, out existing))
            {
                existing?.Destroy();
                closeTimers.Remove(id);
            }
        }

        private void Unload()
        {
            foreach (var player in BasePlayer.activePlayerList)
            {
                if (player != null)
                    CuiHelper.DestroyUi(player, PanelName);
            }
            foreach (var entry in closeTimers)
                entry.Value?.Destroy();
            closeTimers.Clear();
            shown.Clear();
        }
    }
}

Hooks used: OnPlayerSleepEnded, OnPlayerDisconnected

Config file

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

WelcomePanel.json
{
  "Title": "WELCOME TO OUR SERVER",
  "Lines": [
    "Wipe schedule: first Thursday of every month",
    "Group limit: 4 players",
    "No cheating, no exploit abuse"
  ],
  "Links": [
    "Discord: discord.gg/example",
    "Website: example.com"
  ],
  "Auto Close Seconds": 15,
  "Cursor Enabled": true,
  "Panel Anchor Min": "0.28 0.30",
  "Panel Anchor Max": "0.72 0.70",
  "Background Color": "0.10 0.10 0.10 0.92",
  "Title Color": "1.00 0.78 0.30 1.00",
  "Text Color": "0.92 0.92 0.92 1.00",
  "Title Font Size": 22,
  "Text Font Size": 14
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/welcomeReopens the welcome panel for yourself.welcomepanel.use
welcomepanel.closeConsole command fired by the close button that destroys the panel and cancels its auto-close timer.Everyone
Permissions
PermissionGrants
welcomepanel.useAllows using /welcome to reopen the panel manually.

How the code works

  1. Track who has seen the panel

    A HashSet<ulong> named shown records players who already received the panel during the current connection. OnPlayerSleepEnded(BasePlayer player) returns early when the player is null or already in the set, then adds the id and calls ShowPanel. OnPlayerDisconnected(BasePlayer player, string reason) removes the id so a reconnecting player sees the panel again. Unload clears the set.

  2. Build the panel from config

    ShowPanel creates a CuiElementContainer and adds a CuiPanel anchored between AnchorMin and AnchorMax from Panel Anchor Min / Panel Anchor Max, with the configured background colour and CursorEnabled = true. A CuiLabel holds Title, another holds the Lines joined with \n, a third holds the Links block. Colours and font sizes are [JsonProperty] fields so admins can rebrand without touching code.

  3. Close button and command route

    The close button is a CuiButton whose Button.Command is welcomepanel.close. A [ConsoleCommand("welcomepanel.close")] method reads arg.Player(), returns if null, and calls CuiHelper.DestroyUi(player, PanelName) after destroying the timeout timer. Because CUI buttons run console commands, this works without any chat input.

  4. Automatic close timer

    The Auto Close Seconds config value drives a timer.Once. The returned Timer is stored in a Dictionary<ulong, Timer> so it can be destroyed when the player closes the panel early, disconnects, or when the plugin unloads. Setting the value to 0 disables the timer and leaves the panel open until the button is pressed.

  5. Manual reopen command

    [ChatCommand("welcome")] checks permission.UserHasPermission(player.UserIDString, PermUse) and then destroys the existing panel and rebuilds it. Messages such as the no-permission reply go through lang.RegisterMessages in LoadDefaultMessages and the Msg helper, so they can be translated in oxide/lang.

  6. Clean up on unload

    Unload iterates BasePlayer.activePlayerList, calls CuiHelper.DestroyUi(player, PanelName) for each, destroys every timer in the dictionary, clears the dictionary and the session set. This prevents orphaned UI elements when the plugin reloads while a panel is open.

Install it on your server

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

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

Variations

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

  • Show only to first-time players

    Persist a data file of player ids so the panel appears only on a player's first connection to the server.

    Generate this version
  • Countdown label

    Add a small label that counts down the remaining seconds before the panel closes.

    Generate this version
  • Rule acceptance gate

    Require the player to press an accept button before the panel closes, useful for rule acknowledgement.

    Generate this version

FAQ

Why does the panel appear twice after a respawn?

OnPlayerSleepEnded fires after waking from sleep, including respawn wakes. The plugin tracks shown ids per connection, so it only shows once. If you want it on every respawn, remove the shown check and let the hook run each time.

Can I make the panel stay open until the player clicks?

Set Auto Close Seconds to 0 in the config. The timer is skipped entirely and only the close button removes the panel. Reload the plugin after editing the config file.

Does the panel block movement?

CursorEnabled = true shows the mouse cursor while the panel is open, which can interfere with aiming. Set Cursor Enabled to false in the config if players should keep control while reading.

How do I install it on Oxide or Carbon?

Place WelcomePanel.cs in oxide/plugins, run oxide.reload WelcomePanel from console, then edit oxide/config/WelcomePanel.json. Carbon loads the same file from its plugins folder and reads the same config path.

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 Oxide welcome panel plugin that shows a CUI panel on OnPlayerSleepEnded with configurable title, message lines, links, colours, cursor flag and a 15 second auto close.
Customize for my server

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

Last updated Oct 26, 2026.

Customize welcome screen

Customize