# KILLS/DEATHS Scoreboard — Root Cause + Fix Plan (Gitea #45) **Status: ROOT-CAUSED, fix NOT written. Requires a DESIGN DECISION (see section 3).** 2026-07-25, from a read-only 9-agent investigation; every binary claim re-verified with capstone against `content/BTL4OPT.EXE`. Provenance: workflow `wf_225a6196-5a8`. Field data: 88 matchlogs in `content/matchlogs/`. ## Headline 1. **The port reads DEATHS from the wrong field.** The binary's `PilotList` draws `pilot+0x27c` (KILLS) and `pilot+0x280` (DEATHS) (`reference/decomp/all/part_014.c:3392-3393`, `fild [edi+0x27c]` / `[edi+0x280]` in `PilotList::Execute` @0x4cabd0). Our port declares `+0x280` as **`pad_0x280`** (`btplayer.hpp:394`), never writes it, and substituted the ENGINE's `Player::deathCount` (+0x200) -- which is the respawn handshake's identity number, seeded to **-2** (`PLAYER.cpp:777`). So a remote pilot's DEATHS cell reads a clamped fake `0` **by construction**. [T1] 2. **A FUNCTION IS MISSING FROM OUR DECOMP EXPORT, and that is why the KB got it wrong.** `BTPlayer::VehicleDeadMessageHandler` lives at **@0x4c05c4** and is **absent** from `reference/decomp/functions_index.tsv`, which jumps `4c052c` (146 B) -> `4c083c` (verified independently). The KB inferred "+0x280 has no writers" from that silence and labelled it padding. The real handler was disassembled from the binary in this pass: it does `cmp ebx,edi / je / inc [edi+0x280] / inc [ebx+0x280]` @0x4c0670-0x4c067a. `btplayer.hpp:231` still misattributes the handler to `@004c012c`, which is really a DropZoneReply-path helper (`inc [ebx+0x200]` @0x4c0139). [T1] **Implication beyond this bug: the export has holes -- an absent function is not evidence of an absent behaviour.** 3. **The observed-tally model is unsound here and only half-wired.** Per kill the port produces exactly TWO counter writes on TWO machines: `++killCount` on the killer's node, `++deathCount` on the victim's node. **Neither rides an update record** -- `Player__UpdateRecord` is `currentScore + dropZoneLocation` only (`PLAYER.h:73-80`) -- so a divergence can never self-heal and **bystanders show 0/0 for everyone, all mission.** [T0/T1] 4. **`BTPlayerCountObservedDeath` is unreachable dead code.** Its only call site (`mech4.cpp:2019-2027`) is gated on `ReplicantInstance` but sits INSIDE `Mech::UpdateDeathState`'s transition block, which a replicant can never enter (it takes the mode-9 early return at `mech4.cpp:1832`). [T1] 5. **"It showed I got a kill I didn't" = last-hitter-takes-all, and COLLISIONS COUNT.** `BTPostKillScore` resolves the killer from `((Mech*)victim)->lastInflictingID`, whose only writer latches the **last applied** hit rather than the fatal one -- and ram damage is an ordinary `TakeDamageMessage`. **5 of the 47 master deaths had a type=0 collision killing blow.** Bump a softened-up mech and the kill posts to your row. `Mech::Reset` also never clears the field. [T1] 6. **The phantom kill is AUTHENTIC 1995.** The binary's kill handler increments `killCount` on the VICTIM's player too (`inc [ebx+0x27c]` @0x4c0397 / `inc [edx+0x27c]` @0x4c03a3). Both of the binary's partner increments write the **wrong column**, which reads as a shipped adjacent-offset slip. [T3 on intent, T1 on the addresses] 7. **A serious unrelated bug found on the way:** `btplayer.cpp:453` does `simulationFlags |= 0x1` where the binary does `or word ptr [ebx+0x18], 1` = `updateModel |= DefaultUpdateModelFlag` = **`ForceUpdate()`** (`SIMULATE.h:146-147`). Bit 0 of `simulationFlags` is `DelayWatchersFlag` (`SIMULATE.h:170`) and **nothing clears it** -- so today **the first death of any pilot permanently disables `ExecuteWatchers` on that player's simulation.** `context/reconstruction-gotchas.md:474-476` flagged this exact line as un-audited; it is now settled. Filed separately. **The fix needs a decision, not just a patch:** the arcade had no convergence channel for the pod's own roster (`@0x513300` slots 6/7 are the ENGINE's Read/WriteUpdateRecord, so 1995 did not replicate the pair either). Section 3 recommends replicating both counters as a documented divergence, because a replicated pair is idempotent and self-healing where an event tally never is. **Step 0 is a BLOCKING PREREQUISITE:** land the #54/#57 death-notification work first, or the DEATHS column stays 0/0/0 in exactly the rounds users complained about and any "fixed" claim would be false. --- # Gitea #45 — "KILLS/DEATHS differ per machine": ROOT CAUSE + FIX PLAN *(read-only pass; nothing modified. All line numbers are the current working tree at `3260238` + the uncommitted `btplayer.cpp` #57 change.)* --- ## 1. VERDICT **The observed-tally model is UNSOUND BY DESIGN in this architecture, and only half of it was ever wired.** Per kill event, the port produces exactly **two** counter writes on **two different machines** and nothing on the other three: `++killCount` runs on the **killer's** node only (the `KillScore` ScoreMessage is built on the victim's node and rerouted over the wire by `Entity::Dispatch`, engine/MUNGA/ENTITY.cpp:244-251 [T0]), and `++deathCount` runs on the **victim's** node only (the `VehicleDead(-1)` handler, game/reconstructed/btplayer.cpp:436) [T1]. Neither counter is in any update record — `Player__UpdateRecord` is `currentScore + dropZoneLocation` and nothing else (engine/MUNGA/PLAYER.h:73-80, PLAYER.cpp:631-641/645-661 [T0]), and `BTPlayer` adds no override — so **a divergence can never self-heal**, and every bystander shows 0/0 for everyone forever. The intended symmetric half, `BTPlayerCountObservedDeath` (btplayer.cpp:1581), is **unreachable dead code**: its only call site (mech4.cpp:2019-2027) is gated on `ReplicantInstance` but sits *inside* `Mech::UpdateDeathState`'s transition block, which a replicant can never enter — a replicant learns of the death as replicated `simulationState 9`, which is exactly what routes it to the mode-9 early return at mech4.cpp:1832 [T1]. On top of that the port **reads DEATHS from the wrong field**: the binary's PilotList draws `pilot+0x27c` and `pilot+0x280` (reference/decomp/all/part_014.c:3392-3393; `fild [edi+0x27c]`/`[edi+0x280]` in `PilotList::Execute` @0x4cabd0) [T1], but the port declares `+0x280` as `pad_0x280` (btplayer.hpp:394), never writes it, and substituted the **engine's** `Player::deathCount` (+0x200) — which is the respawn handshake's identity number (seeded to **−2** at PLAYER.cpp:777, gated at PLAYER.cpp:230) and therefore reads a clamped fake "0" on every remote row by construction (btplayer.cpp:1547-1554) [T1]. Finally, the port faithfully reproduces a **1995 phantom kill**: the `KillScore` branch increments `killCount` on the *victim's* player as well as the killer's (btplayer.cpp:702/704 ≡ `inc [ebx+0x27c]` @0x4c0397 / `inc [edx+0x27c]` @0x4c03a3, decomp part_013.c:10633-10636) [T1]. **Why "faithful" cannot fix this:** the binary is symmetric-but-crossed. I disassembled the un-exported handler myself — `@0x4c05c4` is `BTPlayer::VehicleDeadMessageHandler` (handler table `0x512fec=0x17 / 0x512ff0→"VehicleDead" / 0x512ff4=0x4c05c4`, dumped from `content/BTL4OPT.EXE`) and it does `cmp ebx,edi / je / inc [edi+0x280] / inc [ebx+0x280]` @0x4c0670-0x4c067a, where `edi = GetEntityPointer(msg+0x2c)` is the **killer's Player** (proved by `Player__StatusMessage(edi, type 1, 6.0f)` @0x4c064d added to `this` @0x4c0668, and by `this->objectiveMech(+0x284) = edi->playerVehicle(+0x1fc)` @0x4c06dd-0x4c06e3 — the revenge lock) and `ebx = this` is the **victim's Player** [T1]. So each handler bumps the *same* offset on *both* parties: one kill gives both pilots +1 in column 1 on the killer's machine and +1 in column 2 on the victim's machine. That is the reported bug, in 1995. And `@0x513300` slots 6/7 are `0x42e2ac`/`0x42e2e4` — the **engine's** `Player::Read/WriteUpdateRecord` — so the arcade did not replicate the pair either [T1]. There is no authentic convergence channel for the pod's own pilot list; the console messages (@0x4c06a4-0x4c06da and the kill-side twin) go to `ConsoleClientID` and never feed a pod's roster. **A decision, not just a patch, is required.** Two facts that dissolve standing uncertainties: - **Column semantics are settled without the art.** `+0x27c` is written *only* by the KILL handler and `+0x280` *only* by the DEATH handler (whole-image: ctor zeroes at `param_1[0x9f]/[0xa0]`, part_013.c:10873-10874; the two `inc` pairs; the two `fild` reads). Whatever the Comm pixmap prints, column 1 is the kill-event counter and column 2 the death-event counter. [T1] - **`FUN_004c05c4` is ABSENT from the decomp export** — `reference/decomp/functions_index.tsv` jumps `4c052c` (146 B) → `4c083c` [T1]. That absence is *the* root cause of the whole wrong mental model: the KB inferred "+0x280 has no writers" from silence and labelled it a pad. `btplayer.hpp:231` still attributes `VehicleDeadMessageHandler` to `@004c012c`, which is actually a DropZoneReply-path helper (`inc [ebx+0x200]` @0x4c0139 — I disassembled it). --- ## 2. PER-SYMPTOM MAP Field baseline I re-derived independently (88 files in `content/matchlogs/`, 42 `-egg OPERATOR.EGG`, 32 with `MISSION st=4 run`; **7 death-bearing multi-node rounds** of 5,5,5,4,4,3,3 nodes + one 3-node no-death round; **47** `DEATH inst=M`, **96** `inst=R`, **43** `PLAYER_DEAD`; **max 5 game nodes** — host 1 is the relay, `PEER_UP host=1 type=3`). **The parent brief is wrong on three counts: 88 files not 45; the final round had THREE deaths not two; and the brief's "two nodes each" award claim is a different, earlier round (the `:108/:109` round).** The final round is `-egg OPERATOR.EGG`, `MISSION run 00:04:50.775` LIVINGROOM-local, 5 nodes (2=MS-FIREFLY, 3=XIAOLONG, 4=SIM-PIT, 5=LIVINGROOM-PC, 6=GAMERSLAB), victims **4:21, 6:21, 5:21 — all three killed by 3:21 (XIAOLONG)** — and all three kill awards land on XIAOLONG alone (`SCORE player=3:1 type=2 award=5.00/35.50/11.76 kills=1/2/3`, matching the victims' `killdmg 5.000/35.502/11.764` measured on three *different* machines). **Zero `PLAYER_DEAD` on any of the five nodes.** | Player quote | Speaker's seat | Mechanism (file:line) | Fixed by #57? | |---|---|---|---| | **"im seeing cyd got a kill but no death count"** | the **killer's** node (only a node that ran the kill handler shows any kill at all) | Two stacked bugs on one row: (a) **phantom kill** — `if (sender_owner) ++sender_owner->killCount;` btplayer.cpp:704, where `sender_owner = MECH_OWNING_PLAYER(sender_mech)` and `senderMechID` is the **victim** (btplayer.cpp:1723); the object bumped is the victim's *replicant* `BTPlayer`, which is exactly the object the row reads (roster = local "Players" group, mechmppr.cpp:1258-1300). (b) **no death** on that row because nothing on this node writes the victim's deaths: the observed tally is dead code (mech4.cpp:2019) and the remote copy's `Player::deathCount` is stuck at the −2 seed, clamped to 0 (btplayer.cpp:1551). | **NO** | | **"its showing i got a kill also which i didnt"** | speaker's **own row on their own screen** — the phantom cannot do this (it credits the victim, on the killer's node) | **Last-hitter-takes-all via `lastInflictingID`.** `BTPostKillScore` resolves the killer from `((Mech*)victim)->lastInflictingID` (btplayer.cpp:1706-1710); that field's only writer is `Mech::TakeDamageMessageHandler` (mech.cpp:746) and it latches the **last applied** hit, not the fatal one, and **a bare collision counts**: ram damage is a normal `TakeDamageMessage` (mech4.cpp:6606-6629). 5 of the 47 master deaths had a **type=0 (collision)** killing blow — including this round's `6:21` (`amt=35.502`) and `4:47` (`RAM raw=109268.5 → amt=109.269`). Bump a softened-up mech and the kill posts to **your** node, **your** row. Secondary: `Mech::Reset` never clears `lastInflictingID`/`lastInflictingDamage` (mech4.cpp:1708-1822), and the binary *seeds* it to the mech's own id in the ctor (@0x4a16ed-0x4a16fb) where our port leaves it 0:0. | **NO** | | **"K/D still not showing in upper right"** | a **bystander** (MS-FIREFLY, and any node not party to a kill) | Structural: only the two participants' nodes write anything; neither counter rides an update record (PLAYER.h:73-80). Bystanders show 0/0 for every pilot for the whole mission. | **NO** | | **"I'm not seeing any deaths on my screen"** | any node — this one has **three** layers | (i) **remote pilots' deaths are never tallied anywhere**: `BTPlayerCountObservedDeath` unreachable + no replication + −2 seed → clamped 0. (ii) **in the final round even the victims' OWN deaths were not tallied**: 5 logs, 3 `DEATH inst=M`, **0 `PLAYER_DEAD`** ⇒ the `deathCount == -1` branch never reached `btplayer.cpp:436` on any of the three victim nodes. That is the **#54 defect**, not #45. (iii) DEATHS is read from `Player::deathCount`, i.e. it is coupled to the respawn cycle and to the #57 latch. | (i) NO · (ii) **NO — see below** · (iii) partially | **#57 explains none of the four quotes, and the comment now in the working tree is factually wrong.** btplayer.cpp:1294-1305 asserts "*tonight 3 of 3 deaths in the final round were swallowed, each by a player who had respawned successfully earlier in the same process*." `deathPending` is set at **exactly one site** (btplayer.cpp:413), immediately *downstream* of the dedup and *upstream* of `++deathCount` (:436) and the unconditional `PLAYER_DEAD` emit (:441-443); it starts 0 in the ctor (:1355). Each of those five logs contains exactly **one** `MISSION run` and each victim has exactly **one** `DEATH inst=M` and **zero** `PLAYER_DEAD` — so the failing death was that pilot's **first** death in that process and **no latch could have been stranded**. Same for the 4th case (MYTHRIC-BEAST `6:25`, `74.99.73.241_matchlog_20260724_230825_39056.txt`). Corpus-wide, `PLAYER_DEAD == DEATH inst=M` in **every** log except those four. #57 is a real bug correctly fixed; it is not what bit them. **Do not let it stand in the changelog as the explanation for the final round.** Elapsed-time correlation also rules out a mission-timer/`MissionEndingState` cause: tallied deaths occur at t+415 s, +421 s, +428 s, +464 s, +469 s while untallied ones occur at +274 s, +381 s, +401 s, +497 s. No PEER_DOWN precedes any of the four. The surviving candidate is the one `docs/RESPAWN_REARM_PLAN.md` §6 already named: **`Mech::GetPlayerLink() == 0` on the victim's own master mech**, which is the *single shared gate* of both dispatch sites (btplayer.cpp:1735 and mech4.cpp:2045) — and the kill award landing on the killer's node in all four proves `BTPostKillScore` ran past btplayer.cpp:1724, so the failure is at or after :1735. `playerLink` is NULL-initialised (ENTITY.cpp:935) and written only by `Entity::PlayerLinkMessageHandler` (ENTITY.cpp:807-813) with **no null check, no retry, no log**, fed by one `InitializePlayerLink` at drop-in (btplayer.cpp:1216). --- ## 3. THE FIX **Design decision, stated plainly:** the two counters must be **replicated, not observed**. Justification, in order of force: (1) a per-node observed tally *cannot* work for KILLS on a bystander because the killer is unknown there — all 96 `DEATH inst=R` records carry `killer=0:0 killdmg=0.000`, because `lastInflictingID` rides no record (mech.cpp:2338-2545 writer / :2002-2302 reader) and damage is applied only on the victim's master (0 of 5574 `DMG` records are `inst=R`); (2) the binary offers no convergence channel for the pod roster, so *no* choice here is authentic and the honest move is the smallest documented divergence; (3) `Player` already replicates `currentScore` through this exact record and is already `ForceUpdate()`d on every change (btplayer.cpp:551/765/776) — which is why the SCORE column agrees cross-node while K/D cannot. **A replicated pair is idempotent and self-healing: it converges after a dropped packet, which an event tally never does.** **Double-count safety is achieved by construction: exactly ONE writer per counter, always the owning master, and every other node is a pure reader.** After the deletions below, `killCount` is written only in `BTPlayer::ScoreMessageHandler`, which `Verify`-aborts on a replicant (btplayer.cpp:597-606 ≡ binary @0x4c02f3) — master-only; `deaths` is written only in the `deathCount == -1` branch, reachable only on a master because `Entity::Dispatch` reroutes replicant dispatches. **No node ever both observes and receives.** ### Step 0 — BLOCKING PREREQUISITE: land the #54 death-notification fix first Without it the DEATHS column is still 0/0/0 in the exact round the users complained about, and any "#45 fixed" claim would be false. Land `docs/RESPAWN_REARM_PLAN.md` §4 Step 0 + Step 1 as written: the `DEAD_NOTIFY` record at mech4.cpp:2045 and `PLAYER_LINK` at the tail of `InitializePlayerLink`, plus the reverse-link fallback (`Player::playerVehicle == this`, resolved in `mech.cpp` where both types are complete — **not** a raw offset) at `Mech::PlayerLinkMessageHandler` (mech.cpp:675) and at the point of use (mech4.cpp:2044). Also clear `deathPending = 0` in the first-spawn branch (btplayer.cpp:~1239). ### Step 1 — give DEATHS the authentic field, and stop reading the respawn counter - `game/reconstructed/btplayer.hpp:393-394` — `int pad_0x280;` → `int deaths; // @0x280 the binary's DEATHS cell (inc @0x4c067a)`. **New name, not `deathCount`** — re-declaring an engine-base field is gotcha #1 (reads `0xCDCDCDCD` + mis-offsets). - `btplayer.hpp:324` — `GetDeaths()` returns `deaths`, not the inherited `deathCount`. - `btplayer.cpp:1478` — `pad_0x280 = 0;` → `deaths = 0;`. - `btplayer.cpp:1541-1554` — `BTPilotDeaths`: **delete the `< 0 ? 0` clamp** and the comment block explaining it. The clamp existed only to hide the −2 seed of a field that is no longer the display value; keeping it would mask a real negative. - `game/reconstructed/btl4gau3.cpp:511-516` — fix the comment ("DEATHS uses the real deathCount, not the binary's dead pad_0x280" is now provably backwards); the two `Draw` calls stay as-is. - `btplayer.cpp:436-443` — **add `++deaths;`** immediately before `++deathCount;`, and print `deaths` in the `PLAYER_DEAD` record alongside it. **Keep `++deathCount`**: in the binary +0x200 is advanced on the *respawn* side (`inc [ebx+0x200]` @0x4c0139 inside `FUN_004c012c`, whose decompiled caller is DropZoneReply @0x4bffd0, part_013.c:10480) and `@0x4c05c4` only *reads* it (@0x4c072b) — but our respawn handshake and #57 both depend on the current placement, and changing it risks the identity gate at PLAYER.cpp:230. Document the divergence; do not touch it in this change. ### Step 2 — fix the mis-transcribed dirty mark (this is a bug fix, and it is what makes replication authentic) `btplayer.cpp:453` — `simulationFlags |= 0x1; // request a forced update` → **`ForceUpdate();`**. `context/reconstruction-gotchas.md:474-476` flags this exact line as the one un-audited sibling of Gitea #12 and says "verify against the VehicleDead handler's disasm before changing". **Settled here:** the instruction is `or word ptr [ebx+0x18], 1` at 0x4c0155 = `updateModel |= DefaultUpdateModelFlag` = *exactly* `Simulation::ForceUpdate()` (SIMULATE.h:146-147) [T1]. `simulationFlags` bit 0 is `DelayWatchersFlag` (SIMULATE.h:170) and nothing clears it — so today the first death of any pilot **permanently disables `ExecuteWatchers` on that player's simulation**. The binary marks the player for replication right after the death bookkeeping; we currently do not mark it at all. ### Step 3 — delete the partner increments and the dead tally - `btplayer.cpp:703-704` — **delete** `if (sender_owner) ++sender_owner->killCount;`. This is the phantom kill. **Keep** the `Player__StatusMessage(sender_owner, 0, 6.0f)` block at :717-726 — that is a separate, authentic behaviour (@0x4c0632-0x4c0668's mirror). Do **not** add the binary's partner death bump (`inc [edi+0x280]` @0x4c0674); we never had it. Record the finding: both binary partner increments write the **wrong column** (kill handler → the victim's KILLS instead of DEATHS; death handler → the killer's DEATHS instead of KILLS). The mirror-image symmetry, plus the two self-kill guards (`cmp edi,eax/je` @0x4c0393 and `cmp ebx,edi/je` @0x4c0670) being deliberately per-other-party, makes a shipped 1995 adjacent-offset slip the only economical reading. **[T3 on intent, T1 on all four addresses.]** Under replication the partner writes are redundant *and* would fight the replicated value. - `btplayer.cpp:1572-1592` — **delete `BTPlayerCountObservedDeath`** and its comment block; `btplayer.hpp:389` — delete the `friend`; `mech4.cpp:2014-2027` — delete the whole `ReplicantInstance` block and its comment (which asserts the opposite of mech4.cpp:1836-1840 180 lines earlier). It cannot execute today, and under replication it would double-count. ### Step 4 — replicate the pair (the one deliberate divergence) In `btplayer.hpp`, beside the existing message typedefs: ``` struct BTPlayer__UpdateRecord : public Player::UpdateRecord { int killCount; int deaths; }; ``` plus `typedef BTPlayer__UpdateRecord UpdateRecord;` and, in the `protected:` block next to the handlers, overrides matching `Simulation`'s virtuals (SIMULATE.h:127-131; Player's own overrides at PLAYER.h:431-436 are the pattern): ``` void WriteUpdateRecord(Simulation::UpdateRecord *record, int update_model); void ReadUpdateRecord(Simulation::UpdateRecord *record); ``` Bodies in `btplayer.cpp`, modelled exactly on PLAYER.cpp:645-661 / :631-641: - **Write:** `Player::WriteUpdateRecord(record, update_model);` **then** `update->recordLength = sizeof(*update); update->killCount = killCount; update->deaths = deaths;` — the length overwrite must come **after** the base call, because each level rewrites it (ENTITY.cpp:343 → PLAYER.cpp:657). - **Read:** `Player::ReadUpdateRecord(message);` then copy both ints back. `simulation->ReadUpdateRecord(update)` is a virtual call (ENTITY.cpp:388) so this resolves correctly on a replicant `BTPlayer`. - Add `ForceUpdate();` after `++killCount` (btplayer.cpp:702) — btplayer.cpp:776 already covers the normal exit, but the `playerVehicle == 0` early return at :762-767 must also ship it (it already calls `ForceUpdate()`; verify). - Flag it: **`@0x513300` slots 6/7 are the engine's, so the arcade did NOT override these — this is a documented PORT DIVERGENCE [T3]**, taken because no authentic mechanism converges the pod roster. ### Step 5 — drop the duplicate `VehicleDead(-1)` dispatch `btplayer.cpp:1732-1742` — **delete** the victim dispatch from `BTPostKillScore`; keep `mech4.cpp:2044-2058`. Both fire in one straight-line pass of `UpdateDeathState` (`BTPostKillScore` is called at mech4.cpp:1963, the second dispatch at :2053, no return between) on the same `GetPlayerLink()`, and `Dispatch` at a valid master receives synchronously (ENTITY.cpp:268) — so the second **always** trips the dedup and fires the always-on `SWALLOWED` warning on every single death, making the #57 tripwire a permanent false positive. The binary sends exactly one (built @0x4a07c0, dispatched @0x4a0887). `BTPostKillScore` then means only what its name says. ### Step 6 — same wave, sequenced AFTER the counters: restore the authentic killer channel The 1995 `VehicleDeadMessage` is BT-extended and carries the **killer's PLAYER EntityID at msg+0x2c** (resolved @0x4c0611-0x4c062b); our engine class has only `deathCount` + `dropZoneID` (PLAYER.h:107-123) and `BTPostKillScore` constructs it with no killer at all. Subclass it in BT (`class BTPlayer__VehicleDeadMessage : public Player::VehicleDeadMessage { EntityID killerPlayerID; };`), build it at mech4.cpp:2048, and wire the **three** authentic consumers the port fakes or omits: `killerName` (btplayer.cpp:464 is a canned `SelfDestructName`, and the comment at :456-464 wrongly says the binary reads msg+0x1c — that is the `deathCount` the `-1` test uses at 0x4c05df), the **type-1 "KILLED BY" ticker line** (@0x4c0641-0x4c0668, the mirror of the type-0 cell at btplayer.cpp:717-726), and the **revenge lock** `objectiveMech = killer->playerVehicle` (@0x4c06dd-0x4c06e3). Not required for #45's counters — **do not gate #45 on it.** ### Step 7 — kill attribution (separate defect, same report) - `Mech::Reset` (mech4.cpp ~1755, next to the `graphicAlarm.SetLevel(0)`): clear `lastInflictingID`/`lastInflictingDamage` (already a to-do at `docs/RESPAWN_REARM_PLAN.md` Step 5). - Seed `lastInflictingID` to the mech's **own** EntityID in the ctor — the binary does (`EntityID` copy-ctor `mech+0x43c ← mech+0x184` @0x4a16ed-0x4a16fb) — so an unattributed death resolves to self and `BTPostKillScore`'s `killer != victim` guard (btplayer.cpp:1709) rejects it. Our port leaves it default `0:0`. - Guard `sender_mech` against NULL at btplayer.cpp:661 and :690 (`MECH_OWNING_PLAYER`/`MECH_TONNAGE` are dereferenced before any test; `GetEntityPointer` returns 0 for an unknown id). - Note for a later wave, **not** this one: the binary does not attribute from `mech+0x43c` at all — it builds the award inside the victim's `TakeDamage` handler on the *was-alive→now-dead edge of that hit* (@0x4a052b-0x4a05ce). Replacing `lastInflictingID` with that edge is the faithful cure for "a kill I didn't earn"; it is a bigger change than #45. ### KB / comment sweeps required by this change `btl4gau3.cpp:513-516` and `docs/GAUGE_COMPOSITE.md:531` ("the binary's dead pad_0x280") — it is the authentic DEATHS cell. `context/gauges-hud.md:330-340` + the `open_questions` entry at :15 ("**RESOLVED 2026-07-12 [T2]** deaths now tally per node from locally observed events") — it has never executed once. `btplayer.cpp:1572-1580` and :1692-1702 (both claim the KILLS credit already reaches remote local copies "via the player link") and `mech4.cpp:2014-2018` — all contradicted by ENTITY.cpp:244; mech4.cpp:1836-1840 is the correct statement. `btplayer.cpp:299-301` ("the binary also runs killer/victim score bumps (+0x280 via @0042e580)") — 0x42e580 is `AddStatusMessage`; the +0x280 pair is @0x4c0674/0x4c067a. `btplayer.hpp:231` — `VehicleDeadMessageHandler` is `@004c05c4`, not `@004c012c`. `btplayer.cpp:437` "message+0x38" — that is `FUN_004c012c`'s DropZoneReply message; the VehicleDeadMessage's `deathCount` is at msg+0x1c. `context/gauges-hud.md:332` cites `PLAYER.cpp:759` for the −2 seed; it is :777. `docs/RESPAWN_REARM_PLAN.md` §5 issue map — "**#45 SUBSUMED — same defect, not two**" is **wrong**: the respawn defect explains only the victim's own DEATHS in 4 of 47 deaths; the cross-machine disagreement is structural and independent. And **fix the decomp export for 0x4c05c4** (`reference/ghidra_scripts/`) before anyone else reasons from that gap. --- ## 4. VERIFICATION **Rig** (2-node localhost relay — the recipe verified in `context/multiplayer.md:352-360` and reused by `docs/RESPAWN_REARM_PLAN.md` §6): ``` python tools/btconsole.py --relay 1500 MP_RELAY.EGG --bind 127.0.0.1 # pod A = the VICTIM BT_RELAY=127.0.0.1:1500 BT_SELF=10.99.0.1:1502 BT_MATCHLOG=1 \ BT_SCORE_LOG=1 BT_DEATH_LOG=1 BT_CAM_LOG=1 BT_SPAWN_XZ=0,0 \ btl4 -egg MP_RELAY.EGG -net 1501 # pod B = the KILLER (BT_MP_FORCE_DMG makes B shoot A's replicant once a second, mech4.cpp:4823) BT_RELAY=127.0.0.1:1500 BT_SELF=10.99.0.2:1602 BT_MATCHLOG=1 \ BT_SCORE_LOG=1 BT_DEATH_LOG=1 BT_MP_FORCE_DMG=1 BT_SPAWN_XZ=0,60 \ btl4 -egg MP_RELAY.EGG -net 1601 ``` Use an egg that carries name bitmaps (OPERATOR-class), or the rows render as authentic cache-miss boxes. Kill A **twice: once by weapon fire, once by RAM** (drive B into A) — the collision path is the one that steals credit and it produced 5 of the 47 field deaths. **Baseline (today's build) — the exact failure signature:** - A's `btl4.log`: `[respawn] player H:1 death cycle START (death #1)` followed by **one** `[respawn] WARNING: death for player H:1 SWALLOWED` (the duplicate dispatch) and **no** `[score] *** KILL ***`. - B's `btl4.log`: `[score] *** KILL *** killerPlayer=… killCount=1 …` and **no** death line. - **Neither** log can ever contain `[score] observed death` — that string is unreachable in this build. - A's matchlog: `DEATH … inst=M killer=B:mech killdmg=X`, `PLAYER_DEAD player=A:1 deaths=1`, no `SCORE type=2`. - B's matchlog: `SCORE player=B:1 type=2 award=X kills=1` **plus** `DEATH … inst=R killer=0:0 killdmg=0.000`. - **On screen:** A shows A K0/D1, B K0/D0. B shows B K1/D0 **and A K1/D0** (the phantom). *That divergence is the bug, reproduced on two machines.* **Pass criteria after the fix — on BOTH screens, identical:** | # | artefact | criterion | |---|---|---| | 1 | A `btl4.log` | `[death] VehicleDead(-1) dispatched to the owning player` present, **exactly one** `death cycle START`, and **zero** `SWALLOWED` (Step 5 removed the duplicate — the warning is a tripwire again) | | 2 | A matchlog | `PLAYER_DEAD player=A:1 deaths=1 deathsCell=1`, ~15-35 ms before `DEATH … inst=M` (the working-case ordering, e.g. `69.117.98.17_matchlog_20260724_224438_16708.txt`) | | 3 | B matchlog | exactly **one** `SCORE … type=2 award=X kills=1`; `award` == A's `killdmg` to 2 dp | | 4 | **new** `TALLY` records (below) | on B: exactly **one** `TALLY field=kills target=B:1 inst=M new=1` and **no** `target=A:1` line. On A: exactly **one** `TALLY field=deaths target=A:1 inst=M new=1` | | 5 | **new** `ROSTER` record (below) | after replication settles (≤1 update tick), **byte-identical roster lines on both nodes**: `A kills=0 deaths=1` and `B kills=1 deaths=0`, with `inst=M` on the local row and `inst=R` on the remote one | | 6 | screen, both pods | Comm pilot list: **same two rows, same order, A 0/1, B 1/0.** Kill A again → both show A 0/2, B 2/0. Kill B once → both show A 1/2, B 2/1 | | 7 | ram kill | the credited killer is the rammer and the award appears once, on the rammer's node only — and A's `deaths` still increments | | 8 | control | the SCORE column agreed before this change and must still agree (it is the positive control for the record path) | **Diagnostics to ADD — the current logging cannot show a remote tally at all, which is why the phantom kill is invisible in 88 logs and cost this investigation four independent traces:** 1. `game/reconstructed/btplayer.cpp:702` (kill) and `:436` (death) — a `TALLY` matchlog record at **every** counter write: `field=`, `target=:`, `inst=M|R` (`GetInstance()`), `new=`. Unconditional (matchlog only arms on `-net`, matchlog.cpp:66-80, so solo stays silent). 2. `game/reconstructed/mechmppr.cpp:1265-1295` (tail of `FillPilotArray`) — a `ROSTER` record, one line per occupied slot: `slot=`, `player=:`, `inst=M|R`, `kills=`, `deaths=`, `score=`. **This is the artefact that would let one round's logs answer "what did the killer's screen actually show".** It also directly settles the one claim nobody measured: that the replicant `BTPlayer` copies really are the objects the rows read. 3. `game/reconstructed/btplayer.cpp:1216` (tail of `InitializePlayerLink`) — a `PLAYER_LINK … link=%p` record. `link=0` convicts #54 in one line. 4. Add checks 4/5 to `tools/matchcheck.py` as standing invariants: **`sum(TALLY field=kills) == count(SCORE type=2)`** and **every node's final `ROSTER` block is identical**. Those two make #45 regression-testable without a 5-pod session. **Regression case of record:** the final round — 5 nodes, one player takes all three kills, three victims. Reproduce it at 3 nodes if 5 are unavailable; it is simultaneously "one node got all the kills" and "no node counted any death", the cleanest case in the corpus. Per the standing rule, the tracker entry reads **"fix landed, awaiting human verification"** until a human confirms both screens live. --- ## 5. RISKS (concrete, keyed to `context/reconstruction-gotchas.md`) 1. **Shadow field (#1) — the trap this change walks straight into.** The new member must **not** be named `deathCount`: `Player::deathCount` already exists (PLAYER.h) and a re-declaration shadows the base, reads `0xCDCDCDCD`, and mis-offsets everything after it. Name it `deaths`. Likewise `BTPlayer__UpdateRecord` must **not** re-declare `currentScore` or `dropZoneLocation`. 2. **Databinding trap (#3) + no offset lock.** Our `BTPlayer` layout is **not** offset-faithful — `killerName` is declared *after* `deathPending` even though it is `@0x1d0` (btplayer.hpp:396-409), and the `// @0x2xx` comments are documentation of the 1995 object, not of ours. So: (a) never raw-read `pilot+0x280`; go through `GetDeaths()`/`BTPilotDeaths` (btplayer.cpp:1547) — the pre-#43 raw reads are exactly what produced the blank/`-1` scoreboard; (b) **do not add a `static_assert` on `offsetof(BTPlayer, deaths) == 0x280`** — it will fail and it would be asserting a false claim. Lock what is real instead: `sizeof(BTPlayer__UpdateRecord)` and `sizeof(BTPlayer__UpdateRecord) > sizeof(Player__UpdateRecord)`. 3. **/FORCE trap (#6).** `Read/WriteUpdateRecord` are virtuals reached only through the vtable (`simulation->ReadUpdateRecord`, ENTITY.cpp:388). A declared-but-undefined override links "successfully" under `/FORCE` and AVs at a garbage target near `__ImageBase` on the **first replicated player update** — i.e. seconds into any MP session, far from the edit. **Grep the link log for unresolved `BTPlayer::*UpdateRecord` before running.** 4. **Record growth into a shared fixed buffer (#21 shape).** `Simulation::WriteSimulationUpdate` writes straight into the stream with no bounds check before the write (SIMULATE.cpp:311-321), backed by a single static `Update_Buffer[MAX_UPDATE_LENGTH>>2]`, **`MAX_UPDATE_LENGTH == 1400`** (ENTITY.cpp:472-474). A Player emits **one** record per update, so +8 bytes is ample headroom — but it is also the wire `messageLength` (ENTITY.cpp:589), so keep the pair to two ints and do not let this become the precedent for a fat player record. 5. **Wire-format skew.** The record is self-describing (`AdvancePointer(update->recordLength)`, ENTITY.cpp:390) but a **mixed-build session mis-parses the stream from that record onward** — a pod on the old build feeds the new one a short record. All pods must be on the same build for the test; state it in the test instructions. 6. **Dirty-bit mis-mapping (#20) — the change in Step 2 is inside that trap.** Confirm at review that the edit is `ForceUpdate()` (`updateModel@0x18`) and **not** any `simulationFlags@0x28` write. Also check whether the pre-existing `DelayWatchersFlag` that btplayer.cpp:453 has been setting since it shipped was silently suppressing a player-simulation watcher — removing it may *change* behaviour elsewhere (that is a fix, but it will look like a side effect). 7. **Do not remove the entity.** Nothing in this plan may issue `DestroyEntityMessage` on a death — the wreck stays (the P5 teardown crash). 8. **Respawn-handshake coupling.** `Player::deathCount` is the respawn identity gate (`message->deathCount == deathCount`, PLAYER.cpp:230) *and* the #57 latch's sequence number. Step 1 deliberately **leaves it alone** and only stops *displaying* it. Any temptation to "clean up" its increment site while in here will destabilise respawn — defer it to its own change with its own verification. 9. **Deleting `BTPlayerCountObservedDeath` is safe but must be complete.** It survives today only via the `extern` + the `friend` at btplayer.hpp:389; remove the definition, the call site, the extern and the friend together, or `/FORCE` hides the stragglers again. 10. **Deviation ledger.** Two intentional departures ship here — dropping btplayer.cpp:704 (a byte-faithful 1995 increment) and adding the update-record override the binary does not have. Both must land in `docs/RECONCILE.md` with tiers (`[T3]` intent, `[T1]` addresses) so a future reader does not "restore fidelity" and re-open #45. **Residual unknowns, each with the one check that settles it.** (a) *Why `GetPlayerLink()` was NULL for those four deaths* — the `PLAYER_LINK`/`DEAD_NOTIFY` records of Step 0 in one 2-node run; `link=0` convicts it, `link!=0` with no `death cycle START` moves it to the handler. (b) *Whether the rerouted `BTPlayer` ScoreMessage is reliable-ordered on the Steam/relay seam* — nothing retries it, so a dropped packet loses a kill forever today; with Step 4 the record heals it, which is a further argument for replication. Confirm `Application::SendMessage` to `EntityManagerClientID` in `engine/MUNGA_L4/L4NET.CPP`. (c) *The printed column labels* — only needed to name the columns for the user, not to place the fields; read the Comm background pixmap or `reference/manual/Tesla40_BT_manual.pdf`'s Comm page. (d) *Whether the six corpus-wide excess kill awards (53 `SCORE type=2` vs 47 `DEATH inst=M`, all six outside the multi-node rounds) are a second double-scoring path* — the `TALLY` records of §4 answer it in one solo `BT_SPAWN_ENEMY` run; run it down before anyone claims "kills are awarded exactly once". --- # ADDENDUM 2026-07-25 (late) — the missing mechanism; the DESIGN DECISION is resolved **Status change: ROOT-CAUSED, FIXED, and RIG-VERIFIED on a 2-node loopback. Awaiting live multi-pod verification by a human.** This addendum supersedes the §3 framing that a convergence rule had to be *invented*. ## What was missing §2/§3 established that neither counter is replicated, and reasoned that a per-node observed tally therefore had to be added — noting only in passing (residual unknown (b)) that the ScoreMessage is "rerouted". That reroute IS the mechanism, and it changes the conclusion: `BTPostKillScore` runs on the **victim's** node (the only node whose mech carries a populated `lastInflictingID`). It resolves the killer's `Player` — which on that node is a **REPLICANT** — and calls `Dispatch()` on it. `Entity::Dispatch` reroutes a replicant's message to the owning host: ```cpp if (GetInstance() == ReplicantInstance) // ENTITY.cpp:244-251 application->SendMessage(ownerID, EntityManagerClientID, message); ``` So the `++killCount` is **not lost — it is relocated to the killer's own machine**, and it is correct there. Nothing carries it back out, and `Player__UpdateRecord` is `currentScore + dropZoneLocation` only, so every other pod's copy stays 0 all mission. `playerLink` was never NULL for these kills — the dispatch proves it resolved. [T0 on the reroute and the record contents.] ## Field proof **Field proof, as INVARIANT RATIOS.** (An earlier draft cited "125 of 125 over 255 node-logs". That was true when measured but is not reproducible: every rig run appends to the same corpus, and the verification runs for this fix alone grew it from ~255 to 425 files. Cite ratios, not counts.) - **0 of 18818** `DMG` rows are `inst=R` — damage is applied MASTER-SIDE ONLY, so a non-victim node cannot attribute a kill even in principle. This is also why §4's "unreachable observed tally" could never have worked. - **439 of 439** `DEATH inst=R` rows read `killer=0:0 killdmg=0.000`, against **0 of 225** `inst=M` rows — a replicant victim's `lastInflictingID` is never populated locally (sole writer `mech.cpp:746`). Both re-derive with a one-line scan over `matchlogs/**` + `content/matchlog*.txt` + `content/matchlogs/**` and neither drifts as the corpus grows. [T2 — log-corpus field evidence is T2 per the CLAUDE.md tier table, not T1.] - Tonight's 3 nodes, one kill: host 3 logged `player=3:1 type=2 award=5.88`; the award equals the `killdmg=5.881` recorded on **host 4** — the value crossed the wire. Host 4 (the victim, the user's pod) logged the death and no kill at all. ## Why the design decision collapses The owner's counter is already the single authoritative copy, incremented exactly once per kill through the engine's own reroute. There is no second writer to reconcile and no rule to choose: the fix is a **one-way owner→replicant mirror**. Step 1 (a new `deaths` member, changing the displayed field) is NOT needed for the user-visible bug — `deathCount` already tallies deaths correctly on the owner (`++deathCount`, btplayer.cpp:469; `PLAYER_DEAD deaths=1..4` matches). ## What landed (unbuilt) `BTPlayer__UpdateRecord` (btplayer.hpp) = `Player::UpdateRecord` + `killTally` + `deathTally`, with `Read/WriteUpdateRecord` overrides (btplayer.cpp). Adds no data member and no new vtable slot, so `sizeof(BTPlayer)==0x28c` is untouched. Both counter writes already `ForceUpdate()` (:508 death, :829/:840 score), so the record ships without a new dirty-bit edit — §5 risk 6 is avoided entirely. Satisfies §5: risk 1 avoided (no new member → no shadow), risk 2 locks added (`sizeof(UpdateRecord) > sizeof(Player::UpdateRecord)` and `== base + 2*sizeof(int)`), risk 4 respected (exactly two ints), risk 5 **corrected and improved** — that risk's premise is wrong. A mixed-build session does NOT desync the stream: both sides advance by the WRITER's stamped `recordLength` (`ENTITY.cpp:390` / `SIMULATE.cpp:323`) and no reader asserts a length. The real hazard the `recordLength` guard prevents is a READ PAST THE ALLOCATION — an inbound update message lives in a per-event heap block sized from `messageLength`, so on a TRAILING legacy record the two new fields would be read off the end of it. With the guard a mixed session degrades to "remote counters don't move". **§Headline-6 phantom kill — BOUNDED, not masked (corrected).** The binary's partner `inc [+0x27c]` on the VICTIM's player always lands on a replicant copy, so the owner's record corrects it. But it does NOT arrive after it: an inbound update record is posted at `UpdateEventPriority == MaxEventPriority` and drained by `ExecuteBackgroundTask`, while the rerouted score message is posted at `EntityManagerEventPriority == DefaultEventPriority` and popped later — so the correction usually lands FIRST and the phantom after it. Net effect: on the killer's pod only, that victim's KILLS reads +1 until the next record, i.e. at most one heartbeat (2 s) instead of "until the victim next scores or dies". The increment itself stays byte-faithful; whether to drop it is the open fidelity question. ## Verification (2026-07-25, 2-node loopback, Release, `BT_MP_FORCE_DMG=1`) Rig: `scratchpad/rig45_up.ps1` / `rig45_down.ps1` — two Release nodes on 1501/1601 pinned to disjoint cores (the task #50 TCP-batching artifact) plus the console relay, recording only its own PIDs so teardown never kills by name. Logs: `content/matchlog_20260725_213627_{28896,22980}.txt`. - **Build clean.** 0 `error C` (so both new `static_assert`s hold), and **no `BTPlayer::*UpdateRecord` among the unresolved externals** — §5 risk 3 cleared. The 40 `LNK2019`s in that log are the pre-existing `/FORCE`-tolerated `CreateStreamedSubsystem` family from `mech3.obj`, untouched by this change. - **Cross-node convergence exact.** Owner 3:1 finished kills 3 / deaths 2 and the peer read `kills=3 deaths=2`; owner 2:1 finished kills 2 / deaths 3 and the peer read `kills=2 deaths=3`. Before the fix a remote pilot's column never left 0. - **DEATHS now uses the binary's own column, and §Headline-1 is CLOSED.** The port displayed `Player::deathCount` (+0x200) -- the respawn-handshake identity, seeded -2, which is exactly why it needed a display clamp. DEATHS is now `BTPlayer::deathTally` (our offset 0x274 = the binary's `+0x280`, what `PilotList::Execute` @0x4cabd0 actually draws with `fild [edi+0x280]`, two runtime writers image-wide). `deathCount` is left to the engine's handshake. So this half of the change moves TOWARD the binary. `PLAYER_DEAD` now logs both (`deaths=` handshake, `tally=` scoreboard) so a future divergence between them is visible. - **`NOCREDIT` and `PLAYER_LINK` records added, and they retire the "barrier 1" hypothesis.** A skipped credit used to be completely silent -- the counter just never moved. `NOCREDIT` now names the failing link (missing killer entity / self-kill / unregistered mech / NULL link) and `PLAYER_LINK` records whether the one-shot link resolved. Both rigs: `NOCREDIT` empty, and `PLAYER_LINK ... inst=R ... resolved=1` -- a replicant's `playerLink` DOES resolve, so a NULL link was never the cause. [T2] - **Respawn unaffected** (the §5 risk-8 worry): 3 respawns per side, `deaths=` sequences intact, no drop-zone or latch anomaly. `deathCount` is only ever overwritten on a REPLICANT copy, and the handshake runs on masters. - **Phantom kill now self-corrects**: the rig captured the owner's value overwriting the locally-applied partner increment repeatedly (`wasKills=3 -> kills=2`, `wasKills=2 -> kills=1`). - No crash, no `GLITCH` (plane-audit) rows, both nodes alive through 3 death/respawn cycles. ## Review-driven refinements (adversarial pass, same day) A 4-dimension review with a refutation pass ran against the first cut. What survived, and what shipped because of it: 1. **The mirror is NOT self-healing on its own — update records are UNRELIABLE.** `Entity::UpdateMessage` clears `ReliableFlag` (`ENTITY3.h:112`) and the relay's UDP path drops stale/reordered datagrams behind a per-sender sequence gate. Since the pair was dirtied only on a score/death EVENT, one lost datagram would leave every peer stale until that pilot's next kill or death — forever, for the last kill of a round. **Fixed:** a 2 s scoreboard heartbeat re-dirties the record (`BTPlayer::PlayerSimulation`), so the pair reconverges within a beat. Cheap — a Player has no 60 Hz stream of its own and the record is 68 bytes. The timer is a function static on purpose: a data member would change `sizeof(BTPlayer)` and break the offset locks. [T0 on the unreliability] 2. **The phantom kill's correction was a race.** Same fix: the visibility window for the binary's wrong-column partner increment is now bounded by one heartbeat instead of "whenever that pilot next scores". The increment itself stays byte-faithful. 3. **The edge filter could never be false, and the log lied.** The `SBMIRROR` row and its change-detect both still read the engine's `deathCount` (a constant −2 here) instead of the replicated `deathTally`. That is what made the first rig runs *look* like DEATHS was broken (`deaths=-2` on every row) when the transport was provably correct — a `WRITE`/`READ` trace showed the right values on both sides. **Fixed**, and confirmed by the row count dropping from ~20 to 3 per node (one per real change). 4. **Per-bit record layouts.** `update_model` is a BIT INDEX and `Entity::WriteUpdateRecord` switches on it (`ENTITY.cpp:329-352`); the DamageZone bit emits a variable-length packed stream whose length it computes itself. Appending two ints and re-stamping `recordLength` over that would corrupt it. A pilot only ever sets the default bit, which is why the engine's own `Player::WriteUpdateRecord` is bit-agnostic — **guarded anyway**. 5. **The dead observed-death tally is DELETED** — definition, call site, `extern` and `friend` together (`/FORCE` hides stragglers). Under replication it would have been a second writer of a replicated counter, and it incremented a field the scoreboard no longer reads. 6. **A skipped credit is no longer silent.** New `NOCREDIT` record names the failing link, and `PLAYER_LINK` records whether the one-shot link resolved. Both rigs: `NOCREDIT` empty and `PLAYER_LINK ... inst=R ... resolved=1` — which **retires the NULL-playerLink hypothesis outright**: a replicant's link does resolve, so that was never the cause. **Not acted on, recorded instead:** the review measured kill credits landing while the application is not in RunningMission (`st=` on every matchlog row is the APPLICATION state). In this rig those are an artifact of `BT_MP_FORCE_DMG` firing during load; the corpus instances are a pre-existing separate concern that replication makes more visible, and gating or resetting the counters per round is a design change for its own pass. `killCount`/`deathTally` are still zeroed only in the ctor, exactly as `killCount` always was. ## Still to do 1. **Live multi-pod verification by a human** — the memory rule: this stays "fix landed, awaiting verification" until confirmed in a real session. 2. All pods must run the same build for a scoreboard test (§5 risk 5) — the guard makes a mixed session degrade safely rather than corrupt, but the numbers only converge when both ends have the extension. 3. Still open, deliberately untouched: §Headline-1 (which field the LOCAL pod's DEATHS column should read, `+0x280` vs `deathCount`), §Headline-5 (last-hitter-takes-all and ram kills), and §Headline-6's fidelity question (the wrong-column slip is still reproduced; it is merely no longer visible).