Table of Contents

Class ChatHelper

Namespace
FishMMO.Shared
Assembly
FishMMO.Shared.dll

Static helper class for chat-related functionality, including command parsing, channel mapping, and message sanitization.

public static class ChatHelper
Inheritance
ChatHelper
Inherited Members

Fields

GUILD_ERROR_TARGET_IN_GUILD

Error code for when the target is already in a guild.

public const string GUILD_ERROR_TARGET_IN_GUILD = "FISHMMO_GUILD_ERROR_TARGET_IN_GUILD"

Field Value

string

PARTY_ERROR_TARGET_IN_PARTY

Error code for when the target is already in a party.

public const string PARTY_ERROR_TARGET_IN_PARTY = "FISHMMO_PARTY_ERROR_TARGET_IN_PARTY"

Field Value

string

TARGET_OFFLINE

Error code for when the target is offline.

public const string TARGET_OFFLINE = "FISHMMO_TARGET_OFFLINE"

Field Value

string

TELL_ERROR_MESSAGE_SELF

Error code for sending a tell message to oneself.

public const string TELL_ERROR_MESSAGE_SELF = "FISHMMO_TELL_ERROR_MESSAGE_SELF"

Field Value

string

TELL_RELAYED

Code for relayed tell messages.

public const string TELL_RELAYED = "FISHMMO_TELL_RELAYED"

Field Value

string

Properties

ChannelCommandMap

Dictionary mapping chat channels to their supported command strings.

public static Dictionary<ChatChannel, List<string>> ChannelCommandMap { get; }

Property Value

Dictionary<ChatChannel, List<string>>

ChatChannelCommands

Dictionary mapping chat channels to their command details.

public static Dictionary<ChatChannel, ChatCommandDetails> ChatChannelCommands { get; }

Property Value

Dictionary<ChatChannel, ChatCommandDetails>

CommandChannelMap

Dictionary mapping command strings to chat command details.

public static Dictionary<string, ChatCommandDetails> CommandChannelMap { get; }

Property Value

Dictionary<string, ChatCommandDetails>

Commands

Registered slash commands, keyed by command word without the leading slash.

public static Dictionary<string, ChatCommandRegistration> Commands { get; }

Property Value

Dictionary<string, ChatCommandRegistration>

Remarks

Case-insensitive. Players type commands by hand and /LeaveInstance is the same intent as /leaveinstance; an ordinal comparer silently treated the first as ordinary chat and broadcast it to the channel.

Methods

AddCommands(Dictionary<string, ChatCommand>)

Registers slash commands runnable by any player.

public static void AddCommands(Dictionary<string, ChatCommand> commands)

Parameters

commands Dictionary<string, ChatCommand>

Dictionary of command strings and their delegates.

AddCommands(Dictionary<string, ChatCommand>, AccessLevel)

Registers slash commands that require at least minimumAccessLevel.

public static void AddCommands(Dictionary<string, ChatCommand> commands, AccessLevel minimumAccessLevel)

Parameters

commands Dictionary<string, ChatCommand>

Dictionary of command strings and their delegates.

minimumAccessLevel AccessLevel

Lowest access level permitted to run them.

Remarks

The level is attached at registration rather than checked inside each handler, so a command cannot be added without one being considered. TryParseCommand(string, IPlayerCharacter, ChatBroadcast) is the single place the check happens.

GetCommandAndTrim(ref string)

Extracts the leading slash command from text, removing it from the text and returning it including its leading slash.

public static string GetCommandAndTrim(ref string text)

Parameters

text string

Reference to the input text. The command is removed from it.

Returns

string

The command including its leading slash, or an empty string when there is none.

Remarks

The leading slash is part of the command, because it is part of every key the command is looked up under. Commands is registered with literals like "/leaveinstance" and ChannelCommandMap with "/w", "/guild" and so on — these are the spellings players type and the spellings the registrations use.

This used to strip it, and the result matched nothing in either dictionary. The whole slash-command layer was therefore dead: no registered command ever ran, and every channel prefix fell through TryParseChatCommand(string, out ChatCommandDetails)'s /say fallback, so /w hello was said locally instead of going to world chat and /leaveinstance did nothing at all. Nothing surfaced it because both failures are silent by construction — the fallback is a legitimate branch, and a command with no arguments leaves empty text that the caller discards.

GetWordAndTrimmed(string, out string)

Attempts to get and remove the first single space-separated word from the rest of the text. If no targets are found it returns an empty string.

public static string GetWordAndTrimmed(string text, out string trimmed)

Parameters

text string

Input text to parse.

trimmed string

Output text with the first word removed.

Returns

string

