# BT compile-wave reconciliation notes

Cross-family header needs + link-time externs reported by family agents.
Resolve AFTER all 7 families report (Stage 1 reconciliation), then again at link (Stage 3).

## From controls/player (mechmppr, btplayer) — DONE, 0 errors

### Cross-family header changes needed
- **mech.hpp (mech-core family)**: expose on `Mech` — `tonnage` (+0x4bc), `damageBiasFactor` (+0x354),
  `owningPlayer` (+0x190, `BTPlayer*`). Currently reached via raw offset macros in btplayer.cpp.
- **ScenarioRole (engine SCNROLE.h)**: needs decrement of `returnFromDeath`
  (used `SetReturnFromDeath(GetReturnFromDeath()-1)`); no `returnDelay` field (role+0xf0) →
  `roleReturnDelay` stubbed to 0.
- **Player__VehicleDeadMessage**: BT build carried `killerName` (+0x1c) the engine base lacks; read via offset.

### Stubbed (no WinTesla analog) — revisit at integration
- BTPlayer ctor role/mission-registry resolution: `GetMissionRegistry()` / `GetRoleRegistry()->Lookup()` /
  `GetMission()->GetGameModel()` have no WinTesla analog → stubbed; base-set `scenarioRole` stands.

### Link-time externs to be PROVIDED by owning modules (Stage 3)
- `ToggleVoiceAssist`, `Is_Destroyed` (mechmppr)
- `Set_Alarm_Level` (GaugeAlarm has no `SetLevel` in WinTesla)
- `Notify_Objective_Reached`, `StatusMessagePool`
- `BTPlayerVTable`, `BTStatusMessageVTable`, `SelfDestructName`, `ResetDelay`, `TicksPerSecond`

## From gauges/HUD (btl4gaug, gau2, gau3, galm, hud) — DONE, 0 errors

### Cross-family header changes needed
- **mech family**: `LWord BT_GetSegmentFlags(Mech*)` — declared `extern` in hud.cpp (mirrors powersub's
  `BT_GetMessageManager` pattern); needs a real definition in the mech layer.
- **powersub/heat family**: HUD inherited watchdog accessors stubbed to return 0 — bind to real inherited
  members once exposed: heat-state level @0x140, watched-voltage alarm level @0x198, heat-watcher flag @0x40.

### Foundation fix worth doing properly
- `DebugStream` is double-defined incompatibly: `heat.hpp` (`std::ostream&`) vs `mechrecon.hpp` (`ReconStream`).
  Gauge .cpps work around it by pre-defining `BT_DEBUGSTREAM_DEFINED` to suppress heat.hpp's variant.
  → settle on ONE definition in the shared foundation. (NOTE: a `mechrecon.hpp` exists — investigate.)

### Stubbed (unrecovered .data) — revisit
- Gauge resource-pool config (rates/modes/colours/image-names) not recovered from the optimised image →
  typed placeholder statics. HUD `FindAttributeIndex` shim (no engine analog).

## From heat/power (heat, heatfamily_reslice, powersub, gnrator) — DONE, 0 errors

OWNS the subsystem base classes. Established the authoritative base virtual surface (HeatableSubsystem/
HeatSink/Condenser/Reservoir/PoweredSubsystem/Generator/Myomers/PowerWatcher/HeatWatcher) — see report:
GetClassDerivations()/DefaultData 4-arg idiom; HeatAlarm/HeatFilter/SubsystemConnection wrappers in heat.hpp.

### Cross-family header changes needed
- **mech family** — provide on `Mech` (declared as extern prototypes `BT_GetMessageManager/BT_IsBusLive/
  BT_ClearMyomers(Mech*)` in powersub.hpp so TUs link):
  - `Mech::GetMessageManager()` → subsystem message manager (`owner+0x190`)
  - `Mech::IsBusLive()` → power-bus-live flag (`*(owner+0x190)+0x274`)
  - `Mech::ClearMyomers()` → clears myomer back-pointer (`owner+0x374`)
  - Cockpit HUD gauge fields `field_438`/`field_5b4` used by `Condenser::MoveValve` (stubbed null).

## From btl4-app (btl4app/mssn/rdr/grnd/vid/pb/mppr, btdirect) — DONE (4 clean, 4 only cross-family residual)

btl4mssn/grnd/vid/direct = 0. btl4app=1, btl4mppr=2, btl4pb=3, btl4rdr=1 — ALL residuals are in
mech.hpp / mechrecon.hpp (not owned). Will clear when mech family + mechrecon are reconciled.

### Cross-family header changes needed
- **mech.hpp — add to `class Mech`** (offsets from binary oracle):
  - `void SetMappingSubsystem(Subsystem*);` (btl4app.cpp:567)
  - `void Reset();` + `void MissionReviewReset();` (btl4pb.cpp:555)
  - `Logical GetMissionReviewMode();` (reads mech+0x414, btl4mppr.cpp:366)
  - `void SetTargetRange(Scalar);` (writes mech+0x404, btl4mppr.cpp:407)
  - (non-blocking) egg fields `vehicleColor`/`vehicleBadge`/`vehiclePatch` (mech+0x848/0x844/0x84c)
- **mechrecon.hpp(89)** — declares `Random` as a FUNCTION, collides with engine `extern RandomGenerator
  Random;` (RANDOM.h:40). RENAME/REMOVE it — sole residual blocking btl4rdr.cpp. *Investigate what
  mechrecon.hpp is + who owns it (mentioned by gauges/HUD + btl4-app).*

## From mech-subsystems (gyro/torso/myomers/sensor/mechtech/searchlight/thermalsight/messmgr) — DONE, 0 errors

`mechrecon.hpp` confirmed = emergent SHARED COMPAT-SHIM header (provides ReconAlarm etc.); families lean
on it. → CONSOLIDATE + settle ownership in reconciliation (also the `Random`-function bug above).

### Cross-family header changes needed (locally shimmed; real homes are shared bases)
- **Mech/Entity/Subsystem**: `Word GetSegmentFlags() const` (used by 6 files); `Mech::ResolveJoint(const
  char*)`; segment-roster accessors; `Mech::GetSegmentCount()`.
- **HeatSink**: `HeatState HeatStateLevel() const`, `Logical HeatModelOff() const`, public `heatEnergy` accessor.
- **PoweredSubsystem**: `ElectricalState ElectricalStateLevel() const`.
- **PowerWatcher**: confirm `ResetToInitialState()`/`HandleDeathMessage(Message&)`/`UpdateWatch()`↔
  `Simulation(Scalar)`/`WatchedVoltageLevel()`/`HostShutDown()` signatures (now derives HeatableSubsystem).
- **Subsystem**: `GetExplosionID()`; renderer/clock globals for messmgr explosion submission.
- **powersub.hpp**: DELETE stale mislabeled `Myomers`; **RegisteredClass**: add `MyomersClassID == 0xBC6`
  (enum still has the 0xBC3 mislabel = really Sensor).

## From mech-core (mech/mech2/3/4/mechsub/mechdmg/dmgtable) — PARTIAL (4 of 7 clean)

CLEAN: mech2, mech4, mechsub, dmgtable. BLOCKED: mech.cpp, mech3.cpp, mechdmg.cpp.
**OWNS `mechrecon.hpp`** (the shared compat shim) → fix the `Random`-function collision HERE.
Added ~60 Mech members (gait/locomotion/clip/sim) to mech.hpp.

### The big remaining integration task — THE SUBSYSTEM FACTORY
mech.cpp ctor + mech3.cpp `CreateSubsystemStream`/`CreateSubsystemsStream`/`SubsystemDefaultData` dispatch
over ~21 sibling subsystem classes. Each needs, uniformly:
  `static <X> CreateStreamedSubsystem(NotationFile*, const char*, const char*, SubsystemResourceBlob*,
       NotationFile*, const ResourceDirectories*, int)`  AND  `static SharedData DefaultData`
Classes: HeatSink, Reservoir, Condenser, PoweredSubsystem, Myomers, Generator, HUD, Searchlight,
ThermalSight, Sensor, Gyroscope, Torso, Emitter, PPC, ProjectileWeapon, MissileLauncher, GaussRifle,
AmmoBin, MechControlsMapper, MechTech, SubsystemMessageManager (+ mech.cpp-only: Cockpit, HeatSinkSource,
Actuator, JumpJet, LegSubsystem, MechDisplay).
→ This is a dedicated follow-up pass AFTER all sibling headers are final (needs their headers included in
  mech.cpp/mech3.cpp + the uniform static signatures present). mech.cpp also has ~180 raw `this[0xNN]`
  pseudo-assignments to convert, and ChainOf<> default-ctor/SetOwner/iteration semantics.
- mechdmg.cpp: mostly reducible (Mech__DamageZone members structureLevel/graphicState/flags + word
  accessor; Read→ReadBytes); + Generator cross-family dep (powersub) + engine skeleton-walk types
  (SegmentRecord/SegmentIterator/DZRef).

## From weapons (mechweap/emitter/projweap/mislanch/missile/seeker/projtile/misthrst/ammobin) — DONE, 0 errors

No blocking cross-family needs (adapted to heat/powersub header changes live). Watch points:
- `mechweap.cpp` calls `PoweredSubsystem::GetClassDerivations()` — must track if powersub reverts.
- `ammobin.hpp` includes `<heatfamily_reslice.hpp>` (where HeatWatcher is actually defined), NOT
  `<heatfamily.hpp>` (forwards to heat.hpp, no HeatWatcher). Follow if HeatWatcher moves.
- Added per-class `X__UpdateRecord : Base::UpdateRecord`; `UpdateTargeting()` now returns `Logical`.

## ============ WAVE COMPLETE: 36/43 clean. Remaining: 4 btl4-app (reconcile) + 3 mech-core (factory) ============

## RECONCILIATION PASS 1 done → 40/43 clean
- mechrecon.hpp `Random()` template → renamed `RandomUnit()` (collided w/ engine `extern RandomGenerator
  Random`); updated dmgtable.cpp 2 call sites. btl4rdr now 0.
- mech.hpp: added Mech app/cockpit interface — `SetMappingSubsystem(Subsystem*)`, `GetMissionReviewMode()`,
  `SetTargetRange(Scalar)`, `Reset(const Origin&, int mode)`, and `enum ResetMode { MissionReviewReset=0 }`.
  CORRECTION: MissionReviewReset is a MODE CONSTANT (0), not a method (call site proved it).
  → btl4app/mppr/pb all 0. (Definitions of these Mech methods still owed by mech.cpp at link time.)

REMAINING: mech.cpp, mech3.cpp, mechdmg.cpp (the subsystem factory) → dedicated agent.

## RECONCILIATION PASS 2 + STAGE 2 PREP (overnight)
- Factory agent #1 stalled (stream watchdog) with mech.cpp at 67 (from >100); mech3/mechdmg untouched.
  It regressed hud.cpp (0→1) via a `ZeroVector` it added to mechrecon.hpp:181 → FIXED by renaming
  hud.cpp's local `ZeroVector`→`HudZeroVector` (kept mechrecon's for the mech files). hud.cpp back to 0.
- Resumed factory agent (a52bd3052af4ade1a) from transcript with file-by-file + verify-often guidance.
- STAGE 2 (executable) staged, all non-conflicting:
  - `btbuild/btl4main.cpp` = WinMain launcher cloned from RP_L4/RPL4.CPP, BTL4Application path, BTL4.RES,
    800x600 window, RP-only review/playback branch dropped. COMPILES CLEAN (0 errors).
  - `btbuild/CMakeLists.txt` now also defines exe target `btl4` (WIN32): links bt410_l4 + munga_engine.lib
    (enginebuild/build/Debug) + OpenAL32/libsndfile-1 (trunk/lib) + d3d9/d3dx9/dinput8/dxguid/DxErr
    (DXSDK Lib/x86) + winmm/dbghelp/shell32/user32/gdi32/ole32. CMake CONFIGURES clean (Win32 Debug).
  - Version bytes hardcoded {4,1,0} — TODO verify vs BTL4.RES header at runtime.

## NEXT (once factory lib is green): cmake --build → if lib green, link btl4.exe → triage undefined
   symbols (Stage 3) → define/reconstruct or stub-with-flag → iterate to a linked exe → attempt boot.

## ====== ALL 43 RECONSTRUCTED MODULES COMPILE (factory done by stopped-but-finished agent) ======
## FULL BUILD #1: 0 compile errors, 45 unresolved externals at link. Buckets:
##   - ~25 Winsock (L4NET) → FIXED: added ws2_32 to btl4 link.
##   - Surviving BT source not compiled in (BTMission/BTRegistry/BTL4ModeManager/Gauss/PPC/BTTeam/
##     BTScenarioRole/BTConsole) → FIXED: added BTMSSN/BTREG/BTTEAM/BTSCNRL/BTCNSL/GAUSS/PPC/
##     BTL4MODE/BTL4ARND .CPP to the bt410_l4 sources.
## FULL BUILD #2: surviving files have 12 compile errors (410-era ClassDerivations→GetClassDerivations
##   API drift; BTSCNRL needs btscnrl.hpp; BTMSSN Exit()). Link not reached yet.
## STILL-UNDEFINED (buckets 3+4, expected after survivors compile) — provide real def or flagged stub:
##   Mech::{GetMissionReviewMode,SetTargetRange,SetMappingSubsystem,Reset,IsAirborne,RaiseStatusAlarm},
##   Mech__DamageZone::LoadCriticalSubsystems, MechSubsystem::{TakeDamage,OnAlarmChanged,HandleMessage},
##   Is_Destroyed, ToggleVoiceAssist, Generator::{ClassDerivations,ForceShortRecovery},
##   FrameTimeScale, allPresets, BTL4MethodDescription, BTL4AudioRenderer ctor, BTL4VideoRenderer ctor.

