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 839e5d05a8 - Show all commits

View File

@@ -1,7 +1,7 @@
# `module-rust` — the plan # `module-rust` — the plan
**Status:** approved in outline 2026-09-15, not started. Five decisions of record settled; one **Status:** approved in outline 2026-09-15, not started. Six decisions of record settled; two
question open (§3). questions open (§3).
The [dry run](../rust-dryrun.md) designed this module on paper and deliberately did not build it. 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 This is the document that builds it. Where the two disagree, this one is later and wins — but the dry
@@ -20,15 +20,21 @@ chapter by chapter. This document is a *schedule and a set of decisions*, not a
Three new repositories, mirroring the three the platform already has for Ultima Online, plus the Three new repositories, mirroring the three the platform already has for Ultima Online, plus the
optional fourth part that lives inside the module: optional fourth part that lives inside the module:
| Part | New repo | Mirrors | What it is | | Part | Repo | Mirrors | What it is |
|---|---|---|---| |---|---|---|---|
| Website module | `RunicGateway/Module-rust` | `Module-uo` | Routes, schema fragment, prebuilt client chunk, nav | | Website module | [`RunicGateway/Module-Rust`](https://gitea.whitlocktech.com/RunicGateway/Module-Rust) | `Module-uo` | Routes, schema fragment, prebuilt client chunk, nav |
| Sidecar | `RunicGateway/rust-link` | `link` | Owns the game connection and the durable copy | | Sidecar | [`RunicGateway/Rust-Link`](https://gitea.whitlocktech.com/RunicGateway/Rust-Link) | `link` | Owns the game connection and the durable copy |
| Oxide bridge plugin | `RunicGateway/rust-plugins` | `servuo-plugins` | C# inside the game, dials out, never blocks | | Oxide bridge plugin | [`RunicGateway/Rust-Plugins`](https://gitea.whitlocktech.com/RunicGateway/Rust-Plugins) | `servuo-plugins` | C# inside the game, dials out, never blocks |
| *(event capability)* | — | — | Declarations inside `Module-rust` ([kit][kit] ch. 5) | | *(event capability)* | — | — | Declarations inside `Module-Rust` ([kit][kit] ch. 5) |
None exist yet. The module is built from [`integration-kit/template/`][kit], which CI holds against a All three were created empty on **2026-09-15**. The module is built from
pinned core — currently `MODULE_API_VERSION` **1.10.0**. [`integration-kit/template/`][kit], which CI holds against a pinned core — currently
`MODULE_API_VERSION` **1.10.0**.
**The repository name is not the module id.** `Module-uo` ships a module whose `id` is `uo`; this one
ships `rust`. §2.1 of the contract requires `id` to equal the directory core loads it from, which is
`modules/rust/`, and it is the prefix of every table and every mount — so the capitalisation in the
repository name reaches nothing inside the bundle.
Settled before this document and unchanged by it (dry run, org lead, 2026-08-19): Settled before this document and unchanged by it (dry run, org lead, 2026-08-19):
@@ -157,8 +163,83 @@ The distinction matters more than the names suggest, so it is worth stating prec
provider should be fed. Clans is the right choice and this is why. 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 - **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 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 is null when absent, making it an R3-shaped dependency that must refuse with a reason.
not currently staged locally and would need pulling and mirroring.
**Reading that plugin's source settled R5 far more firmly than the reasoning above did, and in a
direction worth stating plainly: for Teams, the plugin is *worse* than first-party, not richer.**
`Clans` v0.2.10 (k1lly0u, MIT, 2,692 lines) publishes **fifteen `[HookMethod]`s and every one of
them is a mutation** — `CreateClan`, `JoinClan`, `LeaveClan`, `KickPlayer`, `PromotePlayer`,
`DemotePlayer`, `DisbandClan`, and the eight alliance verbs. **There is no read API whatsoever**: no
`GetClan`, no `GetClanOf`, no `GetClanMembers`, no `GetAllClans`. And it raises exactly **three**
hooks — `OnClanCreate`, `OnClanChat`, `OnAllianceChat` — **none of which is a membership
transition**.
So it cannot answer any of core's three provider questions from its published surface, while
first-party clans answer all three. Feeding the Team provider from first-party clans is therefore
**permanent, not a first step**.
What the plugin genuinely adds is **alliances and clan/alliance chat** — richer in *features*, not in
roster data. That is what phase 9's adapter surfaces, and it sits beside the Team provider rather
than under it.
One route is deliberately not taken: the plugin persists to its own files under `oxide/data/`, and a
determined integration could read those directly. **That is reaching into another plugin's private
storage, not using an API** — it breaks without warning on any upstream refactor and is not a
contract anybody owes us. If phase 9 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
**Named 2026-09-15 (org lead).** All three are MIT, all by the same author, 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` |
Every one has a direct `.cs` download, so phase 0's install step is three `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.
`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.
**Kits is the opposite of Clans and is a genuinely good dependency.** Twenty-three `[HookMethod]`s,
including the three things the event work actually needs:
- `GiveKit(BasePlayer player, string name)` — the reward action's call;
- `GetKitNames(List<string>)` / `GetAllKits()`**the option source** for the authoring form, so an
operator picks a kit from the live server instead of typing an identifier from memory;
- `GetKitInfo` / `KitDescription` / `KitImage` / `KitMax` / `KitCooldown` /
`GetPlayerKitUses` / `GetPlayerKitCooldown` — form metadata and per-player eligibility.
It also **raises `OnKitRedeemed(BasePlayer player, string kitName)`**, which the bridge can listen on
to report a redemption as an ordinary event, whoever triggered it.
**Two traps in `GiveKit` that phase 8 must handle, both found by reading it rather than by reasoning
about it.**
**`GiveKit` returns `null` on a failure path, and `null` is Oxide's idiom for "no opinion".** The
first line is `if (!player) return null;`. Everywhere else in this ecosystem a null return means
*abstain*, so the reflex — treat null as fine — reports a reward as delivered when there was no
player to deliver it to. The success value is the literal `true`; a refusal is a **message string**,
which drops straight into chapter 5's `error` field. So the mapping is
`result is bool ok && ok` for success, a string for a refusal reason, and **`null` is a failure, not
a success.**
**`GiveKit` takes a `BasePlayer`, so the player must be connected.** There is no offline grant in
this API. An event that rewards participants at two in the morning rewards only whoever is online at
that moment, silently. Phase 8 has to choose: accept online-only and say so in the action's
description, or keep our own persisted pending-grant queue in the bridge and redeem it on next
connect. **The queue is the honest answer and it is not free** — it is a second at-most-once store
with its own idempotency, which is exactly the machinery chapter 4 says to persist in the world save.
Decide it deliberately at phase 8 rather than discovering it from a complaint.
`PopupNotifications` is small and does exactly one thing: `CreatePopupNotification(string message,
BasePlayer player = null, float duration = 0f)`, where a null player makes it global. That is the
server-side notification surface, and it needs no more than that.
**The one gap to design around:** the seven first-party hooks carry created, disbanded, added, **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 kicked and left — but **no promote or leader-changed event**. Core's provider requires
@@ -166,13 +247,22 @@ kicked and left — but **no promote or leader-changed event**. Core's provider
transitions. That makes phase 6 *partly* snapshot-driven where the dry run predicted it would be 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. fully event-driven — a small correction to that document, recorded here rather than silently.
## 3. Open question ## 3. Open questions
**Which other uMod plugins are in the required base set?** Kits is named. The rest of "a couple" is The base set question is closed by R6. Two smaller ones are open, neither blocking:
not, and phase 0 cannot finish its install list without it. Everything beyond the base set is
phase 9's optional tier.
`D:\rust\oxide\plugins\` is currently **empty**, so whatever the set is, all of it is a fresh install. **Clans is in the base set *and* the Team provider reads first-party clans.** Those are not in
conflict and this plan reads them as complementary: the plugin is installed because a community
wants alliances and clan chat, while core's Teams are fed from the first-party system that actually
publishes membership transitions (R5). Phase 9's adapter surfaces the plugin's extra features beside
the provider rather than under it. **Worth confirming** — it is an interpretation, not something that
was stated.
**Offline reward grants.** `GiveKit` requires a connected `BasePlayer` (R6). Whether phase 8 accepts
online-only or builds a persisted pending-grant queue is a real decision with real cost, and it is
deliberately deferred to that phase rather than guessed now.
`D:\rust\oxide\plugins\` is **empty**, so the whole base set is a fresh install in phase 0.
## 4. The test rig ## 4. The test rig