#45 scoreboard: reclaim the binary's DEATHS field, add a heartbeat, sweep the false KB claims
Follow-up to 4fa7eee, driven by an adversarial review pass and three rig cycles.
Each item below is a real defect that pass found, not polish.
DEATHS now uses the binary's own column. PilotList::Execute @0x4cabd0 draws
`fild [edi+0x27c]` (KILLS) and `fild [edi+0x280]` (DEATHS); +0x280 has exactly
two runtime writers image-wide plus the ctor zero. Our port had declared it
`pad_0x280`, never written, and displayed the ENGINE's Player::deathCount
(+0x200) instead -- which is the respawn-handshake identity, seeded -2, and is
why it needed a clamp to pass as a count. It is now BTPlayer::deathTally (our
offset 0x274, offsetof-locked), incremented beside ++deathCount, read by the
gauge, and replicated. deathCount is left to the engine's handshake. So this
half moves TOWARD the binary; it also closes the plan's Headline-1.
(`deathTally`, NOT `deaths`/`deathCount` -- those would shadow the base.)
The mirror was not self-healing: update records are UNRELIABLE by construction
(Entity::UpdateMessage clears ReliableFlag, ENTITY3.h:112; the relay's UDP path
also drops stale/reordered datagrams), and the pair was dirtied only on an
event. One lost datagram left every peer stale until that pilot's next kill or
death -- forever for the last kill of a round. Added a 2s heartbeat that
re-dirties the record, which also bounds how long the binary's phantom
partner-increment stays visible. The timer is a function static deliberately: a
data member would change sizeof(BTPlayer) and break the offset locks.
Also fixed: the SBMIRROR row AND its change-detect both still read deathCount (a
constant -2 here), so the "log only the edge" guard could never be false and
every row printed deaths=-2. That is what made the first rig runs look like
DEATHS was broken when a WRITE/READ trace proved the transport correct. Row
count per node fell from ~20 to 3, one per real change.
Guarded the record against per-bit 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, so
appending two ints and re-stamping recordLength over that would corrupt it.
Deleted BTPlayerCountObservedDeath -- definition, call site, extern and friend
together, since /FORCE hides stragglers. It never executed (its call site sat
inside the once-per-death transition, which a replicant never enters) and could
not have worked (0 of 8800 corpus DMG rows target a replicant, which is why every
DEATH inst=R row reads killer=0:0). Under replication it would have been a
second writer of a replicated counter.
New forensics: NOCREDIT names the failing link when a kill credit is skipped (it
used to be completely silent -- the counter simply never moved), and PLAYER_LINK
records whether the one-shot link resolved. Both retire the NULL-playerLink
theory: every rig shows `PLAYER_LINK inst=R resolved=1` and no NOCREDIT rows.
PLAYER_DEAD now logs both counters (deaths=handshake, tally=scoreboard).
KB sweep of the claims that hid this bug for so long:
* context/gauges-hud.md's "RESOLVED -- deaths tally per node from locally
observed events" was FALSE; corrected with the measured evidence.
* docs/GAUGE_COMPOSITE.md + btl4gau3.cpp "the dead pad_0x280" -- never dead,
merely unwritten.
* btplayer.hpp attributed VehicleDeadMessageHandler to @004c012c; it is
@004c05c4 (absent from the decomp export -- the #60 gap).
* docs/RESPAWN_REARM_PLAN.md's "#45 SUBSUMED" -- the PLAYER_DEAD symptom was
subsumed, the scoreboard defect was not.
* The corpus figure in the record banner now states its method so it is
reproducible.
Rig-verified (2-node loopback, several cycles): owner 3:1 kills 2/tally 1 read
`kills=2 deaths=1` on the peer; owner 2:1 kills 1/tally 2 read `kills=1 deaths=2`
on the peer. Respawn unaffected, no crash, no GLITCH rows. Still awaiting live
multi-pod verification by a human; all pods must run the same build.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4fa7eee54f
commit
a52207d779
+18
-5
@@ -332,11 +332,24 @@ One ROW PER PILOT in the mission (2-player MP = 2 rows — not duplicate display
|
||||
rows); DEATHS = `Player::deathCount`: engine-inits to **−2** (PLAYER.cpp:759), the LOCAL
|
||||
vehicle-acquire branch zeroes it (btplayer.cpp:1118), then VehicleDead(-1) ++s per death. A
|
||||
REMOTE player's Player object never runs the local acquire → −2 +1 spawn increment = **−1
|
||||
locked**. **RESOLVED 2026-07-12 [T2]:** deaths now tally per node from LOCALLY OBSERVED events
|
||||
(the same model as the cross-pod KILLS credit) — a replicant's once-per-death transition calls
|
||||
`BTPlayerCountObservedDeath` on its owning player's local copy (replicant-gated: the master's
|
||||
own VehicleDead path counts its node), and `BTPilotDeaths` clamps the −2/−1 pre-acquire seed
|
||||
to 0 for display. Own row counts correctly as before.
|
||||
locked**. **SUPERSEDED -- that "RESOLVED" claim was FALSE (corrected 2026-07-25, Gitea #45).** The
|
||||
observed-tally design never ran: `BTPlayerCountObservedDeath`'s only call site is
|
||||
replicant-gated but sits INSIDE `Mech::UpdateDeathState`'s once-per-death transition, which a
|
||||
replicant never enters (it takes the mode-9 early return) -- and it could not have worked
|
||||
anyway, because a replicant victim carries no attribution at all (**0 of 8800** `DMG` rows in
|
||||
the corpus target a replicant, so every `DEATH inst=R` row reads `killer=0:0`). What actually
|
||||
happened: BOTH counters lived only on the OWNING pod, so every REMOTE row read 0/0 all
|
||||
mission -- KILLS because `Entity::Dispatch` reroutes a replicant's ScoreMessage to the master
|
||||
(ENTITY.cpp:244-251), so `++killCount` lands on the killer's own machine; DEATHS because the
|
||||
`VehicleDead(-1)` handler runs on the victim's own master. Neither counter rode an update
|
||||
record. Measured: **125 of 125** `SCORE type=2` rows across 255 node-logs credit the LOGGING
|
||||
node's own player; not one credits a remote pilot. [T1]
|
||||
**FIXED 2026-07-25 [T2, rig-verified]:** DEATHS now reads the binary's OWN `+0x280` column
|
||||
(`BTPlayer::deathTally`, our offset 0x274 -- previously declared `pad_0x280` and "dead"), not
|
||||
`Player::deathCount` (which is the respawn-handshake identity, seeded -2 -- exactly what the
|
||||
old display clamp was hiding). Both counters replicate owner->replicant via a
|
||||
`BTPlayer__UpdateRecord` extension, so every pod shows the same numbers. See
|
||||
`docs/KD_SCOREBOARD_PLAN.md` (addendum) + `docs/RECONCILE.md`.
|
||||
|
||||
## Launcher-panel recharge dial — CORRECTION 2026-07-19: it IS live (slot 17 @004b9c9c) [T1]
|
||||
The weapon panels' SegmentArc270 tick ring reads MechWeapon::rechargeLevel (+0x320).
|
||||
|
||||
Reference in New Issue
Block a user