---
mvp: yes
subsystem: chat
---

# Chat

Chat in BNO is unencrypted, area-keyed broadcast. A client sends a chat-line opcode; the server broadcasts it to every other player whose `p_area` matches the sender's. There are no whispers, no channels, no DM history — just one global "current area" lane per player. Chat lines are also the surface where the original ships its administrative side-channel: messages beginning with the backtick character (`` ` ``) are command strings the server's operator instance interprets directly.

This document narrates the server-side chat path. Client-side rendering / HUD is in [../extracted-engine/client-networking.md](../extracted-engine/client-networking.md) (the chat HUD specifically lives in `objects/0223-chatob` on the client side too).

## Chat-line opcode (4 c2s)

`0359-server_receive.gml` case 4 handles incoming chat:

```gml
// 0359-server_receive.gml — case 4 (chat-line)
case 4:
  tempstr = readstring();
  // ... validate, prepend username, broadcast ...
```

The dispatch then walks `global.p_online[*]` and rebroadcasts as opcode 4 (s2c) to every other player whose area matches. The 4-byte opcode is reused in both directions (c2s and s2c) — distinguished only by direction of travel through the wire. See [./protocol.md](./protocol.md) for the full opcode table emitted by Phase 3 plan 02.

## Server-operator command parsing (`0008-ChtCmdRec.gml`)

