diff --git a/CLAUDE.md b/CLAUDE.md index dd2b740..bbd687f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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. diff --git a/docs/VEHICLE_SUBSYSTEMS.md b/docs/VEHICLE_SUBSYSTEMS.md new file mode 100644 index 0000000..4874620 --- /dev/null +++ b/docs/VEHICLE_SUBSYSTEMS.md @@ -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 ". 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. diff --git a/reference/ghidra_scripts/DecompVSS.java b/reference/ghidra_scripts/DecompVSS.java new file mode 100644 index 0000000..aa521cb --- /dev/null +++ b/reference/ghidra_scripts/DecompVSS.java @@ -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); + } +}