Skip to content

Player Chat

Player chat is one of the most frequently intercepted events in RocketMod plugins. Chat messages are the primary way players interact with commands, communicate with each other, and trigger automated responses. RocketMod exposes the OnPlayerChatted event, which gives plugin developers control over every message sent through the server's chat system.

This article covers the OnPlayerChatted event in detail, including its signature, parameter usage, common patterns for chat filtering and formatting, how to implement custom chat commands, and best practices for chat-related plugins.

57 Studios operates multiple Unturned servers with custom chat formatting, chat-based commands, and automated chat moderation. The patterns documented here are drawn from production experience managing player chat across communities of varying sizes.

Prerequisites

  • A working RocketMod installation. See RocketMod and OpenMod Plugin Basics.
  • Visual Studio with C# development workload.
  • Familiarity with RocketMod event subscription patterns.
  • Understanding of ref and out parameters in C#.

What you'll learn

  • The exact signature of the OnPlayerChatted event and what each parameter does.
  • How to intercept, modify, and cancel chat messages.
  • How to implement chat-based commands using the chat event.
  • How to add chat formatting (colors, prefixes, badges).
  • How to implement chat cooldowns and spam protection.
  • Best practices for chat event handling in production.

The OnPlayerChatted event

OnPlayerChatted is a static event on the UnturnedPlayerEvents class. It fires every time a player sends a chat message, before the message is broadcast to other players.

Event signature

csharp
public static event OnPlayerChatted OnPlayerChatted;

public delegate void OnPlayerChatted(
    ref Color color,
    ref bool cancel
);

The event delegate takes two parameters, both passed by reference:

  • ref Color color — The color the chat message will be displayed in. Changing this value changes the message color for all recipients.
  • ref bool cancel — Set to true to prevent the message from being broadcast. The message is suppressed entirely.

The player who sent the message is available through the calling context. In practice, the event is invoked with the UnturnedPlayer sender, and the player is accessed through the event subscription's closure or through a cast from a shared context.

Parameter order

The parameters are ordered as follows in the delegate signature:

  1. ref Color color — The chat color (reference, modifiable).
  2. ref bool cancel — Whether to cancel the message (reference, modifiable).

This means the color parameter comes first and the cancel flag comes second. When implementing the handler, your method signature must match this order exactly.

csharp
// CORRECT parameter order:
private void OnPlayerChatted(ref Color color, ref bool cancel)
{
    // color is the first parameter (ref Color)
    // cancel is the second parameter (ref bool)
}

// WRONG parameter order — the compiler interprets the first
// ref parameter as the Color and the second as the cancel flag.
// If you swap them, you get a compile error because Color and bool
// are not interchangeable types.
private void OnPlayerChatted(ref bool cancel, ref Color color)
{
    // This does NOT compile — the signatures do not match.
    // See the table below for which parameter does what.
}

Since ref Color and ref bool are different types, swapping them causes a compile error. Be sure to match the delegate signature exactly when writing the handler.

Parameter reference

ParameterTypeDirectionPurpose
First parameterref ColorRead/writeThe color of the chat message. Default is Color.white. Modify to change the message color.
Second parameterref boolRead/writeWhether to cancel the message. Set to true to suppress the broadcast. Default is false.

The cancel flag controls message suppression. When cancel is set to true, the message is not broadcast to other players. The player who sent the message does not see their own message either. Set cancel to true when the message was intercepted for a command, or when it violates chat rules and should be suppressed.

Basic chat interception

The following example demonstrates a minimal chat interception plugin that logs all messages and prevents messages containing banned words.

csharp
using Rocket.API;
using Rocket.Core.Plugins;
using Rocket.Unturned.Events;
using Rocket.Unturned.Player;
using System.Collections.Generic;
using UnityEngine;

namespace ChatLogger
{
    public class ChatLoggerPlugin : RocketPlugin<ChatLoggerConfiguration>
    {
        public static ChatLoggerPlugin Instance { get; private set; }

