Skip to content
RustPluginGenerator

How to make a Rust join and leave messages plugin (Oxide/Carbon)

Broadcasts a configurable chat message when a player connects to the server and when a player disconnects, including the disconnect reason. Uses the OnPlayerConnected and OnPlayerDisconnected hooks and hides players with a permission from announcements.

By default a Rust server does not announce when someone joins or leaves. When a queue drains or a player crashes, nobody in global chat knows it happened. This plugin listens to OnPlayerConnected and OnPlayerDisconnected, formats a line from the templates registered in the language file, and sends it to every connected player. The leave line can include whichever reason string Rust reports. Two config switches let you mute join and leave messages independently. A permission hides staff accounts so admin logins do not spam the broadcast, and a /jlm chat command lets admins reload the config or flip the switches in game. Messages carry chat tags defined in the language file, which can be edited without recompiling.

The plugin code

JoinLeaveMessages.cs
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;

namespace Oxide.Plugins
{
    [Info("Join Leave Messages", "RustPluginGenerator", "1.0.0")]
    [Description("Broadcasts configurable join and leave messages, optionally including the disconnect reason.")]
    public class JoinLeaveMessages : RustPlugin
    {
        private const string PermHidden = "joinleavemessages.hidden";
        private const string PermAdmin = "joinleavemessages.admin";

        private PluginConfig config;

        private class PluginConfig
        {
            [JsonProperty("Announce joins")]
            public bool AnnounceJoins { get; set; } = true;

            [JsonProperty("Announce leaves")]
            public bool AnnounceLeaves { get; set; } = true;

            [JsonProperty("Show disconnect reason")]
            public bool ShowReason { get; set; } = true;

            [JsonProperty("Write events to server console")]
            public bool ConsoleLog { get; set; } = true;

            [JsonProperty("Console join line (0 = name)")]
            public string ConsoleJoin { get; set; } = "{0} connected";

            [JsonProperty("Console leave line (0 = name, 1 = reason)")]
            public string ConsoleLeave { get; set; } = "{0} disconnected ({1})";
        }

        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>
            {
                ["Join"] = "<color=#88cc88>{0}</color> joined the server",
                ["Leave"] = "<color=#cc8888>{0}</color> left the server",
                ["LeaveReason"] = "<color=#cc8888>{0}</color> left the server ({1})",
                ["UnknownReason"] = "unknown",
                ["JoinOn"] = "Join announcements are now enabled",
                ["JoinOff"] = "Join announcements are now disabled",
                ["LeaveOn"] = "Leave announcements are now enabled",
                ["LeaveOff"] = "Leave announcements are now disabled",
                ["Reloaded"] = "Configuration reloaded",
                ["NoPermission"] = "You do not have permission to use this command",
                ["Usage"] = "Usage: /jlm reload | /jlm toggle join | /jlm toggle leave"
            }, 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(PermHidden, this);
            permission.RegisterPermission(PermAdmin, this);
        }

        private bool IsHidden(BasePlayer player)
        {
            return permission.UserHasPermission(player.UserIDString, PermHidden);
        }

        private void OnPlayerConnected(BasePlayer player)
        {
            if (player == null || !config.AnnounceJoins) return;
            if (IsHidden(player)) return;

            string name = player.displayName ?? "Unknown";
            Server.Broadcast(Msg("Join", null, name));

            if (config.ConsoleLog)
            {
                Puts(string.Format(config.ConsoleJoin ?? "{0} connected", name));
            }
        }

        private void OnPlayerDisconnected(BasePlayer player, string reason)
        {
            if (player == null || !config.AnnounceLeaves) return;
            if (IsHidden(player)) return;

            string name = player.displayName ?? "Unknown";
            string safeReason = string.IsNullOrEmpty(reason) ? Msg("UnknownReason") : reason;

            // Strip anything that could break the chat line
            safeReason = safeReason.Replace("<", string.Empty).Replace(">", string.Empty);

            if (config.ShowReason)
            {
                Server.Broadcast(Msg("LeaveReason", null, name, safeReason));
            }
            else
            {
                Server.Broadcast(Msg("Leave", null, name));
            }

            if (config.ConsoleLog)
            {
                Puts(string.Format(config.ConsoleLeave ?? "{0} disconnected ({1})", name, safeReason));
            }
        }

        [ChatCommand("jlm")]
        private void CmdJlm(BasePlayer player, string command, string[] args)
        {
            if (player == null) return;

            if (!permission.UserHasPermission(player.UserIDString, PermAdmin))
            {
                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":
                    LoadConfig();
                    LoadDefaultMessages();
                    SendReply(player, Msg("Reloaded", player.UserIDString));
                    return;

                case "toggle":
                    if (args.Length < 2)
                    {
                        SendReply(player, Msg("Usage", player.UserIDString));
                        return;
                    }

                    if (args[1].Equals("join", StringComparison.OrdinalIgnoreCase))
                    {
                        config.AnnounceJoins = !config.AnnounceJoins;
                        SaveConfig();
                        SendReply(player, Msg(config.AnnounceJoins ? "JoinOn" : "JoinOff", player.UserIDString));
                        return;
                    }

                    if (args[1].Equals("leave", StringComparison.OrdinalIgnoreCase))
                    {
                        config.AnnounceLeaves = !config.AnnounceLeaves;
                        SaveConfig();
                        SendReply(player, Msg(config.AnnounceLeaves ? "LeaveOn" : "LeaveOff", player.UserIDString));
                        return;
                    }

                    SendReply(player, Msg("Usage", player.UserIDString));
                    return;

                default:
                    SendReply(player, Msg("Usage", player.UserIDString));
                    return;
            }
        }
    }
}

