#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)
|
||||
|
||||
Reference in New Issue
Block a user