        protected override void Load()
        {
            Instance = this;
            UnturnedPlayerEvents.OnPlayerChatted += OnPlayerChatted;
            Rocket.Core.Logging.Logger.Log("ChatLogger loaded.");
        }

        protected override void Unload()
        {
            UnturnedPlayerEvents.OnPlayerChatted -= OnPlayerChatted;
            Instance = null;
            Rocket.Core.Logging.Logger.Log("ChatLogger unloaded.");
        }

        private void OnPlayerChatted(ref Color color, ref bool cancel)
        {
            // Log all chat messages
            Rocket.Core.Logging.Logger.Log(
                $"CHAT: {Player?.DisplayName ?? "Unknown"}: {message}"
            );

            // Note: The full UnturnedPlayer is obtained through
            // the event's calling context or by caching the player.
        }
    }
}

The event handler signature uses ref Color and ref bool as described above. The color parameter is modifiable, and the cancel parameter suppresses the message when set to true.

Accessing the player and message

The OnPlayerChatted delegate signature does not include the player or the message text in its parameters. These are obtained through RocketMod's internal event invocation pattern. The standard approach is to capture the player reference from the calling code or use the broader UnturnedPlayerEvents context.

In practice, the player can be accessed through alternative patterns such as subscribing to the lower-level chat event or by storing a reference to the player. The following pattern shows the practical approach used in production plugins:

csharp
using Rocket.Unturned.Player;
using Rocket.Unturned.Events;
using SDG.Unturned;
using UnityEngine;

public class ChatHandler
{
    private void OnChatted(SteamPlayer steamPlayer, EChatMode mode, ref Color color, ref bool cancel, string message)
    {
        var player = UnturnedPlayer.FromSteamPlayer(steamPlayer);

        Rocket.Core.Logging.Logger.Log(
            $"[{mode}] {player.DisplayName}: {message}"
        );
    }
}

For the OnPlayerChatted event on UnturnedPlayerEvents, the handler signature uses only the color and cancel parameters. The player and message text are obtained through the UnturnedChat class's lower-level events or through the ChatManager.onChatted event from SDG.Unturned.

Modifying chat color

Changing the chat color based on the player's group is one of the most common chat customizations:

csharp
private void OnPlayerChatted(ref Color color, ref bool cancel)
{
    // Determine the player's group and apply color
    if (playerHasPermission("group.admin"))
    {
        color = Color.red;
    }
    else if (playerHasPermission("group.moderator"))
    {
        color = Color.magenta;
    }
    else if (playerHasPermission("group.vip"))
    {
        color = Color.yellow;
    }
    else
    {
        color = Color.white;
    }
}

Supported color values

Unity's Color struct supports both named colors and custom RGB values:

csharp
// Named colors
color = Color.red;
color = Color.green;
color = Color.blue;
color = Color.yellow;
color = Color.magenta;
color = Color.cyan;
color = Color.white;
color = Color.gray;
color = Color.black;

// Custom RGB (values 0.0 to 1.0)
color = new Color(0.8f, 0.2f, 0.1f);  // Custom red
color = new Color(0.1f, 0.8f, 0.3f);  // Custom green
color = new Color(0.5f, 0.3f, 0.8f);  // Custom purple

Chat commands

Many plugins use the OnPlayerChatted event to implement chat-based commands that begin with a prefix character:

csharp
private void OnPlayerChatted(ref Color color, ref bool cancel)
{
    string message = GetPlayerMessage(); // Obtained from context

    if (message.StartsWith("!"))
    {
        cancel = true; // Suppress the chat message
        string[] parts = message.Substring(1).Split(' ');
        string commandName = parts[0].ToLower();
        string[] args = parts.Length > 1
            ? parts.Skip(1).ToArray()
            : new string[0];

        HandleChatCommand(player, commandName, args);
    }
}

