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
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.
{
"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
| Command | What it does | Permission |
|---|---|---|
rules.accept | Console command sent by the Accept button; records the player and closes the panel. | Everyone |
rules.decline | Console command sent by the Decline button; kicks the player with the configured reason. | Everyone |
/rules | Reopens 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 |
| Permission | Grants |
|---|---|
rulesacceptpanel.show | Allows a player to reopen the rules panel with /rules. |
rulesacceptpanel.admin | Allows use of the rules.reset console command. |
How the code works
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'sUserIDStringis already in the accepted set. Otherwise it callsShowPanel, which destroys any existing panel and adds a freshCuiElementContainer. Using this hook instead ofOnPlayerConnectedavoids sending UI while the client is still loading.Build the panel and buttons with CUI
The container adds a full-screen dimmer panel under
Overlay, then an inner panel withCursorEnabled = true. The rules text comes from the language keyRulesText, split into lines and rendered as oneCuiLabelper line so long rule lists stay inside the frame. The twoCuiButtonelements point at the plugin's console commands throughCuiButton.Button.Command, so clicks reach the server without any chat input. Every element name is prefixed with a constant soCuiHelper.DestroyUican clean up reliably.Handle Accept and store it
rules.acceptis a[ConsoleCommand]method. It resolves the calling player witharg.Player(), null-checks, addsplayer.UserIDStringto theHashSet<string>of accepted IDs, callsSaveData()and destroys the panel. The set is persisted withInterface.Oxide.DataFileSystem.WriteObject(Name, data)intooxide/data/RulesAcceptPanel.json. Because the command is only reachable from the button and from the player's own client, no separate permission is required.Handle Decline and repeat joins
rules.declinedestroys the UI and then callsplayer.Kick(reason)with the message from the language keyKickReason. Declining is intentionally not stored, so a player who reconnects sees the panel again and can accept. Accepted players never see it again becauseOnPlayerSleepEndedreturns beforeShowPanelwhen their ID is in the data file. Admins can be exempted through theExemptAdminsconfig key.Admin tools and shutdown
/rulesreopens the panel for testing, guarded by therulesacceptpanel.showpermission.rules.reset <steamid>removes an entry from the data set and replies througharg.ReplyWith, so you can force a specific player to accept again after a rules change. InUnloadthe plugin destroys the panel for every player inBasePlayer.activePlayerListand writes the data file once more, so no state is lost on a reload.
Install it on your server
Save RulesAcceptPanel.cs
Keep the file name exactly as the class name. Oxide matches them.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.Edit the config if you want
On first load the default config is written tooxide/config/RulesAcceptPanel.json(Carbon:carbon/configs/RulesAcceptPanel.json). After editing, reload it:oxide.reload RulesAcceptPanel c.reload RulesAcceptPanel # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip: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 versionBlock movement until accepted
Freeze the player with input blocking until they accept instead of kicking on decline.
Generate this versionTeam 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.
Edit the prompt on the next screen. 2 free plugins a month, no card.
More ui plugins
- Admin menuBuilds 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โฆ
- Announcement bannerAdds an /announce chat command that draws a CUI banner at the top of every connected player's screen for a configurable duration, plus timedโฆOnServerInitializedOnPlayerConnected
- Command shortcut menuAdds a /menu chat command that opens a CUI grid of configurable buttons; each button closes the panel and runs its configured chat command for thatโฆ
Last updated Oct 26, 2026.
Customize rules acceptance screen
Customize