--- id: subsystems title: "Subsystems — the factory + the reconstruction waves" status: established source_sections: "PROGRESS_LOG.md §10d; docs/SUBSYS_PLAN.md; docs/VEHICLE_SUBSYSTEMS.md; docs/RESOURCE_AUDIT.md" related_topics: [decomp-reference, reconstruction-gotchas, combat-damage, gauges-hud] key_terms: [subsystem, factory, ClassID, subsystem-roster, PoweredSubsystem, bridge] open_questions: - "Myomers coupling (gates on owning-player @mech+0x190); factory capability-roster loops 2-4 still dead (SubProxy stub)" --- # Subsystems The mech's components (heat/power/weapons/sensor/gyro/torso/myomers/…), built by the Mech-ctor factory. Full per-family plan: `docs/SUBSYS_PLAN.md`; the engineering-cluster panels: `docs/VEHICLE_SUBSYSTEMS.md`; the layout audit: `docs/RESOURCE_AUDIT.md`. ClassID map: [[decomp-reference]] §2. ## The factory The Mech ctor switches on the resource ClassID to build each subsystem into the [[subsystem-roster]] (`subsystemArray@0x128`). **The `case ClassID` LABELS are systematically MISLABELED** — the real class is the `// FUN_004xxxxx` ctor-address comment reconciled via `CLASSMAP.md`, not the case name (e.g. 0xBBE case="Sensor" is actually AggregateHeatSink). Each real class is wired via a `CreateSubsystem(Mech*,int,void*)` **bridge** in the class's own `.cpp` (do NOT `#include` the real subsystem headers into mech.cpp — the local stubs collide). Keep the alloc SIZE + special-cache. [T2] ## Class hierarchy (two branches, shared only at MechSubsystem) - **Heat leaf:** `PoweredSubsystem : HeatSink : HeatableSubsystem : MechSubsystem`. Emitter/PPC/ Sensor/Myomers/ProjectileWeapon/MissileLauncher/GaussRifle chain here. Uses 0xC `SubsystemConnection` + 0x54 `GaugeAlarm54` (alarm level at +0x14). [T1] - **Watcher branch:** `Torso/Gyroscope/Searchlight/ThermalSight/HUD : PowerWatcher : HeatWatcher : MechSubsystem`; `AmmoBin : HeatWatcher`. Uses a 0xC `WatchedConnection` + 0x54 `WatcherGaugeAlarm`. [T1] Making a base byte-exact GROWS every subclass — they must be re-based TOGETHER in one build, each `static_assert`-locked (the P7 alarm-unification: HeatSink 0x1D0 → PoweredSubsystem 0x31C → MechWeapon 0x3F0 → Emitter 0x478). [T2] ## Reconstruction waves (state: ALL 20 factory cases wired to real classes — 0xBD3 = the real SubsystemMessageManager since task #7, 2026-07-11; the mapper lives in roster slot 0) - **WAVE 1** — HeatableSubsystem re-based onto MechSubsystem (de-shadow cascade). - **WAVE 2** — heat family (Condenser/AggregateHeatSink/Reservoir) + HUD + MechTech (un-swap). - **WAVE 3** — power bus (Generator/PoweredSubsystem) + Emitter/PPC fire-path (end-to-end fire; heat conducts to the central sink via the linked-sink roster). - **WAVE 4** — standalone readouts: Sensor/Searchlight/ThermalSight/AmmoBin (de-shim, gate fixes). - ✅ **Searchlight + ThermalSight buttons WIRED 2026-07-25 (#61)** — both classes' handler sets were default-constructed blackholes (systemic cause #1, `docs/INPUT_PATH_AUDIT.md`). Each now chains `PowerWatcher::GetMessageHandlers()` with its own id-3 `ToggleLamp`. **Verified live**: pad `0x14` → `requestedOn 0→1` → `lightState 0→1`; pad `0x12` → `requestedOn 0→1` → `thermalActive 0→1`. - ❌ **RETRACTED: the "ORIGINAL 1995 latent bug" (old task #63) NEVER EXISTED.** It was an artifact of a **swapped table attribution**: the claim compared Searchlight's Performance (@004b841c, reads `requestedOn`@0x1E0) against **ThermalSight's** `ToggleLamp` (@004b860c, toggles `0x1DC`) and concluded "no 0x1DC→0x1E0 bridge, so the lamp can never light". @004b860c is not Searchlight's. Searchlight's own handler is **@004b838c** (table @0x51117C) and — like every sibling — toggles the field its OWN Performance reads, `requestedOn`@0x1E0. Each class is internally self-consistent. [T1 throughout. Attribution: un-pooled `"ToggleLamp"` strings adjacent to their own class names, `section_dump.txt:69661-69711`. Body: @004b838c is a Ghidra EXPORT GAP (#60) but was **recovered by raw disasm** of `content/BTL4OPT.EXE` (`scratchpad/dis838c.py`) — it toggles 0x1E0, raises `updateModel` (+0x18) unconditionally, and has **NO novice gate** (ThermalSight-only).] Consequence: the searchlight→fog swap was **NOT** inert in the arcade, and making it work in the port is FAITHFUL, not a designer-intent deviation. See [[open-questions]] + [[rendering]] fog, both corrected. - ⚠ Still open on Searchlight: `commandedOn`@0x1DC (ctor-seeded from subsystem resource +0x28, @004b84dc) has **no identified role** — it is neither the toggle target nor read by @004b841c, and it measured **21** live (not a boolean), which hints our SubsystemResource +0x28 ≠ the binary's. - ⚠ ThermalSight's published attributes are mis-named: we publish `"LightState"→thermalActive@0x1D8`, but the binary's table @0x511268 has **`"LightOn"`(id 3)→0x1D8** and `"LightState"`(id 4)→**0x1E0** (the alarm). A gauge binding `ThermalSight/LightOn` currently misses. Not yet changed (gauge-binding change needs its own verification pass). - **WAVE 5** — Torso (aim twist/elevation; joint-I/O reconstructed; the BLH record disables torso → faithfully no visible twist). - **WAVE 6** — Myomers (mover-coupled; structural un-stub is INERT — no live mover/heat coupling). - **WAVE 7** — projectile/missile weapons (byte-exact; flying projectiles are a PORT reconstruction — the 2007 Entity is 0x1BC vs the binary's 0x300, so raw base-offset reads fail). - **TASK #56 (2026-07-11)** — Gyroscope LIVE byte-exact (ctor @004b3778, sizeof 0x3D0 locked; integrators + `FUN_004b2980` damage fan-out; joint dispatch from the Mech performance tail @0x4aaf74/83; `BT_GYRO_LOG`/`BT_GYRO_TRACE`) — see [[cockpit-view]]. - **✅ DONE (task #7, `afefaee`)** — SubsystemMessageManager 0xBD3 (a **damage/explosion consolidation hub** cached to `Mech[0x10d]`=0x434; the factory builds the REAL class and `mech.hpp` names the slot `messageManager` — the old `controlsMapper` mislabel is swept, the real mapper is roster slot 0. NOT the valve/heat-model gate — that's the owning **BTPlayer** @mech+0x190 carrying the EXPERIENCE-level flags, see [[experience-levels]]). [T2] ## The watcher electrical chain (task #57, 2026-07-13 — the torso power gate) The Watcher branch is POWERED indirectly: a watcher WATCHES another roster subsystem and mirrors its electrical state. The full chain, byte-verified [T1]: - **Data:** the model entry `WatchedSubsystem=` → segment index **+2** at resource+0xE4 (`HeatWatcher::CreateStreamedSubsystem` @004aec54); the ctor @004aeb40 stores it at watcher+0x128 (`watchedSubsystem`). - **Bind (factory post-roster loop 1):** vtable slot 14 (+0x38) — `@004aee2c` (HeatWatcher) / `@004b1a40` (PowerWatcher/Torso override, byte-identical); **Ghidra missed both function starts** (recovered from raw bytes; the vtables.tsv rows have GAPS where the exporter skipped slots — dump the exe bytes at vtable+slot*4 when a slot looks missing). Master-gated (`(owner->simulationFlags & 0xC)==0 && (flags & 0x100)`); binds `watchedLink(+0x114).Add( owner->roster[+0x128][watchedSubsystem])`. Port: `BTWatcherWatchedIndex`/`BTWatcherBindTarget` bridges (heatfamily_reslice.cpp) called from the mech.cpp factory loop. - **Tick:** `@004b181c` = the REAL `PowerWatcher` Performance (PTR @0050f5fc → 004b181c) = `UpdateWatch()`: heat mirror (FUN_004aeac4) + `watchdogAlarm.SetLevel(watched->electrical level @+0x278)` + brownout downgrade to 1 when `gen->outputVoltage(+0x1DC) <= minVoltage(+0x180) × gen->ratedVoltage(+0x1D8)`. The Torso sims (@004b5cf0/@004b65f8) call it first-line. (`@004b1804` is slot-10 **ResetToInitialState**, NOT the Simulation — old recon mislabel, fixed.) - **MinVoltageScale = 0.01** — a 10-byte x87 literal at 0x4b1924 (`0a d7 a3 70 3d 0a d7 a3 f8 3f`); the port had 1.0f, making minVoltage 100× too big → the brownout latched every watchdog at 1. - **PowerWatcher::GetClassDerivations chains HeatWatcher** (real base) — the old HeatableSubsystem stand-in broke `IsDerivedFrom(HeatWatcher)` for Torso/Searchlight/ThermalSight and silently skipped them in the connect pass. - **Effect:** `Torso::ElectricalStateLevel()==Ready(4)` un-gates `effectiveTwistRate` — the MadCat torso twists at its authored 50°/s (±140° limits, roster 17 watching 15 → generator @10000V); the BLH is authentically fixed (`horizontalEnabled=0`, limits ±0.01°). [T2 live-verified] - **STILL DEAD:** factory loops 2-4 (heatable/weapon/damageable capability rosters) go through the `SubProxy` stub whose `IsDerivedFrom` returns 0 — they add NOTHING. See [[open-questions]]. ## The coolant FLUSH (Gitea #7, 2026-07-19) — Reservoir InjectCoolant end-to-end [T2 live-verified] The manual-p24 coolant button (coolant MFD top-right; punch or HOLD): message id **4 "InjectCoolant"** on the Reservoir (handler table @**0x50e680**, one entry → @4aee70 — same per-receiver id space as the Condenser's MoveValve @0x50E52C). Handler @4aee70: novice-lockout gate (FUN_004ac9c8), press (data≥1) → `reservoirAlarm.SetLevel(1)` **gated on coolantLevel@0x12C > 0** (+300 decimal = 0x12C = coolantLevel — the old "currentTemperature" reading was a misdecode), release → level 0; ForceUpdate. While level==1 the CoolantSimulation Performance (@4aef78) runs **InjectCoolant @4aefa4** each frame: work list = condenser chain @+0x7cc + weapon chain @+0x7bc + heatable chain @+0x7ac + linkedSinks master (port walks the roster with the same membership tests — chains are SubProxy-dead; condensers/ weapons/bank get the binary's duplicate-visit weighting; watcher-branch members are filtered to HeatSink [T2 guarded]); per sink with flowScale@0x15C≠0: move `squirtMass@0x224 × flowScale × dt` clamped to [-resLevel, 0] (reservoir drains → the coolant vertBar C `Reservoir/CoolantMass` drops), sink level clamped [0, capacity], and `pendingHeat@0x1C8 += -(sinkE×|Δ|/sinkLvl − resE×|Δ|/resLvl)` capped at `sinkMass@0x154 × res->startingTemperature@0x13C`, clamped ≤0 (only cools) — the heat relief rides the existing heat model. **Two ctor corrections surfaced:** (1) the master gate @4af408 (and @4aeb40 HeatWatcher) reads `*(param_2+0x28)` = the **owner MECH's simulationFlags**, not the resource's subsystemFlags (the resource read left the Reservoir Performance unregistered → no drain); (2) `(float10)*(int*)(link+0x1d0)` is a **FILD of the bank's integer HeatSinkCount** — capacity = 0.05 (`_DAT_004af518`) × heatSinkCount × streamed CoolantCapacity (BLH: 0.05×6×20 = 6), not a float reinterpret (which gave ~1e-44 → empty tank). Visual: the binary's mode-1 coolant-effect renderable (FUN_00456a68, built for classID 0xBC0 on the "ReservoirState" attr, part_014.c:5439; tick @part_007.c:8780) spawns **psfx 19 = FLUSH.PFX** ("Coolant flush", BTDPL.INI:1018, bluish smoke) on the state's 0→1 edge — port: `BTSpawnFlushCloud` (mech4.cpp) on the same alarm edge, attached emitter at torso height. Audio (CoolantDump layers) rides the ReservoirState watchers. Desktop input: **'H' HELD = Flush** (CONTROLS.MAP action; press+release dispatch). Diags: `BT_FLUSH_LOG` ([flush-tx] press/release, [flush] level drain, cloud spawn), `BT_FLUSH_TEST=` scripted ~2 s hold. Verified live (FOGDAY): level 6→0 over ~0.6 s held (≈3-4 short punches to empty — matches the manual), bluish cloud screenshot-confirmed (scratchpad/flush_cloud.png). Replicant note: the state receive @4aeef8 sets the alarm from the state stream — the port's reservoir state replication is not wired, so remote clouds are deferred (solo/master path complete). ## Valve boost A/B/C (2026-07-23, field "boosted loop still cooked" audit) [T2 measured] Blackhawk, sustained BT_AUTOFIRE, 150 s soaks, `[heat-t]` 5 s cadence. Valve shares are zero-sum (`valveState/Σ`); detent cycle per press = **1 → 5 → 50 → 0 (CLOSED) → 1** (@4ae464, one past max turns the loop OFF). SRM6s (+ GeneratorB + Avionics) are on **Condenser2** ([heat-link] dump; the MFD loop readout matches the heatSinkIndex truth). Results, SRM6_1: - **boost 2 (share 0.91)**: plateau ~531, then COOLS under nonstop fire — never jams/fails. - **balanced (0.167)**: ~1162 @ 70 s and climbing (the jam band; field jams logged 1043-1469). - **starved (0.018)**: ~1440 @ 70 s, slope → the 2000 FailureHeat line. The valve mechanic is REAL and powerful; MoveValve reruns the redistribute on every press (not a placebo), veteran role passes the guard. The field double-failure with "loop 2 boosted" is therefore a DETENT question — most likely one press past max (detent 0 = closed, share 0) — now diagnosable: valve presses log always-on (`[valve] ... -> detent N`, with a CLOSED warning). Solo clock shim verified the same day (60 s egg self-ends; `[mission] solo game clock expired`). ## The four systemic checks (every subsystem) See [[reconstruction-gotchas]]: (1) shadowing (re-declared engine-base fields), (2) the `Wword` trap, (3) message-handler chaining, (4) entity validity. Plus resource-struct layout (must mirror the class hierarchy, byte-exact — the RESOURCE_AUDIT found 8 bugs) and object-layout (alias/phantom/interior fields). A `+0x128`-style owner offset is the ROSTER, not the segment table (the heat-link type confusion that caused a heap-corruption via OOB `ConductHeat` writes). [T2] ## Engineering cluster panels (`vehicleSubSystems`) Not a widget — a per-subsystem FACTORY (`FUN_004cbaf0`) building a status CLUSTER onto one of 12 aux MFD positions, dispatching on classID → HeatSinkCluster/MyomerCluster/EnergyWeaponCluster/ BallisticWeaponCluster. Reads `PoweredSubsystem::auxScreenNumber/Placement/Label` (res +0x104/8/C) via the `BTGetSubsystemAuxScreen` bridge. See `docs/VEHICLE_SUBSYSTEMS.md` + [[gauges-hud]]. [T2] ## Key Relationships - Data: [[decomp-reference]] (ClassIDs/hierarchy). Bugs: [[reconstruction-gotchas]]. - Feeds: [[combat-damage]] (weapons/damage), [[gauges-hud]] (attribute state). - Plan: `docs/SUBSYS_PLAN.md`.