private void HandleChatCommand(UnturnedPlayer player, string command, string[] args)
{
    switch (command)
    {
        case "help":
            player.SendChat("Available commands: !help, !discord, !rules, !report");
            break;
        case "discord":
            player.SendChat("Join our Discord: discord.gg/57studios");
            break;
        case "rules":
            player.SendChat("Server rules: 1. No griefing. 2. Respect others. 3. Have fun.");
            break;
        case "report":
            if (args.Length > 0)
            {
                string report = string.Join(" ", args);
                ForwardReportToStaff(player, report);
                player.SendChat("Report sent to staff.", Color.green);
            }
            else
            {
                player.SendChat("Usage: !report <reason>", Color.red);
            }
            break;
        default:
            player.SendChat($"Unknown command: {command}. Type !help for available commands.", Color.red);
            break;
    }
}

Chat filtering and moderation

Spam detection

csharp
private readonly Dictionary<ulong, DateTime> _lastMessageTime =
    new Dictionary<ulong, DateTime>();
private const float SPAM_THRESHOLD_SECONDS = 2f;

private void OnPlayerChatted(ref Color color, ref bool cancel)
{
    ulong steamId = player.CSteamID.m_SteamID;
    DateTime now = DateTime.UtcNow;

    if (_lastMessageTime.TryGetValue(steamId, out DateTime lastTime))
    {
        if ((now - lastTime).TotalSeconds < SPAM_THRESHOLD_SECONDS)
        {
            cancel = true;
            player.SendChat("Please slow down. You are sending messages too quickly.", Color.red);
            return;
        }
    }

    _lastMessageTime[steamId] = now;
}

Profanity filter

csharp
private readonly List<string> _bannedWords = new List<string>
{
    "badword1",
    "badword2",
    "badword3"
};

private void OnPlayerChatted(ref Color color, ref bool cancel)
{
    string message = GetPlayerMessage().ToLower();

    foreach (string word in _bannedWords)
    {
        if (message.Contains(word))
        {
            cancel = true;
            player.SendChat("Your message was blocked for containing inappropriate language.", Color.red);
            Rocket.Core.Logging.Logger.Log(
                $"BLOCKED: {player.DisplayName} tried to send: {GetPlayerMessage()}"
            );
            return;
        }
    }
}

Message length limits

csharp
private void OnPlayerChatted(ref Color color, ref bool cancel)
{
    string message = GetPlayerMessage();

    if (message.Length > 200)
    {
        cancel = true;
        player.SendChat("Your message is too long. Maximum 200 characters.", Color.red);
    }
}

Complete chat plugin example

The following plugin demonstrates chat formatting, command handling, spam protection, and chat logging all integrated into a single plugin.

ChatManagerPlugin.cs:

csharp
using Rocket.API;
using Rocket.Core.Plugins;
using Rocket.Unturned.Events;
using Rocket.Unturned.Player;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

namespace ChatManager
{
    public class ChatManagerPlugin : RocketPlugin<ChatManagerConfiguration>
    {
        public static ChatManagerPlugin Instance { get; private set; }

        // Spam tracking
        private readonly Dictionary<ulong, DateTime> _lastMessageTime =
            new Dictionary<ulong, DateTime>();
        private const float SPAM_THRESHOLD = 2f;

        // Message history for duplicate detection
        private readonly Dictionary<ulong, string> _lastMessage =
            new Dictionary<ulong, string>();

        protected override void Load()
        {
            Instance = this;
            UnturnedPlayerEvents.OnPlayerChatted += OnPlayerChatted;
            Rocket.Core.Logging.Logger.Log("ChatManager loaded.");
        }

        protected override void Unload()
        {
            UnturnedPlayerEvents.OnPlayerChatted -= OnPlayerChatted;
            _lastMessageTime.Clear();
            _lastMessage.Clear();
            Instance = null;
            Rocket.Core.Logging.Logger.Log("ChatManager unloaded.");
        }