The "operator chat box" on the server-running machine pre-processes every keystroke to detect backtick commands. This is a separate code path from the c2s opcode 4 receive — it runs on the operator's keyboard, not on a remote player's network input. It is documented here because the surface (the chat input on the server's `commandob`) is shared with normal chat. **Anything tagged "REJECTED-AS-PORTED" lives in [./admin-anti-port.md](./admin-anti-port.md), not here.**

```gml
// 0008-ChtCmdRec.gml (excerpt)
rec = 0
if(keyboard_string == '') {rec = 1; exit}
if(keyboard_string == '`goto ocs' && global.online = 1) {room_goto(Online_Command_Screen); rec = 1}
if(keyboard_string == '`run take-a-break.exe') {datafile_export(takeABreak,"take-a-break.exe"); execute_program('take-a-break.exe',0,0); rec = 1}
if(keyboard_string == '`end session')
{
  rec = 1;
  caddline("Shutting down...");
  global.pnum = 0;
  // ... send opcode-12 "execute_string" payload to every connected client (REJECTED-AS-PORTED — see admin-anti-port.md) ...
}
if(keyboard_string == '`command') { /* prints help text */ }
if(keyboard_string == '`clear screen') { /* clears chat scrollback */ }
if(keyboard_string == '`maint' && global.maintmode) { /* toggles maintenance mode */ }
if(keyboard_string == '`servermsg') { /* prints current MOTD */ }
if(keyboard_string == '`clientver') { /* prints required client version */ }
if(string_copy(keyboard_string,0,11) == '`servermsg ') { /* sets new MOTD; calls save_settings() */ }
if(string_copy(keyboard_string,0,11) == '`clientver ') { /* sets new minimum client version; calls save_settings() */ }
if(!rec) caddline('You:>> ' + keyboard_string);
```

The rebuild does NOT port `` `run *.exe `` or `` `end session `` (RCE-as-feature; see hard rule #3 in [CLAUDE.md](../../CLAUDE.md)). The benign commands (`maint`, `servermsg`, `clientver`, `command`, `clear screen`, `goto ocs`) become typed admin-UI endpoints in Phase 7 PAR-07 — see [./admin-anti-port.md](./admin-anti-port.md) for the full disposition table.

## Chat scrollback (server-local)

`global.cline[i]` is a 30-line rolling chat scrollback rendered on the server-operator screen. `0001-addline.gml` and `0006-scroll.gml` push new lines and shift the buffer; `0093-dynamicaddline.gml` is the entry point from anywhere in the server to log a line ("Initializing new user...", "PID 3 has changed to room 5", etc.):

```gml
// 0093-dynamicaddline.gml
if(room == Online_Command_Screen) addline(argument0);
else caddline(argument0,argument1);
```

`caddline` is the in-game chat broadcast variant (sends opcode 4 s2c); `addline` is the server-operator-screen-local push (no network). They share a buffer. This is why the server operator sees a unified feed of game events + chat, not two streams.

## Area scoping

Chat broadcast is area-scoped:

- The `chatob` instance carries `chatob.carea` (current area) + per-line area tags.
- When a player runs into an `ac_*` area-change object, [room-management.md](./room-management.md) updates `global.area`, and the chat object flags `carea = 1` so the next render flushes scrollback specific to the new area.
- The server's broadcast loop in case 4 walks `global.p_online[*]` and only forwards to players whose area matches the sender's.

There is **no server-side message persistence** for chat lines. Disconnect = forget. Phase 4 may add chat-history retention (configurable retention window) but it is NOT in CLI-08 scope.

## Rebuild guidance

- **Server-authoritative chat broadcast.** Per CLAUDE.md hard rule #1, the server tags every chat line with `senderId` from socket auth, not from the payload. Client cannot spoof another player's name.
- **Area filter is a tag query**, not a room-key match. Phase 4 stores `area` as a player-schema field; broadcast is `room.broadcast({type: 'chat', area, line}, exceptSelf)` filtered by listener's area.
- **No backtick command surface in production.** Admin actions are out-of-band (separate authenticated web UI per Phase 7 PAR-07). The chat input only sends chat.
- **Rate limit & message length** are NEW (not present in original). Phase 4 SRV-08 caps per-player chat to N messages / second + max line length.

## See also

- [./protocol.md](./protocol.md) — opcode 4 (chat c2s + s2c)
- [../extracted-engine/client-networking.md](../extracted-engine/client-networking.md) — client-side chat HUD + send path
- [./admin-anti-port.md](./admin-anti-port.md) — backtick-command disposition (REJECTED-AS-PORTED for RCE-shaped ones)
- [./room-management.md](./room-management.md) — area-change driver for chat scope

## Scripts referenced in this subsystem

<!-- AUTOGEN:scripts:start -->
| Script ID | Name | Lines | Used in objects |
|-----------|------|-------|------------------|
| 0000 | script_initlines | 14 | — |
| 0001 | addline | 28 | — |
| 0002 | script_drawlines | 14 | — |
| 0004 | CmdRec | 9 | — |
| 0005 | CmdExe | 5 | — |
| 0006 | scroll | 1 | — |
| 0008 | ChtCmdRec | 54 | — |
| 0093 | dynamicaddline | 8 | — |
| 0100 | chat_initlines | 15 | — |
| 0102 | chat_drawlines | 33 | — |
| 0106 | caddline | 24 | — |
<!-- AUTOGEN:scripts:end -->

## Objects referenced in this subsystem

<!-- AUTOGEN:objects:start -->
| Object ID | Name | Sprite | Mask | Events |
|-----------|------|--------|------|--------|
| 0223 | chatob | 61 | -1 | 19 |
<!-- AUTOGEN:objects:end -->

## Engine functions used

<!-- AUTOGEN:gml-functions:start -->
| GML function | Call sites | Sample script | Wiki link |
|--------------|------------|---------------|-----------|
| `caddline` | 4 | 0008-ChtCmdRec | [wiki/07-gml-core-functions](../../decomp/wiki/07-gml-core-functions.md) |
| `dynamicaddline` | 4 | 0359-server_receive | [wiki/07-gml-core-functions](../../decomp/wiki/07-gml-core-functions.md) |
| `string_copy` | 10 | 0008-ChtCmdRec | [wiki/07-gml-core-functions](../../decomp/wiki/07-gml-core-functions.md) |
<!-- AUTOGEN:gml-functions:end -->
