Commit Graph
2 Commits
Author SHA1 Message Date
arcattackandClaude Opus 5 a52207d779 #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>
2026-07-25 22:09:00 -05:00
arcattackandClaude Opus 5 326023812a Respawn re-arm ROOT-CAUSED: David's death transition never started (#54) -- plan doc only
9-agent read-only investigation, every binary claim re-verified with capstone.

VERDICT: respawn re-arm WORKS in 4.11.524 -- 21 own-deaths in tonight's
matchlogs each have a 1:1 PLAYER_DEAD, except David's (1 death, 0 PLAYER_DEAD).
So #22's fix is holding and is NOT implicated (my earlier claim retracted).

His cycle never STARTED: PLAYER_DEAD is written unconditionally at
btplayer.cpp:422 in the same straight-line block as ++deathCount, so its absence
proves VehicleDeadMessageHandler never entered the deathCount==-1 branch -> no
drop-zone hunt -> no DropZoneReply -> Mech::Reset never ran.  Corroborated: a
zone reads lvl=1.0000 11.18s AFTER death, which Reset would have healed.

With no Reset movementMode stays 9, and ONE predicate -- Mech::IsDisabled()
(mode 2||9) -- darkens all six panels via two authentic gates (emitter.cpp:431
currentLevel=0; projweap.cpp:707 recoil=rechargeRate + alarm 7).  Ring and
ready-disc are the SAME scalar, so six dark panels is one data fault.

SEPARATE second bug: his AFC 100 went silent 123s BEFORE death holding 13
rounds, and one LRM 66s before while its twin kept firing -- that is the #21
family, untouched by this.

Port divergences found on the way: mech4.cpp:1928 calls DeathShutdown(1) but the
binary sweeps with 0 and forwards to DeathReset -- and our DeathShutdown is an
empty base nobody overrides, so the whole death sweep is a no-op; only 7 of ~20
subsystem classes implement DeathReset; HeatSink::ResetToInitialState clobbers
the startingTemperature it just wrote and stops short of the authentic terminus
@004ac22c (which heals each subsystem's own crit zone + status alarm).

UNKNOWN: why the -1 branch never ran.  One survivor after static elimination
(GetPlayerLink()==0 on his own master mech); section 6 'Check 0' settles it with
no code change.  Do that first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg
2026-07-24 23:56:09 -05:00