# Legacy Recon 07 — Social Layer: Message Boards, Chat, Friend Data

Reverse-engineered from extracted GML (plain text, not hex). Sources:
- Client = `extracted/client-5-8/`
- Server = `extracted/server-5-4/`

The single most important file is the server packet dispatcher
`extracted/server-5-4/scripts/0359-server_receive.gml` (627 lines) and its
client mirror `extracted/client-5-8/scripts/0097-client_receive.gml` (447
lines). Both carry a `messageid` comment table at the top. Everything social
flows through these two switch statements.

Confidence legend: **[confirmed]** = directly read in code; **[inferred]** =
deduced from surrounding code; **[guess]** = flagged uncertainty.

---

## 1. MESSAGE BOARDS

### Object structure [confirmed]
- `mbparent` (object id 352, `client-5-8/objects/0352-mbparent/`) is the shared
  parent. ~24 per-zone board objects inherit from it
  (`bncentral_mb`=305, `scrollwood_mb`=306, `divine_mb`=307, `galewind_mb`=308,
  … `lostletters_mb`=582). `meta.json` for each shows `"parentId": 352`.
- Each child sets only one field in its Create event:
  `event_inherited(); mbid = N;`
  - `bncentral_mb` → `mbid = 0`
    (`objects/0305-bncentral_mb/events/Create.gml`)
  - `scrollwood_mb` → `mbid = 1`
  - `divine_mb` → `mbid = 2`, etc.
  So `mbid` is a stable global board index 0..(N-1), assigned by object, not
  by room. The board's display name comes from `global.mb_board[mbid]`.
- `ac_news` (object 409) is **NOT a board** — its only event is
  `Collision-0.gml → change_area("News Center")`. It is a floor trigger that
  sets the player's "area" string to "News Center" (the News Center is the
  in-world zone that physically contains the boards). [confirmed]

### Data model [confirmed]
Three parallel global arrays on the server, all string-packed:
- `global.mb_board[i]` — board i's header, format `Moderator[BoardName]`
- `global.mb_topic[t,0]` — topic t's header, `Author[Title]Body`;
  `mb_topic[t,1]` = reply count; `mb_topic[t,2]` = owning board id (mbid)
- `global.mb_reply[t,r]` — reply r of topic t, `Author[Title]Body`
- `global.mb_total[0]` = number of boards; `global.mb_total[1]` = number of
  topics (global across all boards).

The `Writer[Title]Body` packing is decoded by three tiny parsers
(`scripts/0252-mb_getname.gml` → text in `[]`,
`0253-mb_getwriter.gml` → text before `[`,
`0254-mb_getbody.gml` → text after `]`). Title-draw labels confirm semantics:
boards are "Moderated by <writer>", topics "Begun by", replies "Written by"
(`client-5-8/objects/0175-mb_titledraw/events/Draw.gml`).

### Global vs per-user split [confirmed]
- **Board content is GLOBAL — one shared wall per board.** Topics and replies
  live in the server's `mb_topic`/`mb_reply` arrays and are broadcast to all.
  There is no per-user board content.
- **Per-user state is ONLY the unread/"new" flag:**
  `global.u_mb_newmsg[uid, mbid]` is a per-user × per-board boolean ("has an
  unread post since you last opened this board"). This is the "user news"
  system. So boards = mix: shared content, private read-state.

### How a player READS a board [confirmed]
Client `mbparent` `KeyPress-35` (the End key) when facing up into the board's
collision (`client-5-8/objects/0352-mbparent/events/KeyPress-35.gml`):
```
global.mb_cboard = mbid;
clearbuffer(); writebyte(13); writeint(global.mb_cboard);
sendmessage(global.s_tcpsocket);   // request topic list for this board
```
Server case 13 (`0359-server_receive.gml:305`) replies with msgid 13: total
topic count, then for every `mb_topic[*,2]==boardid` it writes topic id, header
string, reply count, board id. It then **clears the unread flag**
(`global.u_mb_newmsg[uid, boardid] = 0`) and pushes a fresh msgid-19 unread
snapshot. Opening a single topic's replies uses msgid 14 (server case 14,
line 347) which streams `mb_reply[topic, 0..n]` and likewise clears unread.

The `mb_titledraw` / `mb_bodydraw` / `mb_optiondraw` client objects render the
board UI; navigation is Up/Down = select topic/reply, Left/Right = select
option, End = execute (`ChtCmdRec` "message board help" text).

### How a player POSTS [confirmed]
- **New topic:** client msgid 11 → server case 11 (`:269`). Server appends to
  `mb_topic[mb_total[1]]`, sets its board id, `mb_total[1]++`, calls
  `mb_backup()`, then sets `u_mb_newmsg[everyone-except-poster, board] = 1` and
  flags `allupdate[6]` so every online user gets a fresh unread snapshot.
