How to make a Rust chat mentions plugin (@name pings)
Detects @name tokens in chat and sends the mentioned online player a private notice plus a notification sound, with a per-sender cooldown. Uses OnPlayerChat.
On a busy Rust server, global chat scrolls fast enough that players miss messages aimed at them. A mention system fixes that: when someone types @name, only the target gets a private notice and a sound cue, so nobody has to repeat themselves in chat. The tricky parts are matching names reliably, ignoring self-mentions and duplicate tokens, and stopping spam.
This plugin hooks OnPlayerChat, which receives the sender's BasePlayer, the raw message and the chat channel. It splits the message on spaces, checks each token that starts with @, and resolves it with BasePlayer.Find as an exact case-insensitive match, falling back to the single connected player whose display name starts with the token. Each hit gets a lang-formatted private reply and a sound played at the target. A per-sender cooldown tracked in memory stops one player pinging the whole server in a loop. The sound prefab, cooldown, notice prefix and message length are all configurable.
The plugin code
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Chat Mentions", "RustPluginGenerator", "1.0.0")]
[Description("Notifies players when someone mentions their name with @ in chat.")]
public class ChatMentions : RustPlugin
{
private PluginConfig config;
private readonly Dictionary<ulong, float> lastMention = new Dictionary<ulong, float>();
private class PluginConfig
{
[JsonProperty("Cooldown Seconds")]
public float CooldownSeconds = 10f;
[JsonProperty("Notify sender about unknown mentions")]
public bool NotifyUnknown = true;
[JsonProperty("Sound at ear height")]
public bool SoundAtEars = true;
[JsonProperty("Sound prefab")]
public string SoundPrefab = "assets/bundled/prefabs/fx/notice/item.select.fx.prefab";
[JsonProperty("Notice prefix")]
public string NoticePrefix = "@";
[JsonProperty("Max message length")]
public int MaxMessageLength = 200;
}
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>
{
["Mention Notice"] = "{0} mentioned you in chat: {1}",
["Mention Delivered"] = "You mentioned {0}.",
["Mention Cooldown"] = "You are mentioning people too often, wait a moment.",
["Unknown Player"] = "No online player matches @{0}."
}, this);
}
private string Msg(string key, string userId = null, params object[] args)
{
return string.Format(lang.GetMessage(key, this, userId), args);
}
private object OnPlayerChat(BasePlayer player, string message, ConVar.Chat.ChatChannel channel)
{
if (player == null || player.IsNpc || !player.IsConnected) return null;
if (string.IsNullOrEmpty(message) || message.IndexOf('@') < 0) return null;
float now = UnityEngine.Time.realtimeSinceStartup;
float last;
if (config.CooldownSeconds > 0f && lastMention.TryGetValue(player.userID, out last) && now - last < config.CooldownSeconds)
{
SendReply(player, Msg("Mention Cooldown", player.UserIDString));
return null;
}
var tokens = message.Split(' ');
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
bool anyDelivered = false;
foreach (var raw in tokens)
{
if (raw.Length < 2 || raw[0] != '@') continue;
string name = raw.Substring(1).TrimEnd('!', '?', ',', '.', ':', ';');
if (name.Length == 0 || !seen.Add(name)) continue;
BasePlayer target = Resolve(name);
if (target == null || target == player)
{
if (config.NotifyUnknown && target == null)
{
SendReply(player, Msg("Unknown Player", player.UserIDString, name));
}
continue;
}
string snippet = message.Length > config.MaxMessageLength ? message.Substring(0, config.MaxMessageLength) + "..." : message;
SendReply(target, string.Format("{0} {1}", config.NoticePrefix, Msg("Mention Notice", target.UserIDString, player.displayName, snippet)));
if (!string.IsNullOrEmpty(config.SoundPrefab))
{
Vector3 pos = config.SoundAtEars && target.eyes != null ? target.eyes.position : target.transform.position;
Effect.server.Run(config.SoundPrefab, pos);
}
SendReply(player, Msg("Mention Delivered", player.UserIDString, target.displayName));
anyDelivered = true;
}
if (anyDelivered) lastMention[player.userID] = now;
return null;
}
private BasePlayer Resolve(string name)
{
BasePlayer exact = BasePlayer.Find(name);
if (exact != null && exact.IsConnected) return exact;
BasePlayer found = null;
foreach (var candidate in BasePlayer.activePlayerList)
{
if (candidate == null || !candidate.IsConnected) continue;
if (!candidate.displayName.StartsWith(name, StringComparison.OrdinalIgnoreCase)) continue;
if (found != null && found != candidate) return null;
found = candidate;
}
return found;
}
private void OnPlayerDisconnected(BasePlayer player, string reason)
{
if (player != null) lastMention.Remove(player.userID);
}
private void Unload()
{
lastMention.Clear();
}
}
}Hooks used: OnPlayerChat, OnPlayerDisconnected
Config file
Created automatically on first load at oxide/config/ChatMentions.json.
{
"Cooldown Seconds": 10,
"Notify sender about unknown mentions": true,
"Sound at ear height": true,
"Sound prefab": "assets/bundled/prefabs/fx/notice/item.select.fx.prefab",
"Notice prefix": "@",
"Max message length": 200
}How the code works
Hook signature and early exits
OnPlayerChatreturnsobject; returningnullleaves vanilla chat behavior untouched, which is what we want because the plugin only adds private notices. The hook runs on every chat line, so it first bails out on a null player, an NPC (player.IsNpc), a disconnected player, or a message that is empty or contains no@at all. ThatIndexOf('@')test keeps the cost near zero for normal chat. Nothing is ever removed or reworded from the message.Tokenizing the message
The message is split with
message.Split(' '). A token is considered only when it has at least two characters and its first character is@; the rest is the candidate name, trimmed of trailing!,?,,,.,:,;because players type those casually. AHashSet<string>withStringComparer.OrdinalIgnoreCaserecords every name already handled in this message, so repeated tokens like@name @nameproduce a single notice. An empty name after trimming is skipped.Resolving names to players
Resolvefirst callsBasePlayer.Find(name), an exact case-insensitive lookup that returns a connected player or null. If that fails it walksBasePlayer.activePlayerListand collects players whosedisplayNamestarts with the token usingStartsWith(name, StringComparison.OrdinalIgnoreCase). If more than one connected player matches the prefix, the method returns null rather than guessing. The hook also skips a resolved target that is the sender themselves, so nobody pings themselves.Sending the notice and the sound
A resolved target gets
SendReplywith theMention Noticelang message, formatted with the sender'sdisplayNameand the message body truncated toMax message lengthcharacters plus.... The sender seesMention Delivered. For the cue, the plugin callsEffect.server.Run(config.SoundPrefab, pos)at the target'seyes.positionwhenSound at ear heightis enabled andeyesis not null, otherwise attransform.position. An emptySound prefabskips the call entirely.Per-sender cooldown
A
Dictionary<ulong, float>stores the last delivery time per sender usingUnityEngine.Time.realtimeSinceStartup. Before processing tokens the hook reads the entry and, whenCooldown Secondsis above zero and the elapsed time is below it, sendsMention Cooldownto the sender and returns. The timestamp is only written after at least one notice was actually delivered (anyDelivered), so a message full of unknown names does not consume the cooldown.OnPlayerDisconnectedremoves the sender's entry so the dictionary does not grow forever.Config and messages
PluginConfigholdsCooldown Seconds,Notify sender about unknown mentions,Sound at ear height,Sound prefab,Notice prefixandMax message length.LoadDefaultConfig,LoadConfigandSaveConfigfollow the standard pattern: a try/catch aroundConfig.ReadObject<PluginConfig>(), a warning plus defaults on failure, then a save. Lang keysMention Notice,Mention Delivered,Mention CooldownandUnknown Playerare registered inLoadDefaultMessagesand read through theMsghelper, which formats with the receiver'sUserIDString.
Install it on your server
Save ChatMentions.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/ChatMentions.json(Carbon:carbon/configs/ChatMentions.json). After editing, reload it:oxide.reload ChatMentions c.reload ChatMentions # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Mention only real teammates
Restrict mentions to players who share the sender's team so pings cannot cross team boundaries.
Generate this versionStaff highlight
Give moderators a distinct ping when they are mentioned.
Generate this versionRate-limited mentions
Limit mentions per message and per minute to stop targeted spam.
Generate this version
FAQ
What happens when a mention does not match any online player?
Resolve returns null and, when Notify sender about unknown mentions is enabled, the sender gets the Unknown Player lang message with the token they typed. If the token only matches two or more connected players by prefix, the same path runs because the prefix fallback refuses to guess.
Does the plugin block or reword the chat message?
No. OnPlayerChat returns null, so the message is shown exactly as typed. The plugin only adds a private SendReply to the mentioned player and a sound; the sender still sees their own message in shared chat.
Where does the notification sound come from?
The Sound prefab config value defaults to the bundled notice/item.select.fx.prefab effect, played with Effect.server.Run at the target's eye or body position. Any valid prefab path from the game bundle works; an invalid path simply plays nothing.
How do I change the cooldown or turn the sound off?
Edit oxide/config/ChatMentions.json. Cooldown Seconds accepts decimals; 0 disables the limit. Setting Sound prefab to an empty string skips the Effect.server.Run call. Run oxide.reload ChatMentions in the server console afterwards.
How do I install it on Oxide or Carbon?
Drop the compiled ChatMentions.cs into oxide/plugins/ (Carbon reads the same folder) and restart the server or run oxide.reload ChatMentions. The config and language files are generated on first load.
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 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 chat mentions
Customize