docs(modules): the Rust dry run, revisited for Teams
The kit's README sends a reader here FIRST - it is the shortest honest picture of
the whole job - and it predated Teams, so it taught a second game to build its
teams as private module data and never mentioned the provider. The two places the
contract changed since it was written are now in it, and nothing else moved:
all four findings stand, including the identity gap, which is still the one a
real second module hits first.
What Rust adds that UO does not, and why it was worth revisiting rather than
noting:
- externalId must survive a rename and a Rust team HAS no name - it is a numeric
team id in the save. The right answer, and the one a designer is least likely
to reach for.
- `complete` is per SERVER, not per community. Six servers are six team spaces,
so a provider that can reach five must leave `complete` off or core archives
every team on the sixth.
- A wipe empties every team, so { ok: true, complete: true, teams: [] } is TRUE
once a month 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.
- The team route carries a server id as well as a team id, so the external id is
<serverId>:<teamId>. Core stores that and never parses it; an external id is
opaque to core by design, and this is the case that shows why.
Also: rust_teams stays the module's table and core's teams stays core's, which is
the boundary worth stating in the one document where both appear; and the kit is
nine members now, not seven.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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 `<serverId>:<teamId>` 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
|
||||
|
||||
Reference in New Issue
Block a user