How to make a Rust welcome messages plugin (first-join aware)
Sends a configurable multi-line private welcome message when a player fully wakes up, with a separate message for players who have never joined the server before. Uses OnPlayerConnected to mark new players and OnPlayerSleepEnded to deliver the text.
New players join, load the map, and get nothing but the HUD. A welcome message tells them the server rules, your Discord, and where to find commands, but only if it arrives at a moment when they can actually read chat. Sending on connect is unreliable because the client is still loading and chat can be missed.
This plugin tracks which connections belong to a first-time player in OnPlayerConnected, then prints the configured lines in OnPlayerSleepEnded, which fires when the player's character is awake and in the world. Returning players see your normal message; accounts that have never been seen before get a separate first-join block. Seen IDs are stored in oxide/data so the check survives restarts. Lines support {name}, {online}, and {id} placeholders.
The plugin code
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Welcome Messages", "RustPluginGenerator", "1.0.0")]
[Description("Sends a configurable private welcome message when a player wakes up, with a separate first-join message.")]
public class WelcomeMessages : RustPlugin
{
private const string PermAdmin = "welcomemessages.admin";
private const string DataFile = "WelcomeMessages/seen";
private PluginConfig config;
private HashSet<ulong> seenPlayers = new HashSet<ulong>();
private readonly HashSet<ulong> pendingFirstJoin = new HashSet<ulong>();
private readonly HashSet<ulong> welcomedThisSession = new HashSet<ulong>();
private Timer saveTimer;
private bool enabled = true;
private bool dataDirty;
private class PluginConfig
{
[JsonProperty("Welcome lines (returning players)")]
public string[] WelcomeLines =
{
"<color=#ffcc00>Welcome back, {name}!</color>",
"Server rules are in the F1 console: type <color=#88ddff>rules</color>.",
"Type <color=#88ddff>/help</color> for a list of commands.",
"{online} players are online right now."
};
[JsonProperty("First join lines (new players)")]
public string[] FirstJoinLines =
{
"<color=#aaffaa>Welcome to the server, {name}!</color>",
"This looks like your first time here. Read the rules before you build.",
"You can claim a starter kit with <color=#88ddff>/kit starter</color>.",
"Discord and wipe schedule are posted in the F1 console.",
"{online} players are online right now."
};
[JsonProperty("Only send welcome once per session")]
public bool OnlyOncePerSession = false;
[JsonProperty("Save seen players interval (seconds)")]
public float SaveInterval = 120f;
}
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>
{
["ToggleOn"] = "Welcome messages are now enabled.",
["ToggleOff"] = "Welcome messages are now disabled.",
["NoPermission"] = "You do not have permission to use this command.",
["TestSent"] = "Test welcome message sent to your chat."
}, this);
}
private void Init()
{
permission.RegisterPermission(PermAdmin, this);
}
private void OnServerInitialized()
{
try
{
seenPlayers = Interface.Oxide.DataFileSystem.ReadObject<HashSet<ulong>>(DataFile) ?? new HashSet<ulong>();
}
catch
{
PrintWarning("Could not read seen players data, starting with an empty list");
seenPlayers = new HashSet<ulong>();
}
saveTimer = timer.Every(Mathf.Max(10f, config.SaveInterval), () =>
{
if (dataDirty) SaveData();
});
}
private void Unload()
{
saveTimer?.Destroy();
saveTimer = null;
SaveData();
}
private void SaveData()
{
if (!dataDirty) return;
Interface.Oxide.DataFileSystem.WriteObject(DataFile, seenPlayers);
dataDirty = false;
}
private void OnPlayerConnected(BasePlayer player)
{
if (player == null || player.IsNpc) return;
if (player.net?.connection == null) return;
ulong id = player.userID.Get();
if (!seenPlayers.Contains(id)) pendingFirstJoin.Add(id);
}
private void OnPlayerSleepEnded(BasePlayer player)
{
if (!enabled || player == null || player.IsNpc || !player.IsConnected) return;
ulong id = player.userID.Get();
bool firstJoin = pendingFirstJoin.Contains(id);
pendingFirstJoin.Remove(id);
if (!firstJoin && config.OnlyOncePerSession && welcomedThisSession.Contains(id)) return;
welcomedThisSession.Add(id);
string[] lines = firstJoin ? config.FirstJoinLines : config.WelcomeLines;
SendLines(player, lines);
if (firstJoin && !seenPlayers.Contains(id))
{
seenPlayers.Add(id);
dataDirty = true;
}
}
private void SendLines(BasePlayer player, string[] lines)
{
if (lines == null) return;
foreach (string raw in lines)
{
if (string.IsNullOrEmpty(raw)) continue;
SendReply(player, Format(raw, player));
}
}
private string Format(string line, BasePlayer player)
{
if (string.IsNullOrEmpty(line)) return line;
return line
.Replace("{name}", player.displayName ?? "player")
.Replace("{online}", BasePlayer.activePlayerList.Count.ToString())
.Replace("{id}", player.UserIDString);
}
private string Msg(string key, string userId = null, params object[] args) =>
string.Format(lang.GetMessage(key, this, userId), args);
[ChatCommand("welcome")]
private void CmdWelcome(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, PermAdmin))
{
SendReply(player, Msg("NoPermission", player.UserIDString));
return;
}
SendLines(player, config.WelcomeLines);
SendReply(player, Msg("TestSent", player.UserIDString));
}
[ConsoleCommand("welcomemessages.toggle")]
private void CcmdToggle(ConsoleSystem.Arg arg)
{
if (arg == null) return;
if (arg.Player() != null && !permission.UserHasPermission(arg.Player().UserIDString, PermAdmin))
{
arg.ReplyWith(Msg("NoPermission", arg.Player().UserIDString));
return;
}
enabled = !enabled;
arg.ReplyWith(enabled ? Msg("ToggleOn") : Msg("ToggleOff"));
}
}
}Hooks used: OnServerInitialized, OnPlayerConnected, OnPlayerSleepEnded
Config file
Created automatically on first load at oxide/config/WelcomeMessages.json.
{
"Welcome lines (returning players)": [
"<color=#ffcc00>Welcome back, {name}!</color>",
"Server rules are in the F1 console: type <color=#88ddff>rules</color>.",
"Type <color=#88ddff>/help</color> for a list of commands.",
"{online} players are online right now."
],
"First join lines (new players)": [
"<color=#aaffaa>Welcome to the server, {name}!</color>",
"This looks like your first time here. Read the rules before you build.",
"You can claim a starter kit with <color=#88ddff>/kit starter</color>.",
"Discord and wipe schedule are posted in the F1 console.",
"{online} players are online right now."
],
"Only send welcome once per session": false,
"Save seen players interval (seconds)": 120
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/welcome | Prints the configured welcome message to yourself, useful for testing edits without rejoining. | welcomemessages.admin |
welcomemessages.toggle | Enables or disables sending welcome messages at runtime. Run from server console or RCON. | welcomemessages.admin |
| Permission | Grants |
|---|---|
welcomemessages.admin | Allows using /welcome and the welcomemessages.toggle console command. |
How the code works
Detect first-time players on connect
OnPlayerConnectedruns as the client is admitted. The plugin checksplayer.userID.Get()against the savedHashSet<ulong>of seen players and stores a flag for that connection inpendingFirstJoin. On connect you must not send chat: the player is not spawned yet and the message will not be visible. Also null-checkplayerbecause the hook can arrive with a partial object in edge cases.Deliver the message when the character is awake
OnPlayerSleepEndedfires when the player's character leaves the sleeping state and can see chat. The plugin readspendingFirstJoinfor that user, picksFirstJoinLinesorWelcomeLinesfrom the config, formats the placeholders, and sends each line withSendReply. Skip NPCs and disconnected players, and remove the pending flag so a later respawn does not repeat the message.Placeholder formatting
A small
Formathelper replaces{name}withplayer.displayName,{online}withBasePlayer.activePlayerList.Count, and{id}withplayer.UserIDString. It is applied per line so admins can mix static text and player-specific values. Colours use Rust rich text tags like<color=#ffcc00>inside the config strings; they pass straight toSendReply.Persisting first-join state
The seen set is loaded in
OnServerInitializedfromInterface.Oxide.DataFileSystem.ReadObject<HashSet<ulong>>("WelcomeMessages/seen")and written back inUnloadand on a slowtimer.Everysave. Wrap reads in try/catch so a corrupt file falls back to an empty set instead of breaking the plugin. New IDs are added inOnPlayerSleepEndedwhen the welcome text is actually delivered.Configuration and messages
WelcomeLinesandFirstJoinLinesarestring[]fields under[JsonProperty]names, so the config file gets readable JSON arrays. Both default to a short block of lines. TheOnlyOncePerSessionswitch decides whether returning players get the normal message on every wake-up or only on the first connect of that session, controlled by thependingFirstJoinandwelcomedThisSessionsets.Runtime toggle and testing
welcomemessages.toggleis a[ConsoleCommand]that flips theenabledbool and replies with the new state viaarg.ReplyWith. Both the console command and/welcomecheckpermission.UserHasPermissionagainstwelcomemessages.admin./welcomecalls the same formatting path and sends the result to the caller only, so you can verify edits without reconnecting.
Install it on your server
Save WelcomeMessages.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/WelcomeMessages.json(Carbon:carbon/configs/WelcomeMessages.json). After editing, reload it:oxide.reload WelcomeMessages c.reload WelcomeMessages # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default welcomemessages.admin c.grant group default welcomemessages.admin # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Delayed delivery
Adds a short timer after wake-up so the message appears after the spawn screen and initial chat spam.
Generate this versionReward on first join
Gives new players a starter item or scrap alongside the first-join message.
Generate this versionBroadcast to server
Announces the new player's name to everyone when the first-join block fires.
Generate this version
FAQ
Why not send the message in OnPlayerConnected?
The client is still loading the world at that point, so chat lines are often lost or arrive before the player can read them. OnPlayerSleepEnded fires once the character exists and chat is visible, which is the reliable moment for a private message.
How does the plugin know a player is new?
It keeps a set of user IDs in oxide/data/WelcomeMessages/seen. On OnPlayerConnected the plugin checks the set; if the ID is missing, that connection is flagged as a first join. The ID is added once the first-join message is delivered, so the message is not repeated.
Will the message repeat on respawn?
No. The pending flag is cleared after sending. If OnlyOncePerSession is true, returning players get the normal message once per session; with it false, the normal message is shown every time they wake up, which some admins prefer for rules reminders.
How do I install it?
Place the .cs file in oxide/plugins on Oxide or Carbon and let it compile, then edit oxide/config/WelcomeMessages.json. Grant welcomemessages.admin to your group with oxide.grant group admin welcomemessages.admin and use /welcome to test.
Can I use different messages for different ranks?
Not in this version: the plugin sends one block to returning players and one to first-timers. You can branch further in OnPlayerSleepEnded by checking permission.UserHasPermission for a group permission and sending an extra line from a separate config array.
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
- Automatic announcementsAutoBroadcast sends a list of chat messages to every online player on a repeating timer, in order or random order, and fills {players} and {time}โฆOnServerInitialized
- 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
Last updated Oct 26, 2026.
Customize welcome messages
Customize