## ====== FULL BUILD #3: ✅ btl4.exe LINKS (build/Debug/btl4.exe, 1,849,344 bytes, valid PE) ======
## 0 compile errors, 0 unresolved externals. Achieved via:
## TASK 1 (12 compile errors in surviving 410-era files — all FIXED):
##   - GAUSS.CPP/PPC.CPP: ClassDerivations/DefaultData now pass ADDRESSES (&Emitter::ClassDerivations,
##     &X::ClassDerivations) — matches reconstructed emitter.cpp member-idiom (Derivation ctor now takes
##     Derivation*). Headers unchanged (still `static Derivation ClassDerivations`).
##   - BTTEAM.CPP/.HPP: migrated to GetClassDerivations()/GetAttributeIndex() accessor idiom (mirrors
##     engine TEAM.cpp); TestInstance uses *GetClassDerivations().
##   - BTL4ARND.CPP:77: JointedMover::ClassDerivations → *JointedMover::GetClassDerivations().
##   - BTMSSN.CPP:53: Exit(AbortExitCodeID) → Fail(...) (WinTesla engine has no free Exit(); Fail is the
##     fatal-abort macro already used in this file). FLAGGED.
##   - btscnrl.hpp AUTHORED (decomp/reconstructed/) — minimal BTScenarioRole : ScenarioRole (BTSCNRL.CPP
##     is an empty TU; nothing else references the class).
## TASK 2 (link): three link waves. Wave A=21 unresolved, Wave B=34 (defining the BTL4VideoRenderer ctor
##   pulled btl4vid.obj fully in, exposing the unreconstructed BT renderable module + dpl2d + renderer
##   helpers), Wave C=6 (engine VideoComponent never implemented in WinTesla).
##   REAL fixes (co-located, not stubs):
##     - mechdmg.cpp:358 Generator::ClassDerivations → *Generator::GetClassDerivations() (idiom).
##     - btl4grnd.cpp: defined BTL4MethodDescription[] = { &BTL4ChainToPrevious } (minimal real table).
##     - btl4vid.cpp: defined BTL4VideoRenderer ctor/dtor/TestInstance (ctor adapts old IG calibration
##       args → DPLRenderer(GetActiveWindow(),800,600,false,...); FLAGGED window/size).
##   FLAGGED STUBS (new file decomp/reconstructed/btstubs.cpp; added to bt410_l4 in CMakeLists):
##     data globals: FrameTimeScale(=1.0f), allPresets[2][100], StatusMessagePool, BTPlayerVTable,
##       BTStatusMessageVTable.
##     free fns: Is_Destroyed, ToggleVoiceAssist, Set_Alarm_Level, Notify_Objective_Reached.
##     Mech: GetMissionReviewMode, IsAirborne, SetTargetRange, SetMappingSubsystem, RaiseStatusAlarm.
##     Mech__DamageZone::LoadCriticalSubsystems; MechSubsystem::TakeDamage/OnAlarmChanged;
##       Generator::ForceShortRecovery.
##     BT_GetMessageManager/BT_IsBusLive/BT_ClearMyomers (Mech<->subsystem bridge).
##     dpl2d_* (NewDisplayList/Begin/SetColor/Circle/Push/PopMatrix/MoveTo/End/Compile) +
##       dpl_SetMaterialNameCallback (libDPL 2D layer never ported to L4D3D).
##     BTL4VideoRenderer::GetScene/LoadObject/AttachToEyeDCS/AddDynamicRenderable/LoadMissionImplementation.
##     ENTIRE BT RENDERABLE MODULE (unreconstructed): BTWorldContainer/Root/DCSObject/Hinge/BallJoint/
##       BallTranslateJoint/Eye/DeathEffect/MarkerWatcher/Translocation/POVStartEnd/Tracer/EmitterBeam/
##       PPCBeam/LightConnection ctors (chain to BTRenderableBase) + BTReticleRenderable ctor/dtor/Execute.
##     ENGINE VideoComponent ctor/dtor/Add/Connect/ReceiveControl/Execute (declared in L4VIDRND.h but
##       NEVER implemented in WinTesla — superseded by VideoRenderable; stubbed to link).
##   LINK FLAG: target_link_options(btl4 PRIVATE /FORCE:MULTIPLE) absorbs ~124 LNK2005 / 200 LNK4006 —
##     1995 header-defined non-inline globals/free-fns (operator==(sockaddr_in&), EnumAxes/JoysticksCallback,
##     gOpNames, gReplacementData) compiled into multiple TUs. Identical defs → linker keeps first.
## ⚠ RUNTIME BRING-UP WORKLIST = every stub above (all carry TODO(bring-up) comments; grep btstubs.cpp).
##   The exe LINKS but the BT renderable pipeline + 2D HUD + several Mech accessors are inert until the
##   stubs get real bodies (from BTL4OPT.EXE oracle / RP analog). First boot will likely reach init/menu
##   but not render a mech.  NEXT: attempt boot; reconstruct the BT renderable module + dpl2d-over-D3D.

## MISSING-FILE LEDGER (from BT.MAK+BTL4.MAK = 53 modules): ~10 survived, ~43 reconstructed,
##   4 unaccounted: btl4(main→recreated as btl4main.cpp), path(AI pathfinding-not referenced in link),
##   mechbld(offline tool), btl4tool(offline tool). Nothing runtime-critical lost except possibly `path`.

## ============ STAGE 4: IT BOOTS. btl4.exe runs to a stable window. ============
FULL BUILD: 0 errors, 0 unresolved → btl4.exe (1.85MB PE32) LINKS.
Boot smoke-test (run/ dir: exe + stub OpenAL32.dll/libsndfile-1.dll + x86 debug CRT; cwd = pod content
tree tesla4pod_extracted/.../REL410/BT; arg `-egg TEST.EGG`):

PROGRESS LADDER (each was a real fix):
  1. exit 3, "no source for egg"     → wired L4Application::ParseCommandLine + ParseToken in btl4main.cpp
                                        (egg via `-egg <file>`; single-user mode = no real net needed).
  2. exit 3, "btl4.res v1.1.0.6 obsolete" → version check compares our version[i] vs file versionArray[i+1]
                                        (skips format byte). File=1.1.0.6 → set version[3]={1,0,6}. PASSES.
  3. NOW: boots → window "BattleTech" (800x600) appears → resources OK → ApplicationManager OK →
     BTL4Application constructs OK → **BLOCKS inside ApplicationManager::StartApplication** (idle, CPU~0,
     no crash, stable 16s+). Waiting on a mission/network state-machine event the inert stubs don't produce.

### RUN RECIPE (for morning / interactive)
  exe:  C:\git\nick-games\btbuild\run\btl4.exe   (+ stub DLLs + debug CRT already there)
  cwd:  C:\git\nick-games\tesla4pod_extracted\Tesla4PodPCNovell\CopyofNovellDisk\REL410\BT
  arg:  -egg TEST.EGG     (a BattleTech/cavern/freeforall/night mission)
  log:  btl4.log in the cwd (has the [boot] markers from btl4main.cpp).

### NEXT (bring-up, best done interactively + with a debugger):
  - Trace why StartApplication blocks: the mission/net state machine (InitializingState→WaitingForEgg→
    LoadingMission→...) likely waits for a step a STUB no-ops. Prime suspects = the btstubs.cpp list
    (BT*Renderable, dpl2d_*, BTL4VideoRenderer::Load*/GetScene, network/host local-establishment).
  - Replace stubs with real bodies from the BTL4OPT.EXE oracle / RP analog, in dependency order of the
    boot state machine. Each real body lets the state machine advance one step.
  - Audio is stubbed (no real OpenAL/libsndfile DLLs) — fine for now; swap real DLLs later.

## ============ STAGE 4 BRING-UP (debugger-driven, 2026-06-30) ============
Method: run under cdb (x86), catch fault/abort, fix, repeat. Run recipe unchanged
(run\btl4.exe, cwd=pod BT dir, -egg TEST.EGG; symbols via -y run\ + btl4.pdb).

PROGRESS LADDER (each a real fix; all advance the boot):
  1. HANG in video init: DPLRenderer(=D3D)->loadTables fread(NULL) -> CRT assert MessageBox.
     loadTables reads VIDEO\REPLACEMATS.tbl + VIDEO\MATREPLACETABLE.tbl (WinTesla-era, absent).
     FIX: created empty (numMats=0, 4 zero bytes) tables in pod VIDEO\. -> video init passes,
     monitor detection runs (windowed mode), StartApplication returns, enters RunMissions.
  2. AV in mission load: BTPlayer ctor -> ScenarioRole::GetReturnFromDeath on NULL (role lookup
     stubbed/commented at btplayer.cpp:630). FIX: null-guard btplayer.cpp:644 (default index 2).
  3. AV in frame loop: Entity::Execute->PerformAndWatch called NULL fn ptr; the BTPlayer's vtable
     was overwritten with the zeroed stub `BTPlayerVTable`. ROOT: btplayer.cpp:568/698 had decompiler
     artifact `*(void**)this = &BTPlayerVTable` (+447 status msg). FIX: removed those 3 artifacts
     (C++ ctor sets the real vtable). -> **D3D WINDOW APPEARS**, game simulates.
  4. abort() in frame loop: Player::HuntForDropZone (spawn logic) -> DropZone reply ->
     base Player::DropZoneReplyMessageHandler = Fail("...base player class!"). ROOT: BTPlayer never
     defines DropZoneReplyMessageHandler, so MESSAGE_ENTRY(BTPlayer,DropZoneReply) resolves to the
     inherited base Fail-stub.  <-- CURRENT FRONTIER.

NEXT: reconstruct BTPlayer::DropZoneReplyMessageHandler (the spawn handshake: place mech at dropzone +
create player vehicle) from the BTL4OPT.EXE oracle. Likely a small CLUSTER of missing BTPlayer/Mech
gameplay overrides will follow (now genuine game-logic reconstruction, not mechanical link fixes).

## ============ STAGE 4 BRING-UP (cont., 2026-06-30 session 2) ============
The SPAWN PIPELINE is now reconstructed end-to-end. Boot advances from the DropZoneReply abort all the
way INTO Mech construction. PROGRESS LADDER (each a real fix):
  5. abort() Player::DropZoneReplyMessageHandler base Fail.  ROOT: BTPlayer had no override.
     FIX (REAL reconstruction): reconstructed the whole spawn handshake -
       - BTPlayer::DropZoneReplyMessageHandler  <- binary oracle FUN_004bffd0 (handler table @00512fd8,
         msg id 0x15; confirmed VehicleDead=FUN_004c05c4 NOT 004c012c, and the real 6-entry table:
         DropZoneReply/VehicleDead/Score/ScoreUpdate/MissionStarting/MissionEnding). !playerVehicle ->
         CreatePlayerVehicle+InitializePlayerLink+SetPerformance(by classID: Mech 0xbb9 / CameraShip 0x45)
         +AlwaysExecute+deathCount=0; else-same-death -> MissionEnding ignore / !DropZoneAcquired ->
         VehicleDead(reset); then mech->Reset(dropZoneLocation,True).
       - BTPlayer::CreatePlayerVehicle(Origin)  <- vtable slot +0x40 (FUN_004bfcac, not in the
         assert-anchored decomp) modelled on RPPlayer::CreatePlayerVehicle: base CreatePlayerVehicle
         (camera ships) then make a Mech via MakeAndLinkViewpointEntity from playerMission->GetGameModel()
         ModelList resource, names = GetBadgeName/GetColorName/((BTMission*))->GetPatchName.
       - BTPlayer::InitializePlayerLink  <- FUN_004bfee0 / RPPlayer analog (PlayerLink dispatch).
       - Mech::MakeMessage ctor (mech.hpp) <- mirror of VTV__MakeMessage entity_ID form + 3 name strings.
       - Mech::Reset(const Origin&,int) (btstubs.cpp) <- MINIMAL of FUN_0049fb74: set localOrigin/
         localToWorld + ForceUpdate. TODO(bring-up): full ~50-field state + per-subsystem reset deferred.
     RESULT: HuntForDropZone->AssignDropZone->DropZoneReply->CreatePlayerVehicle->MakeAndLinkViewpoint->
       BTL4Application::MakeViewpointEntity->Mech::Make->Mech::Mech runs. Spawn pipeline FUNCTIONAL.
  6. AV mech.cpp:407 (Mech ctor) deref Wword(0xbb)+0xc, Wword(0xbb)=0 (engine segment record).
     FIX: guarded the NULL deref (heatLevel default 0). TODO(bring-up) - real value needs the segment
     record wired (see frontier below).
  7. AV mech.cpp:591 (Mech ctor) gyro<->sinkSource cross-link wrote through gyroSubsystem=0xcdcdcdcd.
     ROOT (REAL BUG): the ctor's subsystem-pointer zero-inits at mech.cpp:359-361 targeted the Wword
     SCRATCH BANK (Wword(0x14a/0x10e/0x16d)=0) instead of the real members, so gyroSubsystem/
     sinkSourceSubsystem/mechTechSubsystem were never zeroed -> debug-fill passed the !=0 guard.
     FIX (REAL): zero the real members gyroSubsystem/sinkSourceSubsystem/mechTechSubsystem/sensorSubsystem
     (matches binary @9873-9875). NOT a guard - a genuine init bug.
  8. AV mech.cpp:641 (Mech ctor Pass 2) model = ResourceFind(Wword(0x6f),0xf) -> NULL -> model->base deref.
     <-- CURRENT FRONTIER (ROOT, structural).

### THE FRONTIER = the Mech object-layout / resource-streaming "factory" (deferred big task)
ROOT CAUSE, definitively localised: in `decomp/reconstructed/mechrecon.hpp` the resource-streaming layer
is a set of NO-OP placeholder shims:
  - `ResourceFind(...) -> Recon()` (mechrecon.hpp:86)  [stubs ResourceFile::Find FUN_00407064]
  - `ResourceStream` (mechrecon.hpp:117-119) = template-ctor no-op; SegmentCount()=0 -> 0 subsystems
  - `ResourceFindByName`, `LoadSkeleton`, `WorldTransform`, `ResourceManagerHas` = same no-op shims
  - `Wword(i)` (mechrecon.hpp:188) = a single shared `static BTVal bank[0x400]` SCRATCH bank: every raw
    `this[0xNN]` in mech.cpp reads/writes a throwaway slot, NOT real object memory. ~145 sites in mech.cpp.
CONSEQUENCE: the Mech ctor compiles and runs but never streams its model (segment 0xf), subsystem list
(0x11), damage zones (0x14) or skeleton; `model` is NULL and the model-resource handle `Wword(0x6f)`
(set by the BASE ctor in the binary, an inherited field) is 0. So NO functional mech can be constructed.
Every further mech-ctor fault (407, 591, 641, ...) is one symptom of this same root - guarding them one
by one yields only a hollow, non-functional mech. This is exactly the "THE SUBSYSTEM FACTORY" + "~180 raw
this[0xNN]" item flagged for a dedicated pass (see CLAUDE.md mech-core / Roadmap Track 2).
NEXT (dedicated factory pass, NOT a fault-grind): (a) map the ~145 Wword(0xNN) sites + the segment-record
fields (0xba/0xbb) and model handle (0x6f) to REAL Mech/base members at the documented offsets; (b)
implement ResourceFind/ResourceStream/ResourceFindByName against the real engine ResourceFile::Find over
the BT .res segment format (0x11/0xf/0x14/0x1e/0x1d); (c) verify the ModelResource/SubsystemSegment field
layouts vs the binary; (d) then the per-class subsystem factory (CreateStreamedSubsystem/DefaultData)
actually instantiates subsystems. Oracle = Mech ctor FUN_004a1674 (part_012.c @9696, 5690 bytes).
Files touched this session: decomp/reconstructed/btplayer.{hpp,cpp}, mech.hpp (MakeMessage ctor),
mech.cpp (lines ~359-361 real zero-init, ~406 heat guard), btstubs.cpp (Mech::Reset). Build GREEN, 0 errs.

## ============ STAGE 5: MECH STREAMS + CONSTRUCTS (resource-streaming pass, 2026-06-30 sess 3) ============
GOAL of this pass: replace the no-op resource-streaming shims with REAL engine streaming + map the
raw object-layout fields to real members, then grind faults until the Mech is fully constructed.
RESULT: **the Mech now constructs end-to-end from real streamed resources**, the spawn pipeline +
control-mapping binding complete, and the game advances into mission-entity RENDER setup.

