diff --git a/game/reconstructed/btplayer.cpp b/game/reconstructed/btplayer.cpp index 9f45193..a642be1 100644 --- a/game/reconstructed/btplayer.cpp +++ b/game/reconstructed/btplayer.cpp @@ -1863,6 +1863,14 @@ int BTPlayerAdvancedDamageOn(void *player_v) return (player_v != 0) ? ((BTPlayer *)player_v)->advancedDamageOn2 : 0; } +// #89 death blast, second gate: suppressConsole (+0x258) is SET by the eject +// bookkeeping -- an EJECTED pilot's abandoned mech never blast-splashes +// (raw disasm @0x4a0ac8-0x4a0ad6). +int BTPlayerConsoleSuppressed(void *player_v) +{ + return (player_v != 0) ? (int)((BTPlayer *)player_v)->suppressConsole : 0; +} + int BTPilotKills(void *pilot) { int k = pilot ? ((BTPlayer *)pilot)->GetKillCount() : 0; diff --git a/game/reconstructed/btplayer.hpp b/game/reconstructed/btplayer.hpp index 64b4a5a..c3242eb 100644 --- a/game/reconstructed/btplayer.hpp +++ b/game/reconstructed/btplayer.hpp @@ -438,6 +438,7 @@ class DropZone__ReplyMessage; friend int BTPlayerExperienceHeatModelOn(void *); // +0x260 reader (issue #2): FUN_004ad7d4 heat-model gate friend int BTPlayerAdvancedDamageOn(void *); // +0x268 reader (#83): FUN_0049ffcc collision-rattle gate friend Scalar BTPlayerEjectBookkeeping(void *); // +0x258 latch + role killBonus (Mech::EjectPilot @0049f854) + friend int BTPlayerConsoleSuppressed(void *); // +0x258 reader (#89): death-blast eject gate @0x4a0ace // TARGET DESIGNATION (Gitea #48/#57): the mapper used to write the target // RAW at `pilot + 0x284` -- the BINARY's objectiveMech offset, which on our // compiled object is `deathPending` (the respawn latch). These bridges diff --git a/game/reconstructed/mech.cpp b/game/reconstructed/mech.cpp index e8062e2..06d3fa3 100644 --- a/game/reconstructed/mech.cpp +++ b/game/reconstructed/mech.cpp @@ -1126,6 +1126,11 @@ 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) { @@ -1212,6 +1217,34 @@ void (float)message->damageData.impactPoint.y, (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): + // 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.) + if (deathBlastArmed && IsMechDestroyed()) + { + extern void BTApplyDeathSplash(void *mech_v); + BTApplyDeathSplash((void *)this); + } } // @@ -1898,8 +1931,11 @@ Mech::Mech( Wword(0x68) = model->cameraOffset; // FUN_00408440(this+0x1a0, rec+0xA8) Wword(0x130) = model->deathEffectResourceID; // rec+0x74 -> mech+0x4c0 - Wword(0x148) = model->deathSplashDamage; // rec+0x78 -> mech+0x520 - Wword(0x149) = model->deathSplashRadius; // rec+0x7C -> mech+0x524 + deathSplashDamage = model->deathSplashDamage; // rec+0x78 -> mech+0x520 (#89: + deathSplashRadius = model->deathSplashRadius; // rec+0x7C -> mech+0x524 named + // members -- the Wword bank is + // GLOBAL and clobbered per-mech + // values chassis-to-chassis) Wword(0x195) = Wword(0x196) = 0; Wword(0xfd) = 0; masterAlarm.SetLevel(0); // FUN_0041bbd8(this+0xe7,0) diff --git a/game/reconstructed/mech.hpp b/game/reconstructed/mech.hpp index e9c8e85..6544e3a 100644 --- a/game/reconstructed/mech.hpp +++ b/game/reconstructed/mech.hpp @@ -654,6 +654,12 @@ public: // binary part_012.c:9938-9940 + 9974-9975). By-name access only; declared // after the layout-locked fields so nothing shifts. Scalar standingTemplateMaxY; // binary @0x518 collisionTemplate->maxY at ctor + // #89 DEATH BLAST (raw disasm @0x4a07b8-0x4a0bda): the authored death- + // explosion pair, model "gamedata" keys -> ctor. PROMOTED from the Wword + // scratch bank (the bank is one GLOBAL array -- per-mech authored values + // were silently clobbered to the last-loaded chassis). + Scalar deathSplashDamage; // binary @0x520 <- model rec+0x78 "DeathSplashDamage" + Scalar deathSplashRadius; // binary @0x524 <- model rec+0x7C "DeathSplashRadius" Scalar duckedTemplateMaxY; // binary @0x51c 0.6 x standing (duck preset) Scalar templateBottomLift; // binary @0x4b8 0.05 x (volume maxX-minX) diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 4024ccf..5eb34e3 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -1235,6 +1235,10 @@ static Scalar return radius; } +static void + BTSplashCore(Entity *shooter, const Point3D ¢er, Entity *directVictim, + const Damage &dmgTemplate, Scalar radius); // #89 shared core (below) + void BTApplySplashDamage(Entity *shooter, int weapon_subsys, const Point3D ¢er, Entity *directVictim, const Damage &dmgTemplate) @@ -1246,7 +1250,18 @@ void << " radius=" << radius << "\n" << std::flush; if (radius <= 0.0f) return; // this weapon authors no splash + BTSplashCore(shooter, center, directVictim, dmgTemplate, radius); +} +// +// #89: the splash CORE, shared by the weapon-detonation caller above and the +// DEATH BLAST below (the binary's two Explosion::SplashDamage call sites). +// +static void + BTSplashCore(Entity *shooter, const Point3D ¢er, Entity *directVictim, + const Damage &dmgTemplate, Scalar radius) +{ + static const int s_log = getenv("BT_SPLASH_LOG") ? 1 : 0; extern int BTIsRegisteredMech(Entity *e); extern int BTGetTargetCandidates(Entity *shooter, Entity **out, int maxOut); Entity *cand[32]; @@ -1322,6 +1337,71 @@ void } } +// +// #89 DEATH BLAST -- the second authentic SplashDamage call site (raw disasm +// @0x4a07b8-0x4a0bda, the un-exported TakeDamage tail; provenance banner at +// the caller, mech.cpp). A dying mech's explosion damages every mech inside +// its authored DeathSplashRadius: +// gates : owning player's advancedDamageOn (+0x264) AND +// NOT suppressConsole (+0x258 -- eject sets it: punch-outs never +// blast) [T1 @0x4a0aa8-0x4a0ad6] +// damage : type 2 Explosive [T1 @0x4a0aef], amount = deathSplashDamage +// (mech+0x520, authored "DeathSplashDamage"), impact = mech origin +// bursts : round(0.001 * moverMass(+0x20c) * 15.0) [T1 @0x4a0b21-0x4a0b33] +// -- the blast scales with the chassis mass; the per-victim +// falloff (bursts/dist^1.25) applies in the core +// radius : deathSplashRadius (mech+0x524, authored "DeathSplashRadius") +// excluded: the dying mech itself (the binary's Explosion anchors on it; +// the core's candidate walk already excludes the shooter) +// +void BTApplyDeathSplash(void *mech_v) +{ + Mech *m = (Mech *)mech_v; + static const int s_log = getenv("BT_SPLASH_LOG") ? 1 : 0; + if (m == 0) + return; + extern int BTPlayerAdvancedDamageOn(void *player_v); + extern int BTPlayerConsoleSuppressed(void *player_v); + void *player = m->GetPlayerLink(); + if (player == 0) + { + // binary derefs mech+0x190 unguarded (every arcade mech had an owner); + // the port's link-less solo dummy skips, loudly under the log. + if (s_log) + DEBUG_STREAM << "[splash] DEATH: no player link, skipped\n" << std::flush; + return; + } + if (!BTPlayerAdvancedDamageOn(player) || BTPlayerConsoleSuppressed(player)) + { + if (s_log) + DEBUG_STREAM << "[splash] DEATH: gated (advDmg=" + << BTPlayerAdvancedDamageOn(player) + << " suppress=" << BTPlayerConsoleSuppressed(player) + << ")\n" << std::flush; + return; + } + if (!(m->deathSplashRadius > 0.0f)) + return; // chassis authors no blast + + Damage dmg; // Damage::Damage @0x41db7c + dmg.damageType = (Enumeration)2; // Explosive + dmg.damageAmount = m->deathSplashDamage; + dmg.impactPoint = m->localOrigin.linearPosition; + int bursts = (int)(0.5f + 0.001f * (float)m->moverMass * 15.0f); + if (bursts < 1) bursts = 1; + dmg.burstCount = bursts; + + if (s_log) + DEBUG_STREAM << "[splash] DEATH BLAST mech=" << m->GetEntityID() + << " radius=" << (float)m->deathSplashRadius + << " amount=" << (float)m->deathSplashDamage + << " bursts=" << bursts + << " (mass=" << (float)m->moverMass << ")\n" << std::flush; + + BTSplashCore((Entity *)m, m->localOrigin.linearPosition, (Entity *)m, + dmg, m->deathSplashRadius); +} + // Called from ProjectileWeapon / MissileLauncher::FireWeapon (via the extern below) // with the live muzzle, the SHOOTER mech (to resolve the real launch port), the owner's // locked target entity + point, the launch speed (|muzzleVelocity|), and per-shot damage. diff --git a/scratchpad/night12/deathblast.sh b/scratchpad/night12/deathblast.sh new file mode 100644 index 0000000..45406e2 --- /dev/null +++ b/scratchpad/night12/deathblast.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# #89 DEATH-BLAST verify: B walks to ~12u of A and stands; A self-destructs +# (fast self-damage); A's death blast must splash B (Explosive, burst-scaled, +# radius-gated). PASS: A log [splash] DEATH BLAST + SPLASH matchlog to B; +# B log [dmghit] type=2 from A after A's death. advancedDamage=1 rides the +# expert egg already. +set -x +. /c/git/bt411/scratchpad/night6/bench_common.sh +cd /c/git/bt411/content || exit 1 +taskkill //F //IM btl4.exe > /dev/null 2>&1 +sleep 2 +rm -f db_a.log db_b.log +bt_expert_egg MP.EGG DB.EGG +sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" DB.EGG + +( + export BT_SELF_DAMAGE=40 BT_SPLASH_LOG=1 + export BT_DMG_LOG=1 BT_DEATH_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1 + bt_launch db_a.log DB.EGG 0x0C -net 1601 +) +sleep 2 +( + export BT_GOTO=enemy BT_GOTO_STOP=12 BT_KEY_NOFOCUS=1 BT_SPLASH_LOG=1 + export BT_DMG_LOG=1 BT_DEATH_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1 + bt_launch db_b.log DB.EGG 0x03 -net 1501 +) +sleep 5 +python ../tools/btconsole.py DB.EGG 127.0.0.1:1501 127.0.0.1:1601 > db_relay.log 2>&1 & +RELAY=$! +sleep 120 +kill $RELAY 2>/dev/null +taskkill //F //IM btl4.exe > /dev/null 2>&1 +sleep 3 +echo "=== A death + blast:" +grep -aE "death cycle START|DEATH BLAST|\[splash\]" db_a.log | head -8 +echo "=== B splash received (type=2 from A):" +grep -a "dmghit" db_b.log | grep "type=2" | head -6 +echo "=== SPLASH matchlog on A:" +grep -a "SPLASH" db_a.log | head -3 diff --git a/scratchpad/night12/deathblast2.sh b/scratchpad/night12/deathblast2.sh new file mode 100644 index 0000000..f6d0633 --- /dev/null +++ b/scratchpad/night12/deathblast2.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# #89 DEATH-BLAST verify: B walks to ~12u of A and stands; A self-destructs +# (fast self-damage); A's death blast must splash B (Explosive, burst-scaled, +# radius-gated). PASS: A log [splash] DEATH BLAST + SPLASH matchlog to B; +# B log [dmghit] type=2 from A after A's death. advancedDamage=1 rides the +# expert egg already. +set -x +. /c/git/bt411/scratchpad/night6/bench_common.sh +cd /c/git/bt411/content || exit 1 +taskkill //F //IM btl4.exe > /dev/null 2>&1 +sleep 2 +rm -f db2_a.log db2_b.log +bt_expert_egg MP.EGG DB2.EGG +sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" DB2.EGG + +( + export BT_SELF_DAMAGE=5 BT_SPLASH_LOG=1 + export BT_DMG_LOG=1 BT_DEATH_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1 + bt_launch db2_a.log DB2.EGG 0x0C -net 1601 +) +sleep 2 +( + export BT_GOTO=enemy BT_GOTO_STOP=12 BT_KEY_NOFOCUS=1 BT_SPLASH_LOG=1 + export BT_DMG_LOG=1 BT_DEATH_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1 + bt_launch db2_b.log DB2.EGG 0x03 -net 1501 +) +sleep 5 +python ../tools/btconsole.py DB2.EGG 127.0.0.1:1501 127.0.0.1:1601 > db2_relay.log 2>&1 & +RELAY=$! +sleep 190 +kill $RELAY 2>/dev/null +taskkill //F //IM btl4.exe > /dev/null 2>&1 +sleep 3 +echo "=== A death + blast:" +grep -aE "death cycle START|DEATH BLAST|\[splash\]" db2_a.log | head -8 +echo "=== B splash received (type=2 from A):" +grep -a "dmghit" db2_b.log | grep "type=2" | head -6 +echo "=== SPLASH matchlog on A:" +grep -a "SPLASH" db2_a.log | head -3