Death-state sequence: collapse + subsystem shutdown + freeze (wreck stays)

Reconstructs the mech DEATH state machine — the un-exported master-perf death
branch (region 0x4a9770-0x4ab188) — from its EXPORTED consumers + the RP
VTV::DeathShutdown analog, wired into the active path (the bring-up drive
override bypasses the authentic Simulate where this normally lives).

Mech::UpdateDeathState() + IsMechDestroyed() (mech4.cpp), called for every mech
early in PerformAndWatch: on a vital kill (graphicAlarm >= 9, raised by the
damage side) it
  1. sets movementMode = 5 -> the collapse clip's one-shot latch in
     AdvanceBodyAnimation (fall direction 5-8 is un-exported -> 5 [T3]);
  2. loops the roster calling Subsystem::DeathShutdown(1) (RP VTV::DeathShutdown
     analog; the base is a no-op virtual, overrides act -- a SHUTDOWN not a
     teardown, so it frees nothing and never removes the entity: the wreck STAYS);
  3. next frame settles to movementMode = 9 -> IsDisabled -> locomotion frozen.
The drive's lone `movementMode = 1` write (mech4.cpp ~1309) is guarded on
!IsMechDestroyed so the death state is not clobbered back to a live gait.

Runtime-verified (forced kill, BT_DEATH_LOG=1):
  [death] mech destroyed -> collapse + subsystem shutdown (wreck stays)
  [death] mech settled -> disabled (IsDisabled=1, frozen wreck)
mech frozen in place, subsystem tick + renderer keep running, no crash, wreck
stays.

Also records the full death-sequence decomp map in combat-damage.md (the
exported consumers: AdvanceLeg/BodyAnimation collapse latch, IsDestroyed, the
MechDeathHandler effect engine; and the un-exported orchestration gap).

