How to make a Rust chat titles and colors plugin (Oxide group tags)
Replaces vanilla global chat with formatted lines: a group tag plus separate name and message colors picked by the player's highest-priority Oxide group, keeping the speaker's avatar. Uses OnPlayerChat to cancel the original message and send the formatted one.
Vanilla Rust chat is a flat line of white text: no rank tag, no color, no way to tell staff from players at a glance. Admins usually end up pasting tags into player names, which breaks name lookups and looks the same to everyone.
This plugin intercepts chat with OnPlayerChat, cancels the original global message and sends its own formatted line through the speaker. Formatting is resolved from the player's Oxide groups: each group in the config has a priority and its own tag, name color and message color, and the highest-priority group the player belongs to wins. Players in no configured group fall back to the Default group block. The line is sent with chat.add through the player, so it keeps the player's avatar and shows the tag before the name. Admins get an optional marker, and the whole line is built from one editable Chat format string.
The plugin code
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Libraries.Covalence;
namespace Oxide.Plugins
{
[Info("Better Chat Titles", "RustPluginGenerator", "1.0.0")]
[Description("Formats chat lines with an Oxide group tag and colors, keeping the player's avatar.")]
public class BetterChatTitles : RustPlugin
{
private const string PermAdmin = "betterchattitles.admin";
private PluginConfig config;
private class GroupFormat
{
[JsonProperty("Priority")]
public int Priority { get; set; }
[JsonProperty("Tag")]
public string Tag { get; set; }
[JsonProperty("Name color")]
public string NameColor { get; set; }
[JsonProperty("Message color")]
public string MessageColor { get; set; }
}
private class PluginConfig
{
[JsonProperty("Chat format")]
public string Format { get; set; }
[JsonProperty("Show admin marker")]
public bool ShowAdminMarker { get; set; }
[JsonProperty("Admin marker")]
public string AdminMarker { get; set; }
[JsonProperty("Default group")]
public GroupFormat Default { get; set; }
[JsonProperty("Groups")]
public Dictionary<string, GroupFormat> Groups { get; set; }
}
protected override void LoadDefaultConfig()
{
config = new PluginConfig
{
Format = "{tag}{name}: {message}",
ShowAdminMarker = true,
AdminMarker = "[ADMIN] ",
Default = new GroupFormat
{
Priority = 0,
Tag = string.Empty,
NameColor = "#FFFFFF",
MessageColor = "#EEEEEE"
},
Groups = new Dictionary<string, GroupFormat>
{
["vip"] = new GroupFormat { Priority = 10, Tag = "[VIP] ", NameColor = "#FFD166", MessageColor = "#FFFFFF" },
["moderator"] = new GroupFormat { Priority = 50, Tag = "[MOD] ", NameColor = "#66CCFF", MessageColor = "#E6F7FF" },
["admin"] = new GroupFormat { Priority = 100, Tag = "[ADMIN] ", NameColor = "#FF6666", MessageColor = "#FFE6E6" }
}
};
}
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>
{
["FormatInfo"] = "Your tag: '{0}' | name color: {1} | message color: {2}\nTemplate: {3}"
}, 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(PermAdmin, this);
}
private object OnPlayerChat(BasePlayer player, string message, ConVar.Chat.ChatChannel channel)
{
if (player == null || string.IsNullOrEmpty(message)) return null;
if (channel != ConVar.Chat.ChatChannel.Global) return null;
message = message.Trim();
if (message.Length == 0) return null;
var format = ResolveFormat(player);
string tag = format.Tag ?? string.Empty;
if (config.ShowAdminMarker && player.IsAdmin)
tag = (config.AdminMarker ?? string.Empty) + tag;
string line = (config.Format ?? "{tag}{name}: {message}")
.Replace("{tag}", tag)
.Replace("{name}", Colorize(player.displayName, format.NameColor))
.Replace("{message}", Colorize(message, format.MessageColor));
player.SendConsoleCommand("chat.add", 0, player.userID.Get(), line);
return true;
}
private GroupFormat ResolveFormat(BasePlayer player)
{
GroupFormat best = null;
var groups = permission.GetUserGroups(player.UserIDString);
if (groups != null && config.Groups != null)
{
foreach (var group in groups)
{
if (string.IsNullOrEmpty(group)) continue;
GroupFormat entry;
if (!config.Groups.TryGetValue(group, out entry) || entry == null) continue;
if (best == null || entry.Priority > best.Priority) best = entry;
}
}
return best ?? config.Default ?? new GroupFormat
{
Priority = 0,
Tag = string.Empty,
NameColor = "#FFFFFF",
MessageColor = "#EEEEEE"
};
}
private static string Colorize(string text, string color)
{
if (string.IsNullOrEmpty(color)) return text;
return string.Format("<color={0}>{1}</color>", color, text);
}
[ChatCommand("chatformat")]
private void CmdChatFormat(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, PermAdmin))
{
player.ChatMessage("You do not have permission to use this command.");
return;
}
var format = ResolveFormat(player);
player.ChatMessage(Msg("FormatInfo", player.UserIDString,
format.Tag ?? string.Empty,
format.NameColor ?? "none",
format.MessageColor ?? "none",
config.Format ?? "{tag}{name}: {message}"));
}
}
}Hooks used: OnPlayerChat
Config file
Created automatically on first load at oxide/config/BetterChatTitles.json.
{
"Chat format": "{tag}{name}: {message}",
"Show admin marker": true,
"Admin marker": "[ADMIN] ",
"Default group": {
"Priority": 0,
"Tag": "",
"Name color": "#FFFFFF",
"Message color": "#EEEEEE"
},
"Groups": {
"vip": {
"Priority": 10,
"Tag": "[VIP] ",
"Name color": "#FFD166",
"Message color": "#FFFFFF"
},
"moderator": {
"Priority": 50,
"Tag": "[MOD] ",
"Name color": "#66CCFF",
"Message color": "#E6F7FF"
},
"admin": {
"Priority": 100,
"Tag": "[ADMIN] ",
"Name color": "#FF6666",
"Message color": "#FFE6E6"
}
}
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/chatformat | Shows the caller's resolved tag, name color and message color plus the current chat format template. | betterchattitles.admin |
| Permission | Grants |
|---|---|
betterchattitles.admin | Grants /chatformat for inspecting the active formatting rule |
How the code works
Cancel only the global line
OnPlayerChat(BasePlayer player, string message, Chat.ChatChannel channel)returns an object. When the channel is notChat.ChatChannel.Globalthe plugin returnsnull, so other channels are handled by the game as usual. For global chat it trims the message, ignores empty input, sends the formatted replacement and returnstrue, which tells Oxide to skip the original chat handling so only one line appears.Resolve the player's formatting rule
permission.GetUserGroups(player.UserIDString)returns the Oxide groups the player is in.ResolveFormatloops those group names, looks each up in theGroupsdictionary of the config and keeps the entry with the highestPriority. If no group matches, it returns theDefault groupblock. If that is missing too, a hard-coded white fallback is used, so ungrouped players still get a readable line.Build the line from a template
The
Chat formatconfig value contains{tag},{name}and{message}placeholders.Colorizewraps a string in<color=#hex></color>using the entry'sName colorandMessage color, and does nothing when the color field is empty. TheTagvalue is inserted as-is, brackets and spacing included, so an empty tag simply leaves the placeholder blank while the rest of the format stays intact.Keep the player's avatar
Instead of a server-wide broadcast, the plugin calls
player.SendConsoleCommand("chat.add", 0, player.userID.Get(), line). The game renders that exactly as if the player had spoken, so the message carries the speaker's avatar. This is why the hook returns a non-null value: the client must not also receive the original chat entry.Admin marker and a config inspection command
When
Show admin markeris true andplayer.IsAdminis set, the configuredAdmin markeris prefixed to the tag./chatformatreplies throughMsgandlangwith the caller's resolved tag, colors and the format template, which makes tuning priorities and hex codes quick without joining as a test player. The command is gated by thebetterchattitles.adminpermission registered inInit.
Install it on your server
Save BetterChatTitles.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/BetterChatTitles.json(Carbon:carbon/configs/BetterChatTitles.json). After editing, reload it:oxide.reload BetterChatTitles c.reload BetterChatTitles # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default betterchattitles.admin c.grant group default betterchattitles.admin # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Team-colored tags
Derive the tag color from the player's team instead of the group entry.
Generate this versionSlow mode per group
Add a per-group chat cooldown stored in config.
Generate this versionMute integration
Block chat for players listed in a local mute file.
Generate this version
FAQ
Why does the plugin return true from OnPlayerChat?
A non-null return value from OnPlayerChat suppresses the game's own chat handling. The plugin sends its own line through the player with chat.add, so returning null would show both the vanilla line and the formatted one. Channels other than Global return null on purpose so the game keeps handling them.
Which group wins when a player is in several groups?
The configured group with the highest Priority value. Priorities are plain integers, so staff groups get high numbers and default ranks get low ones. A player in two groups with the same priority gets whichever one permission.GetUserGroups returns first, so keep priorities unique inside the Groups dictionary.
Why does the message still show without a tag?
The Tag value in a group entry includes its own brackets and spacing. Setting it to an empty string leaves the {tag} placeholder blank but keeps the rest of the Chat format string intact. If you remove the {tag} placeholder from the format entirely, tags are skipped.
Does this work with Carbon?
Yes. Carbon loads the same .cs file and provides the same OnPlayerChat hook and permission API. Drop the file in the plugins folder, let it compile, then edit oxide/config/BetterChatTitles.json and reload with oxide.reload BetterChatTitles.
Are the color values validated?
No. Colorize writes the string into <color=...> exactly as configured and skips the tag when the field is empty or null. Use values the client accepts, such as six-digit hex like #FF6666. A malformed value is passed through unchanged and may render as plain text.
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 chat titles and colors
Customize