Skip to content
RustPluginGenerator

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

WelcomeMessages.cs
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.

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

Chat and console commands
CommandWhat it doesPermission
/welcomePrints the configured welcome message to yourself, useful for testing edits without rejoining.welcomemessages.admin
welcomemessages.toggleEnables or disables sending welcome messages at runtime. Run from server console or RCON.welcomemessages.admin
Permissions
PermissionGrants
welcomemessages.adminAllows using /welcome and the welcomemessages.toggle console command.

How the code works

  1. Detect first-time players on connect

    OnPlayerConnected runs as the client is admitted. The plugin checks player.userID.Get() against the saved HashSet<ulong> of seen players and stores a flag for that connection in pendingFirstJoin. On connect you must not send chat: the player is not spawned yet and the message will not be visible. Also null-check player because the hook can arrive with a partial object in edge cases.

  2. Deliver the message when the character is awake

    OnPlayerSleepEnded fires when the player's character leaves the sleeping state and can see chat. The plugin reads pendingFirstJoin for that user, picks FirstJoinLines or WelcomeLines from the config, formats the placeholders, and sends each line with SendReply. Skip NPCs and disconnected players, and remove the pending flag so a later respawn does not repeat the message.

  3. Placeholder formatting

    A small Format helper replaces {name} with player.displayName, {online} with BasePlayer.activePlayerList.Count, and {id} with player.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 to SendReply.

  4. Persisting first-join state

    The seen set is loaded in OnServerInitialized from Interface.Oxide.DataFileSystem.ReadObject<HashSet<ulong>>("WelcomeMessages/seen") and written back in Unload and on a slow timer.Every save. Wrap reads in try/catch so a corrupt file falls back to an empty set instead of breaking the plugin. New IDs are added in OnPlayerSleepEnded when the welcome text is actually delivered.

  5. Configuration and messages

    WelcomeLines and FirstJoinLines are string[] fields under [JsonProperty] names, so the config file gets readable JSON arrays. Both default to a short block of lines. The OnlyOncePerSession switch decides whether returning players get the normal message on every wake-up or only on the first connect of that session, controlled by the pendingFirstJoin and welcomedThisSession sets.

  6. Runtime toggle and testing

    welcomemessages.toggle is a [ConsoleCommand] that flips the enabled bool and replies with the new state via arg.ReplyWith. Both the console command and /welcome check permission.UserHasPermission against welcomemessages.admin. /welcome calls the same formatting path and sends the result to the caller only, so you can verify edits without reconnecting.

Install it on your server

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

    Give it to every player, or swap default for a group such as vip: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 version
  • Reward on first join

    Gives new players a starter item or scrap alongside the first-join message.

    Generate this version
  • Broadcast 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.

Prompt: Create a Rust Oxide plugin that sends a private multi-line welcome message on OnPlayerSleepEnded, with a separate first-join message, {name} and {online} placeholders, saved seen players, and an admin toggle command.
Customize for my server

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

Last updated Oct 26, 2026.

Customize welcome messages

Customize