diff --git a/modules/rust/PLAN.md b/modules/rust/PLAN.md index c07e82c..a8db9f2 100644 --- a/modules/rust/PLAN.md +++ b/modules/rust/PLAN.md @@ -1,7 +1,7 @@ # `module-rust` — the plan -**Status:** approved in outline 2026-09-15, not started. Five decisions of record settled; one -question open (§3). +**Status:** approved in outline 2026-09-15, not started. Six decisions of record settled; two +questions open (§3). 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 @@ -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 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 | -| Sidecar | `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 | -| *(event capability)* | — | — | Declarations inside `Module-rust` ([kit][kit] ch. 5) | +| 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`](https://gitea.whitlocktech.com/RunicGateway/Rust-Link) | `link` | Owns the game connection and the durable copy | +| 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) | -None exist yet. The module is built from [`integration-kit/template/`][kit], which CI holds against a -pinned core — currently `MODULE_API_VERSION` **1.10.0**. +All three were created empty on **2026-09-15**. The module is built from +[`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): @@ -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. - **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. + is null when absent, making it an R3-shaped dependency that must refuse with a reason. + +**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)` / `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, 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 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 -not, and phase 0 cannot finish its install list without it. Everything beyond the base set is -phase 9's optional tier. +The base set question is closed by R6. Two smaller ones are open, neither blocking: -`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