vehicleSubSystems: full reverse-engineering + reconstruction spec (blocked)

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>
This commit is contained in:
arcattack
2026-07-06 23:55:56 -05:00
co-authored by Claude Opus 4.8
parent 099871ef4c
commit a5fa9f1c79
3 changed files with 200 additions and 0 deletions
+20
View File
@@ -734,6 +734,26 @@ not a distance error). REMAINING: `PlayerStatus`, `vehicleSubSystems` (task #13)
video-object infra (`GetVideoObject`/`LookUpPip`/`GetNameID`, stubbed → cross-blip stand-in) + `SetTargetRange`
(radar zoom, 1km default) + the aggregate HeatSink-bank `AmbientTemperature` (`#if 0`'d 0xBBE) + the Reservoir
`coolantCapacity` shadow + the Searchlight `LightOn` table (duck button already resolves via the 0x37 Mech entry).
**`PlayerStatus` DONE** (mission-review scoreboard; lives in the config's `cameraInit` block, not `MechInit`,
so it is build/link/mech-cockpit-safe but not runtime-verifiable via a normal mech egg — commit b691863).
**`vehicleSubSystems` — FULLY REVERSE-ENGINEERED, reconstruction NOT started (blocked; full spec in
`docs/VEHICLE_SUBSYSTEMS.md`).** ⚠ It is NOT a widget — it is the whole **engineering-screen (MFD)
subsystem-panel system**: its Make (`FUN_004cbaf0`) is a per-subsystem FACTORY that builds a status *cluster
panel* onto one of 12 aux MFD positions, dispatching on subsystem classID → `HeatSinkCluster`(0xBC3)/
`MyomerCluster`(0xBC6)/`EnergyWeaponCluster`(0xBC8/0xBD4)/`BallisticWeaponCluster`(0xBCD/0xBD0). The whole
`SubsystemCluster` family (base + 5 subclasses, ctors+Executes+dtors) + 4 btl4gau2 sub-gauges (`CoolingLoop`/
`PowerSource`/`ScalarBarGauge`/`ConfigMapGauge`) are DECLARED in btl4gau2.hpp but NOT reconstructed (btl4gau2.cpp
defines only `SeekVoltageGraph`). Full Ghidra decomp of all ~28 functions captured; the engine primitives it
uses (`TwoState`/`NumericDisplayScalar/Integer`/`BackgroundBitmap` + GaugeConnections) already exist + are
callable. ⚠⚠ **BLOCKER:** the Make reads `subsystem[0x1dc]` (auxiliary-screen position 1..12; miss ⇒ panel
skipped), `[0x1e4]`/`[0x1e0]`/`[0x224]` — **base subsystem fields our reconstruction (`MechSubsystem` ends at
0x114) does NOT have or populate**; the `MechSubsystem` resource ctor `FUN_004ac644` writes only through ~0x110,
so 0x1dc is set by a deeper base (HeatSink) or a config pass not yet traced. ⇒ **Phase-1 (the cluster family +
Make + register) renders NOTHING until Phase-2 populates 0x1dc**, and Phase-2 touches the WORKING combat/heat
subsystem layout (regression risk per the §10 systemic rules). Tooling: `reference/ghidra_scripts/DecompVSS.java`
(headless address-list decompiler — reusable for any function the assert-anchored exporter skipped; run via the
existing `vssproj` Ghidra project). Left checkpointed at full-spec pending a go/no-go on the large core-touching
implementation.
**Alternate route considered:** run original `btrel410.exe` under DOSBox — **blocked**: it needs the
VPX/IG board DOSBox can't emulate; bypassing it = the shim work anyway.
+110
View File
@@ -0,0 +1,110 @@
# `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 of `BTL4OPT.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`)
- **`SubsystemCluster`** base @`0x4c8140` (vt `0x51a020`) : `GraphicGauge` — builds ~11 sub-gauges
(HorizTwoPartBar temp bar, CoolingLoop ×2, PowerSource ×2, BackgroundBitmap, OneOfSeveralInt modeLamp,
GeneratorVoltage/ScalarBarGauge, OneOfSeveralStates stateLamp, VertTwoPartBar ×2, LeakGauge). dtor
`0x4c87dc`, Execute `0x4c88e4`, draw-bg helper `0x4c89c4`, BecameActive-draw `0x4c8820`, TestInstance
`0x4c8990`, ShowInstance `0x4c8a28`. Reads attributes InputVoltage/HeatSink/CurrentTemperature/
DegradationTemperature/FailureTemperature/CoolantMassLeakRate via `FUN_0041bfc0` (=`GetAttributeIndex`
by name, returns `owner + idx - 1` or 0).
- **`HeatSinkCluster`** @`0x4c8a6c` (vt `0x519fd4`) : SubsystemCluster — adds 4 fail TwoStates
(btehfail/btepfail/btesfail) + a NumericDisplayScalar. Execute `0x4c8db0` scales `this[0x35]` by
`_DAT_004c8df0` (=100.0). dtor `0x4c8d18`.
- **`MyomerCluster`** @`0x4c8df4` (vt `0x519f88`) : SubsystemCluster — adds a SeekVoltageGraph
("EngrGraph", edestryd.pcc) + a seek-step OneOfSeveralInt (bteseek.pcc, `subsys+0x800`). Execute =
base. dtor `0x4c8f54`.
- **`WeaponCluster`** @`0x4c8fc4` (vt `0x519f38`) : SubsystemCluster — adds SegmentArc270 recharge dial
(reads "PercentDone") + ConfigMapGauge. Make-time warns "WeaponCluster missing <name>". Execute
`0x4c9290` toggles a warn lamp when `this[0x36] > _DAT_004c92e0`. DrawWarningLamp `0x4c932c`,
BecameActive `0x4c9258`, dtor `0x4c91d4`, slot17 `0x4c92e4`.
- **`EnergyWeaponCluster`** @`0x4c93b0` (vt `0x519ee8`) : WeaponCluster — adds SeekVoltageGraph
(OutputVoltage) + seek-step OneOfSeveralInt (`subsys+0x3f0`). dtor `0x4c94dc`.
- **`BallisticWeaponCluster`** @`0x4c9558` (vt `0x519e98`) : WeaponCluster — adds 2× NumericDisplayInteger
(ammo, `link+0x180`), jam/fire TwoStates (btejam/btefire), NumericDisplayScalarTwoState, eject
BitMapInverseWipeScalar (bteejtm, `subsys+0x3f8`), a GraphicsViewRecord erase tracker. Execute
`0x4c9a38` tracks weaponAlarm==5 (Jammed, `subsys+0x364`) + the reload clock. dtor `0x4c9940`,
BecameActive `0x4c99cc`, TestInstance `0x4c9adc`, slots `0x4c9b24`/`0x4c9b50`.
## Sub-gauge classes to reconstruct (btl4gau2, declared not defined)
- **`AnimatedSubsystemLamp`** (CoolingLoop) @`0x4c70a4` (vt `0x51a174`) : OneOfSeveral + connection
`FUN_004c3134` (vt `0x518ea8`, subsystem-state driven) at `+0x2b`. dtor `0x4c7134`.
- **`AnimatedSourceLamp`** (PowerSource) @`0x4c7160` (vt `0x51a130`) : OneOfSeveral + connection
`FUN_004c31ec` (vt `0x518e9c`) at `+0x2b`. dtor `0x4c71f0`.
- **`ScalarBarGauge`** (GeneratorVoltage) @`0x4c721c` (vt `0x51a0e4`) : engine bar `FUN_00472ef0`
(VertNormalSlider-ish) + connection `FUN_004c3288` (vt `0x518e90`) at `+0x2d`. 2nd ctor `0x4c72ac`
(uses the engine `FUN_00474855` scalar-attr connection). dtor `0x4c733c`.
- **`ConfigMapGauge`** @`0x4c6d80` (vt `0x51a1b8`) : GraphicGauge — 5 pcc names (btjoy/cm_off/cm_other/
cm_only/cm_both) interned into the warehouse; Execute `0x4c6f1c` reads the control-mapper state from
`app->GetModeManager()` (`DAT_004efc94+0x3c`, group `0x1c0 + i*0x20`) and blits the matching frame.
SetColor `0x4c6ee0` (writes `this+0x94`). dtor `0x4c6e54`.
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 under `BT_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.
+70
View File
@@ -0,0 +1,70 @@
// Comprehensive decompile of the vehicleSubSystems Make dispatcher, the whole
// SubsystemCluster family, and the sub-gauge ctors they build. Also dumps the
// per-group geometry table and the relevant vtables. Writes to scratchpad.
import ghidra.app.script.GhidraScript;
import ghidra.app.decompiler.*;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.*;
import ghidra.program.model.mem.*;
import ghidra.program.model.symbol.*;
import java.io.*;
public class DecompVSS extends GhidraScript {
public void run() throws Exception {
String outPath = "C:\\Users\\epilectrik\\AppData\\Local\\Temp\\claude\\C--git-bt411\\09bf24bb-f92d-423a-b815-1eb662736f58\\scratchpad\\vss_sub.txt";
PrintWriter out = new PrintWriter(new FileWriter(outPath));
FunctionManager fm = currentProgram.getFunctionManager();
Memory mem = currentProgram.getMemory();
DecompInterface dec = new DecompInterface();
dec.openProgram(currentProgram);
long[] targets = {
// btl4gau2 sub-gauge ctors that must be reconstructed
0x4c70a4L, 0x4c7134L, 0x4c3134L, // CoolingLoop/AnimatedSubsystemLamp + dtor + connection
0x4c7160L, 0x4c71f0L, 0x4c31ecL, // PowerSource/AnimatedSourceLamp + dtor + connection
0x4c721cL, 0x4c72acL, 0x4c733cL, // ScalarBarGauge/GeneratorVoltage + ctor2 + dtor
0x4c6d80L, 0x4c6e54L, 0x4c6ee0L, // ConfigMapGauge + dtor + SetColor
0x4c6f34L, // ConfigMapGauge Execute (guess)
// SubsystemCluster helpers
0x4c89c4L, 0x4c8820L, 0x4c8990L, 0x4c8a28L, 0x4c2ec4L,
// Weapon/Ballistic extra vtable slots
0x4c92e4L, 0x4c9b24L, 0x4c9b50L, 0x4c8990L,
// connections
0x4749deL, 0x474855L, 0x4c3288L,
};
for (long t : targets) {
Address a = toAddr(t);
Function f = fm.getFunctionContaining(a);
if (f == null) {
try { disassemble(a); f = createFunction(a, "FUN_" + Long.toHexString(t)); }
catch (Exception e) { out.println("// could not create fn at " + a + ": " + e); }
}
out.println("\n//====================================================================");
out.println("// FUN_" + Long.toHexString(t) + (f!=null ? " ("+f.getName()+")" : ""));
out.println("//====================================================================");
if (f == null) { out.println("// NO FUNCTION"); continue; }
DecompileResults r = dec.decompileFunction(f, 60, monitor);
if (r != null && r.decompileCompleted()) out.println(r.getDecompiledFunction().getC());
else out.println("// decompile failed: " + (r!=null?r.getErrorMessage():"null"));
}
// vtables for the cluster family
long[] vts = { 0x51a020L /*SubsystemCluster*/, 0x519fd4L /*HeatSink*/, 0x519f88L /*Myomer*/,
0x519f38L /*Weapon*/, 0x519ee8L /*Energy*/, 0x519e98L /*Ballistic*/ };
out.println("\n//==== cluster vtables ====");
for (long vt : vts) {
StringBuilder sb = new StringBuilder(String.format("vt 0x%x: ", vt));
for (int i=0;i<20;i++){ long v = mem.getInt(toAddr(vt+i*4))&0xffffffffL; sb.append(String.format("[%d]=0x%x ",i,v)); }
out.println(sb.toString());
}
// geometry table
out.println("\n//==== geometry table @0x51bf34 (12 groups x 7 dwords) ====");
for (int g=0; g<12; g++) {
StringBuilder sb = new StringBuilder("group "+(g+1)+": ");
for (int k=0;k<7;k++){ long v = mem.getInt(toAddr(0x51bf34L+(g*7L+k)*4))&0xffffffffL; sb.append(String.format("0x%x ",v)); }
out.println(sb.toString());
}
out.close();
println("DecompVSS full done -> " + outPath);
}
}