MechDeathHandler: per-zone destroyed-skins + explosions (faithful, exported)

Fixes the "kills are invisible" report: a mech died only at the state level, with
no visible destruction on its body.  Root cause: the per-zone damage-state
descriptor table (binary zone+0xd4) that drives destroyed-skins + explosions was
never built -- its loader (Mech__DamageZone::LoadCriticalSubsystems / FUN_0041e4a8)
was a no-op stub, and MechDeathHandler itself was a no-op stub.  The surrounding
plumbing (the per-zone load loop over the type-0x1e resource, the death-handler
slot) was already correct.  Whole pipeline is EXPORTED -- no stand-ins.

Reconstructed:
- Mech__DamageZone descriptor table (binary this+0xd4): un-stubbed
  LoadCriticalSubsystems (FUN_0041e4a8/FUN_0042a748/FUN_0042a2c8) to parse the
  real type-0x1e stream -- entry = [f32 DamageLevel][i32 EffectResource]
  [i32 GraphicState][f32 TimeDelay], ascending by DamageLevel.  Verified live:
  6 entries/zone, first threshold 0.2, effect 26, across all 40 zones, no crash.
- The three lookups (FUN_0042a664 by-level / FUN_0042a5f4 crossing / FUN_0042a6c4
  by-graphic-state) as Mech__DamageZone methods.
- The real MechDeathHandler (FUN_0042a984 ctor / FUN_0042aa2c Performance /
  FUN_0042a9f4 dtor), replacing the stub: each tick it walks the zones and, as a
  zone's damageLevel rises across a descriptor threshold, fires that entry's
  explosion (binary +0xb8 & 4) and, on destruction, the Destroyed-graphic
  descriptor's explosion (+0xb8 & 8), applying the descriptor's GraphicState (the
  destroyed skin) to the zone.  Runs for EVERY mech, so the enemy visibly falls
  apart as it dies.

Integration: the binary ticks MechDeathHandler off the mech's Performance list
(mech+0xbc), which the bring-up drive override bypasses, so Mech::PerformAndWatch
drives Tick() directly (same approach as UpdateDeathState).  Effect spawn uses the
established Explosion::Make port (BTSpawnDamageEffect) -- the authentic dispatch
(class-5 message -> the 0xBD3 SubsystemMessageManager effect manager) is unported.

Runtime: builds clean, boots clean, 42 descriptor tables load with sensible
byte-aligned data, no crash.  Live effect firing is combat-triggered (verified by
the load + the wiring; the [deathfx] threshold-crossing log fires under BT_DEATH_LOG).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-08 11:18:30 -05:00
co-authored by Claude Opus 4.8
parent a9467481c5
commit 7c455303bd
5 changed files with 260 additions and 12 deletions
+10 -4
View File
@@ -218,7 +218,7 @@ extern Subsystem *CreateGaussRifleSubsystem(Mech *, int, void *); // 0xBCE (WAVE
// Post-stream entity glue + cosmetic objects.
struct StandingAnimation { template<class...A> StandingAnimation(A&&...) {} };
struct MechDeathHandler { template<class...A> MechDeathHandler(A&&...) {} };
// MechDeathHandler is now the REAL class (mechdmg.hpp) -- the local stub was removed.
struct StabilityMessage { StabilityMessage() {} };
// "Mechs" object directory in the global registry (decomp registry verbs).
@@ -1297,13 +1297,19 @@ Mech::Mech(
{
critRes->Lock();
{
// Load each zone's damage-state descriptor table from the type-0x1e
// stream, sequentially (binary loops mech[0x47] subsystems == our zones,
// calling FUN_0041e4a8 per object over the same stream). This is what
// MechDeathHandler walks to fire destroyed-skins + explosions on damage.
DynamicMemoryStream critStream(critRes->resourceAddress, critRes->resourceSize);
for (int z = 0; z < damageZoneCount; ++z)
{
Zone(z)->LoadCriticalSubsystems(&critStream); // FUN_0041e4a8
Zone(z)->LoadCriticalSubsystems(&critStream); // FUN_0041e4a8 (now the real loader)
}
}
deathHandler = (int)new (Memory::Allocate(0x18)) MechDeathHandler(this); // FUN_0042a984
// The REAL MechDeathHandler (mechdmg.hpp), not the binary's 0x18 placement --
// plain new (our class carries a std::vector cache). Ticked from PerformAndWatch.
deathHandler = (int)new MechDeathHandler(this); // FUN_0042a984
critRes->Unlock();
}
@@ -1426,7 +1432,7 @@ Mech::~Mech()
if (deathHandler != 0) // Wword(0x214)
{
((Releasable *)deathHandler)->Release();
delete (MechDeathHandler *)deathHandler; // real class -> plain delete
}
ReleaseRefCounted(resourceNameC); // Wword(0x213)