FAULT LADDER CLEARED (each = real reconstruction unless noted):
  8. mech.cpp:641  model = ResourceFind(...) == NULL  (THE documented frontier).  ROOT: mechrecon
     ResourceFind/ResourceStream were no-op shims (SegmentCount()=0).  FIX (REAL streaming): added
     MechFindResource() = application->GetResourceFile()->SearchList(creation_message->resourceID, type)
     -- the exact engine idiom the JointedMover base ctor + RP/VTV use.  Rewrote BOTH passes:
       - Pass 1 (subsystems, segment 0x11 SubsystemModelStreamResourceType): MemoryStream over the
         locked resource; header int = subsystem count; per-blob loop reads Subsystem::SubsystemResource
         via GetPointer()/AdvancePointer(subsystemModelSize).  (mirrors VTV::VTV @1379-1583)
       - Pass 2 (model, segment 0xf GameModelResourceType): model = res->resourceAddress (kept locked).
     `Wword(0x6f)` handle -> creation_message->resourceID (authoritative; base ctor already streamed
     the skeleton from it).  -> model non-NULL, subsystem stream real.
  9. mech.cpp:729  *(float*)(this + 0x15d) = ...  AV.  ROOT (decompiler artifact): typed-pointer
     arithmetic on Mech* (this + 0x15d == this + 0x15d*sizeof(Mech) == +0x89028).  FIX: route to the
     Wword(0x15d/0x15e) scratch bank (the decomp intent is word slot this[0x15d], an unnamed pose angle).
 10. **OBJECT-LAYOUT BUG (the big one):** mech.hpp re-declared `int subsystemCount; Subsystem **subsystems;`
     -- these SHADOWED the inherited Entity base members `subsystemCount` / `subsystemArray` that the
     engine reads in Entity::GetSimulation/GetSubsystem.  The ctor streamed into the shadow members, so
     the engine never saw the roster (GetSimulation returned NULL).  FIX (REAL layout mapping): removed
     the shadow decls; renamed all mech.cpp `subsystems` -> inherited `subsystemArray`; `subsystemCount`
     now resolves to the Entity base field.  this[0x49]/this[0x4a] === Entity::subsystemCount/subsystemArray.
 11. MechTech::MechTech (mechtech.cpp:213) AV walking owner->GetSubsystem(0..count): array TAIL was
     uninitialised heap garbage (loop only zeroed the current slot).  FIX: zero the WHOLE subsystemArray
     right after allocation (every not-yet-filled slot must read NULL for roster scans).
 12. MechTech roster walk AV: the streamed roster entries were HOLLOW stubs (RECON_SUBSYS = empty
     ReconSubsystemStub, no vtable) -> sub->IsDerivedFrom() faulted.  FIX (REAL base wiring): made
     ReconSubsystemStub DERIVE FROM the real MechSubsystem base (proven ctor -- MechTech uses it), so
     every streamed entry is now a genuine registered, vtable-valid Subsystem WITH a DamageZone.
     -> **Mech::Mech now RETURNS (full construction: model + 31 streamed subsystems + MechTech walk).**
 13. btl4app.cpp:554 Fail("Mech has no controls mapping"): primaryControlType==None on a dev box (no
     RIO/Thrustmaster joystick).  FIX (bring-up): default case falls back to MechRIOMapper (the pod's
     real L4 device) instead of Fail.  FLAGGED -- keyboard/gamepad remap is a pod Phase-8 task.
 14. mechmppr.cpp:327 (MechControlsMapper ctor) AV: torso-recenter reads a ptr at the BINARY byte
     offset owner+0x438 then derefs it -- invalid because the RECONSTRUCTED Mech layout != binary's
     0x854.  FIX (bring-up GUARD): skipped (the Mech ctor already identity-inits the torso aim, so it
     starts centered).  FLAGGED -- re-enable via a named accessor once cross-module layout is mapped.
 15. L4CTRL CreateStreamedMappings -> GetSimulation(0) == NULL -> GetAttributePointer on NULL.  ROOT:
     the streamed control-mapping resource binds DirectMappings to subsystemID 0 (the ControlsMapper
     slot, cf. VTV::ControlsMapperSubsystem==0), but Mech::SetMappingSubsystem was a no-op STUB, so
     subsystemArray[0] stayed NULL.  FIX (REAL, mirrors VTV::SetMappingSubsystem): SetMappingSubsystem
     now installs the mapper into subsystemArray[0] (+ mirrors controlsMapper).  -> control binding runs.
 16. btplayer.cpp:225 VehicleDeadMessageHandler AV: scenarioRole->GetReturnFromDeath() on a NULL role
     (scoring role not looked up for the minimal TEST.EGG; btplayer.cpp:815 deferred).  FIX (guard,
     consistent w/ the existing :644 null-role handling): only debit a life when scenarioRole != 0.

### END STATE
The Mech is **fully constructed from streamed resources** and spawned; controls bind; the mission loads
entities and enters the RENDER path.  Boot now reaches:
  "Entity 1:NN classNN couldn't figure out how to MakeEntityRenderables" (the stubbed BT renderable
  module) then AV at **btl4vid.cpp:808** BTL4VideoRenderer::SetupMaterialSubstitutionList ->
  NotationFile::GetEntry -> NotePage::FindNote.  CAUSE: the renderer looks up the mech's egg
  color/badge/patch via entity->GetAttributePointer("VehicleColor"/"VehicleBadge"/"VehiclePatch") -- a
  FLAGGED cross-family gap (those egg strings aren't in the attribute index; the call returns garbage
  that FindNote then derefs).  This is the BT video-renderer / material-substitution module (btl4vid.cpp,
  NOT a mech-construction file) -- the "ideally renders" frontier, a separate reconstruction task.

### REMAINING STUBS / GUARDS introduced or relied on this pass (bring-up worklist):
  - mech.cpp RECON_SUBSYS subsystems: now real MechSubsystem-derived objects but with MINIMAL class
    behaviour (Cockpit/Sensor/Generator/Gyro/Myomers/weapons/... all share the base body).  Per-class
    reconstruction (use the existing sensor.cpp/gyro.cpp/gnrator.cpp/powersub.cpp/emitter.cpp/... real
    classes in the switch) is the next refinement.  6 classes have NO real recon at all (Cockpit,
    Actuator, JumpJet, LegSubsystem, HeatSinkSource, MechDisplay).
  - mech.cpp Pass 3 (damage-zone segment 0x14) + crit-subsystem (0x1e) + standing-anim (0x1d): still use
    the mechrecon shim proxies (damageZoneCount scratch==0 -> loops inert).  Not yet real-streamed.
  - mech.cpp model field reads (Pass 2): write mostly to the Wword scratch bank; the Mech__ModelResource
    struct offsets vs the on-disk GameModel record are unverified (reads are buffer-safe, values unused
    on the critical path).  The capability sub-rosters (heatable/powered/damageable) stay EMPTY because
    the SubProxy::IsDerivedFrom shim returns 0 (no per-frame consumer reached yet).
  - mechmppr.cpp:327 torso-recenter: GUARDED/skipped (layout-dependent raw offset).
  - btl4app.cpp default controls case: MechRIOMapper fallback (no-joystick dev box).
  - btplayer.cpp:225 scenarioRole debit: NULL-guarded (no scoring role in TEST.EGG).
  - btstubs.cpp SetMappingSubsystem: now REAL (slot 0 install).  Remaining btstubs stubs unchanged.

### NEXT (for whoever continues): the render path.  btl4vid.cpp SetupMaterialSubstitutionList needs the
  mech's egg color/badge/patch (resourceNameA/B/C, set in the Mech ctor from the MakeMessage) exposed to
  the renderer -- either via the attribute index ("VehicleColor"/...) or a named Mech accessor -- and the
  VEHICLE table entries to match.  Then the BT renderable module (BTWorldContainer/DCSObject/... ctors,
  currently btstubs) must be reconstructed for MakeEntityRenderables to actually build geometry.

## ============ STAGE 6: RENDER PATH RECONSTRUCTED — mech tree builds + presents (black: asset gap) ============
GOAL: clear the render-path faults until a frame draws (mech in cavern) or reach the deepest render fault.
RESULT: the BT world-render path is reconstructed and runs end-to-end to a STABLE PRESENTING FRAME with no
crash; the mech's 51-segment renderable tree + cockpit camera build correctly.  The frame is currently
BLACK — honest status: nothing visibly draws — but the cause is now precisely localised to a CONTENT
(asset-pipeline) gap, NOT a render-code fault.  Files touched: decomp/reconstructed/btl4vid.{cpp,hpp}.

FAULT LADDER CLEARED (each a real fix):
  R1. btl4vid.cpp:808 SetupMaterialSubstitutionList AV (strcmp NULL) — GetEntry("color",egg_color,..)
      with egg_color from GetAttributePointer("VehicleColor") returns garbage; NotePage::FindNote
      deref-crashed.  FIX (mirrors RP RPL4VID.cpp:1562 which tolerates a missing colour/badge): the egg
      color/badge/patch reads are NULL-guarded and a table miss is no longer fatal (was Fail("Exiting"))
      — veh_color/badge/patch stay NULL and the %placeholder% substitution drops to default materials.
      TODO(bring-up): re-wire the real egg names (Mech::resourceNameA/B/C @0x844/0x848/0x84c) via a named
      accessor once the Mech layout is mapped; the attribute-index path is not wired.
  R2. ARCHITECTURE PIVOT — the recovered MakeMechRenderables built a 1996 pre-DPL renderable hierarchy
      (BTRootRenderable/BTHingeRenderable/... parenting on raw dpl_DCS*) on top of VideoComponent, which
      WinTesla never implemented (it was superseded by VideoRenderable; btstubs.cpp had to stub
      VideoComponent + every BT*Renderable just to link → all inert → "couldn't MakeEntityRenderables").
      FIX (the real work): REWROTE BTL4VideoRenderer::MakeMechRenderables to build the tree with the
      engine's WORKING D3D renderables — RootRenderable / HingeRenderable / BallJointRenderable /
      BallTranslateJointRenderable / DPLStaticChildRenderable / DPLEyeRenderable (MUNGA_L4/L4VIDRND) —
      exactly as RP's port does (RPL4VideoRenderer::MakeJointedMoverRenderables / RecurseSKLFile).  These
      self-register (RootRenderable→AddRenderable; VideoRenderable→AddDynamicRenderable), build their own
      DCS, parent on the PARENT RENDERABLE (HierarchicalDrawComponent*, not a dpl_DCS*), and hook the root
      to entity->localToWorld.  Walks the EntitySegment table; per segment loads geometry via
      d3d_OBJECT::LoadObject(GetDevice(), "<name>.bgf"), picks Hinge/Ball/BallTranslate/Static by joint
      type, and builds the cockpit camera (DPLEyeRenderable→mCamera) at the "siteeyepoint" site.
      MakeMechRenderables return type VideoComponent* → HierarchicalDrawComponent* (.hpp updated); the
      caller no longer double-registers via the AddDynamicRenderable stub.  The BT*Renderable stubs in
      btstubs.cpp are now DEAD CODE for the body (kept for link; the 2D reticle path still references the
      BTReticleRenderable stub but that block is deferred — see below).
  R3. _RTC_UninitUse at L4VIDEO.cpp:5211 (DPLRenderer::MakeEntityRenderables) — exposed when the default
      (non-mech) case was routed to DPLRenderer::MakeEntityRenderables.  ROOT (pre-existing ENGINE port
      bug): `this_instance` is used uninitialised (the DPL-stubbed engine left its assignment commented
      out) and /RTC1 traps it.  FIX (faithful + safe): the default case calls the generic base
      VideoRenderer::MakeEntityRenderables (FUN_0045e744) — which is exactly what the shipped BT renderer
      did (it deferred to the grandparent "couldn't figure out how to MakeEntityRenderables" handler, NOT
      the DPL prop builder).  Non-mech props/terrain pieces therefore log + draw nothing for now.
      TODO(bring-up): route world props through DPLRenderer::MakeEntityRenderables once that engine
      uninitialised-`this_instance` gap is fixed (out of scope here — shared engine source).

END STATE / VERIFIED BY DIAGNOSTICS (btl4.log, flagged [BTrender] lines):
  "[BTrender] MakeMechRenderables: 51 segments, view=1"        <- render path runs, inside (cockpit) view
  "[BTrender]   no mesh for 'blx_cop.bgf' (expects VIDEO\*.x)" <- geometry load FAILS
  "[BTrender] mech tree built: meshes 0/1 loaded, eye=1"       <- tree + camera built OK; 0 meshes loaded
  Window: 804x600 "BattleTech", stable, ALIVE@20s+, no crash, captured frame = solid black.

