Class ChatHelper
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
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
TARGET_OFFLINE
Error code for when the target is offline.
public const string TARGET_OFFLINE = "FISHMMO_TARGET_OFFLINE"
Field Value
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
TELL_RELAYED
Code for relayed tell messages.
public const string TELL_RELAYED = "FISHMMO_TELL_RELAYED"
Field Value
Properties
ChannelCommandMap
Dictionary mapping chat channels to their supported command strings.
public static Dictionary<ChatChannel, List<string>> ChannelCommandMap { get; }
Property Value
ChatChannelCommands
Dictionary mapping chat channels to their command details.
public static Dictionary<ChatChannel, ChatCommandDetails> ChatChannelCommands { get; }
Property Value
CommandChannelMap
Dictionary mapping command strings to chat command details.
public static Dictionary<string, ChatCommandDetails> CommandChannelMap { get; }
Property Value
Commands
Registered slash commands, keyed by command word without the leading slash.
public static Dictionary<string, ChatCommandRegistration> Commands { get; }
Property Value
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
commandsDictionary<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
commandsDictionary<string, ChatCommand>Dictionary of command strings and their delegates.
minimumAccessLevelAccessLevelLowest 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
textstringReference 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
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
onGetChannelCommandFunc<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
channelChatChannelChat 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
commandsIEnumerable<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
messagestringInput 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
messagestringUntrusted text.
maxLengthintHard 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
cmdstringCommand string to parse.
commandDetailsChatCommandDetailsOutput 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
cmdstringCommand string to parse, without the leading slash.
senderIPlayerCharacterSender character. Its access level is authoritative.
msgChatBroadcastChat 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
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.