K/D scoreboard ROOT-CAUSED for real (#45): the kill credit was rerouted, not lost

Kills/deaths only ever appeared on ONE machine.  The cause is not a missing
tally -- it is Entity::Dispatch:

    if (GetInstance() == ReplicantInstance)              // ENTITY.cpp:244-251
        application->SendMessage(ownerID, EntityManagerClientID, message);

BTPostKillScore runs on the VICTIM's node (the only node whose mech carries a
populated lastInflictingID), resolves the killer's Player -- a REPLICANT there --
and Dispatch()es to it.  The engine reroutes that to the owning host, so
++killCount lands on the killer's own PC and nowhere else, and nothing carried it
back out: Player__UpdateRecord is currentScore + dropZoneLocation only.  Every
other pod's copy therefore read 0 all mission.  playerLink was never NULL for
these kills -- the dispatch proves it resolved.

Field proof over the whole corpus (255 node-logs): 125 of 125 SCORE type=2 rows
credit the LOGGING node's own player, not one credits a remote pilot; and 0 of
8800 DMG rows target a replicant, so no other node can even know the killer.

This means the owner's counter is already the single authoritative copy,
incremented exactly once per kill.  So the "design decision" the plan was blocked
on collapses: there is no second writer to reconcile, only a one-way
owner->replicant mirror to add.

  * BTPlayer__UpdateRecord = Player::UpdateRecord + killTally + deathTally, with
    Read/WriteUpdateRecord overrides.  No new data member, no new virtual ->
    sizeof(BTPlayer)==0x28c and every existing offset lock still holds.
  * Both counter writes already ForceUpdate() (:508 death, :829/:840 score), so
    no dirty-bit edit was needed (plan risk 6 avoided).
  * recordLength guard in ReadUpdateRecord: a pod on a build without the
    extension degrades to "remote counters don't move" instead of reading a
    neighbouring record as a kill count.
  * Size locks added per plan risk 2 (no false offsetof assert).

Rig-verified, 2-node loopback (scratchpad/rig45_up.ps1, BT_MP_FORCE_DMG):
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.  Exact convergence
where a remote column previously never left 0.  3 respawns per side with intact
death sequences (deathCount is only overwritten on replicant copies; the
handshake runs on masters), no crash, no GLITCH rows.

Kept byte-faithful: the kill handler's partner increment (the binary's
inc [ebx+0x27c] / inc [edx+0x27c] wrong-column slip) is still reproduced.  It
always lands on a replicant copy, so the owner's record now overwrites it -- the
rig caught the correction repeatedly (wasKills=3 -> kills=2).  The phantom kill
stops being visible without silently deciding the fidelity question.

Still open and deliberately untouched: which field the LOCAL pod's DEATHS column
should read (+0x280 vs deathCount), last-hitter-takes-all/ram kills, and the
slip's fidelity.  Awaiting live multi-pod verification by a human.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-25 21:40:59 -05:00
co-authored by Claude Opus 5
parent da617e6f8f
commit 4fa7eee54f
6 changed files with 372 additions and 1 deletions
+44
View File
@@ -1562,3 +1562,47 @@ absent inter-sink linkage (interdependence). Reservoir (0xBC0) and HeatSink bank
RefrigerationSimulation in its ctor) — REQUIRES heat.cpp edits (out of scope for this pilot's file set).
5. THEN instantiate the heat family together (Condenser+Reservoir+bank) and verify the tick rises by
their count without the UpdateCoolant fault.
---
## Deliberate divergence from the binary: BTPlayer update-record override (Gitea #45)
**2026-07-25. One departure, verified live. Do NOT "restore fidelity" here — doing so re-opens #45.**
**What the binary does.** `BTPlayer`'s vtable slots 6/7 (`@0x513300`) point at the ENGINE's
`Player::Read/WriteUpdateRecord`; the 1995 class adds no override, and `Player__UpdateRecord`
carries `currentScore + dropZoneLocation` only (`PLAYER.h:73-80`). [T0/T1]
**What we ship.** `BTPlayer__UpdateRecord` = `Player::UpdateRecord` + `killTally` + `deathTally`,
with `Read/WriteUpdateRecord` overrides (`btplayer.hpp`, `btplayer.cpp`). Our vtable slots 6/7
therefore point at BTPlayer's versions. No new data member and no new virtual, so
`sizeof(BTPlayer) == 0x28c` is unchanged and the existing offset locks still hold. **[T3 on the
divergence being *necessary*; T0 on the mechanism that makes it necessary.]**
**Why it is necessary.** A kill is credited by `BTPostKillScore` on the VICTIM's node, which
resolves the killer's `Player` — a REPLICANT there — and calls `Dispatch()`. `Entity::Dispatch`
reroutes a replicant's message to the owning host (`ENTITY.cpp:244-251`), so `++killCount` lands
on the killer's own machine and nowhere else, and nothing ever carried it back out. Field proof
before the fix: across 255 node-logs, **125 of 125** `SCORE type=2` rows credit the LOGGING node's
own player and **not one** credits a remote pilot; **0 of 8800** `DMG` rows target a replicant, so
no other node can even know the killer. The pods' closed LAN made the one-way engine record
sufficient in 1995 only because nothing ever needed the counters off-node — the arcade cabinet
scoreboard each pod drew was its own.
**Verified live** (2-node loopback, Release, `BT_MP_FORCE_DMG`, 2026-07-25): both nodes converged
exactly — owner 3:1 (kills 3 / deaths 2) read `kills=3 deaths=2` on the peer, owner 2:1
(kills 2 / deaths 3) read `kills=2 deaths=3` on the peer. 3 respawns per side, death sequences
intact, no crash.
**What we did NOT change (kept byte-faithful).** The kill handler's partner increment
`++sender_owner->killCount` (btplayer.cpp, the binary's `inc [ebx+0x27c]` / `inc [edx+0x27c]`
@0x4c0397/@0x4c03a3 wrong-column slip) is still reproduced as shipped. It always lands on a
REPLICANT copy of the victim, so the owner's authoritative record now overwrites it on the next
update — the rig captured the correction repeatedly (`wasKills=3 -> kills=2`,
`wasKills=2 -> kills=1`). The phantom kill stops being *visible* without editing the
reconstructed handler, so the fidelity question stays open rather than being silently decided.
**Mixed-build note.** `ReadUpdateRecord` early-returns when `recordLength < sizeof(UpdateRecord)`,
so a pod on a build without the extension degrades to "remote counters don't move" instead of
reading a neighbouring record as a kill count. All pods should still run the same build for a
scoreboard test.