        private void OnPlayerChatted(ref Color color, ref bool cancel)
        {
            string message = GetPlayerMessage();
            ulong steamId = player.CSteamID.m_SteamID;

            // 1. Spam check
            DateTime now = DateTime.UtcNow;
            if (_lastMessageTime.TryGetValue(steamId, out DateTime lastTime))
            {
                if ((now - lastTime).TotalSeconds < SPAM_THRESHOLD)
                {
                    cancel = true;
                    player.SendChat("Please wait before sending another message.", Color.red);
                    return;
                }
            }
            _lastMessageTime[steamId] = now;

            // 2. Duplicate message check
            if (_lastMessage.TryGetValue(steamId, out string lastMsg) &&
                lastMsg.Equals(message, StringComparison.OrdinalIgnoreCase))
            {
                cancel = true;
                player.SendChat("Please do not send duplicate messages.", Color.red);
                return;
            }
            _lastMessage[steamId] = message;

            // 3. Chat command handling
            if (message.StartsWith("!"))
            {
                cancel = true;
                HandleChatCommand(player, message);
                return;
            }

            // 4. Color by permission
            if (R.Permissions.HasPermission(player, "chatmanager.color.admin"))
            {
                color = Color.red;
            }
            else if (R.Permissions.HasPermission(player, "chatmanager.color.mod"))
            {
                color = Color.magenta;
            }
            else if (R.Permissions.HasPermission(player, "chatmanager.color.vip"))
            {
                color = Color.yellow;
            }

            // 5. Log the message
            Rocket.Core.Logging.Logger.Log(
                $"CHAT [{player.DisplayName}]: {message}"
            );
        }

        private void HandleChatCommand(UnturnedPlayer player, string message)
        {
            string[] parts = message.Substring(1).Split(' ');
            string cmd = parts[0].ToLower();
            string[] args = parts.Length > 1
                ? parts.Skip(1).ToArray()
                : new string[0];

            switch (cmd)
            {
                case "help":
                    player.SendChat("Commands: !help, !discord, !rules, !report <text>, !players", Color.cyan);
                    break;
                case "discord":
                    player.SendChat("Discord: discord.gg/57studios", Color.cyan);
                    break;
                case "rules":
                    player.SendChat("Rules: 57studios.net/rules", Color.cyan);
                    break;
                case "report":
                    if (args.Length > 0)
                    {
                        string report = string.Join(" ", args);
                        ForwardToStaff(player, report);
                        player.SendChat("Your report has been sent to online staff.", Color.green);
                    }
                    else
                    {
                        player.SendChat("Usage: !report <description of issue>", Color.red);
                    }
                    break;
                case "players":
                    int count = Provider.clients.Count;
                    player.SendChat($"Players online: {count}", Color.cyan);
                    break;
                default:
                    string suggestion = FindClosestCommand(cmd);
                    if (suggestion != null)
                        player.SendChat($"Unknown command '{cmd}'. Did you mean !{suggestion}?", Color.red);
                    else
                        player.SendChat($"Unknown command '{cmd}'. Type !help for available commands.", Color.red);
                    break;
            }
        }

        private string FindClosestCommand(string cmd)
        {
            var knownCommands = new[] { "help", "discord", "rules", "report", "players" };
            return knownCommands
                .Where(c => c.StartsWith(cmd[0].ToString()))
                .OrderBy(c => LevenshteinDistance(cmd, c))
                .FirstOrDefault();
        }

        private int LevenshteinDistance(string a, string b)
        {
            // Simple Levenshtein distance for command suggestions
            int[,] d = new int[a.Length + 1, b.Length + 1];
            for (int i = 0; i <= a.Length; i++) d[i, 0] = i;
            for (int j = 0; j <= b.Length; j++) d[0, j] = j;
            for (int i = 1; i <= a.Length; i++)
                for (int j = 1; j <= b.Length; j++)
                    d[i, j] = Mathf.Min(
                        Mathf.Min(d[i - 1, j] + 1, d[i, j - 1] + 1),
                        d[i - 1, j - 1] + (a[i - 1] == b[j - 1] ? 0 : 1)
                    );
            return d[a.Length, b.Length];
        }

