docs(teams): the Teams bet, as built (Teams cutover 6/6) #169
@@ -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
|
> Nothing here re-specifies the contract. [`../website/MODULE_API.md`](../website/MODULE_API.md) is
|
||||||
> normative; this document only *uses* it.
|
> 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 }),
|
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.onBoot(async () => { await rcon.connectAll() })
|
||||||
api.onShutdown(async () => { await rcon.closeAll() })
|
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
|
Everything above is a call the contract already has, used the way module-uo uses it. Three details
|
||||||
worth pointing at:
|
are worth pointing at:
|
||||||
|
|
||||||
- **`rust.wipe` and `rust.raid` are namespaced**, with no grandfathering request. Module-uo's seven
|
- **`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
|
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 —
|
- **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
|
`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.
|
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
|
### Tables
|
||||||
|
|
||||||
`rust_servers`, `rust_wipes`, `rust_players`, `rust_player_stats`, `rust_teams`, `rust_events`,
|
`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_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
|
**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
|
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 —
|
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.registerFeatureProvider('rust', 'rust', useRustFeatures)
|
||||||
registry.registerExtension('rust', 'admin.users.detail', LinkedSteamAccounts)
|
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,
|
`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.
|
is an ordinary row once it is interleaved.
|
||||||
|
|
||||||
The pages need `PublicLayout`, `PageHeader`, the three `PageState` components, `useAsync` and
|
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
|
`useAuth`: **six of the kit's nine members**, plus `useSite` on the wipe-schedule page for the site's
|
||||||
for the site's timezone. A second game, unrelated to the first, wanting exactly what the kit
|
timezone and `Slot` on the team page. A second game, unrelated to the first, wanting exactly what the
|
||||||
contains is the strongest evidence available that §3.4 was curated at the right altitude.
|
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
|
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
|
bundles one, which is the answer §3.4 already gives ("everything else a module bundles itself"), and
|
||||||
|
|||||||
Reference in New Issue
Block a user