diff --git a/modules/rust/PLAN.md b/modules/rust/PLAN.md index 7b5fb10..bd1c0e7 100644 --- a/modules/rust/PLAN.md +++ b/modules/rust/PLAN.md @@ -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 `/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 `::`.** The clan id alone is not an identity: the game's clan database is `clans..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 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 ---