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:
co-authored by
Claude Opus 5
parent
da617e6f8f
commit
4fa7eee54f
@@ -234,6 +234,22 @@ void BTPlayerLayoutSelfCheck() // never called -- a compile-time lock only
|
||||
static_assert(sizeof(BTPlayer) == 0x28c,
|
||||
"BTPlayer size changed -- re-measure the offsets above before trusting any "
|
||||
"raw-offset code that touches this class");
|
||||
|
||||
//
|
||||
// Scoreboard replication (Gitea #45). There is no offsetof lock to make here
|
||||
// -- our BTPlayer is not offset-faithful -- so lock what IS real: the record
|
||||
// must stay strictly larger than the base (or the two counters are not being
|
||||
// carried at all) and must stay exactly two ints wider (the update stream is
|
||||
// a single fixed 1400-byte buffer, ENTITY.cpp:472-474, and this record must
|
||||
// not become the precedent for a fat player record).
|
||||
//
|
||||
static_assert(sizeof(BTPlayer::UpdateRecord) > sizeof(Player::UpdateRecord),
|
||||
"BTPlayer::UpdateRecord no longer extends Player::UpdateRecord -- the "
|
||||
"KILLS/DEATHS columns would silently stop replicating (Gitea #45)");
|
||||
static_assert(sizeof(BTPlayer::UpdateRecord)
|
||||
== sizeof(Player::UpdateRecord) + 2 * sizeof(int),
|
||||
"BTPlayer::UpdateRecord grew beyond the two scoreboard counters -- "
|
||||
"re-check the 1400-byte update buffer before widening the player record");
|
||||
}
|
||||
|
||||
static const char *SelfDestructName = "self destruct"; // &DAT_00524b38
|
||||
@@ -1770,6 +1786,82 @@ void BTPlayerCountObservedDeath(void *player)
|
||||
<< " deaths=" << p->deathCount << "\n" << std::flush;
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// WriteUpdateRecord / ReadUpdateRecord (Gitea #45)
|
||||
//
|
||||
// SCOREBOARD REPLICATION -- port record extension; the full root-cause banner is
|
||||
// on the record struct in btplayer.hpp. In one line: a kill is credited by
|
||||
// `BTPostKillScore` on the VICTIM's node, which Dispatch()es to the killer's
|
||||
// Player -- a replicant there -- and `Entity::Dispatch` reroutes a replicant's
|
||||
// message to the master (ENTITY.cpp:244-251), so `++killCount` lands on the
|
||||
// killer's OWN machine only. Nothing carried it back out, so every other pod's
|
||||
// copy read 0 all mission. The owner's counter is therefore already the single
|
||||
// authoritative value; this is a plain one-way owner->replicant mirror.
|
||||
//
|
||||
// Sends for free: both counter writes already dirty the record --
|
||||
// `ForceUpdate()` on the death path (:508) and on every score award (:829/:840).
|
||||
//
|
||||
// Side effect worth knowing: the binary's kill handler also bumps killCount on
|
||||
// the VICTIM's player (the @0x4c0397/@0x4c03a3 wrong-column slip we reproduce
|
||||
// faithfully). That partner increment always lands on a REPLICANT copy of the
|
||||
// victim, so the owner's authoritative value now overwrites it on the next
|
||||
// record -- the phantom kill stops being visible without touching the
|
||||
// reconstructed handler.
|
||||
//
|
||||
void
|
||||
BTPlayer::WriteUpdateRecord(Simulation__UpdateRecord *message, int update_model)
|
||||
{
|
||||
Player::WriteUpdateRecord(message, update_model);
|
||||
|
||||
UpdateRecord *rec = (UpdateRecord *)message;
|
||||
rec->recordLength = sizeof(UpdateRecord);
|
||||
rec->killTally = killCount;
|
||||
rec->deathTally = deathCount;
|
||||
}
|
||||
|
||||
void
|
||||
BTPlayer::ReadUpdateRecord(Simulation__UpdateRecord *message)
|
||||
{
|
||||
Player::ReadUpdateRecord(message);
|
||||
|
||||
UpdateRecord *rec = (UpdateRecord *)message;
|
||||
|
||||
//
|
||||
// MIXED-VERSION GUARD: a peer on a build without this extension writes a
|
||||
// plain Player-sized record, and reading the two fields off the end would
|
||||
// pick up whatever record was packed after it in the update stream and
|
||||
// display it as a kill count. Trust the length the writer stamped.
|
||||
//
|
||||
if (rec->recordLength < sizeof(UpdateRecord))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int was_kills = killCount;
|
||||
int was_deaths = deathCount;
|
||||
|
||||
killCount = rec->killTally;
|
||||
deathCount = rec->deathTally;
|
||||
|
||||
//
|
||||
// Log only the EDGE -- this runs at update rate, so an unconditional line
|
||||
// would drown the log (and the matchlog) in per-frame noise.
|
||||
//
|
||||
if (killCount != was_kills || deathCount != was_deaths)
|
||||
{
|
||||
if (BTMatchLogActive())
|
||||
BTMatchLog("SBMIRROR",
|
||||
"player=%d:%d kills=%d deaths=%d wasKills=%d wasDeaths=%d",
|
||||
BTMatchHostOf(GetEntityID()), (int)GetEntityID(),
|
||||
killCount, deathCount, was_kills, was_deaths);
|
||||
if (getenv("BT_SCORE_LOG"))
|
||||
DEBUG_STREAM << "[score] scoreboard mirror: pilot "
|
||||
<< (int)GetEntityID() << " kills " << was_kills << "->"
|
||||
<< killCount << " deaths " << was_deaths << "->" << deathCount
|
||||
<< "\n" << std::flush;
|
||||
}
|
||||
}
|
||||
|
||||
// Is `pilot` the owner of `local_player`'s current target? (the SELECT-TARGET row
|
||||
// highlight). Computed here via accessors -- the PilotList's raw-offset version
|
||||
// (local+0x284 objectiveMech, then deref target+0x190) reads garbage in our layout
|
||||
|
||||
Reference in New Issue
Block a user