        private void ForwardToStaff(UnturnedPlayer reporter, string report)
        {
            foreach (var client in Provider.clients)
            {
                var staffPlayer = UnturnedPlayer.FromSteamPlayer(client);
                if (R.Permissions.HasPermission(staffPlayer, "chatmanager.staff.reports"))
                {
                    staffPlayer.SendChat(
                        $"REPORT from {reporter.DisplayName}: {report}",
                        Color.magenta
                    );
                }
            }
        }

        private string GetPlayerMessage()
        {
            // In practice, this is obtained through the event context
            return string.Empty;
        }
    }
}

Chat command vs IRocketCommand

RocketMod supports both chat-based commands (via the OnPlayerChatted event) and registered commands (via IRocketCommand). The choice depends on the use case:

AspectChat commands (!help)IRocketCommand (/help)
RegistrationManual parsing in eventAutomatic via command system
Permission checksManualAutomatic via Permissions list
Syntax helpManualAutomatic via Help and Syntax
Tab completionNot supportedSupported
Console executionNot supportedSupported via AllowedCaller
Auto-generated helpNoYes, via /help

For simple player-facing commands, IRocketCommand is recommended. Chat commands are useful for:

  • Commands that should not appear in the /help list.
  • Commands that need to process the raw message text.
  • Integration with external systems that use custom prefixes.

Chat formatting best practices

Keep messages concise

Unturned's chat display has limited width. Messages longer than approximately 100 characters wrap to multiple lines, which can be visually disruptive. Keep automated messages under 80 characters when possible.

Use color meaningfully

Establish a server-wide color convention and stick to it:

ColorTypical meaning
WhiteNormal player chat
CyanServer information
GreenSuccess, confirmation
RedError, warning, admin
YellowSystem notice
MagentaStaff communication

Avoid chat spam

Sending multiple messages in rapid succession (for example, a multi-line command output) should use a single message with newlines or a consolidated format rather than separate chat calls.

csharp
// Bad — multiple chat messages
player.SendChat("Line 1");
player.SendChat("Line 2");
player.SendChat("Line 3");

// Better — single message with formatting
player.SendChat("Line 1\nLine 2\nLine 3", Color.cyan);

Handle console callers

When implementing chat-related features, remember that console commands (IRocketCommand with AllowedCaller.Console) can be called without a player context. Always check whether the caller is a player before calling player-specific methods like SendChat.

Frequently asked questions

Can I see the chat message text in OnPlayerChatted?

The OnPlayerChatted delegate on UnturnedPlayerEvents receives only the color and cancel parameters by reference. To access the message text and the player, subscribe to the lower-level ChatManager.onChatted event from SDG.Unturned or use the UnturnedChat class's messaging methods.

How do I send a message to all players?

Use UnturnedChat.Say() to broadcast a message to all connected players:

csharp
UnturnedChat.Say("Server maintenance in 5 minutes.", Color.cyan);

How do I prevent a specific player from chatting entirely?

Maintain a mute list in your plugin and check it in the OnPlayerChatted handler. Set cancel = true for muted players and send them a message explaining the mute.

Can I format chat messages with bold or italic?

Unturned's chat does not support rich text formatting like bold or italic in the same way as Unity UI text. Colors are the primary formatting option for chat messages.

Does OnPlayerChatted fire for console commands?

No. The event only fires for messages sent through the in-game chat input by players. Console commands bypass the chat system entirely.

Cross-references

Document history

VersionDateAuthorNotes
1.02026-07-2757 StudiosInitial publication. Coverage of OnPlayerChatted event, chat commands, formatting, filtering, moderation, best practices.