#162 K/D chain fixed at the authority model, once and for all: death tail master-gated (the 1995 sender one-shot restored -- the binary has NO receiver dedup, +0x290 never set [T1 byte-scan]); the #81 latch-clear misattribution corrected (binary site = DropZoneReply tail); steam eggs stamp the host's advancedDamage mission-wide (penalty was silently off for every steam match); #38 badge/patch ride the lobby (bd/pt keys). Bench: kd_bench.sh, five hard invariants, 4/4 deaths+costs+credits, zero dupes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Joe DiPrima
2026-08-11 00:21:46 -05:00
co-authored by Claude Fable 5
parent 40c35946cd
commit 3545056395
6 changed files with 132 additions and 39 deletions
+46
View File
@@ -1254,3 +1254,49 @@ code involved; grammar = the binary ctors @0x49ea48/@0x49e5e4). Results:
## Key Relationships
- Weapons/roster: [[subsystems]]. Aim source: [[locomotion]] (drive/facing). Effects: [[rendering]].
- P5 forensics: `docs/HARD_PROBLEMS.md`. Data: [[decomp-reference]] §4-5.
## The K/D-score AUTHORITY MODEL -- settled once and for all (2026-08-11, #162) [T1]
The 1995 chain has ONE emitter and NO receiver dedup; every counter is single-owner.
Fixed twice piecemeal (#150 kills, #162 deaths) before the full audit settled it -- do
not touch any single limb of this chain again without re-reading this section.
**Emitter (Mech::TakeDamageMessageHandler @0x4a0230 -- the victim's MASTER only;
replicant-asserted part_012.c:14613):** entry latch `local_14` = destroyed-at-entry
(modes 2|9, @0x49fb54). Three id-0x16 reports: **A** type-2 KILL -> shooter's player,
alive->dead edge only, exclusive with B (basis = victim role's killBonus, @0x4a0506);
**B** type-0 Inflicted -> shooter, non-lethal AND corpse hits (tally != 0); **C** type-1
Received -> victim's own player, latch-INDEPENDENT (corpse hits included; basis =
intended damage = bursts x amount). Death tail (gate `local_14==0 && mode 9|10`,
@0x4a07b5): exactly ONE **VehicleDead** (id 0x17, deathCount=-1, killer player id) ->
victim's own player, + explosion + delayed burn + splash (fresh TakeDamages, gate
advancedDamageOn && !suppressConsole) + ForceUpdate. Collision (type-0 damage) diverts
past ALL reports straight to the tail: a ram death counts + costs but credits no one.
**Consumer (BTPlayer):** 0x16 type-2 @0x4c02e4: CalcKillScore, killCount(+0x27c)++ dual
(the phantom wrong-column partner -- keep byte-faithful), suicide fchs negation skips
both increments. 0x17 @0x4c05c4 deathCount==-1 arm: deaths(+0x280)++ then STRAIGHT-LINE
the -500 cost (gate advancedDamageOn(+0x264) ONLY; -role->specialCaseDeathPenalty
(+0x20) via DIRECT Player::ScoreMessageHandler base call -- never on the wire, never in
a matchlog; the [deathcost] DEBUG line is its only receipt). **The receiver dedup gate
+0x290 is NEVER written non-zero anywhere in BTL4OPT.EXE [T1 image byte-scan]** -- the
binary's single-increment guarantee is purely the SENDER's one-shot edge. Its clear
pair (+0x290=0, +0x258=0) lives in the DropZoneReply tail (FUN_004c012c,
part_013.c:10519) -- the #81-era port put it in the VehicleDead arm by misattribution,
disarming the port's defensive latch (corrected 2026-08-11).
**Replication:** the player update record carries currentScore(+0x1c8) +
dropZoneLocation ONLY. killCount/deaths never cross the wire in 1995; each node's
scoreboard columns are locally accumulated (the port's SBMIRROR is a deliberate
deviation that makes boards consistent).
**Port rules that keep it correct:** (1) the report block AND the death tail are both
master-gated + entry-latched (mech.cpp) -- replicant-side damage application is a
visual-only deviation and must never emit; (2) advancedDamageOn comes from the mission
egg per-pilot page -- every stock egg stamps `advancedDamage=1` on every page, and the
FE egg writer now stamps the host's menu choice mission-wide (a memset-zero pilot
record shipped penalty-off for entire steam nights, #162); (3) the bench for ANY change
here is scratchpad/night15/kd_bench.sh -- five HARD invariants (deaths==increments,
cost-per-death, credit-per-kill, zero swallowed, arithmetic witness); its predecessor's
assertions were greps that matched nothing, which is how both halves shipped broken.
+17
View File
@@ -1200,6 +1200,7 @@ int
{
BTLobbyRoster roster;
if (BTLobby_HostAndRoom(self.name, self.vehicle, self.color, self.experience,
self.badge, self.patch,
&roster, spec->steamMyToken, sizeof(spec->steamMyToken),
spec->steamMap, sizeof(spec->steamMap)) != 0 ||
roster.memberCount == 0)
@@ -1207,6 +1208,15 @@ int
return 1; // cancelled / Steam unavailable
}
spec->mode = BTFeLaunchHostSteam;
// #162: capture the host's ADV. DAMAGE menu choice BEFORE the roster
// loop -- `self` aliases mission.pilots[0], which the loop overwrites
// with the first lobby member. advancedDamage is a MISSION-level
// setting (every stock 1995 egg stamps the same value on every pilot
// page); leaving it unset here shipped advancedDamage=0 for EVERY
// pilot of EVERY steam match, which silently disabled the -500 death
// cost and the death splash all night (night-15 [exp] receipts:
// advDamage=0 on 24 of 26 group-mission spawns).
const int host_adv_damage = self.advancedDamage;
mission.pilotCount = 0;
sprintf(spec->podList, "127.0.0.1:%d", console_port);
for (int m = 0; m < roster.memberCount && m < 8; ++m)
@@ -1226,6 +1236,12 @@ int
// but now only for clients that never published a choice.
strncpy(pilot.experience, member.experience,
sizeof(pilot.experience) - 1);
// #38: badge + patch ride the lobby the same way (absent = the
// writer's VGL/Red fallback, which is exactly what every non-host
// player got all night while these fields were never carried).
strncpy(pilot.badge, member.badge, sizeof(pilot.badge) - 1);
strncpy(pilot.patch, member.patch, sizeof(pilot.patch) - 1);
pilot.advancedDamage = host_adv_damage; // #162: mission-level
sprintf(pilot.address, "%s:%d",
member.fakeAddress, member.gamePort);
if (!member.isSelf)
@@ -1245,6 +1261,7 @@ int
if (menu.steamAction == 2)
{
if (BTLobby_JoinAndWait(self.name, self.vehicle, self.color, self.experience,
self.badge, self.patch,
spec->steamMyToken, sizeof(spec->steamMyToken),
spec->steamMap, sizeof(spec->steamMap)) != 0)
{
+19 -5
View File
@@ -129,7 +129,7 @@ static CSteamID currentLobby;
static void
PublishSelf(const char *pilot_name, const char *vehicle, const char *color,
const char *experience)
const char *experience, const char *badge, const char *patch)
{
//
// Identity is implicit (the member's SteamID); the roster TOKENS are
@@ -140,6 +140,10 @@ static void
matchmaking->SetLobbyMemberData(currentLobby, "vh", vehicle);
matchmaking->SetLobbyMemberData(currentLobby, "cl", color);
matchmaking->SetLobbyMemberData(currentLobby, "xp", experience);
// #38: badge + patch complete the paint identity (the egg's fourth and
// fifth pilot fields) -- without them every joiner rendered VGL/Red.
matchmaking->SetLobbyMemberData(currentLobby, "bd", badge);
matchmaking->SetLobbyMemberData(currentLobby, "pt", patch);
// BUILD GATE, member half: publish our exact build so the HOST can
// reject mismatches at GO time. This is what catches OLD exes -- they
// predate the joiner-side lobby filter, but they can't fake a "bv" key
@@ -178,6 +182,16 @@ static int
if (xp != NULL)
strncpy(out->experience, xp, sizeof(out->experience) - 1);
}
{
// #38: badge + patch, same tolerance -- an older client that never
// published them keeps the old VGL/Red fallback, nobody else does.
const char *bd = matchmaking->GetLobbyMemberData(lobby, member, "bd");
if (bd != NULL)
strncpy(out->badge, bd, sizeof(out->badge) - 1);
const char *pt = matchmaking->GetLobbyMemberData(lobby, member, "pt");
if (pt != NULL)
strncpy(out->patch, pt, sizeof(out->patch) - 1);
}
out->isSelf = (member == SteamUser()->GetSteamID());
if (out->name[0] == 0)
{
@@ -379,7 +393,7 @@ static int
int
BTLobby_HostAndRoom(
const char *pilot_name, const char *vehicle, const char *color,
const char *experience,
const char *experience, const char *badge, const char *patch,
BTLobbyRoster *roster_out,
char *my_token_out, int my_token_capacity,
char *steam_map_out, int steam_map_capacity)
@@ -409,7 +423,7 @@ int
// one stale-zip player desyncs one connection's stream). Same build or
// no entry.
SteamMatchmaking()->SetLobbyData(currentLobby, "btl4ver", BT_VERSION_STRING);
PublishSelf(pilot_name, vehicle, color, experience);
PublishSelf(pilot_name, vehicle, color, experience, badge, patch);
LobbyLog("host: lobby up (%llu)", (unsigned long long)created.m_ulSteamIDLobby);
if (RunRoom(1) != 0)
@@ -496,7 +510,7 @@ int
int
BTLobby_JoinAndWait(
const char *pilot_name, const char *vehicle, const char *color,
const char *experience,
const char *experience, const char *badge, const char *patch,
char *my_token_out, int my_token_capacity,
char *steam_map_out, int steam_map_capacity)
{
@@ -586,7 +600,7 @@ int
return -1;
}
}
PublishSelf(pilot_name, vehicle, color, experience);
PublishSelf(pilot_name, vehicle, color, experience, badge, patch);
LobbyLog("join: in lobby, waiting for GO");
int result = RunRoom(0);
+5 -2
View File
@@ -34,6 +34,9 @@ struct BTLobbyMember
// without it on the wire, the host's
// egg authored every JOINER as the
// WriteEgg "veteran" fallback
char badge[16]; // #38: same class as experience --
char patch[16]; // absent on the wire, every joiner
// spawned VGL-badged with a Red patch
int isSelf;
unsigned long long steamID;
};
@@ -51,7 +54,7 @@ struct BTLobbyRoster
int
BTLobby_HostAndRoom(
const char *pilot_name, const char *vehicle, const char *color,
const char *experience,
const char *experience, const char *badge, const char *patch,
BTLobbyRoster *roster_out,
char *my_token_out, int my_token_capacity,
char *steam_map_out, int steam_map_capacity);
@@ -63,6 +66,6 @@ int
int
BTLobby_JoinAndWait(
const char *pilot_name, const char *vehicle, const char *color,
const char *experience,
const char *experience, const char *badge, const char *patch,
char *my_token_out, int my_token_capacity,
char *steam_map_out, int steam_map_capacity);
+25 -31
View File
@@ -736,38 +736,25 @@ void
}
//
// RELEASE THE DEATH LATCH (#81, the GHOST MECH fix -- 2026-07-30).
// #162 CORRECTION (2026-08-11): the #81 clear that used to sit HERE was a
// MISATTRIBUTION. FUN_004c012c -- whose tail is `Post(...); *(+0x290)=0;
// *(+0x258)=0` (part_013.c:10519-10523) -- is the DROP-ZONE REPLY handler,
// not this arm. Clearing the latch at the end of THIS arm disarmed the
// port's own dedup within the same invocation, so the duplicate
// VehicleDeads from the replicant-side death tails (the #162 multi-count:
// deaths=1,2 one ms apart) sailed straight through the :546 gate. The
// latch now clears where the binary clears it: the DropZoneReply paths
// (fresh spawn :1799, abandoned cycle :1810, already-alive probe :1829,
// respawn complete :1900) -- which also keep the #81 ghost fix intact:
// every terminal outcome of a cycle releases it. suppressConsole rides
// the same binary tail, so its clear moves to DropZoneReply with it
// (post-eject corpse hits keep their console posts suppressed until the
// respawn -- authentic). The binary itself never SETS +0x290 [T1 image
// byte-scan: three writes, all zero-stores] -- its guarantee is the
// sender's one-shot edge, restored in mech.cpp's death tail the same day;
// our set at :559 stays as a defensive tripwire (a duplicate that still
// arrives is SWALLOWED and logged loudly instead of counted).
//
// The binary clears it right here, between the re-post and suppressConsole:
// FUN_004c012c's tail is Post(...) ; *(this+0x290) = 0 ; *(this+0x258) = 0
// (part_013.c:10519-10523). We had the Post and the suppressConsole and
// were missing the middle instruction, so `deathPending` -- which we DO set
// (:505, and the dedup gate that reads it IS authentic; the binary's own
// @004c05c4 does `mov edx,[ebx+0x290]; test edx,edx; jne ret`) -- was only
// ever cleared on the SUCCESS paths. One failed respawn therefore latched
// the pilot dead for the rest of the mission: every later death hit the
// dedup and was SWALLOWED, so the cycle could never restart. That is what
// turned a transient respawn hiccup into a PERMANENT ghost (dead,
// un-Reset, still driveable, a burning wreck on every peer that sinks out
// of the world after ~18 s and can never be drawn again).
//
// Field signature it explains exactly: 2026-07-29, 8 death cycles, 6
// stranded, NONE of them ever recovering.
//
// Binary evidence that the latch must not persist: `+0x290` is written in
// exactly THREE places in the whole of BTL4OPT.EXE (file offsets 0x0b75fb,
// 0x0bffe3, 0x0c0a05) and ALL THREE store a zeroed register (`xor` on the
// preceding instruction); there is no write of 1 -- or of any non-zero
// value, in any instruction form -- anywhere in the executable. So in 1995
// the gate exists but can never block. [T1]
//
// Ordering matters and is preserved: the re-entrant death that arrives
// while the death EFFECTS are being dispatched still lands while the latch
// is up, so the "one death, one cycle" dedup is untouched.
//
deathPending = 0; // this+0x290 (binary: FUN_004c012c tail)
suppressConsole = 0; // this+0x258
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1799,6 +1786,8 @@ void
deathPending = 0; // #55 step 1: a latch carried into a
// fresh spawn would permanently kill
// every later respawn (the #57 class)
suppressConsole = 0; // this+0x258 (binary: FUN_004c012c
// tail pairs both clears -- #162)
// (warp fired in the shared placement below -- initial drop-in + respawn)
}
else if (deathCount == message->deathCount) // param_2[0xe] == param_1[0x80]
@@ -1898,6 +1887,11 @@ void
// the same process. The cycle's completion is HERE, so clear it here.
//
deathPending = 0; // this+0x290
suppressConsole = 0; // this+0x258 -- the binary pairs
// both clears in FUN_004c012c's
// tail (part_013.c:10519-10523);
// moved here from the VehicleDead
// arm with the #162 correction
DEBUG_STREAM << "[respawn] player " << BTMatchHostOf(GetEntityID())
<< ":" << (int)GetEntityID() << " RESET at drop zone (death #"
<< deathCount << ") -- death cycle complete, latch cleared\n"
+20 -1
View File
@@ -1333,7 +1333,26 @@ void
// on every path through here, so the edges coincide; this exact predicate
// is the one the #89 blast benches verified both ways.
death_tail:
if (deathBlastArmed && IsMechDestroyed())
// #162 (2026-08-11) -- MASTER ONLY: the discipline #150 restored for the
// score reports, now applied to the death tail it forgot. In 1995 this
// ENTIRE handler was replicant-asserted (part_012.c:14613), so the tail
// could only run on the victim's master: ONE VehicleDead per death -- and
// the receiver needs no dedup, because the binary's deathPending gate
// (+0x290) is never written non-zero anywhere in BTL4OPT.EXE [T1 image
// byte-scan]; the sender's one-shot edge IS the 1995 guarantee. Our port
// deliberately applies damage on replicant copies for visual response,
// and on those copies movementMode does not flip to 9 until the owner's
// type-6 death record round-trips -- so deathBlastArmed stayed armed
// there, and EVERY salvo of the killing volley re-fired this tail on
// EVERY peer: a duplicate VehicleDead per salvo per node, rerouted to the
// victim's master (night-15 receipts: 5 real deaths -> 8 PLAYER_DEAD,
// deaths=1,2 one ms apart, x3 same-millisecond; multiplicity tracks the
// killing-window ROUNDS, not shooters or peers), plus a duplicate death
// splash per peer. Peers' death visuals ride the type-6 record path
// (UpdateDeathState wreck/explosion), never this tail -- gating it here
// removes only the phantom emissions the binary never made.
if (deathBlastArmed && IsMechDestroyed()
&& GetInstance() != ReplicantInstance)
{
extern void BTMechPostVehicleDead(void *, void *, int);
BTMechPostVehicleDead((void *)this, (void *)message, reportZone);