docs(modules): the module-rust plan — 18 decisions of record and a 21-phase schedule #249

Merged
whitlocktech merged 10 commits from docs/rust-module-plan into main 2026-09-15 17:41:17 +00:00
Showing only changes of commit 7d589ff5f2 - Show all commits

View File

@@ -1,6 +1,6 @@
# `module-rust` — the plan
**Status:** approved in outline 2026-09-15, not started. Three decisions of record settled; one
**Status:** approved in outline 2026-09-15, not started. Five decisions of record settled; one
question open (§3).
The [dry run](../rust-dryrun.md) designed this module on paper and deliberately did not build it.
@@ -112,6 +112,60 @@ Two details from [kit][kit] ch. 5 that this depends on and are easy to get wrong
`budgetMs`, our own `retry: false` is unreachable code. Derive one constant from the other and
assert the inequality in a test. The first module this project shipped had exactly that pairing.
### R4 — Rust reaches an operator through the existing installer, behind `--game`
**Decided 2026-09-15 (org lead).** Not a second binary and not a shared-core refactor: the shipped
[`installer`](../../installer/PLAN.md) grows a game dimension, `--game servuo|rust`, and keeps one
release stream, one `doctor`, one `update`, one `uninstall`.
The shape of the work is set by where the coupling already is. **The reusable half is already
game-agnostic** — `src/service.rs` and `src/main.rs` mention ServUO zero times, and `net.rs`,
`diff.rs`, `paths.rs` and `ui.rs` barely more. **The UO-specific half is concentrated in four files**
`install.rs`, `doctor.rs`, `overlay.rs`, `tier.rs` — plus the bundle manifest, where
`OverlayComponent` and `ServUoCompat` name the game in the schema itself.
So the change is: make the bundle's game payload a **variant** rather than an overlay, and
`ServUoCompat` a per-game compat block. That is a schema change on the published **`bundles`**
branch, and it is the part to design before touching code.
**The Rust payload is much simpler than the UO one**, which is what makes this affordable: no source
tree to overlay, no `patches/`, no opt-in patch tier — a Rust install is a `.cs` file dropped into
`oxide/plugins/`, plus the sidecar and its service, which the installer already knows how to do. What
it gains instead is a **prerequisite check**: is Oxide installed, and is its build current enough.
That is `doctor`'s shape, not a new concept.
The protocol pairing check generalises unchanged. `servuo-plugins/overlay.toml` declares the protocol
the overlay speaks and the installer refuses to pair a disagreeing sidecar; `rust-plugins` needs the
same declaration under whatever name the variant gives it, and the refusal is the same refusal.
### R5 — Teams come from Rust's first-party clans; the uMod Clans plugin is the richer tier
**Decided 2026-09-15 (org lead).** Basic functionality is built on **Rust's own clan system**; the
third-party **uMod Clans plugin** is an optional layer for a much richer experience, and lands in
phase 9 rather than phase 6.
The distinction matters more than the names suggest, so it is worth stating precisely:
- **Rust's first-party clans** are seven hooks in [`HOOKS.md`](HOOKS.md) — `OnClanCreated`,
`OnClanDisbanded`, `OnClanMemberAdded`, `OnClanMemberKicked`, `OnClanMemberLeft`, plus colour and
logo — each handing you a `LocalClan`. **All seven are "no return behavior"**, which is exactly
what a read-only bridge wants: there is nothing to abstain from, so [kit][kit] ch. 4's
return-`null`-from-every-veto rule has no work to do here. They need no plugin, and the rig is
already running them (`D:\rust\server\server1\clans.287.db`).
- **Rust's first-party *Teams* are a different system** — twelve hooks, mostly vetoable — and are the
transient in-game squad, not the persistent organisation. They are **not** what core's Team
provider should be fed. Clans is the right choice and this is why.
- **The uMod Clans plugin's API is not in our mirror.** [`HOOKS.md`](HOOKS.md) is the *game's* 477
hooks; a plugin's own API is its own documentation. It is reached through `[PluginReference]` and
is null when absent, making it an R3-shaped dependency that must refuse with a reason. It is also
not currently staged locally and would need pulling and mirroring.
**The one gap to design around:** the seven first-party hooks carry created, disbanded, added,
kicked and left — but **no promote or leader-changed event**. Core's provider requires
`getTeamLeaders`, so leadership is read off `LocalClan` at snapshot time rather than tracked from
transitions. That makes phase 6 *partly* snapshot-driven where the dry run predicted it would be
fully event-driven — a small correction to that document, recorded here rather than silently.
## 3. Open question
**Which other uMod plugins are in the required base set?** Kits is named. The rest of "a couple" is
@@ -129,10 +183,13 @@ means R2's mechanism can be exercised on day one.
Two traps recorded here because both cost time before they were understood:
- **`D:\rust\start.bat` updates the wrong directory.** It runs
`steamcmd +force_install_dir c:\rustserver\ +app_update 258550` and then launches
`D:\rust\RustDedicated.exe`. The server that boots has never been updated by its own script. That
- **`D:\rust\start.bat` updated the wrong directory — fixed 2026-09-15.** It ran
`steamcmd +force_install_dir c:\rustserver\ +app_update 258550` and then launched
`D:\rust\RustDedicated.exe`. The server that boots had never been updated by its own script, which
is why a second, never-booted install exists at `C:\rustserver` and why `D:\rust` is a wipe behind.
Now reads `+force_install_dir d:\rust\`; the original is kept at `D:\rust\start.bat.bak`.
`D:\rust\steamapps\appmanifest_258550.acf` was already present at the same buildid, so the first
corrected run is a delta to the current wipe rather than a 5.9 GB re-download.
- **`C:\oxide_files` is a 2025-04-23 Oxide and must not be copied anywhere.** Oxide ships a patched
`Assembly-CSharp.dll`; that bundle's is 6,842,880 bytes against the live 9,780,224, so copying it
over a real install is a hard downgrade. `D:\rust` is already correct and needs nothing from it.
@@ -143,22 +200,24 @@ re-checks it.
## 5. The phases
Phases 03 produce a working read-only Rust site. 45 are the permissions product. 78 are events.
9 is the optional tier. Each phase ends with its findings written down, as every workstream here does.
Phases 03 produce a working read-only Rust site. 45 are the permissions product. 6 is Teams. 78
are events. 9 is the optional tier. 10 is how any of it reaches somebody who is not us. Each phase
ends with its findings written down, as every workstream here does.
| # | Phase | Repos | Done when |
|---|---|---|---|
| 0 | **The rig.** Fix `start.bat`, update to the current wipe, confirm the Oxide build still matches, install the base set, prove a grant made at the console is visible to a plugin | docs | A current server boots with the base mods loaded and `oxide.grant` demonstrably gates something |
| 0 | **The rig.** Update to the current wipe (the script is fixed), confirm the Oxide build still matches, install the base set, prove a grant made at the console is visible to a plugin | docs | A current server boots with the base mods loaded and `oxide.grant` demonstrably gates something |
| 1 | **Protocol 1 and three skeletons.** Bounded drop-oldest queue, one writer thread, reconnect with a tagged epoch, dial-out; sidecar listener + SQLite + always-on token auth + version header; module from `template/` | all 3 + docs | One hello line travels game → sidecar → module, and killing the sidecar does not stall the game |
| 2 | **The read path.** First hook wave from [`HOOKS.md`](HOOKS.md); events and snapshots kept distinct at the wire; `wipe_id` everywhere; every board re-emitted on connect | all 3 + docs | A restarted sidecar is fully populated within one connection, with no negotiation |
| 3 | **The first pages.** Server list, per-server status, killfeed, leaderboard; `capabilities`; nav | Module-rust | The site renders the last thing the game said while the game is off |
| 4 | **Identity** (R1) | 3 + docs | A player links an account in-game and the site names their Steam ID |
| 5 | **Site-owned permissions** (R2). Groups and grants authored on the site; full set pushed on connect, deltas after; drift reported | all 3 + docs | A grant made on the website gates a third-party plugin in-game, and survives a wipe |
| 6 | **Teams provider.** Event-driven with a baseline on connect — Rust delivers membership transitions in real time, so no sweep | Module-rust + 2 | Core's reconciler is answered from live transitions, `complete` claimed only per reachable server |
| 6 | **Teams provider** (R5). Rust's first-party clans; membership event-driven, leadership read off `LocalClan` at snapshot | Module-rust + 2 | Core's reconciler is answered from live transitions, `complete` claimed only per reachable server |
| 7 | **Events: one budget, one lease.** [kit][kit] ch. 5's own ordering — the lease before the action | Module-rust + 2 | The leased value is observed changing in the running game and restored, per key |
| 8 | **Events: the Kits reward action** (R3) | all 3 | A retried step grants loot once, and the ledger and the world agree |
| 9 | **Optional mod integrations.** One adapter per plugin, each detecting via `[PluginReference]` and degrading to absent | rust-plugins + docs | A server missing every optional mod still runs the module |
| 10 | **Docs, kit feedback, cutover** | docs + Integration-kit | `docs/` describes what shipped; R2's missing chapter answered either way |
| 9 | **Optional mod integrations**, the uMod **Clans** plugin first (R5). One adapter per plugin, each detecting via `[PluginReference]` and degrading to absent | rust-plugins + docs | A server missing every optional mod still runs the module, Teams included |
| 10 | **The installer** (R4). `--game servuo|rust`, the bundle payload as a variant, an Oxide prerequisite check in `doctor`, the protocol pairing refusal carried over | installer + docs | An operator sets a Rust server up with the released binary and nothing hand-copied |
| 11 | **Docs, kit feedback, cutover** | docs + Integration-kit | `docs/` describes what shipped; R2's missing chapter answered either way |
### Why the lease comes before the reward action