---
mvp: yes
subsystem: persistence
---

# Persistence (`.bno` / `.bnb` / `.bnu` write cadence)

The BNO 5-4 server has **no database**. Every persistent thing is a flat text file (despite the file extensions making them look binary — see Phase 3 plan 01 wiki errata that reclassified `file_bin_*` calls as actually `file_text_*`). Writes are triggered either:

1. **Per-event** — a player logs out, a setting changes, a message-board topic is added → write-back inline,
2. **Scheduled** — `0349-operations` step event drains a per-player save alarm; `all_backup` pushes everything to disk on a longer cadence.

Per-format byte grammar lives in [./save-formats.md](./save-formats.md). This document documents the **write cadence** and the **crash-loss surface** — i.e. what happens between two disk writes, which is what Phase 4 + Phase 5 have to engineer around.

## File inventory

| File | Format | Owning subsystem | Reference |
|------|--------|------------------|-----------|
| `MSettings.bno` | text — server-wide settings | server config | [./save-formats.md](./save-formats.md) |
| `User_DBUpdated.bnu` | text — username/password pairs | [./account-auth.md](./account-auth.md) | [./save-formats.md](./save-formats.md) |
| `localList.txt` | text — username/password pairs (cold start) | [./account-auth.md](./account-auth.md) | [./save-formats.md](./save-formats.md) |
| `User_Inv.bnu` | text — inventory per uid | persistence | [./save-formats.md](./save-formats.md) |
| `User_Area.bnu` | text — per-area state per uid | [./room-management.md](./room-management.md) | [./save-formats.md](./save-formats.md) |
| `User_News.bnu` | text — read-flags for server news per uid | persistence | [./save-formats.md](./save-formats.md) |
| `User_Hxb.bnu` | text — per-uid hexport (teleport) bookmark state | persistence | [./save-formats.md](./save-formats.md) |
| `MB_Log.bnb` | text — message-board topics + replies | [./message-board.md](./message-board.md) | [./save-formats.md](./save-formats.md) |

## Per-event write cadence

These calls fire from inside the GameMaker step event — i.e. **synchronously, on the only thread** the server has. A slow disk write blocks the entire game loop.

| Event | Script | Files written |
|-------|--------|---------------|
| `` `servermsg foo`` admin command | `0387-save_settings.gml` | `MSettings.bno` |
| `` `clientver 51`` admin command | `0387-save_settings.gml` | `MSettings.bno` |
| Player area change | `0369-uarea_backup.gml` | `User_Area.bnu` (single uid row rewritten) |
| Inventory update | `0376-uinv_backup.gml` | `User_Inv.bnu` (single uid row rewritten) |
| News marked read | `0371-unews_backup.gml` | `User_News.bnu` (single uid row rewritten) |
| Hexport bookmark set | `0384-uhxb_backup.gml` | `User_Hxb.bnu` (single uid row rewritten) |
| MB topic / reply added | `0365-mb_backup.gml` | `MB_Log.bnb` (full file rewritten) |

```gml
// 0365-mb_backup.gml — full MB log rewrite (excerpt)
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);
// ... topic + reply blocks ...
```

The whole MB file is rewritten on every change. With ~50-100 topics and ~1000 replies, this is a multi-KB write — fine on local disk, awkward over network FS, and a noticeable game-loop pause if the disk is contended.

## Scheduled (alarm-driven) write cadence

Every 30 seconds (`global.p_sendalarm[pid] = 900` at 30 fps), `0349-operations.gml` fires a per-player aggregated backup. Every ~90 seconds (`p_logalarm = 2700`), it runs the disconnect-pruner.

