#45/#134 authentic score/death report tail -- replaces the scoring stand-ins

Reconstructs the dark-gap tail of Mech::TakeDamageMessageHandler
(@0x4a02f4-0x4a0890, raw disasm): the three id-0x16 score reports (kill to
the shooter's player / type-0 wire-fidelity / received to the victim's
player) and the BT 0x38-byte VehicleDeadMessage extension {killed-by player,
kill zone} dispatched from the death tail.  Retires BTPostDamageScore /
BTPostKillScore and the per-hit inflicted credit (never existed in 1995:
@0x4c0200 is bound in no handler-table entry -- byte-scan receipt in
decomp-reference).  Suicides now dispatch and the handler negates the award
(the #134 panic penalty).  Collision divert falls through to the death tail
per @0x4a0375 (wall deaths respawn + blast; no score).  ScoreMessage fields
renamed to decoded truth (vitalHit/zoneIndex/subsysID) + wire asserts;
console VTVDamaged points_transfered corrected (Round(award), not Now()).

Benches: scorekill.sh cross-node kill (kills=1 award=4.88, killedBy=2:1
zone=3, single death cycle), scoreself.sh suicide (type=2 award=-39.00
kills=0), deathblast2.sh re-verified (72 bursts at ~9u).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Joe DiPrima
2026-08-05 18:36:58 -05:00
co-authored by Claude Fable 5
parent 06a8edbff2
commit 91bd28669e
12 changed files with 706 additions and 296 deletions
+33 -74
View File
@@ -705,10 +705,12 @@ static int gEnemyDestroyed = 0;
// BRING-UP: the spawned target/enemy mech (defined in btplayer.cpp). The player
// mech locks onto it as its current target each frame (see targeting step below).
extern Entity *gEnemyMech;
// gauge scoring wave: producers (btplayer.cpp) -- feed the scoreboard from combat.
extern void BTPostDamageScore(Entity *victim, Scalar damage, Entity *shooter); // per-hit SCORE (ScoreInflicted)
// RETIRED 2026-08-05: the BTPostDamageScore / BTPostKillScore producers.
// Scoring now flows through the authentic report tail in the victim's
// TakeDamageMessageHandler (BTMechPostCombatReports, btplayer.cpp) -- the
// shooter-side posts here double-credited against it. See the tombstone in
// btplayer.cpp.
float gBTWalkElev = 0.0f; // #124 zone walker: servo-driven aim elevation (rad)
extern void BTPostKillScore(Entity *victim, Scalar damage); // KILL (+ MP death)
// Mech target slots (verified vs the binary's weapon/fire path, part_013.c):
// mech+0x37c Point3D current target world position (range/aim source)
@@ -1311,12 +1313,11 @@ static void
shooter->GetEntityID(), -1 /*unaimed -> cylinder resolves*/, dmg);
e->Dispatch(&td);
// SCORE (#95 follow-up): the binary's victim handler tallies EVERY
// TakeDamage -- splash included -- and reports it to the INFLICTING
// player (id-0x16, deferred #45), so a bystander blast is score too.
// The bridge itself refuses a shooter that is not the local vehicle
// (this function's death-blast caller passes the DYING mech).
BTPostDamageScore(e, dmg.damageAmount * (Scalar)bursts, shooter);
// SCORE: handled by the victim's own TakeDamage handler now -- the
// dispatch above lands in the authentic report tail (#45), which
// posts the kill/received reports itself. The shooter-side
// BTPostDamageScore call that lived here was RETIRED 2026-08-05
// (per-hit inflicted credit never existed in the 1995 pod).
// MP MATCH FORENSICS (matchlog.hpp): shooter-side splash delivery
// to a bystander (pairs with the victim's DMG burst>1 line).
@@ -1864,17 +1865,12 @@ static void
(p.shooter != 0) ? p.shooter->GetEntityID() : EntityID::Null,
-1 /*unaimed -> cylinder resolves*/, dmg, p.weaponSubsys);
tgt->Dispatch(&take_damage);
// gauge scoring wave (Step 6): a projectile hit credits SCORE with
// the DELIVERED amount -- per-missile damage x the rolled cluster
// count, the same figure the dispatch above hands the victim's
// handler (which applies it burstCount times, tallying the amount
// per burst @0x4a04da). Posting the bare per-missile amount here
// was the "LRM10 registered ~3 points to score" defect (#95): the
// armour took the full salvo while the scoreboard banked one
// missile. (Crit bonuses in the tally remain victim-side
// knowledge -- the deferred id-0x16 report, #45.)
BTPostDamageScore((Entity *)tgt,
p.damage * (Scalar)dmg.burstCount, p.shooter);
// SCORE: handled by the victim's own TakeDamage handler now --
// the dispatch above lands in the authentic report tail (#45),
// which tallies every burst (crit bonuses included) itself.
// The shooter-side BTPostDamageScore call that lived here (and
// its #95 salvo-credit fix) was RETIRED 2026-08-05: per-hit
// inflicted credit never existed in the 1995 pod.
// MP MATCH FORENSICS (matchlog.hpp): shooter-side
// projectile impact delivery (pairs with the victim's
@@ -2425,12 +2421,11 @@ void
// boresight pick skips a dead mech, so that block never ran against
// it again: internally dead, smoking, standing, invulnerable.
{
// KILL score (once; the ownerless dummy yields no death for us).
// task #60: pass the REAL killing-blow magnitude (latched in the damage
// handler), not the flat bring-up kShotDamage -- the score handler
// @0x4c02e4 derives the whole kill award from this damageAmount, so a
// flat 12 made every kill score identically regardless of weapon.
BTPostKillScore((Entity *)this, lastInflictingDamage);
// KILL score: RETIRED here 2026-08-05 -- the authentic kill report now
// posts from the TakeDamage report tail (@0x4a04da, block A) with the
// REAL applied tally, which supersedes the lastInflictingDamage relay
// this site needed (#60). Suicides dispatch too (the handler negates
// the award -- the #134 panic-penalty path this site's filter blocked).
if ((Entity *)this == gEnemyMech)
gEnemyDestroyed = 1; // latches off damage score
@@ -2496,55 +2491,19 @@ void
}
//
// --- RESPAWN CYCLE (task #52): notify the owning player of the death. ---
// The BTPlayer VehicleDead handler @004c05c4 receives this with
// deathCount == -1 (the ctor default = "immediate death notification"),
// does the death bookkeeping, severs playerVehicle (this wreck entity
// STAYS in the world) and re-posts the message to itself at +5 seconds
// (5.0f @004c0830) -> the engine drop-zone hunt -> DropZoneReply ->
// CreatePlayerVehicle (a NEW mech). Only the owner pod's master has a
// live playerLink, which is exactly where the cycle must run; replicant
// wrecks just mirror the master's death. [T1 the handler @004c05c4;
// T3 this dispatch site -- the binary's exact sender is undecoded, but
// the once-per-death transition is the only death edge and the message
// ctor's deathCount=-1 default exists for precisely this notification.]
// --- RESPAWN CYCLE (task #52): the VehicleDead notify MOVED 2026-08-05. ---
// This site carried [T3 -- the binary's exact sender is undecoded]. The
// sender is now decoded [T1]: the death tail of the TakeDamage handler
// that killed the mech (@0x4a07d4-0x4a0890, raw disasm) builds the BT
// 0x38-byte VehicleDeadMessage extension {killed-by player, kill zone}
// and dispatches it to the owning player. BTMechPostVehicleDead
// (btplayer.cpp) is that sender; the #55 NULL-playerLink fallback and the
// DEAD_NOTIFY forensics moved into it intact. Every path that reaches
// this transition does so inside the killing TakeDamage (weapon zones,
// collision rattle via the divert fallthrough, the eject charge), so the
// tail covers every death this site covered -- with one dispatch, keeping
// the #81 single-VehicleDead rule.
//
{
Player *owner = GetPlayerLink();
// #55 step 1 (the David fix): the death notification used to hinge on
// this single never-retried pointer -- playerLink is written once by
// Entity::PlayerLinkMessageHandler with no null check, so a lost race
// silently swallowed the whole respawn cycle. Resolve the SAME object
// by the reverse link the binary's own respawn branch uses
// (player+0x1FC == playerVehicle): the mission player whose vehicle is
// this mech. Not a stand-in -- the identical entity, second index.
if (owner == 0 && application != 0)
{
Player *mission_player = (Player *)application->GetMissionPlayer();
if (mission_player != 0
&& mission_player->GetPlayerVehicle() == (Entity *)this)
{
owner = mission_player;
}
}
// #55 step 0: a NULL link was a SILENT swallow -- record the
// resolution unconditionally so the matchlog can prove it either way.
BTMatchLog("DEAD_NOTIFY", "mech=%d:%d link=%p",
BTMatchHostOf(GetEntityID()), (int)GetEntityID(),
(void *)owner);
if (owner != 0)
{
Player::VehicleDeadMessage
vehicle_dead(
Player::VehicleDeadMessageID,
sizeof(Player::VehicleDeadMessage)
);
owner->Dispatch(&vehicle_dead);
if (getenv("BT_DEATH_LOG"))
DEBUG_STREAM << "[death] VehicleDead(-1) dispatched to the owning player\n"
<< std::flush;
}
}
}
// Peer heading published each sim update, sampled by the render loop (BT_RENDHDG).