docs(modules): R6 names the base mod set, and reading Clans settles R5 the other way

The three repos exist and are named: Module-Rust, Rust-Link, Rust-Plugins. Notes
that a repository name is not a module id - Module-uo ships id `uo`, this ships
`rust`, and 2.1 requires id to equal the directory core loads it from.

R6 - the required base set is Kits 4.4.9, Clans 0.2.10 and PopupNotifications
0.2.1, all k1lly0u, all MIT, each with a direct .cs download, so phase 0's
install step is three curls rather than manual retrieval. Clans is listed as a
Universal plugin, written against Covalence, which is why its API takes IPlayer
rather than BasePlayer.

Kits is a good dependency: 23 HookMethods, including GiveKit for the reward
action, GetKitNames/GetAllKits for the authoring form's OPTION SOURCE so an
operator picks a kit from the live server instead of typing an identifier, and
per-player uses and cooldown for eligibility. It also raises OnKitRedeemed,
which the bridge can report as an ordinary event whoever triggered it.

Two traps in GiveKit, both found by reading it rather than reasoning about it.
It returns null on a failure path - `if (!player) return null` - and null is
Oxide's idiom for "no opinion", so the reflex of treating null as fine reports a
reward as delivered when there was no player to deliver it to. Success is the
literal true and a refusal is a message string, which drops straight into
chapter 5's `error`. And GiveKit takes a BasePlayer, so there is no offline
grant: an event rewarding participants at 2am rewards only whoever is online.
Phase 8 chooses between accepting online-only and keeping a persisted
pending-grant queue, which is a second at-most-once store and is not free.

R5 is settled far more firmly, and the other way round from how it was argued.
For Teams the Clans plugin is WORSE than first-party, not richer: it publishes
fifteen HookMethods and every one is a mutation, with no read API at all - no
GetClan, no GetClanMembers, no GetAllClans - and it raises exactly three hooks,
none of them a membership transition. It cannot answer any of core's three
provider questions from its published surface, while first-party clans answer
all three. So first-party is PERMANENT for the provider, not a first step. What
the plugin actually adds is alliances and clan/alliance chat - richer in
features, not in roster data - which is what phase 9 surfaces beside the
provider rather than under it. Reading its own data files is recorded as
deliberately not taken: that is another plugin's private storage, not an API.

Two questions left open in 3, neither blocking: whether Clans being in the base
set while the provider reads first-party is the intended reading (an
interpretation, not something stated), and the offline-grant choice, deferred to
phase 8 on purpose.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
This commit is contained in:
2026-09-15 11:40:10 -05:00
parent 7d589ff5f2
commit 839e5d05a8

View File

@@ -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<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,
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