docs(rust): phase 9 as built — protocol 6 and first-party clans as Teams
PLAN.md §24.4-24.7: walked on the Oxide rig end to end and on Carbon hook by hook; what the rig found (the refusal-first reconcile, the dropped first feed line and its re-offer, core's 40-character dedupe key, one module per site). R6 amended by D47. PROTOCOL.md §12 specifies protocol 6. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -50,7 +50,7 @@ it is listening without one.
|
||||
|
||||
## 2. Versioning
|
||||
|
||||
The wire version is a single integer — **5** as of configuration from the site (§11) — declared in
|
||||
The wire version is a single integer — **6** as of first-party clans (§12) — declared in
|
||||
**four** places that must agree:
|
||||
|
||||
| Where | Repo |
|
||||
@@ -335,8 +335,8 @@ arrives with the phase that needs it, and each is a version bump:
|
||||
|
||||
- ~~identity and the in-game link code (phase 6)~~ — **protocol 3, §9**
|
||||
- ~~the permission mirror (phase 7)~~ — **protocol 4, §10**
|
||||
- plugin configuration edited from the site (phase 7b)
|
||||
- clans, for core's Team provider (phase 9)
|
||||
- ~~plugin configuration edited from the site (phase 7b)~~ — **protocol 5, §11**
|
||||
- ~~clans, for core's Team provider (phase 9)~~ — **protocol 6, §12**
|
||||
- leases, budgets and the event actions (phases 12-13)
|
||||
- the map image over the asset-bridge shape (phase 14)
|
||||
|
||||
@@ -1095,3 +1095,104 @@ re-read rather than to guess, and names the reload window that is probably still
|
||||
running.
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 12. Protocol 6 — first-party clans
|
||||
|
||||
Phase 9, R5. Rust's **own** clan system becomes core's Teams: the plugin reports it, the module
|
||||
answers core's Team provider from it, and the website owns the clan page. Design of record:
|
||||
[`PLAN.md`](../modules/rust/PLAN.md) §24 (D47–D58).
|
||||
|
||||
It is not the uMod **Clans** plugin. That is a separate system that never touches the game's
|
||||
`ClanManager` (D47), so a server running it has two unrelated clan systems, and only the game's
|
||||
becomes Teams. The plugin reads nothing of it except whether it is loaded.
|
||||
|
||||
**The sidecar changed nothing but its version.** One board and five events, filed by `type` (§8.1).
|
||||
|
||||
### 12.1 The `clans` board
|
||||
|
||||
A snapshot, re-sent on connect, on the 60-second cadence, and about three seconds after any clan
|
||||
hook fires, so a roster follows the change that caused it.
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": "clans", "type": "snapshot", "t": 1790158748054, "serverId": "rust-oxide",
|
||||
"enabled": true, "backend": "LocalClanBackend", "supported": true, "truncated": false,
|
||||
"umodClans": false, "count": 1,
|
||||
"clans": [{
|
||||
"clanId": 1, "createdMs": 1790158729260, "name": "Northwatch", "color": "#3fa9f5",
|
||||
"score": 0, "maxMembers": 100,
|
||||
"members": [{ "steamId": "76561190000000001", "rank": 1, "role": "Leader", "joinedMs": 1790158729264, "name": "…" }]
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Meaning |
|
||||
|---|---|
|
||||
| `enabled` | The game's `clan.enabled` convar. `false` is an authoritative answer — no clans — and is `supported` |
|
||||
| `supported` / `reason` | Could the plugin read the clans at all. `false` when the backend has not started, or is not the local one (a **Nexus** server keeps its clans elsewhere) — refused with a reason rather than guessed at |
|
||||
| `truncated` | There may be clans the board does not list. See §12.3 |
|
||||
| `umodClans` | The uMod Clans plugin is loaded. The website warns that its clans are not Teams |
|
||||
| `rank` | The member's role rank. **Rank 1 is leader**, and several members may hold it. Absent when the member's role id matched no role — never defaulted |
|
||||
| `name` | Present when the framework knows the player. Absent, not empty, otherwise |
|
||||
|
||||
A member's `LastSeen` is **not sent**. It is presence, and nothing names who is online by default
|
||||
(PLAN.md §23).
|
||||
|
||||
### 12.2 The five events
|
||||
|
||||
| `kind` | Hook | Carries |
|
||||
|---|---|---|
|
||||
| `clan.created` | `OnClanCreated(LocalClan, ulong)` | clan, founder (`steamId`, `name`) |
|
||||
| `clan.disbanded` | `OnClanDisbanded(LocalClan, ulong)` | clan, who disbanded it |
|
||||
| `clan.member.added` | `OnClanMemberAdded(long, ulong)` | clan, the new member |
|
||||
| `clan.member.left` | `OnClanMemberLeft(LocalClan, ulong)` | clan, the member |
|
||||
| `clan.member.kicked` | `OnClanMemberKicked(LocalClan, ulong, ulong)` | clan, the member, and who kicked them (`bySteamId`, `byName`) |
|
||||
|
||||
"Clan" is always `clanId`, `createdMs` and `clanName`. Every one is **`staff` class** in §8.5's
|
||||
terms: clan membership is members-only (D49), so the public feed never carries it. It reaches a
|
||||
clan's members through core's Team feed, where core decides who is a member.
|
||||
|
||||
`OnClanColorChanged` is hooked too, but produces no event: a colour is a property of the clan, so it
|
||||
travels on the board, and the hook only brings the next board forward.
|
||||
|
||||
Three facts the hook sites impose, all read from the game's assemblies:
|
||||
|
||||
- **The founder's membership fires no `OnClanMemberAdded`.** The game adds them inside the
|
||||
creation, so `clan.created` implies it.
|
||||
- **`OnClanMemberAdded` hands over a bare id**, fired from inside the database layer before the
|
||||
game's cached clan is refreshed. The plugin reads the clan back for `createdMs` and `clanName`; if
|
||||
even that fails the frame carries `clanId` alone and the website matches on it.
|
||||
- **The game raises no promote or demote hook.** Leadership travels on the board only, and the
|
||||
website diffs one board against the next (D54).
|
||||
|
||||
### 12.3 Identity, and the ceiling
|
||||
|
||||
**A clan's identity is `clanId` AND `createdMs`.** The game keeps clans in `clans.<version>.db` with
|
||||
the version hard-coded, so a game update that bumps it starts a fresh file whose ids restart at 1.
|
||||
The website keys a Team on `<serverId>:<clanId>:<createdMs>` (D52), and every clan frame carries
|
||||
both halves for that reason.
|
||||
|
||||
**The game has no "list every clan" call.** Its backend offers get-by-id and get-by-member; the only
|
||||
listing is the clan leaderboard, which runs `SELECT … ORDER BY score DESC LIMIT ?` with the limit
|
||||
**clamped to 100**. So the board lists at most the top 100 clans by score. A board at that ceiling
|
||||
cannot be told apart from one with exactly 100 clans, and says `truncated: true` either way. It also
|
||||
says so if its rows would pass **768 KiB**, well inside the sidecar's 1 MiB line cap, which drops a
|
||||
longer line outright — a board that never arrived would read as a server with no clans.
|
||||
|
||||
The org lead accepted the ceiling (D55) over reading the game's private SQLite schema directly. A
|
||||
truncated board is answered to core as partial, so core adds and updates Teams there and never
|
||||
removes one on its word.
|
||||
|
||||
### 12.4 A hook name another plugin also raises
|
||||
|
||||
The uMod Clans plugin raises `OnClanDisbanded(string tag, List<ulong> members)`, and a Universal
|
||||
form with `List<string>`. Both have **the same name and arity** as the game's
|
||||
`OnClanDisbanded(LocalClan, ulong)`. The bridge declares the game's types exactly, and the framework
|
||||
matches a call to a method by its argument types, so neither call reaches it.
|
||||
|
||||
**Walked on the Oxide rig (2026-09-23):** both uMod-shaped calls were raised from a rig plugin, and
|
||||
the bridge's own `rg.hooks` count for `OnClanDisbanded` stayed at the one real disband, with nothing
|
||||
logged. A loosely typed signature (`object, object`) would have filed the plugin's clans as the
|
||||
game's.
|
||||
|
||||
Reference in New Issue
Block a user