First word if found, otherwise empty string.

InitializeOnce(Func<ChatChannel, ChatCommand>)

Initializes chat channel commands once, mapping each channel to its command function.

public static void InitializeOnce(Func<ChatChannel, ChatCommand> onGetChannelCommand)

Parameters

onGetChannelCommand Func<ChatChannel, ChatCommand>

Function to get the command delegate for each channel.

ParseChatChannel(ChatChannel)

Gets the chat command delegate for a given chat channel.

public static ChatCommand ParseChatChannel(ChatChannel channel)

Parameters

channel ChatChannel

Chat channel to parse.

Returns

ChatCommand

ChatCommand delegate if found, otherwise null.

RemoveCommands(IEnumerable<string>)

Unregisters slash commands.

public static void RemoveCommands(IEnumerable<string> commands)

Parameters

commands IEnumerable<string>

Command strings to remove.

Remarks

Every system that registers must remove on teardown. Commands is static and holds delegates bound to ScriptableObject server behaviours, which survive a play-session restart in the editor while the objects they point at do not — so a command left behind either runs against a destroyed instance or, worse, against the previous session's state.

ResetChannelCommands()

Clears the channel-command registration so InitializeOnce(Func<ChatChannel, ChatCommand>) will run again.

public static void ResetChannelCommands()

Remarks

FishMMO.Shared.ChatHelper.initialized is static and was never reset, which is only invisible while the process is also the lifetime of the registration. In the editor with domain reload disabled it is not: the second play session skipped InitializeOnce entirely and kept the first session's delegates, which are bound to ScriptableObject instances that no longer belong to the running server. Called from the chat system's teardown.

Sanitize(string)

Removes every Unity Rich Text formatting tag from a chat message.

public static string Sanitize(string message)

Parameters

message string

Input chat message.

Returns

string

Sanitized message with formatting removed, or empty if it could not be cleaned.

Remarks

Delegates to StripRichText(string), which loops to a fixed point, matches case-insensitively and fails closed. The implementation this replaced did none of those three things and could be bypassed by any of them; see that method for the detail. Kept as a member of this class because a good deal of code already calls it.

This strips markup only. Untrusted text arriving at the network boundary should go through SanitizeIncoming(string, int) instead, which also deals with line breaks, bidirectional overrides, in-band control codes and length.

SanitizeIncoming(string, int)

Full cleaning pipeline for untrusted chat text: control characters, rich text, FISHMMO_ control codes, then a hard length cap.

public static string SanitizeIncoming(string message, int maxLength)

Parameters

message string

Untrusted text.

maxLength int

Hard cap applied after cleaning; values below one disable it.

Returns

string

Clean single-line text, or empty if nothing survived.

Remarks

This is what the server runs on everything a client sends, and what the Discord bridge runs on everything Discord sends. See SanitizeIncoming(string, int) for why the order of the passes matters.

TryParseChatCommand(string, out ChatCommandDetails)

Attempts to parse a chat command and get its details. If not found, defaults to the /say channel.

public static bool TryParseChatCommand(string cmd, out ChatCommandDetails commandDetails)

Parameters

cmd string

Command string to parse.

commandDetails ChatCommandDetails

Output details for the command.

Returns

bool

True if the command was found or /say channel is available, otherwise false.

TryParseCommand(string, IPlayerCharacter, ChatBroadcast)

Attempts to run a registered slash command.

public static bool TryParseCommand(string cmd, IPlayerCharacter sender, ChatBroadcast msg)

Parameters

cmd string

Command string to parse, without the leading slash.

sender IPlayerCharacter

Sender character. Its access level is authoritative.

msg ChatBroadcast

Chat message broadcast.

Returns

bool

True when the command was recognised, whether or not it was permitted.

Remarks

A command the sender is not allowed to run is consumed rather than rejected back into the chat pipeline. Two reasons, both mattering:

  • Falling through would broadcast the refused text to whatever channel the player is on. "/admin shutdown 60" appearing in world chat is worse than the command running.
  • Returning false for a command that exists but is not permitted, and false for one that does not exist, are indistinguishable to the caller — which is the point. An unprivileged player probing for command names learns nothing from the response.

The attempt is reported through OnCommandRefused so the server can log it; privileged commands being tried by unprivileged accounts is worth seeing.

Events

OnCommandRefused

Raised when a character runs a command it does not have the access level for.

public static event Action<IPlayerCharacter, string, AccessLevel> OnCommandRefused

Event Type

Action<IPlayerCharacter, string, AccessLevel>

Remarks

A refused privileged command is a security event, and this class is engine-agnostic shared code with no server context to log it against. The server subscribes and records who tried what.