docs(link): protocol 6 as built — a guarantee, and the kind that pays for it

`link/v6.md` is the spec of record. It is the first bump that adds a promise
rather than data: a command carrying an `idempotencyKey` is executed at most once,
and a repeat is answered with the original reply.

Also in this PR:

- `INTEGRATION.md` — §2 gains v6, the write plane gains a "retrying a command
  safely" section with the four rules a caller needs, §7 gains 425 and the
  paragraph on why it is not 409, and the event catalog gains `champ.boss.killed`.
- `PLAN.md` — §5.5 records that a per-KILL damage table needs no override even
  though a per-HIT event does, which is the observation the new kind rests on;
  §7 documents the key on the inbound frames.
- `EVENTS.md` — §A row 7 and two §G capability rows close, and the failure table's
  "an action succeeds but the ack is lost" loses its "does not exist today".
- `EVENTS_PLAN.md` — Phase 11 splits into 11a and 11b, and `installer` leaves the
  phase: PLAN.md §7.4 made sure no protocol version is hardcoded there, so the
  bundle gate is version-agnostic and needed no change for 5 either.

The live walk's defect is written up in v6.md §6.1 because the naive check
confirms it: an active ChampionSpawn registers an unnamed region over its own
spawn area, so the innermost region containing a champion boss is guaranteed to
have no name — and region registration is deferred, so a lookup at spawn time
answers "Britain" while one at the kill does not.

CRLF preserved throughout; links and anchors checked by hand, this repo has no CI.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-09-04 14:57:42 -05:00
parent 6cb54ecdb7
commit 8b4096dd18
5 changed files with 569 additions and 25 deletions

View File

@@ -178,10 +178,16 @@ Payload per transition: house serial, `from`→`to` level, `X/Y/Z`, `Map`, `BanL
| Player death | `PlayerDeath` | low | |
| Murder | `PlayerMurdered` | low | High-signal for the website. |
| Killer attribution | `OnKilledBy` | medium | `Killed` + `KilledBy`. Better than `PlayerDeath` for PvP feeds. |
| Creature death | `CreatureDeath` | **high** | Every mob kill. Filter or aggregate. |
| Creature death | `CreatureDeath` | **high** | Every mob kill. Filter or aggregate. **Protocol 6 uses it for `champ.boss.killed`**: the handler's first act is to decide this is not a champion, by type, which is one `is` check per death. |
| **Damage table at a kill** | `Mobile.DamageEntries`, read inside `CreatureDeath` | — | Not a stream: the accumulated per-attacker totals, readable **only at the death**, since ServUO discards them with the creature. This is what makes a boss kill attributable without a per-hit event. |
| Aggression | `AggressiveAction` | med-high | Per aggression state change, **not** per swing. |
> ⚑ **No per-hit damage event.** Damage numbers require overriding `Mobile.Damage` / weapon `OnHit`, not an EventSink.
>
> But per-*kill* attribution needs no override at all, and Protocol 6 is where that was noticed:
> `Mobile.DamageEntries` already holds a running total per attacker, and a `CreatureDeath` handler
> reads it before the creature is disposed of. A whole participation signal, for free, at the one
> moment it is worth having.
### 5.6 Progression & activity
@@ -295,6 +301,22 @@ Newline-delimited JSON, one object per line, `serial` as the primary key.
Every inbound handler marshals to the Core thread before touching world state.
### Any inbound command may carry an `idempotencyKey` (Protocol 6)
```jsonc
{"kind":"admin.broadcast","reqId":"r-17","actor":"event:412","text":"…","idempotencyKey":"5f2c…"}
```
The shard executes a key **at most once** and answers a repeat with the original reply, restamped
with the repeat's correlation id and marked `"replayed": true`. That is what makes a world-writing
command safe to retry after a lost acknowledgement, and it is a precondition for every world verb
the event system adds later — see [`v6.md`](v6.md) §2.1.
The gate sits in `BridgeBoot`'s inbound dispatch rather than in each handler, so it covers every
kind including ones a later protocol adds. A command with no key behaves exactly as it did before.
One new outbound kind comes with it, **`bridge.busy`**: a repeat arrived while the original is still
in flight, nothing ran, come back.
### `server.hello` is per-connection, not per-boot
The sidecar restarts independently of the shard, so anything it needs up front must be re-sent on **every** connect. An earlier draft emitted `server.started` once at `EventSink.ServerStarted`; a sidecar that came up second never received it and had no idea which shard it was attached to.