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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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`.
|
||||
|
||||
Reference in New Issue
Block a user