Files
BT411/context/subsystems.md
T
Joe DiPrimaandClaude Fable 5 31552c0444 #88 part 2: the central coolant pull-through -- HeatSink::DrawCoolant was a
return-0 stub; the Reservoir BAR now drops when a damaged mech leaks

The user watched the coolant panel's level bar during the leak bench and it
never moved -- correctly: a leaking subsystem drained only its own loop,
because the slot-14 top-up (DrawCoolant) was stubbed.  Disassembled the real
base @0x4add00: the draw RECURSES UP the sink linkage, scaling by
coolantFlowScale@0x15C per hop, terminating at the Reservoir's supply
(@0x4af3b0 -- clamp to [0, coolantLevel], drain the tank, return granted --
already faithfully ported as Reservoir::DrawCoolant).  The binary's terminal
hop is the bank's slot-14 override @0x4ae8b0 resolving its reservoir
connection @0x1D8 (the port's 'helper' member -- not vestigial after all);
the port's existing Reservoir-ctor Attach + Reservoir override terminate the
same chain equivalently (the extra bank hop scales by ctor-default 1.0).

Implemented the base recursion (null-guarded -- the binary calls through the
resolved link unguarded; authored topology always links) + a BT_COOL_LOG
[resdraw] probe at the supply.

Verified live: a destroyed myomer's damage leak pulled the central tank
6.0 -> 5.58 over ~30s ([resdraw] granted lines) -- Reservoir/CoolantMass,
the cockpit coolant bar, now visibly drops as a damaged mech bleeds
coolant, and DeathReset refills it on respawn.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J
2026-07-31 08:47:55 -05:00

259 lines
22 KiB
Markdown
Raw 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.
---
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 <Name>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
`Create<Class>Subsystem(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; the mover feed is still inert — WAVE 6 cutover — but **DRIVE
HEAT IS LIVE**, #85 fix 2026-07-31, see below).
**✅ MYOMER DRIVE HEAT LIVE (#85, 2026-07-31) [T2].** The five Mech motion operands of the
drive-heat integrator `@004b8d18` were `return 0.0f` cross-family shims, so the (fully
reconstructed) integrator accumulated `ratio² · damageGain · 0` every tick — myomers ice cold at
any seek (Oracle's night-7 panel-confirmed report). Operands re-grounded from the raw disasm +
the decomp and mapped to named members via the complete-TU bridge `BTMechMyomerMotionSample`
(mech4.cpp): `+0x1C4` vec = `localVelocity.linearMotion`; `+0x82C` vec =
`localAcceleration.linearMotion` (the authentic 15-ring smoothed AccelerationLastFrame);
`+0x20C` = `moverMass` (the collision divert's cell — the old "motion gain" label was wrong);
`*(+0x250)` = the environment **gravity pointer** (`FUN_00421e2c` does `vy -= **(+0x250)`/tick).
Physics: `heat += ratio²·(1+dmg)·[0.005·½mv² + 0.2·m·g·|vy|·dt + 0.2·m|v||a|·dt]` — kinetic work
+ climb power + acceleration power. The binary `fabs`es v.y (the port's old draft didn't).
**Authored tuning extracted from BTL4.RES** (all 18 mech variants share one record):
VelocityEfficiency=0.995, AccelerationEfficiency=0.8, seek gears 0.3/0.5/0.7/0.9999 rec idx 2;
myomer thermal profile startT=77 / degradation=1000 / failure=2000 / conductance=1.9e5 /
thermalMass=2.5e5. Live-verified (solo arena, BT_MYO_LOG): standstill ≈ 0 generation, hard
circling drove T 77→1225 (past degradation) and the coolant loop pulled it back to ~520
equilibrium on slowing. ⚠ Two carried caveats: (1) the kinetic work term has **no dt** in the
binary (per-tick accumulate, a 1995 fixed-frame assumption) → heat/s scales mildly with frame
rate; kept verbatim, field-calibrate against pod veterans. (2) the climb term is wired but
inert: `Mover::localEnvironment` is never populated in the port (EnvironmentZone res type 23
unwired), so g reads 0 — see [[open-questions]].
⚠ **The registered Performance is the WRAPPER @004b8b9c, not the inner integrator @004b8d18**
[T1, fixed 2026-07-28]. The ctor @004b8fec stores `[0x511620]` into activePerformance, and that
pointer resolves to `0x4b8b9c`, whose first instruction is `call 0x4b0bd0` =
`PoweredSubsystem::PoweredSubsystemSimulation`; @004b8d18 is the drive-heat integrator it then
runs. The port had registered the INNER function, so Myomers never advanced its own electrical
state machine — and since that machine is the only thing that both enters `NoVoltage`
(`source == 0`) and walks it back (`NoVoltage → Starting → Ready`), a Myomers that lost power in
the death/reset window stayed there **forever**. The Torso is a PowerWatcher that MIRRORS its
watched subsystem, so `torso.cpp:570` held `effectiveTwistRate` at 0 = **Gitea #70, "torso twist
stops working after a death/respawn."** Measured: 2 dead windows per respawn before, 0 after;
pristine missions never showed it. **Order is load-bearing** — the base call precedes the
heat-model gate, and gating first also denied the electrical machine to every non-expert pilot
(`OwnerAdvancedDamage()` is the +0x260 flag, off below veteran).
**General rule this establishes:** when a subsystem's Performance address in a `PTR_LAB_*` slot
does not match the function you reconstructed, check whether the real target is a wrapper that
chains the base sim — the whole family does (`Generator`/`PoweredSubsystem` lead with
`HeatSink::HeatSinkSimulation`; the Torso's own perf @004b5cf0 leads with `UpdateWatch`).
**The full wrapper is now reconstructed** [T1/T2, 2026-07-28] — five blocks in binary order:
`@004b8bab` chain `PoweredSubsystemSimulation`; `@004b8bb9` the un-powered self-repair (see below);
`@004b8c1e` republish `outputVoltage@0x344` from the resolved source when the alarm is Ready;
`@004b8c5a` republish `speedEffect@0x31C` = `AvailableOutput(clamped V) / OwnerBaseSpeed()` — the
Mech base speed CANCELS, so `speedEffect` is a **0..1 fraction** of full drive carrying gear ratio,
thermal curve and zone damage; `@004b8ceb` run the inner integrator **only when `outputVoltage > 0`**.
Verified live: healthy `outV=10000 speed=1`, un-powered `outV=0 speed=0`, and 96/96 torso samples at
`elec=4` across two death/respawn cycles.
✅ **CORRECTED 2026-07-29 (#80): `@004b8bb9` — the un-powered self-repair — is LIVE, in 1995 and
now in the port.** The 2026-07-28 "dead code in the original too" verdict here was wrong about the
original: the frozen-at-0.6 experiment was measuring a PORT bug (the subsystem ctor wrote the
zone's armour/scales through the `ReconDamageZone` proxy at struct offsets +4/+8 instead of the
engine members at +0x140/+0x144, so the real `damageScale[]` stayed zero). The binary ctor
(`@0x4ac7bb`) initializes them from the resource keys `WeaponDamagePoints` + the five per-type
`...DamagePoints`; the port now does the same through the engine's named members, and the CSS
parses the keys. An un-powered, not-yet-destroyed myomer heals at `0.011 × damageScale[Explosive]`
per tick. The retracted "cannot damage a subsystem's own zone" rule is superseded — see
[[combat-damage]] §CORRECTED for the full mechanism (this also revived the crit sink, #80).
Also un-stubbed here: `Myomers::DamageStructureLevel()` returned a hardcoded `0.0f`, which pinned
`AvailableOutput`'s `(1 - damage)` factor at 1 — a shot-up myomer drove exactly as well as a fresh
one. Now routed to the base bridge `GetSubsystemDamageLevel()`; measured `dmg=0.6 → speed=0.4`.
- **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=<name>` → 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]].
## Coolant LEAKS (Gitea #88 fix, 2026-07-31) — the #64 shadow was the single break [T2]
`HeatSink::UpdateCoolant` (@004adbf8) prices the damage-driven leak: `coolantDraw =
ownZone->damageLevel × heatLoad`, floor 0.0025, and the `coolantActive@0x138` hysteresis
(ON >0.003) IS the `ReportLeak` attribute the 19 authored leak watchers (3-note warning) ride.
The read is the **qualified ENGINE member** `Subsystem::damageZone@0xE0` — which the port's
MechSubsystem ctor never assigned (it filled only its re-declared shadow, gitea #64 / gotcha #1),
so `zoneDamage` pinned 0 and a leak was **structurally impossible** regardless of damage. Fixed by
ALIASING the engine base member to the same zone in both MechSubsystem ctors (mechsub.cpp) — every
shadow reader is untouched (same object), the engine base `Subsystem::TakeDamage` null-AV is
disarmed, and the #64 full de-shadow sweep remains the long-term cleanup. Verified live: collision
rattle and weapon crits (`[critroll]`) both drive `[cool]` leak lines (draw ≈ dmg×heatLoad, level
draining, hysteresis arming). **Authored damage routing** (BTL4.RES): collision rattle targets
HeatSinkBank 0.3 / Gyro 0.35 / Torso 0.25 / Myomers 0.35 — Condenser + Reservoir are authored 0
for collisions; weapon crits select via the ZONE's crit-entry list.
**The central pull-through is ALSO live (same day):** `HeatSink::DrawCoolant` was a `return 0`
stub; the real base @0x4add00 (disassembled) RECURSES UP the sink linkage — `return
resolve(linkedSinks)->DrawCoolant(requested × coolantFlowScale@0x15C)` — terminating at the
Reservoir's supply (@0x4af3b0, already faithfully ported as `Reservoir::DrawCoolant`: clamp to
[0, coolantLevel], drain, return granted). Binary terminal hop = the bank's slot-14 override
@0x4ae8b0 resolving its reservoir connection @0x1D8 (the port's `helper` member — NOT vestigial);
the port terminates equivalently via the Reservoir-ctor Attach into the bank's `linkedSinks` +
the Reservoir override (the extra bank hop scales by its ctor-default flowScale 1.0 — a no-op).
Verified live (`[resdraw]`): a destroyed myomer's leak pulled the central Reservoir tank
6.0 → 5.58 over ~30 s — **`Reservoir/CoolantMass`, the cockpit coolant BAR, now visibly drops
as a damaged mech bleeds coolant**, and DeathReset refills it on respawn.
## 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=<frame>` 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`.