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:
co-authored by
Claude Opus 4.8
parent
a9467481c5
commit
7c455303bd
@@ -194,14 +194,8 @@ void Mech::Reset(const Origin &origin, int /*mode*/)
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
//===========================================================================//
|
||||
// Mech__DamageZone method stub.
|
||||
//===========================================================================//
|
||||
|
||||
// TODO(bring-up): stream in the per-zone critical-subsystem plug list.
|
||||
void Mech__DamageZone::LoadCriticalSubsystems(MemoryStream * /*stream*/)
|
||||
{
|
||||
}
|
||||
// (Mech__DamageZone::LoadCriticalSubsystems was a no-op stub here; it is now the
|
||||
// real type-0x1e damage-state descriptor-table loader in mechdmg.cpp.)
|
||||
|
||||
//===========================================================================//
|
||||
// MechSubsystem method stubs.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -843,6 +843,36 @@ static void
|
||||
}
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
// MechDeathHandler effect-spawn bridge (mechdmg.cpp calls this)
|
||||
//
|
||||
// Spawns a damage-state descriptor's explosion at the mech. The authentic path
|
||||
// (FUN_0043663c -> FUN_004364e4) broadcasts a class-5 message to the effect
|
||||
// manager app+0x38 (the 0xBD3 SubsystemMessageManager, unreconstructed); we use
|
||||
// the same established Explosion::Make port the weapon path uses. The binary
|
||||
// derives the position from the subsystem (mech+0x184); we use the mech origin.
|
||||
//###########################################################################
|
||||
void
|
||||
BTSpawnDamageEffect(Mech *mech, int effect_resource)
|
||||
{
|
||||
if (mech == 0)
|
||||
return;
|
||||
ResourceDescription::ResourceID res = (ResourceDescription::ResourceID)effect_resource;
|
||||
if (res <= 0)
|
||||
res = gExplodeRes; // fall back to the resolved generic explosion
|
||||
if (res <= 0)
|
||||
return; // nothing to spawn yet
|
||||
Origin o = mech->localOrigin; // at the mech (binary: per-subsystem)
|
||||
Explosion::MakeMessage m(
|
||||
Explosion::MakeMessageID, sizeof(Explosion::MakeMessage),
|
||||
(Entity::ClassID)RegisteredClass::ExplosionClassID, EntityID::Null,
|
||||
res, Explosion::DefaultFlags, o,
|
||||
mech->GetEntityID(), mech->GetEntityID());
|
||||
Explosion *e = Explosion::Make(&m);
|
||||
if (e)
|
||||
Register_Object(e);
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
// Death sequence -- the un-exported master-perf death branch (region
|
||||
// 0x4a9770-0x4ab188), reconstructed from its EXPORTED consumers + the RP
|
||||
@@ -920,6 +950,13 @@ void
|
||||
// dead mech keeps its death state instead of reverting to a live gait.
|
||||
UpdateDeathState();
|
||||
|
||||
// MechDeathHandler (@0042aa2c): per-frame destroyed-skin + explosion effects as
|
||||
// this mech's zones cross damage thresholds. The binary ticks it off the mech's
|
||||
// Performance list (mech+0xbc), which the bring-up drive override bypasses, so
|
||||
// it is driven here. Runs for EVERY mech (so the enemy visibly falls apart too).
|
||||
if (deathHandler != 0)
|
||||
((MechDeathHandler *)deathHandler)->Tick();
|
||||
|
||||
// WAVE 7 Phase B: advance/render/impact the flying projectiles once per frame (viewpoint).
|
||||
if (isPlayerMech && dt > 0.0001f)
|
||||
BTUpdateProjectiles(dt);
|
||||
|
||||
@@ -817,3 +817,155 @@ int
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
//#############################################################################
|
||||
// Damage-state descriptor table -- @0041e4a8 / @0042a748 / @0042a2c8
|
||||
//
|
||||
// Streams this zone's damage-state descriptor table from the type-0x1e resource
|
||||
// (binary this+0xd4). Each entry (0x1C bytes) is [f32 DamageLevel][i32
|
||||
// EffectResource][i32 GraphicState][f32 TimeDelay], written ascending by
|
||||
// DamageLevel. (Was a no-op stub -> the table was never built, so
|
||||
// MechDeathHandler had nothing to walk and death was invisible.)
|
||||
//#############################################################################
|
||||
void
|
||||
Mech__DamageZone::LoadCriticalSubsystems(MemoryStream *stream) // @0041e4a8
|
||||
{
|
||||
int count = 0;
|
||||
stream->ReadBytes(&count, 4); // FUN_0042a748: entry count
|
||||
damageDescriptors.reserve(count > 0 ? count : 0);
|
||||
for (int i = 0; i < count; ++i) // FUN_0042a2c8 per entry
|
||||
{
|
||||
DamageDescriptor d;
|
||||
stream->ReadBytes(&d.damageLevel, 4); // +0xc
|
||||
stream->ReadBytes(&d.effectResource, 4); // +0x14
|
||||
stream->ReadBytes(&d.graphicState, 4); // +0x10
|
||||
stream->ReadBytes(&d.timeDelay, 4); // +0x18
|
||||
damageDescriptors.push_back(d);
|
||||
}
|
||||
if (count != 0 && getenv("BT_DEATH_LOG"))
|
||||
DEBUG_STREAM << "[deathfx] zone descriptor table loaded: " << count
|
||||
<< " entries (first threshold=" << (count > 0 ? damageDescriptors[0].damageLevel : -1.0f)
|
||||
<< " effect=" << (count > 0 ? damageDescriptors[0].effectResource : -1) << ")\n" << std::flush;
|
||||
}
|
||||
|
||||
//
|
||||
// @0042a664 -- first entry whose threshold is strictly greater than 'level'
|
||||
// (0 if none / the zone is past the last threshold, i.e. fully destroyed).
|
||||
//
|
||||
const Mech__DamageZone::DamageDescriptor *
|
||||
Mech__DamageZone::DescriptorForLevel(Scalar level) const
|
||||
{
|
||||
for (unsigned i = 0; i < damageDescriptors.size(); ++i)
|
||||
if (damageDescriptors[i].damageLevel > level)
|
||||
return &damageDescriptors[i];
|
||||
return 0;
|
||||
}
|
||||
|
||||
//
|
||||
// @0042a5f4 -- true iff some entry's threshold lies in (oldLevel, newLevel]
|
||||
// (a damage-state threshold was crossed this tick).
|
||||
//
|
||||
Logical
|
||||
Mech__DamageZone::DescriptorCrossed(Scalar oldLevel, Scalar newLevel) const
|
||||
{
|
||||
for (unsigned i = 0; i < damageDescriptors.size(); ++i)
|
||||
{
|
||||
Scalar t = damageDescriptors[i].damageLevel;
|
||||
if (t > oldLevel && t <= newLevel)
|
||||
return True;
|
||||
}
|
||||
return False;
|
||||
}
|
||||
|
||||
//
|
||||
// @0042a6c4 -- first entry whose GraphicState enum == gstate (0 if none).
|
||||
//
|
||||
const Mech__DamageZone::DamageDescriptor *
|
||||
Mech__DamageZone::DescriptorForGraphicState(int gstate) const
|
||||
{
|
||||
for (unsigned i = 0; i < damageDescriptors.size(); ++i)
|
||||
if (damageDescriptors[i].graphicState == gstate)
|
||||
return &damageDescriptors[i];
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Inherited engine DamageZone::damageLevel (binary this+0x158) -- the [0,1]
|
||||
// structural damage MechDeathHandler compares against the descriptor thresholds.
|
||||
Scalar
|
||||
Mech__DamageZone::GetStructureDamageLevel() const
|
||||
{
|
||||
return damageLevel;
|
||||
}
|
||||
|
||||
void
|
||||
Mech__DamageZone::ApplyDamageGraphicState(int gstate)
|
||||
{
|
||||
SetGraphicState(gstate); // engine DamageZone::SetGraphicState -> destroyed skin
|
||||
}
|
||||
|
||||
//#############################################################################
|
||||
// MechDeathHandler -- @0042a984 / @0042aa2c / @0042a9f4
|
||||
//#############################################################################
|
||||
|
||||
// Effect-spawn bridge (mech4.cpp -- where Explosion::Make + the mech transform
|
||||
// live): spawns the descriptor's explosion resource at the mech. The authentic
|
||||
// path dispatches a class-5 message to the effect manager (app+0x38 -> the 0xBD3
|
||||
// SubsystemMessageManager, unreconstructed); we use the established Explosion port.
|
||||
extern void BTSpawnDamageEffect(Mech *mech, int effect_resource);
|
||||
|
||||
MechDeathHandler::MechDeathHandler(Mech *mech) // @0042a984
|
||||
: owner(mech)
|
||||
{
|
||||
// per-zone last-damage cache, zeroed (binary this[0x10], size mech+0x11c).
|
||||
int count = (mech != 0 && mech->damageZoneCount > 0) ? mech->damageZoneCount : 0;
|
||||
lastLevel.assign(count, 0.0f);
|
||||
}
|
||||
|
||||
MechDeathHandler::~MechDeathHandler() // @0042a9f4
|
||||
{
|
||||
}
|
||||
|
||||
//
|
||||
// @0042aa2c -- each tick, walk the zones; where a zone's damageLevel rose since
|
||||
// last tick, fire the current damage-band descriptor's explosion (binary +0xb8 &
|
||||
// 4) and, on destruction, the Destroyed-graphic descriptor's explosion (+0xb8 &
|
||||
// 8), applying that descriptor's GraphicState (the destroyed skin) to the zone.
|
||||
//
|
||||
void
|
||||
MechDeathHandler::Tick() // @0042aa2c
|
||||
{
|
||||
if (owner == 0)
|
||||
return;
|
||||
|
||||
int n = owner->damageZoneCount;
|
||||
for (int i = 0; i < n && i < (int)lastLevel.size(); ++i)
|
||||
{
|
||||
Mech__DamageZone *zone = owner->Zone(i);
|
||||
if (zone == 0)
|
||||
continue;
|
||||
|
||||
Scalar level = zone->GetStructureDamageLevel();
|
||||
Scalar prev = lastLevel[i];
|
||||
if (level <= prev) // no new damage on this zone
|
||||
continue;
|
||||
lastLevel[i] = level;
|
||||
|
||||
const Mech__DamageZone::DamageDescriptor *d = zone->DescriptorForLevel(level);
|
||||
if (level >= 1.0f && prev < 1.0f) // just destroyed -> the Destroyed descriptor
|
||||
{
|
||||
const Mech__DamageZone::DamageDescriptor *dd =
|
||||
zone->DescriptorForGraphicState(DamageZone::DestroyedGraphicState); // enum 1
|
||||
if (dd != 0)
|
||||
d = dd;
|
||||
}
|
||||
if (d != 0)
|
||||
{
|
||||
BTSpawnDamageEffect(owner, d->effectResource); // explosion at the zone
|
||||
zone->ApplyDamageGraphicState(d->graphicState); // destroyed skin
|
||||
if (getenv("BT_DEATH_LOG"))
|
||||
DEBUG_STREAM << "[deathfx] zone " << i << " level " << level
|
||||
<< " -> effect " << d->effectResource << " gstate " << d->graphicState
|
||||
<< "\n" << std::flush;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
#if !defined(MECHDMG_HPP)
|
||||
# define MECHDMG_HPP
|
||||
|
||||
#include <vector>
|
||||
|
||||
# if !defined(DAMAGE_HPP)
|
||||
# include <damage.hpp>
|
||||
# endif
|
||||
@@ -140,6 +142,7 @@
|
||||
// damage code (FUN_004a0230 / FUN_0049fb54 etc.) is a Mech method touching
|
||||
// these zone fields directly.
|
||||
friend class Mech;
|
||||
friend class MechDeathHandler; // walks the descriptor table + applies the destroyed skin
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// LOD Support
|
||||
//
|
||||
@@ -248,6 +251,36 @@
|
||||
MechCriticalSubsystem
|
||||
**criticalSubsystems; // @0x1b4 array[criticalSubsystemCount]
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Damage-state descriptor table (binary this+0xd4). Streamed from the
|
||||
// type-0x1e resource by LoadCriticalSubsystems (FUN_0041e4a8 -> FUN_0042a748);
|
||||
// as this zone's damageLevel crosses each entry's threshold, MechDeathHandler
|
||||
// fires the entry's explosion + applies its GraphicState (destroyed skin).
|
||||
// Entries ascend by damageLevel. (Was un-built -- the loader was a stub.)
|
||||
//
|
||||
public:
|
||||
struct DamageDescriptor // binary entry 0x1C (ctor FUN_0042a2c8)
|
||||
{
|
||||
Scalar damageLevel; // +0xc threshold [0,1]
|
||||
int graphicState; // +0x10 GraphicState enum (Destroyed=1)
|
||||
int effectResource; // +0x14 ExplosionModelFile resource id
|
||||
Scalar timeDelay; // +0x18
|
||||
};
|
||||
// @0042a664: first entry whose threshold > level (0 if none / fully destroyed).
|
||||
const DamageDescriptor *DescriptorForLevel(Scalar level) const;
|
||||
// @0042a5f4: true iff some entry's threshold lies in (oldLevel, newLevel].
|
||||
Logical DescriptorCrossed(Scalar oldLevel, Scalar newLevel) const;
|
||||
// @0042a6c4: first entry whose GraphicState == gstate (0 if none).
|
||||
const DamageDescriptor *DescriptorForGraphicState(int gstate) const;
|
||||
Scalar GetStructureDamageLevel() const; // inherited damageLevel (binary +0x158)
|
||||
// Apply a descriptor's GraphicState (Destroyed=1) to this zone -- the
|
||||
// destroyed-skin swap the renderer reads (public wrapper so MechDeathHandler
|
||||
// can drive it; the base SetGraphicState is otherwise unreachable from it).
|
||||
void ApplyDamageGraphicState(int gstate);
|
||||
protected:
|
||||
std::vector<DamageDescriptor>
|
||||
damageDescriptors; // binary this+0xd4 collection
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Graphic State Support
|
||||
//
|
||||
@@ -297,4 +330,30 @@
|
||||
);
|
||||
};
|
||||
|
||||
//###########################################################################
|
||||
//###########################################################################
|
||||
// MechDeathHandler (@0042a984 / @0042aa2c)
|
||||
//
|
||||
// A per-frame handler built by the Mech ctor (cached at mech[0x214]) that turns
|
||||
// subsystem/zone damage into VISIBLE destruction: each tick it walks the mech's
|
||||
// damage zones and, as a zone's damageLevel crosses a descriptor threshold,
|
||||
// spawns that entry's explosion effect at the zone + applies its GraphicState
|
||||
// (the destroyed skin). The binary registers it on the mech's Performance list
|
||||
// (mech+0xbc); the bring-up drive override does not tick that list, so
|
||||
// Mech::PerformAndWatch drives Tick() directly. Effect spawn uses the
|
||||
// established Explosion::Make port (the authentic effect-message manager, the
|
||||
// 0xBD3 SubsystemMessageManager, is unreconstructed) -- see BTSpawnDamageEffect.
|
||||
//###########################################################################
|
||||
class MechDeathHandler
|
||||
{
|
||||
public:
|
||||
MechDeathHandler(Mech *mech); // @0042a984
|
||||
~MechDeathHandler(); // @0042a9f4
|
||||
void Tick(); // @0042aa2c (the Performance)
|
||||
|
||||
private:
|
||||
Mech *owner; // @0x14
|
||||
std::vector<Scalar> lastLevel; // @0x10 per-zone last damageLevel cache
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user