#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:
co-authored by
Claude Fable 5
parent
06a8edbff2
commit
91bd28669e
+285
-136
@@ -203,6 +203,35 @@ static_assert(offsetof(BTPlayer::MakeMessage, roleName) == 0x90,
|
||||
static_assert(sizeof(BTPlayer::MakeMessage) == 0xD0,
|
||||
"BTPlayer::MakeMessage wire size must be 0xD0");
|
||||
|
||||
// Wire-format lock (2026-08-05): the combat score reports and the extended
|
||||
// VehicleDead cross nodes raw (replicant-player Dispatch reroute), and the
|
||||
// sender/receiver field map was transcribed from the binary's builds
|
||||
// (@0x4a04da/@0x4a05d9/@0x4a06c0 and @0x4a07d4) -- lock every offset.
|
||||
static_assert(offsetof(BTPlayer::ScoreMessage, scoreAward) == 0x1c,
|
||||
"ScoreMessage::scoreAward must be at wire offset 0x1c");
|
||||
static_assert(offsetof(BTPlayer::ScoreMessage, scoreType) == 0x20,
|
||||
"ScoreMessage::scoreType must be at wire offset 0x20");
|
||||
static_assert(offsetof(BTPlayer::ScoreMessage, damageAmount) == 0x24,
|
||||
"ScoreMessage::damageAmount must be at wire offset 0x24");
|
||||
static_assert(offsetof(BTPlayer::ScoreMessage, vitalHit) == 0x28,
|
||||
"ScoreMessage::vitalHit must be at wire offset 0x28");
|
||||
static_assert(offsetof(BTPlayer::ScoreMessage, zoneIndex) == 0x2c,
|
||||
"ScoreMessage::zoneIndex must be at wire offset 0x2c");
|
||||
static_assert(offsetof(BTPlayer::ScoreMessage, subsysID) == 0x30,
|
||||
"ScoreMessage::subsysID must be at wire offset 0x30");
|
||||
static_assert(offsetof(BTPlayer::ScoreMessage, senderMechID) == 0x34,
|
||||
"ScoreMessage::senderMechID must be at wire offset 0x34");
|
||||
static_assert(sizeof(BTPlayer::ScoreMessage) == 0x3c,
|
||||
"ScoreMessage wire size must be 0x3c (binary build @0x4a052b)");
|
||||
static_assert(offsetof(BTPlayer::VehicleDeadMessage, reserved28) == 0x28,
|
||||
"VehicleDeadMessage::reserved28 must be at wire offset 0x28");
|
||||
static_assert(offsetof(BTPlayer::VehicleDeadMessage, killedByPlayerID) == 0x2c,
|
||||
"VehicleDeadMessage::killedByPlayerID must be at wire offset 0x2c");
|
||||
static_assert(offsetof(BTPlayer::VehicleDeadMessage, killZone) == 0x34,
|
||||
"VehicleDeadMessage::killZone must be at wire offset 0x34");
|
||||
static_assert(sizeof(BTPlayer::VehicleDeadMessage) == 0x38,
|
||||
"VehicleDeadMessage wire size must be 0x38 (binary build @0x4a07f6)");
|
||||
|
||||
//
|
||||
// LAYOUT LOCK (Gitea #48/#57). Our compiled BTPlayer is NOT the binary's -- the
|
||||
// Entity base differs -- so the 1995 offsets must never be used as raw byte
|
||||
@@ -851,30 +880,45 @@ void
|
||||
{
|
||||
//
|
||||
// Points lost for damage we took (unless we hit ourselves).
|
||||
// sender null guard is PORT SAFETY: the binary derefs the registry
|
||||
// lookup unguarded (@0x4c0453) -- an inflictor that died between
|
||||
// shot and impact would have crashed the 1995 pod.
|
||||
//
|
||||
if (sender_mech != our_mech && scenarioRole != 0) // scoring wave: guard the NULL bring-up role
|
||||
if (sender_mech != 0 && our_mech != 0 // port guards (dead-window straggler)
|
||||
&& sender_mech != our_mech && scenarioRole != 0) // scoring wave: guard the NULL bring-up role
|
||||
{
|
||||
Scalar received =
|
||||
scenarioRole->CalcDamageReceivedScore(message->damageAmount); // FUN_00429b94
|
||||
award = (MECH_TONNAGE(our_mech) / MECH_TONNAGE(sender_mech)) * received;
|
||||
}
|
||||
// BINARY NUANCE [T1 raw disasm @0x4c0433]: on SELF-inflicted damage
|
||||
// (sender == own vehicle) the 1995 arm skips the math and the common
|
||||
// tail folds its UNINITIALIZED award local into currentScore --
|
||||
// stack garbage, every fall/ram self-hit. The port keeps award=0.0,
|
||||
// a deliberate safe deviation.
|
||||
|
||||
//
|
||||
// Tell the console (if any, and not suppressed) that our VTV
|
||||
// was damaged.
|
||||
// was damaged. points_transfered CORRECTED 2026-08-05: the binary
|
||||
// rounds THE COMPUTED AWARD here (raw disasm @0x4c04a7: fld
|
||||
// [ebp-0xc]; call 0x4dcd94 -- an ST0-arg __ftol the decompiler
|
||||
// rendered argless, which an earlier pass misread as Now()).
|
||||
//
|
||||
Host *console_host =
|
||||
application->GetHostManager()->GetConsoleHost(); // FUN_00429078
|
||||
if (console_host && suppressConsole == 0) // this+0x258
|
||||
BTPlayer *damager_player =
|
||||
(sender_mech != 0) ? MECH_OWNING_PLAYER(sender_mech) : 0; // port guard (dummy shooter)
|
||||
if (console_host && suppressConsole == 0 // this+0x258
|
||||
&& damager_player != 0)
|
||||
{
|
||||
ConsolePlayerVTVDamagedMessage damaged_message(
|
||||
ownerID, // this+0x18c
|
||||
MECH_OWNING_PLAYER(sender_mech)->ownerID, // *(sender+0x190)+0x18c
|
||||
damager_player->ownerID, // *(sender+0x190)+0x18c
|
||||
Round(message->damageAmount), // FUN_004078fc
|
||||
message->pointSenderLo, // msg+0x2c
|
||||
message->pointSenderHi, // msg+0x28
|
||||
(int)Now().ticks, // FUN_004dcd94
|
||||
message->auxID // msg+0x30
|
||||
message->zoneIndex, // msg+0x2c
|
||||
message->vitalHit, // msg+0x28
|
||||
Round(award), // FUN_004dcd94(ST0)
|
||||
message->subsysID // msg+0x30
|
||||
);
|
||||
|
||||
application->SendMessage( // app+0x20, slot+0x18
|
||||
@@ -888,6 +932,13 @@ void
|
||||
|
||||
case BTPlayer::ScoreMessage::KillScore: // 2
|
||||
{
|
||||
// PORT SAFETY: an unresolvable victim mech (removed wreck racing the
|
||||
// cross-node dispatch) or a severed own vehicle skips the arm; the
|
||||
// binary derefs both unguarded (@0x4c037e).
|
||||
if (sender_mech == 0 || our_mech == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
// gauge scoring wave: the attacking mech's owner (NULL for the ownerless
|
||||
// BT_SPAWN_ENEMY dummy). Resolved once, guarded everywhere below.
|
||||
BTPlayer *sender_owner = MECH_OWNING_PLAYER(sender_mech); // *(sender+0x190), NULL for the dummy
|
||||
@@ -2191,149 +2242,247 @@ Logical BTResolveMessageBoard(Entity * /*tracked_mech*/, int *messageId, BitMap
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// gauge scoring wave: PRODUCERS -- posted by the combat path (mech4.cpp) to feed
|
||||
// the scoreboard. The inflictor is always the viewpoint mech in bring-up, so the
|
||||
// local (crediting) player is application->GetMissionPlayer(). Bridges (not inline
|
||||
// in mech4.cpp) so the message construction lives in this complete-BTPlayer TU.
|
||||
// THE AUTHENTIC COMBAT REPORT SENDER (#45/#134 -- 2026-08-05)
|
||||
//
|
||||
void BTPostDamageScore(Entity *victim, Scalar damage, Entity *shooter) // Step 6: per-hit SCORE
|
||||
// Reconstructed from the raw disasm of the Mech::TakeDamageMessageHandler
|
||||
// report tail @0x4a02f4-0x4a0890 (dark-gap region, not in the export). This
|
||||
// RETIRES the bring-up producers BTPostDamageScore / BTPostKillScore (see the
|
||||
// tombstone below). The binary's flow, transcribed:
|
||||
//
|
||||
// resolve: shooterEntity = registry find(msg->inflictingEntity) @0x4a033c
|
||||
// shooterPlayer = shooterEntity+0x190 @0x4a034f
|
||||
// victimPlayer = mech+0x190 @0x4a0358
|
||||
// block A @0x4a04da (was ALIVE at entry, destroyed now):
|
||||
// ScoreMessage{id 0x16, type 2 KillScore,
|
||||
// scoreAward = the APPLIED damage tally,
|
||||
// damageAmount = victim role's killBonus (role+0x1c),
|
||||
// vitalHit, zone, subsysID,
|
||||
// senderMechID = the VICTIM} -> the SHOOTER's player
|
||||
// block B @0x4a05d9 (not newly killed, tally != 0):
|
||||
// same shape, type 0 -- which the only registered 0x16 receiver
|
||||
// (@0x4c02e4) VERIFY-rejects; 1995 then folded an UNINITIALIZED
|
||||
// award. Sent for wire fidelity; our handler banks 0.
|
||||
// block C @0x4a06c0 (tally > 0; runs after A too -- kills included):
|
||||
// ScoreMessage{type 1 DamageReceivedScore,
|
||||
// scoreAward = tally,
|
||||
// damageAmount = INTENDED (burstCount x amount),
|
||||
// senderMechID = the INFLICTOR} -> the victim's player
|
||||
//
|
||||
// Score-model consequence [T1]: shooters are credited ONLY for kills; victims
|
||||
// are PENALIZED for damage received (CalcDamageReceivedScore returns the
|
||||
// negative). Per-hit inflicted credit never existed in the 1995 pod.
|
||||
//
|
||||
// Port accommodations, all guarded: registry/player lookups null-checked (the
|
||||
// binary derefs them raw); killBonus basis 0 for the ownerless dummy victim;
|
||||
// a skipped kill credit keeps the #45 NOCREDIT matchlog forensics.
|
||||
//
|
||||
void BTMechPostCombatReports(
|
||||
void *mech_v, // the VICTIM mech (the handler's this)
|
||||
void *msg_v, // the TakeDamageMessage being handled
|
||||
float damage_tally, // loop local_24: applied damage + crit bonuses
|
||||
int vital_hit, // loop local_2c: a zone entered BurningState
|
||||
int report_zone, // msg->damageZone at loop ENTRY (the binary
|
||||
// reads msg+0x24, never rewritten mid-loop)
|
||||
int newly_destroyed) // alive at entry && IsMechDestroyed() now
|
||||
{
|
||||
if (application == 0 || victim == 0)
|
||||
Mech *victim = (Mech *)mech_v;
|
||||
Entity::TakeDamageMessage *msg = (Entity::TakeDamageMessage *)msg_v;
|
||||
if (application == 0 || victim == 0 || msg == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
BTPlayer *local_player = (BTPlayer *)application->GetMissionPlayer();
|
||||
if (local_player == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// The credit belongs to the SHOOTER, not the viewer: the binary reports the
|
||||
// victim handler's damage tally to the INFLICTING player (the id-0x16 report
|
||||
// tail @0x4a04da, deferred #45). This stand-in can only bank score on the
|
||||
// LOCAL player, so refuse any post whose shooter is not the local vehicle --
|
||||
// an AI master's fire on the player (SP), or a dying mech's death-blast
|
||||
// splash, must not credit the viewer. MP is unaffected: only local fire
|
||||
// carries live damage on this node.
|
||||
if (shooter != (Entity *)local_player->GetPlayerVehicle())
|
||||
{
|
||||
return;
|
||||
}
|
||||
// ScoreInflicted (scoreType 0): senderMechID = the mech that TOOK the damage.
|
||||
// -> ScoreInflictedMessageHandler: currentScore += tonnageRatio*CalcInflictedScore.
|
||||
BTPlayer::ScoreMessage message(
|
||||
BTPlayer::ScoreInflictedMessageID, // 0x16
|
||||
sizeof(BTPlayer::ScoreMessage),
|
||||
BTPlayer::ScoreMessage::DamageInflictedScore, // 0
|
||||
0.0f, // scoreAward (unused for inflicted)
|
||||
damage, // damageAmount
|
||||
victim->GetEntityID()); // senderMechID = victim
|
||||
local_player->Dispatch(&message);
|
||||
if (getenv("BT_SCORE_LOG"))
|
||||
DEBUG_STREAM << "[score] +damage " << damage << " -> currentScore="
|
||||
<< (Scalar)local_player->GetScore() << "\n" << std::flush;
|
||||
}
|
||||
|
||||
void BTPostKillScore(Entity *victim, Scalar damage) // Step 7: KILL (+ MP death)
|
||||
{
|
||||
if (application == 0 || victim == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// KILL credit. Runs on the VICTIM's node -- the only node whose mech carries a
|
||||
// populated `lastInflictingID` (damage is applied master-side only: 0 of 8800
|
||||
// corpus DMG rows target a replicant, which is why every `DEATH inst=R` row reads
|
||||
// `killer=0:0`). The credit is dispatched to the killer's Player, which HERE is a
|
||||
// REPLICANT, so `Entity::Dispatch` reroutes it to the owning host
|
||||
// (ENTITY.cpp:244-251) and `++killCount` lands on the killer's OWN machine.
|
||||
//
|
||||
// CORRECTED 2026-07-25 (Gitea #45). The previous banner claimed "every node
|
||||
// maintains LOCAL score copies ... each node witnesses the death transition and
|
||||
// tallies its own copies self-consistently". That was FALSE and it is what hid
|
||||
// this bug: the reroute means exactly ONE node increments, no other node can
|
||||
// observe the killer at all, and nothing carried the value back out -- so every
|
||||
// remote pilot's KILLS read 0 on every other pod, all mission (corpus invariant:
|
||||
// 0 of 18818 DMG rows are inst=R, so no other node can even attribute the kill;
|
||||
// every DEATH inst=R row reads killer=0:0). The counters are
|
||||
// now replicated owner->replicant instead; see Read/WriteUpdateRecord above.
|
||||
BTPlayer *killer_player = 0;
|
||||
BTPlayer *shooter_player = 0;
|
||||
Entity *shooter = 0;
|
||||
extern int BTIsRegisteredMech(Entity *e);
|
||||
if (application->GetHostManager() != 0)
|
||||
{
|
||||
Entity *killer = application->GetHostManager()->GetEntityPointer(
|
||||
((Mech *)victim)->lastInflictingID);
|
||||
extern int BTIsRegisteredMech(Entity *e);
|
||||
if (killer != 0 && killer != victim && BTIsRegisteredMech(killer))
|
||||
killer_player = (BTPlayer *)((Mech *)killer)->GetPlayerLink();
|
||||
shooter = application->GetHostManager()->GetEntityPointer(
|
||||
msg->inflictingEntity);
|
||||
if (shooter != 0 && BTIsRegisteredMech(shooter))
|
||||
shooter_player = (BTPlayer *)((Mech *)shooter)->GetPlayerLink();
|
||||
}
|
||||
if (killer_player != 0)
|
||||
BTPlayer *victim_player = (BTPlayer *)victim->GetPlayerLink();
|
||||
|
||||
if (newly_destroyed)
|
||||
{
|
||||
// KillScore (scoreType 2): senderMechID MUST be the VICTIM (!= the
|
||||
// receiver's mech) so the handler credits `killCount++` -- a suicide
|
||||
// (killer == victim) never reaches here.
|
||||
BTPlayer::ScoreMessage kill(
|
||||
Player::ScoreMessageID, // 0x12
|
||||
//
|
||||
// Block A: the KILL report, to the SHOOTER's player. On the victim's
|
||||
// node that player is a replicant, so Entity::Dispatch reroutes to the
|
||||
// owning host (ENTITY.cpp:244) and the credit lands on the killer's own
|
||||
// machine -- the #45 flow, unchanged. killer == victim (the eject
|
||||
// charge) IS dispatched: the handler negates the award. That is the
|
||||
// #134 panic-penalty path the old BTPostKillScore filtered out.
|
||||
//
|
||||
Scalar kill_bonus = 0.0f; // basis: victim role+0x1c
|
||||
if (victim_player != 0 && victim_player->GetScenarioRole() != 0)
|
||||
kill_bonus = victim_player->GetScenarioRole()->GetKillBonus();
|
||||
if (shooter_player != 0)
|
||||
{
|
||||
BTPlayer::ScoreMessage kill(
|
||||
Player::ScoreMessageID, // 0x16
|
||||
sizeof(BTPlayer::ScoreMessage), // 0x3c
|
||||
BTPlayer::ScoreMessage::KillScore, // 2
|
||||
damage_tally, // +0x1c
|
||||
kill_bonus, // +0x24
|
||||
victim->GetEntityID(), // +0x34 the VICTIM
|
||||
vital_hit, // +0x28
|
||||
report_zone, // +0x2c
|
||||
msg->inflictingSubsystemID); // +0x30
|
||||
shooter_player->Dispatch(&kill);
|
||||
if (getenv("BT_SCORE_LOG"))
|
||||
DEBUG_STREAM << "[score] *** KILL report *** -> shooterPlayer="
|
||||
<< (void *)shooter_player << " tally=" << damage_tally
|
||||
<< " bonus=" << kill_bonus << "\n" << std::flush;
|
||||
}
|
||||
else if (BTMatchLogActive())
|
||||
{
|
||||
// #45 forensics, carried from BTPostKillScore: WHY the credit was
|
||||
// skipped -- missing killer entity, unregistered, or NULL link.
|
||||
BTMatchLog("NOCREDIT",
|
||||
"victim=%d:%d killerID=%d:%d found=%d registered=%d link=%d",
|
||||
BTMatchHostOf(victim->GetEntityID()), (int)victim->GetEntityID(),
|
||||
BTMatchHostOf(msg->inflictingEntity), (int)msg->inflictingEntity,
|
||||
(int)(shooter != 0),
|
||||
(int)(shooter != 0 && BTIsRegisteredMech(shooter)),
|
||||
(int)(shooter != 0 && ((Mech *)shooter)->GetPlayerLink() != 0));
|
||||
}
|
||||
}
|
||||
else if (damage_tally != 0.0f && shooter_player != 0)
|
||||
{
|
||||
//
|
||||
// Block B: the plain inflicted report. Wire fidelity only -- the 0x16
|
||||
// handler Verify-rejects type 0 and banks award 0 (1995 banked an
|
||||
// uninitialized stack float; see the handler's type-0 arm note).
|
||||
//
|
||||
BTPlayer::ScoreMessage inflicted(
|
||||
Player::ScoreMessageID,
|
||||
sizeof(BTPlayer::ScoreMessage),
|
||||
BTPlayer::ScoreMessage::KillScore, // 2
|
||||
0.0f, // scoreAward (killBonus; 0 for bring-up)
|
||||
damage, // damageAmount (killing-blow)
|
||||
victim->GetEntityID()); // senderMechID = victim
|
||||
killer_player->Dispatch(&kill);
|
||||
if (getenv("BT_SCORE_LOG"))
|
||||
DEBUG_STREAM << "[score] *** KILL *** killerPlayer=" << (void *)killer_player
|
||||
<< " killCount=" << killer_player->GetKillCount()
|
||||
<< " deaths=" << killer_player->GetDeaths()
|
||||
<< " score=" << (Scalar)killer_player->GetScore() << std::endl;
|
||||
}
|
||||
else if (BTMatchLogActive())
|
||||
{
|
||||
//
|
||||
// #45 observability: the credit was SKIPPED. This used to be completely
|
||||
// silent, which is why the bug survived so long -- the counter simply never
|
||||
// moved and no log said why. Record which link in the chain broke so a
|
||||
// single matchlog convicts it: a missing killer entity, an unregistered
|
||||
// mech, a self-kill, or the NULL playerLink hazard.
|
||||
//
|
||||
Entity *k = (application->GetHostManager() != 0)
|
||||
? application->GetHostManager()->GetEntityPointer(
|
||||
((Mech *)victim)->lastInflictingID)
|
||||
: 0;
|
||||
extern int BTIsRegisteredMech(Entity *e);
|
||||
BTMatchLog("NOCREDIT",
|
||||
"victim=%d:%d killerID=%d:%d found=%d self=%d registered=%d link=%d",
|
||||
BTMatchHostOf(victim->GetEntityID()), (int)victim->GetEntityID(),
|
||||
BTMatchHostOf(((Mech *)victim)->lastInflictingID),
|
||||
(int)((Mech *)victim)->lastInflictingID,
|
||||
(int)(k != 0), (int)(k == victim),
|
||||
(int)(k != 0 && BTIsRegisteredMech(k)),
|
||||
(int)(k != 0 && ((Mech *)k)->GetPlayerLink() != 0));
|
||||
BTPlayer::ScoreMessage::DamageInflictedScore, // 0
|
||||
damage_tally, // +0x1c
|
||||
damage_tally, // +0x24 basis = tally
|
||||
victim->GetEntityID(),
|
||||
vital_hit, report_zone, msg->inflictingSubsystemID);
|
||||
shooter_player->Dispatch(&inflicted);
|
||||
}
|
||||
|
||||
// MP DEATH: the victim's death tally.
|
||||
//
|
||||
// REMOVED 2026-07-30 (#81): this used to dispatch a second
|
||||
// `Player::VehicleDeadMessage` to the victim's own player "to credit a
|
||||
// death". That message is the RESPAWN-CYCLE TRIGGER, not a scoreboard
|
||||
// increment, and the victim already receives one from the mech's death
|
||||
// transition (mech4.cpp:2110) -- which is the hardened, authentic notify
|
||||
// (it carries the #55 NULL-playerLink fallback). Both fire inside the SAME
|
||||
// death transition (BTPostKillScore is called at mech4.cpp:2006, the notify
|
||||
// at :2110), so they were always paired: every death dispatched the message
|
||||
// TWICE. The tally itself is credited by the handler's `++deathTally`
|
||||
// (:538) on the first one, so this dispatch never added anything.
|
||||
//
|
||||
// It was invisible because the `deathPending` latch silently deduped it --
|
||||
// that is what EVERY "death ... SWALLOWED" warning in the field logs
|
||||
// actually was (8 of 8 deaths, a 100% base rate, which is exactly why it
|
||||
// correlated with nothing). Once the latch was released to match the
|
||||
// binary (see the death handler's tail), the duplicate stopped being
|
||||
// masked and started a SECOND death cycle: deathCount double-incremented,
|
||||
// the first cycle's re-post went stale and tripped the drop-zone
|
||||
// `*** MISMATCH ***`, and one respawn burned two cycles. Caught on the
|
||||
// first solo bench of the latch fix.
|
||||
//
|
||||
// The kill credit above is unaffected -- it dispatches a ScoreMessage, the
|
||||
// correct message for a scoreboard change.
|
||||
if (damage_tally > 0.0f && victim_player != 0)
|
||||
{
|
||||
//
|
||||
// Block C: the RECEIVED report, to the victim's own player -- kills
|
||||
// included (the binary runs this after block A as well). Carries the
|
||||
// INTENDED damage (burstCount x amount, @0x4a06d2 fild/fmul) as the
|
||||
// penalty basis and the INFLICTOR as senderMechID; feeds the score
|
||||
// penalty and the operator-console VTVDamaged line.
|
||||
//
|
||||
int bursts = msg->damageData.burstCount;
|
||||
if (bursts < 1)
|
||||
bursts = 1; // port guard (mirrors the loop)
|
||||
BTPlayer::ScoreMessage received(
|
||||
Player::ScoreMessageID,
|
||||
sizeof(BTPlayer::ScoreMessage),
|
||||
BTPlayer::ScoreMessage::DamageReceivedScore, // 1
|
||||
damage_tally, // +0x1c
|
||||
(Scalar)bursts * msg->damageData.damageAmount, // +0x24 intended
|
||||
msg->inflictingEntity, // +0x34 the INFLICTOR
|
||||
vital_hit, report_zone, msg->inflictingSubsystemID);
|
||||
victim_player->Dispatch(&received);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// The authentic VehicleDead sender (@0x4a07d4-0x4a0890): the respawn-cycle
|
||||
// trigger, dispatched from the death tail of the SAME TakeDamage that killed
|
||||
// the mech, to the mech's own player -- now carrying the killed-by player and
|
||||
// the killing zone (the BT 0x38-byte message extension). Replaces the mech4
|
||||
// death-transition dispatch site, which carried a [T3 -- the binary's exact
|
||||
// sender is undecoded] flag; this IS that sender, decoded. The #55
|
||||
// NULL-playerLink fallback and the DEAD_NOTIFY forensics move here intact.
|
||||
//
|
||||
void BTMechPostVehicleDead(void *mech_v, void *msg_v, int report_zone)
|
||||
{
|
||||
Mech *victim = (Mech *)mech_v;
|
||||
Entity::TakeDamageMessage *msg = (Entity::TakeDamageMessage *)msg_v;
|
||||
if (application == 0 || victim == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Player *owner = victim->GetPlayerLink();
|
||||
// #55 (the David fix), moved intact: resolve the SAME object by the
|
||||
// reverse link the binary's own respawn branch uses when the once-written
|
||||
// playerLink was lost.
|
||||
if (owner == 0)
|
||||
{
|
||||
Player *mission_player = (Player *)application->GetMissionPlayer();
|
||||
if (mission_player != 0
|
||||
&& mission_player->GetPlayerVehicle() == (Entity *)victim)
|
||||
{
|
||||
owner = mission_player;
|
||||
}
|
||||
}
|
||||
BTMatchLog("DEAD_NOTIFY", "mech=%d:%d link=%p",
|
||||
BTMatchHostOf(victim->GetEntityID()), (int)victim->GetEntityID(),
|
||||
(void *)owner);
|
||||
if (owner == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// killed-by: the shooter PLAYER's own EntityID (@0x4a07d4 copies
|
||||
// shooterPlayer+0x184); Null when the inflictor or its player is gone.
|
||||
EntityID killed_by = EntityID::Null;
|
||||
if (msg != 0 && application->GetHostManager() != 0)
|
||||
{
|
||||
Entity *shooter = application->GetHostManager()->GetEntityPointer(
|
||||
msg->inflictingEntity);
|
||||
extern int BTIsRegisteredMech(Entity *e);
|
||||
if (shooter != 0 && BTIsRegisteredMech(shooter))
|
||||
{
|
||||
Player *shooter_player = ((Mech *)shooter)->GetPlayerLink();
|
||||
if (shooter_player != 0)
|
||||
killed_by = shooter_player->GetEntityID();
|
||||
}
|
||||
}
|
||||
|
||||
BTPlayer::VehicleDeadMessage vehicle_dead(
|
||||
Player::VehicleDeadMessageID, // 0x17
|
||||
sizeof(BTPlayer::VehicleDeadMessage), // 0x38
|
||||
killed_by,
|
||||
report_zone);
|
||||
owner->Dispatch(&vehicle_dead);
|
||||
if (getenv("BT_DEATH_LOG"))
|
||||
DEBUG_STREAM << "[death] VehicleDead(-1) dispatched to the owning player"
|
||||
<< " (killedBy=" << BTMatchHostOf(killed_by) << ":" << (int)killed_by
|
||||
<< " zone=" << report_zone << ")\n" << std::flush;
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// TOMBSTONE (2026-08-05): BTPostDamageScore / BTPostKillScore -- the gauge
|
||||
// scoring wave's bring-up producers -- are RETIRED, replaced by the authentic
|
||||
// report tail above (BTMechPostCombatReports / BTMechPostVehicleDead, called
|
||||
// from Mech::TakeDamageMessageHandler at the binary's exact positions).
|
||||
// What changed for the scoreboard:
|
||||
// - per-hit INFLICTED credit is GONE: it was a port invention riding the
|
||||
// never-registered @0x4c0200 handler (the binary's BTPlayer table, byte-
|
||||
// scanned at file 0x112dxx, has 6 entries and none binds it). 1995
|
||||
// scoring = kill awards + received-damage penalties, nothing per-hit.
|
||||
// This also retires the #95 salvo-credit fix that rode it.
|
||||
// - suicides now DISPATCH the kill report and the handler NEGATES the
|
||||
// award -- the #134 panic-eject penalty path the old filter blocked.
|
||||
// - kill credit resolves the shooter from msg->inflictingEntity of the
|
||||
// killing TakeDamage itself; lastInflictingID (stamped from the same
|
||||
// field at handler entry) stays for the LOD router + effect orientation.
|
||||
// - the cross-node reroute story is unchanged: the kill report still
|
||||
// Dispatches to a replicant player and lands on the owner host
|
||||
// (ENTITY.cpp:244-251); KILLS/DEATHS still replicate owner->replicant
|
||||
// (Read/WriteUpdateRecord + the scoreboard heartbeat).
|
||||
// - the #81 single-VehicleDead rule is unchanged: exactly one dispatch per
|
||||
// death, now from the authentic tail instead of the death transition.
|
||||
// The ScoreInflictedMessageID channel and its handler remain registered but
|
||||
// idle (port infra; the binary's analog was dead code in the table).
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
//#############################################################################
|
||||
// BTPlayerRoleLocksAdvanced -- complete-type bridge (task #12)
|
||||
|
||||
@@ -57,14 +57,26 @@ class DropZone__ReplyMessage;
|
||||
// generated the points. Parallels RPPlayer__ScoreMessage, but the BT
|
||||
// score types describe mech combat instead of score zones.
|
||||
//
|
||||
// Observed message layout (param_2 in the handlers):
|
||||
// +0x1c scoreAward (Scalar, base Player::ScoreMessage)
|
||||
// Message layout, sizeof 0x3C (field meanings CORRECTED 2026-08-05 from the
|
||||
// raw disasm of the three sender builds in the Mech::TakeDamageMessageHandler
|
||||
// report tail @0x4a04da/@0x4a05d9/@0x4a06c0 -- the old "pointSender" names
|
||||
// were guesses from RP's smaller message, which stops at +0x28):
|
||||
// +0x1c scoreAward (Scalar, base) -- the sender puts the APPLIED
|
||||
// damage tally here; the receiving handler
|
||||
// overwrites it with the computed award before
|
||||
// delegating to Player::ScoreMessageHandler
|
||||
// +0x20 scoreType (int)
|
||||
// +0x24 damageAmount (Scalar) raw damage / point quantity
|
||||
// +0x28 pointSenderHi (EntityID word)
|
||||
// +0x2c pointSenderLo (EntityID word)
|
||||
// +0x30 auxID (EntityID / host word)
|
||||
// +0x34 senderMechID (EntityID -- resolved to the inflicting Mech)
|
||||
// +0x24 damageAmount (Scalar) the scoring BASIS: type 0 = tally,
|
||||
// type 1 = intended (burstCount x amount),
|
||||
// type 2 = the VICTIM role's killBonus (role+0x1c)
|
||||
// +0x28 vitalHit (int) a zone reached BurningState during
|
||||
// this message's applications (loop local_2c)
|
||||
// +0x2c zoneIndex (int) the struck damage zone (msg+0x24,
|
||||
// the INITIAL resolve -- not the last burst's)
|
||||
// +0x30 subsysID (int) inflictingSubsystemID passed through
|
||||
// from the TakeDamageMessage (+0x5c)
|
||||
// +0x34 senderMechID (EntityID) types 0/2: the VICTIM mech;
|
||||
// type 1: the INFLICTING mech
|
||||
//
|
||||
class BTPlayer__ScoreMessage:
|
||||
public Player::ScoreMessage
|
||||
@@ -72,11 +84,16 @@ class DropZone__ReplyMessage;
|
||||
public:
|
||||
//
|
||||
// Kind of scoring event. Recovered from the branch selector at
|
||||
// @004c02e4 (this->scoreType, message+0x20) and the dedicated
|
||||
// inflicted-damage handler at @004c0200.
|
||||
// @004c02e4 (this->scoreType, message+0x20). NOTE: type 0 has NO
|
||||
// scoring arm in the binary -- @004c02e4 Verify-rejects it (line 662)
|
||||
// and @004c0200, the only function that accepts it, appears in no
|
||||
// handler-table entry (byte-scan 2026-08-05: the BTPlayer table at
|
||||
// file 0x112dxx has exactly 6 entries, none binding it). 1995 pod
|
||||
// scoring = kills + received-damage penalties; per-hit inflicted
|
||||
// credit never existed.
|
||||
//
|
||||
enum ScoreType {
|
||||
DamageInflictedScore = 0, // to ScoreInflictedMessageHandler
|
||||
DamageInflictedScore = 0, // sent, but scores nothing (see above)
|
||||
DamageReceivedScore = 1, // I took damage
|
||||
KillScore = 2 // I destroyed / was destroyed
|
||||
};
|
||||
@@ -85,21 +102,19 @@ class DropZone__ReplyMessage;
|
||||
scoreType; // +0x20
|
||||
|
||||
Scalar
|
||||
damageAmount; // +0x24
|
||||
damageAmount; // +0x24 scoring basis (see layout table)
|
||||
|
||||
//
|
||||
// Point-source / auxiliary handles carried alongside the damage record
|
||||
// (read by ScoreMessageHandler when building the console feed message).
|
||||
//
|
||||
int
|
||||
pointSenderHi; // +0x28
|
||||
vitalHit; // +0x28 zone entered BurningState this message
|
||||
|
||||
int
|
||||
pointSenderLo; // +0x2c
|
||||
zoneIndex; // +0x2c struck damage zone
|
||||
|
||||
int
|
||||
auxID; // +0x30
|
||||
subsysID; // +0x30 inflicting weapon subsystem
|
||||
|
||||
EntityID
|
||||
senderMechID; // +0x34 inflicting mech (point sender)
|
||||
senderMechID; // +0x34 victim (types 0/2) / inflictor (type 1)
|
||||
|
||||
BTPlayer__ScoreMessage(
|
||||
Receiver::MessageID message_ID,
|
||||
@@ -107,15 +122,63 @@ class DropZone__ReplyMessage;
|
||||
int score_type,
|
||||
Scalar score_award,
|
||||
Scalar damage_amount,
|
||||
const EntityID &sender_mech_ID
|
||||
const EntityID &sender_mech_ID,
|
||||
int vital_hit = 0,
|
||||
int zone_index = 0,
|
||||
int subsys_ID = 0
|
||||
):
|
||||
Player::ScoreMessage(message_ID, length, score_award),
|
||||
scoreType(score_type),
|
||||
damageAmount(damage_amount),
|
||||
vitalHit(vital_hit),
|
||||
zoneIndex(zone_index),
|
||||
subsysID(subsys_ID),
|
||||
senderMechID(sender_mech_ID)
|
||||
{}
|
||||
};
|
||||
|
||||
//###########################################################################
|
||||
//################# BTPlayer::VehicleDeadMessage ######################
|
||||
//###########################################################################
|
||||
//
|
||||
// BT extension of the engine Player__VehicleDeadMessage (PLAYER.h:
|
||||
// deathCount @0x1c, dropZoneID @0x20). Decoded 2026-08-05 from the death
|
||||
// tail of Mech::TakeDamageMessageHandler (@0x4a07d4-0x4a0890, raw disasm):
|
||||
// the binary builds a 0x38-byte message {size 0x38, id 0x17 ==
|
||||
// Player::VehicleDeadMessageID, pri 1, deathCount -1 (the engine ctor
|
||||
// default), dropZoneID Null, +0x28 = 0, +0x2c = the KILLER PLAYER's own
|
||||
// EntityID (shooterPlayer+0x184), +0x34 = the killing zone (msg+0x24)} and
|
||||
// dispatches it to the dying mech's own player -- the respawn-cycle
|
||||
// trigger, now carrying the killed-by attribution. Whether the @004c05c4
|
||||
// handler consumes the two extension fields is still undecoded (the
|
||||
// function is not in the export); the sender carries them faithfully.
|
||||
//
|
||||
class BTPlayer__VehicleDeadMessage:
|
||||
public Player::VehicleDeadMessage
|
||||
{
|
||||
public:
|
||||
int
|
||||
reserved28; // +0x28 zeroed by the binary (@0x4a0856)
|
||||
|
||||
EntityID
|
||||
killedByPlayerID; // +0x2c the killer PLAYER's EntityID
|
||||
|
||||
int
|
||||
killZone; // +0x34 zone the killing damage struck
|
||||
|
||||
BTPlayer__VehicleDeadMessage(
|
||||
Receiver::MessageID message_ID,
|
||||
size_t length,
|
||||
const EntityID &killed_by_player_ID,
|
||||
int kill_zone
|
||||
):
|
||||
Player::VehicleDeadMessage(message_ID, length), // deathCount -1, dropZone Null
|
||||
reserved28(0),
|
||||
killedByPlayerID(killed_by_player_ID),
|
||||
killZone(kill_zone)
|
||||
{}
|
||||
};
|
||||
|
||||
//###########################################################################
|
||||
//##################### BTPlayer::MakeMessage #########################
|
||||
//###########################################################################
|
||||
@@ -202,8 +265,9 @@ class DropZone__ReplyMessage;
|
||||
// Player::ScoreMessage.)
|
||||
//
|
||||
public:
|
||||
typedef BTPlayer__ScoreMessage ScoreMessage;
|
||||
typedef BTPlayer__MakeMessage MakeMessage;
|
||||
typedef BTPlayer__ScoreMessage ScoreMessage;
|
||||
typedef BTPlayer__MakeMessage MakeMessage;
|
||||
typedef BTPlayer__VehicleDeadMessage VehicleDeadMessage; // BT 0x38 ext (killed-by)
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Message Support
|
||||
|
||||
+70
-41
@@ -1063,24 +1063,40 @@ void
|
||||
(float)(message->damageData.impactPoint.z - localOrigin.linearPosition.z));
|
||||
}
|
||||
|
||||
//
|
||||
// Death-edge latch, MOVED 2026-08-05 to the authentic position (binary
|
||||
// @0x4a0303: captured at handler ENTRY, before the collision divert). It
|
||||
// arms the whole death tail -- kill report, VehicleDead, death blast.
|
||||
// Capturing it after the divert (where #89 first placed it) meant a
|
||||
// COLLISION death could never arm the tail.
|
||||
//
|
||||
const int deathBlastArmed = !IsMechDestroyed(); // [ebp-0x10], inverted
|
||||
//
|
||||
// The zone the reports + VehicleDead carry: msg+0x24 as of loop entry.
|
||||
// The binary never rewrites msg+0x24 after the initial cylinder resolve;
|
||||
// our loop-exit matchlog write (damageZone = LAST burst's zone) must not
|
||||
// leak into them.
|
||||
//
|
||||
int reportZone = message->damageZone;
|
||||
DamageLookupTable *table = (DamageLookupTable *)damageLookupTable; // named member (Wword absorbs!)
|
||||
|
||||
//
|
||||
// @0x4a0368 -- the COLLISION DIVERT (#83, closing the #82 chain). Damage
|
||||
// type 0 == the mover's collision damage (what the crash response policy
|
||||
// forwards). It NEVER reaches the zone/armor loop below: it is priced +
|
||||
// distributed as INTERNAL RATTLE by DistributeCollisionDamage (@0049ffcc)
|
||||
// and the handler is done. This was the missing half of the crash
|
||||
// self-damage reconstruction -- without the divert, the raw kinetic-energy
|
||||
// amounts (a 60-ton mech prices ~1000+ per wall tap) fell through into the
|
||||
// WEAPON loop and a single bump killed the mech (run-14 field report).
|
||||
// distributed as INTERNAL RATTLE by DistributeCollisionDamage (@0049ffcc).
|
||||
// CORRECTED 2026-08-05: the binary does NOT return here -- it JUMPS TO THE
|
||||
// DEATH TAIL (raw disasm @0x4a0375: jmp 0x4a07b5), so a mech that dies of
|
||||
// the rattle still posts VehicleDead (the respawn trigger) and still
|
||||
// blasts. The early `return` this block shipped with was a latent
|
||||
// "wall-death strands the pilot" hazard. No score reports on this path --
|
||||
// the jump bypasses them; a collision death credits no one. Authentic.
|
||||
//
|
||||
if (message->damageData.damageType == 0)
|
||||
{
|
||||
DistributeCollisionDamage(&message->damageData);
|
||||
Check_Fpu();
|
||||
return;
|
||||
goto death_tail;
|
||||
}
|
||||
|
||||
DamageLookupTable *table = (DamageLookupTable *)damageLookupTable; // named member (Wword absorbs!)
|
||||
// #92: when this gate fails the zone keeps whatever the message carried --
|
||||
// which for an unaimed weapon hit is 0, i.e. EVERY shot lands on zone 0.
|
||||
if (getenv("BT_DMGTABLE_LOG"))
|
||||
@@ -1105,6 +1121,7 @@ void
|
||||
<< message->damageData.impactPoint.z << ")\n" << std::flush;
|
||||
}
|
||||
}
|
||||
reportZone = message->damageZone; // post-resolve (@0x4a0396 write)
|
||||
//
|
||||
// #80 -- the faithful application loop (binary @0x4a0423-0x4a04d8), which
|
||||
// REPLACES the engine-base single application. Three things the base
|
||||
@@ -1126,11 +1143,6 @@ void
|
||||
// nothing (matches ENTITY.cpp:878; the binary would deref -1 -- it can't
|
||||
// happen there because every mech ships a lookup table).
|
||||
//
|
||||
// #89 DEATH-BLAST edge arm: the binary's tail (@0x4a07b8) gates on the
|
||||
// victim ENTERING dead/eject during THIS message (entry test edx = the
|
||||
// pre-application disabled state). Capture the pre-state here.
|
||||
const int deathBlastArmed = !IsMechDestroyed();
|
||||
|
||||
if (damageZones != 0 && message->damageZone >= 0
|
||||
&& message->damageZone < damageZoneCount)
|
||||
{
|
||||
@@ -1181,14 +1193,20 @@ void
|
||||
}
|
||||
message->damageZone = zoneIndex; // matchlog sees the LAST zone
|
||||
|
||||
// Binary tail (@0x4a04da-0x4a07b2, decoded + deferred): builds id-0x16
|
||||
// damage/kill REPORT messages -- {tally, zone, zoneDestroyed flag,
|
||||
// inflicting subsystem, victim name} -- to the shooter's player (with a
|
||||
// kill-flagged variant when this damage NEWLY disabled the mech) and to
|
||||
// the victim's player. That is the authentic stats plumbing (#45); the
|
||||
// port's matchlog + BTPostDamageScore cover the bookkeeping today [T3].
|
||||
(void)zoneDestroyed;
|
||||
(void)damageTally;
|
||||
//
|
||||
// THE REPORT TAIL (@0x4a04da-0x4a07b2), reconstructed 2026-08-05 [T1]:
|
||||
// the kill / inflicted / received score reports to the shooter's and
|
||||
// victim's players. Sender lives in btplayer.cpp (complete-BTPlayer
|
||||
// TU, per the databinding rule); it receives the APPLIED tally, the
|
||||
// vital-wreck flag, and the loop-ENTRY zone. This retires
|
||||
// BTPostDamageScore/BTPostKillScore -- see the btplayer.cpp tombstone.
|
||||
//
|
||||
{
|
||||
extern void BTMechPostCombatReports(void *, void *, float, int, int, int);
|
||||
BTMechPostCombatReports((void *)this, (void *)message,
|
||||
(float)damageTally, zoneDestroyed, reportZone,
|
||||
deathBlastArmed && IsMechDestroyed());
|
||||
}
|
||||
}
|
||||
|
||||
// MP MATCH FORENSICS (matchlog.hpp): the victim-side authoritative damage
|
||||
@@ -1218,30 +1236,41 @@ void
|
||||
(float)message->damageData.impactPoint.z);
|
||||
}
|
||||
|
||||
// #89 DEATH BLAST (raw disasm @0x4a07b8-0x4a0bda -- the un-exported tail
|
||||
// of THIS handler; found by call-scanning Explosion::SplashDamage
|
||||
// @0042fad0, which has exactly TWO callers: Missile::Perform and here).
|
||||
// The binary, when the victim ENTERS dead(9)/eject(10) during the
|
||||
// applications above:
|
||||
// 1. self-dispatches SetBurningState (id 0x17, Damage payload, zone -1)
|
||||
// -- the burning wreck. DEFERRED here: our 0x17 handler is not yet
|
||||
// reconstructed; the port's wreck-fire visuals come from the death
|
||||
// effect layer meanwhile.
|
||||
// 2. spawns the death Explosion (Explosion::Make model 0x31 at the mech
|
||||
// origin) -- the port's kill path already fires the authored death
|
||||
// list visuals; not double-spawned.
|
||||
// 3. calls SplashDamage on it, gated on the owning player's
|
||||
// advancedDamageOn (+0x264) AND NOT suppressConsole (+0x258 -- set
|
||||
// by the eject bookkeeping: an EJECTED pilot's mech never blasts):
|
||||
// THE DEATH TAIL (@0x4a07b5-0x4a0bda, raw disasm -- the un-exported tail
|
||||
// of THIS handler). Gate [T1]: mech was ALIVE at entry ([ebp-0x10]) and
|
||||
// is dead(9)/eject(10) now. Sequence:
|
||||
// 1. Player::VehicleDeadMessage (id 0x17; the BT 0x38-byte extension
|
||||
// carrying killed-by player + kill zone) -> the mech's OWN player:
|
||||
// the respawn-cycle trigger. CORRECTED 2026-08-05: the #89 banner
|
||||
// previously read this build as "SetBurningState (mech id 0x17)" --
|
||||
// it is a PLAYER-table id: the binary's BTPlayer handler table (file
|
||||
// 0x112dxx) binds 0x17 -> @0x4c05c4 VehicleDeadMessageHandler; the
|
||||
// mech-table 0x17 (@0x49f674) is unrelated. Dispatched from HERE --
|
||||
// this is the decoded sender the mech4 transition site's
|
||||
// [T3 sender-undecoded] flag was waiting for.
|
||||
// 2. the death Explosion (id 3 MakeMessage, 0x5C bytes, model 0x31 at
|
||||
// the mech origin, @0x4a08bd) -- the port fires the authored per-mech
|
||||
// death list from the death transition instead (#42 history): same
|
||||
// once-per-death edge, not double-spawned.
|
||||
// 3. SplashDamage (#89), gated advancedDamageOn (+0x264) AND NOT
|
||||
// suppressConsole (+0x258 -- ejected pilots' mechs never blast):
|
||||
// Damage{type=2 Explosive, amount=deathSplashDamage@0x520,
|
||||
// impact=mech origin,
|
||||
// burstCount=round(0.001 * moverMass@0x20c * 15.0)}
|
||||
// radius=deathSplashRadius@0x524, excluded=the dying mech.
|
||||
// Splash falloff (bursts / dist^1.25) applies inside per victim.
|
||||
// 4. ForceUpdate(1). (A score/console post also rides this tail --
|
||||
// the deferred id-0x16 family, tracked with #134.)
|
||||
// radius=deathSplashRadius@0x524, excluded=the dying mech;
|
||||
// falloff bursts/dist^1.25 per victim inside the shared core.
|
||||
// 4. ForceUpdate(1).
|
||||
// The gate predicate here is IsMechDestroyed() (graphicAlarm >= 9, the
|
||||
// structural flag @0x49fb54) -- the binary tests movementMode 9|10, but
|
||||
// the death transition sets mode 9 synchronously with the structural flag
|
||||
// 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())
|
||||
{
|
||||
extern void BTMechPostVehicleDead(void *, void *, int);
|
||||
BTMechPostVehicleDead((void *)this, (void *)message, reportZone);
|
||||
|
||||
extern void BTApplyDeathSplash(void *mech_v);
|
||||
BTApplyDeathSplash((void *)this);
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -740,18 +740,12 @@ void
|
||||
target->Dispatch(&message); // no manager: direct (bring-up)
|
||||
}
|
||||
|
||||
// PORT bookkeeping (score lived in the retired mech4 fire block): credit
|
||||
// the delivered amount to the shooter-side score exactly as before.
|
||||
extern void BTPostDamageScore(Entity *victim, Scalar damage, Entity *shooter);
|
||||
extern Logical BTIsRegisteredMech(Entity *e);
|
||||
if (BTIsRegisteredMech(target)
|
||||
&& !((Mech *)target)->IsMechDestroyed())
|
||||
{
|
||||
// Direct-fire damageData always authors burstCount = 1 (emitter.cpp:355),
|
||||
// so the plain amount IS the delivered amount here. The shooter param
|
||||
// lets the bridge refuse AI-master fire (score is the shooter's, #45).
|
||||
BTPostDamageScore(target, damageData.damageAmount, (Entity *)mech);
|
||||
}
|
||||
// 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; kills + received-damage penalties only).
|
||||
extern Logical BTIsRegisteredMech(Entity *e); // still read by the matchlog below
|
||||
|
||||
// MP MATCH FORENSICS (matchlog.hpp): the shooter-side per-shot damage
|
||||
// submission -- pairs with the victim's DMG application line. mech=1
|
||||
|
||||
Reference in New Issue
Block a user