docs(modules): the module-rust plan — 18 decisions of record and a 21-phase schedule #249
@@ -1,7 +1,8 @@
|
||||
# `module-rust` — the plan
|
||||
|
||||
**Status:** approved in outline 2026-09-15, not started. **Sixteen decisions of record, no open
|
||||
questions.** Audited against the whole contract, not just the game-facing chapters (§7).
|
||||
**Status:** approved in outline 2026-09-15, not started. **Seventeen decisions of record, no open
|
||||
questions.** Audited against the whole contract, not just the game-facing chapters (§7); the event and
|
||||
engagement catalogues are §9 and §10.
|
||||
|
||||
The [dry run](../rust-dryrun.md) designed this module on paper and deliberately did not build it.
|
||||
This is the document that builds it. Where the two disagree, this one is later and wins — but the dry
|
||||
@@ -188,20 +189,28 @@ storage, not using an API** — it breaks without warning on any upstream refact
|
||||
contract anybody owes us. If phase 17 wants roster data from the plugin, the honest path is an
|
||||
upstream pull request adding a read method, not a file reader.
|
||||
|
||||
### R6 — the required base set is Kits, Clans and PopupNotifications, all k1lly0u
|
||||
### R6 — the required base set: Kits, Clans, PopupNotifications and ZoneManager
|
||||
|
||||
**Named 2026-09-15 (org lead).** All three are MIT, all by the same author, all fetched at plan time:
|
||||
**Named 2026-09-15 (org lead), extended the same day by R17.** All four are MIT; three are k1lly0u's
|
||||
and BetterChat's author differs only in the optional tier. All fetched at plan time:
|
||||
|
||||
| Plugin | Version | Released | Source |
|
||||
|---|---|---|---|
|
||||
| [Kits](https://umod.org/plugins/rust-kits) | 4.4.9 | 2026-06-04 | `https://umod.org/plugins/Kits.cs` |
|
||||
| [Clans](https://umod.org/plugins/clans) | 0.2.10 | 2026-05-06 | `https://umod.org/plugins/Clans.cs` |
|
||||
| [PopupNotifications](https://umod.org/plugins/popup-notifications) | 0.2.1 | 2026-08-09 | `https://umod.org/plugins/PopupNotifications.cs` |
|
||||
| [ZoneManager](https://umod.org/plugins/zone-manager) (R17) | 3.1.14 | 2026-09-02 | `https://umod.org/plugins/ZoneManager.cs` |
|
||||
|
||||
Every one has a direct `.cs` download, so phase 0's install step is three `curl`s into
|
||||
Every one has a direct `.cs` download, so phase 0's install step is four `curl`s into
|
||||
`oxide/plugins/` and no manual retrieval. **Pull them fresh rather than using the copies staged in
|
||||
`Downloads`**, which are an older vintage.
|
||||
|
||||
**The four base plugins use three different conventions for their callable API**, which is worth
|
||||
knowing before someone goes hunting for the wrong one: Kits uses `[HookMethod]`, BetterChat uses
|
||||
`API_`-prefixed methods, and ZoneManager uses plain private methods resolved by name. All three are
|
||||
reached the same way from our side — `[PluginReference]` plus `Call()` — but only the first is
|
||||
greppable as a declared API.
|
||||
|
||||
`Clans` is listed by uMod as a **Universal** plugin rather than a Rust one — it is written against
|
||||
Covalence, and its API takes `IPlayer` rather than `BasePlayer`. That is the portable half of the
|
||||
uMod surface, and it is why the same plugin serves several games.
|
||||
@@ -371,6 +380,63 @@ probe cannot see all of core's** — several core endpoints are mounted at the t
|
||||
under a prefix. A noun from our own domain that equals the module id cannot collide, where
|
||||
`/servers` or `/map` very well might.
|
||||
|
||||
### R17 — ZoneManager joins the required base set, because events need to know where people are
|
||||
|
||||
**Decided 2026-09-15 (org lead).** **[ZoneManager](https://umod.org/plugins/zone-manager)** (k1lly0u,
|
||||
3.1.14 released 2026-09-02, MIT, Rust, ~218k downloads) is a fourth **required** plugin, not an
|
||||
optional one. It is what makes an event able to answer *where a player is*.
|
||||
|
||||
Reading its source changed two things this plan had been vague about, and promoted one action out of
|
||||
the optional tier.
|
||||
|
||||
**Its API is private methods reached through Oxide's reflection `Call()`** — no `[HookMethod]`, no
|
||||
`API_` prefix, which is a third convention among the four base plugins and worth knowing before
|
||||
someone goes looking for one that is not there.
|
||||
|
||||
| Surface | What it gives an event |
|
||||
|---|---|
|
||||
| `IsPlayerInZone(zoneId, player)` → `bool` | the direct question |
|
||||
| `GetPlayerZoneIDs(player)` → `string[]` | every zone a player is in |
|
||||
| **`GetPlayerZoneIDsNoAlloc(player, List<string>)`** | the allocation-free variant — **the one to use on any sweep** |
|
||||
| `CreateOrUpdateZone(zoneId, args, position)` | make a zone |
|
||||
| **`CreateOrUpdateTemporaryZone(..., Plugin owner)`** | make a zone *owned by our plugin* |
|
||||
| **`EraseTemporaryZone(Plugin owner, zoneId)`** | remove one, **scoped to the owner** |
|
||||
| `GetZoneIDs` / `GetZoneName` / `GetZoneLocation` / `CheckZoneID` | the catalogue, for an option source |
|
||||
|
||||
And nine hooks raised: `OnEnterZone` / `OnExitZone`, `OnEntityEnterZone` / `OnEntityExitZone`,
|
||||
`OnZoneInitialize` / `OnZoneUpdated` / `OnZoneDestroyed` / `OnZoneErased`, plus `CanSpawnInZone`,
|
||||
which is a veto our bridge abstains from like every other.
|
||||
|
||||
**Three things this settles.**
|
||||
|
||||
**Participation stops being the hard part.** [`EVENTS.md`](../../website/EVENTS.md) §H rates
|
||||
participation *"the hard part"* for UO and *"substantially easier"* for Rust because hooks carry
|
||||
attacker and victim. ZoneManager makes it exact rather than merely easier: `OnEnterZone` and
|
||||
`OnExitZone` are **presence transitions delivered as events**, so the participation ledger is fed
|
||||
from what happened rather than reconstructed from a sweep. An action's reply carries its
|
||||
`participants` envelope member, and this is where that member gets its content.
|
||||
|
||||
**Advance conditions become expressible.** A phase that waits until *"ten players are at the
|
||||
monument"* is a real gate rather than a wish — it reads zone membership. Without ZoneManager the
|
||||
same condition is distance arithmetic against a point, recomputed on a timer, which is both more
|
||||
expensive and less accurate.
|
||||
|
||||
**`rust.zone.open` moves from the optional tier into the base catalogue, and it can be
|
||||
`reversible: 'ledger'` honestly.** This is the nicer half: `CreateOrUpdateTemporaryZone` takes a
|
||||
**`Plugin owner`** and `EraseTemporaryZone` is **scoped to that owner**, so ZoneManager already has a
|
||||
first-class notion of a zone belonging to the plugin that made it. That is most of the persisted
|
||||
ownership registry [kit][kit] ch. 4 demands — we still keep our own map from core's resource
|
||||
reference to the zone id, but we are not inventing ownership, we are borrowing a concept the plugin
|
||||
already has. And erasing a zone that is gone is a success, which is what `revert` needs.
|
||||
|
||||
**One trap to design against, and it is chapter 4's rule meeting a chatty hook.** `OnEnterZone` and
|
||||
`OnExitZone` fire on the game thread and a large zone with a busy server produces a great many of
|
||||
them. The emit path already enqueues and returns, so the game cannot stall — but **the bridge should
|
||||
subscribe selectively rather than forwarding every transition in every zone**. A zone no event cares
|
||||
about should cost nothing on the wire. Decide the filter with the hooks in front of you at phase 12,
|
||||
and measure it, because a sweep over `GetPlayerZoneIDsNoAlloc` is cheap and a flood of wire traffic
|
||||
is not.
|
||||
|
||||
### R15 — an optional-integration tier, with BetterChat as the first member
|
||||
|
||||
**Decided 2026-09-15 (org lead).** Beyond the required base set (R6) the module carries a tier of
|
||||
@@ -488,7 +554,7 @@ Each phase ends with its findings written down, as every workstream here does.
|
||||
|
||||
| # | Phase | Repos | Done when |
|
||||
|---|---|---|---|
|
||||
| 0 | **The rig.** Update to the current wipe (the script is fixed), confirm the Oxide build still matches, install the base set (R6), prove a console grant reaches a plugin | docs | A current server boots with Kits, Clans and PopupNotifications 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 — Kits, Clans, PopupNotifications, ZoneManager (R6, R17) — prove a console grant reaches a plugin | docs | A current server boots with all four loaded, `oxide.grant` demonstrably gates something, and a test zone reports who is standing in it |
|
||||
| 1 | **Protocol 1, three skeletons, and every bundle seam at once.** Plugin: bounded drop-oldest queue, one writer thread, tagged reconnect epoch, dial-out. Sidecar: listener, SQLite, always-on token auth, version header, rpc correlation. Module: `id: rust`, `/rust` on all three tiers (R14), `schema.sql` **and `purge.sql`**, the vite aliases and shims, `checkExternals`, `checkImports`, the swagger fragment and its staleness check, explicit `onBoot`/`onShutdown`, `capabilities` | all 3 + docs | One hello line travels game -> sidecar -> module; killing the sidecar does not stall the game; all five guards green on an untouched skeleton |
|
||||
| 2 | **Packaging and release.** `release.yml`, the install manifest, the `sha256`, the host allowlist — and a real install into a running core from a manifest URL | Module-Rust + docs | An operator installs the empty module from Admin -> Modules and it reaches `started` |
|
||||
| 3 | **The read path.** First hook wave from [`HOOKS.md`](HOOKS.md); events and snapshots distinct at the wire; `wipe_id` **and server id** on every row (R8); all-time rollups (R12); every board re-emitted on connect | all 3 + docs | A restarted sidecar is fully populated within one connection, and a wipe does not erase a player's history |
|
||||
@@ -682,7 +748,7 @@ its reasoning as the starting point rather than inventing a parallel set.
|
||||
| Dimension | Counts |
|
||||
|---|---|
|
||||
| `rust.prefabs` | objects placed into the world by a run |
|
||||
| `rust.zone.minutes` | zone/dome time held |
|
||||
| `rust.zone.minutes` | zone time held — real since R17 |
|
||||
| `rust.grants` | entitlements granted (R16) |
|
||||
| `rust.announcements` | in-game broadcasts |
|
||||
|
||||
@@ -699,6 +765,7 @@ operator setting a cap of 30 prefabs is setting it per server. Say so on the fie
|
||||
| `rust.options.permissions` | registered permissions |
|
||||
| `rust.options.prefabs` | a plugin-declared constructible allowlist — the analogue of UO's spawn atlas |
|
||||
| `rust.options.monuments` | monument names, shared with the map work (R9) |
|
||||
| `rust.options.zones` | ZoneManager `GetZoneIDs` / `GetZoneName` (R17) |
|
||||
|
||||
Every one resolves from live data and returns `[]` on failure rather than defending with a hardcoded
|
||||
list that will be wrong. A source that refuses degrades its field to free text with a warning and
|
||||
@@ -738,7 +805,7 @@ cleanly, reads back cleanly, and does nothing at all, and neither core nor revie
|
||||
| `rust.kit.entitle` | `change` | `ledger` | R16 — grants the kit's `RequiredPermission`; revert revokes |
|
||||
| `rust.prefab.place` | `change` | `ledger` | §H's verb; revert kills the entity, and needs the persisted ownership registry ch. 4 describes |
|
||||
| `rust.announce` | `notify` | `none` | via PopupNotifications (R6) — global or targeted |
|
||||
| `rust.zone.open` | `change` | `ledger` | §H's other verb; needs a zone plugin, so it belongs in the optional tier (R15) rather than the base |
|
||||
| `rust.zone.open` | `change` | `ledger` | §H's other verb. **Base, not optional, since R17** — `CreateOrUpdateTemporaryZone` takes a `Plugin owner` and `EraseTemporaryZone` is scoped to it, so the undo is real |
|
||||
|
||||
**Rewards are not a contract member.** `EVENTS.md` deleted a `registerEventRewards` registry because
|
||||
it carried four Ultima Online nouns inside a core signature. A reward here is an ordinary action —
|
||||
|
||||
Reference in New Issue
Block a user