docs(rust): phase 9 plan of record (PLAN.md §24, D47-D58)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -987,7 +987,7 @@ Each phase ends with its findings written down, as every workstream here does.
|
||||
| 7 | **Site-owned permissions** (R2). ✅ **Built 2026-09-21 — as built and findings in §20.** Protocol 4: one verb carrying the whole desired set, diffed by the plugin and drained in bounded steps; groups mirrored as groups; drift reported with adopt and revoke; the `PermissionExists` pre-check as the mechanism; the permission hooks as a live drift *signal* rather than the record. Seven org-lead decisions (§20.0), four defects a browser walk found and a fifth a test did (§20.5). **The site's half is walked end to end, including a restart that emptied the store; the acceptance line below is NOT met** — it needs a second, non-admin Steam account on the rig (§12.5, §20.7) | all 3 + docs | A grant made on the website gates a third-party plugin in-game, survives a wipe, and behaves the same against Oxide's JSON store and Carbon's Protobuf/SQLite one |
|
||||
| 7b | **Mod configuration from the site** (R18). ✅ **Built 2026-09-22 — as built and findings in §21.** Protocol 5: a recursive walk rooted at the framework's own `ConfigDirectory`, a form generated from the values, a raw tier, an explicit reload target, and a write that backs the set up, reloads, watches `OnPluginLoaded` and **restores everything automatically** when it does not arrive. Four org-lead decisions (§21.0), a span-splicing editor so no untouched float is ever rewritten (§21.1), and **four defects a browser found that 179 green tests did not** (§21.5) — the worst being that every save of the bridge's own config was refused while the page said otherwise. **The site's half is walked end to end and the plugin compiles and loads on the live Oxide rig; the acceptance line below needs the sidecar and the game on one host** (§21.4) | all 3 + docs | An admin flips a ZoneManager setting from the website and it takes effect; a deliberately broken config rolls itself back and says why; a nested `<Mod>/x.json` is found and reloads the right plugin |
|
||||
| 8 | **Android leg B** (R10), and the half of R2 a player may see. ✅ **Built 2026-09-22 — as built and findings in §22.** One drawer row under the player group shaped like `module-uo`'s own (the code card first, then what the code got them), gated on `rust` and `PLAYER`; the four refusals kept four pieces of advice; and **one new website route**, `GET /player/rust/permissions`, because phase 7 gave an operator every view of in-game privilege and a player none. Three org-lead decisions (§22.0), the scope arithmetic answered on the server so `inScope` exists once (§22.1), and **the rig moved the sidecar inside the game container** ([`INSTALL_RIG.md`](../../rust-link/INSTALL_RIG.md)), which retired the firewall wall phases 6, 7 and 7b each stopped at | Android-app + Module-Rust + docs | A player links from the app |
|
||||
| 9 | **Teams from first-party clans** (R5). *Preceded by the presence fix (§23), which this phase's roster question produced.* Membership event-driven, leadership read off `LocalClan` at snapshot; **`declareModuleSlot` × 3** for core's `team.notify` / `team.activity` / `team.forum` | Module-Rust + 2 | The clan page is ours, core's contributions land in places we named, and every slot empty still reads correctly |
|
||||
| 9 | **Teams from first-party clans** (R5). *Preceded by the presence fix (§23), which this phase's roster question produced.* **Plan of record in §24** (D47–D58), incl. the 100-clan ceiling the game imposes (D55). Membership event-driven, leadership read off `LocalClan` at snapshot; **`declareModuleSlot` × 3** for core's `team.notify` / `team.activity` / `team.forum` | Module-Rust + 2 | The clan page is ours, core's contributions land in places we named, and every slot empty still reads correctly |
|
||||
| 10 | **Notifications and engagement** (R7). Streams, triggers with `ceiling` and `subjectKey`, audiences, engagement seeds, announce leg, post hook — **the catalogue is §10**, including the in-game-popup question | Module-Rust + docs | The offline raid alert reaches the player whose base it was, and nobody else |
|
||||
| 11 | **Android leg C** (R10). Inbox and notification preferences for Rust triggers | Android-app | A Rust notification arrives on a phone and can be switched off there |
|
||||
| 12 | **Events: budgets, option sources and the leases** (§9). [kit][kit] ch. 5's own ordering — leases before actions — and every key verified live before it is advertised | Module-Rust + 2 | A leased value is observed changing in the running game and restored, per key; `rust.group.membership` expires without core asking |
|
||||
@@ -3407,6 +3407,102 @@ own Integration-kit PR alongside this one.
|
||||
|
||||
---
|
||||
|
||||
## 24. Phase 9 — Teams from first-party clans (plan of record, 2026-09-23)
|
||||
|
||||
R5's phase. Rust's **own** clan system becomes core's Team provider: a clan is a Team, its page is
|
||||
this module's, and core's three contributions (the notification control, the activity feed and the
|
||||
forum) land in places the module names. It was preceded by the presence fix (§23), which this
|
||||
phase's roster question produced, and it builds on that fix's settings table.
|
||||
|
||||
Three repositories and this document: [Rust-Plugins][rp], [Rust-Link][rl] and [Module-Rust][mr],
|
||||
all into `edge`. Android is not in this phase; R10's legs trail by one, and the Team surface reaches
|
||||
the app with core's own Team screens rather than a Rust-specific one.
|
||||
|
||||
### 24.0 The decisions this phase needed
|
||||
|
||||
| # | Decision |
|
||||
|---|---|
|
||||
| **D47** | **The uMod Clans plugin becomes OPTIONAL.** It moves out of `overlay.toml`'s `requires_plugins` and into phase 17's optional tier, which **amends R6**. The two systems share no data (the plugin never touches `ClanManager`), so a server running both splits its players across two unrelated clan systems. First-party clans are the only Teams source, and the admin surface warns when the plugin is loaded that its clans are not Teams. |
|
||||
| **D48** | **A clan roster is shown to clan members and staff by default.** One fleet-wide operator setting with three rungs: `members` (the clan's own linked members, plus staff) · `signed_in` · `public`. It is §23's rule applied to a roster: a roster says who is in a clan and, inside its audience, who of them is on. **Widening it warns that it publishes online status too**, because core's `projectRoster` can withhold rows but not fields. |
|
||||
| **D49** | **The clan activity feed carries membership only (founded, joined, left, removed), and every item is members-only.** Written with `visibility: 'members'` on every item. |
|
||||
| **D50** | **Clan score and colour are in.** Score sorts the Clans tab and colour is shown as a swatch. **Deferred:** clan logos (images over the bridge into `ctx.uploads` is asset-bridge-shaped work that would roughly double the phase) and a Discord `/clan` command (stays in phase 16). |
|
||||
| **D51** | **Protocol 6 is one board and five events.** A `clans` snapshot, re-sent on connect and on the 60-second cadence, plus `clan.created`, `clan.disbanded`, `clan.member.added`, `clan.member.left` and `clan.member.kicked`. The sidecar files by `type` (§8.1), so it only bumps its version. |
|
||||
| **D52** | **`externalId` is `<serverId>:<clanId>:<createdMs>`.** The clan id alone is not an identity: the game's clan database is `clans.<version>.db` with the version hard-coded, so a Facepunch bump starts a fresh file whose ids restart at 1. Without the creation time, the new clan #1 would inherit the old clan #1's Team, its forum and its members-only history, and core would read the swap as a rename. |
|
||||
| **D53** | **`getTeams` is `complete: true` only when every server's board is fresh**, supported and untruncated. Otherwise it answers `complete: false`, which core reads as "add and update, remove nothing". One server being offline must never archive its clans. When no server's board is fresh it refuses. |
|
||||
| **D54** | **Leadership comes from the board, diffed one snapshot against the next**, and is published as `team.leader.added` / `team.leader.removed`. The game raises no promote or demote hook, so this is correct to within one cadence (about a minute). A leader is any member whose role has rank 1, and there may be several. The snapshot is shaped so phase 17 can sharpen it with the uMod plugin's `OnClanUpdate` rather than replace it. |
|
||||
| **D55** | **The board is bounded, and its ceiling is the game's.** The game offers no "list every clan" call; the only listing is the clan leaderboard, which **the game clamps to 100 clans by score**. The org lead accepted that ceiling (2026-09-23) over reading the game's private SQLite schema directly. A server at 100 clans or more, or whose board would not fit on one line of the game link, reports `truncated: true` and the answer becomes partial: Teams there are added and updated but never removed. A **non-local clan backend** (Nexus) is refused with a reason rather than guessed at. |
|
||||
| **D56** | **The pages are `/rust/clans/:externalId`, with a Clans tab on each server's page.** `pageUrlTemplate` substitutes only `{externalId}`, which rules out a route nested under the server. Three module slots are declared: `rust.clan.header` (core's `team.notify`), `rust.clan.detail` (`team.activity`) and `rust.clan.forum` (`team.forum`), one per place, as `module-uo` does. |
|
||||
| **D57** | **Linking or unlinking an account calls `ctx.teams.reconcile`**, so a member's `userId` appears straight away instead of on core's next sweep. |
|
||||
| **D58** | **The clan LIST is public; the roster is not.** Each clan's name, colour, score and member count names no player, so the Clans tab shows them to anyone (org lead, 2026-09-23). Rosters, leaders and who is online stay behind D48's audience. |
|
||||
|
||||
### 24.1 Facts the phase rests on
|
||||
|
||||
Read from the game's own assemblies (`Rust.Clans.Local.dll`, `Rust.Clans.dll` and `ClanManager`
|
||||
in `Assembly-CSharp.dll`, decompiled off the Oxide rig), not from a catalogue:
|
||||
|
||||
- **`clan.enabled` defaults to `true`**, so every server has first-party clans unless an operator
|
||||
turns them off. `ClanManager.ServerInstance.Backend` is null until the backend initialises.
|
||||
- **The backend API has no enumerate-all call.** `IClanBackend` offers `Get`, `TryGet`,
|
||||
`GetByMember`, `Create`, `ListInvitations` and `GetLeaderboard`. `GetLeaderboard(limit)` runs
|
||||
`ListTopClans`, which is `SELECT clan_id, name, score FROM clans WHERE deleted IS NULL ORDER BY
|
||||
score DESC LIMIT ?` with the limit clamped to **10..100**. That clamp is D55's ceiling.
|
||||
- **The local backend is synchronous under an async signature.** Every `ValueTask` it returns has
|
||||
already completed, so the plugin can read results on the main thread without blocking. A
|
||||
backend whose task has not completed is treated as unsupported rather than waited on.
|
||||
- **A leader is any member whose role has `Rank == 1`, and several are allowed.** The game only
|
||||
stops you removing the last one. **Clans cannot be renamed**: `IClan` has no `SetName`.
|
||||
- **Hook sites.** `OnClanCreated(LocalClan, ulong)` fires in `LocalClanBackend.Create` after the
|
||||
commit. `OnClanMemberAdded(long clanId, ulong steamId)` fires inside
|
||||
`LocalClanDatabase.AcceptInvite` with a **bare id**, before the cached `LocalClan` has been
|
||||
refreshed. `OnClanMemberLeft`, `OnClanMemberKicked` and `OnClanDisbanded` fire from `LocalClan`.
|
||||
**The founder's own membership fires no `OnClanMemberAdded`**, so `clan.created` implies it.
|
||||
- **The uMod Clans plugin raises a hook with the SAME name.** It calls `OnClanDisbanded(string tag,
|
||||
List<ulong> members)`, the same name and arity as the game's `OnClanDisbanded(LocalClan, ulong)`.
|
||||
The bridge declares the game's types precisely, so a call with the plugin's types does not match
|
||||
it. This is checked on the rig, which runs the plugin.
|
||||
|
||||
### 24.2 What the plan builds
|
||||
|
||||
**The plugin (protocol 6).** The `clans` board carries, per clan: `clanId`, `createdMs`, `name`,
|
||||
`color` (`#rrggbb`), `score`, `maxMembers`, and `members` (each `steamId`, `rank`, `role`,
|
||||
`joinedMs`, and `name` when the framework knows one). It also carries the board-level facts D53
|
||||
and D55 need: `enabled`, `backend`, `supported`, `truncated`, and `umodClans`, which is whether the
|
||||
optional plugin is loaded (D47's warning). A member's `LastSeen` is deliberately **not** sent: it
|
||||
is presence (§23). The five events carry `clanId`, `createdMs`, the clan's `name` and the Steam
|
||||
ids involved. A membership event also schedules a board re-send a couple of seconds later, so the
|
||||
roster that follows it does not wait a minute.
|
||||
|
||||
**The sidecar.** `PROTOCOL_VERSION` becomes 6. Nothing else changes.
|
||||
|
||||
**The module.**
|
||||
- **The store:** `rust_clans` and `rust_clan_members`, replaced per server from each board, plus
|
||||
`rust_clan_boards`, which records per server when the board's `t` last advanced by the
|
||||
**website's** clock. Freshness is judged by that, so clock skew between the game host and the
|
||||
website cannot make a stale board look fresh.
|
||||
- **The provider:** `getTeams`, `getTeamMembers`, `getTeamLeaders` and `projectRoster` (D48), plus
|
||||
`pageUrlTemplate: '/rust/clans/{externalId}'`. `userId` comes from the module's own link table,
|
||||
`online` from its own presence board, and `rankLabel` from the clan's role name.
|
||||
- **The push:** each `clan.*` event is published to core (`team.created`, `team.disbanded`,
|
||||
`team.member.added`, `team.member.removed`) and written to the Team feed as a members-only item
|
||||
(D49), with a `dedupeKey` from the event's own id so a replay is a no-op. A board that changes
|
||||
the set of clans asks core to reconcile, and a board that changes a leader publishes it (D54).
|
||||
- **The pages:** the Clans tab (public list, D58) and `/rust/clans/:externalId`, whose roster is
|
||||
projected by the same function core calls. The roster setting is a second section on the
|
||||
existing **Rust visibility** admin page, which also lists any server running the uMod plugin.
|
||||
- **The feed:** the five kinds are `staff` class in the public feed's allowlist. They reach the
|
||||
public only through core's members-only Team feed.
|
||||
|
||||
**The docs.** This section as built, `rust-link/PROTOCOL.md` §12 (protocol 6), the R6 amendment
|
||||
and `overlay.toml`'s requirement list.
|
||||
|
||||
### 24.3 Done when
|
||||
|
||||
The phase row's criterion: *the clan page is ours, core's contributions land in places we named,
|
||||
and every slot empty still reads correctly.* Walked against the Oxide rig with real clans, plus a
|
||||
Carbon load of the same plugin file.
|
||||
|
||||
---
|
||||
|
||||
[aa]: https://gitea.whitlocktech.com/RunicGateway/Android-app
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user