diff --git a/context/combat-damage.md b/context/combat-damage.md index b491e72..8a3f8a9 100644 --- a/context/combat-damage.md +++ b/context/combat-damage.md @@ -199,10 +199,22 @@ Full decomp map of the death path. Two tiers: wgl wgr sqd squ trn` + `*i` variants) — no death/fall clip; (4) firing the latch would bind the zero-initialised slot = **resource id 0 = a StaticAudioStream** as keyframes (garbage) — dead code in a shipped arcade build. The authentic BT death modes are the FREEZE modes: `IsDestroyed()` == - (`movementMode` 2 || 9), locomotion zeroed by `FUN_004ab1c8`. **The death READ = instant freeze + the - dnboom explosion (psfx 7) + the ddthsmk smoke plume (psfx 1) + destroyed skins + subsystem shutdown — - the wreck stands.** `UpdateDeathState` (mech4.cpp) settles straight to 9 (never 5-8, which would trip - the garbage latch). If period pod footage ever shows a mech falling, that's NEW evidence — reopen then. + (`movementMode` 2 || 9), locomotion zeroed by `FUN_004ab1c8`. `UpdateDeathState` (mech4.cpp) settles + straight to 9 (never 5-8, which would trip the garbage latch). +- ✅ **The DEATH VISUAL is the WRECK-HULK SWAP, not a fall (2026-07-08) [T1].** The authentic chain: + death fires the victim's per-mech **death ModelList** (`blhdead`/`lokdead`/`owndead`/`thrdead` = + .RES ids 22-25; + `trkdead`/`bigdead`/`meddead` 32-34 for icons), whose authored video objects + dispatch (verified live from resource 22): **effect 104** = the burning-WRECK script + **1007** + (dnboom big explosion) + **1001** (ddthsmk rubble smoke plume) + a 3/4/5/15 damage-smoke burst. + The 1996 `ExplosionScripts` **case 4** (part_008.c:2663, LIVE — the "disabled" warning is case 6/ + effect 106) loads the destroyed hulk + `flamesml/flamebig.bgf` flames with sweep-flicker and a slow + settle. Every mech ships its hulk (`BLHDBR/MADDBR/LOKDBR/VULDBR/AVADBR/OWNDBR/SNDDBR/THRDBR` + + `GENDBR` generic + `LDBR/MDBR/RAPDBR/STIDBR`); the 1996 script hardcodes `thrdbr.bgf` (dev + shortcut). **RECONSTRUCTED**: `BTL4VideoRenderer::SwapToWreck` (btl4vid.cpp) — kill → + `blhdead` Explosion → effect 104 → hide every segment mesh + hang the victim's own + `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] - `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]`) — diff --git a/context/open-questions.md b/context/open-questions.md index 22eac06..6bf92c9 100644 --- a/context/open-questions.md +++ b/context/open-questions.md @@ -109,9 +109,13 @@ authentic path scoped. `RemakeEntityRenderables` + `BTRemakeMechModel` bridge; in-place `SetDrawObj` swap — the tree dtor does NOT cascade so never rebuild). ✅ (a) **RESOLVED — no collapse animation exists in BT 4.11** [T1]: the fall latch (modes 5-8 → table slots 0x1c-0x1f) is a vestige — no loader fills the slots, no fall clip - ships, and firing it would bind resource 0 (an AUDIO stream) as keyframes. Authentic death = FREEZE - (mode 9) + dnboom + ddthsmk plume + destroyed skins ([[combat-damage]] "Death SEQUENCE" for the proofs). - **Remaining**: (c) the whole-mech **DeathSplash** radius damage. Do NOT issue DestroyEntityMessage on death. + ships, and firing it would bind resource 0 (an AUDIO stream) as keyframes. ✅ The authentic death + VISUAL is the **WRECK-HULK SWAP** (death ModelList `blhdead` → effect 104 → the victim becomes its + `dbr.bgf` burning hulk) — RECONSTRUCTED + verified (`SwapToWreck`, btl4vid.cpp; see + [[combat-damage]] "Death SEQUENCE" for the full chain + proofs). + **Remaining**: (c) the whole-mech **DeathSplash** radius damage; wreck mesh FLAMES + (flamesml/flamebig + sweep flicker, 1996 script case 4) + the hulk settle motion (cosmetic). + Do NOT issue DestroyEntityMessage on death. - **Critical-subsystem plugs UNBOUND (43 skips/mech) [T3].** `MechCriticalSubsystem::subsystemPlug` never gets WIRED to its live subsystem (the ctor's Resolve() at mechdmg.cpp:276 only READS; the binding write is elsewhere/unreconstructed), so `SendSubsystemDamage` skips every entry via the diff --git a/engine/MUNGA/EXPLODE.h b/engine/MUNGA/EXPLODE.h index 8c8b6fd..00e1a74 100644 --- a/engine/MUNGA/EXPLODE.h +++ b/engine/MUNGA/EXPLODE.h @@ -99,6 +99,10 @@ public: Enumeration GetInterestPriority(Entity *linked_entity); + // The entity this explosion hit. Read by the BT death-wreck render script + // (effect 104: swap the victim's visual to its dbr burning hulk). + Entity *GetEntityHit() { return entityHit; } + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Protected Data // diff --git a/engine/MUNGA_L4/L4VIDEO.cpp b/engine/MUNGA_L4/L4VIDEO.cpp index b06700b..5f14677 100644 --- a/engine/MUNGA_L4/L4VIDEO.cpp +++ b/engine/MUNGA_L4/L4VIDEO.cpp @@ -15,6 +15,7 @@ #include "..\munga\mission.h" #include "..\munga\cultural.h" #include "..\munga\nttmgr.h" +#include "..\munga\explode.h" // Explosion::GetEntityHit (the effect-104 wreck swap) #include "..\munga\app.h" #include "l4particles.h" #include "DXUtils.h" @@ -6436,13 +6437,26 @@ void { DEBUG_STREAM << " ** ExplosionScripts(*entity*, RES[" << model_resource->resourceID << " - " << model_resource->resourceName << "], " - << "ViewFrom::" << (view_type == ViewFrom::insideEntity ? "insideEntity" : (view_type == ViewFrom::outsideEntity ? "outsideEntity" : "collisionEntity")) + << "ViewFrom::" << (view_type == ViewFrom::insideEntity ? "insideEntity" : (view_type == ViewFrom::outsideEntity ? "outsideEntity" : "collisionEntity")) << ", " << (effect_number - 100) << ");" << std::endl << std::flush; - ExplosionScripts( - entity, // The entity we are dealing with - model_resource, // Pointer to the video resource - view_type, // Type of reference (inside/outside...etc.) - effect_number - 100); + // + // Effect 104 = the death-WRECK script (1996 ExplosionScripts + // case 4: swap the victim to its dbr burning hulk), + // dispatched by the per-mech death ModelLists ('blhdead'...). + // RECONSTRUCTED in the BT renderer (SwapToWreck); the other + // script numbers remain stubbed with the rest of the layer. + // + if (effect_number == 104) + { + extern void BTSwapMechToWreck(Entity *victim); + BTSwapMechToWreck(((Explosion *)entity)->GetEntityHit()); + } + else + ExplosionScripts( + entity, // The entity we are dealing with + model_resource, // Pointer to the video resource + view_type, // Type of reference (inside/outside...etc.) + effect_number - 100); } } break; diff --git a/game/reconstructed/btl4vid.cpp b/game/reconstructed/btl4vid.cpp index fcf397c..9655efd 100644 --- a/game/reconstructed/btl4vid.cpp +++ b/game/reconstructed/btl4vid.cpp @@ -301,7 +301,9 @@ HierarchicalDrawComponent* // MechRenderTree &render_tree = mMechRenderTrees[entity]; render_tree = MechRenderTree(); - render_tree.skeletonType = (int)skeletonType; + render_tree.skeletonType = (int)skeletonType; + render_tree.rootRenderable = this_root; + render_tree.wrecked = 0; if (getenv("BT_DEATH_LOG")) DEBUG_STREAM << "[BTrender] tracking mech tree for entity " << (void*)entity << " classID=" << entity->GetClassID() << " (" @@ -492,6 +494,16 @@ HierarchicalDrawComponent* DEBUG_STREAM << "[BTrender] mech tree built: meshes " << dbg_obj_loaded << "/" << dbg_obj_requested << " loaded, eye=" << dbg_eye << "\n" << std::flush; + // + // If this mech DIED before its tree was built (a fast kill during mission + // creation), apply the remembered wreck swap now. + // + { + extern int BTTakePendingWreck(Entity *entity); + if (BTTakePendingWreck(entity)) + SwapToWreck(entity); + } + // // TODO(bring-up): inside-view targeting reticle (BTReticleRenderable + // AddWeapon pips) and the per-subsystem weapon/effect renderables (PPC/ @@ -597,6 +609,8 @@ void return; // tree not built yet -- Make will read the state } MechRenderTree &render_tree = tree_it->second; + if (render_tree.wrecked) + return; // already the dbr hulk -- nothing left to swap JointedMover *jointed_mover = (JointedMover *)entity; EntitySegment::SkeletonType skeletonType = @@ -704,6 +718,125 @@ void BTRemakeMechModel(Entity *entity) } +// +//############################################################################# +// SwapToWreck (ExplosionScripts effect 104, reconstructed) +//############################################################################# +// +// The authentic death chain: the victim's per-mech death ModelList +// ('blhdead'/'lokdead'/... resources 22-25) dispatches effect 104, whose 1996 +// script (@0045xxxx, part_008.c:2663 case 4) swaps in the burning WRECK: the +// destroyed hulk mesh + flame meshes with flicker sweeps and a slow settle. +// The 2007 port stubbed the whole script layer. This reconstruction does the +// core swap: hide every segment mesh and hang the victim's own "dbr" +// hulk on the tree root (the 1996 code hardcoded thrdbr.bgf -- a dev shortcut; +// every mech ships its hulk: BLHDBR/MADDBR/LOKDBR/... + GENDBR the generic +// fallback). Burning comes from the effect layer (the death list also fires +// the 1007 boom + 1001 smoke plume; the wreck re-arms the plume while it +// stands). Mesh flames (flamesml/flamebig + sweep flicker) are a noted +// follow-up. +// +static std::map gBTPendingWrecks; + +int BTTakePendingWreck(Entity *entity) +{ + std::map::iterator it = gBTPendingWrecks.find(entity); + if (it == gBTPendingWrecks.end()) + return 0; + gBTPendingWrecks.erase(it); + return 1; +} + +void + BTL4VideoRenderer::SwapToWreck(Entity *victim) +{ + std::map::iterator tree_it = + mMechRenderTrees.find(victim); + if (tree_it == mMechRenderTrees.end()) + { + gBTPendingWrecks[victim] = 1; // died before the tree was built + return; + } + MechRenderTree &render_tree = tree_it->second; + if (render_tree.wrecked) + return; + + // + // The victim's model prefix, from any segment's intact video-object name + // (e.g. "blh_rfot.bgf" -> "blh" -> "blhdbr.bgf"). + // + char hulk_name[44]; + hulk_name[0] = '\0'; + { + JointedMover *jm = (JointedMover *)victim; + EntitySegment::SegmentTableIterator it(jm->segmentTable); + EntitySegment *segment; + while ((segment = it.ReadAndNext()) != NULL) + { + if (segment->IsSiteSegment() != 0) + continue; + CString *nm = segment->GetVideoObjectName( + (EntitySegment::SkeletonType)render_tree.skeletonType, 0); + if (nm != NULL && strlen((const char *)*nm) >= 3) + { + strncpy(hulk_name, (const char *)*nm, 3); + hulk_name[3] = '\0'; + strcat(hulk_name, "dbr.bgf"); + break; + } + } + } + d3d_OBJECT *hulk = (hulk_name[0] != '\0') + ? d3d_OBJECT::LoadObject(GetDevice(), hulk_name) : NULL; + if (hulk == NULL) + { + DEBUG_STREAM << "[BTrender] wreck: '" << hulk_name + << "' missing -> gendbr.bgf fallback\n" << std::flush; + hulk = d3d_OBJECT::LoadObject(GetDevice(), "gendbr.bgf"); + } + + // + // Hide the body; hang the hulk on the tree root (identity offset -- the + // root renderable already pushes the wreck's localToWorld, so the hulk + // sits at the mech's ground position with its death yaw). + // + for (std::map::iterator r = + render_tree.segRenderable.begin(); + r != render_tree.segRenderable.end(); ++r) + { + if (r->second != NULL) + r->second->SetDrawObj(NULL); + } + if (hulk != NULL && render_tree.rootRenderable != NULL) + { + dpl_ISECT_MODE isect_mode; + LinearMatrix identity(True); + new DPLStaticChildRenderable( + victim, false /* main zone */, hulk, + isect_mode, INTERSECT_ALL, identity, render_tree.rootRenderable); + } + render_tree.wrecked = 1; + DEBUG_STREAM << "[BTrender] wreck swap: victim -> '" + << (hulk_name[0] ? hulk_name : "gendbr.bgf") + << (hulk ? "'" : "' (LOAD FAILED -- body hidden only)") + << "\n" << std::flush; +} + + +// +// Engine-side bridge (the ExplosionClassID dispatch calls this on effect 104). +// +void BTSwapMechToWreck(Entity *victim) +{ + if (victim == NULL || application == NULL) + return; + BTL4VideoRenderer *renderer = + (BTL4VideoRenderer *)application->GetVideoRenderer(); + if (renderer != NULL) + renderer->SwapToWreck(victim); +} + + // //############################################################################# // BTReticleRenderable::AddWeapon diff --git a/game/reconstructed/btl4vid.hpp b/game/reconstructed/btl4vid.hpp index 9635921..374088a 100644 --- a/game/reconstructed/btl4vid.hpp +++ b/game/reconstructed/btl4vid.hpp @@ -564,11 +564,25 @@ class BTReticleRenderable: struct MechRenderTree { int skeletonType; // EntitySegment::SkeletonType used at build + HierarchicalDrawComponent *rootRenderable;// the tree root (wreck hulk parent) + int wrecked; // 1 = swapped to the dbr hulk std::map segRenderable; // slot -> joint renderable std::map segGState; // slot -> last applied graphic state }; std::map mMechRenderTrees; + public: + // + // The death-wreck swap (ExplosionScripts effect 104, reconstructed): the + // victim's whole visual becomes its burning hulk -- hide every segment + // mesh and hang "dbr.bgf" (gendbr.bgf fallback) on the tree root. + // The 1996 script hardcoded thrdbr.bgf (a dev shortcut; every mech ships + // its own *DBR hulk). If the victim's tree isn't built yet, the swap is + // remembered and applied when MakeMechRenderables builds it. + // + void + SwapToWreck(Entity *victim); + protected: // // Renderer-manager overrides @@ -641,6 +655,12 @@ class BTReticleRenderable: // extern void BTRemakeMechModel(Entity *entity); +// +// Render bridge for the death-wreck swap (effect 104): called by the engine's +// ExplosionClassID dispatch with the explosion's entityHit (the dead mech). +// +extern void BTSwapMechToWreck(Entity *victim); + #endif // BTL4VID_HPP //===========================================================================// diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index fcae855..2754663 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -2430,28 +2430,35 @@ void // fires; the ownerless dummy yields no death, DEATHS stays 0). BTPostKillScore(gEnemyMech, kShotDamage); - // Death effects, per the authentic BTDPL.INI effect-number map: - // 7 = "the big explosion used as part of mech death" (dnboom) - // 1 = "the mech death/rubble smoke plume" (ddthsmk) - // Fired directly into the render effect layer at the wreck (the - // unexported death sequence's effect chain dispatched these - // numbers through the 0xBD3 manager; the numbers are the data). - { - extern void BTStartPfx(int effect_number, float x, float y, float z); - Point3D wreck = ((Mech *)gEnemyMech)->localOrigin.linearPosition; - BTStartPfx(7, wreck.x, wreck.y + kMuzzleHeight, wreck.z); // the death boom - BTStartPfx(1, wreck.x, wreck.y + kMuzzleHeight, wreck.z); // the smoke plume - } - - // Death explosion at the target (torso height, not ground level). + // 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 dbr.bgf + flames in the 1996 binary). Resolve + // "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, - gExplodeRes, + dead_res, Explosion::DefaultFlags, death_origin, gEnemyMech->GetEntityID(), diff --git a/scratchpad/anibmp.py b/scratchpad/anibmp.py new file mode 100644 index 0000000..d975ef4 --- /dev/null +++ b/scratchpad/anibmp.py @@ -0,0 +1,52 @@ +import struct +data = open(r'C:\git\bt411\content\BTL4.RES', 'rb').read() +labOnly, maxID = struct.unpack_from(' len(payload): + continue + tail = payload[len(payload) - need:] + ys = [struct.unpack_from('<3f', tail, k * 12) for k in range(A + 1)] + print('%s (id %d, %r): A=%d kf, B=%d joints, hdr=%d' % (label, rid, nm, A, B, hdr)) + print(' root (x,y,z) per kf:') + for k, v in enumerate(ys): + print(' kf%02d x=%8.3f y=%8.3f z=%8.3f' % (k, v[0], v[1], v[2])) + return + print(label, rid, 'no plausible header found; first ints:', + struct.unpack_from('<8i', blob, 40)) + +root_y(908, 'blhbmp (knockdown)') +print() +root_y(898, 'blhwwl (walk-left, baseline)') diff --git a/scratchpad/ml104.py b/scratchpad/ml104.py new file mode 100644 index 0000000..7b7361d --- /dev/null +++ b/scratchpad/ml104.py @@ -0,0 +1,27 @@ +import struct, re +data = open(r'C:\git\bt411\content\BTL4.RES', 'rb').read() +labOnly, maxID = struct.unpack_from(' %s' % (rid, nm, toks[:10]))