# Virtual World Pod Games — Port Project (BattleTech / Red Planet) > ⚠️ **REPO ORIENTATION (bt411):** This is the **clean standalone bt411 repo** — start with > `README.md` for the current build/run and the actual tree layout (`engine/ game/ content/ > docs/ reference/ tools/`). The historical/progressive sections below were written in the > original RE workspace and still cite absolute `C:/git/nick-games/...` paths and its scattered > layout (`decomp/reconstructed/`, `elsewhen_extracted/.../MUNGA_L4/`, `btbuild/`, `enginebuild/`, > the pod content tree); those map into this repo as: reconstructed BT → `game/reconstructed/`, > the engine → `engine/MUNGA{,_L4}/`, content → `content/`, raw decomp → `reference/decomp/`. > Treat those paths as history; the build of record is the top-level `CMakeLists.txt` + `README.md`. > Everything else below (decomp findings, reconstruction history, gotchas) is accurate and durable. Knowledge base + roadmap for porting Virtual World Entertainment's arcade **pod** games (Tesla platform, release 4.10, ~1995–96) to run on modern Windows and, ultimately, the actual arcade pod hardware. Primary target: **BattleTech (BT)**; Red Planet (RP) shares the engine. > Companion docs: `docs/BGF_FORMAT.md` (model format), `docs/ASSET_PIPELINE.md` (textures/materials/ > gauges). The working port code is under `port/` (see "Port codebase" below). > **📌 This is a PROGRESSIVE CONTEXT system — keep it current.** This file is the single, durable source > of project context across sessions. **Whenever you learn something new and non-obvious about the > project — a decompilation finding, a build/runtime gotcha, a reconstruction insight, an offset/format > detail, a decision or its rationale, a dead end to avoid — ADD it here** (in the most relevant section, > or §10 for combat bring-up / reconstruction findings) as part of the same task, not as an afterthought. > Update existing statements when reality changes them rather than letting the doc drift. Goal: a later > session (or a fresh agent) can read this file and have the full working context with nothing lost to > chat history. Durable project facts → here; per-session/cross-cutting reminders → the `memory/` files > (indexed in `MEMORY.md`); detailed running findings → `docs/RECONCILE.md`. --- ## 1. What this is - Two games share one engine: **BT (BattleTech)** and **RP (Red Planet)**. Engine = **MUNGA**; hardware abstraction layer = **L4** (`MUNGA_L4`). Game logic = `CODE/BT`, `CODE/RP/RP`. - Original platform: **32-bit DOS** under a **DPMI32** extender, built with **Borland C++ 5** + **TASM32**. Ran on **Novell DOS** pods. - 3D was rendered by a proprietary **Division "IG" image-generator board (dual EISA)**, driven by a closed-source library **`libDPL.lib`** (ships as binary only, + `VREND*.BTL/.MNG` board microcode). **The port replaces this** with a `dpl_*` shim on a modern GPU. (Nick: "we'll just yoink that out.") - Pods now run **Windows 10** with modern-ish GPUs + a Voodoo/Glide wrapper. BT was abandoned; MechWarrior 4 was ported for the current pods. This project revives BT. ## 2. People / goals - **arcattack** (user): doing the port. Has NVIDIA 3060 + A6000 dev GPUs. - **Nick**: owns the pods + the software license (porting is authorized). Runs the pod hardware. - Goal: run on (a) a modern dev box and (b) the fixed pod hardware. Multiplayer (pod-to-pod) wanted. ## 3. Pod hardware (fixed target) - **2 video cards → 7 monitors**: main 3D view **800×600**; radar **640×480**; five monochrome MFDs driven as one **1280×480** horizontally-spanned surface. - Requires **old NVIDIA drivers** for the MFD horizontal spanning — a hard constraint that bounds the graphics API. → **Target Direct3D 9** (lowest common denominator that runs on old-driver pods AND modern cards; one maintainable codepath). NOT Vulkan/D3D11+. CUDA is irrelevant (compute, not display). - Cockpit I/O ("RIO") = joystick X/Y, throttle, pedals, buttons over **serial COM** (`L4RIO`, `L4SERIAL`). Must be remapped to keyboard/gamepad on a dev box; real wiring is a pod bring-up task. ## 4. Where the content lives (3 archives, in repo root) The deployed game data was **never in source control** — it lived on the pod machines. Spread across: | Archive | What it is | Has | |---|---|---| | `410srczipped.zip` | **Source code** (36MB) | all `.cpp/.hpp`, geometry `.bgf`, material defs `.bmf`, **animations `.ani`** (428), a few sample textures/skeletons. NO per-mech skins/skeletons. | | `Tesla4NovellTechPC-*.zip` | pod **technician/test PC** (77MB) | game binary `btrel410.exe` (in `vgl_labs/archive/`), Division **VPX board diagnostic tools** (`vwetest/vpx/`), shared generic textures, Novell DOS. | | `Tesla4PodPCNovell-*.zip` | the pod **game machine** (92MB) | **the full deployed tree** at `…/CopyofNovellDisk/REL410/` (BT+RP): per-mech **skeletons** (`AVA.SKL` etc., 124), **skins** (`AVATAR.BSL` etc.), 1143 models, 1624 material libs, textures. | | `Elsewhen RP411 Source-*.zip` | **the WINDOWS PORT** ("WinTesla", VS2008) ⭐ | modernized source: `WinTesla.sln`; MUNGA(150 cpp)+MUNGA_L4(43); **`L4D3D.cpp` = IG-board→Direct3D renderer bypass DONE**; **OpenAL+libsndfile = SOS audio replaced**; full **Red Planet** game logic (`VTV/VTVMPPR/WEAPSYS/...`); asset exporters (`MeshExp/TextureExp`); `DivLoader`. Solution wires both `RP_L4` AND a `BT410_L4` target — **but the `BT410_L4` folder + all BT game source are ABSENT** (only the .sln reference remains → BT was being ported, code not included). Extracted: `elsewhen_extracted/`. | Extracted to: `410srczipped_extracted/`, `tesla4_extracted/`, `tesla4pod_extracted/`. **Content root for the port** = `tesla4pod_extracted/Tesla4PodPCNovell/CopyofNovellDisk/REL410/` (use the pod tree for skeleton/skins/geometry; **animations** come from the source tree's `410srczipped_extracted/.../CONTENT/BT/ANIMS/`). Still likely missing / ask Nick: confirmation this is the complete content master. (UPDATE: the walk/run animation set is NOT missing after all — `CONTENT/BT/ANIMS/` has the full per-mech set, e.g. `BLHRRL/BLHRRR/BLHWWLI/BLHWWRI...`, and they are compiled into `BTL4.RES` — the game plays `blhrrl`=904. The earlier "only AVABMP present" worry was looking at an incomplete slice.) ### 4a. LEVELS / MAPS — what's loadable (content audit) The mission is an INI `.EGG` (`[mission] map= scenario= time= weather=`). The engine resolves `map=` from `BTL4.RES` (`MISSION.cpp:344 FindResourceDescription(name, MakeMessageStreamResourceType)`; a missing name → null-deref crash at `:355`). **The runtime `BTL4.RES` (pod tree == `decomp/BTL4.RES`, byte-identical, 3.23 MB) contains 8 loadable maps:** `cavern` (the default night cave, enclosed/no sky), `grass` (grassland), `rav` (ravine), `polar3`/`polar4` (snow), `arena1`/`arena2`, `dbase`. **Switch levels by editing a COPY of `TEST.EGG`** (e.g. `map=grass` + `time=day`) and running `-egg .egg` — verified: `grass` boots + the mech walks, 0 crashes (`GRASSDAY.EGG` created). ⚠ Names are `arena1`/`polar3`/`rav` (with the suffix), NOT bare `arena`/`polar`; `map=burnt`/`frstrm`/`des` are NOT in this RES (they crash) — those are lab/other-build maps whose `.map` SOURCE exists in `CONTENT/BT/MAPS/` (`DES.MAP` = a sky+buttes desert) but would need compiling into a RES via the DOS `btl4tool.exe` (`BUILDRES.BAT`, manifest `CONTENT/BT/BTL4.BLD`) — a separate content-build sub-project. **The full content SOURCE tree** `410srczipped_extracted/.../CONTENT/BT/` is comprehensive (17 `.map`, 636 `MODELS/`, 219 `SOLIDS/` collision, 331 `ANIMS/`, 1958 `VIDEO/` textures, 207 `AUDIO/`, 404 `GAUGE/`) — i.e. the assets to build ANY map exist; the runtime tree is the already-built subset. No consolidation needed to test the 8 maps. ⚠ Collision is OFF on ALL maps until `MoveAndCollide` is reconstructed (the mech phases through terrain/objects) — the `.sld` collision solids ARE present in the RES + `SOLIDS/`, so it is a code-revival task, not a content gap. ## 5. Asset formats (all reverse-engineered; details in docs/) - **`.MOD`** — INI mech/object definition: skeleton, skins, collision, gauges, physics, animations, class. - **`.BGF`** — `DIV-BIZ2` little-endian nested-TLV geometry. OBJECT→LOD→PATCH(=geogroup)→PMESH; interleaved float32 verts; **quad faces (triangulate!)**; materials by `library:material` name; damage/articulation are **name-driven via `SV_SPECIAL` `dz_*` tokens**, not stored geometry. - **`.BMF`** — `DIV-BIZ2` (FILETYPE=1) material+texture **library** (no pixels). Material→DIFFUSE color + `MATERIAL_TEXTURE`→texture→`TEXTURE_MAP` image basename. - Texture **pixels**: **`.tga`** (Truevision), **`.vtx`** (`DIV-VTX2`, trailing RGB), **`.bsl`** (`DIV-BSL2`: a **BIT-SLICED container** — w*h 32-bit texel words holding SIX independent 4-bit GRAYSCALE sub-images (nibble pair-swapped; byte 0 pad) or RGB444/RGBA4444 (sliceType 7/8); the BMF TEXTURE tag `0x18 BITSLICE` selects the slice, absent = 0. The old "[pad,R,G,B] confirmed" note was WRONG — full spec in `docs/ASSET_PIPELINE.md` §Pixel formats; reference reader `DivLoader/VGCDivLoader.cpp:323-410`). - **`.SKL`** — INI joint skeleton: `[joint] parent=, Type=(hingex/y/z|ball|balltranslate), Object=part.bgf, dzone=, tranx/y/z, pitch/yaw/roll`. Build a DCS tree; neutral pose ≈ translations (pyr ≈ 0). - **`.ANI`** — INI keyframe animation: `[HEADER] framecount/framerate(30)/skeletonfile`, `[Time]` frameN→seconds, `[JointType]` per-joint DOF, `[frameN]` joint→`rx ry rz` radians (balltranslate→translate). Walk anims have **no baked root translation** → forward motion is procedural/physics (see §7). - **`.GIM`** (vector MFD/radar line maps) + **`.GAT`** (color table) + **`.PCC`** (ZSoft PCX 8-bit gauge rasters) — the cockpit HUD assets. **`.DZM`** = INI damage-zone→material map (mech skin states). - **`.SLD`** collision solids, **`.DMG`/`.TBL`** damage zones/tables, **`.SUB`** subsystems, **`.CTL`** control maps. ## 5a. ⚠ SOURCE COMPLETENESS — the gating blocker The archive has the **engine and all headers, but NOT the game's implementation code**. Verified across all three zips: - **MUNGA engine** (shared): ~183 `.cpp` — largely complete (renderer, `MOVER/JMOVER`, math, scene). - **All game `.hpp` headers** present (every class/interface defined). - **BT game logic `.cpp`: MISSING** — `mech`, `mech2/3/4`, `mechsub`, `mechmppr`, `mechtech`, `heat`, `sensor`, `gyro`, `torso`, `myomers`, `hud`, `mechweap`, `powersub`, `btplayer`, and the **main app `btl4`** are all absent. Only 10 BT `.cpp` are present (BTCNSL/BTMSSN/BTREG/BTSCNRL/BTTEAM/BTTOOL + weapons GAUSS/PPC + BTL4ARND/BTL4MODE). RP game logic is similarly gutted (5 `.cpp`). - Compiled **`btrel410.exe`** exists (binary only, in the TechPC zip). - **RP can't backfill BT:** RP's game source is gutted the same way (only `RPCNSL/RPMSSN/RPTOOL/VTVPWR/ VTVSUB` + L4 glue); none of the missing BT files are under RP. Only shared *engine* game-logic is present (`MUNGA\DAMAGE.CPP`, `MUNGA\MOVER.CPP`). The per-game source for BOTH titles is missing — the archive captured the engine fully but only a partial subset of game source → a complete repo likely exists elsewhere (Nick). **Consequence:** Route A (recompile/port original source) is **NOT possible as-is** — the code that *is* the game isn't here. Paths forward: (1) **get the missing `.cpp` from Nick** [gating — top priority]; (2) run `btrel410.exe` by **emulating the VPX/IG board** in DOSBox (deep RE project; no source needed); (3) **decompile `btrel410.exe` + reconstruct from headers** (Plan B, analyzed below); (4) rewrite game logic from headers blind (worst). Do not start the shim/port build until the source situation is resolved. **`btrel410.exe` (UNWRAPPED — see `decomp/`):** it's a **PKZIP self-extractor** (`unzip` lists 324 files). Inside is the real game binary **`BTL4OPT.EXE` (1.24 MB, clean uncompressed PE32, Phar Lap TNT `32rtm` stub)** + `BTL4.RES`, `NETNUB.EXE`, TNT extender, IG microcode, full content. Extracted to `decomp/`. `BTL4OPT.EXE` is a near-ideal decompile target: class names, `Class::Method` strings, and **source-file paths in asserts** (`d:\tesla_bt\bt\mech.cpp`) are left in — confirming the missing modules (`mech`, `mech2/4`, `mechmppr`, `mechweap`, `heat`, `powersub`, `btplayer`, `btl4app/gaug/mssn/rdr/vid`, …) ARE in the binary. No COFF symbol table (naming comes from those strings). Mined: `decomp/src_files_in_binary.txt` (78 files compiled in), `decomp/methods_in_binary.txt` (151 methods). **Decompilation — UNDERWAY (Plan B in progress).** JDK21 + **Ghidra 12.1.2 installed** under `tools/` (portable). `tools/ghidra_scripts/ExportBTSource.java` run headless on `BTL4OPT.EXE` → **real decompiled C recovered** to `decomp/recovered/` for heat/mech/mech2/mechmppr/mechweap/powersub/btplayer/btmssn/ btl4app/gaug/mssn/rdr/vid (proven: heat.cpp HeatSink ctor w/ real constants+asserts). Current pass = assert-anchored only (few funcs/file). Next: decompile ALL funcs + recover vtables → map to header classes for COMPLETE modules → reconstruct to compilable C++ (header + RP analog + binary oracle) → WinTesla `BT410_L4` → build/verify. This is mechanized now but the reconstruction volume (~40 files) is large. **PILOT DONE — `heat` reconstructed** (`decomp/reconstructed/heat.{hpp,cpp}`). From the 46-func cluster: ~17 confidently reconstructed (the real heat logic — ctor, ResetToInitialState, heat-flow/coolant formulas, status/message handlers, resource parse), 5 correctly identified as inherited base methods (excluded), ~24 template/glue folded into members; Condenser bodies fell just outside the captured window (needs a re-slice). Recovered the full field layout (offset→named member), HeatSink vtable slot map, and resource struct. Output is faithful, named, VWE-styled C++ — a strong draft a human finishes (won't compile clean yet). **Honest effort: ~0.5–1.5 hr/module → ~30–50 agent-hours + human review for ~40 modules, with a long tail** (modules with no surviving `.TCP`/`.HPP` fragment, or split across decomp windows, cost more). Process upgrades before scaling: per-class function lists (not address clusters), complete vtable rows, and recover `.data` float constants (the biggest "couldn't" bucket = tuning values). **Decompilation feasibility (why viable):** the **headers are an answer key** (exact class layouts, vtable order, signatures) and the **MUNGA engine source (~183 cpp) anchors ~half the binary** by pattern-matching — and scope is bounded to only the ~15 missing modules. Stages: unwrap (PKLITE→TNT image→strip zip) → load Ghidra/IDA → compile engine to auto-identify known funcs → decompile missing modules method-by-method (vtable-located, header-typed) → rewrite into compilable C++. Result = behavior-equivalent **reconstruction** (binary usable as an oracle to verify), NOT original source; ~months of expert RE. Pilot before committing: unwrap, prove anchoring, decompile one module (e.g. `heat`). ## 5b. ⭐ The Windows port (WinTesla) changes the plan The `Elsewhen RP411` archive is a **working Windows port of the engine** (`elsewhen_extracted/.../trunk/`, `WinTesla.sln`). It means the hardest infrastructure is **already solved** and need not be rebuilt: - **Renderer bypass: DONE** (`MUNGA_L4/L4D3D.cpp` + `DXUtils` → Direct3D; libDPL/IG board removed — no `libdpl.lib` in the tree). Our from-scratch `port/` viewer is now a *reference/sandbox*, not the path. - **Audio: DONE** (OpenAL + libsndfile replacing HMI SOS). - **L4 HAL on Windows + VS build system: DONE**; **Red Planet game logic: COMPLETE & buildable**. So **Red Planet is (at/near) playable today** — build `WinTesla.sln` (VS2008 → upgrade to current VS). And for **BattleTech**: everything below the game layer (engine, renderer, audio, HAL, build) already exists here and is shared. The ONLY thing BT still needs is its **game-logic source** (mech/subsystems/ HUD/app), which is confirmed gone (§5a) → must be **reconstructed**, now with THREE strong anchors: (1) all BT headers, (2) the working WinTesla engine to compile against, (3) **Red Planet's parallel implementations as a live reference** (`VTV`≈mech, `VTVMPPR`≈mechmppr, `WEAPSYS`≈mech weapons, `RP_L4` app≈`BT_L4` app) — plus `btrel410.exe` as a behavioral oracle. ## 6. Port codebase (`port/`) — the standalone viewer sandbox (NOT in bt411) > ⚠️ **This section documents the standalone D3D9 viewer, which lives in the ORIGINAL workspace > (`nick-games/port/`), NOT in bt411.** It is a dev sandbox that proved the renderer bypass + asset > pipeline; the game itself does not use it. Its `image.{cpp,h}` codec IS in bt411 (as > `engine/MUNGA_L4/image.{cpp,h}` — the engine loader shares it). If the viewer is ever wanted here, > it's a small self-contained add. The rest of this section is retained as reference. Standalone **D3D9 model/animation viewer** — proves the renderer bypass + asset pipeline. **Not the game.** - Build: `cmake -S port -B port/build -G "Visual Studio 16 2019" -A x64 -DCMAKE_GENERATOR_INSTANCE="C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools"` then `cmake --build port/build --config Debug`. - **Toolchain gotchas:** the VS2019 *Community* install on this box is broken — must use the *BuildTools* instance via `CMAKE_GENERATOR_INSTANCE`. Don't name a header `math.h` (shadows ``; ours is `vmath.h`). - Files: `vmath.h` (math), `bgf.{h,cpp}` (model + material/texture resolution), `image.{h,cpp}` (tga/vtx/bsl decode), `scene.{h,cpp}` (retained scene graph: Object/DCS/Instance), `skeleton.{h,cpp}` (.skl→DCS), `anim.{h,cpp}` (.ani player), `main.cpp` (Win32+D3D9), `tools/bgftest.cpp` (headless loader test). - Run: `btviewer.exe [animation.ani]`. Keys: `W` wire · `C` cull · `T` textures · `P` pose · `-`/`+` walk speed · `Space` pause · `Esc`. - **Working today:** load any mech (geometry + real BMF/BSL skin), assemble from its `.SKL` skeleton, play real `.ANI` animations, walk across a ground grid with procedural root motion. Verified on Avatar + Raptor. Walk speed currently tuned by hand (being derived from source — see §7). ## 7. Open technical notes - **Walk speed / locomotion (SOLVED):** locomotion is **animation-driven**, NOT physics-derived. Each `.ANI` has a `[RootTranslation]` section (per keyframe `x y z`) where **`.z` = forward root SPEED in world units/sec** (meters/s); `[KeyJointPos].y` = vertical bob; `y` ≈ constant root height. The engine (`RP\MUNGA\JMOVER.CPP` `AnimationInstance::Animate` ~1497-1723) integrates `movement += Δt·rootTrans.z` and moves the body that far — so feet plant by construction (no separate stride constant; scale k=1). Idle anims have z=0 (no travel); walks carry their speed (e.g. RAPWWR z≈20). The viewer reads this: `g_walkPos += dt * forwardSpeedAt(anim, animTime)`. The `.MOD` physics (MoverMass/drag/MaxAcceleration) governed thrust *vehicles* (RP hovercraft `VTVPWR`, camera ship) + collisions, NOT walk gait. NOTE: the BT walking-mech mover/mapper code is NOT in the source dump (BT here is weapons-only; the mech consumer of Animate() is absent) — but the per-clip speed is fully recoverable from the `.ANI` data above. **✅ P3 LOCOMOTION CUTOVER — STEP 1 DONE (`docs/P3_LOCOMOTION.md`).** btl4's live drive (mech4.cpp `PerformAndWatch`) is now ANIMATION-DRIVEN: the engine `AnimationInstance::Animate(dt,True)` return `adv` (the clip's per-frame `[RootTranslation]` distance) drives the forward step (`localOrigin.linearPosition += facing * adv`), replacing the hand-tuned `kDriveMaxSpeed` slide. Verified: mech walks at the clip's AUTHENTIC ~60 u/s (Blackhawk run; the guess was 30), forward, feet plant (travel==stride), combat+damage un-regressed. NOTE: the reconstructed two-channel gait (mech2.cpp `AdvanceLeg/BodyAnimation`, `IntegrateMotion`) is bracketed by no-op stubs (`ReconSeq` controllers → Advance()=0; `Matrix34`=`ReconMatrix` no-op), so the full gait (STEP 7) needs those backed by real engine `AnimationInstance`/affine-matrix first. Plan + 7 steps in `docs/P3_LOCOMOTION.md`; the smallest milestone (STEPs 1-2, real per-clip speeds on the proven engine path) is the near-zero-risk win. **✅ P3 STEP 7 CUTOVER — the real gait controller drives locomotion LIVE (gated `BT_GAIT_CUTOVER=1`; verified).** The BT-specific `SequenceController` (`decomp/reconstructed/seqctl.cpp`, reconstructed from binary `SelectSequence@004277a8` + `Advance@0042790c`) is now reconstructed, built, and wired: the Mech ctor `Init`s `legAnimation`/`bodyAnimation`, and `PerformAndWatch` (mech4.cpp, cutover branch) drives the forward step from `bodyAnimation.Advance(dt,1)` (the real controller: parses the clip, animates the skeleton joints via the engine `Joint` API `SetHinge/SetRotation/SetTranslation`, returns the root-translation forward step) instead of the STEP-1/2 free-standing `AnimationInstance`. Verified live under cdb: `[gait] SequenceController -> blhrrl id=904 (run)`, mech walks (adv≈1.07, matching baseline), 0 crashes; combat is byte-for-byte un-regressed vs a same-harness baseline run (both fire/damage identically). **KEY BUG FIXED:** clip fetch was `SearchList(clip_id,16)` (which treats its arg as a resource LIST → walks the clip bytes as garbage IDs → av); the clip_id from `ResolveAnimationClip` is a DIRECT resource ID → use `FindResourceDescription(clip_id)`+`Lock`, mirroring the engine's own `AnimationInstance::SetAnimation` (JMOVER.cpp:1406) EXACTLY. **DECOMP FACT — CORRECTED (re-verified vs `Advance@0042790c` raw, part_003.c:6878):** the earlier "lumpy distance" claim here was WRONG — the binary's partial-frame branch DOES accrue `(t-currentTime)*stride[currentFrame]` (distance is CONTINUOUS, matching seqctl.cpp:298), and `IntegrateMotion` (@004ab1c8:14997) integrates `localVelocity.z = -adv/dt` directly with NO smoothing. **FOOT-PLANT FACT (raw FUN_004ab430:15076):** the binary advances the BODY controller with `move_joints=1` — the DISPLAYED joints and the travel distance come from the SAME Advance (same clip/phase), so feet plant by construction; the leg channel (also hardcoded `move_joints=1`, FUN_004a5028:12006) runs FIRST and its joint writes are overwritten. Port matched in commit 01312e8 (body channel `Advance(dt,1)`); the same commit also added a ground-velocity smoothing (averaging adv/dt across keyframe spans) that is NOT in the binary — a deliberate deviation; if residual micro foot-slip is ever chased, that smoothing is the first suspect. Cutover is kept GATED (not default) pending two flagged fidelity refinements: (a) end-of-clip uses `carryover*stride[last]` in place of the binary's recursive finished-callback `@0x48` (the source of the occasional adv spike at clip-loop boundaries); (b) partial-frame rotation uses component-LERP vs the binary's slerp (`FUN_00409390`). The deeper full-`IntegrateMotion` velocity path (leg+body channels + world-step transform + `DeadReckonPose`, smoothing the lumpy step) is the remaining STEP-7 work. **✅ STEP 7 — the AUTHENTIC world-step (velocity model) now drives the mech LIVE (under `BT_GAIT_CUTOVER`).** The cutover no longer slides the position directly (`localOrigin += facing*adv`); it runs the real `IntegrateMotion` motion-tail (`@004ab1c8`): `localVelocity = {0,0,-adv/dt}` (forward = local −Z) → rotate into world by the body orientation via the now-**backed `FUN_00408744`** (part_000.c:8331, a matrix×vector; backed with the engine `AffineMatrix::GetFromAxis` column basis — the same convention the drive facing uses, so it is sign-consistent) → `localOrigin.linearPosition += worldVelocity*dt`. Result == `facing*adv`, but through the authentic velocity→rotate→integrate machinery (runs `FUN_00408744` live). **KEY DECOMP FACT (verified):** the 1995 motion transform `{ Point3D @0x260; Quaternion @0x26c }` **IS the engine `localOrigin`** — `Origin = { Point3D linearPosition; Quaternion angularPosition }` (ORIGIN.h:15), and raw `FUN_0040ab44` builds the matrix from BOTH halves (rotation from 0x26c, translation from 0x260). So `IntegrateMotion`'s motion state maps DIRECTLY onto `localOrigin` (NOT the parallel `motionDelta`/ `worldPose` members the earlier draft declared — those were placeholders). Also fixed a latent `IntegrateMotion` bug (unexercised while the fn was dead): it wrote only the separate `spinRate`@0x508 and left `angularAccum[2]` (the velocity.z the world-step reads) stale → now sets it. Verified live under cdb: walk-only (`BT_GAIT_CUTOVER=1`) walks straight forward at the authentic ~60 u/s, **0 crashes**; combat fire/damage identical to STEP-1/2. **Remaining full-`IntegrateMotion` work:** the `AdvanceBodyAnimation` gait STATE MACHINE (stand→walk→run transitions) + `LoadLocomotionClips` (real, mech3.cpp:326 — populates `namedClip[]`/gait-speed caps via `legAnimation.SelectSequence`) in place of the inline clip-select; orientation INTEGRATION (turn via angular rate, not the bring-up `gDriveHeading`); and velocity STORAGE for MP dead-reckon. ⚠ **PRE-EXISTING crash observed (NOT locomotion):** a combat run walked into a building's interest range and faulted in the ENGINE BGF loader (`LoadBgfFile("bld08.bgf")` → `Builder::~Builder` `vector` teardown → bad `operator delete`, bgfload.cpp:491) — a heap corruption in model loading, hit position-dependently (the STEP-1/2 run with a different spawn fired 600+ shots clean). My world-step does zero heap ops + walk-only is clean, so it is not the cause; likely the known enemy-spawn over-`sizeof` or a fragile BGF Builder for certain building assets. Track separately. **✅ STEP 7 steps 1-2 — `LoadLocomotionClips` + the real gait STATE MACHINE drive the walk LIVE (`BT_GAIT_SM`).** (1) **`LoadLocomotionClips`** (mech3.cpp:326, real) is now called in the Mech ctor (gated `BT_GAIT_CUTOVER`) and completes cleanly for the Blackhawk — it resolves the full gait clip set (`swr/wwr/wwl/rrr/rrl/...`) + measures the speed caps via `legAnimation.SelectSequence`+`keyframeData` (verified real: `walkStride=22.02`, `standSpeed=6.83`). **KEY RECONCILIATION:** `namedClip[]`@0x5e0 and `animationClips[]`@0x5cc were declared as SEPARATE arrays but in the binary are ONE (`namedClip[i]==animationClips[i+5]`, 0x5e0==0x5cc+0x14) — so `LoadLocomotionClips`' writes weren't visible to `SetBodyAnimation`/`MeasureClipStride`. Fixed: `namedClip` is now a pointer aliased to `&animationClips[5]` in the ctor (same storage). (2) **`AdvanceBodyAnimation` state machine** (mech2.cpp, real) now drives the walk under `BT_GAIT_SM` (case 6/7: slew `bodyCycleSpeed` toward `bodyTargetSpeed` with the loaded caps → `bodyAnimation.Advance` the `wwr` clip → world-step). Fixed **`bodyAnimationState`@0x728 == `bodyStateAlarm.level`** (also declared-separate; re-synced from the alarm each frame so `SetBodyAnimation`'s level change reaches the switch). Verified live: mech walks forward (z 1600→−975), `adv≈0.35-0.42` at the authentic walk cadence (correctly slower than run's 1.07), clip loops through its 15 keyframes, 0 crashes. **Bring-up substitute (marked):** `SetBodyAnimation` passes a non-null loop sentinel `(void*)1` where the binary passes `PTR_LAB_0050d6fc` — the real body finished-callback (gait-state TRANSITION stand→walk→run + leg alternation, @0x48), NOT yet reconstructed; without it the cycle clip would stick at its last keyframe (`adv=0`). **✅ STEP 7 — the REAL gait TRANSITION CALLBACK reconstructed + wired (walk/run leg alternation + walk→run).** Replaced the `(void*)1` sentinel with the real `FUN_004a6d8c` = `Mech::BodyClipFinished`. RESOLVED the `.data` fn pointer by PE-parsing (`0x50d6fc`→body cb `0x4a6d8c`; `0x50d6f0`→leg cb `0x4a6928`) — not in the decomp, so DISASSEMBLED with capstone + decoded the jump table (`@0x4a6e0a`; 33 states→10 handlers). Reconstructed byte-for-byte: at end-of-clip it dispatches on `bodyAnimationState`, compares `bodyTargetSpeed` to the loaded caps (`standSpeed`/`walkStrideLength`/`reverseSpeedMax`), picks the next state, re-arms it (`SetBodyAnimation`, which re-binds the clip WITH this same callback), and recursively advances the carryover (shared tail `0x4a6e66`/`0x4a6ed1`/`0x4a7001`). Walk alternates 6↔7, run 12↔13, stop→8/9, walk→run 6→11→12. **Plumbing:** `SequenceController::Advance` now CALLS `cbCode` as `Scalar(*)(Mech*,unsigned,Scalar,int)` at end-of-clip (matching the binary's `**(code**)(this+0x48)`), folding the returned distance; inline cutover uses `Mech::LoopBodyClip`. **Verified live:** walk 6↔7 (adv≈0.42), run 12↔13 (adv≈0.5-0.85), walk→run fires, pos advances, 0 crashes; inline cutover + combat un-regressed. **Bring-up gap fixed:** `reverseSpeedMax2`@0x7a0 (run bodyCycleSpeed clamp, AdvanceBodyAnimation case 12/13) is unset by LoadLocomotionClips → read `0xCDCDCDCD` → run exploded (`adv=-7e6`); set to `reverseStrideLength` (the case-12 divisor → ratio≈1). **DECOMP TECHNIQUE:** resolve a Ghidra `PTR_LAB_xxxx` .data fn ptr by PE-parsing the DWORD at its VA, then capstone-disassemble the target (`/tmp/readptr.py`+`disas.py`) — for any callback/vtable slot the assert-anchored decomp didn't export. **Remaining:** the LEG callback `FUN_004a6928` (leg alternation, state@0x3b0 + motion source@0x128); airborne `FUN_004a6344`; gimp handlers (states 16-19, targets `0x70b2`/`0x7161` undecoded → fall back to stand); gait SELECTION from real throttle (`MechControlsMapper` still bypassed; SM forces walk-6 then the callback transitions). - **✅ MoveAndCollide — the mech now COLLIDES with world solids (engine Mover pipeline; gated `BT_COLLISION`).** The bring-up drive wrote `localOrigin.linearPosition` directly with no collision, so the mech phased through terrain/objects + floated. KEY FINDING: the 2007 RP411 engine models **terrain AS `BoxedSolid` collision volumes** (`TERRAIN.h`: `Terrain` = an `Entity` with `collisionVolumes`), so terrain-follow AND object collision go through ONE path — the engine `Mover` collision pipeline (`MOVER.cpp`), NOT the 1995 heightfield query (`FUN_0040e5f0`, which the dead `Simulate` used). Wired into `PerformAndWatch` (mech4.cpp) after the drive's world-step: snapshot the pre-move pos → set `worldLinearVelocity` from the move delta → `MoveCollisionVolume()` (reposition the volume to `localToWorld`) → `GetCurrentCollisions()` (gather overlapping solids) → `ProcessCollisionList(cols, dt, oldPos, &damage)` (the engine resolves each penetration via `ProcessCollision`→`StaticBounce`, WRITING `localOrigin.linearPosition` to push the mech out; MOVER.cpp:1162/1200/1217). All methods are public on `Mover`; guarded on `GetCollisionVolumeCount()>0` so it is a no-op for volume-less mechs. **Verified live:** in the cavern the mech walks flat (Y=−0.35), hits a building solid at z≈150 and is pushed out once (x+7, y+2.9) over ~6 frames then continues stably (Y holds, no drift/fly-away); combat un-regressed (structure→0.533, 0 crashes, 109 collide events while fighting). **✅ GRAVITY / terrain-settle — DONE (same `BT_COLLISION` block; tunable `BT_GRAVITY`, default 20 u/s).** Collision alone only pushes OUT of solids (no downward force → a floating mech never came down). Added a per-frame downward Y (`localOrigin.y -= kGravityRate*dt`) BEFORE the collision resolve, so gravity presses the mech down + the collision holds it at the terrain surface (equilibrium ON the ground). SAFETY NET: a FLOOR CLAMP at the spawn/ground Y (captured on first frame) — on a map with NO terrain solid the mech rests at ground level instead of falling forever; on real terrain (solids) the collision catches it above the floor so it follows hills/valleys. **Verified live:** grass — mech settles at Y=0 (clamp; flat grass has no ground solid) and climbs a terrain feature to Y=4.68; cavern — settles at the floor Y=−0.35, stable while walking, combat un-regressed (structure→0.933), 0 crashes. So the mech now STANDS + walks on the ground + climbs terrain + collides with objects. **Remaining refinements:** (b) wall-blocking vs climb — the engine resolves along the surface normal, so a wall-top contact pushes UP (climb) rather than blocking forward; may need game-tuning. (c) collision damage is computed by `ProcessCollisionList` but not applied here (bring-up). Kept GATED (`BT_COLLISION`) pending (b) + confirmation on more maps. - **✅✅ THE AUTHENTIC 1995 GROUND MODEL — DECODED, IMPLEMENTED, DEFAULT-ON (task #15 CLOSED for ground; `BT_GROUND_REAL`, =0 falls back to the gravity+push baseline above).** Product of the 10-agent `ground-model-decode` workflow (6 mappers over binary/1995-source/2007-engine/postmortem/content + synthesis + 3 adversarial verifiers, all SOUND). **THE MODEL (binary FUN_004a9b5c = the MASTER per-frame performance, PTR_LAB_0050c0f4, decoded from raw asm — the "undefined 5.6KB gap" in mech4's window; FUN_004ab430 is the REPLICANT interior only, NOT "Simulate"):** there is **NO GRAVITY anywhere in the mech** (accelerations zeroed every frame @4a9cb6). Vertical position comes exclusively from an ABSOLUTE per-frame ground snap: (1) ctor lifts `collisionTemplate->minY` by 5% of volume X-width (BLH: 2.0+0.37969=2.37969 — the probe height AND the wall-vs-floor separation); (2) per frame: `MoveCollisionVolume()` (rebuilds localToWorld, places volume, re-finds `containedByNode` in the zone's **BoundingBoxTree** — BOXTREE.cpp, identical 1995→2007, NOT a heightfield); (3) probe `q = origin + (0, lift, 0)` → `BoundingBoxTreeNode::FindBoundingBoxUnder(q, &h)` (h = distance down to the highest solid top; −1 = miss); (4) snap gate `h > 1e-4` (@0x4ab16c): `origin.y -= (h − lift)` ⇒ y = surfaceY EXACTLY (up-slope bounded by the lift window = implicit step allowance; walk-offs drop instantly; **MISS ⇒ NOTHING** — y holds, no runaway possible); (5) `GetCurrentCollisions` → `ProcessCollisionList` → per-contact **`Mech::ProcessCollision` override (FUN_004abb40, vtbl+0x3c** — the fn the old recon misread as "ResolveWeaponImpact"): BoxedSolid resolver → StaticBounce → owner classification — Mover owner = separating-gate + (deferred) mech damage; CulturalIcon owner without `StoppingCollisionVolume` (flags&0x8000) = **crushable, damage=0.00123f sentinel**; plain UnscalableTerrain (cliffs/canyon walls/most buildings) matches neither branch; (6) response policy: sentinel = move STANDS (restore post-snap saves); damage>0 = **FULL FRAME REJECTION** (velocity zeroed, origin=start-of-frame — walls block by rejection, never slide/climb); impactVel²>40 = crash-anim trigger (SetLegAnimation(0x20) — deferred, logged). Implementation: `Mech::AuthenticGroundAndCollide` (mech4.cpp) called for the player (post-drive) AND every non-player master; ctor half in mech.cpp; shared gate `GroundReal()` (mechrecon.hpp). **KEY FIXES/FACTS:** (a) FUN_0040e36c/FUN_0040e5f0 are `BoundingBoxTreeNode::FindSmallestNodeContainingColumn`/`FindBoundingBoxUnder` (BOXTREE.CPP:503/867), ALIVE in the linked 2007 engine — the "Terrain::CellAt/HeightAt heightfield" labels were wrong (the cause of both reverted attempts: v2 assigned the DISTANCE h as an absolute height → the y=13301 ladder). (b) binary 0x2e8/0x2ec/0x2f0/0x2f8 = ENGINE Mover `collisionVolume`/`collisionTemplate`/`containedByNode`/ `lastCollisionList` (NOT game-layer "physicsBody/groundRef/groundCell"). (c) grass DOES have a ground slab (gr100_cv Block Y[−50,0]) — the old "flat grass has no ground solid" note was wrong. (d) **a master mech needs a CollisionAssistant** (GetCurrentCollisions iterates it UNCHECKED, MOVER.cpp:894): only the viewpoint mech gets one (MakeViewpointEntity); the BT_SPAWN_ENEMY dummy now `StartCollisionAssistant()`s at spawn + the block has a defensive assistant guard (in the real game exactly ONE master exists per pod, so the invariant held by construction). (e) BT SOLIDS ship ZERO tiles (only Block/YCyl/Cone/Ramp/Wedge, all FindDistanceBelowBounded clamped ≥0) → h is never negative in BT. **VERIFIED (render-side + telemetry):** grass census lift=0.379688/minY=2.37969 exact, y=0 EXACT on the slab; arena 65s anti-ladder soak y pinned 0, wall BLOCKs hold (no creep); dbase two full hill climbs (y→24/y→31) + exact floor return −0.453125; screenshots standing/climbing; combat DESTROYED after the same ~8 hits; baseline (=0) byte-identical; 7-min BT_HEAPCHECK soak clean. **EXPECTED-AUTHENTIC (do not "fix"):** sticky-ledge frame rejections walking off 2.4-5.9u terraces; crushable props pass through WITH crunch; canyon backdrops (rav pwin*)/arena detail have no solids = pass-through; over void y HOLDS (no fall). **✅ CRASH/KNOCKDOWN LIVE (commit 6452ab7):** wall impacts above iv2>40 (~6.3 u/s) bind the `bmp` bump/stagger clip — `SetLegAnimation(0x20)` + `RequestActionFlags(1)` + `(0x20)` (binary @4aaae2; FUN_004a4c54 == RequestActionFlags, NOT "ForceUpdate"). ALIASING FIX (namedClip bug class): `gimpBaseClip` @0x64c IS `animationClips[0x20]` (0x5cc+32*4) — bmp now loads into slot 32; the clip table is zeroed in the ctor. Recovery = LegClipFinished case 32 → SetLevel(0) (already reconstructed). BRING-UP GUARD (marked): no rebind while legState==0x20 — the bmp clip's ~6.5 u/s root motion re-contacts the wall and the `actionRequestFlags` consumers (likely the stagger drive-suppressor) are unreconstructed; without the guard the clip restarted every frame. Verified: knockdown → play out → stand → walk → re-knockdown at ~3s cadence at a butte wall; mid-stagger screenshot ON the surface; combat clean. **Why leg-burial appeared at walls before this:** frame rejection freezes pre-snap Y while grinding (the snap is undone every rejected frame) — the knockdown is what prevented sustained grinding in the original. Related content facts (measured, tools/mapscan.py + BGF triangle sampling): dune mounds visual≈solid (median 0.2u, p90 1.1u); buttes deviate ±5u typical / ±15-25u at cliff faces — authentic 1995 coarseness. The transform chain (quat→matrix identical formulas both paths, row-vector convention, ToD3DMatrix raw copy) was audit-verified — no visual-vs-solid placement bug exists. **✅ KNOCKDOWN RE-TRIGGER STUTTER — FIXED (user: "leg animation glitchy/stuttery when I run into an obstacle, fixes if I fully stop"; reproduced + spec-confirmed).** Running INTO a blocking solid (a wall, a steep terrain flank, OR the spawned dummy MECH — not slope-specific) at speed made the leg animation flip-flop between the stagger clip (legState 0x20) and walk. Repro (drive into the dummy on grass, `BT_GROUND_LOG`): the FIRST hit at run speed is a genuine knockdown, but then the mech is pressed against the obstacle with throttle held, so every recovery frame it steps ~6.9 u/s back into it and the `iv2>40` (=6.32 u/s) knockdown RE-FIRES → rapid freeze↔walk = the stutter (BLOCK=319 / **CRASH=107** in 22 s; leg flip-flopping 5↔32). Full stop clears it (velocity→0→iv2<40). **The ground-model spec (scratchpad `gd_spec.md`, the hand-decoded master perf `FUN_004a9b5c` @4aaae2) shows the binary's mitigation: `localVelocity = {0,0,−adv/dt}` is ZEROED while crashed (@4aa3d3), so a knocked-down mech can't keep advancing into the obstacle** — our reconstruction re-derived localVelocity from the position delta and never zeroed it, and re-armed the knockdown every fresh block. TWO fixes (mech4.cpp): (1) **crash motion suppression** — while `legState==0x20`, zero `worldLinearVelocity` + `localVelocity.linearMotion` (spec-faithful; no mid-stagger creep, no re-detect during the clip); (2) **contact-hysteresis knockdown gate** — the knockdown fires only on a FRESH strike, where "fresh" = the mech has been OUT of blocking contact for `kBlockHysteresis` (0.4 s). A 1-frame edge (`!gWasBlocked`) was tried FIRST and FAILED for the user: a GLANCING / sliding contact alternates blocked↔not-blocked (the frame rejection pushes the mech out each blocked frame), which flickers a 1-frame edge back to "fresh" and re-knocks-down — that intermittent case is exactly what a head-on force-throttle repro never hits (it shows a clean 1 bind, hiding the bug). The 0.4 s `gBlockCooldown` window bridges those gaps: any sustained OR intermittent press just BLOCKS (runs/walks in place), only a real separate-then-re-approach re-arms it. `gBlockCooldown` is a single global gated to the VIEWPOINT mech (the stationary dummy + MP masters also call `AuthenticGroundAndCollide`, so they keep the original per-fresh-block behavior and never touch the shared state). **Verified across cases:** dune-beeline **21→1**, head-on dummy **→1**, terrain-crossing (BLOCK=497) **→1**; still blocks (no phase-through); combat un-regressed (TARGET DESTROYED, 0 crashes / 0 heap); normal walking unaffected (suppression only triggers on legState 0x20). ⚠ DIAGNOSTIC LESSON: a head-on repro gives 1 bind even with the brittle edge gate — the bug only shows on INTERMITTENT contact (glancing / sliding / rough-terrain), so reproduce with an angled or terrain-crossing approach, not a straight ram. NOTE: holding full-throttle into a wall still yields ONE knockdown then a clean block — a real player releasing throttle sees just the single stagger. **✅ TWO-CHANNEL GAIT DESYNC — FIXED (the ACTUAL "legs stay glitchy after a bump until full stop" bug).** The knockdown re-trigger work above reduced the crash spam but the user reported the legs stayed glitchy AFTER moving away, only clearing on a full stop. ROOT CAUSE (measured, not guessed): the real-controls gait runs TWO animation channels — the BODY channel draws the displayed legs (`AdvanceBodyAnimation(dt,1)`, runs last, wins the joint writes) while the LEG channel drives the mech's TRAVEL (`localAdv = legAdv`). In normal walking they run in EXACT lockstep (the `[sync]` log: `advSum == legSum` frame-for-frame). The knockdown staggered ONLY the leg channel (`SetLegAnimation(0x20)`), freezing TRAVEL while the body channel kept animating the DISPLAY -> the two clocks split PERMANENTLY (measured `advSum=229` vs `legSum=112`: the displayed legs animate ~2× the real travel = foot-slip) and never reconverged until a full stop reset both to Standing. **FIX (one line):** the knockdown now staggers BOTH channels — `SetBodyAnimation(0x20)` alongside `SetLegAnimation(0x20)` — so display + travel freeze and recover from the SAME bmp clip IN PHASE (`BodyClipFinished` case 32 recovers the body, mech2.cpp:308, mirroring the leg). **Verified:** `advSum == legSum` stays EXACT through+after the bump (was 229 vs 112); mech still BLOCKS the obstacle (BLOCK=328, motion stays on the leg channel so no tunnel-through); combat un-regressed (DESTROYED, 0 crashes). NOTE: the now-correct behavior is a FULL STOP + stagger on hard contact (authentic: frame-rejection + the iv2>40 crash anim); the old "run through / run in place" was the desync bug, not original behavior. **✅✅ v4 — DESYNC CLASS KILLED FOR GOOD: travel now comes from the BODY channel (the SAME Advance that writes the displayed joints) — BINARY-FAITHFUL (raw FUN_004ab430:15076 advances the body with move_joints=1 and takes travel from it).** The stagger-both-channels fix (above) killed the KNOCKDOWN trigger, but the desync CLASS resurfaced on the Mad Cat + the analog throttle lever: the lever sweeps the demand THROUGH the walk/run boundaries, and each channel's end-of-clip callback samples the demand at a different instant — near a threshold they pick DIFFERENT next states → permanent phase split → the foot-slip again. v4 sources `localAdv = adv` (body) so display == travel BY CONSTRUCTION — no two-machine agreement needed, ever. The earlier "tunnels through obstacles" objection to exactly this change is OBSOLETE: it predated `SetBodyAnimation(0x20)` — with the knockdown staggering the body channel too, a hard impact freezes travel correctly. RE-VERIFIED: head-on run-speed ram into the dummy = pinned at its face for seconds (BLOCK=801, 1 knockdown, NO tunnel-through, then a normal grazing slide around the corner); combat DESTROYED on BOTH BLH and Mad Cat, 0 crashes. The leg channel keeps running as the binary's local sim (its joint writes land first and are overwritten by the body's). The full `actionRequestFlags`(1/0x20) drive-suppressor + crashed(0x650) lifecycle remain unreconstructed (master-perf ASM gap); the edge gate is the faithful stand-in until they land. **✅ RESIDUAL-SINK ALIGNMENT AUDIT — quantified; verdict = authentic coarseness, NOT a bug or a mesh error (dbase; scratchpad audit scripts).** After the CONN trilist fix, the user still noticed "sink in a bit" in some spots and asked for a triangle-error audit. TWO sweeps run: (1) **GEOMETRY** — all 877 pod GEO BGFs / 127,171 tris: **0** CONN chunks with n%3≠0 (fan-bug corpus-complete), **0** out-of-bounds indices; the only 52 anomalies are **zero-area (repeated-index) no-op** tris, ALL in props/vehicles (AB01/ARBAN/BULLET/MSLBASE/ TK1/WL*), NONE in terrain → no mesh defect causes sinking. (2) **VISUAL-vs-COLLISION deviation** — per dbase terrain model, sampled each visual vertex's Y vs the analytic `.SLD` surface (Cone apex-up / YCyl / Block) at its XZ; worst clip in the walkable band (Y<15): **mesas (buttea-e) 2–3.5u**, **dunes (dhillg1-3) 0–3.1u**, **flat pad (calpb) 1.6u** (⇒ NO global snap offset — the probe/lift is correct), **buildings (bld06/bld08) 12–14u = the outliers**. Butte "overall" 23u is on the VERTICAL CLIFF FACES (unwalkable, the stepped-cylinder cliff approximation — faithful). **KEY per-feature facts:** dbase terrain is placed by class-42 instances (NOT the class-94 vehicle props); mesa collision = a STACK of YCyls + a base Cone whose footprint is ~5–22u narrower than the visual skirt (the small skirt clip). **Building collision = separate COLUMN/WALL Blocks with GAPS** (bld08=26 blocks, bld06=7), and the visual has floors/overhangs spanning the gaps → a mech walks into a gap / under an overhang where there's no solid and its legs clip the visual (THIS is the user's "some things are solid, some aren't" + the biggest residual sink). ALL authentic 1995 coarse collision — the original pods clipped identically; the only "fix" (gap-filling collision on buildings) is an intentional content DEVIATION, not a port correction → left as-is by default. **DEFERRED (bound together, marked in code):** collisionTemporaryState zero @4aa741 + ProcessCollision state tail; 0x4a4/0x4a8/0x4b4 caches (material/local normal/approach speed); the TakeDamageMessage dispatches (zone=−1 dropped by the engine handler today + inflictor global DAT_0050b9ac unmapped); SetLegAnimation(0x20) crash anim; gyro crunch feed; duck maxY swap wiring; replicant-path snap (MP — gate `h != −1`, part_012.c: 15144-15167, the 0x4ab9d8 wrapper). Full spec + verdicts: scratchpad gd_spec.md/gd_verdicts.md (session); the workflow reports are the durable citations. - **Renderer dependency** is isolated to 3 HAL files in the original (`L4VIDEO/L4VIDPER/L4VIDRND` call `dpl_*`). The full `dpl_*`/`dpl2d_*` API surface the game uses is cataloged (see chat history / shim contract). --- ## 8. Roadmap: viewer → playable **REVISED after finding the WinTesla Windows port (§5b).** The original "build a shim + port the engine" plan is largely **already done** in WinTesla. New plan: - **Track 1 — Red Planet build (integration spike measured the gap):** `WinTesla.sln` is **VS2005** (Format 9 / vcproj v8), **225 source files**, and hard-depends on the **legacy DirectX SDK (Jun 2010)** — it includes ``, ``, ``, ``, all REMOVED from the modern Windows SDK (only `d3d9.h` remains). OpenAL32/libsndfile libs are in `trunk/lib`. So building RP is a real **toolchain-modernization sub-project**, not a recompile: (1) ~~obtain legacy DXSDK~~ **DONE — installed at `C:\Program Files (x86)\Microsoft DirectX SDK (June 2010)\` (Include + Lib\x86 present: d3dx9/dinput/dxerr/dxguid).** Note: the installer threw **S1023** (harmless — caused by a newer VC++2010 redist v10.0.40219 already present blocking the bundled 10.0.30319; the SDK files install before that failing step, so they survive — just dismiss the error), (2) drive the 225 files via CMake (relative `..\munga\` includes → compile each from its project dir or add all dirs to the include path) or upgrade the vcprojs, (3) fix VS2005→VS2019 C++/header drift across the tree, (4) link d3dx9/dinput8/dxguid/dxerr + OpenAL/libsndfile. Bounded but a dedicated session; best done where the DXSDK can be installed normally. Validates the platform + yields a playable RP. **BT integration = same build + drop in the reconstructed `decomp/reconstructed/` modules.** **✅ ENGINE COMPILES GREEN** (`C:\git\nick-games\enginebuild\`, CMake/MSVC Win32) — the shared `Munga_L4` 193-file engine+HAL builds to `munga_engine.lib` (0 errors). Recipe (4298→0 errors): (a) include path = **DXSDK Include + `enginebuild/shim` ONLY** — do NOT add MUNGA dirs (they shadow `` → 3840 errors); rely on the code's relative/quoted includes. (b) defines: `WIN32 _WINDOWS UNICODE _UNICODE NOMINMAX _CRT_SECURE_NO_DEPRECATE _CRT_SECURE_NO_WARNINGS _SILENCE_STDEXT_HASH_DEPRECATION_WARNINGS _SILENCE_ALL_CXX17_DEPRECATION_WARNINGS`. (c) opts `/permissive /W0 /EHsc /bigobj /MP`. (d) source edits: `MUNGA/CAMMGR.cpp` `std::ios.in`→`std::ios::in` (2007 typo); add `Time(const Time&)` copy ctor to `MUNGA/time.h`. (e) `enginebuild/shim/atlbase.h`+`atlconv.h` = minimal ATL `USES_CONVERSION`/`W2A` shim. Build Unicode (matches original — wide literals → `...W` APIs). - **Track 2 — BattleTech:** reuse the WinTesla engine (shared MUNGA/MUNGA_L4/L4D3D/audio/build); add a `BT410_L4` + BT game library by **reconstructing the missing BT `.cpp`** (mech, subsystems, HUD, app) from: BT headers + RP's parallel code (§5b) + `btrel410.exe` oracle + pod content. This is the real remaining work for BT — bounded to game logic, since the engine/renderer/HAL/audio are done. **→ NOW LIVE (§10):** `btl4.exe` boots + renders + runs a drive→animate→target→fire→damage→destroy single-player loop; the active work is reconstructing each subsystem's authentic behavior (damage zones first, then weapon/heat/sensor subsystems — they are still `RECON_SUBSYS` stubs). See §10 for the build/run/debug recipe, the no-stand-ins reconstruction method, and the per-subsystem decomp findings. Old roadmap below is superseded for Phases 1–3 (engine/renderer/HAL/audio = done in WinTesla); Phases 4–8 still apply to BT's game layer + pod bring-up. The from-scratch `libDPL` shim is **no longer needed** (WinTesla replaced libDPL with L4D3D outright). (Historical plan — keep for reference of remaining game-layer/pod work:) - **Phase 0 — Foundation ✅ (done):** asset formats cracked + documented; D3D9 viewer renders skinned, skeleton-assembled, animated mechs. Renderer bypass proven feasible. - **Phase 1 — `libDPL` shim as a real API:** turn the viewer renderer into a drop-in lib exposing the actual `dpl_*` (3D) + `dpl2d_*` (2D) C functions the game calls, backed by D3D9. *Linchpin: once the API matches, the game's own rendering code compiles unchanged.* Risk: matching semantics (fog, LOD, materials, `dpl_SectPixel` picking, 2D display lists, morph/damage/articulation). - **Phase 2 — Compile the game code (modern toolchain):** get `MUNGA` + `BT` C++ building on MSVC/Clang — modernize Borland-isms, replace `.asm`, stub the `L4` layer — linked against the shim. Goal: it *builds*. Risk: pervasive non-standard 1995 C++; DOS/DPMI assumptions. - **Phase 3 — L4 HAL on SDL/Win32:** replace `L4` video-out, input, timing, file I/O. Goal: the game first *runs* — boots to menu / loads a mission. - **Phase 4 — Cockpit (HUD/controls):** `dpl2d_` HUD + `.pcc`/`.gim` gauges + radar; map RIO cockpit controls API → keyboard/gamepad; data-driven multi-surface output (dev windows vs pod's 7 monitors). - **Phase 5 — Gameplay loop:** one single-player mission end to end — drive, fire, heat, damage, AI bots. → **playable (single player)**. - **Phase 6 — Audio:** HMI "SOS" sound engine → modern mixer (SDL_audio / similar). - **Phase 7 — Multiplayer:** ⚠ **CORRECTED (scope-hardest workflow, verified):** NOT "reimplement over UDP" — the WinTesla port ALREADY replaced the DOS `NETNUB` driver with a **3446-line WinSock2 TCP** reimplementation (`MUNGA_L4/L4NET.CPP`, `SOCK_STREAM`/`ReliableMode`, zero `SOCK_DGRAM`) and the master/replicant distributed- sim core (InterestManager/Entity/HostManager) is complete. So multiplayer = **integrate + smoke-test that existing TCP stack**, gated on P3 (locomotion update writer) + P5 (entity create/destroy lifecycle) + subsystem WAVEs. Unknown: whether that TCP path has ever run successfully even in RP (a send-size bug at `L4NET.CPP:1853`). This is problem **P6** in `docs/HARD_PROBLEMS.md`. **⭐ P6 SMOKE TEST PASSED — TWO INSTANCES SHARE A WORLD (cross-pod entity replication live).** The full chain works on one box: console egg delivery → TCP mesh → synchronized mission start → bidirectional entity replication (each instance renders its own mech + the peer's REPLICANT; `[BTrender] mech tree built` ×2 on both sides; 0 crashes). HOW IT WORKS (workflow-mapped + verified): (1) `-net ` CLI arg = networked mode; the pod listens on for a CONSOLE and boots `ConsoleOnly`; game mesh port = +1. Solo (no `-net`) never touches WinSock. (2) The mission egg's `[pilots]` section IS the session roster (`pilot=ip[:port]` + a per-address page with hostType=0/name/vehicle/dropzone...); each pod self-identifies by local-IP+game-port match, CONNECTS to pilots listed before it, LISTENS for those after — a deterministic full mesh; mission loads when all peers connect ("All connections completed!"). (3) The CONSOLE (the operator station — ABSENT from every archive) delivers the egg as chunked `ReceiveEggFileMessage` packets; **`tools/btconsole.py` is our console emulator** (wire spec in its docstring, constants probed from the build via `BT_NET_PROBE=1`). ⚠ `NotationFile::ReadText` expects NUL-SEPARATED LINES, not raw file text (NOTATION.cpp:1043 walks strchr(buf,0)+1) — the tool converts. (4) One-box recipe: instance A `-egg MP.EGG -net 1501 BT_LOG=mp_a.log BT_AFFINITY=0x1`, instance B `-net 1601 BT_LOG=mp_b.log BT_AFFINITY=0x2` (new env vars: per-instance log + CPU pin), then `python tools/btconsole.py MP.EGG 127.0.0.1:1501 127.0.0.1:1601`. MP.EGG/MP1.EGG (2-pilot loopback / 1-pilot networked) live in `btbuild/` (copy to the pod BT dir). Egg entries MUST carry explicit :port on one box (else both instances self-identify as the same pilot). (5) **WIRE-FORMAT BUG CLASS FOUND+FIXED: MakeMessages replicate RAW over TCP, so string payload must be INLINE** — the reconstructed `BTPlayer::MakeMessage` carried `const char*` pointers (fine solo, garbage cross-pod: the first remote BTPlayer replication crashed in `CString(roleName)`); fixed to inline `char[0x40]` at the binary offsets (+0x50 teamName/+0x90 roleName, static_assert-locked, size 0xD0). CHECK EVERY MakeMessage for pointer payloads before it replicates (swept: no others currently). (6) The rumored L4NET.CPP:1853 send-size bug is DEAD CODE (`#if MESSAGE_BUFFERING` == enum True == 0 to the preprocessor); live send paths are correct. **⭐⭐ MOVEMENT REPLICATION COMPLETE — A's mech WALKS ON B'S SCREEN (replicant tracks the master to ~1.5u through dead-reckoning).** The full authentic mission-start ladder now runs: console egg → CreatingMission → (event-queue quiet timeout, APP.cpp:667 `QUIET_TIME_OUT` 3s) → LoadingMission → WaitingForLaunch → **console sends `RunMissionMessage` ×2** (the operator's LAUNCH button; `btconsole.py` sends them after a 20s settle + 4s step; ID=5, clientID=4, probed) → LaunchingMission → **RunningMission (state 5)**. SIX bugs/gaps found+fixed to get here (each verified live): (1) Mech ctor now `SetDeadReckoner(&Mover::AcceleratedDeadReckoner)` (the VTV pattern); (2) REPLICANT MOTION: `Mech::PerformAndWatch` runs `DeadReckon(dt)` (engine lerp toward projectedOrigin) for `GetInstance()==ReplicantInstance` instead of the drive; (3) MASTER EMISSION: the VTV error-threshold block (self-predict via own deadReckoner; `ForceUpdate()` on posErr²>0.04 / quat-w<0.997 / 2s heartbeat) — without it masters NEVER emit update records; (4) emission GATED on `RunningMission` — pre-launch update spam kept the PEER's CreatingMission event-queue from ever going quiet (a cross-pod deadlock); (5) the console must send the LAUNCH — **even solo had been running entirely on the `IsPreRunnable()` player-escape in a pre-launch state, forever** (nothing posts RunMissionMessage locally); solo still works that way (unchanged); (6) **REPLICANT VALIDITY: an invalid entity defers EVERY message as an event forever** (`Entity::Receive`/`Dispatch`, the '07 edit defers all) — replicants never get the master's MakeReady/CheckLoad handshake (the interest layer is partial), so their network updates never applied AND the deferred-event churn also blocked the quiet-timeout. FIX: `Mech::Make`/`BTPlayer::Make` `SetValidFlag()` replicant instances at creation (same fix the spawned test dummy needed). **Debug tooling added (env `BT_NET_TRACE`, permanent, in the ENGINE tree):** `[net-tx]`/`[net-rx]` (L4NET send/receive), `[net-upd]` (update entity lookup), `[upd-repl]` (replicant scheduling), `[ent-exec]` (Execute state ladder, 1-per-240 frames) — plus `[emit]`/`[repl]`/`[mech-exec]` in mech4.cpp under `BT_REPL_LOG`. Update stream measured: ~60 Hz × 144-byte records per moving master. **NEXT (P6 phase 4):** cross-pod combat (target a REPLICANT + damage routed to the owning master — Entity::Dispatch on a replicant already reroutes to the master via SendMessage), interactive 2-window driving, replicant gait animation (replicants don't animate joints yet — derive from replicated velocity), then the pod-LAN config (real IPs, both `-net 1501`, bare-IP pilot entries). - **Phase 8 — Pod bring-up (with Nick):** 7-monitor layout, old-driver compatibility, RIO cockpit I/O wiring, on-pod testing. **✅ PLATFORM PROFILE scaffold DONE (`-platform pod|dev`, or `BT_PLATFORM` env, or `run\run.cmd [EGG] pod`; default DEV).** The gate before Phase-4 HUD/MFD work: a boot switch selecting a runtime profile WITHOUT forking the codepath — it just picks WHICH environment preset the existing video/gauge/input code reads (btl4main.cpp env-defaults block). **KEY FINDING (a workflow map): the pod multi-surface path already EXISTS and is intact** — `DPLRenderer::FindBestAdapterIndices` (multi-adapter/monitor selector; honors PRIMGAUGE/SECGAUGE/MFDGAUGE/ SPANDISABLE), `SVGA16::BuildWindows` (L4VB16.cpp — a fullscreen D3D device + window per gauge/MFD surface, MFD span = width×2), `L4GaugeRenderer` (gated on `getenv("L4GAUGE")`, btl4app.cpp:353), and **`content/SETENV.BAT` IS the authentic pod env preset** (L4CONTROLS=RIO,KEYBOARD; L4GAUGE=640x480x16; L4PLASMA=com2). So the profile is an env-preset selector: **DEV (default)** = keyboard + single 800×600 window (no L4GAUGE → MakeGaugeRenderer NULL → single surface); **POD** = RIO cockpit input (→ keyboard fallback when no serial) + the multi-surface gauges/MFDs. Verified: dev un-regressed (boots/ticks/combat DESTROYED); POD boots on a dev box single-window, 0 crashes; both the CLI (`-platform pod` — ParseCommandLine tolerates it) and env (`BT_PLATFORM=pod`) forms work. ⚠ The multi-surface is a POD-HARDWARE capability (each surface needs its OWN fullscreen device on one of the pod's 2 video CARDS — monitor count alone is NOT a valid proxy), so `-platform pod` does NOT auto-enable L4GAUGE: it stays bootable everywhere as single-window + pod input; on the real pod the gauges come from SETENV.BAT (or an explicit L4GAUGE), running the UNTOUCHED FindBestAdapterIndices/SVGA16 path. Also fixed a real engine bug (L4VB16.cpp:201): `SVGA16::BuildWindows` PostQuitMessage'd on `CreateDevice` failure but fell through to a null `mDevice[j]` deref (segfault) → now `break`s (inert on pod where CreateDevice succeeds). **FOLLOW-UPS** (the deep work this scaffold gates): (1) **MFD compositing on a dev box** — render the gauges/MFDs into the single window / extra windows so POD mode is VISUALLY testable off-pod (the real "dev test of the pod HUD"); (2) **graceful multi-surface fallback** — the gauge renderer returns NULL instead of crashing when its surfaces can't be built, so a forced L4GAUGE on non-pod hardware degrades cleanly; (3) **pod main-view fullscreen/adapter tuning** (btl4vid.cpp:863 still binds the main 3D view to 800×600 windowed on `::GetActiveWindow()` — fine for dev + the pod's 800×600 main monitor, but pod fullscreen-on-a-chosen-adapter is a Phase-8 detail). The Phase-4 HUD/MFD render work now targets the profile's LOGICAL surfaces, not a hardcoded single window. **✅✅ GAUGE DEV-COMPOSITE — the pod's SECONDARY/RADAR MFD renders LIVE on a dev box (opt-in `BT_DEV_GAUGES`; full map + findings in `docs/GAUGE_COMPOSITE.md`).** The FOLLOW-UP (1) above is DONE for the secondary surface: an inset in the bottom-left of the 800×600 window shows the authentic BT cockpit secondary MFD — **radar/ tactical grid** (SCALE/SECTOR + crosshair), **SPEED/HEADING/MAGNETIC/PROP dials**, and the color-coded **ARMOR DAMAGE mech schematic** (green intact / red damaged) — REAL content (`nzSec=27247` non-zero pixels). The draw model is CPU-raster→texture-upload, DEVICE-INDEPENDENT: gauge widgets raster 16-bit R5G6B5 into ONE shared `Video16BitBuffered::pixelBuffer`; `SVGA16::DrawDevInset` (L4VB16.cpp) palette-expands the secondary plane into a MANAGED texture on the MAIN device + draws an `XYZRHW|TEX1` inset quad; `BTDrawGaugeInset(mDevice)` is called from `DPLRenderer::ExecuteImplementation` as the last draw before `EndScene`. **THREE recon bugs fixed to get real content (1 genuine fix + 2 gated dev accommodations):** (a) **⭐ `BTL4Application:: MakeGaugeRenderer` SIGNATURE MISMATCH** — the reconstructed no-arg override only HID the 2007-engine's widened 3-arg virtual `MakeGaugeRenderer(int*,int*,int*)` (APP.cpp:382), so the engine built a BASE `L4GaugeRenderer` whose ctor never calls `BuildConfigurationFile` → `gauge\l4gauge.cfg` was NEVER PARSED → `undefined label 'bhk1Init'` → no ports. FIXED to the 3-arg signature (args ignored; BT is fullscreen). Same bug class as the `BTL4GaugeRenderer(false,NULL,NULL,NULL)` ctor fix — recon written to the ORIGINAL signature vs the WIDER 2007 virtual. (The gauge label = `GetGameModel()`+`"Init"`=`bhk1Init`; CFG has `Bhk1Init` but the symbol lookup is `stricmp`, so case is irrelevant — the table was just empty. `MechInit`, CFG:4390, is the shared macro that builds the sec/overlay/Mfd/Heat/Comm PORTS via the base `configure` primitive.) (b) **parse HUNG on undefined primitives** — `BTL4MethodDescription` (btl4grnd.cpp) is a STUB (BT gauge widget classes unreconstructed), so an unknown primitive → `ReportParsingError`→`Fail()`→a MODAL dialog freezing mid-parse; gated skip (GAUGREND.cpp) walks past the unknown primitive's params so labels register + base `configure` builds the ports. (c) **gauge widgets AV on NULL data bindings** — `NumericDisplayScalar` value_pointer NULL → `GaugeConnectionDirectOf` ctor deref (bind NULL→static zero, gauge.h); `RankAndScore::Execute` derefs unreconstructed game state (`Gauge::GuardedExecute` SEH wrapper → `Disable(True)` on first fault). All gated `BT_DEV_GAUGES`; **default DEV (no gauges) un-regressed (TARGET DESTROYED, 0 crashes) + pod path byte-unchanged** (strict `Fail`, no guards). **REMAINING = the real gauge WIDGET reconstruction:** the `BTL4MethodDescription` method table (BT gauge classes: `PlayerStatus` + ~16 recovered `methodDescription` entries) + the gauge→ game-state DATA BINDINGS (why some gauges resolve NULL) — that is what the dev skip/guards stand in for, and what the POD needs. Then Step 2 (RP↔BT MFD port-name reconcile → the 5 MFDs) + Step 3 (2-window layout). **✅✅ GAUGE MILESTONE C — ALL SIX instrument surfaces in a SEPARATE dev window (default under `BT_DEV_GAUGES`; `BT_DEV_GAUGES_DOCK=1` = docked-in-main-window instead; full map: `docs/GAUGE_COMPOSITE.md`).** Steps 2+3 done for the dev path. A 960×384 top-level window tiles all 6 pod screens with authentic content: **Heat** (COOLANT/BALANCE/RES + condenser/temp-leak gauges), **Comm** (KILLS/DEATHS/SELECT TARGET), **Mfd1/2/3** (DISPLAY/PROGRAM/NEAREST/TEMP-STATUS frames), and the color **radar** (SCALE grid + dials + ARMOR DAMAGE). Main 800×600 3D view is un-occluded; default DEV un-regressed (TARGET DESTROYED, 0 crashes). **Architecture (from the `mfd-multisurface-map` workflow):** all 6 surfaces are bit-plane MASKS over the ONE shared `SVGA16`/`pixelBuffer` (`sec`=palette low byte; `Heat`=0x4000 UL, `Mfd2`=0x0400 UC, `Comm`=0x8000 UR, `Mfd1`=0x0100 LL, `Mfd3`=0x1000 LR; `Eng1-3` = engineering-mode alt planes, not extra monitors), so the compositor reaches the SVGA16 ONCE and extracts each by mask. **No port-name reconcile needed on the dev path** — we fetch the BT port names directly via `GetGraphicsPort` (the RP `auxUL2/…` names only matter to the POD's own `SVGA16::Update` demux — a deferred pod-only fallback). `SVGA16::DrawDevSurface` has two kernels: palette-LUT (sec, == Update case 0) + mono bit-plane→ tint (`(word&mask)?tint:0`, the reduced core of Update cases 1/2); `BTDrawGaugeSurfaces` iterates a 6-entry table. The **separate window = one `CreateAdditionalSwapChain` on the EXISTING device** (no 2nd D3D device); rendered AFTER the main `EndScene` (`BTGaugeWindowRenderAndPresent`, L4VIDEO): SetRenderTarget→**SetDepthStencilSurface(NULL)** →Clear→BeginScene→6 tiles→EndScene→restore→`swap->Present`. **⚠ KEY BUG (cost a cycle): depth-stencil size mismatch** — the main 800×600 depth stays bound when you SetRenderTarget to the 960×384 gauge backbuffer; a bound depth SMALLER than the RT is INVALID → all draws silently fail (window shows the Clear color, no geometry). Fix: unbind depth (Z is off anyway), restore after. Pod `BuildWindows`/`Update` path byte-unchanged. REMAINING = the widget recon (above) + the pod MFD port-name fallback. **✅✅ GAUGE WIDGET RECONSTRUCTION — the cockpit instruments now read LIVE game state (attribute wave DONE; full mechanism + technique in `docs/GAUGE_COMPOSITE.md` §Attribute wave).** After Milestone C put all 6 surfaces on screen (drawing NULL), two waves made them read real data. (a) The **ColorMapper family + headingPointer** (prior session): the palette-tint schematics (`cmHeat`/`cmCrit`/`cmArmor`/`colorMapperMultiArmor`) + the rotating compass, each a reconstructed BT gauge class registered in `BTL4MethodDescription[]` (btl4grnd.cpp). (b) The **attribute wave** (this session, 4 commits): gauges bind to game state by NAME via the engine attribute-pointer system — the config's `Subsystem/Attribute` (e.g. `HeatSink/CurrentTemperature`) or bare `Attribute` (e.g. `LinearSpeed`) resolves through `ParseAttribute`→`FindSubsystem`(stricmp on `GetName()`)→`GetAttributePointer`→ the class's `activeAttributeIndex`. A class publishes attributes by declaring an `AttributeID` enum + a `static AttributePointers[]` (`ATTRIBUTE_ENTRY`) + `GetAttributeIndex()` chained to its parent, and passing `GetAttributeIndex()` to its `DefaultData`. **⚠ DENSE-TABLE HAZARD (systemic): `AttributeIndexSet::Build` leaves gap slots uninitialized and `Find` strcmps EVERY slot → a published table MUST be a dense prefix from the parent's `NextAttributeID`; fill gaps with a shared read-only pad member.** Done: **HeatSink table** (`CurrentTemperature` → Heat readout shows 77), **Mech table** (dense prefix 0x15..0x21, `LinearSpeed` → SPEED readout shows ~225), **vertBar** (coolant bars; x87 pixel math recovered by disassembly), **segmentArcRatio** (speed arc; thin subclass of engine `SegmentArc`), **oneOfSeveralPixInt** (the button-state lamps — reconstructed the whole OneOfSeveral strip-selector family + PixMap8 blit path), and **map** (the RADAR / tactical display — the marquee; the MapDisplay renderer was already reconstructed, so this was the registration glue + data: the Mech table extended to the full 0x15..0x38 with the radar Point3D*/Quaternion* attributes + a Sensor "Avionics"/RadarPercent table + a reconstructed `MapDisplay::Make`/`methodDescription`; renders the FOV view wedge live). **Base primitives `numeric`/`numericSpeed`/ `digitalClock`/`rankAndScore` are ENGINE (L4GAUGE.cpp) — only need the data; `vertBar`/`segmentArcRatio`/ `oneOfSeveralPixInt`/`map`/`PlayerStatus`/`vehicleSubSystems` are BT-specific (reconstruct + register).** Technique added: `tools/disas2.py ` (recover FPU math Ghidra drops — `FUN_004dcd94`=round, `FUN_004dcd00`=fabs) + `tools/vtdump.py ` (find a vtable override the assert-anchored decomp didn't export). **⚠ SYSTEMIC PATTERN (map): a newly-consumed reconstructed subsystem/gauge is often the FIRST reader of a stubbed helper — the map exposed 3 (Sensor radarPercent negative from un-normalized heatEnergy; `ResolveOperatorEntity`→NULL; unguarded NULL entity), each fixed with the established viewpoint-fallback/guard patterns.** **⭐ MAP CONTACTS RENDER (2026-07):** the radar plots the enemy contact inside the FOV wedge. Two more fixes: (1) the gauge renderer's entity grid was NEVER populated (the port dropped `ExecuteImplementation`'s InterestingEntity feed) → added `GaugeRenderer:: RebuildEntityGrid()` filling `movingEntities` from the world DynamicMaster+Replicant iterators (the vehicles; the map draws a cross+box blip for cls-0xBB9 mechs, the pip table being stubbed); (2) the blip PROJECTION was wrong — `worldToView` is a camera matrix whose baked scale is wrong for a point transform (120u contact → 167px not 44px) → project `delta = entity − viewpoint` directly (rotate by −heading, scale by pixelsPerMeter). Diagnosed via `BT_MAP_LOG` (the enemy reads dsq=14400 = exactly 120u; the earlier "377-828m" was the wrong scale × player drift, not a distance error). REMAINING: `PlayerStatus`, `vehicleSubSystems` (task #13) + the authentic pip symbol/name/ video-object infra (`GetVideoObject`/`LookUpPip`/`GetNameID`, stubbed → cross-blip stand-in) + `SetTargetRange` (radar zoom, 1km default) + the aggregate HeatSink-bank `AmbientTemperature` (`#if 0`'d 0xBBE) + the Reservoir `coolantCapacity` shadow + the Searchlight `LightOn` table (duck button already resolves via the 0x37 Mech entry). **◐ `PlayerStatus` DONE** (mission-review scoreboard; lives in the config's `cameraInit` block, not `MechInit`, so it is build/link/mech-cockpit-safe but not runtime-verifiable via a normal mech egg — commit b691863). **✅✅ `vehicleSubSystems` — DONE: the engineering-screen (MFD) subsystem cluster-panel system is reconstructed, registered, and RENDERING (full spec + phasing in `docs/VEHICLE_SUBSYSTEMS.md`).** It is NOT a widget — its Make (`FUN_004cbaf0`) is a per-subsystem FACTORY building a status *cluster panel* onto one of 12 aux MFD positions, dispatching on classID → `HeatSinkCluster`(0xBC3)/`MyomerCluster`(0xBC6)/`EnergyWeaponCluster` (0xBC8/0xBD4)/`BallisticWeaponCluster`(0xBCD/0xBD0), placed via the 12-row geometry table @0x51bf34. Reconstructed the whole `SubsystemCluster` family (base + 5 subclasses) + 4 sub-gauges (`AnimatedSubsystemLamp`/`AnimatedSourceLamp`/ `ScalarBarGauge`/`ConfigMapGauge`) + 3 connections + the Make + registered in `BTL4MethodDescription[]`; ALSO reconstructed 4 base gauges the clusters build that were declared-but-undefined (`HorizTwoPartBar`/`OneOfSeveralInt`/ `OneOfSeveralStates`/`BitMapInverseWipe`) + `StateConnection` + `SeekVoltageGraph`'s virtuals (their `/FORCE`-stubbed ctors segfaulted when a cluster built them). **⭐ THE AUX-SCREEN BLOCKER RESOLVED CLEANLY (no core re-base needed):** the Make reads `subsystem[0x1dc]`/`[0x1e0]`/`[0x1e4]` (aux-screen position/placement/label) which are **PoweredSubsystem** fields (`auxScreenNumber`/`auxScreenPlacement`/`auxScreenLabel`, resource +0x104/+0x108/+0x10C) that **our PoweredSubsystem ctor ALREADY populates** — the raw offsets read 0xCDCDCDCD only because the reconstructed heat-leaf branch isn't byte-exact. Fix = a bridge `BTGetSubsystemAuxScreen()` (powersub.cpp) casting through the real `PoweredSubsystem` type to return the NAMED fields (also the `FUN_0041a1a4`/`0x50f4bc` type filter); the Make + `SubsystemCluster` read via the bridge. **Render-verified (BT_DEV_GAUGES_DOCK): 7 authentic panels build + draw — SENSOR CLUSTER, MYOMERS, ER MED LASER·RANGE 500M ×3, PPC·RANGE 500M ×2 — with bar gauges/recharge dials/lamps, 0 crashes, combat un-regressed.** Diagnostics `BT_VSS_LOG` (Make entry + per-subsystem classID/auxScreen dump). Tooling: `reference/ghidra_scripts/DecompVSS.java` (headless address-list decompiler — reusable for any function the assert-anchored exporter skipped; via the `vssproj` Ghidra project). Follow-ups (non-blocking, in the doc): the `SegmentArc270` recharge-dial + `BitMapInverseWipeScalar` eject-wipe children (placeholder ctors, deferred NULL); the remaining raw heat-leaf-branch value-pointer reads (BEST-EFFORT — need the branch re-base or per-field bridges); the separate 960×384 gauge window (docked mode renders; separate-window is existing dev-gauge infra). **Alternate route considered:** run original `btrel410.exe` under DOSBox — **blocked**: it needs the VPX/IG board DOSBox can't emulate; bypassing it = the shim work anyway. ## 9. Things to get from Nick - **#1 (gating): the BattleTech game SOURCE CODE** — the missing implementation `.cpp` (mech, subsystems, mapper, HUD, main app; see §5a). Without it the source port can't proceed. Likely on a backup/dev drive/repo separate from these machine images. - Confirmation the PodPC image is the complete content master (or where the rest is). NOTE: the runtime `BTL4.RES` already has 8 maps + the full mech anim set (§4a); genuinely-absent items are the lab/other-build maps (des/burnt/frstrm — source-only) and possibly a fuller content build. Likely low priority now. - A working session on pod specifics: the 7-monitor driver setup, the RIO cockpit I/O protocol, and how the current Win10+wrapper pods are configured (for Phase 8). --- ## 10. Combat bring-up — `btl4.exe` runs a single-player loop ⭐ (this is the live front) The reconstructed BT (`C:\git\bt411` → `build\Debug\btl4.exe`, run via `run\run.cmd`) now boots, renders the cavern world + a skinned Blackhawk, and runs a **drive → animate → target → fire → damage → destroy** loop. The remaining work is making each link *authentic* (reconstructed from the binary), not bringing it up. Companion ledgers: `docs/RECONCILE.md` (running findings), `docs/BT_SOURCE_STATUS.md` (missing-file map), memory files `bt-combat-bringup`, `reconstruction-no-standins`. ### 10a. Build / run / debug recipe (bt411 layout — see README.md) - **Configure once** (32-bit / Win32 — the DXSDK link libs are `Lib/x86`): `cmake -S C:\git\bt411 -B C:\git\bt411\build -G "Visual Studio 16 2019" -A Win32 -DCMAKE_GENERATOR_INSTANCE="C:/Program Files (x86)/Microsoft Visual Studio/2019/BuildTools"`. - **Build:** `cmake --build C:\git\bt411\build --config Debug`. The one top-level `CMakeLists.txt` builds `munga_engine` (engine lib, `engine/MUNGA{,_L4}/`) + `bt410_l4` (reconstructed BT game lib, `game/reconstructed/` + `game/original/`) + `btl4.exe`, linking DXSDK d3d9/d3dx9/dinput8 + OpenAL/libsndfile (`engine/lib/`). Linker uses `/FORCE` (tolerates header-defined globals + the dead offline-factory unresolved externals in `mech3.cpp` — see the CMakeLists comment; a cleanup TODO). ⚠ Editing an `engine/` file rebuilds the engine lib automatically now (one project) — no separate engine build step; but a NEW member on a `DPLRenderer`/`d3d_OBJECT` class still needs the game objs that embed its layout recompiled (delete stale objs if layout-mismatch corruption appears). - **Run:** `run\run.cmd [EGG]` (default `DEV.EGG`) — it `cd`s to `content\` and launches `build\Debug\btl4.exe -egg `. **cwd MUST be `content\`** (the engine resolves `BTL4.RES`, `VIDEO\`, `BTDPL.INI`, eggs relative to cwd — see the `loadTables` gotcha below). Logs to `btl4.log` in `content\` (grep `[anim]/[drive]/[target]/[fire]/[damage]/[boot]` markers). The DLLs (OpenAL32/libsndfile) are copied next to the exe automatically at build time. - **⭐ THE AUTHENTIC STACK IS NOW DEFAULT-ON** (`BTEnvOn`, mechrecon.hpp): `BT_GAIT_CUTOVER`, `BT_GAIT_SM`, `BT_COLLISION`, `BT_REAL_CONTROLS` all default ON — set `=0` to fall back to the historic bring-up path (e.g. `BT_GAIT_SM=0`). Verified: an env-free run gets the full authentic chain (mapper speed/turn demands → two-channel gait → collision+gravity) with combat/damage intact, and each `=0` fallback still works. - **Bring-up env vars** (DEBUG/headless; default OFF): `BT_FORCE_THROTTLE=1` (auto-walk fwd, no key), `BT_SPAWN_ENEMY=1` (spawn a target mech 120u ahead ALONG THE SPAWN FACING — facing-aware since the real-controls drive keeps the authentic spawn orientation), `BT_FORCE_FIRE=1` (auto-fire on cooldown), `BT_ASSERT_TO_DEBUGGER=1` (route CRT asserts to the debugger instead of a modal box), `BT_HEAPCHECK=1` (whole-heap validation on every alloc/free — heap-corruption hunts), `BT_PROBE_BGF=` (+ `BT_PROBE_N`) (boot-time BGF loader probe, exits after). Interactive: **WASD** drive, **Space/Ctrl** fire, **X** all-stop. Gated/marked in `btl4main.cpp` + `mech4.cpp`. **⭐ VIRTUAL CONTROLS (dev keyboard → the pod's analog inputs; mech4.cpp drive block):** the pod's throttle was an ABSOLUTE analog LEVER + an analog turn stick — momentary 0/1 keys can't express either. After the perf fix took the game 10fps→60fps, every key tap landed as a frame-perfect FULL demand (at 10fps most taps aliased away between sim ticks, which had LOOKED like fine control) → "controls too sensitive, tap-back reverses instead of stopping". mech4 integrates dt-scaled: **W/S sweep a PERSISTENT throttle lever** (tap ≈ ±0.08 step, full sweep ~1.4s; a lever below standSpeed = the mech stands, authentic) with a **DETENT at zero** (braking from forward stops AT 0 and latches until the key is released — no accidental reverse), **X = all stop**; **A/D deflect a momentary stick** (~0.4s to full, auto-centers) so a tap = a gentle nudge. `gBTDrive.throttle/turn` are the integrator OUTPUTS; the mapper bridge + forced-throttle harness consume them unchanged (BT_FORCE_THROTTLE bypasses the lever). **⚠⚠ KEY-INPUT GOTCHA — the WndProc NEVER RECEIVES WM_KEYUP:** the engine's per-frame keyboard reader (`L4CTRL.cpp:1506`) `PeekMessage(PM_NOREMOVE)` + `GetMessage()`s every `WM_KEYUP`/`WM_SYSKEYUP`/`WM_CHAR` OUT of the queue for its key-command channel — only KEYDOWNs reach the launcher WndProc, so ANY message-based key-state tracking latches keys on forever (this was ALSO true of the old 0/1 scheme: its throttle/turn/fire never cleared on release; the old "feel" was that quirk + 10fps aliasing). The integrator therefore POLLS `GetAsyncKeyState` per frame (via GetProcAddress, no headers needed), gated on the game window being FOREGROUND (`GetForegroundWindow` pid == ours) so background typing doesn't drive the mech. **`BT_KEY_NOFOCUS=1`** = test hook accepting keys without focus (SendInput automation from a background shell can't grant real foreground; `scratchpad/realkey.ps1` injects real keys for harness runs); **`BT_KEY_LOG=1`** = 1 Hz `[vctl]` key/lever/stick state. BTDriveInput struct is duplicated in btl4main/mech4/mechmppr/emitter — keep in sync (the emitter copy had already silently drifted). **⭐ Levels:** default test EGG is now `DEV.EGG` (`map=grass` + `time=day`, outdoor); the RES has 8 maps (`cavern`/`grass`/`rav`/`polar3`/`polar4`/`arena1`/`arena2`/`dbase`) — swap via a copied EGG's `map=` field (§4a). **⭐ MECH SELECTION IS EGG DATA — the code path is mech-agnostic (VERIFIED with a second mech).** The player vehicle = the egg's pilot page `vehicle=` (DEV.EGG: `bhk1`), resolved via `playerMission->GetGameModel()` → `FindResourceDescription(name, ModelListResourceType)` (btplayer.cpp `CreatePlayerVehicle`). RES type-18 names incl.: `bhk1/blkhawk ava1/avatar mad1/mad2/madcat lok1/lok2/loki thor/thr1 vul1/vulture owens/own1 snd1 sunder strk`. **`MECH2.EGG` (pod BT dir) = DEV.EGG with `vehicle=madcat` — the Mad Cat booted, rendered, walked and fought ON FIRST TRY with ZERO code changes:** own clips loaded (walkStride 18.5 vs BLH 22.0 — real per-mech data measured), 22 damage zones (BLH 20), 30-subsystem roster constructed, gait/collision/shadow/heat live, combat → TARGET DESTROYED, 0 crashes. **The per-mech worklist that surfaced (all known warts, nothing new):** (1) the visual-fire muzzles fell back to the centre point (the gun-port site names are hardcoded BLH.SKL names — the real fix is per-weapon `.SUB` `segmentReference` muzzle resolution, already how `Emitter::GetMuzzlePoint` works); (2) roster 32 vs 30 present = its LRM racks hit the still-stubbed missile factory cases (0xBCE/0xBD0 — SUBSYS_PLAN WAVEs, per weapon FAMILY not per mech); (3) beam colour still the BLH red (per-weapon `VideoObjectName` pending weapon enumeration). Everything else is foundational. - **cdb (x86):** `"C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\cdb.exe"`. Pattern for a faulting stack (set cwd to `content\` first): `-g -c ".lines;sxe av;g;kp 24;q"` with `BT_ASSERT_TO_DEBUGGER=1`. The debug CRT marks fresh heap **0xCDCDCDCD** (uninitialized) and freed **0xFEEEFEEE** — invaluable for "was this ever constructed?" questions. gflags PageHeap (needs elevation) caught the earlier subsystem heap overruns. ### 10a-bis. Repo layout — where everything lives (CONSOLIDATED into bt411) The build was formerly a "virtual assembly" scattered across 6 absolute-path locations in the RE workspace; it is now **one relocatable tree** (`C:\git\bt411`, all repo-relative). Map of the tree (full detail in `README.md`): - **`engine/MUNGA/` + `engine/MUNGA_L4/`** — the shared 2007 MUNGA engine + Win32/D3D9 HAL, carrying our BT render/loader work (`bgfload`/`L4D3D`/`L4VIDEO` + the `image.{cpp,h}` codec). Plus `engine/shim/` (ATL shim), `engine/lib/` (OpenAL/libsndfile libs + runtime DLLs), `engine/rp/` (the ~21 RP *headers* the audio HAL includes via `..\rp\vtv.h` — headers only, no RP game source), and `engine/DPLSTUB.h`/`LOGGER.h` (the trunk-root headers reached via `..\`). - **`game/reconstructed/`** — the reconstructed BT source (the bulk; was `decomp/reconstructed/`). - **`game/original/BT/` + `game/original/BT_L4/`** — the surviving *original* BT `.cpp` (`BTMSSN/BTREG/BTTEAM/BTSCNRL/BTCNSL/GAUSS/PPC/BTL4MODE/BTL4ARND`) **+ all the BT headers** (these two dirs are the game's header include path). - **`game/fwd/`** — the ~186 forwarding shims letting reconstructed code do `#include `; each now `#include "../../engine/MUNGA/.h"` (relativized). `fwd/app.thp` is a self-contained `TestApplication` reconstruction shim; `fwd/bgfload.h` forwards to the engine loader header. - **`game/btl4main.cpp`** — the WinMain launcher. - **`content/`** — the runtime tree (was `tesla4pod_extracted/.../REL410/BT/`): `BTL4.RES`, `VIDEO/`, `GAUGE/`, `AUDIO/`, eggs, `BTDPL.INI`, `VIDEO/REPLACEMATS.tbl`. **Run cwd = `content/`.** - **`reference/decomp/`** — the raw Ghidra pseudocode `all/part_*.c` (was `decomp/recovered/`), the source-of-truth every reconstruction is verified against. `reference/ghidra_scripts/` = the exporter. - **`docs/`** — format specs (`BGF_FORMAT`, `ASSET_PIPELINE`, `BT_SOURCE_STATUS`) + the reconstruction ledgers (`RECONCILE`, `HARD_PROBLEMS`, `RESOURCE_AUDIT`, `SUBSYS_PLAN`, `WAVE_PLAN`, `P3_LOCOMOTION`). - **`tools/`** — `btconsole.py` (MP console emulator) + `mapscan.py`/`resscan.py` scanners. - External (not vendored): **DXSDK June 2010** (overridable `-DDXSDK`). ⚠ Historical sections below still cite the old `nick-games/…` absolute paths as provenance — see the orientation banner at the top; they map onto the dirs above. **NOT copied into bt411:** the standalone D3D9 viewer (§6 `port/`, a dev sandbox) and the RE toolchain blobs (Ghidra/JDK) — those stay in the original workspace. ### 10b. Reconstruction workflow (the method) — **RULE: no stand-ins** The loop for each feature: (1) read the RAW decomp `decomp/recovered/all/part_*.c` for the relevant `FUN_xxxx`; (2) map `FUN_`/`DAT_`/`this+0xNN` to engine symbols using BT headers + the WinTesla MUNGA source + `decomp/reconstructed/CLASSMAP.md` + RP's parallel code; (3) write the **real** reconstruction into `decomp/reconstructed/*.cpp`; (4) `compile1.cmd` then full build; (5) run env-gated + read `btl4.log`; (6) cdb on any crash. **Never write stand-in/placeholder code for an apparent "gap"** — the full game logic IS in the pseudocode (the binary ran the game); a "gap" is just a reconstruction stub not yet filled (e.g. `ResourceStream`/`MemStreamX` no-op proxies, an uninitialised field). Said by the user: "there are no gaps, just work to be done." Bring-up scaffolding (the env-var paths, the explosion-for-beam, a player-gated drive) is **clearly marked and meant to be replaced** by the real reconstructed system, never to substitute for reading the decomp. For exhaustive multi-function decomp analysis, fan out a read-only **Workflow** (understanding phase), then implement hands-on so each change is visible. ### 10c. Decompilation insights (per subsystem — durable facts) - **Resource lookup is by NAME or by ID.** `ResourceFile::FindResourceDescription(name, type, index=-1)` (raw `FUN_00406ff8` = find-by-type-and-name; `FUN_00407064` = `SearchList(resourceID, type)`). Resource **type enum**: SkeletonStream, **DamageZoneStream**, ModelList, **AnimationResourceType==16 (0x10)**, damage-zone segment list **type 0x14**, critical-subsystem segment **0x1e**. Animation clip names in `BTL4.RES` are **lowercase** (`blhrrl`=904, `blhrrr`=905, `blhwwli`=910, `blhrrli`=916). `Mech:: ResolveAnimationClip(prefix,suffix)` builds the name and calls FindResourceDescription. - **Animation (engine `MUNGA/JMOVER.cpp`):** `AnimationInstance(JointedMover*)` then `SetAnimation( ResourceID, finishedCallback)` (Lock()s the clip, parses `frameCount/jointCount/jointIndices/frameStart/ keyFrames/rootTranslations` from `resourceAddress`). `Animate(dt, move_joints=True)` writes each joint's DCS per keyframe. **At end-of-clip it calls `(moverToAnimate->*finishedCallback)(...)` (line ~1592) — a NULL callback crashes.** Loop a gait via a **non-virtual** `Mech::OnBodyAnimFinished` re-arming the clip, passed `reinterpret_cast` (Mech derives from JointedMover; the member- ptr cast is how the original stores a Mech method in the JointedMover-typed slot). - **`loadTables` gotcha (`MUNGA_L4/L4VIDEO.cpp:849`):** `fopen("VIDEO\\REPLACEMATS.tbl","rb")` is **relative to cwd with no null-check** → fread on NULL if cwd isn't the pod BT dir. (RP material-substitution table.) - **⚠ `application`/`l4_application` is NULL outside the frame loop** — `ApplicationManager` (APPMGR.cpp:73) REASSIGNS the global `application` while iterating the running-app chain each frame and leaves it NULL between frames. Any code running at Windows message-pump time (WndProc handlers, timers) reads NULL even while the game runs. Pattern: record the input in a global at message time; consume/apply it inside a per-frame path where `this` is the engine object (how the window-resize aspect fix works, L4VIDEO.cpp `gWindowAspect` → per-frame dirty-check rebuild; commit 06683cf). - **Entity spawn:** `Mech::Make(MakeMessage*)` (`FUN_004a2d48`) allocates `sizeof(Mech)=0x854` + runs the ctor; the base `Entity` ctor self-registers it into the world (renders + ticks). `Registry::MakeEntity` dispatches per-class `makeHandler` (BT registry maps `MechClassID=0xBB9 → Mech::Make`). Unique IDs via `HostManager::MakeUniqueEntityID()`. **The mission system is NETWORKED and BT is PvP-ONLY — it never had NPCs/AI** (verified, not a recovery gap): `BTREG.cpp`'s entity registry knows only `Mech`/`Projectile`/ `Missile`/`BTPlayer`/`BTTeam`; the COMPLETE BT header set has no Bot/Brain/AI class; `BTL4OPT.EXE` has no AI strings; every mech is driven by a `BTPlayer` (or a `BTCameraDirector` for the "camera"/spectator game model), players grouped into `BTTeam`s. Opponents were always other human pods over the net (`NETNUB`/ `L4NET`). The AI entities `Crusher`/`Blocker`/`Runner` are **Red Planet** classes (`RP/`), NOT BT and NOT engine — reference only for *how the engine supports AI if we ever ADD it* (that would be new dev, not a port). ⇒ A solo mission has no enemy by design; the env-gated `BT_SPAWN_ENEMY` mech is a **test dummy** (uncontrolled `Mech`), not a game feature. The authentic opponent path is multiplayer (Phase 7). - **`Mech::PerformAndWatch` runs for EVERY mech** → gate player-only logic on `this == application->GetViewpointEntity()` (else a spawned enemy is driven by player input / shares the player's AnimationInstance). - **Targeting (mech offsets verified vs `part_013.c`):** `mech+0x37c` = `Point3D` target world pos (range/ aim source), `mech+0x388` = `Entity*` target (**the field `HasActiveTarget()` / `Emitter::FireWeapon` gate on**), `mech+0x38c` = `int` targeted sub-zone (-1 = whole). Weapons cache `hasTarget/targetPoint/ muzzlePoint`, refreshed each frame from the mech's target by the mech's targeting update. - **Firing:** `Emitter::FireWeapon` (`emitter.cpp`, real) builds a muzzle→target beam but **weapon-effect renderables (beams/tracers) are NOT built in the port** (`btl4vid.cpp:455`) → nothing draws. **Explosions DO render** (`L4VIDEO.cpp:5356`), so a shot stands in as `Explosion::Make(MakeMessage*)` at the target (effect resource **"explode" = id 13**). Weapon ClassIDs (CLASSMAP, note the factory enum mislabels): EmitterWeapon 0xBC8, MechWeapon 0xBCD, Missile 0xBCE, Ballistic 0xBD0, **PPC 0xBD4** (factory calls it "GaussRifle"), MechTech 0xBD6. Mech ctor offsets: `damageZoneCount @0x11c (param[0x47])`, `damageZones @0x120 (param[0x48])`, `resourceID @0x1bc (param[0x6f])`. - **Damage delivery:** `Entity::TakeDamageMessage(id, size, inflictingEntityID, zone, Damage&)` → `target->Dispatch(&msg)`. **Base `Entity::TakeDamageMessageHandler` IGNORES `zone==-1`**; the comment (`ENTITY3.h`) says "Mechs use cylinder lookup table" for an unaimed hit — **that Mech override is NOT yet reconstructed**, so send a VALID zone index for now. `class Damage{ damageType(enum Collision/Ballistic/ Explosive/Laser/Energy), damageAmount, damageForce, surfaceNormal, impactPoint, burstCount }`. `DestroyEntityMessage(id, size)` removes an entity. Engine `Explosion` also splash-damages nearby entities via a collision query (fragile) — direct Dispatch to the known target is what we do. - **✅ Damage zones — REAL damage model reconstructed & working** (replaced the health-model stand-in). The chain: aimed `TakeDamageMessage` → engine `Entity::TakeDamageMessageHandler` → `Mech__DamageZone:: TakeDamage` → engine base armor model (`damageLevel += damageAmount * damageScale[type]`) → accumulates to 1.0 (zone destroyed) → graphic-state/death branch → death detected. Verified: `structure` climbs 0.133/hit, vital zone destroyed → `*** TARGET DESTROYED ***`, stable. Build mechanics: engine `Entity` ctor (`ENTITY.cpp:961-987`) reads `damageZoneCount` from the `DamageZoneStreamResourceType` (type 0x14) resource and allocates the array (entries left `0xCDCDCDCD`); the **Mech ctor populates that inherited array** (mech.cpp Pass-3): `MechFindResource(resourceID, 0x14)` → `Lock` → `DynamicMemoryStream(addr, size, initial_offset=4)` (skips the count word the base consumed) → `for zfield - (char*)res`) vs the binary's known offsets and dumping raw record bytes as int+float; anchor the true field offsets from the raw decomp of the resource parser (`CreateStreamedSubsystem`/@004ac9ec — `param_4+0xNN` reads). See §10d heat-flow. **Two more non-field fixes were needed for damage to flow:** (1) **message-handler chaining** — a reconstructed class's `MessageHandlers` set must be built chained to the parent's (`Receiver::Message HandlerSet(Entity::GetMessageHandlers())`); an empty default-ctor set has NO parent chain, so `Receiver::Receive` finds no handler and **every inherited message (TakeDamage, etc.) is silently dropped**. (2) **entity validity** — `Entity::Dispatch` only delivers synchronously (`Receiver::Receive`) for a VALID master; an invalid entity `Post`s the message as a deferred event that never fires. A manually-spawned entity (no `MakeViewpointEntity`/CheckLoad handshake) must call `SetValidFlag()` itself. **Remaining damage gaps (scoped, non-blocking):** EnergyDamageType(4) shorts attached generators via `criticalSubsystems[]->subsystemPlug.Resolve()` → needs the REAL subsystem roster (still `RECON_SUBSYS` stubs) → use ExplosiveDamageType meanwhile (correct for the explosion weapon anyway). Per-impact aim needs the cylinder lookup (deferred STEP 6 — `Mech::TakeDamageMessageHandler` override `FUN_004a0230` + `FUN_0049ed0c`); meanwhile aim a vital zone directly. `IsDisabled()` (`FUN_0049fb54`) reconstructed (reads `movementMode==2||9`), but `movementMode` is set by the bypassed gait/death-transition → bring-up death reads the real zone state directly (a `vitalDamageZone` with `damageLevel>=1.0`). Removing the wreck (`DestroyEntityMessage`) crashes on teardown — death = explosion + stop-targeting for now. ⚠ **ROOT-CAUSE PIVOT (P5 — full writeup `docs/HARD_PROBLEMS.md`; repro `BT_ENABLE_TEARDOWN=1`):** it is a **TEARDOWN-SEQUENCE double-free, NOT a base-region stomp** (two earlier theories DISPROVEN). (a) "solids never built" — WRONG (`Mover::Mover`:1756 allocs `collisionLists` unconditionally). (b) "reconstructed Mech raw offsets stomp the engine collision cluster" — ALSO WRONG: those stomps (`Mech::Simulate` collision/ terrain/telemetry, `FeedHeat*Gauge`) are **DEAD CODE, never called** (grep-confirmed). The `BT_ENABLE_TEARDOWN` dump proves the enemy's engine base is **FULLY VALID at death** (`collisionLists@0x2e4`, `segmentTable@0x2f0` =51 segs, `jointSubsystem@0x30c` all valid). The crash is in the destruction SEQUENCE (`FryDeathRow → ~Mech → ~JointedMover → ~Mover`): cdb shows `collisionLists`'s array already **freed** (`0xdd`/`count=0xdddddddb`) by the time `~Mover` `delete[]`s it → a double-free. `~Mech` (reconstructed) runs before the engine base dtors → prime suspect is a Mech member dtor / the minimal-spawn collision-or-death-row setup freeing (or aliasing) a base resource the engine dtor frees again. ✅ **PINNED (trace done):** it's the **skeleton SEGMENT teardown double-free**, not collisionLists. `~JointedMover` (JMOVER.cpp:436 `SegmentTableIterator(segmentTable).DeletePlugs()`) → `SocketIterator::DeletePlugs` (SOCKET.cpp:157-161 `delete plug`) force-`delete`s each of the enemy's 51 `EntitySegment`s with `defeat_release_node=1` (bypassing the ref-count release path); one is **already freed** → `STATUS_HEAP_CORRUPTION 0xC0000374` + `0xFEEE` fill. ⭐ **P5 CLOSED — the whole premise was WRONG (verified):** a killed mech does NOT disappear in the real game, so `DestroyEntityMessage`-on-death should NEVER be issued. RP death = `VTV::DeathShutdown` (VTV.cpp:1681 — subsystems shut down, vehicle STAYS); `CondemnToDeathRow`/removal is ONLY for transient objects (RIVET projectiles, DEMOPACK cleanup). BT death = a STATE transition (`SetGraphicState(Destroyed GraphicState)` mechdmg.cpp:355 + death anim `deathAnimationLatched@0x650` + effect/splash) → the mech becomes a persistent WRECK. So the **wreck-stays behavior we already ship IS faithful**; the teardown crash is an artifact of forcing a removal the original never does (only reachable via `BT_ENABLE_TEARDOWN`). NOTHING to fix. Real future death work = reconstruct DeathShutdown + collapse anim + destroyed skin (does NOT touch the teardown path). Later trace also showed it's heap corruption in a SINGLE teardown, not the double-free the line above hypothesized. Full trail: `docs/HARD_PROBLEMS.md`. ⭐ **P5 TRULY CLOSED — ROOT CAUSE FOUND & FIXED (the whole trail above was chasing run-2 of a DOUBLED dtor chain).** `Mech::~Mech` (mech.cpp:1106) contained an EXPLICIT `JointedMover::~JointedMover();` — the Ghidra decomp shows the binary's `FUN_00425550(this,0)` tail call, which IS the compiler-emitted implicit base-dtor; reconstructing it as a source statement ran the whole `~JointedMover → ~Mover → ~Entity` chain TWICE per Mech. Run 2 re-`delete[]`d `Mover::collisionLists` (count word read from the 0xDD freed fill = the observed `0xdddddddb`) and re-ran `DeletePlugs` over the destroyed segment table (= the P5 "EntitySegment already freed" 0xC0000374). ONE bug = BOTH crashes (death-row teardown AND app exit — every window close crashed once entities existed; symptom timing varied with heap reuse, which is why idle-close sometimes survived and why the earlier traces looked contradictory; the old ENTRY/EXIT probes bracketed the EXPLICIT call, misreading run-1 as "a member dtor freed it"). **⚠ RECONSTRUCTION RULE (add to the systemic checklist):** in a decompiled DESTRUCTOR, the trailing member-dtor calls (`FUN_xxx(this+N, 2)`), the base-dtor call (`FUN_xxx(this, 0)`), and the `(flags&1) && operator delete(this)` tail are COMPILER EPILOGUE GLUE — reconstruct only the dtor body ABOVE them; C++ re-emits member+base destruction implicitly at the closing brace. Explicit member-dtor calls were also removed from ~Mech (were benign only because the Recon proxies are no-op) and the same pattern documented out of `Mech__DamageZone`'s dtor (mechdmg.cpp). VERIFIED: interactive play → window close under `BT_HEAPCHECK=1` = clean exit; combat kill + `BT_ENABLE_TEARDOWN=1` forced entity teardown = `TARGET DESTROYED`, teardown completes, sim continues, clean close — 0 crashes across the matrix. **App exit no longer crashes; entity removal (`DestroyEntityMessage`) is usable again** (relevant to MP disconnect cleanup). Repro harness kept: `BT_FORCE_SECONDS=` (release forced throttle after n sim-s) + `BT_FORCE_TURN=` (hold steering) in mech4.cpp — the tap-release/turn paths no soak had exercised. (Forensics: a 14-agent adversarial workflow; secondary REAL-but-dormant hazard found on the way: `AddCollisionToList` BNDGBOX.cpp:389 has no live bounds check — `Verify` compiles out at DEBUG_LEVEL=0 — so >10 simultaneous collision contacts from >1 source overflow the 10-slot list; **NOW FIXED** — a live clamp in `AddCollisionToList` (BNDGBOX.cpp, engine tree) drops overflow contacts instead of writing past the block. Trigger scenario was real: the user spawned INSIDE a building solid on grass (mass simultaneous contacts + gravity pressing into terrain boxes) → silent AV mid-play, where the CRT assert dialog ITSELF crashed while formatting (why no popup appeared). Verified: full combat+teardown+close matrix under `BT_HEAPCHECK=1` = 0 crashes, clean exit. Spawn-inside-solid placement itself is a separate tuning item.) (Latent, non-causal: compiled `sizeof(Mech)=0x638` < binary `0x854`; the over-sizeof writes `0x7e0-0x828` live only in the dead Simulate, so no live overflow — but pad to 0x854 before reviving that code.) ### 10d. Reconstructed modules wired into `bt410_l4` (state) ~43 reconstructed `.cpp` compile+link. Mech runtime (`mech.cpp/2/3/4`, `mechdmg`, `mechsub`, `mechtech`), heat (`heat`), weapons (`emitter`/`mechweap`/`mislanch`/`missile`/`projweap`/`projtile`/`gauss`/`ppc`), subsystems (`gyro`/`sensor`/`gnrator`/`powersub`/`myomers`/`torso`/...), app/HUD (`btl4app`/`btl4vid`/ `btl4mppr`/`btl4mssn`/`btplayer`/`hud`/`dpl2d`). **Subsystem instances are `RECON_SUBSYS` STUBS — the un-stub wave is IN PROGRESS** (full per-family plan: `docs/SUBSYS_PLAN.md`, from the `bt-subsystem-recon` workflow). KEY FINDING: the **real subsystem classes mostly already exist** (heat→`heatfamily_reslice`, weapons→`emitter`/`projweap`/`mislanch`, gyro/sensor/power/torso/myomers/searchlight/thermalsight/ammobin all reconstructed); the work is *wiring* them in + the 4 systemic checks (shadowing, `Wword` trap, handler chain, validity). **The factory case LABELS are systematically MISLABELED** — the real class is identified by the `// FUN_004xxxxx` ctor-address comment reconciled via `CLASSMAP.md`, NOT the `case ClassID` label. Master map (case→real): 0xBBD→Condenser, 0xBBE→HeatSink-bank (cache slot=heat bank, not "sensor"), 0xBC0→Reservoir, 0xBC1→Generator, 0xBC2→PoweredSubsystem, 0xBC3→Sensor, 0xBC4→Gyroscope, 0xBC5→Torso, 0xBC6→Myomers, 0xBC8→Emitter, 0xBCB→AmmoBin, 0xBCD→ProjectileWeapon, 0xBCE→UNRESOLVED (decompile `4bdcb4`), 0xBD0→MissileLauncher, 0xBD3→SubsystemMessageManager, 0xBD4→PPC(Emitter), 0xBD6→HUD, 0xBDC→MechTech, 0xBD8→Searchlight, 0xBDE→ThermalSight. Two bugs to fix: case 0xBDC discards the ptr (no `subsystemArray[id]=`); MechTech(real)+HUD are SWAPPED (MechTech wrongly built at 0xBD6). **Swap recipe:** per-class `Create Subsystem` bridge fn in the class's own `.cpp` (do NOT `#include` real subsystem headers into mech.cpp — the local stubs collide) + `extern` it in mech.cpp; keep the alloc SIZE + `++weaponCount`/special-cache. **Order:** WAVE 1 heat base re-base (`HeatableSubsystem : Subsystem`→`: MechSubsystem`, delete shadow fields owner/flags/statusFlags/statusBits/destroyed, fix thermal offsets) = universal prerequisite; then heat family + MechTech/HUD fix; then power bus + Emitter/PPC; then standalone readouts; gyro/torso/myomers later. DEFER: `MechControlsMapper` tick (reads unddocumented App offsets → AV, mech4 bypasses it); 0xBCE. **✅ WAVE 1 DONE** — `HeatableSubsystem` re-based onto `MechSubsystem`; shadow fields removed; the cascade fixed across heat.cpp/heatfamily_reslice/mechsub + powersub/hud/mechweap (the de-shadow propagates to EVERY heat/powered subclass: `statusFlags`→`simulationFlags`, `destroyed`→`simulationState`, `statusBits`→ `ForceUpdate()`, and `ResetToInitialState()`→`(Logical powered)` on every override). Full build GREEN, exe runs, combat (zones/anim/damage→death) un-regressed. **DEEPER FINDING: the shadowing is more systemic than the plan estimated — `MechSubsystem` ITSELF shadows the engine `Subsystem::damageZone` (mechsub.hpp:252 `ReconDamageZone *damageZone` shadows `SUBSYSTM.h:159 DamageZone *damageZone`), exposed once MechWeapon's chain ran through MechSubsystem. Worked around by qualifying `this->Subsystem::damageZone` (mechweap.cpp); the proper fix = remove the `MechSubsystem::damageZone` shim shadow (cascades to mechsub.cpp) — a follow-up. Two include fixes also needed: `heat.hpp` now `#include `; `mechrecon.hpp` now `#include ` (self-sufficient Quaternion, for myomers's include order). Next = WAVE 2 (heat factory wiring + MechTech/HUD un-swap) per `docs/SUBSYS_PLAN.md`.** **✅ WAVE 2 DONE** — real heat family + HUD + MechTech wired into the factory. Swap mechanism: a per-class `CreateSubsystem(Mech*,int,void*)` BRIDGE fn appended to each real class's own `.cpp` (heat.cpp: Condenser 0x230 + HeatSink-bank 0x1e4; heatfamily_reslice.cpp: Reservoir 0x230; hud.cpp: HUD 0x2a4; mechtech.cpp: MechTech 0x140), `extern`-declared + called from the mislabel-correct factory cases in mech.cpp (0xBBD→Condenser, 0xBBE→HeatSink-bank+cache, 0xBC0→Reservoir, 0xBD6→HUD, 0xBDC→MechTech+store+cache — fixing the MechTech/HUD swap + the discarded-ptr bug). RESULT: **30 of 31 subsystems now tick their real per-frame Performance** (1 bypassed = controls mapper), up from MechTech-only; build green, 16s stable, combat un-regressed. RUNTIME FIX: `HUD::HUD` read the torso source at raw `owner+0x438` (garbage in the recon Mech layout) → defaulted the torso/range readouts to 0 (HudSimulation refreshes per-frame). NEW SYSTEMIC PATTERN for the remaining waves: subsystem ctors read the owner Mech at RAW binary offsets (`*(int*)(owner+0xNN)`) that are wrong in the recon Mech → guard/zero + per-frame refresh, or wire a real Mech accessor. Heat self-load from firing (temperature climb) needs WAVE 3 (real Emitter writing the inherited heat accumulator). Next = WAVE 3: power bus (Generator/PoweredSubsystem) + Emitter/PPC weapon. **✅ WAVE 3 (power bus + Emitter) — CONSTRUCT+TICK done; full fire-path pending.** 3a: bridges for Generator (0xBC1, 0x250) + PoweredSubsystem (0xBC2, 0x31c) in powersub.cpp → factory wired → power bus real. 3b: `CreateEmitterSubsystem` (0x478) in emitter.cpp, wired for BOTH 0xBC8 (Emitter) AND 0xBD4 (PPC = Emitter subclass, same FireWeapon) → real Emitter constructs + ticks its `EmitterSimulation` state machine. Runtime fix (E2): `Emitter::TrackSeekVoltage` (emitter.cpp:311) derefs a null voltage source (the source plug at raw `this+0x1d0` isn't registry-linked in bring-up) → guarded `if (src==0 || dtScale==0) return` (no charge that frame). All real subsystems now construct + tick; build green, combat un-regressed. **✅ WAVE 3 fire-path (E3-E8) DONE — the real Emitter fires end-to-end** (verified: repeated `[emitter] FIRED damage=.. heat=0.49 pendingHeat=0.49`, 17s stable, combat intact). Implemented: E3 `MechWeapon:: HasActiveTarget()`/`GetTargetPosition` read the owner's target slots (`owner+0x388`/`+0x37c` via the inherited `MechSubsystem::owner`) not the never-set `hasTarget`; E4 `firingArmed=1` in the Emitter ctor; E5 deleted the Emitter-local `heatAccumulator` SHADOW, `FireWeapon` now writes the inherited `HeatSink:: pendingHeat`@0x1c8 (the heat-sim input); E7 force-charge in EmitterSimulation Loading (the source plug at raw `this+0x1d0` never resolves -> `currentLevel=seekVoltage[idx]` -> outputVoltage 1.0 -> Loaded; also pre-init the seek table in the ctor so it's valid when the ctor src is null); E8 trigger via a per-FRAME `int gBTWeaponTrigger` pulsed by mech4 (controls mapper bypassed) -> CheckFireEdge sees clean edges. **ROOT-CAUSE BUG fixed along the way (benefits ALL subsystems): the MechSubsystem ctor set `hostEntity` but never set `owner`** (mechsub.cpp) -> every subsystem's `owner` was garbage once heat de-shadowed; now set in both MechSubsystem ctors. Also: the Mech ctor now zeroes the target slots (+0x37c/+0x388/+0x38c) so an un-targeted mech (spawned enemy) reads "no target" (else its weapons fire at a garbage pointer). **✅ WAVE 4 (standalone readouts) DONE — Sensor / Searchlight / ThermalSight / AmmoBin un-stubbed + ticking** (verified: **BLH tick 20→26 executable**, Mad Cat 24, combat **DESTROYED** un-regressed on both, 0 crashes). The 4 readout classes were reconstructed but built as `RECON_SUBSYS` stubs; each is now wired via a `CreateSubsystem` bridge (in its own .cpp) at the mislabel-correct factory case: 0xBC3→**Sensor** (`MyomersClassID` label), 0xBD8→**Searchlight** (`LegSubsystem`), 0xBDE→**ThermalSight** (`MechDisplay`), 0xBCB→**AmmoBin** (`JumpJet`). Each got the Torso de-shim: delete the cross-family shadow fields, redirect accessors to the real inherited base, add a `friend struct LayoutCheck` with `static_assert` offset/sizeof locks. Redirects by branch: **Sensor** (PoweredSubsystem:HeatSink leaf) → `heatAlarm`/ `electricalStateAlarm`/`simulationState`/`heatEnergy`/`simulationFlags|=8`; **Searchlight/ThermalSight** (PowerWatcher) → `watchdogAlarm`/`heatAlarm`/`simulationState`/`ForceUpdate()`/`simulationFlags`; **AmmoBin** (HeatWatcher) → `simulationState` (deleted the `statusState`@0x40 shadow). - **⭐ FUNCTIONAL BUG FIXED (Searchlight + ThermalSight): the ctor gate read the shadow `segmentFlags` (=0), so `((0&0xC)==0 && (0&0x100)!=0)` was ALWAYS FALSE → their Performance NEVER installed (dead since reconstruction).** The de-shim switches the gate to `owner->simulationFlags` (binary-exact part_013.c: 6012/6140; mirrors torso.cpp:181) → they now tick. (Sensor uses the simpler `!=4` gate; AmmoBin's gate was already correct.) - **AmmoBin layout: `ammoAlarm` retyped `HeatAlarm`(8B)→`WatcherGaugeAlarm`(0x54) + the `statusState` shadow deleted → `ammoModelFile` lands at the binary 0x1E8, `sizeof==0x22C` exact (locked, the load-bearing lock).** ⚠ CORRECTION to an earlier assumption: the all-laser Blackhawk is NOT AmmoBin-free — it DOES carry AmmoBins (the un-stub exposed the crash below ON THE BLH, and BLH tick rose 20→26 = Sensor+Searchlight+Thermal+AmmoBins). - **⚠ SHARED LATENT BUG EXPOSED + GUARDED (`HeatWatcher::WatchSimulation`, heatfamily_reslice.cpp:305):** the un-stub made `AmmoBinSimulation` (and via `PowerWatcher::Simulation`, the light/thermal sims) call the HeatWatcher base `WatchSimulation`, which derefs `watchedLink.Resolve()->currentTemperature` — the watch link isn't resolved in bring-up (the "WatchedSubsystem" name→roster-segment bind doesn't happen), so `Resolve()` returns null → AV (`AmmoBinSimulation`→`WatchSimulation`, caught under cdb). The binary never null-checks (its link always resolves at load). GUARDED (marked): `if (watched==0){ heatAlarm.SetLevel(0); return; }` — same pattern as `Emitter::TrackSeekVoltage`. FAITHFUL FOLLOW-UP = resolve `watchedLink` so the heat-watch alarm tracks the real watched temperature (it gates cook-off / light-blank / thermal-blank — secondary behaviors, inert until then). - **LAYOUT-LOCK FINDING (a new systemic fact): the heat-leaf branch (PoweredSubsystem:HeatSink) is NOT byte-exact to the binary** — Sensor's exact-offset asserts (radarPercent@0x31C) FAILED at compile time, unlike the re-based WATCHER branch (Searchlight/ThermalSight/AmmoBin offsets ARE exact and locked). So Sensor uses only the OVERFLOW lock (`sizeof<=0x328`); its own fields don't land at the binary offsets — fine until the attribute-table RAW read (the RadarPercent gauge) is wired (a Phase-4 HUD follow-up needing the heat-leaf branch re-based, same surgery the Watcher branch got). Nothing reads these subsystems at raw offsets today, so named-member access (compiled-consistent) is correct meanwhile. - **Factory now: 14 of 20 cases wired to real ticking classes** (was 10). Remaining stubs: Myomers (0xBC6, WAVE 6), ProjectileWeapon/MissileLauncher + the unresolved 0xBCE (0xBCD/0xBD0, WAVE 7), SubsystemMessageManager (0xBD3, WAVE 8), Gyroscope (0xBC4, deferred — NaN integrator). Kept DEFAULT-ON (no env gate — pure readouts, no bring-up fallback needed). **✅ WAVE 6 (Myomers 0xBC6, mover-coupled) — STRUCTURAL un-stub DONE (gated `BT_MYOMERS`, default ON; verified: BLH tick 26→27, locomotion + combat un-regressed, 0 crashes; `BT_MYOMERS=0` falls back to the Actuator stub → 26).** The real Myomers (the mech's artificial-muscle drive — voltage→`speedEffect`, throttled by heat + damage; ctor @4b8fec, `PoweredSubsystem:HeatSink` leaf) is now built at factory case 0xBC6 ("Actuator" mislabel) via `CreateMyomersSubsystem`. **KEY: it is wired INERT** — it constructs + ticks its real Performance but does NOT affect gameplay, which is exactly what makes a mover-coupled subsystem safe to land: (a) `MyomersSimulation` early-returns on `OwnerAdvancedDamage()` (the advanced-damage sim gate; the real source is the still-stubbed SubsystemMessageManager 0xBD3, so it's a `False` constant → no motion-heat); (b) the mover feed (`ConnectToMover`→`MoverAttach`) + `SetOwnerMaxSpeed` are no-op/local stubs → **the live JointedMover is never touched → the gait/drive cannot regress** (confirmed: the mech walks normally, pos advances, adv ramps 0.13→1.05). The ctor DOES do real work: resolves the powering Generator (`ResolveVoltageSource`) + builds the `seekVoltage[]` gear table from the resource. **De-shim:** the ~10 cross-family Mech/Mover/DamageZone shim BACKING fields (ownerBaseSpeed/…/damageStructureLevel + segmentFlags) were DROPPED and their accessors return the neutral defaults the ctor primed — necessary because the real object is EXACTLY 0x358 (zero headroom) and the shim fields would overflow the alloc; locked `static_assert(sizeof(Myomers)<=0x358)` (heat-leaf branch not byte-exact → overflow-lock only, like Sensor). The `Myomers` **name collision** is already handled in myomers.hpp (`#define Myomers Myomers__powersub_stale` around the powersub.hpp include, since powersub.hpp still carries the OLD Sensor-mislabel "Myomers" — that stale class should eventually be deleted from powersub.hpp). **⚠ AUTHENTIC COUPLING = the real Wave-6 payoff, DEFERRED:** making the mech actually SLOW when damaged/overheated + motion GENERATE heat needs (1) the advanced-damage gate from a real messmgr (0xBD3, WAVE 8), (2) the `owner*` shims redirected to real Mech motion/mass/motion-gain accessors, and (3) `MoverAttach` actually routing `speedEffect@0x31C` into the JointedMover roster — which **drives the LIVE mover**, so it must be reconciled with the gait cutover FIRST (both would feed the mover) before enabling. Factory now: **15 of 20 cases wired to real classes** (remaining: ProjectileWeapon/0xBCE/MissileLauncher 0xBCD/0xBD0 [WAVE 7], SubsystemMessageManager 0xBD3 [WAVE 8], Gyroscope 0xBC4 [deferred]). **✅ WAVE 7 (projectile/missile weapon families) — Phase A DONE: the three weapon SUBSYSTEMS are wired byte-exact (un-mislabeled per VDATA.h enum); Phase B (flying projectile/missile ENTITIES) deferred.** An 8-agent decomp-mapping workflow (scratchpad/wave7_maps.txt) mapped every ctor + fire/spawn path. **KEY IDENTITY CORRECTION (VDATA.h enum, base 3000=0xBB8):** 0xBCD=**ProjectileWeapon**, 0xBCE=**GaussRifle** (NOT MissileLauncher!), 0xBD0=**MissileLauncher** (NOT BallisticWeapon). The factory ctor-address COMMENTS were right but the built CLASS names were stubs/base. Un-mislabeled + wired via `CreateSubsystem` bridges (mirroring CreateEmitterSubsystem): **0xBCD→ProjectileWeapon** (@4bc3fc : MechWeapon, sizeof **0x448** — FIX: `AmmoBinConnection` was an empty 1B struct → retyped to the binary's 0xC SharedData::Connection, was making the object 8B short); **0xBD0→MissileLauncher** (@4bcff0 : ProjectileWeapon, sizeof **0x44C** — FIX: deleted `muzzleVelocity` [ALIAS of inherited `launchVelocity@0x410`] + `salvoCount` [SHADOW of `damageData.burstCount@0x3d4`], keeping `missileCount@0x448`); **0xBCE→GaussRifle** (@4bdcb4 : Emitter, sizeof **0x484** = Emitter+Vector3D muzzleVelocity — a DEDICATED bridge, not CreateEmitter, because its FireWeapon is a NO-OP `mov [eax+0x414],0` = a non-functional weapon in this 1995 build; also fixed GAUSS.CPP's discharge write `rechargeLevel`→`currentLevel@0x414` per the binary). All three `static_assert`-locked (offsetof/sizeof). Verified: Mad Cat (LRM/ballistic) + BLH construct the real weapon classes + tick authentic charge/ammo/heat/recoil, DESTROYED-in-8, 0 crashes, heapcheck-clean through construction. Factory now **18 of 20** (remaining: SubsystemMessageManager 0xBD3 [WAVE 8], Gyroscope 0xBC4 [deferred]). **✅✅ WAVE 7 Phase B DONE — the Mad Cat's LRMs now LAUNCH flying missiles that fly + damage (autocannon rounds too).** The byte-exact WORLD-ENTITY reconstruction (Projectile @4be1bc 0x340 / Missile @4bf5b4 0x368) is **infeasible on the engine**: measured `sizeof(2007 engine Entity)=0x1BC` vs the 1995 binary's 0x300, so the reconstruction's raw base-offset reads (velocity@0x1dc, roster@0x124, motion@0x250) read GARBAGE on the engine (the Mech 0x638-vs-0x854 gap, but the entity INTEGRATOR depends on those offsets). So — like the mech drive / beam renderer — flying projectiles are a **PORT reconstruction** (`BTPushProjectile`/`BTUpdateProjectiles` in mech4.cpp): seeded from the launcher's fire with the decomp's real muzzle (`GetMuzzlePoint`) / launch speed (`|launchVelocity|`) / per-shot damage (`damageData`, split across `missileCount`), they fly to the target (tracer via `BTPushBeam`) and deliver the weapon's damage on impact through the SAME `Entity::TakeDamage` path as the beam (aim `Mech::FirstVitalZone()`). **THREE bring-up fixes were needed to make a projectile weapon fire at all** (all found by tracing, marked): (1) **the trigger** — `fireImpulse` was only driven for the Emitter; `ProjectileWeaponSimulation` now sets `fireImpulse = gBTWeaponTrigger` too (else `CheckFireEdge` never sees an edge); (2) **the ammo bin** — `OwnerSubsystemCount/OwnerSubsystem` were stubbed `->0` so `ammoBinLink` never resolved and `ConsumeRound` always failed; redirected to the real roster (`owner->GetSubsystemCount()/ GetSubsystem(i)` — the AmmoBin 0xBCB constructs before the weapons 0xBCD/0xBD0); (3) **the jam roll** — `UniformRandom()` was stubbed `return 0.0f`, so `CheckForJam`'s `0 < jamChance` ALWAYS jammed (a projectile weapon could NEVER fire); replaced with a real LCG `[0,1)` (fires ~1-jamChance of the time — authentic occasional jams). Also: the mech's own target slot (`owner+0x388`) isn't populated in bring-up (the visible fire targets the `gEnemyMech` global), so `BTPushProjectile` **falls back to `gEnemyMech`**. Verified: Mad Cat PUSH=62 / IMPACT=31 (LRM missiles 3.33 dmg each split across the salvo + AFC100 autocannon 25 dmg), TARGET DESTROYED, 0 crashes; BLH un-regressed (also fires its ballistic weapon now). Diagnostics `BT_PROJ_LOG` (`[projectile] PUSH/IMPACT` + per-weapon fire trace). **The energy weapons (Emitter/PPC) still damage via the mech4 beam path.** REMAINING (deferred): the byte-exact world-entity Missile (`Projectile : Mover`, MP-replicable via `Registry::MakeEntity`) — the port projectile is master-local only (no MP replication); the real per-weapon fire-rate/heat wiring off the subsystem sim (the mech4 beam path is still the bring-up harness). **✅ HEAT-FLOW LINKAGE CONNECTED — weapon heat now conducts to the central sink** (verified: `[heat] conduct self.E=.. -> other.E`, sustained fire `FIRED #1..#21+`, combat intact, no crash). THREE fixes, each faithful to the decomp (no stand-ins): 1. **EmitterSimulation ran a no-op stub** `FUN_004b0bd0` for the "PoweredSubsystem step" -> the emitter never ran its heat sim. Replaced with the REAL `PoweredSubsystem::PoweredSubsystemSimulation` (@004b0bd0, chains to `HeatSink::HeatSinkSimulation` = absorb pendingHeat + `ConductHeat`). Emitter is `Emitter:MechWeapon:PoweredSubsystem:HeatSink`, so the qualified base call is valid. (emitter.cpp) 2. **Linked-sink Add-gate read the wrong flags.** Raw decomp of the HeatSink ctor @004adda0 gates BOTH the Performance-install AND the `linkedSinks.Add` on `param_2+0x28` = `owner->simulationFlags`, NOT the per-segment `subsystem_resource->subsystemFlags` (which stream 0 -> dead gate -> no links). The reconstruction had only half-fixed this (Performance gate on owner, Add-gate still on resource). Fixed the Add-gate to owner flags too. (heat.cpp) The link TARGET is still per-subsystem `heatSinkIndex`. 3. **⚠ NEW SYSTEMIC BUG — resource-struct layout mismatch (a sibling of the field-shadowing bug).** `HeatableSubsystem__SubsystemResource` inherited `Subsystem::SubsystemResource` (ends 0x30) instead of `MechSubsystem__SubsystemResource` (ends 0xE4) -- the class hierarchy is `HeatableSubsystem:MechSubsystem` so the RESOURCE must mirror it. Result: every heat field slid 0xB4 low; `heatSinkIndex` read a neighbouring float (`10.0f`=1/thermalMass) -> `GetSegment` OOB -> no link, and thermalMass/startTemp were garbage. Fixed the base. Then an 8-byte residual gap: `alarmModel` was typed as a 4-byte `ResourceID` (`typedef int`), but the RECORD reserves a 12-byte field at 0xCC (raw `@004ac9ec` parses it via FUN_00408944 as 3 floats, with `PrintSimulationState` at 0xD8). Added `int _alarmModelReserved[2]`. Now `startTemp=0xE4 thermalMass=0xF4 heatSinkIndex=0xF8` EXACTLY match the binary; heatSinkIndex reads real indices (5,2,4,..), thermalMass real values (1.39e6..), links resolve, ConductHeat flows. **DIAGNOSTIC TECHNIQUE (reusable for any resource-layout doubt):** log compiled member offsets via `(char*)&res->field - (char*)res` and compare to the binary's known offsets; dump raw record bytes as int+float side-by-side to see the true on-disk layout (records are pre-built 256-byte blobs loaded verbatim at fixed offsets, so OUR struct MUST match the binary or every member read is wrong). **NOTE on the payoff:** with the REAL data, `currentTemperature` stays ~77 (startTemp) because `thermalMass` is genuinely huge (~1e6) vs ~0.24 heat/shot; the visible metric that responds to fire is `heatLoad` (the smoothed radiated-heat reading), which rises/decays with firing. Not a bug -- authentic data behavior. (The "linked `other` sink reads heatEnergy~0" follow-up is RESOLVED — the link target was WRONG; see the heap-corruption root cause below.) **✅ THE BGF-LOAD HEAP CORRUPTION — ROOT-CAUSED & FIXED (was: `LoadBgfFile("bld08.bgf")` → `Builder::~Builder` → AV in `operator delete`, position-dependent).** A 4-agent workflow + hands-on forensics converged: - **The loader was INNOCENT** — an exact Python mirror of `bgfload.cpp` swept all 879 content BGFs with zero anomalies; every Builder write is bounds-guarded; and the crashed 0x768 block is exactly the correct 474-float capacity for bld08's 472 verts (MSVC 1.5× growth ...316→474). The AV was ntdll's free-list walk reading `0xDDDDDDDD` (the CRT freed-fill) inside the ADJACENT block's NT heap metadata — prior corruption, merely DETECTED at the loader's teardown. `BT_PROBE_BGF=` + `BT_PROBE_N` (btl4main.cpp) now direct-loads models at boot for regression (bld08 clean 25×; bld08_lp is a bound-only file, benign FAIL). - **THE CULPRIT — a TYPE CONFUSION in the heat/power linkage (reconstruction bug):** `HeatSink`'s ctor resolved its linked sink via `owner->GetSegment(heatSinkIndex)` — the Nth skeleton **EntitySegment** (288 bytes) — cast to `Subsystem*`. The binary (`@004adda0`, part_012.c:16999) instead reads **`owner->subsystemArray[heatSinkIndex]`** (the SUBSYSTEM ROSTER @0x128, bounds-checked vs subsystemCount@0x124, null-guarded). Through the bogus pointer, every per-frame `ConductHeat` wrote `other->pendingHeat` at offset 388 = **100 bytes past the 288-byte block**, and `BalanceCoolant` wrote `coolantLevel` 20 bytes past — thousands of 4-byte OOB heap writes during sustained fire → smashed NT free-list links next to the mech's EntitySegments → the next big alloc/free churn (a lazy building load) AV'd. **Sibling bug in `PoweredSubsystem`** (raw part_013.c:1198): `voltageSourceIndex` (res+0xFC) is ALSO a roster index; the GetSegment draft made `AttachToVoltageSource` write `currentTapCount` 136 bytes past the segment block at every mech spawn. BOTH fixed to the roster lookup via the public `owner->GetSubsystemCount()/GetSubsystem(i)` (+ the powersub else-gate fixed to OWNER flags per raw). Verified: sustained combat under `BT_HEAPCHECK=1` (`_CRTDBG_CHECK_ALWAYS_DF`, whole-heap validation on every alloc/free — now a runtime env gate in btl4main.cpp) = 100+ shots, ZERO detections; the heat link now reads a REAL sink (`heatEnergy=1.34e+07`, was ~0 through the segment). **LESSON (add to the systemic checklist): a `+0x128`-style owner offset in subsystem code is the subsystem ROSTER (`subsystemArray`), NOT the segment table — check every `GetSegment(int)` call in reconstructed subsystem ctors.** - Bonus findings from the audit (non-corrupting, tracked): `keyframeData[keyframeCount]` in LoadLocomotionClips is BINARY-FAITHFUL (the raw does the same read; the value is stable/in-bounds in real clip data — do NOT "fix" to [count-1]); crashClipA/B/C+gimpBaseClip are separate members here but live INSIDE `animationClips[]` in the binary ([3]/[2]/[4]/[32]) — a wrong-clip fidelity gap for crash/gimp gait states when those go live; `Mech::CreateModelResource` alloc 200 < sizeof 356 is a LATENT overflow in dead offline-tool code; Torso 640==0x280 alloc is an EXACT fit (zero headroom — any Torso/Watcher growth must bump the factory alloc); StatusMessagePool->New() at btplayer.cpp:460 derefs a NULL stub pool (latent); mechdmg.cpp:289 leaks IntegerPlugs into a no-op DZIndexTable::Add. **✅ RESOURCE-LAYOUT AUDIT — DONE (full report: `docs/RESOURCE_AUDIT.md`; commits 802a7a6/7de66ad/975a397).** A 33-agent adversarial audit of all 24 `*__SubsystemResource` structs found **8 confirmed layout bugs** (same silent-garbage class as the heat fix), all rooting to 2 broken base resources (`HeatWatcher`, `PowerWatcher`) + 2 field defects (`Gyroscope`, `Searchlight`). Raw-decomp-verified + FIXED: **HeatWatcher re-based onto MechSubsystem** (record 0xF0; class+resource+ctor+parser, AmmoBin child follows) and **PowerWatcher re-based onto HeatWatcher** (record 0xF4; HUD/ThermalSight/Torso children follow) — the reconstruction had approximated BOTH as `: public HeatableSubsystem`, sliding every Watcher field low. Gyroscope got its unread +0xF4 record slot (exageration→0xF8); Searchlight now reads the inherited `segmentIndex`@0x28 (was a bogus appended field). Combat + heat-flow un-regressed. **KEY PRACTICE ESTABLISHED: lock every resource-struct layout with `static_assert(offsetof(...)==0xNN)` + `static_assert(sizeof(...)==0xNN)`** against the binary's known offsets — compile-time proof that can never silently regress (far better than a runtime log; the test mech may not even instantiate the subsystem). Cascade notes: re-basing a Watcher class off HeatableSubsystem un-shadows its `degradation/failureTemperature`; child `HandleMessage`/`Simulation` delegates that called `HeatableSubsystem::` must redirect to the real base (`MechSubsystem::HandleMessage`, `HeatWatcher::ResetToInitialState`); a re-based class's header needs a guarded `#include` of its new base's header (powersub.hpp now includes heatfamily_reslice.hpp). **✅ HEAT-LEAF OBJECT-LAYOUT RE-BASE — the CORE heat leaf is now byte-exact (P7 core DONE; full status `docs/HARD_PROBLEMS.md` §P7).** The OBJECT-layout analog of the resource-layout audit: the reconstruction modeled the binary's shared alarm/connection types with UNDERSIZED stand-ins, sliding every field above them low (the 72-byte `PoweredSubsystem::auxScreenNumber` gap). FIXED byte-exact + `static_assert`-LOCKED: **`HeatSink`** (heatEnergy@0x158/linkedSinks@0x164/heatAlarm@0x170/pendingHeat@0x1C8, sizeof 0x1D0), **`Condenser`** (0x230 region), **`Reservoir`** (sizeof 0x230), **`Generator`** (sizeof 0x250), **`Myomers`** (phantom tail removed). The shared types are now correct: **`SubsystemConnection` = 0xC** (was 4; binary link node, `FUN_00417ab4` two-level-derefs +8) and **`GaugeAlarm54` = 0x54** (the real `AlarmIndicator` `FUN_0041b9ec`; STATUS level at **+0x14**, so `subsystem+0x184 == heatAlarm+0x14 == GetLevel()`; `WatcherGaugeAlarm` typedefs to it). Heat conduction reads the REAL `heatEnergy=1.34e7`; combat DESTROYED-in-8, 0 crashes, heapcheck-clean through construction. **THREE new systemic bug classes fixed (add to the checklist):** (1) an **alias field** = a subclass member re-declaring an inherited slot the ctor reuses under a new name (Condenser `refrigerationOutput`==inherited `massScale@0x160`; Reservoir `coolantCapacity`==`thermalCapacity@0x128`; Emitter `outputVoltage`==inherited `rechargeLevel@0x320`; Emitter `beamLengthRatio`==`beamScale.z@0x434`; Emitter `firingArmed`==inherited `useConfiguredPip@0x3E0`) → delete the member, use the inherited/existing name; (2) an **alarm-interior field** = a value the binary reads at `alarm+0x14` modeled as a separate member (HeatSink `heatState@0x184`, Reservoir `injectActive@0x1e4`) → route to `alarm.GetLevel()`; (3) a **phantom field** = a member at an offset PAST the object (Generator `shortFlag@0x25C` is really `*(owner+0x190)+0x25c` the msg-manager; Myomers `moverConnection@0x110` a write-only base slot; MechWeapon `segmentReference/pipSegment/hasTarget/targetPoint/muzzlePoint` past 0x3F0; Emitter `beamHitPoint/beamImpact/beamImpactScalar/beamColor/beamHitData/energyRampTime` — binary writes into inherited `damageData`/`voltageScale` or the value is a method local) → remove it, read the real source or use a local. **✅✅ SUBSYSTEM-TREE ALARM UNIFICATION — DONE: the WHOLE PoweredSubsystem weapon/power subtree is now byte-exact (P7 CLOSED; full per-class map from the 8-agent `alarm-unification-map` workflow, saved scratchpad/alarm_unify_maps.txt).** Making `PoweredSubsystem` byte-exact (its two 0x54 alarms `electricalStateAlarm@0x264`/`modeAlarm@0x2B8`) grows it +0x98, cascading into every subclass — so all were byte-exacted TOGETHER in one build (each `static_assert`-locked against its ctor + factory alloc; the chain HeatSink 0x1D0 → **PoweredSubsystem 0x31C** → **MechWeapon 0x3F0** → **Emitter 0x478** → PPC 0x478; PoweredSubsystem → **Sensor 0x328**, → **Myomers 0x358**, all verified against the raw ctors @004b0f74/@004b99a8/@004bb120/@004b1d18/@004b8fec). Each class: retype its `AlarmIndicator`(==`ReconAlarm`==4B) /`HeatAlarm`(8B) stand-in for the binary 0x54 alarm → `GaugeAlarm54`; delete phantom/alias/duplicate fields; `friend struct XLayoutCheck` with `offsetof`/`sizeof` locks (protected members need the friend; public fields work at namespace scope). **KEY non-layout fixes required in the same wave:** (a) `outputVoltage`→`rechargeLevel` also in the compiled **`GAUSS.CPP:74/93`** (external readers of the removed Emitter field — grep EVERY TU, not just the class's own .cpp); (b) `MechWeapon::GetMuzzlePoint` reimplemented faithfully (the removed `muzzlePoint` member collided with Emitter's own fields at 0x3F0) via a **`BTResolveWeaponMuzzle` void\* bridge in mech4.cpp** (a complete-Mech TU with the segment API; mechweap.cpp reaches `owner` only as a raw pointer) resolving the weapon's mount segment (inherited `this+0xdc`) through `GetSegment(int)`+segment→world; (c) `DetachFromVoltageSource` was setting the WRONG alarm (`modeAlarm`→`electricalStateAlarm`, raw @004b0e30 writes 0x264). Verified: combat DESTROYED-in-8, 28 shots, 0 crashes, heat `heatEnergy=1.34e7`, all locks pass. **The vehicleSubSystems aux-screen gauge raw reads (`btl4gau2.cpp:868/952` at `subsystem+0x2b8`/`+0x278`) + the Sensor RadarPercent path now read the CORRECT byte offsets** (they were garbage under the short layout). ⚠ `runtime Check(sizeof<=alloc)` in a factory bridge does NOT fail the build (it's a runtime assert → heap overflow at construction); use a `static_assert` sizeof lock to catch alloc overflow at COMPILE time (this is why the earlier isolated PoweredSubsystem retype silently overflowed the Emitter alloc). **TECHNIQUE:** a read-only decomp-mapping Workflow (one agent per class decodes its ctor → offset map + phantom/alias findings, + adversarial verify of the composed chain) then hands-on implementation — the right shape for a systemic multi-class byte-exact re-base. **✅ MechControlsMapper REVIVED — the real input-interpretation tick runs LIVE (gated `BT_REAL_CONTROLS=1`).** The fully-reconstructed mapper family (mechmppr.cpp `MechControlsMapper` @004afbe0-@004b08c0; btl4mppr.cpp `L4MechControlsMapper`/`MechThrustmasterMapper`/`MechRIOMapper` @004d11e8-@004d27f8) was already built + constructed at runtime (btl4app `MakeViewpointEntity` builds a `MechRIOMapper` into roster slot 0 via `SetMappingSubsystem`); the ONLY bypass was the tick being skipped (mech4.cpp roster walk) because of raw-offset bugs. FIXED (a 4-agent workflow mapped the binary/API/state first): (1) **wild typed-pointer arithmetic** in `InterpretControls` (`*(Scalar*)(mech + 0x34c)` on `Mech*` = +0x34c*sizeof(Mech) — THE AV) → all owner reads now reconciled members: `mech+0x34c→reverseStrideLength` (naming caveat: it is the TOP/run speed the throttle scales — CONFIRMED by data: speedDemand=61.5 == the authentic run speed), `0x534→walkStrideLength`, `0x5c0→forwardThrottleScale` (NEW member; writer not in decomp, ctor=1.0), `0x438→sinkSourceSubsystem` (real binary-exact Torso: `SetAnalogTwist/ElevationAxis`@0x1F0/0x1F4, `GetHorizontalEnabled`@0x250), `0x5b4→hudSubsystem`; (2) **the 0x5b4 cache was MISWIRED**: raw factory (part_012.c:10155-10164) case 0xbd6 (HUD, alloc 0x2a4) writes `param_1[0x16d]` — case 0xbdc (MechTech, binary alloc 0x104!) has NO cache; our factory had it on MechTech (renamed member `mechTechSubsystem`→`hudSubsystem`, cache moved to the HUD case; MechTech's 0x104 alloc could never hold the mapper's `+0x28c` write); (3) **HUD `flickerPhase`@0x28C renamed `freeAimSlew`** — it IS the mapper's free-aim/reticle-slew output (was init-only placeholder); (4) **`FillPilotArray`'s wild app reads** (`application+0x6c→station+0x190` — the OTHER AV) → the native `application->GetMissionPlayer()` + `GetEntityID()` (the same drift-fix btl4mppr's SendFakeButtonEvent uses); (5) the look/eyepoint COMMIT deferred (marked): its binary offsets collide with declared members (`mechName`@0x360, `stateFlags`@0x410, `poweredSubsystems`@0x7bc) — arbitrate before enabling; no port consumer. **Wiring (mech4.cpp, gated):** an INPUT BRIDGE writes the mapper's published input attributes from WASD/ env exactly as a streamed Direct `.CTL` mapping would (the dev box has no RIO/Thrustmaster; interpretation stays 100% authentic), the roster walk un-skips the mapper, the SM consumes `bodyTargetSpeed=|speedDemand|` and the drive heading uses `turnDemand` (mode-shaped steering with the authentic speed-vs-turn clamps). **Verified live:** `[mppr] thr=1 → speedDemand=61.501` → SM walk→run at the commanded speed; full tick chain (RIO→L4→main + BuildPilotArray) clean; baseline (env off) combat un-regressed (FIRED #221); heapcheck run with real controls + combat = 61+ shots, zero detections. **Controls plumbing facts (from the workflow):** the engine is a PUSH model — `LBE4ControlsManager` groups (`scalarGroup/joystickGroup/keyboardGroup/ buttonGroup`) are fed by ALL devices (RIO serial on the pod, DirectInput on dev) and mapper members register via `group[elem].Add(mask,&member,this)`; `L4CONTROLS=KEYBOARD` gives NO analog axes (key-RELEASE events only) — so a real stick needs `L4CONTROLS=DIJOYSTICK`/`THRUSTMASTER`, or the bridge. **Remaining mapper work:** fire-trigger through the mapper (streamed `.CTL` event mappings → weapon demand; gBTWeaponTrigger still the bring-up), look/eyepoint offset arbitration, control-mode cycling (message handlers exist), `ControlsMapper`-vs-`SubsystemMessageManager` untangle at factory 0xBD3 (binary 0xBD3 = messmgr; the mapper's canonical home is roster slot 0 — our 0xBD3 case builds a base mapper as a stand-in). **✅ PILLAR A — THE MOTION MODEL IS COMPLETE & DEFAULT-ON (locomotion P3 closed for MP purposes).** (1) **LEG callback reconstructed** (`FUN_004a6928` == `PTR_LAB_0050d6f0`, PE-parse + capstone jump table @0x4a69aa): `Mech::LegClipFinished` — same 33-state shape as the body's but compares the LIVE mapper `speedDemand` (binary: `*(subsystemArray[0])+0x128`; typed mirror `controlsMapper`), slews `legCycleSpeed` @0x348, re-arms via `SetLegAnimation`, gimp alternates 0x12↔0x13 with |ratio| (negative gimp stride). (2) **The two-channel split is LIVE** (under real-controls): the LEG channel writes the skeleton JOINTS from the live demand, the BODY channel is pure motion (`Advance(dt, move_joints=0)`) — the binary's leg=local-sim / body=displayed-motion model. The leg enters NATURALLY from Standing: verified progression legState 0→5 (stand→walk) →11 (walk→run) →13↔12 alternation converging on the commanded 61.5 u/s. **KEY RAW FIX: the Standing case was INVERTED in BOTH channels** (recon had `commanded < standSpeed → walk`; raw FUN_004a5028/FUN_004a5678 case 0 is `standSpeed < commanded → SetAnimation(5)`, `<0 → reverse 0x10`, else stand) — without the fix a commanded mech never left Standing. Also fixed: `AdvanceLegAnimation` double-deref'd the never-initialized `controlSource` alias (AV) → reads `controlsMapper->speedDemand` (null → 0 = idle); `legAnimationState`@0x3b0 == `legStateAlarm.level` re-synced (same as body). (3) **Orientation** now composes the yaw RATE into `localOrigin.angularPosition` via the engine integrate op (`Quaternion::Add` == FUN_00409f58) instead of rebuilding from a scalar heading — the mech keeps its authentic spawn orientation (rate CONSTANT still bring-up; the per-mech turn-rate param is deferred). (4) **Velocity storage every frame** (was collision-gated): `worldLinearVelocity` from the frame delta + `localVelocity` (linear fwd −Z + yaw angular) — the exact fields `Mover::WriteUpdateRecord` publishes = the MP dead-reckon update-writer data. `DeadReckonPose` was already reconstructed; consuming it is MP-side. (5) **Defaults flipped** (see §10a). Consequence of the authentic spawn orientation: `BT_SPAWN_ENEMY` now places the dummy along the spawn FACING (btplayer.cpp; the fixed z-offset assumed the old forced heading and the mech walked away → "no damage" red herring). Verified default-stack combat: structure→0.933. **Remaining locomotion polish (non-gating):** authentic turn-rate constant; body-callback gimp handlers (targets 0x70b2/0x7161 undecoded); airborne callbacks (`FUN_004a6344`/`FUN_004a7970`); wall-block-vs-climb tuning; collision damage application; the eyepoint/look commit (mapper). **✅ SHADOW — the AUTHENTIC 1995 model decoded + implemented, render-verified (task #20, commit b38e94b).** BT 4.10's shadow is NOT a projection: it is the flat **`*_tshd.bgf` proxy** drawn as ordinary geometry, posed by **`jointshadow`** (balltranslate under ROOT; SKL comment "apply terrain angle to pitch and roll") and its child **`jointtshadow`** (hingey; "apply torso twist to yaw", Torso record key `TorsoHorizontalShadowJoint`). Configured by the model record's **`ShadowJointName` @0xB4** (≤19 chars, "Unspecified"=none), resolved in the Mech ctor (part_012.c:10285-10310 → **`this[0x10b]` @0x42c** = the shadow Joint — the earlier recon misread this block as a death-effect lookup; now `Mech::shadowJointNode`). `dpl_SetGeometryShadowVolume` exists in libDPL headers but has NO surviving game callers. Port implementation: (1) `d3d_OBJECT::mIsShadow` + a dedicated DrawMesh path — blend-pass only, SRCALPHA/INVSRCALPHA, no Z-write, constant TFACTOR `0x8C000000` (~55% black), all touched states saved/restored exactly; btl4vid tags `tshd` objects at load AND sets each drawOp `alphaTest=true` (**pass scheduling fact:** `HierarchicalDrawComponent:: Execute` L4VIDRND.cpp:149 routes objects into PASS lists per-drawOp on alphaTest/drawAsDecal/drawAsSky — an object with all-false drawOps only ever reaches PASS_OPAQUE). (2) `Mech::UpdateShadowJoint(normal)` writes pitch/roll to jointshadow per-frame. **✅ SHADOW TERRAIN-TILT NOW LIVE (was the flat-up placeholder; default-on, gate `BT_SHADOW_TILT=0` restores flat).** The user reported the shadow DISAPPEARS on elevation + the feet look sunk. Root cause: mech4.cpp fed `UpdateShadowJoint` a hardcoded flat-up normal, so on a slope the flat `*_tshd` quad was buried in the hillside and Z-culled (it draws Z-tested, no Z-write, in PASS_ALPHABLEND) → gone; and the missing ground-contact cue made the feet READ as sunk even though the origin is measurably ON the surface (alignment probe at the mech's climb positions: visual−origin = 0.05–2.7u, i.e. NOT a placement bug — the snap is correct). Fix (mech4.cpp shadow block): now that the ground decode (#15) is live, sample the collision surface via `GetMoverCollisionRoot()->FindBoundingBoxUnder` at the mech's XZ + two world-axis offsets (D=12u), build the world normal from the height gradient `(-dH/dx,1,-dH/dz)`, rotate it into the mech-local frame by `-gDriveHeading` about Y (mech is upright, yaw only), and pass it in — jointshadow's SKL comment is literally "apply terrain angle to pitch and roll". A probe miss (mech at its collision node's edge) falls back to flat. **Render-verified:** on a butte-flank slope the shadow now LIES ON the ground (tilted, follows the slope) and anchors the feet; pre-fix the same steep mid-climb showed NO shadow; combat un-regressed (TARGET DESTROYED after 8 hits), 0 crashes / 0 heap detections under BT_HEAPCHECK. NOTE the probe uses the mech's `containedByNode` so far offsets can miss (→ flat fallback); a residual uphill-foot dip on steep slopes is the authentic upright-biped geometry + coarse-collision (0–2.7u), not a placement bug. (3) Torso yaw→jointtshadow was already wired (Torso::UpdateJoints; data-disabled on the BLH record, byte-faithful). **✅ THE "MECH SINKS INTO INCLINES + SHADOW DISAPPEARS" BUG — ROOT-CAUSED & FIXED (shadow DEPTH BIAS, L4D3D.cpp mIsShadow block; BT_SHADOW_BIAS= tunes, =0 restores plain Z-test).** The user reported feet sinking + shadow vanishing on ANY moderate incline (original vids show neither). Systematic elimination: (a) flat ground placement EXACT — dg100's instance Y is −0.453125 (map dumps printing "−0" hid it); visual floor AND slab top both at −0.4531 = the mech's resting Y, zero offset; (b) snap faithful (origin = solid surfaceY, verified decode); (c) LOD selection correct — BGF LODs are FINEST-FIRST (dhillg1 136→47 tris) and the loader takes the first; ⚠ DG100 the desert-floor model is PERFECTLY FLAT (all verts Y=0) — the rolling desert relief is the giant dhillg mounds (r≈950); (d) LOD0-exact content gap (earlier merged-LOD measurements were inflated): mound visual runs 0–2.1u ABOVE the collision cones on slopes (median 0–0.5, p90 1.2–1.5) = ankle-to-shin foot clip at worst patches — authentic, the binary had the same. **THE REAL RENDER DIVERGENCE: the shadow proxy drew with a plain Z-TEST, so wherever the visual rose even ~0.3u over the quad the WHOLE shadow was buried+culled — and without its ground-contact cue the small authentic foot-clip reads as "the mech is sinking".** The IG board drew ground shadows decal-style (depth-biased coplanar geometry — the classic blob-shadow technique; the shadow FLICKER in pod footage is that bias fighting at edges). FIX: the mIsShadow draw now sets `D3DRS_DEPTHBIAS` (default −0.004 normalized ≈ 2–4u at chase range) + `SLOPESCALEDEPTHBIAS −1`, saved/restored exactly. **Render-verified:** standing mid-slope at the WORST measured gap spot (606,621 on dhillg2) = shadow fully present lying on the slope + feet visible planted (was: shadow gone, feet "buried"); moderate inclines while walking ✓; flat ground crisp, no bleed; combat un-regressed (DESTROYED after 8 hits, 0 heap detections). Note the residual ≤2.1u foot-clip at worst patches is authentic content coarseness — with the shadow anchoring the mech it reads as standing in soft sand, matching the vids. (A whole-mech terrain-tilt experiment was tried and REJECTED — the 1995 mech is upright-only; do not tilt the body.) `BT_CAM_Y`/`BT_CAM_Z` (btl4vid.cpp) override the debug chase-camera offset — diagnostic for separating camera occlusion from real geometry clipping. **✅✅ VISUAL-GROUND CONFORM — the residual foot-clip is now GONE too (btvisgnd.cpp, PORT ADDITION, presentation-only; gate `BT_VISUAL_GROUND` default ON).** The user (rightly) rejected "authentic ≤2.1u clip": the resolution to "how did the devs let this slide" is **the 1995 game never rendered this view** — it was cockpit-only (you can't see your own feet; other mechs are 100+m away = subpixel), so the artists never needed visual-solid agreement; OUR port adds a close external camera, so the port also owns making it correct. The fix samples the ACTUAL rendered terrain triangles under the mech and lifts the RENDER matrix to the visible surface: at mission load, walk the map's MakeMessage stream (`mission->GetMapID()` → `FindResourceDescription` → Lock; class-42 instances, skip includes/sky/profile/rwin backdrops), load each terrain model's LOD0 triangles via the ENGINE `LoadBgfFile` (bgfload.h — game-side callable; indices are double-sided, 6 per tri, take first 3), cache per-model + per-instance world XZ AABBs; per frame `BTVisualGroundLift(x,y,z)` = highest triangle surface at the mech XZ within a LOCAL band [y−1, y+3] (band keeps a butte-base query on the skirt, not the mesa top; clamp [−1,+3]) → `localToWorld(3,1) += lift` at the PerformAndWatch tail (mech4.cpp). **`localOrigin` is NEVER touched — collision/aim/damage/dead-reckoning stay on the authentic solid model; next frame's drive rebuilds localToWorld, so the lift is per-frame render-only.** Zero tuning constants — exact content geometry both ways (lift up where visual bulges over the solid, settle down where it dips). **Verified:** flat desert lift≈0 (float epsilon — confirms both sampler and the flat-exactness finding); full mound crossing lift tracks ±0.02..0.79u continuously; standing + walking mid-mound = feet ON the visible sand, shadow present (the user's exact repro case); combat un-regressed (DESTROYED after 8 hits), 0 crashes / 0 heap detections. NOTE: masters only (replicant render-conform is an MP follow-up); needs cwd = pod BT dir (LoadBgfFile indexes VIDEO/ relative — already a runtime requirement). **⚠ ENGINE-CLASS GOTCHA (add to the checklist):** a NEW member added to a 2007 engine class (d3d_OBJECT etc.) MUST be initialized in EVERY ctor init-list — the debug heap fills fresh allocs 0xCDCDCDCD, so an uninitialized flag reads TRUE (here: every object in the world drew as translucent-black "shadow" → near-black scene); and any state the shadow/special draw path sets on the D3D device must be save/restored exactly (a leaked `LIGHTING FALSE` + `ALPHAARG1=TFACTOR` also darkened the whole frame). Render-verified via PrintWindow A/B on DBASE: baseline opaque black blob → dark translucent silhouette, terrain/feet visible through it. **✅ BACKFACE CULLING RESTORED (task #20, commit a2ed21a; `BT_CULL` env cw/ccw/off, default CW).** The bring-up `D3DCULL_NONE` drew interior/back faces the original culled — the "dark wedge" shapes in the distance were the INSIDES of dune/butte meshes, and standing inside a mound visual looked like being sealed in rock. CW is the correct winding for our BGF quad triangulation `[a b c][a c d]` (CCW/none selectable for debugging). Render-verified on dbase; combat clean. **CONTENT FACT (the "mound swallow" investigation):** dbase's dune-mound groups (`dhillg*`) have CRESCENT-RIDGE visuals with HOLLOW interiors but solid FULL CONES (hillg2_c.sld: 2 cones r=200/250 h=36/60) — where the visual exists it matches the solid within ~1u (verified by triangle-exact BGF parsing vs the analytic cone), but a mech can walk through a ridge crest into the hollow and ride the invisible cone — AUTHENTIC 1995 coarse collision, not a port bug. Butte cliff tiers verified BLOCKING (beeline repro: 403 frame rejections, zero penetration). Analysis tooling now permanent: `[contact]` telemetry in Mech::ProcessCollision, an INSIDE-SOLID streak detector (BT_GROUND_LOG), `BT_GOTO="x z"` beeline harness (mech4 computes → mapper bridge consumes; the turn-sign/convergence is rough — debug only), and the RES/map/solid scanners preserved in the scratchpad (`resscan.py`/`mapscan.py` — worth moving to tools/). **✅✅ THE AUTHENTIC DPL ENVIRONMENT IS NOW HONORED — day sky + sun + haze + draw distance (task #20, commits 5b615e7 + 9ca9a48).** The user compared the desert level to pod footage — ours was dark/muddy with a wrong sky; the fix transformed it to match. ROOT CAUSE: the env pipeline (`DPLReadEnvironment` → `DPLReadINIPage`, reading **`BTDPL.INI`** in the pod BT dir — set via `L4DPLCFG`) **already ran and correctly resolved** the map+time via compare/branch (`main → dpl_defaults → des_day → dsdayclear` for dbase/day), setting the AUTHENTIC **clip=2100**, **fog=600/2050** (0.46,0.46,0.68 haze), **ambient=0.45 0.4 0.45**, and a **directional sun** (1.10,0.9,1.00 @ −50,10,0) — but THREE bring-up stubs clobbered it: (1) `EnsureValidProjection` (the mission-load safety net, btstubs.cpp) forced clip far=1300, killed fog (`currentFogNear/Far=1e9`+FOGENABLE FALSE), set WHITE ambient → now a FALLBACK that only overrides when the env failed (`viewAngle<=0 || clipFar<=clipNear`); when valid it just re-asserts the env projection. (2) the per-frame lighting block re-set `D3DRS_AMBIENT` to gray `0x404040` (~0.25) every frame → now re-asserts the captured `mEnvAmbient` (from the map's `ambient=`); `BT_AMBIENT=` still overrides. (3) `mCloudRed/Green/Blue`+`mCloudEmit*` were UNINITIALIZED in the ctor (0xCDCDCDCD garbage into the sky material multiply) → init 1.0/0.0. **THE SKY:** the sky is a `*sky.bgf` dome (dbase=`dsky`, grass/arena= `sky`) authored with a cloud TEXTURE (`btfx:dsky_mtl`) + white vertex colours. The original drew it in a dedicated fullbright **PASS_SKY** (D3DRS_LIGHTING FALSE + extended fog, already present from earlier task-#20 work), but the BGF loader route hardcoded `drawAsSky=false` (the `.sky` sidecar the `.x` path keys on doesn't exist), so the dome fell into PASS_OPAQUE → LIT (half-dark) + FOGGED → the dark navy dome. FIX: `LoadObjectBGF` tags any `*sky*` object `drawAsSky=true` → PASS_SKY → the cloud texture shows fullbright. **Render-verified dbase A/B:** dark muddy + navy dome → bright warm sand + blue cloudy sky + soft horizon haze + distant mesas (2100 far). Combat clean. **KEY DEBUG TECHNIQUE:** the env parser's `DPLReadINIPage processing ` + `Clip/Fog/Ambient/Light` banners are gated on `[main] debug=` — copy `BTDPL.INI` → `*DBG.INI` with `debug=True`, point `L4DPLCFG` at it, and the log shows every page visited + value set. **STILL STUBBED (follow-ups):** the `objectpath`/`materialpath`/`texmappath` day/night PRIORITY is commented out (`dpl_AddToObjectFilePath` calls) — the BGF loader (`bgfload.cpp`) recursively indexes the whole `VIDEO/` tree **first-match-wins**, which happens to pick `DAY/` (alphabetically first) for day missions, so day is accidentally correct; NIGHT/morning/evening missions would get the wrong (day) sky+textures, and the grass map renders pale pink/purple terrain (a wrong-time or wrong material picked). Proper fix = reimplement the path priority so the loader prefers the env's objectpath/materialpath dirs in order. **◐ DAY/NIGHT PATH PRIORITY — infrastructure done, materials gated (commit 90a8d92).** `bgfload.cpp` now ranks every index candidate by the INI's search dirs (most-specific-first) and keeps the best per stem; `SetVideoPathPriority(cacheKey, dirs)` (bgf/bmf/img) is fed the accumulated objectpath/materialpath/texmappath lists from `DPLReadEnvironment`. **BGF priority (geometry) is ON; BMF/IMG (materials/textures) are GATED `BT_MATPRI=1`, default OFF** — a naive "prefer mat\day" over-applies to the SHARED desert terrain materials and washes the warm day terrain to GRAY (render-verified regression on the user's dbase/day). The sky's day/night is itself MATERIAL-driven (`btfx:dsky_mtl` in mat\day|night), so a correct night sky needs BMF priority — which trades against the day terrain; resolving cleanly needs the terrain-material model understood (why mat\day grays the desert — likely a shared lib whose mat\day variant differs, or a resolution-failure gray fallback). **⚠⚠ CRITICAL GOTCHA — `/FORCE` turns an unresolved symbol into a runtime crash, NOT a link error.** `SetVideoPathPriority` was first defined INSIDE bgfload.cpp's anonymous namespace → internal linkage → `L4VIDEO.obj`'s call was unresolved → the BT game link (uses `/FORCE`) silently stubbed it → a call jumped to `__ImageBase` (`0x009d0000`), a DETERMINISTIC AV in `LoadMissionImplementation` that looked like heap corruption. Fix: define it OUTSIDE the anonymous namespace. **When a `/FORCE`-linked build crashes with a garbage call target near the image base, grep the link log for `unresolved external` — the "successful" build is lying.** Also: `BTL4VideoRenderer` is allocated by the GAME (btl4app.cpp:242) as a `DPLRenderer` subclass, so ANY member added to `DPLRenderer` (L4VIDEO.h) needs the game's btl4app/btstubs/btl4vid objects recompiled — CMake header-dep tracking through the `btbuild/fwd/*` shims is unreliable; delete stale `.obj`s if layout-mismatch corruption appears. **✅✅ RENDER FIDELITY — the dbase desert now matches the pod footage (task #20; a 10-agent measured render-fidelity workflow found 6 defects, 3 fixed = the "big three").** The user compared our dbase to authentic footage (`C:/git/original.PNG`): ours was harsh/saturated/faceted; the original is soft, pale, low-contrast. Root causes (measured, not guessed): 1. **⭐ Mountains "inside-out/faceted" = MALFORMED GEOMETRY, not lighting (commit d189371).** `BGF_FORMAT.md` wrongly documented **`CONNECTION_LIST` (tag 0x0047) as "one polygon per chunk, fan-triangulate"**. It is actually a **FLAT TRIANGLE LIST** (3 indices/face). Fan-triangulating the whole chunk as one N-gon turned buttec's 115 real triangles into 341 fan-garbage with smeared UVs + cancelled normals. FIX: group-of-3 walk in the `!gotPconn` branch (bgfload.cpp buildPmesh + port/src/bgf.cpp). **Corpus-verified: ALL 3488 CONN chunks in the pod GEO tree have n%3==0.** bgftest BUTTEC.BGF: 341→115 tris. Gated BT_TERRAIN_TRILIST. 2. **Horizon seam = flat sky plane truncated by the world projection (commit 2434bbc).** DSKY is a FLAT plane at Y~110 (±6000), NOT a dome. The world far=2100 projection truncated it above the true horizon → the gap showed the fog-colour Clear as a hard lavender band; the old sky-pass fog (near×3/far×6) left the rim unfogged. FIX: sky-pass gets its own far=9000 projection + the SAME fog as the world (drop ×3/×6), BOTH restored after the sky loop (mandatory — the alpha/particle/HUD passes need the world projection). Gated BT_SKY_FAR. 3. **Sky "two-tone navy, nothing like clouds" = MODULATE vs SCREEN (commit 2a8aa37).** The cloud art (bintA) is a GRAYSCALE intensity map; the sky colour is the material tint (0.3,0.5,1.0) baked into the vertex diffuse. MODULATE (texel×tint) → saturated navy. The original SCREENed toward white: `screen(D,T)=T+D·(1-T)=lerp(D,white,T)` → bright texels near-white, dark → pale tint. FIX: one texture stage `D3DTOP_LERP(Arg0=TEXTURE, Arg1=TFACTOR=white, Arg2=DIFFUSE)`, states saved/restored around the sky draw. Measured `screen(0.3,0.5,1.0,0.85)=(228,236,255)` ≈ original top `(229,230,255)`. Gated BT_SKY_SCREEN. **THE AUTHENTIC IG-BOARD SHADING MODEL (from libDPL DPLTYPES.H:189-199):** shading is selected PER-GEOMETRY by vertex type — no-normal geometry (XYZ_UV / XYZ_RGBA_UV: mesas/ground/sky/buildings/mech, all with WHITE baked verts) is drawn UNLIT with the texture colorized by the material's 2-endpoint RAMP (`dpl_SetMaterialRamp`, texture luminance indexes a low→high gradient — rock (0.25,0.21,0.16)→(0.8,0.5,0.4) = warm tan); only ~150 vehicle/missile files (XYZ_N_UV) carry real normals and were lit by the single map light. 4. **⭐ RAMP colorize + day/night material priority — DONE (commits d5c155b, cc0055a); the ground/mountain colour match + sky whites.** The ground looked PALER than the mountains with a seam because they use different material libs: the mountains' `ravinerock_mtl` has a warm diffuse (0.781,0.525,0.353) so they render warm, but the ground's `grass_mtl` has NO diffuse — it relied ENTIRELY on its colour RAMP, which we didn't apply → gray placeholder = the pale ground. The authentic IG board colours terrain by the material's **2-endpoint RAMP** (`dpl_SetMaterialRamp`): texture luminance indexes a low→high gradient. Implemented: `bgfload` parses the ramp def (0x30 name + 0x31 6-floats) + the material's ramp ref (0x28), threads rampLo/Hi onto `BgfDrawBatch`→drawOp; `L4D3D` bakes `lerp(rampLo,rampHi,texLum)` into the texture at load and draws ramped batches with a **WHITE material + WHITE vertex colour** (so the ramp colour shows untinted — critical: a leftover tint MODULATEs the ramp again). **DAY/NIGHT MATERIAL PRIORITY (bmf/img) is now DEFAULT ON**: the mat\day libs carry the AUTHENTIC warm ramps (rock 0.25,0.21,0.16→0.8,0.5,0.4 = warm tan; grass →warm brown); first-match had picked the generic GEO/*.BMF whose ramps are GRAY (0.8,0.75,0.75) — which only looked warm before because the vertex tint covered for it. The earlier "BT_MATPRI grays the terrain" regression is GONE precisely because the ramp is now applied. **SKY via its own ramp**: dsky_mtl has a 'sky' ramp (0,0,0.6→0.99,0.99,0.99) → bright cloud texels → 0.99 near-white = the "whites" the user wanted; `BT_SKY_SCREEN` (the pre-ramp heuristic) now default OFF. **Render-verified ALL DEFAULTS**: dbase/day = pale pastel sky + soft white clouds + uniform warm tan ground matching the warm mesas = the pod footage; dbase/night = dark twilight + warm-lit red mesas; grass = warm brown; combat un-regressed. Gates: BT_RAMP / BT_MATPRI / BT_SKY_SCREEN. 5. **UNLIT no-normal geometry (STEP 5) — NOT done, and no longer needed.** Since STEP 1 fixed the geometry + the ramp gives correct colour, the mesas/terrain look natural lit; the residual faceting is gone. Deferred (⚠ per-drawOp NOT per-object — APC/HUMMER mix XYZ_N_UV + XYZ_UV; ⚠ 0xCD ctor-init gotcha; ⚠ would change the mech to unlit). Full spec: scratchpad rf_plan.md/rf_verdicts.md; the workflow reports are the durable cites. 6. **⭐ RAMP over-applied to LIT vehicle geometry → "whitish-tan blobs" — FIXED (bgfload.cpp `hasNormals` gate).** The user saw the map's scattered vehicles (hummer/semi/dozer/apc…) render as uniform dusty-white blobs. ROOT CAUSE: the vehicles' shared `basev` material library (`VIDEO/MAT/BASEV.BMF`) gives every material a REAL diffuse colour (bv1_mtl tan 0.66,0.47,0.30; bv1gry gray 0.41; bv5dkgrn green; bv5red; bv1ylw; blk) PLUS a ramp ref (tag 0x28) to the `cdusty` ramp (0.33,0.27,0.22→0.90,0.85,0.80 = dark-brown→near-white) over the shared atlas `BASEV.BSL`. Our task-#20 ramp path set `batch.hasRamp` purely from the material, with **no vertex-type gate** → it ramped the vehicles too, baking cdusty over BASEV.BSL + forcing a WHITE material → every vehicle washed to the same dusty-white, killing its diffuse colour. But the authentic IG model (DPLTYPES.H) selects shading BY VERTEX TYPE: **normal-bearing geometry (vehicles/missiles, `XYZ_N_UV`/ `XYZ_N`) is LIT and shows diffuse×texture; the RAMP is a NO-NORMAL feature only** (terrain/mesas/sky/ buildings/mech). Vehicles carry `XYZ_N_UV` (SEMI = 400 XYZ_N_UV + 248 XYZ_RGBA_UV). FIX: `bool hasNormals (id)` (0x81/0x89) + `useRamp = currentHasRamp && !hasNormals(pmeshVtag)` per-PMESH in `buildPmesh` — replaces `currentHasRamp` for the vertex colour + `batch.hasRamp`/`batch.color`. Now normal-bearing batches keep their diffuse colour + BASEV.BSL texture (colored trucks); no-normal batches (incl. the vehicles' own no-normal detail parts) still ramp. Per-PMESH so a mixed-vertex-type model splits correctly. **Verified A/B (zoomed distant-vehicle crop):** before = gray/white blobs → after = gray trucks + tan/brown cargo + brown container, distinct materials; terrain ramp un-regressed; combat un-regressed (TARGET DESTROYED after 8 hits); no crash / 0 heap detections under BT_HEAPCHECK. NOTE: not separately env-gated — it's a correctness fix inside the existing BT_RAMP system (BT_RAMP=0 still disables all ramping in L4D3D). **CORRECTION to an in-chat claim: the vehicles are ALREADY per-light shaded once un-ramped — the loader is NOT missing normals.** `buildPmesh` pushes 0 into nx/ny/nz only as INITIALISATION; `emitTri` then accumulates the forward face normal into each vertex (bgfload.cpp:442-444) and the finalise pass normalizes them (`:597-601`) = smooth per-vertex normals for ALL geometry. Lighting is globally ON whenever the map ships a light: `DPLReadINIPage` parses the env page's `light` entry (BTDPL.INI, e.g. dbase/day `dsdayclear`) into `sceneLight[]` as a `D3DLIGHT_DIRECTIONAL` sun (`L4VIDEO.cpp:3086` SetLight+LightEnable), and the frame setup (`:6196-6233`) sets `D3DRS_LIGHTING TRUE` + `NORMALIZENORMALS` + `COLORVERTEX` + diffuse/ambient from vertex COLOR1 when `sceneLightCount>0`. Non-ramp batches get material Diffuse=bv color, Emissive=0 (L4D3D.cpp:303-310), so a vehicle renders `texture × [ambient·color + sun·(N·L)·color]` = real directional shading (render-verified: a gray tanker shows a bright sun-lit top, dark shadowed side faces, smooth gradients — a flat render would be uniform). The ramp-white was the ONLY thing suppressing it; no separate lighting work was needed. (STEP 5 — making no-normal geometry authentically UNLIT — is the OPPOSITE change and still "not needed": global lighting on the ramped terrain/mesas/mech looks correct vs the footage.) **✅ WEAPON FIRE IS NOW VISIBLE — the muzzle→hit BEAM renders + fires on the trigger, target or not.** The user pressed SPACE and saw nothing: (1) the whole fire path was gated `if (gEnemyMech != 0)` (the BT_SPAWN_ENEMY test dummy) → in solo there was NO target so the trigger ran no fire code; (2) even against the dummy the only visual was an Explosion at the target — the `dpl_*` weapon-beam renderable was never ported. FIX (two parts): **(a) a self-contained additive-quad BEAM renderer in the engine** — `BTPushBeam(from,to,color,ttl,width)` queues a world segment, `BTDrawBeams(dev,view,dt)` (L4VIDEO.cpp, called in the alpha pass so beams are Z-occluded by terrain) billboards each as a camera-facing additive quad and fades it; no content needed (fills the unported dpl_ layer). **(b) a visual-fire block in `Mech::PerformAndWatch` (mech4.cpp) that runs ALWAYS** — computes the muzzle (origin + kMuzzleHeight 7u / kMuzzleForward 3u) + the mech's −Z forward, picks the aim = the locked enemy (torso) OR the crosshair raycast to terrain (`BTGroundRayHit`, btvisgnd.cpp, else muzzle+fwd*range), and on the trigger (own cooldown `gBeamCooldown`) pushes the bolt = **red-orange glow (w6) + white-hot core (w1.6) + muzzle flash** and (solo) an impact Explosion at the hit. The existing enemy-damage block is UNCHANGED and still runs below (beam → "target" while locked, then "free" after the kill). **Render-verified:** solo desert — a bright white-cored laser fires forward to the aim point; combat — `[fire] BEAM -> target` while the dummy is locked, structure 0.8→1.0, **TARGET DESTROYED after 8 hits**, then "free" beams post-kill; 0 crashes / 0 heap detections under BT_HEAPCHECK. SPACE/Ctrl fire the beam interactively (btl4main VK_SPACE→gBTDrive.fire). **✅ AUTHENTIC MUZZLES — the beams now fire from the mech's REAL gun-port SITE segments (arm guns).** The BLH.SKL has arm-gun joints `jointrgun`/`jointlgun` with muzzle site segments `siter/lu/dgunport` + `sitel/lu/dgunport` (upper/down/back ports per arm). The fire block (mech4.cpp) resolves them by name each frame — `GetSegment(CString("siterugunport"))` → `EntitySegment::GetSegmentToEntity()` (segment→entity LinearMatrix) → `AffineMatrix::Multiply(segToEntity, localToWorld)` → `Point3D = matrix` (extracts the W_Axis translation) = the world muzzle — and fires one bolt per resolved port, all converging on the aim point (falls back to a centre muzzle if none resolve). **Verified:** muzzle logged at (−2.6,+4.4,+2.7) off the origin = the arm gun, NOT the centre; render shows TWO beams emerging from the left+right arm guns in a converging V, with muzzle flashes at each gun, tracking the arms; combat un-regressed (DESTROYED after 8 hits), 0 crashes / 0 heap detections. The beams are now RED LASER (glow 0x00E01808 + white-hot core), authentic for the all-laser BLH (Nova). **◐ TIE-TO-REAL-FIRE — TRIED, REVERTED (over-saturates; needs a beam throttle).** The authentic next step is driving the beam from each weapon's OWN fire: `Emitter::FireWeapon` has `owner` (Mech*), `muzzlePoint`, `targetPoint`, `dischargeTime`, and `damageData.damageType` all in scope, so pushing `BTPushBeam` there (muzzle from `owner->GetSegment(segmentReference)`→segmentToEntity×localToWorld, coloured by damageType: Laser red / Energy blue / Ballistic amber) gives real rate + heat + per-type colour, and it BUILT + ran (combat: `[emitter] FIRED heat=0.239`, DESTROYED-in-8). BUT the BLH is a ~12-laser ALPHA STRIKE and `FireWeapon` runs ~16×/s across the roster; a persistent (`dischargeTime`) additive beam pushed per fire ACCUMULATES into a white screen-flood at point-blank. So `Emitter::FireWeapon` no longer draws (kept the mech4 controlled 0.8s salvo). To revive: throttle to ONE beam per weapon per discharge (push only on the fire EDGE, `dischargeTimer` just reset) + short TTL (no per-frame stacking) + dim per-beam / alpha-blend the glow so N overlapping beams don't saturate. `simulationFlags & 0x8` is NOT a safe weapon discriminator (bit 3 is in the engine host/replicant `flags & 0xc` mask) — enumerate weapons via the factory class IDs, not the flag. **✅ FIRE RATE — grounded on the recovered weapon data (was a 0.8s bring-up guess).** The user noticed the laser cadence looked too slow vs the pod videos — correct, 0.8s was arbitrary. The real laser params (`ERMLASER.SUB`/`ERLLASER.SUB`/... — the BLH is ER-medium): **`DischargeTime=0.2`** (beam-on), `WeaponRange=500`, `DamageAmount=3.5`, `PipColor=1.0 0.0 0.0` (RED, confirms the laser colour), `RechargeRate` scales with size (ER-S 1.0 / ER-M 2.0 / ER-L 4.0 / PPC 5.0). Applied: beam-on = `kBeamOnTime=0.2` (DischargeTime, recovered hard value); cadence `kFireCooldown=0.3` (0.2 + short recharge) → ~62% beam duty cycle, snappy rapid fire vs the old ~15%. **✅✅ RECHARGE CADENCE — FULLY DECODED from the binary (RechargeRate IS the recharge in seconds).** PE-parsed the charge-curve constants from BTL4OPT.EXE (`.data` at the `_DAT_` VAs, read as the x87 80-bit `float10` the decomp uses, NOT 32-bit float): **C1=`_DAT_004bb3c4`=1.0, C2=`_DAT_004bb3b8`=1e-4, `_DAT_004bb3b4`=0.5** (the energyCoefficient ½). The Emitter ctor (raw part_013.c:7899-7906) computes `energyCoefficient = energyTotal/ (0.5·seekV²)` and `energyRampTime = rechargeRate / (−VoltageCurve(C1−C2·seekV)) / energyCoefficient`; the per-frame charge (`FUN_004b0d50` dtScale × `TrackSeekVoltage`) is an exponential approach toward the source voltage with time-constant τ = dtScale·energyCoefficient, and the fire threshold is `seekVoltage[rec] = 0.8·RatedVoltage`. **The constants make it cancel exactly:** seekV = 0.8·RatedVoltage, RatedVoltage (GENRATOR.SUB) = 1e4 → seekV = 8000 → curve arg = `1 − 1e-4·8000` = 0.2 → `VoltageCurve=ln` (must be, for `energyRampTime>0`) → `−ln(0.2) = ln5 = 1.609`; and the exponential charge from 0 to 0.8·srcV also takes `ln(1/0.2) = ln5`. The two ln5 factors cancel ⇒ **recharge time = RechargeRate seconds exactly** (ER-S 1.0 / ER-M 2.0 / ER-L 4.0 / PPC 5.0). So one laser's full cycle = `DischargeTime + RechargeRate` = **0.2 + 2.0 = 2.2s** for the BLH's ER-mediums — much SLOWER per weapon than any guess; the fast on-screen fire is the ~12 lasers firing STAGGERED (chain), aggregate ≈ 12/2.2 ≈ 5/s. Applied (mech4.cpp): `kPortRecharge=2.2`, fire ONE gun port per stagger tick (`kPortRecharge/nMuz`) rotating through the 6 arm ports (added siter/lbgunport) → a rapid alternating single-beam stream, each port honouring the 2.2s recharge (render-verified: single red bolt per frame, rotating ports; combat DESTROYED after 8 hits, 0 crashes / 0 heap). ⚠ ASSUMPTION: that the game STAGGERS rather than synced-volleys (a synced alpha strike would flash every 2.2s = slow, contradicting the pod footage) — the per-weapon 2.2s is decoded-exact; the stagger *pattern* + exact weapon→port count is the estimate (needs the weapon enumeration via factory class IDs, not simulationFlags). **✅✅ BEAM APPEARANCE — DECODED from the assets (the "gritty" look is a real scrolling texture, NOT invented).** The user noted the lasers looked too clean. Decoded the real beam: the weapon resource names its beam model (`VideoObjectName=ermlaser.bgf`, `GraphicLength=2000`; `beamLengthRatio=dist/2000` scales it). `ermlaser.bgf` = a thin TUBE (89 verts, Z 0..−2000) with UVs tiled ~8× down its length, material `btfx:beamwhite_mtl` (core) + `btfx:beamred_mtl` (glow). The GRIT is a SCROLLING TEXTURE: **`VIDEO/MAT/DAY/BTFX.VMF` (DIV-VIZ2 source)** defines `TEXTURE(NAME="beamwhite_scr_tex"; SPECIAL="SCROLL 0.0 0.0 0.10 9.5") { MAP {"bexp"} }` — i.e. the core samples the **`bexp` image = `VIDEO/TEX/BEXP.BSL`** (a 128×64 chaotic red/yellow/green noise) scrolled FAST along the beam (u=0.10, v=9.5), ramp-colourised by `softer` (0.25→0.99) to a white core; the glow is solid red (`fiery` ramp). ⚠ **I FIRST WRONGLY SAID THE TEXTURE WAS MISSING** — it is present; I'd missed the `MAP{}` directive in the `.VMF` (the `.BMF` only names the texture, the `.VMF` binds it to the image + carries the SCROLL). **THE `.VMF` IS THE KEY EFFECTS ASSET** (scroll speeds for beams/muzzle/smoke/sky: beamwhite v=9.5, beamyellow v=−5.3, beamgreen v=−1.3, muzinner/muzouter, firesmoke, sky). Beam COLOR is per-weapon via the beam model: ER lasers→ermlaser(RED), standard lasers→l/m/slaser(YELLOW), PPC→ppc.bgf(BLUE), AC/Gauss→ballistic (no beam), missiles→projectiles. Implemented (L4VIDEO.cpp BTDrawBeams; gate BT_BEAM_TEX): load BEXP.BSL once, ramp its luminance by `softer` → grayscale grit texture, MODULATE the beam colour by it, tile U down the length + scroll (u=0.10/s, v=9.5/s) → the streaming gritty beam. Render-verified: the beam shows banded/streaky grit (was a clean gradient); combat un-regressed (DESTROYED after 8), 0 crashes / 0 heap detections. **✅ THE REAL `ermlaser.bgf` TUBE NOW RENDERS (replaced the camera-facing billboard quad; gate `BT_BEAM_TUBE`, default ON).** `BTDrawBeams` (L4VIDEO.cpp) loads the tube geometry ONCE (`LoadBgfFile("ermlaser.bgf")` → static `s_tubeVB`/`s_tubeIB`, x,y,z,u,v per vert) and per beam builds a world transform `Scale(w,w,beamLen/2000) × Rotate(local −Z → muzzle→target dir) × Translate(muzzle)` — the tube's native Z 0..−2000 maps onto the shot, its baked UVs (U 0..7.73 tiled down the length) carry the scrolling `bexp` grit via a `D3DTS_TEXTURE0` transform (`D3DTTFF_COUNT2`, `_31=uScroll _32=vScroll`), beam colour from `D3DRS_TEXTUREFACTOR` (the tube verts carry no diffuse — FVF `XYZ|TEX1`, COLORARG2/ALPHAARG1 = `TFACTOR`; the billboard path stays `XYZ|DIFFUSE|TEX1` with `DIFFUSE`, kept as the `BT_BEAM_TUBE=0` fallback). Native tube is ~0.3u wide so a width scale is applied: `BT_BEAM_WIDTH` (default **3.0** → ~0.9u; the width scaling ONLY affects the tube path, which is how a run was confirmed to use the tube not the billboard). All touched state saved/restored (TEXTUREFACTOR, TEXTURETRANSFORMFLAGS, D3DTS_TEXTURE0). **Render-verified** (dbase/grass, `BT_FORCE_FIRE`+`BT_SPAWN_ENEMY`): a clean 3D volumetric bolt from the arm-gun muzzle to the target, staggered single-port fire, scene un-regressed, 7+ shots, 0 crashes. ⚠ **TEST-HARNESS GOTCHA (cost an hour — not a game bug):** the env gates check `getenv(name)` for NON-NULL, so **`$env:BT_HEAPCHECK=""` (empty string) still ENABLES heapcheck** — every run after I first set it crawled through `CrtCheckMemory` on each string alloc in the material `scanDir` (O(n²) during mission BGF indexing) and looked like a mission-load HANG at `[zonebuild]` (`Responding:False`). cdb on the "hung" process showed the main thread live in `ucrtbased!CrtCheckMemory` ← `scanDir`/`bmfIndex` ← `LoadBgfFile` ← `DPLReadINIPage`, not stuck. FIX: `Remove-Item Env:\BT_HEAPCHECK` to actually clear it (setting `=""` does not). Applies to ALL `BTEnvOn`-style gates. **✅✅ AUTHENTIC DISTANCE LODs — "first LOD only" was WRONG; the arena "structures not rendering fully" bug (bgfload.cpp + L4D3D + L4VIDEO).** The user found arena1 structures rendering as floating fragments (poles/ letters/rails with no massing). Triage ruled out the CONN fix (A/B: 28 vs 29 tris on AR03, identical) and load failures (none). ROOT CAUSE: **each BGF LOD chunk carries a `0x2046` header = [near..far) VIEWING BAND** (AD01: [0..57.7]=close-up 3D logo letters, [57.7..231]=full billboard, [231..1155]=far; AB07: near band = a sliver+pole DETAIL SUBSET, [115..1155] = the whole building) — the IG board selected the LOD whose band contains the camera distance. Our loader always took the FIRST LOD; for classic reduce-LODs (calpb 152→16 verts, dhillg, buttes) first==finest so it LOOKED right, but the arena's composite structures author their massing in the FAR bands → only near-detail fragments drew. FIX: the loader collects EVERY LOD tagging batches with the band (BgfDrawBatch/L4DRAWOP lodNear/lodFar); DrawMesh selects per draw-op by |object − camera| (camera published per frame via `d3d_OBJECT::SetCameraPosition`, eye = −t·Rᵀ from the view matrix). **CONSOLIDATION IS NOW DEFAULT OFF (`BT_CONSOL=1` re-enables)** — merged ops combine many instances + LODs of one material, so per-op bands can't survive the merge (a consolidated run would draw EVERY LOD simultaneously). Frustum culling + HWVP carry the cost — measured FASTER than consolidation: dbase combat avg 1.05ms, 79 batches (distant objects draw their small far-LODs). VERIFIED: arena renders a coherent scene (continuous perimeter wall, full buildings, no floaters); dbase terrain/buttes unchanged; combat TARGET DESTROYED after 8 hits, 0 crashes. Viewer: `BT_LOD_INDEX=n` inspects a specific LOD. FOLLOW-UP: a few PINK horizon patches on dbase = far-LOD materials that never resolved before (their libs never loaded); minor. ⚠ LESSON: "first LOD" was validated only on reduce-style models — a format assumption verified on a subset can still be wrong for another authoring style; the 0x2046 header was readable all along. **⚠⚠ LOD-DISTANCE BUGS FIXED THE NEXT SESSION (user: "mech disappearing/reappearing with viewing angle", then "scenery still blinks"):** (1) the CAMERA EXTRACTION from the view matrix was TRANSPOSED — the D3D row-vector LookAt stores the camera basis in COLUMNS (xaxis=(_11,_21,_31); eye.x = −(t·col1) = −(_41·_11+_42·_12+_43·_13)); using ROWS gave a pseudo-position that VARIED WITH THE VIEW ANGLE, so LOD bands popped in/out as the camera rotated (structures AND the mech blinked). (2) the METRIC IS THE INSTANCE ORIGIN distance — a bounding-sphere-SURFACE metric (tried briefly, blamed for a floor vanish that was really bug #1) subtracts each component's OWN radius, smearing the COORDINATED band handoffs of composite structures (ADWLK1 ends [..115] exactly where AB07's far band [115..] takes over) by 20–50u per piece → holes/doubles the orbiting chase camera swept pieces in and out of = the scenery blinking. Floor slabs' single bands are wide (ctarmac [0..1155], dtarmac [0..1039], afloor [0..577]) — origin distance keeps them visible across the play space; LODing out beyond that range (far outside the arena) is authentic. `BT_CULL_LOG=1` dumps big culled objects (bounds sanity). Verified: 10-frame full-rotation sweep = mech + floor + scenery stable (2 frames camera-inside-a-wall, expected); dbase combat DESTROYED after 8 hits. ⚠ LESSON: changing two things at once (camera fix + metric) attributed the floor vanish to the wrong one — the sphere metric was never needed. **⚠⚠ RUNTIME LOD SELECTION — REVERTED TO EXPERIMENTAL (`BT_LODSEL=1`); DEFAULT = SHIPPING-ENGINE BEHAVIOR.** Even with the correct camera + origin metric + the authentic mech-eyepoint reference (fed via `BTSetLodEye`, kept wired), runtime band selection still broke arena content (floor vanished when the mech wandered >577u from `afloor`'s origin — impossible in the real game, so the 1995 semantics for huge grounds involve something not yet decoded: per-LOD reference points (`dpfGetLODReference`/`dpl_SetObjectLodHotSpot` exist in the libDPL API!), the unparsed class-0 arena map records, or ground-type exemptions). **DEFINITIVE REFERENCE FOUND: `DivLoader/VGCDivLoader.cpp parseLOD` — the SHIPPING 2007 engine keeps ONLY the nearest-band LOD (`if (in < 5.0f) parse = true`) and does NO runtime LOD selection at all** — i.e. our original "first LOD" behavior IS what Red Planet shipped with; the arena-composite fragments exist in the 2007 engine too. DEFAULT restored to that (first LOD + consolidation ON; `BT_LODSEL=1` switches to the experimental full-LOD path and auto-disables consolidation). TO FINISH PROPERLY: decompile BTL4OPT.EXE's own LOD path (the 1995 software renderer's selection incl. hot-spots) instead of guessing — then the arena backdrop can be made authentic. All the plumbing (bands parsed+tagged, per-op DrawMesh test, eyepoint feed) is in place and gated. **✅✅ ADDITIVE_LODS DECODED — the arena "fragment structures" FIXED, authentically (default ON, gate `BT_ADDLOD=0`).** The follow-up research cracked it WITHOUT decompiling the binary: **the 1995 `MUNGA_L4/L4VIDEO.CPP` `TestSpecialCallBack` (~line 486) reads the OBJECT-level SV_SPECIAL (0x2037) token `ADDITIVE_LODS` from the BGF itself and calls `dpl_SetObjectAdditiveLODs`** (DPL_VPX.H). **135 pod BGFs carry the token** — ALL arena structures (AB*/AD*/AR*/AW*), the PGN calliope-turret family, buildings (HUT/TWR/BNK/JAX/FT/HN), vehicle props (TRK*), and EVERY `*D` mech damage model. Corpus sweep (841 GEO BGFs): additive objects author their LODs as a clean PARTITION of [0..outmax) with COMPLEMENTARY geometry (AW01: LOD1 = upper structure, LOD2 = base — different regions, not nested representations) — so replacement-selection is provably WRONG for them (one complementary piece at any distance) and first-LOD-only shows just the near-detail piece = the "floating fragments". **THE RULE: additive object → at eye distance d, EVERY LOD with `d < OutDist` draws (InDist ignored; near detail ADDS onto the coarser massing and drops first as you retreat).** One corpus quirk: MSLG.BGF has an inverted band → clamp max(in,out). Implementation: `bgfload.cpp` TAG_OBJECT parses 0x2037 → additive objects collect ALL LODs, each batch banded [0..OutDist); non-additive keep shipping first-LOD-only (mech/terrain/afloor → zero regression risk); `L4VIDEO.cpp RecurseStaticObject` EXCLUDES banded objects from static consolidation (merged runs can't band-gate) — they render individually via the per-op DrawMesh distance test (eye = the `BTSetLodEye` mech feed). **Verified:** arena1 spawn A/B — `BT_ADDLOD=0` reproduces the user's exact floating-panels bug AT THE SPAWN (the spawn sits inside a dropzone gate whose massing never drew!), default ON = the complete bay/gate + walkway, coherent structures at all ranges; consol srcMeshes 255→152 (103 additive instances excluded); grass combat TARGET DESTROYED, 0 crashes; cavern + dbase identical to baseline. NOTE: the arena spawn's debug CHASE camera now starts inside the (correctly solid) bay wall — the pod's cockpit camera sat at the mech's head so the original never saw this; drive forward to exit. **✅✅✅ THE FULL LOD DECODE — √3 RANGES + ORIGIN METRIC + PUNCH CUTOUTS (the 6-agent `lod-blink-decode` workflow; closes the arena "blinks with movement" + "noisy unstable surfaces" reports).** 1. **⭐ THE √3 RANGE DECODE (corpus-exact):** every stored 0x2046 band value is the AUTHORED euclidean distance ÷ √3 — 92% of the corpus' 3263 nonzero band slots are EXACTLY nice-number/√3 to 6 significant figures, with hand-conversion TYPOS as proof (BUTTEB 2000.73 for 2020.73=3500/√3; MSLG's inverted band = the author typing 10 for 100). afloor 577.35 = authored **1000** (the historic floor-vanish explained); ADWLK1 115.47 = 200; the arena massing 1154.7 = **2000** ≈ des_day clip 2100. Comparing plain euclidean eye distance against stored values made EVERY piece switch at 57.7% of its authored range — inside the arena's clear air (fog 200→1250, clip 1300 [ardayclear]) = the position-dependent blinking. FIX: `bgfload.cpp` multiplies parsed bands by √3 (`kLodRangeScale=1.7320508`). With it, NO massing pops in-play (2000 ≥ the arena's max reachable 1972; 54 of 104 popping instances — ALL walls/buildings/gates — stop popping; the remaining are authored detail dropouts at 400-900u, 19-66% fogged). A disjoint 7.45% RAW-integer band population exists (one prop-family artist skipped the conversion — GD/RB/WL/SW/SB/TK1 etc), 100% NON-additive → unaffected today. 2. **METRIC = the instance-transformed OBJECT HOT SPOT (default = origin), ONE shared d for all LODs of an object** (`s_dplobject.lod_hot_spot`; InDist doesn't even survive to the runtime — the board keeps only `lod_ranges[16]` switch-out floats, which STRUCTURALLY confirms the cumulative rule). The bounding-sphere-SURFACE metric (previous commit) was UNAUTHENTIC — per-piece radius offsets desync sibling bands (the OpenFlight "use previous slant range" contract: siblings must range against the SAME point). Reverted to origin distance in DrawMesh. 3. **✅ PUNCH cutouts (dpl_Punchize) implemented** — PATCH-level SV_SPECIAL `PUNCH` (1995 TestSpecialCallBack L4VIDEO.CPP:563-569) marks cutout geogroups (arena scaffold layers AR01-04, gratings): black texels = HOLES. The port drew them SOLID, shingled 0.05-0.5u over the wall massing = near-coplanar depth shimmer in motion (the "noisy unstable surfaces"). Now: loader tags batches (bgfload punch flag) → texture loads with alpha-0 on near-black texels → DrawMesh alpha-TESTS punch ops in the opaque pass (ALPHAREF 0x80, z-write preserved, states restored). Gate `BT_PUNCH=0`. Render-verified: the catwalk rails/barriers draw as see-through lattice. **Corpus classification (the same workflow):** additive LODs are overwhelmingly COMPLEMENTARY (families: detail+massing 54, progressive layered series 49 [AR01 = trim→scaffold→scaffold+panels→ SOLID WALL, disjoint layers, correct under cumulative], single-detail 9, mixed 23); real coplanar DOUBLED overlap only in props (TRK1/3/4 grille detail flush on the body) + mech damage arms — those carry `GEOMETRIZE 0x8000xxxx` hints (decal/priority, unimplemented lead), authentic-coexisting on the board too, left as-is. **Other decoded facts:** map class-0 records = INCLUDE directives (`mid==3` + type-14 target; arena1→{arenall→cavern, artrucks} — the arena sits INSIDE the cavern map; class-48 = named dropzones); NO 0x2047/0x2048 exists anywhere in the corpus; Division dVS has no additive concept (VPX-local extension); OpenFlight LOD semantics (OGC 19-065) match the tag set 1:1 (reference-point ranging, transition ranges) — the reference for future replacement-LOD work. **Diagnostics kept (permanent):** `BT_LOD_LOG=1` → `[lodband]` per-model banded-op census at load, `[lodeye]` eye-feed trace, `[lodflip]` per-op band-visibility FLIP events with model name + band + d (how the oscillation was caught: awbox1 flipping IN 3× at d≈461 vs edge 461.88). Verified: arena traverse = no massing flips (only authored detail handoffs at 400-900u under fog); walk-out renders banners + see-through rails + complete walls; grass combat TARGET DESTROYED, 0 crashes; dbase/cavern identical; avg frame 4.5ms. **✅✅ THE COPLANAR-SHELL DECODE — the "extremely unstable walls" (noise/diagonal/dot patterns crawling with movement, stable parked) ROOT-CAUSED + FIXED (submission-order depth bias; commit bcc9210).** Diagnosis chain (each step eliminated a hypothesis): two stills at one position = pixel-identical (pattern is a pure function of camera position); layer-isolation imaging (`BT_ONLY_LOD=k`) showed the dot lattice INSIDE A SINGLE LOD layer (not layer-vs-layer); `BT_PUNCH=0` left it unchanged; analytic geometry (AR02 LOD2) found the truth: **the content layers EXACTLY-COPLANAR full shells inside one LOD** — a solid concrete shell + a coplanar PUNCH-cutout overlay + an inner shell, plane separations measured 0.000–0.003u. The Division board composited coplanar polygons DETERMINISTICALLY (exact per-polygon plane equations → identical depth → later-submitted wins via less-equal); D3D9 interpolates depth from vertices, so two tessellations of one plane differ by rounding PER PIXEL = crawling interference at ANY distance (near-plane precision, per-LOD bias, punch alpha all irrelevant — hence the failed attempts). **FIX = reproduce the board's rule:** every batch of an additive object is depth-biased ~4 LSB of D24 CLOSER per submission ordinal (file order = authored order; `2.5e-7` NDC/step, ordinal capped 20, `BT_LAYER_BIAS` overrides) — coplanar contests resolve to the overlay patch at all distances. Verified by bias on/off A/B at a teleport-reproduced viewpoint. **Also: punch cutout mips are built MANUALLY with alpha RE-BINARIZED per level** (box-filtered alpha hovers at the test threshold → texels flicker as movement shifts the mip level). **And `BT_LOD_SCALE` is BLANKET again (commit 1bea626):** the outermost-only restriction guarded against extending shingled detail into z-fight range — obsolete under the bias — so inner detail (wall-top caps, authored 800) now holds to 2000 = zero wall-detail pops in-arena (traverse flip census 104 → 7, remaining = two small pieces at 375–500u under fog). **HARNESSES (permanent):** `BT_SPAWN_AT="x z [hdg]"` teleports the player on the first driven frame for exact-viewpoint A/B repro — MUST set the orientation QUATERNION (`localOrigin.angularPosition = EulerAngles(0,h,0)`), not just the scalar `gDriveHeading` mirror (the chase camera derives from the quat; without it the view keeps the random dropzone's facing). Teleporting INTO a dune can leave the mech sunk (ground probe from inside a solid) — cosmetic, walking recovers. `BT_ONLY_LOD=k` = additive layer-isolation imaging. ⚠ **GOTCHA (cost a loop): `bgfload.cpp` builds into the ENGINE lib** — a game-only rebuild silently ships the stale loader; grep the exe for a new string literal when in doubt. Remaining authentic-LOD work: replacement selection for NON-additive multi-LOD models (detail pop at range — needs the board's hot-spot/reference semantics; leads catalogued: `s_dplobject.lod_ranges[16]/ lod_hot_spot/lod_fade_range` in DPL_PRIV.H, per-LOD 0x2047 reference points, 0x2048 transition snap|blend, `dpl_SetLodStress` perf governor, `dpl_SetInstanceForceLOD`). **RESEARCH TECHNIQUE (reusable): `LIBDPL.LIB` is an OMF library with full symbol names — parse THEADR/PUBDEF records (python ~40 lines) to map every `dpl_*` entry point; the 1995 DOS `MUNGA_L4` HAL source is the game-side caller reference; `dsys/PFBIZTAG.H` names every BGF tag (0x2046=LOD_DISTANCE, 0x2047=LOD_REFERENCE, 0x2048=LOD_TRANSITION, 0x2037=SV_SPECIAL).** **✅✅ THE RAINBOW/GRAFFITI MECH SKINS — THE BSL BIT-SLICE FORMAT DECODED + FIXED (user: "are the mechs colored accurately? they all seem to have a rainbow/graffiti motif" — they were not; every mech now renders its authentic gray-metal paint).** A 4-agent workflow (BSL corpus census / format ground truth / our-loader audit / BMF material spec) + hands-on. ROOT CAUSE: `.BSL` is NOT one RGBA image — it is a **bit-sliced container**: w*h 32-bit texel words, byte 0 pad, the other 3 bytes = SIX independent **4-bit grayscale sub-images** packed 2-per-byte with the nibble pair SWAPPED (even slice = HIGH nibble; `shift=(c+((c+1)%2)*2)*4`), plus truecolor sliceTypes 7=RGB444/8=RGBA4444 (r=s5,g=s4,b=s3,a=s2) that COEXIST with mono slices in one file. BLKHWK.BSL holds the Blackhawk's 4 skin sheets this way; our decoder read bytes[1..3] as RGB = 2-3 different gray sheets overlaid as colour channels = the graffiti. **Slice selection = the BMF TEXTURE record tag `0x18 BITSLICE` (u8, absent=0)** — the entry NAMES in the BSL directory are authoring records, read-and-discarded at runtime (proof: BASEV.BMF `bexp9_tex`→slice 8 vs entry named `bexp99`). Definitive reference found: **the shipping 2007 loader `DivLoader/VGCDivLoader.cpp:323-410` (`LoadBSLFile`/`getBSLData`)** + Division `dsys/PIMAGE.H dpiBSLTYPE` + content build scripts (`img2vtx.exe -b -m0..-m5 `); corpus-verified on all 66 archive BSLs (GEN.BSL's 6 entries can't fit any byte-plane model — the smoking gun). FIXES: (1) `port/src/image.cpp decodeBSL` rewritten to slice decode (one file serves engine+viewer; `BT_BSL=0` = legacy fallback); (2) `bgfload.cpp` parses tag 0x18 → `MatInfo.texChannel` → `BgfDrawBatch.texChannel` → `L4D3D decodeImage(tp, channel)`; (3) **cross-library RAMP registry** — every mech-skin material references ramp `softer` (neutral 0.25→0.99) which is defined only in OTHER libs (BTARENA/BTFX/BTPOLAR/BTVEH); per-file lookup left mech skins un-ramped forever; on first miss the loader now sweeps the indexed BMFs once into a global registry (`BT_RAMP_XLIB=0` disables); (4) **RAMP TINT RULE** — the content pins the (binary-only) board combine from both sides: dsky_mtl has diffuse (0.3,0.5,1.0) + the COLOURED 'sky' ramp yet renders footage-white ⇒ diffuse must NOT modulate coloured ramps; the mech variants (gen2medgry vs gen3drkgry etc.) share texture+NEUTRAL 'softer' ramp and differ ONLY by diffuse ⇒ diffuse MUST tint neutral ramps. Rule: explicit diffuse + NEUTRAL ramp ⇒ tint; coloured ramp ⇒ white (`BT_RAMP_TINT=0` restores always-white). Terrain/sky pixels UNCHANGED by construction; (5) truecolor slices (≥6, e.g. damcolor's bdam8 damage sheet) bypass the ramp; (6) beam grit (L4VIDEO BTDrawBeams inline decode) now samples slice 0 (bexp1 — beamwhite_scr_tex has no 0x18 tag); (7) viewer mirrored + its texture cache re-keyed `path#channel` (4 textures share BLKHWK.BSL). **Render-verified:** grass + close-up = coherent gray-metal Blackhawk with crisp panel/vent detail, dark charcoal accent zones, mauve `gen*a` leg tints (arena shot), green lgo6 leg-logo edge visible — the BMF-spec oracle look ("gray panel-detailed mech", per-zone) exactly; dbase terrain/sky/vehicles byte-identical to the tuned look; arena walls/punch gate clean, 0 lodflips; combat under BT_HEAPCHECK = TARGET DESTROYED after 8 hits (exact baseline), 0 crashes / 0 heap detections. Docs corrected (ASSET_PIPELINE.md §Pixel formats, CLAUDE.md §5). FOLLOW-UPS: RGBA4444 alpha (TREE.BSL cutout) is decoded but not yet alpha-tested outside punch batches; VTX/TGA paths untouched; the DZM damage-state material swap (THOR stores damage skins as extra slices thor12d/thor34d) still pending damage-graphic-state work. **✅ WRECK FLOOR-TILE FLICKER — FIXED (baked ground-shadow models + consolidated-world recession; commits 15fc70d, 7d5071a).** The user reported the floor tile under arena wreckage flickering exactly like the wall panels (pattern changes with movement, freezes parked — the coplanar-shell signature, and visible in original demo vids too; user chose to fix). TWO coplanar fighters found: (1) **baked ground-shadow MODELS** — the `S.BGF` naming convention (MECHMOVS/COMS/ARMRS/COOLS = shadow companions to wreck props): flat quads of `basev:shadow_mtl` at y≈0.1. Detection = all-shadow-material model (`shadowMat` per batch in bgfload; material name contains "shadow") → routed through the mech-shadow pipeline (`SetIsShadow`: translucent dark, depth-biased, no z-write) instead of opaque. (2) The ACTUAL fighter for the tile: **MECHMOVR's own ground plates at y=0.000 vs the consolidated arena floor** (cross-object coplanarity the per-object submission bias can't order) → **consolidated static world is now recessed `+7.5e-7` NDC (~3 LSB)** (ConsolidateSingleObject op copy), so individually-drawn objects (entity props, additive structures, the mech) win floor-plane ties deterministically. Verified at the user's parked repro spot. Shadow-material models are also EXCLUDED from static consolidation (GetIsShadow → banded), same as banded additive objects. **✅ POLAR MAPS VERIFIED — two "bugs" that are AUTHENTIC + the emissive decode (commit cfbe6d2).** polar3/ polar4 boot + render + fight clean. (1) **No mech shadow on polar = AUTHORED**: `GEO/POLAR/*_TSHD.BGF` are 188-byte STUBS (deliberate shadow disable on snow; only RAP_TSHD is real) — chased through burial/tilt/opaque- color probes first (BT_SHADOW_BIAS/BT_SHADOW_TILT/BT_SHADOW_COLOR all no-effect) before checking the FILES. Check the content before debugging the pipeline. (2) **Pink ice-mound perimeter = AUTHENTIC**: the `pint` material family (`MAT/DAY/BTPOLAR.BMF` pintBIceEmit_mtl: diffuse BLACK + emissive 0.676/0.614/0.718 + ramp 'icy') = pink-tinted self-lit ice. This surfaced that **EMISSIVE (tag 0x26) wasn't parsed** — now threaded through MatInfo→BgfDrawBatch (`hasEmissive`/`emissive[3]`); PURE-emissive materials (diffuse black + emissive set) render as an UNLIT glow: vertex color forced BLACK (the engine drives lit diffuse from vertex COLOR1) + `m.Emissive = batch.emissive` — tex × emissive, no sun/ambient. ⚠ pure-emissive test is `(color&0xFFFFFF)==0`; a material with BOTH diffuse and emissive keeps the lit path. **✅ ALL 8 RES MAPS BOOT + RENDER (map sweep complete):** cavern, grass, rav, polar3, polar4, arena1, arena2, dbase — each verified live (screenshot + log: 0 crashes, 0 lodflips). Eggs for every map now exist in the pod BT dir (POLAR3/POLAR4/RAV/ARENA1M/ARENA2/DBASE/MECH2/DEV/TEST...). Arena2 note: the flat brown band between wall-top and sky is the cavern backdrop (arena maps sit inside the cavern map via class-0 includes) — believed authored; re-check only if it misbehaves in motion. **✅✅ CONN+PCONN COEXIST — 370 of 841 MODELS WERE MISSING TRIANGLES (the "turret base missing panels" bug; bgfload.cpp + port/src/bgf.cpp).** The user spotted the calliope turret (`calp` gun + `calpb` base pad, on dbase AND cavern) rendering with gaps in the base and confirmed vs pod videos. Chain that found it: loader-mirror (bgfcheck.py) said calpb loads "clean" at 78 tris → the standalone port VIEWER showed the same gaps (ruling out the new consolidation/cull work) → dumping the chunk TREE showed calpb LOD0's PMESH carries **BOTH** `PCONN_LIST` chunks (37 quads + 1 hexagon = 78 tris; note **ppf=6 hexagons exist**) **AND** a `CONNECTION_LIST` chunk (24 plain triangles = the base panels) — and `buildPmesh` processed CONN only `if (!gotPconn)`. **CONN and PCONN are NOT alternatives — a pmesh carries quads/hexes in PCONN plus loose triangles in CONN.** Corpus scan: **370/841 GEO models** have mixed pmeshes (incl. AVA mech parts, buildings, vehicles) — all silently missing their CONN triangles until now. FIX: process both, always (both loaders); probe verified calpb 78→102 tris (+24 = exact); viewer A/B shows the base gaps filled; combat regression clean. BGF_FORMAT.md §Indices + Open-Items updated (second correction to the CONN story: first "one polygon" → flat trilist (task #20), now "either/or" → BOTH). ⚠ LESSON: a "loads clean, all indices in bounds" audit can still be dropping whole chunks — audit what's NOT consumed, not just what is. **✅✅ THE "TURN HITCH" / 10FPS BASELINE — ROOT-CAUSED & FIXED (~50-80× render speedup; user: "game occasionally hangs/stutters... definitely somehow related to turning").** Instrumentation chain that found it ([spike] frame-time log → [loadobj] load timer → cdb stack sampling → [rstat] draw/present split → per-draw section timers): the hitches happened with AND without input (the user's own repro log: 3 of 6 spikes at turn=0) — turning only made them VISIBLE (at 10fps a 400ms frame while holding a turn snaps the view through 4 frames of rotation = reads as a freeze; going straight the same stall just pauses the scenery). THE REAL PROBLEM was the render baseline: ~107ms CPU per frame (Present <1ms, GPU IDLE). THREE stacked causes, all fixed (engine tree, git add -f): 1. **`D3DCREATE_SOFTWARE_VERTEXPROCESSING` (L4VIDEO.cpp:2023) — THE ROOT CAUSE.** Every vertex of every draw was transformed + LIT + fogged on the CPU. The first CreateDevice attempt used SOFTWARE (the error message below it says "Couldn't create HARDWARE..." — the fallback was always intended); now HARDWARE first, software fallback kept. This alone: dense-view frames 490ms → ~3ms. Also why the RelWithDebInfo build was EQUALLY slow (the D3D software pipeline is external optimized code either way). 2. **D3DX `DrawSubset` attribute-table scan (~44µs/call × ~1400).** Our BGF triangles are DOUBLE-SIDED (each face emitted twice) → `GenerateAdjacency` degenerate → `OptimizeInplace(ATTRSORT)` failed SILENTLY → no attribute table → DrawSubset scanned the whole attribute buffer per call; on the huge CONSOLIDATED static meshes ~354µs/call = the 490ms stretches whenever a merged map chunk entered the view. FIX: the batches are already contiguous face-sorted ranges — `SetAttributeTable` EXPLICITLY (no adjacency at all) + per-op `bgfStartIndex/bgfPrimCount` in L4DRAWOP and a direct `DrawIndexedPrimitive` path in DrawMesh (DrawSubset kept as fallback for non-BGF meshes). ⚠ `D3DXConcatenateMeshes` (static-world consolidation) REQUIRES an attribute table on its INPUTS — removing OptimizeInplace without SetAttributeTable made it fail hr=0x8876086c and the WHOLE TERRAIN silently skipped ("206 static meshes") — the mech floated in fog-colored void. ConsolidateSingleObject now sorts faces by op itself (no D3DX optimize) + sets the table + direct-draw ranges. 3. **No frustum culling** (the 1995 IG board clipped in hardware; the port drew EVERYTHING every frame). Added bounding-sphere culling: model-space sphere computed at load (mesh ctor), per-frame frustum from view × mProjectionMatrix (NOT a GetTransform read-back — device state between frames holds pass-specific matrices and the planes silently degenerate), 4 side planes + FAR plane (near skipped for pass-projection differences; sky pass never culled; `mCullRadius<=0` never culled). Gate `BT_CULL_FRUSTUM` (default ON). **Measured: dbase circling 107ms/2729 batches → 1.2-2.8ms; grass combat avg 4.5-8.7ms; spikes 8→~1 (residual = first-view managed-buffer upload). Verified: full scene renders (terrain/mesas/sky/mech/ shadow/beams), combat DESTROYED, 0 crashes.** Diagnostic infra kept (cheap): `[spike]` (mech4, frame>0.2s), `[loadobj]` (L4D3D, load>5ms), `[rslow]`/`[rstat]` (L4VIDEO, 1Hz frame/batch/cull stats). Also gated the weapon-aim terrain raycast (mech4) to trigger-down/discharge-active frames (was marching SampleBand over every map instance EVERY frame). ⚠ LESSON: the user plays the DEBUG build — but the 10fps was NOT the debug build (RelWithDebInfo identical); always split draw-vs-present and check the DEVICE flags before blaming the compiler. ⚠ D3DXComputeBoundingSphere/SetAttributeTable/etc: any NEW d3d_OBJECT member must be inited in BOTH ctors (0xCD fill reads as garbage bounds → everything culled). **✅ FIRING ARC — no more shooting the enemy out of your back; the arc is DATA-DRIVEN from the torso.** The bring-up fire path aimed the beam AND applied damage at the locked target regardless of where the mech pointed (turning away fired lasers backward THROUGH the mech at the enemy behind). **DECOMP FINDING: there is NO firing-arc check in the binary** — `Emitter::FireWeapon` (@004bb6d4) fires whenever `HasActiveTarget()` (entity+0x388) and builds a straight muzzle→target beam, and the weapon `.SUB` resources carry **NO arc field** (ERMLASER.SUB = range/damage/recharge/heat only). The 1995 game got away with this because it was **cockpit- only**: a beam from a front muzzle to a target behind you passes behind the camera — invisible to the pilot (same reason the foot-clip never mattered). Our external camera exposes it, so the port owns the fix. The AUTHENTIC per-mech weapon traverse is the **TORSO twist range** (what actually lets a mech point its guns off dead-ahead): `Torso::GetHorizontalReach()` = the wider of the software limits `horizontalLimitLeft/Right` (res +0x100/+0xFC), or **0 when `TorsoHorizontalEnabled=0`** (the Blackhawk → guns lined up with the mech facing). Wired: `Mech::GetHorizontalFiringReach()` (mechmppr.cpp — the TU that safely knows the full `Torso` type; mech.cpp/mech4.cpp must not include subsystem headers, stub-collision) reads the cached torso @0x438; the mech4 fire block gates BOTH the visual beam aim AND the real Emitter trigger + explosion/damage on `targetInArc` = angle-to-target ≤ (BASE aim tolerance + torso reach). Base tolerance = `BT_FIRE_ARC` degrees (default 30) — the reticle/convergence box, a labeled PORT presentation parameter (the cockpit view never needed one); the torso reach is the authentic data part (0 for BLH, widens the arc for a twist-capable mech). In-arc: the beam converges on the target and the shot hits; out-of-arc: the guns fire straight ahead and nothing is hit. **Verified live** (`[target]` log shows `torsoReach=0rad` for the BLH → arc = base 30°): circling — enemy sweeping out of arc → beams go `-> free` (forward), no hit; facing → `-> target` converge + damage; walking in to close range → **TARGET DESTROYED after 8 hits** (un-regressed). ⚠ `kWeaponRange=100` (mech4.cpp) is a stale BRING-UP damage-gate threshold (the real ERMLASER `WeaponRange=500`); the enemy spawns at 120u so a STATIONARY facing test sits just outside it (visual beam fires, no damage until you close to <100u) — cosmetic bring-up wart, not the arc. **Remaining:** wire the real torso ACTIVE-TRACK (the torso twists toward the target within its limits, so a twist mech's guns visibly follow) — currently the reach only WIDENS the hit test; the joints don't move in combat (torso sim dormant, §Torso). **✅ BEAM ORIGIN TRACKS THE LIVE MUZZLE during the discharge (you can't run out from under your own beam).** The user saw the beam's near end lag behind the mech when running — the bring-up pushed ONE world-space beam at fire time with a 0.2s life, so the mech moved out from under the frozen segment. **DECOMP: the real Emitter re-anchors the beam every frame.** `Emitter::FireWeapon` (`FUN_004bace8`) computes the muzzle LIVE via `GetMuzzlePoint` (`FUN_004b9948` — resolves the gun's muzzle SEGMENT current world position from `owner+0x300` segment table, ref `+0xdc`) and **does NOT store the muzzle** on the emitter (only the aim orientation @0x107 and the target point @0xf2 are saved); then `EmitterSimulation` case 0 (Firing) calls `ContinueDischarge` (`FUN_004baa20`) EVERY frame while `dischargeTimer>0`, re-sending the beam keepalive. Since the muzzle isn't stored, the display re-obtains it live each frame → the beam ORIGIN follows the gun as the mech moves (invisible in the 1995 cockpit-only view; our chase cam exposes the lag). Reproduced in the mech4 visual: a single active discharge is tracked (`s_dischargePort`/`s_dischargeRemain`, one at a time since beam-on 0.2s < stagger 0.37s); on the trigger edge it STARTS the discharge, then a per-frame KEEPALIVE redraws glow+core+flash from `muzzles[port]` (recomputed from the gun segments every frame) to the live aim with a one-frame ttl (exactly one beam alive/frame = same brightness, re-anchored to the moving gun). **Verified:** running forward while firing, the beam emanates from the arm gun (near end at the gun, not behind); combat un-regressed (TARGET DESTROYED after 8 hits, 0 crashes) while the mech walked the whole time. **⚠ FOLLOW-ON (colour/grit washout from the run-out fix):** re-pushing the beam every frame at a one-frame ttl made `f = ttl/maxTtl = 1.0` CONSTANT (the old 0.2s ttl FADED, so it was only briefly at full brightness). Three full-bright overlapping tubes (red glow + white core + white flash) at the SAME `s_tubeWidth` then additively saturated to a solid WHITE cylinder — the red and the scrolling bexp grit vanished. Root cause was TWO things: (1) the tube path ignored per-beam width (all beams drew at the global `s_tubeWidth`), so the white core covered the FULL beam width instead of just the centre; (2) too much additive brightness. FIX: the tube now scales by **`b.width * s_tubeWidth`** (per-beam width honoured; `BT_BEAM_WIDTH` is now a global MULTIPLIER, default 1.0), and the keepalive pushes just TWO dimmer beams — a WIDE dim red glow (`0x00C81404`, width 3.0 → carries the ER-laser colour + grit) + a THIN warm core (`0x00A07868`, width 0.9 → hot centre, no white-out) — dropping the always-on white flash. Result: a steady red bolt with a bright core and visible scrolling grit (render-verified, 24 beams, 0 crashes). A laser is steady-on (not fading), so constant brightness is correct — the bug was saturation, not the lack of fade. **Remaining (real weapon fidelity):** per-weapon color from the beam model (blue PPC, yellow standard lasers) once weapon enumeration is wired (factory class IDs, not simulationFlags); the throttled real-fire beam (drive off Emitter::FireWeapon, heat); damage by the aimed zone (cylinder lookup, STEP 6). Remaining subsystem families (projectile/missile weapons 0xBCD/0xBCE/0xBD0 [WAVE 7]; SubsystemMessageManager 0xBD3 [WAVE 8]; Gyroscope 0xBC4 deferred — NaN integrator) per SUBSYS_PLAN.md. DONE: Torso (WAVE 5) + Sensor/Searchlight/ThermalSight/AmmoBin readouts (WAVE 4) + Myomers structural un-stub (WAVE 6, INERT — authentic mover/heat coupling deferred). **✅ TORSO twist/elevation aim — joint-I/O + simulation RECONSTRUCTED (dormant; blocked on the WAVE-4 un-stub).** The torso's aim→skeleton path is now fully reconstructed and builds green with zero combat regression, but is NOT yet live because the Torso subsystem itself is still a `HeatSinkSource` RECON_SUBSYS stub at factory case 0xBC5 (its real class is **Torso**, ctor @004b6b0c — the "SinkSource" label is the usual mislabel). What was done: - **`Mech::ResolveJoint(name)→Joint*`** (mech.cpp, == binary `FUN_00424b60`): resolves a named skeleton node via the PUBLIC engine path `GetSegment(CString)` → `EntitySegment::GetJointIndex()` → `JointSubsystem:: GetJoint(i)` (JMOVER.h/SEGMENT.h/JOINT.h, all in `munga_engine.lib`) — never touches the protected `EntitySegment::jointPointer`. The Torso ctor @004b6b0c + Gyro ctor @004b3778 inline this; hoisted as the reuse point (gyro un-stub will call it too). `Torso::ResolveJoint` forwards to `owner->ResolveJoint` (out-of-line — needs complete Mech). - **`Torso::PushTwist(Joint*,Scalar)`** (== `FUN_004b66b4`/`FUN_004b67ec` inner block): dispatch on `Joint::GetJointType()` — hinge (0..2)→`SetRotation(Radian(twist))` (`FUN_0041d0a8`); ball (4..5)→ `GetEulerAngles()` keep pitch[0]/roll[2], **yaw[1]←twist**, `SetRotation(EulerAngles)` (`FUN_0041cfa0`/ `FUN_0041d020`). Nodes retyped `void*`→`Joint*`. - **`TorsoSimulation` @004b5cf0 corrected to match the raw decomp** (part_013.c 4471-4693): the earlier draft had the command roles SWAPPED — verified true roles: **@0x1F8/@0x1FC = elevate up/down** (drive `currentElevation` 0x1e4, clamp vertTop/Bottom), **@0x200/@0x204 = twist left/right** (drive `currentTwist` 0x1d8, clamp horiz Left/Right), @0x208 = recenter; **analog axes @0x1F0 = twist, @0x1F4 = elevation** (the draft misused `elevationHalfBottom`@0x22C and omitted @0x1F4). Settle test uses `targetTwist`(0x218) + the OLD twistRate snapshot; limit latches are **edge-triggered** with @0x260/0x264=twist L/R, @0x258/0x25C=elev top/bottom (the draft had these swapped/non-edge). `ResetToInitialState` @004b5bf8 likewise made faithful (clears all 4 latches + analog axes + aim state). - **Per-frame skeleton write wired.** KEY DECOMP FACT: `TorsoSimulation` AND `TorsoCopySimulation` compute `currentTwist` but **never write the joints**; `WriteJoints` (@004b66b4) is called ONLY by `ResetToInitialState`; `UpdateJoints` (@004b67ec, the out-param-free twin) has **ZERO direct callers** — the per-frame torso→skeleton write was dispatched by the engine's generic joint pass (indirect/virtual). This port ticks the torso's own Performance, so `UpdateJoints()` is now called at the end of both Torso(Copy)Simulation — the resolved equivalent at the same cadence. Verification is env-gated `BT_TORSO_LOG` (ctor resolve + layout dump; PushTwist fire log). **✅ WAVE-4 Torso base-chain RE-BASE + un-stub — DONE (real Torso is live, binary-exact layout, zero regression).** The Torso is now built by the real `Torso` class at factory case 0xBC5 (`CreateTorsoSubsystem`), no longer a stub. - **The corrected hierarchy** (verified from headers + a 6-agent decomp-mapping workflow): `Torso : PowerWatcher : HeatWatcher : MechSubsystem` — **NOT** HeatableSubsystem. The Watcher branch is SEPARATE from the working `PoweredSubsystem : HeatSink : HeatableSubsystem : MechSubsystem` heat leaves; the two share ONLY `MechSubsystem`. So the 156-byte deficit lives entirely in the Watcher branch and re-basing it CANNOT touch the working heat leaves. (Corollary, verified: Torso is **NOT** `IsDerivedFrom(HeatSink)` → it never joins the heat-conduction roster, so there was never a heat-roster-corruption risk; the earlier note to the contrary was wrong.) - **The +156 fix (all in the Watcher branch; `MechSubsystem` untouched):** the binary's HeatWatcher/PowerWatcher use a **0xC-byte** connection (`watchedLink`) and a **0x54-byte** gauge alarm (`heatAlarm`/`watchdogAlarm` = a full GaugeAlarm), NOT the 4-byte `SubsystemConnection` / 8-byte `HeatAlarm` the HeatSink branch uses. Introduced Watcher-**local** `WatchedConnection`(0xC) + `WatcherGaugeAlarm`(0x54) in `heatfamily_reslice.hpp` (NOT heat.hpp, so the shared types the working leaves depend on are never resized). HeatWatcher: `watchedLink`→0xC (+8), `heatAlarm`→0x54 (+76) = +84 → own ends 0x180. PowerWatcher: DELETE the shadow `watchedLink` (the binary has ONE, inherited@0x114) (−4), `watchdogAlarm`→0x54 (+76) = +72 → `minVoltage`@0x180, `watchdogAlarm`@0x184, own ends 0x1D8. - **Torso own block:** DELETED the 7 CROSS-FAMILY shim backing fields (segmentFlags/heatModelOff/heatStateLevel/ electricalState/creationTime/currentTime/movedFlag, −28) and their accessors now read the REAL inherited base (`heatAlarm.GetLevel()`, `watchdogAlarm.GetLevel()`, `MechSubsystem::simulationState`, engine `Simulation:: lastPerformance`/`ForceUpdate()`); ADDED the 4-byte `_reserved0x270` slot (+4) so `recenterActive`@0x274 / joints@0x278/0x27C match the binary. Net −24 → own block 0xA8, **`currentTwist`@0x1D8, `sizeof(Torso)==0x280`**. - **Compile-time LOCKS** (the practice from the resource audit, now applied to OBJECT layout — protected fields need a `friend struct XLayoutCheck`): `TorsoLayoutCheck` (currentTwist==0x1D8, sizeof==0x280, +0x1E4/0x1F0/0x1F4/0x250/ 0x274/0x27C), HeatWatcher (watchedLink@0x114, heatAlarm@0x12C, sizeof==0x180), PowerWatcher (minVoltage@0x180, watchdogAlarm@0x184, sizeof==0x1D8), `WatcherGaugeAlarm`==0x54 / `WatchedConnection`==0xC, and **`HUDLayoutCheck`** (HUD::flickerRate@0x1D8 — HUD is the only OTHER live Watcher-branch class; the lock proves the re-base fixes HUD's own torso+0x1D8 read too). All pass. **KEY: `sizeof()` static_asserts work at namespace scope, but `offsetof()` on a PROTECTED object member needs a `friend struct` (namespace-scope offsetof → C2248).** - **Verified live** (`BT_TORSO_LOG`): the real Torso constructs with `currentTwist@472 (0x1D8)`, `sizeof=640 (0x280)`, no crash over 26s; heat converges to the identical steady state (`pendingHeat` 0.239, temp 77, heatLoad plateaus — the transient −0.265 on shot #1 was benign startup-timing variance); full combat loop intact (**TARGET DESTROYED after 8 hits** at walk speed, matching the prior baseline). The gyro cross-link `sinkSourceSubsystem+0x1D8` now reads the live `currentTwist` (was stub garbage). - **Why the torso still doesn't visibly twist for the Blackhawk (a FAITHFUL result, NOT a bug):** the Blackhawk's 0xBC5 record has **`TorsoHorizontalEnabled=0`** + empty joint names + `hRotPerSec=0` (dumped raw; the record IS a valid Torso record — classID=0xBC5, modelSize=0x158). Our resource struct is CONFIRMED correct — the binary ctor `@004b6b0c` reads enabled at `param_4+0x14C` (line 5038) and gates joint resolution on `enabled!=0` (line 5039), joints at `+0x10C`/`+0x12C`, exactly our layout. So the ORIGINAL binary ALSO skips torso joints for this mech; our behavior is byte-faithful. Visible twist needs a **twist-enabled mech** (a record with enabled=1 + real joint names), or the clearly-marked bring-up force-enable below. - **✅ FULL PATH PROVEN end-to-end via `BT_FORCE_TORSO` (bring-up demo; default OFF; NOT faithful).** Since the Blackhawk record disables the torso, a gated demo (`BT_FORCE_TORSO=1`, torso.cpp ctor + TorsoSimulation) force- enables the torso, resolves the REAL `BLH.SKL` joints, widens the limits, and sweeps an analog twist demand. Verified (`BT_TORSO_LOG`): `Mech::ResolveJoint` resolved **`jointshakey2`→type=4 (Ball, torso body)** + **`jointtshadow`→type=1 (HingeY, the skeleton's "apply torso twist to yaw" joint)**; `PushTwist` then fires every frame with `currentTwist` sweeping cleanly `0→+0.7` (clamped at limit) → reverse → `-0.7` → back — i.e. the WHOLE reconstructed chain (analog axis → TorsoSimulation integrate → clamp → UpdateJoints → PushTwist → `Joint::SetRotation`, ball branch yaw←twist) runs correctly; no crash over 14s. The only unconfirmed link is the render pass consuming the joint (needs an eyeball). The env-gated demo (+`BT_FORCE_TORSO_JOINT=` to pick the main joint) stays as an on-demand verifier; faithful behavior is untouched when unset. BLH.SKL torso joints for reference: `jointshakey2` (ball, blh_tor.bgf, under jointhip) = torso body; `jointtshadow` (hingey, under jointshadow) = ground-shadow twist. - **Follow-on for the sibling un-stubs (Gyro/Searchlight/ThermalSight):** they inherit PowerWatcher, so after this re-base their own fields also begin at 0x1D8, but they STILL carry the same CROSS-FAMILY shim fields → each must get the same shim-delete + accessor-redirect (like Torso) BEFORE its un-stub, or wiring it will over-size the object. **◐ WAVE-5 GYROSCOPE (0xBC4) — layout + joint-I/O DONE; un-stub DEFERRED (ctor/integrator reconstruction incomplete).** The gyro plumbing is reconstructed and the build is GREEN with the gyro back on its stub (reverted — see why below); the infra is dormant + ready in gyro.hpp/gyro.cpp: - **Re-base + layout (DONE, locked):** `Gyroscope : PowerWatcher : HeatWatcher : MechSubsystem` (Watcher branch). Deleted its 4 CROSS-FAMILY shim fields (segmentFlags/heatModelOff/heatStateLevel/electricalState) + redirected the 3 used accessors to the real inherited base (heatAlarm/watchdogAlarm/simulationState), retyped eyeJointNode/mechJointNode `void*`→`Joint*`. `GyroLayoutCheck` friend locks `exageration@0x1D8` + `sizeof<=0x3D0` (both pass live). - **Joint I/O (DONE, real):** the 8 node-helper stubs (NodeType/NodeScalar/SetNodeScalar/NodeVector/SetNodeVector/ NodeRotationEquals/SetNodeRotation/NodeVectorEquals) now call the real engine `Joint` API — `GetJointType`/`GetRadians`/ `SetRotation(Radian)` [hinge]/`GetEulerAngles`/`SetRotation(EulerAngles)` [ball]/`GetTranslation`/`SetTranslation` [BallTranslation]; **FUN_0041d11c = SetTranslation** (writes `*(joint+0xc)` + jointModified). `ResolveJoint` forwards to `Mech::ResolveJoint`. WriteEyeJoint/WriteMechJoint (no direct caller in the decomp, like the torso's UpdateJoints — dispatched by the engine joint pass) are wired at the end of `GyroscopeSimulation`. - **Verified live then REVERTED:** the real gyro constructs, resolves REAL joints (`EyeJoint='jointlocal'`, `MechJoint='jointeye'`, both BallTranslation type=5), and `WriteMechJoint` FIRES every frame — but with **garbage values** (`-4.31e8` = 0xCDCDCDCD-as-float, and NaN) written to `jointlocal` (the ROOT joint). Root cause: the gyro's ctor field-init + integrator field-MAPPING are incomplete/incorrect in the reconstruction. The binary ctor @004b3778 puts **springConstant@0x1E8, dampingConstant@0x1F4, posSpring@0x218, negSpring@0x224, rotation springs @0x2C0/0x2CC/ 0x2F0/0x2FC**, but the recon mislabels several (e.g. assigns springConstant to eyeOrientation@0x1DC; the field the integrator reads as `eyeSpringConstant`@0x1F4 is really dampingConstant) and leaves the integrator accumulators (eyeAccel/eyeForce/eyeWork/bodyOrientation/bodyAccel/bodyForce/bodyWork/eyeVelocity/limits) UNINITIALISED. Writing NaN to the root joint is unshippable, so 0xBC4 was reverted to the `Gyro` stub (combat/heat un-regressed, TARGET DESTROYED). - **⚠ Cross-link landmine (mech.cpp):** the gyro↔torso cross-link `((SubProxy*)gyro)->linkTarget = sinkSource+0x1D8` writes through `SubProxy::linkTarget` which sits at **gyro+4** (an engine base field), not the real gyro torso-link offset — harmless vs the stub, would STOMP the live gyro base. Commented out; needs the real gyro torso-link field. - **Remaining work (the real gyro un-stub):** faithfully reconstruct the ctor field-init + the two integrators (IntegrateEyeJoint @004b2ec0 / IntegrateBody @004b30ec) and the field LAYOUT/labels from @004b3778 (the recon has a ~196-byte layout gap + uncertain field map). This is a full subsystem-simulation reconstruction (bigger than the torso), not a quick un-stub — do NOT guess the integrator mapping (no stand-ins). Env `BT_GYRO_LOG` dumps the layout + joint resolve + WriteMechJoint values for verification.