---
mvp: no
subsystem: message-board
---

# Message Board

The BNO server hosts a small set of named message boards (the in-game "MB" surfaces — `bncentral_mb`, `divine_mb`, `galewind_mb`, `scrollwood_mb`, `mb_optiondraw`, `mb_titledraw`, `mb_bodydraw`). Each board is a flat list of topics; each topic has a flat list of replies. State is persisted to a single file `MB_Log.bnb` (text format despite the extension — see Phase 3 plan 01 wiki errata).

Message boards are **not part of the CLI-08 MVP slice**. Phase 6 ships chat + movement only. Phase 7 PAR (parity) restores message-board functionality.

## State shape

Three parallel data structures in `global.*`:

- `global.mb_board[i]` — board name (string), `i` ∈ `[0, mb_total[0])`
- `global.mb_topic[i, j]` — topic metadata: `[i,0]` = title (string); `[i,1]` = reply count (real); `[i,2]` = some-flag (real, observed but not deeply traced); `i` ∈ `[0, mb_total[1])`
- `global.mb_reply[i, j]` — reply text where `i` is the topic index, `j` is the reply slot, `j` ∈ `[0, mb_topic[i,1])`

The flat 2D-array layout is a GameMaker idiom — there is no relational structure in memory.

## Persistence (`0365-mb_backup.gml` + `0366-mb_restore.gml`)

The backup script rewrites the entire `MB_Log.bnb` file on every change:

```gml
// 0365-mb_backup.gml — full file rewrite
logfile = file_text_open_write("MB_Log.bnb");
//Write all board info:
for(i = 0; i < global.mb_total[0]; i += 1)
{
  file_text_write_string(logfile,global.mb_board[i]);
  file_text_writeln(logfile);
}
//Write all topic info:
file_text_write_string(logfile,"@TOPIC");
file_text_writeln(logfile);
for(i = 0; i < global.mb_total[1]; i += 1)
{
  file_text_write_string(logfile,global.mb_topic[i,0]);  // title
  file_text_writeln(logfile);
  file_text_write_real(logfile,global.mb_topic[i,1]);    // reply count
  file_text_writeln(logfile);
  file_text_write_real(logfile,global.mb_topic[i,2]);    // flag
  file_text_writeln(logfile);
}
//Write all reply info:
file_text_write_string(logfile,"@REPLY");
file_text_writeln(logfile);
for(i = 0; i < global.mb_total[1]; i += 1)
{
  for(j = 0; j < global.mb_topic[i,1]; j += 1)
  {
    file_text_write_string(logfile,global.mb_reply[i,j]);
    // ... and so on ...
  }
}
```

Three sentinel-delimited sections (`@TOPIC`, `@REPLY`) separate the three concerns.

The restore script (`0366-mb_restore.gml`) reads the same shape:

```gml
// 0366-mb_restore.gml (excerpt)
logfile = file_text_open_read("MB_Log.bnb");
global.mb_total[0] = 0;
global.mb_total[1] = 0;
//Assign all board info:
while(1)
{
  tempstr = file_text_read_string(logfile);
  if(tempstr != "@TOPIC") {
    global.mb_board[global.mb_total[0]] = tempstr;
    global.mb_total[0] += 1;
    file_text_readln(logfile);
  } else {
    file_text_readln(logfile);
    break;
  }
}
//Assign all topic info: ...
```

The full grammar — including reply layout — is documented as a save-format row in [./save-formats.md](./save-formats.md) (forward-link from Phase 3 plan 03).

## Topic / reply lifecycle

Adding a topic:

1. Player sends a server-receive opcode for "new topic" (one of the post-MVP opcodes 13-23 — exact mapping in [./protocol.md](./protocol.md)).
2. Server-side handler appends to `global.mb_topic[mb_total[1], *]` and increments `mb_total[1]`.
3. Server calls `mb_backup()` immediately — full file rewrite.

Adding a reply:

1. Player sends a "new reply" opcode targeting topic index `i`.
2. Server appends to `global.mb_reply[i, mb_topic[i,1]]` and increments `mb_topic[i,1]`.
3. Server calls `mb_backup()` immediately.