`0389-all_backup.gml` is the omnibus "save everything" caller — invoked at `` `end session`` shutdown:

```gml
// 0389-all_backup.gml
all_uarea_rb();   // all User_Area rows
all_unews_rb();   // all User_News rows
all_uinv_rb();    // all User_Inv rows
all_uhxb_rb();    // all User_Hxb rows
mb_backup();      // MB_Log
save_settings();  // MSettings
```

Each `all_*_rb` script walks every `pid` and rewrites the corresponding `.bnu` for every uid that has unsaved changes.

## Crash-loss surface

The original is **not crash-safe**. From [.planning/codebase/CONCERNS.md](../../.planning/codebase/CONCERNS.md):

> A machine restart loses everything between saves: 30 seconds of player movement state, any chat lines, any in-flight area changes that hadn't reached the per-player save alarm yet.

The only "transactional" write in the original is single-line file rewrite (open + truncate + write all + close). There is no journaling, no fsync discipline, no double-buffer / atomic-rename pattern. A crash mid-rewrite produces a truncated `.bnu` and the next `users_restore` reads garbage rows until EOF or a malformed record blows up the parser.

## Rebuild persistence answer (forward-link)

The Litestream-replicated SQLite layout in [../adr/0002-persistence-layer.md](../adr/0002-persistence-layer.md) (forward-link; lands in plan 03-07) is the rebuild's answer:

- **All writes are SQLite transactions.** Atomic by construction; no half-rewrite hazard.
- **Litestream replicates WAL pages to Tigris** every 1 s. Crash-loss window drops from "between saves" (~30 s typical, up to 90 s for some files) to ~1 s.
- **No on-disk text files.** `.bno` / `.bnb` / `.bnu` exist only as MIGRATION-INPUT files; the migration in Phase 4 SRV-09 / SRV-10 / SRV-11 reads them once and translates into the SQLite schema, then they are read-only forever.
- **Save cadence is event-driven.** No "every 30 s" alarm — every state change is its own transaction. Throughput is fine because SQLite + WAL handles 10-20k tx/s on Fly.io's local SSD.

## Original file size envelope (informational)

Per CONCERNS, the deployed servers we have in `legacy/servers/enlyzeam-current/` have:

- `localList.txt`: ~50 user rows
- `MB_Log.bnb`: ~80 topics / ~600 replies (single largest file at ~150 KB)
- `User_Inv.bnu`: ~50 rows × ~4 KB / row
- `User_Area.bnu`: ~50 rows × ~1 KB / row

The total persistent footprint is ≪ 1 MB. SQLite handles this without breathing hard.

## See also

- [./save-formats.md](./save-formats.md) — every format's byte grammar
- [./account-auth.md](./account-auth.md) — `localList.txt` / `User_DBUpdated.bnu` migration into argon2id
- [./message-board.md](./message-board.md) — `MB_Log.bnb` lifecycle
- [./room-management.md](./room-management.md) — `User_Area.bnu` per-area state semantics
- [../adr/0002-persistence-layer.md](../adr/0002-persistence-layer.md) — forward-link; lands in plan 03-07
- [../../decomp/wiki/16-bno-bnb-notes.md](../../decomp/wiki/16-bno-bnb-notes.md) — errata-corrected save format reference
- [.planning/codebase/CONCERNS.md](../../.planning/codebase/CONCERNS.md) — crash-loss + plaintext-creds risk register

## Scripts referenced in this subsystem

<!-- AUTOGEN:scripts:start -->
| Script ID | Name | Lines | Used in objects |
|-----------|------|-------|------------------|
| 0368 | uarea_restore | 15 | — |
| 0369 | uarea_backup | 10 | — |
| 0370 | all_uarea_rb | 21 | — |
| 0371 | unews_backup | 10 | — |
| 0372 | unews_restore | 15 | — |
| 0373 | all_unews_rb | 21 | — |
| 0374 | uinv_set | 17 | — |
| 0375 | uinv_get | 7 | — |
| 0376 | uinv_backup | 18 | — |
| 0377 | uinv_restore | 27 | — |
| 0378 | all_uinv_rb | 21 | — |
| 0383 | uhxb_restore | 17 | — |
| 0384 | uhxb_backup | 12 | — |
| 0385 | all_uhxb_rb | 21 | — |
| 0386 | load_settings | 13 | — |
| 0387 | save_settings | 9 | — |
| 0388 | pindex_opt | 29 | — |
| 0389 | all_backup | 4 | — |
| 0390 | debug_log | 7 | — |
<!-- AUTOGEN:scripts:end -->

## Objects referenced in this subsystem

<!-- AUTOGEN:objects:start -->
| Object ID | Name | Sprite | Mask | Events |
|-----------|------|--------|------|--------|
<!-- AUTOGEN:objects:end -->

## Engine functions used

<!-- AUTOGEN:gml-functions:start -->
| GML function | Call sites | Sample script | Wiki link |
|--------------|------------|---------------|-----------|
| `file_text_open_read` | 10 | 0366-mb_restore | [wiki/07-gml-core-functions](../../decomp/wiki/07-gml-core-functions.md) |
| `file_text_open_write` | 6 | 0365-mb_backup | [wiki/07-gml-core-functions](../../decomp/wiki/07-gml-core-functions.md) |
| `file_text_close` | 17 | 0365-mb_backup | [wiki/07-gml-core-functions](../../decomp/wiki/07-gml-core-functions.md) |
| `file_exists` | 8 | 0367-users_restore | [wiki/07-gml-core-functions](../../decomp/wiki/07-gml-core-functions.md) |
| `file_delete` | 2 | 0367-users_restore | [wiki/07-gml-core-functions](../../decomp/wiki/07-gml-core-functions.md) |
<!-- AUTOGEN:gml-functions:end -->