- **New reply:** client msgid 12 → server case 12 (`:285`). Appends to
  `mb_reply[topic, reply_count]`, increments reply count, `mb_trise(topic)`
  (bumps topic to top), `mb_backup()`, same per-user unread fan-out.
- **Edit existing:** msgid 6 (edit topic header text, case 6 `:240`) and msgid 7
  (edit a reply, case 7 `:246`). Both edit in place; a code comment notes the
  author-only guard was removed *"Wouldn't work for moderators"* — so **there
  is no server-side authorship/permission check on edits** [confirmed]. Any
  client that sends msgid 6/7 with a topic index overwrites that text.
- **Collapse/delete:** msgid 19→server case 19 (`:410`): byte 0 =
  `mb_tcollapse` (delete topic), else `mb_rcollapse` (delete reply), then
  `mb_backup()`. (Client uses msgid 19 for this; note client→server 19 ≠
  server→client 19 which is the unread snapshot — the same byte is overloaded
  per direction.)
- Board *list/names* fetched via msgid 18 (case 18 `:398`) returning
  `mb_board[*]`. msgid 13's client handler (`client_receive` case 13) also
  carries a large commented-out block for board-name updates — board *names*
  are static/server-seeded, only topic/reply content is mutable at runtime.

### Persistence [confirmed]
- `mb_backup()` (`scripts/0365-mb_backup.gml`) writes the entire global board
  state to one flat text file **`MB_Log.bnb`**: all `mb_board` lines, then a
  `@TOPIC` sentinel + each topic (header, reply-count, board-id), then a
  `@REPLY` sentinel + every reply body. `mb_restore()` (`0366`) reads it back,
  rebuilding `mb_total[0]`/`[1]` from the sentinels. Called on every post.
- Per-user unread flags persist separately. `unews_backup(uid)`
  (`scripts/0371-unews_backup.gml`) writes
  `UserData\MB_News\News_<uid>.bnu` — one real per board = that user's
  `u_mb_newmsg` row. `unews_restore(uid)` (`0372`) reads it back (guarded by
  `file_exists`). `all_unews_rb(mode)` (`0373`) loops all users for bulk
  restore(0)/backup(1). **So `unews_*` = "user news" = per-user board-unread
  persistence, NOT per-user message content.**

### Client vs server note
Boards are fully **server-authoritative for content & persistence** (single
shared `MB_Log.bnb`). The client is a thin terminal: it requests a board (13),
a topic's replies (14), or the board name list (18), and posts via 11/12/6/7.
The unread-badge logic is driven entirely by server msgid-19 snapshots; the
client just renders `global.mb_newmsg[mbid]` (see `mbparent` Step event
swapping to a "new post" sprite). **No edit/authorship enforcement on the
server** — a parity rebuild must add it (the original relied on obscurity).

---

## 2. CHAT

### Send path [confirmed]
All chat input funnels through the client `chatob` object (id 223). Enter
(`objects/0223-chatob/events/KeyRelease-13.gml`) length-caps `keyboard_string`
then calls `ChtCmdRec()` (`scripts/0008-ChtCmdRec.gml`, the command parser).
If the line is not a recognized command (`rec == 0`), it is treated as public
chat:
```
caddline('You:>> ' + keyboard_string);
clearbuffer(); writebyte(4); writebyte(99); writestring(keyboard_string);
sendmessage(global.s_tcpsocket);
```
So **public chat = msgid 4 with a leading byte of 99**.

### Scope = same-room AND proximity [confirmed — this is the key finding]
Server case 4 (`0359-server_receive.gml:151`): if the target byte is 99 it sets
`allupdate[4]=1`. The fan-out at line 598 only forwards the chat to player `up`
when:
```
global.p_room[pid] == global.p_room[up]
  && abs(p_x[pid]-p_x[up]) <= 480
  && abs(p_y[pid]-p_y[up]) <= 360
```
**Public chat is PROXIMITY-scoped within the current room** — same room AND
within a 480×360 px box (roughly ¾ of the 640×480 view, centered on speaker).
Not whole-server, not whole-room. Client case 4 also re-checks
`room == global.p_room[pid]` before printing (`client_receive.gml:116`).

### Whispers / DMs [confirmed]
Trigger: `name~message` syntax parsed in `ChtCmdRec` (the `~` handler). A bare
leading `~` continues the last whisper target (`global.whispid`). Whisper send
reuses msgid 4 but with the **target's pid as the first byte instead of 99**:
```
writebyte(4); writebyte(global.whispid); writestring(keyboard_string);
```
Server case 4 `else if(global.p_online[bytetemp])` branch (`:159`) routes it
**directly to that one socket** with a "whisper" marker byte (1), no proximity
check, cross-room OK. Client prints whispers in `global.c_whisper` color
(`:121`). Inventory friend-data also offers a "Whisper…" action that pre-fills
`name~` (`iv_setactions.gml`). Whisper blocked into `Digital_Abyss` room.

