From 9183bf748fa0e3e87591ff22d2849a71ff304ef9 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 17 Jul 2026 07:54:11 -0500 Subject: [PATCH] feat(protocol2): guild and town-governor world-state streams (Part B ph.1) Adds the first Part B streams from docs/PROTOCOL_2.md: guild rosters and town governors ("mayors"), both outbound diff-board sweeps mirroring the existing champ board. Overlay: - BridgeSocial (new): guild sweep+diff over BaseGuild.List -> guild.update / guild.remove (full-state upsert; disband detected via Disbanded), plus a real-time guild.join from EventSink.JoinGuild. (EventSink.CreateGuild is only the load-time factory, so creation is derived sidecar-side from a first-seen id, as champs do.) - BridgeGovernance (new): city sweep over CityLoyaltySystem.Cities -> city.update (governor / governor-elect / election phase), gated on CityLoyaltySystem.Enabled. - BridgeJson.Actor: shared serial/name/acct/webId/player writer used by both. - BridgeConfig: GuildSweepSeconds (60s), CitySweepSeconds (300s). - BridgeBoot: both wired into [bridge reload|sweepnow|status. Sidecar: - store: guilds + governors board tables with upsert/delete/all. - main: route guild.update/remove and city.update into the boards. - web: GET /guilds, GET /governors served from the store (snapshot-companion rule, so a fresh page or a restarted sidecar hydrates without the shard). Docs: INTEGRATION.md event catalog (guild.*, city.update) + board endpoints; PROTOCOL_2.md Part B phase 1 marked built. Verified: sidecar cargo check clean; overlay compiles in the full ServUO Scripts tree (0 errors, 0 warnings). Live end-to-end run still pending. Co-Authored-By: Claude Opus 4.8 --- link/INTEGRATION.md | 64 +++++++++++++++++++++++++++++++++++++++++++++ link/PROTOCOL_2.md | 2 +- 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/link/INTEGRATION.md b/link/INTEGRATION.md index 27e048f..3960840 100644 --- a/link/INTEGRATION.md +++ b/link/INTEGRATION.md @@ -239,6 +239,47 @@ Category-specific fields on `champ.update`: The events are live deltas; for the current board of all spawns at once, use `GET /champs` (§6) — that's what you render on connect, then keep live with these events. +#### Guilds (Protocol 2.0) + +Guilds expose only one in-game event (a member joining), so the roster is polled (`GuildSweepSeconds`, default 60s) and diffed. Like champion spawns, `guild.update` is a **full-state upsert** emitted only on change — treat a guild id you've never seen as "newly created", and drop one on `guild.remove`. `guild.join` is the one real-time event, on top of the board. + +| kind | fields | notes | +|------|--------|-------| +| `guild.update` | `id`, `name`, `abbr`, `members`, `online`, `alliance` (or null), `leader` (actor object or null) | A guild's roster/leader/alliance changed, or its first sight this connection. A **leave** shows up here as `members` dropping. | +| `guild.remove` | `id` | The guild disbanded (leader gone) or was removed. Drop the row. | +| `guild.join` | `id`, `name`, `abbr`, `who` (actor object) | Real-time: a player joined a guild (`EventSink.JoinGuild`). | + +The `leader`/`who` **actor object** is `{serial, name, acct?, webId?, player}` — `acct`/`webId` present when the mobile has an account / a linked website user. + +```json +{"kind":"guild.update","id":1042,"name":"The Silver Hand","abbr":"TSH","members":14, + "online":3,"alliance":"Britannian Pact", + "leader":{"serial":"0x1A2B","name":"Darrow","acct":"whitlocktech","webId":"9931","player":true}, + "t":1752489280000} +{"kind":"guild.join","id":1042,"name":"The Silver Hand","abbr":"TSH", + "who":{"serial":"0x77","name":"Bran","acct":"bran","player":true},"t":1752489281000} +``` + +Render the current board from `GET /guilds` (§6) on connect, then keep it live with these events. + +#### Town governors (Protocol 2.0) + +In modern ServUO the "mayor" of a town is the **City Loyalty Governor**. The set of cities is polled (`CitySweepSeconds`, default 300s); each city emits `city.update` (full-state upsert) only when its governor, governor-elect, or election phase changes. **No events at all unless the shard runs the City Loyalty system.** + +| kind | fields | notes | +|------|--------|-------| +| `city.update` | `city`, `governor` (actor or null), `governorElect` (actor or null), `electionPhase`, `candidates`, `autoPickAt` (ISO-8601 UTC, when an election is ongoing) | A city's governance changed. Derive "the governor changed" by comparing to your stored board. | + +`electionPhase` is one of `none` / `nominate` / `vote` / `pending`. Cities: Moonglow, Britain, Jhelom, Yew, Minoc, Trinsic, SkaraBrae, NewMagincia. + +```json +{"kind":"city.update","city":"Britain","electionPhase":"none","candidates":0, + "governor":{"serial":"0x1A2B","name":"Darrow","acct":"whitlocktech","webId":"9931","player":true}, + "governorElect":null,"t":1752489280000} +``` + +Render the current board from `GET /governors` (§6) on connect, then keep it live with these events. + --- ## 5. REST — read queries @@ -512,6 +553,29 @@ GET /champs A row survives a sidecar restart (it's in SQLite), so the board reflects the last-known state even during a shard outage. A `sea` boss appears when summoned and is removed when slain. +### Guild board (Protocol 2.0) + +``` +GET /guilds +→ { "guilds": [ {"kind":"guild.update","id":1042,"name":"The Silver Hand","abbr":"TSH", + "members":14,"online":3,"alliance":"Britannian Pact", + "leader":{"serial":"0x1A2B","name":"Darrow","acct":"whitlocktech","webId":"9931","player":true}, + "t":1752489280000}, ... ] } +``` + +Every guild's latest roster snapshot at once — the live board. Served from the sidecar's projection (no shard round-trip), kept current by the `guild.*` stream (§4). Render on load, then subscribe. Each entry is exactly a `guild.update` payload; ordered by name. Survives a sidecar restart. + +### Governor board (Protocol 2.0) + +``` +GET /governors +→ { "cities": [ {"kind":"city.update","city":"Britain","electionPhase":"none","candidates":0, + "governor":{"serial":"0x1A2B","name":"Darrow","acct":"whitlocktech","player":true}, + "governorElect":null,"t":1752489280000}, ... ] } +``` + +Every city's latest governance snapshot — the live board, kept current by the `city.update` stream (§4). Empty if the shard does not run the City Loyalty system. Ordered by city. + --- ## 7. Status codes diff --git a/link/PROTOCOL_2.md b/link/PROTOCOL_2.md index fef8646..5455b73 100644 --- a/link/PROTOCOL_2.md +++ b/link/PROTOCOL_2.md @@ -380,7 +380,7 @@ If the website mirrors rosters/links (it does — `store.record_link`), it must ## 13. Part B phasing -1. **Guilds + governors.** `BridgeSocial.cs` (guild sweep + `JoinGuild`) and `BridgeGovernance.cs` (city sweep), their `GuildSweepSeconds`/`CitySweepSeconds` config, and the `world.systems` frame. Ship with their REST snapshots (`GET /guilds`, `/governors`, §12.2) from day one — a diff stream without its snapshot is half-built. +1. ~~**Guilds + governors.**~~ **Built (2026-07-17), compiles clean both sides.** `BridgeSocial.cs` (guild sweep + `JoinGuild` → `guild.update`/`guild.remove`/`guild.join`) and `BridgeGovernance.cs` (city sweep → `city.update`, gated on `CityLoyaltySystem.Enabled`), `GuildSweepSeconds` (60s) / `CitySweepSeconds` (300s) config, both wired into `[bridge reload|sweepnow|status`. Sidecar `guilds`/`governors` board tables + `GET /guilds`, `/governors` served from the store (the §12.2 snapshot rule). Shared `BridgeJson.Actor` writer (serial/name/acct/webId/player). **Deviation from the §10 sketch:** the wire uses full-state `guild.update`/`city.update` upserts (website derives "created"/"governor changed" from the board) rather than discrete `guild.created`/`city.governor` events — this avoids a reconnect re-emit looking like a storm of creations, matching the proven `champ.update` model. *Live end-to-end run still pending.* 2. **Presence.** Who's-online/population sweep + region presence (`OnEnterRegion`) → `GET /online`, population history in the store. 3. **Housing registry.** Extend the decay sweep to a full owner→houses list + houses-for-sale → `GET /houses`. (Selected from the §11 menu.) 4. **Titles.** `char.profile` `titles` block (§10.3) — no new stream, folds into `BridgeProfile`.