Deferred (honest): the visible collapse ANIMATION latch through the active path
(gait SM shows state=0 post-death; spawned mechs don't advance body anim);
MechDeathHandler (FUN_0042a984/FUN_0042aa2c -- the exported per-subsystem
destroyed-skin + explosion engine, still a stub) for death explosions; the
whole-mech DeathSplash radius damage (un-exported).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-08 09:30:34 -05:00
co-authored by Claude Opus 4.8
parent 2af401eef8
commit a9467481c5
4 changed files with 121 additions and 3 deletions
+45
View File
@@ -170,6 +170,51 @@ thing: an EXPLICIT `JointedMover::~JointedMover()` in the reconstructed `~Mech`
chain TWICE (the dtor-epilogue rule, [[reconstruction-gotchas]] §7) → a double-free of `collisionLists`
+ the segment table. Removing it fixed BOTH the death-row crash AND app-exit crashes. [T2]
## Death SEQUENCE reconstruction — the map (2026-07-08)
Full decomp map of the death path. Two tiers:
**EXPORTED (reconstructable) [T1 read]:**
- `Mech::AdvanceLegAnimation` (`FUN_004a5028`) / `AdvanceBodyAnimation` (`FUN_004a5678`) — the **collapse-anim
latch**: `movementMode` (mech+0x40) **5/6/7/8** = the four fall directions → clips **0x1c-0x1f** (via
`SetLegAnimation`/`SetBodyAnimation` = `FUN_004a7fc4`/`FUN_004a800c`), one-shot via `deathAnimationLatched`
(mech+0x650). Already reconstructed (mech2.cpp:550-557/817-822). The active drive path DOES call
`AdvanceBodyAnimation` (mech4.cpp:317) → reachable.
- `Mech::IsDestroyed()` (`FUN_0049fb54`) = `movementMode==2||9` (reconstructed as `IsDisabled`, btstubs.cpp);
`FUN_004ab1c8` freezes locomotion (zeros mech+0x298) when destroyed.
- **`MechDeathHandler`** (ctor `FUN_0042a984` + Performance `FUN_0042aa2c`, cached mech+0x850 / `mech[0x214]`) —
the **per-subsystem destroyed-skin + explosion engine**. Each tick walks the damage subsystems
(mech+0x120[mech+0x11c]); as a subsystem's damage crosses a descriptor-table threshold (`FUN_0042a5f4`
detects the crossing, `FUN_0042a664` looks up the state) it applies the `GraphicState` ("Destroyed" = enum 1,
parsed @part_003.c:9099) to the subsystem graphic (`sub+0xd4`) AND dispatches an effect-creation message
(`FUN_0043663c`→`FUN_004364e4` to the effect mgr `app+0x38`). **Fully exported but a no-op STUB** in the
recon (mech.cpp:221). Gamedata `DeathSplashDamage`/`Radius`/`Effect` load into a descriptor @ +0x74/78/7c
(`FUN_004a2da8`). Destroyed-skin variant set (`destroyed`, `destroyeds`…`destroyedd`) @part_003.c:5720.
**NOT EXPORTED (master-perf gap 0x4a9770-0x4ab188 + the un-exported TakeDamage) [T4]:**
- The `movementMode` WRITER (sets 5-8 then 2/9 on death) + the fall-direction (5/6/7/8) selection.
- The `Mech::DeathShutdown` roster loop (RP `VTV::DeathShutdown` analog — loop subsystems calling the
`Subsystem::DeathShutdown(int)` vtable slot; the base is an empty virtual, SUBSYSTM.h:166).
- The whole-mech `DeathEffect`/`DeathSplash` radius dispatch + the `graphicAlarm.SetLevel(9)` call site (the
recon already raises the alarm in mechdmg.cpp:426/586).
**The problem:** the orchestration lives in the per-frame Simulate that the **bring-up drive override
bypasses** (btstubs.cpp:116-119), so the death consumer must be reconstructed from its exported consumers +
the RP analog and wired into the active path (mech4.cpp `PerformAndWatch`).
### ✅ Death-STATE core DONE (2026-07-08) [T2]
`Mech::UpdateDeathState()` + `IsMechDestroyed()` (mech4.cpp) reconstruct the un-exported death branch from
its exported consumers + the RP analog, called for EVERY mech early in `PerformAndWatch`: on `graphicAlarm
>= 9` it (1) sets `movementMode = 5` → the collapse clip's latch in `AdvanceBodyAnimation` (fall direction
5-8 un-exported → 5 [T3]); (2) loops the roster calling `Subsystem::DeathShutdown(1)` (RP `VTV::DeathShutdown`
analog; base is a no-op virtual, a SHUTDOWN not teardown → safe, wreck STAYS); (3) next frame settles to
`movementMode = 9` → `IsDisabled` → locomotion frozen. The drive's lone `movementMode = 1` write (mech4.cpp
~1309) is guarded on `!IsMechDestroyed` so the death state isn't clobbered. **Runtime-verified** (forced kill):
`[death] … collapse + subsystem shutdown` → `[death] … settled -> disabled (IsDisabled=1, frozen wreck)`,
mech frozen in place (pos unchanged), subsystem tick + renderer keep running, **no crash, wreck stays**.
Env gate `BT_DEATH_LOG=1`.
**Deferred (honest):** (a) the visible **collapse ANIMATION** may not latch through the active path — the
gait SM showed `state=0` post-death (the clip trigger via `AdvanceBodyAnimation` needs the death `movementMode`
to reach its latch; and spawned NON-player mechs don't advance their body anim at all). (b) **`MechDeathHandler`**
(the exported per-subsystem destroyed-skin + explosion engine, `FUN_0042a984`/`FUN_0042aa2c`) is still the
no-op stub → no death explosions/skin-swaps yet. (c) the whole-mech **DeathSplash** radius damage (un-exported).
## Key Relationships
- Weapons/roster: [[subsystems]]. Aim source: [[locomotion]] (drive/facing). Effects: [[rendering]].
- P5 forensics: `docs/HARD_PROBLEMS.md`. Data: [[decomp-reference]] §4-5.
+9 -2
View File
@@ -98,8 +98,15 @@ authentic path scoped.
container ctors + the lookup as a `BTCylinderResolveZone` bridge (ground the cell zone-dict stream format vs
the real .RES bytes); 6d register the handler + verify. Feasible; the only open risk is the cell stream format.
The earlier-cited `FUN_004a0230`/`FUN_0049ed0c` don't exist.
- Death: reconstruct `DeathShutdown` (RP `VTV::DeathShutdown` analog) + collapse anim + destroyed
skin. The wreck-stays behavior we ship is faithful; do NOT issue DestroyEntityMessage on death.
- **Death sequence — CORE DONE 2026-07-08 [T2]; effects+anim deferred.** `Mech::UpdateDeathState()`
(mech4.cpp) reconstructs the death STATE machine (collapse `movementMode` → RP `VTV::DeathShutdown`
subsystem loop → settle to disabled/frozen; wreck stays, no crash) — runtime-verified (`BT_DEATH_LOG`).
Full map + citations in [[combat-damage]] "Death SEQUENCE". **Remaining** (all in the un-exported master-perf
gap 0x4a9770-0x4ab188 OR a big exported reconstruction): (a) the visible **collapse ANIMATION** latch
through the active path (+ spawned mechs don't advance body anim); (b) reconstruct **`MechDeathHandler`**
(`FUN_0042a984`/`FUN_0042aa2c`) — the EXPORTED per-subsystem destroyed-skin + explosion engine (still a
stub, mech.cpp:221) — the biggest visible payoff; (c) the whole-mech **DeathSplash** radius damage. Do NOT
issue DestroyEntityMessage on death.
## Multiplayer (Phase 7 / P6)
- Cross-pod COMBAT (target a replicant + route damage to the owning master).
+4
View File
@@ -667,6 +667,10 @@ protected:
Scalar AdvanceBodyAnimationAirborne(Scalar time_slice, int loop);
Scalar AdvanceLegAnimationAirborne(Scalar time_slice);
Logical IsDisabled(); // FUN_0049fb54
// --- death sequence (the un-exported master-perf death branch, rebuilt
// from its exported consumers + the RP VTV::DeathShutdown analog) ---
Logical IsMechDestroyed(); // damage-side death flag: graphicAlarm level >= 9
void UpdateDeathState(); // collapse movementMode + subsystem DeathShutdown + freeze
// --- clip loaders (mech3) -------------------------------------------
ResourceDescription::ResourceID *
+63 -1
View File
@@ -843,6 +843,61 @@ static void
}
}
//###########################################################################
// Death sequence -- the un-exported master-perf death branch (region
// 0x4a9770-0x4ab188), reconstructed from its EXPORTED consumers + the RP
// VTV::DeathShutdown analog. See context/combat-damage.md "Death SEQUENCE".
//
// On a vital kill the damage side raises graphicAlarm to 9 (mechdmg.cpp:426/586).
// The authentic per-frame death transition (movementMode 5-8 = collapse -> 2/9 =
// disabled) lives in the Simulate the bring-up drive bypasses, so it is run here.
//###########################################################################
Logical
Mech::IsMechDestroyed()
{
// graphicAlarm level 9 = a vital zone / leg gone -> going down (mechdmg.cpp).
return graphicAlarm.GetLevel() >= 9 ? True : False;
}
void
Mech::UpdateDeathState()
{
if (!IsMechDestroyed()) // alive
return;
if (movementMode == 9) // already settled (disabled/frozen)
return;
if (movementMode < 5 || movementMode > 8) // FIRST destroyed frame
{
// Collapse: a fall movementMode fires the one-shot death clip via
// AdvanceBodyAnimation's latch (fall DIRECTION 5-8 is un-exported -> 5).
movementMode = 5; // [T3] direction not determinable from the export
// RP VTV::DeathShutdown analog: shut every subsystem down. The base
// Subsystem::DeathShutdown is an empty virtual (SUBSYSTM.h); overrides
// act. This is a SHUTDOWN, not teardown -- it frees nothing and never
// removes the entity, so it is safe (the wreck STAYS).
for (int i = 0; i < GetSubsystemCount(); ++i)
{
Subsystem *s = GetSubsystem(i);
if (s != 0)
s->DeathShutdown(1);
}
if (getenv("BT_DEATH_LOG"))
DEBUG_STREAM << "[death] mech " << GetEntityID()
<< " destroyed -> collapse + subsystem shutdown (wreck stays)\n" << std::flush;
}
else // SECOND frame (movementMode 5-8): settle to disabled
{
// The collapse clip (latched frame 1 if the anim path ran) plays out via
// the controller after this; movementMode 9 => IsDisabled => locomotion
// frozen regardless of whether the clip latched.
movementMode = 9;
if (getenv("BT_DEATH_LOG"))
DEBUG_STREAM << "[death] mech " << GetEntityID()
<< " settled -> disabled (IsDisabled=" << (int)IsDisabled() << ", frozen wreck)\n" << std::flush;
}
}
void
Mech::PerformAndWatch(const Time& till, MemoryStream *update_stream)
{
@@ -859,6 +914,12 @@ void
const Logical isPlayerMech =
(application != 0 && (Entity *)this == application->GetViewpointEntity());
// DEATH consumer -- runs for EVERY mech (player + spawned targets). On a
// vital kill it collapses + shuts the subsystems down + settles to disabled;
// the drive's movementMode write below is guarded on !IsMechDestroyed so a
// dead mech keeps its death state instead of reverting to a live gait.
UpdateDeathState();
// WAVE 7 Phase B: advance/render/impact the flying projectiles once per frame (viewpoint).
if (isPlayerMech && dt > 0.0001f)
BTUpdateProjectiles(dt);
@@ -1306,7 +1367,8 @@ void
}
if (!(forwardCycleRate >= 25.0f && forwardCycleRate < 10000.0f))
forwardCycleRate = 25.0f;
movementMode = 1; // ground, non-death, non-airborne
if (!IsMechDestroyed()) // a dead mech keeps its death movementMode
movementMode = 1; // ground, non-death, non-airborne
// reverseSpeedMax2@0x7a0 is the run-cycle bodyCycleSpeed CLAMP (AdvanceBody
// Animation case 12/13); LoadLocomotionClips does not set it -> it reads
// 0xCDCDCDCD (-4.3e8) and the clamp clobbers bodyCycleSpeed -> the run cycle