### Slash-commands [confirmed]
Command prefix is the **backtick `` ` ``** (key left of 1), not slash. Full set
from `ChtCmdRec` + the in-game `` `command `` help text:
- Movement/teleport: `` `goto ocs `` (return to Online Command Screen)
- Toggles (persisted via `save_settings`): `` `alpha ``, `` `notify ``,
  `` `mb show game ``, `` `watchable ``
- Social requests: `` `join <name> ``, `` `summon <name> ``, `` `add <name> ``
  (friend), `` `accept <name> ``, `` `decline <name> ``, `` `cancel ``,
  `` `ignore <name> ``
- Spectating: `` `watch ``, `` `shake eyes `` (force off spectators)
- Session: `` `end session `` (msgid 24)
- Screen: `` `clear screen ``
- Help: `` `command ``, `` `ctrl help ``, `` `game/chat help ``,
  `` `message board help ``
There are also Ctrl-key shortcuts (Ctrl+I inventory, Ctrl+R run, Ctrl+C/N copy
chat line, Ctrl+W watch, etc.) — listed in the `` `ctrl help `` block.

### Emotes / channels [inferred]
No emote command and no named channels found. "Channels" are implicitly the
per-room proximity bubble. Chat colors (`global.c_server`, `c_whisper`,
`c_request`) are message *categories*, not subscribable channels.

### Rate limiting / filtering [confirmed absent]
- **No server-side rate limiting** on chat — server case 4 broadcasts
  immediately, no cooldown/token bucket. (There IS a per-player log-alarm
  msgid 26 keepalive `p_logalarm[pid]=2700`, but that is an idle/keepalive
  timer, not a chat throttle.)
- **No profanity/content filter** anywhere in the chat path.
- The only limit is a **client-side length cap** in `chatob` KeyRelease-13
  (`floor(global.mlheight/2)*global.maxlength`). A modified client bypasses it.

### Client vs server note
Chat is server-relayed but **not server-validated** beyond room+proximity
routing. The server trusts the sender byte (99 vs a pid) and the body string.
Rebuild must add server-side rate limiting and (recommended) filtering, and
must enforce the 480×360 proximity rule server-side (it already is — keep it).

---

## 3. FRIEND DATA  (inventory category 2)

### Where it lives [confirmed]
Inventory is a server-persisted `iv_itemdb[cat, idx]` (client) /
`uinv_*` (server). Category map (from
`client-5-8/objects/0393-iv_backdraw/events/Create.gml:130-150`):
`0`=stackable, `1`=key, then **collective** categories: **`2`=Friend Data**,
`3`=Link Data, `4`=Message Data, `5`=Spectrum (color) Data.
A friend entry is one string `"<uid>|<name>"`, e.g. `"7|Nerdy Bandit"`. A
sentinel row `"99|Add New Data"` always sits in the category as the "add" tile.

### How a friend is ADDED — request/trade, NOT a drop [confirmed]
- **There is NO `d_friend` collectable.** Searched objects + scripts: only
  `d_spectrum`, `d_link`, `d_key` drop objects exist (cats 5/3/1). Friend data
  has no world drop. [confirmed — grep returned nothing for `d_friend`]
- Friend data is acquired by a **mutual request handshake**:
  - `` `add <name> `` → client `rq_add` (`scripts/0331-rq_add.gml`) sends
    msgid 15 / request-type **5** to the target's pid.
  - Server case 15 (`0359-server_receive.gml:370`) is a dumb relay: it forwards
    the request byte to the target socket. No server-side friend store.
  - Client case 15 / type 5 (`client_receive.gml:286`): the recipient either
    sees *"X has requested your friend data"* (`rq_new` returns 0, pending) or,
    if a reciprocal request already existed, the trade COMPLETES on the spot:
    `iv_additem(2, "<uid>|<name>")` for each side, message *"Traded friend data
    with X!"*. Mutual `` `add `` (or `` `accept ``) is required — it is a
    two-way trade, like exchanging contact cards.
- The whole `rq_*` request system (`rq_new` type table in
  `scripts/0272-rq_new.gml`) is shared by friend-add (5/6), join (0/3),
  summon (1/2), and duo dig/spot (7/8/9/10). Requests time out
  (`rq_timer = 900` steps = 30 s at 30 fps) and `` `cancel ``/`` `decline ``/
  `` `ignore `` manage them.

### What holding Friend Data lets you DO [confirmed]
Selecting a friend in the inventory (`scripts/0308-iv_setactions.gml`, the
`argument0 == 2` branch) exposes four actions, each just pre-fills a chat
command:
1. **Whisper…** → `name~`
2. **Join** → `` `join <name> `` (teleport yourself to them)
3. **Summon** → `` `summon <name> `` (pull them to you)
4. **Delete Data** → removes the entry.
The `"99|Add New Data"` tile's only action is **Add Friend…** (pre-fills
`` `add Name ``). So friend data = a persistent **address book** that makes
whisper/join/summon convenient — it is NOT a hard gate (you can still
`` `join ``/`` `whisper `` anyone online by typing their name; `nametopid`
matches any online player, `scripts/0268-nametopid.gml`).

