Files
BT412/docs/SUBSYS_PLAN.md
T
arcattackandClaude Opus 4.8 8b36440d05 subsystems: un-stub Wave 4 readouts + Wave 6 Myomers (10->15 factory cases)
WAVE 4 (standalone readouts) -- Sensor(0xBC3)/Searchlight(0xBD8)/
ThermalSight(0xBDE)/AmmoBin(0xBCB) un-stubbed via Create<Class>Subsystem
bridges + Torso-style de-shim (drop cross-family shadow fields, redirect
accessors to the real inherited base, static_assert layout locks).
- FIX: Searchlight/ThermalSight ctors gated their Performance on the shadow
  segmentFlags(=0) so it NEVER installed; switched to owner->simulationFlags.
- AmmoBin: retype ammoAlarm HeatAlarm->WatcherGaugeAlarm(0x54) + drop the
  statusState@0x40 shadow -> exact 0x22C layout.
- Guard HeatWatcher::WatchSimulation against the unresolved watchedLink
  (null-deref exposed once these sims run; faithful fix = resolve the link).
- Heat-leaf branch (Sensor) is not byte-exact -> overflow-lock only.

WAVE 6 (Myomers 0xBC6, mover-coupled) -- structural un-stub, gated
BT_MYOMERS (default on; =0 -> Actuator stub). Wired INERT: MyomersSimulation
early-returns (advanced-damage gate stubbed) + no-op mover feed, so the live
JointedMover is untouched and the gait cannot regress. De-shim drops the
owner*/segmentFlags shims to fit the exact-0x358 alloc. Authentic mover/heat
coupling deferred (needs messmgr 0xBD3 + reconciling the mover feed with the
gait cutover).

Verified: BLH tick 20->27, Mad Cat 24, combat DESTROYED un-regressed,
locomotion un-regressed, 0 crashes, 0 heap detections under BT_HEAPCHECK.

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

32 KiB

I have verified the factory switch (mech.cpp 556-741), the stub block (145-181), the corrected ClassID map (CLASSMAP.md), and the heat base re-declarations (heat.hpp 168-242 vs mechsub.hpp 120-255). The findings are corroborated by the source. Here is the unified spec.


UN-STUB THE BATTLETECH SUBSYSTEM ROSTER — SYNTHESIS PLAN

Ground truth used throughout: the ctor address in each factory case's // FUN_004xxxxx comment, reconciled via CLASSMAP.md — NOT the case <Name>ClassID enum label, which is systematically mislabeled. The numeric classID is correct; the label and stub-struct name are wrong.

Master case → real-class table (authoritative; from mech.cpp 567-689 + CLASSMAP)

