docs(modules): the module-rust plan — 18 decisions of record and a 21-phase schedule #249
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user