Text and messaging
Four ways to put text in front of a player, each with a different lifetime and destination.
| Mechanism | Where it appears | Lifetime |
|---|---|---|
messageClient / messageTeam / messageAll |
The chat log | Scrolls away |
centerPrint |
Large, centre screen | Timed |
bottomPrint |
Above the HUD | Timed |
echo / error / warn |
The console | Permanent in the log |
The message system
Server side [script]:
function messageClient(%client, %msgType, %msgString, %a1, %a2, … , %a13)
function messageTeam(%team, %msgType, %msgString, %a1, %a2, … , %a13)
function messageAll(%msgType, %msgString, %a1, %a2, … , %a13)
| Argument | Purpose |
|---|---|
%client / %team |
Recipient |
%msgType |
A tagged string identifying the message type. "" for plain chat. |
%msgString |
The text, with %1–%13 substitution slots |
%a1–%a13 |
Substitution values |
Real uses [script]:
messageClient(%obj.client, 'MsgRepairPackOn', '\c2Repair pack activated.');
messageClient(%plyr.client, 'MsgTeamDeploySuccess', "");
messageClient(%targetObject.client, 'msgTeamConcussionGrenade',
'\c1You were hit by %1\'s concussion grenade.',
getTaggedString(%sourceObject.client.name));
Three conventions to copy:
- The type is single-quoted — it is a tagged string that crosses the network as an integer.
%1slots rather than string concatenation. This keeps the format string interned and cheap, and is essential for anything localisable.- A message with an empty body still carries a type.
messageClient(%client, 'MsgTeamDeploySuccess', "")sends no text at all — the type is the payload, and the client reacts to it. This is a genuine RPC channel disguised as chat.
Client-side callbacks
Register a handler for a message type [script]:
function addMessageCallback(%msgType, %func)
{
for(%i = 0; (%afunc = $MSGCB[%msgType, %i]) !$= ""; %i++)
{
// only add each callback once
if(%afunc $= %func)
return;
}
$MSGCB[%msgType, %i] = %func;
}
and the dispatcher [script]:
function clientCmdServerMessage(%msgType, %msgString, %a1, … , %a10)
{
%tag = getWord(%msgType, 0);
for(%i = 0; (%func = $MSGCB["", %i]) !$= ""; %i++)
call(%func, %msgType, %msgString, %a1, … , %a10); // ← "" sees everything
if(%tag !$= "")
for(%i = 0; (%func = $MSGCB[%tag, %i]) !$= ""; %i++)
call(%func, %msgType, %msgString, %a1, … , %a10);
}
So:
addMessageCallback('MsgClientJoin', myModOnClientJoin); // one type
addMessageCallback("", myModSeeEverything); // all messages
scripts/message.cs is loaded at line 16 of console_end.cs, deliberately before the autoexec loop, with
the comment “message.cs is loaded so autoexec can add new message callbacks” [script]. Register
yours from your autoexec script.
This is the cleanest client-side extension point in the game: a client mod can react to server events without the server knowing it exists.
Colour codes
\c<n> sets the colour of the rest of the line. Usage across the shipped scripts [script]:
| Code | Uses | Convention in the shipped scripts |
|---|---|---|
\c0 |
457 | Default / plain |
\c1 |
92 | Warnings, being hit |
\c2 |
222 | Success, positive status |
\c3 |
24 | Emphasis |
\c4 |
8 | Rare |
\c5 |
21 | Rare |
\c6 |
7 | Rare |
\c7 |
7 | Rare |
\c8 |
6 | Rare |
\c9 |
10 | Rare |
The actual RGB values come from the client’s message HUD profile, so codes are semantic, not literal
colours — which means they respect the player’s settings. Use \c2 for good news and \c1 for bad, as
the shipped code does, and your messages will look native.
'\c2Repair pack activated.'
'\c2Surface is too steep to place this item on.%1'
'\c1You were hit by %1\'s concussion grenade.'
Note \' to escape an apostrophe inside a single-quoted tagged string.
Embedded sounds
~w followed by a path plays a WAV alongside the text [script]:
%errorSnd = '~wfx/misc/misc.error.wav';
The handler splits the message at the marker, plays the sound if it is under
$MaxMessageWavLength (5200 ms), and prints the remaining text. See Audio.
Centre and bottom print
scripts/centerPrint.cs [script]:
$CenterPrintSizes[1] = 20;
$CenterPrintSizes[2] = 36;
$CenterPrintSizes[3] = 56;
function centerPrint( %client, %message, %time, %lines )
{
if( %lines $= "" || ((%lines > 3) || (%lines < 1)) )
%lines = 1;
commandToClient( %client, 'CenterPrint', %message, %time, %lines );
}
function bottomPrint( %client, %message, %time, %lines )
{
if( %lines $= "" || ((%lines > 3) || (%lines < 1)) )
%lines = 1;
commandToClient( %client, 'BottomPrint', %message, %time, %lines );
}
| Function | Recipients |
|---|---|
centerPrint(%client, %message, %time, %lines) |
One client |
bottomPrint(%client, %message, %time, %lines) |
One client |
centerPrintAll(%message, %time, %lines) |
Everyone |
bottomPrintAll(%message, %time, %lines) |
Everyone |
clearCenterPrint(%client) / clearBottomPrint(%client) |
Clear |
ClearCenterPrintAll() / ClearBottomPrintAll() |
Clear for everyone |
| Argument | Meaning |
|---|---|
%time |
Seconds to display. The client-side signature comments say so explicitly [script]: function clientCmdCenterPrint( %message, %time, %lines ) // time is specified in seconds |
%lines |
1, 2, or 3 — selects the text size from $CenterPrintSizes. 1 is largest. Anything outside the range is clamped to 1. |
The All variants skip AI clients [script]:
%count = ClientGroup.getCount();
for (%i = 0; %i < %count; %i++)
{
%cl = ClientGroup.getObject(%i);
if( !%cl.isAIControlled() )
commandToClient( %cl, 'centerPrint', %message, %time, %lines );
}
Copy that isAIControlled() guard in any broadcast loop you write — sending client commands to bots is
wasted work and can produce console errors.
loadMission clears both at every mission change [script]:
ClearCenterPrintAll();
ClearBottomPrintAll();
Tagged strings
Player names and other repeated strings travel as integer tags. Convert at the boundaries:
| Call | Direction |
|---|---|
addTaggedString(%text) |
Text → tag |
getTaggedString(%tag) |
Tag → text |
detag(%tagged) |
Strip the tag wrapper from a received string |
$TeamName[$index] = addTaggedString($Host::TeamName[$index]);
messageClient(%targetObject.client, 'msgTeamConcussionGrenade',
'\c1You were hit by %1\'s concussion grenade.',
getTaggedString(%sourceObject.client.name));
%message = detag( %msgString );
%client.name is a tag, not text. Passing it directly into a message body prints a number. Wrap it in
getTaggedString(). This is the most common messaging bug in new mods.
Chat
Client → server [script]:
commandToServer( 'MessageSent', %text );
Server handlers [script]:
function serverCmdMessageSent(%client, %text) { … }
function serverCmdTeamMessageSent(%client, %text) { … }
These are the hooks for chat commands. A package override that inspects %text for a prefix and handles
it, otherwise calling Parent::, is the standard way to add server chat commands:
package MyMod
{
function serverCmdMessageSent(%client, %text)
{
%msg = detag(%text);
if (getSubStr(%msg, 0, 1) $= "!")
{
myModHandleCommand(%client, %msg);
return;
}
Parent::serverCmdMessageSent(%client, %text);
}
};
Validate.
serverCmdfunctions are callable by any client with any arguments. Check what%clientis allowed to do before acting. See Client/server split.
Spam control is already present — message.cs has a $SPAM_MESSAGE path with a wait timer [script].
Console output
| Call | Use |
|---|---|
echo(%text) |
Normal output |
warn(%text) |
Warning |
error(%text) |
Error — shows in red |
Used liberally by the shipped code for both diagnostics and, in a few places, deliberate logging:
echo("LOADING MISSION: " @ %missionName);
echo("GOT DATA BLOCKS DONE FOR: " @ %client);
error( "Failed to open the EULA file!" );
There is also a gated logger [script]:
function logEcho(%msg)
{
if($LogEchoEnabled)
echo("LOG: " @ %msg);
}
Worth copying the pattern — a $MyMod::Debug flag gating your own echo calls means you can ship with
diagnostics in place and off by default.
See Debugging.
The message HUD
addMessageHudLine(%message) appends to the chat log; GuiMessageVectorCtrl renders it. The default
handler [script]:
function defaultMessageCallback(%msgType, %msgString, %a1, … , %a10)
{
if ( %msgString $= "" )
return;
%message = detag( %msgString );
…
addMessageHudLine( %message );
}
Note the early return on an empty body — this is what makes type-only messages (the RPC channel described above) print nothing.
Under the community patches
messageClient / messageTeam / messageAll, the $MSGCB callback system, colour codes, tagged
strings, centerPrint / bottomPrint, and the ~w sound marker are all unchanged.
Two filters are inserted on the inbound client path.
Chat tags are filtered
MessageVector::pushBackLine and MessageVector::validateTag are overridden to strip <t2server:> and
<tribe:> tags out of incoming chat [patch-script] — a guard against messages that would otherwise
inject clickable link markup into other players’ chat logs.
If your mod sends custom markup in chat text, expect it to be filtered. Send structured data as a message type with arguments instead:
messageClient(%client, 'MyModEvent', "", %arg1, %arg2);
That is the RPC-shaped use of the message system described above, and it passes through untouched.
A bad-word filter runs on inbound messages
clientCmdChatMessage is overridden to apply filterString() when $pref::enableBadWordFilter is set
[patch-script]. Client-side and user-toggleable. Vanilla containsBadWords() / addBadWord() still
exist.
IRC
IRCClient::connect is overridden so a connection only fires when JoinChatDlg is awake
[patch-script]. RC2a additionally packages GetIRCServerList, IRCClient::notify, and
IRCClient::away, and rewrites $IRCClient::NickName from the auth info at file scope [patch-script].
The in-game IRC client is largely vestigial on both patches; the QoL patch deactivates the EMAIL, BROWSER, and CHAT launch tabs entirely.
Message callbacks still register from autoexec
scripts/message.cs is still executed at line 16 of console_end.cs, before the autoexec glob
[script], so addMessageCallback() from your entry script works exactly as documented above. Neither
patch touches this.
Related
- Client/server split — the transport under all of this
- HUD — the HUD elements messages interact with
- Audio — the
~wsound marker - GUI system —
GuiMLTextCtrlmarkup, which is a separate tag syntax - TribesNEXT QoL patch — the chat overrides