Files
BT412/docs/RECONCILE.md
arcattackandClaude Fable 5 267059ab88 Damage economy RECONCILED: 3 stand-ins fixed, 3 paths confirmed authentic (task #60)
Full 5-path audit of the damage economy vs the decomp (5-finder +
adversarial-verify workflow), resolving the KB self-contradiction the
binary-coverage audit flagged.

CONFIRMED AUTHENTIC as-is (no change needed):
- Energy beam (emitter.cpp): damagePortion = authored DamageAmount x
  (charge/seekV)^2; the ctor x1e7 and fire x1e-7 cancel (_DAT_004bafbc
  dumped from the exe = x87 80-bit 1e-7 exactly).
- Autocannon (projweap.cpp): full authored DamageAmount from resource
  +0x19C delivered unmodified; the 0.0625 at :667 is the shooter's own
  gyro recoil, not the round.
- Zone-armor BASE model: damageLevel += amount x damageScale[type]
  (engine DAMAGE.cpp:379, called mechdmg.cpp:427), legs x0.5, 1.0=dead.

3 STAND-INS FIXED (all byte-verified against the decomp):
- A. mechdmg.cpp:451 -- read the phantom `stance` member (no binary
  offset, zero writers -> perma-0), so the leg-shot-out -> fall/death
  branch was DEAD. Now MovementMode() (mech+0x40, @part_012.c:6910).
- B. mechdmg.cpp:458 -- guarded the leg partial-failure graphic on the
  always-0 IsAirborne() stub where the binary calls IsDisabled()
  (@0049fb54 = movementMode 2||9). On a wreck the binary SUPPRESSES the
  write; the stub let it corrupt graphicAlarm 9->4/3 -- the task-#52
  wreck-graphic bug, now fixed AT SOURCE (was only masked by the
  IsMechDestroyed latch).
- C. mech4.cpp:1551 -- flat kShotDamage=12 fed as the kill-score
  damageAmount (the KB self-contradiction: task #8 claimed it retired,
  but it was live). The score handler @0x4c02e4 derives the whole kill
  award from it, so every kill scored identically regardless of weapon.
  Now lastInflictingDamage -- the real killing-blow magnitude, latched
  in TakeDamageMessageHandler (mech.cpp:624), mirroring the per-hit path
  (mech4.cpp:1207). The phantom `int stance` slot is reused for the new
  Scalar member (size-neutral, no layout shift); init 0 in the ctor.

DEFERRED (task #60-D, documented): the missile CLUSTER model -- the port
fires N flying rounds (net armor total authentic) vs the binary's ONE
missile with a random burstCount cluster roll (loses cluster variance +
single-zone concentration). Blocked on an OPEN decomp semantic (does
burstCount multiply armor or only the gyro kick? settle at FUN_004bef78
-> FUN_004be078 -> EXPLODE.cpp:209-210).

VERIFIED live: clean build; 2-node fight -> clean center-mass kill (no
crash, kills 0->1); [zone-armor] dump confirms per-zone armor 50-140 +
legs x0.5. NB the displayed POINTS score still reads 0 -- a SEPARATE
open gap (scoreAward + role/team/tonnage multipliers unwired); fix C
corrected the damage INPUT to that formula.

KB swept: open-questions.md (self-contradiction resolved + task #60
summary + deferred missile item), combat-damage.md (damageScale is
type-indexed not even/odd; task-#52 source fix; kill-score section),
RECONCILE.md (missile = ONE spawn not N), stale comments in mechweap.cpp
(SendDamageMessage is LIVE), mislanch.hpp, mechdmg.cpp (FUN_0049fb54 =
IsDisabled).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 15:38:04 -05:00

1560 lines
113 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.