Skip to content
RustPluginGenerator

How to make a Rust rules acceptance screen plugin (CUI + persistent accept)

Shows a CUI rules panel with Accept and Decline buttons to players who have not accepted yet, kicks on decline, and stores accepted Steam IDs in oxide/data so the panel never reappears.

Servers with custom rules lose players who never read them. A one-time panel that forces an explicit choice fixes that: new players get the rules once, and anything but Accept is a clear refusal. The plugin below builds a centered CUI panel with a scrollable-looking text block and two buttons.

Acceptance is handled by two console commands generated through CUI buttons. Accepting writes the player's Steam ID into a data file so the panel is skipped on every later join. Declining kicks the player with a configurable reason. The check runs in OnPlayerSleepEnded, which fires after the player is fully spawned and can receive UI, so the panel appears as soon as they are in the world rather than during the loading screen. Text, button labels, colors and the kick message all live in the config and the language file.

The plugin code

RulesAcceptPanel.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("Rules Accept Panel", "RustPluginGenerator", "1.0.0")]
    [Description("Shows a one-time CUI rules window with Accept and Decline buttons to new players.")]
    public class RulesAcceptPanel : RustPlugin
    {
        private const string PermShow = "rulesacceptpanel.show";
        private const string PermAdmin = "rulesacceptpanel.admin";
        private const string PanelName = "RulesAcceptPanel.Main";
        private const string DimName = "RulesAcceptPanel.Dim";

        private PluginConfig config;
        private StoredData data;

        private class PluginConfig
        {
            [JsonProperty("Exempt admins")]
            public bool ExemptAdmins { get; set; } = true;

            [JsonProperty("Panel background color")]
            public string PanelColor { get; set; } = "0.08 0.08 0.08 0.96";

            [JsonProperty("Accept button color")]
            public string AcceptColor { get; set; } = "0.25 0.55 0.25 1";

            [JsonProperty("Decline button color")]
            public string DeclineColor { get; set; } = "0.65 0.2 0.2 1";
        }

        private class StoredData
        {
            [JsonProperty("Accepted steam ids")]
            public HashSet<string> Accepted = new HashSet<string>();
        }

        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>
            {
                ["RulesTitle"] = "SERVER RULES",
                ["Accept"] = "ACCEPT",
                ["Decline"] = "DECLINE",
                ["KickReason"] = "You must accept the server rules to play here.",
                ["RulesText"] = "1. No cheating or third-party software.\n2. No racism, harassment or spam in chat.\n3. Do not exploit bugs; report them instead.\n4. Raiding is allowed, griefing spawn areas is not.\n5. Admins have the final say on rule enforcement.",
                ["ResetUsage"] = "Usage: rules.reset <steamid>",
                ["ResetDone"] = "Removed {0} from the accepted list.",
                ["NoPermission"] = "You do not have permission to use this command."
            }, this);
        }

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

        private void Init()
        {
            permission.RegisterPermission(PermShow, this);
            permission.RegisterPermission(PermAdmin, this);
            data = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(Name) ?? new StoredData();
        }

        private void SaveData() => Interface.Oxide.DataFileSystem.WriteObject(Name, data);

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

        private void OnPlayerSleepEnded(BasePlayer player)
        {
            if (player == null || player.IsNpc) return;
            if (config.ExemptAdmins && player.IsAdmin) return;
            if (data.Accepted.Contains(player.UserIDString)) return;

            ShowPanel(player);
        }

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

            CuiHelper.DestroyUi(player, PanelName);

            var c = new CuiElementContainer();
            c.Add(new CuiPanel
            {
                Image = { Color = "0 0 0 0.65" },
                RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" },
                CursorEnabled = true
            }, "Overlay", DimName);

            c.Add(new CuiPanel
            {
                Image = { Color = config.PanelColor },
                RectTransform = { AnchorMin = "0.3 0.22", AnchorMax = "0.7 0.78" }
            }, DimName, PanelName);

            c.Add(new CuiLabel
            {
                Text = { Text = Msg("RulesTitle", player.UserIDString), FontSize = 24, Align = TextAnchor.MiddleCenter, Color = "1 1 1 1" },
                RectTransform = { AnchorMin = "0.05 0.86", AnchorMax = "0.95 0.98" }
            }, PanelName);

            string body = Msg("RulesText", player.UserIDString);
            c.Add(new CuiLabel
            {
                Text = { Text = body, FontSize = 14, Align = TextAnchor.UpperLeft, Color = "0.9 0.9 0.9 1" },
                RectTransform = { AnchorMin = "0.06 0.25", AnchorMax = "0.94 0.85" }
            }, PanelName);

            c.Add(new CuiButton
            {
                Button = { Command = "rules.accept", Color = config.AcceptColor },
                RectTransform = { AnchorMin = "0.06 0.06", AnchorMax = "0.47 0.18" },
                Text = { Text = Msg("Accept", player.UserIDString), FontSize = 16, Align = TextAnchor.MiddleCenter, Color = "1 1 1 1" }
            }, PanelName);

            c.Add(new CuiButton
            {
                Button = { Command = "rules.decline", Color = config.DeclineColor },
                RectTransform = { AnchorMin = "0.53 0.06", AnchorMax = "0.94 0.18" },
                Text = { Text = Msg("Decline", player.UserIDString), FontSize = 16, Align = TextAnchor.MiddleCenter, Color = "1 1 1 1" }
            }, PanelName);

            CuiHelper.AddUi(player, c);
        }

        [ConsoleCommand("rules.accept")]
        private void CcmdAccept(ConsoleSystem.Arg arg)
        {
            var player = arg.Player();
            if (player == null) return;

            data.Accepted.Add(player.UserIDString);
            SaveData();
            CuiHelper.DestroyUi(player, PanelName);
            CuiHelper.DestroyUi(player, DimName);
        }

        [ConsoleCommand("rules.decline")]
        private void CcmdDecline(ConsoleSystem.Arg arg)
        {
            var player = arg.Player();
            if (player == null) return;

            CuiHelper.DestroyUi(player, PanelName);
            CuiHelper.DestroyUi(player, DimName);
            player.Kick(Msg("KickReason", player.UserIDString));
        }

        [ChatCommand("rules")]
        private void CmdRules(BasePlayer player, string command, string[] args)
        {
            if (player == null) return;
            if (!permission.UserHasPermission(player.UserIDString, PermShow))
            {
                player.ChatMessage(Msg("NoPermission", player.UserIDString));
                return;
            }
            ShowPanel(player);
        }

        [ConsoleCommand("rules.reset")]
        private void CcmdReset(ConsoleSystem.Arg arg)
        {
            var id = arg.GetString(0, string.Empty);
            if (string.IsNullOrEmpty(id))
            {
                arg.ReplyWith(Msg("ResetUsage"));
                return;
            }

            var player = arg.Player();
            if (player != null && !player.IsAdmin && !permission.UserHasPermission(player.UserIDString, PermAdmin))
            {
                arg.ReplyWith(Msg("NoPermission", player.UserIDString));
                return;
            }

            data.Accepted.Remove(id);
            SaveData();
            arg.ReplyWith(Msg("ResetDone", null, id));
        }
    }
}

