Files
BT411/docs/PROGRESS_LOG.md
T
arcattackandClaude Opus 4.8 f914fc040a context-system: complete migration -> CLAUDE.md is now a 160-line router (zero context lost)
Full migration of the 2236-line monolithic CLAUDE.md into the progressive-context
knowledge graph (per spark-lesson / expert-seed.md), so the deep RE knowledge loads
on-demand instead of every session.

ZERO CONTEXT LOST:
- docs/PROGRESS_LOG.md = the complete old CLAUDE.md, VERBATIM (byte-identical) -- the
  lossless safety net + the "full detail" quick-lookup fallback.
- 18 context/*.md topic files (1343 lines) digest every section (§1-3 -> project-overview,
  §4 -> content-archives, §5 -> asset-formats/bgf-format, §5a -> source-completeness,
  §5b/§8 -> wintesla-port, §7/§10 -> locomotion, §10a -> build-and-run, §10b ->
  reconstruction-method, §10c -> combat-damage + reconstruction-gotchas, §10d -> subsystems,
  render notes -> rendering, gauges -> gauges-hud, MP -> multiplayer, §9 -> open-questions).
- reference/glossary.yaml (53 terms). decomp-reference.md = the offsets/ClassIDs/addresses hub.

CLAUDE.md (160 lines) = router: identity, answer/reason protocols, quick-lookup table,
evidence tiers (T0 engine-truth / T1 decompiled+verified / T2 reconstructed+runtime /
T3 guarded / T4 hypothesis), conventions + DO-NOT (the systemic bug classes), structure.
Retains the load-bearing work directives (build recipe pointer, "keep current" mandate).

Knowledge graph validates CLEAN (scratchpad/checkctx.py -- all [[links]] + quick-lookup +
docs refs resolve; [[name]] -> topic file or glossary term). docs/*.md ledgers stay as the
detailed logs; context/*.md are the curated digests that route into them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 22:19:50 -05:00

2237 lines
220 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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, ~199596) 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=<name>`
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 <that>.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.51.5 hr/module → ~3050 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 `<math.h>`; 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 <model.bgf | skeleton.skl> [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<float>` 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) 23.5u**, **dunes (dhillg1-3) 03.1u**,
**flat pad (calpb) 1.6u** (⇒ NO global snap offset — the probe/lift is correct), **buildings (bld06/bld08)
1214u = 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 ~522u
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 `<d3dx9.h>`, `<d3dx9math.h>`, `<dinput.h>`, `<dxerr.h>`, 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
`<time.h>` → 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 13 (engine/renderer/HAL/audio = done in WinTesla); Phases
48 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 <port>` CLI arg
= networked mode; the pod listens on <port> for a CONSOLE and boots `ConsoleOnly`; game mesh port =
<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 `<Name>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 <VA> <len>` (recover FPU math Ghidra drops — `FUN_004dcd94`=round, `FUN_004dcd00`=fabs) +
`tools/vtdump.py <vtVA> <n>` (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).
**✅✅✅ GAUGE SYSTEM COMPLETE — every config attribute binds + every config gauge widget builds (full map:
`docs/GAUGE_COMPOSITE.md`).** Closing the data-binding wave (user: "complete the full guage system"):
- **Attribute bindings: 0 NULL** (`BT_GAUGE_ATTR_LOG` = 50 OK). The LAST unresolved binding
(`HeatSink/AmbientTemperature`) is cleared by the reconstructed **`AggregateHeatSink` 0xBBE** (the heat-sink
BANK class, ctor @4ae8d0, GUID 0x50e590, byte-exact sizeof 0x1E4; publishes HeatSinkCount + AmbientTemperature=300,
frozen). Deliberate deviation: keep the base HeatSinkSimulation, do NOT install the authentic Performance @4ae73c
(it derefs a raw self+0xE0→[+0x158] that doesn't map in our layout → AV for EVERY mech; ambient is a frozen
constant so the gauge reads 300 either way).
- **Widgets: 0 parse-skips** (`BT_GAUGE_SKIP_LOG` = 0). The last 3 unregistered primitives reconstructed + registered:
**SectorDisplay** (radar SECTOR X/Z read-out — LIVE: `Round(-Z*0.01)+500`/`Round(X*0.01)+500` 100-unit sectors,
verified sectorA=510/sectorB=504 from live position), **PrepEngrScreen** (12 engineering-screen label overlays —
base `GraphicGaugeBackground` paint-on-activation, NOT Execute), **MessageBoard** (sec-MFD comm ticker — DEFERRED
empty: SetSource has no caller + StatusMessagePool is a NULL stub, so no messages exist; safe no-op).
- **Condenser valve gauge**: fixed the `/FORCE` ODR dup (heat.cpp stub Condenser ctor shadowed the real reslice one
→ valveState=garbage) + reconstructed **`RecomputeCondenserValves`** (FUN_0049f788, was a no-op `FinishConstruction`
stub) → coolantFlowScale = valveState/Σvalve, so `ValveSetting` reads the authentic **1/N** (BLH 6 condensers →
0.1667 each). **`Condenser::MoveValve`** replaced (the reslice body was mislabeled @4afbe0 controls-mapper; real
@4ae464 disassembled: cycle 1→5→50→0; DORMANT — route/guard via 0xBD3).
- **Remaining gauge work = DATA FEEDS, not widgets** (deferred systems, marked): the valve/message CONTROL routes
(0xBD3 SubsystemMessageManager, WAVE 8) + the status-message queue (StatusMessagePool); `SeekVoltageGraph`'s 4
Seek* attributes (cluster-child, not config-called). ⚠ VERIFICATION GOTCHA: the gauge renderer builds LAZILY
(BuildConfigurationFile) — a too-early process kill shows a false "0 skips / not built"; wait for the gauge window.
**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 <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=<name|ALL>` (+
`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=<name>` (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 <EXPLODE.hpp>`;
each now `#include "../../engine/MUNGA/<NAME>.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<JointedMover::AnimationCallback>` (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 z<damageZoneCount: new
(Memory::Allocate(0x1b8)) Mech__DamageZone(this,z,&stream)` → `SetLODParentPointers` → crit subsystems
(type 0x1e) + `MechDeathHandler`. The engine `DamageZone` ctor (`DAMAGE.cpp:200`, shared MUNGA == binary
`FUN_0041df5c`) reads name/effect-sites/`defaultArmorPoints`/`damageScale[5]`/materials — so stream
alignment is exact.
**⚠ THE SYSTEMIC BUG (check this first on every reconstructed class): re-declared engine base fields.**
The reconstructed BT classes re-declared fields that already exist in their WinTesla engine base, at the
*binary's* offsets. Two failures result: (a) the re-declaration **shadows** the engine field (the engine
ctor writes the base field; the reconstruction reads its own uninitialised copy = `0xCDCDCDCD`), and
(b) compiled as a subclass of the engine base, the copy lands at a **different offset** than the binary
assumed. Fixed instances this session — `Mech`: `damageZoneCount`/`damageZones` (shadowed `Entity`'s →
zones never built); `Mech__DamageZone`: `structureLevel`→engine `damageLevel`, `damageScale[5]`/
`defaultArmorPoints` (raw `Wword(0x51)`/`Wword(0x50)` offsets → inherited members), `graphicState`/
`graphicStateAlarm`/`flags`/`state`→engine `StateIndicator`s `damageZoneState`/`damageZoneGraphicState`
(+ `SetDamageZoneState`/`SetGraphicState`/`GetGraphicState` accessors; the reconstructed `SetGraphicState`
override was byte-identical to the base → removed). Use the inherited member/accessor, never a raw
`Wword(N)`/re-declared copy, for any field the engine base owns. (`subsystemCount`/`subsystemArray` were
already fixed this way earlier — same lesson.) **`Wword(int i)` is itself a TRAP**: `mechrecon.hpp:192`
defines it as `static BTVal bank[0x400]; return bank[i&0x3ff]` — a global scratch bank, NOT `this+i*4`.
Any `Wword(N)` used for object access reads/writes garbage (e.g. `(Mech*)Wword(3)` for a zone's owner →
use `GetOwningSimulation()`).
**⚠ SIBLING SYSTEMIC BUG — resource-struct layout mismatch (check when RESOURCE fields read garbage).**
The `*__SubsystemResource` structs are overlaid on **pre-built 256-byte records loaded verbatim** at the
binary's fixed offsets, so OUR struct MUST match the binary byte-for-byte or every member read is wrong
(and the failure is *silent* — garbage that happens to be non-fatal, like a heatSinkIndex reading `10.0f`).
Two ways it breaks: (a) **wrong resource inheritance base** — the resource must mirror the CLASS hierarchy
(`HeatableSubsystem:MechSubsystem` → resource must inherit `MechSubsystem__SubsystemResource` (0xE4), not
`Subsystem::SubsystemResource` (0x30)); a skipped base slides every field low. (b) **under-sized fields** —
e.g. `alarmModel` at res+0xCC is a 12-byte record field but was typed as a 4-byte `ResourceID` (`typedef
int`) → 8-byte gap. DIAGNOSE by logging compiled offsets (`(char*)&res->field - (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=<n>` (release forced throttle after n sim-s)
+ `BT_FORCE_TURN=<t>` (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 <Name>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<Class>
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 <mechsub.hpp>`; `mechrecon.hpp` now `#include
<ROTATION.hpp>` (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
`Create<Class>Subsystem(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
`Create<Class>Subsystem` 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 <Class>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 `Create<Class>Subsystem` 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=<name|ALL>` + `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: visualorigin = 0.052.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 (02.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=<float> 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 02.1u ABOVE the collision cones on slopes (median 00.5, p90 1.21.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 ≈ 24u 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 [y1, 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=<hex>` 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 <page>` + `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(C1C2·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 2050u 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.0000.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 375500u 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 <tga>`); 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
`<prop>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=<name>` 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.