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:
co-authored by
Claude Opus 4.8
parent
099871ef4c
commit
a5fa9f1c79
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user