docs(rust): protocol 13 opens — the configuration save that answers first (PROTOCOL.md §19)
§19 starts protocol 13 with its first piece: config.write answers pending with a writeId and ceilingMs, a reload fails on the framework log's evidence under a 30 s ceiling judged by what is loaded, a rollback waits for the restore, and the outcome is a staff-class config.outcome event (F9, F10, D178). F15's UTF-8 decode rides in the same release. §11.4 points at the amendment; §2 says 13. PLAYER_WALK.md's configuration steps now expect "Saved. Reloading…", add a cold-compile save and a non-ASCII save, use Kits' chat command instead of a ZoneManager setting that does not exist (D-2), and name Carbon's failure wording as the thing step 4 pins. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -205,9 +205,11 @@ other, which is the whole point of putting them in one container.
|
|||||||
| # | Do this | You should see |
|
| # | Do this | You should see |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 1 | **Open Admin → Rust mod config** and pick the server | The tree the framework actually uses — `oxide/config` on Oxide, `carbon/configs` on Carbon — grouped by plugin, with every loaded plugin's version beside it |
|
| 1 | **Open Admin → Rust mod config** and pick the server | The tree the framework actually uses — `oxide/config` on Oxide, `carbon/configs` on Carbon — grouped by plugin, with every loaded plugin's version beside it |
|
||||||
| 2 | **Open `ZoneManager.json`, change a setting, leave the reload target on its guess, and save** | "Saved, and the plugin reloaded." At the console, `oxide.show`/`c.show` is irrelevant — the proof is the plugin behaving differently, so pick a setting you can see: `Auto Show Zones`, or an entry message |
|
| 2 | **Open `Kits.json`, change Kits' chat command, leave the reload target on its guess, and save** | "Saved. Reloading Kits…", then — without leaving the page — "Saved, and the plugin reloaded." The proof is the game behaving differently: the new command works in chat for every player. (ZoneManager 3.1.14 has no "Auto Show Zones", D-2) |
|
||||||
|
| 2a | **Do step 2 again after the server has been idle over a minute** (Oxide's compiler stops after 60 s) | The same answer, however long the cold compile takes. Protocol 12 rolled this edit back (F9); `rg.config` at the console shows the write waiting, and for how long |
|
||||||
|
| 2b | **Save a value with é, — and an emoji** in a string field | The file on the host holds the same characters, not `é` or `â€"` (F15) |
|
||||||
| 3 | **Check a float nobody touched**, e.g. a rate ending `.0`, in the file on the host | It is still `1.0`, not `1`. This is the trap the whole editor exists for, and a server whose configs are full of whole-numbered floats is where it bites |
|
| 3 | **Check a float nobody touched**, e.g. a rate ending `.0`, in the file on the host | It is still `1.0`, not `1`. This is the trap the whole editor exists for, and a server whose configs are full of whole-numbered floats is where it bites |
|
||||||
| 4 | **Break a config on purpose** — in Raw JSON, give a numeric field a string, or anything the plugin's own class cannot deserialize — and save with that plugin as the reload target | Within about four seconds: *"The plugin did not come back, so the old file was put back automatically"*, the compiler's own line underneath it, and the file on the host back as it was. `oxide.plugins` shows the plugin **loaded** — because the restore was reloaded too |
|
| 4 | **Break a config on purpose** — in Raw JSON, give a numeric field a string, or anything the plugin's own class cannot deserialize — and save with that plugin as the reload target | "Saved. Reloading…", then within a few seconds *"The plugin did not come back, so the old file was put back automatically"*, the framework's own line underneath it, and the file on the host back as it was. `oxide.plugins` shows the plugin **loaded** — the outcome waits for the restore's reload, and says so if the plugin did not come back on the old file either |
|
||||||
| 5 | **Save a nested file** (`Kits/kits.json`, or any `config/<Mod>/x.json`) **and confirm the reload target** | The right plugin reloads. Reloading the wrong one is the failure this field exists to prevent, and it reports success — so check `oxide.plugins`' timestamps, not the website's word |
|
| 5 | **Save a nested file** (`Kits/kits.json`, or any `config/<Mod>/x.json`) **and confirm the reload target** | The right plugin reloads. Reloading the wrong one is the failure this field exists to prevent, and it reports success — so check `oxide.plugins`' timestamps, not the website's word |
|
||||||
| 6 | **Open the bridge's own config** | `Host`, `Port` and `ServerId` are read-only with the reason; `QueueCap` saves; the reload dropdown does not offer this plugin. The save says it was written and **not** reloaded, which is the honest answer — our settings apply on the next deliberate reload |
|
| 6 | **Open the bridge's own config** | `Host`, `Port` and `ServerId` are read-only with the reason; `QueueCap` saves; the reload dropdown does not offer this plugin. The save says it was written and **not** reloaded, which is the honest answer — our settings apply on the next deliberate reload |
|
||||||
| 7 | **Edit a file on the host over SSH while the website has it open, then save from the website** | A conflict, with the current file offered — never an overwrite |
|
| 7 | **Edit a file on the host over SSH while the website has it open, then save from the website** | A conflict, with the current file offered — never an overwrite |
|
||||||
@@ -222,9 +224,12 @@ this phase has two specific things to confirm rather than assume:
|
|||||||
exactly like a command that worked (`CARBON.md` §5), so the proof is `OnPluginLoaded` arriving,
|
exactly like a command that worked (`CARBON.md` §5), so the proof is `OnPluginLoaded` arriving,
|
||||||
not the command being accepted.
|
not the command being accepted.
|
||||||
- **`OnPluginLoaded` / `OnPluginUnloaded` firing at all.** They are the rollback's only evidence. If
|
- **`OnPluginLoaded` / `OnPluginUnloaded` firing at all.** They are the rollback's only evidence. If
|
||||||
either does not fire on a framework, every save there rolls itself back four seconds later and
|
either does not fire on a framework, every save there waits out the 30-second ceiling, and the
|
||||||
reports a plugin that is in fact perfectly fine. `rg.hooks` at the console is the standing answer:
|
plugin then has to judge by whether the target is loaded (PROTOCOL.md §19.1). `rg.hooks` at the console is the standing answer:
|
||||||
both names are in `ExpectedHooks`, so a framework that never raises one shows a zero.
|
both names are in `ExpectedHooks`, so a framework that never raises one shows a zero.
|
||||||
|
- **The failure words.** Since protocol 13 a broken reload is recognised by the framework's log line
|
||||||
|
(PROTOCOL.md §19.1). Step 4 on Carbon is what pins Carbon's wording: if it waits the full 30 seconds
|
||||||
|
before rolling back, the line Carbon printed is not on the list yet.
|
||||||
|
|
||||||
**What counts as a pass:** a setting typed on the website changes what the running game does; a
|
**What counts as a pass:** a setting typed on the website changes what the running game does; a
|
||||||
deliberately broken config leaves the plugin loaded and the operator holding the reason; and no file
|
deliberately broken config leaves the plugin loaded and the operator holding the reason; and no file
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ it is listening without one.
|
|||||||
|
|
||||||
## 2. Versioning
|
## 2. Versioning
|
||||||
|
|
||||||
The wire version is a single integer — **12** as of the optional mods (§18) — declared in
|
The wire version is a single integer — **13** as of the first player walk's fixes (§19) — declared in
|
||||||
**four** places that must agree:
|
**four** places that must agree:
|
||||||
|
|
||||||
| Where | Repo |
|
| Where | Repo |
|
||||||
@@ -1026,6 +1026,11 @@ The plugin, in order:
|
|||||||
not, **restores every file, reloads again, and reports the failure with the
|
not, **restores every file, reloads again, and reports the failure with the
|
||||||
tail of the server's newest log file.**
|
tail of the server's newest log file.**
|
||||||
|
|
||||||
|
> **Amended by protocol 13 (§19.1).** Steps 5 and 6 no longer happen inside the
|
||||||
|
> reply: the plugin answers `pending` once the files are written, fails a reload
|
||||||
|
> on the log's evidence under a 30-second ceiling, and reports the result as a
|
||||||
|
> `config.outcome` event. What follows describes protocol 5 to 12.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "kind": "config.report", "type": "reply", "reqId": "r-9",
|
{ "kind": "config.report", "type": "reply", "reqId": "r-9",
|
||||||
"ok": false, "reloaded": false, "rolledBack": true,
|
"ok": false, "reloaded": false, "rolledBack": true,
|
||||||
@@ -1938,3 +1943,105 @@ untouched.
|
|||||||
| Route | Command | |
|
| Route | Command | |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `POST /titles` | `titles.set` | Opaque object; `cmd` and `reqId` written over the caller's |
|
| `POST /titles` | `titles.set` | Opaque object; `cmd` and `reqId` written over the caller's |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 19. Protocol 13 — the first player walk's fixes
|
||||||
|
|
||||||
|
Planned in [`PLAN_FIXES.md`](../modules/rust/PLAN_FIXES.md): one bump for every wire change the org
|
||||||
|
lead's first walk with a player in the game found (§5 there). It is built on `edge` in Rust-Plugins,
|
||||||
|
Rust-Link and Module-Rust and released together (D177). **This section grows as the pieces land**;
|
||||||
|
the first is the configuration save.
|
||||||
|
|
||||||
|
One fix rides in the same release without a wire change: **the plugin decodes what the sidecar sends
|
||||||
|
as UTF-8** (F15). Protocols 1 to 12 decoded each byte as a character — Latin-1 — so every non-ASCII
|
||||||
|
character the site sent arrived as two or three wrong ones: announcements, titles, kit and zone names,
|
||||||
|
and the text of every configuration file saved from the site. Lines are split as bytes, which is safe
|
||||||
|
because UTF-8 never uses `0x0A` inside a character, and each finished line is decoded whole, so a
|
||||||
|
character that straddles two reads survives. `MaxInboundLineBytes` now counts bytes, as its name said.
|
||||||
|
|
||||||
|
### 19.1 `config.write` answers before the reload does (F9, F10)
|
||||||
|
|
||||||
|
Protocol 5 held the reply across the reload and rolled the edit back if `OnPluginLoaded` had not arrived
|
||||||
|
within four seconds — a window fixed by the sidecar's RPC timeout (§11.4). The walk saw Oxide's compiler,
|
||||||
|
idle for a minute, restart and load Kits 2.85 s later, and a valid edit was put back (F9); and after
|
||||||
|
giving up, the plugin restored and reloaded while that compile was still landing, so what loaded
|
||||||
|
depended on timing (F10).
|
||||||
|
|
||||||
|
**The reply.** Once the files are written and the reload is asked for, the plugin answers at once:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "kind": "config.report", "type": "reply", "reqId": "r-9",
|
||||||
|
"ok": true, "reloaded": false, "rolledBack": false,
|
||||||
|
"pending": true, "writeId": "9f1c…", "reload": "Kits", "ceilingMs": 30000,
|
||||||
|
"files": [ { "path": "Kits.json", "version": "1a4-…", "bytes": 5120, "rewritten": false } ] }
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`writeId`** is the plugin's own id for the write, and the outcome carries it. The sidecar's `reqId`
|
||||||
|
cannot serve: it restarts with the sidecar.
|
||||||
|
- **`ceilingMs`** is how long the plugin will wait for each reload. A website that has heard nothing after
|
||||||
|
twice that (the edit's reload and a restore's) plus a margin can call the write lost, not slow.
|
||||||
|
- A write with **no `reload`**, or whose target is **not loaded**, is answered as before: `pending` is
|
||||||
|
`false` and the reply is the whole answer. So is a framework that **refuses** the reload
|
||||||
|
(`config.error`, `reload-failed`, with the files already restored).
|
||||||
|
- A second write while one is pending is still refused `busy`. The window is longer now, so `busy` is
|
||||||
|
more likely, and a website should hold its save button while it waits.
|
||||||
|
|
||||||
|
**Failing on evidence (D178).** While a reload is pending the plugin reads the framework's newest log
|
||||||
|
file once a second, from where it stood when the reload was asked for. A line that names the target as
|
||||||
|
a whole word (`Kits`, `Kits.cs`, `'Kits v4.4.9'`, never `KitsExtended`) beside one of *error while
|
||||||
|
compiling*, *failed to initialize*, *failed initializing*, *failed to compile*, *failed compiling*,
|
||||||
|
*failed to load*, *failed loading* or *unable to load* is a failure, and the edit is rolled back at
|
||||||
|
once. The bridge's own lines are skipped. A slow compile logs nothing until it succeeds, so it is simply
|
||||||
|
waited for. The Carbon pass of the re-walk pins Carbon's wording.
|
||||||
|
|
||||||
|
**The ceiling, 30 s per phase (F10).** A reload that says nothing either way is judged by **what is
|
||||||
|
loaded**, not by the clock:
|
||||||
|
|
||||||
|
| At the ceiling | Meaning | Outcome |
|
||||||
|
|---|---|---|
|
||||||
|
| loaded, and seen unloading | it reloaded and the hook was missed | the edit stands: `reloaded: true`, with a reason |
|
||||||
|
| loaded, never seen unloading | the framework never reloaded it | the edit stands: `reloaded: false`, "applies at its next load" |
|
||||||
|
| not loaded | it went down and did not come back | rolled back |
|
||||||
|
|
||||||
|
**A rollback waits for the restore.** The files are put back and the target reloaded again, under its
|
||||||
|
own ceiling and the same log watch, and only then is the outcome sent — so it can say whether the
|
||||||
|
plugin is back.
|
||||||
|
|
||||||
|
**The outcome** is an `event`, filed and fed by the sidecar like any other (§8.1):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "kind": "config.outcome", "type": "event", "t": 1790000000000, "serverId": "main", "wipeId": "…",
|
||||||
|
"writeId": "9f1c…", "reload": "Kits",
|
||||||
|
"ok": false, "reloaded": false, "rolledBack": true, "restored": true,
|
||||||
|
"reason": "'Kits' failed to load: Failed to initialize plugin 'Kits v4.4.9'",
|
||||||
|
"log": "…", "elapsedMs": 4210,
|
||||||
|
"files": [ { "path": "Kits.json", "version": "1a4-…", "bytes": 5120, "rewritten": false } ] }
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`restored`** is present only after a rollback: whether the target came back on its old file. `false`
|
||||||
|
means it is down on both files and a person has to look.
|
||||||
|
- **`log`** is present only for a failure: the lines that showed it, or the log's tail when the ceiling
|
||||||
|
passed. It is an operator's console, so the website classes the kind **staff** (§8.5).
|
||||||
|
- **No outcome follows** if the bridge itself is unloaded while a write is pending. The files stay as
|
||||||
|
written; the website calls the write lost after its deadline and re-reads.
|
||||||
|
|
||||||
|
`rg.config` shows a pending write's target, phase (`edit` or `restore`) and the seconds spent of the
|
||||||
|
ceiling.
|
||||||
|
|
||||||
|
### 19.2 The sidecar
|
||||||
|
|
||||||
|
`PROTOCOL_VERSION` becomes 13. No route changes: `config.outcome` is an `event`, so it is stored and fed
|
||||||
|
by its `type` like every other. What goes is the arithmetic protocol 5 needed — `CONFIG_RELOAD_WINDOW`
|
||||||
|
and the test that held two windows inside `REPLY_TIMEOUT` — because the reply no longer waits for a
|
||||||
|
reload. A `504` on `/config/write` still says to re-read, and no longer points at a rollback in
|
||||||
|
progress.
|
||||||
|
|
||||||
|
### 19.3 The website
|
||||||
|
|
||||||
|
Module-Rust records a pending save as **`reloading`** in `rust_config_writes`, with the plugin's
|
||||||
|
`write_id` and a `settle_by` of two ceilings plus 20 s on the database's clock. Ingest settles the row
|
||||||
|
from `config.outcome` by `(server_id, write_id)`, and only while it is still `reloading`, so a replayed
|
||||||
|
frame moves nothing and a late one still lands. A row past `settle_by` reads as **`lost`**. The page polls
|
||||||
|
`GET /api/v1/admin/rust/config/{serverId}/writes/{writeId}` every two seconds until the write settles
|
||||||
|
(D179).
|
||||||
|
|||||||
Reference in New Issue
Block a user