Direction: Both (C→S "I'm typing into chat"; S→C "render this colored line") Numeric ID: 16 Server handler: ServerNet.bb:183 Client handler: ClientNet.bb:1213
The catch-all text channel. Every user-typed chat line, every slash command, every server-side notification, every BVM Output(...) script line travels on this opcode. The client only renders; the server parses, applies access gates, fans out to the right audience (area / online / party / guild / single-target), and may dispatch script work as a side effect.
There is no sub-code byte at the start of the wire payload. The whole packet is a free-form string. Discrimination happens via two parallel signals:
- First-character semantics on C→S:
/and\enter the slash-command parser; everything else is general chat. - First-byte colour escape on S→C: bytes in
[250..254]are rendered as colour escapes; anything else is normal-text body.
| Offset | Width | Type | Field | Notes |
|---|---|---|---|---|
| 1 | N | String | Raw text the user typed | No length prefix; M\MessageData$ is the whole payload. |
The handler bails immediately if Len(M\MessageData$) <= 0 Or AI = Null (ServerNet.bb:185). After that:
Left$(M\MessageData$, 1) = "/" Or Left$(M\MessageData$, 1) = "\"→ strip the leading char, split on first space,Command$=Upper$(...), dispatch into the giantSelect Command$(ServerNet.bb:187-693).- Otherwise → general chat: prepend
"<" + AI\Name$ + "> ", broadcast over the per-areaFirstInZonechain, log toChatLogperChatLoggingMode(ServerNet.bb:694-712).
| Offset | Width | Type | Field | Notes |
|---|---|---|---|---|
| 1 | 1 | Byte | Colour escape (optional) | One of 250..254, or any other byte → start-of-normal-text. |
| 2 | 3 | Byte×3 | R, G, B | Only present when offset 1 = 250. Each in [0..255]. |
| 2 or 5 | N | String | Message body | Rendered through Output(...) or BubbleOutput(...). |
Colour-escape semantics (ClientNet.bb:1215-1227):
| Prefix | RGB | Channel / use |
|---|---|---|
Chr$(254) |
255, 255, 0 (yellow) |
System / GM / party-join / time-of-day / invite — server-authored notifications. |
Chr$(253) |
255, 50, 50 (red) |
Yell, error, ignore confirmation, ability-not-recharged. |
Chr$(252) |
200, 10, 200 (magenta) |
/me emote, /pm private-message, NPC→player chat ("Player: ..."). |
Chr$(251) |
20, 220, 50 (green) |
Guild chat, party chat. |
Chr$(250) |
(next 3 bytes) | Custom RGB. Primary emitter is BVM_OUTPUT(actor, text, R, G, B) at ScriptingCommands.bb:2793; engine also uses it for the critical-damage notification at three sites in GameServer.bb (Chr$(255) + Chr$(225) + Chr$(100) — peach/orange). |
| other | (default) | Normal text. If body starts "<NAME> " and UseBubbles > 1, renders as a 3D chat bubble above the named actor. |
The chat-bubble path (ClientNet.bb:1231-1254) only fires for the no-escape case — coloured lines never get bubbles. Bubble lookup goes through FindPlayerFromName(...); if no actor matches, the line falls back to normal text output.
The dispatch is a single Select Command$ keyed on LanguageString$(LS_SC*) (Language.bb) — nearly every command is localizable. The two exceptions are /help and /?, which are hardcoded at ServerNet.bb:680 (Case "HELP", "?") pending an LS_SCHelp entry. Below uses the English defaults.
| Command | Gate | Effect |
|---|---|---|
/kick <name> |
DM | Sends RCE_PlayerKicked + P_KickedPlayer to target. |
/ignore <name> / /unignore <name> |
none | Mutates the inviter's Account\Ignore$ CSV. |
/me <text> |
none | Area broadcast with Chr$(252) + "* " + Name + " " + text. |
/yell <text> |
none | Server-wide broadcast (walks FirstOnlinePlayer chain). |
/g <text> |
guild member | Walk FirstOnlinePlayer, send to same-TeamID. |
/p <text> |
party member | Walk Party\Player[0..7]. |
/pm <name>, <text> |
none | Walk FirstOnlinePlayer, deliver to first matching name. |
/invite / /accept / /leave |
none | Party state machine. |
/pet <name>, <cmd>, <args> |
own a pet | Walk AI\FirstSlave chain; dispatch CommandPet(...). |
/trade <name> |
none | Player→player trade offer. |
/players / /allplayers |
none | Counter — current area vs. server-wide. |
/time / /date / /season |
none | Game-clock report. |
/warp <area>[,<instance>] |
DM | Self-warp to the area's first defined portal (the Instance arg picks the area-instance, not coordinates). |
/warpother <name>, <area>[,<instance>] |
DM | Warp another player to the area's first defined portal. |
/xp <amount> |
DM | GiveXP(AI, n). |
/gold <amount> |
DM | Direct gold adjustment + P_GoldChange. |
/setattribute <attr>, <n> / /setattributemax <attr>, <n> |
DM | Calls UpdateAttribute / UpdateAttributeMax for Health/Speed/Energy, otherwise writes through and broadcasts P_StatUpdate. |
/script <name>, <func> |
DM | Spawns a ThreadScript(...) with privileged=1 — the script can call any privileged BVM. |
/gm <text> |
DM | Broadcast to all DMs only. |
/ability / /give / /weather / /netdump |
DM | Misc DM tools. |
| other | none | Falls through to the ScriptExists%("In-game Commands") hook → ThreadScript("In-game Commands", Command$, ..., Params$), otherwise replies "Unknown command:" (ServerNet.bb:688-692). |
/help [topic] is special: it emits one P_ChatMessage per line of help text, all prefixed Chr$(254). The entry point SendChatHelp lives at ServerNet.bb:31-60; the per-topic detail dispatcher SendChatHelpDetail at ServerNet.bb:65-109. The DM-only summary block at ServerNet.bb:57-58 is gated by the same Account\IsDM check.
C → S handler (ServerNet.bb:183-712)
The packet body is free-form, so length / shape gates are minimal. The defences live in the per-command logic:
- Sender validity:
AI = FindActorInstanceFromRNID(M\FromID); bails onNull. - Non-empty body:
Len(M\MessageData$) > 0. - Account-Null discipline: every DM-gated command does
A.Account = Object.Account(AI\Account) : If A <> Null And A\IsDM = True. TheA <> Nullcheck is load-bearing — a mid-logout account that has beenDeleted but whoseHandleis still onAI\AccountreturnsNullfromObject.Account(...), and bareA\IsDMwould crash the server from a chat command (see audit-comment at ServerNet.bb:200-202). Apply this to every new/commandthat needs a DM gate — the pattern is non-negotiable. /petchain walk: walksAI\FirstSlave / NextSlave(ServerNet.bb:266-273) — the per-leader chain that PR #287 introduced, replacing a globalEach ActorInstancescan./yell//g//gm//pm//allplayerschain walks: walkFirstOnlinePlayer / NextOnlinePlayer(ServerNet.bb:408-462) — the engine-wide players chain (PR #288 era). Filters that used to requireIf A2\RNID > 0are gone because the chain only contains online players.- Mid-warp
AreaInstanceguard:/me,/players, and the general-chat fallback all doAInstance.AreaInstance = Object.AreaInstance(AI\ServerArea) : If AInstance <> Null Then ...— soft-fails when the actor is in the brief window betweenSetArearebinding zones. The chat line just doesn't broadcast that tick; no crash. - PlayerIgnoring filter: every fan-out skips recipients whose ignore-list contains the sender (ServerNet.bb:394 / 410 / 457).
The DM gate is not the BVM RequirePrivileged() gate. Slash commands are dispatched directly from the packet handler — they never enter the BVM, so BVM_RequirePrivileged is irrelevant here. The Account\IsDM boolean is the only check. Two notes:
/scriptspawns withprivileged=1(ServerNet.bb:385) — this is the only path from chat that lets a script call privileged BVMs (Ban/Kick/Warp/CreateUDPStream/etc.). The DM gate is what authorises the elevation; downstream BVMs check the privileged bit onScriptInstance.- Unknown commands fall through to
"In-game Commands"(ServerNet.bb:688-692) — these scripts are spawned with the default (un-privileged) flag. Content authors who want player-facing slash commands should put them there; only the engine's DM-only set should ever reach theprivileged=1path.
Every RCE_Send(Host, target_rnid, P_ChatMessage, Pa$, True) follows the same shape: the colour-escape byte (or none) is the first character of Pa$. Recipients:
- Self-only notifications (errors, confirmations,
/playersresults) — sent toAI\RNID. - Area broadcast (
/me, general chat) — walksAInstance\FirstInZonechain, sends per actor withRNID > 0. - Server-wide broadcast (
/yell,/gm,/g) — walksFirstOnlinePlayerchain. - Party broadcast (
/p, party-join notification) —For i = 0 To 7 : Party\Player[i]array. - Single target (
/pm,/invite,/trade) — direct send to the resolved target'sRNID.
The general-chat fallback at ServerNet.bb:705-710 also adds the line to the server's Game\ChatText ListBox (for the dedicated-server console) and writes it to ChatLog when ChatLoggingMode > 0.
P_ChatMessage is not the high-stakes packet P_AttackActor is, but it has the largest privilege surface of any packet because of /script. The defences:
- Wire payload is text, not opcode — a malformed packet can produce a garbled chat line but cannot reach
/scriptunless the sender hasIsDMon their account. - DM bit is server-side only — clients cannot fabricate
IsDM; it lives on theAccountrow in MySQL /accounts.dat. - Privileged spawn is gated on DM bit — losing DM in the middle of
/scriptwould not retroactively un-privilege a running script; the bit is captured at spawn. That's fine — administering a DM-set should be done in a separate channel anyway. - Soft-fail on every Account-Null path — the
A <> Nullchecks ensure a mid-logout DM cannot crash the server by typing into chat as the account is being freed.
| PR | Fixed |
|---|---|
| Audit pre-PR | Account\IsDM reads after Object.Account(...) did not check Null first — server-crash via slash command from a mid-logout DM. Audit-comments at ServerNet.bb:200-202 / 220-221 / 237 record the pattern. |
| #287 | /pet walks the per-leader FirstSlave chain (replacing global Each ActorInstance scan). |
| #288 and predecessors | /yell / /g / /gm / /pm / /allplayers walk the FirstOnlinePlayer chain. |
| PRs #154 / #155 / #176 / #182–#188 | Object.AreaInstance(...) Null discipline sweep — covers /me, /players, and general-chat fallback paths. |
| Unknown-command notify | Pre-fix, an unknown slash command silently no-op'd. Now replies "Unknown command: /xxx. Type /help for a list." (ServerNet.bb:691). |
P_StandardUpdate—Chr$(254)system lines often follow a warp; the warp itself ridesP_StandardUpdate.P_AttackActor—/kickis the chat-equivalent of forcibly removing an actor; combat is the other way out.P_GoldChange—/goldemits this directly; the chat line is just confirmation.P_KickedPlayer— paired with/kickto actually disconnect the target.
../encoding.md— wire-encoding primitives.../handler-conventions.md— soft-fail discipline, Account-Null pattern, FirstOnlinePlayer / FirstInZone / FirstSlave chain walks.../../modules/scripting.md—/scriptand the privileged-spawn flag.ScriptingCommands.bb'sBVM_OUTPUT— the primary S→CChr$(250)(custom-RGB) emitter. The engine itself also usesChr$(250)for critical-damage notifications at three sites inGameServer.bb(lines 432, 469, 507 — one perCombatFormulavariant).