Both classes' Receiver::MessageHandlerSet were default-constructed blackholes (input-path audit systemic cause #1): entryCount 0, no parent chain, Find() returns NullHandler for every id, Receive drops silently. The new unhandled- message trace printed it on the first press: [btntest] PRESS 0x14 at poll 400 [msg] UNHANDLED: Searchlight has no handler for message id 3 Each class now has a GetMessageHandlers() function-local static chained to PowerWatcher's (which name-resolves through HeatWatcher/MechSubsystem to Receiver's empty ROOT set, so id 3 does NOT collide with HeatSink's ToggleCooling), plus a correctly-typed forwarder -- Receiver::Handler is void(const Message*) while the decomp shape is Logical(Message&), so a cast would be UB that merely happens to work on x86 __thiscall. DefaultData re-pointed off the dead empty sets. MessageArg now reads the NAMED ReceiverDataMessageOf<int>::dataContents with a static_assert locking it to the binary's message+0xC (was a raw offset read -- databinding rule). ROOT CAUSE of a long-standing misattribution: @004b860c is **ThermalSight's** ToggleLamp (table @0x51120C), NOT Searchlight's. The two TUs emit parallel shared-data blocks with identical stride (msg entry, +0x5C attrs, +0x84 Performance triple); what pins each entry to its TU is that "ToggleLamp" is NOT pooled across them -- two copies exist, each emitted immediately before its own class-name string ("Searchlight"@0x51144B, "ThermalSight"@0x511475). [T1: reference/decomp/section_dump.txt:69661-69711] Searchlight's own handler is @004b838c, which sits in a Ghidra EXPORT GAP (#60). RECOVERED by raw disassembly of content/BTL4OPT.EXE (scratchpad/dis838c.py, the EjectAmmo technique) -- and it corrected two things I had inferred wrong: * NO ControlsAllowLights/+0x25C novice gate. Searchlight tests the press alone; that lock is ThermalSight-only. A NOVICE pilot CAN work the lamp. * `or word ptr [this+0x18],1` sits AT the jle target -> the graphics-dirty bit (updateModel == ForceUpdate(), per #59) is raised UNCONDITIONALLY. Consequence: the "ORIGINAL 1995 LATENT BUG -- the searchlight can never light" claim is RETRACTED. It compared Searchlight's Performance (@004b841c, reads requestedOn@0x1E0) against ThermalSight's toggle (0x1DC) -- two different classes -- and so invented a missing 0x1DC->0x1E0 bridge. Every sibling toggles the field its own Performance reads. The searchlight DID light in the arcade, the searchlight->fog swap was NOT inert there, and building PullFogRenderable is FAITHFUL rather than a designer-intent deviation (the 2026-07-13 "left as-is" decision is void; the sim needs no repair). Verified live, real click seam (BT_BTNTEST): 0x14 -> [light] requested ON -> reported lightState 0 -> 1 (lamp lights) 0x12 -> [light] thermal sight requested ON -> thermalActive 0 -> 1 UNHANDLED lines for both classes gone; 40 LNK2019 unchanged (the pre-existing CreateStreamedSubsystem + Entity__SharedData::DefaultData families only). Swept the misattribution out of searchlight.cpp/.hpp, thermalsight.cpp/.hpp, hud.cpp:282 (it had claimed @00511180/@004b838c as HUD's), btplayer.cpp's +0x25C consumer list, context/{decomp-reference,subsystems,rendering,open-questions, pod-hardware,experience-levels}.md, docs/{GLASS_COCKPIT,INPUT_PATH_AUDIT}.md. checkctx.py CLEAN. Still open (documented, not fixed): ThermalSight has no visible IR effect (ToggleGlobalThermalVision is a marked no-op, pvision unported, IsLocallyViewed returns False); ThermalSight publishes "LightState"->0x1D8 where the binary has "LightOn"->0x1D8 and "LightState"->0x1E0; Searchlight's commandedOn@0x1DC has no identified role and measured 21 live, hinting our SubsystemResource +0x28 differs from the binary's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
14 KiB
id, title, status, source_sections, related_topics, key_terms, open_questions
| id | title | status | source_sections | related_topics | key_terms | open_questions | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| subsystems | Subsystems — the factory + the reconstruction waves | established | PROGRESS_LOG.md §10d; docs/SUBSYS_PLAN.md; docs/VEHICLE_SUBSYSTEMS.md; docs/RESOURCE_AUDIT.md |
|
|
|
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 0xCSubsystemConnection- 0x54
GaugeAlarm54(alarm level at +0x14). [T1]
- 0x54
- Watcher branch:
Torso/Gyroscope/Searchlight/ThermalSight/HUD : PowerWatcher : HeatWatcher : MechSubsystem;AmmoBin : HeatWatcher. Uses a 0xCWatchedConnection+ 0x54WatcherGaugeAlarm. [T1] Making a base byte-exact GROWS every subclass — they must be re-based TOGETHER in one build, eachstatic_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 chainsPowerWatcher::GetMessageHandlers()with its own id-3ToggleLamp. Verified live: pad0x14→requestedOn 0→1→lightState 0→1; pad0x12→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'sToggleLamp(@004b860c, toggles0x1DC) 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 ofcontent/BTL4OPT.EXE(scratchpad/dis838c.py) — it toggles 0x1E0, raisesupdateModel(+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 bindingThermalSight/LightOncurrently misses. Not yet changed (gauge-binding change needs its own verification pass).
- ✅ Searchlight + ThermalSight buttons WIRED 2026-07-25 (#61) — both classes' handler sets were
default-constructed blackholes (systemic cause #1,
- 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_004b2980damage 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 toMech[0x10d]=0x434; the factory builds the REAL class andmech.hppnames the slotmessageManager— the oldcontrolsMappermislabel 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)); bindswatchedLink(+0x114).Add( owner->roster[+0x128][watchedSubsystem]). Port:BTWatcherWatchedIndex/BTWatcherBindTargetbridges (heatfamily_reslice.cpp) called from the mech.cpp factory loop. - Tick:
@004b181c= the REALPowerWatcherPerformance (PTR @0050f5fc → 004b181c) =UpdateWatch(): heat mirror (FUN_004aeac4) +watchdogAlarm.SetLevel(watched->electrical level @+0x278)+ brownout downgrade to 1 whengen->outputVoltage(+0x1DC) <= minVoltage(+0x180) × gen->ratedVoltage(+0x1D8). The Torso sims (@004b5cf0/@004b65f8) call it first-line. (@004b1804is 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-gateseffectiveTwistRate— 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
SubProxystub whoseIsDerivedFromreturns 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=<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.