factory case (line) classID ctor alloc current stub REAL class file (status) special action
CockpitClassID (569) 0xBBD 4ae568 0x230 Cockpit Condenser heatfamily_reslice (DONE)
SensorClassID (574) 0xBBE 4ae8d0 0x1e4 Sensor HeatSink aggregate/bank heatfamily_reslice (#if 0 skeleton) sensorSubsystem=arr[id] (0x1f7) = the heat bank, NOT a sensor
CondenserClassID (580) 0xBC0 4af408 0x230 Condenser Reservoir heatfamily_reslice (DONE)
GeneratorClassID (585) 0xBC1 4b225c 0x250 Generator Generator powersub.cpp (DONE)
PoweredSubsystemClassID (590) 0xBC2 4b0f74 0x31c PoweredSubsystem PoweredSubsystem powersub.cpp (DONE)
MyomersClassID (595) 0xBC3 4b1d18 0x328 Myomers Sensor sensor.cpp (DONE)
GyroClassID (600) 0xBC4 4b3778 0x3d0 Gyro Gyroscope gyro.cpp (DONE) gyroSubsystem=arr[id] (0x14a)
SinkSourceClassID (606) 0xBC5 4b6b0c 0x280 HeatSinkSource Torso torso.cpp (DONE) sinkSourceSubsystem=arr[id] (0x10e)
ActuatorClassID (612) 0xBC6 4b8fec 0x358 Actuator Myomers myomers.cpp (DONE)
WeaponEmitterClassID (617) 0xBC8 4bb120 0x478 EmitterWeapon Emitter emitter.cpp (DONE) ++weaponCount
JumpJetClassID (623) 0xBCB 4bd5c4 0x22c JumpJet AmmoBin ammobin.cpp (DONE)
MechWeaponClassID (628) 0xBCD 4bc3fc 0x448 MechWeapon ProjectileWeapon projweap.cpp (DONE) ++weaponCount
MissileWeaponClassID (634) 0xBCE 4bdcb4 0x484 MissileLauncher UNRESOLVED (no CLASSMAP entry) ++weaponCountBLOCKED
BallisticWeaponClassID (640) 0xBD0 4bcff0 0x44c BallisticWeapon MissileLauncher mislanch.cpp (DONE) ++weaponCount
MechControlsMapperID (646) 0xBD3 49bca4 0x130 MechControlsMapper SubsystemMessageManager messmgr.cpp (DONE) controlsMapper=arr[id] (0x10d) — verify, see Risk 4
GaussWeaponClassID (652) 0xBD4 4bb888 0x478 GaussRifle PPC (Emitter subclass) emitter.cpp bridge ++weaponCount
MechTechClassID (658) 0xBD6 4b7f94 0x2a4 MechTech (real, MIS-WIRED) HUD hud.cpp (DONE) mechTechSubsystem=arr[id] (0x16d) — see Risk 4
LegSubsystemClassID (664) 0xBD8 4b84dc 0x238 LegSubsystem Searchlight searchlight.cpp (DONE)
HeatableClassID (669) 0xBDC 4ad228 0x140 HeatableSubsystem (ptr discarded — bug) MechTech mechtech.cpp (DONE) must arr[id]=… + cache mechTech
DisplayClassID (679) 0xBDE 4b8718 0x234 MechDisplay ThermalSight thermalsight.cpp (DONE)

There is NO real Cockpit / Sensor(@0xBBE) / Actuator / JumpJet / BallisticWeapon / LegSubsystem / MechDisplay class — every one of those stub names is a mislabel for a different, already-reconstructed class.

Two confirmed wiring bugs visible in the source: (a) case 0xBDC builds HeatableSubsystem and never stores the pointer (line 676 has no subsystemArray[id] =); (b) the real MechTech class is instantiated at case 0xBD6 (line 660, alloc 0x2a4 = HUD's size) — MechTech and HUD are swapped.


1. THE STUB → REAL SWAP RECIPE (general)

Architecture decision (applies to every family): use a per-family factory-bridge function, do NOT #include the real subsystem headers into mech.cpp. mech.cpp defines local bring-up stubs whose names collide with the real classes (Condenser, Generator, PoweredSubsystem, Myomers, MechWeapon, MissileLauncher, …). Including the real headers there is a redefinition storm. Instead:

Per real class, in its own .cpp (which already includes its real header):

Subsystem *
Create<Class>Subsystem(Mech *owner, int id, void *seg)
{
    Check(sizeof(<Class>) <= SIZE);        // SIZE = the factory alloc literal (binary's true size)
    return (Subsystem *) new (Memory::Allocate(SIZE))
        <Class>(owner, id, (<Class>::SubsystemResource *)seg
                /*, <Class>::DefaultData [, extra real ctor params] */);
}
  • SIZE = the exact Memory::Allocate(0x…) literal already in the factory case (these are the binary's real allocation sizes — keep them).
  • The Check(sizeof <= SIZE) guard catches a placement-new overrun, which is the source of the intermittent ~1-in-6 startup heap-corruption crash (base ctor writes vitalSubsystemIndex@0x110 → object must be ≥ 0x114).
  • The trailing <Class>::DefaultData matches the real ctor signature (Mech*, int, SubsystemResource*, SharedData& = DefaultData [, …]). Where the factory already passes …::DefaultData/,0,0, preserve those real tail params.

In mech.cpp:

extern Subsystem *Create<Class>Subsystem(Mech *, int, void *);     // near the forward-decls
...
case <Name>ClassID:                       // keep the numeric classID; the LABEL stays as-is
    subsystemArray[id] = Create<Class>Subsystem(this, id, seg);
    /* preserve ++weaponCount and/or the special cache (sensorSubsystem=…/gyroSubsystem=…) */
    break;

Then delete the now-dead RECON_SUBSYS(<StubName>); line for that class. Leave a RECON_SUBSYS only where no reconstruction exists yet (the 0xBCE @4bdcb4 case).

The 4 systemic checks — apply to EVERY real class before/while swapping

  1. Shadowing (re-declared base fields). Grep the class + its bases for fields that already exist in the engine Subsystem/Simulation chain or in the reconstructed BT base (MechSubsystem/HeatableSubsystem/HeatSink/PoweredSubsystem): owner/hostEntity (→ Subsystem::owningEntity, use a GetEntity() cast), damageZone (→ engine Subsystem::damageZone), simulationState/destroyed (→ MechSubsystem::simulationState), simulationFlags/flags/statusFlags, subsystemID/subsystemId2, subsystemName, lastPerformance/lastUpdate/updateModel, sharedData, classID. Delete the re-declaration; use the inherited member/accessor. A shadow both (a) leaves the engine's value unread (the recon copy is 0xCDCDCDCD) and (b) lands at the wrong compiled offset. NOT a shadow (leave as is): each class's own typedef … Performance; + SetPerformance(Performance) (deliberately writes the inherited activePerformance), its own typedef … SubsystemResource;, and static …DefaultData. After de-shadowing, confirm sizeof(Class) ≤ alloc SIZE.
  2. Wword(N) trap. mechrecon.hpp:192 Wword(i) is a global scratch bank (bank[i&0x3ff]), NOT this+i*4. Grep the class body for Wword( used as object/pointer access → replace with the named inherited member or an engine accessor (e.g. owner via (Mech*)GetOwningSimulation()). The heat/gyro/sensor/torso/myomers/powersub bodies are already clean here — keep them that way; Wword use is a mech.cpp-only concern.
  3. MessageHandler chaining. Each class's handler set must chain to its parent: Receiver::MessageHandlerSet X(Parent::GetMessageHandlers());. An empty/parentless set makes Receiver::Receive find no handler → inherited messages (TakeDamage, InjectCoolant, ToggleLamp, Jammed, …) silently dropped. Gyroscope/Sensor/Myomers and all heat classes currently define GetClassDerivations but an empty/missing GetMessageHandlers — fix on swap.
  4. Performance install + validity. Confirm the real ctor calls SetPerformance(&Class::XxxSimulation) under the live-primary gate ((owner->simulationFlags & 0xC)==0, some also require & 0x100). Without it, activePerformance stays &Simulation::DoNothingOnce, which on its first tick calls NeverExecute() → sets DontExecuteFlag → the subsystem is skipped forever (this is exactly why a stub "ticks once then goes silent"). Entity-validity for message delivery rides the mech's validity (already handled in bring-up).

Tick is already wired — no new plumbing. mech4.cpp Mech::PerformAndWatch (~825-871) walks subsystemArray[0..subsystemCount-1], and for each s->IsNonReplicantExecutable() calls s->PerformAndWatchSimulation::PerformAndWatch (SIMULATE.cpp:448) → Perform(slice)(this->*activePerformance)(slice). Installing activePerformance (check #4) is the entire difference between a stub and a running subsystem.

Build/verify each swap: cmake --build C:/git/nick-games/btbuild/build --config Debug; single file C:/git/nick-games/btbuild/compile1.cmd <file>.cpp. Watch the [tick] first frame: dispatched to N … of M present log — N should climb toward M (minus the bypassed controlsMapper) as stubs become real.


The whole roster sits on the BT base chain engine Subsystem → MechSubsystem → HeatableSubsystem → HeatSink → {PoweredSubsystem, Generator, Condenser, Reservoir, Bank} and PoweredSubsystem → {PowerWatcher → Gyro/Torso/HUD/Searchlight/ThermalSight, MechWeapon → Emitter/PPC, ProjectileWeapon → MissileLauncher}. So the heat base re-base is the universal prerequisite — it must be correct before any leaf is trustworthy. This aligns with the user mandate (heat first).

  • WAVE 1 — Heat base re-base + de-shadow (PREREQUISITE, no factory change). Reconcile mechsub.hppheat.hpp; make HeatableSubsystem : MechSubsystem; delete shadow fields; fix the thermal offsets. Compile heat.cpp, heatfamily_reslice.cpp, mechsub.cpp green. Unblocks every family.
  • WAVE 2 — HEAT family + the MechTech/HUD wiring fix (user: "heat first"). Un-stub Condenser (0xBBD), Reservoir (0xBC0), aggregate sink (0xBBE). Fold in the MechTech↔HUD swap (0xBDC→MechTech with store + cache; 0xBD6→HUD) because the 0xBDC case currently instantiates the heat base and discards the pointer — it has to change here anyway, and it validates the one already-real class in its correct slot. Verify heat builds/dissipates.
  • WAVE 3 — Energy weapons (user: "emitter/PPC second").
    • 3a (dependency): power bus — Generator (0xBC1) + PoweredSubsystem (0xBC2). Low-risk, foundational; energy weapons need it to charge (GetVoltageState()==4).
    • 3b: Emitter (0xBC8) + PPC (0xBD4). Construct+tick+fire underneath; keep the explosion stand-in for the visual (beam renderable deferred — see §4).
  • WAVE 4 — Standalone readouts: DONE + WIRED (live, verified). Sensor (0xBC3), Searchlight (0xBD8), ThermalSight (0xBDE), AmmoBin (0xBCB) all un-stubbed via Create<Class>Subsystem bridges + de-shim + <Class>LayoutCheck static_asserts. BLH tick 20→26, Mad Cat 24, combat DESTROYED un-regressed, 0 crashes. KEY finds: (a) Searchlight/ThermalSight ctor gate read the shadow segmentFlags(=0) → Performance NEVER installed → fixed to owner->simulationFlags; (b) AmmoBin ammoAlarm retyped HeatAlarmWatcherGaugeAlarm(0x54) + statusState@0x40 shadow deleted; (c) the heat-leaf branch (Sensor's PoweredSubsystem:HeatSink) is NOT layout-exact → overflow-lock only (offset re-base deferred to Phase-4 HUD); (d) shared HeatWatcher::WatchSimulation null-deref on the unresolved watchedLink GUARDED (faithful follow-up = resolve the watch link). See CLAUDE.md §10d "WAVE 4".
  • WAVE 5 — Torso (0xBC5): DONE (live, binary-exact layout, zero regression). The joint I/O (Mech::ResolveJoint/Torso::PushTwist + corrected TorsoSimulation + per-frame UpdateJoints()) AND the base-chain re-base are complete. Hierarchy is Torso : PowerWatcher : HeatWatcher : MechSubsystem (a SEPARATE branch from the HeatSink leaves — shares only MechSubsystem, so the +156 fix stays in the Watcher branch). Fix: Watcher-local WatchedConnection(0xC) + WatcherGaugeAlarm(0x54) in heatfamily_reslice.hpp (never resize the shared SubsystemConnection/HeatAlarm); grew HeatWatcher (+84) + PowerWatcher (+72, shadow watchedLink deleted); Torso shim fields deleted + 0x270 slot added → currentTwist@0x1D8, sizeof==0x280, compile-time locked (TorsoLayoutCheck/HUDLayoutCheck + Watcher-base asserts). Real Torso un-stubbed at 0xBC5; verified no crash, heat/combat un-regressed (TARGET DESTROYED after 8 hits). Full detail: CLAUDE.md §10d "WAVE-4 Torso base-chain". NOTE: the torso doesn't visibly twist for the Blackhawk because its 0xBC5 record has TorsoHorizontalEnabled=0 (a FAITHFUL result — the binary ctor @004b6b0c gates on it identically); visible twist needs a twist-enabled mech. Gyroscope (0xBC4) — layout+joint-I/O DONE, un-stub DEFERRED (reverted to stub). Shim-delete + accessor-redirect + node-I/O (backed by the real engine Joint API, incl. FUN_0041d11c=SetTranslation) + ResolveJoint reuse + WriteEye/ MechJoint wiring are done and locked (GyroLayoutCheck: exageration@0x1D8, sizeof<=0x3D0). Verified live: gyro constructs, resolves real joints (jointlocal/jointeye), WriteMechJoint fires — but with GARBAGE (0xCDCDCDCD/NaN) because the ctor field-init + integrator field-MAPPING are incomplete/incorrect (binary @004b3778: springConstant@0x1E8, dampingConstant@0x1F4, ...; recon mislabels + leaves accumulators uninit). Reverted to the stub (no NaN to the root joint). Remaining = a full ctor+integrator reconstruction from @004b3778 (bigger than the torso). Full detail: CLAUDE.md §10d "WAVE-5 GYROSCOPE". Also: the mech.cpp gyro↔torso cross-link write is broken (SubProxy::linkTarget→gyro+4) — commented out.
  • WAVE 6 — Mover-coupled: ◐ STRUCTURAL un-stub DONE + WIRED (gated BT_MYOMERS default ON; verified INERT, no locomotion/combat regression). Myomers (0xBC6) constructs + ticks its real Performance via CreateMyomersSubsystem, but is INERT: MyomersSimulation early-returns on OwnerAdvancedDamage()==False (real gate = messmgr 0xBD3), and the mover feed (MoverAttach) + SetOwnerMaxSpeed are no-op stubs so the live JointedMover is never touched. De-shim dropped the owner*/segmentFlags shim fields (accessors return neutral defaults) to fit the exact-0x358 alloc (sizeof lock). ⚠ AUTHENTIC COUPLING DEFERRED: routing SpeedEffect@0x31C into the mover + real owner-motion accessors + the advanced-damage heat gate — this DRIVES THE LIVE MOVER, so reconcile with the gait cutover FIRST. BLH tick 26→27; BT_MYOMERS=0 → Actuator stub. See CLAUDE.md §10d "WAVE 6".
  • WAVE 7 — Projectile/missile weapons: ProjectileWeapon (0xBCD), MissileLauncher (0xBD0). PARTIALLY BLOCKED: case 0xBCE calls @4bdcb4, which has no CLASSMAP entry — decompile it first and resolve the 0xBCD/0xBCE/0xBD0 label tangle (Risk 1) before swapping these.
  • WAVE 8 — Message hub: SubsystemMessageManager (0xBD3). Real, but verify the cache-slot/tick-bypass interaction (Risk 4) before enabling its tick.

DEFER / DO NOT ENABLE:

  • MechControlsMapper tick — the real mapper is installed at roster slot 0 by btl4app::MakeViewpointEntity (not by this switch); ticking it calls InterpretControls→FillPilotArray, reading undocumented WinTesla Application offsets (application+0x6c→+0x190→pilot+0x1e0) → access violation (mech4.cpp:854-866 deliberately bypasses it). Keep bypassed; this is a Phase-8 input-remap task.
  • 0xBCE @4bdcb4 — leave its RECON_SUBSYS(MissileLauncher) stub until decompiled.

3. HEAT FAMILY — detailed ordered edit list

Step 1 — heat.hpp re-base + de-shadow (PREREQUISITE; do this before any factory edit)

All in C:/git/nick-games/decomp/reconstructed/heat.hpp (+ matching touch-ups in heat.cpp / heatfamily_reslice.cpp). The collisions are confirmed: mechsub.hpp already owns Mech *owner (line 134), int simulationState;//@0x40 (251), ReconDamageZone *damageZone;//@0xE0 (252), Mech *hostEntity;//@0xE8 (254), int subsystemId2;//@0xEC (255), plus GetStatusFlags@4ac144 / HandleMessage@4ac0bc / ResetToInitialState@4ac1d4 / PrintState@4ac8c0 virtuals — while heat.hpp re-declares all of these on top of : public Subsystem.

  1. Base change. heat.hpp:33 — ensure #include <mechsub.hpp>. heat.hpp:168-169 — class HeatableSubsystem: public Subsystem: public MechSubsystem.
  2. Delete the shadow fields (heat.hpp:234-238):
    • Mech *owner → inherited MechSubsystem::owner (or a Mech* GetEntity(){return (Mech*)Subsystem::GetEntity();} accessor, matching surviving MECHTECH.HPP).
    • LWord flags / LWord statusFlags (both @0x28) → engine Simulation::simulationFlags. Retarget flags |= 8/2 (Condenser/Reservoir) to simulationFlags.
    • LWord statusBits (@0x60) → engine dirty word; Reservoir statusBits |= 1ForceUpdate().
    • int destroyed (@0x40) → MechSubsystem::simulationState (DestroyedState); IsDamaged() reads it.
  3. Thermal offsets (heat.hpp:240-241 + HeatSink). CLASSMAP:472 — the 0x114-0x184 block belongs to HeatableSubsystem: keep currentTemperature@0x114, move degradationTemperature@0x118 / failureTemperature@0x11C UP from HeatSink into HeatableSubsystem between currentTemperature and heatLoad, so heatLoad lands at @0x120 (today it compiles to 0x118).
  4. Virtual surface (heat.hpp:192-201) now overrides MechSubsystem's real slots — do not add parallel slots. Match the binary signature (watch MechSubsystem::ResetToInitialState(Logical powered) vs heat's ResetToInitialState()).
  5. HeatSink overlap words (~heat.hpp:382-412): valveState@0x1D0/field_1d0@0x1D0 and refrigerationOutput@0x160/massScale@0x160 are the SAME binary word reused by subclasses — declare each once in the base it belongs to so subclasses extend rather than re-append.
  6. Handler chains (systemic #3): define GetMessageHandlers() for each heat class to chain to its parent (Receiver::MessageHandlerSet(HeatableSubsystem::GetMessageHandlers()), etc.). Today only GetClassDerivations is defined → HeatSink::HandleMessage(1) and Reservoir InjectCoolant are silently dropped.

Verify: compile1.cmd heat.cpp and compile1.cmd heatfamily_reslice.cpp green; sizeof(Condenser) ≤ 0x230, sizeof(Reservoir) ≤ 0x230.

Step 2 — factory bridges (in heatfamily_reslice.cpp)

Add CreateCondenserSubsystem (SIZE 0x230), CreateReservoirSubsystem (SIZE 0x230), CreateHeatSinkBankSubsystem (SIZE 0x1e4) per the §1 template (Condenser/Reservoir take …::DefaultData).

Step 3 — mech.cpp factory edits

  • extern the three bridges near the forward-decls.
  • Case 0xBBD (CockpitClassID, 569-572): subsystemArray[id] = CreateCondenserSubsystem(this, id, seg);
  • Case 0xBC0 (CondenserClassID, 580-583): subsystemArray[id] = CreateReservoirSubsystem(this, id, seg);
  • Case 0xBBE (SensorClassID, 574-578): subsystemArray[id] = CreateHeatSinkBankSubsystem(this, id, seg); sensorSubsystem = subsystemArray[id];keep the cache (slot 0x1f7 is the heat bank, per CLASSMAP:455-458; the name "sensor" is a mech.cpp mis-guess).
    • 0xBBE interim (still real, not a stand-in): the aggregate is a #if 0 skeleton (Performance @4ae73c, 351 bytes, unfilled). Until reconstructed, have CreateHeatSinkBankSubsystem instantiate the real HeatSink base (alloc 0x1e4) so the master gate + HeatSinkSimulation run. This sink is the central heat engine that relaxes stored heat toward the ~300K ambient — without it the mech has no heat dissipation (Risk 8).
  • Delete RECON_SUBSYS(Cockpit) (161), RECON_SUBSYS(Sensor) (162), RECON_SUBSYS(Condenser) (163). Leave the others for later waves.

Step 4 — MechTech ↔ HUD un-swap (folded into this wave)

  • Add CreateHUDSubsystem (hud.cpp, SIZE 0x2a4) and CreateMechTechSubsystem (mechtech.cpp, SIZE 0x140 — ≥ real 0x104 and ≥ the 0x114 base-ctor write).
  • Case 0xBD6 (MechTechClassID, 658-662): build HUDsubsystemArray[id] = CreateHUDSubsystem(this, id, seg);.
  • Case 0xBDC (HeatableClassID, 669-677): build MechTechsubsystemArray[id] = CreateMechTechSubsystem(this, id, seg); mechTechSubsystem = subsystemArray[id]; (this fixes the discarded-pointer bug).
  • The mechTechSubsystem cache (slot 0x16d) — see Risk 4 for whether it caches MechTech or HUD; default to caching the real MechTech at 0xBDC.

Step 5 — internal heat linkage (depends on the bank existing)

The HeatSink ctor resolves heatSinkIndex and linkedSinks.Add(master); Reservoir ctor calls link->Attach(this) and scales by link->field_1d0. These need the 0xBBE master sink (Step 3) present so linkedSinks.Resolve() is non-null; the null-master guards (heat.cpp:595/646/693) keep it safe-but-inert otherwise. The mech.cpp:700-704 gyro↔+0x1d8 link is Torso, not heat — leave it for Wave 5.

Verify heat build/dissipate

  1. In each heat ctor, after the master-gate block, log when the gate arms (name << " heat gate armed"). No "armed" line for the 0xBBE sink ⇒ the gate/owner->simulationFlags path is wrong.
  2. In HeatSink::HeatSinkSimulation (heat.cpp:516), gated to the viewpoint mech, log per tick: currentTemperature, heatEnergy, pendingHeat, coolantLevel, heatLoad, coolantActive.
  3. With BT_FORCE_FIRE=1 BT_SPAWN_ENEMY=1: weapons deposit into pendingHeatcurrentTemperature/heatEnergy climb, heatLoad (15-sample average) ramps, coolantActive→1 and coolantLevel drains; on cease-fire the aggregate relaxes currentTemperature toward ~300K and coolantLevel recovers via Reservoir::DrawCoolant. Alarm transitions log via HeatSink::PrintState (Normal/Degradation/Failure).
  4. Offset proof: the cockpit Heat gauge (btl4gaug Heat connection @4c3720 reads currentTemperature@0x114, CLASSMAP:260) must track the logged temperature. Garbage/0xCDCDCDCD ⇒ the MechSubsystem re-base/offsets are still off.

4. EMITTER / PPC WEAPON — detailed ordered edit list (make ONE real beam weapon fire)

Do heat first (E5 below depends on it). All offsets per CLASSMAP:476-484.

E1 — Bridge (emitter.cpp). Add CreateEmitterSubsystem(Mech*, int, void* seg)new (Memory::Allocate(0x478)) Emitter(owner, id, (Emitter::SubsystemResource*)seg, Emitter::DefaultData);. Use it for both 0xBC8 and 0xBD4 — PPC is an Emitter subclass whose FireWeapon@4bb878 just tail-calls Emitter::FireWeapon@4bace8, and the renderer keys off the streamed classID, so behavior is identical now. (A struct PPC : Emitter {…} for vtable/classID fidelity can be added later.)

E2 — ctor null-deref + uninit (emitter.cpp ~653-680). FUN_00417ab4 (voltage-source resolver) returns 0 until the bus is real, then src->ratedVoltage and TrackSeekVoltage's src->voltage deref null = construction-time crash. Add energyCoefficient = 1.0f; (@0x454) before the electrical branch (it is read by FireWeapon at line 160 but assigned only inside that branch). Resolve src once; gate the whole electrical block on src != 0 && src->ratedVoltage > 0.0f; in the else path keep seekVoltage[i] as the raw resource fractions, dividing by v only when v > 0.

E3 — inert targeting (mechweap.cpp/.hpp). HasActiveTarget() (mechweap.hpp:182) returns a never-set local hasTarget → fire gate permanently false. Read the owner instead (mech4.cpp already writes mech+0x388 = MECH_TARGET_ENTITY):

Logical MechWeapon::HasActiveTarget() const {
    Mech *owner = (Mech *)GetOwningSimulation();
    return (owner && *(Entity **)((char *)owner + 0x388)) ? True : False;
}

Have GetTargetPosition read owner +0x37c; drop the hasTarget member (systemic #1).

E4 — Loaded→Firing gate reachable (emitter.cpp:259). firingArmed (aliases MechWeapon::recoil@0x3E8) is uninitialized and written by nothing → the if (firingArmed && HasActiveTarget()) gate reads garbage. Initialize it True in the ctor / ResetToInitialState for bring-up, pending its real semantic (Risk 3).

E5 — heat self-load (emitter.hpp:283). heatAccumulator is declared as an Emitter member → lands ~0x3f0+, NOT the inherited HeatableSubsystem accumulator @0x1c8, so FireWeapon's heatAccumulator += heatPortion never reaches the heat sim. Delete the Emitter-local copy; write the inherited @0x1c8 accumulator. Depends on the heat module (Wave 1/2) — this is why heat is first.

E6 — mech.cpp factory. extern Subsystem *CreateEmitterSubsystem(Mech*,int,void*);. Replace case 0xBC8 (617-621) and case 0xBD4 (652-656): subsystemArray[id] = CreateEmitterSubsystem(this, id, seg); ++weaponCount; break;. Delete RECON_SUBSYS(EmitterWeapon) (170) and RECON_SUBSYS(GaussRifle) (174). The Emitter ctor already installs activePerformance = EmitterSimulation (emitter.cpp:627), so the roster loop ticks it every frame for the authoritative mech — no extra plumbing.

E7 — power bus (Wave 3a, required for the real fire path). While Generator/PoweredSubsystem are stubs, GetVoltageState() never returns 4 → currentLevel never charges → the weapon never reaches Loaded. Faithful path: un-stub Generator (0xBC1) and PoweredSubsystem (0xBC2) so FUN_00417ab4 resolves a real VoltageSource. Bring-up shortcut (still drives the real state machine): force-charge — currentLevel = seekVoltage[idx]; weaponAlarm → Loaded.

E8 — trigger wiring (mech4.cpp ~707-736). The mapper is bypassed, so nothing writes fireImpulse@0x31c and CheckFireEdge() never sees a rising edge. Where gBTDrive.fire is handled, locate the emitter in subsystemArray (classID 0xBC8/0xBD4) and set its fireImpulse > 0 for the frame (small setter/friend on the weapon). This is the faithful replacement for the explosion stand-in's input read.

What blocks a VISIBLE shot (and the honest interim). btl4vid.cpp::MakeMechRenderables dispatches 0xBC8 Emitter beam / 0xBD4 PPC beam (CLASSMAP:334) but defers the beam renderable to the unported dpl2d_ 2D display-list layer (btl4vid.cpp:244-245, 452-458). So a perfectly-firing Emitter draws nothing. Honest interim: keep the mech4.cpp explosion stand-in (707-736) for the visual while the real Emitter mechanics (charge → LoadedCheckFireEdgeFiringFireWeapon splits damagePortion/heatPortion, sets dischargeTimer/beamFlag/targetEntity) run underneath. The shot is real; only its rendering is borrowed until the dpl2d_ beam lands. (Net: E1/E2/E6 = construct+tick safely; E3/E4/E5 + E7 = actually fire and self-heat; E8 = trigger; the dpl2d_ beam = finally visible.)

Net dependency: E5 needs heat (Waves 1-2); the real-fire path needs the power bus (E7 = Wave 3a); the visible beam needs dpl2d_ (out of scope — keep the stand-in).


5. RISKS / UNKNOWNS — and how to settle each at the binary

  1. 0xBCE @4bdcb4 unresolved (blocks Wave 7). No CLASSMAP/recon at that address; sits between AmmoBin (4bd5c4) and MissileLauncher (4bcff0). Settle: run tools/ghidra_scripts/ExportBTSource.java on 4bdcb4 (and its CSS/vtable); it is most likely a ballistic-autocannon variant of ProjectileWeapon. Reconcile the 0xBCD(ProjectileWeapon)/0xBCE(?)/0xBD0(MissileLauncher) label tangle before swapping any of the three. Leave RECON_SUBSYS(MissileLauncher) at 0xBCE until done.
  2. HeatableSubsystem base: MechSubsystem vs Subsystem. mechsub.hpp and heat.hpp are overlapping reconstructions of the same cluster (vtable 0x50e210, classID 0xBBB, dtor 0x4ac868, model 0xE4). Settle: confirm sizeof(MechSubsystem) == 0x114 and that currentTemperature begins exactly at 0x114 (HeatSink temp init @4adda0). Recommended resolution (Wave 1): HeatableSubsystem : MechSubsystem, one DamageZone built by the MechSubsystem resource ctor (@4ac644). Cross-check via the Heat gauge offset proof (§3 verify #4).
  3. firingArmed real semantic (@0x3E8, aliases MechWeapon::recoil). Settle: decompile around the Loaded→Firing gate and the fire-group dispatch to find what writes this+0x3e8 in the binary (a weapon-selected/enabled flag from the controls path). Until then init True for bring-up (E4).
  4. Special-cache slots — re-derive each member's TRUE meaning. The Wword index is ground truth for which Mech member is written, but the names are mislabeled: sensorSubsystem(0x1f7)=heat bank; mechTechSubsystem(0x16d)=MechTech-or-HUD; controlsMapper(0x10d)=mapper-or-message-manager; gyroSubsystem(0x14a)=gyro; sinkSourceSubsystem(0x10e)=torso. Settle: read which Mech offset the binary writes at each slot and who consumes it. Critical for SubsystemMessageManager (0xBD3, Wave 8): verify slot 0x10d is NOT the same member mech4.cpp bypasses in the tick (if (s == controlsMapper) continue;) — if the message manager lands in the bypassed slot it will never tick. And confirm whether the HUD (0xBD6) needs its own cache distinct from mechTechSubsystem.
  5. MechControlsMapper tick AV (confirmed). InterpretControls→FillPilotArray reads application+0x6c→+0x190→pilot+0x1e0 (DAT_004efc94) → wild pilot pointer → AV (mechmppr.cpp:46). Settle: only as a Phase-8 input-remap task with the real WinTesla Application layout. Until then keep it bypassed; the factory's 0xBD3 case is the message manager, not this mapper.
  6. Gyro/Torso joint writes — TORSO SIDE RESOLVED. Stubbed no-ops needing a Skeleton/DCS API. The real engine already exposes it: JointedMover::GetSegment(CString)EntitySegment::GetJointIndex()JointSubsystem::GetJoint(i)Joint::SetRotation(Radian|EulerAngles) (all PUBLIC, in munga_engine.lib). Torso's ResolveJoint/PushTwist are reconstructed against this (DONE). Gyro's WriteEyeJoint/WriteMechJoint/ ResolveJoint should reuse the same hoisted Mech::ResolveJoint + the joint-write dispatch (follow-up). Not a separate DCS task — the write path is engine-native. (Torso's live wiring is still gated on the WAVE-5 base re-base, not on this.)
  7. Myomers mover coupling (Wave 6). ConnectToMover routes SpeedEffect@0x31C into **(Mech+0x128); RegisterMaxOutput writes maxSpeed@0x7A0 and loops the myomer list @0x1EB. Settle: verify those Mech offsets exist/are valid in the reconstructed Mech before enabling, else no-op/wild writes. Also note powersub.hpp still declares a STALE Myomers (the old Sensor mislabel) — #include <myomers.hpp> first and watch for the name collision at link.
  8. Aggregate heat sink (0xBBE) unfinished. Performance @4ae73c (351 bytes) is a #if 0 skeleton; it's the load-bearing central heat engine. Settle: decompile 4ae8d0/4ae73c, finish AggregateHeatSink, give it GetClassDerivations/DefaultData. Interim = real HeatSink base at 0xBBE (real, not a stand-in) so the master gate + HeatSinkSimulation run.
  9. Placement-new overrun (general). After de-shadowing, sizeof(Class) can exceed the factory alloc SIZE → heap corruption (the 1-in-6 crash). Settle: the Check(sizeof(<Class>) <= SIZE) guard in every bridge (§1) makes this a hard, immediate assert instead of an intermittent corruption.

Key files (all under C:/git/nick-games/decomp/reconstructed/): mech.cpp (factory 556-741, stubs 145-181, enum mech.hpp:253-272), mech4.cpp (tick 825-871, target slots/explosion 707-736), heat.hpp/heat.cpp/heatfamily_reslice.{hpp,cpp}, mechsub.hpp, mechtech.{cpp,hpp}, hud.cpp, emitter.{cpp,hpp}, mechweap.{cpp,hpp}, powersub.{cpp,hpp}, sensor/gyro/torso/myomers/searchlight/thermalsight/ammobin/projweap/mislanch/messmgr.cpp, CLASSMAP.md (authoritative). Engine bases: C:/git/nick-games/elsewhen_extracted/Elsewhen RP411 Source/trunk/MUNGA/{SUBSYSTM,SIMULATE,RECEIVER,VDATA}.h, SIMULATE.cpp (PerformAndWatch @448), ENTITY.cpp (roster tick @698).