Reverse-engineered the vehicleSubSystems config primitive end to end (Ghidra headless, all ~28 functions). It is NOT a widget: it is the engineering-screen (MFD) subsystem-panel system. Its Make (FUN_004cbaf0) is a per-subsystem factory that builds a SubsystemCluster-family status panel onto one of 12 auxiliary MFD positions, dispatching on subsystem classID (HeatSink/Myomer/Energy/Ballistic clusters). The whole cluster family (base + 5 subclasses) + 4 btl4gau2 sub-gauges (CoolingLoop/PowerSource/ScalarBarGauge/ConfigMapGauge) are declared in btl4gau2.hpp but not reconstructed. BLOCKER: the Make reads base subsystem fields subsystem[0x1dc] (aux-screen position), [0x1e4]/[0x1e0]/[0x224] that our MechSubsystem reconstruction (ends 0x114) does not have or populate -- so the panels render nothing until the core subsystem layout is extended + populated from the resource parse, which touches the working combat/heat subsystem code (regression risk). Checkpointed at full spec pending go/no-go on the large core-touching implementation. - docs/VEHICLE_SUBSYSTEMS.md: complete reconstruction spec (dispatch table, geometry table, class family map, sub-gauge inventory, engine-primitive reuse, the blocker, the Phase-1/Phase-2 plan). - reference/ghidra_scripts/DecompVSS.java: headless address-list decompiler (reusable for any function the assert-anchored exporter skipped). - CLAUDE.md: record the finding in the gauge-widget notes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
8.1 KiB
vehicleSubSystems — the engineering-screen subsystem cluster panels (reconstruction plan)
Status: reconstruction UNDERWAY (from-scratch). This is the largest single gauge item — not a widget but the whole engineering-screen (MFD) subsystem-panel system. Full decomp captured in the session scratchpad (
vss_full.txt/vss_sub.txt, Ghidra headless ofBTL4OPT.EXE); this doc is the durable spec so the analysis survives across sessions.
What it is
The config primitive vehicleSubSystems(qXXX0.pcc .. qXXX7.pcc) (26 uses in L4GAUGE.CFG, one per
vehicle init block after MechInit;) registers a Make dispatcher (FUN_004cbaf0,
methodDescription @0x51c080, 8 typeString params = the 8 strip bitmaps). At cockpit-build time the
Make walks the mech's subsystem roster (entity+0x124 count / +0x128 array, skipping slot 0 = the
controls mapper) and, for each subsystem, builds a status cluster panel onto one of 12 auxiliary MFD
positions, dispatching on the subsystem's classID (sub+4):
| classID | subsystem | child cluster | ctor | new size | id string |
|---|---|---|---|---|---|
| 0xBC3 | Sensor | HeatSinkCluster (looks up the "HUD" subsystem via FindSubsystem, FUN_0041f98c) |
FUN_004c8a6c |
0xf0 | "SensorCluster" |
| 0xBC6 | Myomers | MyomerCluster |
FUN_004c8df4 |
0xd8 | "MyomerCluster" |
| 0xBC8 / 0xBD4 | Emitter / PPC | EnergyWeaponCluster |
FUN_004c93b0 |
0xf0 | "EnergyWeaponCluster" |
| 0xBCD / 0xBD0 | Projectile / Missile | BallisticWeaponCluster |
FUN_004c9558 |
0x11c | "ProjWeaponCluster" |
| 0xBCE | Gauss | — (error "Gauss rifle not yet supported") | — | — | — |
| all others | skip |
Type filter before dispatch: FUN_0041a1a4(**(sub+0xc), "") (an IsA/registry check) must pass.
The per-group geometry table @0x51bf34 (12 groups × 7 dwords, keyed by sub[0x1dc]-1)
Each group = { X, Y, planeMask, subBit, mfdPortName, engPortName, planeIndex }:
grp X Y planeMask subBit mfdPort engPort idx
1 0 240 0x1 0x2 mfd1 eng1 0
2 322 240 0x1 0x4 mfd1 eng1 0
3 0 28 0x1 0x8 mfd1 eng1 0
4 322 28 0x1 0x10 mfd1 eng1 1
5 0 240 0x20 0x40 mfd2 eng2 1
6 322 240 0x20 0x80 mfd2 eng2 1
7 0 28 0x20 0x100 mfd2 eng2 1
8 322 28 0x20 0x200 mfd2 eng2 2
9 0 240 0x400 0x800 mfd3 eng3 2
10 322 240 0x400 0x1000 mfd3 eng3 2
11 0 28 0x400 0x2000 mfd3 eng3 2
12 322 28 0x400 0x4000 mfd3 eng3 (self)
mfdPort → renderer->GetGraphicsPort(name) (the visible MFD surface); engPort (string) is the
engineering-screen render port name passed to the cluster sub-gauges.
Class family (all btl4gau2, all DECLARED in btl4gau2.hpp, NONE reconstructed pre-this-effort
except SeekVoltageGraph)
SubsystemClusterbase @0x4c8140(vt0x51a020) :GraphicGauge— builds ~11 sub-gauges (HorizTwoPartBar temp bar, CoolingLoop ×2, PowerSource ×2, BackgroundBitmap, OneOfSeveralInt modeLamp, GeneratorVoltage/ScalarBarGauge, OneOfSeveralStates stateLamp, VertTwoPartBar ×2, LeakGauge). dtor0x4c87dc, Execute0x4c88e4, draw-bg helper0x4c89c4, BecameActive-draw0x4c8820, TestInstance0x4c8990, ShowInstance0x4c8a28. Reads attributes InputVoltage/HeatSink/CurrentTemperature/ DegradationTemperature/FailureTemperature/CoolantMassLeakRate viaFUN_0041bfc0(=GetAttributeIndexby name, returnsowner + idx - 1or 0).HeatSinkCluster@0x4c8a6c(vt0x519fd4) : SubsystemCluster — adds 4 fail TwoStates (btehfail/btepfail/btesfail) + a NumericDisplayScalar. Execute0x4c8db0scalesthis[0x35]by_DAT_004c8df0(=100.0). dtor0x4c8d18.MyomerCluster@0x4c8df4(vt0x519f88) : SubsystemCluster — adds a SeekVoltageGraph ("EngrGraph", edestryd.pcc) + a seek-step OneOfSeveralInt (bteseek.pcc,subsys+0x800). Execute = base. dtor0x4c8f54.WeaponCluster@0x4c8fc4(vt0x519f38) : SubsystemCluster — adds SegmentArc270 recharge dial (reads "PercentDone") + ConfigMapGauge. Make-time warns "WeaponCluster missing ". Execute0x4c9290toggles a warn lamp whenthis[0x36] > _DAT_004c92e0. DrawWarningLamp0x4c932c, BecameActive0x4c9258, dtor0x4c91d4, slot170x4c92e4.EnergyWeaponCluster@0x4c93b0(vt0x519ee8) : WeaponCluster — adds SeekVoltageGraph (OutputVoltage) + seek-step OneOfSeveralInt (subsys+0x3f0). dtor0x4c94dc.BallisticWeaponCluster@0x4c9558(vt0x519e98) : WeaponCluster — adds 2× NumericDisplayInteger (ammo,link+0x180), jam/fire TwoStates (btejam/btefire), NumericDisplayScalarTwoState, eject BitMapInverseWipeScalar (bteejtm,subsys+0x3f8), a GraphicsViewRecord erase tracker. Execute0x4c9a38tracks weaponAlarm==5 (Jammed,subsys+0x364) + the reload clock. dtor0x4c9940, BecameActive0x4c99cc, TestInstance0x4c9adc, slots0x4c9b24/0x4c9b50.
Sub-gauge classes to reconstruct (btl4gau2, declared not defined)
AnimatedSubsystemLamp(CoolingLoop) @0x4c70a4(vt0x51a174) : OneOfSeveral + connectionFUN_004c3134(vt0x518ea8, subsystem-state driven) at+0x2b. dtor0x4c7134.AnimatedSourceLamp(PowerSource) @0x4c7160(vt0x51a130) : OneOfSeveral + connectionFUN_004c31ec(vt0x518e9c) at+0x2b. dtor0x4c71f0.ScalarBarGauge(GeneratorVoltage) @0x4c721c(vt0x51a0e4) : engine barFUN_00472ef0(VertNormalSlider-ish) + connectionFUN_004c3288(vt0x518e90) at+0x2d. 2nd ctor0x4c72ac(uses the engineFUN_00474855scalar-attr connection). dtor0x4c733c.ConfigMapGauge@0x4c6d80(vt0x51a1b8) : GraphicGauge — 5 pcc names (btjoy/cm_off/cm_other/ cm_only/cm_both) interned into the warehouse; Execute0x4c6f1creads the control-mapper state fromapp->GetModeManager()(DAT_004efc94+0x3c, group0x1c0 + i*0x20) and blits the matching frame. SetColor0x4c6ee0(writesthis+0x94). dtor0x4c6e54.
Engine primitives it calls (ALREADY in munga_engine.lib, callable — do NOT reconstruct): TwoState
(FUN_004739d8, vt 0x4fb740), NumericDisplayScalar (0x470888, vt 0x4fbb40),
NumericDisplayInteger (0x470cfc), NumericDisplayScalarTwoState (0x473c94), BackgroundBitmap
(0x471d00), and the GaugeConnection ctors FUN_00474855/FUN_004749de. Already reconstructed:
HorizTwoPartBar/VertTwoPartBar/OneOfSeveralInt/OneOfSeveralStates/BitMapInverseWipe(Scalar)/SegmentArc270/
SeekVoltageGraph.
⚠ THE BLOCKER — core MechSubsystem layout (Phase 2)
The Make reads subsystem[0x1dc] (auxiliary-screen position, INT 1..12; 0/miss → the panel is skipped
with "Auxiliary screen position = zero"), [0x1e4] (display label), [0x1e0] (damage-zone index),
[0x224] (a name). Our reconstructed MechSubsystem ends at 0x114 — these are unreconstructed base
fields not populated from the resource parse. So the panels render NOTHING until the core subsystem
layout is extended + populated (from MechSubsystem::CreateStreamedSubsystem / the resource ctor
FUN_004ac644) — a change to the WORKING combat/heat subsystem code (regression risk per the §10 systemic
rules). The bytes exist in the allocation (subclass layouts are locked to binary offsets, so there is
padding between 0x114 and the subclass fields); the task is to CARVE named fields out of that region + wire
the resource parse, verifying combat/heat un-regressed. This is the final, sign-off-gated step.
Phasing
- Phase 1 (SAFE, gated): reconstruct the cluster family + the 4 sub-gauge classes + connections + the
Make dispatcher + register in
BTL4MethodDescription[]. Gauges only build underBT_DEV_GAUGES/pod; with[0x1dc]unpopulated the Make hits its skip path, so combat is untouched. Buildable + committable. - Phase 2 (core-layout, sign-off): extend + populate
MechSubsystem[0x1dc/0x1e0/0x1e4/0x224]from the resource → the panels render live on mfd1/mfd2/mfd3. Verify combat/heat un-regressed.