diff --git a/context/combat-damage.md b/context/combat-damage.md index 1ba3a05..b2d5dcc 100644 --- a/context/combat-damage.md +++ b/context/combat-damage.md @@ -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. diff --git a/context/open-questions.md b/context/open-questions.md index b669baf..328cc43 100644 --- a/context/open-questions.md +++ b/context/open-questions.md @@ -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). diff --git a/game/reconstructed/mech.hpp b/game/reconstructed/mech.hpp index 5a7965e..9bc7bc0 100644 --- a/game/reconstructed/mech.hpp +++ b/game/reconstructed/mech.hpp @@ -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 * diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 1b582bc..1b7214f 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -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