Replies and topics can be deleted by reflowing the parallel arrays; the supporting helper scripts are `0381-mb_tcollapse.gml` (topic collapse — shifts higher-indexed topics down by 1), `0380-mb_rcollapse.gml` (reply collapse), and `0382-mb_trise.gml` (topic rise — moves a topic up the list, used as a "bump on new reply" idiom).

## Per-board / per-user persistence

Read-state per user (which topics has user X seen) is stored separately in `User_News.bnu` — see [./persistence.md](./persistence.md) and [./save-formats.md](./save-formats.md). Both files are independent — a user's read-state can become orphaned if the corresponding topic is deleted; the original silently ignores orphaned read-marks.

## Rebuild guidance (Phase 7 PAR)

- **One SQL table per concern.** `boards`, `topics`, `replies`, `read_marks`. Foreign keys; cascading deletes. The 2D-array model is gone.
- **Per-write transaction.** No "rewrite the whole file on every change" — single-row insert/update/delete.
- **Topic + reply pagination on read.** The original loads everything into memory at server start; for ~80 topics + ~600 replies that's fine, but the rebuild should not assume bounded growth.
- **Moderation hooks.** Phase 7 PAR-07 admin UI exposes `mb-moderate` typed intent (delete topic / delete reply / pin / lock) — see [./admin-anti-port.md](./admin-anti-port.md) for the TS interface.

## See also

- [./protocol.md](./protocol.md) — opcodes 13-23 (post-MVP feature opcodes including MB)
- [./save-formats.md](./save-formats.md) — `MB_Log.bnb` byte grammar (forward-link, plan 03-03)
- [./persistence.md](./persistence.md) — `mb_backup` write cadence + crash-loss surface
- [./admin-anti-port.md](./admin-anti-port.md) — `mb-moderate` admin intent (Phase 7 PAR-07)
- [./parity-checklist.md](./parity-checklist.md) — forward-link, plan 03-05; MB is in PAR scope
- [../../decomp/wiki/16-bno-bnb-notes.md](../../decomp/wiki/16-bno-bnb-notes.md) — errata-corrected save format reference

## Scripts referenced in this subsystem

<!-- AUTOGEN:scripts:start -->
| Script ID | Name | Lines | Used in objects |
|-----------|------|-------|------------------|
| 0252 | mb_getname | 11 | — |
| 0253 | mb_getwriter | 10 | — |
| 0254 | mb_getbody | 10 | — |
| 0262 | mb_repwrite | 19 | — |
| 0263 | mb_topdraw | 31 | — |
| 0264 | mb_repdraw | 37 | — |
| 0265 | mb_up | 38 | — |
| 0266 | mb_down | 39 | — |
| 0267 | mb_newdraw | 15 | — |
| 0365 | mb_backup | 35 | — |
| 0366 | mb_restore | 54 | — |
| 0380 | mb_rcollapse | 16 | — |
| 0381 | mb_tcollapse | 23 | — |
| 0382 | mb_trise | 29 | — |
<!-- AUTOGEN:scripts:end -->

## Objects referenced in this subsystem

<!-- AUTOGEN:objects:start -->
| Object ID | Name | Sprite | Mask | Events |
|-----------|------|--------|------|--------|
| 0170 | mb_bodydraw | -1 | -1 | 4 |
| 0175 | mb_titledraw | -1 | -1 | 4 |
| 0302 | mb_optiondraw | -1 | -1 | 11 |
| 0305 | bncentral_mb | 353 | 354 | 4 |
| 0358 | scrollwood_mb | 353 | 354 | 4 |
| 0359 | divine_mb | 353 | 354 | 4 |
| 0360 | galewind_mb | 353 | 354 | 4 |
<!-- AUTOGEN:objects:end -->

## Engine functions used

<!-- AUTOGEN:gml-functions:start -->
| GML function | Call sites | Sample script | Wiki link |
|--------------|------------|---------------|-----------|
| `file_text_write_string` | 5 | 0365-mb_backup | [wiki/07-gml-core-functions](../../decomp/wiki/07-gml-core-functions.md) |
| `file_text_read_string` | 7 | 0366-mb_restore | [wiki/07-gml-core-functions](../../decomp/wiki/07-gml-core-functions.md) |
<!-- AUTOGEN:gml-functions:end -->