### Join / Summon mechanics [confirmed]
- `` `join `` (`rq_join`, msgid 15 type 1) teleports YOU to the target; requires
  you hold that zone's link data (`iv_zonesearch`) unless you're in a duo.
- `` `summon `` (`rq_summon`, msgid 15 type 0) pulls the TARGET to you; gated on
  your zone link data + target consent.
- Both are consent-based (reciprocal request) and gated by **zone link data**,
  not by friend data. Blocked if target is in the Online Command Screen or the
  Digital Abyss.

### Presence / online status [confirmed — this is the central social finding]
- **Presence is GLOBAL and ephemeral, tracked only as `global.p_online[pid]`
  per connected socket** on both client and server. There is no persistent
  friends-online list, no presence subscription, no "last seen".
- A player's connect/disconnect is broadcast to ALL clients (server msgid 0 on
  login fan-out, msgid 5 on logout; `server_receive.gml:520` & `:538`). But the
  **client only PRINTS the notification if you hold that player's friend data**:
  `client_receive.gml:53` (login) and `:130` (logout) both guard the
  `dynamicaddline(... "has connected/logged out")` with
  `iv_itemsearch(2, "<uid>|<name>") > 0`. So friend data's *presence* benefit is
  purely a **client-side notification filter** — the server tells everyone about
  everyone; your client only surfaces friends' comings and goings.
- There is no way to query "is my friend online right now" beyond:
  (a) seeing the login/logout line, or (b) trying `` `join ``/`` `summon ``/
  whisper and getting *"He or she may be offline"* if `nametopid` fails. You
  cannot locate a friend's room/coords from the client; only the server-side
  mod query (msgid 20 type 4) can list everyone's room.

### Client vs server note
Friend data is **server-persisted inventory** (via `uinv_set`/`uinv_get`,
`scripts/0374`/`0375`, backed by `uinv_backup`/`uinv_restore` `0376`/`0377`),
but the friend *handshake* is a pure client-to-client relay through msgid 15 —
the server keeps no friend graph and does no presence bookkeeping beyond the
live `p_online` socket array. **Presence filtering is entirely client-side**, so
a modified client could see everyone's login/logout. A faithful-but-safe
rebuild should move the friend graph and presence-visibility decision to the
server.

---

## ~10-LINE SUMMARY

1. **Boards** are global shared walls (~24, one per zone, indexed by a static
   `mbid` on each `mbparent` child). Content + persistence are 100%
   server-authoritative in one flat file `MB_Log.bnb`.
2. The only **per-user board state** is an unread flag
   `u_mb_newmsg[uid][board]`, persisted per user as `UserData\MB_News\News_<uid>.bnu`
   — that's what the `unews_*` ("user news") scripts do.
3. Posting (msgid 11 topic / 12 reply) fans an unread flag out to every other
   user; editing (msgid 6/7) has **no authorship check** (comment admits it).
4. **Chat is proximity-scoped:** public msgid-4 (sender byte 99) reaches only
   same-room players within 480×360 px of the speaker — not server- or
   room-wide.
5. **Whispers** reuse msgid 4 with the target's pid; routed point-to-point,
   cross-room, via `name~message`. Command prefix is backtick `` ` ``.
6. **No rate limiting and no profanity filtering** exist anywhere; the only cap
   is client-side message length (trivially bypassable).
7. **Friend Data** = inventory category 2, stored as `"uid|name"`,
   server-persisted via `uinv_*`. **No `d_friend` drop exists.**
8. Friends are added by a **mutual `` `add `` trade** (msgid 15 type 5), a
   client-to-client relay; the server keeps no friend graph.
9. Holding friend data just adds convenience actions (Whisper/Join/Summon) and
   filters connect/disconnect notifications — it is not a hard gate; you can
   whisper/join/summon anyone *online* by name.
10. **Presence is global + ephemeral** (`p_online` socket array, broadcast to
    all). There is no persistent presence service and no friend-online list;
    "is my friend online" is answered only by the client-side login/logout
    notice (friend-filtered) or a failed `` `join ``/whisper. Locating a friend
    (room/coords) is impossible client-side. Rebuild should server-authorize the
    friend graph + presence visibility and add chat rate-limit/filtering.
