diff --git a/CLAUDE.md b/CLAUDE.md index d0d0fde..9bcdf59 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,2236 +1,160 @@ -# Virtual World Pod Games — Port Project (BattleTech / Red Planet) +# BattleTech Pod-Port — Reverse-Engineering Expert Knowledge Base -> ⚠️ **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. +> **This file is a ROUTER, not a knowledge store.** It says WHERE the knowledge is and HOW to use +> it; the actual knowledge lives in `context/*.md` (read on-demand). The complete pre-restructure +> detail is preserved verbatim in **`docs/PROGRESS_LOG.md`** (the old monolithic CLAUDE.md) — the +> ultimate fallback if a topic digest is thin. Migration history: `phases/phase-01-context-restructure.md`. -Knowledge base + roadmap for porting Virtual World Entertainment's arcade **pod** games -(Tesla platform, release 4.10, ~1995–96) to run on modern Windows and, ultimately, the actual -arcade pod hardware. Primary target: **BattleTech (BT)**; Red Planet (RP) shares the engine. - -> Companion docs: `docs/BGF_FORMAT.md` (model format), `docs/ASSET_PIPELINE.md` (textures/materials/ -> gauges). The working port code is under `port/` (see "Port codebase" below). - -> **📌 This is a PROGRESSIVE CONTEXT system — keep it current.** This file is the single, durable source -> of project context across sessions. **Whenever you learn something new and non-obvious about the -> project — a decompilation finding, a build/runtime gotcha, a reconstruction insight, an offset/format -> detail, a decision or its rationale, a dead end to avoid — ADD it here** (in the most relevant section, -> or §10 for combat bring-up / reconstruction findings) as part of the same task, not as an afterthought. -> Update existing statements when reality changes them rather than letting the doc drift. Goal: a later -> session (or a fresh agent) can read this file and have the full working context with nothing lost to -> chat history. Durable project facts → here; per-session/cross-cutting reminders → the `memory/` files -> (indexed in `MEMORY.md`); detailed running findings → `docs/RECONCILE.md`. +**Project:** Port VWE's 1995 arcade **BattleTech** (BT) pod game (MUNGA engine, Tesla platform +4.10) to modern Windows + the pod hardware, by reconstructing the missing BT game logic from the +`BTL4OPT.EXE` binary on top of the working WinTesla engine. +**Repo of record:** the top-level `CMakeLists.txt` + `README.md` build `btl4.exe`. Layout: +`engine/ game/ content/ docs/ reference/ tools/ context/`. +**Current front:** `btl4.exe` runs a full single-player loop; the gauge system is complete; the +active work is reconstructing each subsystem's authentic behavior from the binary. Details + +what's-next: `context/project-overview.md`, `context/open-questions.md`, recent git log. --- -## 1. What this is +## YOU ARE THE BATTLETECH PORT EXPERT -- 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. +**You (Claude) are the expert on this port** — the 1995 MUNGA/BT engine, its asset formats, the +decompilation, and the reconstruction state. The `context/*.md` files, `reference/glossary.yaml`, +`reference/decomp/` (the raw pseudocode), and the `docs/*.md` ledgers are YOUR knowledge base. -## 2. People / goals +**ALWAYS consult the context system before answering a question about the engine, formats, a +subsystem, or the reconstruction state — and before writing reconstruction code.** Do not rely on +training data; this is a specific, reverse-engineered 1995 codebase. The knowledge here is more +precise than anything you can infer. -- **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. +### How to answer a question +1. **Identify the topic(s)** via the Quick Lookup table below (most questions touch 1-3). +2. **Read the full topic file** — don't guess from the filename. +3. **Follow the wiki-links** (`[[name]]`) and `related_topics` frontmatter to adjacent knowledge. A + `[[name]]` resolves to the topic file `context/.md` if one exists, else to the glossary + term in `reference/glossary.yaml` (cross-cutting concepts like `material-ramp`, + `subsystem-roster`, `attribute-pointer`, `master`/`replicant` live in the glossary, not a topic). +4. **Check `context/decomp-reference.md`** for any offset, ClassID, resource type, address, or env gate. +5. **Check `reference/glossary.yaml`** for a term's meaning. +6. **Check `context/open-questions.md`** for known unknowns / deferred systems. +7. **Cite** the file + section (and the `docs/*.md` ledger or `reference/decomp` address when relevant). -## 3. Pod hardware (fixed target) +### How to reason about / reconstruct something novel +1. **Ground it in the decomp.** Read the raw `FUN_xxxx` in `reference/decomp/all/part_*.c`; map + `FUN_`/`DAT_`/`this+0xNN` to engine symbols via BT headers + the WinTesla MUNGA source + + `game/reconstructed/CLASSMAP.md` + RP's parallel code. +2. **Check the gotchas.** `context/reconstruction-gotchas.md` — layout (shadow/alias/phantom + fields, resource mismatch), linkage (`/FORCE`), dtor-epilogue, databinding trap. Most bugs are here. +3. **RULE: no stand-ins.** The full logic IS in the pseudocode; a "gap" is an unfilled stub, not a + hole. Never write placeholder logic — read the decomp. Bring-up scaffolding is marked + temporary. +4. **Verify honestly.** `static_assert`-lock layouts; run env-gated; read `btl4.log`; cdb on crashes. + Tag claims with the evidence tier; flag T3/T4. +5. **Persist insights.** A genuinely new finding → add it to the right `context/*.md` (+ the `docs/` + ledger for detail), with an evidence tier. Keep the knowledge base current — this is a mandate, + not an afterthought. -- **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. +### Quick Lookup +| User asks about... | Read this file | +|---|---| +| What the project is, engine, platform, goal | `context/project-overview.md` | +| Missing BT source, decompilation strategy | `context/source-completeness.md` | +| The WinTesla Windows port (renderer/audio/HAL) | `context/wintesla-port.md` | +| Build / run / debug / repo layout / env gates | `context/build-and-run.md` + `context/decomp-reference.md` §6 | +| Asset formats (MOD/SKL/ANI/BMF/BSL/SLD/…) | `context/asset-formats.md` | +| BGF geometry, LODs, CONN/PCONN, ramps | `context/bgf-format.md` | +| A layout/linkage/databinding BUG | `context/reconstruction-gotchas.md` | +| The reconstruction method / workflow | `context/reconstruction-method.md` | +| Walking, gait, ground model, collision | `context/locomotion.md` | +| Subsystems, the factory, heat/weapons/power | `context/subsystems.md` | +| Damage zones, targeting, firing, death | `context/combat-damage.md` | +| Rendering, LODs, materials, sky, shadows, beams | `context/rendering.md` | +| Cockpit gauges / MFD HUD | `context/gauges-hud.md` | +| Multiplayer, replication, netcode | `context/multiplayer.md` | +| Pod hardware, monitors, RIO, MFD surfaces | `context/pod-hardware.md` | +| Content archives, maps, where data lives | `context/content-archives.md` | +| Offsets, ClassIDs, addresses, resource types, env vars | `context/decomp-reference.md` | +| What we don't know / deferred systems | `context/open-questions.md` | +| Term / acronym definitions | `reference/glossary.yaml` | +| The complete verbatim detail (fallback) | `docs/PROGRESS_LOG.md` (the old 2236-line CLAUDE.md) | +| Detailed running ledgers | `docs/RECONCILE.md`, `docs/GAUGE_COMPOSITE.md`, `docs/HARD_PROBLEMS.md`, `docs/SUBSYS_PLAN.md`, `docs/P3_LOCOMOTION.md`, `docs/RESOURCE_AUDIT.md`, `docs/VEHICLE_SUBSYSTEMS.md`, `docs/BGF_FORMAT.md`, `docs/ASSET_PIPELINE.md`, `docs/BT_SOURCE_STATUS.md`, `docs/WAVE_PLAN.md` | -## 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: +## Evidence Tiers (adapted for reverse-engineering) -| Archive | What it is | Has | +Tag individual claims (not sections) inline: `[T1]`, `[T2]`, … A claim inherits the **highest +(least certain)** tier of its inputs. Flag T3/T4 to the user. + +| Tier | Label | Meaning | |---|---|---| -| `410srczipped.zip` | **Source code** (36MB) | all `.cpp/.hpp`, geometry `.bgf`, material defs `.bmf`, **animations `.ani`** (428), a few sample textures/skeletons. NO per-mech skins/skeletons. | -| `Tesla4NovellTechPC-*.zip` | pod **technician/test PC** (77MB) | game binary `btrel410.exe` (in `vgl_labs/archive/`), Division **VPX board diagnostic tools** (`vwetest/vpx/`), shared generic textures, Novell DOS. | -| `Tesla4PodPCNovell-*.zip` | the pod **game machine** (92MB) | **the full deployed tree** at `…/CopyofNovellDisk/REL410/` (BT+RP): per-mech **skeletons** (`AVA.SKL` etc., 124), **skins** (`AVATAR.BSL` etc.), 1143 models, 1624 material libs, textures. | -| `Elsewhen RP411 Source-*.zip` | **the WINDOWS PORT** ("WinTesla", VS2008) ⭐ | modernized source: `WinTesla.sln`; MUNGA(150 cpp)+MUNGA_L4(43); **`L4D3D.cpp` = IG-board→Direct3D renderer bypass DONE**; **OpenAL+libsndfile = SOS audio replaced**; full **Red Planet** game logic (`VTV/VTVMPPR/WEAPSYS/...`); asset exporters (`MeshExp/TextureExp`); `DivLoader`. Solution wires both `RP_L4` AND a `BT410_L4` target — **but the `BT410_L4` folder + all BT game source are ABSENT** (only the .sln reference remains → BT was being ported, code not included). Extracted: `elsewhen_extracted/`. | - -Extracted to: `410srczipped_extracted/`, `tesla4_extracted/`, `tesla4pod_extracted/`. -**Content root for the port** = `tesla4pod_extracted/Tesla4PodPCNovell/CopyofNovellDisk/REL410/` -(use the pod tree for skeleton/skins/geometry; **animations** come from the source tree's -`410srczipped_extracted/.../CONTENT/BT/ANIMS/`). - -Still likely missing / ask Nick: confirmation this is the complete content master. -(UPDATE: the walk/run animation set is NOT missing after all — `CONTENT/BT/ANIMS/` has the full per-mech -set, e.g. `BLHRRL/BLHRRR/BLHWWLI/BLHWWRI...`, and they are compiled into `BTL4.RES` — the game plays -`blhrrl`=904. The earlier "only AVABMP present" worry was looking at an incomplete slice.) - -### 4a. LEVELS / MAPS — what's loadable (content audit) -The mission is an INI `.EGG` (`[mission] map= scenario= time= weather=`). The engine resolves `map=` -from `BTL4.RES` (`MISSION.cpp:344 FindResourceDescription(name, MakeMessageStreamResourceType)`; a missing -name → null-deref crash at `:355`). **The runtime `BTL4.RES` (pod tree == `decomp/BTL4.RES`, byte-identical, -3.23 MB) contains 8 loadable maps:** `cavern` (the default night cave, enclosed/no sky), `grass` (grassland), -`rav` (ravine), `polar3`/`polar4` (snow), `arena1`/`arena2`, `dbase`. **Switch levels by editing a COPY of -`TEST.EGG`** (e.g. `map=grass` + `time=day`) and running `-egg .egg` — verified: `grass` boots + the -mech walks, 0 crashes (`GRASSDAY.EGG` created). ⚠ Names are `arena1`/`polar3`/`rav` (with the suffix), NOT -bare `arena`/`polar`; `map=burnt`/`frstrm`/`des` are NOT in this RES (they crash) — those are lab/other-build -maps whose `.map` SOURCE exists in `CONTENT/BT/MAPS/` (`DES.MAP` = a sky+buttes desert) but would need -compiling into a RES via the DOS `btl4tool.exe` (`BUILDRES.BAT`, manifest `CONTENT/BT/BTL4.BLD`) — a -separate content-build sub-project. **The full content SOURCE tree** `410srczipped_extracted/.../CONTENT/BT/` -is comprehensive (17 `.map`, 636 `MODELS/`, 219 `SOLIDS/` collision, 331 `ANIMS/`, 1958 `VIDEO/` textures, -207 `AUDIO/`, 404 `GAUGE/`) — i.e. the assets to build ANY map exist; the runtime tree is the already-built -subset. No consolidation needed to test the 8 maps. ⚠ Collision is OFF on ALL maps until `MoveAndCollide` -is reconstructed (the mech phases through terrain/objects) — the `.sld` collision solids ARE present in the -RES + `SOLIDS/`, so it is a code-revival task, not a content gap. - -## 5. Asset formats (all reverse-engineered; details in docs/) - -- **`.MOD`** — INI mech/object definition: skeleton, skins, collision, gauges, physics, animations, class. -- **`.BGF`** — `DIV-BIZ2` little-endian nested-TLV geometry. OBJECT→LOD→PATCH(=geogroup)→PMESH; - interleaved float32 verts; **quad faces (triangulate!)**; materials by `library:material` name; - damage/articulation are **name-driven via `SV_SPECIAL` `dz_*` tokens**, not stored geometry. -- **`.BMF`** — `DIV-BIZ2` (FILETYPE=1) material+texture **library** (no pixels). Material→DIFFUSE color - + `MATERIAL_TEXTURE`→texture→`TEXTURE_MAP` image basename. -- Texture **pixels**: **`.tga`** (Truevision), **`.vtx`** (`DIV-VTX2`, trailing RGB), **`.bsl`** - (`DIV-BSL2`: a **BIT-SLICED container** — w*h 32-bit texel words holding SIX independent 4-bit GRAYSCALE - sub-images (nibble pair-swapped; byte 0 pad) or RGB444/RGBA4444 (sliceType 7/8); the BMF TEXTURE tag - `0x18 BITSLICE` selects the slice, absent = 0. The old "[pad,R,G,B] confirmed" note was WRONG — full - spec in `docs/ASSET_PIPELINE.md` §Pixel formats; reference reader `DivLoader/VGCDivLoader.cpp:323-410`). -- **`.SKL`** — INI joint skeleton: `[joint] parent=, Type=(hingex/y/z|ball|balltranslate), Object=part.bgf, - dzone=, tranx/y/z, pitch/yaw/roll`. Build a DCS tree; neutral pose ≈ translations (pyr ≈ 0). -- **`.ANI`** — INI keyframe animation: `[HEADER] framecount/framerate(30)/skeletonfile`, `[Time]` - frameN→seconds, `[JointType]` per-joint DOF, `[frameN]` joint→`rx ry rz` radians (balltranslate→translate). - Walk anims have **no baked root translation** → forward motion is procedural/physics (see §7). -- **`.GIM`** (vector MFD/radar line maps) + **`.GAT`** (color table) + **`.PCC`** (ZSoft PCX 8-bit gauge - rasters) — the cockpit HUD assets. **`.DZM`** = INI damage-zone→material map (mech skin states). -- **`.SLD`** collision solids, **`.DMG`/`.TBL`** damage zones/tables, **`.SUB`** subsystems, **`.CTL`** control maps. - -## 5a. ⚠ SOURCE COMPLETENESS — the gating blocker - -The archive has the **engine and all headers, but NOT the game's implementation code**. Verified across -all three zips: -- **MUNGA engine** (shared): ~183 `.cpp` — largely complete (renderer, `MOVER/JMOVER`, math, scene). -- **All game `.hpp` headers** present (every class/interface defined). -- **BT game logic `.cpp`: MISSING** — `mech`, `mech2/3/4`, `mechsub`, `mechmppr`, `mechtech`, `heat`, - `sensor`, `gyro`, `torso`, `myomers`, `hud`, `mechweap`, `powersub`, `btplayer`, and the **main app - `btl4`** are all absent. Only 10 BT `.cpp` are present (BTCNSL/BTMSSN/BTREG/BTSCNRL/BTTEAM/BTTOOL + - weapons GAUSS/PPC + BTL4ARND/BTL4MODE). RP game logic is similarly gutted (5 `.cpp`). -- Compiled **`btrel410.exe`** exists (binary only, in the TechPC zip). -- **RP can't backfill BT:** RP's game source is gutted the same way (only `RPCNSL/RPMSSN/RPTOOL/VTVPWR/ - VTVSUB` + L4 glue); none of the missing BT files are under RP. Only shared *engine* game-logic is - present (`MUNGA\DAMAGE.CPP`, `MUNGA\MOVER.CPP`). The per-game source for BOTH titles is missing — the - archive captured the engine fully but only a partial subset of game source → a complete repo likely - exists elsewhere (Nick). - -**Consequence:** Route A (recompile/port original source) is **NOT possible as-is** — the code that *is* -the game isn't here. Paths forward: (1) **get the missing `.cpp` from Nick** [gating — top priority]; -(2) run `btrel410.exe` by **emulating the VPX/IG board** in DOSBox (deep RE project; no source needed); -(3) **decompile `btrel410.exe` + reconstruct from headers** (Plan B, analyzed below); (4) rewrite game -logic from headers blind (worst). Do not start the shim/port build until the source situation is resolved. - -**`btrel410.exe` (UNWRAPPED — see `decomp/`):** it's a **PKZIP self-extractor** (`unzip` lists 324 files). -Inside is the real game binary **`BTL4OPT.EXE` (1.24 MB, clean uncompressed PE32, Phar Lap TNT `32rtm` -stub)** + `BTL4.RES`, `NETNUB.EXE`, TNT extender, IG microcode, full content. Extracted to `decomp/`. -`BTL4OPT.EXE` is a near-ideal decompile target: class names, `Class::Method` strings, and **source-file -paths in asserts** (`d:\tesla_bt\bt\mech.cpp`) are left in — confirming the missing modules (`mech`, -`mech2/4`, `mechmppr`, `mechweap`, `heat`, `powersub`, `btplayer`, `btl4app/gaug/mssn/rdr/vid`, …) ARE in -the binary. No COFF symbol table (naming comes from those strings). Mined: `decomp/src_files_in_binary.txt` -(78 files compiled in), `decomp/methods_in_binary.txt` (151 methods). - -**Decompilation — UNDERWAY (Plan B in progress).** JDK21 + **Ghidra 12.1.2 installed** under `tools/` -(portable). `tools/ghidra_scripts/ExportBTSource.java` run headless on `BTL4OPT.EXE` → **real decompiled -C recovered** to `decomp/recovered/` for heat/mech/mech2/mechmppr/mechweap/powersub/btplayer/btmssn/ -btl4app/gaug/mssn/rdr/vid (proven: heat.cpp HeatSink ctor w/ real constants+asserts). Current pass = -assert-anchored only (few funcs/file). Next: decompile ALL funcs + recover vtables → map to header classes -for COMPLETE modules → reconstruct to compilable C++ (header + RP analog + binary oracle) → WinTesla -`BT410_L4` → build/verify. This is mechanized now but the reconstruction volume (~40 files) is large. - -**PILOT DONE — `heat` reconstructed** (`decomp/reconstructed/heat.{hpp,cpp}`). From the 46-func cluster: -~17 confidently reconstructed (the real heat logic — ctor, ResetToInitialState, heat-flow/coolant -formulas, status/message handlers, resource parse), 5 correctly identified as inherited base methods -(excluded), ~24 template/glue folded into members; Condenser bodies fell just outside the captured -window (needs a re-slice). Recovered the full field layout (offset→named member), HeatSink vtable slot -map, and resource struct. Output is faithful, named, VWE-styled C++ — a strong draft a human finishes -(won't compile clean yet). **Honest effort: ~0.5–1.5 hr/module → ~30–50 agent-hours + human review for -~40 modules, with a long tail** (modules with no surviving `.TCP`/`.HPP` fragment, or split across decomp -windows, cost more). Process upgrades before scaling: per-class function lists (not address clusters), -complete vtable rows, and recover `.data` float constants (the biggest "couldn't" bucket = tuning values). - -**Decompilation feasibility (why viable):** the **headers are an answer key** (exact -class layouts, vtable order, signatures) and the **MUNGA engine source (~183 cpp) anchors ~half the -binary** by pattern-matching — and scope is bounded to only the ~15 missing modules. Stages: unwrap -(PKLITE→TNT image→strip zip) → load Ghidra/IDA → compile engine to auto-identify known funcs → decompile -missing modules method-by-method (vtable-located, header-typed) → rewrite into compilable C++. Result = -behavior-equivalent **reconstruction** (binary usable as an oracle to verify), NOT original source; -~months of expert RE. Pilot before committing: unwrap, prove anchoring, decompile one module (e.g. `heat`). - -## 5b. ⭐ The Windows port (WinTesla) changes the plan - -The `Elsewhen RP411` archive is a **working Windows port of the engine** (`elsewhen_extracted/.../trunk/`, -`WinTesla.sln`). It means the hardest infrastructure is **already solved** and need not be rebuilt: -- **Renderer bypass: DONE** (`MUNGA_L4/L4D3D.cpp` + `DXUtils` → Direct3D; libDPL/IG board removed — no - `libdpl.lib` in the tree). Our from-scratch `port/` viewer is now a *reference/sandbox*, not the path. -- **Audio: DONE** (OpenAL + libsndfile replacing HMI SOS). -- **L4 HAL on Windows + VS build system: DONE**; **Red Planet game logic: COMPLETE & buildable**. - -So **Red Planet is (at/near) playable today** — build `WinTesla.sln` (VS2008 → upgrade to current VS). -And for **BattleTech**: everything below the game layer (engine, renderer, audio, HAL, build) already -exists here and is shared. The ONLY thing BT still needs is its **game-logic source** (mech/subsystems/ -HUD/app), which is confirmed gone (§5a) → must be **reconstructed**, now with THREE strong anchors: -(1) all BT headers, (2) the working WinTesla engine to compile against, (3) **Red Planet's parallel -implementations as a live reference** (`VTV`≈mech, `VTVMPPR`≈mechmppr, `WEAPSYS`≈mech weapons, `RP_L4` -app≈`BT_L4` app) — plus `btrel410.exe` as a behavioral oracle. - -## 6. Port codebase (`port/`) — the standalone viewer sandbox (NOT in bt411) - -> ⚠️ **This section documents the standalone D3D9 viewer, which lives in the ORIGINAL workspace -> (`nick-games/port/`), NOT in bt411.** It is a dev sandbox that proved the renderer bypass + asset -> pipeline; the game itself does not use it. Its `image.{cpp,h}` codec IS in bt411 (as -> `engine/MUNGA_L4/image.{cpp,h}` — the engine loader shares it). If the viewer is ever wanted here, -> it's a small self-contained add. The rest of this section is retained as reference. - -Standalone **D3D9 model/animation viewer** — proves the renderer bypass + asset pipeline. **Not the game.** - -- Build: `cmake -S port -B port/build -G "Visual Studio 16 2019" -A x64 -DCMAKE_GENERATOR_INSTANCE="C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools"` then `cmake --build port/build --config Debug`. -- **Toolchain gotchas:** the VS2019 *Community* install on this box is broken — must use the *BuildTools* - instance via `CMAKE_GENERATOR_INSTANCE`. Don't name a header `math.h` (shadows ``; ours is `vmath.h`). -- Files: `vmath.h` (math), `bgf.{h,cpp}` (model + material/texture resolution), `image.{h,cpp}` (tga/vtx/bsl - decode), `scene.{h,cpp}` (retained scene graph: Object/DCS/Instance), `skeleton.{h,cpp}` (.skl→DCS), - `anim.{h,cpp}` (.ani player), `main.cpp` (Win32+D3D9), `tools/bgftest.cpp` (headless loader test). -- Run: `btviewer.exe [animation.ani]`. Keys: `W` wire · `C` cull · `T` textures · - `P` pose · `-`/`+` walk speed · `Space` pause · `Esc`. -- **Working today:** load any mech (geometry + real BMF/BSL skin), assemble from its `.SKL` skeleton, - play real `.ANI` animations, walk across a ground grid with procedural root motion. Verified on Avatar - + Raptor. Walk speed currently tuned by hand (being derived from source — see §7). - -## 7. Open technical notes - -- **Walk speed / locomotion (SOLVED):** locomotion is **animation-driven**, NOT physics-derived. Each - `.ANI` has a `[RootTranslation]` section (per keyframe `x y z`) where **`.z` = forward root SPEED in - world units/sec** (meters/s); `[KeyJointPos].y` = vertical bob; `y` ≈ constant root height. The engine - (`RP\MUNGA\JMOVER.CPP` `AnimationInstance::Animate` ~1497-1723) integrates `movement += Δt·rootTrans.z` - and moves the body that far — so feet plant by construction (no separate stride constant; scale k=1). - Idle anims have z=0 (no travel); walks carry their speed (e.g. RAPWWR z≈20). The viewer reads this: - `g_walkPos += dt * forwardSpeedAt(anim, animTime)`. The `.MOD` physics (MoverMass/drag/MaxAcceleration) - governed thrust *vehicles* (RP hovercraft `VTVPWR`, camera ship) + collisions, NOT walk gait. NOTE: the - BT walking-mech mover/mapper code is NOT in the source dump (BT here is weapons-only; the mech consumer - of Animate() is absent) — but the per-clip speed is fully recoverable from the `.ANI` data above. - **✅ P3 LOCOMOTION CUTOVER — STEP 1 DONE (`docs/P3_LOCOMOTION.md`).** btl4's live drive (mech4.cpp - `PerformAndWatch`) is now ANIMATION-DRIVEN: the engine `AnimationInstance::Animate(dt,True)` return `adv` - (the clip's per-frame `[RootTranslation]` distance) drives the forward step (`localOrigin.linearPosition - += facing * adv`), replacing the hand-tuned `kDriveMaxSpeed` slide. Verified: mech walks at the clip's - AUTHENTIC ~60 u/s (Blackhawk run; the guess was 30), forward, feet plant (travel==stride), combat+damage - un-regressed. NOTE: the reconstructed two-channel gait (mech2.cpp `AdvanceLeg/BodyAnimation`, `IntegrateMotion`) - is bracketed by no-op stubs (`ReconSeq` controllers → Advance()=0; `Matrix34`=`ReconMatrix` no-op), so the - full gait (STEP 7) needs those backed by real engine `AnimationInstance`/affine-matrix first. Plan + - 7 steps in `docs/P3_LOCOMOTION.md`; the smallest milestone (STEPs 1-2, real per-clip speeds on the - proven engine path) is the near-zero-risk win. - **✅ P3 STEP 7 CUTOVER — the real gait controller drives locomotion LIVE (gated `BT_GAIT_CUTOVER=1`; verified).** - The BT-specific `SequenceController` (`decomp/reconstructed/seqctl.cpp`, reconstructed from binary - `SelectSequence@004277a8` + `Advance@0042790c`) is now reconstructed, built, and wired: the Mech ctor - `Init`s `legAnimation`/`bodyAnimation`, and `PerformAndWatch` (mech4.cpp, cutover branch) drives the - forward step from `bodyAnimation.Advance(dt,1)` (the real controller: parses the clip, animates the - skeleton joints via the engine `Joint` API `SetHinge/SetRotation/SetTranslation`, returns the - root-translation forward step) instead of the STEP-1/2 free-standing `AnimationInstance`. Verified live - under cdb: `[gait] SequenceController -> blhrrl id=904 (run)`, mech walks (adv≈1.07, matching baseline), - 0 crashes; combat is byte-for-byte un-regressed vs a same-harness baseline run (both fire/damage - identically). **KEY BUG FIXED:** clip fetch was `SearchList(clip_id,16)` (which treats its arg as a - resource LIST → walks the clip bytes as garbage IDs → av); the clip_id from `ResolveAnimationClip` is a - DIRECT resource ID → use `FindResourceDescription(clip_id)`+`Lock`, mirroring the engine's own - `AnimationInstance::SetAnimation` (JMOVER.cpp:1406) EXACTLY. **DECOMP FACT — CORRECTED (re-verified vs `Advance@0042790c` - raw, part_003.c:6878):** the earlier "lumpy distance" claim here was WRONG — the binary's partial-frame - branch DOES accrue `(t-currentTime)*stride[currentFrame]` (distance is CONTINUOUS, matching seqctl.cpp:298), - and `IntegrateMotion` (@004ab1c8:14997) integrates `localVelocity.z = -adv/dt` directly with NO smoothing. - **FOOT-PLANT FACT (raw FUN_004ab430:15076):** the binary advances the BODY controller with `move_joints=1` — - the DISPLAYED joints and the travel distance come from the SAME Advance (same clip/phase), so feet plant by - construction; the leg channel (also hardcoded `move_joints=1`, FUN_004a5028:12006) runs FIRST and its joint - writes are overwritten. Port matched in commit 01312e8 (body channel `Advance(dt,1)`); the same commit also - added a ground-velocity smoothing (averaging adv/dt across keyframe spans) that is NOT in the binary — a - deliberate deviation; if residual micro foot-slip is ever chased, that smoothing is the first suspect. - Cutover is kept GATED (not default) pending two flagged fidelity refinements: (a) end-of-clip uses - `carryover*stride[last]` in place of the binary's recursive finished-callback `@0x48` (the source of the - occasional adv spike at clip-loop boundaries); (b) partial-frame rotation uses component-LERP vs the - binary's slerp (`FUN_00409390`). The deeper full-`IntegrateMotion` velocity path (leg+body channels + - world-step transform + `DeadReckonPose`, smoothing the lumpy step) is the remaining STEP-7 work. - **✅ STEP 7 — the AUTHENTIC world-step (velocity model) now drives the mech LIVE (under `BT_GAIT_CUTOVER`).** - The cutover no longer slides the position directly (`localOrigin += facing*adv`); it runs the real - `IntegrateMotion` motion-tail (`@004ab1c8`): `localVelocity = {0,0,-adv/dt}` (forward = local −Z) → - rotate into world by the body orientation via the now-**backed `FUN_00408744`** (part_000.c:8331, a - matrix×vector; backed with the engine `AffineMatrix::GetFromAxis` column basis — the same convention the - drive facing uses, so it is sign-consistent) → `localOrigin.linearPosition += worldVelocity*dt`. Result - == `facing*adv`, but through the authentic velocity→rotate→integrate machinery (runs `FUN_00408744` live). - **KEY DECOMP FACT (verified):** the 1995 motion transform `{ Point3D @0x260; Quaternion @0x26c }` **IS the - engine `localOrigin`** — `Origin = { Point3D linearPosition; Quaternion angularPosition }` (ORIGIN.h:15), - and raw `FUN_0040ab44` builds the matrix from BOTH halves (rotation from 0x26c, translation from 0x260). - So `IntegrateMotion`'s motion state maps DIRECTLY onto `localOrigin` (NOT the parallel `motionDelta`/ - `worldPose` members the earlier draft declared — those were placeholders). Also fixed a latent - `IntegrateMotion` bug (unexercised while the fn was dead): it wrote only the separate `spinRate`@0x508 and - left `angularAccum[2]` (the velocity.z the world-step reads) stale → now sets it. Verified live under cdb: - walk-only (`BT_GAIT_CUTOVER=1`) walks straight forward at the authentic ~60 u/s, **0 crashes**; combat - fire/damage identical to STEP-1/2. **Remaining full-`IntegrateMotion` work:** the `AdvanceBodyAnimation` - gait STATE MACHINE (stand→walk→run transitions) + `LoadLocomotionClips` (real, mech3.cpp:326 — populates - `namedClip[]`/gait-speed caps via `legAnimation.SelectSequence`) in place of the inline clip-select; - orientation INTEGRATION (turn via angular rate, not the bring-up `gDriveHeading`); and velocity STORAGE - for MP dead-reckon. ⚠ **PRE-EXISTING crash observed (NOT locomotion):** a combat run walked into a - building's interest range and faulted in the ENGINE BGF loader (`LoadBgfFile("bld08.bgf")` → - `Builder::~Builder` `vector` teardown → bad `operator delete`, bgfload.cpp:491) — a heap - corruption in model loading, hit position-dependently (the STEP-1/2 run with a different spawn fired 600+ - shots clean). My world-step does zero heap ops + walk-only is clean, so it is not the cause; likely the - known enemy-spawn over-`sizeof` or a fragile BGF Builder for certain building assets. Track separately. - **✅ STEP 7 steps 1-2 — `LoadLocomotionClips` + the real gait STATE MACHINE drive the walk LIVE (`BT_GAIT_SM`).** - (1) **`LoadLocomotionClips`** (mech3.cpp:326, real) is now called in the Mech ctor (gated `BT_GAIT_CUTOVER`) - and completes cleanly for the Blackhawk — it resolves the full gait clip set (`swr/wwr/wwl/rrr/rrl/...`) + - measures the speed caps via `legAnimation.SelectSequence`+`keyframeData` (verified real: `walkStride=22.02`, - `standSpeed=6.83`). **KEY RECONCILIATION:** `namedClip[]`@0x5e0 and `animationClips[]`@0x5cc were declared as - SEPARATE arrays but in the binary are ONE (`namedClip[i]==animationClips[i+5]`, 0x5e0==0x5cc+0x14) — so - `LoadLocomotionClips`' writes weren't visible to `SetBodyAnimation`/`MeasureClipStride`. Fixed: `namedClip` - is now a pointer aliased to `&animationClips[5]` in the ctor (same storage). (2) **`AdvanceBodyAnimation` - state machine** (mech2.cpp, real) now drives the walk under `BT_GAIT_SM` (case 6/7: slew `bodyCycleSpeed` - toward `bodyTargetSpeed` with the loaded caps → `bodyAnimation.Advance` the `wwr` clip → world-step). Fixed - **`bodyAnimationState`@0x728 == `bodyStateAlarm.level`** (also declared-separate; re-synced from the alarm each - frame so `SetBodyAnimation`'s level change reaches the switch). Verified live: mech walks forward (z 1600→−975), - `adv≈0.35-0.42` at the authentic walk cadence (correctly slower than run's 1.07), clip loops through its 15 - keyframes, 0 crashes. **Bring-up substitute (marked):** `SetBodyAnimation` passes a non-null loop sentinel - `(void*)1` where the binary passes `PTR_LAB_0050d6fc` — the real body finished-callback (gait-state - TRANSITION stand→walk→run + leg alternation, @0x48), NOT yet reconstructed; without it the cycle clip would - stick at its last keyframe (`adv=0`). - **✅ STEP 7 — the REAL gait TRANSITION CALLBACK reconstructed + wired (walk/run leg alternation + walk→run).** - Replaced the `(void*)1` sentinel with the real `FUN_004a6d8c` = `Mech::BodyClipFinished`. RESOLVED the `.data` - fn pointer by PE-parsing (`0x50d6fc`→body cb `0x4a6d8c`; `0x50d6f0`→leg cb `0x4a6928`) — not in the decomp, so - DISASSEMBLED with capstone + decoded the jump table (`@0x4a6e0a`; 33 states→10 handlers). Reconstructed - byte-for-byte: at end-of-clip it dispatches on `bodyAnimationState`, compares `bodyTargetSpeed` to the loaded - caps (`standSpeed`/`walkStrideLength`/`reverseSpeedMax`), picks the next state, re-arms it (`SetBodyAnimation`, - which re-binds the clip WITH this same callback), and recursively advances the carryover (shared tail - `0x4a6e66`/`0x4a6ed1`/`0x4a7001`). Walk alternates 6↔7, run 12↔13, stop→8/9, walk→run 6→11→12. **Plumbing:** - `SequenceController::Advance` now CALLS `cbCode` as `Scalar(*)(Mech*,unsigned,Scalar,int)` at end-of-clip - (matching the binary's `**(code**)(this+0x48)`), folding the returned distance; inline cutover uses - `Mech::LoopBodyClip`. **Verified live:** walk 6↔7 (adv≈0.42), run 12↔13 (adv≈0.5-0.85), walk→run fires, pos - advances, 0 crashes; inline cutover + combat un-regressed. **Bring-up gap fixed:** `reverseSpeedMax2`@0x7a0 - (run bodyCycleSpeed clamp, AdvanceBodyAnimation case 12/13) is unset by LoadLocomotionClips → read `0xCDCDCDCD` - → run exploded (`adv=-7e6`); set to `reverseStrideLength` (the case-12 divisor → ratio≈1). **DECOMP TECHNIQUE:** - resolve a Ghidra `PTR_LAB_xxxx` .data fn ptr by PE-parsing the DWORD at its VA, then capstone-disassemble the - target (`/tmp/readptr.py`+`disas.py`) — for any callback/vtable slot the assert-anchored decomp didn't export. - **Remaining:** the LEG callback `FUN_004a6928` (leg alternation, state@0x3b0 + motion source@0x128); airborne - `FUN_004a6344`; gimp handlers (states 16-19, targets `0x70b2`/`0x7161` undecoded → fall back to stand); gait - SELECTION from real throttle (`MechControlsMapper` still bypassed; SM forces walk-6 then the callback transitions). -- **✅ MoveAndCollide — the mech now COLLIDES with world solids (engine Mover pipeline; gated `BT_COLLISION`).** - The bring-up drive wrote `localOrigin.linearPosition` directly with no collision, so the mech phased through - terrain/objects + floated. KEY FINDING: the 2007 RP411 engine models **terrain AS `BoxedSolid` collision - volumes** (`TERRAIN.h`: `Terrain` = an `Entity` with `collisionVolumes`), so terrain-follow AND object - collision go through ONE path — the engine `Mover` collision pipeline (`MOVER.cpp`), NOT the 1995 heightfield - query (`FUN_0040e5f0`, which the dead `Simulate` used). Wired into `PerformAndWatch` (mech4.cpp) after the - drive's world-step: snapshot the pre-move pos → set `worldLinearVelocity` from the move delta → - `MoveCollisionVolume()` (reposition the volume to `localToWorld`) → `GetCurrentCollisions()` (gather - overlapping solids) → `ProcessCollisionList(cols, dt, oldPos, &damage)` (the engine resolves each penetration - via `ProcessCollision`→`StaticBounce`, WRITING `localOrigin.linearPosition` to push the mech out; - MOVER.cpp:1162/1200/1217). All methods are public on `Mover`; guarded on `GetCollisionVolumeCount()>0` so it - is a no-op for volume-less mechs. **Verified live:** in the cavern the mech walks flat (Y=−0.35), hits a - building solid at z≈150 and is pushed out once (x+7, y+2.9) over ~6 frames then continues stably (Y holds, - no drift/fly-away); combat un-regressed (structure→0.533, 0 crashes, 109 collide events while fighting). - **✅ GRAVITY / terrain-settle — DONE (same `BT_COLLISION` block; tunable `BT_GRAVITY`, default 20 u/s).** - Collision alone only pushes OUT of solids (no downward force → a floating mech never came down). Added a - per-frame downward Y (`localOrigin.y -= kGravityRate*dt`) BEFORE the collision resolve, so gravity presses - the mech down + the collision holds it at the terrain surface (equilibrium ON the ground). SAFETY NET: a - FLOOR CLAMP at the spawn/ground Y (captured on first frame) — on a map with NO terrain solid the mech rests - at ground level instead of falling forever; on real terrain (solids) the collision catches it above the floor - so it follows hills/valleys. **Verified live:** grass — mech settles at Y=0 (clamp; flat grass has no ground - solid) and climbs a terrain feature to Y=4.68; cavern — settles at the floor Y=−0.35, stable while walking, - combat un-regressed (structure→0.933), 0 crashes. So the mech now STANDS + walks on the ground + climbs - terrain + collides with objects. **Remaining refinements:** (b) wall-blocking vs climb — the engine resolves - along the surface normal, so a wall-top contact pushes UP (climb) rather than blocking forward; may need - game-tuning. (c) collision damage is computed by `ProcessCollisionList` but not applied here (bring-up). Kept - GATED (`BT_COLLISION`) pending (b) + confirmation on more maps. -- **✅✅ THE AUTHENTIC 1995 GROUND MODEL — DECODED, IMPLEMENTED, DEFAULT-ON (task #15 CLOSED for ground; - `BT_GROUND_REAL`, =0 falls back to the gravity+push baseline above).** Product of the 10-agent - `ground-model-decode` workflow (6 mappers over binary/1995-source/2007-engine/postmortem/content + synthesis - + 3 adversarial verifiers, all SOUND). **THE MODEL (binary FUN_004a9b5c = the MASTER per-frame performance, - PTR_LAB_0050c0f4, decoded from raw asm — the "undefined 5.6KB gap" in mech4's window; FUN_004ab430 is the - REPLICANT interior only, NOT "Simulate"):** there is **NO GRAVITY anywhere in the mech** (accelerations - zeroed every frame @4a9cb6). Vertical position comes exclusively from an ABSOLUTE per-frame ground snap: - (1) ctor lifts `collisionTemplate->minY` by 5% of volume X-width (BLH: 2.0+0.37969=2.37969 — the probe - height AND the wall-vs-floor separation); (2) per frame: `MoveCollisionVolume()` (rebuilds localToWorld, - places volume, re-finds `containedByNode` in the zone's **BoundingBoxTree** — BOXTREE.cpp, identical - 1995→2007, NOT a heightfield); (3) probe `q = origin + (0, lift, 0)` → - `BoundingBoxTreeNode::FindBoundingBoxUnder(q, &h)` (h = distance down to the highest solid top; −1 = miss); - (4) snap gate `h > 1e-4` (@0x4ab16c): `origin.y -= (h − lift)` ⇒ y = surfaceY EXACTLY (up-slope bounded by - the lift window = implicit step allowance; walk-offs drop instantly; **MISS ⇒ NOTHING** — y holds, no - runaway possible); (5) `GetCurrentCollisions` → `ProcessCollisionList` → per-contact **`Mech::ProcessCollision` - override (FUN_004abb40, vtbl+0x3c** — the fn the old recon misread as "ResolveWeaponImpact"): BoxedSolid - resolver → StaticBounce → owner classification — Mover owner = separating-gate + (deferred) mech damage; - CulturalIcon owner without `StoppingCollisionVolume` (flags&0x8000) = **crushable, damage=0.00123f sentinel**; - plain UnscalableTerrain (cliffs/canyon walls/most buildings) matches neither branch; (6) response policy: - sentinel = move STANDS (restore post-snap saves); damage>0 = **FULL FRAME REJECTION** (velocity zeroed, - origin=start-of-frame — walls block by rejection, never slide/climb); impactVel²>40 = crash-anim trigger - (SetLegAnimation(0x20) — deferred, logged). Implementation: `Mech::AuthenticGroundAndCollide` (mech4.cpp) - called for the player (post-drive) AND every non-player master; ctor half in mech.cpp; shared gate - `GroundReal()` (mechrecon.hpp). **KEY FIXES/FACTS:** (a) FUN_0040e36c/FUN_0040e5f0 are - `BoundingBoxTreeNode::FindSmallestNodeContainingColumn`/`FindBoundingBoxUnder` (BOXTREE.CPP:503/867), ALIVE - in the linked 2007 engine — the "Terrain::CellAt/HeightAt heightfield" labels were wrong (the cause of both - reverted attempts: v2 assigned the DISTANCE h as an absolute height → the y=13301 ladder). (b) binary - 0x2e8/0x2ec/0x2f0/0x2f8 = ENGINE Mover `collisionVolume`/`collisionTemplate`/`containedByNode`/ - `lastCollisionList` (NOT game-layer "physicsBody/groundRef/groundCell"). (c) grass DOES have a ground slab - (gr100_cv Block Y[−50,0]) — the old "flat grass has no ground solid" note was wrong. (d) **a master mech - needs a CollisionAssistant** (GetCurrentCollisions iterates it UNCHECKED, MOVER.cpp:894): only the viewpoint - mech gets one (MakeViewpointEntity); the BT_SPAWN_ENEMY dummy now `StartCollisionAssistant()`s at spawn + - the block has a defensive assistant guard (in the real game exactly ONE master exists per pod, so the - invariant held by construction). (e) BT SOLIDS ship ZERO tiles (only Block/YCyl/Cone/Ramp/Wedge, all - FindDistanceBelowBounded clamped ≥0) → h is never negative in BT. **VERIFIED (render-side + telemetry):** - grass census lift=0.379688/minY=2.37969 exact, y=0 EXACT on the slab; arena 65s anti-ladder soak y pinned 0, - wall BLOCKs hold (no creep); dbase two full hill climbs (y→24/y→31) + exact floor return −0.453125; - screenshots standing/climbing; combat DESTROYED after the same ~8 hits; baseline (=0) byte-identical; 7-min - BT_HEAPCHECK soak clean. **EXPECTED-AUTHENTIC (do not "fix"):** sticky-ledge frame rejections walking off - 2.4-5.9u terraces; crushable props pass through WITH crunch; canyon backdrops (rav pwin*)/arena detail have - no solids = pass-through; over void y HOLDS (no fall). - **✅ CRASH/KNOCKDOWN LIVE (commit 6452ab7):** wall impacts above iv2>40 (~6.3 u/s) bind the `bmp` - bump/stagger clip — `SetLegAnimation(0x20)` + `RequestActionFlags(1)` + `(0x20)` (binary @4aaae2; - FUN_004a4c54 == RequestActionFlags, NOT "ForceUpdate"). ALIASING FIX (namedClip bug class): `gimpBaseClip` - @0x64c IS `animationClips[0x20]` (0x5cc+32*4) — bmp now loads into slot 32; the clip table is zeroed in - the ctor. Recovery = LegClipFinished case 32 → SetLevel(0) (already reconstructed). BRING-UP GUARD - (marked): no rebind while legState==0x20 — the bmp clip's ~6.5 u/s root motion re-contacts the wall and - the `actionRequestFlags` consumers (likely the stagger drive-suppressor) are unreconstructed; without the - guard the clip restarted every frame. Verified: knockdown → play out → stand → walk → re-knockdown at ~3s - cadence at a butte wall; mid-stagger screenshot ON the surface; combat clean. **Why leg-burial appeared at - walls before this:** frame rejection freezes pre-snap Y while grinding (the snap is undone every rejected - frame) — the knockdown is what prevented sustained grinding in the original. Related content facts - (measured, tools/mapscan.py + BGF triangle sampling): dune mounds visual≈solid (median 0.2u, p90 1.1u); - buttes deviate ±5u typical / ±15-25u at cliff faces — authentic 1995 coarseness. The transform chain - (quat→matrix identical formulas both paths, row-vector convention, ToD3DMatrix raw copy) was - audit-verified — no visual-vs-solid placement bug exists. - **✅ KNOCKDOWN RE-TRIGGER STUTTER — FIXED (user: "leg animation glitchy/stuttery when I run into an obstacle, - fixes if I fully stop"; reproduced + spec-confirmed).** Running INTO a blocking solid (a wall, a steep terrain - flank, OR the spawned dummy MECH — not slope-specific) at speed made the leg animation flip-flop between the - stagger clip (legState 0x20) and walk. Repro (drive into the dummy on grass, `BT_GROUND_LOG`): the FIRST hit - at run speed is a genuine knockdown, but then the mech is pressed against the obstacle with throttle held, so - every recovery frame it steps ~6.9 u/s back into it and the `iv2>40` (=6.32 u/s) knockdown RE-FIRES → rapid - freeze↔walk = the stutter (BLOCK=319 / **CRASH=107** in 22 s; leg flip-flopping 5↔32). Full stop clears it - (velocity→0→iv2<40). **The ground-model spec (scratchpad `gd_spec.md`, the hand-decoded master perf - `FUN_004a9b5c` @4aaae2) shows the binary's mitigation: `localVelocity = {0,0,−adv/dt}` is ZEROED while crashed - (@4aa3d3), so a knocked-down mech can't keep advancing into the obstacle** — our reconstruction re-derived - localVelocity from the position delta and never zeroed it, and re-armed the knockdown every fresh block. TWO - fixes (mech4.cpp): (1) **crash motion suppression** — while `legState==0x20`, zero `worldLinearVelocity` + - `localVelocity.linearMotion` (spec-faithful; no mid-stagger creep, no re-detect during the clip); (2) - **contact-hysteresis knockdown gate** — the knockdown fires only on a FRESH strike, where "fresh" = the mech - has been OUT of blocking contact for `kBlockHysteresis` (0.4 s). A 1-frame edge (`!gWasBlocked`) was tried - FIRST and FAILED for the user: a GLANCING / sliding contact alternates blocked↔not-blocked (the frame - rejection pushes the mech out each blocked frame), which flickers a 1-frame edge back to "fresh" and - re-knocks-down — that intermittent case is exactly what a head-on force-throttle repro never hits (it shows a - clean 1 bind, hiding the bug). The 0.4 s `gBlockCooldown` window bridges those gaps: any sustained OR - intermittent press just BLOCKS (runs/walks in place), only a real separate-then-re-approach re-arms it. - `gBlockCooldown` is a single global gated to the VIEWPOINT mech (the stationary dummy + MP masters also call - `AuthenticGroundAndCollide`, so they keep the original per-fresh-block behavior and never touch the shared - state). **Verified across cases:** dune-beeline **21→1**, head-on dummy **→1**, terrain-crossing (BLOCK=497) - **→1**; still blocks (no phase-through); combat un-regressed (TARGET DESTROYED, 0 crashes / 0 heap); normal - walking unaffected (suppression only triggers on legState 0x20). ⚠ DIAGNOSTIC LESSON: a head-on repro gives - 1 bind even with the brittle edge gate — the bug only shows on INTERMITTENT contact (glancing / sliding / - rough-terrain), so reproduce with an angled or terrain-crossing approach, not a straight ram. NOTE: holding - full-throttle into a wall still yields ONE knockdown then a clean block — a real player releasing throttle - sees just the single stagger. - **✅ TWO-CHANNEL GAIT DESYNC — FIXED (the ACTUAL "legs stay glitchy after a bump until full stop" bug).** - The knockdown re-trigger work above reduced the crash spam but the user reported the legs stayed glitchy - AFTER moving away, only clearing on a full stop. ROOT CAUSE (measured, not guessed): the real-controls gait - runs TWO animation channels — the BODY channel draws the displayed legs (`AdvanceBodyAnimation(dt,1)`, runs - last, wins the joint writes) while the LEG channel drives the mech's TRAVEL (`localAdv = legAdv`). In normal - walking they run in EXACT lockstep (the `[sync]` log: `advSum == legSum` frame-for-frame). The knockdown - staggered ONLY the leg channel (`SetLegAnimation(0x20)`), freezing TRAVEL while the body channel kept - animating the DISPLAY -> the two clocks split PERMANENTLY (measured `advSum=229` vs `legSum=112`: the - displayed legs animate ~2× the real travel = foot-slip) and never reconverged until a full stop reset both to - Standing. **FIX (one line):** the knockdown now staggers BOTH channels — `SetBodyAnimation(0x20)` alongside - `SetLegAnimation(0x20)` — so display + travel freeze and recover from the SAME bmp clip IN PHASE - (`BodyClipFinished` case 32 recovers the body, mech2.cpp:308, mirroring the leg). **Verified:** `advSum == - legSum` stays EXACT through+after the bump (was 229 vs 112); mech still BLOCKS the obstacle (BLOCK=328, motion - stays on the leg channel so no tunnel-through); combat un-regressed (DESTROYED, 0 crashes). NOTE: the - now-correct behavior is a FULL STOP + stagger on hard contact (authentic: frame-rejection + the iv2>40 crash - anim); the old "run through / run in place" was the desync bug, not original behavior. - **✅✅ v4 — DESYNC CLASS KILLED FOR GOOD: travel now comes from the BODY channel (the SAME Advance that - writes the displayed joints) — BINARY-FAITHFUL (raw FUN_004ab430:15076 advances the body with move_joints=1 - and takes travel from it).** The stagger-both-channels fix (above) killed the KNOCKDOWN trigger, but the - desync CLASS resurfaced on the Mad Cat + the analog throttle lever: the lever sweeps the demand THROUGH the - walk/run boundaries, and each channel's end-of-clip callback samples the demand at a different instant — near - a threshold they pick DIFFERENT next states → permanent phase split → the foot-slip again. v4 sources - `localAdv = adv` (body) so display == travel BY CONSTRUCTION — no two-machine agreement needed, ever. The - earlier "tunnels through obstacles" objection to exactly this change is OBSOLETE: it predated - `SetBodyAnimation(0x20)` — with the knockdown staggering the body channel too, a hard impact freezes travel - correctly. RE-VERIFIED: head-on run-speed ram into the dummy = pinned at its face for seconds (BLOCK=801, - 1 knockdown, NO tunnel-through, then a normal grazing slide around the corner); combat DESTROYED on BOTH - BLH and Mad Cat, 0 crashes. The leg channel keeps running as the binary's local sim (its joint writes land - first and are overwritten by the body's). The full `actionRequestFlags`(1/0x20) drive-suppressor + - crashed(0x650) lifecycle remain unreconstructed (master-perf ASM gap); the edge gate is the faithful - stand-in until they land. - **✅ RESIDUAL-SINK ALIGNMENT AUDIT — quantified; verdict = authentic coarseness, NOT a bug or a mesh error - (dbase; scratchpad audit scripts).** After the CONN trilist fix, the user still noticed "sink in a bit" in - some spots and asked for a triangle-error audit. TWO sweeps run: (1) **GEOMETRY** — all 877 pod GEO BGFs / - 127,171 tris: **0** CONN chunks with n%3≠0 (fan-bug corpus-complete), **0** out-of-bounds indices; the only - 52 anomalies are **zero-area (repeated-index) no-op** tris, ALL in props/vehicles (AB01/ARBAN/BULLET/MSLBASE/ - TK1/WL*), NONE in terrain → no mesh defect causes sinking. (2) **VISUAL-vs-COLLISION deviation** — per dbase - terrain model, sampled each visual vertex's Y vs the analytic `.SLD` surface (Cone apex-up / YCyl / Block) at - its XZ; worst clip in the walkable band (Y<15): **mesas (buttea-e) 2–3.5u**, **dunes (dhillg1-3) 0–3.1u**, - **flat pad (calpb) 1.6u** (⇒ NO global snap offset — the probe/lift is correct), **buildings (bld06/bld08) - 12–14u = the outliers**. Butte "overall" 23u is on the VERTICAL CLIFF FACES (unwalkable, the stepped-cylinder - cliff approximation — faithful). **KEY per-feature facts:** dbase terrain is placed by class-42 instances - (NOT the class-94 vehicle props); mesa collision = a STACK of YCyls + a base Cone whose footprint is ~5–22u - narrower than the visual skirt (the small skirt clip). **Building collision = separate COLUMN/WALL Blocks with - GAPS** (bld08=26 blocks, bld06=7), and the visual has floors/overhangs spanning the gaps → a mech walks into a - gap / under an overhang where there's no solid and its legs clip the visual (THIS is the user's "some things - are solid, some aren't" + the biggest residual sink). ALL authentic 1995 coarse collision — the original pods - clipped identically; the only "fix" (gap-filling collision on buildings) is an intentional content DEVIATION, - not a port correction → left as-is by default. - **DEFERRED (bound together, marked in code):** collisionTemporaryState zero @4aa741 + ProcessCollision state - tail; 0x4a4/0x4a8/0x4b4 caches (material/local normal/approach speed); the TakeDamageMessage dispatches - (zone=−1 dropped by the engine handler today + inflictor global DAT_0050b9ac unmapped); SetLegAnimation(0x20) - crash anim; gyro crunch feed; duck maxY swap wiring; replicant-path snap (MP — gate `h != −1`, part_012.c: - 15144-15167, the 0x4ab9d8 wrapper). Full spec + verdicts: scratchpad gd_spec.md/gd_verdicts.md (session); - the workflow reports are the durable citations. -- **Renderer dependency** is isolated to 3 HAL files in the original (`L4VIDEO/L4VIDPER/L4VIDRND` call `dpl_*`). - The full `dpl_*`/`dpl2d_*` API surface the game uses is cataloged (see chat history / shim contract). +| **T0** | Engine/header truth | A fact from the WinTesla MUNGA/L4 source or a BT header — the actual code we compile against. Authoritative. | +| **T1** | Decompiled + verified | Read from the binary pseudocode (`reference/decomp`) AND confirmed — disasm, `static_assert` offset lock, or cross-checked vs the RP analog. | +| **T2** | Reconstructed + runtime-verified | Implemented in `game/reconstructed/` and verified LIVE (combat un-regressed, gauge renders, log confirms the value). | +| **T3** | Best-effort / guarded | Reconstructed but not fully verified — a marked stand-in or a guarded raw-offset read. Runs, fidelity uncertain. | +| **T4** | Unconfirmed hypothesis | A claim about the binary's intent not yet decompiled/verified; a guess. | --- -## 8. Roadmap: viewer → playable +## Conventions (CRITICAL — full detail in `context/reconstruction-gotchas.md`) +- **Never raw-read a compiled object's offsets** (`*(T*)(obj+0xNN)`) — our layout ≠ the 1995 binary. + Use named members / accessors / a **bridge** fn in a complete-type TU. (databinding trap) +- **Never re-declare an engine-base field** — it shadows the base (reads `0xCDCDCDCD`) + mis-offsets. +- **`/FORCE` hides unresolved symbols** — an unresolved external → runtime AV near `__ImageBase`, not + a link error. Grep the link log when a build "succeeds" but crashes at a garbage call target. +- **Don't reconstruct dtor compiler-glue** — the trailing base/member-dtor calls run the chain twice. +- **`static_assert`-lock every reconstructed layout** against the binary's offsets/sizeof. +- **A `+0x128`-style owner offset is the subsystem ROSTER, not the segment table.** +- **RULE: no stand-ins** — read the decomp; a gap is an unfilled stub. +- **Keep the knowledge base current** — new durable findings → `context/*.md` (+ `docs/` for detail). -**REVISED after finding the WinTesla Windows port (§5b).** The original "build a shim + port the engine" -plan is largely **already done** in WinTesla. New plan: - -- **Track 1 — Red Planet build (integration spike measured the gap):** `WinTesla.sln` is **VS2005** - (Format 9 / vcproj v8), **225 source files**, and hard-depends on the **legacy DirectX SDK (Jun 2010)** — - it includes ``, ``, ``, ``, all REMOVED from the modern Windows - SDK (only `d3d9.h` remains). OpenAL32/libsndfile libs are in `trunk/lib`. So building RP is a real - **toolchain-modernization sub-project**, not a recompile: (1) ~~obtain legacy DXSDK~~ **DONE — installed at `C:\Program Files (x86)\Microsoft DirectX SDK - (June 2010)\` (Include + Lib\x86 present: d3dx9/dinput/dxerr/dxguid).** Note: the installer threw - **S1023** (harmless — caused by a newer VC++2010 redist v10.0.40219 already present blocking the bundled - 10.0.30319; the SDK files install before that failing step, so they survive — just dismiss the error), (2) drive the 225 files via CMake (relative `..\munga\` includes → compile each from - its project dir or add all dirs to the include path) or upgrade the vcprojs, (3) fix VS2005→VS2019 - C++/header drift across the tree, (4) link d3dx9/dinput8/dxguid/dxerr + OpenAL/libsndfile. Bounded but a - dedicated session; best done where the DXSDK can be installed normally. Validates the platform + yields a - playable RP. **BT integration = same build + drop in the reconstructed `decomp/reconstructed/` modules.** - - **✅ ENGINE COMPILES GREEN** (`C:\git\nick-games\enginebuild\`, CMake/MSVC Win32) — the shared - `Munga_L4` 193-file engine+HAL builds to `munga_engine.lib` (0 errors). Recipe (4298→0 errors): - (a) include path = **DXSDK Include + `enginebuild/shim` ONLY** — do NOT add MUNGA dirs (they shadow - `` → 3840 errors); rely on the code's relative/quoted includes. (b) defines: `WIN32 _WINDOWS - UNICODE _UNICODE NOMINMAX _CRT_SECURE_NO_DEPRECATE _CRT_SECURE_NO_WARNINGS _SILENCE_STDEXT_HASH_DEPRECATION_WARNINGS - _SILENCE_ALL_CXX17_DEPRECATION_WARNINGS`. (c) opts `/permissive /W0 /EHsc /bigobj /MP`. (d) source edits: - `MUNGA/CAMMGR.cpp` `std::ios.in`→`std::ios::in` (2007 typo); add `Time(const Time&)` copy ctor to - `MUNGA/time.h`. (e) `enginebuild/shim/atlbase.h`+`atlconv.h` = minimal ATL `USES_CONVERSION`/`W2A` shim. - Build Unicode (matches original — wide literals → `...W` APIs). -- **Track 2 — BattleTech:** reuse the WinTesla engine (shared MUNGA/MUNGA_L4/L4D3D/audio/build); add a - `BT410_L4` + BT game library by **reconstructing the missing BT `.cpp`** (mech, subsystems, HUD, app) - from: BT headers + RP's parallel code (§5b) + `btrel410.exe` oracle + pod content. This is the real - remaining work for BT — bounded to game logic, since the engine/renderer/HAL/audio are done. - **→ NOW LIVE (§10):** `btl4.exe` boots + renders + runs a drive→animate→target→fire→damage→destroy - single-player loop; the active work is reconstructing each subsystem's authentic behavior (damage zones - first, then weapon/heat/sensor subsystems — they are still `RECON_SUBSYS` stubs). See §10 for the - build/run/debug recipe, the no-stand-ins reconstruction method, and the per-subsystem decomp findings. - -Old roadmap below is superseded for Phases 1–3 (engine/renderer/HAL/audio = done in WinTesla); Phases -4–8 still apply to BT's game layer + pod bring-up. The from-scratch `libDPL` shim is **no longer needed** -(WinTesla replaced libDPL with L4D3D outright). - -(Historical plan — keep for reference of remaining game-layer/pod work:) - -- **Phase 0 — Foundation ✅ (done):** asset formats cracked + documented; D3D9 viewer renders skinned, - skeleton-assembled, animated mechs. Renderer bypass proven feasible. -- **Phase 1 — `libDPL` shim as a real API:** turn the viewer renderer into a drop-in lib exposing the - actual `dpl_*` (3D) + `dpl2d_*` (2D) C functions the game calls, backed by D3D9. *Linchpin: once the API - matches, the game's own rendering code compiles unchanged.* Risk: matching semantics (fog, LOD, materials, - `dpl_SectPixel` picking, 2D display lists, morph/damage/articulation). -- **Phase 2 — Compile the game code (modern toolchain):** get `MUNGA` + `BT` C++ building on MSVC/Clang — - modernize Borland-isms, replace `.asm`, stub the `L4` layer — linked against the shim. Goal: it *builds*. - Risk: pervasive non-standard 1995 C++; DOS/DPMI assumptions. -- **Phase 3 — L4 HAL on SDL/Win32:** replace `L4` video-out, input, timing, file I/O. Goal: the game first - *runs* — boots to menu / loads a mission. -- **Phase 4 — Cockpit (HUD/controls):** `dpl2d_` HUD + `.pcc`/`.gim` gauges + radar; map RIO cockpit - controls API → keyboard/gamepad; data-driven multi-surface output (dev windows vs pod's 7 monitors). -- **Phase 5 — Gameplay loop:** one single-player mission end to end — drive, fire, heat, damage, AI bots. - → **playable (single player)**. -- **Phase 6 — Audio:** HMI "SOS" sound engine → modern mixer (SDL_audio / similar). -- **Phase 7 — Multiplayer:** ⚠ **CORRECTED (scope-hardest workflow, verified):** NOT "reimplement over UDP" — - the WinTesla port ALREADY replaced the DOS `NETNUB` driver with a **3446-line WinSock2 TCP** reimplementation - (`MUNGA_L4/L4NET.CPP`, `SOCK_STREAM`/`ReliableMode`, zero `SOCK_DGRAM`) and the master/replicant distributed- - sim core (InterestManager/Entity/HostManager) is complete. So multiplayer = **integrate + smoke-test that - existing TCP stack**, gated on P3 (locomotion update writer) + P5 (entity create/destroy lifecycle) + subsystem - WAVEs. Unknown: whether that TCP path has ever run successfully even in RP (a send-size bug at `L4NET.CPP:1853`). - This is problem **P6** in `docs/HARD_PROBLEMS.md`. - **⭐ P6 SMOKE TEST PASSED — TWO INSTANCES SHARE A WORLD (cross-pod entity replication live).** The full - chain works on one box: console egg delivery → TCP mesh → synchronized mission start → bidirectional - entity replication (each instance renders its own mech + the peer's REPLICANT; `[BTrender] mech tree - built` ×2 on both sides; 0 crashes). HOW IT WORKS (workflow-mapped + verified): (1) `-net ` CLI arg - = networked mode; the pod listens on for a CONSOLE and boots `ConsoleOnly`; game mesh port = - +1. Solo (no `-net`) never touches WinSock. (2) The mission egg's `[pilots]` section IS the session - roster (`pilot=ip[:port]` + a per-address page with hostType=0/name/vehicle/dropzone...); each pod - self-identifies by local-IP+game-port match, CONNECTS to pilots listed before it, LISTENS for those after - — a deterministic full mesh; mission loads when all peers connect ("All connections completed!"). - (3) The CONSOLE (the operator station — ABSENT from every archive) delivers the egg as chunked - `ReceiveEggFileMessage` packets; **`tools/btconsole.py` is our console emulator** (wire spec in its - docstring, constants probed from the build via `BT_NET_PROBE=1`). ⚠ `NotationFile::ReadText` expects - NUL-SEPARATED LINES, not raw file text (NOTATION.cpp:1043 walks strchr(buf,0)+1) — the tool converts. - (4) One-box recipe: instance A `-egg MP.EGG -net 1501 BT_LOG=mp_a.log BT_AFFINITY=0x1`, instance B - `-net 1601 BT_LOG=mp_b.log BT_AFFINITY=0x2` (new env vars: per-instance log + CPU pin), then - `python tools/btconsole.py MP.EGG 127.0.0.1:1501 127.0.0.1:1601`. MP.EGG/MP1.EGG (2-pilot loopback / - 1-pilot networked) live in `btbuild/` (copy to the pod BT dir). Egg entries MUST carry explicit :port on - one box (else both instances self-identify as the same pilot). (5) **WIRE-FORMAT BUG CLASS FOUND+FIXED: - MakeMessages replicate RAW over TCP, so string payload must be INLINE** — the reconstructed - `BTPlayer::MakeMessage` carried `const char*` pointers (fine solo, garbage cross-pod: the first remote - BTPlayer replication crashed in `CString(roleName)`); fixed to inline `char[0x40]` at the binary offsets - (+0x50 teamName/+0x90 roleName, static_assert-locked, size 0xD0). CHECK EVERY MakeMessage for pointer - payloads before it replicates (swept: no others currently). (6) The rumored L4NET.CPP:1853 send-size bug - is DEAD CODE (`#if MESSAGE_BUFFERING` == enum True == 0 to the preprocessor); live send paths are correct. - **⭐⭐ MOVEMENT REPLICATION COMPLETE — A's mech WALKS ON B'S SCREEN (replicant tracks the master to - ~1.5u through dead-reckoning).** The full authentic mission-start ladder now runs: console egg → - CreatingMission → (event-queue quiet timeout, APP.cpp:667 `QUIET_TIME_OUT` 3s) → LoadingMission → - WaitingForLaunch → **console sends `RunMissionMessage` ×2** (the operator's LAUNCH button; - `btconsole.py` sends them after a 20s settle + 4s step; ID=5, clientID=4, probed) → LaunchingMission → - **RunningMission (state 5)**. SIX bugs/gaps found+fixed to get here (each verified live): - (1) Mech ctor now `SetDeadReckoner(&Mover::AcceleratedDeadReckoner)` (the VTV pattern); - (2) REPLICANT MOTION: `Mech::PerformAndWatch` runs `DeadReckon(dt)` (engine lerp toward - projectedOrigin) for `GetInstance()==ReplicantInstance` instead of the drive; - (3) MASTER EMISSION: the VTV error-threshold block (self-predict via own deadReckoner; `ForceUpdate()` - on posErr²>0.04 / quat-w<0.997 / 2s heartbeat) — without it masters NEVER emit update records; - (4) emission GATED on `RunningMission` — pre-launch update spam kept the PEER's CreatingMission - event-queue from ever going quiet (a cross-pod deadlock); - (5) the console must send the LAUNCH — **even solo had been running entirely on the - `IsPreRunnable()` player-escape in a pre-launch state, forever** (nothing posts RunMissionMessage - locally); solo still works that way (unchanged); - (6) **REPLICANT VALIDITY: an invalid entity defers EVERY message as an event forever** - (`Entity::Receive`/`Dispatch`, the '07 edit defers all) — replicants never get the master's - MakeReady/CheckLoad handshake (the interest layer is partial), so their network updates never applied - AND the deferred-event churn also blocked the quiet-timeout. FIX: `Mech::Make`/`BTPlayer::Make` - `SetValidFlag()` replicant instances at creation (same fix the spawned test dummy needed). - **Debug tooling added (env `BT_NET_TRACE`, permanent, in the ENGINE tree):** `[net-tx]`/`[net-rx]` - (L4NET send/receive), `[net-upd]` (update entity lookup), `[upd-repl]` (replicant scheduling), - `[ent-exec]` (Execute state ladder, 1-per-240 frames) — plus `[emit]`/`[repl]`/`[mech-exec]` in - mech4.cpp under `BT_REPL_LOG`. Update stream measured: ~60 Hz × 144-byte records per moving master. - **NEXT (P6 phase 4):** cross-pod combat (target a REPLICANT + damage routed to the owning master — - Entity::Dispatch on a replicant already reroutes to the master via SendMessage), interactive 2-window - driving, replicant gait animation (replicants don't animate joints yet — derive from replicated - velocity), then the pod-LAN config (real IPs, both `-net 1501`, bare-IP pilot entries). -- **Phase 8 — Pod bring-up (with Nick):** 7-monitor layout, old-driver compatibility, RIO cockpit I/O - wiring, on-pod testing. - -**✅ PLATFORM PROFILE scaffold DONE (`-platform pod|dev`, or `BT_PLATFORM` env, or `run\run.cmd [EGG] pod`; -default DEV).** The gate before Phase-4 HUD/MFD work: a boot switch selecting a runtime profile WITHOUT forking -the codepath — it just picks WHICH environment preset the existing video/gauge/input code reads (btl4main.cpp -env-defaults block). **KEY FINDING (a workflow map): the pod multi-surface path already EXISTS and is intact** — -`DPLRenderer::FindBestAdapterIndices` (multi-adapter/monitor selector; honors PRIMGAUGE/SECGAUGE/MFDGAUGE/ -SPANDISABLE), `SVGA16::BuildWindows` (L4VB16.cpp — a fullscreen D3D device + window per gauge/MFD surface, MFD -span = width×2), `L4GaugeRenderer` (gated on `getenv("L4GAUGE")`, btl4app.cpp:353), and **`content/SETENV.BAT` -IS the authentic pod env preset** (L4CONTROLS=RIO,KEYBOARD; L4GAUGE=640x480x16; L4PLASMA=com2). So the profile -is an env-preset selector: **DEV (default)** = keyboard + single 800×600 window (no L4GAUGE → MakeGaugeRenderer -NULL → single surface); **POD** = RIO cockpit input (→ keyboard fallback when no serial) + the multi-surface -gauges/MFDs. Verified: dev un-regressed (boots/ticks/combat DESTROYED); POD boots on a dev box single-window, -0 crashes; both the CLI (`-platform pod` — ParseCommandLine tolerates it) and env (`BT_PLATFORM=pod`) forms work. -⚠ The multi-surface is a POD-HARDWARE capability (each surface needs its OWN fullscreen device on one of the -pod's 2 video CARDS — monitor count alone is NOT a valid proxy), so `-platform pod` does NOT auto-enable L4GAUGE: -it stays bootable everywhere as single-window + pod input; on the real pod the gauges come from SETENV.BAT (or an -explicit L4GAUGE), running the UNTOUCHED FindBestAdapterIndices/SVGA16 path. Also fixed a real engine bug -(L4VB16.cpp:201): `SVGA16::BuildWindows` PostQuitMessage'd on `CreateDevice` failure but fell through to a null -`mDevice[j]` deref (segfault) → now `break`s (inert on pod where CreateDevice succeeds). **FOLLOW-UPS** (the deep -work this scaffold gates): (1) **MFD compositing on a dev box** — render the gauges/MFDs into the single window / -extra windows so POD mode is VISUALLY testable off-pod (the real "dev test of the pod HUD"); (2) **graceful -multi-surface fallback** — the gauge renderer returns NULL instead of crashing when its surfaces can't be built, -so a forced L4GAUGE on non-pod hardware degrades cleanly; (3) **pod main-view fullscreen/adapter tuning** -(btl4vid.cpp:863 still binds the main 3D view to 800×600 windowed on `::GetActiveWindow()` — fine for dev + the -pod's 800×600 main monitor, but pod fullscreen-on-a-chosen-adapter is a Phase-8 detail). The Phase-4 HUD/MFD -render work now targets the profile's LOGICAL surfaces, not a hardcoded single window. - -**✅✅ GAUGE DEV-COMPOSITE — the pod's SECONDARY/RADAR MFD renders LIVE on a dev box (opt-in `BT_DEV_GAUGES`; -full map + findings in `docs/GAUGE_COMPOSITE.md`).** The FOLLOW-UP (1) above is DONE for the secondary surface: -an inset in the bottom-left of the 800×600 window shows the authentic BT cockpit secondary MFD — **radar/ -tactical grid** (SCALE/SECTOR + crosshair), **SPEED/HEADING/MAGNETIC/PROP dials**, and the color-coded -**ARMOR DAMAGE mech schematic** (green intact / red damaged) — REAL content (`nzSec=27247` non-zero pixels). -The draw model is CPU-raster→texture-upload, DEVICE-INDEPENDENT: gauge widgets raster 16-bit R5G6B5 into ONE -shared `Video16BitBuffered::pixelBuffer`; `SVGA16::DrawDevInset` (L4VB16.cpp) palette-expands the secondary -plane into a MANAGED texture on the MAIN device + draws an `XYZRHW|TEX1` inset quad; `BTDrawGaugeInset(mDevice)` -is called from `DPLRenderer::ExecuteImplementation` as the last draw before `EndScene`. **THREE recon bugs -fixed to get real content (1 genuine fix + 2 gated dev accommodations):** (a) **⭐ `BTL4Application:: -MakeGaugeRenderer` SIGNATURE MISMATCH** — the reconstructed no-arg override only HID the 2007-engine's widened -3-arg virtual `MakeGaugeRenderer(int*,int*,int*)` (APP.cpp:382), so the engine built a BASE `L4GaugeRenderer` -whose ctor never calls `BuildConfigurationFile` → `gauge\l4gauge.cfg` was NEVER PARSED → `undefined label -'bhk1Init'` → no ports. FIXED to the 3-arg signature (args ignored; BT is fullscreen). Same bug class as the -`BTL4GaugeRenderer(false,NULL,NULL,NULL)` ctor fix — recon written to the ORIGINAL signature vs the WIDER 2007 -virtual. (The gauge label = `GetGameModel()`+`"Init"`=`bhk1Init`; CFG has `Bhk1Init` but the symbol lookup is -`stricmp`, so case is irrelevant — the table was just empty. `MechInit`, CFG:4390, is the shared macro that -builds the sec/overlay/Mfd/Heat/Comm PORTS via the base `configure` primitive.) (b) **parse HUNG on undefined -primitives** — `BTL4MethodDescription` (btl4grnd.cpp) is a STUB (BT gauge widget classes unreconstructed), so -an unknown primitive → `ReportParsingError`→`Fail()`→a MODAL dialog freezing mid-parse; gated skip -(GAUGREND.cpp) walks past the unknown primitive's params so labels register + base `configure` builds the -ports. (c) **gauge widgets AV on NULL data bindings** — `NumericDisplayScalar` value_pointer NULL → -`GaugeConnectionDirectOf` ctor deref (bind NULL→static zero, gauge.h); `RankAndScore::Execute` derefs -unreconstructed game state (`Gauge::GuardedExecute` SEH wrapper → `Disable(True)` on first fault). All gated -`BT_DEV_GAUGES`; **default DEV (no gauges) un-regressed (TARGET DESTROYED, 0 crashes) + pod path byte-unchanged** -(strict `Fail`, no guards). **REMAINING = the real gauge WIDGET reconstruction:** the `BTL4MethodDescription` -method table (BT gauge classes: `PlayerStatus` + ~16 recovered `methodDescription` entries) + the gauge→ -game-state DATA BINDINGS (why some gauges resolve NULL) — that is what the dev skip/guards stand in for, and -what the POD needs. Then Step 2 (RP↔BT MFD port-name reconcile → the 5 MFDs) + Step 3 (2-window layout). - -**✅✅ GAUGE MILESTONE C — ALL SIX instrument surfaces in a SEPARATE dev window (default under -`BT_DEV_GAUGES`; `BT_DEV_GAUGES_DOCK=1` = docked-in-main-window instead; full map: `docs/GAUGE_COMPOSITE.md`).** -Steps 2+3 done for the dev path. A 960×384 top-level window tiles all 6 pod screens with authentic content: -**Heat** (COOLANT/BALANCE/RES + condenser/temp-leak gauges), **Comm** (KILLS/DEATHS/SELECT TARGET), **Mfd1/2/3** -(DISPLAY/PROGRAM/NEAREST/TEMP-STATUS frames), and the color **radar** (SCALE grid + dials + ARMOR DAMAGE). Main -800×600 3D view is un-occluded; default DEV un-regressed (TARGET DESTROYED, 0 crashes). **Architecture (from the -`mfd-multisurface-map` workflow):** all 6 surfaces are bit-plane MASKS over the ONE shared `SVGA16`/`pixelBuffer` -(`sec`=palette low byte; `Heat`=0x4000 UL, `Mfd2`=0x0400 UC, `Comm`=0x8000 UR, `Mfd1`=0x0100 LL, `Mfd3`=0x1000 LR; -`Eng1-3` = engineering-mode alt planes, not extra monitors), so the compositor reaches the SVGA16 ONCE and -extracts each by mask. **No port-name reconcile needed on the dev path** — we fetch the BT port names directly via -`GetGraphicsPort` (the RP `auxUL2/…` names only matter to the POD's own `SVGA16::Update` demux — a deferred -pod-only fallback). `SVGA16::DrawDevSurface` has two kernels: palette-LUT (sec, == Update case 0) + mono bit-plane→ -tint (`(word&mask)?tint:0`, the reduced core of Update cases 1/2); `BTDrawGaugeSurfaces` iterates a 6-entry table. -The **separate window = one `CreateAdditionalSwapChain` on the EXISTING device** (no 2nd D3D device); rendered -AFTER the main `EndScene` (`BTGaugeWindowRenderAndPresent`, L4VIDEO): SetRenderTarget→**SetDepthStencilSurface(NULL)** -→Clear→BeginScene→6 tiles→EndScene→restore→`swap->Present`. **⚠ KEY BUG (cost a cycle): depth-stencil size -mismatch** — the main 800×600 depth stays bound when you SetRenderTarget to the 960×384 gauge backbuffer; a bound -depth SMALLER than the RT is INVALID → all draws silently fail (window shows the Clear color, no geometry). Fix: -unbind depth (Z is off anyway), restore after. Pod `BuildWindows`/`Update` path byte-unchanged. REMAINING = the -widget recon (above) + the pod MFD port-name fallback. - -**✅✅ GAUGE WIDGET RECONSTRUCTION — the cockpit instruments now read LIVE game state (attribute wave DONE; -full mechanism + technique in `docs/GAUGE_COMPOSITE.md` §Attribute wave).** After Milestone C put all 6 -surfaces on screen (drawing NULL), two waves made them read real data. (a) The **ColorMapper family + headingPointer** -(prior session): the palette-tint schematics (`cmHeat`/`cmCrit`/`cmArmor`/`colorMapperMultiArmor`) + the rotating -compass, each a reconstructed BT gauge class registered in `BTL4MethodDescription[]` (btl4grnd.cpp). (b) The -**attribute wave** (this session, 4 commits): gauges bind to game state by NAME via the engine attribute-pointer -system — the config's `Subsystem/Attribute` (e.g. `HeatSink/CurrentTemperature`) or bare `Attribute` (e.g. -`LinearSpeed`) resolves through `ParseAttribute`→`FindSubsystem`(stricmp on `GetName()`)→`GetAttributePointer`→ -the class's `activeAttributeIndex`. A class publishes attributes by declaring an `AttributeID` enum + a -`static AttributePointers[]` (`ATTRIBUTE_ENTRY`) + `GetAttributeIndex()` chained to its parent, and passing -`GetAttributeIndex()` to its `DefaultData`. **⚠ DENSE-TABLE HAZARD (systemic): `AttributeIndexSet::Build` leaves -gap slots uninitialized and `Find` strcmps EVERY slot → a published table MUST be a dense prefix from the parent's -`NextAttributeID`; fill gaps with a shared read-only pad member.** Done: **HeatSink table** (`CurrentTemperature` -→ Heat readout shows 77), **Mech table** (dense prefix 0x15..0x21, `LinearSpeed` → SPEED readout shows ~225), -**vertBar** (coolant bars; x87 pixel math recovered by disassembly), **segmentArcRatio** (speed arc; thin subclass -of engine `SegmentArc`), **oneOfSeveralPixInt** (the button-state lamps — reconstructed the whole OneOfSeveral -strip-selector family + PixMap8 blit path), and **map** (the RADAR / tactical display — the marquee; the MapDisplay -renderer was already reconstructed, so this was the registration glue + data: the Mech table extended to the full -0x15..0x38 with the radar Point3D*/Quaternion* attributes + a Sensor "Avionics"/RadarPercent table + a reconstructed -`MapDisplay::Make`/`methodDescription`; renders the FOV view wedge live). **Base primitives `numeric`/`numericSpeed`/ -`digitalClock`/`rankAndScore` are ENGINE (L4GAUGE.cpp) — only need the data; `vertBar`/`segmentArcRatio`/ -`oneOfSeveralPixInt`/`map`/`PlayerStatus`/`vehicleSubSystems` are BT-specific (reconstruct + register).** Technique -added: `tools/disas2.py ` (recover FPU math Ghidra drops — `FUN_004dcd94`=round, `FUN_004dcd00`=fabs) + -`tools/vtdump.py ` (find a vtable override the assert-anchored decomp didn't export). **⚠ SYSTEMIC PATTERN -(map): a newly-consumed reconstructed subsystem/gauge is often the FIRST reader of a stubbed helper — the map -exposed 3 (Sensor radarPercent negative from un-normalized heatEnergy; `ResolveOperatorEntity`→NULL; unguarded NULL -entity), each fixed with the established viewpoint-fallback/guard patterns.** **⭐ MAP CONTACTS RENDER (2026-07):** -the radar plots the enemy contact inside the FOV wedge. Two more fixes: (1) the gauge renderer's entity grid was -NEVER populated (the port dropped `ExecuteImplementation`'s InterestingEntity feed) → added `GaugeRenderer:: -RebuildEntityGrid()` filling `movingEntities` from the world DynamicMaster+Replicant iterators (the vehicles; the -map draws a cross+box blip for cls-0xBB9 mechs, the pip table being stubbed); (2) the blip PROJECTION was wrong — -`worldToView` is a camera matrix whose baked scale is wrong for a point transform (120u contact → 167px not 44px) → -project `delta = entity − viewpoint` directly (rotate by −heading, scale by pixelsPerMeter). Diagnosed via -`BT_MAP_LOG` (the enemy reads dsq=14400 = exactly 120u; the earlier "377-828m" was the wrong scale × player drift, -not a distance error). REMAINING: `PlayerStatus`, `vehicleSubSystems` (task #13) + the authentic pip symbol/name/ -video-object infra (`GetVideoObject`/`LookUpPip`/`GetNameID`, stubbed → cross-blip stand-in) + `SetTargetRange` -(radar zoom, 1km default) + the aggregate HeatSink-bank `AmbientTemperature` (`#if 0`'d 0xBBE) + the Reservoir -`coolantCapacity` shadow + the Searchlight `LightOn` table (duck button already resolves via the 0x37 Mech entry). -**◐ `PlayerStatus` DONE** (mission-review scoreboard; lives in the config's `cameraInit` block, not `MechInit`, -so it is build/link/mech-cockpit-safe but not runtime-verifiable via a normal mech egg — commit b691863). -**✅✅ `vehicleSubSystems` — DONE: the engineering-screen (MFD) subsystem cluster-panel system is -reconstructed, registered, and RENDERING (full spec + phasing in `docs/VEHICLE_SUBSYSTEMS.md`).** It is NOT a -widget — its Make (`FUN_004cbaf0`) is a per-subsystem FACTORY building a status *cluster panel* onto one of 12 -aux MFD positions, dispatching on classID → `HeatSinkCluster`(0xBC3)/`MyomerCluster`(0xBC6)/`EnergyWeaponCluster` -(0xBC8/0xBD4)/`BallisticWeaponCluster`(0xBCD/0xBD0), placed via the 12-row geometry table @0x51bf34. Reconstructed -the whole `SubsystemCluster` family (base + 5 subclasses) + 4 sub-gauges (`AnimatedSubsystemLamp`/`AnimatedSourceLamp`/ -`ScalarBarGauge`/`ConfigMapGauge`) + 3 connections + the Make + registered in `BTL4MethodDescription[]`; ALSO -reconstructed 4 base gauges the clusters build that were declared-but-undefined (`HorizTwoPartBar`/`OneOfSeveralInt`/ -`OneOfSeveralStates`/`BitMapInverseWipe`) + `StateConnection` + `SeekVoltageGraph`'s virtuals (their `/FORCE`-stubbed -ctors segfaulted when a cluster built them). **⭐ THE AUX-SCREEN BLOCKER RESOLVED CLEANLY (no core re-base needed):** -the Make reads `subsystem[0x1dc]`/`[0x1e0]`/`[0x1e4]` (aux-screen position/placement/label) which are -**PoweredSubsystem** fields (`auxScreenNumber`/`auxScreenPlacement`/`auxScreenLabel`, resource +0x104/+0x108/+0x10C) -that **our PoweredSubsystem ctor ALREADY populates** — the raw offsets read 0xCDCDCDCD only because the reconstructed -heat-leaf branch isn't byte-exact. Fix = a bridge `BTGetSubsystemAuxScreen()` (powersub.cpp) casting through the real -`PoweredSubsystem` type to return the NAMED fields (also the `FUN_0041a1a4`/`0x50f4bc` type filter); the Make + -`SubsystemCluster` read via the bridge. **Render-verified (BT_DEV_GAUGES_DOCK): 7 authentic panels build + draw — -SENSOR CLUSTER, MYOMERS, ER MED LASER·RANGE 500M ×3, PPC·RANGE 500M ×2 — with bar gauges/recharge dials/lamps, 0 -crashes, combat un-regressed.** Diagnostics `BT_VSS_LOG` (Make entry + per-subsystem classID/auxScreen dump). -Tooling: `reference/ghidra_scripts/DecompVSS.java` (headless address-list decompiler — reusable for any function -the assert-anchored exporter skipped; via the `vssproj` Ghidra project). Follow-ups (non-blocking, in the doc): -the `SegmentArc270` recharge-dial + `BitMapInverseWipeScalar` eject-wipe children (placeholder ctors, deferred -NULL); the remaining raw heat-leaf-branch value-pointer reads (BEST-EFFORT — need the branch re-base or per-field -bridges); the separate 960×384 gauge window (docked mode renders; separate-window is existing dev-gauge infra). - -**✅✅✅ 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). +## DO NOT +- Present T3/T4 claims as established without flagging the tier. +- Trust a factory `case ClassID` label — it's mislabeled; use the ctor-address + `CLASSMAP.md`. +- Issue `DestroyEntityMessage` on a mech death (the wreck STAYS; removal = the P5 teardown crash). +- Conclude "gauge not built" from an early process kill — the gauge renderer builds LAZILY. +- Use `DebugStream` (ReconStream, a no-op) for a log — use `DEBUG_STREAM`. --- -## 10. Combat bring-up — `btl4.exe` runs a single-player loop ⭐ (this is the live front) +## Project Structure +``` +bt411/ +├── CLAUDE.md # this router (was the 2236-line monolith) +├── README.md # build of record +├── context/ # topic files (curated knowledge graph) +│ ├── decomp-reference.md # offsets/ClassIDs/addresses/env gates (the "equations" hub) +│ ├── reconstruction-gotchas.md # the systemic bug classes (conventions hub) +│ ├── open-questions.md # deferred systems + get-from-Nick +│ └── .md # one concept each +├── reference/ +│ ├── glossary.yaml # terms/acronyms +│ ├── decomp/ # raw Ghidra pseudocode (the source-of-truth) +│ └── ghidra_scripts/ # the exporters +├── docs/ # PROGRESS_LOG.md (the full old CLAUDE.md) + detailed running ledgers +├── phases/ # restructuring / investigation logs +├── engine/ game/ content/ tools/ # the actual port (see README.md) +``` -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`. +### How to add content +- **New finding on an existing topic** → edit the `context/*.md` (+ the `docs/` ledger for full detail). +- **New topic** → new `context/*.md` with YAML frontmatter; add to the Quick Lookup table + cross-refs. +- **Split** a topic file that exceeds ~25k tokens into finer subtopics. +- **New investigation** → a `phases/*.md` log. +- The `docs/*.md` ledgers stay as the DETAILED logs; `context/*.md` are the curated digests that route to them. -### 10a. Build / run / debug recipe (bt411 layout — see README.md) -- **Configure once** (32-bit / Win32 — the DXSDK link libs are `Lib/x86`): - `cmake -S C:\git\bt411 -B C:\git\bt411\build -G "Visual Studio 16 2019" -A Win32 - -DCMAKE_GENERATOR_INSTANCE="C:/Program Files (x86)/Microsoft Visual Studio/2019/BuildTools"`. -- **Build:** `cmake --build C:\git\bt411\build --config Debug`. The one top-level `CMakeLists.txt` - builds `munga_engine` (engine lib, `engine/MUNGA{,_L4}/`) + `bt410_l4` (reconstructed BT game lib, - `game/reconstructed/` + `game/original/`) + `btl4.exe`, linking DXSDK d3d9/d3dx9/dinput8 + - OpenAL/libsndfile (`engine/lib/`). Linker uses `/FORCE` (tolerates header-defined globals + the dead - offline-factory unresolved externals in `mech3.cpp` — see the CMakeLists comment; a cleanup TODO). - ⚠ Editing an `engine/` file rebuilds the engine lib automatically now (one project) — no separate - engine build step; but a NEW member on a `DPLRenderer`/`d3d_OBJECT` class still needs the game objs - that embed its layout recompiled (delete stale objs if layout-mismatch corruption appears). -- **Run:** `run\run.cmd [EGG]` (default `DEV.EGG`) — it `cd`s to `content\` and launches - `build\Debug\btl4.exe -egg `. **cwd MUST be `content\`** (the engine resolves `BTL4.RES`, - `VIDEO\`, `BTDPL.INI`, eggs relative to cwd — see the `loadTables` gotcha below). Logs to `btl4.log` - in `content\` (grep `[anim]/[drive]/[target]/[fire]/[damage]/[boot]` markers). The DLLs - (OpenAL32/libsndfile) are copied next to the exe automatically at build time. -- **⭐ THE AUTHENTIC STACK IS NOW DEFAULT-ON** (`BTEnvOn`, mechrecon.hpp): `BT_GAIT_CUTOVER`, - `BT_GAIT_SM`, `BT_COLLISION`, `BT_REAL_CONTROLS` all default ON — set `=0` to fall back to the - historic bring-up path (e.g. `BT_GAIT_SM=0`). Verified: an env-free run gets the full authentic chain - (mapper speed/turn demands → two-channel gait → collision+gravity) with combat/damage intact, and each - `=0` fallback still works. -- **Bring-up env vars** (DEBUG/headless; default OFF): `BT_FORCE_THROTTLE=1` (auto-walk fwd, no key), - `BT_SPAWN_ENEMY=1` (spawn a target mech 120u ahead ALONG THE SPAWN FACING — facing-aware since the - real-controls drive keeps the authentic spawn orientation), `BT_FORCE_FIRE=1` (auto-fire on cooldown), - `BT_ASSERT_TO_DEBUGGER=1` (route CRT asserts to the debugger instead of a modal box), `BT_HEAPCHECK=1` - (whole-heap validation on every alloc/free — heap-corruption hunts), `BT_PROBE_BGF=` (+ - `BT_PROBE_N`) (boot-time BGF loader probe, exits after). Interactive: - **WASD** drive, **Space/Ctrl** fire, **X** all-stop. Gated/marked in `btl4main.cpp` + `mech4.cpp`. - **⭐ VIRTUAL CONTROLS (dev keyboard → the pod's analog inputs; mech4.cpp drive block):** the pod's - throttle was an ABSOLUTE analog LEVER + an analog turn stick — momentary 0/1 keys can't express either. - After the perf fix took the game 10fps→60fps, every key tap landed as a frame-perfect FULL demand (at - 10fps most taps aliased away between sim ticks, which had LOOKED like fine control) → "controls too - sensitive, tap-back reverses instead of stopping". mech4 integrates dt-scaled: **W/S sweep a PERSISTENT - throttle lever** (tap ≈ ±0.08 step, full sweep ~1.4s; a lever below standSpeed = the mech stands, authentic) - with a **DETENT at zero** (braking from forward stops AT 0 and latches until the key is released — no - accidental reverse), **X = all stop**; **A/D deflect a momentary stick** (~0.4s to full, auto-centers) so a - tap = a gentle nudge. `gBTDrive.throttle/turn` are the integrator OUTPUTS; the mapper bridge + - forced-throttle harness consume them unchanged (BT_FORCE_THROTTLE bypasses the lever). - **⚠⚠ KEY-INPUT GOTCHA — the WndProc NEVER RECEIVES WM_KEYUP:** the engine's per-frame keyboard reader - (`L4CTRL.cpp:1506`) `PeekMessage(PM_NOREMOVE)` + `GetMessage()`s every `WM_KEYUP`/`WM_SYSKEYUP`/`WM_CHAR` - OUT of the queue for its key-command channel — only KEYDOWNs reach the launcher WndProc, so ANY - message-based key-state tracking latches keys on forever (this was ALSO true of the old 0/1 scheme: its - throttle/turn/fire never cleared on release; the old "feel" was that quirk + 10fps aliasing). The - integrator therefore POLLS `GetAsyncKeyState` per frame (via GetProcAddress, no headers needed), gated on - the game window being FOREGROUND (`GetForegroundWindow` pid == ours) so background typing doesn't drive - the mech. **`BT_KEY_NOFOCUS=1`** = test hook accepting keys without focus (SendInput automation from a - background shell can't grant real foreground; `scratchpad/realkey.ps1` injects real keys for harness runs); - **`BT_KEY_LOG=1`** = 1 Hz `[vctl]` key/lever/stick state. BTDriveInput struct is duplicated in - btl4main/mech4/mechmppr/emitter — keep in sync (the emitter copy had already silently drifted). - **⭐ Levels:** default test EGG is now `DEV.EGG` (`map=grass` + `time=day`, outdoor); the RES has 8 maps - (`cavern`/`grass`/`rav`/`polar3`/`polar4`/`arena1`/`arena2`/`dbase`) — swap via a copied EGG's `map=` - field (§4a). - **⭐ MECH SELECTION IS EGG DATA — the code path is mech-agnostic (VERIFIED with a second mech).** The - player vehicle = the egg's pilot page `vehicle=` (DEV.EGG: `bhk1`), resolved via - `playerMission->GetGameModel()` → `FindResourceDescription(name, ModelListResourceType)` (btplayer.cpp - `CreatePlayerVehicle`). RES type-18 names incl.: `bhk1/blkhawk ava1/avatar mad1/mad2/madcat lok1/lok2/loki - thor/thr1 vul1/vulture owens/own1 snd1 sunder strk`. **`MECH2.EGG` (pod BT dir) = DEV.EGG with - `vehicle=madcat` — the Mad Cat booted, rendered, walked and fought ON FIRST TRY with ZERO code changes:** - own clips loaded (walkStride 18.5 vs BLH 22.0 — real per-mech data measured), 22 damage zones (BLH 20), - 30-subsystem roster constructed, gait/collision/shadow/heat live, combat → TARGET DESTROYED, 0 crashes. - **The per-mech worklist that surfaced (all known warts, nothing new):** (1) the visual-fire muzzles fell - back to the centre point (the gun-port site names are hardcoded BLH.SKL names — the real fix is per-weapon - `.SUB` `segmentReference` muzzle resolution, already how `Emitter::GetMuzzlePoint` works); (2) roster 32 vs - 30 present = its LRM racks hit the still-stubbed missile factory cases (0xBCE/0xBD0 — SUBSYS_PLAN WAVEs, - per weapon FAMILY not per mech); (3) beam colour still the BLH red (per-weapon `VideoObjectName` pending - weapon enumeration). Everything else is foundational. -- **cdb (x86):** `"C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\cdb.exe"`. Pattern for a faulting - stack (set cwd to `content\` first): `-g -c ".lines;sxe av;g;kp 24;q"` with `BT_ASSERT_TO_DEBUGGER=1`. - The debug CRT marks fresh heap **0xCDCDCDCD** (uninitialized) and freed **0xFEEEFEEE** — invaluable for - "was this ever constructed?" questions. gflags PageHeap (needs elevation) caught the earlier subsystem - heap overruns. - -### 10a-bis. Repo layout — where everything lives (CONSOLIDATED into bt411) -The build was formerly a "virtual assembly" scattered across 6 absolute-path locations in the RE -workspace; it is now **one relocatable tree** (`C:\git\bt411`, all repo-relative). Map of the tree -(full detail in `README.md`): -- **`engine/MUNGA/` + `engine/MUNGA_L4/`** — the shared 2007 MUNGA engine + Win32/D3D9 HAL, carrying - our BT render/loader work (`bgfload`/`L4D3D`/`L4VIDEO` + the `image.{cpp,h}` codec). Plus - `engine/shim/` (ATL shim), `engine/lib/` (OpenAL/libsndfile libs + runtime DLLs), `engine/rp/` (the - ~21 RP *headers* the audio HAL includes via `..\rp\vtv.h` — headers only, no RP game source), and - `engine/DPLSTUB.h`/`LOGGER.h` (the trunk-root headers reached via `..\`). -- **`game/reconstructed/`** — the reconstructed BT source (the bulk; was `decomp/reconstructed/`). -- **`game/original/BT/` + `game/original/BT_L4/`** — the surviving *original* BT `.cpp` - (`BTMSSN/BTREG/BTTEAM/BTSCNRL/BTCNSL/GAUSS/PPC/BTL4MODE/BTL4ARND`) **+ all the BT headers** (these two - dirs are the game's header include path). -- **`game/fwd/`** — the ~186 forwarding shims letting reconstructed code do `#include `; - each now `#include "../../engine/MUNGA/.h"` (relativized). `fwd/app.thp` is a self-contained - `TestApplication` reconstruction shim; `fwd/bgfload.h` forwards to the engine loader header. -- **`game/btl4main.cpp`** — the WinMain launcher. -- **`content/`** — the runtime tree (was `tesla4pod_extracted/.../REL410/BT/`): `BTL4.RES`, `VIDEO/`, - `GAUGE/`, `AUDIO/`, eggs, `BTDPL.INI`, `VIDEO/REPLACEMATS.tbl`. **Run cwd = `content/`.** -- **`reference/decomp/`** — the raw Ghidra pseudocode `all/part_*.c` (was `decomp/recovered/`), the - source-of-truth every reconstruction is verified against. `reference/ghidra_scripts/` = the exporter. -- **`docs/`** — format specs (`BGF_FORMAT`, `ASSET_PIPELINE`, `BT_SOURCE_STATUS`) + the reconstruction - ledgers (`RECONCILE`, `HARD_PROBLEMS`, `RESOURCE_AUDIT`, `SUBSYS_PLAN`, `WAVE_PLAN`, `P3_LOCOMOTION`). -- **`tools/`** — `btconsole.py` (MP console emulator) + `mapscan.py`/`resscan.py` scanners. -- External (not vendored): **DXSDK June 2010** (overridable `-DDXSDK`). ⚠ Historical sections below - still cite the old `nick-games/…` absolute paths as provenance — see the orientation banner at the - top; they map onto the dirs above. **NOT copied into bt411:** the standalone D3D9 viewer (§6 `port/`, - a dev sandbox) and the RE toolchain blobs (Ghidra/JDK) — those stay in the original workspace. - -### 10b. Reconstruction workflow (the method) — **RULE: no stand-ins** -The loop for each feature: (1) read the RAW decomp `decomp/recovered/all/part_*.c` for the relevant -`FUN_xxxx`; (2) map `FUN_`/`DAT_`/`this+0xNN` to engine symbols using BT headers + the WinTesla MUNGA -source + `decomp/reconstructed/CLASSMAP.md` + RP's parallel code; (3) write the **real** reconstruction -into `decomp/reconstructed/*.cpp`; (4) `compile1.cmd` then full build; (5) run env-gated + read `btl4.log`; -(6) cdb on any crash. **Never write stand-in/placeholder code for an apparent "gap"** — the full game -logic IS in the pseudocode (the binary ran the game); a "gap" is just a reconstruction stub not yet -filled (e.g. `ResourceStream`/`MemStreamX` no-op proxies, an uninitialised field). Said by the user: -"there are no gaps, just work to be done." Bring-up scaffolding (the env-var paths, the explosion-for-beam, -a player-gated drive) is **clearly marked and meant to be replaced** by the real reconstructed system, -never to substitute for reading the decomp. For exhaustive multi-function decomp analysis, fan out a -read-only **Workflow** (understanding phase), then implement hands-on so each change is visible. - -### 10c. Decompilation insights (per subsystem — durable facts) -- **Resource lookup is by NAME or by ID.** `ResourceFile::FindResourceDescription(name, type, index=-1)` - (raw `FUN_00406ff8` = find-by-type-and-name; `FUN_00407064` = `SearchList(resourceID, type)`). Resource - **type enum**: SkeletonStream, **DamageZoneStream**, ModelList, **AnimationResourceType==16 (0x10)**, - damage-zone segment list **type 0x14**, critical-subsystem segment **0x1e**. Animation clip names in - `BTL4.RES` are **lowercase** (`blhrrl`=904, `blhrrr`=905, `blhwwli`=910, `blhrrli`=916). `Mech:: - ResolveAnimationClip(prefix,suffix)` builds the name and calls FindResourceDescription. -- **Animation (engine `MUNGA/JMOVER.cpp`):** `AnimationInstance(JointedMover*)` then `SetAnimation( - ResourceID, finishedCallback)` (Lock()s the clip, parses `frameCount/jointCount/jointIndices/frameStart/ - keyFrames/rootTranslations` from `resourceAddress`). `Animate(dt, move_joints=True)` writes each joint's - DCS per keyframe. **At end-of-clip it calls `(moverToAnimate->*finishedCallback)(...)` (line ~1592) — a - NULL callback crashes.** Loop a gait via a **non-virtual** `Mech::OnBodyAnimFinished` re-arming the clip, - passed `reinterpret_cast` (Mech derives from JointedMover; the member- - ptr cast is how the original stores a Mech method in the JointedMover-typed slot). -- **`loadTables` gotcha (`MUNGA_L4/L4VIDEO.cpp:849`):** `fopen("VIDEO\\REPLACEMATS.tbl","rb")` is **relative - to cwd with no null-check** → fread on NULL if cwd isn't the pod BT dir. (RP material-substitution table.) -- **⚠ `application`/`l4_application` is NULL outside the frame loop** — `ApplicationManager` (APPMGR.cpp:73) - REASSIGNS the global `application` while iterating the running-app chain each frame and leaves it NULL - between frames. Any code running at Windows message-pump time (WndProc handlers, timers) reads NULL even - while the game runs. Pattern: record the input in a global at message time; consume/apply it inside a - per-frame path where `this` is the engine object (how the window-resize aspect fix works, L4VIDEO.cpp - `gWindowAspect` → per-frame dirty-check rebuild; commit 06683cf). -- **Entity spawn:** `Mech::Make(MakeMessage*)` (`FUN_004a2d48`) allocates `sizeof(Mech)=0x854` + runs the - ctor; the base `Entity` ctor self-registers it into the world (renders + ticks). `Registry::MakeEntity` - dispatches per-class `makeHandler` (BT registry maps `MechClassID=0xBB9 → Mech::Make`). Unique IDs via - `HostManager::MakeUniqueEntityID()`. **The mission system is NETWORKED and BT is PvP-ONLY — it never had - NPCs/AI** (verified, not a recovery gap): `BTREG.cpp`'s entity registry knows only `Mech`/`Projectile`/ - `Missile`/`BTPlayer`/`BTTeam`; the COMPLETE BT header set has no Bot/Brain/AI class; `BTL4OPT.EXE` has no - AI strings; every mech is driven by a `BTPlayer` (or a `BTCameraDirector` for the "camera"/spectator game - model), players grouped into `BTTeam`s. Opponents were always other human pods over the net (`NETNUB`/ - `L4NET`). The AI entities `Crusher`/`Blocker`/`Runner` are **Red Planet** classes (`RP/`), NOT BT and NOT - engine — reference only for *how the engine supports AI if we ever ADD it* (that would be new dev, not a - port). ⇒ A solo mission has no enemy by design; the env-gated `BT_SPAWN_ENEMY` mech is a **test dummy** - (uncontrolled `Mech`), not a game feature. The authentic opponent path is multiplayer (Phase 7). -- **`Mech::PerformAndWatch` runs for EVERY mech** → gate player-only logic on - `this == application->GetViewpointEntity()` (else a spawned enemy is driven by player input / shares the - player's AnimationInstance). -- **Targeting (mech offsets verified vs `part_013.c`):** `mech+0x37c` = `Point3D` target world pos (range/ - aim source), `mech+0x388` = `Entity*` target (**the field `HasActiveTarget()` / `Emitter::FireWeapon` - gate on**), `mech+0x38c` = `int` targeted sub-zone (-1 = whole). Weapons cache `hasTarget/targetPoint/ - muzzlePoint`, refreshed each frame from the mech's target by the mech's targeting update. -- **Firing:** `Emitter::FireWeapon` (`emitter.cpp`, real) builds a muzzle→target beam but **weapon-effect - renderables (beams/tracers) are NOT built in the port** (`btl4vid.cpp:455`) → nothing draws. **Explosions - DO render** (`L4VIDEO.cpp:5356`), so a shot stands in as `Explosion::Make(MakeMessage*)` at the target - (effect resource **"explode" = id 13**). Weapon ClassIDs (CLASSMAP, note the factory enum mislabels): - EmitterWeapon 0xBC8, MechWeapon 0xBCD, Missile 0xBCE, Ballistic 0xBD0, **PPC 0xBD4** (factory calls it - "GaussRifle"), MechTech 0xBD6. Mech ctor offsets: `damageZoneCount @0x11c (param[0x47])`, - `damageZones @0x120 (param[0x48])`, `resourceID @0x1bc (param[0x6f])`. -- **Damage delivery:** `Entity::TakeDamageMessage(id, size, inflictingEntityID, zone, Damage&)` → - `target->Dispatch(&msg)`. **Base `Entity::TakeDamageMessageHandler` IGNORES `zone==-1`**; the comment - (`ENTITY3.h`) says "Mechs use cylinder lookup table" for an unaimed hit — **that Mech override is NOT yet - reconstructed**, so send a VALID zone index for now. `class Damage{ damageType(enum Collision/Ballistic/ - Explosive/Laser/Energy), damageAmount, damageForce, surfaceNormal, impactPoint, burstCount }`. - `DestroyEntityMessage(id, size)` removes an entity. Engine `Explosion` also splash-damages nearby - entities via a collision query (fragile) — direct Dispatch to the known target is what we do. -- **✅ Damage zones — REAL damage model reconstructed & working** (replaced the health-model stand-in). - The chain: aimed `TakeDamageMessage` → engine `Entity::TakeDamageMessageHandler` → `Mech__DamageZone:: - TakeDamage` → engine base armor model (`damageLevel += damageAmount * damageScale[type]`) → accumulates - to 1.0 (zone destroyed) → graphic-state/death branch → death detected. Verified: `structure` climbs - 0.133/hit, vital zone destroyed → `*** TARGET DESTROYED ***`, stable. Build mechanics: engine `Entity` - ctor (`ENTITY.cpp:961-987`) reads `damageZoneCount` from the `DamageZoneStreamResourceType` (type 0x14) - resource and allocates the array (entries left `0xCDCDCDCD`); the **Mech ctor populates that inherited - array** (mech.cpp Pass-3): `MechFindResource(resourceID, 0x14)` → `Lock` → `DynamicMemoryStream(addr, - size, initial_offset=4)` (skips the count word the base consumed) → `for zfield - (char*)res`) vs the binary's - known offsets and dumping raw record bytes as int+float; anchor the true field offsets from the raw decomp - of the resource parser (`CreateStreamedSubsystem`/@004ac9ec — `param_4+0xNN` reads). See §10d heat-flow. - - **Two more non-field fixes were needed for damage to flow:** (1) **message-handler chaining** — a - reconstructed class's `MessageHandlers` set must be built chained to the parent's (`Receiver::Message - HandlerSet(Entity::GetMessageHandlers())`); an empty default-ctor set has NO parent chain, so - `Receiver::Receive` finds no handler and **every inherited message (TakeDamage, etc.) is silently - dropped**. (2) **entity validity** — `Entity::Dispatch` only delivers synchronously (`Receiver::Receive`) - for a VALID master; an invalid entity `Post`s the message as a deferred event that never fires. A - manually-spawned entity (no `MakeViewpointEntity`/CheckLoad handshake) must call `SetValidFlag()` itself. - - **Remaining damage gaps (scoped, non-blocking):** EnergyDamageType(4) shorts attached generators via - `criticalSubsystems[]->subsystemPlug.Resolve()` → needs the REAL subsystem roster (still `RECON_SUBSYS` - stubs) → use ExplosiveDamageType meanwhile (correct for the explosion weapon anyway). Per-impact aim - needs the cylinder lookup (deferred STEP 6 — `Mech::TakeDamageMessageHandler` override `FUN_004a0230` + - `FUN_0049ed0c`); meanwhile aim a vital zone directly. `IsDisabled()` (`FUN_0049fb54`) reconstructed - (reads `movementMode==2||9`), but `movementMode` is set by the bypassed gait/death-transition → bring-up - death reads the real zone state directly (a `vitalDamageZone` with `damageLevel>=1.0`). Removing the - wreck (`DestroyEntityMessage`) crashes on teardown — death = explosion + stop-targeting for now. - ⚠ **ROOT-CAUSE PIVOT (P5 — full writeup `docs/HARD_PROBLEMS.md`; repro `BT_ENABLE_TEARDOWN=1`):** it is - a **TEARDOWN-SEQUENCE double-free, NOT a base-region stomp** (two earlier theories DISPROVEN). (a) "solids - never built" — WRONG (`Mover::Mover`:1756 allocs `collisionLists` unconditionally). (b) "reconstructed Mech - raw offsets stomp the engine collision cluster" — ALSO WRONG: those stomps (`Mech::Simulate` collision/ - terrain/telemetry, `FeedHeat*Gauge`) are **DEAD CODE, never called** (grep-confirmed). The `BT_ENABLE_TEARDOWN` - dump proves the enemy's engine base is **FULLY VALID at death** (`collisionLists@0x2e4`, `segmentTable@0x2f0` - =51 segs, `jointSubsystem@0x30c` all valid). The crash is in the destruction SEQUENCE (`FryDeathRow → ~Mech - → ~JointedMover → ~Mover`): cdb shows `collisionLists`'s array already **freed** (`0xdd`/`count=0xdddddddb`) - by the time `~Mover` `delete[]`s it → a double-free. `~Mech` (reconstructed) runs before the engine base - dtors → prime suspect is a Mech member dtor / the minimal-spawn collision-or-death-row setup freeing (or - aliasing) a base resource the engine dtor frees again. ✅ **PINNED (trace done):** it's the **skeleton - SEGMENT teardown double-free**, not collisionLists. `~JointedMover` (JMOVER.cpp:436 - `SegmentTableIterator(segmentTable).DeletePlugs()`) → `SocketIterator::DeletePlugs` (SOCKET.cpp:157-161 - `delete plug`) force-`delete`s each of the enemy's 51 `EntitySegment`s with `defeat_release_node=1` - (bypassing the ref-count release path); one is **already freed** → `STATUS_HEAP_CORRUPTION 0xC0000374` + - `0xFEEE` fill. ⭐ **P5 CLOSED — the whole premise was WRONG (verified):** a killed mech does NOT disappear - in the real game, so `DestroyEntityMessage`-on-death should NEVER be issued. RP death = `VTV::DeathShutdown` - (VTV.cpp:1681 — subsystems shut down, vehicle STAYS); `CondemnToDeathRow`/removal is ONLY for transient - objects (RIVET projectiles, DEMOPACK cleanup). BT death = a STATE transition (`SetGraphicState(Destroyed - GraphicState)` mechdmg.cpp:355 + death anim `deathAnimationLatched@0x650` + effect/splash) → the mech becomes - a persistent WRECK. So the **wreck-stays behavior we already ship IS faithful**; the teardown crash is an - artifact of forcing a removal the original never does (only reachable via `BT_ENABLE_TEARDOWN`). NOTHING to - fix. Real future death work = reconstruct DeathShutdown + collapse anim + destroyed skin (does NOT touch the - teardown path). Later trace also showed it's heap corruption in a SINGLE teardown, not the double-free the - line above hypothesized. Full trail: `docs/HARD_PROBLEMS.md`. - ⭐ **P5 TRULY CLOSED — ROOT CAUSE FOUND & FIXED (the whole trail above was chasing run-2 of a DOUBLED - dtor chain).** `Mech::~Mech` (mech.cpp:1106) contained an EXPLICIT `JointedMover::~JointedMover();` — - the Ghidra decomp shows the binary's `FUN_00425550(this,0)` tail call, which IS the compiler-emitted - implicit base-dtor; reconstructing it as a source statement ran the whole `~JointedMover → ~Mover → - ~Entity` chain TWICE per Mech. Run 2 re-`delete[]`d `Mover::collisionLists` (count word read from the - 0xDD freed fill = the observed `0xdddddddb`) and re-ran `DeletePlugs` over the destroyed segment table - (= the P5 "EntitySegment already freed" 0xC0000374). ONE bug = BOTH crashes (death-row teardown AND - app exit — every window close crashed once entities existed; symptom timing varied with heap reuse, - which is why idle-close sometimes survived and why the earlier traces looked contradictory; the old - ENTRY/EXIT probes bracketed the EXPLICIT call, misreading run-1 as "a member dtor freed it"). - **⚠ RECONSTRUCTION RULE (add to the systemic checklist):** in a decompiled DESTRUCTOR, the trailing - member-dtor calls (`FUN_xxx(this+N, 2)`), the base-dtor call (`FUN_xxx(this, 0)`), and the - `(flags&1) && operator delete(this)` tail are COMPILER EPILOGUE GLUE — reconstruct only the dtor body - ABOVE them; C++ re-emits member+base destruction implicitly at the closing brace. Explicit member-dtor - calls were also removed from ~Mech (were benign only because the Recon proxies are no-op) and the same - pattern documented out of `Mech__DamageZone`'s dtor (mechdmg.cpp). VERIFIED: interactive play → window - close under `BT_HEAPCHECK=1` = clean exit; combat kill + `BT_ENABLE_TEARDOWN=1` forced entity teardown - = `TARGET DESTROYED`, teardown completes, sim continues, clean close — 0 crashes across the matrix. - **App exit no longer crashes; entity removal (`DestroyEntityMessage`) is usable again** (relevant to MP - disconnect cleanup). Repro harness kept: `BT_FORCE_SECONDS=` (release forced throttle after n sim-s) - + `BT_FORCE_TURN=` (hold steering) in mech4.cpp — the tap-release/turn paths no soak had exercised. - (Forensics: a 14-agent adversarial workflow; secondary REAL-but-dormant hazard found on the way: - `AddCollisionToList` BNDGBOX.cpp:389 has no live bounds check — `Verify` compiles out at DEBUG_LEVEL=0 — - so >10 simultaneous collision contacts from >1 source overflow the 10-slot list; **NOW FIXED** — a live - clamp in `AddCollisionToList` (BNDGBOX.cpp, engine tree) drops overflow contacts instead of writing past - the block. Trigger scenario was real: the user spawned INSIDE a building solid on grass (mass simultaneous - contacts + gravity pressing into terrain boxes) → silent AV mid-play, where the CRT assert dialog ITSELF - crashed while formatting (why no popup appeared). Verified: full combat+teardown+close matrix under - `BT_HEAPCHECK=1` = 0 crashes, clean exit. Spawn-inside-solid placement itself is a separate tuning item.) - (Latent, non-causal: compiled `sizeof(Mech)=0x638` < binary `0x854`; the over-sizeof writes `0x7e0-0x828` - live only in the dead Simulate, so no live overflow — but pad to 0x854 before reviving that code.) - -### 10d. Reconstructed modules wired into `bt410_l4` (state) -~43 reconstructed `.cpp` compile+link. Mech runtime (`mech.cpp/2/3/4`, `mechdmg`, `mechsub`, `mechtech`), -heat (`heat`), weapons (`emitter`/`mechweap`/`mislanch`/`missile`/`projweap`/`projtile`/`gauss`/`ppc`), -subsystems (`gyro`/`sensor`/`gnrator`/`powersub`/`myomers`/`torso`/...), app/HUD (`btl4app`/`btl4vid`/ -`btl4mppr`/`btl4mssn`/`btplayer`/`hud`/`dpl2d`). **Subsystem instances are `RECON_SUBSYS` STUBS — the -un-stub wave is IN PROGRESS** (full per-family plan: `docs/SUBSYS_PLAN.md`, from the `bt-subsystem-recon` -workflow). KEY FINDING: the **real subsystem classes mostly already exist** (heat→`heatfamily_reslice`, -weapons→`emitter`/`projweap`/`mislanch`, gyro/sensor/power/torso/myomers/searchlight/thermalsight/ammobin -all reconstructed); the work is *wiring* them in + the 4 systemic checks (shadowing, `Wword` trap, handler -chain, validity). **The factory case LABELS are systematically MISLABELED** — the real class is identified -by the `// FUN_004xxxxx` ctor-address comment reconciled via `CLASSMAP.md`, NOT the `case ClassID` -label. Master map (case→real): 0xBBD→Condenser, 0xBBE→HeatSink-bank (cache slot=heat bank, not "sensor"), -0xBC0→Reservoir, 0xBC1→Generator, 0xBC2→PoweredSubsystem, 0xBC3→Sensor, 0xBC4→Gyroscope, 0xBC5→Torso, -0xBC6→Myomers, 0xBC8→Emitter, 0xBCB→AmmoBin, 0xBCD→ProjectileWeapon, 0xBCE→UNRESOLVED (decompile `4bdcb4`), -0xBD0→MissileLauncher, 0xBD3→SubsystemMessageManager, 0xBD4→PPC(Emitter), 0xBD6→HUD, 0xBDC→MechTech, -0xBD8→Searchlight, 0xBDE→ThermalSight. Two bugs to fix: case 0xBDC discards the ptr (no `subsystemArray[id]=`); -MechTech(real)+HUD are SWAPPED (MechTech wrongly built at 0xBD6). **Swap recipe:** per-class `Create -Subsystem` bridge fn in the class's own `.cpp` (do NOT `#include` real subsystem headers into mech.cpp — -the local stubs collide) + `extern` it in mech.cpp; keep the alloc SIZE + `++weaponCount`/special-cache. -**Order:** WAVE 1 heat base re-base (`HeatableSubsystem : Subsystem`→`: MechSubsystem`, delete shadow -fields owner/flags/statusFlags/statusBits/destroyed, fix thermal offsets) = universal prerequisite; then -heat family + MechTech/HUD fix; then power bus + Emitter/PPC; then standalone readouts; gyro/torso/myomers -later. DEFER: `MechControlsMapper` tick (reads unddocumented App offsets → AV, mech4 bypasses it); 0xBCE. -**✅ WAVE 1 DONE** — `HeatableSubsystem` re-based onto `MechSubsystem`; shadow fields removed; the cascade -fixed across heat.cpp/heatfamily_reslice/mechsub + powersub/hud/mechweap (the de-shadow propagates to EVERY -heat/powered subclass: `statusFlags`→`simulationFlags`, `destroyed`→`simulationState`, `statusBits`→ -`ForceUpdate()`, and `ResetToInitialState()`→`(Logical powered)` on every override). Full build GREEN, exe -runs, combat (zones/anim/damage→death) un-regressed. **DEEPER FINDING: the shadowing is more systemic than -the plan estimated — `MechSubsystem` ITSELF shadows the engine `Subsystem::damageZone` (mechsub.hpp:252 -`ReconDamageZone *damageZone` shadows `SUBSYSTM.h:159 DamageZone *damageZone`), exposed once MechWeapon's -chain ran through MechSubsystem. Worked around by qualifying `this->Subsystem::damageZone` (mechweap.cpp); -the proper fix = remove the `MechSubsystem::damageZone` shim shadow (cascades to mechsub.cpp) — a follow-up. -Two include fixes also needed: `heat.hpp` now `#include `; `mechrecon.hpp` now `#include -` (self-sufficient Quaternion, for myomers's include order). Next = WAVE 2 (heat factory -wiring + MechTech/HUD un-swap) per `docs/SUBSYS_PLAN.md`.** -**✅ WAVE 2 DONE** — real heat family + HUD + MechTech wired into the factory. Swap mechanism: a per-class -`CreateSubsystem(Mech*,int,void*)` BRIDGE fn appended to each real class's own `.cpp` (heat.cpp: -Condenser 0x230 + HeatSink-bank 0x1e4; heatfamily_reslice.cpp: Reservoir 0x230; hud.cpp: HUD 0x2a4; -mechtech.cpp: MechTech 0x140), `extern`-declared + called from the mislabel-correct factory cases in -mech.cpp (0xBBD→Condenser, 0xBBE→HeatSink-bank+cache, 0xBC0→Reservoir, 0xBD6→HUD, 0xBDC→MechTech+store+cache -— fixing the MechTech/HUD swap + the discarded-ptr bug). RESULT: **30 of 31 subsystems now tick their real -per-frame Performance** (1 bypassed = controls mapper), up from MechTech-only; build green, 16s stable, -combat un-regressed. RUNTIME FIX: `HUD::HUD` read the torso source at raw `owner+0x438` (garbage in the -recon Mech layout) → defaulted the torso/range readouts to 0 (HudSimulation refreshes per-frame). NEW -SYSTEMIC PATTERN for the remaining waves: subsystem ctors read the owner Mech at RAW binary offsets -(`*(int*)(owner+0xNN)`) that are wrong in the recon Mech → guard/zero + per-frame refresh, or wire a real -Mech accessor. Heat self-load from firing (temperature climb) needs WAVE 3 (real Emitter writing the -inherited heat accumulator). Next = WAVE 3: power bus (Generator/PoweredSubsystem) + Emitter/PPC weapon. -**✅ WAVE 3 (power bus + Emitter) — CONSTRUCT+TICK done; full fire-path pending.** 3a: bridges for Generator -(0xBC1, 0x250) + PoweredSubsystem (0xBC2, 0x31c) in powersub.cpp → factory wired → power bus real. 3b: -`CreateEmitterSubsystem` (0x478) in emitter.cpp, wired for BOTH 0xBC8 (Emitter) AND 0xBD4 (PPC = Emitter -subclass, same FireWeapon) → real Emitter constructs + ticks its `EmitterSimulation` state machine. Runtime -fix (E2): `Emitter::TrackSeekVoltage` (emitter.cpp:311) derefs a null voltage source (the source plug at raw -`this+0x1d0` isn't registry-linked in bring-up) → guarded `if (src==0 || dtScale==0) return` (no charge that -frame). All real subsystems now construct + tick; build green, combat un-regressed. -**✅ WAVE 3 fire-path (E3-E8) DONE — the real Emitter fires end-to-end** (verified: repeated `[emitter] -FIRED damage=.. heat=0.49 pendingHeat=0.49`, 17s stable, combat intact). Implemented: E3 `MechWeapon:: -HasActiveTarget()`/`GetTargetPosition` read the owner's target slots (`owner+0x388`/`+0x37c` via the -inherited `MechSubsystem::owner`) not the never-set `hasTarget`; E4 `firingArmed=1` in the Emitter ctor; E5 -deleted the Emitter-local `heatAccumulator` SHADOW, `FireWeapon` now writes the inherited `HeatSink:: -pendingHeat`@0x1c8 (the heat-sim input); E7 force-charge in EmitterSimulation Loading (the source plug at -raw `this+0x1d0` never resolves -> `currentLevel=seekVoltage[idx]` -> outputVoltage 1.0 -> Loaded; also -pre-init the seek table in the ctor so it's valid when the ctor src is null); E8 trigger via a per-FRAME -`int gBTWeaponTrigger` pulsed by mech4 (controls mapper bypassed) -> CheckFireEdge sees clean edges. -**ROOT-CAUSE BUG fixed along the way (benefits ALL subsystems): the MechSubsystem ctor set `hostEntity` -but never set `owner`** (mechsub.cpp) -> every subsystem's `owner` was garbage once heat de-shadowed; now -set in both MechSubsystem ctors. Also: the Mech ctor now zeroes the target slots (+0x37c/+0x388/+0x38c) so -an un-targeted mech (spawned enemy) reads "no target" (else its weapons fire at a garbage pointer). -**✅ WAVE 4 (standalone readouts) DONE — Sensor / Searchlight / ThermalSight / AmmoBin un-stubbed + ticking** -(verified: **BLH tick 20→26 executable**, Mad Cat 24, combat **DESTROYED** un-regressed on both, 0 crashes). -The 4 readout classes were reconstructed but built as `RECON_SUBSYS` stubs; each is now wired via a -`CreateSubsystem` bridge (in its own .cpp) at the mislabel-correct factory case: 0xBC3→**Sensor** -(`MyomersClassID` label), 0xBD8→**Searchlight** (`LegSubsystem`), 0xBDE→**ThermalSight** (`MechDisplay`), -0xBCB→**AmmoBin** (`JumpJet`). Each got the Torso de-shim: delete the cross-family shadow fields, redirect -accessors to the real inherited base, add a `friend struct LayoutCheck` with `static_assert` -offset/sizeof locks. Redirects by branch: **Sensor** (PoweredSubsystem:HeatSink leaf) → `heatAlarm`/ -`electricalStateAlarm`/`simulationState`/`heatEnergy`/`simulationFlags|=8`; **Searchlight/ThermalSight** -(PowerWatcher) → `watchdogAlarm`/`heatAlarm`/`simulationState`/`ForceUpdate()`/`simulationFlags`; **AmmoBin** -(HeatWatcher) → `simulationState` (deleted the `statusState`@0x40 shadow). -- **⭐ FUNCTIONAL BUG FIXED (Searchlight + ThermalSight): the ctor gate read the shadow `segmentFlags` (=0), so - `((0&0xC)==0 && (0&0x100)!=0)` was ALWAYS FALSE → their Performance NEVER installed (dead since - reconstruction).** The de-shim switches the gate to `owner->simulationFlags` (binary-exact part_013.c: - 6012/6140; mirrors torso.cpp:181) → they now tick. (Sensor uses the simpler `!=4` gate; AmmoBin's gate was - already correct.) -- **AmmoBin layout: `ammoAlarm` retyped `HeatAlarm`(8B)→`WatcherGaugeAlarm`(0x54) + the `statusState` shadow - deleted → `ammoModelFile` lands at the binary 0x1E8, `sizeof==0x22C` exact (locked, the load-bearing lock).** - ⚠ CORRECTION to an earlier assumption: the all-laser Blackhawk is NOT AmmoBin-free — it DOES carry AmmoBins - (the un-stub exposed the crash below ON THE BLH, and BLH tick rose 20→26 = Sensor+Searchlight+Thermal+AmmoBins). -- **⚠ SHARED LATENT BUG EXPOSED + GUARDED (`HeatWatcher::WatchSimulation`, heatfamily_reslice.cpp:305):** the - un-stub made `AmmoBinSimulation` (and via `PowerWatcher::Simulation`, the light/thermal sims) call the - HeatWatcher base `WatchSimulation`, which derefs `watchedLink.Resolve()->currentTemperature` — the watch - link isn't resolved in bring-up (the "WatchedSubsystem" name→roster-segment bind doesn't happen), so - `Resolve()` returns null → AV (`AmmoBinSimulation`→`WatchSimulation`, caught under cdb). The binary never - null-checks (its link always resolves at load). GUARDED (marked): `if (watched==0){ heatAlarm.SetLevel(0); - return; }` — same pattern as `Emitter::TrackSeekVoltage`. FAITHFUL FOLLOW-UP = resolve `watchedLink` so the - heat-watch alarm tracks the real watched temperature (it gates cook-off / light-blank / thermal-blank — - secondary behaviors, inert until then). -- **LAYOUT-LOCK FINDING (a new systemic fact): the heat-leaf branch (PoweredSubsystem:HeatSink) is NOT - byte-exact to the binary** — Sensor's exact-offset asserts (radarPercent@0x31C) FAILED at compile time, - unlike the re-based WATCHER branch (Searchlight/ThermalSight/AmmoBin offsets ARE exact and locked). So - Sensor uses only the OVERFLOW lock (`sizeof<=0x328`); its own fields don't land at the binary offsets — - fine until the attribute-table RAW read (the RadarPercent gauge) is wired (a Phase-4 HUD follow-up needing - the heat-leaf branch re-based, same surgery the Watcher branch got). Nothing reads these subsystems at raw - offsets today, so named-member access (compiled-consistent) is correct meanwhile. -- **Factory now: 14 of 20 cases wired to real ticking classes** (was 10). Remaining stubs: Myomers (0xBC6, - WAVE 6), ProjectileWeapon/MissileLauncher + the unresolved 0xBCE (0xBCD/0xBD0, WAVE 7), SubsystemMessageManager - (0xBD3, WAVE 8), Gyroscope (0xBC4, deferred — NaN integrator). Kept DEFAULT-ON (no env gate — pure readouts, - no bring-up fallback needed). -**✅ WAVE 6 (Myomers 0xBC6, mover-coupled) — STRUCTURAL un-stub DONE (gated `BT_MYOMERS`, default ON; verified: -BLH tick 26→27, locomotion + combat un-regressed, 0 crashes; `BT_MYOMERS=0` falls back to the Actuator stub → -26).** The real Myomers (the mech's artificial-muscle drive — voltage→`speedEffect`, throttled by heat + -damage; ctor @4b8fec, `PoweredSubsystem:HeatSink` leaf) is now built at factory case 0xBC6 ("Actuator" -mislabel) via `CreateMyomersSubsystem`. **KEY: it is wired INERT** — it constructs + ticks its real Performance -but does NOT affect gameplay, which is exactly what makes a mover-coupled subsystem safe to land: (a) -`MyomersSimulation` early-returns on `OwnerAdvancedDamage()` (the advanced-damage sim gate; the real source is -the still-stubbed SubsystemMessageManager 0xBD3, so it's a `False` constant → no motion-heat); (b) the mover -feed (`ConnectToMover`→`MoverAttach`) + `SetOwnerMaxSpeed` are no-op/local stubs → **the live JointedMover is -never touched → the gait/drive cannot regress** (confirmed: the mech walks normally, pos advances, adv ramps -0.13→1.05). The ctor DOES do real work: resolves the powering Generator (`ResolveVoltageSource`) + builds the -`seekVoltage[]` gear table from the resource. **De-shim:** the ~10 cross-family Mech/Mover/DamageZone shim -BACKING fields (ownerBaseSpeed/…/damageStructureLevel + segmentFlags) were DROPPED and their accessors return -the neutral defaults the ctor primed — necessary because the real object is EXACTLY 0x358 (zero headroom) and -the shim fields would overflow the alloc; locked `static_assert(sizeof(Myomers)<=0x358)` (heat-leaf branch not -byte-exact → overflow-lock only, like Sensor). The `Myomers` **name collision** is already handled in -myomers.hpp (`#define Myomers Myomers__powersub_stale` around the powersub.hpp include, since powersub.hpp -still carries the OLD Sensor-mislabel "Myomers" — that stale class should eventually be deleted from -powersub.hpp). **⚠ AUTHENTIC COUPLING = the real Wave-6 payoff, DEFERRED:** making the mech actually SLOW when -damaged/overheated + motion GENERATE heat needs (1) the advanced-damage gate from a real messmgr (0xBD3, -WAVE 8), (2) the `owner*` shims redirected to real Mech motion/mass/motion-gain accessors, and (3) -`MoverAttach` actually routing `speedEffect@0x31C` into the JointedMover roster — which **drives the LIVE -mover**, so it must be reconciled with the gait cutover FIRST (both would feed the mover) before enabling. -Factory now: **15 of 20 cases wired to real classes** (remaining: ProjectileWeapon/0xBCE/MissileLauncher -0xBCD/0xBD0 [WAVE 7], SubsystemMessageManager 0xBD3 [WAVE 8], Gyroscope 0xBC4 [deferred]). - -**✅ WAVE 7 (projectile/missile weapon families) — Phase A DONE: the three weapon SUBSYSTEMS are wired -byte-exact (un-mislabeled per VDATA.h enum); Phase B (flying projectile/missile ENTITIES) deferred.** -An 8-agent decomp-mapping workflow (scratchpad/wave7_maps.txt) mapped every ctor + fire/spawn path. -**KEY IDENTITY CORRECTION (VDATA.h enum, base 3000=0xBB8):** 0xBCD=**ProjectileWeapon**, 0xBCE=**GaussRifle** -(NOT MissileLauncher!), 0xBD0=**MissileLauncher** (NOT BallisticWeapon). The factory ctor-address COMMENTS were -right but the built CLASS names were stubs/base. Un-mislabeled + wired via `CreateSubsystem` bridges -(mirroring CreateEmitterSubsystem): **0xBCD→ProjectileWeapon** (@4bc3fc : MechWeapon, sizeof **0x448** — FIX: -`AmmoBinConnection` was an empty 1B struct → retyped to the binary's 0xC SharedData::Connection, was making the -object 8B short); **0xBD0→MissileLauncher** (@4bcff0 : ProjectileWeapon, sizeof **0x44C** — FIX: deleted -`muzzleVelocity` [ALIAS of inherited `launchVelocity@0x410`] + `salvoCount` [SHADOW of `damageData.burstCount@0x3d4`], -keeping `missileCount@0x448`); **0xBCE→GaussRifle** (@4bdcb4 : Emitter, sizeof **0x484** = Emitter+Vector3D -muzzleVelocity — a DEDICATED bridge, not CreateEmitter, because its FireWeapon is a NO-OP `mov [eax+0x414],0` -= a non-functional weapon in this 1995 build; also fixed GAUSS.CPP's discharge write `rechargeLevel`→`currentLevel@0x414` -per the binary). All three `static_assert`-locked (offsetof/sizeof). Verified: Mad Cat (LRM/ballistic) + BLH -construct the real weapon classes + tick authentic charge/ammo/heat/recoil, DESTROYED-in-8, 0 crashes, -heapcheck-clean through construction. Factory now **18 of 20** (remaining: SubsystemMessageManager 0xBD3 -[WAVE 8], Gyroscope 0xBC4 [deferred]). -**✅✅ WAVE 7 Phase B DONE — the Mad Cat's LRMs now LAUNCH flying missiles that fly + damage (autocannon rounds -too).** The byte-exact WORLD-ENTITY reconstruction (Projectile @4be1bc 0x340 / Missile @4bf5b4 0x368) is -**infeasible on the engine**: measured `sizeof(2007 engine Entity)=0x1BC` vs the 1995 binary's 0x300, so the -reconstruction's raw base-offset reads (velocity@0x1dc, roster@0x124, motion@0x250) read GARBAGE on the engine -(the Mech 0x638-vs-0x854 gap, but the entity INTEGRATOR depends on those offsets). So — like the mech drive / -beam renderer — flying projectiles are a **PORT reconstruction** (`BTPushProjectile`/`BTUpdateProjectiles` in -mech4.cpp): seeded from the launcher's fire with the decomp's real muzzle (`GetMuzzlePoint`) / launch speed -(`|launchVelocity|`) / per-shot damage (`damageData`, split across `missileCount`), they fly to the target -(tracer via `BTPushBeam`) and deliver the weapon's damage on impact through the SAME `Entity::TakeDamage` path as -the beam (aim `Mech::FirstVitalZone()`). **THREE bring-up fixes were needed to make a projectile weapon fire at -all** (all found by tracing, marked): (1) **the trigger** — `fireImpulse` was only driven for the Emitter; -`ProjectileWeaponSimulation` now sets `fireImpulse = gBTWeaponTrigger` too (else `CheckFireEdge` never sees an -edge); (2) **the ammo bin** — `OwnerSubsystemCount/OwnerSubsystem` were stubbed `->0` so `ammoBinLink` never -resolved and `ConsumeRound` always failed; redirected to the real roster (`owner->GetSubsystemCount()/ -GetSubsystem(i)` — the AmmoBin 0xBCB constructs before the weapons 0xBCD/0xBD0); (3) **the jam roll** — -`UniformRandom()` was stubbed `return 0.0f`, so `CheckForJam`'s `0 < jamChance` ALWAYS jammed (a projectile -weapon could NEVER fire); replaced with a real LCG `[0,1)` (fires ~1-jamChance of the time — authentic occasional -jams). Also: the mech's own target slot (`owner+0x388`) isn't populated in bring-up (the visible fire targets the -`gEnemyMech` global), so `BTPushProjectile` **falls back to `gEnemyMech`**. Verified: Mad Cat PUSH=62 / IMPACT=31 -(LRM missiles 3.33 dmg each split across the salvo + AFC100 autocannon 25 dmg), TARGET DESTROYED, 0 crashes; BLH -un-regressed (also fires its ballistic weapon now). Diagnostics `BT_PROJ_LOG` (`[projectile] PUSH/IMPACT` + -per-weapon fire trace). **The energy weapons (Emitter/PPC) still damage via the mech4 beam path.** REMAINING -(deferred): the byte-exact world-entity Missile (`Projectile : Mover`, MP-replicable via `Registry::MakeEntity`) -— the port projectile is master-local only (no MP replication); the real per-weapon fire-rate/heat wiring off -the subsystem sim (the mech4 beam path is still the bring-up harness). -**✅ HEAT-FLOW LINKAGE CONNECTED — weapon heat now conducts to the central sink** (verified: `[heat] -conduct self.E=.. -> other.E`, sustained fire `FIRED #1..#21+`, combat intact, no crash). THREE fixes, -each faithful to the decomp (no stand-ins): - 1. **EmitterSimulation ran a no-op stub** `FUN_004b0bd0` for the "PoweredSubsystem step" -> the emitter - never ran its heat sim. Replaced with the REAL `PoweredSubsystem::PoweredSubsystemSimulation` - (@004b0bd0, chains to `HeatSink::HeatSinkSimulation` = absorb pendingHeat + `ConductHeat`). Emitter is - `Emitter:MechWeapon:PoweredSubsystem:HeatSink`, so the qualified base call is valid. (emitter.cpp) - 2. **Linked-sink Add-gate read the wrong flags.** Raw decomp of the HeatSink ctor @004adda0 gates BOTH - the Performance-install AND the `linkedSinks.Add` on `param_2+0x28` = `owner->simulationFlags`, NOT - the per-segment `subsystem_resource->subsystemFlags` (which stream 0 -> dead gate -> no links). The - reconstruction had only half-fixed this (Performance gate on owner, Add-gate still on resource). - Fixed the Add-gate to owner flags too. (heat.cpp) The link TARGET is still per-subsystem `heatSinkIndex`. - 3. **⚠ NEW SYSTEMIC BUG — resource-struct layout mismatch (a sibling of the field-shadowing bug).** - `HeatableSubsystem__SubsystemResource` inherited `Subsystem::SubsystemResource` (ends 0x30) instead of - `MechSubsystem__SubsystemResource` (ends 0xE4) -- the class hierarchy is `HeatableSubsystem:MechSubsystem` - so the RESOURCE must mirror it. Result: every heat field slid 0xB4 low; `heatSinkIndex` read a - neighbouring float (`10.0f`=1/thermalMass) -> `GetSegment` OOB -> no link, and thermalMass/startTemp - were garbage. Fixed the base. Then an 8-byte residual gap: `alarmModel` was typed as a 4-byte `ResourceID` - (`typedef int`), but the RECORD reserves a 12-byte field at 0xCC (raw `@004ac9ec` parses it via - FUN_00408944 as 3 floats, with `PrintSimulationState` at 0xD8). Added `int _alarmModelReserved[2]`. - Now `startTemp=0xE4 thermalMass=0xF4 heatSinkIndex=0xF8` EXACTLY match the binary; heatSinkIndex reads - real indices (5,2,4,..), thermalMass real values (1.39e6..), links resolve, ConductHeat flows. - **DIAGNOSTIC TECHNIQUE (reusable for any resource-layout doubt):** log compiled member offsets via - `(char*)&res->field - (char*)res` and compare to the binary's known offsets; dump raw record bytes as - int+float side-by-side to see the true on-disk layout (records are pre-built 256-byte blobs loaded verbatim - at fixed offsets, so OUR struct MUST match the binary or every member read is wrong). - **NOTE on the payoff:** with the REAL data, `currentTemperature` stays ~77 (startTemp) because `thermalMass` - is genuinely huge (~1e6) vs ~0.24 heat/shot; the visible metric that responds to fire is `heatLoad` (the - smoothed radiated-heat reading), which rises/decays with firing. Not a bug -- authentic data behavior. - (The "linked `other` sink reads heatEnergy~0" follow-up is RESOLVED — the link target was WRONG; see the - heap-corruption root cause below.) -**✅ THE BGF-LOAD HEAP CORRUPTION — ROOT-CAUSED & FIXED (was: `LoadBgfFile("bld08.bgf")` → `Builder::~Builder` - → AV in `operator delete`, position-dependent).** A 4-agent workflow + hands-on forensics converged: - - **The loader was INNOCENT** — an exact Python mirror of `bgfload.cpp` swept all 879 content BGFs with zero - anomalies; every Builder write is bounds-guarded; and the crashed 0x768 block is exactly the correct - 474-float capacity for bld08's 472 verts (MSVC 1.5× growth ...316→474). The AV was ntdll's free-list walk - reading `0xDDDDDDDD` (the CRT freed-fill) inside the ADJACENT block's NT heap metadata — prior corruption, - merely DETECTED at the loader's teardown. `BT_PROBE_BGF=` + `BT_PROBE_N` (btl4main.cpp) now - direct-loads models at boot for regression (bld08 clean 25×; bld08_lp is a bound-only file, benign FAIL). - - **THE CULPRIT — a TYPE CONFUSION in the heat/power linkage (reconstruction bug):** `HeatSink`'s ctor - resolved its linked sink via `owner->GetSegment(heatSinkIndex)` — the Nth skeleton **EntitySegment** - (288 bytes) — cast to `Subsystem*`. The binary (`@004adda0`, part_012.c:16999) instead reads - **`owner->subsystemArray[heatSinkIndex]`** (the SUBSYSTEM ROSTER @0x128, bounds-checked vs - subsystemCount@0x124, null-guarded). Through the bogus pointer, every per-frame `ConductHeat` wrote - `other->pendingHeat` at offset 388 = **100 bytes past the 288-byte block**, and `BalanceCoolant` wrote - `coolantLevel` 20 bytes past — thousands of 4-byte OOB heap writes during sustained fire → smashed NT - free-list links next to the mech's EntitySegments → the next big alloc/free churn (a lazy building load) - AV'd. **Sibling bug in `PoweredSubsystem`** (raw part_013.c:1198): `voltageSourceIndex` (res+0xFC) is - ALSO a roster index; the GetSegment draft made `AttachToVoltageSource` write `currentTapCount` 136 bytes - past the segment block at every mech spawn. BOTH fixed to the roster lookup via the public - `owner->GetSubsystemCount()/GetSubsystem(i)` (+ the powersub else-gate fixed to OWNER flags per raw). - Verified: sustained combat under `BT_HEAPCHECK=1` (`_CRTDBG_CHECK_ALWAYS_DF`, whole-heap validation on - every alloc/free — now a runtime env gate in btl4main.cpp) = 100+ shots, ZERO detections; the heat link - now reads a REAL sink (`heatEnergy=1.34e+07`, was ~0 through the segment). **LESSON (add to the systemic - checklist): a `+0x128`-style owner offset in subsystem code is the subsystem ROSTER (`subsystemArray`), - NOT the segment table — check every `GetSegment(int)` call in reconstructed subsystem ctors.** - - Bonus findings from the audit (non-corrupting, tracked): `keyframeData[keyframeCount]` in - LoadLocomotionClips is BINARY-FAITHFUL (the raw does the same read; the value is stable/in-bounds in real - clip data — do NOT "fix" to [count-1]); crashClipA/B/C+gimpBaseClip are separate members here but live - INSIDE `animationClips[]` in the binary ([3]/[2]/[4]/[32]) — a wrong-clip fidelity gap for crash/gimp gait - states when those go live; `Mech::CreateModelResource` alloc 200 < sizeof 356 is a LATENT overflow in dead - offline-tool code; Torso 640==0x280 alloc is an EXACT fit (zero headroom — any Torso/Watcher growth must - bump the factory alloc); StatusMessagePool->New() at btplayer.cpp:460 derefs a NULL stub pool (latent); - mechdmg.cpp:289 leaks IntegerPlugs into a no-op DZIndexTable::Add. -**✅ RESOURCE-LAYOUT AUDIT — DONE (full report: `docs/RESOURCE_AUDIT.md`; commits 802a7a6/7de66ad/975a397).** -A 33-agent adversarial audit of all 24 `*__SubsystemResource` structs found **8 confirmed layout bugs** (same -silent-garbage class as the heat fix), all rooting to 2 broken base resources (`HeatWatcher`, `PowerWatcher`) -+ 2 field defects (`Gyroscope`, `Searchlight`). Raw-decomp-verified + FIXED: **HeatWatcher re-based onto -MechSubsystem** (record 0xF0; class+resource+ctor+parser, AmmoBin child follows) and **PowerWatcher re-based -onto HeatWatcher** (record 0xF4; HUD/ThermalSight/Torso children follow) — the reconstruction had approximated -BOTH as `: public HeatableSubsystem`, sliding every Watcher field low. Gyroscope got its unread +0xF4 record -slot (exageration→0xF8); Searchlight now reads the inherited `segmentIndex`@0x28 (was a bogus appended field). -Combat + heat-flow un-regressed. **KEY PRACTICE ESTABLISHED: lock every resource-struct layout with -`static_assert(offsetof(...)==0xNN)` + `static_assert(sizeof(...)==0xNN)`** against the binary's known offsets — -compile-time proof that can never silently regress (far better than a runtime log; the test mech may not even -instantiate the subsystem). Cascade notes: re-basing a Watcher class off HeatableSubsystem un-shadows its -`degradation/failureTemperature`; child `HandleMessage`/`Simulation` delegates that called `HeatableSubsystem::` -must redirect to the real base (`MechSubsystem::HandleMessage`, `HeatWatcher::ResetToInitialState`); a re-based -class's header needs a guarded `#include` of its new base's header (powersub.hpp now includes heatfamily_reslice.hpp). - -**✅ HEAT-LEAF OBJECT-LAYOUT RE-BASE — the CORE heat leaf is now byte-exact (P7 core DONE; full status -`docs/HARD_PROBLEMS.md` §P7).** The OBJECT-layout analog of the resource-layout audit: the reconstruction modeled -the binary's shared alarm/connection types with UNDERSIZED stand-ins, sliding every field above them low (the -72-byte `PoweredSubsystem::auxScreenNumber` gap). FIXED byte-exact + `static_assert`-LOCKED: **`HeatSink`** -(heatEnergy@0x158/linkedSinks@0x164/heatAlarm@0x170/pendingHeat@0x1C8, sizeof 0x1D0), **`Condenser`** (0x230 -region), **`Reservoir`** (sizeof 0x230), **`Generator`** (sizeof 0x250), **`Myomers`** (phantom tail removed). -The shared types are now correct: **`SubsystemConnection` = 0xC** (was 4; binary link node, `FUN_00417ab4` -two-level-derefs +8) and **`GaugeAlarm54` = 0x54** (the real `AlarmIndicator` `FUN_0041b9ec`; STATUS level at -**+0x14**, so `subsystem+0x184 == heatAlarm+0x14 == GetLevel()`; `WatcherGaugeAlarm` typedefs to it). Heat -conduction reads the REAL `heatEnergy=1.34e7`; combat DESTROYED-in-8, 0 crashes, heapcheck-clean through -construction. **THREE new systemic bug classes fixed (add to the checklist):** (1) an **alias field** = a subclass -member re-declaring an inherited slot the ctor reuses under a new name (Condenser `refrigerationOutput`==inherited -`massScale@0x160`; Reservoir `coolantCapacity`==`thermalCapacity@0x128`; Emitter `outputVoltage`==inherited -`rechargeLevel@0x320`; Emitter `beamLengthRatio`==`beamScale.z@0x434`; Emitter `firingArmed`==inherited -`useConfiguredPip@0x3E0`) → delete the member, use the inherited/existing name; (2) an **alarm-interior field** = a -value the binary reads at `alarm+0x14` modeled as a separate member (HeatSink `heatState@0x184`, Reservoir -`injectActive@0x1e4`) → route to `alarm.GetLevel()`; (3) a **phantom field** = a member at an offset PAST the object -(Generator `shortFlag@0x25C` is really `*(owner+0x190)+0x25c` the msg-manager; Myomers `moverConnection@0x110` a -write-only base slot; MechWeapon `segmentReference/pipSegment/hasTarget/targetPoint/muzzlePoint` past 0x3F0; Emitter -`beamHitPoint/beamImpact/beamImpactScalar/beamColor/beamHitData/energyRampTime` — binary writes into inherited -`damageData`/`voltageScale` or the value is a method local) → remove it, read the real source or use a local. - -**✅✅ SUBSYSTEM-TREE ALARM UNIFICATION — DONE: the WHOLE PoweredSubsystem weapon/power subtree is now byte-exact -(P7 CLOSED; full per-class map from the 8-agent `alarm-unification-map` workflow, saved scratchpad/alarm_unify_maps.txt).** -Making `PoweredSubsystem` byte-exact (its two 0x54 alarms `electricalStateAlarm@0x264`/`modeAlarm@0x2B8`) grows it -+0x98, cascading into every subclass — so all were byte-exacted TOGETHER in one build (each `static_assert`-locked -against its ctor + factory alloc; the chain HeatSink 0x1D0 → **PoweredSubsystem 0x31C** → **MechWeapon 0x3F0** → -**Emitter 0x478** → PPC 0x478; PoweredSubsystem → **Sensor 0x328**, → **Myomers 0x358**, all verified against the raw -ctors @004b0f74/@004b99a8/@004bb120/@004b1d18/@004b8fec). Each class: retype its `AlarmIndicator`(==`ReconAlarm`==4B) -/`HeatAlarm`(8B) stand-in for the binary 0x54 alarm → `GaugeAlarm54`; delete phantom/alias/duplicate fields; -`friend struct XLayoutCheck` with `offsetof`/`sizeof` locks (protected members need the friend; public fields work -at namespace scope). **KEY non-layout fixes required in the same wave:** (a) `outputVoltage`→`rechargeLevel` also in -the compiled **`GAUSS.CPP:74/93`** (external readers of the removed Emitter field — grep EVERY TU, not just the -class's own .cpp); (b) `MechWeapon::GetMuzzlePoint` reimplemented faithfully (the removed `muzzlePoint` member -collided with Emitter's own fields at 0x3F0) via a **`BTResolveWeaponMuzzle` void\* bridge in mech4.cpp** (a -complete-Mech TU with the segment API; mechweap.cpp reaches `owner` only as a raw pointer) resolving the weapon's -mount segment (inherited `this+0xdc`) through `GetSegment(int)`+segment→world; (c) `DetachFromVoltageSource` was -setting the WRONG alarm (`modeAlarm`→`electricalStateAlarm`, raw @004b0e30 writes 0x264). Verified: combat -DESTROYED-in-8, 28 shots, 0 crashes, heat `heatEnergy=1.34e7`, all locks pass. **The vehicleSubSystems aux-screen -gauge raw reads (`btl4gau2.cpp:868/952` at `subsystem+0x2b8`/`+0x278`) + the Sensor RadarPercent path now read the -CORRECT byte offsets** (they were garbage under the short layout). ⚠ `runtime Check(sizeof<=alloc)` in a factory -bridge does NOT fail the build (it's a runtime assert → heap overflow at construction); use a `static_assert` -sizeof lock to catch alloc overflow at COMPILE time (this is why the earlier isolated PoweredSubsystem retype -silently overflowed the Emitter alloc). **TECHNIQUE:** a read-only decomp-mapping Workflow (one agent per class -decodes its ctor → offset map + phantom/alias findings, + adversarial verify of the composed chain) then hands-on -implementation — the right shape for a systemic multi-class byte-exact re-base. - -**✅ MechControlsMapper REVIVED — the real input-interpretation tick runs LIVE (gated `BT_REAL_CONTROLS=1`).** -The fully-reconstructed mapper family (mechmppr.cpp `MechControlsMapper` @004afbe0-@004b08c0; btl4mppr.cpp -`L4MechControlsMapper`/`MechThrustmasterMapper`/`MechRIOMapper` @004d11e8-@004d27f8) was already built + -constructed at runtime (btl4app `MakeViewpointEntity` builds a `MechRIOMapper` into roster slot 0 via -`SetMappingSubsystem`); the ONLY bypass was the tick being skipped (mech4.cpp roster walk) because of raw-offset -bugs. FIXED (a 4-agent workflow mapped the binary/API/state first): (1) **wild typed-pointer arithmetic** in -`InterpretControls` (`*(Scalar*)(mech + 0x34c)` on `Mech*` = +0x34c*sizeof(Mech) — THE AV) → all owner reads -now reconciled members: `mech+0x34c→reverseStrideLength` (naming caveat: it is the TOP/run speed the throttle -scales — CONFIRMED by data: speedDemand=61.5 == the authentic run speed), `0x534→walkStrideLength`, -`0x5c0→forwardThrottleScale` (NEW member; writer not in decomp, ctor=1.0), `0x438→sinkSourceSubsystem` (real -binary-exact Torso: `SetAnalogTwist/ElevationAxis`@0x1F0/0x1F4, `GetHorizontalEnabled`@0x250), -`0x5b4→hudSubsystem`; (2) **the 0x5b4 cache was MISWIRED**: raw factory (part_012.c:10155-10164) case 0xbd6 -(HUD, alloc 0x2a4) writes `param_1[0x16d]` — case 0xbdc (MechTech, binary alloc 0x104!) has NO cache; our -factory had it on MechTech (renamed member `mechTechSubsystem`→`hudSubsystem`, cache moved to the HUD case; -MechTech's 0x104 alloc could never hold the mapper's `+0x28c` write); (3) **HUD `flickerPhase`@0x28C renamed -`freeAimSlew`** — it IS the mapper's free-aim/reticle-slew output (was init-only placeholder); (4) -**`FillPilotArray`'s wild app reads** (`application+0x6c→station+0x190` — the OTHER AV) → the native -`application->GetMissionPlayer()` + `GetEntityID()` (the same drift-fix btl4mppr's SendFakeButtonEvent uses); -(5) the look/eyepoint COMMIT deferred (marked): its binary offsets collide with declared members -(`mechName`@0x360, `stateFlags`@0x410, `poweredSubsystems`@0x7bc) — arbitrate before enabling; no port consumer. -**Wiring (mech4.cpp, gated):** an INPUT BRIDGE writes the mapper's published input attributes from WASD/ -env exactly as a streamed Direct `.CTL` mapping would (the dev box has no RIO/Thrustmaster; interpretation -stays 100% authentic), the roster walk un-skips the mapper, the SM consumes `bodyTargetSpeed=|speedDemand|` -and the drive heading uses `turnDemand` (mode-shaped steering with the authentic speed-vs-turn clamps). -**Verified live:** `[mppr] thr=1 → speedDemand=61.501` → SM walk→run at the commanded speed; full tick chain -(RIO→L4→main + BuildPilotArray) clean; baseline (env off) combat un-regressed (FIRED #221); heapcheck run -with real controls + combat = 61+ shots, zero detections. **Controls plumbing facts (from the workflow):** -the engine is a PUSH model — `LBE4ControlsManager` groups (`scalarGroup/joystickGroup/keyboardGroup/ -buttonGroup`) are fed by ALL devices (RIO serial on the pod, DirectInput on dev) and mapper members register -via `group[elem].Add(mask,&member,this)`; `L4CONTROLS=KEYBOARD` gives NO analog axes (key-RELEASE events -only) — so a real stick needs `L4CONTROLS=DIJOYSTICK`/`THRUSTMASTER`, or the bridge. **Remaining mapper -work:** fire-trigger through the mapper (streamed `.CTL` event mappings → weapon demand; gBTWeaponTrigger -still the bring-up), look/eyepoint offset arbitration, control-mode cycling (message handlers exist), -`ControlsMapper`-vs-`SubsystemMessageManager` untangle at factory 0xBD3 (binary 0xBD3 = messmgr; the mapper's -canonical home is roster slot 0 — our 0xBD3 case builds a base mapper as a stand-in). - -**✅ PILLAR A — THE MOTION MODEL IS COMPLETE & DEFAULT-ON (locomotion P3 closed for MP purposes).** -(1) **LEG callback reconstructed** (`FUN_004a6928` == `PTR_LAB_0050d6f0`, PE-parse + capstone jump table -@0x4a69aa): `Mech::LegClipFinished` — same 33-state shape as the body's but compares the LIVE mapper -`speedDemand` (binary: `*(subsystemArray[0])+0x128`; typed mirror `controlsMapper`), slews `legCycleSpeed` -@0x348, re-arms via `SetLegAnimation`, gimp alternates 0x12↔0x13 with |ratio| (negative gimp stride). -(2) **The two-channel split is LIVE** (under real-controls): the LEG channel writes the skeleton JOINTS -from the live demand, the BODY channel is pure motion (`Advance(dt, move_joints=0)`) — the binary's -leg=local-sim / body=displayed-motion model. The leg enters NATURALLY from Standing: verified progression -legState 0→5 (stand→walk) →11 (walk→run) →13↔12 alternation converging on the commanded 61.5 u/s. -**KEY RAW FIX: the Standing case was INVERTED in BOTH channels** (recon had `commanded < standSpeed → -walk`; raw FUN_004a5028/FUN_004a5678 case 0 is `standSpeed < commanded → SetAnimation(5)`, `<0 → reverse -0x10`, else stand) — without the fix a commanded mech never left Standing. Also fixed: `AdvanceLegAnimation` -double-deref'd the never-initialized `controlSource` alias (AV) → reads `controlsMapper->speedDemand` -(null → 0 = idle); `legAnimationState`@0x3b0 == `legStateAlarm.level` re-synced (same as body). -(3) **Orientation** now composes the yaw RATE into `localOrigin.angularPosition` via the engine integrate -op (`Quaternion::Add` == FUN_00409f58) instead of rebuilding from a scalar heading — the mech keeps its -authentic spawn orientation (rate CONSTANT still bring-up; the per-mech turn-rate param is deferred). -(4) **Velocity storage every frame** (was collision-gated): `worldLinearVelocity` from the frame delta + -`localVelocity` (linear fwd −Z + yaw angular) — the exact fields `Mover::WriteUpdateRecord` publishes = -the MP dead-reckon update-writer data. `DeadReckonPose` was already reconstructed; consuming it is MP-side. -(5) **Defaults flipped** (see §10a). Consequence of the authentic spawn orientation: `BT_SPAWN_ENEMY` now -places the dummy along the spawn FACING (btplayer.cpp; the fixed z-offset assumed the old forced heading -and the mech walked away → "no damage" red herring). Verified default-stack combat: structure→0.933. -**Remaining locomotion polish (non-gating):** authentic turn-rate constant; body-callback gimp handlers -(targets 0x70b2/0x7161 undecoded); airborne callbacks (`FUN_004a6344`/`FUN_004a7970`); wall-block-vs-climb -tuning; collision damage application; the eyepoint/look commit (mapper). - -**✅ SHADOW — the AUTHENTIC 1995 model decoded + implemented, render-verified (task #20, commit b38e94b).** -BT 4.10's shadow is NOT a projection: it is the flat **`*_tshd.bgf` proxy** drawn as ordinary geometry, -posed by **`jointshadow`** (balltranslate under ROOT; SKL comment "apply terrain angle to pitch and roll") -and its child **`jointtshadow`** (hingey; "apply torso twist to yaw", Torso record key -`TorsoHorizontalShadowJoint`). Configured by the model record's **`ShadowJointName` @0xB4** (≤19 chars, -"Unspecified"=none), resolved in the Mech ctor (part_012.c:10285-10310 → **`this[0x10b]` @0x42c** = the -shadow Joint — the earlier recon misread this block as a death-effect lookup; now `Mech::shadowJointNode`). -`dpl_SetGeometryShadowVolume` exists in libDPL headers but has NO surviving game callers. Port implementation: -(1) `d3d_OBJECT::mIsShadow` + a dedicated DrawMesh path — blend-pass only, SRCALPHA/INVSRCALPHA, no Z-write, -constant TFACTOR `0x8C000000` (~55% black), all touched states saved/restored exactly; btl4vid tags `tshd` -objects at load AND sets each drawOp `alphaTest=true` (**pass scheduling fact:** `HierarchicalDrawComponent:: -Execute` L4VIDRND.cpp:149 routes objects into PASS lists per-drawOp on alphaTest/drawAsDecal/drawAsSky — -an object with all-false drawOps only ever reaches PASS_OPAQUE). (2) `Mech::UpdateShadowJoint(normal)` -writes pitch/roll to jointshadow per-frame. **✅ SHADOW TERRAIN-TILT NOW LIVE (was the flat-up placeholder; -default-on, gate `BT_SHADOW_TILT=0` restores flat).** The user reported the shadow DISAPPEARS on elevation + -the feet look sunk. Root cause: mech4.cpp fed `UpdateShadowJoint` a hardcoded flat-up normal, so on a slope -the flat `*_tshd` quad was buried in the hillside and Z-culled (it draws Z-tested, no Z-write, in -PASS_ALPHABLEND) → gone; and the missing ground-contact cue made the feet READ as sunk even though the origin -is measurably ON the surface (alignment probe at the mech's climb positions: visual−origin = 0.05–2.7u, i.e. -NOT a placement bug — the snap is correct). Fix (mech4.cpp shadow block): now that the ground decode (#15) is -live, sample the collision surface via `GetMoverCollisionRoot()->FindBoundingBoxUnder` at the mech's XZ + two -world-axis offsets (D=12u), build the world normal from the height gradient `(-dH/dx,1,-dH/dz)`, rotate it into -the mech-local frame by `-gDriveHeading` about Y (mech is upright, yaw only), and pass it in — jointshadow's -SKL comment is literally "apply terrain angle to pitch and roll". A probe miss (mech at its collision node's -edge) falls back to flat. **Render-verified:** on a butte-flank slope the shadow now LIES ON the ground -(tilted, follows the slope) and anchors the feet; pre-fix the same steep mid-climb showed NO shadow; combat -un-regressed (TARGET DESTROYED after 8 hits), 0 crashes / 0 heap detections under BT_HEAPCHECK. NOTE the probe -uses the mech's `containedByNode` so far offsets can miss (→ flat fallback); a residual uphill-foot dip on -steep slopes is the authentic upright-biped geometry + coarse-collision (0–2.7u), not a placement bug. (3) -Torso yaw→jointtshadow was already wired (Torso::UpdateJoints; data-disabled on the BLH record, byte-faithful). -**✅ THE "MECH SINKS INTO INCLINES + SHADOW DISAPPEARS" BUG — ROOT-CAUSED & FIXED (shadow DEPTH BIAS, -L4D3D.cpp mIsShadow block; BT_SHADOW_BIAS= tunes, =0 restores plain Z-test).** The user reported feet -sinking + shadow vanishing on ANY moderate incline (original vids show neither). Systematic elimination: -(a) flat ground placement EXACT — dg100's instance Y is −0.453125 (map dumps printing "−0" hid it); visual -floor AND slab top both at −0.4531 = the mech's resting Y, zero offset; (b) snap faithful (origin = solid -surfaceY, verified decode); (c) LOD selection correct — BGF LODs are FINEST-FIRST (dhillg1 136→47 tris) and -the loader takes the first; ⚠ DG100 the desert-floor model is PERFECTLY FLAT (all verts Y=0) — the rolling -desert relief is the giant dhillg mounds (r≈950); (d) LOD0-exact content gap (earlier merged-LOD measurements -were inflated): mound visual runs 0–2.1u ABOVE the collision cones on slopes (median 0–0.5, p90 1.2–1.5) = -ankle-to-shin foot clip at worst patches — authentic, the binary had the same. **THE REAL RENDER DIVERGENCE: -the shadow proxy drew with a plain Z-TEST, so wherever the visual rose even ~0.3u over the quad the WHOLE -shadow was buried+culled — and without its ground-contact cue the small authentic foot-clip reads as "the -mech is sinking".** The IG board drew ground shadows decal-style (depth-biased coplanar geometry — the classic -blob-shadow technique; the shadow FLICKER in pod footage is that bias fighting at edges). FIX: the mIsShadow -draw now sets `D3DRS_DEPTHBIAS` (default −0.004 normalized ≈ 2–4u at chase range) + `SLOPESCALEDEPTHBIAS −1`, -saved/restored exactly. **Render-verified:** standing mid-slope at the WORST measured gap spot (606,621 on -dhillg2) = shadow fully present lying on the slope + feet visible planted (was: shadow gone, feet "buried"); -moderate inclines while walking ✓; flat ground crisp, no bleed; combat un-regressed (DESTROYED after 8 hits, -0 heap detections). Note the residual ≤2.1u foot-clip at worst patches is authentic content coarseness — with -the shadow anchoring the mech it reads as standing in soft sand, matching the vids. (A whole-mech terrain-tilt -experiment was tried and REJECTED — the 1995 mech is upright-only; do not tilt the body.) `BT_CAM_Y`/`BT_CAM_Z` -(btl4vid.cpp) override the debug chase-camera offset — diagnostic for separating camera occlusion from real -geometry clipping. -**✅✅ VISUAL-GROUND CONFORM — the residual foot-clip is now GONE too (btvisgnd.cpp, PORT ADDITION, -presentation-only; gate `BT_VISUAL_GROUND` default ON).** The user (rightly) rejected "authentic ≤2.1u clip": -the resolution to "how did the devs let this slide" is **the 1995 game never rendered this view** — it was -cockpit-only (you can't see your own feet; other mechs are 100+m away = subpixel), so the artists never needed -visual-solid agreement; OUR port adds a close external camera, so the port also owns making it correct. The fix -samples the ACTUAL rendered terrain triangles under the mech and lifts the RENDER matrix to the visible surface: -at mission load, walk the map's MakeMessage stream (`mission->GetMapID()` → `FindResourceDescription` → Lock; -class-42 instances, skip includes/sky/profile/rwin backdrops), load each terrain model's LOD0 triangles via the -ENGINE `LoadBgfFile` (bgfload.h — game-side callable; indices are double-sided, 6 per tri, take first 3), cache -per-model + per-instance world XZ AABBs; per frame `BTVisualGroundLift(x,y,z)` = highest triangle surface at the -mech XZ within a LOCAL band [y−1, y+3] (band keeps a butte-base query on the skirt, not the mesa top; clamp -[−1,+3]) → `localToWorld(3,1) += lift` at the PerformAndWatch tail (mech4.cpp). **`localOrigin` is NEVER touched -— collision/aim/damage/dead-reckoning stay on the authentic solid model; next frame's drive rebuilds localToWorld, -so the lift is per-frame render-only.** Zero tuning constants — exact content geometry both ways (lift up where -visual bulges over the solid, settle down where it dips). **Verified:** flat desert lift≈0 (float epsilon — -confirms both sampler and the flat-exactness finding); full mound crossing lift tracks ±0.02..0.79u continuously; -standing + walking mid-mound = feet ON the visible sand, shadow present (the user's exact repro case); combat -un-regressed (DESTROYED after 8 hits), 0 crashes / 0 heap detections. NOTE: masters only (replicant render-conform -is an MP follow-up); needs cwd = pod BT dir (LoadBgfFile indexes VIDEO/ relative — already a runtime requirement). **⚠ ENGINE-CLASS GOTCHA -(add to the checklist):** a NEW member added to a 2007 engine class (d3d_OBJECT etc.) MUST be initialized -in EVERY ctor init-list — the debug heap fills fresh allocs 0xCDCDCDCD, so an uninitialized flag reads -TRUE (here: every object in the world drew as translucent-black "shadow" → near-black scene); and any -state the shadow/special draw path sets on the D3D device must be save/restored exactly (a leaked -`LIGHTING FALSE` + `ALPHAARG1=TFACTOR` also darkened the whole frame). Render-verified via PrintWindow -A/B on DBASE: baseline opaque black blob → dark translucent silhouette, terrain/feet visible through it. - -**✅ BACKFACE CULLING RESTORED (task #20, commit a2ed21a; `BT_CULL` env cw/ccw/off, default CW).** The -bring-up `D3DCULL_NONE` drew interior/back faces the original culled — the "dark wedge" shapes in the -distance were the INSIDES of dune/butte meshes, and standing inside a mound visual looked like being -sealed in rock. CW is the correct winding for our BGF quad triangulation `[a b c][a c d]` (CCW/none -selectable for debugging). Render-verified on dbase; combat clean. **CONTENT FACT (the "mound swallow" -investigation):** dbase's dune-mound groups (`dhillg*`) have CRESCENT-RIDGE visuals with HOLLOW interiors -but solid FULL CONES (hillg2_c.sld: 2 cones r=200/250 h=36/60) — where the visual exists it matches the -solid within ~1u (verified by triangle-exact BGF parsing vs the analytic cone), but a mech can walk -through a ridge crest into the hollow and ride the invisible cone — AUTHENTIC 1995 coarse collision, not -a port bug. Butte cliff tiers verified BLOCKING (beeline repro: 403 frame rejections, zero penetration). -Analysis tooling now permanent: `[contact]` telemetry in Mech::ProcessCollision, an INSIDE-SOLID streak -detector (BT_GROUND_LOG), `BT_GOTO="x z"` beeline harness (mech4 computes → mapper bridge consumes; the -turn-sign/convergence is rough — debug only), and the RES/map/solid scanners preserved in the scratchpad -(`resscan.py`/`mapscan.py` — worth moving to tools/). - -**✅✅ THE AUTHENTIC DPL ENVIRONMENT IS NOW HONORED — day sky + sun + haze + draw distance (task #20, -commits 5b615e7 + 9ca9a48).** The user compared the desert level to pod footage — ours was dark/muddy with -a wrong sky; the fix transformed it to match. ROOT CAUSE: the env pipeline (`DPLReadEnvironment` → -`DPLReadINIPage`, reading **`BTDPL.INI`** in the pod BT dir — set via `L4DPLCFG`) **already ran and -correctly resolved** the map+time via compare/branch (`main → dpl_defaults → des_day → dsdayclear` for -dbase/day), setting the AUTHENTIC **clip=2100**, **fog=600/2050** (0.46,0.46,0.68 haze), **ambient=0.45 0.4 -0.45**, and a **directional sun** (1.10,0.9,1.00 @ −50,10,0) — but THREE bring-up stubs clobbered it: -(1) `EnsureValidProjection` (the mission-load safety net, btstubs.cpp) forced clip far=1300, killed fog -(`currentFogNear/Far=1e9`+FOGENABLE FALSE), set WHITE ambient → now a FALLBACK that only overrides when the -env failed (`viewAngle<=0 || clipFar<=clipNear`); when valid it just re-asserts the env projection. -(2) the per-frame lighting block re-set `D3DRS_AMBIENT` to gray `0x404040` (~0.25) every frame → now -re-asserts the captured `mEnvAmbient` (from the map's `ambient=`); `BT_AMBIENT=` still overrides. -(3) `mCloudRed/Green/Blue`+`mCloudEmit*` were UNINITIALIZED in the ctor (0xCDCDCDCD garbage into the sky -material multiply) → init 1.0/0.0. **THE SKY:** the sky is a `*sky.bgf` dome (dbase=`dsky`, grass/arena= -`sky`) authored with a cloud TEXTURE (`btfx:dsky_mtl`) + white vertex colours. The original drew it in a -dedicated fullbright **PASS_SKY** (D3DRS_LIGHTING FALSE + extended fog, already present from earlier task-#20 -work), but the BGF loader route hardcoded `drawAsSky=false` (the `.sky` sidecar the `.x` path keys on doesn't -exist), so the dome fell into PASS_OPAQUE → LIT (half-dark) + FOGGED → the dark navy dome. FIX: -`LoadObjectBGF` tags any `*sky*` object `drawAsSky=true` → PASS_SKY → the cloud texture shows fullbright. -**Render-verified dbase A/B:** dark muddy + navy dome → bright warm sand + blue cloudy sky + soft horizon -haze + distant mesas (2100 far). Combat clean. **KEY DEBUG TECHNIQUE:** the env parser's `DPLReadINIPage -processing ` + `Clip/Fog/Ambient/Light` banners are gated on `[main] debug=` — copy `BTDPL.INI` → -`*DBG.INI` with `debug=True`, point `L4DPLCFG` at it, and the log shows every page visited + value set. -**STILL STUBBED (follow-ups):** the `objectpath`/`materialpath`/`texmappath` day/night PRIORITY is commented -out (`dpl_AddToObjectFilePath` calls) — the BGF loader (`bgfload.cpp`) recursively indexes the whole `VIDEO/` -tree **first-match-wins**, which happens to pick `DAY/` (alphabetically first) for day missions, so day is -accidentally correct; NIGHT/morning/evening missions would get the wrong (day) sky+textures, and the grass -map renders pale pink/purple terrain (a wrong-time or wrong material picked). Proper fix = reimplement the -path priority so the loader prefers the env's objectpath/materialpath dirs in order. -**◐ DAY/NIGHT PATH PRIORITY — infrastructure done, materials gated (commit 90a8d92).** `bgfload.cpp` now ranks -every index candidate by the INI's search dirs (most-specific-first) and keeps the best per stem; -`SetVideoPathPriority(cacheKey, dirs)` (bgf/bmf/img) is fed the accumulated objectpath/materialpath/texmappath -lists from `DPLReadEnvironment`. **BGF priority (geometry) is ON; BMF/IMG (materials/textures) are GATED -`BT_MATPRI=1`, default OFF** — a naive "prefer mat\day" over-applies to the SHARED desert terrain materials and -washes the warm day terrain to GRAY (render-verified regression on the user's dbase/day). The sky's day/night is -itself MATERIAL-driven (`btfx:dsky_mtl` in mat\day|night), so a correct night sky needs BMF priority — which -trades against the day terrain; resolving cleanly needs the terrain-material model understood (why mat\day grays -the desert — likely a shared lib whose mat\day variant differs, or a resolution-failure gray fallback). **⚠⚠ -CRITICAL GOTCHA — `/FORCE` turns an unresolved symbol into a runtime crash, NOT a link error.** `SetVideoPathPriority` -was first defined INSIDE bgfload.cpp's anonymous namespace → internal linkage → `L4VIDEO.obj`'s call was -unresolved → the BT game link (uses `/FORCE`) silently stubbed it → a call jumped to `__ImageBase` -(`0x009d0000`), a DETERMINISTIC AV in `LoadMissionImplementation` that looked like heap corruption. Fix: define -it OUTSIDE the anonymous namespace. **When a `/FORCE`-linked build crashes with a garbage call target near the -image base, grep the link log for `unresolved external` — the "successful" build is lying.** Also: `BTL4VideoRenderer` -is allocated by the GAME (btl4app.cpp:242) as a `DPLRenderer` subclass, so ANY member added to `DPLRenderer` -(L4VIDEO.h) needs the game's btl4app/btstubs/btl4vid objects recompiled — CMake header-dep tracking through the -`btbuild/fwd/*` shims is unreliable; delete stale `.obj`s if layout-mismatch corruption appears. - -**✅✅ RENDER FIDELITY — the dbase desert now matches the pod footage (task #20; a 10-agent measured -render-fidelity workflow found 6 defects, 3 fixed = the "big three").** The user compared our dbase to -authentic footage (`C:/git/original.PNG`): ours was harsh/saturated/faceted; the original is soft, pale, -low-contrast. Root causes (measured, not guessed): -1. **⭐ Mountains "inside-out/faceted" = MALFORMED GEOMETRY, not lighting (commit d189371).** `BGF_FORMAT.md` - wrongly documented **`CONNECTION_LIST` (tag 0x0047) as "one polygon per chunk, fan-triangulate"**. It is - actually a **FLAT TRIANGLE LIST** (3 indices/face). Fan-triangulating the whole chunk as one N-gon turned - buttec's 115 real triangles into 341 fan-garbage with smeared UVs + cancelled normals. FIX: group-of-3 - walk in the `!gotPconn` branch (bgfload.cpp buildPmesh + port/src/bgf.cpp). **Corpus-verified: ALL 3488 - CONN chunks in the pod GEO tree have n%3==0.** bgftest BUTTEC.BGF: 341→115 tris. Gated BT_TERRAIN_TRILIST. -2. **Horizon seam = flat sky plane truncated by the world projection (commit 2434bbc).** DSKY is a FLAT plane - at Y~110 (±6000), NOT a dome. The world far=2100 projection truncated it above the true horizon → the gap - showed the fog-colour Clear as a hard lavender band; the old sky-pass fog (near×3/far×6) left the rim - unfogged. FIX: sky-pass gets its own far=9000 projection + the SAME fog as the world (drop ×3/×6), BOTH - restored after the sky loop (mandatory — the alpha/particle/HUD passes need the world projection). Gated - BT_SKY_FAR. -3. **Sky "two-tone navy, nothing like clouds" = MODULATE vs SCREEN (commit 2a8aa37).** The cloud art (bintA) - is a GRAYSCALE intensity map; the sky colour is the material tint (0.3,0.5,1.0) baked into the vertex - diffuse. MODULATE (texel×tint) → saturated navy. The original SCREENed toward white: - `screen(D,T)=T+D·(1-T)=lerp(D,white,T)` → bright texels near-white, dark → pale tint. FIX: one texture - stage `D3DTOP_LERP(Arg0=TEXTURE, Arg1=TFACTOR=white, Arg2=DIFFUSE)`, states saved/restored around the sky - draw. Measured `screen(0.3,0.5,1.0,0.85)=(228,236,255)` ≈ original top `(229,230,255)`. Gated BT_SKY_SCREEN. -**THE AUTHENTIC IG-BOARD SHADING MODEL (from libDPL DPLTYPES.H:189-199):** shading is selected PER-GEOMETRY by -vertex type — no-normal geometry (XYZ_UV / XYZ_RGBA_UV: mesas/ground/sky/buildings/mech, all with WHITE baked -verts) is drawn UNLIT with the texture colorized by the material's 2-endpoint RAMP (`dpl_SetMaterialRamp`, -texture luminance indexes a low→high gradient — rock (0.25,0.21,0.16)→(0.8,0.5,0.4) = warm tan); only ~150 -vehicle/missile files (XYZ_N_UV) carry real normals and were lit by the single map light. -4. **⭐ RAMP colorize + day/night material priority — DONE (commits d5c155b, cc0055a); the ground/mountain - colour match + sky whites.** The ground looked PALER than the mountains with a seam because they use - different material libs: the mountains' `ravinerock_mtl` has a warm diffuse (0.781,0.525,0.353) so they - render warm, but the ground's `grass_mtl` has NO diffuse — it relied ENTIRELY on its colour RAMP, which we - didn't apply → gray placeholder = the pale ground. The authentic IG board colours terrain by the material's - **2-endpoint RAMP** (`dpl_SetMaterialRamp`): texture luminance indexes a low→high gradient. Implemented: - `bgfload` parses the ramp def (0x30 name + 0x31 6-floats) + the material's ramp ref (0x28), threads - rampLo/Hi onto `BgfDrawBatch`→drawOp; `L4D3D` bakes `lerp(rampLo,rampHi,texLum)` into the texture at load - and draws ramped batches with a **WHITE material + WHITE vertex colour** (so the ramp colour shows untinted - — critical: a leftover tint MODULATEs the ramp again). **DAY/NIGHT MATERIAL PRIORITY (bmf/img) is now - DEFAULT ON**: the mat\day libs carry the AUTHENTIC warm ramps (rock 0.25,0.21,0.16→0.8,0.5,0.4 = warm tan; - grass →warm brown); first-match had picked the generic GEO/*.BMF whose ramps are GRAY (0.8,0.75,0.75) — - which only looked warm before because the vertex tint covered for it. The earlier "BT_MATPRI grays the - terrain" regression is GONE precisely because the ramp is now applied. **SKY via its own ramp**: dsky_mtl - has a 'sky' ramp (0,0,0.6→0.99,0.99,0.99) → bright cloud texels → 0.99 near-white = the "whites" the user - wanted; `BT_SKY_SCREEN` (the pre-ramp heuristic) now default OFF. **Render-verified ALL DEFAULTS**: dbase/day - = pale pastel sky + soft white clouds + uniform warm tan ground matching the warm mesas = the pod footage; - dbase/night = dark twilight + warm-lit red mesas; grass = warm brown; combat un-regressed. Gates: - BT_RAMP / BT_MATPRI / BT_SKY_SCREEN. -5. **UNLIT no-normal geometry (STEP 5) — NOT done, and no longer needed.** Since STEP 1 fixed the geometry + - the ramp gives correct colour, the mesas/terrain look natural lit; the residual faceting is gone. Deferred - (⚠ per-drawOp NOT per-object — APC/HUMMER mix XYZ_N_UV + XYZ_UV; ⚠ 0xCD ctor-init gotcha; ⚠ would change the - mech to unlit). Full spec: scratchpad rf_plan.md/rf_verdicts.md; the workflow reports are the durable cites. -6. **⭐ RAMP over-applied to LIT vehicle geometry → "whitish-tan blobs" — FIXED (bgfload.cpp `hasNormals` gate).** - The user saw the map's scattered vehicles (hummer/semi/dozer/apc…) render as uniform dusty-white blobs. ROOT - CAUSE: the vehicles' shared `basev` material library (`VIDEO/MAT/BASEV.BMF`) gives every material a REAL - diffuse colour (bv1_mtl tan 0.66,0.47,0.30; bv1gry gray 0.41; bv5dkgrn green; bv5red; bv1ylw; blk) PLUS a - ramp ref (tag 0x28) to the `cdusty` ramp (0.33,0.27,0.22→0.90,0.85,0.80 = dark-brown→near-white) over the - shared atlas `BASEV.BSL`. Our task-#20 ramp path set `batch.hasRamp` purely from the material, with **no - vertex-type gate** → it ramped the vehicles too, baking cdusty over BASEV.BSL + forcing a WHITE material → - every vehicle washed to the same dusty-white, killing its diffuse colour. But the authentic IG model - (DPLTYPES.H) selects shading BY VERTEX TYPE: **normal-bearing geometry (vehicles/missiles, `XYZ_N_UV`/ - `XYZ_N`) is LIT and shows diffuse×texture; the RAMP is a NO-NORMAL feature only** (terrain/mesas/sky/ - buildings/mech). Vehicles carry `XYZ_N_UV` (SEMI = 400 XYZ_N_UV + 248 XYZ_RGBA_UV). FIX: `bool hasNormals - (id)` (0x81/0x89) + `useRamp = currentHasRamp && !hasNormals(pmeshVtag)` per-PMESH in `buildPmesh` — replaces - `currentHasRamp` for the vertex colour + `batch.hasRamp`/`batch.color`. Now normal-bearing batches keep their - diffuse colour + BASEV.BSL texture (colored trucks); no-normal batches (incl. the vehicles' own no-normal - detail parts) still ramp. Per-PMESH so a mixed-vertex-type model splits correctly. **Verified A/B (zoomed - distant-vehicle crop):** before = gray/white blobs → after = gray trucks + tan/brown cargo + brown container, - distinct materials; terrain ramp un-regressed; combat un-regressed (TARGET DESTROYED after 8 hits); no crash / - 0 heap detections under BT_HEAPCHECK. NOTE: not separately env-gated — it's a correctness fix inside the - existing BT_RAMP system (BT_RAMP=0 still disables all ramping in L4D3D). **CORRECTION to an in-chat claim: - the vehicles are ALREADY per-light shaded once un-ramped — the loader is NOT missing normals.** `buildPmesh` - pushes 0 into nx/ny/nz only as INITIALISATION; `emitTri` then accumulates the forward face normal into each - vertex (bgfload.cpp:442-444) and the finalise pass normalizes them (`:597-601`) = smooth per-vertex normals - for ALL geometry. Lighting is globally ON whenever the map ships a light: `DPLReadINIPage` parses the env - page's `light` entry (BTDPL.INI, e.g. dbase/day `dsdayclear`) into `sceneLight[]` as a `D3DLIGHT_DIRECTIONAL` - sun (`L4VIDEO.cpp:3086` SetLight+LightEnable), and the frame setup (`:6196-6233`) sets `D3DRS_LIGHTING TRUE` + - `NORMALIZENORMALS` + `COLORVERTEX` + diffuse/ambient from vertex COLOR1 when `sceneLightCount>0`. Non-ramp - batches get material Diffuse=bv color, Emissive=0 (L4D3D.cpp:303-310), so a vehicle renders - `texture × [ambient·color + sun·(N·L)·color]` = real directional shading (render-verified: a gray tanker - shows a bright sun-lit top, dark shadowed side faces, smooth gradients — a flat render would be uniform). - The ramp-white was the ONLY thing suppressing it; no separate lighting work was needed. (STEP 5 — making - no-normal geometry authentically UNLIT — is the OPPOSITE change and still "not needed": global lighting on - the ramped terrain/mesas/mech looks correct vs the footage.) - -**✅ WEAPON FIRE IS NOW VISIBLE — the muzzle→hit BEAM renders + fires on the trigger, target or not.** -The user pressed SPACE and saw nothing: (1) the whole fire path was gated `if (gEnemyMech != 0)` (the -BT_SPAWN_ENEMY test dummy) → in solo there was NO target so the trigger ran no fire code; (2) even against -the dummy the only visual was an Explosion at the target — the `dpl_*` weapon-beam renderable was never -ported. FIX (two parts): **(a) a self-contained additive-quad BEAM renderer in the engine** — -`BTPushBeam(from,to,color,ttl,width)` queues a world segment, `BTDrawBeams(dev,view,dt)` (L4VIDEO.cpp, -called in the alpha pass so beams are Z-occluded by terrain) billboards each as a camera-facing additive -quad and fades it; no content needed (fills the unported dpl_ layer). **(b) a visual-fire block in -`Mech::PerformAndWatch` (mech4.cpp) that runs ALWAYS** — computes the muzzle (origin + kMuzzleHeight 7u / -kMuzzleForward 3u) + the mech's −Z forward, picks the aim = the locked enemy (torso) OR the crosshair -raycast to terrain (`BTGroundRayHit`, btvisgnd.cpp, else muzzle+fwd*range), and on the trigger (own cooldown -`gBeamCooldown`) pushes the bolt = **red-orange glow (w6) + white-hot core (w1.6) + muzzle flash** and (solo) -an impact Explosion at the hit. The existing enemy-damage block is UNCHANGED and still runs below (beam → -"target" while locked, then "free" after the kill). **Render-verified:** solo desert — a bright white-cored -laser fires forward to the aim point; combat — `[fire] BEAM -> target` while the dummy is locked, structure -0.8→1.0, **TARGET DESTROYED after 8 hits**, then "free" beams post-kill; 0 crashes / 0 heap detections under -BT_HEAPCHECK. SPACE/Ctrl fire the beam interactively (btl4main VK_SPACE→gBTDrive.fire). -**✅ AUTHENTIC MUZZLES — the beams now fire from the mech's REAL gun-port SITE segments (arm guns).** The -BLH.SKL has arm-gun joints `jointrgun`/`jointlgun` with muzzle site segments `siter/lu/dgunport` + -`sitel/lu/dgunport` (upper/down/back ports per arm). The fire block (mech4.cpp) resolves them by name each -frame — `GetSegment(CString("siterugunport"))` → `EntitySegment::GetSegmentToEntity()` (segment→entity -LinearMatrix) → `AffineMatrix::Multiply(segToEntity, localToWorld)` → `Point3D = matrix` (extracts the W_Axis -translation) = the world muzzle — and fires one bolt per resolved port, all converging on the aim point (falls -back to a centre muzzle if none resolve). **Verified:** muzzle logged at (−2.6,+4.4,+2.7) off the origin = -the arm gun, NOT the centre; render shows TWO beams emerging from the left+right arm guns in a converging V, -with muzzle flashes at each gun, tracking the arms; combat un-regressed (DESTROYED after 8 hits), 0 crashes / -0 heap detections. The beams are now RED LASER (glow 0x00E01808 + white-hot core), authentic for the all-laser BLH (Nova). -**◐ TIE-TO-REAL-FIRE — TRIED, REVERTED (over-saturates; needs a beam throttle).** The authentic next step is -driving the beam from each weapon's OWN fire: `Emitter::FireWeapon` has `owner` (Mech*), `muzzlePoint`, -`targetPoint`, `dischargeTime`, and `damageData.damageType` all in scope, so pushing `BTPushBeam` there -(muzzle from `owner->GetSegment(segmentReference)`→segmentToEntity×localToWorld, coloured by damageType: -Laser red / Energy blue / Ballistic amber) gives real rate + heat + per-type colour, and it BUILT + ran -(combat: `[emitter] FIRED heat=0.239`, DESTROYED-in-8). BUT the BLH is a ~12-laser ALPHA STRIKE and -`FireWeapon` runs ~16×/s across the roster; a persistent (`dischargeTime`) additive beam pushed per fire -ACCUMULATES into a white screen-flood at point-blank. So `Emitter::FireWeapon` no longer draws (kept the -mech4 controlled 0.8s salvo). To revive: throttle to ONE beam per weapon per discharge (push only on the -fire EDGE, `dischargeTimer` just reset) + short TTL (no per-frame stacking) + dim per-beam / alpha-blend the -glow so N overlapping beams don't saturate. `simulationFlags & 0x8` is NOT a safe weapon discriminator -(bit 3 is in the engine host/replicant `flags & 0xc` mask) — enumerate weapons via the factory class IDs, -not the flag. **✅ FIRE RATE — grounded on the recovered weapon data (was a 0.8s bring-up guess).** The user noticed the -laser cadence looked too slow vs the pod videos — correct, 0.8s was arbitrary. The real laser params -(`ERMLASER.SUB`/`ERLLASER.SUB`/... — the BLH is ER-medium): **`DischargeTime=0.2`** (beam-on), `WeaponRange=500`, -`DamageAmount=3.5`, `PipColor=1.0 0.0 0.0` (RED, confirms the laser colour), `RechargeRate` scales with size -(ER-S 1.0 / ER-M 2.0 / ER-L 4.0 / PPC 5.0). Applied: beam-on = `kBeamOnTime=0.2` (DischargeTime, recovered -hard value); cadence `kFireCooldown=0.3` (0.2 + short recharge) → ~62% beam duty cycle, snappy rapid fire vs -the old ~15%. **✅✅ RECHARGE CADENCE — FULLY DECODED from the binary (RechargeRate IS the recharge in seconds).** PE-parsed -the charge-curve constants from BTL4OPT.EXE (`.data` at the `_DAT_` VAs, read as the x87 80-bit `float10` the -decomp uses, NOT 32-bit float): **C1=`_DAT_004bb3c4`=1.0, C2=`_DAT_004bb3b8`=1e-4, `_DAT_004bb3b4`=0.5** (the -energyCoefficient ½). The Emitter ctor (raw part_013.c:7899-7906) computes `energyCoefficient = energyTotal/ -(0.5·seekV²)` and `energyRampTime = rechargeRate / (−VoltageCurve(C1−C2·seekV)) / energyCoefficient`; the -per-frame charge (`FUN_004b0d50` dtScale × `TrackSeekVoltage`) is an exponential approach toward the source -voltage with time-constant τ = dtScale·energyCoefficient, and the fire threshold is `seekVoltage[rec] = -0.8·RatedVoltage`. **The constants make it cancel exactly:** seekV = 0.8·RatedVoltage, RatedVoltage -(GENRATOR.SUB) = 1e4 → seekV = 8000 → curve arg = `1 − 1e-4·8000` = 0.2 → `VoltageCurve=ln` (must be, for -`energyRampTime>0`) → `−ln(0.2) = ln5 = 1.609`; and the exponential charge from 0 to 0.8·srcV also takes -`ln(1/0.2) = ln5`. The two ln5 factors cancel ⇒ **recharge time = RechargeRate seconds exactly** (ER-S 1.0 / -ER-M 2.0 / ER-L 4.0 / PPC 5.0). So one laser's full cycle = `DischargeTime + RechargeRate` = **0.2 + 2.0 = -2.2s** for the BLH's ER-mediums — much SLOWER per weapon than any guess; the fast on-screen fire is the ~12 -lasers firing STAGGERED (chain), aggregate ≈ 12/2.2 ≈ 5/s. Applied (mech4.cpp): `kPortRecharge=2.2`, fire ONE -gun port per stagger tick (`kPortRecharge/nMuz`) rotating through the 6 arm ports (added siter/lbgunport) → -a rapid alternating single-beam stream, each port honouring the 2.2s recharge (render-verified: single red -bolt per frame, rotating ports; combat DESTROYED after 8 hits, 0 crashes / 0 heap). ⚠ ASSUMPTION: that the -game STAGGERS rather than synced-volleys (a synced alpha strike would flash every 2.2s = slow, contradicting -the pod footage) — the per-weapon 2.2s is decoded-exact; the stagger *pattern* + exact weapon→port count is -the estimate (needs the weapon enumeration via factory class IDs, not simulationFlags). -**✅✅ BEAM APPEARANCE — DECODED from the assets (the "gritty" look is a real scrolling texture, NOT invented).** -The user noted the lasers looked too clean. Decoded the real beam: the weapon resource names its beam model -(`VideoObjectName=ermlaser.bgf`, `GraphicLength=2000`; `beamLengthRatio=dist/2000` scales it). `ermlaser.bgf` -= a thin TUBE (89 verts, Z 0..−2000) with UVs tiled ~8× down its length, material `btfx:beamwhite_mtl` (core) -+ `btfx:beamred_mtl` (glow). The GRIT is a SCROLLING TEXTURE: **`VIDEO/MAT/DAY/BTFX.VMF` (DIV-VIZ2 source)** -defines `TEXTURE(NAME="beamwhite_scr_tex"; SPECIAL="SCROLL 0.0 0.0 0.10 9.5") { MAP {"bexp"} }` — i.e. the -core samples the **`bexp` image = `VIDEO/TEX/BEXP.BSL`** (a 128×64 chaotic red/yellow/green noise) scrolled -FAST along the beam (u=0.10, v=9.5), ramp-colourised by `softer` (0.25→0.99) to a white core; the glow is -solid red (`fiery` ramp). ⚠ **I FIRST WRONGLY SAID THE TEXTURE WAS MISSING** — it is present; I'd missed the -`MAP{}` directive in the `.VMF` (the `.BMF` only names the texture, the `.VMF` binds it to the image + carries -the SCROLL). **THE `.VMF` IS THE KEY EFFECTS ASSET** (scroll speeds for beams/muzzle/smoke/sky: beamwhite v=9.5, -beamyellow v=−5.3, beamgreen v=−1.3, muzinner/muzouter, firesmoke, sky). Beam COLOR is per-weapon via the beam -model: ER lasers→ermlaser(RED), standard lasers→l/m/slaser(YELLOW), PPC→ppc.bgf(BLUE), AC/Gauss→ballistic -(no beam), missiles→projectiles. Implemented (L4VIDEO.cpp BTDrawBeams; gate BT_BEAM_TEX): load BEXP.BSL once, -ramp its luminance by `softer` → grayscale grit texture, MODULATE the beam colour by it, tile U down the -length + scroll (u=0.10/s, v=9.5/s) → the streaming gritty beam. Render-verified: the beam shows banded/streaky -grit (was a clean gradient); combat un-regressed (DESTROYED after 8), 0 crashes / 0 heap detections. -**✅ THE REAL `ermlaser.bgf` TUBE NOW RENDERS (replaced the camera-facing billboard quad; gate `BT_BEAM_TUBE`, -default ON).** `BTDrawBeams` (L4VIDEO.cpp) loads the tube geometry ONCE (`LoadBgfFile("ermlaser.bgf")` → static -`s_tubeVB`/`s_tubeIB`, x,y,z,u,v per vert) and per beam builds a world transform `Scale(w,w,beamLen/2000) × -Rotate(local −Z → muzzle→target dir) × Translate(muzzle)` — the tube's native Z 0..−2000 maps onto the shot, -its baked UVs (U 0..7.73 tiled down the length) carry the scrolling `bexp` grit via a `D3DTS_TEXTURE0` -transform (`D3DTTFF_COUNT2`, `_31=uScroll _32=vScroll`), beam colour from `D3DRS_TEXTUREFACTOR` (the tube verts -carry no diffuse — FVF `XYZ|TEX1`, COLORARG2/ALPHAARG1 = `TFACTOR`; the billboard path stays `XYZ|DIFFUSE|TEX1` -with `DIFFUSE`, kept as the `BT_BEAM_TUBE=0` fallback). Native tube is ~0.3u wide so a width scale is applied: -`BT_BEAM_WIDTH` (default **3.0** → ~0.9u; the width scaling ONLY affects the tube path, which is how a run was -confirmed to use the tube not the billboard). All touched state saved/restored (TEXTUREFACTOR, -TEXTURETRANSFORMFLAGS, D3DTS_TEXTURE0). **Render-verified** (dbase/grass, `BT_FORCE_FIRE`+`BT_SPAWN_ENEMY`): a -clean 3D volumetric bolt from the arm-gun muzzle to the target, staggered single-port fire, scene un-regressed, -7+ shots, 0 crashes. ⚠ **TEST-HARNESS GOTCHA (cost an hour — not a game bug):** the env gates check -`getenv(name)` for NON-NULL, so **`$env:BT_HEAPCHECK=""` (empty string) still ENABLES heapcheck** — every run -after I first set it crawled through `CrtCheckMemory` on each string alloc in the material `scanDir` (O(n²) -during mission BGF indexing) and looked like a mission-load HANG at `[zonebuild]` (`Responding:False`). cdb on -the "hung" process showed the main thread live in `ucrtbased!CrtCheckMemory` ← `scanDir`/`bmfIndex` ← -`LoadBgfFile` ← `DPLReadINIPage`, not stuck. FIX: `Remove-Item Env:\BT_HEAPCHECK` to actually clear it (setting -`=""` does not). Applies to ALL `BTEnvOn`-style gates. -**✅✅ AUTHENTIC DISTANCE LODs — "first LOD only" was WRONG; the arena "structures not rendering fully" bug -(bgfload.cpp + L4D3D + L4VIDEO).** The user found arena1 structures rendering as floating fragments (poles/ -letters/rails with no massing). Triage ruled out the CONN fix (A/B: 28 vs 29 tris on AR03, identical) and -load failures (none). ROOT CAUSE: **each BGF LOD chunk carries a `0x2046` header = [near..far) VIEWING BAND** -(AD01: [0..57.7]=close-up 3D logo letters, [57.7..231]=full billboard, [231..1155]=far; AB07: near band = a -sliver+pole DETAIL SUBSET, [115..1155] = the whole building) — the IG board selected the LOD whose band -contains the camera distance. Our loader always took the FIRST LOD; for classic reduce-LODs (calpb 152→16 -verts, dhillg, buttes) first==finest so it LOOKED right, but the arena's composite structures author their -massing in the FAR bands → only near-detail fragments drew. FIX: the loader collects EVERY LOD tagging -batches with the band (BgfDrawBatch/L4DRAWOP lodNear/lodFar); DrawMesh selects per draw-op by -|object − camera| (camera published per frame via `d3d_OBJECT::SetCameraPosition`, eye = −t·Rᵀ from the view -matrix). **CONSOLIDATION IS NOW DEFAULT OFF (`BT_CONSOL=1` re-enables)** — merged ops combine many instances -+ LODs of one material, so per-op bands can't survive the merge (a consolidated run would draw EVERY LOD -simultaneously). Frustum culling + HWVP carry the cost — measured FASTER than consolidation: dbase combat -avg 1.05ms, 79 batches (distant objects draw their small far-LODs). VERIFIED: arena renders a coherent -scene (continuous perimeter wall, full buildings, no floaters); dbase terrain/buttes unchanged; combat -TARGET DESTROYED after 8 hits, 0 crashes. Viewer: `BT_LOD_INDEX=n` inspects a specific LOD. FOLLOW-UP: a -few PINK horizon patches on dbase = far-LOD materials that never resolved before (their libs never loaded); -minor. ⚠ LESSON: "first LOD" was validated only on reduce-style models — a format assumption verified on a -subset can still be wrong for another authoring style; the 0x2046 header was readable all along. -**⚠⚠ LOD-DISTANCE BUGS FIXED THE NEXT SESSION (user: "mech disappearing/reappearing with viewing angle", -then "scenery still blinks"):** (1) the CAMERA EXTRACTION from the view matrix was TRANSPOSED — the D3D -row-vector LookAt stores the camera basis in COLUMNS (xaxis=(_11,_21,_31); eye.x = −(t·col1) = -−(_41·_11+_42·_12+_43·_13)); using ROWS gave a pseudo-position that VARIED WITH THE VIEW ANGLE, so LOD -bands popped in/out as the camera rotated (structures AND the mech blinked). (2) the METRIC IS THE INSTANCE -ORIGIN distance — a bounding-sphere-SURFACE metric (tried briefly, blamed for a floor vanish that was -really bug #1) subtracts each component's OWN radius, smearing the COORDINATED band handoffs of composite -structures (ADWLK1 ends [..115] exactly where AB07's far band [115..] takes over) by 20–50u per piece → -holes/doubles the orbiting chase camera swept pieces in and out of = the scenery blinking. Floor slabs' -single bands are wide (ctarmac [0..1155], dtarmac [0..1039], afloor [0..577]) — origin distance keeps them -visible across the play space; LODing out beyond that range (far outside the arena) is authentic. -`BT_CULL_LOG=1` dumps big culled objects (bounds sanity). Verified: 10-frame full-rotation sweep = mech + -floor + scenery stable (2 frames camera-inside-a-wall, expected); dbase combat DESTROYED after 8 hits. -⚠ LESSON: changing two things at once (camera fix + metric) attributed the floor vanish to the wrong one — -the sphere metric was never needed. -**⚠⚠ RUNTIME LOD SELECTION — REVERTED TO EXPERIMENTAL (`BT_LODSEL=1`); DEFAULT = SHIPPING-ENGINE BEHAVIOR.** -Even with the correct camera + origin metric + the authentic mech-eyepoint reference (fed via `BTSetLodEye`, -kept wired), runtime band selection still broke arena content (floor vanished when the mech wandered >577u -from `afloor`'s origin — impossible in the real game, so the 1995 semantics for huge grounds involve -something not yet decoded: per-LOD reference points (`dpfGetLODReference`/`dpl_SetObjectLodHotSpot` exist in -the libDPL API!), the unparsed class-0 arena map records, or ground-type exemptions). **DEFINITIVE REFERENCE -FOUND: `DivLoader/VGCDivLoader.cpp parseLOD` — the SHIPPING 2007 engine keeps ONLY the nearest-band LOD -(`if (in < 5.0f) parse = true`) and does NO runtime LOD selection at all** — i.e. our original "first LOD" -behavior IS what Red Planet shipped with; the arena-composite fragments exist in the 2007 engine too. -DEFAULT restored to that (first LOD + consolidation ON; `BT_LODSEL=1` switches to the experimental full-LOD -path and auto-disables consolidation). TO FINISH PROPERLY: decompile BTL4OPT.EXE's own LOD path (the 1995 -software renderer's selection incl. hot-spots) instead of guessing — then the arena backdrop can be made -authentic. All the plumbing (bands parsed+tagged, per-op DrawMesh test, eyepoint feed) is in place and gated. -**✅✅ ADDITIVE_LODS DECODED — the arena "fragment structures" FIXED, authentically (default ON, gate -`BT_ADDLOD=0`).** The follow-up research cracked it WITHOUT decompiling the binary: **the 1995 -`MUNGA_L4/L4VIDEO.CPP` `TestSpecialCallBack` (~line 486) reads the OBJECT-level SV_SPECIAL (0x2037) token -`ADDITIVE_LODS` from the BGF itself and calls `dpl_SetObjectAdditiveLODs`** (DPL_VPX.H). **135 pod BGFs -carry the token** — ALL arena structures (AB*/AD*/AR*/AW*), the PGN calliope-turret family, buildings -(HUT/TWR/BNK/JAX/FT/HN), vehicle props (TRK*), and EVERY `*D` mech damage model. Corpus sweep (841 GEO -BGFs): additive objects author their LODs as a clean PARTITION of [0..outmax) with COMPLEMENTARY geometry -(AW01: LOD1 = upper structure, LOD2 = base — different regions, not nested representations) — so -replacement-selection is provably WRONG for them (one complementary piece at any distance) and -first-LOD-only shows just the near-detail piece = the "floating fragments". **THE RULE: additive object → -at eye distance d, EVERY LOD with `d < OutDist` draws (InDist ignored; near detail ADDS onto the coarser -massing and drops first as you retreat).** One corpus quirk: MSLG.BGF has an inverted band → clamp -max(in,out). Implementation: `bgfload.cpp` TAG_OBJECT parses 0x2037 → additive objects collect ALL LODs, -each batch banded [0..OutDist); non-additive keep shipping first-LOD-only (mech/terrain/afloor → zero -regression risk); `L4VIDEO.cpp RecurseStaticObject` EXCLUDES banded objects from static consolidation -(merged runs can't band-gate) — they render individually via the per-op DrawMesh distance test (eye = -the `BTSetLodEye` mech feed). **Verified:** arena1 spawn A/B — `BT_ADDLOD=0` reproduces the user's exact -floating-panels bug AT THE SPAWN (the spawn sits inside a dropzone gate whose massing never drew!), -default ON = the complete bay/gate + walkway, coherent structures at all ranges; consol srcMeshes 255→152 -(103 additive instances excluded); grass combat TARGET DESTROYED, 0 crashes; cavern + dbase identical to -baseline. NOTE: the arena spawn's debug CHASE camera now starts inside the (correctly solid) bay wall — -the pod's cockpit camera sat at the mech's head so the original never saw this; drive forward to exit. -**✅✅✅ THE FULL LOD DECODE — √3 RANGES + ORIGIN METRIC + PUNCH CUTOUTS (the 6-agent `lod-blink-decode` -workflow; closes the arena "blinks with movement" + "noisy unstable surfaces" reports).** -1. **⭐ THE √3 RANGE DECODE (corpus-exact):** every stored 0x2046 band value is the AUTHORED euclidean - distance ÷ √3 — 92% of the corpus' 3263 nonzero band slots are EXACTLY nice-number/√3 to 6 significant - figures, with hand-conversion TYPOS as proof (BUTTEB 2000.73 for 2020.73=3500/√3; MSLG's inverted band - = the author typing 10 for 100). afloor 577.35 = authored **1000** (the historic floor-vanish - explained); ADWLK1 115.47 = 200; the arena massing 1154.7 = **2000** ≈ des_day clip 2100. Comparing - plain euclidean eye distance against stored values made EVERY piece switch at 57.7% of its authored - range — inside the arena's clear air (fog 200→1250, clip 1300 [ardayclear]) = the position-dependent - blinking. FIX: `bgfload.cpp` multiplies parsed bands by √3 (`kLodRangeScale=1.7320508`). With it, NO - massing pops in-play (2000 ≥ the arena's max reachable 1972; 54 of 104 popping instances — ALL - walls/buildings/gates — stop popping; the remaining are authored detail dropouts at 400-900u, 19-66% - fogged). A disjoint 7.45% RAW-integer band population exists (one prop-family artist skipped the - conversion — GD/RB/WL/SW/SB/TK1 etc), 100% NON-additive → unaffected today. -2. **METRIC = the instance-transformed OBJECT HOT SPOT (default = origin), ONE shared d for all LODs of - an object** (`s_dplobject.lod_hot_spot`; InDist doesn't even survive to the runtime — the board keeps - only `lod_ranges[16]` switch-out floats, which STRUCTURALLY confirms the cumulative rule). The - bounding-sphere-SURFACE metric (previous commit) was UNAUTHENTIC — per-piece radius offsets desync - sibling bands (the OpenFlight "use previous slant range" contract: siblings must range against the - SAME point). Reverted to origin distance in DrawMesh. -3. **✅ PUNCH cutouts (dpl_Punchize) implemented** — PATCH-level SV_SPECIAL `PUNCH` (1995 - TestSpecialCallBack L4VIDEO.CPP:563-569) marks cutout geogroups (arena scaffold layers AR01-04, - gratings): black texels = HOLES. The port drew them SOLID, shingled 0.05-0.5u over the wall massing = - near-coplanar depth shimmer in motion (the "noisy unstable surfaces"). Now: loader tags batches - (bgfload punch flag) → texture loads with alpha-0 on near-black texels → DrawMesh alpha-TESTS punch - ops in the opaque pass (ALPHAREF 0x80, z-write preserved, states restored). Gate `BT_PUNCH=0`. - Render-verified: the catwalk rails/barriers draw as see-through lattice. - **Corpus classification (the same workflow):** additive LODs are overwhelmingly COMPLEMENTARY - (families: detail+massing 54, progressive layered series 49 [AR01 = trim→scaffold→scaffold+panels→ - SOLID WALL, disjoint layers, correct under cumulative], single-detail 9, mixed 23); real coplanar - DOUBLED overlap only in props (TRK1/3/4 grille detail flush on the body) + mech damage arms — those - carry `GEOMETRIZE 0x8000xxxx` hints (decal/priority, unimplemented lead), authentic-coexisting on the - board too, left as-is. - **Other decoded facts:** map class-0 records = INCLUDE directives (`mid==3` + type-14 target; - arena1→{arenall→cavern, artrucks} — the arena sits INSIDE the cavern map; class-48 = named dropzones); - NO 0x2047/0x2048 exists anywhere in the corpus; Division dVS has no additive concept (VPX-local - extension); OpenFlight LOD semantics (OGC 19-065) match the tag set 1:1 (reference-point ranging, - transition ranges) — the reference for future replacement-LOD work. - **Diagnostics kept (permanent):** `BT_LOD_LOG=1` → `[lodband]` per-model banded-op census at load, - `[lodeye]` eye-feed trace, `[lodflip]` per-op band-visibility FLIP events with model name + band + d - (how the oscillation was caught: awbox1 flipping IN 3× at d≈461 vs edge 461.88). - Verified: arena traverse = no massing flips (only authored detail handoffs at 400-900u under fog); - walk-out renders banners + see-through rails + complete walls; grass combat TARGET DESTROYED, 0 - crashes; dbase/cavern identical; avg frame 4.5ms. -**✅✅ THE COPLANAR-SHELL DECODE — the "extremely unstable walls" (noise/diagonal/dot patterns crawling -with movement, stable parked) ROOT-CAUSED + FIXED (submission-order depth bias; commit bcc9210).** -Diagnosis chain (each step eliminated a hypothesis): two stills at one position = pixel-identical -(pattern is a pure function of camera position); layer-isolation imaging (`BT_ONLY_LOD=k`) showed the -dot lattice INSIDE A SINGLE LOD layer (not layer-vs-layer); `BT_PUNCH=0` left it unchanged; analytic -geometry (AR02 LOD2) found the truth: **the content layers EXACTLY-COPLANAR full shells inside one LOD** -— a solid concrete shell + a coplanar PUNCH-cutout overlay + an inner shell, plane separations measured -0.000–0.003u. The Division board composited coplanar polygons DETERMINISTICALLY (exact per-polygon plane -equations → identical depth → later-submitted wins via less-equal); D3D9 interpolates depth from -vertices, so two tessellations of one plane differ by rounding PER PIXEL = crawling interference at ANY -distance (near-plane precision, per-LOD bias, punch alpha all irrelevant — hence the failed attempts). -**FIX = reproduce the board's rule:** every batch of an additive object is depth-biased ~4 LSB of D24 -CLOSER per submission ordinal (file order = authored order; `2.5e-7` NDC/step, ordinal capped 20, -`BT_LAYER_BIAS` overrides) — coplanar contests resolve to the overlay patch at all distances. -Verified by bias on/off A/B at a teleport-reproduced viewpoint. **Also: punch cutout mips are built -MANUALLY with alpha RE-BINARIZED per level** (box-filtered alpha hovers at the test threshold → texels -flicker as movement shifts the mip level). **And `BT_LOD_SCALE` is BLANKET again (commit 1bea626):** -the outermost-only restriction guarded against extending shingled detail into z-fight range — obsolete -under the bias — so inner detail (wall-top caps, authored 800) now holds to 2000 = zero wall-detail pops -in-arena (traverse flip census 104 → 7, remaining = two small pieces at 375–500u under fog). -**HARNESSES (permanent):** `BT_SPAWN_AT="x z [hdg]"` teleports the player on the first driven frame for -exact-viewpoint A/B repro — MUST set the orientation QUATERNION (`localOrigin.angularPosition = -EulerAngles(0,h,0)`), not just the scalar `gDriveHeading` mirror (the chase camera derives from the -quat; without it the view keeps the random dropzone's facing). Teleporting INTO a dune can leave the -mech sunk (ground probe from inside a solid) — cosmetic, walking recovers. `BT_ONLY_LOD=k` = additive -layer-isolation imaging. ⚠ **GOTCHA (cost a loop): `bgfload.cpp` builds into the ENGINE lib** — a -game-only rebuild silently ships the stale loader; grep the exe for a new string literal when in doubt. -Remaining authentic-LOD work: replacement selection for NON-additive multi-LOD models (detail pop at -range — needs the board's hot-spot/reference semantics; leads catalogued: `s_dplobject.lod_ranges[16]/ -lod_hot_spot/lod_fade_range` in DPL_PRIV.H, per-LOD 0x2047 reference points, 0x2048 transition snap|blend, -`dpl_SetLodStress` perf governor, `dpl_SetInstanceForceLOD`). **RESEARCH TECHNIQUE (reusable): -`LIBDPL.LIB` is an OMF library with full symbol names — parse THEADR/PUBDEF records (python ~40 lines) to -map every `dpl_*` entry point; the 1995 DOS `MUNGA_L4` HAL source is the game-side caller reference; -`dsys/PFBIZTAG.H` names every BGF tag (0x2046=LOD_DISTANCE, 0x2047=LOD_REFERENCE, 0x2048=LOD_TRANSITION, -0x2037=SV_SPECIAL).** - -**✅✅ THE RAINBOW/GRAFFITI MECH SKINS — THE BSL BIT-SLICE FORMAT DECODED + FIXED (user: "are the mechs -colored accurately? they all seem to have a rainbow/graffiti motif" — they were not; every mech now renders -its authentic gray-metal paint).** A 4-agent workflow (BSL corpus census / format ground truth / our-loader -audit / BMF material spec) + hands-on. ROOT CAUSE: `.BSL` is NOT one RGBA image — it is a **bit-sliced -container**: w*h 32-bit texel words, byte 0 pad, the other 3 bytes = SIX independent **4-bit grayscale -sub-images** packed 2-per-byte with the nibble pair SWAPPED (even slice = HIGH nibble; -`shift=(c+((c+1)%2)*2)*4`), plus truecolor sliceTypes 7=RGB444/8=RGBA4444 (r=s5,g=s4,b=s3,a=s2) that COEXIST -with mono slices in one file. BLKHWK.BSL holds the Blackhawk's 4 skin sheets this way; our decoder read -bytes[1..3] as RGB = 2-3 different gray sheets overlaid as colour channels = the graffiti. **Slice selection -= the BMF TEXTURE record tag `0x18 BITSLICE` (u8, absent=0)** — the entry NAMES in the BSL directory are -authoring records, read-and-discarded at runtime (proof: BASEV.BMF `bexp9_tex`→slice 8 vs entry named -`bexp99`). Definitive reference found: **the shipping 2007 loader `DivLoader/VGCDivLoader.cpp:323-410` -(`LoadBSLFile`/`getBSLData`)** + Division `dsys/PIMAGE.H dpiBSLTYPE` + content build scripts (`img2vtx.exe --b -m0..-m5 `); corpus-verified on all 66 archive BSLs (GEN.BSL's 6 entries can't fit any byte-plane -model — the smoking gun). FIXES: (1) `port/src/image.cpp decodeBSL` rewritten to slice decode (one file -serves engine+viewer; `BT_BSL=0` = legacy fallback); (2) `bgfload.cpp` parses tag 0x18 → `MatInfo.texChannel` -→ `BgfDrawBatch.texChannel` → `L4D3D decodeImage(tp, channel)`; (3) **cross-library RAMP registry** — every -mech-skin material references ramp `softer` (neutral 0.25→0.99) which is defined only in OTHER libs -(BTARENA/BTFX/BTPOLAR/BTVEH); per-file lookup left mech skins un-ramped forever; on first miss the loader now -sweeps the indexed BMFs once into a global registry (`BT_RAMP_XLIB=0` disables); (4) **RAMP TINT RULE** — -the content pins the (binary-only) board combine from both sides: dsky_mtl has diffuse (0.3,0.5,1.0) + the -COLOURED 'sky' ramp yet renders footage-white ⇒ diffuse must NOT modulate coloured ramps; the mech variants -(gen2medgry vs gen3drkgry etc.) share texture+NEUTRAL 'softer' ramp and differ ONLY by diffuse ⇒ diffuse MUST -tint neutral ramps. Rule: explicit diffuse + NEUTRAL ramp ⇒ tint; coloured ramp ⇒ white (`BT_RAMP_TINT=0` -restores always-white). Terrain/sky pixels UNCHANGED by construction; (5) truecolor slices (≥6, e.g. -damcolor's bdam8 damage sheet) bypass the ramp; (6) beam grit (L4VIDEO BTDrawBeams inline decode) now samples -slice 0 (bexp1 — beamwhite_scr_tex has no 0x18 tag); (7) viewer mirrored + its texture cache re-keyed -`path#channel` (4 textures share BLKHWK.BSL). **Render-verified:** grass + close-up = coherent gray-metal -Blackhawk with crisp panel/vent detail, dark charcoal accent zones, mauve `gen*a` leg tints (arena shot), -green lgo6 leg-logo edge visible — the BMF-spec oracle look ("gray panel-detailed mech", per-zone) exactly; -dbase terrain/sky/vehicles byte-identical to the tuned look; arena walls/punch gate clean, 0 lodflips; combat -under BT_HEAPCHECK = TARGET DESTROYED after 8 hits (exact baseline), 0 crashes / 0 heap detections. Docs -corrected (ASSET_PIPELINE.md §Pixel formats, CLAUDE.md §5). FOLLOW-UPS: RGBA4444 alpha (TREE.BSL cutout) is -decoded but not yet alpha-tested outside punch batches; VTX/TGA paths untouched; the DZM damage-state -material swap (THOR stores damage skins as extra slices thor12d/thor34d) still pending damage-graphic-state -work. - -**✅ WRECK FLOOR-TILE FLICKER — FIXED (baked ground-shadow models + consolidated-world recession; commits -15fc70d, 7d5071a).** The user reported the floor tile under arena wreckage flickering exactly like the wall -panels (pattern changes with movement, freezes parked — the coplanar-shell signature, and visible in original -demo vids too; user chose to fix). TWO coplanar fighters found: (1) **baked ground-shadow MODELS** — the -`S.BGF` naming convention (MECHMOVS/COMS/ARMRS/COOLS = shadow companions to wreck props): flat quads of -`basev:shadow_mtl` at y≈0.1. Detection = all-shadow-material model (`shadowMat` per batch in bgfload; material -name contains "shadow") → routed through the mech-shadow pipeline (`SetIsShadow`: translucent dark, depth-biased, -no z-write) instead of opaque. (2) The ACTUAL fighter for the tile: **MECHMOVR's own ground plates at y=0.000 -vs the consolidated arena floor** (cross-object coplanarity the per-object submission bias can't order) → -**consolidated static world is now recessed `+7.5e-7` NDC (~3 LSB)** (ConsolidateSingleObject op copy), so -individually-drawn objects (entity props, additive structures, the mech) win floor-plane ties deterministically. -Verified at the user's parked repro spot. Shadow-material models are also EXCLUDED from static consolidation -(GetIsShadow → banded), same as banded additive objects. - -**✅ POLAR MAPS VERIFIED — two "bugs" that are AUTHENTIC + the emissive decode (commit cfbe6d2).** polar3/ -polar4 boot + render + fight clean. (1) **No mech shadow on polar = AUTHORED**: `GEO/POLAR/*_TSHD.BGF` are -188-byte STUBS (deliberate shadow disable on snow; only RAP_TSHD is real) — chased through burial/tilt/opaque- -color probes first (BT_SHADOW_BIAS/BT_SHADOW_TILT/BT_SHADOW_COLOR all no-effect) before checking the FILES. -Check the content before debugging the pipeline. (2) **Pink ice-mound perimeter = AUTHENTIC**: the `pint` -material family (`MAT/DAY/BTPOLAR.BMF` pintBIceEmit_mtl: diffuse BLACK + emissive 0.676/0.614/0.718 + ramp -'icy') = pink-tinted self-lit ice. This surfaced that **EMISSIVE (tag 0x26) wasn't parsed** — now threaded -through MatInfo→BgfDrawBatch (`hasEmissive`/`emissive[3]`); PURE-emissive materials (diffuse black + emissive -set) render as an UNLIT glow: vertex color forced BLACK (the engine drives lit diffuse from vertex COLOR1) + -`m.Emissive = batch.emissive` — tex × emissive, no sun/ambient. ⚠ pure-emissive test is `(color&0xFFFFFF)==0`; -a material with BOTH diffuse and emissive keeps the lit path. - -**✅ ALL 8 RES MAPS BOOT + RENDER (map sweep complete):** cavern, grass, rav, polar3, polar4, arena1, arena2, -dbase — each verified live (screenshot + log: 0 crashes, 0 lodflips). Eggs for every map now exist in the pod -BT dir (POLAR3/POLAR4/RAV/ARENA1M/ARENA2/DBASE/MECH2/DEV/TEST...). Arena2 note: the flat brown band between -wall-top and sky is the cavern backdrop (arena maps sit inside the cavern map via class-0 includes) — believed -authored; re-check only if it misbehaves in motion. - -**✅✅ CONN+PCONN COEXIST — 370 of 841 MODELS WERE MISSING TRIANGLES (the "turret base missing panels" -bug; bgfload.cpp + port/src/bgf.cpp).** The user spotted the calliope turret (`calp` gun + `calpb` base -pad, on dbase AND cavern) rendering with gaps in the base and confirmed vs pod videos. Chain that found it: -loader-mirror (bgfcheck.py) said calpb loads "clean" at 78 tris → the standalone port VIEWER showed the same -gaps (ruling out the new consolidation/cull work) → dumping the chunk TREE showed calpb LOD0's PMESH carries -**BOTH** `PCONN_LIST` chunks (37 quads + 1 hexagon = 78 tris; note **ppf=6 hexagons exist**) **AND** a -`CONNECTION_LIST` chunk (24 plain triangles = the base panels) — and `buildPmesh` processed CONN only -`if (!gotPconn)`. **CONN and PCONN are NOT alternatives — a pmesh carries quads/hexes in PCONN plus loose -triangles in CONN.** Corpus scan: **370/841 GEO models** have mixed pmeshes (incl. AVA mech parts, buildings, -vehicles) — all silently missing their CONN triangles until now. FIX: process both, always (both loaders); -probe verified calpb 78→102 tris (+24 = exact); viewer A/B shows the base gaps filled; combat regression -clean. BGF_FORMAT.md §Indices + Open-Items updated (second correction to the CONN story: first "one polygon" -→ flat trilist (task #20), now "either/or" → BOTH). ⚠ LESSON: a "loads clean, all indices in bounds" audit -can still be dropping whole chunks — audit what's NOT consumed, not just what is. - -**✅✅ THE "TURN HITCH" / 10FPS BASELINE — ROOT-CAUSED & FIXED (~50-80× render speedup; user: "game -occasionally hangs/stutters... definitely somehow related to turning").** Instrumentation chain that found it -([spike] frame-time log → [loadobj] load timer → cdb stack sampling → [rstat] draw/present split → per-draw -section timers): the hitches happened with AND without input (the user's own repro log: 3 of 6 spikes at -turn=0) — turning only made them VISIBLE (at 10fps a 400ms frame while holding a turn snaps the view through -4 frames of rotation = reads as a freeze; going straight the same stall just pauses the scenery). THE REAL -PROBLEM was the render baseline: ~107ms CPU per frame (Present <1ms, GPU IDLE). THREE stacked causes, all -fixed (engine tree, git add -f): - 1. **`D3DCREATE_SOFTWARE_VERTEXPROCESSING` (L4VIDEO.cpp:2023) — THE ROOT CAUSE.** Every vertex of every - draw was transformed + LIT + fogged on the CPU. The first CreateDevice attempt used SOFTWARE (the error - message below it says "Couldn't create HARDWARE..." — the fallback was always intended); now HARDWARE - first, software fallback kept. This alone: dense-view frames 490ms → ~3ms. Also why the RelWithDebInfo - build was EQUALLY slow (the D3D software pipeline is external optimized code either way). - 2. **D3DX `DrawSubset` attribute-table scan (~44µs/call × ~1400).** Our BGF triangles are DOUBLE-SIDED - (each face emitted twice) → `GenerateAdjacency` degenerate → `OptimizeInplace(ATTRSORT)` failed SILENTLY - → no attribute table → DrawSubset scanned the whole attribute buffer per call; on the huge CONSOLIDATED - static meshes ~354µs/call = the 490ms stretches whenever a merged map chunk entered the view. FIX: the - batches are already contiguous face-sorted ranges — `SetAttributeTable` EXPLICITLY (no adjacency at all) - + per-op `bgfStartIndex/bgfPrimCount` in L4DRAWOP and a direct `DrawIndexedPrimitive` path in DrawMesh - (DrawSubset kept as fallback for non-BGF meshes). ⚠ `D3DXConcatenateMeshes` (static-world consolidation) - REQUIRES an attribute table on its INPUTS — removing OptimizeInplace without SetAttributeTable made it - fail hr=0x8876086c and the WHOLE TERRAIN silently skipped ("206 static meshes") — the mech floated in - fog-colored void. ConsolidateSingleObject now sorts faces by op itself (no D3DX optimize) + sets the - table + direct-draw ranges. - 3. **No frustum culling** (the 1995 IG board clipped in hardware; the port drew EVERYTHING every frame). - Added bounding-sphere culling: model-space sphere computed at load (mesh ctor), per-frame frustum from - view × mProjectionMatrix (NOT a GetTransform read-back — device state between frames holds pass-specific - matrices and the planes silently degenerate), 4 side planes + FAR plane (near skipped for pass-projection - differences; sky pass never culled; `mCullRadius<=0` never culled). Gate `BT_CULL_FRUSTUM` (default ON). - **Measured: dbase circling 107ms/2729 batches → 1.2-2.8ms; grass combat avg 4.5-8.7ms; spikes 8→~1 - (residual = first-view managed-buffer upload). Verified: full scene renders (terrain/mesas/sky/mech/ - shadow/beams), combat DESTROYED, 0 crashes.** Diagnostic infra kept (cheap): `[spike]` (mech4, frame>0.2s), - `[loadobj]` (L4D3D, load>5ms), `[rslow]`/`[rstat]` (L4VIDEO, 1Hz frame/batch/cull stats). Also gated the - weapon-aim terrain raycast (mech4) to trigger-down/discharge-active frames (was marching SampleBand over - every map instance EVERY frame). ⚠ LESSON: the user plays the DEBUG build — but the 10fps was NOT the debug - build (RelWithDebInfo identical); always split draw-vs-present and check the DEVICE flags before blaming - the compiler. ⚠ D3DXComputeBoundingSphere/SetAttributeTable/etc: any NEW d3d_OBJECT member must be inited - in BOTH ctors (0xCD fill reads as garbage bounds → everything culled). - -**✅ FIRING ARC — no more shooting the enemy out of your back; the arc is DATA-DRIVEN from the torso.** -The bring-up fire path aimed the beam AND applied damage at the locked target regardless of where the mech -pointed (turning away fired lasers backward THROUGH the mech at the enemy behind). **DECOMP FINDING: there is -NO firing-arc check in the binary** — `Emitter::FireWeapon` (@004bb6d4) fires whenever `HasActiveTarget()` -(entity+0x388) and builds a straight muzzle→target beam, and the weapon `.SUB` resources carry **NO arc field** -(ERMLASER.SUB = range/damage/recharge/heat only). The 1995 game got away with this because it was **cockpit- -only**: a beam from a front muzzle to a target behind you passes behind the camera — invisible to the pilot -(same reason the foot-clip never mattered). Our external camera exposes it, so the port owns the fix. The -AUTHENTIC per-mech weapon traverse is the **TORSO twist range** (what actually lets a mech point its guns off -dead-ahead): `Torso::GetHorizontalReach()` = the wider of the software limits `horizontalLimitLeft/Right` -(res +0x100/+0xFC), or **0 when `TorsoHorizontalEnabled=0`** (the Blackhawk → guns lined up with the mech -facing). Wired: `Mech::GetHorizontalFiringReach()` (mechmppr.cpp — the TU that safely knows the full `Torso` -type; mech.cpp/mech4.cpp must not include subsystem headers, stub-collision) reads the cached torso @0x438; -the mech4 fire block gates BOTH the visual beam aim AND the real Emitter trigger + explosion/damage on -`targetInArc` = angle-to-target ≤ (BASE aim tolerance + torso reach). Base tolerance = `BT_FIRE_ARC` degrees -(default 30) — the reticle/convergence box, a labeled PORT presentation parameter (the cockpit view never -needed one); the torso reach is the authentic data part (0 for BLH, widens the arc for a twist-capable mech). -In-arc: the beam converges on the target and the shot hits; out-of-arc: the guns fire straight ahead and -nothing is hit. **Verified live** (`[target]` log shows `torsoReach=0rad` for the BLH → arc = base 30°): -circling — enemy sweeping out of arc → beams go `-> free` (forward), no hit; facing → `-> target` converge + -damage; walking in to close range → **TARGET DESTROYED after 8 hits** (un-regressed). ⚠ `kWeaponRange=100` -(mech4.cpp) is a stale BRING-UP damage-gate threshold (the real ERMLASER `WeaponRange=500`); the enemy spawns -at 120u so a STATIONARY facing test sits just outside it (visual beam fires, no damage until you close to -<100u) — cosmetic bring-up wart, not the arc. **Remaining:** wire the real torso ACTIVE-TRACK (the torso -twists toward the target within its limits, so a twist mech's guns visibly follow) — currently the reach only -WIDENS the hit test; the joints don't move in combat (torso sim dormant, §Torso). -**✅ BEAM ORIGIN TRACKS THE LIVE MUZZLE during the discharge (you can't run out from under your own beam).** -The user saw the beam's near end lag behind the mech when running — the bring-up pushed ONE world-space beam at -fire time with a 0.2s life, so the mech moved out from under the frozen segment. **DECOMP: the real Emitter -re-anchors the beam every frame.** `Emitter::FireWeapon` (`FUN_004bace8`) computes the muzzle LIVE via -`GetMuzzlePoint` (`FUN_004b9948` — resolves the gun's muzzle SEGMENT current world position from `owner+0x300` -segment table, ref `+0xdc`) and **does NOT store the muzzle** on the emitter (only the aim orientation @0x107 -and the target point @0xf2 are saved); then `EmitterSimulation` case 0 (Firing) calls `ContinueDischarge` -(`FUN_004baa20`) EVERY frame while `dischargeTimer>0`, re-sending the beam keepalive. Since the muzzle isn't -stored, the display re-obtains it live each frame → the beam ORIGIN follows the gun as the mech moves (invisible -in the 1995 cockpit-only view; our chase cam exposes the lag). Reproduced in the mech4 visual: a single active -discharge is tracked (`s_dischargePort`/`s_dischargeRemain`, one at a time since beam-on 0.2s < stagger 0.37s); -on the trigger edge it STARTS the discharge, then a per-frame KEEPALIVE redraws glow+core+flash from -`muzzles[port]` (recomputed from the gun segments every frame) to the live aim with a one-frame ttl (exactly one -beam alive/frame = same brightness, re-anchored to the moving gun). **Verified:** running forward while firing, -the beam emanates from the arm gun (near end at the gun, not behind); combat un-regressed (TARGET DESTROYED -after 8 hits, 0 crashes) while the mech walked the whole time. -**⚠ FOLLOW-ON (colour/grit washout from the run-out fix):** re-pushing the beam every frame at a one-frame -ttl made `f = ttl/maxTtl = 1.0` CONSTANT (the old 0.2s ttl FADED, so it was only briefly at full brightness). -Three full-bright overlapping tubes (red glow + white core + white flash) at the SAME `s_tubeWidth` then -additively saturated to a solid WHITE cylinder — the red and the scrolling bexp grit vanished. Root cause was -TWO things: (1) the tube path ignored per-beam width (all beams drew at the global `s_tubeWidth`), so the -white core covered the FULL beam width instead of just the centre; (2) too much additive brightness. FIX: -the tube now scales by **`b.width * s_tubeWidth`** (per-beam width honoured; `BT_BEAM_WIDTH` is now a global -MULTIPLIER, default 1.0), and the keepalive pushes just TWO dimmer beams — a WIDE dim red glow (`0x00C81404`, -width 3.0 → carries the ER-laser colour + grit) + a THIN warm core (`0x00A07868`, width 0.9 → hot centre, no -white-out) — dropping the always-on white flash. Result: a steady red bolt with a bright core and visible -scrolling grit (render-verified, 24 beams, 0 crashes). A laser is steady-on (not fading), so constant -brightness is correct — the bug was saturation, not the lack of fade. -**Remaining (real weapon fidelity):** per-weapon color from the beam model (blue PPC, yellow standard lasers) -once weapon enumeration is wired (factory class IDs, not simulationFlags); the throttled real-fire beam (drive -off Emitter::FireWeapon, heat); damage by the aimed zone (cylinder lookup, STEP 6). -Remaining subsystem families (projectile/missile weapons 0xBCD/0xBCE/0xBD0 [WAVE 7]; SubsystemMessageManager -0xBD3 [WAVE 8]; Gyroscope 0xBC4 deferred — NaN integrator) per SUBSYS_PLAN.md. DONE: Torso (WAVE 5) + -Sensor/Searchlight/ThermalSight/AmmoBin readouts (WAVE 4) + Myomers structural un-stub (WAVE 6, INERT — -authentic mover/heat coupling deferred). - -**✅ TORSO twist/elevation aim — joint-I/O + simulation RECONSTRUCTED (dormant; blocked on the WAVE-4 un-stub).** -The torso's aim→skeleton path is now fully reconstructed and builds green with zero combat regression, but is -NOT yet live because the Torso subsystem itself is still a `HeatSinkSource` RECON_SUBSYS stub at factory case -0xBC5 (its real class is **Torso**, ctor @004b6b0c — the "SinkSource" label is the usual mislabel). What was done: -- **`Mech::ResolveJoint(name)→Joint*`** (mech.cpp, == binary `FUN_00424b60`): resolves a named skeleton node - via the PUBLIC engine path `GetSegment(CString)` → `EntitySegment::GetJointIndex()` → `JointSubsystem:: - GetJoint(i)` (JMOVER.h/SEGMENT.h/JOINT.h, all in `munga_engine.lib`) — never touches the protected - `EntitySegment::jointPointer`. The Torso ctor @004b6b0c + Gyro ctor @004b3778 inline this; hoisted as the reuse - point (gyro un-stub will call it too). `Torso::ResolveJoint` forwards to `owner->ResolveJoint` (out-of-line — - needs complete Mech). -- **`Torso::PushTwist(Joint*,Scalar)`** (== `FUN_004b66b4`/`FUN_004b67ec` inner block): dispatch on - `Joint::GetJointType()` — hinge (0..2)→`SetRotation(Radian(twist))` (`FUN_0041d0a8`); ball (4..5)→ - `GetEulerAngles()` keep pitch[0]/roll[2], **yaw[1]←twist**, `SetRotation(EulerAngles)` (`FUN_0041cfa0`/ - `FUN_0041d020`). Nodes retyped `void*`→`Joint*`. -- **`TorsoSimulation` @004b5cf0 corrected to match the raw decomp** (part_013.c 4471-4693): the earlier draft had - the command roles SWAPPED — verified true roles: **@0x1F8/@0x1FC = elevate up/down** (drive `currentElevation` - 0x1e4, clamp vertTop/Bottom), **@0x200/@0x204 = twist left/right** (drive `currentTwist` 0x1d8, clamp horiz - Left/Right), @0x208 = recenter; **analog axes @0x1F0 = twist, @0x1F4 = elevation** (the draft misused - `elevationHalfBottom`@0x22C and omitted @0x1F4). Settle test uses `targetTwist`(0x218) + the OLD twistRate - snapshot; limit latches are **edge-triggered** with @0x260/0x264=twist L/R, @0x258/0x25C=elev top/bottom - (the draft had these swapped/non-edge). `ResetToInitialState` @004b5bf8 likewise made faithful (clears all 4 - latches + analog axes + aim state). -- **Per-frame skeleton write wired.** KEY DECOMP FACT: `TorsoSimulation` AND `TorsoCopySimulation` compute - `currentTwist` but **never write the joints**; `WriteJoints` (@004b66b4) is called ONLY by `ResetToInitialState`; - `UpdateJoints` (@004b67ec, the out-param-free twin) has **ZERO direct callers** — the per-frame torso→skeleton - write was dispatched by the engine's generic joint pass (indirect/virtual). This port ticks the torso's own - Performance, so `UpdateJoints()` is now called at the end of both Torso(Copy)Simulation — the resolved - equivalent at the same cadence. Verification is env-gated `BT_TORSO_LOG` (ctor resolve + layout dump; PushTwist - fire log). - -**✅ WAVE-4 Torso base-chain RE-BASE + un-stub — DONE (real Torso is live, binary-exact layout, zero regression).** -The Torso is now built by the real `Torso` class at factory case 0xBC5 (`CreateTorsoSubsystem`), no longer a stub. -- **The corrected hierarchy** (verified from headers + a 6-agent decomp-mapping workflow): `Torso : PowerWatcher : - HeatWatcher : MechSubsystem` — **NOT** HeatableSubsystem. The Watcher branch is SEPARATE from the working - `PoweredSubsystem : HeatSink : HeatableSubsystem : MechSubsystem` heat leaves; the two share ONLY `MechSubsystem`. - So the 156-byte deficit lives entirely in the Watcher branch and re-basing it CANNOT touch the working heat leaves. - (Corollary, verified: Torso is **NOT** `IsDerivedFrom(HeatSink)` → it never joins the heat-conduction roster, so - there was never a heat-roster-corruption risk; the earlier note to the contrary was wrong.) -- **The +156 fix (all in the Watcher branch; `MechSubsystem` untouched):** the binary's HeatWatcher/PowerWatcher use - a **0xC-byte** connection (`watchedLink`) and a **0x54-byte** gauge alarm (`heatAlarm`/`watchdogAlarm` = a full - GaugeAlarm), NOT the 4-byte `SubsystemConnection` / 8-byte `HeatAlarm` the HeatSink branch uses. Introduced - Watcher-**local** `WatchedConnection`(0xC) + `WatcherGaugeAlarm`(0x54) in `heatfamily_reslice.hpp` (NOT heat.hpp, - so the shared types the working leaves depend on are never resized). HeatWatcher: `watchedLink`→0xC (+8), - `heatAlarm`→0x54 (+76) = +84 → own ends 0x180. PowerWatcher: DELETE the shadow `watchedLink` (the binary has ONE, - inherited@0x114) (−4), `watchdogAlarm`→0x54 (+76) = +72 → `minVoltage`@0x180, `watchdogAlarm`@0x184, own ends 0x1D8. -- **Torso own block:** DELETED the 7 CROSS-FAMILY shim backing fields (segmentFlags/heatModelOff/heatStateLevel/ - electricalState/creationTime/currentTime/movedFlag, −28) and their accessors now read the REAL inherited base - (`heatAlarm.GetLevel()`, `watchdogAlarm.GetLevel()`, `MechSubsystem::simulationState`, engine `Simulation:: - lastPerformance`/`ForceUpdate()`); ADDED the 4-byte `_reserved0x270` slot (+4) so `recenterActive`@0x274 / - joints@0x278/0x27C match the binary. Net −24 → own block 0xA8, **`currentTwist`@0x1D8, `sizeof(Torso)==0x280`**. -- **Compile-time LOCKS** (the practice from the resource audit, now applied to OBJECT layout — protected fields need a - `friend struct XLayoutCheck`): `TorsoLayoutCheck` (currentTwist==0x1D8, sizeof==0x280, +0x1E4/0x1F0/0x1F4/0x250/ - 0x274/0x27C), HeatWatcher (watchedLink@0x114, heatAlarm@0x12C, sizeof==0x180), PowerWatcher (minVoltage@0x180, - watchdogAlarm@0x184, sizeof==0x1D8), `WatcherGaugeAlarm`==0x54 / `WatchedConnection`==0xC, and **`HUDLayoutCheck`** - (HUD::flickerRate@0x1D8 — HUD is the only OTHER live Watcher-branch class; the lock proves the re-base fixes HUD's - own torso+0x1D8 read too). All pass. **KEY: `sizeof()` static_asserts work at namespace scope, but `offsetof()` on a - PROTECTED object member needs a `friend struct` (namespace-scope offsetof → C2248).** -- **Verified live** (`BT_TORSO_LOG`): the real Torso constructs with `currentTwist@472 (0x1D8)`, `sizeof=640 (0x280)`, - no crash over 26s; heat converges to the identical steady state (`pendingHeat` 0.239, temp 77, heatLoad plateaus — - the transient −0.265 on shot #1 was benign startup-timing variance); full combat loop intact (**TARGET DESTROYED - after 8 hits** at walk speed, matching the prior baseline). The gyro cross-link `sinkSourceSubsystem+0x1D8` now - reads the live `currentTwist` (was stub garbage). -- **Why the torso still doesn't visibly twist for the Blackhawk (a FAITHFUL result, NOT a bug):** the Blackhawk's - 0xBC5 record has **`TorsoHorizontalEnabled=0`** + empty joint names + `hRotPerSec=0` (dumped raw; the record IS a - valid Torso record — classID=0xBC5, modelSize=0x158). Our resource struct is CONFIRMED correct — the binary ctor - `@004b6b0c` reads enabled at `param_4+0x14C` (line 5038) and gates joint resolution on `enabled!=0` (line 5039), - joints at `+0x10C`/`+0x12C`, exactly our layout. So the ORIGINAL binary ALSO skips torso joints for this mech; our - behavior is byte-faithful. Visible twist needs a **twist-enabled mech** (a record with enabled=1 + real joint - names), or the clearly-marked bring-up force-enable below. -- **✅ FULL PATH PROVEN end-to-end via `BT_FORCE_TORSO` (bring-up demo; default OFF; NOT faithful).** Since the - Blackhawk record disables the torso, a gated demo (`BT_FORCE_TORSO=1`, torso.cpp ctor + TorsoSimulation) force- - enables the torso, resolves the REAL `BLH.SKL` joints, widens the limits, and sweeps an analog twist demand. Verified - (`BT_TORSO_LOG`): `Mech::ResolveJoint` resolved **`jointshakey2`→type=4 (Ball, torso body)** + **`jointtshadow`→type=1 - (HingeY, the skeleton's "apply torso twist to yaw" joint)**; `PushTwist` then fires every frame with `currentTwist` - sweeping cleanly `0→+0.7` (clamped at limit) → reverse → `-0.7` → back — i.e. the WHOLE reconstructed chain - (analog axis → TorsoSimulation integrate → clamp → UpdateJoints → PushTwist → `Joint::SetRotation`, ball branch - yaw←twist) runs correctly; no crash over 14s. The only unconfirmed link is the render pass consuming the joint - (needs an eyeball). The env-gated demo (+`BT_FORCE_TORSO_JOINT=` to pick the main joint) stays as an on-demand - verifier; faithful behavior is untouched when unset. BLH.SKL torso joints for reference: `jointshakey2` (ball, - blh_tor.bgf, under jointhip) = torso body; `jointtshadow` (hingey, under jointshadow) = ground-shadow twist. -- **Follow-on for the sibling un-stubs (Gyro/Searchlight/ThermalSight):** they inherit PowerWatcher, so after this - re-base their own fields also begin at 0x1D8, but they STILL carry the same CROSS-FAMILY shim fields → each must get - the same shim-delete + accessor-redirect (like Torso) BEFORE its un-stub, or wiring it will over-size the object. - -**◐ WAVE-5 GYROSCOPE (0xBC4) — layout + joint-I/O DONE; un-stub DEFERRED (ctor/integrator reconstruction incomplete).** -The gyro plumbing is reconstructed and the build is GREEN with the gyro back on its stub (reverted — see why below); the -infra is dormant + ready in gyro.hpp/gyro.cpp: -- **Re-base + layout (DONE, locked):** `Gyroscope : PowerWatcher : HeatWatcher : MechSubsystem` (Watcher branch). Deleted - its 4 CROSS-FAMILY shim fields (segmentFlags/heatModelOff/heatStateLevel/electricalState) + redirected the 3 used - accessors to the real inherited base (heatAlarm/watchdogAlarm/simulationState), retyped eyeJointNode/mechJointNode - `void*`→`Joint*`. `GyroLayoutCheck` friend locks `exageration@0x1D8` + `sizeof<=0x3D0` (both pass live). -- **Joint I/O (DONE, real):** the 8 node-helper stubs (NodeType/NodeScalar/SetNodeScalar/NodeVector/SetNodeVector/ - NodeRotationEquals/SetNodeRotation/NodeVectorEquals) now call the real engine `Joint` API — `GetJointType`/`GetRadians`/ - `SetRotation(Radian)` [hinge]/`GetEulerAngles`/`SetRotation(EulerAngles)` [ball]/`GetTranslation`/`SetTranslation` - [BallTranslation]; **FUN_0041d11c = SetTranslation** (writes `*(joint+0xc)` + jointModified). `ResolveJoint` forwards to - `Mech::ResolveJoint`. WriteEyeJoint/WriteMechJoint (no direct caller in the decomp, like the torso's UpdateJoints — - dispatched by the engine joint pass) are wired at the end of `GyroscopeSimulation`. -- **Verified live then REVERTED:** the real gyro constructs, resolves REAL joints (`EyeJoint='jointlocal'`, - `MechJoint='jointeye'`, both BallTranslation type=5), and `WriteMechJoint` FIRES every frame — but with **garbage - values** (`-4.31e8` = 0xCDCDCDCD-as-float, and NaN) written to `jointlocal` (the ROOT joint). Root cause: the gyro's - ctor field-init + integrator field-MAPPING are incomplete/incorrect in the reconstruction. The binary ctor @004b3778 - puts **springConstant@0x1E8, dampingConstant@0x1F4, posSpring@0x218, negSpring@0x224, rotation springs @0x2C0/0x2CC/ - 0x2F0/0x2FC**, but the recon mislabels several (e.g. assigns springConstant to eyeOrientation@0x1DC; the field the - integrator reads as `eyeSpringConstant`@0x1F4 is really dampingConstant) and leaves the integrator accumulators - (eyeAccel/eyeForce/eyeWork/bodyOrientation/bodyAccel/bodyForce/bodyWork/eyeVelocity/limits) UNINITIALISED. Writing NaN - to the root joint is unshippable, so 0xBC4 was reverted to the `Gyro` stub (combat/heat un-regressed, TARGET DESTROYED). -- **⚠ Cross-link landmine (mech.cpp):** the gyro↔torso cross-link `((SubProxy*)gyro)->linkTarget = sinkSource+0x1D8` - writes through `SubProxy::linkTarget` which sits at **gyro+4** (an engine base field), not the real gyro torso-link - offset — harmless vs the stub, would STOMP the live gyro base. Commented out; needs the real gyro torso-link field. -- **Remaining work (the real gyro un-stub):** faithfully reconstruct the ctor field-init + the two integrators - (IntegrateEyeJoint @004b2ec0 / IntegrateBody @004b30ec) and the field LAYOUT/labels from @004b3778 (the recon has a - ~196-byte layout gap + uncertain field map). This is a full subsystem-simulation reconstruction (bigger than the - torso), not a quick un-stub — do NOT guess the integrator mapping (no stand-ins). Env `BT_GYRO_LOG` dumps the layout + - joint resolve + WriteMechJoint values for verification. +## Topic file format +```markdown +--- +id: topic-id +title: "Title" +status: established | provisional | deferred | living +source_sections: "CLAUDE.md §X; docs/FILE.md; reference/decomp addresses" +related_topics: [other-topic-a, other-topic-b] +key_terms: [glossary-term-1] +open_questions: ["tracked uncertainty"] +--- +# Title +## sections… +## Key Relationships +- Uses: [[other-topic]] · Feeds: [[other-topic]] +``` diff --git a/context/_ROUTER-DRAFT.md b/context/_ROUTER-DRAFT.md deleted file mode 100644 index 17873c6..0000000 --- a/context/_ROUTER-DRAFT.md +++ /dev/null @@ -1,153 +0,0 @@ -# BattleTech Pod-Port — Reverse-Engineering Expert Knowledge Base - -> ⚠️ **DRAFT of the proposed slim `CLAUDE.md`.** This is the router that would REPLACE the -> 2236-line `CLAUDE.md`, moving its deep knowledge into `context/*.md`. It is a draft for review -> — the live `CLAUDE.md` is unchanged until this is approved + the remaining topic files are -> migrated. See `phases/phase-01-context-restructure.md` for the migration plan + status. - -**Project:** Port VWE's 1995 arcade **BattleTech** (BT) pod game (MUNGA engine, Tesla platform -4.10) to modern Windows + the pod hardware, by reconstructing the missing BT game logic from the -`BTL4OPT.EXE` binary on top of the working WinTesla engine. -**Repo of record:** the top-level `CMakeLists.txt` + `README.md` build `btl4.exe`. Layout: -`engine/ game/ content/ docs/ reference/ tools/ context/`. - ---- - -## YOU ARE THE BATTLETECH PORT EXPERT - -**You (Claude) are the expert on this port** — the 1995 MUNGA/BT engine, its asset formats, the -decompilation, and the reconstruction state. The `context/*.md` files, `reference/glossary.yaml`, -`reference/decomp/` (the raw pseudocode), and the `docs/*.md` ledgers are YOUR knowledge base. - -**ALWAYS consult the context system before answering a question about the engine, formats, a -subsystem, or the reconstruction state — and before writing reconstruction code.** Do not rely on -training data; this is a specific, reverse-engineered 1995 codebase. The knowledge here is more -precise than anything you can infer. - -### How to answer a question -1. **Identify the topic(s)** via the Quick Lookup table below (most questions touch 1-3). -2. **Read the full topic file** — don't guess from the filename. -3. **Follow `[[wiki-links]]`** and `related_topics` frontmatter to adjacent topics. -4. **Check `context/decomp-reference.md`** for any offset, ClassID, resource type, address, or env gate. -5. **Check `reference/glossary.yaml`** for a term's meaning. -6. **Check `context/open-questions.md`** for known unknowns / deferred systems. -7. **Cite** the file + section (and the `docs/*.md` ledger or `reference/decomp` address when relevant). - -### How to reason about / reconstruct something novel -1. **Ground it in the decomp.** Read the raw `FUN_xxxx` in `reference/decomp/all/part_*.c`; map - `FUN_`/`DAT_`/`this+0xNN` to engine symbols via BT headers + the WinTesla MUNGA source + - `game/reconstructed/CLASSMAP.md` + RP's parallel code. -2. **Check the gotchas.** `context/reconstruction-gotchas.md` — layout (shadow/alias/phantom - fields, resource mismatch), linkage (`/FORCE`), dtor-epilogue, databinding trap. Most bugs are here. -3. **RULE: no stand-ins.** The full logic IS in the pseudocode; a "gap" is an unfilled stub, not a - hole. Never write placeholder logic — read the decomp. Bring-up scaffolding is marked + temporary. -4. **Verify honestly.** `static_assert`-lock layouts; run env-gated; read `btl4.log`; cdb on crashes. - Tag claims with the evidence tier; flag T3/T4. -5. **Persist insights.** A genuinely new finding → add it to the right `context/*.md` (+ the `docs/` - ledger for detail), with an evidence tier. Keep the knowledge base current — this is a mandate, - not an afterthought. - -### Quick Lookup -| User asks about... | Read this file | -|---|---| -| What the project is, engine, platform, goal | `context/project-overview.md` | -| Missing BT source, decompilation strategy | `context/source-completeness.md` | -| The WinTesla Windows port (renderer/audio/HAL) | `context/wintesla-port.md` | -| Build / run / debug / repo layout / env gates | `context/build-and-run.md` + `context/decomp-reference.md` §6 | -| Asset formats (MOD/SKL/ANI/BMF/BSL/SLD/…) | `context/asset-formats.md` | -| BGF geometry, LODs, CONN/PCONN, ramps | `context/bgf-format.md` | -| A layout/linkage/databinding BUG | `context/reconstruction-gotchas.md` | -| The reconstruction method / workflow | `context/reconstruction-method.md` | -| Walking, gait, ground model, collision | `context/locomotion.md` | -| Subsystems, the factory, heat/weapons/power | `context/subsystems.md` | -| Damage zones, targeting, firing, death | `context/combat-damage.md` | -| Rendering, LODs, materials, sky, shadows, beams | `context/rendering.md` | -| Cockpit gauges / MFD HUD | `context/gauges-hud.md` | -| Multiplayer, replication, netcode | `context/multiplayer.md` | -| Pod hardware, monitors, RIO, MFD surfaces | `context/pod-hardware.md` | -| Content archives, maps, where data lives | `context/content-archives.md` | -| Offsets, ClassIDs, addresses, resource types, env vars | `context/decomp-reference.md` | -| What we don't know / deferred systems | `context/open-questions.md` | -| Term / acronym definitions | `reference/glossary.yaml` | -| Detailed running ledgers | `docs/RECONCILE.md`, `docs/GAUGE_COMPOSITE.md`, `docs/HARD_PROBLEMS.md`, `docs/SUBSYS_PLAN.md`, `docs/P3_LOCOMOTION.md` | - ---- - -## Evidence Tiers (adapted for reverse-engineering) - -Tag individual claims (not sections) inline: `[T1]`, `[T2]`, … A claim inherits the **highest -(least certain)** tier of its inputs. Flag T3/T4 to the user. - -| Tier | Label | Meaning | -|---|---|---| -| **T0** | Engine/header truth | A fact from the WinTesla MUNGA/L4 source or a BT header — the actual code we compile against. Authoritative. | -| **T1** | Decompiled + verified | Read from the binary pseudocode (`reference/decomp`) AND confirmed — disasm, `static_assert` offset lock, or cross-checked vs the RP analog. | -| **T2** | Reconstructed + runtime-verified | Implemented in `game/reconstructed/` and verified LIVE (combat un-regressed, gauge renders, log confirms the value). | -| **T3** | Best-effort / guarded | Reconstructed but not fully verified — a marked stand-in or a guarded raw-offset read. Runs, fidelity uncertain. | -| **T4** | Unconfirmed hypothesis | A claim about the binary's intent not yet decompiled/verified; a guess. | - ---- - -## Conventions (CRITICAL — full detail in `context/reconstruction-gotchas.md`) -- **Never raw-read a compiled object's offsets** (`*(T*)(obj+0xNN)`) — our layout ≠ the 1995 binary. - Use named members / accessors / a **bridge** fn in a complete-type TU. (databinding trap) -- **Never re-declare an engine-base field** — it shadows the base (reads `0xCDCDCDCD`) + mis-offsets. -- **`/FORCE` hides unresolved symbols** — an unresolved external → runtime AV near `__ImageBase`, not - a link error. Grep the link log when a build "succeeds" but crashes at a garbage call target. -- **Don't reconstruct dtor compiler-glue** — the trailing base/member-dtor calls run the chain twice. -- **`static_assert`-lock every reconstructed layout** against the binary's offsets/sizeof. -- **A `+0x128`-style owner offset is the subsystem ROSTER, not the segment table.** -- **RULE: no stand-ins** — read the decomp; a gap is an unfilled stub. -- **Keep the knowledge base current** — new durable findings → `context/*.md` (+ `docs/` for detail). - -## DO NOT -- Present T3/T4 claims as established without flagging the tier. -- Trust a factory `case ClassID` label — it's mislabeled; use the ctor-address + `CLASSMAP.md`. -- Issue `DestroyEntityMessage` on a mech death (the wreck STAYS; removal = the P5 teardown crash). -- Conclude "gauge not built" from an early process kill — the gauge renderer builds LAZILY. -- Use `DebugStream` (ReconStream, a no-op) for a log — use `DEBUG_STREAM`. - ---- - -## Project Structure -``` -bt411/ -├── CLAUDE.md # this router (was the 2236-line monolith) -├── README.md # build of record -├── context/ # topic files (curated knowledge graph) -│ ├── decomp-reference.md # offsets/ClassIDs/addresses/env gates (the "equations" hub) -│ ├── reconstruction-gotchas.md # the systemic bug classes (conventions hub) -│ ├── open-questions.md # deferred systems + get-from-Nick -│ └── .md # one concept each -├── reference/ -│ ├── glossary.yaml # terms/acronyms -│ ├── decomp/ # raw Ghidra pseudocode (the source-of-truth) -│ └── ghidra_scripts/ # the exporters -├── docs/ # detailed running ledgers (RECONCILE, GAUGE_COMPOSITE, HARD_PROBLEMS, …) -├── phases/ # restructuring / investigation logs -├── engine/ game/ content/ tools/ # the actual port (see README.md) -``` - -### How to add content -- **New finding on an existing topic** → edit the `context/*.md` (+ the `docs/` ledger for full detail). -- **New topic** → new `context/*.md` with YAML frontmatter; add to the Quick Lookup table + cross-refs. -- **Split** a topic file that exceeds ~25k tokens into finer subtopics. -- **New investigation** → a `phases/*.md` log. -- The `docs/*.md` ledgers stay as the DETAILED logs; `context/*.md` are the curated digests that route to them. - -## Topic file format -```markdown ---- -id: topic-id -title: "Title" -status: established | provisional | deferred | living -source_sections: "CLAUDE.md §X; docs/FILE.md; reference/decomp addresses" -related_topics: [other-topic-a, other-topic-b] -key_terms: [glossary-term-1] -open_questions: ["tracked uncertainty"] ---- -# Title -## sections… -## Key Relationships -- Uses: [[other-topic]] · Feeds: [[other-topic]] -``` diff --git a/context/asset-formats.md b/context/asset-formats.md new file mode 100644 index 0000000..aa68618 --- /dev/null +++ b/context/asset-formats.md @@ -0,0 +1,54 @@ +--- +id: asset-formats +title: "Asset Formats — MOD/SKL/ANI/BMF/BSL/textures/gauges/collision" +status: established +source_sections: "PROGRESS_LOG.md §5; docs/ASSET_PIPELINE.md; docs/BGF_FORMAT.md" +related_topics: [bgf-format, rendering, locomotion, combat-damage, gauges-hud] +key_terms: [MOD, SKL, ANI, BMF, BSL, VTX, SLD, DZM, PCC, GIM, material-ramp] +--- + +# Asset Formats + +All reverse-engineered. Geometry (BGF) has its own topic ([[bgf-format]]); the full pixel/material +spec is `docs/ASSET_PIPELINE.md`. Full list: `docs/PROGRESS_LOG.md §5`. + +## Definition / skeleton / animation +- **`.MOD`** — INI mech/object definition: skeleton, skins, collision, gauges, physics, animations, + class. The `.MOD` physics (MoverMass/drag/MaxAcceleration) governed thrust VEHICLES + collisions, + NOT walk gait ([[locomotion]]). [T1] +- **`.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). [T1] +- **`.ANI`** — INI keyframe animation. `[HEADER] framecount/framerate(30)/skeletonfile`; `[frameN]` + joint→`rx ry rz` radians (balltranslate→translate); `[RootTranslation]` per-keyframe `x y z` + where **`.z` = forward root SPEED** (units/s). Walk anims have no baked root translation — forward + motion is animation-driven from `.z` ([[locomotion]]). [T1] + +## Materials / textures +- **`.BMF`** — `DIV-BIZ2` (FILETYPE=1) material+texture LIBRARY (no pixels). Material → DIFFUSE color + + `MATERIAL_TEXTURE`→texture→`TEXTURE_MAP` image basename. Tags: `0x18 BITSLICE` (BSL slice select), + `0x28` ramp ref, `0x26` emissive, `0x30/0x31` ramp def. [T1] +- **Texture pixels:** `.tga` (Truevision), `.vtx` (`DIV-VTX2`, trailing RGB), and **`.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 + `0x18 BITSLICE` tag picks the slice, absent=0). Decoding BSL as RGB = the "rainbow/graffiti mech" + bug. Reference reader: `DivLoader/VGCDivLoader.cpp:323-410`. [T2] +- **`.VMF`** — `DIV-VIZ2` material SOURCE. Carries the `SCROLL` animation + `MAP{}` image binding for + effects (beams/muzzle/smoke/sky). THE key effects asset ([[rendering]]). [T2] +- **[[material-ramp]]** — the IG board colours no-normal geometry by a 2-endpoint RAMP; details in + [[bgf-format]] + [[rendering]]. [T2] + +## Collision / damage / subsystems +- **`.SLD`** collision solids (Block/YCyl/Cone/Ramp/Wedge). Terrain is modeled AS collision volumes; + see [[locomotion]] ground model + [[combat-damage]]. [T1] +- **`.DZM`** = INI damage-zone→material map (mech skin damage states). **`.DMG`/`.TBL`** damage + zones/tables, **`.SUB`** subsystems (weapon params — DischargeTime/RechargeRate/WeaponRange), + **`.CTL`** control maps. [T1] + +## Cockpit HUD assets ([[gauges-hud]]) +- **`.GIM`** vector MFD/radar line maps + **`.GAT`** color table; **`.PCC`** ZSoft PCX 8-bit gauge + rasters (fonts helv15/helv42, label strips, dials). [T1] + +## Key Relationships +- Geometry detail: [[bgf-format]]; pixel/material spec: `docs/ASSET_PIPELINE.md`. +- Consumed by: [[rendering]], [[locomotion]] (SKL/ANI), [[combat-damage]] (SLD/DZM/SUB), [[gauges-hud]] (GIM/PCC). diff --git a/context/build-and-run.md b/context/build-and-run.md new file mode 100644 index 0000000..3d57e70 --- /dev/null +++ b/context/build-and-run.md @@ -0,0 +1,68 @@ +--- +id: build-and-run +title: "Build / Run / Debug — recipe, repo layout, env gates" +status: established +source_sections: "PROGRESS_LOG.md §10a, §10a-bis; README.md" +related_topics: [wintesla-port, decomp-reference, reconstruction-method] +key_terms: [BTL4OPT, cdb, BTL4.RES, EGG, WinTesla] +--- + +# Build / Run / Debug + +The one top-level `CMakeLists.txt` builds `munga_engine` (engine) + `bt410_l4` (reconstructed BT +game lib) + `btl4.exe`. Full recipe + repo-layout map: `docs/PROGRESS_LOG.md §10a / §10a-bis` + +`README.md`. Env-gate table: [[decomp-reference]] §6. + +## Build (Win32 / VS2019 BuildTools — the DXSDK link libs are Lib/x86) +``` +# configure once: +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 +``` +- Links DXSDK d3d9/d3dx9/dinput8 + OpenAL/libsndfile (`engine/lib/`). DXSDK June 2010 at + `C:\Program Files (x86)\Microsoft DirectX SDK (June 2010)\` (overridable `-DDXSDK`). [T2] +- Linker uses **`/FORCE`** — tolerates header-defined globals + the **dead offline-factory + unresolved externals in mech3.cpp** (`Mech::CreateSubsystemStream` references `void*`-signature + `CreateStreamedSubsystem` for every class; never called at runtime). ⚠ `/FORCE` also HIDES real + unresolved symbols as runtime AVs — see [[reconstruction-gotchas]] §6. [T2] +- Editing an `engine/` file rebuilds the engine lib automatically (one project). A NEW member on a + `DPLRenderer`/`d3d_OBJECT` class needs the game objs that embed its layout recompiled — delete + stale objs if layout-mismatch corruption appears. [T2] +- ⚠ Kill the running exe before rebuilding (`taskkill //F //IM btl4.exe`) or you get LNK1104. + +## Run +``` +run\run.cmd [EGG] # default DEV.EGG; cd's to content\ and runs btl4.exe -egg +``` +- **cwd MUST be `content\`** — the engine resolves `BTL4.RES`, `VIDEO\`, `BTDPL.INI`, eggs relative + to cwd (the `loadTables` gotcha: `L4VIDEO.cpp:849` `fopen("VIDEO\\REPLACEMATS.tbl")` is relative + + unchecked → fread on NULL if cwd is wrong). Logs to `btl4.log` in `content\` (or `BT_LOG=`). [T2] +- Interactive: **WASD** drive, **Space/Ctrl** fire, **X** all-stop. Default egg = `DEV.EGG` + (map=grass, time=day). Swap mech via the egg's `vehicle=` (bhk1/madcat/…). The code path is + mech-agnostic (verified — Mad Cat booted + fought with zero code changes). [T2] + +## Debug (cdb x86) +`"C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\cdb.exe"`. Pattern for a faulting stack (cwd += content\): `-g -c ".lines;sxe av;g;kp 24;q"` with `BT_ASSERT_TO_DEBUGGER=1`. Debug CRT fills fresh +heap **0xCDCDCDCD** (uninit) + freed **0xFEEEFEEE** — invaluable for "was this ever constructed?". +`BT_HEAPCHECK=1` = whole-heap validation every alloc/free (O(n²) at mission load — SLOW). To attach +to a frozen abort dialog: `cdb -p -c ".lines;~*kp 30;q"`. [T2] + +## Repo layout (bt411) +- **`engine/MUNGA/` + `engine/MUNGA_L4/`** — the shared 2007 MUNGA engine + Win32/D3D9 HAL (carries + our BT render/loader work: bgfload/L4D3D/L4VIDEO + the image codec). `engine/shim/` (ATL), + `engine/lib/` (OpenAL/libsndfile), `engine/rp/` (RP *headers* the audio HAL includes). +- **`game/reconstructed/`** — the reconstructed BT source (the bulk). +- **`game/original/BT/` + `BT_L4/`** — surviving original BT `.cpp` + **all BT headers** (the include path). +- **`game/fwd/`** — ~186 forwarding shims (`#include ` → `../../engine/MUNGA/.h`). +- **`game/btl4main.cpp`** — the WinMain launcher. +- **`content/`** — the runtime tree (BTL4.RES, VIDEO/, GAUGE/, AUDIO/, eggs, BTDPL.INI). Run cwd. +- **`reference/decomp/`** — the raw Ghidra pseudocode (`all/part_*.c`) — the source-of-truth. +- **`docs/`** — the detailed ledgers (incl. `PROGRESS_LOG.md`, the full pre-restructure CLAUDE.md). +- **`tools/`** — btconsole.py, disas2.py, map/res scanners. **`context/`** — this knowledge base. + +## Key Relationships +- Base: [[wintesla-port]] (the engine build recipe). +- Verify loop: [[reconstruction-method]]; env gates: [[decomp-reference]] §6. diff --git a/context/combat-damage.md b/context/combat-damage.md new file mode 100644 index 0000000..25b793d --- /dev/null +++ b/context/combat-damage.md @@ -0,0 +1,74 @@ +--- +id: combat-damage +title: "Combat & Damage — targeting, firing, damage zones, death" +status: established +source_sections: "PROGRESS_LOG.md §10c; docs/HARD_PROBLEMS.md (P5)" +related_topics: [subsystems, decomp-reference, reconstruction-gotchas, locomotion, rendering] +key_terms: [damage-zone, TakeDamage, viewpoint-entity, master, replicant] +open_questions: + - "Per-impact aim (cylinder lookup); collision damage application; DeathShutdown + collapse anim" +--- + +# Combat & Damage + +The drive→target→fire→damage→destroy loop. Full detail: `docs/PROGRESS_LOG.md §10c`; the death/ +teardown forensics: `docs/HARD_PROBLEMS.md` (P5). + +## No AI — BT is PvP-only (verified, not a recovery gap) +The mission system is NETWORKED and BT never had NPCs/AI. Every mech is driven by a `BTPlayer` (or a +`BTCameraDirector` spectator). The BT registry knows only Mech/Projectile/Missile/BTPlayer/BTTeam; no +Bot/Brain/AI class exists; `BTL4OPT.EXE` has no AI strings. `Crusher/Blocker/Runner` are RED PLANET +classes, NOT BT. ⇒ a solo mission has no enemy by design; `BT_SPAWN_ENEMY` is a TEST DUMMY +(uncontrolled Mech). The authentic opponent path is [[multiplayer]]. [T1] + +## Entity spawn + the per-mech gate +`Mech::Make(MakeMessage*)` (`FUN_004a2d48`) allocs `sizeof(Mech)` + runs the ctor; the base Entity +ctor self-registers it (renders + ticks). `Mech::PerformAndWatch` runs for EVERY mech → gate +player-only logic on `this == application->GetViewpointEntity()` (else a spawned enemy is driven by +player input). [T1] + +## Targeting (mech offsets, [[decomp-reference]] §3) +`mech+0x37c` = target world Point3D; `mech+0x388` = target `Entity*` (the `HasActiveTarget()` gate); +`mech+0x38c` = targeted sub-zone (−1=whole). Weapons cache hasTarget/targetPoint/muzzlePoint, +refreshed each frame. [T1] + +## Firing arc + muzzles +There is NO firing-arc field in the binary (`Emitter::FireWeapon` fires whenever HasActiveTarget) — +the 1995 game got away with it because it was **cockpit-only** (a beam behind you is off-camera). The +port's external camera exposes it, so the arc is gated on the **torso twist range** +(`Torso::GetHorizontalReach`, 0 for the twist-disabled Blackhawk) + a base aim tolerance +(`BT_FIRE_ARC`, a labeled PORT presentation param). Beams fire from the mech's real gun-port SITE +segments (siterugunport etc.) resolved via `GetSegment(name)→segmentToEntity×localToWorld`. [T2] + +## Weapon fire is a PORT reconstruction (the dpl_* beam layer is unported) +`Emitter::FireWeapon` builds a muzzle→target beam but the `dpl_*` beam renderable was never ported → +the beam is drawn by `BTPushBeam`/`BTDrawBeams` (L4VIDEO — an additive quad / the real `ermlaser.bgf` +tube with the scrolling `bexp` grit). Fire rate is decoded-exact (DischargeTime 0.2 + RechargeRate 2.2 +per ER-M laser; [[decomp-reference]] §5). Flying projectiles (LRM/autocannon) are also a PORT +reconstruction (`BTPushProjectile` — the 2007 Entity is too small for the binary's raw integrator +offsets). [T2] + +## Damage delivery + the real damage model +`Entity::TakeDamageMessage(id, size, inflictingEntityID, zone, Damage&)` → `target->Dispatch`. +**Base handler IGNORES zone==−1** (the Mech cylinder-lookup override for unaimed hits is not +reconstructed — send a VALID zone). `Mech__DamageZone::TakeDamage` → engine armor model (`damageLevel ++= damageAmount*damageScale[type]`) → 1.0 = zone destroyed → death. The Mech ctor populates the +inherited `damageZones[]` from the DamageZoneStream (type 0x14) resource. `class Damage{ damageType +(Collision/Ballistic/Explosive/Laser/Energy), damageAmount, damageForce, surfaceNormal, impactPoint, +burstCount }`. Verified: `structure` climbs, vital zone destroyed → `*** TARGET DESTROYED ***`. [T2] +Two non-field fixes were needed: **message-handler chaining** (an unchained set drops TakeDamage +silently) + **entity validity** (`SetValidFlag()` on a manually-spawned entity) — see +[[reconstruction-gotchas]] §9. [T2] + +## Death — the wreck STAYS (P5 CLOSED) +A killed mech does NOT disappear — BT death is a STATE transition +(`SetGraphicState(DestroyedGraphicState)` + death anim + effect/splash), the mech becomes a persistent +WRECK (RP analog: `VTV::DeathShutdown`). **NEVER issue `DestroyEntityMessage` on death** — the +teardown crash was an artifact of forcing a removal the original never does. The actual bug was ONE +thing: an EXPLICIT `JointedMover::~JointedMover()` in the reconstructed `~Mech` ran the whole base-dtor +chain TWICE (the dtor-epilogue rule, [[reconstruction-gotchas]] §7) → a double-free of `collisionLists` ++ the segment table. Removing it fixed BOTH the death-row crash AND app-exit crashes. [T2] + +## Key Relationships +- Weapons/roster: [[subsystems]]. Aim source: [[locomotion]] (drive/facing). Effects: [[rendering]]. +- P5 forensics: `docs/HARD_PROBLEMS.md`. Data: [[decomp-reference]] §4-5. diff --git a/context/content-archives.md b/context/content-archives.md new file mode 100644 index 0000000..b290f5a --- /dev/null +++ b/context/content-archives.md @@ -0,0 +1,48 @@ +--- +id: content-archives +title: "Content Archives — where the game data lives + loadable maps" +status: established +source_sections: "PROGRESS_LOG.md §4, §4a" +related_topics: [asset-formats, build-and-run, decomp-reference] +key_terms: [BTL4.RES, EGG, WinTesla, BT] +--- + +# Content Archives + +The deployed game data was **never in source control** — it lived on the pod machines, spread +across archives. The bt411 `content/` dir is the assembled runtime tree. Full detail: +`docs/PROGRESS_LOG.md §4, §4a`. + +## The archives (historical provenance) +| Archive | What | Has | +|---|---|---| +| `410srczipped.zip` | Source code (36MB) | all `.cpp/.hpp`, geometry `.bgf`, materials `.bmf`, **animations `.ani`** (428). NO per-mech skins/skeletons. | +| `Tesla4NovellTechPC-*.zip` | pod technician/test PC (77MB) | `btrel410.exe`, Division VPX board diagnostics, shared generic textures, Novell DOS. | +| `Tesla4PodPCNovell-*.zip` | the pod GAME machine (92MB) | **the full deployed tree** — per-mech skeletons (`AVA.SKL`, 124), skins (`AVATAR.BSL`), 1143 models, 1624 material libs, textures. | +| `Elsewhen RP411 Source-*.zip` ⭐ | the **WinTesla Windows port** | the modernized engine (see [[wintesla-port]]). | + +**Content root for the port** = the pod tree (skeletons/skins/geometry); **animations** come from +the source tree's `CONTENT/BT/ANIMS/`. All assembled into bt411 `content/`. [T2] + +## The runtime archive +`content/BTL4.RES` (3.23 MB, byte-identical to `reference/decomp/BTL4.RES`) contains the built +subset: models, materials, skeletons, animation clips, damage-zone/subsystem records, and **8 +loadable maps**. Resolved by name/ID via `FindResourceDescription` (a missing map name null-derefs +at `MISSION.cpp:355`). [T1] + +## Loadable maps (the 8 in the RES) +`cavern` (default night cave, enclosed), `grass` (grassland), `rav` (ravine), `polar3`/`polar4` +(snow), `arena1`/`arena2`, `dbase` (desert). **Switch via a copied `.EGG`'s `map=` field** (+ +`time=day` for outdoor). ⚠ Names carry the suffix (`arena1`/`polar3`/`rav`), NOT bare +`arena`/`polar`. `des`/`burnt`/`frstrm` are NOT in this RES (lab/other-build maps — `.map` source +exists in `CONTENT/BT/MAPS/`, would need compiling via the DOS `btl4tool.exe`). All 8 boot + render ++ fight clean (map sweep complete). [T2] + +## The full content SOURCE tree +`CONTENT/BT/` (in `410srczipped`) is comprehensive: 17 `.map`, 636 `MODELS/`, 219 `SOLIDS/`, 331 +`ANIMS/`, 1958 `VIDEO/` textures, 207 `AUDIO/`, 404 `GAUGE/`. The assets to build ANY map exist; the +runtime RES is the already-built subset. [T1] + +## Key Relationships +- Formats: [[asset-formats]], [[bgf-format]]. +- Loaded by: [[build-and-run]] (run cwd = content/), [[rendering]]. diff --git a/context/locomotion.md b/context/locomotion.md new file mode 100644 index 0000000..fbff5ea --- /dev/null +++ b/context/locomotion.md @@ -0,0 +1,83 @@ +--- +id: locomotion +title: "Locomotion — gait, the ground model, collision, shadow" +status: established +source_sections: "PROGRESS_LOG.md §7, §10 (gait/ground/collision/shadow); docs/P3_LOCOMOTION.md" +related_topics: [asset-formats, decomp-reference, reconstruction-gotchas, combat-damage, rendering] +key_terms: [RootTranslation, ground-model, SequenceController, MechControlsMapper, SLD, joint] +open_questions: + - "Authentic per-mech turn-rate constant (currently bring-up constant)" + - "Body-callback gimp handlers (states 16-19); airborne callbacks" +--- + +# Locomotion + +Walking, the gait controller, the ground/collision model, and the shadow. All DEFAULT-ON now +(`BT_GAIT_CUTOVER`, `BT_GAIT_SM`, `BT_COLLISION`, `BT_REAL_CONTROLS` — set `=0` to fall back). Full +detail: `docs/P3_LOCOMOTION.md` + `docs/PROGRESS_LOG.md §7, §10`. + +## Walk speed is animation-driven (not physics) +Each `.ANI` has a `[RootTranslation]` where **`.z` = forward root SPEED** (world units/s). The +engine `AnimationInstance::Animate(dt, move_joints)` integrates `movement += Δt·rootTrans.z` and +moves the body that far → feet plant by construction (no stride constant; k=1). Idle anims have +z=0. The `.MOD` physics governed thrust VEHICLES, NOT walk gait. [T1] + +## The two-channel gait (real controls) +The BT `SequenceController` (`seqctl.cpp`, from `SelectSequence@004277a8`+`Advance@0042790c`) drives +locomotion. Under `BT_REAL_CONTROLS` two channels run: +- **LEG channel** writes the skeleton JOINTS from the live demand (a local sim; its joint writes are + overwritten by the body). +- **BODY channel** is the DISPLAYED motion — travel comes from the SAME Advance that writes the + displayed joints (`move_joints=1`), so display == travel BY CONSTRUCTION (v4 fix — killed the + desync class). [T2] +The gait STATE MACHINE (`AdvanceBodyAnimation`, mech2.cpp) slews `bodyCycleSpeed` toward the demand +through walk/run caps (`LoadLocomotionClips`, mech3.cpp:326 — measures `walkStride`/`standSpeed` via +`legAnimation.SelectSequence`). Transition callbacks (`Mech::BodyClipFinished @0x4a6d8c`, +`LegClipFinished @0x4a6928` — PE-parsed jump tables) alternate walk 6↔7 / run 12↔13 + walk→run. [T2] +⚠ **Aliasing bug class (namedClip):** `namedClip[]`@0x5e0 == `animationClips[]`@0x5cc+0x14 (ONE array); +`gimpBaseClip`@0x64c == `animationClips[0x20]`; `bodyAnimationState`@0x728 == `bodyStateAlarm.level`. +Declared-separate members don't see each other's writes — alias them. + +## The AUTHENTIC 1995 ground model (default-on, `BT_GROUND_REAL`) +Decoded by the 10-agent `ground-model-decode` workflow (binary `FUN_004a9b5c` = the master perf). +**NO GRAVITY anywhere** — accelerations zeroed every frame. Vertical position = an ABSOLUTE per-frame +ground SNAP: +1. ctor lifts `collisionTemplate->minY` by 5% of volume X-width (the probe height + wall-vs-floor separation). +2. per frame `MoveCollisionVolume()` re-finds the containing node in the zone's **BoundingBoxTree** + (BOXTREE.cpp — NOT a heightfield). +3. probe `q = origin + (0, lift, 0)` → `FindBoundingBoxUnder(q, &h)` (h = distance down to the highest + solid top; −1 = miss). +4. snap gate `h > 1e-4`: `origin.y -= (h − lift)` ⇒ y = surfaceY exactly. **MISS ⇒ NOTHING** (y holds, + no runaway). Up-slope bounded by the lift window (implicit step allowance); walk-offs drop instantly. +5. `GetCurrentCollisions` → `ProcessCollisionList` → `Mech::ProcessCollision` (@0x4abb40, vtbl+0x3c): + BoxedSolid resolver → StaticBounce → owner classification. Crushable icon (flags&0x8000) = damage + 0.00123f sentinel = move stands; damage>0 = **FULL FRAME REJECTION** (walls block, never slide); + impactVel²>40 = crash-anim (SetLegAnimation(0x20)). [T2] +KEY: `FindSmallestNodeContainingColumn`/`FindBoundingBoxUnder` (`FUN_0040e36c`/`0040e5f0`, BOXTREE.CPP) +are ALIVE in the 2007 engine — the "heightfield" labels were wrong (caused two reverted attempts). A +master mech needs a **CollisionAssistant** (GetCurrentCollisions iterates it unchecked). BT solids +ship ZERO tiles (h never negative). [T2] + +## Knockdown / crash + the desync/stutter fixes +Wall impacts iv²>40 (~6.3 u/s) bind the `bmp` stagger clip. Two documented bug fixes: (a) the +knockdown must stagger BOTH channels (`SetBodyAnimation(0x20)` + `SetLegAnimation(0x20)`) or display ++ travel split permanently (foot-slip); (b) a contact-hysteresis gate (0.4 s `gBlockCooldown`) +stops the knockdown re-firing on sustained/glancing contact. `localVelocity` is zeroed while crashed +(spec-faithful) so a knocked mech can't keep advancing. [T2] + +## Controls (`BT_REAL_CONTROLS`, default-on) +`MechControlsMapper` (mechmppr.cpp @004afbe0; btl4mppr.cpp mappers) interprets input → `speedDemand` +/ `turnDemand`. ⚠ **WndProc NEVER receives WM_KEYUP** (the engine's per-frame reader `GetMessage`s +them out) → poll `GetAsyncKeyState` per frame, gated on foreground. Virtual controls: W/S sweep a +persistent throttle LEVER with a zero detent; A/D a momentary auto-centering stick. [T2] + +## Shadow (default-on, `BT_SHADOW_TILT`) +BT 4.10's shadow is the flat `*_tshd.bgf` proxy drawn as blend-pass geometry, posed by `jointshadow` +(terrain tilt) + `jointtshadow` (torso twist). Terrain-tilt samples the collision surface gradient; +a depth-bias (`D3DRS_DEPTHBIAS`, decal-style) stops it z-fighting/vanishing on inclines. Polar maps +have STUB `*_tshd` (shadow authored-off on snow). Also the visual-ground conform (`btvisgnd.cpp`, +render-only lift to the visible terrain triangles — presentation only, never touches localOrigin). [T2] + +## Key Relationships +- Detail: `docs/P3_LOCOMOTION.md`. Uses: [[asset-formats]] (SKL/ANI), [[decomp-reference]] (offsets). +- Feeds: [[combat-damage]] (collision→damage), [[rendering]] (shadow/visual-conform). diff --git a/context/multiplayer.md b/context/multiplayer.md new file mode 100644 index 0000000..038b565 --- /dev/null +++ b/context/multiplayer.md @@ -0,0 +1,63 @@ +--- +id: multiplayer +title: "Multiplayer — replication, netcode, the console" +status: provisional +source_sections: "PROGRESS_LOG.md §7 (Phase 7), §8 (P6); docs/HARD_PROBLEMS.md (P6)" +related_topics: [wintesla-port, combat-damage, locomotion, decomp-reference] +key_terms: [master, replicant, EGG, dead-reckon] +open_questions: + - "Cross-pod combat (damage to a replicant's master); replicant gait; pod-LAN config" +--- + +# Multiplayer + +Pod-to-pod networked play. The hard infrastructure exists (WinTesla replaced DOS `NETNUB` with a +WinSock2 TCP stack); the work is integrating + smoke-testing it for BT. Full detail: +`docs/PROGRESS_LOG.md §7, §8`; the problem writeup: `docs/HARD_PROBLEMS.md` (P6). + +## The stack (already reconstructed) +- WinTesla replaced `NETNUB` with a **3446-line WinSock2 TCP** reimplementation (`MUNGA_L4/L4NET.CPP`, + `SOCK_STREAM`/ReliableMode, zero SOCK_DGRAM); the master/replicant distributed-sim core + (InterestManager/Entity/HostManager) is complete. So MP = integrate + smoke-test that stack (gated + on P3 locomotion + P5 entity lifecycle + subsystem WAVEs). The rumored `L4NET.CPP:1853` send-size + bug is DEAD CODE (`#if MESSAGE_BUFFERING`==0). [T2] + +## Master / replicant model +- **[[master]]** = a locally-simulated entity (your mech + dummies); drives itself + EMITS update records. +- **[[replicant]]** = a peer's mech, a local proxy; DeadReckons toward the master's replicated position. + MUST `SetValidFlag()` at creation or every message defers forever ([[reconstruction-gotchas]] §9). + +## How a session forms +`-net ` = networked mode (the pod listens on for a CONSOLE, boots ConsoleOnly; game mesh += +1). Solo (no `-net`) never touches WinSock. The mission egg's `[pilots]` section IS the +roster (`pilot=ip[:port]` + a per-address page); each pod self-identifies by local-IP+game-port, +connects to earlier pilots, listens for later ones — a deterministic full mesh; the mission loads when +all connect. The **CONSOLE** (operator station — ABSENT from every archive) delivers the egg as chunked +`ReceiveEggFileMessage` packets + the LAUNCH `RunMissionMessage`; **`tools/btconsole.py` is our console +emulator** (⚠ `NotationFile::ReadText` expects NUL-SEPARATED lines). [T2] + +## Verified milestones (one box, two instances) +- **P6 smoke test:** two instances share a world — console egg → TCP mesh → synchronized start → + bidirectional entity replication (each renders its own mech + the peer's REPLICANT), 0 crashes. +- **Movement replication:** A's mech WALKS on B's screen (the replicant tracks the master to ~1.5u via + dead-reckoning). The authentic mission-start ladder runs (CreatingMission→…→RunningMission via the + console LAUNCH). SIX bugs fixed to get here (dead-reckoner install, replicant-motion DeadReckon, + master emission threshold, emission gated on RunningMission, the console-must-LAUNCH fact, replicant + validity). [T2] +- **Wire-format bug class found+fixed:** MakeMessages replicate RAW over TCP, so string payload must be + INLINE (`char[N]` at the binary offsets), not a `const char*` pointer (garbage cross-pod). Check + EVERY MakeMessage for pointer payloads. [T2] + +## Debug tooling (`BT_NET_TRACE`, permanent) +`[net-tx]/[net-rx]` (L4NET), `[net-upd]` (update lookup), `[upd-repl]`, `[ent-exec]` (state ladder). +Per-instance: `BT_LOG=` + `BT_AFFINITY=` (CPU pin). Update stream ≈ 60 Hz × 144-byte +records per moving master. [T2] + +## Remaining (P6 phase 4 / Phase 7) +Cross-pod COMBAT (target a replicant + route damage to the owning master — Entity::Dispatch already +reroutes); interactive 2-window driving; replicant GAIT animation (derive from replicated velocity); +the pod-LAN config (real IPs, bare-IP pilot entries). See [[open-questions]]. [T3] + +## Key Relationships +- Base: [[wintesla-port]] (L4NET). Depends on: [[locomotion]] (update writer), [[combat-damage]] + (entity lifecycle). Detail: `docs/HARD_PROBLEMS.md` (P6). diff --git a/context/pod-hardware.md b/context/pod-hardware.md new file mode 100644 index 0000000..b0125b4 --- /dev/null +++ b/context/pod-hardware.md @@ -0,0 +1,50 @@ +--- +id: pod-hardware +title: "Pod Hardware — the fixed target (7 monitors, D3D9, RIO)" +status: established +source_sections: "PROGRESS_LOG.md §3; the PLATFORM PROFILE + GAUGE DEV-COMPOSITE notes" +related_topics: [gauges-hud, rendering, project-overview] +key_terms: [pod, RIO, MFD, IG-board, SVGA16] +--- + +# Pod Hardware (the fixed target) + +The port must run on (a) a dev box and (b) the fixed arcade pod. The pod is a hard constraint that +bounds the graphics API. Full detail: `docs/PROGRESS_LOG.md §3`. + +## Displays +- **2 video cards → 7 monitors:** main 3D view **800×600**; radar **640×480**; five monochrome MFDs + driven as one **1280×480** horizontally-spanned surface. [T1] +- 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 codepath). NOT Vulkan/D3D11+. CUDA is irrelevant (compute, not display). [T1] + +## 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 (Phase 8). The engine is a PUSH +model — `LBE4ControlsManager` groups are fed by all devices (RIO on the pod, DirectInput on dev); +the `MechControlsMapper` interprets them ([[locomotion]]). [T2] + +## Multi-surface gauge path (intact, pod-only by default) +The pod multi-surface path EXISTS and is intact: `DPLRenderer::FindBestAdapterIndices` (multi- +adapter selector, honors PRIMGAUGE/SECGAUGE/MFDGAUGE/SPANDISABLE), `SVGA16::BuildWindows` (a +fullscreen D3D device + window per gauge/MFD surface, MFD span = width×2), `L4GaugeRenderer` (gated +on `L4GAUGE`). `content/SETENV.BAT` is the authentic pod env preset (L4CONTROLS=RIO,KEYBOARD; +L4GAUGE=640x480x16; L4PLASMA=com2). [T2] + +## Platform profile switch +`-platform pod|dev` (or `BT_PLATFORM` env; default DEV) selects a runtime env preset WITHOUT forking +the codepath. **DEV** = keyboard + single 800×600 window. **POD** = RIO input (keyboard fallback off +serial) + multi-surface gauges (from SETENV.BAT/L4GAUGE on real hardware). `-platform pod` does NOT +auto-enable L4GAUGE (each surface needs its own fullscreen device on the pod's 2 cards). Off-pod, the +6 MFD surfaces render in a dev window via `BT_DEV_GAUGES` ([[gauges-hud]]). [T2] + +## MFD surface model +All cockpit surfaces are bit-plane MASKS over ONE shared `SVGA16` pixelBuffer: `sec`=palette low +byte; `Heat`=0x4000, `Mfd2`=0x0400, `Comm`=0x8000, `Mfd1`=0x0100, `Mfd3`=0x1000; `Eng1-3` = +engineering-mode alt planes; `overlay`=0x00C0 (shares the sec surface). See [[gauges-hud]]. [T2] + +## Key Relationships +- Renders: [[gauges-hud]] (the MFD surfaces), [[rendering]] (the main 3D view). +- Input: [[locomotion]] (the mapper). +- Bring-up: Phase 8 (get pod specifics from Nick — [[open-questions]]). diff --git a/context/project-overview.md b/context/project-overview.md new file mode 100644 index 0000000..0f689d5 --- /dev/null +++ b/context/project-overview.md @@ -0,0 +1,48 @@ +--- +id: project-overview +title: "Project Overview — what this is, people, goals" +status: established +source_sections: "PROGRESS_LOG.md §1-3; README.md" +related_topics: [pod-hardware, wintesla-port, source-completeness, build-and-run] +key_terms: [BT, RP, MUNGA, Tesla-platform, WinTesla, pod] +--- + +# Project Overview + +Port VWE's arcade **pod** games (Tesla platform, release 4.10, ~1995-96) to modern Windows and, +ultimately, the actual arcade pod hardware. Primary target: **BattleTech (BT)**; **Red Planet +(RP)** shares the engine. Full detail: `docs/PROGRESS_LOG.md §1-3`. + +## The stack +- Two games, one engine: BT + RP. Engine = **MUNGA**; HAL = **L4** (`MUNGA_L4`). Game logic = + `CODE/BT`, `CODE/RP`. [T1] +- Original platform: **32-bit DOS** under a **DPMI32** extender, Borland C++ 5 + TASM32, on Novell + DOS pods. 3D by a proprietary Division **IG image-generator board** (dual EISA), driven by the + closed **`libDPL.lib`** (binary + VREND microcode). [T1] +- **The port replaces the IG board/libDPL** with a `dpl_*` → Direct3D9 shim (done in WinTesla as + L4D3D). Pods now run Windows 10 + a Voodoo/Glide wrapper. BT was abandoned (MechWarrior 4 runs on + the current pods); this project revives BT. [T1] + +## People / goals +- **arcattack** (user): doing the port. NVIDIA 3060 + A6000 dev GPUs. +- **Nick:** owns the pods + the software license (porting is authorized). Runs the pod hardware. The + gating dependency — the missing BT source likely lives on his backup drives (see [[source-completeness]]). +- **Goal:** run on (a) a modern dev box and (b) the fixed pod hardware. Multiplayer (pod-to-pod) wanted. + +## Repo orientation (bt411) +This is the **clean standalone bt411 repo**. `README.md` + the top-level `CMakeLists.txt` are the +build of record. Tree: `engine/ game/ content/ docs/ reference/ tools/ context/`. Historical +sections in `docs/PROGRESS_LOG.md` cite old `C:/git/nick-games/...` paths — treat as provenance; +they map into this repo (reconstructed BT → `game/reconstructed/`, engine → `engine/MUNGA{,_L4}/`, +content → `content/`, raw decomp → `reference/decomp/`). [T2] + +## Current state (2026-07) +`btl4.exe` boots, renders the world + a skinned mech, and runs a **drive → animate → target → fire +→ damage → destroy** single-player loop. The engine/renderer/HAL/audio are done (WinTesla); the +active work is reconstructing each BT subsystem's authentic behavior from the binary. The **gauge +system is complete** ([[gauges-hud]]). [T2] + +## Key Relationships +- Full detail: `docs/PROGRESS_LOG.md`. +- Enables: everything — see [[wintesla-port]] (the base), [[source-completeness]] (the gap), + [[build-and-run]] (how to build). diff --git a/context/reconstruction-method.md b/context/reconstruction-method.md new file mode 100644 index 0000000..28c6aa3 --- /dev/null +++ b/context/reconstruction-method.md @@ -0,0 +1,56 @@ +--- +id: reconstruction-method +title: "Reconstruction Method — the loop, the no-stand-ins rule, decomp technique" +status: established +source_sections: "PROGRESS_LOG.md §10b, §5a (decompilation), §10c" +related_topics: [reconstruction-gotchas, decomp-reference, source-completeness] +key_terms: [decomp, oracle, bridge, BTL4OPT, WinTesla] +--- + +# Reconstruction Method + +How the missing BT game logic is rebuilt from the binary. The governing rule: **no stand-ins.** +Full detail: `docs/PROGRESS_LOG.md §10b`; the systemic bug classes are [[reconstruction-gotchas]]. + +## The loop (per feature) +1. Read the RAW decomp `reference/decomp/all/part_*.c` for the relevant `FUN_xxxx`. +2. Map `FUN_`/`DAT_`/`this+0xNN` to engine symbols using: the BT headers + the WinTesla MUNGA + source + `game/reconstructed/CLASSMAP.md` + **RP's parallel code** (VTV≈mech, WEAPSYS≈weapons). +3. Write the **real** reconstruction into `game/reconstructed/*.cpp`. +4. Build; run env-gated; read `btl4.log` (grep `[anim]/[drive]/[target]/[fire]/[damage]` markers). +5. cdb on any crash. `static_assert`-lock the layout against the binary's offsets. [T2] + +## RULE: no stand-ins +The full game logic IS in the pseudocode (the binary ran the game); a "gap" is a reconstruction +stub not yet filled, not a hole in the original. **Never write stand-in/placeholder logic for an +apparent gap — read the decomp.** (User: "there are no gaps, just work to be done.") Bring-up +scaffolding (env-var paths, 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. [T2] + +## Decompilation stack (Plan B — in progress) +- **Ghidra 12.1.2 + JDK21** installed under `tools/` (portable). `ExportBTSource.java` run headless + on `BTL4OPT.EXE` → real decompiled C in `reference/decomp/recovered/`. The pass is + **assert-anchored** (few funcs/file — anchored on the asserts that carry source paths). [T2] +- For a `FUN_` the assert-anchored exporter skipped: **`tools/disas2.py [len]`** (capstone + disassembly of `BTL4OPT.EXE` — recovers x87 math Ghidra drops, folds known calls + float + constants). Or `DecompVSS.java` (headless address-list decompiler). [T2] +- **Resolving a `.data` fn-pointer** (a `PTR_LAB_xxxx` callback/vtable slot the decomp didn't + export): PE-parse the DWORD at its VA, then capstone-disassemble the target. Used for the gait + callbacks (`@0x4a6d8c`), the valve handler (`@0x4ae464`), gauge widget slots. [T2] +- **`.data` float constants** are the biggest "couldn't recover" bucket (tuning values); read them + as the x87 80-bit `float10` the decomp uses, not 32-bit float (`scratchpad/rdtbyte.py`). [T2] + +## Effort model (honest) +~0.5-1.5 hr/module → ~30-50 agent-hours + human review for ~40 modules, with a long tail (modules +with no surviving `.HPP` fragment, or split across decomp windows, cost more). Process upgrades: +per-class function lists (not address clusters), complete vtable rows, recover `.data` constants. [T3] + +## Workflows (for scale) +For exhaustive multi-function decomp analysis, fan out a **read-only Workflow** (understanding +phase — one agent per function/class produces an offset map + findings, + an adversarial verify), +THEN implement hands-on so each change is visible + consistent. This is the §10c pattern; used for +the ground-model decode (10 agents), the alarm-unification (8), the gauge-widget decode (6). [T2] + +## Key Relationships +- Bug classes: [[reconstruction-gotchas]] (check FIRST). Reference data: [[decomp-reference]]. +- Why it's needed: [[source-completeness]]. diff --git a/context/rendering.md b/context/rendering.md new file mode 100644 index 0000000..5c00f31 --- /dev/null +++ b/context/rendering.md @@ -0,0 +1,75 @@ +--- +id: rendering +title: "Rendering — the D3D9 pipeline, materials, sky, beams, performance" +status: established +source_sections: "PROGRESS_LOG.md §10 (render-fidelity notes); docs/BGF_FORMAT.md" +related_topics: [bgf-format, asset-formats, wintesla-port, locomotion, pod-hardware] +key_terms: [L4D3D, material-ramp, BSL, LOD, DPL, beam] +open_questions: + - "Replacement-LOD for NON-additive multi-LOD models; day/night material path priority (BT_MATPRI)" +--- + +# Rendering + +The port renders via the WinTesla `L4D3D`/`L4VIDEO` Direct3D9 path ([[wintesla-port]]), carrying our +BT loader (`bgfload.cpp`) + fidelity work. The goal: match the authentic pod footage. Geometry/LOD +mechanics are in [[bgf-format]]; this topic covers the D3D9 draw path + the fidelity fixes. Full +detail: `docs/PROGRESS_LOG.md §10`. + +## The IG-board shading model (from libDPL DPLTYPES.H) +Shading is selected PER-GEOMETRY by vertex type: **no-normal geometry** (terrain/mesas/sky/buildings/ +mech — WHITE baked verts) is UNLIT, colorized by the material's 2-endpoint **[[material-ramp]]** +(`dpl_SetMaterialRamp` — texture luminance indexes a low→high gradient); **normal-bearing geometry** +(~150 vehicle/missile files) is LIT by the map light. Applying the ramp to lit geometry = the "dusty +white blobs" bug (gate on `hasNormals`). [T2] + +## The big render-fidelity fixes (dbase/desert matched to pod footage) +- **Geometry:** CONN is a flat trilist (not fan); CONN+PCONN coexist — see [[bgf-format]]. Malformed + fan-garbage was the "faceted mountains" bug. [T2] +- **RAMP colorize** (default-on `BT_RAMP`) + **day/night material priority** (default-on `BT_MATPRI` for + the ramps): the `mat\day` libs carry the warm ramps; the generic GEO libs are gray. Terrain/mesas + render warm tan; the sky's `dsky_mtl` ramp (0→0.99) gives bright clouds. `hasNormals` gate keeps + vehicles LIT (colored trucks). Ramp tint rule: explicit diffuse + NEUTRAL ramp ⇒ tint; coloured ramp + ⇒ white. [T2] +- **Sky:** a `*sky.bgf` dome/plane drawn in a dedicated fullbright **PASS_SKY** (LIGHTING off, extended + far=9000 + world fog); tagged `drawAsSky` at load (the `.x` sidecar path doesn't exist). [T2] +- **The env pipeline** (`DPLReadEnvironment`→`DPLReadINIPage`, reads **`content/BTDPL.INI`** via + `L4DPLCFG`): resolves map+time → clip/fog/ambient + a directional sun. Bring-up stubs + (EnsureValidProjection / per-frame ambient / uninit cloud colours) had clobbered it — now they + RE-ASSERT the env values. Debug: copy BTDPL.INI → *DBG.INI with `debug=True`. [T2] + +## Backface culling (default CW, `BT_CULL`) +The bring-up `D3DCULL_NONE` drew interior/back faces (the "dark wedge" shapes = mesh insides). CW is +correct for the BGF quad triangulation `[a b c][a c d]`. [T2] + +## LODs (default: shipping-engine first-LOD + ADDITIVE_LODS; `BT_ADDLOD`/`BT_LOD_SCALE`/`BT_PUNCH`) +See [[bgf-format]] for the √3 range decode, the ADDITIVE_LODS cumulative rule, PUNCH cutouts, and the +coplanar-shell submission-order depth bias. Replacement LOD selection for NON-additive multi-LOD +models is deferred (needs the board's hot-spot semantics). [T2] + +## Mech skins (BSL bit-slice — the rainbow-mech fix) +`.BSL` is a bit-sliced container (6 grayscale sub-images per file); the BMF `0x18 BITSLICE` tag picks +the slice. Reading it as RGB overlaid 2-3 gray sheets = the "rainbow/graffiti" bug. Fixed: +`image.cpp decodeBSL` slice-decodes; `bgfload` threads the 0x18 channel; a cross-library RAMP registry +resolves the mech-skin `softer` ramp (defined in OTHER libs). Every mech renders authentic gray-metal +paint now. See [[asset-formats]]. [T2] + +## Weapon beams (the unported dpl_* layer) +The `dpl_*` beam renderable was never ported → beams are drawn by `BTPushBeam`/`BTDrawBeams` +(L4VIDEO). The real look is the `ermlaser.bgf` TUBE (native Z 0..−2000, UVs tiled) with the scrolling +`bexp` grit texture (from the `.VMF` `SCROLL`). ER lasers red, PPC blue, standard yellow. The beam +origin re-anchors to the live muzzle every frame (can't run out from under your own beam). See +[[combat-damage]] for fire-rate/muzzle. [T2] + +## Performance — the 10fps→60fps fix (~50-80× render speedup) +The 10fps baseline had THREE stacked causes, all fixed (engine tree): (1) **`D3DCREATE_SOFTWARE_ +VERTEXPROCESSING`** — every vertex transformed+lit on the CPU → HARDWARE first; (2) D3DX `DrawSubset` +attribute-table scan (our double-sided tris broke OptimizeInplace) → explicit `SetAttributeTable` + +direct `DrawIndexedPrimitive`; (3) no frustum culling → bounding-sphere culling per frame. ⚠ LESSON: +the user plays the DEBUG build, but the 10fps was NOT the debug build (RelWithDebInfo identical) — +split draw-vs-present + check the DEVICE flags before blaming the compiler. Diagnostics kept: +`[spike]/[loadobj]/[rstat]`. [T2] + +## Key Relationships +- Geometry/LOD: [[bgf-format]]. Base: [[wintesla-port]] (L4D3D). Shadow/visual-conform: [[locomotion]]. +- Renders on: [[pod-hardware]] (main 3D view). diff --git a/context/source-completeness.md b/context/source-completeness.md new file mode 100644 index 0000000..aba85c1 --- /dev/null +++ b/context/source-completeness.md @@ -0,0 +1,51 @@ +--- +id: source-completeness +title: "Source Completeness — the gating blocker + the decompilation route" +status: established +source_sections: "PROGRESS_LOG.md §5a; docs/BT_SOURCE_STATUS.md" +related_topics: [wintesla-port, reconstruction-method, decomp-reference, open-questions] +key_terms: [BTL4OPT, decomp, oracle, WinTesla, BT] +open_questions: + - "Get the missing BT .cpp from Nick (gating for Route A)" +--- + +# Source Completeness — the gating blocker + +The archive has the **engine and all headers, but NOT the BT game's implementation `.cpp`.** +This is THE gating fact that shapes the whole project. Full map: `docs/BT_SOURCE_STATUS.md`. + +## What's present vs missing (verified across all 3 zips + the WinTesla archive) +- **MUNGA engine** (shared): ~183 `.cpp` — largely complete (renderer, MOVER/JMOVER, math, scene). [T1] +- **All BT game `.hpp` headers** present — every class/interface defined (the reconstruction's "answer key"). [T1] +- **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` survive (BTCNSL/BTMSSN/BTREG/BTSCNRL/BTTEAM/BTTOOL + + GAUSS/PPC + BTL4ARND/BTL4MODE). [T1] +- **RP can't backfill BT** — RP's game source is gutted the same way. The per-game source for BOTH + titles is missing; only shared *engine* game-logic survives. A complete repo likely exists + elsewhere (Nick). [T1] + +## Consequence — routes forward +Route A (recompile original source) is **impossible as-is** — the code that IS the game isn't here. +Paths: (1) **get the missing `.cpp` from Nick** [gating, top priority]; (2) run `btrel410.exe` under +DOSBox by emulating the VPX/IG board (blocked — needs board emulation); (3) **decompile +`BTL4OPT.EXE` + reconstruct from headers** — the chosen route (in progress). [T2] + +## The binary is a near-ideal decompile target +`btrel410.exe` is a PKZIP self-extractor; inside is **`BTL4OPT.EXE`** (1.24 MB, clean uncompressed +PE32, Phar Lap TNT). It retains **class names, `Class::Method` strings, and source-file paths in +asserts** (`d:\tesla_bt\bt\mech.cpp`) — confirming the missing modules ARE in the binary. No COFF +symbol table (naming comes from those strings). Extracted to `reference/decomp/`. [T1] + +## Why reconstruction is viable (THREE anchors) +1. **All BT headers** = exact class layouts, vtable order, signatures (the answer key). +2. **The working WinTesla engine** to compile against ([[wintesla-port]]). +3. **Red Planet's parallel implementations** as a live reference (`VTV`≈mech, `VTVMPPR`≈mechmppr, + `WEAPSYS`≈mech weapons, `RP_L4` app≈BT app). +Plus `BTL4OPT.EXE` as a behavioral **oracle** (run under cdb / read the decomp). Result = +behavior-equivalent **reconstruction**, NOT original source. [T2] + +## Key Relationships +- Feeds: [[reconstruction-method]] (the loop that fills the gap), [[decomp-reference]]. +- Gated on: [[open-questions]] (get the source from Nick). +- Built atop: [[wintesla-port]]. diff --git a/context/subsystems.md b/context/subsystems.md new file mode 100644 index 0000000..0345fca --- /dev/null +++ b/context/subsystems.md @@ -0,0 +1,68 @@ +--- +id: subsystems +title: "Subsystems — the factory + the reconstruction waves" +status: established +source_sections: "PROGRESS_LOG.md §10d; docs/SUBSYS_PLAN.md; docs/VEHICLE_SUBSYSTEMS.md; docs/RESOURCE_AUDIT.md" +related_topics: [decomp-reference, reconstruction-gotchas, combat-damage, gauges-hud] +key_terms: [subsystem, factory, ClassID, subsystem-roster, PoweredSubsystem, bridge] +open_questions: + - "Gyroscope (0xBC4) integrator; Myomers authentic coupling; 0xBD3 message manager (WAVE 8)" +--- + +# Subsystems + +The mech's components (heat/power/weapons/sensor/gyro/torso/myomers/…), built by the Mech-ctor +factory. Full per-family plan: `docs/SUBSYS_PLAN.md`; the engineering-cluster panels: +`docs/VEHICLE_SUBSYSTEMS.md`; the layout audit: `docs/RESOURCE_AUDIT.md`. ClassID map: +[[decomp-reference]] §2. + +## The factory +The Mech ctor switches on the resource ClassID to build each subsystem into the [[subsystem-roster]] +(`subsystemArray@0x128`). **The `case ClassID` LABELS are systematically MISLABELED** — the +real class is the `// FUN_004xxxxx` ctor-address comment reconciled via `CLASSMAP.md`, not the case +name (e.g. 0xBBE case="Sensor" is actually AggregateHeatSink). Each real class is wired via a +`CreateSubsystem(Mech*,int,void*)` **bridge** in the class's own `.cpp` (do NOT `#include` the +real subsystem headers into mech.cpp — the local stubs collide). Keep the alloc SIZE + special-cache. +[T2] + +## Class hierarchy (two branches, shared only at MechSubsystem) +- **Heat leaf:** `PoweredSubsystem : HeatSink : HeatableSubsystem : MechSubsystem`. Emitter/PPC/ + Sensor/Myomers/ProjectileWeapon/MissileLauncher/GaussRifle chain here. Uses 0xC `SubsystemConnection` + + 0x54 `GaugeAlarm54` (alarm level at +0x14). [T1] +- **Watcher branch:** `Torso/Gyroscope/Searchlight/ThermalSight/HUD : PowerWatcher : HeatWatcher : + MechSubsystem`; `AmmoBin : HeatWatcher`. Uses a 0xC `WatchedConnection` + 0x54 `WatcherGaugeAlarm`. [T1] +Making a base byte-exact GROWS every subclass — they must be re-based TOGETHER in one build, each +`static_assert`-locked (the P7 alarm-unification: HeatSink 0x1D0 → PoweredSubsystem 0x31C → MechWeapon +0x3F0 → Emitter 0x478). [T2] + +## Reconstruction waves (state: ~18-19 of 20 factory cases wired to real classes) +- **WAVE 1** — HeatableSubsystem re-based onto MechSubsystem (de-shadow cascade). +- **WAVE 2** — heat family (Condenser/AggregateHeatSink/Reservoir) + HUD + MechTech (un-swap). +- **WAVE 3** — power bus (Generator/PoweredSubsystem) + Emitter/PPC fire-path (end-to-end fire; heat + conducts to the central sink via the linked-sink roster). +- **WAVE 4** — standalone readouts: Sensor/Searchlight/ThermalSight/AmmoBin (de-shim, gate fixes). +- **WAVE 5** — Torso (aim twist/elevation; joint-I/O reconstructed; the BLH record disables torso → + faithfully no visible twist). +- **WAVE 6** — Myomers (mover-coupled; structural un-stub is INERT — no live mover/heat coupling). +- **WAVE 7** — projectile/missile weapons (byte-exact; flying projectiles are a PORT reconstruction — + the 2007 Entity is 0x1BC vs the binary's 0x300, so raw base-offset reads fail). +- **DEFERRED** — Gyroscope 0xBC4 (NaN integrator), SubsystemMessageManager 0xBD3 (WAVE 8 — the valve/ + message/advanced-damage hub @mech+0x190). [T2] + +## The four systemic checks (every subsystem) +See [[reconstruction-gotchas]]: (1) shadowing (re-declared engine-base fields), (2) the `Wword` trap, +(3) message-handler chaining, (4) entity validity. Plus resource-struct layout (must mirror the class +hierarchy, byte-exact — the RESOURCE_AUDIT found 8 bugs) and object-layout (alias/phantom/interior +fields). A `+0x128`-style owner offset is the ROSTER, not the segment table (the heat-link type +confusion that caused a heap-corruption via OOB `ConductHeat` writes). [T2] + +## Engineering cluster panels (`vehicleSubSystems`) +Not a widget — a per-subsystem FACTORY (`FUN_004cbaf0`) building a status CLUSTER onto one of 12 aux +MFD positions, dispatching on classID → HeatSinkCluster/MyomerCluster/EnergyWeaponCluster/ +BallisticWeaponCluster. Reads `PoweredSubsystem::auxScreenNumber/Placement/Label` (res +0x104/8/C) via +the `BTGetSubsystemAuxScreen` bridge. See `docs/VEHICLE_SUBSYSTEMS.md` + [[gauges-hud]]. [T2] + +## Key Relationships +- Data: [[decomp-reference]] (ClassIDs/hierarchy). Bugs: [[reconstruction-gotchas]]. +- Feeds: [[combat-damage]] (weapons/damage), [[gauges-hud]] (attribute state). +- Plan: `docs/SUBSYS_PLAN.md`. diff --git a/context/wintesla-port.md b/context/wintesla-port.md new file mode 100644 index 0000000..70b4dee --- /dev/null +++ b/context/wintesla-port.md @@ -0,0 +1,49 @@ +--- +id: wintesla-port +title: "The WinTesla Windows Port — the base everything builds on" +status: established +source_sections: "PROGRESS_LOG.md §5b, §8" +related_topics: [source-completeness, rendering, build-and-run, multiplayer] +key_terms: [WinTesla, L4D3D, MUNGA, RP, DPL] +--- + +# The WinTesla Windows Port + +The `Elsewhen RP411` archive is a **working Windows port of the MUNGA engine** (VS2005, +`WinTesla.sln`). It means the hardest infrastructure is **already solved** — the bt411 build sits +on top of it. Full detail: `docs/PROGRESS_LOG.md §5b, §8`. + +## What WinTesla already did (do NOT rebuild) +- **Renderer bypass: DONE** — `MUNGA_L4/L4D3D.cpp` + `DXUtils` replace libDPL / the IG board with + Direct3D9. No `libdpl.lib` in the tree. (Our from-scratch `port/` D3D9 viewer is now a + reference/sandbox, not the path.) [T1] +- **Audio: DONE** — OpenAL + libsndfile replacing the HMI "SOS" engine. [T1] +- **L4 HAL on Windows + the VS build system: DONE.** [T1] +- **Red Planet game logic: COMPLETE & buildable** — `VTV/VTVMPPR/WEAPSYS/...`. RP is at/near playable. [T1] +- **Multiplayer: the DOS `NETNUB` driver is already replaced by a 3446-line WinSock2 TCP + reimplementation** (`MUNGA_L4/L4NET.CPP`) + the master/replicant distributed-sim core. See + [[multiplayer]]. [T1] + +## What BT still needs (the only remaining work below the game layer is NONE) +Everything below the game layer — engine, renderer, audio, HAL, build — exists and is shared. The +ONLY thing BT needs is its **game-logic source**, which is gone (see [[source-completeness]]) → it +must be **reconstructed**, with the three anchors (BT headers + the WinTesla engine to compile +against + RP's parallel code + the binary oracle). [T2] + +## Build implications +- `WinTesla.sln` is **VS2005** and hard-depends on the **legacy DirectX SDK (June 2010)** (`d3dx9`, + `dinput`, `dxerr` — all removed from the modern Windows SDK). The DXSDK is installed at + `C:\Program Files (x86)\Microsoft DirectX SDK (June 2010)\`. [T2] +- The shared engine builds GREEN to `munga_engine.lib` via CMake/MSVC Win32 (see [[build-and-run]]). + Recipe gotchas (include-path shadowing, the CAMMGR/time.h fixes, the ATL shim) are in + `docs/PROGRESS_LOG.md §8`. [T2] + +## Roadmap position +Old plan ("build a libDPL shim + port the engine") is **superseded** — WinTesla did it. New plan: +reuse the WinTesla engine (shared) + reconstruct the BT game library (`BT410_L4` + game logic). The +from-scratch libDPL shim is no longer needed. Phases 4-8 (BT game layer + pod bring-up) still apply. +[T2] + +## Key Relationships +- Solves: the infrastructure that [[source-completeness]] would otherwise require. +- Feeds: [[build-and-run]], [[rendering]], [[multiplayer]]. diff --git a/docs/PROGRESS_LOG.md b/docs/PROGRESS_LOG.md new file mode 100644 index 0000000..d0d0fde --- /dev/null +++ b/docs/PROGRESS_LOG.md @@ -0,0 +1,2236 @@ +# Virtual World Pod Games — Port Project (BattleTech / Red Planet) + +> ⚠️ **REPO ORIENTATION (bt411):** This is the **clean standalone bt411 repo** — start with +> `README.md` for the current build/run and the actual tree layout (`engine/ game/ content/ +> docs/ reference/ tools/`). The historical/progressive sections below were written in the +> original RE workspace and still cite absolute `C:/git/nick-games/...` paths and its scattered +> layout (`decomp/reconstructed/`, `elsewhen_extracted/.../MUNGA_L4/`, `btbuild/`, `enginebuild/`, +> the pod content tree); those map into this repo as: reconstructed BT → `game/reconstructed/`, +> the engine → `engine/MUNGA{,_L4}/`, content → `content/`, raw decomp → `reference/decomp/`. +> Treat those paths as history; the build of record is the top-level `CMakeLists.txt` + `README.md`. +> Everything else below (decomp findings, reconstruction history, gotchas) is accurate and durable. + +Knowledge base + roadmap for porting Virtual World Entertainment's arcade **pod** games +(Tesla platform, release 4.10, ~1995–96) to run on modern Windows and, ultimately, the actual +arcade pod hardware. Primary target: **BattleTech (BT)**; Red Planet (RP) shares the engine. + +> Companion docs: `docs/BGF_FORMAT.md` (model format), `docs/ASSET_PIPELINE.md` (textures/materials/ +> gauges). The working port code is under `port/` (see "Port codebase" below). + +> **📌 This is a PROGRESSIVE CONTEXT system — keep it current.** This file is the single, durable source +> of project context across sessions. **Whenever you learn something new and non-obvious about the +> project — a decompilation finding, a build/runtime gotcha, a reconstruction insight, an offset/format +> detail, a decision or its rationale, a dead end to avoid — ADD it here** (in the most relevant section, +> or §10 for combat bring-up / reconstruction findings) as part of the same task, not as an afterthought. +> Update existing statements when reality changes them rather than letting the doc drift. Goal: a later +> session (or a fresh agent) can read this file and have the full working context with nothing lost to +> chat history. Durable project facts → here; per-session/cross-cutting reminders → the `memory/` files +> (indexed in `MEMORY.md`); detailed running findings → `docs/RECONCILE.md`. + +--- + +## 1. What this is + +- Two games share one engine: **BT (BattleTech)** and **RP (Red Planet)**. Engine = **MUNGA**; + hardware abstraction layer = **L4** (`MUNGA_L4`). Game logic = `CODE/BT`, `CODE/RP/RP`. +- Original platform: **32-bit DOS** under a **DPMI32** extender, built with **Borland C++ 5** + + **TASM32**. Ran on **Novell DOS** pods. +- 3D was rendered by a proprietary **Division "IG" image-generator board (dual EISA)**, driven by a + closed-source library **`libDPL.lib`** (ships as binary only, + `VREND*.BTL/.MNG` board microcode). + **The port replaces this** with a `dpl_*` shim on a modern GPU. (Nick: "we'll just yoink that out.") +- Pods now run **Windows 10** with modern-ish GPUs + a Voodoo/Glide wrapper. BT was abandoned; + MechWarrior 4 was ported for the current pods. This project revives BT. + +## 2. People / goals + +- **arcattack** (user): doing the port. Has NVIDIA 3060 + A6000 dev GPUs. +- **Nick**: owns the pods + the software license (porting is authorized). Runs the pod hardware. +- Goal: run on (a) a modern dev box and (b) the fixed pod hardware. Multiplayer (pod-to-pod) wanted. + +## 3. Pod hardware (fixed target) + +- **2 video cards → 7 monitors**: main 3D view **800×600**; radar **640×480**; five monochrome MFDs + driven as one **1280×480** horizontally-spanned surface. +- Requires **old NVIDIA drivers** for the MFD horizontal spanning — a hard constraint that bounds the + graphics API. → **Target Direct3D 9** (lowest common denominator that runs on old-driver pods AND + modern cards; one maintainable codepath). NOT Vulkan/D3D11+. CUDA is irrelevant (compute, not display). +- Cockpit I/O ("RIO") = joystick X/Y, throttle, pedals, buttons over **serial COM** (`L4RIO`, + `L4SERIAL`). Must be remapped to keyboard/gamepad on a dev box; real wiring is a pod bring-up task. + +## 4. Where the content lives (3 archives, in repo root) + +The deployed game data was **never in source control** — it lived on the pod machines. Spread across: + +| Archive | What it is | Has | +|---|---|---| +| `410srczipped.zip` | **Source code** (36MB) | all `.cpp/.hpp`, geometry `.bgf`, material defs `.bmf`, **animations `.ani`** (428), a few sample textures/skeletons. NO per-mech skins/skeletons. | +| `Tesla4NovellTechPC-*.zip` | pod **technician/test PC** (77MB) | game binary `btrel410.exe` (in `vgl_labs/archive/`), Division **VPX board diagnostic tools** (`vwetest/vpx/`), shared generic textures, Novell DOS. | +| `Tesla4PodPCNovell-*.zip` | the pod **game machine** (92MB) | **the full deployed tree** at `…/CopyofNovellDisk/REL410/` (BT+RP): per-mech **skeletons** (`AVA.SKL` etc., 124), **skins** (`AVATAR.BSL` etc.), 1143 models, 1624 material libs, textures. | +| `Elsewhen RP411 Source-*.zip` | **the WINDOWS PORT** ("WinTesla", VS2008) ⭐ | modernized source: `WinTesla.sln`; MUNGA(150 cpp)+MUNGA_L4(43); **`L4D3D.cpp` = IG-board→Direct3D renderer bypass DONE**; **OpenAL+libsndfile = SOS audio replaced**; full **Red Planet** game logic (`VTV/VTVMPPR/WEAPSYS/...`); asset exporters (`MeshExp/TextureExp`); `DivLoader`. Solution wires both `RP_L4` AND a `BT410_L4` target — **but the `BT410_L4` folder + all BT game source are ABSENT** (only the .sln reference remains → BT was being ported, code not included). Extracted: `elsewhen_extracted/`. | + +Extracted to: `410srczipped_extracted/`, `tesla4_extracted/`, `tesla4pod_extracted/`. +**Content root for the port** = `tesla4pod_extracted/Tesla4PodPCNovell/CopyofNovellDisk/REL410/` +(use the pod tree for skeleton/skins/geometry; **animations** come from the source tree's +`410srczipped_extracted/.../CONTENT/BT/ANIMS/`). + +Still likely missing / ask Nick: confirmation this is the complete content master. +(UPDATE: the walk/run animation set is NOT missing after all — `CONTENT/BT/ANIMS/` has the full per-mech +set, e.g. `BLHRRL/BLHRRR/BLHWWLI/BLHWWRI...`, and they are compiled into `BTL4.RES` — the game plays +`blhrrl`=904. The earlier "only AVABMP present" worry was looking at an incomplete slice.) + +### 4a. LEVELS / MAPS — what's loadable (content audit) +The mission is an INI `.EGG` (`[mission] map= scenario= time= weather=`). The engine resolves `map=` +from `BTL4.RES` (`MISSION.cpp:344 FindResourceDescription(name, MakeMessageStreamResourceType)`; a missing +name → null-deref crash at `:355`). **The runtime `BTL4.RES` (pod tree == `decomp/BTL4.RES`, byte-identical, +3.23 MB) contains 8 loadable maps:** `cavern` (the default night cave, enclosed/no sky), `grass` (grassland), +`rav` (ravine), `polar3`/`polar4` (snow), `arena1`/`arena2`, `dbase`. **Switch levels by editing a COPY of +`TEST.EGG`** (e.g. `map=grass` + `time=day`) and running `-egg .egg` — verified: `grass` boots + the +mech walks, 0 crashes (`GRASSDAY.EGG` created). ⚠ Names are `arena1`/`polar3`/`rav` (with the suffix), NOT +bare `arena`/`polar`; `map=burnt`/`frstrm`/`des` are NOT in this RES (they crash) — those are lab/other-build +maps whose `.map` SOURCE exists in `CONTENT/BT/MAPS/` (`DES.MAP` = a sky+buttes desert) but would need +compiling into a RES via the DOS `btl4tool.exe` (`BUILDRES.BAT`, manifest `CONTENT/BT/BTL4.BLD`) — a +separate content-build sub-project. **The full content SOURCE tree** `410srczipped_extracted/.../CONTENT/BT/` +is comprehensive (17 `.map`, 636 `MODELS/`, 219 `SOLIDS/` collision, 331 `ANIMS/`, 1958 `VIDEO/` textures, +207 `AUDIO/`, 404 `GAUGE/`) — i.e. the assets to build ANY map exist; the runtime tree is the already-built +subset. No consolidation needed to test the 8 maps. ⚠ Collision is OFF on ALL maps until `MoveAndCollide` +is reconstructed (the mech phases through terrain/objects) — the `.sld` collision solids ARE present in the +RES + `SOLIDS/`, so it is a code-revival task, not a content gap. + +## 5. Asset formats (all reverse-engineered; details in docs/) + +- **`.MOD`** — INI mech/object definition: skeleton, skins, collision, gauges, physics, animations, class. +- **`.BGF`** — `DIV-BIZ2` little-endian nested-TLV geometry. OBJECT→LOD→PATCH(=geogroup)→PMESH; + interleaved float32 verts; **quad faces (triangulate!)**; materials by `library:material` name; + damage/articulation are **name-driven via `SV_SPECIAL` `dz_*` tokens**, not stored geometry. +- **`.BMF`** — `DIV-BIZ2` (FILETYPE=1) material+texture **library** (no pixels). Material→DIFFUSE color + + `MATERIAL_TEXTURE`→texture→`TEXTURE_MAP` image basename. +- Texture **pixels**: **`.tga`** (Truevision), **`.vtx`** (`DIV-VTX2`, trailing RGB), **`.bsl`** + (`DIV-BSL2`: a **BIT-SLICED container** — w*h 32-bit texel words holding SIX independent 4-bit GRAYSCALE + sub-images (nibble pair-swapped; byte 0 pad) or RGB444/RGBA4444 (sliceType 7/8); the BMF TEXTURE tag + `0x18 BITSLICE` selects the slice, absent = 0. The old "[pad,R,G,B] confirmed" note was WRONG — full + spec in `docs/ASSET_PIPELINE.md` §Pixel formats; reference reader `DivLoader/VGCDivLoader.cpp:323-410`). +- **`.SKL`** — INI joint skeleton: `[joint] parent=, Type=(hingex/y/z|ball|balltranslate), Object=part.bgf, + dzone=, tranx/y/z, pitch/yaw/roll`. Build a DCS tree; neutral pose ≈ translations (pyr ≈ 0). +- **`.ANI`** — INI keyframe animation: `[HEADER] framecount/framerate(30)/skeletonfile`, `[Time]` + frameN→seconds, `[JointType]` per-joint DOF, `[frameN]` joint→`rx ry rz` radians (balltranslate→translate). + Walk anims have **no baked root translation** → forward motion is procedural/physics (see §7). +- **`.GIM`** (vector MFD/radar line maps) + **`.GAT`** (color table) + **`.PCC`** (ZSoft PCX 8-bit gauge + rasters) — the cockpit HUD assets. **`.DZM`** = INI damage-zone→material map (mech skin states). +- **`.SLD`** collision solids, **`.DMG`/`.TBL`** damage zones/tables, **`.SUB`** subsystems, **`.CTL`** control maps. + +## 5a. ⚠ SOURCE COMPLETENESS — the gating blocker + +The archive has the **engine and all headers, but NOT the game's implementation code**. Verified across +all three zips: +- **MUNGA engine** (shared): ~183 `.cpp` — largely complete (renderer, `MOVER/JMOVER`, math, scene). +- **All game `.hpp` headers** present (every class/interface defined). +- **BT game logic `.cpp`: MISSING** — `mech`, `mech2/3/4`, `mechsub`, `mechmppr`, `mechtech`, `heat`, + `sensor`, `gyro`, `torso`, `myomers`, `hud`, `mechweap`, `powersub`, `btplayer`, and the **main app + `btl4`** are all absent. Only 10 BT `.cpp` are present (BTCNSL/BTMSSN/BTREG/BTSCNRL/BTTEAM/BTTOOL + + weapons GAUSS/PPC + BTL4ARND/BTL4MODE). RP game logic is similarly gutted (5 `.cpp`). +- Compiled **`btrel410.exe`** exists (binary only, in the TechPC zip). +- **RP can't backfill BT:** RP's game source is gutted the same way (only `RPCNSL/RPMSSN/RPTOOL/VTVPWR/ + VTVSUB` + L4 glue); none of the missing BT files are under RP. Only shared *engine* game-logic is + present (`MUNGA\DAMAGE.CPP`, `MUNGA\MOVER.CPP`). The per-game source for BOTH titles is missing — the + archive captured the engine fully but only a partial subset of game source → a complete repo likely + exists elsewhere (Nick). + +**Consequence:** Route A (recompile/port original source) is **NOT possible as-is** — the code that *is* +the game isn't here. Paths forward: (1) **get the missing `.cpp` from Nick** [gating — top priority]; +(2) run `btrel410.exe` by **emulating the VPX/IG board** in DOSBox (deep RE project; no source needed); +(3) **decompile `btrel410.exe` + reconstruct from headers** (Plan B, analyzed below); (4) rewrite game +logic from headers blind (worst). Do not start the shim/port build until the source situation is resolved. + +**`btrel410.exe` (UNWRAPPED — see `decomp/`):** it's a **PKZIP self-extractor** (`unzip` lists 324 files). +Inside is the real game binary **`BTL4OPT.EXE` (1.24 MB, clean uncompressed PE32, Phar Lap TNT `32rtm` +stub)** + `BTL4.RES`, `NETNUB.EXE`, TNT extender, IG microcode, full content. Extracted to `decomp/`. +`BTL4OPT.EXE` is a near-ideal decompile target: class names, `Class::Method` strings, and **source-file +paths in asserts** (`d:\tesla_bt\bt\mech.cpp`) are left in — confirming the missing modules (`mech`, +`mech2/4`, `mechmppr`, `mechweap`, `heat`, `powersub`, `btplayer`, `btl4app/gaug/mssn/rdr/vid`, …) ARE in +the binary. No COFF symbol table (naming comes from those strings). Mined: `decomp/src_files_in_binary.txt` +(78 files compiled in), `decomp/methods_in_binary.txt` (151 methods). + +**Decompilation — UNDERWAY (Plan B in progress).** JDK21 + **Ghidra 12.1.2 installed** under `tools/` +(portable). `tools/ghidra_scripts/ExportBTSource.java` run headless on `BTL4OPT.EXE` → **real decompiled +C recovered** to `decomp/recovered/` for heat/mech/mech2/mechmppr/mechweap/powersub/btplayer/btmssn/ +btl4app/gaug/mssn/rdr/vid (proven: heat.cpp HeatSink ctor w/ real constants+asserts). Current pass = +assert-anchored only (few funcs/file). Next: decompile ALL funcs + recover vtables → map to header classes +for COMPLETE modules → reconstruct to compilable C++ (header + RP analog + binary oracle) → WinTesla +`BT410_L4` → build/verify. This is mechanized now but the reconstruction volume (~40 files) is large. + +**PILOT DONE — `heat` reconstructed** (`decomp/reconstructed/heat.{hpp,cpp}`). From the 46-func cluster: +~17 confidently reconstructed (the real heat logic — ctor, ResetToInitialState, heat-flow/coolant +formulas, status/message handlers, resource parse), 5 correctly identified as inherited base methods +(excluded), ~24 template/glue folded into members; Condenser bodies fell just outside the captured +window (needs a re-slice). Recovered the full field layout (offset→named member), HeatSink vtable slot +map, and resource struct. Output is faithful, named, VWE-styled C++ — a strong draft a human finishes +(won't compile clean yet). **Honest effort: ~0.5–1.5 hr/module → ~30–50 agent-hours + human review for +~40 modules, with a long tail** (modules with no surviving `.TCP`/`.HPP` fragment, or split across decomp +windows, cost more). Process upgrades before scaling: per-class function lists (not address clusters), +complete vtable rows, and recover `.data` float constants (the biggest "couldn't" bucket = tuning values). + +**Decompilation feasibility (why viable):** the **headers are an answer key** (exact +class layouts, vtable order, signatures) and the **MUNGA engine source (~183 cpp) anchors ~half the +binary** by pattern-matching — and scope is bounded to only the ~15 missing modules. Stages: unwrap +(PKLITE→TNT image→strip zip) → load Ghidra/IDA → compile engine to auto-identify known funcs → decompile +missing modules method-by-method (vtable-located, header-typed) → rewrite into compilable C++. Result = +behavior-equivalent **reconstruction** (binary usable as an oracle to verify), NOT original source; +~months of expert RE. Pilot before committing: unwrap, prove anchoring, decompile one module (e.g. `heat`). + +## 5b. ⭐ The Windows port (WinTesla) changes the plan + +The `Elsewhen RP411` archive is a **working Windows port of the engine** (`elsewhen_extracted/.../trunk/`, +`WinTesla.sln`). It means the hardest infrastructure is **already solved** and need not be rebuilt: +- **Renderer bypass: DONE** (`MUNGA_L4/L4D3D.cpp` + `DXUtils` → Direct3D; libDPL/IG board removed — no + `libdpl.lib` in the tree). Our from-scratch `port/` viewer is now a *reference/sandbox*, not the path. +- **Audio: DONE** (OpenAL + libsndfile replacing HMI SOS). +- **L4 HAL on Windows + VS build system: DONE**; **Red Planet game logic: COMPLETE & buildable**. + +So **Red Planet is (at/near) playable today** — build `WinTesla.sln` (VS2008 → upgrade to current VS). +And for **BattleTech**: everything below the game layer (engine, renderer, audio, HAL, build) already +exists here and is shared. The ONLY thing BT still needs is its **game-logic source** (mech/subsystems/ +HUD/app), which is confirmed gone (§5a) → must be **reconstructed**, now with THREE strong anchors: +(1) all BT headers, (2) the working WinTesla engine to compile against, (3) **Red Planet's parallel +implementations as a live reference** (`VTV`≈mech, `VTVMPPR`≈mechmppr, `WEAPSYS`≈mech weapons, `RP_L4` +app≈`BT_L4` app) — plus `btrel410.exe` as a behavioral oracle. + +## 6. Port codebase (`port/`) — the standalone viewer sandbox (NOT in bt411) + +> ⚠️ **This section documents the standalone D3D9 viewer, which lives in the ORIGINAL workspace +> (`nick-games/port/`), NOT in bt411.** It is a dev sandbox that proved the renderer bypass + asset +> pipeline; the game itself does not use it. Its `image.{cpp,h}` codec IS in bt411 (as +> `engine/MUNGA_L4/image.{cpp,h}` — the engine loader shares it). If the viewer is ever wanted here, +> it's a small self-contained add. The rest of this section is retained as reference. + +Standalone **D3D9 model/animation viewer** — proves the renderer bypass + asset pipeline. **Not the game.** + +- Build: `cmake -S port -B port/build -G "Visual Studio 16 2019" -A x64 -DCMAKE_GENERATOR_INSTANCE="C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools"` then `cmake --build port/build --config Debug`. +- **Toolchain gotchas:** the VS2019 *Community* install on this box is broken — must use the *BuildTools* + instance via `CMAKE_GENERATOR_INSTANCE`. Don't name a header `math.h` (shadows ``; ours is `vmath.h`). +- Files: `vmath.h` (math), `bgf.{h,cpp}` (model + material/texture resolution), `image.{h,cpp}` (tga/vtx/bsl + decode), `scene.{h,cpp}` (retained scene graph: Object/DCS/Instance), `skeleton.{h,cpp}` (.skl→DCS), + `anim.{h,cpp}` (.ani player), `main.cpp` (Win32+D3D9), `tools/bgftest.cpp` (headless loader test). +- Run: `btviewer.exe [animation.ani]`. Keys: `W` wire · `C` cull · `T` textures · + `P` pose · `-`/`+` walk speed · `Space` pause · `Esc`. +- **Working today:** load any mech (geometry + real BMF/BSL skin), assemble from its `.SKL` skeleton, + play real `.ANI` animations, walk across a ground grid with procedural root motion. Verified on Avatar + + Raptor. Walk speed currently tuned by hand (being derived from source — see §7). + +## 7. Open technical notes + +- **Walk speed / locomotion (SOLVED):** locomotion is **animation-driven**, NOT physics-derived. Each + `.ANI` has a `[RootTranslation]` section (per keyframe `x y z`) where **`.z` = forward root SPEED in + world units/sec** (meters/s); `[KeyJointPos].y` = vertical bob; `y` ≈ constant root height. The engine + (`RP\MUNGA\JMOVER.CPP` `AnimationInstance::Animate` ~1497-1723) integrates `movement += Δt·rootTrans.z` + and moves the body that far — so feet plant by construction (no separate stride constant; scale k=1). + Idle anims have z=0 (no travel); walks carry their speed (e.g. RAPWWR z≈20). The viewer reads this: + `g_walkPos += dt * forwardSpeedAt(anim, animTime)`. The `.MOD` physics (MoverMass/drag/MaxAcceleration) + governed thrust *vehicles* (RP hovercraft `VTVPWR`, camera ship) + collisions, NOT walk gait. NOTE: the + BT walking-mech mover/mapper code is NOT in the source dump (BT here is weapons-only; the mech consumer + of Animate() is absent) — but the per-clip speed is fully recoverable from the `.ANI` data above. + **✅ P3 LOCOMOTION CUTOVER — STEP 1 DONE (`docs/P3_LOCOMOTION.md`).** btl4's live drive (mech4.cpp + `PerformAndWatch`) is now ANIMATION-DRIVEN: the engine `AnimationInstance::Animate(dt,True)` return `adv` + (the clip's per-frame `[RootTranslation]` distance) drives the forward step (`localOrigin.linearPosition + += facing * adv`), replacing the hand-tuned `kDriveMaxSpeed` slide. Verified: mech walks at the clip's + AUTHENTIC ~60 u/s (Blackhawk run; the guess was 30), forward, feet plant (travel==stride), combat+damage + un-regressed. NOTE: the reconstructed two-channel gait (mech2.cpp `AdvanceLeg/BodyAnimation`, `IntegrateMotion`) + is bracketed by no-op stubs (`ReconSeq` controllers → Advance()=0; `Matrix34`=`ReconMatrix` no-op), so the + full gait (STEP 7) needs those backed by real engine `AnimationInstance`/affine-matrix first. Plan + + 7 steps in `docs/P3_LOCOMOTION.md`; the smallest milestone (STEPs 1-2, real per-clip speeds on the + proven engine path) is the near-zero-risk win. + **✅ P3 STEP 7 CUTOVER — the real gait controller drives locomotion LIVE (gated `BT_GAIT_CUTOVER=1`; verified).** + The BT-specific `SequenceController` (`decomp/reconstructed/seqctl.cpp`, reconstructed from binary + `SelectSequence@004277a8` + `Advance@0042790c`) is now reconstructed, built, and wired: the Mech ctor + `Init`s `legAnimation`/`bodyAnimation`, and `PerformAndWatch` (mech4.cpp, cutover branch) drives the + forward step from `bodyAnimation.Advance(dt,1)` (the real controller: parses the clip, animates the + skeleton joints via the engine `Joint` API `SetHinge/SetRotation/SetTranslation`, returns the + root-translation forward step) instead of the STEP-1/2 free-standing `AnimationInstance`. Verified live + under cdb: `[gait] SequenceController -> blhrrl id=904 (run)`, mech walks (adv≈1.07, matching baseline), + 0 crashes; combat is byte-for-byte un-regressed vs a same-harness baseline run (both fire/damage + identically). **KEY BUG FIXED:** clip fetch was `SearchList(clip_id,16)` (which treats its arg as a + resource LIST → walks the clip bytes as garbage IDs → av); the clip_id from `ResolveAnimationClip` is a + DIRECT resource ID → use `FindResourceDescription(clip_id)`+`Lock`, mirroring the engine's own + `AnimationInstance::SetAnimation` (JMOVER.cpp:1406) EXACTLY. **DECOMP FACT — CORRECTED (re-verified vs `Advance@0042790c` + raw, part_003.c:6878):** the earlier "lumpy distance" claim here was WRONG — the binary's partial-frame + branch DOES accrue `(t-currentTime)*stride[currentFrame]` (distance is CONTINUOUS, matching seqctl.cpp:298), + and `IntegrateMotion` (@004ab1c8:14997) integrates `localVelocity.z = -adv/dt` directly with NO smoothing. + **FOOT-PLANT FACT (raw FUN_004ab430:15076):** the binary advances the BODY controller with `move_joints=1` — + the DISPLAYED joints and the travel distance come from the SAME Advance (same clip/phase), so feet plant by + construction; the leg channel (also hardcoded `move_joints=1`, FUN_004a5028:12006) runs FIRST and its joint + writes are overwritten. Port matched in commit 01312e8 (body channel `Advance(dt,1)`); the same commit also + added a ground-velocity smoothing (averaging adv/dt across keyframe spans) that is NOT in the binary — a + deliberate deviation; if residual micro foot-slip is ever chased, that smoothing is the first suspect. + Cutover is kept GATED (not default) pending two flagged fidelity refinements: (a) end-of-clip uses + `carryover*stride[last]` in place of the binary's recursive finished-callback `@0x48` (the source of the + occasional adv spike at clip-loop boundaries); (b) partial-frame rotation uses component-LERP vs the + binary's slerp (`FUN_00409390`). The deeper full-`IntegrateMotion` velocity path (leg+body channels + + world-step transform + `DeadReckonPose`, smoothing the lumpy step) is the remaining STEP-7 work. + **✅ STEP 7 — the AUTHENTIC world-step (velocity model) now drives the mech LIVE (under `BT_GAIT_CUTOVER`).** + The cutover no longer slides the position directly (`localOrigin += facing*adv`); it runs the real + `IntegrateMotion` motion-tail (`@004ab1c8`): `localVelocity = {0,0,-adv/dt}` (forward = local −Z) → + rotate into world by the body orientation via the now-**backed `FUN_00408744`** (part_000.c:8331, a + matrix×vector; backed with the engine `AffineMatrix::GetFromAxis` column basis — the same convention the + drive facing uses, so it is sign-consistent) → `localOrigin.linearPosition += worldVelocity*dt`. Result + == `facing*adv`, but through the authentic velocity→rotate→integrate machinery (runs `FUN_00408744` live). + **KEY DECOMP FACT (verified):** the 1995 motion transform `{ Point3D @0x260; Quaternion @0x26c }` **IS the + engine `localOrigin`** — `Origin = { Point3D linearPosition; Quaternion angularPosition }` (ORIGIN.h:15), + and raw `FUN_0040ab44` builds the matrix from BOTH halves (rotation from 0x26c, translation from 0x260). + So `IntegrateMotion`'s motion state maps DIRECTLY onto `localOrigin` (NOT the parallel `motionDelta`/ + `worldPose` members the earlier draft declared — those were placeholders). Also fixed a latent + `IntegrateMotion` bug (unexercised while the fn was dead): it wrote only the separate `spinRate`@0x508 and + left `angularAccum[2]` (the velocity.z the world-step reads) stale → now sets it. Verified live under cdb: + walk-only (`BT_GAIT_CUTOVER=1`) walks straight forward at the authentic ~60 u/s, **0 crashes**; combat + fire/damage identical to STEP-1/2. **Remaining full-`IntegrateMotion` work:** the `AdvanceBodyAnimation` + gait STATE MACHINE (stand→walk→run transitions) + `LoadLocomotionClips` (real, mech3.cpp:326 — populates + `namedClip[]`/gait-speed caps via `legAnimation.SelectSequence`) in place of the inline clip-select; + orientation INTEGRATION (turn via angular rate, not the bring-up `gDriveHeading`); and velocity STORAGE + for MP dead-reckon. ⚠ **PRE-EXISTING crash observed (NOT locomotion):** a combat run walked into a + building's interest range and faulted in the ENGINE BGF loader (`LoadBgfFile("bld08.bgf")` → + `Builder::~Builder` `vector` teardown → bad `operator delete`, bgfload.cpp:491) — a heap + corruption in model loading, hit position-dependently (the STEP-1/2 run with a different spawn fired 600+ + shots clean). My world-step does zero heap ops + walk-only is clean, so it is not the cause; likely the + known enemy-spawn over-`sizeof` or a fragile BGF Builder for certain building assets. Track separately. + **✅ STEP 7 steps 1-2 — `LoadLocomotionClips` + the real gait STATE MACHINE drive the walk LIVE (`BT_GAIT_SM`).** + (1) **`LoadLocomotionClips`** (mech3.cpp:326, real) is now called in the Mech ctor (gated `BT_GAIT_CUTOVER`) + and completes cleanly for the Blackhawk — it resolves the full gait clip set (`swr/wwr/wwl/rrr/rrl/...`) + + measures the speed caps via `legAnimation.SelectSequence`+`keyframeData` (verified real: `walkStride=22.02`, + `standSpeed=6.83`). **KEY RECONCILIATION:** `namedClip[]`@0x5e0 and `animationClips[]`@0x5cc were declared as + SEPARATE arrays but in the binary are ONE (`namedClip[i]==animationClips[i+5]`, 0x5e0==0x5cc+0x14) — so + `LoadLocomotionClips`' writes weren't visible to `SetBodyAnimation`/`MeasureClipStride`. Fixed: `namedClip` + is now a pointer aliased to `&animationClips[5]` in the ctor (same storage). (2) **`AdvanceBodyAnimation` + state machine** (mech2.cpp, real) now drives the walk under `BT_GAIT_SM` (case 6/7: slew `bodyCycleSpeed` + toward `bodyTargetSpeed` with the loaded caps → `bodyAnimation.Advance` the `wwr` clip → world-step). Fixed + **`bodyAnimationState`@0x728 == `bodyStateAlarm.level`** (also declared-separate; re-synced from the alarm each + frame so `SetBodyAnimation`'s level change reaches the switch). Verified live: mech walks forward (z 1600→−975), + `adv≈0.35-0.42` at the authentic walk cadence (correctly slower than run's 1.07), clip loops through its 15 + keyframes, 0 crashes. **Bring-up substitute (marked):** `SetBodyAnimation` passes a non-null loop sentinel + `(void*)1` where the binary passes `PTR_LAB_0050d6fc` — the real body finished-callback (gait-state + TRANSITION stand→walk→run + leg alternation, @0x48), NOT yet reconstructed; without it the cycle clip would + stick at its last keyframe (`adv=0`). + **✅ STEP 7 — the REAL gait TRANSITION CALLBACK reconstructed + wired (walk/run leg alternation + walk→run).** + Replaced the `(void*)1` sentinel with the real `FUN_004a6d8c` = `Mech::BodyClipFinished`. RESOLVED the `.data` + fn pointer by PE-parsing (`0x50d6fc`→body cb `0x4a6d8c`; `0x50d6f0`→leg cb `0x4a6928`) — not in the decomp, so + DISASSEMBLED with capstone + decoded the jump table (`@0x4a6e0a`; 33 states→10 handlers). Reconstructed + byte-for-byte: at end-of-clip it dispatches on `bodyAnimationState`, compares `bodyTargetSpeed` to the loaded + caps (`standSpeed`/`walkStrideLength`/`reverseSpeedMax`), picks the next state, re-arms it (`SetBodyAnimation`, + which re-binds the clip WITH this same callback), and recursively advances the carryover (shared tail + `0x4a6e66`/`0x4a6ed1`/`0x4a7001`). Walk alternates 6↔7, run 12↔13, stop→8/9, walk→run 6→11→12. **Plumbing:** + `SequenceController::Advance` now CALLS `cbCode` as `Scalar(*)(Mech*,unsigned,Scalar,int)` at end-of-clip + (matching the binary's `**(code**)(this+0x48)`), folding the returned distance; inline cutover uses + `Mech::LoopBodyClip`. **Verified live:** walk 6↔7 (adv≈0.42), run 12↔13 (adv≈0.5-0.85), walk→run fires, pos + advances, 0 crashes; inline cutover + combat un-regressed. **Bring-up gap fixed:** `reverseSpeedMax2`@0x7a0 + (run bodyCycleSpeed clamp, AdvanceBodyAnimation case 12/13) is unset by LoadLocomotionClips → read `0xCDCDCDCD` + → run exploded (`adv=-7e6`); set to `reverseStrideLength` (the case-12 divisor → ratio≈1). **DECOMP TECHNIQUE:** + resolve a Ghidra `PTR_LAB_xxxx` .data fn ptr by PE-parsing the DWORD at its VA, then capstone-disassemble the + target (`/tmp/readptr.py`+`disas.py`) — for any callback/vtable slot the assert-anchored decomp didn't export. + **Remaining:** the LEG callback `FUN_004a6928` (leg alternation, state@0x3b0 + motion source@0x128); airborne + `FUN_004a6344`; gimp handlers (states 16-19, targets `0x70b2`/`0x7161` undecoded → fall back to stand); gait + SELECTION from real throttle (`MechControlsMapper` still bypassed; SM forces walk-6 then the callback transitions). +- **✅ MoveAndCollide — the mech now COLLIDES with world solids (engine Mover pipeline; gated `BT_COLLISION`).** + The bring-up drive wrote `localOrigin.linearPosition` directly with no collision, so the mech phased through + terrain/objects + floated. KEY FINDING: the 2007 RP411 engine models **terrain AS `BoxedSolid` collision + volumes** (`TERRAIN.h`: `Terrain` = an `Entity` with `collisionVolumes`), so terrain-follow AND object + collision go through ONE path — the engine `Mover` collision pipeline (`MOVER.cpp`), NOT the 1995 heightfield + query (`FUN_0040e5f0`, which the dead `Simulate` used). Wired into `PerformAndWatch` (mech4.cpp) after the + drive's world-step: snapshot the pre-move pos → set `worldLinearVelocity` from the move delta → + `MoveCollisionVolume()` (reposition the volume to `localToWorld`) → `GetCurrentCollisions()` (gather + overlapping solids) → `ProcessCollisionList(cols, dt, oldPos, &damage)` (the engine resolves each penetration + via `ProcessCollision`→`StaticBounce`, WRITING `localOrigin.linearPosition` to push the mech out; + MOVER.cpp:1162/1200/1217). All methods are public on `Mover`; guarded on `GetCollisionVolumeCount()>0` so it + is a no-op for volume-less mechs. **Verified live:** in the cavern the mech walks flat (Y=−0.35), hits a + building solid at z≈150 and is pushed out once (x+7, y+2.9) over ~6 frames then continues stably (Y holds, + no drift/fly-away); combat un-regressed (structure→0.533, 0 crashes, 109 collide events while fighting). + **✅ GRAVITY / terrain-settle — DONE (same `BT_COLLISION` block; tunable `BT_GRAVITY`, default 20 u/s).** + Collision alone only pushes OUT of solids (no downward force → a floating mech never came down). Added a + per-frame downward Y (`localOrigin.y -= kGravityRate*dt`) BEFORE the collision resolve, so gravity presses + the mech down + the collision holds it at the terrain surface (equilibrium ON the ground). SAFETY NET: a + FLOOR CLAMP at the spawn/ground Y (captured on first frame) — on a map with NO terrain solid the mech rests + at ground level instead of falling forever; on real terrain (solids) the collision catches it above the floor + so it follows hills/valleys. **Verified live:** grass — mech settles at Y=0 (clamp; flat grass has no ground + solid) and climbs a terrain feature to Y=4.68; cavern — settles at the floor Y=−0.35, stable while walking, + combat un-regressed (structure→0.933), 0 crashes. So the mech now STANDS + walks on the ground + climbs + terrain + collides with objects. **Remaining refinements:** (b) wall-blocking vs climb — the engine resolves + along the surface normal, so a wall-top contact pushes UP (climb) rather than blocking forward; may need + game-tuning. (c) collision damage is computed by `ProcessCollisionList` but not applied here (bring-up). Kept + GATED (`BT_COLLISION`) pending (b) + confirmation on more maps. +- **✅✅ THE AUTHENTIC 1995 GROUND MODEL — DECODED, IMPLEMENTED, DEFAULT-ON (task #15 CLOSED for ground; + `BT_GROUND_REAL`, =0 falls back to the gravity+push baseline above).** Product of the 10-agent + `ground-model-decode` workflow (6 mappers over binary/1995-source/2007-engine/postmortem/content + synthesis + + 3 adversarial verifiers, all SOUND). **THE MODEL (binary FUN_004a9b5c = the MASTER per-frame performance, + PTR_LAB_0050c0f4, decoded from raw asm — the "undefined 5.6KB gap" in mech4's window; FUN_004ab430 is the + REPLICANT interior only, NOT "Simulate"):** there is **NO GRAVITY anywhere in the mech** (accelerations + zeroed every frame @4a9cb6). Vertical position comes exclusively from an ABSOLUTE per-frame ground snap: + (1) ctor lifts `collisionTemplate->minY` by 5% of volume X-width (BLH: 2.0+0.37969=2.37969 — the probe + height AND the wall-vs-floor separation); (2) per frame: `MoveCollisionVolume()` (rebuilds localToWorld, + places volume, re-finds `containedByNode` in the zone's **BoundingBoxTree** — BOXTREE.cpp, identical + 1995→2007, NOT a heightfield); (3) probe `q = origin + (0, lift, 0)` → + `BoundingBoxTreeNode::FindBoundingBoxUnder(q, &h)` (h = distance down to the highest solid top; −1 = miss); + (4) snap gate `h > 1e-4` (@0x4ab16c): `origin.y -= (h − lift)` ⇒ y = surfaceY EXACTLY (up-slope bounded by + the lift window = implicit step allowance; walk-offs drop instantly; **MISS ⇒ NOTHING** — y holds, no + runaway possible); (5) `GetCurrentCollisions` → `ProcessCollisionList` → per-contact **`Mech::ProcessCollision` + override (FUN_004abb40, vtbl+0x3c** — the fn the old recon misread as "ResolveWeaponImpact"): BoxedSolid + resolver → StaticBounce → owner classification — Mover owner = separating-gate + (deferred) mech damage; + CulturalIcon owner without `StoppingCollisionVolume` (flags&0x8000) = **crushable, damage=0.00123f sentinel**; + plain UnscalableTerrain (cliffs/canyon walls/most buildings) matches neither branch; (6) response policy: + sentinel = move STANDS (restore post-snap saves); damage>0 = **FULL FRAME REJECTION** (velocity zeroed, + origin=start-of-frame — walls block by rejection, never slide/climb); impactVel²>40 = crash-anim trigger + (SetLegAnimation(0x20) — deferred, logged). Implementation: `Mech::AuthenticGroundAndCollide` (mech4.cpp) + called for the player (post-drive) AND every non-player master; ctor half in mech.cpp; shared gate + `GroundReal()` (mechrecon.hpp). **KEY FIXES/FACTS:** (a) FUN_0040e36c/FUN_0040e5f0 are + `BoundingBoxTreeNode::FindSmallestNodeContainingColumn`/`FindBoundingBoxUnder` (BOXTREE.CPP:503/867), ALIVE + in the linked 2007 engine — the "Terrain::CellAt/HeightAt heightfield" labels were wrong (the cause of both + reverted attempts: v2 assigned the DISTANCE h as an absolute height → the y=13301 ladder). (b) binary + 0x2e8/0x2ec/0x2f0/0x2f8 = ENGINE Mover `collisionVolume`/`collisionTemplate`/`containedByNode`/ + `lastCollisionList` (NOT game-layer "physicsBody/groundRef/groundCell"). (c) grass DOES have a ground slab + (gr100_cv Block Y[−50,0]) — the old "flat grass has no ground solid" note was wrong. (d) **a master mech + needs a CollisionAssistant** (GetCurrentCollisions iterates it UNCHECKED, MOVER.cpp:894): only the viewpoint + mech gets one (MakeViewpointEntity); the BT_SPAWN_ENEMY dummy now `StartCollisionAssistant()`s at spawn + + the block has a defensive assistant guard (in the real game exactly ONE master exists per pod, so the + invariant held by construction). (e) BT SOLIDS ship ZERO tiles (only Block/YCyl/Cone/Ramp/Wedge, all + FindDistanceBelowBounded clamped ≥0) → h is never negative in BT. **VERIFIED (render-side + telemetry):** + grass census lift=0.379688/minY=2.37969 exact, y=0 EXACT on the slab; arena 65s anti-ladder soak y pinned 0, + wall BLOCKs hold (no creep); dbase two full hill climbs (y→24/y→31) + exact floor return −0.453125; + screenshots standing/climbing; combat DESTROYED after the same ~8 hits; baseline (=0) byte-identical; 7-min + BT_HEAPCHECK soak clean. **EXPECTED-AUTHENTIC (do not "fix"):** sticky-ledge frame rejections walking off + 2.4-5.9u terraces; crushable props pass through WITH crunch; canyon backdrops (rav pwin*)/arena detail have + no solids = pass-through; over void y HOLDS (no fall). + **✅ CRASH/KNOCKDOWN LIVE (commit 6452ab7):** wall impacts above iv2>40 (~6.3 u/s) bind the `bmp` + bump/stagger clip — `SetLegAnimation(0x20)` + `RequestActionFlags(1)` + `(0x20)` (binary @4aaae2; + FUN_004a4c54 == RequestActionFlags, NOT "ForceUpdate"). ALIASING FIX (namedClip bug class): `gimpBaseClip` + @0x64c IS `animationClips[0x20]` (0x5cc+32*4) — bmp now loads into slot 32; the clip table is zeroed in + the ctor. Recovery = LegClipFinished case 32 → SetLevel(0) (already reconstructed). BRING-UP GUARD + (marked): no rebind while legState==0x20 — the bmp clip's ~6.5 u/s root motion re-contacts the wall and + the `actionRequestFlags` consumers (likely the stagger drive-suppressor) are unreconstructed; without the + guard the clip restarted every frame. Verified: knockdown → play out → stand → walk → re-knockdown at ~3s + cadence at a butte wall; mid-stagger screenshot ON the surface; combat clean. **Why leg-burial appeared at + walls before this:** frame rejection freezes pre-snap Y while grinding (the snap is undone every rejected + frame) — the knockdown is what prevented sustained grinding in the original. Related content facts + (measured, tools/mapscan.py + BGF triangle sampling): dune mounds visual≈solid (median 0.2u, p90 1.1u); + buttes deviate ±5u typical / ±15-25u at cliff faces — authentic 1995 coarseness. The transform chain + (quat→matrix identical formulas both paths, row-vector convention, ToD3DMatrix raw copy) was + audit-verified — no visual-vs-solid placement bug exists. + **✅ KNOCKDOWN RE-TRIGGER STUTTER — FIXED (user: "leg animation glitchy/stuttery when I run into an obstacle, + fixes if I fully stop"; reproduced + spec-confirmed).** Running INTO a blocking solid (a wall, a steep terrain + flank, OR the spawned dummy MECH — not slope-specific) at speed made the leg animation flip-flop between the + stagger clip (legState 0x20) and walk. Repro (drive into the dummy on grass, `BT_GROUND_LOG`): the FIRST hit + at run speed is a genuine knockdown, but then the mech is pressed against the obstacle with throttle held, so + every recovery frame it steps ~6.9 u/s back into it and the `iv2>40` (=6.32 u/s) knockdown RE-FIRES → rapid + freeze↔walk = the stutter (BLOCK=319 / **CRASH=107** in 22 s; leg flip-flopping 5↔32). Full stop clears it + (velocity→0→iv2<40). **The ground-model spec (scratchpad `gd_spec.md`, the hand-decoded master perf + `FUN_004a9b5c` @4aaae2) shows the binary's mitigation: `localVelocity = {0,0,−adv/dt}` is ZEROED while crashed + (@4aa3d3), so a knocked-down mech can't keep advancing into the obstacle** — our reconstruction re-derived + localVelocity from the position delta and never zeroed it, and re-armed the knockdown every fresh block. TWO + fixes (mech4.cpp): (1) **crash motion suppression** — while `legState==0x20`, zero `worldLinearVelocity` + + `localVelocity.linearMotion` (spec-faithful; no mid-stagger creep, no re-detect during the clip); (2) + **contact-hysteresis knockdown gate** — the knockdown fires only on a FRESH strike, where "fresh" = the mech + has been OUT of blocking contact for `kBlockHysteresis` (0.4 s). A 1-frame edge (`!gWasBlocked`) was tried + FIRST and FAILED for the user: a GLANCING / sliding contact alternates blocked↔not-blocked (the frame + rejection pushes the mech out each blocked frame), which flickers a 1-frame edge back to "fresh" and + re-knocks-down — that intermittent case is exactly what a head-on force-throttle repro never hits (it shows a + clean 1 bind, hiding the bug). The 0.4 s `gBlockCooldown` window bridges those gaps: any sustained OR + intermittent press just BLOCKS (runs/walks in place), only a real separate-then-re-approach re-arms it. + `gBlockCooldown` is a single global gated to the VIEWPOINT mech (the stationary dummy + MP masters also call + `AuthenticGroundAndCollide`, so they keep the original per-fresh-block behavior and never touch the shared + state). **Verified across cases:** dune-beeline **21→1**, head-on dummy **→1**, terrain-crossing (BLOCK=497) + **→1**; still blocks (no phase-through); combat un-regressed (TARGET DESTROYED, 0 crashes / 0 heap); normal + walking unaffected (suppression only triggers on legState 0x20). ⚠ DIAGNOSTIC LESSON: a head-on repro gives + 1 bind even with the brittle edge gate — the bug only shows on INTERMITTENT contact (glancing / sliding / + rough-terrain), so reproduce with an angled or terrain-crossing approach, not a straight ram. NOTE: holding + full-throttle into a wall still yields ONE knockdown then a clean block — a real player releasing throttle + sees just the single stagger. + **✅ TWO-CHANNEL GAIT DESYNC — FIXED (the ACTUAL "legs stay glitchy after a bump until full stop" bug).** + The knockdown re-trigger work above reduced the crash spam but the user reported the legs stayed glitchy + AFTER moving away, only clearing on a full stop. ROOT CAUSE (measured, not guessed): the real-controls gait + runs TWO animation channels — the BODY channel draws the displayed legs (`AdvanceBodyAnimation(dt,1)`, runs + last, wins the joint writes) while the LEG channel drives the mech's TRAVEL (`localAdv = legAdv`). In normal + walking they run in EXACT lockstep (the `[sync]` log: `advSum == legSum` frame-for-frame). The knockdown + staggered ONLY the leg channel (`SetLegAnimation(0x20)`), freezing TRAVEL while the body channel kept + animating the DISPLAY -> the two clocks split PERMANENTLY (measured `advSum=229` vs `legSum=112`: the + displayed legs animate ~2× the real travel = foot-slip) and never reconverged until a full stop reset both to + Standing. **FIX (one line):** the knockdown now staggers BOTH channels — `SetBodyAnimation(0x20)` alongside + `SetLegAnimation(0x20)` — so display + travel freeze and recover from the SAME bmp clip IN PHASE + (`BodyClipFinished` case 32 recovers the body, mech2.cpp:308, mirroring the leg). **Verified:** `advSum == + legSum` stays EXACT through+after the bump (was 229 vs 112); mech still BLOCKS the obstacle (BLOCK=328, motion + stays on the leg channel so no tunnel-through); combat un-regressed (DESTROYED, 0 crashes). NOTE: the + now-correct behavior is a FULL STOP + stagger on hard contact (authentic: frame-rejection + the iv2>40 crash + anim); the old "run through / run in place" was the desync bug, not original behavior. + **✅✅ v4 — DESYNC CLASS KILLED FOR GOOD: travel now comes from the BODY channel (the SAME Advance that + writes the displayed joints) — BINARY-FAITHFUL (raw FUN_004ab430:15076 advances the body with move_joints=1 + and takes travel from it).** The stagger-both-channels fix (above) killed the KNOCKDOWN trigger, but the + desync CLASS resurfaced on the Mad Cat + the analog throttle lever: the lever sweeps the demand THROUGH the + walk/run boundaries, and each channel's end-of-clip callback samples the demand at a different instant — near + a threshold they pick DIFFERENT next states → permanent phase split → the foot-slip again. v4 sources + `localAdv = adv` (body) so display == travel BY CONSTRUCTION — no two-machine agreement needed, ever. The + earlier "tunnels through obstacles" objection to exactly this change is OBSOLETE: it predated + `SetBodyAnimation(0x20)` — with the knockdown staggering the body channel too, a hard impact freezes travel + correctly. RE-VERIFIED: head-on run-speed ram into the dummy = pinned at its face for seconds (BLOCK=801, + 1 knockdown, NO tunnel-through, then a normal grazing slide around the corner); combat DESTROYED on BOTH + BLH and Mad Cat, 0 crashes. The leg channel keeps running as the binary's local sim (its joint writes land + first and are overwritten by the body's). The full `actionRequestFlags`(1/0x20) drive-suppressor + + crashed(0x650) lifecycle remain unreconstructed (master-perf ASM gap); the edge gate is the faithful + stand-in until they land. + **✅ RESIDUAL-SINK ALIGNMENT AUDIT — quantified; verdict = authentic coarseness, NOT a bug or a mesh error + (dbase; scratchpad audit scripts).** After the CONN trilist fix, the user still noticed "sink in a bit" in + some spots and asked for a triangle-error audit. TWO sweeps run: (1) **GEOMETRY** — all 877 pod GEO BGFs / + 127,171 tris: **0** CONN chunks with n%3≠0 (fan-bug corpus-complete), **0** out-of-bounds indices; the only + 52 anomalies are **zero-area (repeated-index) no-op** tris, ALL in props/vehicles (AB01/ARBAN/BULLET/MSLBASE/ + TK1/WL*), NONE in terrain → no mesh defect causes sinking. (2) **VISUAL-vs-COLLISION deviation** — per dbase + terrain model, sampled each visual vertex's Y vs the analytic `.SLD` surface (Cone apex-up / YCyl / Block) at + its XZ; worst clip in the walkable band (Y<15): **mesas (buttea-e) 2–3.5u**, **dunes (dhillg1-3) 0–3.1u**, + **flat pad (calpb) 1.6u** (⇒ NO global snap offset — the probe/lift is correct), **buildings (bld06/bld08) + 12–14u = the outliers**. Butte "overall" 23u is on the VERTICAL CLIFF FACES (unwalkable, the stepped-cylinder + cliff approximation — faithful). **KEY per-feature facts:** dbase terrain is placed by class-42 instances + (NOT the class-94 vehicle props); mesa collision = a STACK of YCyls + a base Cone whose footprint is ~5–22u + narrower than the visual skirt (the small skirt clip). **Building collision = separate COLUMN/WALL Blocks with + GAPS** (bld08=26 blocks, bld06=7), and the visual has floors/overhangs spanning the gaps → a mech walks into a + gap / under an overhang where there's no solid and its legs clip the visual (THIS is the user's "some things + are solid, some aren't" + the biggest residual sink). ALL authentic 1995 coarse collision — the original pods + clipped identically; the only "fix" (gap-filling collision on buildings) is an intentional content DEVIATION, + not a port correction → left as-is by default. + **DEFERRED (bound together, marked in code):** collisionTemporaryState zero @4aa741 + ProcessCollision state + tail; 0x4a4/0x4a8/0x4b4 caches (material/local normal/approach speed); the TakeDamageMessage dispatches + (zone=−1 dropped by the engine handler today + inflictor global DAT_0050b9ac unmapped); SetLegAnimation(0x20) + crash anim; gyro crunch feed; duck maxY swap wiring; replicant-path snap (MP — gate `h != −1`, part_012.c: + 15144-15167, the 0x4ab9d8 wrapper). Full spec + verdicts: scratchpad gd_spec.md/gd_verdicts.md (session); + the workflow reports are the durable citations. +- **Renderer dependency** is isolated to 3 HAL files in the original (`L4VIDEO/L4VIDPER/L4VIDRND` call `dpl_*`). + The full `dpl_*`/`dpl2d_*` API surface the game uses is cataloged (see chat history / shim contract). + +--- + +## 8. Roadmap: viewer → playable + +**REVISED after finding the WinTesla Windows port (§5b).** The original "build a shim + port the engine" +plan is largely **already done** in WinTesla. New plan: + +- **Track 1 — Red Planet build (integration spike measured the gap):** `WinTesla.sln` is **VS2005** + (Format 9 / vcproj v8), **225 source files**, and hard-depends on the **legacy DirectX SDK (Jun 2010)** — + it includes ``, ``, ``, ``, all REMOVED from the modern Windows + SDK (only `d3d9.h` remains). OpenAL32/libsndfile libs are in `trunk/lib`. So building RP is a real + **toolchain-modernization sub-project**, not a recompile: (1) ~~obtain legacy DXSDK~~ **DONE — installed at `C:\Program Files (x86)\Microsoft DirectX SDK + (June 2010)\` (Include + Lib\x86 present: d3dx9/dinput/dxerr/dxguid).** Note: the installer threw + **S1023** (harmless — caused by a newer VC++2010 redist v10.0.40219 already present blocking the bundled + 10.0.30319; the SDK files install before that failing step, so they survive — just dismiss the error), (2) drive the 225 files via CMake (relative `..\munga\` includes → compile each from + its project dir or add all dirs to the include path) or upgrade the vcprojs, (3) fix VS2005→VS2019 + C++/header drift across the tree, (4) link d3dx9/dinput8/dxguid/dxerr + OpenAL/libsndfile. Bounded but a + dedicated session; best done where the DXSDK can be installed normally. Validates the platform + yields a + playable RP. **BT integration = same build + drop in the reconstructed `decomp/reconstructed/` modules.** + + **✅ ENGINE COMPILES GREEN** (`C:\git\nick-games\enginebuild\`, CMake/MSVC Win32) — the shared + `Munga_L4` 193-file engine+HAL builds to `munga_engine.lib` (0 errors). Recipe (4298→0 errors): + (a) include path = **DXSDK Include + `enginebuild/shim` ONLY** — do NOT add MUNGA dirs (they shadow + `` → 3840 errors); rely on the code's relative/quoted includes. (b) defines: `WIN32 _WINDOWS + UNICODE _UNICODE NOMINMAX _CRT_SECURE_NO_DEPRECATE _CRT_SECURE_NO_WARNINGS _SILENCE_STDEXT_HASH_DEPRECATION_WARNINGS + _SILENCE_ALL_CXX17_DEPRECATION_WARNINGS`. (c) opts `/permissive /W0 /EHsc /bigobj /MP`. (d) source edits: + `MUNGA/CAMMGR.cpp` `std::ios.in`→`std::ios::in` (2007 typo); add `Time(const Time&)` copy ctor to + `MUNGA/time.h`. (e) `enginebuild/shim/atlbase.h`+`atlconv.h` = minimal ATL `USES_CONVERSION`/`W2A` shim. + Build Unicode (matches original — wide literals → `...W` APIs). +- **Track 2 — BattleTech:** reuse the WinTesla engine (shared MUNGA/MUNGA_L4/L4D3D/audio/build); add a + `BT410_L4` + BT game library by **reconstructing the missing BT `.cpp`** (mech, subsystems, HUD, app) + from: BT headers + RP's parallel code (§5b) + `btrel410.exe` oracle + pod content. This is the real + remaining work for BT — bounded to game logic, since the engine/renderer/HAL/audio are done. + **→ NOW LIVE (§10):** `btl4.exe` boots + renders + runs a drive→animate→target→fire→damage→destroy + single-player loop; the active work is reconstructing each subsystem's authentic behavior (damage zones + first, then weapon/heat/sensor subsystems — they are still `RECON_SUBSYS` stubs). See §10 for the + build/run/debug recipe, the no-stand-ins reconstruction method, and the per-subsystem decomp findings. + +Old roadmap below is superseded for Phases 1–3 (engine/renderer/HAL/audio = done in WinTesla); Phases +4–8 still apply to BT's game layer + pod bring-up. The from-scratch `libDPL` shim is **no longer needed** +(WinTesla replaced libDPL with L4D3D outright). + +(Historical plan — keep for reference of remaining game-layer/pod work:) + +- **Phase 0 — Foundation ✅ (done):** asset formats cracked + documented; D3D9 viewer renders skinned, + skeleton-assembled, animated mechs. Renderer bypass proven feasible. +- **Phase 1 — `libDPL` shim as a real API:** turn the viewer renderer into a drop-in lib exposing the + actual `dpl_*` (3D) + `dpl2d_*` (2D) C functions the game calls, backed by D3D9. *Linchpin: once the API + matches, the game's own rendering code compiles unchanged.* Risk: matching semantics (fog, LOD, materials, + `dpl_SectPixel` picking, 2D display lists, morph/damage/articulation). +- **Phase 2 — Compile the game code (modern toolchain):** get `MUNGA` + `BT` C++ building on MSVC/Clang — + modernize Borland-isms, replace `.asm`, stub the `L4` layer — linked against the shim. Goal: it *builds*. + Risk: pervasive non-standard 1995 C++; DOS/DPMI assumptions. +- **Phase 3 — L4 HAL on SDL/Win32:** replace `L4` video-out, input, timing, file I/O. Goal: the game first + *runs* — boots to menu / loads a mission. +- **Phase 4 — Cockpit (HUD/controls):** `dpl2d_` HUD + `.pcc`/`.gim` gauges + radar; map RIO cockpit + controls API → keyboard/gamepad; data-driven multi-surface output (dev windows vs pod's 7 monitors). +- **Phase 5 — Gameplay loop:** one single-player mission end to end — drive, fire, heat, damage, AI bots. + → **playable (single player)**. +- **Phase 6 — Audio:** HMI "SOS" sound engine → modern mixer (SDL_audio / similar). +- **Phase 7 — Multiplayer:** ⚠ **CORRECTED (scope-hardest workflow, verified):** NOT "reimplement over UDP" — + the WinTesla port ALREADY replaced the DOS `NETNUB` driver with a **3446-line WinSock2 TCP** reimplementation + (`MUNGA_L4/L4NET.CPP`, `SOCK_STREAM`/`ReliableMode`, zero `SOCK_DGRAM`) and the master/replicant distributed- + sim core (InterestManager/Entity/HostManager) is complete. So multiplayer = **integrate + smoke-test that + existing TCP stack**, gated on P3 (locomotion update writer) + P5 (entity create/destroy lifecycle) + subsystem + WAVEs. Unknown: whether that TCP path has ever run successfully even in RP (a send-size bug at `L4NET.CPP:1853`). + This is problem **P6** in `docs/HARD_PROBLEMS.md`. + **⭐ P6 SMOKE TEST PASSED — TWO INSTANCES SHARE A WORLD (cross-pod entity replication live).** The full + chain works on one box: console egg delivery → TCP mesh → synchronized mission start → bidirectional + entity replication (each instance renders its own mech + the peer's REPLICANT; `[BTrender] mech tree + built` ×2 on both sides; 0 crashes). HOW IT WORKS (workflow-mapped + verified): (1) `-net ` CLI arg + = networked mode; the pod listens on for a CONSOLE and boots `ConsoleOnly`; game mesh port = + +1. Solo (no `-net`) never touches WinSock. (2) The mission egg's `[pilots]` section IS the session + roster (`pilot=ip[:port]` + a per-address page with hostType=0/name/vehicle/dropzone...); each pod + self-identifies by local-IP+game-port match, CONNECTS to pilots listed before it, LISTENS for those after + — a deterministic full mesh; mission loads when all peers connect ("All connections completed!"). + (3) The CONSOLE (the operator station — ABSENT from every archive) delivers the egg as chunked + `ReceiveEggFileMessage` packets; **`tools/btconsole.py` is our console emulator** (wire spec in its + docstring, constants probed from the build via `BT_NET_PROBE=1`). ⚠ `NotationFile::ReadText` expects + NUL-SEPARATED LINES, not raw file text (NOTATION.cpp:1043 walks strchr(buf,0)+1) — the tool converts. + (4) One-box recipe: instance A `-egg MP.EGG -net 1501 BT_LOG=mp_a.log BT_AFFINITY=0x1`, instance B + `-net 1601 BT_LOG=mp_b.log BT_AFFINITY=0x2` (new env vars: per-instance log + CPU pin), then + `python tools/btconsole.py MP.EGG 127.0.0.1:1501 127.0.0.1:1601`. MP.EGG/MP1.EGG (2-pilot loopback / + 1-pilot networked) live in `btbuild/` (copy to the pod BT dir). Egg entries MUST carry explicit :port on + one box (else both instances self-identify as the same pilot). (5) **WIRE-FORMAT BUG CLASS FOUND+FIXED: + MakeMessages replicate RAW over TCP, so string payload must be INLINE** — the reconstructed + `BTPlayer::MakeMessage` carried `const char*` pointers (fine solo, garbage cross-pod: the first remote + BTPlayer replication crashed in `CString(roleName)`); fixed to inline `char[0x40]` at the binary offsets + (+0x50 teamName/+0x90 roleName, static_assert-locked, size 0xD0). CHECK EVERY MakeMessage for pointer + payloads before it replicates (swept: no others currently). (6) The rumored L4NET.CPP:1853 send-size bug + is DEAD CODE (`#if MESSAGE_BUFFERING` == enum True == 0 to the preprocessor); live send paths are correct. + **⭐⭐ MOVEMENT REPLICATION COMPLETE — A's mech WALKS ON B'S SCREEN (replicant tracks the master to + ~1.5u through dead-reckoning).** The full authentic mission-start ladder now runs: console egg → + CreatingMission → (event-queue quiet timeout, APP.cpp:667 `QUIET_TIME_OUT` 3s) → LoadingMission → + WaitingForLaunch → **console sends `RunMissionMessage` ×2** (the operator's LAUNCH button; + `btconsole.py` sends them after a 20s settle + 4s step; ID=5, clientID=4, probed) → LaunchingMission → + **RunningMission (state 5)**. SIX bugs/gaps found+fixed to get here (each verified live): + (1) Mech ctor now `SetDeadReckoner(&Mover::AcceleratedDeadReckoner)` (the VTV pattern); + (2) REPLICANT MOTION: `Mech::PerformAndWatch` runs `DeadReckon(dt)` (engine lerp toward + projectedOrigin) for `GetInstance()==ReplicantInstance` instead of the drive; + (3) MASTER EMISSION: the VTV error-threshold block (self-predict via own deadReckoner; `ForceUpdate()` + on posErr²>0.04 / quat-w<0.997 / 2s heartbeat) — without it masters NEVER emit update records; + (4) emission GATED on `RunningMission` — pre-launch update spam kept the PEER's CreatingMission + event-queue from ever going quiet (a cross-pod deadlock); + (5) the console must send the LAUNCH — **even solo had been running entirely on the + `IsPreRunnable()` player-escape in a pre-launch state, forever** (nothing posts RunMissionMessage + locally); solo still works that way (unchanged); + (6) **REPLICANT VALIDITY: an invalid entity defers EVERY message as an event forever** + (`Entity::Receive`/`Dispatch`, the '07 edit defers all) — replicants never get the master's + MakeReady/CheckLoad handshake (the interest layer is partial), so their network updates never applied + AND the deferred-event churn also blocked the quiet-timeout. FIX: `Mech::Make`/`BTPlayer::Make` + `SetValidFlag()` replicant instances at creation (same fix the spawned test dummy needed). + **Debug tooling added (env `BT_NET_TRACE`, permanent, in the ENGINE tree):** `[net-tx]`/`[net-rx]` + (L4NET send/receive), `[net-upd]` (update entity lookup), `[upd-repl]` (replicant scheduling), + `[ent-exec]` (Execute state ladder, 1-per-240 frames) — plus `[emit]`/`[repl]`/`[mech-exec]` in + mech4.cpp under `BT_REPL_LOG`. Update stream measured: ~60 Hz × 144-byte records per moving master. + **NEXT (P6 phase 4):** cross-pod combat (target a REPLICANT + damage routed to the owning master — + Entity::Dispatch on a replicant already reroutes to the master via SendMessage), interactive 2-window + driving, replicant gait animation (replicants don't animate joints yet — derive from replicated + velocity), then the pod-LAN config (real IPs, both `-net 1501`, bare-IP pilot entries). +- **Phase 8 — Pod bring-up (with Nick):** 7-monitor layout, old-driver compatibility, RIO cockpit I/O + wiring, on-pod testing. + +**✅ PLATFORM PROFILE scaffold DONE (`-platform pod|dev`, or `BT_PLATFORM` env, or `run\run.cmd [EGG] pod`; +default DEV).** The gate before Phase-4 HUD/MFD work: a boot switch selecting a runtime profile WITHOUT forking +the codepath — it just picks WHICH environment preset the existing video/gauge/input code reads (btl4main.cpp +env-defaults block). **KEY FINDING (a workflow map): the pod multi-surface path already EXISTS and is intact** — +`DPLRenderer::FindBestAdapterIndices` (multi-adapter/monitor selector; honors PRIMGAUGE/SECGAUGE/MFDGAUGE/ +SPANDISABLE), `SVGA16::BuildWindows` (L4VB16.cpp — a fullscreen D3D device + window per gauge/MFD surface, MFD +span = width×2), `L4GaugeRenderer` (gated on `getenv("L4GAUGE")`, btl4app.cpp:353), and **`content/SETENV.BAT` +IS the authentic pod env preset** (L4CONTROLS=RIO,KEYBOARD; L4GAUGE=640x480x16; L4PLASMA=com2). So the profile +is an env-preset selector: **DEV (default)** = keyboard + single 800×600 window (no L4GAUGE → MakeGaugeRenderer +NULL → single surface); **POD** = RIO cockpit input (→ keyboard fallback when no serial) + the multi-surface +gauges/MFDs. Verified: dev un-regressed (boots/ticks/combat DESTROYED); POD boots on a dev box single-window, +0 crashes; both the CLI (`-platform pod` — ParseCommandLine tolerates it) and env (`BT_PLATFORM=pod`) forms work. +⚠ The multi-surface is a POD-HARDWARE capability (each surface needs its OWN fullscreen device on one of the +pod's 2 video CARDS — monitor count alone is NOT a valid proxy), so `-platform pod` does NOT auto-enable L4GAUGE: +it stays bootable everywhere as single-window + pod input; on the real pod the gauges come from SETENV.BAT (or an +explicit L4GAUGE), running the UNTOUCHED FindBestAdapterIndices/SVGA16 path. Also fixed a real engine bug +(L4VB16.cpp:201): `SVGA16::BuildWindows` PostQuitMessage'd on `CreateDevice` failure but fell through to a null +`mDevice[j]` deref (segfault) → now `break`s (inert on pod where CreateDevice succeeds). **FOLLOW-UPS** (the deep +work this scaffold gates): (1) **MFD compositing on a dev box** — render the gauges/MFDs into the single window / +extra windows so POD mode is VISUALLY testable off-pod (the real "dev test of the pod HUD"); (2) **graceful +multi-surface fallback** — the gauge renderer returns NULL instead of crashing when its surfaces can't be built, +so a forced L4GAUGE on non-pod hardware degrades cleanly; (3) **pod main-view fullscreen/adapter tuning** +(btl4vid.cpp:863 still binds the main 3D view to 800×600 windowed on `::GetActiveWindow()` — fine for dev + the +pod's 800×600 main monitor, but pod fullscreen-on-a-chosen-adapter is a Phase-8 detail). The Phase-4 HUD/MFD +render work now targets the profile's LOGICAL surfaces, not a hardcoded single window. + +**✅✅ GAUGE DEV-COMPOSITE — the pod's SECONDARY/RADAR MFD renders LIVE on a dev box (opt-in `BT_DEV_GAUGES`; +full map + findings in `docs/GAUGE_COMPOSITE.md`).** The FOLLOW-UP (1) above is DONE for the secondary surface: +an inset in the bottom-left of the 800×600 window shows the authentic BT cockpit secondary MFD — **radar/ +tactical grid** (SCALE/SECTOR + crosshair), **SPEED/HEADING/MAGNETIC/PROP dials**, and the color-coded +**ARMOR DAMAGE mech schematic** (green intact / red damaged) — REAL content (`nzSec=27247` non-zero pixels). +The draw model is CPU-raster→texture-upload, DEVICE-INDEPENDENT: gauge widgets raster 16-bit R5G6B5 into ONE +shared `Video16BitBuffered::pixelBuffer`; `SVGA16::DrawDevInset` (L4VB16.cpp) palette-expands the secondary +plane into a MANAGED texture on the MAIN device + draws an `XYZRHW|TEX1` inset quad; `BTDrawGaugeInset(mDevice)` +is called from `DPLRenderer::ExecuteImplementation` as the last draw before `EndScene`. **THREE recon bugs +fixed to get real content (1 genuine fix + 2 gated dev accommodations):** (a) **⭐ `BTL4Application:: +MakeGaugeRenderer` SIGNATURE MISMATCH** — the reconstructed no-arg override only HID the 2007-engine's widened +3-arg virtual `MakeGaugeRenderer(int*,int*,int*)` (APP.cpp:382), so the engine built a BASE `L4GaugeRenderer` +whose ctor never calls `BuildConfigurationFile` → `gauge\l4gauge.cfg` was NEVER PARSED → `undefined label +'bhk1Init'` → no ports. FIXED to the 3-arg signature (args ignored; BT is fullscreen). Same bug class as the +`BTL4GaugeRenderer(false,NULL,NULL,NULL)` ctor fix — recon written to the ORIGINAL signature vs the WIDER 2007 +virtual. (The gauge label = `GetGameModel()`+`"Init"`=`bhk1Init`; CFG has `Bhk1Init` but the symbol lookup is +`stricmp`, so case is irrelevant — the table was just empty. `MechInit`, CFG:4390, is the shared macro that +builds the sec/overlay/Mfd/Heat/Comm PORTS via the base `configure` primitive.) (b) **parse HUNG on undefined +primitives** — `BTL4MethodDescription` (btl4grnd.cpp) is a STUB (BT gauge widget classes unreconstructed), so +an unknown primitive → `ReportParsingError`→`Fail()`→a MODAL dialog freezing mid-parse; gated skip +(GAUGREND.cpp) walks past the unknown primitive's params so labels register + base `configure` builds the +ports. (c) **gauge widgets AV on NULL data bindings** — `NumericDisplayScalar` value_pointer NULL → +`GaugeConnectionDirectOf` ctor deref (bind NULL→static zero, gauge.h); `RankAndScore::Execute` derefs +unreconstructed game state (`Gauge::GuardedExecute` SEH wrapper → `Disable(True)` on first fault). All gated +`BT_DEV_GAUGES`; **default DEV (no gauges) un-regressed (TARGET DESTROYED, 0 crashes) + pod path byte-unchanged** +(strict `Fail`, no guards). **REMAINING = the real gauge WIDGET reconstruction:** the `BTL4MethodDescription` +method table (BT gauge classes: `PlayerStatus` + ~16 recovered `methodDescription` entries) + the gauge→ +game-state DATA BINDINGS (why some gauges resolve NULL) — that is what the dev skip/guards stand in for, and +what the POD needs. Then Step 2 (RP↔BT MFD port-name reconcile → the 5 MFDs) + Step 3 (2-window layout). + +**✅✅ GAUGE MILESTONE C — ALL SIX instrument surfaces in a SEPARATE dev window (default under +`BT_DEV_GAUGES`; `BT_DEV_GAUGES_DOCK=1` = docked-in-main-window instead; full map: `docs/GAUGE_COMPOSITE.md`).** +Steps 2+3 done for the dev path. A 960×384 top-level window tiles all 6 pod screens with authentic content: +**Heat** (COOLANT/BALANCE/RES + condenser/temp-leak gauges), **Comm** (KILLS/DEATHS/SELECT TARGET), **Mfd1/2/3** +(DISPLAY/PROGRAM/NEAREST/TEMP-STATUS frames), and the color **radar** (SCALE grid + dials + ARMOR DAMAGE). Main +800×600 3D view is un-occluded; default DEV un-regressed (TARGET DESTROYED, 0 crashes). **Architecture (from the +`mfd-multisurface-map` workflow):** all 6 surfaces are bit-plane MASKS over the ONE shared `SVGA16`/`pixelBuffer` +(`sec`=palette low byte; `Heat`=0x4000 UL, `Mfd2`=0x0400 UC, `Comm`=0x8000 UR, `Mfd1`=0x0100 LL, `Mfd3`=0x1000 LR; +`Eng1-3` = engineering-mode alt planes, not extra monitors), so the compositor reaches the SVGA16 ONCE and +extracts each by mask. **No port-name reconcile needed on the dev path** — we fetch the BT port names directly via +`GetGraphicsPort` (the RP `auxUL2/…` names only matter to the POD's own `SVGA16::Update` demux — a deferred +pod-only fallback). `SVGA16::DrawDevSurface` has two kernels: palette-LUT (sec, == Update case 0) + mono bit-plane→ +tint (`(word&mask)?tint:0`, the reduced core of Update cases 1/2); `BTDrawGaugeSurfaces` iterates a 6-entry table. +The **separate window = one `CreateAdditionalSwapChain` on the EXISTING device** (no 2nd D3D device); rendered +AFTER the main `EndScene` (`BTGaugeWindowRenderAndPresent`, L4VIDEO): SetRenderTarget→**SetDepthStencilSurface(NULL)** +→Clear→BeginScene→6 tiles→EndScene→restore→`swap->Present`. **⚠ KEY BUG (cost a cycle): depth-stencil size +mismatch** — the main 800×600 depth stays bound when you SetRenderTarget to the 960×384 gauge backbuffer; a bound +depth SMALLER than the RT is INVALID → all draws silently fail (window shows the Clear color, no geometry). Fix: +unbind depth (Z is off anyway), restore after. Pod `BuildWindows`/`Update` path byte-unchanged. REMAINING = the +widget recon (above) + the pod MFD port-name fallback. + +**✅✅ GAUGE WIDGET RECONSTRUCTION — the cockpit instruments now read LIVE game state (attribute wave DONE; +full mechanism + technique in `docs/GAUGE_COMPOSITE.md` §Attribute wave).** After Milestone C put all 6 +surfaces on screen (drawing NULL), two waves made them read real data. (a) The **ColorMapper family + headingPointer** +(prior session): the palette-tint schematics (`cmHeat`/`cmCrit`/`cmArmor`/`colorMapperMultiArmor`) + the rotating +compass, each a reconstructed BT gauge class registered in `BTL4MethodDescription[]` (btl4grnd.cpp). (b) The +**attribute wave** (this session, 4 commits): gauges bind to game state by NAME via the engine attribute-pointer +system — the config's `Subsystem/Attribute` (e.g. `HeatSink/CurrentTemperature`) or bare `Attribute` (e.g. +`LinearSpeed`) resolves through `ParseAttribute`→`FindSubsystem`(stricmp on `GetName()`)→`GetAttributePointer`→ +the class's `activeAttributeIndex`. A class publishes attributes by declaring an `AttributeID` enum + a +`static AttributePointers[]` (`ATTRIBUTE_ENTRY`) + `GetAttributeIndex()` chained to its parent, and passing +`GetAttributeIndex()` to its `DefaultData`. **⚠ DENSE-TABLE HAZARD (systemic): `AttributeIndexSet::Build` leaves +gap slots uninitialized and `Find` strcmps EVERY slot → a published table MUST be a dense prefix from the parent's +`NextAttributeID`; fill gaps with a shared read-only pad member.** Done: **HeatSink table** (`CurrentTemperature` +→ Heat readout shows 77), **Mech table** (dense prefix 0x15..0x21, `LinearSpeed` → SPEED readout shows ~225), +**vertBar** (coolant bars; x87 pixel math recovered by disassembly), **segmentArcRatio** (speed arc; thin subclass +of engine `SegmentArc`), **oneOfSeveralPixInt** (the button-state lamps — reconstructed the whole OneOfSeveral +strip-selector family + PixMap8 blit path), and **map** (the RADAR / tactical display — the marquee; the MapDisplay +renderer was already reconstructed, so this was the registration glue + data: the Mech table extended to the full +0x15..0x38 with the radar Point3D*/Quaternion* attributes + a Sensor "Avionics"/RadarPercent table + a reconstructed +`MapDisplay::Make`/`methodDescription`; renders the FOV view wedge live). **Base primitives `numeric`/`numericSpeed`/ +`digitalClock`/`rankAndScore` are ENGINE (L4GAUGE.cpp) — only need the data; `vertBar`/`segmentArcRatio`/ +`oneOfSeveralPixInt`/`map`/`PlayerStatus`/`vehicleSubSystems` are BT-specific (reconstruct + register).** Technique +added: `tools/disas2.py ` (recover FPU math Ghidra drops — `FUN_004dcd94`=round, `FUN_004dcd00`=fabs) + +`tools/vtdump.py ` (find a vtable override the assert-anchored decomp didn't export). **⚠ SYSTEMIC PATTERN +(map): a newly-consumed reconstructed subsystem/gauge is often the FIRST reader of a stubbed helper — the map +exposed 3 (Sensor radarPercent negative from un-normalized heatEnergy; `ResolveOperatorEntity`→NULL; unguarded NULL +entity), each fixed with the established viewpoint-fallback/guard patterns.** **⭐ MAP CONTACTS RENDER (2026-07):** +the radar plots the enemy contact inside the FOV wedge. Two more fixes: (1) the gauge renderer's entity grid was +NEVER populated (the port dropped `ExecuteImplementation`'s InterestingEntity feed) → added `GaugeRenderer:: +RebuildEntityGrid()` filling `movingEntities` from the world DynamicMaster+Replicant iterators (the vehicles; the +map draws a cross+box blip for cls-0xBB9 mechs, the pip table being stubbed); (2) the blip PROJECTION was wrong — +`worldToView` is a camera matrix whose baked scale is wrong for a point transform (120u contact → 167px not 44px) → +project `delta = entity − viewpoint` directly (rotate by −heading, scale by pixelsPerMeter). Diagnosed via +`BT_MAP_LOG` (the enemy reads dsq=14400 = exactly 120u; the earlier "377-828m" was the wrong scale × player drift, +not a distance error). REMAINING: `PlayerStatus`, `vehicleSubSystems` (task #13) + the authentic pip symbol/name/ +video-object infra (`GetVideoObject`/`LookUpPip`/`GetNameID`, stubbed → cross-blip stand-in) + `SetTargetRange` +(radar zoom, 1km default) + the aggregate HeatSink-bank `AmbientTemperature` (`#if 0`'d 0xBBE) + the Reservoir +`coolantCapacity` shadow + the Searchlight `LightOn` table (duck button already resolves via the 0x37 Mech entry). +**◐ `PlayerStatus` DONE** (mission-review scoreboard; lives in the config's `cameraInit` block, not `MechInit`, +so it is build/link/mech-cockpit-safe but not runtime-verifiable via a normal mech egg — commit b691863). +**✅✅ `vehicleSubSystems` — DONE: the engineering-screen (MFD) subsystem cluster-panel system is +reconstructed, registered, and RENDERING (full spec + phasing in `docs/VEHICLE_SUBSYSTEMS.md`).** It is NOT a +widget — its Make (`FUN_004cbaf0`) is a per-subsystem FACTORY building a status *cluster panel* onto one of 12 +aux MFD positions, dispatching on classID → `HeatSinkCluster`(0xBC3)/`MyomerCluster`(0xBC6)/`EnergyWeaponCluster` +(0xBC8/0xBD4)/`BallisticWeaponCluster`(0xBCD/0xBD0), placed via the 12-row geometry table @0x51bf34. Reconstructed +the whole `SubsystemCluster` family (base + 5 subclasses) + 4 sub-gauges (`AnimatedSubsystemLamp`/`AnimatedSourceLamp`/ +`ScalarBarGauge`/`ConfigMapGauge`) + 3 connections + the Make + registered in `BTL4MethodDescription[]`; ALSO +reconstructed 4 base gauges the clusters build that were declared-but-undefined (`HorizTwoPartBar`/`OneOfSeveralInt`/ +`OneOfSeveralStates`/`BitMapInverseWipe`) + `StateConnection` + `SeekVoltageGraph`'s virtuals (their `/FORCE`-stubbed +ctors segfaulted when a cluster built them). **⭐ THE AUX-SCREEN BLOCKER RESOLVED CLEANLY (no core re-base needed):** +the Make reads `subsystem[0x1dc]`/`[0x1e0]`/`[0x1e4]` (aux-screen position/placement/label) which are +**PoweredSubsystem** fields (`auxScreenNumber`/`auxScreenPlacement`/`auxScreenLabel`, resource +0x104/+0x108/+0x10C) +that **our PoweredSubsystem ctor ALREADY populates** — the raw offsets read 0xCDCDCDCD only because the reconstructed +heat-leaf branch isn't byte-exact. Fix = a bridge `BTGetSubsystemAuxScreen()` (powersub.cpp) casting through the real +`PoweredSubsystem` type to return the NAMED fields (also the `FUN_0041a1a4`/`0x50f4bc` type filter); the Make + +`SubsystemCluster` read via the bridge. **Render-verified (BT_DEV_GAUGES_DOCK): 7 authentic panels build + draw — +SENSOR CLUSTER, MYOMERS, ER MED LASER·RANGE 500M ×3, PPC·RANGE 500M ×2 — with bar gauges/recharge dials/lamps, 0 +crashes, combat un-regressed.** Diagnostics `BT_VSS_LOG` (Make entry + per-subsystem classID/auxScreen dump). +Tooling: `reference/ghidra_scripts/DecompVSS.java` (headless address-list decompiler — reusable for any function +the assert-anchored exporter skipped; via the `vssproj` Ghidra project). Follow-ups (non-blocking, in the doc): +the `SegmentArc270` recharge-dial + `BitMapInverseWipeScalar` eject-wipe children (placeholder ctors, deferred +NULL); the remaining raw heat-leaf-branch value-pointer reads (BEST-EFFORT — need the branch re-base or per-field +bridges); the separate 960×384 gauge window (docked mode renders; separate-window is existing dev-gauge infra). + +**✅✅✅ 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 `. **cwd MUST be `content\`** (the engine resolves `BTL4.RES`, + `VIDEO\`, `BTDPL.INI`, eggs relative to cwd — see the `loadTables` gotcha below). Logs to `btl4.log` + in `content\` (grep `[anim]/[drive]/[target]/[fire]/[damage]/[boot]` markers). The DLLs + (OpenAL32/libsndfile) are copied next to the exe automatically at build time. +- **⭐ THE AUTHENTIC STACK IS NOW DEFAULT-ON** (`BTEnvOn`, mechrecon.hpp): `BT_GAIT_CUTOVER`, + `BT_GAIT_SM`, `BT_COLLISION`, `BT_REAL_CONTROLS` all default ON — set `=0` to fall back to the + historic bring-up path (e.g. `BT_GAIT_SM=0`). Verified: an env-free run gets the full authentic chain + (mapper speed/turn demands → two-channel gait → collision+gravity) with combat/damage intact, and each + `=0` fallback still works. +- **Bring-up env vars** (DEBUG/headless; default OFF): `BT_FORCE_THROTTLE=1` (auto-walk fwd, no key), + `BT_SPAWN_ENEMY=1` (spawn a target mech 120u ahead ALONG THE SPAWN FACING — facing-aware since the + real-controls drive keeps the authentic spawn orientation), `BT_FORCE_FIRE=1` (auto-fire on cooldown), + `BT_ASSERT_TO_DEBUGGER=1` (route CRT asserts to the debugger instead of a modal box), `BT_HEAPCHECK=1` + (whole-heap validation on every alloc/free — heap-corruption hunts), `BT_PROBE_BGF=` (+ + `BT_PROBE_N`) (boot-time BGF loader probe, exits after). Interactive: + **WASD** drive, **Space/Ctrl** fire, **X** all-stop. Gated/marked in `btl4main.cpp` + `mech4.cpp`. + **⭐ VIRTUAL CONTROLS (dev keyboard → the pod's analog inputs; mech4.cpp drive block):** the pod's + throttle was an ABSOLUTE analog LEVER + an analog turn stick — momentary 0/1 keys can't express either. + After the perf fix took the game 10fps→60fps, every key tap landed as a frame-perfect FULL demand (at + 10fps most taps aliased away between sim ticks, which had LOOKED like fine control) → "controls too + sensitive, tap-back reverses instead of stopping". mech4 integrates dt-scaled: **W/S sweep a PERSISTENT + throttle lever** (tap ≈ ±0.08 step, full sweep ~1.4s; a lever below standSpeed = the mech stands, authentic) + with a **DETENT at zero** (braking from forward stops AT 0 and latches until the key is released — no + accidental reverse), **X = all stop**; **A/D deflect a momentary stick** (~0.4s to full, auto-centers) so a + tap = a gentle nudge. `gBTDrive.throttle/turn` are the integrator OUTPUTS; the mapper bridge + + forced-throttle harness consume them unchanged (BT_FORCE_THROTTLE bypasses the lever). + **⚠⚠ KEY-INPUT GOTCHA — the WndProc NEVER RECEIVES WM_KEYUP:** the engine's per-frame keyboard reader + (`L4CTRL.cpp:1506`) `PeekMessage(PM_NOREMOVE)` + `GetMessage()`s every `WM_KEYUP`/`WM_SYSKEYUP`/`WM_CHAR` + OUT of the queue for its key-command channel — only KEYDOWNs reach the launcher WndProc, so ANY + message-based key-state tracking latches keys on forever (this was ALSO true of the old 0/1 scheme: its + throttle/turn/fire never cleared on release; the old "feel" was that quirk + 10fps aliasing). The + integrator therefore POLLS `GetAsyncKeyState` per frame (via GetProcAddress, no headers needed), gated on + the game window being FOREGROUND (`GetForegroundWindow` pid == ours) so background typing doesn't drive + the mech. **`BT_KEY_NOFOCUS=1`** = test hook accepting keys without focus (SendInput automation from a + background shell can't grant real foreground; `scratchpad/realkey.ps1` injects real keys for harness runs); + **`BT_KEY_LOG=1`** = 1 Hz `[vctl]` key/lever/stick state. BTDriveInput struct is duplicated in + btl4main/mech4/mechmppr/emitter — keep in sync (the emitter copy had already silently drifted). + **⭐ Levels:** default test EGG is now `DEV.EGG` (`map=grass` + `time=day`, outdoor); the RES has 8 maps + (`cavern`/`grass`/`rav`/`polar3`/`polar4`/`arena1`/`arena2`/`dbase`) — swap via a copied EGG's `map=` + field (§4a). + **⭐ MECH SELECTION IS EGG DATA — the code path is mech-agnostic (VERIFIED with a second mech).** The + player vehicle = the egg's pilot page `vehicle=` (DEV.EGG: `bhk1`), resolved via + `playerMission->GetGameModel()` → `FindResourceDescription(name, ModelListResourceType)` (btplayer.cpp + `CreatePlayerVehicle`). RES type-18 names incl.: `bhk1/blkhawk ava1/avatar mad1/mad2/madcat lok1/lok2/loki + thor/thr1 vul1/vulture owens/own1 snd1 sunder strk`. **`MECH2.EGG` (pod BT dir) = DEV.EGG with + `vehicle=madcat` — the Mad Cat booted, rendered, walked and fought ON FIRST TRY with ZERO code changes:** + own clips loaded (walkStride 18.5 vs BLH 22.0 — real per-mech data measured), 22 damage zones (BLH 20), + 30-subsystem roster constructed, gait/collision/shadow/heat live, combat → TARGET DESTROYED, 0 crashes. + **The per-mech worklist that surfaced (all known warts, nothing new):** (1) the visual-fire muzzles fell + back to the centre point (the gun-port site names are hardcoded BLH.SKL names — the real fix is per-weapon + `.SUB` `segmentReference` muzzle resolution, already how `Emitter::GetMuzzlePoint` works); (2) roster 32 vs + 30 present = its LRM racks hit the still-stubbed missile factory cases (0xBCE/0xBD0 — SUBSYS_PLAN WAVEs, + per weapon FAMILY not per mech); (3) beam colour still the BLH red (per-weapon `VideoObjectName` pending + weapon enumeration). Everything else is foundational. +- **cdb (x86):** `"C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\cdb.exe"`. Pattern for a faulting + stack (set cwd to `content\` first): `-g -c ".lines;sxe av;g;kp 24;q"` with `BT_ASSERT_TO_DEBUGGER=1`. + The debug CRT marks fresh heap **0xCDCDCDCD** (uninitialized) and freed **0xFEEEFEEE** — invaluable for + "was this ever constructed?" questions. gflags PageHeap (needs elevation) caught the earlier subsystem + heap overruns. + +### 10a-bis. Repo layout — where everything lives (CONSOLIDATED into bt411) +The build was formerly a "virtual assembly" scattered across 6 absolute-path locations in the RE +workspace; it is now **one relocatable tree** (`C:\git\bt411`, all repo-relative). Map of the tree +(full detail in `README.md`): +- **`engine/MUNGA/` + `engine/MUNGA_L4/`** — the shared 2007 MUNGA engine + Win32/D3D9 HAL, carrying + our BT render/loader work (`bgfload`/`L4D3D`/`L4VIDEO` + the `image.{cpp,h}` codec). Plus + `engine/shim/` (ATL shim), `engine/lib/` (OpenAL/libsndfile libs + runtime DLLs), `engine/rp/` (the + ~21 RP *headers* the audio HAL includes via `..\rp\vtv.h` — headers only, no RP game source), and + `engine/DPLSTUB.h`/`LOGGER.h` (the trunk-root headers reached via `..\`). +- **`game/reconstructed/`** — the reconstructed BT source (the bulk; was `decomp/reconstructed/`). +- **`game/original/BT/` + `game/original/BT_L4/`** — the surviving *original* BT `.cpp` + (`BTMSSN/BTREG/BTTEAM/BTSCNRL/BTCNSL/GAUSS/PPC/BTL4MODE/BTL4ARND`) **+ all the BT headers** (these two + dirs are the game's header include path). +- **`game/fwd/`** — the ~186 forwarding shims letting reconstructed code do `#include `; + each now `#include "../../engine/MUNGA/.h"` (relativized). `fwd/app.thp` is a self-contained + `TestApplication` reconstruction shim; `fwd/bgfload.h` forwards to the engine loader header. +- **`game/btl4main.cpp`** — the WinMain launcher. +- **`content/`** — the runtime tree (was `tesla4pod_extracted/.../REL410/BT/`): `BTL4.RES`, `VIDEO/`, + `GAUGE/`, `AUDIO/`, eggs, `BTDPL.INI`, `VIDEO/REPLACEMATS.tbl`. **Run cwd = `content/`.** +- **`reference/decomp/`** — the raw Ghidra pseudocode `all/part_*.c` (was `decomp/recovered/`), the + source-of-truth every reconstruction is verified against. `reference/ghidra_scripts/` = the exporter. +- **`docs/`** — format specs (`BGF_FORMAT`, `ASSET_PIPELINE`, `BT_SOURCE_STATUS`) + the reconstruction + ledgers (`RECONCILE`, `HARD_PROBLEMS`, `RESOURCE_AUDIT`, `SUBSYS_PLAN`, `WAVE_PLAN`, `P3_LOCOMOTION`). +- **`tools/`** — `btconsole.py` (MP console emulator) + `mapscan.py`/`resscan.py` scanners. +- External (not vendored): **DXSDK June 2010** (overridable `-DDXSDK`). ⚠ Historical sections below + still cite the old `nick-games/…` absolute paths as provenance — see the orientation banner at the + top; they map onto the dirs above. **NOT copied into bt411:** the standalone D3D9 viewer (§6 `port/`, + a dev sandbox) and the RE toolchain blobs (Ghidra/JDK) — those stay in the original workspace. + +### 10b. Reconstruction workflow (the method) — **RULE: no stand-ins** +The loop for each feature: (1) read the RAW decomp `decomp/recovered/all/part_*.c` for the relevant +`FUN_xxxx`; (2) map `FUN_`/`DAT_`/`this+0xNN` to engine symbols using BT headers + the WinTesla MUNGA +source + `decomp/reconstructed/CLASSMAP.md` + RP's parallel code; (3) write the **real** reconstruction +into `decomp/reconstructed/*.cpp`; (4) `compile1.cmd` then full build; (5) run env-gated + read `btl4.log`; +(6) cdb on any crash. **Never write stand-in/placeholder code for an apparent "gap"** — the full game +logic IS in the pseudocode (the binary ran the game); a "gap" is just a reconstruction stub not yet +filled (e.g. `ResourceStream`/`MemStreamX` no-op proxies, an uninitialised field). Said by the user: +"there are no gaps, just work to be done." Bring-up scaffolding (the env-var paths, the explosion-for-beam, +a player-gated drive) is **clearly marked and meant to be replaced** by the real reconstructed system, +never to substitute for reading the decomp. For exhaustive multi-function decomp analysis, fan out a +read-only **Workflow** (understanding phase), then implement hands-on so each change is visible. + +### 10c. Decompilation insights (per subsystem — durable facts) +- **Resource lookup is by NAME or by ID.** `ResourceFile::FindResourceDescription(name, type, index=-1)` + (raw `FUN_00406ff8` = find-by-type-and-name; `FUN_00407064` = `SearchList(resourceID, type)`). Resource + **type enum**: SkeletonStream, **DamageZoneStream**, ModelList, **AnimationResourceType==16 (0x10)**, + damage-zone segment list **type 0x14**, critical-subsystem segment **0x1e**. Animation clip names in + `BTL4.RES` are **lowercase** (`blhrrl`=904, `blhrrr`=905, `blhwwli`=910, `blhrrli`=916). `Mech:: + ResolveAnimationClip(prefix,suffix)` builds the name and calls FindResourceDescription. +- **Animation (engine `MUNGA/JMOVER.cpp`):** `AnimationInstance(JointedMover*)` then `SetAnimation( + ResourceID, finishedCallback)` (Lock()s the clip, parses `frameCount/jointCount/jointIndices/frameStart/ + keyFrames/rootTranslations` from `resourceAddress`). `Animate(dt, move_joints=True)` writes each joint's + DCS per keyframe. **At end-of-clip it calls `(moverToAnimate->*finishedCallback)(...)` (line ~1592) — a + NULL callback crashes.** Loop a gait via a **non-virtual** `Mech::OnBodyAnimFinished` re-arming the clip, + passed `reinterpret_cast` (Mech derives from JointedMover; the member- + ptr cast is how the original stores a Mech method in the JointedMover-typed slot). +- **`loadTables` gotcha (`MUNGA_L4/L4VIDEO.cpp:849`):** `fopen("VIDEO\\REPLACEMATS.tbl","rb")` is **relative + to cwd with no null-check** → fread on NULL if cwd isn't the pod BT dir. (RP material-substitution table.) +- **⚠ `application`/`l4_application` is NULL outside the frame loop** — `ApplicationManager` (APPMGR.cpp:73) + REASSIGNS the global `application` while iterating the running-app chain each frame and leaves it NULL + between frames. Any code running at Windows message-pump time (WndProc handlers, timers) reads NULL even + while the game runs. Pattern: record the input in a global at message time; consume/apply it inside a + per-frame path where `this` is the engine object (how the window-resize aspect fix works, L4VIDEO.cpp + `gWindowAspect` → per-frame dirty-check rebuild; commit 06683cf). +- **Entity spawn:** `Mech::Make(MakeMessage*)` (`FUN_004a2d48`) allocates `sizeof(Mech)=0x854` + runs the + ctor; the base `Entity` ctor self-registers it into the world (renders + ticks). `Registry::MakeEntity` + dispatches per-class `makeHandler` (BT registry maps `MechClassID=0xBB9 → Mech::Make`). Unique IDs via + `HostManager::MakeUniqueEntityID()`. **The mission system is NETWORKED and BT is PvP-ONLY — it never had + NPCs/AI** (verified, not a recovery gap): `BTREG.cpp`'s entity registry knows only `Mech`/`Projectile`/ + `Missile`/`BTPlayer`/`BTTeam`; the COMPLETE BT header set has no Bot/Brain/AI class; `BTL4OPT.EXE` has no + AI strings; every mech is driven by a `BTPlayer` (or a `BTCameraDirector` for the "camera"/spectator game + model), players grouped into `BTTeam`s. Opponents were always other human pods over the net (`NETNUB`/ + `L4NET`). The AI entities `Crusher`/`Blocker`/`Runner` are **Red Planet** classes (`RP/`), NOT BT and NOT + engine — reference only for *how the engine supports AI if we ever ADD it* (that would be new dev, not a + port). ⇒ A solo mission has no enemy by design; the env-gated `BT_SPAWN_ENEMY` mech is a **test dummy** + (uncontrolled `Mech`), not a game feature. The authentic opponent path is multiplayer (Phase 7). +- **`Mech::PerformAndWatch` runs for EVERY mech** → gate player-only logic on + `this == application->GetViewpointEntity()` (else a spawned enemy is driven by player input / shares the + player's AnimationInstance). +- **Targeting (mech offsets verified vs `part_013.c`):** `mech+0x37c` = `Point3D` target world pos (range/ + aim source), `mech+0x388` = `Entity*` target (**the field `HasActiveTarget()` / `Emitter::FireWeapon` + gate on**), `mech+0x38c` = `int` targeted sub-zone (-1 = whole). Weapons cache `hasTarget/targetPoint/ + muzzlePoint`, refreshed each frame from the mech's target by the mech's targeting update. +- **Firing:** `Emitter::FireWeapon` (`emitter.cpp`, real) builds a muzzle→target beam but **weapon-effect + renderables (beams/tracers) are NOT built in the port** (`btl4vid.cpp:455`) → nothing draws. **Explosions + DO render** (`L4VIDEO.cpp:5356`), so a shot stands in as `Explosion::Make(MakeMessage*)` at the target + (effect resource **"explode" = id 13**). Weapon ClassIDs (CLASSMAP, note the factory enum mislabels): + EmitterWeapon 0xBC8, MechWeapon 0xBCD, Missile 0xBCE, Ballistic 0xBD0, **PPC 0xBD4** (factory calls it + "GaussRifle"), MechTech 0xBD6. Mech ctor offsets: `damageZoneCount @0x11c (param[0x47])`, + `damageZones @0x120 (param[0x48])`, `resourceID @0x1bc (param[0x6f])`. +- **Damage delivery:** `Entity::TakeDamageMessage(id, size, inflictingEntityID, zone, Damage&)` → + `target->Dispatch(&msg)`. **Base `Entity::TakeDamageMessageHandler` IGNORES `zone==-1`**; the comment + (`ENTITY3.h`) says "Mechs use cylinder lookup table" for an unaimed hit — **that Mech override is NOT yet + reconstructed**, so send a VALID zone index for now. `class Damage{ damageType(enum Collision/Ballistic/ + Explosive/Laser/Energy), damageAmount, damageForce, surfaceNormal, impactPoint, burstCount }`. + `DestroyEntityMessage(id, size)` removes an entity. Engine `Explosion` also splash-damages nearby + entities via a collision query (fragile) — direct Dispatch to the known target is what we do. +- **✅ Damage zones — REAL damage model reconstructed & working** (replaced the health-model stand-in). + The chain: aimed `TakeDamageMessage` → engine `Entity::TakeDamageMessageHandler` → `Mech__DamageZone:: + TakeDamage` → engine base armor model (`damageLevel += damageAmount * damageScale[type]`) → accumulates + to 1.0 (zone destroyed) → graphic-state/death branch → death detected. Verified: `structure` climbs + 0.133/hit, vital zone destroyed → `*** TARGET DESTROYED ***`, stable. Build mechanics: engine `Entity` + ctor (`ENTITY.cpp:961-987`) reads `damageZoneCount` from the `DamageZoneStreamResourceType` (type 0x14) + resource and allocates the array (entries left `0xCDCDCDCD`); the **Mech ctor populates that inherited + array** (mech.cpp Pass-3): `MechFindResource(resourceID, 0x14)` → `Lock` → `DynamicMemoryStream(addr, + size, initial_offset=4)` (skips the count word the base consumed) → `for zfield - (char*)res`) vs the binary's + known offsets and dumping raw record bytes as int+float; anchor the true field offsets from the raw decomp + of the resource parser (`CreateStreamedSubsystem`/@004ac9ec — `param_4+0xNN` reads). See §10d heat-flow. + + **Two more non-field fixes were needed for damage to flow:** (1) **message-handler chaining** — a + reconstructed class's `MessageHandlers` set must be built chained to the parent's (`Receiver::Message + HandlerSet(Entity::GetMessageHandlers())`); an empty default-ctor set has NO parent chain, so + `Receiver::Receive` finds no handler and **every inherited message (TakeDamage, etc.) is silently + dropped**. (2) **entity validity** — `Entity::Dispatch` only delivers synchronously (`Receiver::Receive`) + for a VALID master; an invalid entity `Post`s the message as a deferred event that never fires. A + manually-spawned entity (no `MakeViewpointEntity`/CheckLoad handshake) must call `SetValidFlag()` itself. + + **Remaining damage gaps (scoped, non-blocking):** EnergyDamageType(4) shorts attached generators via + `criticalSubsystems[]->subsystemPlug.Resolve()` → needs the REAL subsystem roster (still `RECON_SUBSYS` + stubs) → use ExplosiveDamageType meanwhile (correct for the explosion weapon anyway). Per-impact aim + needs the cylinder lookup (deferred STEP 6 — `Mech::TakeDamageMessageHandler` override `FUN_004a0230` + + `FUN_0049ed0c`); meanwhile aim a vital zone directly. `IsDisabled()` (`FUN_0049fb54`) reconstructed + (reads `movementMode==2||9`), but `movementMode` is set by the bypassed gait/death-transition → bring-up + death reads the real zone state directly (a `vitalDamageZone` with `damageLevel>=1.0`). Removing the + wreck (`DestroyEntityMessage`) crashes on teardown — death = explosion + stop-targeting for now. + ⚠ **ROOT-CAUSE PIVOT (P5 — full writeup `docs/HARD_PROBLEMS.md`; repro `BT_ENABLE_TEARDOWN=1`):** it is + a **TEARDOWN-SEQUENCE double-free, NOT a base-region stomp** (two earlier theories DISPROVEN). (a) "solids + never built" — WRONG (`Mover::Mover`:1756 allocs `collisionLists` unconditionally). (b) "reconstructed Mech + raw offsets stomp the engine collision cluster" — ALSO WRONG: those stomps (`Mech::Simulate` collision/ + terrain/telemetry, `FeedHeat*Gauge`) are **DEAD CODE, never called** (grep-confirmed). The `BT_ENABLE_TEARDOWN` + dump proves the enemy's engine base is **FULLY VALID at death** (`collisionLists@0x2e4`, `segmentTable@0x2f0` + =51 segs, `jointSubsystem@0x30c` all valid). The crash is in the destruction SEQUENCE (`FryDeathRow → ~Mech + → ~JointedMover → ~Mover`): cdb shows `collisionLists`'s array already **freed** (`0xdd`/`count=0xdddddddb`) + by the time `~Mover` `delete[]`s it → a double-free. `~Mech` (reconstructed) runs before the engine base + dtors → prime suspect is a Mech member dtor / the minimal-spawn collision-or-death-row setup freeing (or + aliasing) a base resource the engine dtor frees again. ✅ **PINNED (trace done):** it's the **skeleton + SEGMENT teardown double-free**, not collisionLists. `~JointedMover` (JMOVER.cpp:436 + `SegmentTableIterator(segmentTable).DeletePlugs()`) → `SocketIterator::DeletePlugs` (SOCKET.cpp:157-161 + `delete plug`) force-`delete`s each of the enemy's 51 `EntitySegment`s with `defeat_release_node=1` + (bypassing the ref-count release path); one is **already freed** → `STATUS_HEAP_CORRUPTION 0xC0000374` + + `0xFEEE` fill. ⭐ **P5 CLOSED — the whole premise was WRONG (verified):** a killed mech does NOT disappear + in the real game, so `DestroyEntityMessage`-on-death should NEVER be issued. RP death = `VTV::DeathShutdown` + (VTV.cpp:1681 — subsystems shut down, vehicle STAYS); `CondemnToDeathRow`/removal is ONLY for transient + objects (RIVET projectiles, DEMOPACK cleanup). BT death = a STATE transition (`SetGraphicState(Destroyed + GraphicState)` mechdmg.cpp:355 + death anim `deathAnimationLatched@0x650` + effect/splash) → the mech becomes + a persistent WRECK. So the **wreck-stays behavior we already ship IS faithful**; the teardown crash is an + artifact of forcing a removal the original never does (only reachable via `BT_ENABLE_TEARDOWN`). NOTHING to + fix. Real future death work = reconstruct DeathShutdown + collapse anim + destroyed skin (does NOT touch the + teardown path). Later trace also showed it's heap corruption in a SINGLE teardown, not the double-free the + line above hypothesized. Full trail: `docs/HARD_PROBLEMS.md`. + ⭐ **P5 TRULY CLOSED — ROOT CAUSE FOUND & FIXED (the whole trail above was chasing run-2 of a DOUBLED + dtor chain).** `Mech::~Mech` (mech.cpp:1106) contained an EXPLICIT `JointedMover::~JointedMover();` — + the Ghidra decomp shows the binary's `FUN_00425550(this,0)` tail call, which IS the compiler-emitted + implicit base-dtor; reconstructing it as a source statement ran the whole `~JointedMover → ~Mover → + ~Entity` chain TWICE per Mech. Run 2 re-`delete[]`d `Mover::collisionLists` (count word read from the + 0xDD freed fill = the observed `0xdddddddb`) and re-ran `DeletePlugs` over the destroyed segment table + (= the P5 "EntitySegment already freed" 0xC0000374). ONE bug = BOTH crashes (death-row teardown AND + app exit — every window close crashed once entities existed; symptom timing varied with heap reuse, + which is why idle-close sometimes survived and why the earlier traces looked contradictory; the old + ENTRY/EXIT probes bracketed the EXPLICIT call, misreading run-1 as "a member dtor freed it"). + **⚠ RECONSTRUCTION RULE (add to the systemic checklist):** in a decompiled DESTRUCTOR, the trailing + member-dtor calls (`FUN_xxx(this+N, 2)`), the base-dtor call (`FUN_xxx(this, 0)`), and the + `(flags&1) && operator delete(this)` tail are COMPILER EPILOGUE GLUE — reconstruct only the dtor body + ABOVE them; C++ re-emits member+base destruction implicitly at the closing brace. Explicit member-dtor + calls were also removed from ~Mech (were benign only because the Recon proxies are no-op) and the same + pattern documented out of `Mech__DamageZone`'s dtor (mechdmg.cpp). VERIFIED: interactive play → window + close under `BT_HEAPCHECK=1` = clean exit; combat kill + `BT_ENABLE_TEARDOWN=1` forced entity teardown + = `TARGET DESTROYED`, teardown completes, sim continues, clean close — 0 crashes across the matrix. + **App exit no longer crashes; entity removal (`DestroyEntityMessage`) is usable again** (relevant to MP + disconnect cleanup). Repro harness kept: `BT_FORCE_SECONDS=` (release forced throttle after n sim-s) + + `BT_FORCE_TURN=` (hold steering) in mech4.cpp — the tap-release/turn paths no soak had exercised. + (Forensics: a 14-agent adversarial workflow; secondary REAL-but-dormant hazard found on the way: + `AddCollisionToList` BNDGBOX.cpp:389 has no live bounds check — `Verify` compiles out at DEBUG_LEVEL=0 — + so >10 simultaneous collision contacts from >1 source overflow the 10-slot list; **NOW FIXED** — a live + clamp in `AddCollisionToList` (BNDGBOX.cpp, engine tree) drops overflow contacts instead of writing past + the block. Trigger scenario was real: the user spawned INSIDE a building solid on grass (mass simultaneous + contacts + gravity pressing into terrain boxes) → silent AV mid-play, where the CRT assert dialog ITSELF + crashed while formatting (why no popup appeared). Verified: full combat+teardown+close matrix under + `BT_HEAPCHECK=1` = 0 crashes, clean exit. Spawn-inside-solid placement itself is a separate tuning item.) + (Latent, non-causal: compiled `sizeof(Mech)=0x638` < binary `0x854`; the over-sizeof writes `0x7e0-0x828` + live only in the dead Simulate, so no live overflow — but pad to 0x854 before reviving that code.) + +### 10d. Reconstructed modules wired into `bt410_l4` (state) +~43 reconstructed `.cpp` compile+link. Mech runtime (`mech.cpp/2/3/4`, `mechdmg`, `mechsub`, `mechtech`), +heat (`heat`), weapons (`emitter`/`mechweap`/`mislanch`/`missile`/`projweap`/`projtile`/`gauss`/`ppc`), +subsystems (`gyro`/`sensor`/`gnrator`/`powersub`/`myomers`/`torso`/...), app/HUD (`btl4app`/`btl4vid`/ +`btl4mppr`/`btl4mssn`/`btplayer`/`hud`/`dpl2d`). **Subsystem instances are `RECON_SUBSYS` STUBS — the +un-stub wave is IN PROGRESS** (full per-family plan: `docs/SUBSYS_PLAN.md`, from the `bt-subsystem-recon` +workflow). KEY FINDING: the **real subsystem classes mostly already exist** (heat→`heatfamily_reslice`, +weapons→`emitter`/`projweap`/`mislanch`, gyro/sensor/power/torso/myomers/searchlight/thermalsight/ammobin +all reconstructed); the work is *wiring* them in + the 4 systemic checks (shadowing, `Wword` trap, handler +chain, validity). **The factory case LABELS are systematically MISLABELED** — the real class is identified +by the `// FUN_004xxxxx` ctor-address comment reconciled via `CLASSMAP.md`, NOT the `case ClassID` +label. Master map (case→real): 0xBBD→Condenser, 0xBBE→HeatSink-bank (cache slot=heat bank, not "sensor"), +0xBC0→Reservoir, 0xBC1→Generator, 0xBC2→PoweredSubsystem, 0xBC3→Sensor, 0xBC4→Gyroscope, 0xBC5→Torso, +0xBC6→Myomers, 0xBC8→Emitter, 0xBCB→AmmoBin, 0xBCD→ProjectileWeapon, 0xBCE→UNRESOLVED (decompile `4bdcb4`), +0xBD0→MissileLauncher, 0xBD3→SubsystemMessageManager, 0xBD4→PPC(Emitter), 0xBD6→HUD, 0xBDC→MechTech, +0xBD8→Searchlight, 0xBDE→ThermalSight. Two bugs to fix: case 0xBDC discards the ptr (no `subsystemArray[id]=`); +MechTech(real)+HUD are SWAPPED (MechTech wrongly built at 0xBD6). **Swap recipe:** per-class `Create +Subsystem` bridge fn in the class's own `.cpp` (do NOT `#include` real subsystem headers into mech.cpp — +the local stubs collide) + `extern` it in mech.cpp; keep the alloc SIZE + `++weaponCount`/special-cache. +**Order:** WAVE 1 heat base re-base (`HeatableSubsystem : Subsystem`→`: MechSubsystem`, delete shadow +fields owner/flags/statusFlags/statusBits/destroyed, fix thermal offsets) = universal prerequisite; then +heat family + MechTech/HUD fix; then power bus + Emitter/PPC; then standalone readouts; gyro/torso/myomers +later. DEFER: `MechControlsMapper` tick (reads unddocumented App offsets → AV, mech4 bypasses it); 0xBCE. +**✅ WAVE 1 DONE** — `HeatableSubsystem` re-based onto `MechSubsystem`; shadow fields removed; the cascade +fixed across heat.cpp/heatfamily_reslice/mechsub + powersub/hud/mechweap (the de-shadow propagates to EVERY +heat/powered subclass: `statusFlags`→`simulationFlags`, `destroyed`→`simulationState`, `statusBits`→ +`ForceUpdate()`, and `ResetToInitialState()`→`(Logical powered)` on every override). Full build GREEN, exe +runs, combat (zones/anim/damage→death) un-regressed. **DEEPER FINDING: the shadowing is more systemic than +the plan estimated — `MechSubsystem` ITSELF shadows the engine `Subsystem::damageZone` (mechsub.hpp:252 +`ReconDamageZone *damageZone` shadows `SUBSYSTM.h:159 DamageZone *damageZone`), exposed once MechWeapon's +chain ran through MechSubsystem. Worked around by qualifying `this->Subsystem::damageZone` (mechweap.cpp); +the proper fix = remove the `MechSubsystem::damageZone` shim shadow (cascades to mechsub.cpp) — a follow-up. +Two include fixes also needed: `heat.hpp` now `#include `; `mechrecon.hpp` now `#include +` (self-sufficient Quaternion, for myomers's include order). Next = WAVE 2 (heat factory +wiring + MechTech/HUD un-swap) per `docs/SUBSYS_PLAN.md`.** +**✅ WAVE 2 DONE** — real heat family + HUD + MechTech wired into the factory. Swap mechanism: a per-class +`CreateSubsystem(Mech*,int,void*)` BRIDGE fn appended to each real class's own `.cpp` (heat.cpp: +Condenser 0x230 + HeatSink-bank 0x1e4; heatfamily_reslice.cpp: Reservoir 0x230; hud.cpp: HUD 0x2a4; +mechtech.cpp: MechTech 0x140), `extern`-declared + called from the mislabel-correct factory cases in +mech.cpp (0xBBD→Condenser, 0xBBE→HeatSink-bank+cache, 0xBC0→Reservoir, 0xBD6→HUD, 0xBDC→MechTech+store+cache +— fixing the MechTech/HUD swap + the discarded-ptr bug). RESULT: **30 of 31 subsystems now tick their real +per-frame Performance** (1 bypassed = controls mapper), up from MechTech-only; build green, 16s stable, +combat un-regressed. RUNTIME FIX: `HUD::HUD` read the torso source at raw `owner+0x438` (garbage in the +recon Mech layout) → defaulted the torso/range readouts to 0 (HudSimulation refreshes per-frame). NEW +SYSTEMIC PATTERN for the remaining waves: subsystem ctors read the owner Mech at RAW binary offsets +(`*(int*)(owner+0xNN)`) that are wrong in the recon Mech → guard/zero + per-frame refresh, or wire a real +Mech accessor. Heat self-load from firing (temperature climb) needs WAVE 3 (real Emitter writing the +inherited heat accumulator). Next = WAVE 3: power bus (Generator/PoweredSubsystem) + Emitter/PPC weapon. +**✅ WAVE 3 (power bus + Emitter) — CONSTRUCT+TICK done; full fire-path pending.** 3a: bridges for Generator +(0xBC1, 0x250) + PoweredSubsystem (0xBC2, 0x31c) in powersub.cpp → factory wired → power bus real. 3b: +`CreateEmitterSubsystem` (0x478) in emitter.cpp, wired for BOTH 0xBC8 (Emitter) AND 0xBD4 (PPC = Emitter +subclass, same FireWeapon) → real Emitter constructs + ticks its `EmitterSimulation` state machine. Runtime +fix (E2): `Emitter::TrackSeekVoltage` (emitter.cpp:311) derefs a null voltage source (the source plug at raw +`this+0x1d0` isn't registry-linked in bring-up) → guarded `if (src==0 || dtScale==0) return` (no charge that +frame). All real subsystems now construct + tick; build green, combat un-regressed. +**✅ WAVE 3 fire-path (E3-E8) DONE — the real Emitter fires end-to-end** (verified: repeated `[emitter] +FIRED damage=.. heat=0.49 pendingHeat=0.49`, 17s stable, combat intact). Implemented: E3 `MechWeapon:: +HasActiveTarget()`/`GetTargetPosition` read the owner's target slots (`owner+0x388`/`+0x37c` via the +inherited `MechSubsystem::owner`) not the never-set `hasTarget`; E4 `firingArmed=1` in the Emitter ctor; E5 +deleted the Emitter-local `heatAccumulator` SHADOW, `FireWeapon` now writes the inherited `HeatSink:: +pendingHeat`@0x1c8 (the heat-sim input); E7 force-charge in EmitterSimulation Loading (the source plug at +raw `this+0x1d0` never resolves -> `currentLevel=seekVoltage[idx]` -> outputVoltage 1.0 -> Loaded; also +pre-init the seek table in the ctor so it's valid when the ctor src is null); E8 trigger via a per-FRAME +`int gBTWeaponTrigger` pulsed by mech4 (controls mapper bypassed) -> CheckFireEdge sees clean edges. +**ROOT-CAUSE BUG fixed along the way (benefits ALL subsystems): the MechSubsystem ctor set `hostEntity` +but never set `owner`** (mechsub.cpp) -> every subsystem's `owner` was garbage once heat de-shadowed; now +set in both MechSubsystem ctors. Also: the Mech ctor now zeroes the target slots (+0x37c/+0x388/+0x38c) so +an un-targeted mech (spawned enemy) reads "no target" (else its weapons fire at a garbage pointer). +**✅ WAVE 4 (standalone readouts) DONE — Sensor / Searchlight / ThermalSight / AmmoBin un-stubbed + ticking** +(verified: **BLH tick 20→26 executable**, Mad Cat 24, combat **DESTROYED** un-regressed on both, 0 crashes). +The 4 readout classes were reconstructed but built as `RECON_SUBSYS` stubs; each is now wired via a +`CreateSubsystem` bridge (in its own .cpp) at the mislabel-correct factory case: 0xBC3→**Sensor** +(`MyomersClassID` label), 0xBD8→**Searchlight** (`LegSubsystem`), 0xBDE→**ThermalSight** (`MechDisplay`), +0xBCB→**AmmoBin** (`JumpJet`). Each got the Torso de-shim: delete the cross-family shadow fields, redirect +accessors to the real inherited base, add a `friend struct LayoutCheck` with `static_assert` +offset/sizeof locks. Redirects by branch: **Sensor** (PoweredSubsystem:HeatSink leaf) → `heatAlarm`/ +`electricalStateAlarm`/`simulationState`/`heatEnergy`/`simulationFlags|=8`; **Searchlight/ThermalSight** +(PowerWatcher) → `watchdogAlarm`/`heatAlarm`/`simulationState`/`ForceUpdate()`/`simulationFlags`; **AmmoBin** +(HeatWatcher) → `simulationState` (deleted the `statusState`@0x40 shadow). +- **⭐ FUNCTIONAL BUG FIXED (Searchlight + ThermalSight): the ctor gate read the shadow `segmentFlags` (=0), so + `((0&0xC)==0 && (0&0x100)!=0)` was ALWAYS FALSE → their Performance NEVER installed (dead since + reconstruction).** The de-shim switches the gate to `owner->simulationFlags` (binary-exact part_013.c: + 6012/6140; mirrors torso.cpp:181) → they now tick. (Sensor uses the simpler `!=4` gate; AmmoBin's gate was + already correct.) +- **AmmoBin layout: `ammoAlarm` retyped `HeatAlarm`(8B)→`WatcherGaugeAlarm`(0x54) + the `statusState` shadow + deleted → `ammoModelFile` lands at the binary 0x1E8, `sizeof==0x22C` exact (locked, the load-bearing lock).** + ⚠ CORRECTION to an earlier assumption: the all-laser Blackhawk is NOT AmmoBin-free — it DOES carry AmmoBins + (the un-stub exposed the crash below ON THE BLH, and BLH tick rose 20→26 = Sensor+Searchlight+Thermal+AmmoBins). +- **⚠ SHARED LATENT BUG EXPOSED + GUARDED (`HeatWatcher::WatchSimulation`, heatfamily_reslice.cpp:305):** the + un-stub made `AmmoBinSimulation` (and via `PowerWatcher::Simulation`, the light/thermal sims) call the + HeatWatcher base `WatchSimulation`, which derefs `watchedLink.Resolve()->currentTemperature` — the watch + link isn't resolved in bring-up (the "WatchedSubsystem" name→roster-segment bind doesn't happen), so + `Resolve()` returns null → AV (`AmmoBinSimulation`→`WatchSimulation`, caught under cdb). The binary never + null-checks (its link always resolves at load). GUARDED (marked): `if (watched==0){ heatAlarm.SetLevel(0); + return; }` — same pattern as `Emitter::TrackSeekVoltage`. FAITHFUL FOLLOW-UP = resolve `watchedLink` so the + heat-watch alarm tracks the real watched temperature (it gates cook-off / light-blank / thermal-blank — + secondary behaviors, inert until then). +- **LAYOUT-LOCK FINDING (a new systemic fact): the heat-leaf branch (PoweredSubsystem:HeatSink) is NOT + byte-exact to the binary** — Sensor's exact-offset asserts (radarPercent@0x31C) FAILED at compile time, + unlike the re-based WATCHER branch (Searchlight/ThermalSight/AmmoBin offsets ARE exact and locked). So + Sensor uses only the OVERFLOW lock (`sizeof<=0x328`); its own fields don't land at the binary offsets — + fine until the attribute-table RAW read (the RadarPercent gauge) is wired (a Phase-4 HUD follow-up needing + the heat-leaf branch re-based, same surgery the Watcher branch got). Nothing reads these subsystems at raw + offsets today, so named-member access (compiled-consistent) is correct meanwhile. +- **Factory now: 14 of 20 cases wired to real ticking classes** (was 10). Remaining stubs: Myomers (0xBC6, + WAVE 6), ProjectileWeapon/MissileLauncher + the unresolved 0xBCE (0xBCD/0xBD0, WAVE 7), SubsystemMessageManager + (0xBD3, WAVE 8), Gyroscope (0xBC4, deferred — NaN integrator). Kept DEFAULT-ON (no env gate — pure readouts, + no bring-up fallback needed). +**✅ WAVE 6 (Myomers 0xBC6, mover-coupled) — STRUCTURAL un-stub DONE (gated `BT_MYOMERS`, default ON; verified: +BLH tick 26→27, locomotion + combat un-regressed, 0 crashes; `BT_MYOMERS=0` falls back to the Actuator stub → +26).** The real Myomers (the mech's artificial-muscle drive — voltage→`speedEffect`, throttled by heat + +damage; ctor @4b8fec, `PoweredSubsystem:HeatSink` leaf) is now built at factory case 0xBC6 ("Actuator" +mislabel) via `CreateMyomersSubsystem`. **KEY: it is wired INERT** — it constructs + ticks its real Performance +but does NOT affect gameplay, which is exactly what makes a mover-coupled subsystem safe to land: (a) +`MyomersSimulation` early-returns on `OwnerAdvancedDamage()` (the advanced-damage sim gate; the real source is +the still-stubbed SubsystemMessageManager 0xBD3, so it's a `False` constant → no motion-heat); (b) the mover +feed (`ConnectToMover`→`MoverAttach`) + `SetOwnerMaxSpeed` are no-op/local stubs → **the live JointedMover is +never touched → the gait/drive cannot regress** (confirmed: the mech walks normally, pos advances, adv ramps +0.13→1.05). The ctor DOES do real work: resolves the powering Generator (`ResolveVoltageSource`) + builds the +`seekVoltage[]` gear table from the resource. **De-shim:** the ~10 cross-family Mech/Mover/DamageZone shim +BACKING fields (ownerBaseSpeed/…/damageStructureLevel + segmentFlags) were DROPPED and their accessors return +the neutral defaults the ctor primed — necessary because the real object is EXACTLY 0x358 (zero headroom) and +the shim fields would overflow the alloc; locked `static_assert(sizeof(Myomers)<=0x358)` (heat-leaf branch not +byte-exact → overflow-lock only, like Sensor). The `Myomers` **name collision** is already handled in +myomers.hpp (`#define Myomers Myomers__powersub_stale` around the powersub.hpp include, since powersub.hpp +still carries the OLD Sensor-mislabel "Myomers" — that stale class should eventually be deleted from +powersub.hpp). **⚠ AUTHENTIC COUPLING = the real Wave-6 payoff, DEFERRED:** making the mech actually SLOW when +damaged/overheated + motion GENERATE heat needs (1) the advanced-damage gate from a real messmgr (0xBD3, +WAVE 8), (2) the `owner*` shims redirected to real Mech motion/mass/motion-gain accessors, and (3) +`MoverAttach` actually routing `speedEffect@0x31C` into the JointedMover roster — which **drives the LIVE +mover**, so it must be reconciled with the gait cutover FIRST (both would feed the mover) before enabling. +Factory now: **15 of 20 cases wired to real classes** (remaining: ProjectileWeapon/0xBCE/MissileLauncher +0xBCD/0xBD0 [WAVE 7], SubsystemMessageManager 0xBD3 [WAVE 8], Gyroscope 0xBC4 [deferred]). + +**✅ WAVE 7 (projectile/missile weapon families) — Phase A DONE: the three weapon SUBSYSTEMS are wired +byte-exact (un-mislabeled per VDATA.h enum); Phase B (flying projectile/missile ENTITIES) deferred.** +An 8-agent decomp-mapping workflow (scratchpad/wave7_maps.txt) mapped every ctor + fire/spawn path. +**KEY IDENTITY CORRECTION (VDATA.h enum, base 3000=0xBB8):** 0xBCD=**ProjectileWeapon**, 0xBCE=**GaussRifle** +(NOT MissileLauncher!), 0xBD0=**MissileLauncher** (NOT BallisticWeapon). The factory ctor-address COMMENTS were +right but the built CLASS names were stubs/base. Un-mislabeled + wired via `CreateSubsystem` bridges +(mirroring CreateEmitterSubsystem): **0xBCD→ProjectileWeapon** (@4bc3fc : MechWeapon, sizeof **0x448** — FIX: +`AmmoBinConnection` was an empty 1B struct → retyped to the binary's 0xC SharedData::Connection, was making the +object 8B short); **0xBD0→MissileLauncher** (@4bcff0 : ProjectileWeapon, sizeof **0x44C** — FIX: deleted +`muzzleVelocity` [ALIAS of inherited `launchVelocity@0x410`] + `salvoCount` [SHADOW of `damageData.burstCount@0x3d4`], +keeping `missileCount@0x448`); **0xBCE→GaussRifle** (@4bdcb4 : Emitter, sizeof **0x484** = Emitter+Vector3D +muzzleVelocity — a DEDICATED bridge, not CreateEmitter, because its FireWeapon is a NO-OP `mov [eax+0x414],0` += a non-functional weapon in this 1995 build; also fixed GAUSS.CPP's discharge write `rechargeLevel`→`currentLevel@0x414` +per the binary). All three `static_assert`-locked (offsetof/sizeof). Verified: Mad Cat (LRM/ballistic) + BLH +construct the real weapon classes + tick authentic charge/ammo/heat/recoil, DESTROYED-in-8, 0 crashes, +heapcheck-clean through construction. Factory now **18 of 20** (remaining: SubsystemMessageManager 0xBD3 +[WAVE 8], Gyroscope 0xBC4 [deferred]). +**✅✅ WAVE 7 Phase B DONE — the Mad Cat's LRMs now LAUNCH flying missiles that fly + damage (autocannon rounds +too).** The byte-exact WORLD-ENTITY reconstruction (Projectile @4be1bc 0x340 / Missile @4bf5b4 0x368) is +**infeasible on the engine**: measured `sizeof(2007 engine Entity)=0x1BC` vs the 1995 binary's 0x300, so the +reconstruction's raw base-offset reads (velocity@0x1dc, roster@0x124, motion@0x250) read GARBAGE on the engine +(the Mech 0x638-vs-0x854 gap, but the entity INTEGRATOR depends on those offsets). So — like the mech drive / +beam renderer — flying projectiles are a **PORT reconstruction** (`BTPushProjectile`/`BTUpdateProjectiles` in +mech4.cpp): seeded from the launcher's fire with the decomp's real muzzle (`GetMuzzlePoint`) / launch speed +(`|launchVelocity|`) / per-shot damage (`damageData`, split across `missileCount`), they fly to the target +(tracer via `BTPushBeam`) and deliver the weapon's damage on impact through the SAME `Entity::TakeDamage` path as +the beam (aim `Mech::FirstVitalZone()`). **THREE bring-up fixes were needed to make a projectile weapon fire at +all** (all found by tracing, marked): (1) **the trigger** — `fireImpulse` was only driven for the Emitter; +`ProjectileWeaponSimulation` now sets `fireImpulse = gBTWeaponTrigger` too (else `CheckFireEdge` never sees an +edge); (2) **the ammo bin** — `OwnerSubsystemCount/OwnerSubsystem` were stubbed `->0` so `ammoBinLink` never +resolved and `ConsumeRound` always failed; redirected to the real roster (`owner->GetSubsystemCount()/ +GetSubsystem(i)` — the AmmoBin 0xBCB constructs before the weapons 0xBCD/0xBD0); (3) **the jam roll** — +`UniformRandom()` was stubbed `return 0.0f`, so `CheckForJam`'s `0 < jamChance` ALWAYS jammed (a projectile +weapon could NEVER fire); replaced with a real LCG `[0,1)` (fires ~1-jamChance of the time — authentic occasional +jams). Also: the mech's own target slot (`owner+0x388`) isn't populated in bring-up (the visible fire targets the +`gEnemyMech` global), so `BTPushProjectile` **falls back to `gEnemyMech`**. Verified: Mad Cat PUSH=62 / IMPACT=31 +(LRM missiles 3.33 dmg each split across the salvo + AFC100 autocannon 25 dmg), TARGET DESTROYED, 0 crashes; BLH +un-regressed (also fires its ballistic weapon now). Diagnostics `BT_PROJ_LOG` (`[projectile] PUSH/IMPACT` + +per-weapon fire trace). **The energy weapons (Emitter/PPC) still damage via the mech4 beam path.** REMAINING +(deferred): the byte-exact world-entity Missile (`Projectile : Mover`, MP-replicable via `Registry::MakeEntity`) +— the port projectile is master-local only (no MP replication); the real per-weapon fire-rate/heat wiring off +the subsystem sim (the mech4 beam path is still the bring-up harness). +**✅ HEAT-FLOW LINKAGE CONNECTED — weapon heat now conducts to the central sink** (verified: `[heat] +conduct self.E=.. -> other.E`, sustained fire `FIRED #1..#21+`, combat intact, no crash). THREE fixes, +each faithful to the decomp (no stand-ins): + 1. **EmitterSimulation ran a no-op stub** `FUN_004b0bd0` for the "PoweredSubsystem step" -> the emitter + never ran its heat sim. Replaced with the REAL `PoweredSubsystem::PoweredSubsystemSimulation` + (@004b0bd0, chains to `HeatSink::HeatSinkSimulation` = absorb pendingHeat + `ConductHeat`). Emitter is + `Emitter:MechWeapon:PoweredSubsystem:HeatSink`, so the qualified base call is valid. (emitter.cpp) + 2. **Linked-sink Add-gate read the wrong flags.** Raw decomp of the HeatSink ctor @004adda0 gates BOTH + the Performance-install AND the `linkedSinks.Add` on `param_2+0x28` = `owner->simulationFlags`, NOT + the per-segment `subsystem_resource->subsystemFlags` (which stream 0 -> dead gate -> no links). The + reconstruction had only half-fixed this (Performance gate on owner, Add-gate still on resource). + Fixed the Add-gate to owner flags too. (heat.cpp) The link TARGET is still per-subsystem `heatSinkIndex`. + 3. **⚠ NEW SYSTEMIC BUG — resource-struct layout mismatch (a sibling of the field-shadowing bug).** + `HeatableSubsystem__SubsystemResource` inherited `Subsystem::SubsystemResource` (ends 0x30) instead of + `MechSubsystem__SubsystemResource` (ends 0xE4) -- the class hierarchy is `HeatableSubsystem:MechSubsystem` + so the RESOURCE must mirror it. Result: every heat field slid 0xB4 low; `heatSinkIndex` read a + neighbouring float (`10.0f`=1/thermalMass) -> `GetSegment` OOB -> no link, and thermalMass/startTemp + were garbage. Fixed the base. Then an 8-byte residual gap: `alarmModel` was typed as a 4-byte `ResourceID` + (`typedef int`), but the RECORD reserves a 12-byte field at 0xCC (raw `@004ac9ec` parses it via + FUN_00408944 as 3 floats, with `PrintSimulationState` at 0xD8). Added `int _alarmModelReserved[2]`. + Now `startTemp=0xE4 thermalMass=0xF4 heatSinkIndex=0xF8` EXACTLY match the binary; heatSinkIndex reads + real indices (5,2,4,..), thermalMass real values (1.39e6..), links resolve, ConductHeat flows. + **DIAGNOSTIC TECHNIQUE (reusable for any resource-layout doubt):** log compiled member offsets via + `(char*)&res->field - (char*)res` and compare to the binary's known offsets; dump raw record bytes as + int+float side-by-side to see the true on-disk layout (records are pre-built 256-byte blobs loaded verbatim + at fixed offsets, so OUR struct MUST match the binary or every member read is wrong). + **NOTE on the payoff:** with the REAL data, `currentTemperature` stays ~77 (startTemp) because `thermalMass` + is genuinely huge (~1e6) vs ~0.24 heat/shot; the visible metric that responds to fire is `heatLoad` (the + smoothed radiated-heat reading), which rises/decays with firing. Not a bug -- authentic data behavior. + (The "linked `other` sink reads heatEnergy~0" follow-up is RESOLVED — the link target was WRONG; see the + heap-corruption root cause below.) +**✅ THE BGF-LOAD HEAP CORRUPTION — ROOT-CAUSED & FIXED (was: `LoadBgfFile("bld08.bgf")` → `Builder::~Builder` + → AV in `operator delete`, position-dependent).** A 4-agent workflow + hands-on forensics converged: + - **The loader was INNOCENT** — an exact Python mirror of `bgfload.cpp` swept all 879 content BGFs with zero + anomalies; every Builder write is bounds-guarded; and the crashed 0x768 block is exactly the correct + 474-float capacity for bld08's 472 verts (MSVC 1.5× growth ...316→474). The AV was ntdll's free-list walk + reading `0xDDDDDDDD` (the CRT freed-fill) inside the ADJACENT block's NT heap metadata — prior corruption, + merely DETECTED at the loader's teardown. `BT_PROBE_BGF=` + `BT_PROBE_N` (btl4main.cpp) now + direct-loads models at boot for regression (bld08 clean 25×; bld08_lp is a bound-only file, benign FAIL). + - **THE CULPRIT — a TYPE CONFUSION in the heat/power linkage (reconstruction bug):** `HeatSink`'s ctor + resolved its linked sink via `owner->GetSegment(heatSinkIndex)` — the Nth skeleton **EntitySegment** + (288 bytes) — cast to `Subsystem*`. The binary (`@004adda0`, part_012.c:16999) instead reads + **`owner->subsystemArray[heatSinkIndex]`** (the SUBSYSTEM ROSTER @0x128, bounds-checked vs + subsystemCount@0x124, null-guarded). Through the bogus pointer, every per-frame `ConductHeat` wrote + `other->pendingHeat` at offset 388 = **100 bytes past the 288-byte block**, and `BalanceCoolant` wrote + `coolantLevel` 20 bytes past — thousands of 4-byte OOB heap writes during sustained fire → smashed NT + free-list links next to the mech's EntitySegments → the next big alloc/free churn (a lazy building load) + AV'd. **Sibling bug in `PoweredSubsystem`** (raw part_013.c:1198): `voltageSourceIndex` (res+0xFC) is + ALSO a roster index; the GetSegment draft made `AttachToVoltageSource` write `currentTapCount` 136 bytes + past the segment block at every mech spawn. BOTH fixed to the roster lookup via the public + `owner->GetSubsystemCount()/GetSubsystem(i)` (+ the powersub else-gate fixed to OWNER flags per raw). + Verified: sustained combat under `BT_HEAPCHECK=1` (`_CRTDBG_CHECK_ALWAYS_DF`, whole-heap validation on + every alloc/free — now a runtime env gate in btl4main.cpp) = 100+ shots, ZERO detections; the heat link + now reads a REAL sink (`heatEnergy=1.34e+07`, was ~0 through the segment). **LESSON (add to the systemic + checklist): a `+0x128`-style owner offset in subsystem code is the subsystem ROSTER (`subsystemArray`), + NOT the segment table — check every `GetSegment(int)` call in reconstructed subsystem ctors.** + - Bonus findings from the audit (non-corrupting, tracked): `keyframeData[keyframeCount]` in + LoadLocomotionClips is BINARY-FAITHFUL (the raw does the same read; the value is stable/in-bounds in real + clip data — do NOT "fix" to [count-1]); crashClipA/B/C+gimpBaseClip are separate members here but live + INSIDE `animationClips[]` in the binary ([3]/[2]/[4]/[32]) — a wrong-clip fidelity gap for crash/gimp gait + states when those go live; `Mech::CreateModelResource` alloc 200 < sizeof 356 is a LATENT overflow in dead + offline-tool code; Torso 640==0x280 alloc is an EXACT fit (zero headroom — any Torso/Watcher growth must + bump the factory alloc); StatusMessagePool->New() at btplayer.cpp:460 derefs a NULL stub pool (latent); + mechdmg.cpp:289 leaks IntegerPlugs into a no-op DZIndexTable::Add. +**✅ RESOURCE-LAYOUT AUDIT — DONE (full report: `docs/RESOURCE_AUDIT.md`; commits 802a7a6/7de66ad/975a397).** +A 33-agent adversarial audit of all 24 `*__SubsystemResource` structs found **8 confirmed layout bugs** (same +silent-garbage class as the heat fix), all rooting to 2 broken base resources (`HeatWatcher`, `PowerWatcher`) ++ 2 field defects (`Gyroscope`, `Searchlight`). Raw-decomp-verified + FIXED: **HeatWatcher re-based onto +MechSubsystem** (record 0xF0; class+resource+ctor+parser, AmmoBin child follows) and **PowerWatcher re-based +onto HeatWatcher** (record 0xF4; HUD/ThermalSight/Torso children follow) — the reconstruction had approximated +BOTH as `: public HeatableSubsystem`, sliding every Watcher field low. Gyroscope got its unread +0xF4 record +slot (exageration→0xF8); Searchlight now reads the inherited `segmentIndex`@0x28 (was a bogus appended field). +Combat + heat-flow un-regressed. **KEY PRACTICE ESTABLISHED: lock every resource-struct layout with +`static_assert(offsetof(...)==0xNN)` + `static_assert(sizeof(...)==0xNN)`** against the binary's known offsets — +compile-time proof that can never silently regress (far better than a runtime log; the test mech may not even +instantiate the subsystem). Cascade notes: re-basing a Watcher class off HeatableSubsystem un-shadows its +`degradation/failureTemperature`; child `HandleMessage`/`Simulation` delegates that called `HeatableSubsystem::` +must redirect to the real base (`MechSubsystem::HandleMessage`, `HeatWatcher::ResetToInitialState`); a re-based +class's header needs a guarded `#include` of its new base's header (powersub.hpp now includes heatfamily_reslice.hpp). + +**✅ HEAT-LEAF OBJECT-LAYOUT RE-BASE — the CORE heat leaf is now byte-exact (P7 core DONE; full status +`docs/HARD_PROBLEMS.md` §P7).** The OBJECT-layout analog of the resource-layout audit: the reconstruction modeled +the binary's shared alarm/connection types with UNDERSIZED stand-ins, sliding every field above them low (the +72-byte `PoweredSubsystem::auxScreenNumber` gap). FIXED byte-exact + `static_assert`-LOCKED: **`HeatSink`** +(heatEnergy@0x158/linkedSinks@0x164/heatAlarm@0x170/pendingHeat@0x1C8, sizeof 0x1D0), **`Condenser`** (0x230 +region), **`Reservoir`** (sizeof 0x230), **`Generator`** (sizeof 0x250), **`Myomers`** (phantom tail removed). +The shared types are now correct: **`SubsystemConnection` = 0xC** (was 4; binary link node, `FUN_00417ab4` +two-level-derefs +8) and **`GaugeAlarm54` = 0x54** (the real `AlarmIndicator` `FUN_0041b9ec`; STATUS level at +**+0x14**, so `subsystem+0x184 == heatAlarm+0x14 == GetLevel()`; `WatcherGaugeAlarm` typedefs to it). Heat +conduction reads the REAL `heatEnergy=1.34e7`; combat DESTROYED-in-8, 0 crashes, heapcheck-clean through +construction. **THREE new systemic bug classes fixed (add to the checklist):** (1) an **alias field** = a subclass +member re-declaring an inherited slot the ctor reuses under a new name (Condenser `refrigerationOutput`==inherited +`massScale@0x160`; Reservoir `coolantCapacity`==`thermalCapacity@0x128`; Emitter `outputVoltage`==inherited +`rechargeLevel@0x320`; Emitter `beamLengthRatio`==`beamScale.z@0x434`; Emitter `firingArmed`==inherited +`useConfiguredPip@0x3E0`) → delete the member, use the inherited/existing name; (2) an **alarm-interior field** = a +value the binary reads at `alarm+0x14` modeled as a separate member (HeatSink `heatState@0x184`, Reservoir +`injectActive@0x1e4`) → route to `alarm.GetLevel()`; (3) a **phantom field** = a member at an offset PAST the object +(Generator `shortFlag@0x25C` is really `*(owner+0x190)+0x25c` the msg-manager; Myomers `moverConnection@0x110` a +write-only base slot; MechWeapon `segmentReference/pipSegment/hasTarget/targetPoint/muzzlePoint` past 0x3F0; Emitter +`beamHitPoint/beamImpact/beamImpactScalar/beamColor/beamHitData/energyRampTime` — binary writes into inherited +`damageData`/`voltageScale` or the value is a method local) → remove it, read the real source or use a local. + +**✅✅ SUBSYSTEM-TREE ALARM UNIFICATION — DONE: the WHOLE PoweredSubsystem weapon/power subtree is now byte-exact +(P7 CLOSED; full per-class map from the 8-agent `alarm-unification-map` workflow, saved scratchpad/alarm_unify_maps.txt).** +Making `PoweredSubsystem` byte-exact (its two 0x54 alarms `electricalStateAlarm@0x264`/`modeAlarm@0x2B8`) grows it ++0x98, cascading into every subclass — so all were byte-exacted TOGETHER in one build (each `static_assert`-locked +against its ctor + factory alloc; the chain HeatSink 0x1D0 → **PoweredSubsystem 0x31C** → **MechWeapon 0x3F0** → +**Emitter 0x478** → PPC 0x478; PoweredSubsystem → **Sensor 0x328**, → **Myomers 0x358**, all verified against the raw +ctors @004b0f74/@004b99a8/@004bb120/@004b1d18/@004b8fec). Each class: retype its `AlarmIndicator`(==`ReconAlarm`==4B) +/`HeatAlarm`(8B) stand-in for the binary 0x54 alarm → `GaugeAlarm54`; delete phantom/alias/duplicate fields; +`friend struct XLayoutCheck` with `offsetof`/`sizeof` locks (protected members need the friend; public fields work +at namespace scope). **KEY non-layout fixes required in the same wave:** (a) `outputVoltage`→`rechargeLevel` also in +the compiled **`GAUSS.CPP:74/93`** (external readers of the removed Emitter field — grep EVERY TU, not just the +class's own .cpp); (b) `MechWeapon::GetMuzzlePoint` reimplemented faithfully (the removed `muzzlePoint` member +collided with Emitter's own fields at 0x3F0) via a **`BTResolveWeaponMuzzle` void\* bridge in mech4.cpp** (a +complete-Mech TU with the segment API; mechweap.cpp reaches `owner` only as a raw pointer) resolving the weapon's +mount segment (inherited `this+0xdc`) through `GetSegment(int)`+segment→world; (c) `DetachFromVoltageSource` was +setting the WRONG alarm (`modeAlarm`→`electricalStateAlarm`, raw @004b0e30 writes 0x264). Verified: combat +DESTROYED-in-8, 28 shots, 0 crashes, heat `heatEnergy=1.34e7`, all locks pass. **The vehicleSubSystems aux-screen +gauge raw reads (`btl4gau2.cpp:868/952` at `subsystem+0x2b8`/`+0x278`) + the Sensor RadarPercent path now read the +CORRECT byte offsets** (they were garbage under the short layout). ⚠ `runtime Check(sizeof<=alloc)` in a factory +bridge does NOT fail the build (it's a runtime assert → heap overflow at construction); use a `static_assert` +sizeof lock to catch alloc overflow at COMPILE time (this is why the earlier isolated PoweredSubsystem retype +silently overflowed the Emitter alloc). **TECHNIQUE:** a read-only decomp-mapping Workflow (one agent per class +decodes its ctor → offset map + phantom/alias findings, + adversarial verify of the composed chain) then hands-on +implementation — the right shape for a systemic multi-class byte-exact re-base. + +**✅ MechControlsMapper REVIVED — the real input-interpretation tick runs LIVE (gated `BT_REAL_CONTROLS=1`).** +The fully-reconstructed mapper family (mechmppr.cpp `MechControlsMapper` @004afbe0-@004b08c0; btl4mppr.cpp +`L4MechControlsMapper`/`MechThrustmasterMapper`/`MechRIOMapper` @004d11e8-@004d27f8) was already built + +constructed at runtime (btl4app `MakeViewpointEntity` builds a `MechRIOMapper` into roster slot 0 via +`SetMappingSubsystem`); the ONLY bypass was the tick being skipped (mech4.cpp roster walk) because of raw-offset +bugs. FIXED (a 4-agent workflow mapped the binary/API/state first): (1) **wild typed-pointer arithmetic** in +`InterpretControls` (`*(Scalar*)(mech + 0x34c)` on `Mech*` = +0x34c*sizeof(Mech) — THE AV) → all owner reads +now reconciled members: `mech+0x34c→reverseStrideLength` (naming caveat: it is the TOP/run speed the throttle +scales — CONFIRMED by data: speedDemand=61.5 == the authentic run speed), `0x534→walkStrideLength`, +`0x5c0→forwardThrottleScale` (NEW member; writer not in decomp, ctor=1.0), `0x438→sinkSourceSubsystem` (real +binary-exact Torso: `SetAnalogTwist/ElevationAxis`@0x1F0/0x1F4, `GetHorizontalEnabled`@0x250), +`0x5b4→hudSubsystem`; (2) **the 0x5b4 cache was MISWIRED**: raw factory (part_012.c:10155-10164) case 0xbd6 +(HUD, alloc 0x2a4) writes `param_1[0x16d]` — case 0xbdc (MechTech, binary alloc 0x104!) has NO cache; our +factory had it on MechTech (renamed member `mechTechSubsystem`→`hudSubsystem`, cache moved to the HUD case; +MechTech's 0x104 alloc could never hold the mapper's `+0x28c` write); (3) **HUD `flickerPhase`@0x28C renamed +`freeAimSlew`** — it IS the mapper's free-aim/reticle-slew output (was init-only placeholder); (4) +**`FillPilotArray`'s wild app reads** (`application+0x6c→station+0x190` — the OTHER AV) → the native +`application->GetMissionPlayer()` + `GetEntityID()` (the same drift-fix btl4mppr's SendFakeButtonEvent uses); +(5) the look/eyepoint COMMIT deferred (marked): its binary offsets collide with declared members +(`mechName`@0x360, `stateFlags`@0x410, `poweredSubsystems`@0x7bc) — arbitrate before enabling; no port consumer. +**Wiring (mech4.cpp, gated):** an INPUT BRIDGE writes the mapper's published input attributes from WASD/ +env exactly as a streamed Direct `.CTL` mapping would (the dev box has no RIO/Thrustmaster; interpretation +stays 100% authentic), the roster walk un-skips the mapper, the SM consumes `bodyTargetSpeed=|speedDemand|` +and the drive heading uses `turnDemand` (mode-shaped steering with the authentic speed-vs-turn clamps). +**Verified live:** `[mppr] thr=1 → speedDemand=61.501` → SM walk→run at the commanded speed; full tick chain +(RIO→L4→main + BuildPilotArray) clean; baseline (env off) combat un-regressed (FIRED #221); heapcheck run +with real controls + combat = 61+ shots, zero detections. **Controls plumbing facts (from the workflow):** +the engine is a PUSH model — `LBE4ControlsManager` groups (`scalarGroup/joystickGroup/keyboardGroup/ +buttonGroup`) are fed by ALL devices (RIO serial on the pod, DirectInput on dev) and mapper members register +via `group[elem].Add(mask,&member,this)`; `L4CONTROLS=KEYBOARD` gives NO analog axes (key-RELEASE events +only) — so a real stick needs `L4CONTROLS=DIJOYSTICK`/`THRUSTMASTER`, or the bridge. **Remaining mapper +work:** fire-trigger through the mapper (streamed `.CTL` event mappings → weapon demand; gBTWeaponTrigger +still the bring-up), look/eyepoint offset arbitration, control-mode cycling (message handlers exist), +`ControlsMapper`-vs-`SubsystemMessageManager` untangle at factory 0xBD3 (binary 0xBD3 = messmgr; the mapper's +canonical home is roster slot 0 — our 0xBD3 case builds a base mapper as a stand-in). + +**✅ PILLAR A — THE MOTION MODEL IS COMPLETE & DEFAULT-ON (locomotion P3 closed for MP purposes).** +(1) **LEG callback reconstructed** (`FUN_004a6928` == `PTR_LAB_0050d6f0`, PE-parse + capstone jump table +@0x4a69aa): `Mech::LegClipFinished` — same 33-state shape as the body's but compares the LIVE mapper +`speedDemand` (binary: `*(subsystemArray[0])+0x128`; typed mirror `controlsMapper`), slews `legCycleSpeed` +@0x348, re-arms via `SetLegAnimation`, gimp alternates 0x12↔0x13 with |ratio| (negative gimp stride). +(2) **The two-channel split is LIVE** (under real-controls): the LEG channel writes the skeleton JOINTS +from the live demand, the BODY channel is pure motion (`Advance(dt, move_joints=0)`) — the binary's +leg=local-sim / body=displayed-motion model. The leg enters NATURALLY from Standing: verified progression +legState 0→5 (stand→walk) →11 (walk→run) →13↔12 alternation converging on the commanded 61.5 u/s. +**KEY RAW FIX: the Standing case was INVERTED in BOTH channels** (recon had `commanded < standSpeed → +walk`; raw FUN_004a5028/FUN_004a5678 case 0 is `standSpeed < commanded → SetAnimation(5)`, `<0 → reverse +0x10`, else stand) — without the fix a commanded mech never left Standing. Also fixed: `AdvanceLegAnimation` +double-deref'd the never-initialized `controlSource` alias (AV) → reads `controlsMapper->speedDemand` +(null → 0 = idle); `legAnimationState`@0x3b0 == `legStateAlarm.level` re-synced (same as body). +(3) **Orientation** now composes the yaw RATE into `localOrigin.angularPosition` via the engine integrate +op (`Quaternion::Add` == FUN_00409f58) instead of rebuilding from a scalar heading — the mech keeps its +authentic spawn orientation (rate CONSTANT still bring-up; the per-mech turn-rate param is deferred). +(4) **Velocity storage every frame** (was collision-gated): `worldLinearVelocity` from the frame delta + +`localVelocity` (linear fwd −Z + yaw angular) — the exact fields `Mover::WriteUpdateRecord` publishes = +the MP dead-reckon update-writer data. `DeadReckonPose` was already reconstructed; consuming it is MP-side. +(5) **Defaults flipped** (see §10a). Consequence of the authentic spawn orientation: `BT_SPAWN_ENEMY` now +places the dummy along the spawn FACING (btplayer.cpp; the fixed z-offset assumed the old forced heading +and the mech walked away → "no damage" red herring). Verified default-stack combat: structure→0.933. +**Remaining locomotion polish (non-gating):** authentic turn-rate constant; body-callback gimp handlers +(targets 0x70b2/0x7161 undecoded); airborne callbacks (`FUN_004a6344`/`FUN_004a7970`); wall-block-vs-climb +tuning; collision damage application; the eyepoint/look commit (mapper). + +**✅ SHADOW — the AUTHENTIC 1995 model decoded + implemented, render-verified (task #20, commit b38e94b).** +BT 4.10's shadow is NOT a projection: it is the flat **`*_tshd.bgf` proxy** drawn as ordinary geometry, +posed by **`jointshadow`** (balltranslate under ROOT; SKL comment "apply terrain angle to pitch and roll") +and its child **`jointtshadow`** (hingey; "apply torso twist to yaw", Torso record key +`TorsoHorizontalShadowJoint`). Configured by the model record's **`ShadowJointName` @0xB4** (≤19 chars, +"Unspecified"=none), resolved in the Mech ctor (part_012.c:10285-10310 → **`this[0x10b]` @0x42c** = the +shadow Joint — the earlier recon misread this block as a death-effect lookup; now `Mech::shadowJointNode`). +`dpl_SetGeometryShadowVolume` exists in libDPL headers but has NO surviving game callers. Port implementation: +(1) `d3d_OBJECT::mIsShadow` + a dedicated DrawMesh path — blend-pass only, SRCALPHA/INVSRCALPHA, no Z-write, +constant TFACTOR `0x8C000000` (~55% black), all touched states saved/restored exactly; btl4vid tags `tshd` +objects at load AND sets each drawOp `alphaTest=true` (**pass scheduling fact:** `HierarchicalDrawComponent:: +Execute` L4VIDRND.cpp:149 routes objects into PASS lists per-drawOp on alphaTest/drawAsDecal/drawAsSky — +an object with all-false drawOps only ever reaches PASS_OPAQUE). (2) `Mech::UpdateShadowJoint(normal)` +writes pitch/roll to jointshadow per-frame. **✅ SHADOW TERRAIN-TILT NOW LIVE (was the flat-up placeholder; +default-on, gate `BT_SHADOW_TILT=0` restores flat).** The user reported the shadow DISAPPEARS on elevation + +the feet look sunk. Root cause: mech4.cpp fed `UpdateShadowJoint` a hardcoded flat-up normal, so on a slope +the flat `*_tshd` quad was buried in the hillside and Z-culled (it draws Z-tested, no Z-write, in +PASS_ALPHABLEND) → gone; and the missing ground-contact cue made the feet READ as sunk even though the origin +is measurably ON the surface (alignment probe at the mech's climb positions: visual−origin = 0.05–2.7u, i.e. +NOT a placement bug — the snap is correct). Fix (mech4.cpp shadow block): now that the ground decode (#15) is +live, sample the collision surface via `GetMoverCollisionRoot()->FindBoundingBoxUnder` at the mech's XZ + two +world-axis offsets (D=12u), build the world normal from the height gradient `(-dH/dx,1,-dH/dz)`, rotate it into +the mech-local frame by `-gDriveHeading` about Y (mech is upright, yaw only), and pass it in — jointshadow's +SKL comment is literally "apply terrain angle to pitch and roll". A probe miss (mech at its collision node's +edge) falls back to flat. **Render-verified:** on a butte-flank slope the shadow now LIES ON the ground +(tilted, follows the slope) and anchors the feet; pre-fix the same steep mid-climb showed NO shadow; combat +un-regressed (TARGET DESTROYED after 8 hits), 0 crashes / 0 heap detections under BT_HEAPCHECK. NOTE the probe +uses the mech's `containedByNode` so far offsets can miss (→ flat fallback); a residual uphill-foot dip on +steep slopes is the authentic upright-biped geometry + coarse-collision (0–2.7u), not a placement bug. (3) +Torso yaw→jointtshadow was already wired (Torso::UpdateJoints; data-disabled on the BLH record, byte-faithful). +**✅ THE "MECH SINKS INTO INCLINES + SHADOW DISAPPEARS" BUG — ROOT-CAUSED & FIXED (shadow DEPTH BIAS, +L4D3D.cpp mIsShadow block; BT_SHADOW_BIAS= tunes, =0 restores plain Z-test).** The user reported feet +sinking + shadow vanishing on ANY moderate incline (original vids show neither). Systematic elimination: +(a) flat ground placement EXACT — dg100's instance Y is −0.453125 (map dumps printing "−0" hid it); visual +floor AND slab top both at −0.4531 = the mech's resting Y, zero offset; (b) snap faithful (origin = solid +surfaceY, verified decode); (c) LOD selection correct — BGF LODs are FINEST-FIRST (dhillg1 136→47 tris) and +the loader takes the first; ⚠ DG100 the desert-floor model is PERFECTLY FLAT (all verts Y=0) — the rolling +desert relief is the giant dhillg mounds (r≈950); (d) LOD0-exact content gap (earlier merged-LOD measurements +were inflated): mound visual runs 0–2.1u ABOVE the collision cones on slopes (median 0–0.5, p90 1.2–1.5) = +ankle-to-shin foot clip at worst patches — authentic, the binary had the same. **THE REAL RENDER DIVERGENCE: +the shadow proxy drew with a plain Z-TEST, so wherever the visual rose even ~0.3u over the quad the WHOLE +shadow was buried+culled — and without its ground-contact cue the small authentic foot-clip reads as "the +mech is sinking".** The IG board drew ground shadows decal-style (depth-biased coplanar geometry — the classic +blob-shadow technique; the shadow FLICKER in pod footage is that bias fighting at edges). FIX: the mIsShadow +draw now sets `D3DRS_DEPTHBIAS` (default −0.004 normalized ≈ 2–4u at chase range) + `SLOPESCALEDEPTHBIAS −1`, +saved/restored exactly. **Render-verified:** standing mid-slope at the WORST measured gap spot (606,621 on +dhillg2) = shadow fully present lying on the slope + feet visible planted (was: shadow gone, feet "buried"); +moderate inclines while walking ✓; flat ground crisp, no bleed; combat un-regressed (DESTROYED after 8 hits, +0 heap detections). Note the residual ≤2.1u foot-clip at worst patches is authentic content coarseness — with +the shadow anchoring the mech it reads as standing in soft sand, matching the vids. (A whole-mech terrain-tilt +experiment was tried and REJECTED — the 1995 mech is upright-only; do not tilt the body.) `BT_CAM_Y`/`BT_CAM_Z` +(btl4vid.cpp) override the debug chase-camera offset — diagnostic for separating camera occlusion from real +geometry clipping. +**✅✅ VISUAL-GROUND CONFORM — the residual foot-clip is now GONE too (btvisgnd.cpp, PORT ADDITION, +presentation-only; gate `BT_VISUAL_GROUND` default ON).** The user (rightly) rejected "authentic ≤2.1u clip": +the resolution to "how did the devs let this slide" is **the 1995 game never rendered this view** — it was +cockpit-only (you can't see your own feet; other mechs are 100+m away = subpixel), so the artists never needed +visual-solid agreement; OUR port adds a close external camera, so the port also owns making it correct. The fix +samples the ACTUAL rendered terrain triangles under the mech and lifts the RENDER matrix to the visible surface: +at mission load, walk the map's MakeMessage stream (`mission->GetMapID()` → `FindResourceDescription` → Lock; +class-42 instances, skip includes/sky/profile/rwin backdrops), load each terrain model's LOD0 triangles via the +ENGINE `LoadBgfFile` (bgfload.h — game-side callable; indices are double-sided, 6 per tri, take first 3), cache +per-model + per-instance world XZ AABBs; per frame `BTVisualGroundLift(x,y,z)` = highest triangle surface at the +mech XZ within a LOCAL band [y−1, y+3] (band keeps a butte-base query on the skirt, not the mesa top; clamp +[−1,+3]) → `localToWorld(3,1) += lift` at the PerformAndWatch tail (mech4.cpp). **`localOrigin` is NEVER touched +— collision/aim/damage/dead-reckoning stay on the authentic solid model; next frame's drive rebuilds localToWorld, +so the lift is per-frame render-only.** Zero tuning constants — exact content geometry both ways (lift up where +visual bulges over the solid, settle down where it dips). **Verified:** flat desert lift≈0 (float epsilon — +confirms both sampler and the flat-exactness finding); full mound crossing lift tracks ±0.02..0.79u continuously; +standing + walking mid-mound = feet ON the visible sand, shadow present (the user's exact repro case); combat +un-regressed (DESTROYED after 8 hits), 0 crashes / 0 heap detections. NOTE: masters only (replicant render-conform +is an MP follow-up); needs cwd = pod BT dir (LoadBgfFile indexes VIDEO/ relative — already a runtime requirement). **⚠ ENGINE-CLASS GOTCHA +(add to the checklist):** a NEW member added to a 2007 engine class (d3d_OBJECT etc.) MUST be initialized +in EVERY ctor init-list — the debug heap fills fresh allocs 0xCDCDCDCD, so an uninitialized flag reads +TRUE (here: every object in the world drew as translucent-black "shadow" → near-black scene); and any +state the shadow/special draw path sets on the D3D device must be save/restored exactly (a leaked +`LIGHTING FALSE` + `ALPHAARG1=TFACTOR` also darkened the whole frame). Render-verified via PrintWindow +A/B on DBASE: baseline opaque black blob → dark translucent silhouette, terrain/feet visible through it. + +**✅ BACKFACE CULLING RESTORED (task #20, commit a2ed21a; `BT_CULL` env cw/ccw/off, default CW).** The +bring-up `D3DCULL_NONE` drew interior/back faces the original culled — the "dark wedge" shapes in the +distance were the INSIDES of dune/butte meshes, and standing inside a mound visual looked like being +sealed in rock. CW is the correct winding for our BGF quad triangulation `[a b c][a c d]` (CCW/none +selectable for debugging). Render-verified on dbase; combat clean. **CONTENT FACT (the "mound swallow" +investigation):** dbase's dune-mound groups (`dhillg*`) have CRESCENT-RIDGE visuals with HOLLOW interiors +but solid FULL CONES (hillg2_c.sld: 2 cones r=200/250 h=36/60) — where the visual exists it matches the +solid within ~1u (verified by triangle-exact BGF parsing vs the analytic cone), but a mech can walk +through a ridge crest into the hollow and ride the invisible cone — AUTHENTIC 1995 coarse collision, not +a port bug. Butte cliff tiers verified BLOCKING (beeline repro: 403 frame rejections, zero penetration). +Analysis tooling now permanent: `[contact]` telemetry in Mech::ProcessCollision, an INSIDE-SOLID streak +detector (BT_GROUND_LOG), `BT_GOTO="x z"` beeline harness (mech4 computes → mapper bridge consumes; the +turn-sign/convergence is rough — debug only), and the RES/map/solid scanners preserved in the scratchpad +(`resscan.py`/`mapscan.py` — worth moving to tools/). + +**✅✅ THE AUTHENTIC DPL ENVIRONMENT IS NOW HONORED — day sky + sun + haze + draw distance (task #20, +commits 5b615e7 + 9ca9a48).** The user compared the desert level to pod footage — ours was dark/muddy with +a wrong sky; the fix transformed it to match. ROOT CAUSE: the env pipeline (`DPLReadEnvironment` → +`DPLReadINIPage`, reading **`BTDPL.INI`** in the pod BT dir — set via `L4DPLCFG`) **already ran and +correctly resolved** the map+time via compare/branch (`main → dpl_defaults → des_day → dsdayclear` for +dbase/day), setting the AUTHENTIC **clip=2100**, **fog=600/2050** (0.46,0.46,0.68 haze), **ambient=0.45 0.4 +0.45**, and a **directional sun** (1.10,0.9,1.00 @ −50,10,0) — but THREE bring-up stubs clobbered it: +(1) `EnsureValidProjection` (the mission-load safety net, btstubs.cpp) forced clip far=1300, killed fog +(`currentFogNear/Far=1e9`+FOGENABLE FALSE), set WHITE ambient → now a FALLBACK that only overrides when the +env failed (`viewAngle<=0 || clipFar<=clipNear`); when valid it just re-asserts the env projection. +(2) the per-frame lighting block re-set `D3DRS_AMBIENT` to gray `0x404040` (~0.25) every frame → now +re-asserts the captured `mEnvAmbient` (from the map's `ambient=`); `BT_AMBIENT=` still overrides. +(3) `mCloudRed/Green/Blue`+`mCloudEmit*` were UNINITIALIZED in the ctor (0xCDCDCDCD garbage into the sky +material multiply) → init 1.0/0.0. **THE SKY:** the sky is a `*sky.bgf` dome (dbase=`dsky`, grass/arena= +`sky`) authored with a cloud TEXTURE (`btfx:dsky_mtl`) + white vertex colours. The original drew it in a +dedicated fullbright **PASS_SKY** (D3DRS_LIGHTING FALSE + extended fog, already present from earlier task-#20 +work), but the BGF loader route hardcoded `drawAsSky=false` (the `.sky` sidecar the `.x` path keys on doesn't +exist), so the dome fell into PASS_OPAQUE → LIT (half-dark) + FOGGED → the dark navy dome. FIX: +`LoadObjectBGF` tags any `*sky*` object `drawAsSky=true` → PASS_SKY → the cloud texture shows fullbright. +**Render-verified dbase A/B:** dark muddy + navy dome → bright warm sand + blue cloudy sky + soft horizon +haze + distant mesas (2100 far). Combat clean. **KEY DEBUG TECHNIQUE:** the env parser's `DPLReadINIPage +processing ` + `Clip/Fog/Ambient/Light` banners are gated on `[main] debug=` — copy `BTDPL.INI` → +`*DBG.INI` with `debug=True`, point `L4DPLCFG` at it, and the log shows every page visited + value set. +**STILL STUBBED (follow-ups):** the `objectpath`/`materialpath`/`texmappath` day/night PRIORITY is commented +out (`dpl_AddToObjectFilePath` calls) — the BGF loader (`bgfload.cpp`) recursively indexes the whole `VIDEO/` +tree **first-match-wins**, which happens to pick `DAY/` (alphabetically first) for day missions, so day is +accidentally correct; NIGHT/morning/evening missions would get the wrong (day) sky+textures, and the grass +map renders pale pink/purple terrain (a wrong-time or wrong material picked). Proper fix = reimplement the +path priority so the loader prefers the env's objectpath/materialpath dirs in order. +**◐ DAY/NIGHT PATH PRIORITY — infrastructure done, materials gated (commit 90a8d92).** `bgfload.cpp` now ranks +every index candidate by the INI's search dirs (most-specific-first) and keeps the best per stem; +`SetVideoPathPriority(cacheKey, dirs)` (bgf/bmf/img) is fed the accumulated objectpath/materialpath/texmappath +lists from `DPLReadEnvironment`. **BGF priority (geometry) is ON; BMF/IMG (materials/textures) are GATED +`BT_MATPRI=1`, default OFF** — a naive "prefer mat\day" over-applies to the SHARED desert terrain materials and +washes the warm day terrain to GRAY (render-verified regression on the user's dbase/day). The sky's day/night is +itself MATERIAL-driven (`btfx:dsky_mtl` in mat\day|night), so a correct night sky needs BMF priority — which +trades against the day terrain; resolving cleanly needs the terrain-material model understood (why mat\day grays +the desert — likely a shared lib whose mat\day variant differs, or a resolution-failure gray fallback). **⚠⚠ +CRITICAL GOTCHA — `/FORCE` turns an unresolved symbol into a runtime crash, NOT a link error.** `SetVideoPathPriority` +was first defined INSIDE bgfload.cpp's anonymous namespace → internal linkage → `L4VIDEO.obj`'s call was +unresolved → the BT game link (uses `/FORCE`) silently stubbed it → a call jumped to `__ImageBase` +(`0x009d0000`), a DETERMINISTIC AV in `LoadMissionImplementation` that looked like heap corruption. Fix: define +it OUTSIDE the anonymous namespace. **When a `/FORCE`-linked build crashes with a garbage call target near the +image base, grep the link log for `unresolved external` — the "successful" build is lying.** Also: `BTL4VideoRenderer` +is allocated by the GAME (btl4app.cpp:242) as a `DPLRenderer` subclass, so ANY member added to `DPLRenderer` +(L4VIDEO.h) needs the game's btl4app/btstubs/btl4vid objects recompiled — CMake header-dep tracking through the +`btbuild/fwd/*` shims is unreliable; delete stale `.obj`s if layout-mismatch corruption appears. + +**✅✅ RENDER FIDELITY — the dbase desert now matches the pod footage (task #20; a 10-agent measured +render-fidelity workflow found 6 defects, 3 fixed = the "big three").** The user compared our dbase to +authentic footage (`C:/git/original.PNG`): ours was harsh/saturated/faceted; the original is soft, pale, +low-contrast. Root causes (measured, not guessed): +1. **⭐ Mountains "inside-out/faceted" = MALFORMED GEOMETRY, not lighting (commit d189371).** `BGF_FORMAT.md` + wrongly documented **`CONNECTION_LIST` (tag 0x0047) as "one polygon per chunk, fan-triangulate"**. It is + actually a **FLAT TRIANGLE LIST** (3 indices/face). Fan-triangulating the whole chunk as one N-gon turned + buttec's 115 real triangles into 341 fan-garbage with smeared UVs + cancelled normals. FIX: group-of-3 + walk in the `!gotPconn` branch (bgfload.cpp buildPmesh + port/src/bgf.cpp). **Corpus-verified: ALL 3488 + CONN chunks in the pod GEO tree have n%3==0.** bgftest BUTTEC.BGF: 341→115 tris. Gated BT_TERRAIN_TRILIST. +2. **Horizon seam = flat sky plane truncated by the world projection (commit 2434bbc).** DSKY is a FLAT plane + at Y~110 (±6000), NOT a dome. The world far=2100 projection truncated it above the true horizon → the gap + showed the fog-colour Clear as a hard lavender band; the old sky-pass fog (near×3/far×6) left the rim + unfogged. FIX: sky-pass gets its own far=9000 projection + the SAME fog as the world (drop ×3/×6), BOTH + restored after the sky loop (mandatory — the alpha/particle/HUD passes need the world projection). Gated + BT_SKY_FAR. +3. **Sky "two-tone navy, nothing like clouds" = MODULATE vs SCREEN (commit 2a8aa37).** The cloud art (bintA) + is a GRAYSCALE intensity map; the sky colour is the material tint (0.3,0.5,1.0) baked into the vertex + diffuse. MODULATE (texel×tint) → saturated navy. The original SCREENed toward white: + `screen(D,T)=T+D·(1-T)=lerp(D,white,T)` → bright texels near-white, dark → pale tint. FIX: one texture + stage `D3DTOP_LERP(Arg0=TEXTURE, Arg1=TFACTOR=white, Arg2=DIFFUSE)`, states saved/restored around the sky + draw. Measured `screen(0.3,0.5,1.0,0.85)=(228,236,255)` ≈ original top `(229,230,255)`. Gated BT_SKY_SCREEN. +**THE AUTHENTIC IG-BOARD SHADING MODEL (from libDPL DPLTYPES.H:189-199):** shading is selected PER-GEOMETRY by +vertex type — no-normal geometry (XYZ_UV / XYZ_RGBA_UV: mesas/ground/sky/buildings/mech, all with WHITE baked +verts) is drawn UNLIT with the texture colorized by the material's 2-endpoint RAMP (`dpl_SetMaterialRamp`, +texture luminance indexes a low→high gradient — rock (0.25,0.21,0.16)→(0.8,0.5,0.4) = warm tan); only ~150 +vehicle/missile files (XYZ_N_UV) carry real normals and were lit by the single map light. +4. **⭐ RAMP colorize + day/night material priority — DONE (commits d5c155b, cc0055a); the ground/mountain + colour match + sky whites.** The ground looked PALER than the mountains with a seam because they use + different material libs: the mountains' `ravinerock_mtl` has a warm diffuse (0.781,0.525,0.353) so they + render warm, but the ground's `grass_mtl` has NO diffuse — it relied ENTIRELY on its colour RAMP, which we + didn't apply → gray placeholder = the pale ground. The authentic IG board colours terrain by the material's + **2-endpoint RAMP** (`dpl_SetMaterialRamp`): texture luminance indexes a low→high gradient. Implemented: + `bgfload` parses the ramp def (0x30 name + 0x31 6-floats) + the material's ramp ref (0x28), threads + rampLo/Hi onto `BgfDrawBatch`→drawOp; `L4D3D` bakes `lerp(rampLo,rampHi,texLum)` into the texture at load + and draws ramped batches with a **WHITE material + WHITE vertex colour** (so the ramp colour shows untinted + — critical: a leftover tint MODULATEs the ramp again). **DAY/NIGHT MATERIAL PRIORITY (bmf/img) is now + DEFAULT ON**: the mat\day libs carry the AUTHENTIC warm ramps (rock 0.25,0.21,0.16→0.8,0.5,0.4 = warm tan; + grass →warm brown); first-match had picked the generic GEO/*.BMF whose ramps are GRAY (0.8,0.75,0.75) — + which only looked warm before because the vertex tint covered for it. The earlier "BT_MATPRI grays the + terrain" regression is GONE precisely because the ramp is now applied. **SKY via its own ramp**: dsky_mtl + has a 'sky' ramp (0,0,0.6→0.99,0.99,0.99) → bright cloud texels → 0.99 near-white = the "whites" the user + wanted; `BT_SKY_SCREEN` (the pre-ramp heuristic) now default OFF. **Render-verified ALL DEFAULTS**: dbase/day + = pale pastel sky + soft white clouds + uniform warm tan ground matching the warm mesas = the pod footage; + dbase/night = dark twilight + warm-lit red mesas; grass = warm brown; combat un-regressed. Gates: + BT_RAMP / BT_MATPRI / BT_SKY_SCREEN. +5. **UNLIT no-normal geometry (STEP 5) — NOT done, and no longer needed.** Since STEP 1 fixed the geometry + + the ramp gives correct colour, the mesas/terrain look natural lit; the residual faceting is gone. Deferred + (⚠ per-drawOp NOT per-object — APC/HUMMER mix XYZ_N_UV + XYZ_UV; ⚠ 0xCD ctor-init gotcha; ⚠ would change the + mech to unlit). Full spec: scratchpad rf_plan.md/rf_verdicts.md; the workflow reports are the durable cites. +6. **⭐ RAMP over-applied to LIT vehicle geometry → "whitish-tan blobs" — FIXED (bgfload.cpp `hasNormals` gate).** + The user saw the map's scattered vehicles (hummer/semi/dozer/apc…) render as uniform dusty-white blobs. ROOT + CAUSE: the vehicles' shared `basev` material library (`VIDEO/MAT/BASEV.BMF`) gives every material a REAL + diffuse colour (bv1_mtl tan 0.66,0.47,0.30; bv1gry gray 0.41; bv5dkgrn green; bv5red; bv1ylw; blk) PLUS a + ramp ref (tag 0x28) to the `cdusty` ramp (0.33,0.27,0.22→0.90,0.85,0.80 = dark-brown→near-white) over the + shared atlas `BASEV.BSL`. Our task-#20 ramp path set `batch.hasRamp` purely from the material, with **no + vertex-type gate** → it ramped the vehicles too, baking cdusty over BASEV.BSL + forcing a WHITE material → + every vehicle washed to the same dusty-white, killing its diffuse colour. But the authentic IG model + (DPLTYPES.H) selects shading BY VERTEX TYPE: **normal-bearing geometry (vehicles/missiles, `XYZ_N_UV`/ + `XYZ_N`) is LIT and shows diffuse×texture; the RAMP is a NO-NORMAL feature only** (terrain/mesas/sky/ + buildings/mech). Vehicles carry `XYZ_N_UV` (SEMI = 400 XYZ_N_UV + 248 XYZ_RGBA_UV). FIX: `bool hasNormals + (id)` (0x81/0x89) + `useRamp = currentHasRamp && !hasNormals(pmeshVtag)` per-PMESH in `buildPmesh` — replaces + `currentHasRamp` for the vertex colour + `batch.hasRamp`/`batch.color`. Now normal-bearing batches keep their + diffuse colour + BASEV.BSL texture (colored trucks); no-normal batches (incl. the vehicles' own no-normal + detail parts) still ramp. Per-PMESH so a mixed-vertex-type model splits correctly. **Verified A/B (zoomed + distant-vehicle crop):** before = gray/white blobs → after = gray trucks + tan/brown cargo + brown container, + distinct materials; terrain ramp un-regressed; combat un-regressed (TARGET DESTROYED after 8 hits); no crash / + 0 heap detections under BT_HEAPCHECK. NOTE: not separately env-gated — it's a correctness fix inside the + existing BT_RAMP system (BT_RAMP=0 still disables all ramping in L4D3D). **CORRECTION to an in-chat claim: + the vehicles are ALREADY per-light shaded once un-ramped — the loader is NOT missing normals.** `buildPmesh` + pushes 0 into nx/ny/nz only as INITIALISATION; `emitTri` then accumulates the forward face normal into each + vertex (bgfload.cpp:442-444) and the finalise pass normalizes them (`:597-601`) = smooth per-vertex normals + for ALL geometry. Lighting is globally ON whenever the map ships a light: `DPLReadINIPage` parses the env + page's `light` entry (BTDPL.INI, e.g. dbase/day `dsdayclear`) into `sceneLight[]` as a `D3DLIGHT_DIRECTIONAL` + sun (`L4VIDEO.cpp:3086` SetLight+LightEnable), and the frame setup (`:6196-6233`) sets `D3DRS_LIGHTING TRUE` + + `NORMALIZENORMALS` + `COLORVERTEX` + diffuse/ambient from vertex COLOR1 when `sceneLightCount>0`. Non-ramp + batches get material Diffuse=bv color, Emissive=0 (L4D3D.cpp:303-310), so a vehicle renders + `texture × [ambient·color + sun·(N·L)·color]` = real directional shading (render-verified: a gray tanker + shows a bright sun-lit top, dark shadowed side faces, smooth gradients — a flat render would be uniform). + The ramp-white was the ONLY thing suppressing it; no separate lighting work was needed. (STEP 5 — making + no-normal geometry authentically UNLIT — is the OPPOSITE change and still "not needed": global lighting on + the ramped terrain/mesas/mech looks correct vs the footage.) + +**✅ WEAPON FIRE IS NOW VISIBLE — the muzzle→hit BEAM renders + fires on the trigger, target or not.** +The user pressed SPACE and saw nothing: (1) the whole fire path was gated `if (gEnemyMech != 0)` (the +BT_SPAWN_ENEMY test dummy) → in solo there was NO target so the trigger ran no fire code; (2) even against +the dummy the only visual was an Explosion at the target — the `dpl_*` weapon-beam renderable was never +ported. FIX (two parts): **(a) a self-contained additive-quad BEAM renderer in the engine** — +`BTPushBeam(from,to,color,ttl,width)` queues a world segment, `BTDrawBeams(dev,view,dt)` (L4VIDEO.cpp, +called in the alpha pass so beams are Z-occluded by terrain) billboards each as a camera-facing additive +quad and fades it; no content needed (fills the unported dpl_ layer). **(b) a visual-fire block in +`Mech::PerformAndWatch` (mech4.cpp) that runs ALWAYS** — computes the muzzle (origin + kMuzzleHeight 7u / +kMuzzleForward 3u) + the mech's −Z forward, picks the aim = the locked enemy (torso) OR the crosshair +raycast to terrain (`BTGroundRayHit`, btvisgnd.cpp, else muzzle+fwd*range), and on the trigger (own cooldown +`gBeamCooldown`) pushes the bolt = **red-orange glow (w6) + white-hot core (w1.6) + muzzle flash** and (solo) +an impact Explosion at the hit. The existing enemy-damage block is UNCHANGED and still runs below (beam → +"target" while locked, then "free" after the kill). **Render-verified:** solo desert — a bright white-cored +laser fires forward to the aim point; combat — `[fire] BEAM -> target` while the dummy is locked, structure +0.8→1.0, **TARGET DESTROYED after 8 hits**, then "free" beams post-kill; 0 crashes / 0 heap detections under +BT_HEAPCHECK. SPACE/Ctrl fire the beam interactively (btl4main VK_SPACE→gBTDrive.fire). +**✅ AUTHENTIC MUZZLES — the beams now fire from the mech's REAL gun-port SITE segments (arm guns).** The +BLH.SKL has arm-gun joints `jointrgun`/`jointlgun` with muzzle site segments `siter/lu/dgunport` + +`sitel/lu/dgunport` (upper/down/back ports per arm). The fire block (mech4.cpp) resolves them by name each +frame — `GetSegment(CString("siterugunport"))` → `EntitySegment::GetSegmentToEntity()` (segment→entity +LinearMatrix) → `AffineMatrix::Multiply(segToEntity, localToWorld)` → `Point3D = matrix` (extracts the W_Axis +translation) = the world muzzle — and fires one bolt per resolved port, all converging on the aim point (falls +back to a centre muzzle if none resolve). **Verified:** muzzle logged at (−2.6,+4.4,+2.7) off the origin = +the arm gun, NOT the centre; render shows TWO beams emerging from the left+right arm guns in a converging V, +with muzzle flashes at each gun, tracking the arms; combat un-regressed (DESTROYED after 8 hits), 0 crashes / +0 heap detections. The beams are now RED LASER (glow 0x00E01808 + white-hot core), authentic for the all-laser BLH (Nova). +**◐ TIE-TO-REAL-FIRE — TRIED, REVERTED (over-saturates; needs a beam throttle).** The authentic next step is +driving the beam from each weapon's OWN fire: `Emitter::FireWeapon` has `owner` (Mech*), `muzzlePoint`, +`targetPoint`, `dischargeTime`, and `damageData.damageType` all in scope, so pushing `BTPushBeam` there +(muzzle from `owner->GetSegment(segmentReference)`→segmentToEntity×localToWorld, coloured by damageType: +Laser red / Energy blue / Ballistic amber) gives real rate + heat + per-type colour, and it BUILT + ran +(combat: `[emitter] FIRED heat=0.239`, DESTROYED-in-8). BUT the BLH is a ~12-laser ALPHA STRIKE and +`FireWeapon` runs ~16×/s across the roster; a persistent (`dischargeTime`) additive beam pushed per fire +ACCUMULATES into a white screen-flood at point-blank. So `Emitter::FireWeapon` no longer draws (kept the +mech4 controlled 0.8s salvo). To revive: throttle to ONE beam per weapon per discharge (push only on the +fire EDGE, `dischargeTimer` just reset) + short TTL (no per-frame stacking) + dim per-beam / alpha-blend the +glow so N overlapping beams don't saturate. `simulationFlags & 0x8` is NOT a safe weapon discriminator +(bit 3 is in the engine host/replicant `flags & 0xc` mask) — enumerate weapons via the factory class IDs, +not the flag. **✅ FIRE RATE — grounded on the recovered weapon data (was a 0.8s bring-up guess).** The user noticed the +laser cadence looked too slow vs the pod videos — correct, 0.8s was arbitrary. The real laser params +(`ERMLASER.SUB`/`ERLLASER.SUB`/... — the BLH is ER-medium): **`DischargeTime=0.2`** (beam-on), `WeaponRange=500`, +`DamageAmount=3.5`, `PipColor=1.0 0.0 0.0` (RED, confirms the laser colour), `RechargeRate` scales with size +(ER-S 1.0 / ER-M 2.0 / ER-L 4.0 / PPC 5.0). Applied: beam-on = `kBeamOnTime=0.2` (DischargeTime, recovered +hard value); cadence `kFireCooldown=0.3` (0.2 + short recharge) → ~62% beam duty cycle, snappy rapid fire vs +the old ~15%. **✅✅ RECHARGE CADENCE — FULLY DECODED from the binary (RechargeRate IS the recharge in seconds).** PE-parsed +the charge-curve constants from BTL4OPT.EXE (`.data` at the `_DAT_` VAs, read as the x87 80-bit `float10` the +decomp uses, NOT 32-bit float): **C1=`_DAT_004bb3c4`=1.0, C2=`_DAT_004bb3b8`=1e-4, `_DAT_004bb3b4`=0.5** (the +energyCoefficient ½). The Emitter ctor (raw part_013.c:7899-7906) computes `energyCoefficient = energyTotal/ +(0.5·seekV²)` and `energyRampTime = rechargeRate / (−VoltageCurve(C1−C2·seekV)) / energyCoefficient`; the +per-frame charge (`FUN_004b0d50` dtScale × `TrackSeekVoltage`) is an exponential approach toward the source +voltage with time-constant τ = dtScale·energyCoefficient, and the fire threshold is `seekVoltage[rec] = +0.8·RatedVoltage`. **The constants make it cancel exactly:** seekV = 0.8·RatedVoltage, RatedVoltage +(GENRATOR.SUB) = 1e4 → seekV = 8000 → curve arg = `1 − 1e-4·8000` = 0.2 → `VoltageCurve=ln` (must be, for +`energyRampTime>0`) → `−ln(0.2) = ln5 = 1.609`; and the exponential charge from 0 to 0.8·srcV also takes +`ln(1/0.2) = ln5`. The two ln5 factors cancel ⇒ **recharge time = RechargeRate seconds exactly** (ER-S 1.0 / +ER-M 2.0 / ER-L 4.0 / PPC 5.0). So one laser's full cycle = `DischargeTime + RechargeRate` = **0.2 + 2.0 = +2.2s** for the BLH's ER-mediums — much SLOWER per weapon than any guess; the fast on-screen fire is the ~12 +lasers firing STAGGERED (chain), aggregate ≈ 12/2.2 ≈ 5/s. Applied (mech4.cpp): `kPortRecharge=2.2`, fire ONE +gun port per stagger tick (`kPortRecharge/nMuz`) rotating through the 6 arm ports (added siter/lbgunport) → +a rapid alternating single-beam stream, each port honouring the 2.2s recharge (render-verified: single red +bolt per frame, rotating ports; combat DESTROYED after 8 hits, 0 crashes / 0 heap). ⚠ ASSUMPTION: that the +game STAGGERS rather than synced-volleys (a synced alpha strike would flash every 2.2s = slow, contradicting +the pod footage) — the per-weapon 2.2s is decoded-exact; the stagger *pattern* + exact weapon→port count is +the estimate (needs the weapon enumeration via factory class IDs, not simulationFlags). +**✅✅ BEAM APPEARANCE — DECODED from the assets (the "gritty" look is a real scrolling texture, NOT invented).** +The user noted the lasers looked too clean. Decoded the real beam: the weapon resource names its beam model +(`VideoObjectName=ermlaser.bgf`, `GraphicLength=2000`; `beamLengthRatio=dist/2000` scales it). `ermlaser.bgf` += a thin TUBE (89 verts, Z 0..−2000) with UVs tiled ~8× down its length, material `btfx:beamwhite_mtl` (core) ++ `btfx:beamred_mtl` (glow). The GRIT is a SCROLLING TEXTURE: **`VIDEO/MAT/DAY/BTFX.VMF` (DIV-VIZ2 source)** +defines `TEXTURE(NAME="beamwhite_scr_tex"; SPECIAL="SCROLL 0.0 0.0 0.10 9.5") { MAP {"bexp"} }` — i.e. the +core samples the **`bexp` image = `VIDEO/TEX/BEXP.BSL`** (a 128×64 chaotic red/yellow/green noise) scrolled +FAST along the beam (u=0.10, v=9.5), ramp-colourised by `softer` (0.25→0.99) to a white core; the glow is +solid red (`fiery` ramp). ⚠ **I FIRST WRONGLY SAID THE TEXTURE WAS MISSING** — it is present; I'd missed the +`MAP{}` directive in the `.VMF` (the `.BMF` only names the texture, the `.VMF` binds it to the image + carries +the SCROLL). **THE `.VMF` IS THE KEY EFFECTS ASSET** (scroll speeds for beams/muzzle/smoke/sky: beamwhite v=9.5, +beamyellow v=−5.3, beamgreen v=−1.3, muzinner/muzouter, firesmoke, sky). Beam COLOR is per-weapon via the beam +model: ER lasers→ermlaser(RED), standard lasers→l/m/slaser(YELLOW), PPC→ppc.bgf(BLUE), AC/Gauss→ballistic +(no beam), missiles→projectiles. Implemented (L4VIDEO.cpp BTDrawBeams; gate BT_BEAM_TEX): load BEXP.BSL once, +ramp its luminance by `softer` → grayscale grit texture, MODULATE the beam colour by it, tile U down the +length + scroll (u=0.10/s, v=9.5/s) → the streaming gritty beam. Render-verified: the beam shows banded/streaky +grit (was a clean gradient); combat un-regressed (DESTROYED after 8), 0 crashes / 0 heap detections. +**✅ THE REAL `ermlaser.bgf` TUBE NOW RENDERS (replaced the camera-facing billboard quad; gate `BT_BEAM_TUBE`, +default ON).** `BTDrawBeams` (L4VIDEO.cpp) loads the tube geometry ONCE (`LoadBgfFile("ermlaser.bgf")` → static +`s_tubeVB`/`s_tubeIB`, x,y,z,u,v per vert) and per beam builds a world transform `Scale(w,w,beamLen/2000) × +Rotate(local −Z → muzzle→target dir) × Translate(muzzle)` — the tube's native Z 0..−2000 maps onto the shot, +its baked UVs (U 0..7.73 tiled down the length) carry the scrolling `bexp` grit via a `D3DTS_TEXTURE0` +transform (`D3DTTFF_COUNT2`, `_31=uScroll _32=vScroll`), beam colour from `D3DRS_TEXTUREFACTOR` (the tube verts +carry no diffuse — FVF `XYZ|TEX1`, COLORARG2/ALPHAARG1 = `TFACTOR`; the billboard path stays `XYZ|DIFFUSE|TEX1` +with `DIFFUSE`, kept as the `BT_BEAM_TUBE=0` fallback). Native tube is ~0.3u wide so a width scale is applied: +`BT_BEAM_WIDTH` (default **3.0** → ~0.9u; the width scaling ONLY affects the tube path, which is how a run was +confirmed to use the tube not the billboard). All touched state saved/restored (TEXTUREFACTOR, +TEXTURETRANSFORMFLAGS, D3DTS_TEXTURE0). **Render-verified** (dbase/grass, `BT_FORCE_FIRE`+`BT_SPAWN_ENEMY`): a +clean 3D volumetric bolt from the arm-gun muzzle to the target, staggered single-port fire, scene un-regressed, +7+ shots, 0 crashes. ⚠ **TEST-HARNESS GOTCHA (cost an hour — not a game bug):** the env gates check +`getenv(name)` for NON-NULL, so **`$env:BT_HEAPCHECK=""` (empty string) still ENABLES heapcheck** — every run +after I first set it crawled through `CrtCheckMemory` on each string alloc in the material `scanDir` (O(n²) +during mission BGF indexing) and looked like a mission-load HANG at `[zonebuild]` (`Responding:False`). cdb on +the "hung" process showed the main thread live in `ucrtbased!CrtCheckMemory` ← `scanDir`/`bmfIndex` ← +`LoadBgfFile` ← `DPLReadINIPage`, not stuck. FIX: `Remove-Item Env:\BT_HEAPCHECK` to actually clear it (setting +`=""` does not). Applies to ALL `BTEnvOn`-style gates. +**✅✅ AUTHENTIC DISTANCE LODs — "first LOD only" was WRONG; the arena "structures not rendering fully" bug +(bgfload.cpp + L4D3D + L4VIDEO).** The user found arena1 structures rendering as floating fragments (poles/ +letters/rails with no massing). Triage ruled out the CONN fix (A/B: 28 vs 29 tris on AR03, identical) and +load failures (none). ROOT CAUSE: **each BGF LOD chunk carries a `0x2046` header = [near..far) VIEWING BAND** +(AD01: [0..57.7]=close-up 3D logo letters, [57.7..231]=full billboard, [231..1155]=far; AB07: near band = a +sliver+pole DETAIL SUBSET, [115..1155] = the whole building) — the IG board selected the LOD whose band +contains the camera distance. Our loader always took the FIRST LOD; for classic reduce-LODs (calpb 152→16 +verts, dhillg, buttes) first==finest so it LOOKED right, but the arena's composite structures author their +massing in the FAR bands → only near-detail fragments drew. FIX: the loader collects EVERY LOD tagging +batches with the band (BgfDrawBatch/L4DRAWOP lodNear/lodFar); DrawMesh selects per draw-op by +|object − camera| (camera published per frame via `d3d_OBJECT::SetCameraPosition`, eye = −t·Rᵀ from the view +matrix). **CONSOLIDATION IS NOW DEFAULT OFF (`BT_CONSOL=1` re-enables)** — merged ops combine many instances ++ LODs of one material, so per-op bands can't survive the merge (a consolidated run would draw EVERY LOD +simultaneously). Frustum culling + HWVP carry the cost — measured FASTER than consolidation: dbase combat +avg 1.05ms, 79 batches (distant objects draw their small far-LODs). VERIFIED: arena renders a coherent +scene (continuous perimeter wall, full buildings, no floaters); dbase terrain/buttes unchanged; combat +TARGET DESTROYED after 8 hits, 0 crashes. Viewer: `BT_LOD_INDEX=n` inspects a specific LOD. FOLLOW-UP: a +few PINK horizon patches on dbase = far-LOD materials that never resolved before (their libs never loaded); +minor. ⚠ LESSON: "first LOD" was validated only on reduce-style models — a format assumption verified on a +subset can still be wrong for another authoring style; the 0x2046 header was readable all along. +**⚠⚠ LOD-DISTANCE BUGS FIXED THE NEXT SESSION (user: "mech disappearing/reappearing with viewing angle", +then "scenery still blinks"):** (1) the CAMERA EXTRACTION from the view matrix was TRANSPOSED — the D3D +row-vector LookAt stores the camera basis in COLUMNS (xaxis=(_11,_21,_31); eye.x = −(t·col1) = +−(_41·_11+_42·_12+_43·_13)); using ROWS gave a pseudo-position that VARIED WITH THE VIEW ANGLE, so LOD +bands popped in/out as the camera rotated (structures AND the mech blinked). (2) the METRIC IS THE INSTANCE +ORIGIN distance — a bounding-sphere-SURFACE metric (tried briefly, blamed for a floor vanish that was +really bug #1) subtracts each component's OWN radius, smearing the COORDINATED band handoffs of composite +structures (ADWLK1 ends [..115] exactly where AB07's far band [115..] takes over) by 20–50u per piece → +holes/doubles the orbiting chase camera swept pieces in and out of = the scenery blinking. Floor slabs' +single bands are wide (ctarmac [0..1155], dtarmac [0..1039], afloor [0..577]) — origin distance keeps them +visible across the play space; LODing out beyond that range (far outside the arena) is authentic. +`BT_CULL_LOG=1` dumps big culled objects (bounds sanity). Verified: 10-frame full-rotation sweep = mech + +floor + scenery stable (2 frames camera-inside-a-wall, expected); dbase combat DESTROYED after 8 hits. +⚠ LESSON: changing two things at once (camera fix + metric) attributed the floor vanish to the wrong one — +the sphere metric was never needed. +**⚠⚠ RUNTIME LOD SELECTION — REVERTED TO EXPERIMENTAL (`BT_LODSEL=1`); DEFAULT = SHIPPING-ENGINE BEHAVIOR.** +Even with the correct camera + origin metric + the authentic mech-eyepoint reference (fed via `BTSetLodEye`, +kept wired), runtime band selection still broke arena content (floor vanished when the mech wandered >577u +from `afloor`'s origin — impossible in the real game, so the 1995 semantics for huge grounds involve +something not yet decoded: per-LOD reference points (`dpfGetLODReference`/`dpl_SetObjectLodHotSpot` exist in +the libDPL API!), the unparsed class-0 arena map records, or ground-type exemptions). **DEFINITIVE REFERENCE +FOUND: `DivLoader/VGCDivLoader.cpp parseLOD` — the SHIPPING 2007 engine keeps ONLY the nearest-band LOD +(`if (in < 5.0f) parse = true`) and does NO runtime LOD selection at all** — i.e. our original "first LOD" +behavior IS what Red Planet shipped with; the arena-composite fragments exist in the 2007 engine too. +DEFAULT restored to that (first LOD + consolidation ON; `BT_LODSEL=1` switches to the experimental full-LOD +path and auto-disables consolidation). TO FINISH PROPERLY: decompile BTL4OPT.EXE's own LOD path (the 1995 +software renderer's selection incl. hot-spots) instead of guessing — then the arena backdrop can be made +authentic. All the plumbing (bands parsed+tagged, per-op DrawMesh test, eyepoint feed) is in place and gated. +**✅✅ ADDITIVE_LODS DECODED — the arena "fragment structures" FIXED, authentically (default ON, gate +`BT_ADDLOD=0`).** The follow-up research cracked it WITHOUT decompiling the binary: **the 1995 +`MUNGA_L4/L4VIDEO.CPP` `TestSpecialCallBack` (~line 486) reads the OBJECT-level SV_SPECIAL (0x2037) token +`ADDITIVE_LODS` from the BGF itself and calls `dpl_SetObjectAdditiveLODs`** (DPL_VPX.H). **135 pod BGFs +carry the token** — ALL arena structures (AB*/AD*/AR*/AW*), the PGN calliope-turret family, buildings +(HUT/TWR/BNK/JAX/FT/HN), vehicle props (TRK*), and EVERY `*D` mech damage model. Corpus sweep (841 GEO +BGFs): additive objects author their LODs as a clean PARTITION of [0..outmax) with COMPLEMENTARY geometry +(AW01: LOD1 = upper structure, LOD2 = base — different regions, not nested representations) — so +replacement-selection is provably WRONG for them (one complementary piece at any distance) and +first-LOD-only shows just the near-detail piece = the "floating fragments". **THE RULE: additive object → +at eye distance d, EVERY LOD with `d < OutDist` draws (InDist ignored; near detail ADDS onto the coarser +massing and drops first as you retreat).** One corpus quirk: MSLG.BGF has an inverted band → clamp +max(in,out). Implementation: `bgfload.cpp` TAG_OBJECT parses 0x2037 → additive objects collect ALL LODs, +each batch banded [0..OutDist); non-additive keep shipping first-LOD-only (mech/terrain/afloor → zero +regression risk); `L4VIDEO.cpp RecurseStaticObject` EXCLUDES banded objects from static consolidation +(merged runs can't band-gate) — they render individually via the per-op DrawMesh distance test (eye = +the `BTSetLodEye` mech feed). **Verified:** arena1 spawn A/B — `BT_ADDLOD=0` reproduces the user's exact +floating-panels bug AT THE SPAWN (the spawn sits inside a dropzone gate whose massing never drew!), +default ON = the complete bay/gate + walkway, coherent structures at all ranges; consol srcMeshes 255→152 +(103 additive instances excluded); grass combat TARGET DESTROYED, 0 crashes; cavern + dbase identical to +baseline. NOTE: the arena spawn's debug CHASE camera now starts inside the (correctly solid) bay wall — +the pod's cockpit camera sat at the mech's head so the original never saw this; drive forward to exit. +**✅✅✅ THE FULL LOD DECODE — √3 RANGES + ORIGIN METRIC + PUNCH CUTOUTS (the 6-agent `lod-blink-decode` +workflow; closes the arena "blinks with movement" + "noisy unstable surfaces" reports).** +1. **⭐ THE √3 RANGE DECODE (corpus-exact):** every stored 0x2046 band value is the AUTHORED euclidean + distance ÷ √3 — 92% of the corpus' 3263 nonzero band slots are EXACTLY nice-number/√3 to 6 significant + figures, with hand-conversion TYPOS as proof (BUTTEB 2000.73 for 2020.73=3500/√3; MSLG's inverted band + = the author typing 10 for 100). afloor 577.35 = authored **1000** (the historic floor-vanish + explained); ADWLK1 115.47 = 200; the arena massing 1154.7 = **2000** ≈ des_day clip 2100. Comparing + plain euclidean eye distance against stored values made EVERY piece switch at 57.7% of its authored + range — inside the arena's clear air (fog 200→1250, clip 1300 [ardayclear]) = the position-dependent + blinking. FIX: `bgfload.cpp` multiplies parsed bands by √3 (`kLodRangeScale=1.7320508`). With it, NO + massing pops in-play (2000 ≥ the arena's max reachable 1972; 54 of 104 popping instances — ALL + walls/buildings/gates — stop popping; the remaining are authored detail dropouts at 400-900u, 19-66% + fogged). A disjoint 7.45% RAW-integer band population exists (one prop-family artist skipped the + conversion — GD/RB/WL/SW/SB/TK1 etc), 100% NON-additive → unaffected today. +2. **METRIC = the instance-transformed OBJECT HOT SPOT (default = origin), ONE shared d for all LODs of + an object** (`s_dplobject.lod_hot_spot`; InDist doesn't even survive to the runtime — the board keeps + only `lod_ranges[16]` switch-out floats, which STRUCTURALLY confirms the cumulative rule). The + bounding-sphere-SURFACE metric (previous commit) was UNAUTHENTIC — per-piece radius offsets desync + sibling bands (the OpenFlight "use previous slant range" contract: siblings must range against the + SAME point). Reverted to origin distance in DrawMesh. +3. **✅ PUNCH cutouts (dpl_Punchize) implemented** — PATCH-level SV_SPECIAL `PUNCH` (1995 + TestSpecialCallBack L4VIDEO.CPP:563-569) marks cutout geogroups (arena scaffold layers AR01-04, + gratings): black texels = HOLES. The port drew them SOLID, shingled 0.05-0.5u over the wall massing = + near-coplanar depth shimmer in motion (the "noisy unstable surfaces"). Now: loader tags batches + (bgfload punch flag) → texture loads with alpha-0 on near-black texels → DrawMesh alpha-TESTS punch + ops in the opaque pass (ALPHAREF 0x80, z-write preserved, states restored). Gate `BT_PUNCH=0`. + Render-verified: the catwalk rails/barriers draw as see-through lattice. + **Corpus classification (the same workflow):** additive LODs are overwhelmingly COMPLEMENTARY + (families: detail+massing 54, progressive layered series 49 [AR01 = trim→scaffold→scaffold+panels→ + SOLID WALL, disjoint layers, correct under cumulative], single-detail 9, mixed 23); real coplanar + DOUBLED overlap only in props (TRK1/3/4 grille detail flush on the body) + mech damage arms — those + carry `GEOMETRIZE 0x8000xxxx` hints (decal/priority, unimplemented lead), authentic-coexisting on the + board too, left as-is. + **Other decoded facts:** map class-0 records = INCLUDE directives (`mid==3` + type-14 target; + arena1→{arenall→cavern, artrucks} — the arena sits INSIDE the cavern map; class-48 = named dropzones); + NO 0x2047/0x2048 exists anywhere in the corpus; Division dVS has no additive concept (VPX-local + extension); OpenFlight LOD semantics (OGC 19-065) match the tag set 1:1 (reference-point ranging, + transition ranges) — the reference for future replacement-LOD work. + **Diagnostics kept (permanent):** `BT_LOD_LOG=1` → `[lodband]` per-model banded-op census at load, + `[lodeye]` eye-feed trace, `[lodflip]` per-op band-visibility FLIP events with model name + band + d + (how the oscillation was caught: awbox1 flipping IN 3× at d≈461 vs edge 461.88). + Verified: arena traverse = no massing flips (only authored detail handoffs at 400-900u under fog); + walk-out renders banners + see-through rails + complete walls; grass combat TARGET DESTROYED, 0 + crashes; dbase/cavern identical; avg frame 4.5ms. +**✅✅ THE COPLANAR-SHELL DECODE — the "extremely unstable walls" (noise/diagonal/dot patterns crawling +with movement, stable parked) ROOT-CAUSED + FIXED (submission-order depth bias; commit bcc9210).** +Diagnosis chain (each step eliminated a hypothesis): two stills at one position = pixel-identical +(pattern is a pure function of camera position); layer-isolation imaging (`BT_ONLY_LOD=k`) showed the +dot lattice INSIDE A SINGLE LOD layer (not layer-vs-layer); `BT_PUNCH=0` left it unchanged; analytic +geometry (AR02 LOD2) found the truth: **the content layers EXACTLY-COPLANAR full shells inside one LOD** +— a solid concrete shell + a coplanar PUNCH-cutout overlay + an inner shell, plane separations measured +0.000–0.003u. The Division board composited coplanar polygons DETERMINISTICALLY (exact per-polygon plane +equations → identical depth → later-submitted wins via less-equal); D3D9 interpolates depth from +vertices, so two tessellations of one plane differ by rounding PER PIXEL = crawling interference at ANY +distance (near-plane precision, per-LOD bias, punch alpha all irrelevant — hence the failed attempts). +**FIX = reproduce the board's rule:** every batch of an additive object is depth-biased ~4 LSB of D24 +CLOSER per submission ordinal (file order = authored order; `2.5e-7` NDC/step, ordinal capped 20, +`BT_LAYER_BIAS` overrides) — coplanar contests resolve to the overlay patch at all distances. +Verified by bias on/off A/B at a teleport-reproduced viewpoint. **Also: punch cutout mips are built +MANUALLY with alpha RE-BINARIZED per level** (box-filtered alpha hovers at the test threshold → texels +flicker as movement shifts the mip level). **And `BT_LOD_SCALE` is BLANKET again (commit 1bea626):** +the outermost-only restriction guarded against extending shingled detail into z-fight range — obsolete +under the bias — so inner detail (wall-top caps, authored 800) now holds to 2000 = zero wall-detail pops +in-arena (traverse flip census 104 → 7, remaining = two small pieces at 375–500u under fog). +**HARNESSES (permanent):** `BT_SPAWN_AT="x z [hdg]"` teleports the player on the first driven frame for +exact-viewpoint A/B repro — MUST set the orientation QUATERNION (`localOrigin.angularPosition = +EulerAngles(0,h,0)`), not just the scalar `gDriveHeading` mirror (the chase camera derives from the +quat; without it the view keeps the random dropzone's facing). Teleporting INTO a dune can leave the +mech sunk (ground probe from inside a solid) — cosmetic, walking recovers. `BT_ONLY_LOD=k` = additive +layer-isolation imaging. ⚠ **GOTCHA (cost a loop): `bgfload.cpp` builds into the ENGINE lib** — a +game-only rebuild silently ships the stale loader; grep the exe for a new string literal when in doubt. +Remaining authentic-LOD work: replacement selection for NON-additive multi-LOD models (detail pop at +range — needs the board's hot-spot/reference semantics; leads catalogued: `s_dplobject.lod_ranges[16]/ +lod_hot_spot/lod_fade_range` in DPL_PRIV.H, per-LOD 0x2047 reference points, 0x2048 transition snap|blend, +`dpl_SetLodStress` perf governor, `dpl_SetInstanceForceLOD`). **RESEARCH TECHNIQUE (reusable): +`LIBDPL.LIB` is an OMF library with full symbol names — parse THEADR/PUBDEF records (python ~40 lines) to +map every `dpl_*` entry point; the 1995 DOS `MUNGA_L4` HAL source is the game-side caller reference; +`dsys/PFBIZTAG.H` names every BGF tag (0x2046=LOD_DISTANCE, 0x2047=LOD_REFERENCE, 0x2048=LOD_TRANSITION, +0x2037=SV_SPECIAL).** + +**✅✅ THE RAINBOW/GRAFFITI MECH SKINS — THE BSL BIT-SLICE FORMAT DECODED + FIXED (user: "are the mechs +colored accurately? they all seem to have a rainbow/graffiti motif" — they were not; every mech now renders +its authentic gray-metal paint).** A 4-agent workflow (BSL corpus census / format ground truth / our-loader +audit / BMF material spec) + hands-on. ROOT CAUSE: `.BSL` is NOT one RGBA image — it is a **bit-sliced +container**: w*h 32-bit texel words, byte 0 pad, the other 3 bytes = SIX independent **4-bit grayscale +sub-images** packed 2-per-byte with the nibble pair SWAPPED (even slice = HIGH nibble; +`shift=(c+((c+1)%2)*2)*4`), plus truecolor sliceTypes 7=RGB444/8=RGBA4444 (r=s5,g=s4,b=s3,a=s2) that COEXIST +with mono slices in one file. BLKHWK.BSL holds the Blackhawk's 4 skin sheets this way; our decoder read +bytes[1..3] as RGB = 2-3 different gray sheets overlaid as colour channels = the graffiti. **Slice selection += the BMF TEXTURE record tag `0x18 BITSLICE` (u8, absent=0)** — the entry NAMES in the BSL directory are +authoring records, read-and-discarded at runtime (proof: BASEV.BMF `bexp9_tex`→slice 8 vs entry named +`bexp99`). Definitive reference found: **the shipping 2007 loader `DivLoader/VGCDivLoader.cpp:323-410` +(`LoadBSLFile`/`getBSLData`)** + Division `dsys/PIMAGE.H dpiBSLTYPE` + content build scripts (`img2vtx.exe +-b -m0..-m5 `); corpus-verified on all 66 archive BSLs (GEN.BSL's 6 entries can't fit any byte-plane +model — the smoking gun). FIXES: (1) `port/src/image.cpp decodeBSL` rewritten to slice decode (one file +serves engine+viewer; `BT_BSL=0` = legacy fallback); (2) `bgfload.cpp` parses tag 0x18 → `MatInfo.texChannel` +→ `BgfDrawBatch.texChannel` → `L4D3D decodeImage(tp, channel)`; (3) **cross-library RAMP registry** — every +mech-skin material references ramp `softer` (neutral 0.25→0.99) which is defined only in OTHER libs +(BTARENA/BTFX/BTPOLAR/BTVEH); per-file lookup left mech skins un-ramped forever; on first miss the loader now +sweeps the indexed BMFs once into a global registry (`BT_RAMP_XLIB=0` disables); (4) **RAMP TINT RULE** — +the content pins the (binary-only) board combine from both sides: dsky_mtl has diffuse (0.3,0.5,1.0) + the +COLOURED 'sky' ramp yet renders footage-white ⇒ diffuse must NOT modulate coloured ramps; the mech variants +(gen2medgry vs gen3drkgry etc.) share texture+NEUTRAL 'softer' ramp and differ ONLY by diffuse ⇒ diffuse MUST +tint neutral ramps. Rule: explicit diffuse + NEUTRAL ramp ⇒ tint; coloured ramp ⇒ white (`BT_RAMP_TINT=0` +restores always-white). Terrain/sky pixels UNCHANGED by construction; (5) truecolor slices (≥6, e.g. +damcolor's bdam8 damage sheet) bypass the ramp; (6) beam grit (L4VIDEO BTDrawBeams inline decode) now samples +slice 0 (bexp1 — beamwhite_scr_tex has no 0x18 tag); (7) viewer mirrored + its texture cache re-keyed +`path#channel` (4 textures share BLKHWK.BSL). **Render-verified:** grass + close-up = coherent gray-metal +Blackhawk with crisp panel/vent detail, dark charcoal accent zones, mauve `gen*a` leg tints (arena shot), +green lgo6 leg-logo edge visible — the BMF-spec oracle look ("gray panel-detailed mech", per-zone) exactly; +dbase terrain/sky/vehicles byte-identical to the tuned look; arena walls/punch gate clean, 0 lodflips; combat +under BT_HEAPCHECK = TARGET DESTROYED after 8 hits (exact baseline), 0 crashes / 0 heap detections. Docs +corrected (ASSET_PIPELINE.md §Pixel formats, CLAUDE.md §5). FOLLOW-UPS: RGBA4444 alpha (TREE.BSL cutout) is +decoded but not yet alpha-tested outside punch batches; VTX/TGA paths untouched; the DZM damage-state +material swap (THOR stores damage skins as extra slices thor12d/thor34d) still pending damage-graphic-state +work. + +**✅ WRECK FLOOR-TILE FLICKER — FIXED (baked ground-shadow models + consolidated-world recession; commits +15fc70d, 7d5071a).** The user reported the floor tile under arena wreckage flickering exactly like the wall +panels (pattern changes with movement, freezes parked — the coplanar-shell signature, and visible in original +demo vids too; user chose to fix). TWO coplanar fighters found: (1) **baked ground-shadow MODELS** — the +`S.BGF` naming convention (MECHMOVS/COMS/ARMRS/COOLS = shadow companions to wreck props): flat quads of +`basev:shadow_mtl` at y≈0.1. Detection = all-shadow-material model (`shadowMat` per batch in bgfload; material +name contains "shadow") → routed through the mech-shadow pipeline (`SetIsShadow`: translucent dark, depth-biased, +no z-write) instead of opaque. (2) The ACTUAL fighter for the tile: **MECHMOVR's own ground plates at y=0.000 +vs the consolidated arena floor** (cross-object coplanarity the per-object submission bias can't order) → +**consolidated static world is now recessed `+7.5e-7` NDC (~3 LSB)** (ConsolidateSingleObject op copy), so +individually-drawn objects (entity props, additive structures, the mech) win floor-plane ties deterministically. +Verified at the user's parked repro spot. Shadow-material models are also EXCLUDED from static consolidation +(GetIsShadow → banded), same as banded additive objects. + +**✅ POLAR MAPS VERIFIED — two "bugs" that are AUTHENTIC + the emissive decode (commit cfbe6d2).** polar3/ +polar4 boot + render + fight clean. (1) **No mech shadow on polar = AUTHORED**: `GEO/POLAR/*_TSHD.BGF` are +188-byte STUBS (deliberate shadow disable on snow; only RAP_TSHD is real) — chased through burial/tilt/opaque- +color probes first (BT_SHADOW_BIAS/BT_SHADOW_TILT/BT_SHADOW_COLOR all no-effect) before checking the FILES. +Check the content before debugging the pipeline. (2) **Pink ice-mound perimeter = AUTHENTIC**: the `pint` +material family (`MAT/DAY/BTPOLAR.BMF` pintBIceEmit_mtl: diffuse BLACK + emissive 0.676/0.614/0.718 + ramp +'icy') = pink-tinted self-lit ice. This surfaced that **EMISSIVE (tag 0x26) wasn't parsed** — now threaded +through MatInfo→BgfDrawBatch (`hasEmissive`/`emissive[3]`); PURE-emissive materials (diffuse black + emissive +set) render as an UNLIT glow: vertex color forced BLACK (the engine drives lit diffuse from vertex COLOR1) + +`m.Emissive = batch.emissive` — tex × emissive, no sun/ambient. ⚠ pure-emissive test is `(color&0xFFFFFF)==0`; +a material with BOTH diffuse and emissive keeps the lit path. + +**✅ ALL 8 RES MAPS BOOT + RENDER (map sweep complete):** cavern, grass, rav, polar3, polar4, arena1, arena2, +dbase — each verified live (screenshot + log: 0 crashes, 0 lodflips). Eggs for every map now exist in the pod +BT dir (POLAR3/POLAR4/RAV/ARENA1M/ARENA2/DBASE/MECH2/DEV/TEST...). Arena2 note: the flat brown band between +wall-top and sky is the cavern backdrop (arena maps sit inside the cavern map via class-0 includes) — believed +authored; re-check only if it misbehaves in motion. + +**✅✅ CONN+PCONN COEXIST — 370 of 841 MODELS WERE MISSING TRIANGLES (the "turret base missing panels" +bug; bgfload.cpp + port/src/bgf.cpp).** The user spotted the calliope turret (`calp` gun + `calpb` base +pad, on dbase AND cavern) rendering with gaps in the base and confirmed vs pod videos. Chain that found it: +loader-mirror (bgfcheck.py) said calpb loads "clean" at 78 tris → the standalone port VIEWER showed the same +gaps (ruling out the new consolidation/cull work) → dumping the chunk TREE showed calpb LOD0's PMESH carries +**BOTH** `PCONN_LIST` chunks (37 quads + 1 hexagon = 78 tris; note **ppf=6 hexagons exist**) **AND** a +`CONNECTION_LIST` chunk (24 plain triangles = the base panels) — and `buildPmesh` processed CONN only +`if (!gotPconn)`. **CONN and PCONN are NOT alternatives — a pmesh carries quads/hexes in PCONN plus loose +triangles in CONN.** Corpus scan: **370/841 GEO models** have mixed pmeshes (incl. AVA mech parts, buildings, +vehicles) — all silently missing their CONN triangles until now. FIX: process both, always (both loaders); +probe verified calpb 78→102 tris (+24 = exact); viewer A/B shows the base gaps filled; combat regression +clean. BGF_FORMAT.md §Indices + Open-Items updated (second correction to the CONN story: first "one polygon" +→ flat trilist (task #20), now "either/or" → BOTH). ⚠ LESSON: a "loads clean, all indices in bounds" audit +can still be dropping whole chunks — audit what's NOT consumed, not just what is. + +**✅✅ THE "TURN HITCH" / 10FPS BASELINE — ROOT-CAUSED & FIXED (~50-80× render speedup; user: "game +occasionally hangs/stutters... definitely somehow related to turning").** Instrumentation chain that found it +([spike] frame-time log → [loadobj] load timer → cdb stack sampling → [rstat] draw/present split → per-draw +section timers): the hitches happened with AND without input (the user's own repro log: 3 of 6 spikes at +turn=0) — turning only made them VISIBLE (at 10fps a 400ms frame while holding a turn snaps the view through +4 frames of rotation = reads as a freeze; going straight the same stall just pauses the scenery). THE REAL +PROBLEM was the render baseline: ~107ms CPU per frame (Present <1ms, GPU IDLE). THREE stacked causes, all +fixed (engine tree, git add -f): + 1. **`D3DCREATE_SOFTWARE_VERTEXPROCESSING` (L4VIDEO.cpp:2023) — THE ROOT CAUSE.** Every vertex of every + draw was transformed + LIT + fogged on the CPU. The first CreateDevice attempt used SOFTWARE (the error + message below it says "Couldn't create HARDWARE..." — the fallback was always intended); now HARDWARE + first, software fallback kept. This alone: dense-view frames 490ms → ~3ms. Also why the RelWithDebInfo + build was EQUALLY slow (the D3D software pipeline is external optimized code either way). + 2. **D3DX `DrawSubset` attribute-table scan (~44µs/call × ~1400).** Our BGF triangles are DOUBLE-SIDED + (each face emitted twice) → `GenerateAdjacency` degenerate → `OptimizeInplace(ATTRSORT)` failed SILENTLY + → no attribute table → DrawSubset scanned the whole attribute buffer per call; on the huge CONSOLIDATED + static meshes ~354µs/call = the 490ms stretches whenever a merged map chunk entered the view. FIX: the + batches are already contiguous face-sorted ranges — `SetAttributeTable` EXPLICITLY (no adjacency at all) + + per-op `bgfStartIndex/bgfPrimCount` in L4DRAWOP and a direct `DrawIndexedPrimitive` path in DrawMesh + (DrawSubset kept as fallback for non-BGF meshes). ⚠ `D3DXConcatenateMeshes` (static-world consolidation) + REQUIRES an attribute table on its INPUTS — removing OptimizeInplace without SetAttributeTable made it + fail hr=0x8876086c and the WHOLE TERRAIN silently skipped ("206 static meshes") — the mech floated in + fog-colored void. ConsolidateSingleObject now sorts faces by op itself (no D3DX optimize) + sets the + table + direct-draw ranges. + 3. **No frustum culling** (the 1995 IG board clipped in hardware; the port drew EVERYTHING every frame). + Added bounding-sphere culling: model-space sphere computed at load (mesh ctor), per-frame frustum from + view × mProjectionMatrix (NOT a GetTransform read-back — device state between frames holds pass-specific + matrices and the planes silently degenerate), 4 side planes + FAR plane (near skipped for pass-projection + differences; sky pass never culled; `mCullRadius<=0` never culled). Gate `BT_CULL_FRUSTUM` (default ON). + **Measured: dbase circling 107ms/2729 batches → 1.2-2.8ms; grass combat avg 4.5-8.7ms; spikes 8→~1 + (residual = first-view managed-buffer upload). Verified: full scene renders (terrain/mesas/sky/mech/ + shadow/beams), combat DESTROYED, 0 crashes.** Diagnostic infra kept (cheap): `[spike]` (mech4, frame>0.2s), + `[loadobj]` (L4D3D, load>5ms), `[rslow]`/`[rstat]` (L4VIDEO, 1Hz frame/batch/cull stats). Also gated the + weapon-aim terrain raycast (mech4) to trigger-down/discharge-active frames (was marching SampleBand over + every map instance EVERY frame). ⚠ LESSON: the user plays the DEBUG build — but the 10fps was NOT the debug + build (RelWithDebInfo identical); always split draw-vs-present and check the DEVICE flags before blaming + the compiler. ⚠ D3DXComputeBoundingSphere/SetAttributeTable/etc: any NEW d3d_OBJECT member must be inited + in BOTH ctors (0xCD fill reads as garbage bounds → everything culled). + +**✅ FIRING ARC — no more shooting the enemy out of your back; the arc is DATA-DRIVEN from the torso.** +The bring-up fire path aimed the beam AND applied damage at the locked target regardless of where the mech +pointed (turning away fired lasers backward THROUGH the mech at the enemy behind). **DECOMP FINDING: there is +NO firing-arc check in the binary** — `Emitter::FireWeapon` (@004bb6d4) fires whenever `HasActiveTarget()` +(entity+0x388) and builds a straight muzzle→target beam, and the weapon `.SUB` resources carry **NO arc field** +(ERMLASER.SUB = range/damage/recharge/heat only). The 1995 game got away with this because it was **cockpit- +only**: a beam from a front muzzle to a target behind you passes behind the camera — invisible to the pilot +(same reason the foot-clip never mattered). Our external camera exposes it, so the port owns the fix. The +AUTHENTIC per-mech weapon traverse is the **TORSO twist range** (what actually lets a mech point its guns off +dead-ahead): `Torso::GetHorizontalReach()` = the wider of the software limits `horizontalLimitLeft/Right` +(res +0x100/+0xFC), or **0 when `TorsoHorizontalEnabled=0`** (the Blackhawk → guns lined up with the mech +facing). Wired: `Mech::GetHorizontalFiringReach()` (mechmppr.cpp — the TU that safely knows the full `Torso` +type; mech.cpp/mech4.cpp must not include subsystem headers, stub-collision) reads the cached torso @0x438; +the mech4 fire block gates BOTH the visual beam aim AND the real Emitter trigger + explosion/damage on +`targetInArc` = angle-to-target ≤ (BASE aim tolerance + torso reach). Base tolerance = `BT_FIRE_ARC` degrees +(default 30) — the reticle/convergence box, a labeled PORT presentation parameter (the cockpit view never +needed one); the torso reach is the authentic data part (0 for BLH, widens the arc for a twist-capable mech). +In-arc: the beam converges on the target and the shot hits; out-of-arc: the guns fire straight ahead and +nothing is hit. **Verified live** (`[target]` log shows `torsoReach=0rad` for the BLH → arc = base 30°): +circling — enemy sweeping out of arc → beams go `-> free` (forward), no hit; facing → `-> target` converge + +damage; walking in to close range → **TARGET DESTROYED after 8 hits** (un-regressed). ⚠ `kWeaponRange=100` +(mech4.cpp) is a stale BRING-UP damage-gate threshold (the real ERMLASER `WeaponRange=500`); the enemy spawns +at 120u so a STATIONARY facing test sits just outside it (visual beam fires, no damage until you close to +<100u) — cosmetic bring-up wart, not the arc. **Remaining:** wire the real torso ACTIVE-TRACK (the torso +twists toward the target within its limits, so a twist mech's guns visibly follow) — currently the reach only +WIDENS the hit test; the joints don't move in combat (torso sim dormant, §Torso). +**✅ BEAM ORIGIN TRACKS THE LIVE MUZZLE during the discharge (you can't run out from under your own beam).** +The user saw the beam's near end lag behind the mech when running — the bring-up pushed ONE world-space beam at +fire time with a 0.2s life, so the mech moved out from under the frozen segment. **DECOMP: the real Emitter +re-anchors the beam every frame.** `Emitter::FireWeapon` (`FUN_004bace8`) computes the muzzle LIVE via +`GetMuzzlePoint` (`FUN_004b9948` — resolves the gun's muzzle SEGMENT current world position from `owner+0x300` +segment table, ref `+0xdc`) and **does NOT store the muzzle** on the emitter (only the aim orientation @0x107 +and the target point @0xf2 are saved); then `EmitterSimulation` case 0 (Firing) calls `ContinueDischarge` +(`FUN_004baa20`) EVERY frame while `dischargeTimer>0`, re-sending the beam keepalive. Since the muzzle isn't +stored, the display re-obtains it live each frame → the beam ORIGIN follows the gun as the mech moves (invisible +in the 1995 cockpit-only view; our chase cam exposes the lag). Reproduced in the mech4 visual: a single active +discharge is tracked (`s_dischargePort`/`s_dischargeRemain`, one at a time since beam-on 0.2s < stagger 0.37s); +on the trigger edge it STARTS the discharge, then a per-frame KEEPALIVE redraws glow+core+flash from +`muzzles[port]` (recomputed from the gun segments every frame) to the live aim with a one-frame ttl (exactly one +beam alive/frame = same brightness, re-anchored to the moving gun). **Verified:** running forward while firing, +the beam emanates from the arm gun (near end at the gun, not behind); combat un-regressed (TARGET DESTROYED +after 8 hits, 0 crashes) while the mech walked the whole time. +**⚠ FOLLOW-ON (colour/grit washout from the run-out fix):** re-pushing the beam every frame at a one-frame +ttl made `f = ttl/maxTtl = 1.0` CONSTANT (the old 0.2s ttl FADED, so it was only briefly at full brightness). +Three full-bright overlapping tubes (red glow + white core + white flash) at the SAME `s_tubeWidth` then +additively saturated to a solid WHITE cylinder — the red and the scrolling bexp grit vanished. Root cause was +TWO things: (1) the tube path ignored per-beam width (all beams drew at the global `s_tubeWidth`), so the +white core covered the FULL beam width instead of just the centre; (2) too much additive brightness. FIX: +the tube now scales by **`b.width * s_tubeWidth`** (per-beam width honoured; `BT_BEAM_WIDTH` is now a global +MULTIPLIER, default 1.0), and the keepalive pushes just TWO dimmer beams — a WIDE dim red glow (`0x00C81404`, +width 3.0 → carries the ER-laser colour + grit) + a THIN warm core (`0x00A07868`, width 0.9 → hot centre, no +white-out) — dropping the always-on white flash. Result: a steady red bolt with a bright core and visible +scrolling grit (render-verified, 24 beams, 0 crashes). A laser is steady-on (not fading), so constant +brightness is correct — the bug was saturation, not the lack of fade. +**Remaining (real weapon fidelity):** per-weapon color from the beam model (blue PPC, yellow standard lasers) +once weapon enumeration is wired (factory class IDs, not simulationFlags); the throttled real-fire beam (drive +off Emitter::FireWeapon, heat); damage by the aimed zone (cylinder lookup, STEP 6). +Remaining subsystem families (projectile/missile weapons 0xBCD/0xBCE/0xBD0 [WAVE 7]; SubsystemMessageManager +0xBD3 [WAVE 8]; Gyroscope 0xBC4 deferred — NaN integrator) per SUBSYS_PLAN.md. DONE: Torso (WAVE 5) + +Sensor/Searchlight/ThermalSight/AmmoBin readouts (WAVE 4) + Myomers structural un-stub (WAVE 6, INERT — +authentic mover/heat coupling deferred). + +**✅ TORSO twist/elevation aim — joint-I/O + simulation RECONSTRUCTED (dormant; blocked on the WAVE-4 un-stub).** +The torso's aim→skeleton path is now fully reconstructed and builds green with zero combat regression, but is +NOT yet live because the Torso subsystem itself is still a `HeatSinkSource` RECON_SUBSYS stub at factory case +0xBC5 (its real class is **Torso**, ctor @004b6b0c — the "SinkSource" label is the usual mislabel). What was done: +- **`Mech::ResolveJoint(name)→Joint*`** (mech.cpp, == binary `FUN_00424b60`): resolves a named skeleton node + via the PUBLIC engine path `GetSegment(CString)` → `EntitySegment::GetJointIndex()` → `JointSubsystem:: + GetJoint(i)` (JMOVER.h/SEGMENT.h/JOINT.h, all in `munga_engine.lib`) — never touches the protected + `EntitySegment::jointPointer`. The Torso ctor @004b6b0c + Gyro ctor @004b3778 inline this; hoisted as the reuse + point (gyro un-stub will call it too). `Torso::ResolveJoint` forwards to `owner->ResolveJoint` (out-of-line — + needs complete Mech). +- **`Torso::PushTwist(Joint*,Scalar)`** (== `FUN_004b66b4`/`FUN_004b67ec` inner block): dispatch on + `Joint::GetJointType()` — hinge (0..2)→`SetRotation(Radian(twist))` (`FUN_0041d0a8`); ball (4..5)→ + `GetEulerAngles()` keep pitch[0]/roll[2], **yaw[1]←twist**, `SetRotation(EulerAngles)` (`FUN_0041cfa0`/ + `FUN_0041d020`). Nodes retyped `void*`→`Joint*`. +- **`TorsoSimulation` @004b5cf0 corrected to match the raw decomp** (part_013.c 4471-4693): the earlier draft had + the command roles SWAPPED — verified true roles: **@0x1F8/@0x1FC = elevate up/down** (drive `currentElevation` + 0x1e4, clamp vertTop/Bottom), **@0x200/@0x204 = twist left/right** (drive `currentTwist` 0x1d8, clamp horiz + Left/Right), @0x208 = recenter; **analog axes @0x1F0 = twist, @0x1F4 = elevation** (the draft misused + `elevationHalfBottom`@0x22C and omitted @0x1F4). Settle test uses `targetTwist`(0x218) + the OLD twistRate + snapshot; limit latches are **edge-triggered** with @0x260/0x264=twist L/R, @0x258/0x25C=elev top/bottom + (the draft had these swapped/non-edge). `ResetToInitialState` @004b5bf8 likewise made faithful (clears all 4 + latches + analog axes + aim state). +- **Per-frame skeleton write wired.** KEY DECOMP FACT: `TorsoSimulation` AND `TorsoCopySimulation` compute + `currentTwist` but **never write the joints**; `WriteJoints` (@004b66b4) is called ONLY by `ResetToInitialState`; + `UpdateJoints` (@004b67ec, the out-param-free twin) has **ZERO direct callers** — the per-frame torso→skeleton + write was dispatched by the engine's generic joint pass (indirect/virtual). This port ticks the torso's own + Performance, so `UpdateJoints()` is now called at the end of both Torso(Copy)Simulation — the resolved + equivalent at the same cadence. Verification is env-gated `BT_TORSO_LOG` (ctor resolve + layout dump; PushTwist + fire log). + +**✅ WAVE-4 Torso base-chain RE-BASE + un-stub — DONE (real Torso is live, binary-exact layout, zero regression).** +The Torso is now built by the real `Torso` class at factory case 0xBC5 (`CreateTorsoSubsystem`), no longer a stub. +- **The corrected hierarchy** (verified from headers + a 6-agent decomp-mapping workflow): `Torso : PowerWatcher : + HeatWatcher : MechSubsystem` — **NOT** HeatableSubsystem. The Watcher branch is SEPARATE from the working + `PoweredSubsystem : HeatSink : HeatableSubsystem : MechSubsystem` heat leaves; the two share ONLY `MechSubsystem`. + So the 156-byte deficit lives entirely in the Watcher branch and re-basing it CANNOT touch the working heat leaves. + (Corollary, verified: Torso is **NOT** `IsDerivedFrom(HeatSink)` → it never joins the heat-conduction roster, so + there was never a heat-roster-corruption risk; the earlier note to the contrary was wrong.) +- **The +156 fix (all in the Watcher branch; `MechSubsystem` untouched):** the binary's HeatWatcher/PowerWatcher use + a **0xC-byte** connection (`watchedLink`) and a **0x54-byte** gauge alarm (`heatAlarm`/`watchdogAlarm` = a full + GaugeAlarm), NOT the 4-byte `SubsystemConnection` / 8-byte `HeatAlarm` the HeatSink branch uses. Introduced + Watcher-**local** `WatchedConnection`(0xC) + `WatcherGaugeAlarm`(0x54) in `heatfamily_reslice.hpp` (NOT heat.hpp, + so the shared types the working leaves depend on are never resized). HeatWatcher: `watchedLink`→0xC (+8), + `heatAlarm`→0x54 (+76) = +84 → own ends 0x180. PowerWatcher: DELETE the shadow `watchedLink` (the binary has ONE, + inherited@0x114) (−4), `watchdogAlarm`→0x54 (+76) = +72 → `minVoltage`@0x180, `watchdogAlarm`@0x184, own ends 0x1D8. +- **Torso own block:** DELETED the 7 CROSS-FAMILY shim backing fields (segmentFlags/heatModelOff/heatStateLevel/ + electricalState/creationTime/currentTime/movedFlag, −28) and their accessors now read the REAL inherited base + (`heatAlarm.GetLevel()`, `watchdogAlarm.GetLevel()`, `MechSubsystem::simulationState`, engine `Simulation:: + lastPerformance`/`ForceUpdate()`); ADDED the 4-byte `_reserved0x270` slot (+4) so `recenterActive`@0x274 / + joints@0x278/0x27C match the binary. Net −24 → own block 0xA8, **`currentTwist`@0x1D8, `sizeof(Torso)==0x280`**. +- **Compile-time LOCKS** (the practice from the resource audit, now applied to OBJECT layout — protected fields need a + `friend struct XLayoutCheck`): `TorsoLayoutCheck` (currentTwist==0x1D8, sizeof==0x280, +0x1E4/0x1F0/0x1F4/0x250/ + 0x274/0x27C), HeatWatcher (watchedLink@0x114, heatAlarm@0x12C, sizeof==0x180), PowerWatcher (minVoltage@0x180, + watchdogAlarm@0x184, sizeof==0x1D8), `WatcherGaugeAlarm`==0x54 / `WatchedConnection`==0xC, and **`HUDLayoutCheck`** + (HUD::flickerRate@0x1D8 — HUD is the only OTHER live Watcher-branch class; the lock proves the re-base fixes HUD's + own torso+0x1D8 read too). All pass. **KEY: `sizeof()` static_asserts work at namespace scope, but `offsetof()` on a + PROTECTED object member needs a `friend struct` (namespace-scope offsetof → C2248).** +- **Verified live** (`BT_TORSO_LOG`): the real Torso constructs with `currentTwist@472 (0x1D8)`, `sizeof=640 (0x280)`, + no crash over 26s; heat converges to the identical steady state (`pendingHeat` 0.239, temp 77, heatLoad plateaus — + the transient −0.265 on shot #1 was benign startup-timing variance); full combat loop intact (**TARGET DESTROYED + after 8 hits** at walk speed, matching the prior baseline). The gyro cross-link `sinkSourceSubsystem+0x1D8` now + reads the live `currentTwist` (was stub garbage). +- **Why the torso still doesn't visibly twist for the Blackhawk (a FAITHFUL result, NOT a bug):** the Blackhawk's + 0xBC5 record has **`TorsoHorizontalEnabled=0`** + empty joint names + `hRotPerSec=0` (dumped raw; the record IS a + valid Torso record — classID=0xBC5, modelSize=0x158). Our resource struct is CONFIRMED correct — the binary ctor + `@004b6b0c` reads enabled at `param_4+0x14C` (line 5038) and gates joint resolution on `enabled!=0` (line 5039), + joints at `+0x10C`/`+0x12C`, exactly our layout. So the ORIGINAL binary ALSO skips torso joints for this mech; our + behavior is byte-faithful. Visible twist needs a **twist-enabled mech** (a record with enabled=1 + real joint + names), or the clearly-marked bring-up force-enable below. +- **✅ FULL PATH PROVEN end-to-end via `BT_FORCE_TORSO` (bring-up demo; default OFF; NOT faithful).** Since the + Blackhawk record disables the torso, a gated demo (`BT_FORCE_TORSO=1`, torso.cpp ctor + TorsoSimulation) force- + enables the torso, resolves the REAL `BLH.SKL` joints, widens the limits, and sweeps an analog twist demand. Verified + (`BT_TORSO_LOG`): `Mech::ResolveJoint` resolved **`jointshakey2`→type=4 (Ball, torso body)** + **`jointtshadow`→type=1 + (HingeY, the skeleton's "apply torso twist to yaw" joint)**; `PushTwist` then fires every frame with `currentTwist` + sweeping cleanly `0→+0.7` (clamped at limit) → reverse → `-0.7` → back — i.e. the WHOLE reconstructed chain + (analog axis → TorsoSimulation integrate → clamp → UpdateJoints → PushTwist → `Joint::SetRotation`, ball branch + yaw←twist) runs correctly; no crash over 14s. The only unconfirmed link is the render pass consuming the joint + (needs an eyeball). The env-gated demo (+`BT_FORCE_TORSO_JOINT=` to pick the main joint) stays as an on-demand + verifier; faithful behavior is untouched when unset. BLH.SKL torso joints for reference: `jointshakey2` (ball, + blh_tor.bgf, under jointhip) = torso body; `jointtshadow` (hingey, under jointshadow) = ground-shadow twist. +- **Follow-on for the sibling un-stubs (Gyro/Searchlight/ThermalSight):** they inherit PowerWatcher, so after this + re-base their own fields also begin at 0x1D8, but they STILL carry the same CROSS-FAMILY shim fields → each must get + the same shim-delete + accessor-redirect (like Torso) BEFORE its un-stub, or wiring it will over-size the object. + +**◐ WAVE-5 GYROSCOPE (0xBC4) — layout + joint-I/O DONE; un-stub DEFERRED (ctor/integrator reconstruction incomplete).** +The gyro plumbing is reconstructed and the build is GREEN with the gyro back on its stub (reverted — see why below); the +infra is dormant + ready in gyro.hpp/gyro.cpp: +- **Re-base + layout (DONE, locked):** `Gyroscope : PowerWatcher : HeatWatcher : MechSubsystem` (Watcher branch). Deleted + its 4 CROSS-FAMILY shim fields (segmentFlags/heatModelOff/heatStateLevel/electricalState) + redirected the 3 used + accessors to the real inherited base (heatAlarm/watchdogAlarm/simulationState), retyped eyeJointNode/mechJointNode + `void*`→`Joint*`. `GyroLayoutCheck` friend locks `exageration@0x1D8` + `sizeof<=0x3D0` (both pass live). +- **Joint I/O (DONE, real):** the 8 node-helper stubs (NodeType/NodeScalar/SetNodeScalar/NodeVector/SetNodeVector/ + NodeRotationEquals/SetNodeRotation/NodeVectorEquals) now call the real engine `Joint` API — `GetJointType`/`GetRadians`/ + `SetRotation(Radian)` [hinge]/`GetEulerAngles`/`SetRotation(EulerAngles)` [ball]/`GetTranslation`/`SetTranslation` + [BallTranslation]; **FUN_0041d11c = SetTranslation** (writes `*(joint+0xc)` + jointModified). `ResolveJoint` forwards to + `Mech::ResolveJoint`. WriteEyeJoint/WriteMechJoint (no direct caller in the decomp, like the torso's UpdateJoints — + dispatched by the engine joint pass) are wired at the end of `GyroscopeSimulation`. +- **Verified live then REVERTED:** the real gyro constructs, resolves REAL joints (`EyeJoint='jointlocal'`, + `MechJoint='jointeye'`, both BallTranslation type=5), and `WriteMechJoint` FIRES every frame — but with **garbage + values** (`-4.31e8` = 0xCDCDCDCD-as-float, and NaN) written to `jointlocal` (the ROOT joint). Root cause: the gyro's + ctor field-init + integrator field-MAPPING are incomplete/incorrect in the reconstruction. The binary ctor @004b3778 + puts **springConstant@0x1E8, dampingConstant@0x1F4, posSpring@0x218, negSpring@0x224, rotation springs @0x2C0/0x2CC/ + 0x2F0/0x2FC**, but the recon mislabels several (e.g. assigns springConstant to eyeOrientation@0x1DC; the field the + integrator reads as `eyeSpringConstant`@0x1F4 is really dampingConstant) and leaves the integrator accumulators + (eyeAccel/eyeForce/eyeWork/bodyOrientation/bodyAccel/bodyForce/bodyWork/eyeVelocity/limits) UNINITIALISED. Writing NaN + to the root joint is unshippable, so 0xBC4 was reverted to the `Gyro` stub (combat/heat un-regressed, TARGET DESTROYED). +- **⚠ Cross-link landmine (mech.cpp):** the gyro↔torso cross-link `((SubProxy*)gyro)->linkTarget = sinkSource+0x1D8` + writes through `SubProxy::linkTarget` which sits at **gyro+4** (an engine base field), not the real gyro torso-link + offset — harmless vs the stub, would STOMP the live gyro base. Commented out; needs the real gyro torso-link field. +- **Remaining work (the real gyro un-stub):** faithfully reconstruct the ctor field-init + the two integrators + (IntegrateEyeJoint @004b2ec0 / IntegrateBody @004b30ec) and the field LAYOUT/labels from @004b3778 (the recon has a + ~196-byte layout gap + uncertain field map). This is a full subsystem-simulation reconstruction (bigger than the + torso), not a quick un-stub — do NOT guess the integrator mapping (no stand-ins). Env `BT_GYRO_LOG` dumps the layout + + joint resolve + WriteMechJoint values for verification. diff --git a/phases/phase-01-context-restructure.md b/phases/phase-01-context-restructure.md index a56ad7d..bb4caf7 100644 --- a/phases/phase-01-context-restructure.md +++ b/phases/phase-01-context-restructure.md @@ -18,9 +18,14 @@ knowledge should load on-demand. - **Non-destructive:** the live `CLAUDE.md` is UNCHANGED until the migration is complete + approved. The proposed router is `context/_ROUTER-DRAFT.md`. -## Status +## Status — ✅ COMPLETE (migration done, CLAUDE.md swapped) -### ✅ Foundation built (this pass) +The full migration landed: `CLAUDE.md` is now a **160-line router**; the complete 2236-line +monolith is preserved verbatim as **`docs/PROGRESS_LOG.md`** (byte-identical — zero context lost); +**18 context topic files** (1343 lines) digest every CLAUDE.md section; the knowledge graph +validates CLEAN (all `[[links]]` + quick-lookup + docs refs resolve; `scratchpad/checkctx.py`). + +### ✅ Foundation built (first pass) - `reference/glossary.yaml` — ~45 terms (platform/engine, formats, scene/entity, reconstruction). - `context/decomp-reference.md` — the quantitative hub: resource-type enum, ClassID map, mech offsets, damage delivery, weapon constants, env gates, tools. @@ -31,27 +36,24 @@ knowledge should load on-demand. - `context/_ROUTER-DRAFT.md` — the proposed slim CLAUDE.md (identity, protocols, quick-lookup, evidence tiers, conventions, structure). -### ☐ Remaining topic files (the next tranche — migrate from CLAUDE.md §/docs) -| topic | source in CLAUDE.md / docs | -|---|---| -| `project-overview.md` | §1-3 | -| `pod-hardware.md` | §3 | -| `content-archives.md` | §4, §4a | -| `asset-formats.md` | §5, docs/ASSET_PIPELINE.md | -| `source-completeness.md` | §5a, docs/BT_SOURCE_STATUS.md | -| `wintesla-port.md` | §5b, §8 | -| `build-and-run.md` | §10a, §10a-bis | -| `reconstruction-method.md` | §10b | -| `locomotion.md` | §7, docs/P3_LOCOMOTION.md | -| `subsystems.md` | §10d, docs/SUBSYS_PLAN.md, docs/VEHICLE_SUBSYSTEMS.md | -| `combat-damage.md` | §10c, docs/HARD_PROBLEMS.md | -| `rendering.md` | §10 render-fidelity notes (large) | -| `multiplayer.md` | §7 Phase 7, §8 P6 | +### ✅ Second pass — all topics migrated (§→topic) +project-overview (§1-3), pod-hardware (§3), content-archives (§4/4a), asset-formats (§5), +source-completeness (§5a), wintesla-port (§5b/§8), build-and-run (§10a/10a-bis), reconstruction-method +(§10b), locomotion (§7/§10), subsystems (§10d), combat-damage (§10c), rendering (§10 fidelity), +multiplayer (§7/§8). Plus the first-pass hubs (decomp-reference, reconstruction-gotchas, bgf-format, +gauges-hud, open-questions). §6 (the standalone port/ D3D9 viewer sandbox) is noted in wintesla-port; +full detail in PROGRESS_LOG. -### ☐ Swap (after review + full migration) -1. Move the current `CLAUDE.md` deep content into the topic files above. -2. Replace `CLAUDE.md` with `context/_ROUTER-DRAFT.md` content (preserving the git history of the old). -3. Verify: a fresh session reads the router → quick-lookup → topic files load on demand. +### ✅ Swap done +1. `docs/PROGRESS_LOG.md` = the verbatim old CLAUDE.md (lossless safety net + the "full detail" quick-lookup row). +2. `CLAUDE.md` = the router (git history preserves the old monolith too). +3. Verified: knowledge graph clean; a fresh session reads the router → quick-lookup → topics on demand. + +### Maintenance going forward +New durable finding → the relevant `context/*.md` (+ the `docs/` ledger for exhaustive detail), with +an evidence tier. New topic → a `context/*.md` + a quick-lookup row + cross-refs. The `docs/*.md` +ledgers (incl. PROGRESS_LOG) remain the detailed logs; `context/*.md` are the curated digests. +Re-run `scratchpad/checkctx.py` to validate links after edits. ## Notes - The router keeps the build one-liners + the "keep current" mandate so active port work is diff --git a/scratchpad/checkctx.py b/scratchpad/checkctx.py new file mode 100644 index 0000000..08bab22 --- /dev/null +++ b/scratchpad/checkctx.py @@ -0,0 +1,24 @@ +import re, glob, os, yaml +sys_ids = set(os.path.basename(f)[:-3] for f in glob.glob('context/*.md')) +# glossary top-level keys = valid link targets too +try: + gl = yaml.safe_load(open('reference/glossary.yaml',encoding='utf-8')) + gloss_keys = set(gl.keys()) if isinstance(gl, dict) else set() +except Exception as e: + gloss_keys = set(re.findall(r'^([A-Za-z][A-Za-z0-9_-]*):', open('reference/glossary.yaml',encoding='utf-8').read(), re.M)) +valid = sys_ids | gloss_keys +# template placeholders that are documentation, not real links: +IGNORE = {'other-topic','other-topic-a','other-topic-b','topic-id','name'} +print("topic ids:", len(sys_ids), "| glossary keys:", len(gloss_keys)) +bad=0 +for f in glob.glob('context/*.md') + ['CLAUDE.md']: + txt=open(f,encoding='utf-8',errors='replace').read() + for m in re.findall(r'\[\[([a-z0-9-]+)\]\]', txt): + if m not in valid and m not in IGNORE: + print(f" UNRESOLVED [[{m}]] in {f}"); bad+=1 +cl=open('CLAUDE.md',encoding='utf-8',errors='replace').read() +for m in re.findall(r'`(context/[a-z0-9-]+\.md)`', cl): + if not os.path.exists(m): print(f" MISSING quick-lookup {m}"); bad+=1 +for m in re.findall(r'`(docs/[A-Z0-9_]+\.md)`', cl): + if not os.path.exists(m): print(f" MISSING docs ref {m}"); bad+=1 +print("RESULT:", "CLEAN" if bad==0 else f"{bad} unresolved")