docs(rust): phase 17 as built and walked — protocol 12, D144 (PLAN §33.5-§33.7, M19)

- PLAN.md §33.5 as built (D144: the plugin keeps its titles on disk, after
  the walk found a hot reload emptied them), §33.6 the walk on both rigs,
  §33.7 findings; the phase row marked built.
- PROTOCOL.md §18: titles.set, a group's BetterChat style on perm.sync,
  chat.say delivery and format, integrations, rg.titles, POST /titles. The
  header's current version moves from a stale 10 to 12.
- PLAYER_WALK.md: the steps that need a person in the game.
- android/PLAN.md M19 as built.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-09-25 18:25:43 -05:00
parent a16e6b6f22
commit a5d918207b
4 changed files with 281 additions and 4 deletions

View File

@@ -50,7 +50,7 @@ it is listening without one.
## 2. Versioning
The wire version is a single integer — **10** as of the rewards (§16) — declared in
The wire version is a single integer — **12** as of the optional mods (§18) — declared in
**four** places that must agree:
| Where | Repo |
@@ -1833,3 +1833,104 @@ database (D111).
| `GET /map/chunk?mapKey=&sha256=&n=` | `map.fetch` | All three required |
| `POST /map/render` | `map.render` | Opaque object; `cmd` and `reqId` written over the caller's |
| `GET /map/live` | `map.live` | |
## 18. Protocol 12 — the optional mods
Added in phase 17 ([`PLAN.md`](../modules/rust/PLAN.md) §33). BetterChat and PopupNotifications
become **optional**: the plugin looks each up when it needs it and answers with a reason when it is
not there (R3). **One new command, two widened commands, one field on the server body, and one console
command.** `overlay.toml`'s `requires_plugins` loses PopupNotifications (D141).
### 18.1 `titles.set`
```json
{"cmd":"titles.set","reqId":"r-40","setId":"<sha256>","titles":[{"steamId":"76561198800000001","text":"[#ff4400]Top Killer[/#]"}]}
```
The chat titles each player holds, **as one whole set** that replaces the last. `text` is already in
BetterChat's markup (`[#hex]…[/#]`, space-separated for a player with several); the site composes it
from the current wipe's standings (PLAN.md §33.2).
- **Answered `titles.ok`** with `count` and `betterChat`: whether BetterChat, as loaded right now,
holds the plugin's getter.
- **Refused `titles.error`** with a `reason` and a `message`: `malformed` (no `titles` array, a
Steam id that is not a number, or an empty or over-long `text` — the whole set is refused, never
half of it), or `too-large` past 500 entries.
- **The plugin swaps the set in by reference**, never editing the one BetterChat is reading, and keeps
it in `titles.json` with the wipe it was pushed on (D144). A hot reload reads the file back when that
wipe is still current; the site re-sends on a new boot id or wipe id as well.
- **The getter** is registered with `API_RegisterThirdPartyTitle` at `OnServerInitialized`, and again
on `OnPluginLoaded` for BetterChat, whose registrations die with its instance. It does one
dictionary lookup and returns null for a player with no title. BetterChat appends it **after** its own
titles and after its `MaxTitles` cut.
### 18.2 `perm.sync`: a group's BetterChat style
A group may carry `chat`, the twelve BetterChat group fields, each with the value the site wants and
the value it last pushed **to this server**:
```json
{"name":"staff","title":"Staff","rank":5,"permissions":[],"members":[],
"chat":{"TitleColor":{"value":"#ff2200","expect":null},"Title":{"value":"[Staff]","expect":"[Staff]"}}}
```
Values are text in the form BetterChat's own setter parses: `true`/`false`, decimal integers, a colour
as `#rrggbb` or a word. The plugin, per field:
- **leaves it** when the game already holds the value (`same`);
- **writes it** through `API_SetGroupField` when the game holds `expect`, or holds BetterChat's
default and `expect` is null, or the group was created by this sync;
- **reports it** as drift, and does not write it, when the game holds anything else — a hand edit (D138).
`API_SetGroupField` never saves, so **one `chat group set <group> Priority <current>` per group
touched**, run as the server console, writes BetterChat's file (D143). A retirement of kind
**`chat-group`** removes the group with `chat group remove`, BetterChat's only way (D139). Both
commands' effects are read back through `API_GroupExists` / `API_GetGroupFields`, never assumed.
The report gains **`chat`**. With BetterChat absent it is `{ "loaded": false }` and no style op is
compiled; the permissions sync as before. Otherwise:
| Field | |
|---|---|
| `version` | BetterChat's |
| `applied` | fields written |
| `saved` | groups saved by the console write, read back |
| `same` | fields already right |
| `drift` | `[{ group, field, game }]`, with the game's value |
| `removed` | groups `chat group remove` took out, or that were already gone |
| `failed` | `[{ group, field?, reason }]`: `InvalidField`, `InvalidValue`, `no-answer`, `not-created`, `not-saved`, `still-present`, `betterchat-unloaded`, `malformed` |
### 18.3 `chat.say`: delivery and a voice
Two optional fields. A protocol-11 caller sends neither and is answered as before.
- **`delivery`**: `chat` (the default) or `popup`. A popup is said with PopupNotifications'
`CreatePopupNotification(message, null, 0)`: everybody connected, the plugin's default duration, and
nothing queued for anybody offline. **Without PopupNotifications it is refused
`chat.error { reason: "popup-unavailable" }`**, with a sentence.
- **`format`**: BetterChat markup with exactly one `{message}`, at most 512 characters. The plugin puts
the flattened line in after the 256-character bound (so the bound is the words', not the markup's),
strips `[` and `]` from words that carry `[#` or `[+` (BetterChat's own rule), and turns the markup
into rich text with `covalence.FormatText`. The line has no sender (D140). A popup ignores it.
`chat.ok` gains `delivery`. **Every line said is logged** (`said (chat): …`), because the game logs
neither a broadcast nor a popup.
### 18.4 `integrations`, and the console
`server.hello` and `server.status` carry
`integrations: { betterChat: { loaded, version? }, popupNotifications: { loaded, version? } }`;
`version` only for a plugin that is loaded.
**`rg.titles [steamId]`** prints how many titles are held, BetterChat's version and whether the getter
is registered, and for a Steam id what is held and what BetterChat renders — through
`covalence.Players`, so only for a player the server has seen.
### 18.5 The sidecar
`PROTOCOL_VERSION` becomes 12. One route; `perm.sync`'s and `chat.say`'s new fields pass through
untouched.
| Route | Command | |
|---|---|---|
| `POST /titles` | `titles.set` | Opaque object; `cmd` and `reqId` written over the caller's |