Chat Messaging
Chat messaging is the primary communication channel between a RocketMod plugin and the players on an Unturned™ dedicated server. Nearly every plugin sends messages — success confirmations, error alerts, permission denial notices, broadcast announcements, and debug output. This article covers the complete RocketMod API surface for sending and intercepting chat messages, including color formatting, rich text, console output, and the message interception patterns used for chat filtering and logging.
The patterns documented here are drawn from production chat-handling plugins used on 57 Studios™ Horizon Life RP servers. They have been validated against Unturned 3.x with RocketMod 4.x.

Prerequisites
- A working Unturned dedicated server with RocketMod installed. See RocketMod and OpenMod Plugin Basics for setup.
- Visual Studio 2022 with .NET Framework 4.7.2 targeting.
- Familiarity with
UnturnedPlayer,IRocketCommand, and the RocketMod event model. - Basic understanding of the
UnityEngine.Colortype.
What you'll learn
- How to send messages to individual players using
UnturnedChat.Say. - How to broadcast messages to all players on the server.
- How to format message colors using hex color strings and named color strings.
- How to apply rich text formatting (bold, italic, size) to chat messages.
- How to intercept and modify player chat messages through the
OnPlayerChattedevent. - How to send messages from the server console.
- How to implement a chat filter plugin.
- How to avoid common pitfalls: color parsing errors, null players, message truncation.
The UnturnedChat API
The primary messaging API is the static UnturnedChat class from the Rocket.Unturned.Chat namespace. It provides the Say method with multiple overloads for sending messages to individual players, broadcast groups, and the console.
Sending messages to individual players
csharp
using Rocket.Unturned.Chat;
using Rocket.Unturned.Player;
using UnityEngine;
UnturnedPlayer player = UnturnedPlayer.FromName("Lloyd");
// Send a simple message
UnturnedChat.Say(player, "Welcome to the server!");
// Send a message with a color — pass the color as a hex string
UnturnedChat.Say(player, "You have received a reward.", "#FFD700");
// Send a message with a Color object
UnturnedChat.Say(player, "You have been healed.", Color.green);The Say method accepts the color parameter as either a Color object from the UnityEngine namespace or a hex string. When passing a hex string, the format must be a 6-character hexadecimal color code with a leading #:
| Color | Hex string | Color object |
|---|---|---|
| Red | "#FF0000" | Color.red |
| Green | "#00FF00" | Color.green |
| Blue | "#0000FF" | Color.blue |
| Yellow | "#FFFF00" | Color.yellow |
| Cyan | "#00FFFF" | Color.cyan |
| Magenta | "#FF00FF" | Color.magenta |
| White | "#FFFFFF" | Color.white |
| Gray | "#808080" | Color.gray |
The hex string approach is particularly useful when storing colors in plugin configuration files, since hex strings are human-readable and easily editable in XML or JSON configuration formats.
csharp
// Reading a color from configuration and using it in a message
string configColor = Configuration.Instance.SuccessColor; // e.g., "#00FF00"
UnturnedChat.Say(player, "Operation successful.", configColor);Named color resolution
For common color names, you can use the GetColorFromName utility method. This method resolves a color name string to the corresponding Color object:
csharp
Color messageColor = UnturnedChat.GetColorFromName("red", Color.white);The first parameter is the color name. The second is the fallback color if the name is not recognized. The color name matching is case-sensitive — "red" resolves to Color.red, but "Red" or "RED" will not match and will return the fallback color. Always pass color names in lowercase.
| Input | Result |
|---|---|
"red" | Color.red |
"green" | Color.green |
"blue" | Color.blue |
"yellow" | Color.yellow |
| `"cyan" | Color.cyan |
"Red" | Color.white (fallback — case mismatch) |
"RED" | Color.white (fallback — case mismatch) |
"chartreuse" | Color.white (fallback — unknown color) |
Broadcasting to all players
To send a message to every player currently connected to the server, use the broadcast overload of UnturnedChat.Say:
csharp
// Broadcast a server-wide announcement
UnturnedChat.Say("Server restart in 5 minutes.", "#FFA500");
// Broadcast with Color object
UnturnedChat.Say("Welcome to the server!", Color.green);Sending to the console
RocketMod plugins can also send messages to the server console output. This is useful for logging that should be visible to server administrators watching the console window:
csharp
// Send to console only (not in-game chat)
UnturnedChat.Say("[AdminSuite] Plugin loaded successfully.");
// Send to console with a specific player context
UnturnedChat.Say("Player Lloyd disconnected (timeout).");When Say is called with a bare string and no player or color parameters, it routes to the console output by default.
Message interception
The OnPlayerChatted event
RocketMod fires the OnPlayerChatted event whenever a player sends a chat message. This event allows plugins to intercept, modify, or respond to player messages:
csharp
using Rocket.Unturned.Events;
using Rocket.Unturned.Player;
UnturnedPlayerEvents.OnPlayerChatted += (player, message, color) =>
{
Logger.Log($"[CHAT] {player.CharacterName}: {message}");
};The event parameters are:
| Parameter | Type | Purpose |
|---|---|---|
player | UnturnedPlayer | The player who sent the message |
message | ref string | The message text (can be modified) |
color | ref Color | The message color (can be modified) |
The message and color parameters are passed by reference (ref), which means plugin code can modify them. The modified values are used for the chat broadcast. This is the key mechanism for implementing chat filters, formatting plugins, and chat-triggered commands.
Chat filter implementation
The following example implements a basic chat filter that replaces blacklisted words and logs all messages:
csharp
using Rocket.Unturned.Events;
using Rocket.Unturned.Player;
using System;
using System.Collections.Generic;
using System.Linq;
public class ChatFilter
{
private readonly HashSet<string> _blacklist;
private readonly string _replacement;
public ChatFilter(IEnumerable<string> blacklistedWords, string replacement = "****")
{
_blacklist = new HashSet<string>(blacklistedWords, StringComparer.OrdinalIgnoreCase);
_replacement = replacement;
}
public void Attach()
{
UnturnedPlayerEvents.OnPlayerChatted += OnPlayerChatted;
}
public void Detach()
{
UnturnedPlayerEvents.OnPlayerChatted -= OnPlayerChatted;
}
private void OnPlayerChatted(UnturnedPlayer player, ref string message, ref Color color)
{
// Filter blacklisted words
string original = message;
message = _blacklist.Aggregate(message, (current, word) =>
current.Replace(word, _replacement, StringComparison.OrdinalIgnoreCase));
// Log the original and filtered message
if (message != original)
{
Logger.Log($"[CHAT FILTER] {player.CharacterName}: \"{original}\" -> \"{message}\"");
}
else
{
Logger.Log($"[CHAT] {player.CharacterName}: {message}");
}
// Highlight admin messages in a different color
if (R.Permissions.HasPermission(player, "myadminsuite.chat.highlight"))
{
color = Color.cyan;
}
}
}Chat-triggered commands
You can use the OnPlayerChatted event to implement commands that trigger on specific chat patterns without requiring a / prefix:
csharp
UnturnedPlayerEvents.OnPlayerChatted += (player, ref message, ref color) =>
{
if (message.StartsWith("!report ", StringComparison.OrdinalIgnoreCase))
{
string reportText = message.Substring("!report ".Length);
Logger.Log($"[REPORT] {player.CharacterName}: {reportText}");
UnturnedChat.Say(player, "Your report has been submitted.", Color.green);
message = ""; // Suppress the original message from broadcast
color = Color.clear;
}
};Setting message to an empty string and color to Color.clear suppresses the message from the global chat broadcast. The player sees their own message locally (client-side prediction), but no other player sees it.
Rich text formatting
Unturned's chat system supports a subset of rich text formatting through Unity's rich text tags:
| Tag | Effect | Example |
|---|---|---|
<b>...</b> | Bold text | "<b>Warning:</b> Server restart soon" |
<i>...</i> | Italic text | "<i>This is italic</i>" |
<size=X>...</size> | Font size | "<size=20>Large announcement</size>" |
<color=#RRGGBB>...</color> | Inline color | "<color=#FF0000>Red text</color>" |
<mark=#RRGGBBAA>...</mark> | Background highlight | "<mark=#FFFF0080>Highlighted</mark>" |
csharp
// Rich text formatting in chat messages
UnturnedChat.Say(player, "<b>Notice:</b> You have <color=#00FF00>3 new items</color> in your inventory.", Color.white);
// Combined formatting for broadcast announcements
UnturnedChat.Say(
"<size=18><color=#FFD700>Server Event Starting!</color></size>\n" +
"<i>Capture the flag begins in 5 minutes.</i>",
"#FFFFFF"
);Rich text tags can be nested but must be properly closed. Incorrectly nested tags are ignored by the client and displayed as literal text.
Console messaging patterns
Structured console logging
csharp
using Rocket.Core.Logging;
public static class ChatLogger
{
public static void LogInfo(string module, string message)
{
Logger.Log($"[{module}] {message}");
}
public static void LogWarning(string module, string message)
{
Logger.LogWarning($"[{module}] {message}");
}
public static void LogError(string module, string message)
{
Logger.LogError($"[{module}] {message}");
}
}Echo commands to console
For admin commands, it is good practice to echo the command invocation to the console for the audit trail:
csharp
public void Execute(IRocketPlayer caller, string[] command)
{
string callerName = caller?.DisplayName ?? "Console";
string args = string.Join(" ", command);
Logger.Log($"[CMD] {callerName} executed /{Name} {args}");
// Command body...
}Implementation: Chat relay plugin
The following is a complete RocketMod plugin that relays in-game chat to a console log file and provides a /announce broadcast command:
csharp
using Rocket.API;
using Rocket.Core.Plugins;
using Rocket.Unturned.Chat;
using Rocket.Unturned.Events;
using Rocket.Unturned.Player;
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
namespace ChatRelay
{
public class ChatRelayPlugin : RocketPlugin<ChatRelayConfiguration>
{
public static ChatRelayPlugin Instance { get; private set; }
private StreamWriter _logWriter;
protected override void Load()
{
Instance = this;
UnturnedPlayerEvents.OnPlayerChatted += OnPlayerChatted;
InitializeLogFile();
Logger.Log("[ChatRelay] Loaded");
}
protected override void Unload()
{
UnturnedPlayerEvents.OnPlayerChatted -= OnPlayerChatted;
_logWriter?.Dispose();
Instance = null;
Logger.Log("[ChatRelay] Unloaded");
}
private void InitializeLogFile()
{
string logDir = Path.Combine(Server.Instance.ServerDirectory, "ChatLogs");
Directory.CreateDirectory(logDir);
string logPath = Path.Combine(logDir, $"chat-{DateTime.UtcNow:yyyy-MM-dd}.log");
_logWriter = new StreamWriter(logPath, append: true);
_logWriter.AutoFlush = true;
}
private void OnPlayerChatted(UnturnedPlayer player, ref string message, ref Color color)
{
string timestamp = DateTime.UtcNow.ToString("HH:mm:ss");
_logWriter.WriteLine($"[{timestamp}] {player.CharacterName} ({player.CSteamID}): {message}");
Logger.Log($"[CHAT] {player.CharacterName}: {message}");
}
}
public class ChatRelayConfiguration : IRocketPluginConfiguration
{
public void LoadDefaults() { }
}
}
namespace ChatRelay.Commands
{
public class AnnounceCommand : IRocketCommand
{
public string Name => "announce";
public string Help => "Broadcast an announcement to all players.";
public string Syntax => "/announce <message>";
public List<string> Aliases => new List<string>();
public List<string> Permissions => new List<string> { "chatrelay.announce" };
public AllowedCaller AllowedCaller => AllowedCaller.Both;
public void Execute(IRocketPlayer caller, string[] command)
{
if (command.Length < 1)
{
UnturnedChat.Say(caller, "Usage: /announce <message>", "#FF0000");
return;
}
string message = string.Join(" ", command);
string formatted = $"<size=18><color=#FFD700>[ANNOUNCEMENT]</color></size>\n{message}";
UnturnedChat.Say(formatted, "#FFFFFF");
string callerName = caller?.DisplayName ?? "Console";
Logger.Log($"[ANNOUNCE] {callerName}: {message}");
}
}
}Color management utilities
For plugins that manage multiple color configurations, a color utility class helps keep color handling consistent:
csharp
using UnityEngine;
using System;
public static class ColorUtility
{
public static Color FromHex(string hex)
{
if (string.IsNullOrEmpty(hex))
return Color.white;
hex = hex.TrimStart('#');
if (hex.Length != 6)
return Color.white;
try
{
byte r = Convert.ToByte(hex.Substring(0, 2), 16);
byte g = Convert.ToByte(hex.Substring(2, 2), 16);
byte b = Convert.ToByte(hex.Substring(4, 2), 16);
return new Color32(r, g, b, 255);
}
catch
{
return Color.white;
}
}
public static string ToHex(Color color)
{
Color32 c = (Color32)color;
return $"#{c.r:X2}{c.g:X2}{c.b:X2}";
}
}Common errors and diagnostics
| Symptom | Cause | Resolution |
|---|---|---|
FormatException when passing hex color string | Malformed hex string — missing #, wrong length, or non-hex characters | Use exactly "#RRGGBB" format with 6 hex digits after the # |
GetColorFromName returns fallback color for "Red" | The method is case-sensitive — use lowercase color names | Always pass color names in lowercase: "red", "green", "blue" |
| Message not visible to other players after interception | message set to empty string or color set to Color.clear | This is intentional for suppressing messages; verify the suppression logic |
| Rich text tags displayed as literal text in chat | Tags not properly closed or nested incorrectly | Verify all <tag> has a corresponding </tag> |
NullReferenceException on UnturnedChat.Say with player parameter | Player object is null or disconnected | Check player != null && player.IsConnected before calling Say |
| Broadcast message appears twice | Plugin subscribed to OnPlayerChatted and also calls Say — causes echo | Do not call Say inside an OnPlayerChatted handler for the same message |
| Very long messages truncated in chat | Unturned client truncates messages at ~200 characters | Keep messages under 200 characters, or use multiple Say calls for longer text |
| Color appears wrong on some clients | Some clients have color-blind mode or custom UI | Test messages in multiple lighting conditions |
Console shows [CHAT] prefix for messages sent from Say | RocketMod automatically prefixes console messages | This is expected behavior; do not add your own prefix |
Plugin crash when calling Say in OnPlayerDisconnected | Player is already disconnected and their UnturnedPlayer instance is stale | Guard with player.IsConnected before sending |
Server announcement scheduling
The following plugin component broadcasts timed announcements on an interval, reading messages from a configuration list:
csharp
using System.Collections;
using System.Collections.Generic;
public class AnnouncementScheduler
{
private readonly List<string> _messages;
private readonly float _intervalSeconds;
private Coroutine _coroutine;
public AnnouncementScheduler(List<string> messages, float intervalSeconds)
{
_messages = messages;
_intervalSeconds = intervalSeconds;
}
public void Start()
{
_coroutine = AdminSuitePlugin.Instance.StartCoroutine(AnnouncementLoop());
}
public void Stop()
{
if (_coroutine != null)
{
AdminSuitePlugin.Instance.StopCoroutine(_coroutine);
}
}
private IEnumerator AnnouncementLoop()
{
int index = 0;
while (true)
{
yield return new WaitForSeconds(_intervalSeconds);
string message = _messages[index % _messages.Count];
UnturnedChat.Say($"<color=#FFD700>[SERVER]</color> {message}");
index++;
}
}
}Chat logging to external services
For servers that need chat logs accessible outside the game server (web dashboard, Discord bot, moderation panel), implement an external log writer:
csharp
using System.Net.Http;
using System.Text;
public static class ExternalChatLogger
{
private static readonly HttpClient _client = new HttpClient();
private static string _webhookUrl;
public static void Configure(string webhookUrl)
{
_webhookUrl = webhookUrl;
}
public static async void LogChat(string playerName, ulong steamId, string message)
{
if (string.IsNullOrEmpty(_webhookUrl)) return;
try
{
string json = System.Text.Json.JsonSerializer.Serialize(new
{
player = playerName,
steam_id = steamId.ToString(),
message = message,
timestamp = DateTime.UtcNow.ToString("o")
});
var content = new StringContent(json, Encoding.UTF8, "application/json");
await _client.PostAsync(_webhookUrl, content);
}
catch (Exception ex)
{
Logger.LogError($"[ChatLogger] Failed to send chat log: {ex.Message}");
}
}
}Chat message routing by channel
For servers with multiple chat channels (global, local, faction, admin), implement a routing system:
csharp
public enum ChatChannel
{
Global,
Local,
Faction,
Admin
}
public static class ChatRouter
{
private static readonly Dictionary<ChatChannel, List<ulong>> _channelSubscriptions =
new Dictionary<ChatChannel, List<ulong>>();
public static void RouteMessage(UnturnedPlayer sender, string message, ChatChannel channel, float localRadius = 50f)
{
switch (channel)
{
case ChatChannel.Global:
UnturnedChat.Say($"<color=#FFFFFF>[G] {sender.CharacterName}: {message}</color>");
break;
case ChatChannel.Local:
foreach (UnturnedPlayer player in GetPlayersInRadius(sender.Position, localRadius))
{
UnturnedChat.Say(player, $"<color=#AAAAAA>[L] {sender.CharacterName}: {message}</color>", Color.gray);
}
break;
case ChatChannel.Admin:
foreach (UnturnedPlayer player in GetAllPlayers())
{
if (R.Permissions.HasPermission(player, "myadminsuite.chat.admin"))
{
UnturnedChat.Say(player, $"<color=#FF5555>[A] {sender.CharacterName}: {message}</color>", Color.red);
}
}
break;
}
}
private static List<UnturnedPlayer> GetPlayersInRadius(Vector3 center, float radius)
{
var players = new List<UnturnedPlayer>();
foreach (SteamPlayer client in Provider.clients)
{
UnturnedPlayer player = UnturnedPlayer.FromSteamPlayer(client);
if (Vector3.Distance(player.Position, center) <= radius)
players.Add(player);
}
return players;
}
private static List<UnturnedPlayer> GetAllPlayers()
{
var players = new List<UnturnedPlayer>();
foreach (SteamPlayer client in Provider.clients)
players.Add(UnturnedPlayer.FromSteamPlayer(client));
return players;
}
}Frequently asked questions
Can I send a private message between two players?
RocketMod does not provide a private-message API. To implement private messaging, intercept messages via OnPlayerChatted, check for a /pm or /whisper prefix, and send the message to only the target player using UnturnedChat.Say with the target player's UnturnedPlayer instance. Suppress the original message by setting message to empty string.
How do I make clickable links in chat?
Unturned's chat system does not support clickable links. You can send a URL as plain text, and players must manually copy it. Some RocketMod plugins implement link-shortening that displays a shortened URL in chat, but the client will still not make it clickable.
What is the maximum message length?
Unturned's chat protocol has a practical limit of approximately 200 characters per message. Messages longer than this are truncated on the receiving client. For announcements longer than 200 characters, split the message across multiple Say calls with a 100ms delay between them.
Can I format a message differently for each player?
Yes. Call UnturnedChat.Say individually for each target player with their own formatted message. There is no API to send personalized messages in a single broadcast call.
How do I suppress chat messages from a specific player?
In the OnPlayerChatted handler, check the player's Steam64 ID or permission set, and set message = "" and color = Color.clear to suppress the broadcast. Note that the player who sent the message will still see it in their own chat (client-side prediction).
Does Say work from a background thread?
No. UnturnedChat.Say and all Unity/Unturned API calls must be called from the main Unity thread. Calling Say from a background thread (e.g., a Task.Run callback) will cause a crash or undefined behavior. Use Unity's SynchronizationContext or RocketMod's task scheduler to marshal calls to the main thread.
Cross-references
- RocketMod and OpenMod Plugin Basics — plugin lifecycle, event subscription, permission system.
- Vehicles — the previous article; vehicle management.
- Triggering Effects — the next article; visual and audio effects.
- Teleportation — player and vehicle teleportation.
- Kick, Ban, and Admin Controls — moderation commands that use chat messaging for feedback.
- Server Commands Reference — built-in messaging commands (
say,broadcast).
Document history
| Version | Date | Author | Notes |
|---|---|---|---|
| 1.0 | 2025-07-27 | 57 Studios | Initial publication. UnturnedChat.Say API, color formatting, hex string colors, message interception, chat filter patterns, rich text formatting, and diagnostics. |