DEEPEST RENDER FAULT REACHED = ASSET PIPELINE GAP (not code):
  d3d_OBJECT::LoadObject (MUNGA_L4/L4D3D.cpp:47) loads `VIDEO\<name>.x` via D3DXLoadMeshFromXA — i.e. the
  WinTesla D3D renderer consumes Direct3D .x meshes produced by the port's MeshExp exporter.  The BT pod
  content tree ships 879 `.bgf` (DIV-BIZ2) files and ZERO `.x` files (VIDEO\ + VIDEO\GEO\).  So EVERY
  segment mesh resolves to NULL → renderables are built with a NULL graphicalObject → nothing is drawn →
  black clear.  Inside (cockpit) view requests only the cockpit shell (blx_cop); the 50 body segments have
  no inside-view object name (correct — you're inside the mech).  To actually SEE geometry the .bgf models
  must be exported to .x into VIDEO\ (the MeshExp/ASSET_PIPELINE task), and/or use the OUTSIDE view to load
  the full body.  Terrain/cavern likewise needs either the prop path (R3 TODO) or environment build.

REMAINING STUBS/GUARDS in the render path (bring-up worklist):
  - SetupMaterialSubstitutionList: egg color/badge/patch forced NULL (default materials).  TODO: wire real
    Mech::resourceNameA/B/C.
  - Inside-view targeting reticle (BTReticleRenderable + AddWeapon pips) and per-subsystem weapon/effect
    renderables (PPC/emitter beams, projectile tracers, coolant, searchlight) NOT built — they depend on
    the unported dpl2d_ 2D layer (btstubs.cpp, all inert) + the BT effect renderables (btstubs.cpp).  These
    are the HUD/weapons render bring-up; not needed for the mech body.
  - dpl2d_* (9 fns) + dpl_SetMaterialNameCallback: still inert stubs (btstubs.cpp).
  - Non-mech entity default → VideoRenderer base (no-op + log); props/terrain not rendered (R3 TODO).
  - [BTrender] DEBUG_STREAM diagnostics left in MakeMechRenderables (low volume; remove at cleanup).

================================================================================
PASS: .BGF MESH LOADER (close the asset-pipeline gap so meshes actually load)
================================================================================
PROBLEM (from prior pass): d3d_OBJECT::LoadObject loaded VIDEO\<name>.x via
D3DXLoadMeshFromXA, but the pod ships 879 original .bgf (DIV-BIZ2) models and ZERO
.x -> every segment mesh resolved NULL -> "meshes 0/1 loaded" -> black frame.

FIX (engine mesh loader now reads .bgf natively):
  - NEW MUNGA_L4/bgfload.{h,cpp}: C++14 port of the proven port/src/bgf.cpp loader.
    Parses DIV-BIZ2 (OBJECT->LOD->PATCH->PMESH; quads triangulated; per-PATCH
    SV_SPECIAL material name), resolves .bmf material libs + texture image basenames,
    produces L4VERTEX-compatible verts (XYZ|NORMAL|DIFFUSE|TEX1) + 32-bit index list
    + per-material draw batches. Directory scanning uses Win32 FindFirstFile (NOT
    <filesystem>) so it builds under the engine's C++14 target. Asset resolution:
    recursively indexes VIDEO\ (covers VIDEO\GEO, VIDEO\GEO\<theme>, VIDEO\MAT...),
    basename(lower)->path; LoadObject("blx_cop.bgf") -> VIDEO\GEO\BLX_COP.BGF.
    Geometry is emitted DOUBLE-SIDED (both windings) so it is visible regardless of
    the engine's global D3DCULL_CW (no .x exporter survives to confirm winding);
    smooth normals computed from the forward winding only.
  - L4D3D.cpp: new static d3d_OBJECT::LoadObjectBGF — builds an ID3DXMesh
    (D3DXCreateMeshFVF, MANAGED|32BIT, L4VERTEX_FVF), fills VB/IB, writes one
    attribute id per face = batch index, GenerateAdjacency + OptimizeInplace(ATTRSORT)
    to build the attribute table, then constructs the mesh-backed d3d_OBJECT and fills
    mDrawOps per batch: material Diffuse(+Ambient) from the resolved .bmf color, and a
    decoded texture (port/src/image.cpp decodeImage -> D3DFMT_A8R8G8B8 managed tex).
    LoadObject now falls through to LoadObjectBGF when D3DXLoadMeshFromXA fails (the
    .x path is kept as a fallback), before LoadSpheres.
  - port/src/image.cpp (tga/vtx/bsl decoder, C++14-clean) added to the engine build;
    enginebuild/CMakeLists.txt: + bgfload.cpp, + image.cpp, + port/src include dir
    (no header-name collisions with the engine; verified).

VERIFIED (btl4.log, flagged [BTrender]/[BTdraw] lines):
  "[BTrender] mech tree built: meshes 1/1 loaded, eye=1"   <- inside view: cockpit
        shell blx_cop.bgf now LOADS (was 0/1). N>0 achieved.
  Forced-outside experiment (skeletonType_N, reverted): "meshes 12/12 loaded"
        -> loader handles the full mech body too (12 segments w/ geometry, 0 failures).
  [BTdraw] diagnostic (added then removed): opaqueList=1, batches=9, every frame
        -> the cockpit mesh REACHES the opaque pass list and issues 9 DrawSubset calls
        per frame. The load->D3D-buffer->rasterize pipeline works end to end.

FRAME STATE: still BLACK in the default inside view (captured the BattleTech client
        region via screen grab; solid black even with materials forced fully Emissive
        and with the full body loaded). So this is NOT a loader/lighting problem -- the
        geometry IS rasterized but projects outside the eyepoint camera's frustum.
        ROOT (out of scope here, R2/R3 render-tree + camera): the inside view places the
        camera at the cockpit "siteeyepoint"; the player's own cockpit shell / body sit
        at/behind that eyepoint, and there is NO world to see ahead (terrain not built;
        other entities still hit the default VideoRenderer base = "couldn't figure out
        how to MakeEntityRenderables", R3 TODO). Original BT drew the world through the
        canopy + a 2D gauge HUD (dpl2d, unported). So black is expected for the current
        port state -- a VISIBLE mech needs either an external/chase camera looking at the
        body, or routing enemy JointedMovers through MakeMechRenderables so a mech renders
        ahead. Neither is the asset pipeline; both are render-tree/camera work.

HONEST MILESTONE READ: the asset-pipeline gap is CLOSED (meshes load: 1/1 cockpit,
        12/12 body; rasterized 9 batches/frame). The window is still black because the
        only loaded geometry is the player's own shell behind the eyepoint with no world
        ahead -- a camera/world-render gap, not a mesh gap. Per the strict milestone
        ("window shows geometry, not black"), the on-screen mech is NOT yet achieved.

REMAINING GAPS (next pass, in priority order):
  - Camera/visibility: add an external view (or capture an enemy mech) so loaded body
    geometry lands in the frustum; verify winding (currently double-sided as insurance).
  - Textures: most BT materials resolve to .bmf diffuse COLORS; only ~47 .vtx/.bsl exist
    in the tree, so most batches are flat-colored (no texture). Texture coverage + the
    egg color/badge/patch substitution (SetupMaterialSubstitutionList, still NULL-forced)
    remain for a faithful skin.
  - World/props/terrain rendering (R3) + 2D cockpit HUD (dpl2d_, btstubs inert).

---

## MECH VISIBLE ON SCREEN — first rendered BattleMech (external chase camera)

**WIN:** `btbuild/run/frame.png` shows a flat-shaded Avatar BattleMech (torso, both arms +
gun pods, both legs + feet/toes, hip) centered in the BattleTech window. 12/12 body meshes
assembled and rasterized.

### What was wrong (diagnosis chain)
1. The default player view is `insideEntity` -> `MakeMechRenderables` built the INSIDE
   skeleton (SkeletonType_A) and put the camera AT the cockpit eyepoint; no body geometry,
   nothing ahead -> black.
2. Forcing the OUTSIDE body + adding an external camera still gave black. Instrumented
   `DPLEyeRenderable::Execute` (L4VIDRND.cpp): camera WAS correct — ~29 units in front of the
   mech (mech localToWorld vs camera pos), look-dir within ~5deg of the mech centre.
3. Instrumented the opaque pass: the 12 mech body meshes WERE submitted (DrawSubset) with
   correct world matrices clustered at the mech's world position. Still black. CULLMODE=NONE
   did not help -> not backface culling.
4. **Root cause:** logged the device transforms at draw time — the PROJECTION matrix was
   uninitialised garbage (every entry ~ -4.3e8). `BTL4VideoRenderer::LoadMissionImplementation`
   was an **empty stub** (btstubs.cpp), so `DPLReadEnvironment` never ran and
   `DPLRenderer::mProjectionMatrix` (a private member) was never built -> all geometry clipped.
   Compounded by `D3DXMatrixPerspectiveFovLH` mismatching the `D3DXMatrixLookAtRH` view.

### Changes made
- `decomp/reconstructed/btl4vid.cpp` `MakeMechRenderables`:
  - For the player's own mech (originally `insideEntity`) force the OUTSIDE body build
    (loads all 12 body meshes) and, after the tree is built, install a fixed external chase
    camera: `DPLEyeRenderable` parented to `this_root` with a hand-built `LinearMatrix`
    offset (camera at mech-local (0,9,-28), looking at (0,6,0); +Z=look, +Y=up, W=pos).
- `MUNGA_L4/L4VIDEO.cpp`:
  - `DPLReadINIPage` projection: `PerspectiveFovLH` -> `PerspectiveFovRH` (match the RH view),
    dropped the `mProjectionMatrix(0,0)*=-1` X-flip.
  - Added `DPLRenderer::EnsureValidProjection()` (decl in L4VIDEO.h) — builds a valid RH
    projection from the private members + bright ambient + fog off.
  - Opaque pass: `CULLMODE=NONE` and `LIGHTING=FALSE` (flat-shaded so the mech shows without
    scene lights).
- `decomp/reconstructed/btstubs.cpp`: `BTL4VideoRenderer::LoadMissionImplementation` now calls
  `EnsureValidProjection()` instead of being empty.
- Temporary diagnostics still present (gated to first few calls): `[EYE]`, `[DRAW]`, `[VP]`
  logging in L4VIDRND.cpp / L4D3D.cpp / L4VIDEO.cpp.

### Remaining work for a proper external view
- The current camera is a hard-coded DEBUG chase cam forced on the player mech. A real
  spectator/chase view-mode should be a toggle wired through `BTL4Application` (view mode),
  not a forced override inside `MakeMechRenderables`.
- Mech sits slightly low/small; tune offset (raise target, pull closer) or frame from the
  mech bounding box.
- `EnsureValidProjection` + opaque-pass `LIGHTING=FALSE`/`CULLMODE=NONE` are debug shortcuts.
  Proper fix: implement the real `LoadMissionImplementation` (call `DPLReadEnvironment`) so
  the per-mission viewangle/clip/fog/ambient/lights/paths from BTDPL.INI are honoured, then
  restore lighting + correct cull winding (and verify the RH projection against RP).
- Strip the `[EYE]`/`[DRAW]`/`[VP]` diagnostics once stable.
- Intermittent: ~1 run in 2 crashes during the segment loop before "mech tree built"
  (pre-existing reconstructed-code instability, unrelated to the camera/projection fix).

## ============ STARTUP-CRASH FIX (the intermittent ~1-in-6 heap corruption) ============
Symptom: exit 0xC0000374 (STATUS_HEAP_CORRUPTION), intermittent (2/12 runs), in the Mech ctor
subsystem loop. Diagnosis: debugger masked it (corruption, not AV); nailed via **gflags PageHeap**
(/p /enable btl4.exe /full) which faults at the exact bad access.
ROOT (two object-layout overflows in mech.cpp's subsystem construction):
  1. mech.cpp:654 HeatableSubsystem allocated only **0x104** bytes, but the shared MechSubsystem base
     ctor WRITES this[0x44] (byte 0x110, vitalSubsystemIndex) -> 16-byte write past the allocation =
     heap corruption. FIX: bumped to 0x140 (base extent is 0x114; all other subsystems were already
     >=0x130). THIS was the corruption. (PageHeap caught it at mechsub.cpp:161.)
  2. mech.cpp:497 the subsystem-resource MemoryStream wrapped the .res segment at exact size; the base
     ctor reads each record to res+0xe0, overrunning the buffer for short/last records (controls mapper).
     FIX: stream over a zero-padded (+0x100) copy. (Read-overflow; PageHeap caught at mechsub.cpp:165.)
VERIFIED: PageHeap off, **30/30 runs clean, 0 crashes** (was ~17%). Crash fixed.
RESIDUAL (NOT the crash; latent over-reads PageHeap flags but that read valid adjacent heap in normal
runs, e.g. mech.cpp:741 reading +0x108): object-layout debt -> fold into the Mech layout reconstruction.
Tooling note: gflags needs an ELEVATED prompt; user enabled/disabled. btl4main.cpp has a dormant
BT_HEAPCHECK #ifdef (CRT heap validation) for future hunts.

## ============ WORLD/TERRAIN VISIBLE — mech now stands in the cavern(des) world ============
**WIN:** `btbuild/run/world.png` shows the player mech standing on TEXTURED DESERT TERRAIN
(the `cavern`->`des_day` environment) with a horizon + midground structures, instead of the
black void. 3/3 stability runs clean, no crash. Engine + game both build green.

### Diagnosis chain
1. Set `L4DPLCFG=BTDPL.INI` + wired the real env load (see changes) -> the DPL per-entity
   builder began processing world props (`bld08_lp`, `dish_lp`, `mslr_LP`, `ctar/dtar/calp_lp`)
   but the run then crashed (0xC0000005) in `DPLRenderer::ConsolidateSingleObject`
   (L4VIDEO.cpp:2138), `mov eax,[edx]` edx=0.
2. ROOT: the pod ships `.bgf`, loaded by `d3d_OBJECT::LoadObjectBGF` with **D3DXMESH_32BIT**
   (L4D3D.cpp:212). `ConsolidateStaticObjects` (the post-load static-mesh merge) called
   `D3DXConcatenateMeshes` with only `D3DXMESH_MANAGED` (16-bit output) -> concatenating
   32-bit-index source meshes into a 16-bit output FAILS, leaving `outMesh==NULL`, then
   `outMesh->GetNumFaces()` dereferenced NULL. RP never hit this (its world loads from `.x`
   as 16-bit). This static-consolidation path had NEVER run for BT before, because the world
   default-case was previously a no-op (no static objects existed to consolidate).

### Changes made (all in owned files)
- `btbuild/btl4main.cpp`: added `putenv("L4DPLCFG=BTDPL.INI")` (alongside the other env
  defaults) so `DPLReadEnvironment` reads BT's real renderer config (paths/clip/fog/lights/
  ambient/background/cached shapes), not `dpldflt.ini`. (RP set `RPDPL.INI`.)
- `decomp/reconstructed/btstubs.cpp` `BTL4VideoRenderer::LoadMissionImplementation`: now calls
  `DPLRenderer::LoadMissionImplementation(mission)` (-> `DPLReadEnvironment` + `LoadNameBitmaps`,
  exactly like `RPL4VideoRenderer::LoadMissionImplementation`), then `EnsureValidProjection()`
  as a safety net (mProjectionMatrix is private here; this guarantees a valid RH projection +
  fog off so the mech/terrain are not clipped/fogged). Was previously an empty stub that only
  called EnsureValidProjection -> env never loaded.
- `decomp/reconstructed/btl4vid.cpp` `MakeEntityRenderables` default case: route non-mech
  entities (terrain/props/landmarks) to `DPLRenderer::MakeEntityRenderables` (the DPL prop
  builder) instead of the no-op `VideoRenderer::MakeEntityRenderables` grandparent -- this is
  what RP's default does (RPL4VID.cpp:1436) and is what actually loads world `.bgf` into the
  scene.
- `MUNGA_L4/L4VIDEO.cpp`:
  - `ConsolidateSingleObject`: pass `D3DXMESH_MANAGED | D3DXMESH_32BIT` to
    `D3DXConcatenateMeshes` (match the 32-bit source meshes); init `outMesh=NULL` and return
    NULL on failure with a log line instead of dereferencing NULL. Both callers in
    `ConsolidateStaticObjects` now skip a NULL result. **This was the crash fix.**
  - `MakeEntityRenderables` CulturalIcon/Landmark arm: initialise `dpl_INSTANCE *this_instance
    = NULL` (the per-object instance assignment is DPL-stubbed/commented out) so the /RTC1
    uninitialised-variable check can't fault if a CulturalIcon/Landmark entity is built.

### What renders / what doesn't
- RENDERS: desert ground/terrain (textured), horizon, midground structures, the player mech
  positioned on the terrain at world ~(275,-0.35,48). Static terrain meshes consolidate cleanly
  (no D3DXConcat-skip log).
- NOTE: `BT/VIDEO/GEO/CAVERN/` is EMPTY in this content tree; `cavern` branches to `des_day`
  in BTDPL.INI, so the "world" is the desert/arena prop set in `VIDEO/GEO/` (buildings, dishes,
  missile launchers, targets) -- which the recursive `.bgf` index finds and loads.
- STILL FAILS (harmless): 28 `*_LP.bgf` (low-poly distant-LOD variants: bld08_lp/dish_lp/
  mslr_lp/ctar_lp/dtar_lp/calp_lp). Their high-poly counterparts (BLD08/DISH/MSLR/CALP) ARE
  present and load; the `_LP` files are tiny (216-750B) valid DIV-BIZ2 that the bgf parser
  (bgfload.cpp) rejects -- this is the pre-existing BGF-loader gap (CLAUDE: 621/663 load), NOT
  a terrain-path regression. Fixing the LOD parse would add the distant-LOD billboards.

### Remaining / next
- Capture gotcha (for future sessions): the D3D9 window is windowed; `PrintWindow` returns
  BLACK (can't read the D3D backbuffer) and plain `CopyFromScreen` grabs whatever occludes it.
  WORKS: `SetWindowPos(h, HWND_TOPMOST, 0,0,820,640, SWP_SHOWWINDOW)` then `CopyFromScreen` at
  the client rect -> real frame.
- The chase camera + `EnsureValidProjection`/opaque-pass `LIGHTING=FALSE`/`CULLMODE=NONE` are
  still the debug shortcuts from last pass; now that the env loads, the INI lights/fog/ambient
  could be honoured (restore lighting + correct cull winding) once verified against RP.
- Fix the `_LP` BGF parse (bgfload.cpp) to add distant-LOD props; wire the dpl2d_ HUD.

---

## Tier 2 — DRIVABLE mech (locomotion + camera follow)  [this session]

**WIN: the player mech walks under control and the chase camera follows.**
Verified with forced throttle (headless): position log Z 493.156 -> 160.363
(monotonic, ~30 u/s at full throttle), and the per-render `[EYE] Execute
entityL2W.t` tracks the moving mech each frame (camera follows by
construction). Two warm screenshots (`run/walk_t1.png`, `run/walk_t2.png`) show
the mech centered while the background structures shift. Default (no-throttle)
run is stable: mech spawns idle, no drift, no crash.

### Root cause: mech rendered but never SIMULATED
The mech rendered every frame (EYE/DRAW) but its `localToWorld` was static. The
reconstructed per-frame tick (`Mech::Simulate` via `activePerformance`, set from
a `Recon`->null member-fn-ptr, and full of `this+0xNN` byte-offset-as-index
bugs) is unsafe and was never reached. Two engine gates also block a fresh
master entity from being executed:
1. `Entity::Execute` only calls `PerformAndWatch` when the app is in
   `RunningMission`/`EndingMission` **or** the entity `IsPreRunnable()`
   (ENTITY.cpp ~549). The bring-up never reaches RunningMission.
2. `UpdateManager::Execute` skips a master unless `IsInteresting()`
   (`interestCount!=0`) `&& IsNonReplicantExecutable()` (UPDATE.cpp:148).

### What was changed (4 files; engine untouched)
- `decomp/reconstructed/mech.hpp` / `mech4.cpp`: added
  `Mech::PerformAndWatch(const Time&, MemoryStream*)` — an **override of the
  engine virtual** `Simulation::PerformAndWatch` (the director calls it every
  frame on every entity; stasis/subsystem/activePerformance machinery lives in
  the *base* `Mover::PerformAndWatch`, which we deliberately bypass). It reads
  the drive input, integrates the engine-base `localOrigin` (turn -> yaw about
  world +Y; forward = mech-local -Z from the rebuilt matrix; advance in the
  ground plane preserving Y) and rebuilds `localToWorld` — the matrix the render
  tree AND the chase camera (btl4vid.cpp) are bound to. Bypasses the unsafe
  Simulate/subsystem chain (that's a later step). Logs world pos at 1 Hz while
  moving. Tuning: 30 u/s full throttle, 1.2 rad/s full turn (hand-picked; the
  animation `.ani` `[RootTranslation]` clip-speed path is future work).
- `decomp/reconstructed/btstubs.cpp`: `Mech::Reset` (spawn hook) now sets
  `SetPreRunFlag()` + forces `interestCount = 1` so the update manager ticks the
  mech regardless of mission-run state. Added `Mech::IsDisabled()` stub
  (return False) — was declared but undefined; the new vtable pulls mech2/mech4
  into the link which referenced it.
- `btbuild/btl4main.cpp`: keyboard drive input wired in `WndProc`
  (WASD / arrow keys -> `gBTDrive.throttle`/`turn`). `gBTDrive.forced` is a
  DEBUG toggle (env `BT_FORCE_THROTTLE`, default OFF) used only for headless
  verification (full-ahead, no keypress).

### Notes / caveats (flagged)
- Single local-player assumption: heading is a file-static in mech4.cpp. AI/
  multiplayer mechs would need per-entity state (out of scope; Phase 7/8).
- Idle (no input) the mech re-asserts yaw=0 each frame, overriding any egg
  spawn facing (minor; documented). Position is preserved.
- The bypass skips collision + the real animation-driven gait/`SequenceController`
  clips. Next: drive `commandedSpeed` through the controls-mapper subsystem and
  integrate the `.ani` `[RootTranslation].z` per-clip speed (the established
  animation-driven locomotion model) instead of a flat tuning constant.
- Render is slow in the Debug build (~0.6 fps on the big desert scene); the
  simulation runs ~real-time (dt~0.017). Capture gotcha unchanged: grab a WARM
  frame (>=8 s) — early frames are black (backbuffer not presented yet).

---

## Wave: per-frame SUBSYSTEM SIMULATION TICK wired into Mech::PerformAndWatch

Goal: turn the drivable mech into a live combat harness by ticking the mech's
subsystem roster every frame, WITHOUT calling the unsafe reconstructed
`Mech::Simulate` (raw `this+0xNN`-as-index bugs, null `activePerformance`).

### What was wired (how the tick reaches the subsystems)
Mirrored the shipped engine's `Entity::PerformAndWatch` roster loop
(`MUNGA/ENTITY.cpp` ~698-790; RP's `VTV` ticks its roster the identical way).
In `Mech::PerformAndWatch` (mech4.cpp), AFTER the existing drive/movement
integration and BEFORE `WriteSimulationUpdate`, we now walk the inherited Entity
roster `subsystemArray[0..subsystemCount-1]` and, for each non-null subsystem,
call the **engine** virtual `subsystem->PerformAndWatch(till, update_stream)`.
That base (`Simulation::PerformAndWatch`, SIMULATE.cpp:448) computes the
subsystem's OWN time slice from its own `lastPerformance` and dispatches
`activePerformance` — the reconstructed `<Class>::<Class>Simulation` method each
subsystem's ctor installed via `SetPerformance` (heat/gyro/sensor/power/weapons/
…). We deliberately do NOT call `Mech::Simulate`. Guards: null check +
`IsNonReplicantExecutable()` (mirrors Entity).

### Per-frame confirmation (verified live, headless, BT_FORCE_THROTTLE=1)
btl4.log over a 30 s run (cwd = pod REL410\BT, `-egg TEST.EGG`):
- `[tick] first frame: dispatched to 30 executable subsystem(s) of 30 present / roster 33`
  → the dispatch genuinely reaches **all 30 real subsystems** on frame 1.
- `[tick] subsystems simulated: 1 (executable) of 30 present / roster 33` (1 Hz, steady).
  After frame 1 the subsystems still on the engine default `DoNothingOnce`
  latch `NeverExecute` (DontExecuteFlag) on their first call and drop out of
  `IsNonReplicantExecutable()` — **faithful engine behavior**. The persistent
  N=1 is the single subsystem with an always-executable real Performance. As the
  upcoming per-subsystem wave deepens each subsystem (its ctor calls
  `SetPerformance(realMethod)`), it automatically stays executable and the live
  count climbs — the tick path is already wired to carry it.
- Driving NOT regressed: `[drive] pos=...` advances on Z at ~30 u/s·dt each frame.
- Stability NOT regressed: full 30 s run, no fault (re-verified under cdb).

### Roster shape (from the run)
roster 33 = slot 0 (controls mapper, skipped — see below) + slot 1 (resolved
voltage bus) + 30 streamed subsystems + (slots that streamed NULL). 30 present,
all ticked frame 1.

### One subsystem we could NOT safely reach (flagged, skipped by design)
The **controls-mapping subsystem** (`MechRIOMapper`/`MechControlsMapper`,
installed at roster slot 0 by `Mech::SetMappingSubsystem`, mirrored to the
`controlsMapper` member). Initial wiring crashed here on the FIRST tick:
`MechControlsMapper::FillPilotArray` (mechmppr.cpp:779) does
`pilotIDs[0] = *(int*)(local_pilot+0x1e0)` where `local_pilot` is read through
UNDOCUMENTED WinTesla `Application` byte offsets (`application+0x6c → +0x190`)
that don't exist in the port → wild pointer → 0xC0000005 (its own null guards
can't catch garbage-non-null). This is an **engine gap**, not a stub.
Resolution: skip `subsystem == controlsMapper` in the tick loop. Justified —
in bring-up the player drives the mech DIRECTLY via `gBTDrive` in this same
override, so the input mapper is bypassed by design (the "bring-up RIO fallback"
path). Reaching it safely = pod-bring-up / input-remap work (CLAUDE.md Phase 8).
All OTHER subsystems tick normally.

### Gait bonus (animation-driven speed): SKIPPED (flagged)
Not landed — not quick. The flat 30 u/s `kDriveMaxSpeed` slide remains. Wiring
`.ani [RootTranslation].z` per-clip speed (as `port/` viewer does:
`g_walkPos += dt*forwardSpeedAt(anim,t)`) needs either (a) independently loading
the mech's walk `.ani` and exposing its root-translation track, or (b) calling
the reconstructed `Mech::AdvanceBodyAnimation` (which lives inside the bypassed,
still-unsafe `Simulate`/`IntegrateMotion` chain). Both are a dedicated step, not
a quick add. Deferred to the locomotion/gait wave.

### Files changed (4; engine untouched)
- `decomp/reconstructed/mech4.cpp`: added the subsystem-roster tick loop +
  `#include <subsystm.hpp>`; controls-mapper skip; 1 Hz + one-shot first-frame
  tick logs; new file-statics `gTickLogAccum`, `gTickFirstLogged`.
- `decomp/reconstructed/mech.hpp`, `mechsub.*`, `btstubs.cpp`: not modified
  (the roster + `controlsMapper` member + `IsNonReplicantExecutable` were all
  already present/sufficient).

Build: `bt410_l4` + `btl4.exe` green (only the pre-existing /FORCE:MULTIPLE
LNK4006/4088 warnings). Engine lib untouched.

## COMBAT WAVE — agent 4/7 (sensor, mechtech) — DONE, both compile-clean (exit 0)

Per-frame TICK behavior verified against binary oracle + surviving headers. Both subsystems
install a real `activePerformance` in their ctor, so neither falls back to `DoNothingOnce`
(they tick every frame, not just frame 1).

### Sensor (classID 0xBC3, ctor @004b1d18, vtable @0050fb0c)
- **Behavior reconstructed:** `SensorSimulation(Scalar)` @004b1c4c — faithful to oracle
  (part_013.c:1823). Runs `PoweredSubsystem::PoweredSubsystemSimulation` first, then derives
  the radar-effectiveness readout: `radarPercent = 1.0 - heatMaster.heatEnergy`; zeroed +
  selfTest cleared when thermal model off; `badVoltage`/blind when electrical state != Ready;
  heat-state alarm trims radar (Normal→ok, Degradation→×0.5, Failure→0). This IS the whole of
  the Sensor subsystem's per-frame work in the binary.
- **NOTE on "target sweep/acquisition/lock-on":** confirmed NOT in this class. Oracle
  FUN_004b1c4c does only the radar-effectiveness model above; target acquisition/lock/range
  lives in the HUD/radar brick (other family), not the Sensor subsystem. Did not invent it.
- **SetPerformance wired:** ctor calls `SetPerformance(&Sensor::SensorSimulation)` gated on
  `(GetSegmentFlags() & 0xC) != 4` (damaged-copy segments don't tick) — matches oracle
  `param_1[7]=PTR_FUN_0050fa94`. Also sets has-performance instance flag bit 0x8.

### MechTech (classID 0xBDC, ctor @004ad228, vtable @0050e3a0)
- **Behavior reconstructed:** `TechnicalAssistance(Scalar)` @004ad33c — faithful to oracle
  (part_012.c:16435). Iterates `subsystemMonitors`; per monitor reads watched
  `MechSubsystem::GetStatusFlags()` (vt+0x30), loops the 7 TechStatusTypes, clears expired
  wooHoo latches, edge-detects each status bit, and posts cleared/set cockpit messages on
  change (set carries `alarmModel`). Monitor roster is built in the ctor over damageable
  segments (segment bit 0x8). Faithful to oracle ctor flag gate `(entity+0x28 & 0xC) == 0`.
- **SetPerformance wired:** ctor calls `SetPerformance(&MechTech::TechnicalAssistance)` for
  live primaries; copies/templates flag dormant (this[10] |= 2) and do NOT install a
  Performance — matches oracle (MechTech sets no has-performance bit, just the perf pointer).

### CROSS-FAMILY NEEDS (locally shimmed; real homes are shared bases — unchanged from Stage 1)
- **HeatSink (heat family):** `HeatStateLevel()`, `HeatModelOff()`, `heatEnergy` accessor —
  Sensor backs these locally (heatStateLevel/heatModelOff/heatEnergy members) pending real
  inherited accessors. SensorSimulation reads them; base sim must write them once exposed.
- **PoweredSubsystem (power family):** `ElectricalStateLevel()` — backed locally (electricalState).
- **Mech/Entity/Subsystem (mech family):** `GetSegmentFlags()`, `GetSubsystemCount()`,
  `GetSubsystem(i)`, `MechSubsystem::damageZone`/`GetStatusFlags()`, `Entity::simulationFlags`.
  Currently satisfied via the shared mechrecon.hpp compat shim + entity accessors.

### Stubs (cross-family, revisit at integration — unchanged from Stage 1)
- MechTech cockpit message dispatch: `StatusMessageSink()` returns 0,
  `ReportStatusCleared`/`ReportStatusSet` are no-ops. Real impl routes through
  `*(theApplication+0x38)` + HUD message builders (FUN_004366b8/00436688/004364e4) in the
  btl4gaug/app brick (not this family). Edge-detection + latch-clear in TechnicalAssistance
  are live; only the message emission is stubbed.
- `MechTech::GetTime()` returns `Now()`; oracle compared the subsystem's cached current-time
  field (this+0x10). Behaviorally equivalent for latch expiry; revisit if a per-frame cached
  clock is wired.

## From combat/energy-weapons (mechweap, emitter; +surviving PPC/GAUSS) — DONE, 0 errors

OWNS the energy/beam weapon family: MechWeapon (abstract base), Emitter (charge/discharge beam base),
PPC + GaussRifle (surviving real source, `: public Emitter`). compile1.cmd mechweap.cpp + emitter.cpp
both exit 0.

### Per-weapon fire/heat behavior + SetPerformance wiring

- **MechWeapon (base)** — abstract; `FireWeapon()` is the "should not be fired" assert stub (matches
  oracle @004b95ec / RP GenericGun). No own performance: ctor @004b99a8 only chains to PoweredSubsystem
  + installs vtable PTR_FUN_00511d2c (verified in oracle — does NOT write this[7..9]). Per-frame
  helpers live (UpdateTargeting heat-degrades effectiveRange = (1-heatLoad)*weaponRange; CheckFireEdge
  rising-edge on fireImpulse; recharge/heat-cost fields parsed from resource). Relies on a derived
  class to install the tick → correct, since only Emitter/ProjectileWeapon are instantiable.

- **Emitter (energy base) — FIXED the missing tick wiring.** The ctor @004bb120 was mis-reconstructed
  as choosing a "fire message template" (SetFireMessage). The binary oracle proves `this[7..9]` is the
  `Simulation::activePerformance` member-pointer (same slot every sibling fills via SetPerformance) and
  the ctor installs ONE of two performances, resolved from section_dump:
    - default            : member-ptr @00511e84 = `88 aa 4b 00` -> **0x004baa88 = EmitterSimulation**
    - external model (flags&0xC==4): @00511e90 = `d0 a8 4b 00` -> **0x004ba8d0 = ServiceDischarge**
  Replaced the stub with `SetPerformance(&Emitter::EmitterSimulation)` / `&Emitter::ServiceDischarge`.
  Without this the chained PoweredSubsystem ctor's PoweredSubsystemSimulation would tick instead and the
  Loading->Loaded->Firing charge/fire machine would never run (would drop out via DoNothing-style after
  frame 1). EmitterSimulation (the full state machine), FireWeapon (slot 18, vtables.tsv 005120b8 col3 =
  4bace8 — confirmed: E=½·currentLevel²·energyCoefficient split by damageFraction into damage + self-heat,
  beam build muzzle->target, pip), TrackSeekVoltage (charge integrate + I²R heat back into source),
  ServiceDischarge / ContinueDischarge / ResetFiringState / ComputeOutputVoltage / ReadyToDischarge all
  reconstructed and live. Removed the dead SetFireMessage stand-in + the two fire-template statics.

- **PPC (surviving source, unchanged)** — ctor chains to Emitter ctor → inherits EmitterSimulation tick.
  `FireWeapon()` tail-calls `Emitter::FireWeapon()` (the post-`return` block is `#if 0` dead code in the
  original). Fires + charges per frame via the inherited Emitter machine. No edit needed.

- **GaussRifle (surviving source, unchanged)** — ctor chains to Emitter ctor → inherits EmitterSimulation
  tick. `FireWeapon()` override sets `outputVoltage = 0.0f` (drain). Declares GaussRifleSimulation +
  SetPerformance in GAUSS.HPP but the ctor never calls them → intentionally uses the inherited Emitter
  performance (faithful to shipped GAUSS.CPP). No edit needed.

### CROSS-FAMILY NEEDS
- **messaging (messmgr/SubsystemMessageManager)**: Emitter beam emit + keepalive use
  `Subsystem::SendMessage(template,a,b)` (FUN_0041c350) with message templates PTR_LAB_00511e6c/e78
  (beam-keepalive) and the fire message. ServiceDischarge/ContinueDischarge/FireWeapon currently stub the
  actual SendMessage call (templates retained as statics). Bind to the real message manager at integration.
- **heat/power (heat.hpp/powersub.hpp — NOT edited)**: Emitter charge + self-heat depend on the linked
  VoltageSource (Generator). `FUN_00417ab4` (follow SharedData link to the voltage source) is stubbed →
  TrackSeekVoltage / ReadyToDischarge / ctor seekVoltage-scaling (×source->ratedVoltage) and energyCoefficient
  derivation need the real Generator link. `FUN_004ad7d4` (HeatableSubsystem online?) stubbed to 1 and the
  self-heat dump (`heatAccumulator += heatPortion`) needs to feed the inherited HeatableSubsystem load owned
  by the heat family. EMITTER.TCP confirms intended coupling (Condenser/Generator/PowerBar feed the Emitter).
- **hud**: `DrawWeaponPip` AddElement into the cockpit HUD manager is stubbed in mechweap.cpp (gated on
  pipSegment != -1) — wire to the HUD module's AddElement (FUN_0049b6d8).

### Stubs remaining (engine-internal stand-ins, local to emitter.cpp; bind at link)
- FUN_004ad7d4, FUN_0049fb54, FUN_004b0bd0 (PoweredSubsystem step), FUN_004b0d50 (dt scale),
  FUN_004ac9c8, FUN_00417ab4, FUN_00421070, ResolveSource, VoltageCurve, CopyColor — local stand-ins.
- Firing-physics tuning constants `_DAT_004bafbc` (kDamageScale=1.0f) and `_DAT_004bb3b8` (folded into
  VoltageCurve()→1.0f) are double-precision operands the optimised image left non-finite in the dump →
  left symbolic; a human should re-derive from the FPU stream for exact damage/recharge tuning.

---

## Agent 6/7 — damage / messaging (mechdmg, dmgtable, messmgr)

All three modules were already substantially reconstructed (prior pass); this pass DEEPENED the
damage math against the binary oracle + the WinTesla engine DAMAGE.cpp/.h and re-verified each
compiles clean single-file via `compile1.cmd`. **mechdmg.cpp / dmgtable.cpp / messmgr.cpp → 0 errors.**

### Damage model reconstructed (armor -> structure -> crit)
- **Base armour model confirmed & documented** (`DamageZone::TakeDamage` @0041e4e0, oracle part_002.c;
  identical to the WinTesla engine `MUNGA/DAMAGE.cpp` body we link against). There is **no separate
  armour-then-structure pool**: a single `damageLevel` [0,1] accumulates
  `damageLevel += damage.damageAmount * damageScale[damage.damageType]`, is clamped to [0,1], sets the
  DamageLevelChanged flag (|=4), and at `>= 1.0` marks the zone destroyed (BurningState, DestroyedGraphicState,
  flag |=8). Documented verbatim in `Mech__DamageZone::TakeDamage`.
- **Key correctness fix in the ctor:** the 5 scalars at words 0x51..0x55 were mislabelled "per-facing
  armour". The oracle base TakeDamage indexes them `this[0x51 + damage.damageType]`, so they are
  `damageScale[Damage::DamageTypeCount==5]` indexed by damage TYPE
  (Collision/Ballistic/Explosive/Laser/Energy), NOT by facing. Relabelled the field, loop var
  (`facing`->`type`, bound `< Damage::DamageTypeCount`) and comments. Normalisation math unchanged
  (1/(scale*count); leg zones ×0.5) — verified against oracle FUN_0049ce50.
- **Leg-failure threshold fix:** the non-standing / still-grounded branch of `Mech__DamageZone::TakeDamage`
  compares structure against `_DAT_0049c9a0 == 0.5` (partial leg-failure graphic: right->level 4,
  left->level 3), not `StructureMax (1.0)`. Was using 1.0; now uses new `LegHalfStructure = 0.5f`.
  The standing/walking branch correctly keeps 1.0 -> level 9 (fall). Verified vs oracle FUN_0049c690.
- **Critical-hit roll verified** (`CriticalHit` @0049ccc4): half the damage amount (×0.5, clamped to 1.0)
  flows through the normal `TakeDamage`, then a single uniform draw walks the cumulative `criticalWeight`
  list (`roll <= cum/criticalWeightSum`) to pick one MechCriticalSubsystem, applying `ApplyDamage` and
  accumulating `damagePercentageUsed` (capped at `damagePercentage`). Reconstruction matches oracle
  field offsets (+0x10 pct, +0x14 pctUsed, +0x18 weight) exactly.
- **DMGTABLE roll logic verified** (dmgtable.cpp): three-level resolver
  DamageLookupTable(layers/penetration depth) -> PieSlice(angular sector, optionally torso-relative) ->
  DamageZonePercentTable(weighted zone list summing to 1.0). `SelectZone()` = single uniform draw against
  ascending cumulative %, returns first zone whose cumulative exceeds the draw. Constants confirmed
  (1.0 sum target, 2*PI wrap, 1e-4 sort epsilon). No change needed.

### Message dispatch (SubsystemMessageManager) verified
- `AddDamageMessage` (@0049b6d8) re-verified against oracle: first-of-frame wins for entityHit (+0xE4),
  impactPoint (+0xEC, gated on AlmostEqual-to-origin), damageZoneIndex (+0xE8); each message also queues a
  0x14-byte DamageInformation Plug (damageType=msg+0x2C, subsystemID=msg+0x5C) into the damageInformation
  chain keyed by damage amount (msg+0x30). Field offsets match exactly.
- `ConsolidateAndSendDamage` (Performance @0049b784): walks queued damage, resolves each damaged subsystem
  from the owner roster, queues a per-zone weapon-explosion ResourceID (dedup via chain Find), then flushes
  via `SendQueuedExplosions` (staggered 0.1s/explosion) and clears the common record. Logic intact.

### CROSS-FAMILY NEEDS
- **renderer (btl4vid/L4D3D — NOT edited):** `messmgr::SubmitExplosion` is stubbed. Original built an
  explosion render command (ClassID 0x5C sub-type 3, FUN_004336f0) and submitted it to the global IG
  graphics pipe (`theGraphicsPipe->Submit`, FUN_0041acbc). WinTesla replaced the IG pipe with L4D3D and
  the submission entry point is not in these headers — wire to the L4D3D explosion/effect submit at
  integration, using commonDamageInformation.impactPoint + rendererCompensateTime.
- **subsystem base (subsystm.hpp — NOT edited):** `messmgr::ResolveExplosionID` should read the
  per-subsystem explosion id off the engine `Subsystem` (subsystem+0x3E4, `Subsystem::GetExplosionID()`).
  The WinTesla Subsystem doesn't expose it yet; currently returns the manager's own terrainHitExplosionID
  as a stand-in. Add the accessor to the base, then bind.
- **mech roster (mech.cpp / mech3.cpp — NOT edited):** ConsolidateAndSendDamage relies on
  `owner->GetSubsystem(id)` (mech+0x128 roster). Confirm the reconstructed Mech exposes that accessor with
  the same id->slot indexing the message manager assumes.
- **damage notify message (net/entity — NOT edited):** the consolidated TakeDamage/notify message build
  (ClassID 0x13, vtable @0050b984, target field +0x184) in ConsolidateAndSendDamage is left as a recovered
  note — the message ctor (FUN_00419e18) and target wiring are net/renderer-side and absent from headers.

### Random / stubs remaining
- **`RandomUnit()` (mechrecon.hpp) returns 0** — the unit-random ([0,1)) used by the CriticalHit weighted
  roll, DamageZonePercentTable::SelectZone, LODDamageRouter hysteresis and DamageLookupTable degenerate-angle
  fallback. With it pinned at 0 every weighted roll selects the FIRST entry and LOD always re-uses the same
  child. **Must be backed by the real engine RNG (FUN_00408050) before damage distribution is correct.**
  Flagged here as the single highest-impact bring-up item for this family.
- mechdmg local stand-ins (TheTime/TicksPerSecond clock, SegTable/SegmentIterator segment-tree walk,
  SubProxy2 subsystem facets, NoteX2/RFileX2 offline notation readers) and the dmgtable ReconTable/
  ReconTableIter/notation proxies are behaviour-neutral compile shims — bind to the real engine
  clock / EntitySegment tree / Subsystem / NotationFile + TableOf at integration.
- `LODReuseHysteresis = 0.82f` (_DAT_0049c514) is a TODO: the exact reuse-probability operand wasn't
  cleanly recoverable from the pseudo-C; behaves as a high "stay on the same spot" bias. Re-derive from the
  FPU constant stream for exact LOD damage clustering.

---

## COMBAT WAVE — agent 1/7 (heat/power)  [heat.cpp/.hpp, powersub.cpp/.hpp, gnrator.cpp/.hpp]

Status: all three files compile clean (compile1.cmd → 0 `error C`). The per-frame
TICK is wired for every master subsystem in this family (SetPerformance installed in
ctor; engine `Simulation::PerformAndWatch`→`Perform`→`(this->*activePerformance)` persists
it, no auto-reset — verified against RP MUNGA/SIMULATE.HPP:117-182).

### Per-class per-frame behavior + SetPerformance wiring
- **HeatSink** — `HeatSinkSimulation` (@004ad924): heatEnergy += pendingHeat; currentTemp =
  heatEnergy/thermalMass; UpdateHeatLoad (radiatedHeat = currentTemp*coolant, scaled+clamped+
  15-sample filtered); ConductHeat→ComputeHeatFlow (conductive exchange w/ linked sink via
  expf decay) + BalanceCoolant; UpdateCoolant (draw=master.heatEnergy*heatLoad, top-up via
  DrawCoolant, draw state machine); degradation/failure alarm. WIRED: ctor installs
  `SetPerformance(&HeatSink::HeatSinkSimulation)` for master segments (flags 0x100, copy-bits 0).
  Faithful to FUN_004ad924/7f0/8ac/9ec/da94/dbf8 in heat_cluster.c.
- **PoweredSubsystem** — `PoweredSubsystemSimulation` (@004b0bd0): runs HeatSink sim, resolves
  voltage bus from powering Generator, advances electrical state machine
  (Starting/NoVoltage/Shorted/GeneratorOff/Ready) off generator state + startTimer, AutoConnect
  scan of GeneratorClassID segments. WIRED: ctor installs it for masters. Voltage state per frame
  is live.
- **Generator** — `GeneratorSimulation` (@004b1f7c): spin-up (startTimer→startTime), short
  recovery (shortTimer→shortRecoveryTime), idle/recovered transitions, outputVoltage =
  (1 - sourceLevel)*ratedVoltage, percentVoltageAvailable clamped [0,1]. WIRED: ctor installs it
  for masters. ALSO folded in the one recovered method the powersub cluster omitted:
  `Generator::ResetToInitialState()` (verbatim from GNRATOR.TCP — chains
  HeatableSubsystem::ResetToInitialState, outputVoltage=0), declared in powersub.hpp, defined in
  powersub.cpp.
- **Myomers / PowerWatcher** (in powersub) — already wired (`MyomersSimulation`,
  `PowerWatcher::Simulation`) in their ctors; left as-is.
- **HeatableSubsystem** (base) — owns the damageable-subsystem virtual surface; base
  edits kept additive (Generator::ResetToInitialState chains it). No regressions to derived families.
- **gnrator.cpp/.hpp** — alias TUs (Generator lives in powersub); the recovered method they
  documented is now folded into powersub. Left as alias-only, no code emitted.

### Constants RECOVERED from the oracle (section_dump.txt raw bytes; corrected placeholders)
- `_DAT_004ad870` HeatLoadScale: **0.002f** (was 1.0f placeholder) — 80-bit extended
  `3b df 4f 8d 97 6e 12 83 f6 3f` = 1.023992 × 2^-9. Real: normalises radiatedHeat (~300) into
  the [0,1] heatLoad band; the 1.0f placeholder would have pinned heatLoad at the 1.0 clamp.
- `_DAT_004ad87c`=0.0f, `_DAT_004ad880`=1.0f, `_DAT_004ada90`=1.0f — confirmed correct.
- `_DAT_004adbf4` HeatEqualizeEpsilon & `_DAT_004adcfc` CoolantDrawEpsilon: **1.0e-4f**
  (0x38d1b717) — was 0.0005f JM_CLOSE_ENOUGH placeholder. Now exact.
- Power constants verified already-correct from bytes: VoltageEpsilon 1.0e-4f (@4b0bcc),
  RatedVoltageRef 1.0f (@4b1d10), DegradedDriveScale 0.5f (@4b1d14), MinOutputVoltage 0.0f
  (@4b2158), MaxOutputVoltage 1.0f (@4b2154/4b2258).

### CROSS-FAMILY NEEDS
- `BT_GetMessageManager(Mech*)`, `BT_IsBusLive(Mech*)`, `BT_ClearMyomers(Mech*)` (declared in
  powersub.hpp, definitions belong in the mech family) — used by PoweredSubsystem/Generator/Myomers
  message handlers. Mech family must provide these (owner+0x190 message manager, +0x274 bus-live
  flag, +0x374 myomer back-ptr).
- `Mech::GetSegment(int)` / `EntitySegment` segment roster — relied on for voltage/heat-sink linkage
  resolution and the AutoConnect generator scan. Owned by mech family.

### Remaining stubs / not deepened (out of my 3 DEEPEN targets)
- **Condenser** (RefrigerationSimulation @4ae4d8) — body lives in heatfamily_reslice.cpp (not my
  file). Condenser currently inherits HeatSink ctor → ticks via HeatSinkSimulation (live, not
  DoNothingOnce), but does NOT install RefrigerationSimulation. Deferred to whoever owns
  heatfamily_reslice.cpp; flagged so refrigeration output isn't yet per-frame.
- PowerWatcher MinVoltageScale (`_DAT_004b1924`) left at 1.0f — appears to be a double in the dump
  and is non-load-bearing (PowerWatcher only delegates per frame); not re-derived.
- `MyomersSimulation` SourceLevel/voltage-curve details depend on cross-family Mech back-ptrs above.

---

## COMBAT WAVE — agent 5/7: movement subsystems (gyro / torso / myomers)

Goal: real per-frame behavior + `SetPerformance(&<Class>::<Class>Simulation)` so each subsystem
ticks through `Mech::PerformAndWatch → subsystem->PerformAndWatch → Simulation::PerformAndWatch →
activePerformance` (instead of the default `DoNothingOnce` that drops out after frame 1).

### ROOT-CAUSE FOUND (all three were silently NOT ticking as the master)
The binary registers the per-tick Performance in each ctor by storing the pointer-to-member into
`activePerformance` (`this[7..9]`) under the live-MASTER guard `(segFlags & 0xC)==0 && (segFlags &
0x100)`. Proven identical shape in all three ctors:
- Gyro  @004b3778: `this[7..9] = PTR_FUN_0050fe08` (GyroscopeSimulation), guard bit **0x100**.
- Torso @004b6b0c: master → `PTR_FUN_00510c10` (TorsoSimulation), else → `PTR_FUN_00510c1c`
  (TorsoCopySimulation); `this[0x93]`=isDamagedCopy.
- Myomers @004b8fec: `this[7..9] = PTR_LAB_00511620` (MyomersSimulation), guard bit **0x100**.
The earlier reconstructions (a) tested the wrong bit (`SegmentLiveFlag` 0x01 instead of
`MasterHeatSinkFlag` 0x100) AND (b) primed the local `segmentFlags` shim to 0 → guard always false.
Net effect before this pass: **Gyro never registered its sim; Torso registered the slaved COPY sim
(no command processing); Myomers mislabeled the store a "graphic-state hook" and omitted
registration entirely.** All three would have fallen back to DoNothingOnce.

### Gyroscope (gyro.cpp/.hpp) — balance / tip-over
- Behavior: per-frame `GyroscopeSimulation` verified faithful to @004b275c — PowerWatcher watch
  update, idle-sway ramp toward (impaired? percentageOnDestruction : percentageOnNormal)+swayBias at
  rotationPerSecond·dt with overshoot clamp + [min,max] noise band, then IntegrateEyeJoint
  (@004b2ec0) + IntegrateBody (@004b30ec) spring-dampers. (Sim integrates only; the binary does NOT
  write the joints in-sim — WriteEyeJoint/WriteMechJoint are driven from the render/animate pass.)
- SetPerformance wired: guard fixed to `MasterHeatSinkFlag` (0x100); `segmentFlags` primed to live-
  master so `GyroscopeSimulation` registers. Compile-clean (exit 0).

### Torso (torso.cpp/.hpp) — twist (yaw) + elevation (pitch) aim
- Behavior: `TorsoSimulation` verified faithful to @004b5cf0 — watch update; effective rates gated by
  power/heat (Degradation halves, Failure/not-Ready/HeatModelOff zero); button-accel ramp; per-axis
  command integration (rotate L/R, elevate U/D) clamped to software limits; center/recenter; velocity
  + settle/dirty-bit; statusFlags limit latches. `TorsoCopySimulation` (@004b65f8) retained for the
  copy path.
- SetPerformance wired: guard fixed to `MasterHeatSinkFlag` (0x100); `segmentFlags` primed to master
  so the **command-processing** TorsoSimulation registers (was registering TorsoCopySimulation).
  Compile-clean (exit 0).

### Myomers (myomers.cpp/.hpp, classID 0xBC6) — artificial-muscle drive/actuation
- Behavior: `MyomersSimulation` verified faithful to @004b8d18 — advanced-damage gate (FUN_004ad7d4),
  then `pendingHeat(@0x1C8) += ratio² · (1+damage) · [(1-accelEff)·|vy|·k·m·dt + (1-velEff)·(k·|v|²·½)
  + (1-accelEff)·|v|·|a|·k·dt]` (k=motionGain@0x20c, m=*mass@0x250). Drive helpers AvailableOutput/
  RegisterMaxOutput/GetSpeedReading/HasAdequateVoltage already faithful.
- SetPerformance wired: ADDED the missing registration in the ctor (added a local `segmentFlags`
  Word shim + `SegmentCopyMask`/`MasterHeatSinkFlag` enum to myomers.hpp), primed to live-master,
  guard `(flags&0xC)==0 && (flags&0x100)` per @004b8fec, → `SetPerformance(&Myomers::MyomersSimulation)`.
  Compile-clean (exit 0).

### CROSS-FAMILY NEEDS (movement subsystems)
- **Real segment flags** (`owner+0x28`, read by all three ctors): the live-master vs damaged-copy
  decision and thus which Performance is installed must come from the owning Mech segment, not the
  locally-primed `segmentFlags` shim. Mech family must set these on the subsystem/segment before
  construction (master = 0x100 set, copy-mask 0xC clear). Until then all three are primed to the
  master value so the master sims tick.
- Gyro skeleton-joint I/O: `eyeJointNode`/`mechJointNode` resolution + the node get/set-transform API
  (FUN_0041cfa0/0041d020/0041d0a8/0041d11c) and `Mech::ResolveJoint(name)` are stubbed (NodeType/
  NodeScalar/SetNode*). WriteEyeJoint/WriteMechJoint integrate values are inert until the
  Skeleton/JointedMover node API is reachable from the subsystem.
- Torso: `Mech::ResolveJoint`, skeleton-node scalar writer (`PushTwist`), Simulation dirty/replicate
  bit (`SetMovedFlag`), and PowerWatcher base virtuals (`WatcherResetToInitialState`/
  `WatcherHandleDeathMessage`/`WatcherUpdateWatch`, FUN_004b1804/179c/181c) are stubbed — owned by
  Mech / Simulation / PowerWatcher families.
- Myomers: owner-Mech accessors (base/top speed @0x34C/0x7A0, velocity/drive vectors @0x1C4/0x82C,
  motionGain @0x20C, mass @0x250, advanced-damage gate FUN_004ad7d4), JointedMover roster
  Attach/Detach/Release (mover vtable +0x38/+0x3C/+0x44, fed `&speedEffect`@0x31C), and this segment's
  DamageZone structureLevel @0x158 are all backed by neutral local shims. The drive/heat formulas are
  intact but read placeholder zeros until the Mech/JointedMover/DamageZone accessors land.

### Stubs / not deepened
- Gyro `UpdateAnimationNoise` (@004b357c) procedural-noise generator: structure recovered, node
  injection bodies left as branch stubs (best-effort; not on the per-frame tick path).
- Gyro `WriteEyeJoint`/`WriteMechJoint` quantise-write logic faithful but inert pending the
  skeleton-node API (above).
- Myomers `ToggleSeekVoltage` (@004b8a48) command handler: not in the captured shards (untagged gap);
  cycles currentSeekVoltageIndex within [min,max] — left as a documented stub.

---

## COMBAT WAVE — agent 3/7: projectile / missile weapons

Edited only the 7 assigned module pairs. All 7 compile clean (`compile1.cmd`,
EXIT=0). The per-frame TICK is now wired end-to-end for the launcher and the
flight entity.

### Per class

- **ProjectileWeapon** (`projweap.cpp/.hpp`) — behavior DEEPENED + tick wiring
  FIXED.
  - **Bug fix (matters for the live tick):** the ctor's Performance-install gate
    read `subsystem_resource->subsystemFlags`; the oracle (FUN_004bc3fc @8375)
    reads the **owner's** flags `*(param_2+0x28)` = `owner->simulationFlags`
    (`&0xC==0 && &0x100`) — the *same* authoritative-mech gate the working AmmoBin
    uses. Corrected so launchers actually arm their Performance on the bring-up
    mech instead of dropping out as `DoNothingOnce`.
  - `ProjectileWeaponSimulation` (was an empty stub; body genuinely
    unrecovered @004bbd04) reconstructed from the recovered helpers
    (DrawFiringCharge @004bbc78, UpdateEject @004bbb50, CheckForJam @004bbfcc,
    ComputeTimeOfFlight @004bc06c) + the RP analog `RivetGun::RivetGunSimulation`
    (WEAPSYS.cpp): recharge recoil, advance eject, edge-trigger via
    `CheckFireEdge()`, a Jammed/NoAmmo/ready state machine, jam roll, then
    `FireWeapon()` through the vtable.
  - `FireWeapon` (slot 18, stub @004bc104) reconstructed: `ConsumeRound()` (→
    AmmoBin::FeedAmmo @004bd4f4), start recoil cooldown, `GetMuzzlePoint` +
    `Projectile::New`. New shared helpers `ConsumeRound()` / `ReadyToFire()`.
  - `SetPerformance(&ProjectileWeapon::ProjectileWeaponSimulation)` — wired (ctor).

- **MissileLauncher** (`mislanch.cpp/.hpp`) — `FireWeapon` (override @004bcc60)
  reconstructed: `ConsumeRound()`, recoil cooldown, then spawn ONE guided
  `Missile::New` entity per salvo (task #60 correction — NOT `missileCount`
  entities; the single missile carries `burstCount` = the random cluster-hit
  roll, and per-missile damage = authored/missileCount pre-split in the ctor).
  The PORT re-expresses this as N flying rounds (net armor total identical;
  loses cluster variance + single-zone concentration — see [[open-questions]]
  task #60-D). Ticks via the inherited `ProjectileWeaponSimulation`; its vtable
  `FireWeapon` override is dispatched from there.

- **Missile** (`missile.cpp/.hpp`) — flight-entity tick wiring ADDED.
  - **`SetPerformance(&Missile::MoveAndCollide)` installed in the ctor** (was
    MISSING — the Projectile base ctor only installed the unguided base pass, so
    a spawned missile would not have homed). This is the "flight entity
    simulates" wiring.
  - `MoveAndCollide` now explicitly drives its hosted subsystems each frame
    (`seeker->FindTarget(dt)`, `thruster->MissileThrusterSimulation(dt)`) — the
    behavior the shipped binary folds inline — before the steering/integrate/
    collide pass.

- **Seeker** (`seeker.cpp/.hpp`) — `LeadTarget`/`FindTarget` homing already
  reconstructed; now actually RUN each frame from `Missile::MoveAndCollide`.
  Faithful to the binary, the Seeker installs **no** custom Performance (its
  vtable is all engine slots; its work is folded into the host missile), so it
  is driven by the missile rather than a standalone `SetPerformance`. No change
  needed.

- **Projectile** (`projtile.cpp/.hpp`) — base integrate/collide pass already
  present; `SetPerformance(&Projectile::MoveAndCollide)` already installed by the
  ctor. No change.

- **MissileThruster** (`misthrst.cpp/.hpp`) — burn/steer sim already present;
  `SetPerformance(&MissileThruster::MissileThrusterSimulation)` already installed
  by the ctor, and now also invoked per frame by the host missile. No change.

- **AmmoBin** (`ammobin.cpp/.hpp`) — feed (FeedAmmo @004bd4f4), DumpAmmo
  @004bd588 and CookOff @004bd300 already reconstructed; `AmmoBinSimulation`
  Performance already installed by the ctor under the correct
  `owner->simulationFlags` gate. Verified the feed/dump identities against the
  oracle (FeedAmmo dispenses one round Loaded→Feeding; DumpAmmo empties the bin).
  No change.

### CROSS-FAMILY NEEDS
- **MechWeapon base (agent 2):** the fire/sim path relies on `recoil`,
  `rechargeRate`, `weaponAlarm` (`SetLevel`/`GetState`), `CheckFireEdge()`,
  `UpdateTargeting()`, `GetMuzzlePoint()`, `GetTargetPosition()`, `damageData`,
  `effectiveRange`, `rangeToTarget`. All present in `mechweap.hpp` today; if any
  are renamed/retyped, my two FireWeapon paths + `ProjectileWeaponSimulation`
  need a matching touch-up.
- **Mech (mech family):** the AmmoBin roster lookup (`OwnerSubsystemCount`/
  `OwnerSubsystem`) and the cockpit "live-fire" flag (`LiveFireEnabled`) are still
  isolated stubs in `projweap.cpp` — the real Mech subsystem-roster + controls
  query must wire them so the launcher resolves its bin and the jam roll honors
  the live-fire control.
- **Entity / world (engine):** `Projectile`/`Missile` world-segment registration,
  motion integration (`IntegrateMotion`/`AdvanceModel`), `CollisionQuery`
  (FUN_0042291c) and detonation spawn (FUN_004be078, ClassID 0x5C) remain
  structural stubs in `projtile.cpp`/`missile.cpp` — engine-side, not weapon
  logic. Spawned entities also need the entity-manager registration that ticks
  their installed Performance (engine-side).

### Stubs still standing (flagged, not blocking the tick)
- `projweap.cpp`: `GetStatusFlags` (slot 12 @004bbf88) and `UpdateWeaponState`
  (slot 16 @004bbc20) — bodies genuinely unrecovered; left deferring to base /
  benign. `OwnerSubsystem*` / `LiveFireEnabled` cross-family stubs (above).
- `missile.cpp`: `GetForward`, `ResolveTargetConnection`, `LaunchAimOffset`,
  `ResolveModel`, `SpawnDetonation` — engine/model plumbing stubs.
- `projtile.cpp`: `InitSegmentLink`/`ResolveWorld`/`IntegrateMotion`/
  `CollisionQuery`/`Retire` etc. — Entity-engine stubs.

---

## WAVE agent 7/7 — HUD / 2D cockpit layer (dpl2d_ over D3D9 + heat gauge feed)

### What was done
1. **dpl2d_ 2D vector display-list layer reimplemented over Direct3D 9.**
   Moved the 9 inert `dpl2d_*` stubs OUT of `btstubs.cpp` into a NEW file
   `decomp/reconstructed/dpl2d.cpp` (+ `dpl2d.hpp`), added to the `bt410_l4`
   CMake source list. The recorder calls (`dpl2d_NewDisplayList/Begin/SetColor/
   Circle/PushMatrix/PopMatrix/MoveTo/End/Compile` — prototypes in btl4vid.hpp)
   now build a real in-memory command list (colour, filled disc, outline ring,
   translation-matrix stack + pen). A real **`dpl2d_ExecuteList(list, device)`**
   executor rasterises a compiled list as pre-transformed 2D primitives
   (`D3DFVF_XYZRHW|D3DFVF_DIFFUSE`, mirroring engine `L4VERTEX_2D`/`_2D_FVF`):
   filled circle = `D3DPT_TRIANGLEFAN`, outline = `D3DPT_LINESTRIP`, 32-seg
   tessellation, alpha-blend on, z/lighting/fog off, full render-state
   save/restore, normalised-[0,1]→viewport-pixel mapping. This matches exactly
   what the reconstructed reticle/PIP builder in `btl4vid.cpp` (lines ~563-616)
   issues (`dpl2d_Circle(list,x,y,0.012f,1)` filled blip + `0.014f,0` dark ring).

2. **Heat gauge wired to real subsystem state.**
   Implemented `HeatConnection::Update()` (the per-frame data feed, orig
   "Transfer" @004c3720) in `btl4gaug.cpp`. It reads the live
   `HeatableSubsystem::currentTemperature` (@0x114) of the gauge's bound heat
   subsystem and writes the colour index that `ColorMapper::Execute` pushes into
   the hardware palette slot (no subsystem / destroyed → 100/full tint, else
   `Round(currentTemperature)`). `Update()` is the real base virtual
   (`GaugeConnection::Update`, gauge.h) the gauge framework calls each frame —
   so the cockpit heat tint art is now driven by actual heat state.

### 2D primitives implemented
Coloured filled circle, coloured outline circle, translation matrix stack
(Push/Pop), pen move — over D3D9 via `DrawPrimitiveUP` with pre-transformed
verts. (The port's dpl2d_ API surface is the small reticle/PIP subset; the full
libDPL board API — polylines/polygons/clip regions/AddSetMatrix etc. — is NOT
used by the reconstructed code and was not reimplemented.)

### Gauges wired to real state
- **ColorMapperHeat** ← `HeatableSubsystem::currentTemperature` (DONE, live).
  The ColorMapper draw path itself uses the engine's real L4 gauge raster layer
  (`Video16BitBuffered`/`SVGA16`/`L4GraphicsPort` in MUNGA_L4/L4VB16.cpp), which
  is **already fully implemented over D3D9** in WinTesla (rasters to a PixelMap16
  then uploads changed lines to a D3D texture) — so this path genuinely draws.

### Compile-clean status
All 7 owned files compile clean (exit 0) via `compile1.cmd`:
`hud.cpp btl4gaug.cpp btl4gau2.cpp btl4gau3.cpp btl4galm.cpp btstubs.cpp dpl2d.cpp`.

### Honest "draws vs compiles" assessment
- **Gauge raster path: REAL** (engine L4VB16 D3D path is implemented). The heat
  gauge's data feed is now live and correct.
- **dpl2d_ reticle: records + has a real D3D9 executor, but NOT yet on-screen.**
  No caller invokes `dpl2d_ExecuteList` yet — `BTReticleRenderable::Execute()`
  (btstubs.cpp) is still empty and `btl4vid.cpp` builds the PIP lists but never
  executes them. Hooking execution into the per-frame world render is the last
  mile and lives in btl4vid.cpp / the reticle renderable (NOT my files).
- **ColorMapperHeat::Make() uses placeholder .data resource constants** (empty
  palette names, port 0, colour slot) — the gauge-config resource pool
  (`l4gauge.cfg` / DAT_005150xx) was not recovered from the optimised image, so
  the heat gauge constructs degenerately at runtime until those constants are
  supplied. Data FEED is correct; the gauge's art binding is the gap.
- **Other gauge widgets** in btl4gaug/gau2/gau3 (bars, OneOfSeveral,
  HeadingPointer, arcs) remain comment-summaries (no code bodies); they only
  fail at link if the config references them (currently not constructed).
- **btl4galm.cpp** (alarm/lamp stream virtuals) is serialization glue — no live
  gauge to wire; left as the existing best-effort reconstruction.
- **hud.cpp** is the HUD *subsystem* (torso-horizon gimbal logic), not a 2D
  drawing surface — left as-is (already reconstructed).

### CROSS-FAMILY NEEDS
1. **Reticle execution hookup (btl4vid.cpp / BTReticleRenderable — NOT my files):**
   wire `dpl2d_ExecuteList(pipDisplayListA[i]/B[i], renderer->GetDevice())` into
   the world-view per-frame render (after the 3D scene) so the recorded reticle/
   PIP display lists actually draw. `DPLRenderer::GetDevice()` (L4VIDEO.h) returns
   the main-view device; `dpl2d_ExecuteList` is declared in `decomp/reconstructed/
   dpl2d.hpp`.
2. **Gauge-config resource pool** (`l4gauge.cfg` / the DAT_005150xx constants for
   ColorMapperHeat::Make): needed so the heat gauge binds real palette names /
   port / colour slot. Likely recoverable from the pod content tree or a deeper
   .data slice (binary oracle). Owner: whoever owns gauge-config / content.
3. **Heat temperature→percent scaling** (flagged in btl4gaug.cpp): the recovered
   feed rounds `currentTemperature` (seeded 300.0) straight into a 0..255 index;
   the exact ramp scaling should be reconciled against the original palette ramp.
   Touches heat subsystem semantics (heat.cpp owned by another agent) — read-only
   here; no edit made to heat.* .

---

## INTEGRATION PASS (post-7-agent wave) — subsystem tick reconcile

### TL;DR
- Tasks 1/2/3 landed; build GREEN, stable (3+ clean runs), driving intact.
- Tick count stayed 1 -> 1. Root cause is NOT the gate-source variance the brief
  described. The player mech's subsystem roster is built from local RECON_SUBSYS
  stub classes (mech.cpp:145-181), not the reconstructed real classes. The real
  classes (heat.cpp::HeatSink, gyro.cpp::Gyroscope, powersub.cpp::Generator/
  PoweredSubsystem, sensor.cpp::Sensor, ...) are compiled but never instantiated,
  so their (now-reconciled) gates never run.

### What actually gates the tick (empirically verified, not theory)
The base Simulation ctor installs activePerformance = &DoNothingOnce
(MUNGA/SIMULATE.cpp:352). DoNothingOnce runs once then calls NeverExecute() (sets
DontExecuteFlag 0x2), so a subsystem that never SetPerformance-s a real sim drops
out after frame 1. Frame-1 log = 30 executable, steady-state = 1: i.e. 29
subsystems entered with DoNothingOnce and self-disabled.

A one-time per-slot roster dump (temp diag, since removed) showed the lone
survivor is slot 18 'HUD' = the real MechTech (mech.cpp:643), whose gate
(entity->simulationFlags and 0xC)==0 reads the OWNER's flags and passes. Every
other slot (HeatSink, Reservoir, Condenser1-6, GeneratorA-D, Avionics, Myomers,
Gyroscope, Torso, Searchlight, ThermalSight, PPC_1/2, ERMLaser_1-3, AmmoBinSRM6,
SRM6, MessageManager) is a RECON_SUBSYS/local-struct stub -> DoNothingOnce ->
exec=0. (The brief's claim that AmmoBin is the survivor is incorrect; AmmoBin
isn't instantiated as the real class -- the 'AmmoBinSRM6_*' slots are stubs.)

### Binary-oracle finding on the gate (Task 1, done correctly)
decomp/recovered/all/part_*.c shows the canonical subsystem master-gate ~17x:
(*(uint*)(param_2+0x28) and 0xc)==0 && (*(uint*)(param_2+0x28) and 0x100)!=0
where param_2 is the owner (Mech*), so owner->simulationFlags (owner+0x28) is the
authoritative source -- matching the working AmmoBin/projweap path. The
reconstruction had drifted to per-file sources (subsystem_resource->subsystemFlags
[streams 0], GetSegmentFlags() shim, primed-local segmentFlags,
entity->simulationFlags without 0x100). Aligned all real-class SetPerformance
master gates to owner->simulationFlags: heat.cpp (HeatSink), powersub.cpp
(PoweredSubsystem, Generator, PowerWatcher), gyro.cpp, torso.cpp, myomers.cpp,
sensor.cpp. (mechtech.cpp already reads entity->simulationFlags = owner -> left
as-is; it's the one that works.) Heat/power linked-sink/voltage-source logic left
on per-segment flags (that linkage is genuinely per-segment, and leaving it keeps
the master-missing Verify() paths dormant). These edits are correct and
oracle-matched but LATENT -- they only take effect once the real classes are
instantiated (see blocker below).

### Task 2 -- mech-core accessors (btstubs.cpp)
The reconstructed Mech is NOT byte-exact (named members + a Wword scratch bank),
so the shipped raw offsets (+0x190 msg-mgr, *(+0x190)+0x274 bus, +0x374 myomers)
do NOT address the right members; a raw read would deref garbage. Done against
intent, deref-safe:
- BT_IsBusLive -> True (running player mech is a powered master; un-gates the
  power-dependent paths in powersub when wired). REAL change (was False).
- BT_GetSegmentFlags(Mech*) -> defined = owner->simulationFlags. This symbol was
  declared in hud.cpp but had NO definition -- it only linked via /FORCE (undefined
  return if ever called). Now resolves the dangling extern and feeds the HUD/
  segment gate the same authoritative source. REAL fix.
- BT_GetMessageManager -> still 0 (the real MessageManager is a roster slot but
  can't be located without the byte-exact layout; a bogus non-null would be worse
  -- callers deref). BT_ClearMyomers -> no-op (no known-offset member). Documented.
- Mech::GetSegmentFlags() / EntitySegment roster (owner+0x28) / GetExplosionID
  (+0x3E4): NOT implemented -- no safe byte-exact source in the reconstructed Mech;
  latent like the rest. Flagged.

### Task 3 -- RandomUnit backed by real RNG (DONE, verified)
mechrecon.hpp: RandomUnit() was return 0 (pinned every damage/crit/zone roll to
entry 0). Now #include <RANDOM.hpp> (fwd -> engine RANDOM.h) and returns
(Scalar)Random -- RandomGenerator::operator Scalar() in [0,1), matching
FUN_00408050. Variadic tail + Scalar return keep dmgtable call sites unchanged.
Verified with a temp 1 Hz log (since removed): printed varying triples e.g.
0.00662 0.1477 0.2346 / 0.2919 0.1782 0.3686 / 0.0590 0.0847 0.4086 across runs.
This genuinely de-pins damage/crit/zone selection -- but its consumers (dmgtable
via weapon impacts) are exercised only when the stub weapons fire, so the combat
effect is latent until the weapon roster is real.

### THE REAL BLOCKER to N->~30 (honest)
Replacing the RECON_SUBSYS stubs (mech.cpp:145-181 + the switch @550-672) with the
real reconstructed classes is the genuine remaining work. It was NOT done in this
pass because it cannot be landed stably piecemeal:
1. Two parallel ClassID schemes. The streamed segments use the switch's 0xBBD-0xBDE
   IDs; the real classes stamp a different family (heat.cpp HeatSinkClassID,
   heatfamily Condenser/Reservoir/HeatWatcherClassID, powersub, ...). The class<->
   resource mapping must be reconciled first or a class gets constructed over a
   resource blob meant for another (garbage fields).
2. Unverified per-class alloc sizes. The switch hand-codes Memory::Allocate sizes
   per class; the real objects are larger (e.g. HeatSink/AmmoBin reach >=0x224).
   Under-allocation -> heap corruption (this family already caused the
   "~1-in-6 startup crash", mechsub.cpp:165 note).
3. Real ctors Verify()-assert on segment links. HeatSink @004adda0 asserts
   "Bad subsystem resource ->heatSink" if owner->GetSegment(heatSinkIndex) is null;
   PoweredSubsystem @004b0f74 asserts on GetSegment(voltageSourceIndex). These
   depend on the streamed indices landing at the right resource offsets in our
   (approximate, +0x100-padded) stream.
4. Interdependence. Even a single real class wired alone won't show useful
   computation: HeatSink's HeatSinkSimulation does
   currentTemperature = heatEnergy/thermalMass but pendingHeat stays 0 unless the
   heat sources (Generator/weapons -- all stubs) feed it. The roster must be made
   real together to compute real combat state.

Ordered safe path (next pass): (a) reconcile the streamed ClassID -> real-class
table; (b) recover each real class's exact object size from the binary and size the
allocations; (c) confirm owner->GetSegment(index) returns valid skeleton segments
for the streamed indices (it's the real engine JointedMover::GetSegment(int), so
likely yes) and that resource fields sit at the expected offsets; (d) wire ONE
family at a time (heat first -- it's the pilot), build+run, watch for the asserts,
keep only if green+stable; (e) repeat. The gate alignment (Task 1) and accessors
(Task 2) above are the prerequisites already in place, so each newly-real class
will tick the moment it's instantiated.

### What genuinely runs today (no overclaim)
- Driving: real ([drive] pos advances every second; stable).
- 1 real subsystem ticks: MechTech::TechnicalAssistance every frame (the
  [tick] ... 1 (executable) of 30). The other 29 are stubs decaying to
  DoNothingOnce -- they tick frame 1 only, then opt out (by design of the stub).
- RNG: real and verified (Task 3). Bus-live / segment-flags accessors: real values
  now, but their consumers are stubs -> latent.
- Stability: 3+ headless runs to timeout, no asserts, no faults, gflags PageHeap OFF.
- Build: GREEN (bt410_l4.lib + btl4.exe), links under the pre-existing /FORCE (now
  with one fewer unresolved extern -- BT_GetSegmentFlags resolved).

## ============ HEAT-FAMILY PILOT (instantiate real heat classes) — 2026-06-30 ============
GOAL: make the mech construct the REAL heat-family subsystem classes (heat.cpp/heat.hpp/
heatfamily_reslice: HeatSink/HeatableSubsystem/Condenser/Reservoir/HeatWatcher) instead of the
inert RECON_SUBSYS stubs, proving the instantiation+tick pattern for the other families.
RESULT: **REVERTED to known-good (all stubs).** A real Condenser DOES instantiate and its
per-frame Performance DOES tick (the gate/mechanism works) — but it then crashes in the heat
SIMULATION on a missing inter-sink linkage that cannot be fixed from the allowed files. Build is
back GREEN, 3/3 runs stable, tick back to 1/30, driving intact. This is the conservative outcome
the brief sanctions ("if a class can't be made stable, keep the stub, document, report").

### #1 finding — the factory ClassID enum NAMES (mech.hpp:253-273) are MISLABELED.
The streamed ClassID VALUES are right, but the human-readable enum names + the class each switch
case constructs are guesses that DISAGREE with the authoritative CLASSMAP.md. Cross-referencing the
ctor address each case cites against CLASSMAP gives the TRUE class. Authoritative map (streamed ID
-> ctor -> REAL class // current switch label):
  0xBBD @4ae568 -> Condenser            // labelled CockpitClassID  (builds stub Cockpit)
  0xBBE @4ae8d0 -> HeatSink bank (agg)  // labelled SensorClassID   (builds stub Sensor; ALSO
                                        //   wrongly sets sensorSubsystem — pre-existing mislabel)
  0xBC0 @4af408 -> Reservoir            // labelled CondenserClassID (builds stub Condenser)
  0xBC1 @4b225c -> Generator            // GeneratorClassID  (correct)
  0xBC2 @4b0f74 -> PoweredSubsystem     // correct
  0xBC3 @4b1d18 -> Sensor               // labelled MyomersClassID
  0xBC4 @4b3778 -> Gyroscope            // correct
  0xBC5 @4b6b0c -> Torso                // labelled SinkSourceClassID
  0xBC6 @4b8fec -> Myomers              // labelled ActuatorClassID
  0xBC8 @4bb120 -> Emitter              // WeaponEmitterClassID (correct)
  0xBCB @4bd5c4 -> AmmoBin              // labelled JumpJetClassID
  0xBCD @4bc3fc -> ProjectileWeapon     // MechWeaponClassID (correct family)
  0xBD0 @4bcff0 -> MissileLauncher      // labelled BallisticWeaponClassID
  0xBD3 @49bca4 -> SubsystemMessageManager // labelled MechControlsMapperID
  0xBD4 @4bb888 -> PPC                  // labelled GaussWeaponClassID
  0xBD6 @4b7f94 -> HUD                  // labelled MechTechClassID — and the case builds the REAL
                                        //   MechTech class over it (size 0x2a4 == HUD size, so safe);
                                        //   THIS is the lone steady-state tick survivor.
  0xBD8 @4b84dc -> Searchlight          // labelled LegSubsystemClassID
  0xBDC @4ad228 -> MechTech             // labelled HeatableClassID (builds stub HeatableSubsystem,
                                        //   and that case does NOT even assign subsystemArray[id])
  0xBDE @4b8718 -> ThermalSight         // labelled DisplayClassID
ACTION for the next pass: re-label this enum + re-target every switch case to the CLASSMAP class
BEFORE wiring any real class — a class built over the wrong streamed blob = garbage fields. (I did
NOT renumber the enum this pass to avoid churn/regression; documented here instead.)

### Heat-family streamed in TEST.EGG (from the roster dump): Condenser x6 (0xBBD), Reservoir x1
(0xBC0), "HeatSink" x1 (0xBBE aggregate). The 0xBBE aggregate has NO reconstructed class (only
prose in heatfamily_reslice.hpp), so it is not instantiable. That leaves Condenser and Reservoir.

### Assert/crash analysis (heat.cpp, read-only):
- Condenser is the only ctor-safe heat class. HeatSink ctor @004adda0 (heat.cpp:415-428) resolves
  a linked sink via owner->GetSegment(heatSinkIndex) and Verify-aborts ("Bad subsystem resource
  ->heatSink") if null — BUT it SKIPS that whole block when IsDerivedFrom(Condenser) (true during a
  Condenser ctor iff shared_data == Condenser::DefaultData). So Condenser never hits the segment assert.
- Reservoir runs the segment block (it is HeatSink-derived, not Condenser) -> Verify-abort risk if
  heatSinkIndex doesn't resolve; PLUS its own master branch (heatfamily_reslice.cpp:446-448) does
  link->Attach(this) with no null guard; PLUS btl4vid MakeMechRenderables walks the roster touching
  ClassID 0xBC0 (Reservoir) at BINARY offsets (reconstructed layout != binary) — three independent
  risks. Not attempted past the Condenser result.

### What I actually did + the debugger-confirmed blocker:
Wired the REAL Condenser into case 0xBBD: new (Memory::Allocate(sizeof(Condenser))) Condenser(this,
id,(Condenser::SubsystemResource*)seg, Condenser::DefaultData) (used C++ sizeof(Condenser) for the
alloc — the reconstructed object size, guaranteed to cover every member the ctor writes; NO binary
hand-size, NO under-alloc). Added #include <heat.hpp>; renamed the two colliding local stubs
(Condenser->ReservoirStub, HeatableSubsystem->HeatableStub) so heat.hpp's real names win.
  - Build: GREEN. Construction: SUCCEEDED — all 6 Condensers built, mech spawned (no ctor assert; the
    Condenser skip-path held).
  - The master gate DID arm (owner->simulationFlags has &0xC==0 AND &0x100 — the Task-1 gate
    reconcile is correct): the Condenser installed HeatSink::HeatSinkSimulation and it TICKED frame 1.
  - CRASH (0xC0000005) on frame 1, cdb stack (PageHeap off):
      btl4!HeatSink::UpdateCoolant+0x35   heat.cpp:693
      btl4!HeatSink::HeatSinkSimulation+0xa4  heat.cpp:537<-531
      btl4!Simulation::Perform / PerformAndWatch
      btl4!Mech::PerformAndWatch  mech4.cpp:554
    ROOT: heat.cpp:692-693 `HeatSink *master = linkedSinks.Resolve(); coolantDraw = master->heatEnergy
    * heatLoad;` — linkedSinks is EMPTY for a Condenser (its HeatSink ctor skipped the linkage block),
    so master==NULL and master->heatEnergy derefs null. NB the sibling methods ConductHeat (heat.cpp:595)
    and BalanceCoolant (heat.cpp:646) DO null-guard linkedSinks.Resolve(); UpdateCoolant does NOT — a
    reconstruction omission (the shipped binary @4adbf8 must guard, since real condensers ran). Even the
    "correct" Condenser sim (RefrigerationSimulation @4ae4d8, which the reconstructed Condenser ctor fails
    to install) derefs master the same way (heatfamily_reslice.cpp:125-127) -> same crash.

### Why it can't be made stable from the allowed files (mech.cpp/mech.hpp + heat.hpp/powersub.hpp):
The fix is EITHER (a) a one-line null-master guard in heat.cpp::UpdateCoolant (mirroring its own
ConductHeat/BalanceCoolant guards) — but heat.cpp is outside the allowed edit set and UpdateCoolant's
body can't be patched from heat.hpp; OR (b) give the Condenser a VALID linked master heat sink — which
requires the 0xBBE master heat-sink bank (no reconstructed class) to be real AND the inter-sink linkage
to be wired, i.e. the whole heat family at once. Fabricating a self/peer link to dodge the null deref
would produce meaningless heat numbers (the exact "garbage" the brief warns against). This is precisely
the interdependence blocker already called out in "THE REAL BLOCKER" point 4 above — now proven with a
debugger trace rather than predicted.

### Before/after: tick stayed 1 (executable) of 30 — the Condenser tick CRASHED rather than surviving;
reverted -> 1/30 baseline restored. Stability after revert: 3/3 runs alive to 14s, bad-pattern count 0,
[drive] pos advances every frame. Build GREEN (mech.cpp only recompiled). NO files left modified.

### Reverted-to-stub: Condenser (0xBBD) — blocker = heat.cpp::UpdateCoolant null-master deref +
absent inter-sink linkage (interdependence). Reservoir (0xBC0) and HeatSink bank (0xBBE) — not attempted
(0xBBE has no class; Reservoir carries 3 independent crash risks).

### EXACT path forward for a (necessarily family-wide) heat pass:
1. Re-label the SubsystemClassID enum + re-target every switch case to the CLASSMAP class (table above).
2. Reconstruct the 0xBBE master HeatSink-bank class (heatfamily_reslice.hpp documents it: parses
   HeatSinkCount/AmbientTemperature, ctor @4ae8d0) so a real master exists.
3. Wire the heat inter-sink linkage in the Mech ctor (after the subsystem loop, like the existing
   gyro<->sinkSource cross-link @ mech.cpp ~683): link each Condenser/Reservoir's linkedSinks to the
   master bank. (linkedSinks is public on HeatSink — reachable from mech.cpp.)
4. Add the missing null-master guard to heat.cpp::UpdateCoolant (and install Condenser's real
   RefrigerationSimulation in its ctor) — REQUIRES heat.cpp edits (out of scope for this pilot's file set).
5. THEN instantiate the heat family together (Condenser+Reservoir+bank) and verify the tick rises by
   their count without the UpdateCoolant fault.
