From f8e9ee8cfc749c878aa4ccc5ff8ea30f120e2271 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Jul 2026 05:46:21 -0500 Subject: [PATCH] feat(champ): stream champion-spawn state to the sidecar board MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Champion spawns have no ServUO EventSink, so add a fourth polled stream (BridgeChamps) modeled on BridgeSweeps: enumerate every spawn each tick, fold to a small record, and emit champ.update only on change. No core patch — every field used is public. Covers all three families via a `category` field: - champion: ChampionSpawn (type/level/kills/boss/cooldown ETA) - mini: MiniChamp (type/level; auto-restarts, no kill counter) - sea: BaseSeaChampion (a High Seas world-boss mobile, alive only while summoned; removed via champ.remove when slain) Status folds to active/cooldown/dormant. A (re)connection clears the diff cache so the next sweep re-emits the full board, rebuilding a sidecar that restarted on its own. Transient entries leave via champ.remove. Sidecar: a `champs` current-state table (one row per serial) fed by champ.update (upsert) and champ.remove (delete), exposed at GET /champs as the live board. New ChampSweepSeconds config (default 10s), wired into [bridge reload/sweepnow/status. Documented in docs/INTEGRATION.md. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_0114TpmrNW4wNXsHq5CR72jQ --- link/INTEGRATION.md | 64 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/link/INTEGRATION.md b/link/INTEGRATION.md index 3a37309..d56f249 100644 --- a/link/INTEGRATION.md +++ b/link/INTEGRATION.md @@ -194,6 +194,47 @@ Every event has `t` (epoch ms) and `kind`. A nested actor object looks like `{"s The queue has no in-game event, so it's polled (`PageSweepSeconds`, default 5s) — expect a few seconds' latency, and use `GET /pages` for the authoritative current queue on connect. See §6 to snapshot, respond, and close. +#### Champion spawns + +Champion spawns have no in-game event either, so they're polled (`ChampSweepSeconds`, default 10s) and emitted **only on change**. Three families share the `champ.update` kind, told apart by `category`: + +| `category` | source | what it is | +|------------|--------|-----------| +| `champion` | `ChampionSpawn` | the classic altar spawn (Felucca-style): type, level, kills, boss, cooldown | +| `mini` | `MiniChamp` | the TerMur mini-champ controller: type, level; auto-restarts, no kill counter | +| `sea` | `BaseSeaChampion` | a High Seas world-boss **mobile**, alive only while summoned | + +| kind | fields | notes | +|------|--------|-------| +| `champ.update` | `serial`, `category`, `type`, `name`, `status`, `active`, `map`, `x`,`y`,`z`, `bossUp` — **plus category-specific fields below** | A spawn's state changed (or its first sight this connection). | +| `champ.remove` | `serial` | The spawn left the board: a controller was deleted, or a `sea` boss was slain/despawned. Drop the row. | + +`status` is one of: +- **`active`** — running (or, for `sea`, the boss is alive). +- **`cooldown`** — stopped with a restart pending. For `champion`, `restartAt` (ISO-8601 UTC) is the ETA; `mini` always re-arms but exposes no ETA. +- **`dormant`** — stopped with nothing scheduled (`champion` only; a GM must turn it back on). + +Category-specific fields on `champ.update`: + +| category | extra fields | +|----------|--------------| +| `champion` | `level` (0–16), `rank`, `kills`, `maxKills`, `autoRestart`, `boss` (when `bossUp`), `restartAt` (when `cooldown`), `expireAt` (ISO-8601 UTC — when the current level times out if kills stall, present while `active`) | +| `mini` | `level`, `maxLevel`, `autoRestart` (always true); `bossUp` is always false | +| `sea` | `boss` (its name), `hits`, `hitsMax`; `bossUp` is always true; roams, so `x`,`y`,`z` and `hits` update as it moves/takes damage | + +```json +{"kind":"champ.update","serial":"0x40012345","category":"champion","type":"Abyss", + "name":"Abyss","status":"active","active":true,"level":9,"rank":3,"kills":120, + "maxKills":256,"bossUp":false,"autoRestart":true,"map":"Felucca","x":5187,"y":570,"z":0, + "expireAt":"2026-07-14T11:00:00Z","t":1752489280000} + +{"kind":"champ.update","serial":"0x0002ABCD","category":"sea","type":"Charybdis", + "name":"Charybdis","status":"active","active":true,"bossUp":true,"boss":"Charybdis", + "hits":4200,"hitsMax":5000,"map":"Trammel","x":4123,"y":2311,"z":-5,"t":1752489280000} +``` + +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. + --- ## 5. REST — read queries @@ -402,6 +443,29 @@ GET /economy?limit=200 → { "series": [ {"kind":"economy.supply","accounts":52,"gold":110502898,"t":...}, ... ] } ``` +### Champion-spawn board + +``` +GET /champs +``` + +The current state of **every** champion spawn at once — the live board. Served from the sidecar's own projection (no shard round-trip), kept current by the `champ.update` / `champ.remove` stream (§4). Render this on page load, then subscribe to those events to update in place. Each entry is exactly a `champ.update` payload (same fields, same `category` split); the list is ordered by `name`. + +``` +GET /champs +→ { "spawns": [ + {"kind":"champ.update","serial":"0x40012345","category":"champion","type":"Abyss", + "name":"Abyss","status":"cooldown","active":false,"level":0,"rank":0,"kills":0, + "maxKills":256,"bossUp":false,"autoRestart":true,"map":"Felucca","x":5187,"y":570, + "z":0,"restartAt":"2026-07-14T10:45:00Z","t":1752489280000}, + {"kind":"champ.update","serial":"0x40099999","category":"mini","type":"AbyssalLair", + "name":"AbyssalLair","status":"active","active":true,"level":2,"maxLevel":5, + "bossUp":false,"autoRestart":true,"map":"TerMur","x":987,"y":328,"z":11,"t":...} + ] } +``` + +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. + --- ## 7. Status codes