Skip to content
RustPluginGenerator

How to make a Rust announcement banner plugin (CUI top-screen messages)

Adds an /announce chat command that draws a CUI banner at the top of every connected player's screen for a configurable duration, plus timed scheduled banners read from the config. Uses OnPlayerConnected and OnServerInitialized to re-show or start banners.

Chat messages scroll away and get lost in combat spam, so important notices like restart warnings or event starts need to sit on screen. This plugin draws a CUI panel anchored to the top of the screen and shows it to every connected player, then removes it after a configurable number of seconds.

Admins run /announce <text> (optionally with a duration) and the text appears immediately for everyone. The config holds a Default Duration, a color, a font size and a list of scheduled banners, each with its own interval in seconds and text. A repeating timer posts the scheduled ones. Players who connect while a banner is still visible get it re-drawn through OnPlayerConnected, and every panel is destroyed on Unload so nothing is left stranded on screen.

The plugin code

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

namespace Oxide.Plugins
{
    [Info("Announcement Banner", "RustPluginGenerator", "1.0.0")]
    [Description("Shows a top-screen CUI banner to all players via /announce or scheduled config entries.")]
    public class AnnouncementBanner : RustPlugin
    {
        private const string PanelId = "announcementbanner.panel";
        private const string PermUse = "announcementbanner.use";

        private PluginConfig config;
        private Timer activeTimer;
        private float activeRemaining;
        private readonly List<Timer> scheduledTimers = new List<Timer>();

        private class BannerEntry
        {
            [JsonProperty("Interval Seconds")]
            public float IntervalSeconds = 600f;

            [JsonProperty("Text")]
            public string Text = "Server restart in 10 minutes";
        }

        private class PluginConfig
        {
            [JsonProperty("Default Duration")]
            public float DefaultDuration = 10f;

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

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

            [JsonProperty("Scheduled Banners")]
            public List<BannerEntry> ScheduledBanners = new List<BannerEntry>
            {
                new BannerEntry { IntervalSeconds = 1800f, Text = "Server restart in 10 minutes" }
            };
        }

        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>
            {
                ["Usage"] = "Usage: /announce <text> [seconds]",
                ["Announced"] = "Banner sent to all players.",
                ["NoPlayers"] = "No players are connected."
            }, 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()
        {
            StartScheduled();
        }

        [ChatCommand("announce")]
        private void CmdAnnounce(BasePlayer player, string command, string[] args)
        {
            if (!permission.UserHasPermission(player.UserIDString, PermUse))
            {
                player.ChatMessage("You do not have permission to use that command.");
                return;
            }

            if (args == null || args.Length == 0)
            {
                player.ChatMessage(Msg("Usage", player.UserIDString));
                return;
            }

            float duration = config.DefaultDuration;
            int textCount = args.Length;

            if (args.Length > 1 && float.TryParse(args[args.Length - 1], out float parsed) && parsed > 0f)
            {
                duration = parsed;
                textCount = args.Length - 1;
            }

            string text = string.Join(" ", args, 0, textCount).Trim();
            if (string.IsNullOrEmpty(text))
            {
                player.ChatMessage(Msg("Usage", player.UserIDString));
                return;
            }

            ShowBanner(text, duration);
            player.ChatMessage(Msg("Announced", player.UserIDString));
        }

        private void ShowBanner(string text, float duration)
        {
            if (string.IsNullOrEmpty(text) || duration <= 0f) return;
            if (BasePlayer.activePlayerList.Count == 0) return;

            activeRemaining = duration;

            foreach (var target in BasePlayer.activePlayerList)
            {
                if (target == null || !target.IsConnected) continue;
                DrawFor(target, text);
            }

            activeTimer?.Destroy();
            activeTimer = timer.Once(duration, () =>
            {
                foreach (var target in BasePlayer.activePlayerList)
                {
                    if (target == null || !target.IsConnected) continue;
                    CuiHelper.DestroyUi(target, PanelId);
                }
                activeRemaining = 0f;
                activeTimer = null;
            });
        }

        private void DrawFor(BasePlayer player, string text)
        {
            CuiHelper.DestroyUi(player, PanelId);

            var elements = new CuiElementContainer();
            string panel = elements.Add(new CuiPanel
            {
                Image = { Color = "0.08 0.08 0.08 0.85" },
                RectTransform = { AnchorMin = "0.22 0.88", AnchorMax = "0.78 0.97" }
            }, "Overlay", PanelId);

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

            CuiHelper.AddUi(player, elements);
        }

        private void OnPlayerConnected(BasePlayer player)
        {
            if (player == null) return;
            if (activeRemaining <= 0f) return;
            if (string.IsNullOrEmpty(lastText)) return;

            DrawFor(player, lastText);
            activeTimer?.Destroy();
            activeTimer = timer.Once(activeRemaining, () =>
            {
                foreach (var target in BasePlayer.activePlayerList)
                {
                    if (target == null || !target.IsConnected) continue;
                    CuiHelper.DestroyUi(target, PanelId);
                }
                activeRemaining = 0f;
                activeTimer = null;
            });
        }

        private string lastText;

        private void ShowPersistentBanner(string text, float duration)
        {
            lastText = text;
            ShowBanner(text, duration);
        }

