diff --git a/modules/rust-dryrun.md b/modules/rust-dryrun.md index 27b2359..f26a7b8 100644 --- a/modules/rust-dryrun.md +++ b/modules/rust-dryrun.md @@ -17,6 +17,11 @@ in. If the contract survives that, "game-agnostic" means something. > Nothing here re-specifies the contract. [`../website/MODULE_API.md`](../website/MODULE_API.md) is > normative; this document only *uses* it. +> +> **Revisited 2026-08-19, for Teams** (`MODULE_API_VERSION` 1.6.0, Teams phase 11). A Rust team is a +> Team, so the design grew a provider and an inverted slot — the two places the contract changed since +> this was written. Everything else stands, including all four findings: the identity gap is still +> open and still the one a real second module hits first. --- @@ -94,13 +99,18 @@ module.exports = function register(ctx, api) { classify: (result) => (result.ok ? { outcome: 'done' } : { outcome: 'retry', error: result.error }), }) + // Teams (1.6.0). A Rust "team" is a Team: core owns the tables, the membership + // sync, the access rules, the forum and the activity feed; this module owns the + // word and the roster behind it. + api.registerTeamProvider(teamProvider) + api.onBoot(async () => { await rcon.connectAll() }) api.onShutdown(async () => { await rcon.closeAll() }) } ``` -Everything above is a call the contract already has, used the way module-uo uses it. Two details are -worth pointing at: +Everything above is a call the contract already has, used the way module-uo uses it. Three details +are worth pointing at: - **`rust.wipe` and `rust.raid` are namespaced**, with no grandfathering request. Module-uo's seven bare stream ids are allowlisted because they were in `notification_subs` before the rule existed @@ -108,12 +118,28 @@ worth pointing at: - **The announce leg goes to in-game chat over RCON**, which is a one-shot delivery with retry — `registerAnnounceLeg`, not `registerPostHook`. The distinction §2.4 draws holds up on a game that has nothing in common with the one it was drawn for. +- **The Team provider is the one registration core calls back into**, and Rust makes two of its rules + bite harder than UO does. `externalId` must survive a rename, and a Rust team has no name at all — + it is a numeric team id in the server's save, which is the right answer and the one a designer is + least likely to reach for. And **`complete` is per SERVER, not per community**: a community running + six servers has six team spaces, so a provider that can reach five of them must leave `complete` + off or core archives every Team on the sixth. Wipes make the same point once a month, on purpose — + a wipe empties every team, and `{ ok: true, complete: true, teams: [] }` is then *true* and core + archiving all of them is *correct*. Which is exactly why an unreachable RCON must answer + `{ ok: false }` instead: the two states are one API call apart and only the module can tell them + apart. ### Tables `rust_servers`, `rust_wipes`, `rust_players`, `rust_player_stats`, `rust_teams`, `rust_events`, `rust_bans`, `rust_maps`. All `rust_`-prefixed, all in one idempotent `schema.sql` fragment. +**`rust_teams` stays this module's table, and core's `teams` stays core's.** They hold the same teams +and neither reads the other: the module ingests from RCON into `rust_teams`, core reconciles by +*asking* the provider, and §2.6's prefix rule forbids the module touching core's table even though +the module is what populates it. A module that wrote `team_members` directly would be racing core's +reconciler for rows it does not own. + **Every table that holds gameplay data carries a `wipe_id`.** That is the whole shape of the game in one column: a leaderboard means "since the last wipe", a base means "on this map", and a player's stats are per-wipe with an all-time rollup kept separately. It has no bearing on the contract — @@ -183,6 +209,13 @@ registry.registerNav('rust', { registry.registerFeatureProvider('rust', 'rust', useRustFeatures) registry.registerExtension('rust', 'admin.users.detail', LinkedSteamAccounts) + +// The INVERTED direction (1.6.0): this module declares places on its OWN team +// page and core fills them. Core publishes no team page — it does not own the +// word — so `/rust/servers/:id/teams/:teamId` is this module's, and core's feed +// and forum are contributed into it. +registry.declareModuleSlot('rust', 'rust.team.detail', { core: 'team.activity' }) +registry.declareModuleSlot('rust', 'rust.team.forum', { core: 'team.forum' }) ``` `Play` is a group core does not have; §3.3 appends an unknown group rather than dropping the items, @@ -190,9 +223,15 @@ so this works and lands at the end of the nav — where an operator can move it, is an ordinary row once it is interleaved. The pages need `PublicLayout`, `PageHeader`, the three `PageState` components, `useAsync` and -`useAuth`: **six of the kit's seven members**, and the seventh (`useSite`) on the wipe-schedule page -for the site's timezone. A second game, unrelated to the first, wanting exactly what the kit -contains is the strongest evidence available that §3.4 was curated at the right altitude. +`useAuth`: **six of the kit's nine members**, plus `useSite` on the wipe-schedule page for the site's +timezone and `Slot` on the team page. A second game, unrelated to the first, wanting exactly what the +kit contains is the strongest evidence available that §3.4 was curated at the right altitude. + +**The team route carries a server id as well as a team id**, which is the Rust-shaped consequence of +the finding two sections down: team `4` on one server and team `4` on another are different teams, +so the module's `externalId` has to be `:` and its page needs both. Core stores +that string and never parses it — an external id is opaque to core by design, and this is the case +that shows why. The map view is the one page that wants something the kit does not have — a pan/zoom canvas. It bundles one, which is the answer §3.4 already gives ("everything else a module bundles itself"), and