docs: record the gauge attribute-wave (mechanism + 4 increments) + durable RE tools

Document the cockpit-gauge attribute-pointer system in GAUGE_COMPOSITE.md
(§Attribute wave) and CLAUDE.md §10: how a Subsystem/Attribute or bare-Attribute
binding resolves (ParseAttribute -> FindSubsystem/GetName -> GetAttributePointer
-> activeAttributeIndex), what a class must publish (AttributeID enum +
AttributePointers[] + GetAttributeIndex chained + DefaultData wiring), the
DENSE-TABLE HAZARD (Build leaves gap slots uninitialized, Find strcmps every
slot -> tables must be a dense prefix), and the base-primitive vs BT-specific
widget split.  Records the 4 committed increments (HeatSink table, Mech table,
vertBar, segmentArcRatio) and the follow-ups (oneOfSeveralPixInt/map/PlayerStatus/
vehicleSubSystems, the #if0'd HeatSink-bank AmbientTemperature, the Reservoir
coolantCapacity shadow).

Promote the two reverse-engineering helpers from the session scratchpad into
tools/ (durable): disas2.py (capstone + PE parse -- recovers x87 float math
Ghidra drops; FUN_004dcd94=round, FUN_004dcd00=fabs) and vtdump.py (dump a class
vtable to find an override the assert-anchored decomp never exported, e.g.
SegmentArcRatio::Execute).  Doc references updated scratchpad/ -> tools/.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-06 18:52:53 -05:00
co-authored by Claude Opus 4.8
parent a2ee1181e6
commit c2c4ab1f67
4 changed files with 190 additions and 0 deletions
+78
View File
@@ -344,3 +344,81 @@ reads the public `currentTemperature`). Verified: parses, builds, all subsystems
`segmentArcRatio` speed/`GeneratorCluster` — need `AttributePointers[]` on Mech/HeatableSubsystem, a separate
pass); the XL items (`map`/`vehicleSubSystems`/`PlayerStatus`) last. The POD path needs the FULL 23-entry
table with real ctors (the dev tolerant-skip is the on-ramp, not the finish).
## Attribute wave DONE (2026-07) — the cockpit numeric/bar/arc gauges read LIVE data
The gauge widgets bind to game state by NAME through the engine attribute-pointer system, mapped by the
`attr-wave-decomp-map` workflow and implemented as 4 committed increments. **This is the mechanism every
data-driven gauge depends on — read it before reconstructing any further widget.**
### How a gauge binding resolves (engine, already complete)
The config binds either `Subsystem/Attribute` (e.g. `HeatSink/CurrentTemperature`) or a bare `Attribute`
(e.g. `LinearSpeed`). `GAUGREND.cpp ParseAttribute` (~2130) splits on `/`:
- `Subsystem/Attr``entity->FindSubsystem(subName)` (ENTITY.cpp:631 — `stricmp` on each subsystem's
`GetName()`) → `subsystem->GetAttributePointer(attrName)`.
- bare `Attr``entity->GetAttributePointer(name)`.
`Simulation::GetAttributePointer(name)` (SIMULATE.cpp:427) = `GetSharedData()->activeAttributeIndex->
Find(name)` then `&(this->*attr)`.
### What a class must publish (the per-class work)
1. An `<Name>AttributeID` enum chained from the parent (`= Parent::NextAttributeID … NextAttributeID`).
2. `static const IndexEntry AttributePointers[]` via `ATTRIBUTE_ENTRY(Class,Name,member)` (SIMULATE.h:284).
3. `static AttributeIndexSet& GetAttributeIndex()` chained to the parent (pattern: MOVER.cpp:83).
4. The class's `DefaultData`/`SharedData` ctor must PASS `GetAttributeIndex()` (MOVER.cpp:28-35) — this
sets `activeAttributeIndex`. Bind `ATTRIBUTE_ENTRY` to a REAL named member (compiled ptr-to-member;
NEVER a raw binary offset — the compiled layout != binary). The engine `Mover`/`JointedMover`/`Entity`
tables are dense and real; chain to them.
### ⚠ DENSE-TABLE HAZARD (systemic-bug class — verified in SIMULATE.cpp:565/663)
`AttributeIndexSet::Build` sizes the merged index to `max(entryID)`, copies the inherited slots, places own
entries at `[id-1]`, and **leaves gap slots uninitialized** (`new IndexEntry[n]` on a POD = garbage
`entryName`). `Find(name)` then `strcmp`s EVERY slot → a gap between the parent's `NextAttributeID` and the
highest id you publish = strcmp on a garbage pointer = AV. So a published table MUST be a **dense prefix**
from the parent's `NextAttributeID` up to the max id it includes. You cannot ship just the ids you need:
fill the gaps (bind not-yet-needed ids to a shared read-only pad member).
### Base engine primitives vs BT-specific widgets (don't reconstruct the base ones)
- **Engine base primitives (already registered + drawing; only need the DATA):** `numeric`, `numericSpeed`,
`digitalClock`, `rankAndScore`, `bgPixelMap`, `bgBitMap` (L4GAUGE.cpp). `digitalClock`'s `time` arg is a
FORMAT enum, NOT an attribute — the value comes from the mission clock inside the widget.
- **BT-specific (need reconstruction + registration in `BTL4MethodDescription[]`):** `vertBar`,
`segmentArcRatio`, `oneOfSeveralPixInt`, `GeneratorCluster`, `map` (radar), `PlayerStatus`,
`vehicleSubSystems`, plus the done ColorMapper family + headingPointer.
### Committed increments (each verified live in the dev gauge window; combat un-regressed; heap-clean)
1. **HeatSink attribute table** (heat.{hpp,cpp}) — `CoolantMass`→coolantLevel, `CoolantCapacity`
thermalCapacity, `CurrentTemperature`→currentTemperature. Condenser/Reservoir inherit it (both derive
from HeatSink). → Heat surface temp readout `S` now shows **77** (was 0).
2. **Mech attribute table** (mech.{hpp,cpp}, mech4.cpp) — full enum 0x15..0x38 declared; DENSE PREFIX
0x15..0x21 published (gap ids share `attrPad`; `CurrentSpeed`→legCycleSpeed, `MaxRunSpeed`
reverseStrideLength, `LinearSpeed`→new `linearSpeed` member set to |adv|/dt per frame). Chained to
`JointedMover::GetAttributeIndex()`. → radar SPEED readout now shows **~225** (was 0).
3. **vertBar** (VertTwoPartBar, btl4gaug) — the COOLANT bars. x87 pixel math recovered by disassembly
(`FUN_004dcd94`=`(int)ROUND(ST0)`): `warnPix/valPix = clamp(round(height*low_or_value/high + 0.5),0,
height)`; tile-blit `[0,warnPix)`, fill `[warnPix,valPix)`, clear `[valPix,height)`.
4. **segmentArcRatio** (SegmentArcRatio, btl4gaug) — the SPEED arc. Thin subclass of engine `SegmentArc`;
Execute override (`FUN_004dcd00`=fabs): `currentValue = clamp(|num/den * segmentSpan|,0,1)` then
`SegmentArc::Execute()`. `segmentSpan = (Scalar)(|n|/(|n|-1))*0.75f` (int division → 0.75 for 36 segs).
### Reconstruction-technique additions (durable)
- **x87 float math Ghidra drops** (`FUN_004dcd94()`/`FUN_004dcd00()` show no args = FPU-stack operands):
disassemble the fn with `tools/disas2.py <VA> <len>` (capstone + PE parse; annotates `.data` float
constants + known call targets). `FUN_004dcd94`=round-to-int, `FUN_004dcd00`=fabs.
- **Find a vtable override the assert-anchored decomp didn't export** (e.g. SegmentArcRatio::Execute wasn't
in `part_*.c`): dump the class vtable with `tools/vtdump.py <vtableVA> <n>`; a slot pointing into the
module's own code range (btl4gaug = 0x4c2f94..0x4c6771) is the override → disassemble it.
- **GraphicsView method vtable map** (GRAPH2D.h): +0x08 SetPositionWithinPort, +0x10 SetOrigin, +0x18
SetColor, +0x24 MoveToAbsolute, +0x38 DrawThickLineToAbsolute, +0x48 DrawFilledRectangleToAbsolute.
Resource fetch = `renderer->warehousePointer->bitMapBin.Get/Release(name)` (FUN_00442aec/00442c12).
### Deferred / follow-ups
- `HeatSink/AmbientTemperature` maps to `ambientTemperature`@0x1D4 on the AGGREGATE HeatSink-bank class
(0xBBE, ctor @4ae8d0) which is `#if 0`'d — reviving it + its own table is a separate task (Heat `A`
readout stays 0 until then).
- Reservoir shadow: `Reservoir::coolantCapacity`@0x128 shadows the inherited `thermalCapacity` the gauge
reads → `Reservoir/CoolantCapacity` reads the base default; fix = delete the shadow + write thermalCapacity
in the Reservoir ctor (bundle with the Reservoir coolant-bar verification).
- Remaining widgets (task #13): `oneOfSeveralPixInt` (button lamps — `ControlsMapper/DisplayMode` already
publishes; `DuckState`/`Searchlight/LightOn` need the Mech dense table extended to 0x37 + a Sensor/
Searchlight table), `map` (radar — needs Mech RadarRange/LinearPos/AngularPos published + Sensor
RadarPercent), `PlayerStatus`, `vehicleSubSystems`.