Death effects dispatch at the victim's death transition (task #42)
User report: enemy takes hits, starts smoking, then beams pass through -- no more damage, no death. Root cause: the death-effects dispatch (blhdead explosion chain + wreck swap + KILL score) lived in the SHOOTER's laser fire block kill check. A missile killing blow lands frames later in BTUpdateProjectiles -- outside that block -- and after task #41 the boresight pick skips a dead mech, so the fire block never ran against it again: the death chain was silently skipped, leaving an internally-dead, smoking, standing, invulnerable mech. Fix: the dispatch moved to the VICTIM's own once-per-death transition (UpdateDeathState's movementMode->9 moment) -- fires exactly once for ANY kill source (laser, missile, collision). Killer id for the Explosion message = lastInflictingID (the task-#31 bookkeeping). The BT_ENABLE_TEARDOWN cdb harness was removed with the old block (findings preserved in docs/HARD_PROBLEMS.md). Verified headless (75s soak, missile-heavy kill): destroyed -> 'blhdead' id=22 -> wreck swap 'blhdbr.bgf' -> smoke re-arm -> sink -> buried INERT; post-death picks fall through to terrain; 0 crashes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
e30a61a62f
commit
305c928923
@@ -271,6 +271,14 @@ Full decomp map of the death path. Two tiers:
|
||||
`<prefix>dbr.bgf` on the tree root (gendbr fallback; pending-swap if death precedes tree build).
|
||||
Mesh flames + the settle motion are noted follow-ups; burning reads via 1001/1007 + the wreck-smoke
|
||||
re-arm. Verified: `[BTrender] wreck swap: victim -> 'blhdbr.bgf'`. [T2]
|
||||
⚠ **The death-effects DISPATCH lives at the VICTIM's death transition** (`UpdateDeathState`'s
|
||||
movementMode→9 moment, task #42) — NOT in any shooter's fire path. It originally sat in the
|
||||
laser fire block's kill check, which a MISSILE killing blow (landing frames later in
|
||||
`BTUpdateProjectiles`) never reached — and post-#41 the boresight pick skips a dead mech, so the
|
||||
block never re-ran: internally dead, smoking, standing, invulnerable (user-reported). The
|
||||
transition fires once for ANY kill source (laser/missile/collision); the killer id for the
|
||||
Explosion message comes from `lastInflictingID` (the task-#31 bookkeeping). Verified: missile
|
||||
kill → full chain (blhdead → wreck swap → smoke → sink → burial → INERT). [T2]
|
||||
- `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]`) —
|
||||
|
||||
+72
-102
@@ -1063,6 +1063,67 @@ void
|
||||
DEBUG_STREAM << "[death] mech " << GetEntityID()
|
||||
<< " destroyed -> subsystem shutdown + frozen wreck (IsDisabled="
|
||||
<< (int)IsDisabled() << ")\n" << std::flush;
|
||||
|
||||
// --- DEATH EFFECTS (task #42): dispatched HERE, at the VICTIM's own
|
||||
// once-per-death transition, so they fire regardless of WHAT killed it
|
||||
// (laser fire block, missile impact frames later, collision damage).
|
||||
// They used to live in the shooter's laser fire block, whose kill
|
||||
// check a missile killing blow never reached -- and post-#41 the
|
||||
// boresight pick skips a dead mech, so that block never ran against
|
||||
// it again: internally dead, smoking, standing, invulnerable.
|
||||
{
|
||||
// KILL score (once; the ownerless dummy yields no death for us)
|
||||
BTPostKillScore((Entity *)this, kShotDamage);
|
||||
if ((Entity *)this == gEnemyMech)
|
||||
gEnemyDestroyed = 1; // latches off damage score
|
||||
|
||||
// The victim's AUTHENTIC per-mech death ModelList -- 'blhdead' (RES 22;
|
||||
// the burning-wreck script chain: effect 104 wreck swap + 1007 dnboom +
|
||||
// 1001 ddthsmk + 3/4/5/15). Falls back to the generic explode effect.
|
||||
if (gExplodeReady == 0 && application != 0
|
||||
&& application->GetResourceFile() != 0)
|
||||
{
|
||||
ResourceDescription *exp =
|
||||
application->GetResourceFile()->FindResourceDescription(
|
||||
"explode", (ResourceDescription::ResourceType)1, -1);
|
||||
if (exp != 0) { gExplodeRes = exp->resourceID; gExplodeReady = 1; }
|
||||
else gExplodeReady = -1;
|
||||
}
|
||||
ResourceDescription::ResourceID dead_res = gExplodeRes;
|
||||
if (application != 0 && application->GetResourceFile() != 0)
|
||||
{
|
||||
ResourceDescription *dr =
|
||||
application->GetResourceFile()->FindResourceDescription(
|
||||
"blhdead", (ResourceDescription::ResourceType)1, -1);
|
||||
if (dr != 0)
|
||||
{
|
||||
dead_res = dr->resourceID;
|
||||
if (getenv("BT_DEATH_LOG"))
|
||||
DEBUG_STREAM << "[death] firing authentic death list 'blhdead' id="
|
||||
<< (long)dead_res << "\n" << std::flush;
|
||||
}
|
||||
}
|
||||
if (gExplodeReady == 1 || dead_res != gExplodeRes)
|
||||
{
|
||||
Origin death_origin = localOrigin;
|
||||
death_origin.linearPosition.y += kMuzzleHeight;
|
||||
Explosion::MakeMessage death_exp(
|
||||
Explosion::MakeMessageID,
|
||||
sizeof(Explosion::MakeMessage),
|
||||
(Entity::ClassID)RegisteredClass::ExplosionClassID,
|
||||
EntityID::Null,
|
||||
dead_res,
|
||||
Explosion::DefaultFlags,
|
||||
death_origin,
|
||||
GetEntityID(), // the victim
|
||||
lastInflictingID); // the killer (task #31 bookkeeping)
|
||||
Explosion *boom = Explosion::Make(&death_exp);
|
||||
if (boom) Register_Object(boom);
|
||||
}
|
||||
DEBUG_STREAM << "[damage] *** " << GetEntityID()
|
||||
<< " DESTROYED (death effects dispatched from the death transition) ***\n"
|
||||
<< std::flush;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -2614,108 +2675,17 @@ void
|
||||
}
|
||||
}
|
||||
|
||||
// Death via the REAL damage model: a mech with a destroyed VITAL zone is
|
||||
// dead. We read the reconstructed zone state directly (damageLevel >= 1.0
|
||||
// on a vitalDamageZone). The faithful Mech::IsDisabled() reads movementMode,
|
||||
// which the gait/death-transition (bypassed by the bring-up drive override)
|
||||
// would set from exactly this condition. Friend access (mechdmg.hpp).
|
||||
Logical dead = False;
|
||||
for (int k = 0; k < gEnemyMech->damageZoneCount; ++k)
|
||||
{
|
||||
Mech__DamageZone *dz = ((Mech *)gEnemyMech)->Zone(k);
|
||||
if (dz->vitalDamageZone && dz->damageLevel >= 1.0f) { dead = True; break; }
|
||||
}
|
||||
if (!gEnemyDestroyed && dead)
|
||||
{
|
||||
gEnemyDestroyed = 1;
|
||||
|
||||
// gauge scoring wave (Step 7): credit the KILL to the local player
|
||||
// -> KILLS 0->1 (senderMechID = the VICTIM, so the !=our-mech branch
|
||||
// fires; the ownerless dummy yields no death, DEATHS stays 0).
|
||||
BTPostKillScore(gEnemyMech, kShotDamage);
|
||||
|
||||
// Death explosion: fire the victim's AUTHENTIC per-mech death
|
||||
// ModelList -- resources 22-25 are 'blhdead'/'lokdead'/'owndead'/
|
||||
// 'thrdead' (BTL4.RES); its authored video objects carry the death
|
||||
// effect numbers (the burning-WRECK script chain, ExplosionScripts
|
||||
// case 4 loads <mech>dbr.bgf + flames in the 1996 binary). Resolve
|
||||
// "<prefix>dead" by name (prefix = the anim/model prefix); fall back
|
||||
// to the generic explode resource if the mech has no dead list.
|
||||
Origin death_origin = ((Mech *)gEnemyMech)->localOrigin;
|
||||
death_origin.linearPosition.y += kMuzzleHeight;
|
||||
ResourceDescription::ResourceID dead_res = gExplodeRes;
|
||||
if (application != 0 && application->GetResourceFile() != 0)
|
||||
{
|
||||
ResourceDescription *dr =
|
||||
application->GetResourceFile()->FindResourceDescription(
|
||||
"blhdead", (ResourceDescription::ResourceType)1, -1);
|
||||
if (dr != 0)
|
||||
{
|
||||
dead_res = dr->resourceID;
|
||||
if (getenv("BT_DEATH_LOG"))
|
||||
DEBUG_STREAM << "[death] firing authentic death list 'blhdead' id="
|
||||
<< (long)dead_res << "\n" << std::flush;
|
||||
}
|
||||
}
|
||||
Explosion::MakeMessage death_exp(
|
||||
Explosion::MakeMessageID,
|
||||
sizeof(Explosion::MakeMessage),
|
||||
(Entity::ClassID)RegisteredClass::ExplosionClassID,
|
||||
EntityID::Null,
|
||||
dead_res,
|
||||
Explosion::DefaultFlags,
|
||||
death_origin,
|
||||
gEnemyMech->GetEntityID(),
|
||||
GetEntityID());
|
||||
Explosion *boom = Explosion::Make(&death_exp);
|
||||
if (boom) Register_Object(boom);
|
||||
|
||||
// P5 ROOT-CAUSE (env-gated, DEFAULT OFF): actually tear the wreck down so
|
||||
// the ~Mover collision-dtor crash reproduces under cdb. The shipped death
|
||||
// stand-in below (explosion + stop-targeting) is unchanged when the flag is off.
|
||||
if (getenv("BT_ENABLE_TEARDOWN"))
|
||||
{
|
||||
char *e = (char *)gEnemyMech;
|
||||
DEBUG_STREAM << "[teardown] enemy this=" << (void *)e << " base-region dump (engine fields):\n"
|
||||
<< " collisionVolume@0x2d8=" << *(void **)(e + 0x2d8)
|
||||
<< " collisionTemplate@0x2dc=" << *(void **)(e + 0x2dc)
|
||||
<< " containedByNode@0x2e0=" << *(void **)(e + 0x2e0) << "\n"
|
||||
<< " collisionLists@0x2e4=" << *(void **)(e + 0x2e4)
|
||||
<< " lastCollisionList@0x2e8=" << *(void **)(e + 0x2e8)
|
||||
<< " collisionAssistant@0x2ec=" << *(void **)(e + 0x2ec) << "\n"
|
||||
<< " segmentTable.array@0x2f0=" << *(void **)(e + 0x2f0)
|
||||
<< " segmentCount@0x308=" << *(int *)(e + 0x308)
|
||||
<< " jointSubsystem@0x30c=" << *(void **)(e + 0x30c) << "\n"
|
||||
<< " -> dispatching DestroyEntityMessage\n" << std::flush;
|
||||
Entity::DestroyEntityMessage destroy_msg(
|
||||
Entity::DestroyEntityMessageID,
|
||||
sizeof(Entity::DestroyEntityMessage));
|
||||
gEnemyMech->Dispatch(&destroy_msg);
|
||||
}
|
||||
|
||||
// By default we do NOT tear the wreck down: FryDeathRow -> ~Mech -> ~JointedMover
|
||||
// -> ~Mover CRASHES. ⚠ ROOT-CAUSED (P5, see btbuild/HARD_PROBLEMS.md): NOT
|
||||
// "collision solids never built" (that was wrong -- Mover::Mover allocates
|
||||
// collisionLists@0x2e4 unconditionally; it reads a valid heap ptr at death).
|
||||
// The real cause is the Entity/Mover BASE-REGION LAYOUT divergence: this file's
|
||||
// raw binary-offset accesses (this+0x2d4 netOrientation / +0x2e0 arrivalTime /
|
||||
// +0x2e8 physicsBody / +0x2ec groundRef / +0x2f0 groundCell) land on the elsewhen
|
||||
// engine Mover's collision cluster (collisionVolumeCount@0x2d4 .. collisionAssistant
|
||||
// @0x2ec) and CORRUPT it (incl. deref-writes at :1020 through +0x2ec), so teardown
|
||||
// deletes garbage -- crashing at varying base-member sites (~Mover collisionLists,
|
||||
// ~JointedMover member). Enable with BT_ENABLE_TEARDOWN to repro under cdb.
|
||||
// FIX = reconcile those raw offsets to the relocated declared members (the base-
|
||||
// region audit). Meanwhile the mech "dies" (explosion + no longer targeted).
|
||||
DEBUG_STREAM << "[damage] *** TARGET DESTROYED after "
|
||||
<< gShotCount << " hits ***\n" << std::flush;
|
||||
|
||||
// KEEP the wreck targeted (do NOT null gEnemyMech): the wreck
|
||||
// STAYS in the world (removal = the P5 teardown crash), so the
|
||||
// beam convergence must keep terminating ON it -- nulling the
|
||||
// lock here made every later beam a "free" ray that visibly
|
||||
// passed through the standing wreck. Damage + kill scoring are
|
||||
// latched off above via gEnemyDestroyed.
|
||||
}
|
||||
// DEATH detection + effects moved to the VICTIM's own death
|
||||
// transition (UpdateDeathState, task #42): the killing blow can be
|
||||
// a MISSILE landing frames after this block (or collision damage),
|
||||
// and post-#41 the boresight pick skips a dead mech so this block
|
||||
// never ran against it again -- the death chain silently skipped
|
||||
// (internally dead, smoking, standing, invulnerable). The
|
||||
// transition fires once for ANY kill source. (The
|
||||
// BT_ENABLE_TEARDOWN cdb repro harness was removed with this
|
||||
// block -- its findings are recorded in docs/HARD_PROBLEMS.md;
|
||||
// re-add at the transition if the P5 work resumes. The wreck
|
||||
// STAYS -- teardown = the P5 base-region crash.)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user