How to make a Rust auto broadcast plugin (timed chat announcements)
AutoBroadcast sends a list of chat messages to every online player on a repeating timer, in order or random order, and fills {players} and {time} placeholders before sending. It needs only Init to register data paths, OnServerInitialized to start the timer, and Unload to stop it.
Automatic chat announcements keep server rules, wipe dates and Discord links in front of players without an admin typing commands. The usual problem is either a hard-coded list of strings in the plugin or a single message that repeats forever, so players tune it out.
This plugin stores the messages and their color in the config so you never edit code to change a line. A single timer.Every loop walks the list in order or picks a random entry each time, depending on Random Order. Before each line is sent, {players} is replaced with BasePlayer.activePlayerList.Count and {time} with the server clock, and the result goes out via Server.Broadcast. Timing, prefix and color are all config keys, so the same file works for a small test server and a busy wipe-day community.
The plugin code
using System.Linq;
using UnityEngine;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
namespace Oxide.Plugins
{
[Info("Auto Broadcast", "RustPluginGenerator", "1.0.0")]
[Description("Broadcasts a configurable list of chat messages on a timer with placeholder support.")]
public class AutoBroadcast : RustPlugin
{
private const string PermReload = "autobroadcast.reload";
private PluginConfig config;
private Timer broadcastTimer;
private int currentIndex;
private class PluginConfig
{
[JsonProperty("Interval Seconds")]
public float IntervalSeconds { get; set; } = 300f;
[JsonProperty("Random Order")]
public bool RandomOrder { get; set; } = false;
[JsonProperty("Use Prefix")]
public bool UsePrefix { get; set; } = true;
[JsonProperty("Prefix")]
public string Prefix { get; set; } = "[Server]";
[JsonProperty("Color")]
public string Color { get; set; } = "#ce422b";
[JsonProperty("Messages")]
public List<string> Messages { get; set; } = new List<string>
{
"Welcome! Type /help for available commands.",
"There are {players} players online right now.",
"Server time is {time}. Wipe information is on our Discord.",
"Report rule breakers to admins with /report."
};
}
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>
{
["Reloaded"] = "Configuration reloaded. Interval set to {0} seconds.",
["Sent"] = "Next announcement sent.",
["NoMessages"] = "The message list is empty. Add entries to the config.",
["NoPermission"] = "You do not have permission to use this command.",
["Usage"] = "Usage: /broadcast reload | /broadcast next"
}, 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(PermReload, this);
}
private void OnServerInitialized()
{
if (config.Messages == null || config.Messages.Count == 0)
{
PrintWarning(Msg("NoMessages"));
return;
}
StartTimer();
}
private void Unload()
{
if (broadcastTimer != null)
{
broadcastTimer.Destroy();
broadcastTimer = null;
}
}
private void StartTimer()
{
if (broadcastTimer != null) broadcastTimer.Destroy();
float interval = Math.Max(config.IntervalSeconds, 10f);
broadcastTimer = timer.Every(interval, BroadcastNext);
}
private void BroadcastNext()
{
if (config.Messages == null || config.Messages.Count == 0) return;
string raw;
if (config.RandomOrder)
{
int pick = UnityEngine.Random.Range(0, config.Messages.Count);
raw = config.Messages[pick];
}
else
{
if (currentIndex >= config.Messages.Count) currentIndex = 0;
raw = config.Messages[currentIndex];
currentIndex = (currentIndex + 1) % config.Messages.Count;
}
Server.Broadcast(Format(raw));
}
private string Format(string raw)
{
if (string.IsNullOrEmpty(raw)) return string.Empty;
string message = raw
.Replace("{players}", BasePlayer.activePlayerList.Count.ToString())
.Replace("{time}", DateTime.Now.ToString("HH:mm"));
if (config.UsePrefix && !string.IsNullOrEmpty(config.Prefix))
{
message = config.Prefix + " " + message;
}
if (!string.IsNullOrEmpty(config.Color))
{
message = "<color=" + config.Color + ">" + message + "</color>";
}
return message;
}
[ChatCommand("broadcast")]
private void CmdBroadcast(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, PermReload))
{
SendReply(player, Msg("NoPermission", player.UserIDString));
return;
}
if (args == null || args.Length == 0)
{
SendReply(player, Msg("Usage", player.UserIDString));
return;
}
switch (args[0].ToLower())
{
case "reload":
Interface.Oxide.ReloadPlugin(Name);
break;
case "next":
if (config.Messages == null || config.Messages.Count == 0)
{
SendReply(player, Msg("NoMessages", player.UserIDString));
return;
}
BroadcastNext();
SendReply(player, Msg("Sent", player.UserIDString));
break;
default:
SendReply(player, Msg("Usage", player.UserIDString));
break;
}
}
}
}Hooks used: OnServerInitialized
Config file
Created automatically on first load at oxide/config/AutoBroadcast.json.
{
"Interval Seconds": 300,
"Random Order": false,
"Use Prefix": true,
"Prefix": "[Server]",
"Color": "#ce422b",
"Messages": [
"Welcome! Type /help for available commands.",
"There are {players} players online right now.",
"Server time is {time}. Wipe information is on our Discord.",
"Report rule breakers to admins with /report."
]
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/broadcast reload | Reloads the config from disk and restarts the broadcast timer with the new interval. | autobroadcast.reload |
/broadcast next | Sends the next queued message to everyone immediately, without advancing the timer. | autobroadcast.reload |
| Permission | Grants |
|---|---|
autobroadcast.reload | Grants the /broadcast commands to reload the config or push the next announcement. |
How the code works
Config shape
PluginConfigholdsInterval Seconds,Random Order,Prefix,Use Prefix,Colorand aMessageslist of strings. Each field carries a[JsonProperty]name so the file stays readable.LoadDefaultConfigcreates the defaults, andLoadConfigfalls back to them with a warning if the file fails to parse, then writes the result back withSaveConfigso new keys appear after an update.Starting the loop
OnServerInitializedreads the interval, clamps it to a sane minimum, and callstimer.Every(interval, BroadcastNext), keeping theTimerin a field. Starting there rather than inInitmeans the timer does not tick while the server is still loading plugins. IfMessagesis empty the plugin logs a warning once and returns without scheduling anything.Order and random mode
An integer index points at the next message. In order mode the index increments after each send and wraps with
% Messages.Count. In random modeBroadcastNextpicksUnityEngine.Random.Range(0, count)and does not touch the index, so the same line can appear twice in a row. Both paths go through one method, so/broadcast nextbehaves identically to a scheduled tick.Placeholders
Before the message leaves the plugin,
Formatreplaces{players}withBasePlayer.activePlayerList.Countand{time}withDateTime.Now.ToString("HH:mm"), then applies the prefix and color whenUse Prefixis on. Replacement happens per send, not at load time, so a{players}line shows the current headcount rather than the value from server start.Sending to everyone
Server.Broadcast(string)writes a chat line to every connected player, which suits a message that has no owner and no reply target. Wrapping the tag in<color=#hex></color>works because chat renders rich text tags. The method is used instead of iteratingBasePlayer.activePlayerListwithSendReply, so a single call handles joins and leaves without extra bookkeeping.Commands and cleanup
/broadcast reloadre-reads config, destroys the running timer and schedules a new one, so an interval change applies without a plugin reload./broadcast nextcalls the same send method once. Both checkpermission.UserHasPermissionagainstautobroadcast.reload, registered inInit.Unloaddestroys the timer reference so a reload does not leave two loops sending the same lines.
Install it on your server
Save AutoBroadcast.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/AutoBroadcast.json(Carbon:carbon/configs/AutoBroadcast.json). After editing, reload it:oxide.reload AutoBroadcast c.reload AutoBroadcast # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default autobroadcast.reload c.grant group default autobroadcast.reload # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Per-message intervals
Give each entry its own delay instead of one global interval.
Generate this versionAdmin-only announcements
Restrict some lines to admins or to players with a permission.
Generate this versionJoin welcome message
Send the current announcement to a player who just connected.
Generate this version
FAQ
How do I install it on Oxide or Carbon?
Drop AutoBroadcast.cs into oxide/plugins, wait for the console to report it compiled, then edit oxide/config/AutoBroadcast.json. Carbon loads the same file from its plugin folder. Reload the plugin or run /broadcast reload after editing the config.
Why do my `{players}` placeholders show as text?
Placeholder replacement is case-sensitive and only recognizes the exact tokens {players} and {time}. A typo such as {Players} is sent literally. Curly braces are also rendered as-is by chat, so you should see the token unchanged rather than an error.
Does this survive a plugin reload without duplicating messages?
Yes. Unload destroys the stored Timer, and OnServerInitialized creates a fresh one on the next load. If you edit the code while the server is running, wait for the reload to finish before testing, otherwise two timers can briefly run.
Can I send to a single team or player instead?
Not with this build; Server.Broadcast always targets everyone. For scoped messages you would loop BasePlayer.activePlayerList, filter with currentTeam or a permission check, and call SendReply on each match. That iteration is slower but gives full control over the audience.
Where does the time in `{time}` come from?
It uses DateTime.Now on the machine running the server, formatted as hours and minutes, so it reflects the host OS time zone. Set the server's time zone or convert to UTC in the Format method if your admins are in a different region.
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 chat plugins
- Chat loggerRecords every chat message with timestamp, SteamID, name, channel and text into a capped oxide/data file, and adds a /chatlog command that prints aโฆOnServerInitializedOnPlayerChat
- Chat mentionsDetects @name tokens in chat and sends the mentioned online player a private notice plus a notification sound, with a per-sender cooldown.OnPlayerChatOnPlayerDisconnected
- Chat titles and colorsReplaces vanilla global chat with formatted lines: a group tag plus separate name and message colors picked by the player's highest-priority OxideโฆOnPlayerChat
Last updated Oct 26, 2026.
Customize automatic announcements
Customize