Hooks used: OnPlayerSleepEnded

Config file

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

RulesAcceptPanel.json
{
  "Exempt admins": true,
  "Panel background color": "0.08 0.08 0.08 0.96",
  "Accept button color": "0.25 0.55 0.25 1",
  "Decline button color": "0.65 0.2 0.2 1"
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
rules.acceptConsole command sent by the Accept button; records the player and closes the panel.Everyone
rules.declineConsole command sent by the Decline button; kicks the player with the configured reason.Everyone
/rulesReopens the rules panel for yourself, useful for checking layout after a config edit.rulesacceptpanel.show
rules.reset <steamid>Removes a player's acceptance from the data file so the panel shows again on their next join.rulesacceptpanel.admin
Permissions
PermissionGrants
rulesacceptpanel.showAllows a player to reopen the rules panel with /rules.
rulesacceptpanel.adminAllows use of the rules.reset console command.

How the code works

  1. Detect first-time players in OnPlayerSleepEnded

    OnPlayerSleepEnded(BasePlayer player) fires after the player has spawned and is able to receive CUI. The hook null-checks the player, skips NPCs and admins if configured, and returns early when the player's UserIDString is already in the accepted set. Otherwise it calls ShowPanel, which destroys any existing panel and adds a fresh CuiElementContainer. Using this hook instead of OnPlayerConnected avoids sending UI while the client is still loading.

  2. Build the panel and buttons with CUI

    The container adds a full-screen dimmer panel under Overlay, then an inner panel with CursorEnabled = true. The rules text comes from the language key RulesText, split into lines and rendered as one CuiLabel per line so long rule lists stay inside the frame. The two CuiButton elements point at the plugin's console commands through CuiButton.Button.Command, so clicks reach the server without any chat input. Every element name is prefixed with a constant so CuiHelper.DestroyUi can clean up reliably.

  3. Handle Accept and store it

    rules.accept is a [ConsoleCommand] method. It resolves the calling player with arg.Player(), null-checks, adds player.UserIDString to the HashSet<string> of accepted IDs, calls SaveData() and destroys the panel. The set is persisted with Interface.Oxide.DataFileSystem.WriteObject(Name, data) into oxide/data/RulesAcceptPanel.json. Because the command is only reachable from the button and from the player's own client, no separate permission is required.

  4. Handle Decline and repeat joins

    rules.decline destroys the UI and then calls player.Kick(reason) with the message from the language key KickReason. Declining is intentionally not stored, so a player who reconnects sees the panel again and can accept. Accepted players never see it again because OnPlayerSleepEnded returns before ShowPanel when their ID is in the data file. Admins can be exempted through the ExemptAdmins config key.

  5. Admin tools and shutdown

    /rules reopens the panel for testing, guarded by the rulesacceptpanel.show permission. rules.reset <steamid> removes an entry from the data set and replies through arg.ReplyWith, so you can force a specific player to accept again after a rules change. In Unload the plugin destroys the panel for every player in BasePlayer.activePlayerList and writes the data file once more, so no state is lost on a reload.

Install it on your server

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

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

Variations

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

  • Re-prompt after rules change

    Bump a rules version in the config and show the panel again to everyone whose stored version is older.

    Generate this version
  • Block movement until accepted

    Freeze the player with input blocking until they accept instead of kicking on decline.

    Generate this version
  • Team and clan rules variant

    Show extra rules text to players who are in a team at first join.

    Generate this version

FAQ

Why does the panel appear only after spawning?

OnPlayerSleepEnded runs after the player entity is fully spawned and the client can receive CUI. Sending UI earlier, for example in OnPlayerConnected, can be dropped by the client because the connection is not ready yet. The trade-off is that the player sees the world for a moment before the panel appears.

Where is the acceptance stored?

In oxide/data/RulesAcceptPanel.json through Interface.Oxide.DataFileSystem. The file holds a list of Steam IDs, so it survives restarts, wipes and plugin reloads. Deleting the file resets everyone to unaccepted.

Can players bypass the panel?

They can press Escape, but the panel is only a prompt: nothing forces them to stay. If you need enforcement, pair this with a variation that blocks input or teleports the player to a holding area until the Accept command arrives. The Decline button simply kicks.

How do I install it on Oxide or Carbon?

Drop the compiled .cs file into oxide/plugins, wait for the server to compile it, then edit oxide/config/RulesAcceptPanel.json to set your rules text. Carbon loads the same file from its plugins folder, so no change is needed.

How do I reset one player after changing the rules?

Run rules.reset <steamid> in the server console or RCON. The ID is removed from the data file and the panel shows again the next time that player spawns. You need the rulesacceptpanel.admin permission when running it as a player.

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 plugin that shows a CUI rules acceptance window to new players in OnPlayerSleepEnded, with Accept storing their Steam ID in oxide/data and Decline kicking them with a configured message. Include configurable rules text and colors.
Customize for my server

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

Last updated Oct 26, 2026.

Customize rules acceptance screen

Customize