        private void StartScheduled()
        {
            foreach (var timerItem in scheduledTimers) timerItem?.Destroy();
            scheduledTimers.Clear();

            if (config.ScheduledBanners == null) return;

            foreach (var entry in config.ScheduledBanners)
            {
                if (entry == null) continue;
                if (string.IsNullOrEmpty(entry.Text) || entry.IntervalSeconds <= 0f)
                {
                    PrintWarning("Skipping scheduled banner with empty text or non-positive interval");
                    continue;
                }

                var captured = entry;
                var t = timer.Every(captured.IntervalSeconds, () => ShowBanner(captured.Text, config.DefaultDuration));
                scheduledTimers.Add(t);
            }
        }

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

            activeTimer?.Destroy();
            activeTimer = null;

            foreach (var t in scheduledTimers) t?.Destroy();
            scheduledTimers.Clear();
        }
    }
}

Hooks used: OnServerInitialized, OnPlayerConnected

Config file

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

AnnouncementBanner.json
{
  "Default Duration": 10,
  "Text Color": "1 1 1 1",
  "Font Size": 20,
  "Scheduled Banners": [
    {
      "Interval Seconds": 1800,
      "Text": "Server restart in 10 minutes"
    }
  ]
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/announce <text> [seconds]Shows the given text as a banner at the top of every connected player's screen. Optional trailing integer overrides the configured duration.announcementbanner.use
Permissions
PermissionGrants
announcementbanner.useGrants access to the /announce chat command.

How the code works

  1. Config layout

    The nested PluginConfig class holds Default Duration, Text Color, Font Size and Scheduled Banners, a list of BannerEntry objects each with Interval Seconds and Text. LoadDefaultConfig assigns a populated instance so the first start already produces a valid file, and LoadConfig/SaveConfig read and write it through Oxide's config system.

  2. Chat command and argument parsing

    /announce is registered with [ChatCommand("announce")] and checks announcementbanner.use before doing anything. Arguments are joined back into a string, and if the last argument parses as an integer it is treated as a duration override. Empty text after stripping the duration replies with a usage message from lang instead of drawing an empty panel.

  3. Drawing the CUI panel

    ShowBanner builds a CuiElementContainer with a CuiPanel anchored to 0.25 0.88 / 0.75 0.97 under Overlay, then adds a CuiLabel. The panel id is constant (announcementbanner.panel) so re-drawing replaces the previous banner. CuiHelper.AddUi sends it to the player. Text, font size and color come from the config.

  4. Expiry timer and pending state

    Each broadcast schedules timer.Once(duration, ...) that calls CuiHelper.DestroyUi for every active player and clears a pending flag. While a banner is live the plugin keeps the remaining seconds in a field so OnPlayerConnected can re-send it to a player who just joined, and the same timer fires for them.

  5. Scheduled banners

    In OnServerInitialized the plugin walks config.ScheduledBanners and registers timer.Every(entry.IntervalSeconds, ...) for each. Empty text or a non-positive interval is skipped with a single PrintWarning. Each timer calls ShowBanner with the configured duration, so scheduled notices reuse the same rendering path.

  6. Cleanup on unload

    Unload iterates BasePlayer.activePlayerList, calls CuiHelper.DestroyUi with the panel id for each player, destroys the stored scheduled timers and stops the active banner timer. Without this, a reload would leave panels visible until the client refreshes.

Install it on your server

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

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

Variations

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

  • Rotating banner queue

    Push multiple messages into a queue and cycle through them rather than firing all scheduled banners independently.

    Generate this version
  • Colored per-message banners

    Allow each scheduled banner entry to set its own background color and text size.

    Generate this version
  • Join-only banner

    Show a welcome banner to a player for a few seconds after they connect, using OnPlayerConnected.

    Generate this version

FAQ

How do I install this on Oxide or Carbon?

Drop the compiled .cs file into oxide/plugins. On Oxide or Carbon it loads on the next server start or oxide.reload AnnouncementBanner. The config is written to oxide/config/AnnouncementBanner.json on first load; edit it and reload the plugin.

Does the banner survive a player reconnecting mid-banner?

Yes. The plugin records how long the current banner has left and re-sends it in OnPlayerConnected. The banner disappears for that player when the original timer expires, so their banner may be shorter than the configured duration.

Why does the CUI not appear on some clients?

CUI is written to the player's screen instantly by the server. If the player was mid-load they may miss it; the OnPlayerConnected re-send covers most cases. If the panel id clashes with another plugin, change the id constant in the code.

Can scheduled timers be different lengths?

Yes. Each Scheduled Banners entry owns its own Interval Seconds; the plugin starts an independent timer.Every for it, so two entries can run every 5 minutes and every 2 hours side by side.

Will /announce flood work with server broadcasts?

It is separate. Server.Broadcast sends chat text, this plugin draws a CUI panel. You can run both if you want the message to also appear in chat history.

Customize it for your server

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

Prompt: Write an Oxide Rust plugin with an /announce command that shows a CUI banner at the top of every player's screen for a configurable duration, plus timed scheduled banners listed in config.
Customize for my server

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

Last updated Oct 26, 2026.

Customize announcement banner

Customize