Hooks used: OnPlayerConnected, OnPlayerDisconnected

Config file

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

JoinLeaveMessages.json
{
  "Announce joins": true,
  "Announce leaves": true,
  "Show disconnect reason": true,
  "Write events to server console": true,
  "Console join line (0 = name)": "{0} connected",
  "Console leave line (0 = name, 1 = reason)": "{0} disconnected ({1})"
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/jlm reloadReload the config file and re-register the language messages.joinleavemessages.admin
/jlm toggle joinTurn join announcements on or off.joinleavemessages.admin
/jlm toggle leaveTurn leave announcements on or off.joinleavemessages.admin
Permissions
PermissionGrants
joinleavemessages.hiddenPlayers with this permission are never announced when they join or leave.
joinleavemessages.adminGrants access to the /jlm chat commands.

How the code works

  1. Hooks and parameter types

    OnPlayerConnected(BasePlayer player) fires when a player is accepted by the server. OnPlayerDisconnected(BasePlayer player, string reason) fires while the player is being removed, so displayName and UserIDString are still readable inside it. Both hooks declare BasePlayer, so Oxide calls them for every connection event. The first check in each method is a null test, because a disconnect during authentication can deliver a partial player object.

  2. Message templates and the language file

    LoadDefaultMessages registers the keys Join, Leave and LeaveReason with {0} and {1} placeholders, plus the command replies. Msg calls string.Format on lang.GetMessage and is the only way text reaches a player. The default strings contain chat colour tags, which is why the result goes straight to Server.Broadcast. The config carries separate Console join line and Console leave line templates used only for the Puts log line.

  3. Hiding admins

    IsHidden(player) checks permission.UserHasPermission(player.UserIDString, PermHidden). Grant the joinleavemessages.hidden permission to staff accounts that should not be announced; the hidden player still sees other players' join and leave lines, only their own are skipped. Both joinleavemessages.hidden and joinleavemessages.admin are registered in Init, so the permission is available as soon as the plugin loads.

  4. Disconnect reasons

    The reason argument arrives as a plain string. If it is null or empty the plugin substitutes the UnknownReason lang entry, so the line never ends with a dangling bracket. Angle brackets are stripped from the reason before formatting, which stops a crafted reason from opening a chat tag that would swallow the rest of the message. The Show disconnect reason config key picks between the LeaveReason and Leave templates.

  5. Config switches and the /jlm command

    PluginConfig holds Announce joins, Announce leaves, Show disconnect reason, Write events to server console and the two console line templates. The [ChatCommand("jlm")] handler requires joinleavemessages.admin, answers reload by calling LoadConfig and LoadDefaultMessages, and handles toggle join and toggle leave by flipping a bool, calling SaveConfig and replying with the matching lang key.

  6. Broadcasting and console logging

    Each hook returns early when its config switch is off or the player is hidden, so nothing is sent on a disabled channel. Server.Broadcast then delivers the formatted line to every connected player. The Puts call is gated behind Write events to server console and only runs on connection events, never on a per-tick hook. No timers, UI or data files are used, so there is nothing to tear down on unload.

Install it on your server

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

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

Variations

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

  • Count online players in the message

    Append the current online player count to every join and leave line.

    Generate this version
  • Welcome message with server rules

    Send a longer welcome block to the joining player only.

    Generate this version
  • Discord webhook for joins

    Post connection events to a webhook URL from a companion plugin reference.

    Generate this version

FAQ

Why do leave messages sometimes appear out of order?

OnPlayerDisconnected fires during removal and the line is queued through the server's chat broadcast. If several players leave at once you can see reordered lines. The plugin reads nothing that requires the entity to still exist, so the message content itself is always correct.

How do I stop my admin account from being announced?

Give the account the joinleavemessages.hidden permission through your usual permission management. The permission is registered in Init, so it works without a reload. Hidden players still see other players' join and leave messages; only their own lines are suppressed.

Can I change the colours of the messages?

Yes. Edit the Join, Leave and LeaveReason entries in the language file and change the colour tags around the text, then reload the plugin. The chat text comes from the language file; the console line templates in the config only affect what Puts writes to the server log.

Will this work on Carbon?

Yes. Carbon loads the same Oxide plugin file. Drop JoinLeaveMessages.cs into the plugins folder and the hooks and permission calls behave identically. Config and language files live under the same oxide/config and oxide/lang paths.

Does it announce players who reconnect after a crash?

It does, because every accepted connection fires OnPlayerConnected. If you only want to announce first-time visits, you would need to store seen Steam IDs in a data file; the plugin above does not do that.

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 plugin that broadcasts configurable join and leave chat messages using OnPlayerConnected and OnPlayerDisconnected, includes the disconnect reason, and lets admins hide via a permission, with a /jlm admin 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 join and leave messages

Customize