feat(champ): stream champion-spawn state to the sidecar board
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0114TpmrNW4wNXsHq5CR72jQ
This commit is contained in:
@@ -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.
|
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
|
## 5. REST — read queries
|
||||||
@@ -402,6 +443,29 @@ GET /economy?limit=200
|
|||||||
→ { "series": [ {"kind":"economy.supply","accounts":52,"gold":110502898,"t":...}, ... ] }
|
→ { "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
|
## 7. Status codes
|
||||||
|
|||||||
Reference in New Issue
Block a user