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:
co-authored by
Claude Opus 4.8
parent
a2ee1181e6
commit
c2c4ab1f67
@@ -696,6 +696,29 @@ depth SMALLER than the RT is INVALID → all draws silently fail (window shows t
|
||||
unbind depth (Z is off anyway), restore after. Pod `BuildWindows`/`Update` path byte-unchanged. REMAINING = the
|
||||
widget recon (above) + the pod MFD port-name fallback.
|
||||
|
||||
**✅✅ GAUGE WIDGET RECONSTRUCTION — the cockpit instruments now read LIVE game state (attribute wave DONE;
|
||||
full mechanism + technique in `docs/GAUGE_COMPOSITE.md` §Attribute wave).** After Milestone C put all 6
|
||||
surfaces on screen (drawing NULL), two waves made them read real data. (a) The **ColorMapper family + headingPointer**
|
||||
(prior session): the palette-tint schematics (`cmHeat`/`cmCrit`/`cmArmor`/`colorMapperMultiArmor`) + the rotating
|
||||
compass, each a reconstructed BT gauge class registered in `BTL4MethodDescription[]` (btl4grnd.cpp). (b) The
|
||||
**attribute wave** (this session, 4 commits): gauges bind to game state by NAME via the engine attribute-pointer
|
||||
system — the config's `Subsystem/Attribute` (e.g. `HeatSink/CurrentTemperature`) or bare `Attribute` (e.g.
|
||||
`LinearSpeed`) resolves through `ParseAttribute`→`FindSubsystem`(stricmp on `GetName()`)→`GetAttributePointer`→
|
||||
the class's `activeAttributeIndex`. A class publishes attributes by declaring an `<Name>AttributeID` enum + a
|
||||
`static AttributePointers[]` (`ATTRIBUTE_ENTRY`) + `GetAttributeIndex()` chained to its parent, and passing
|
||||
`GetAttributeIndex()` to its `DefaultData`. **⚠ DENSE-TABLE HAZARD (systemic): `AttributeIndexSet::Build` leaves
|
||||
gap slots uninitialized and `Find` strcmps EVERY slot → a published table MUST be a dense prefix from the parent's
|
||||
`NextAttributeID`; fill gaps with a shared read-only pad member.** Done: **HeatSink table** (`CurrentTemperature`
|
||||
→ Heat readout shows 77), **Mech table** (dense prefix 0x15..0x21, `LinearSpeed` → SPEED readout shows ~225),
|
||||
**vertBar** (coolant bars; x87 pixel math recovered by disassembly), **segmentArcRatio** (speed arc; thin subclass
|
||||
of engine `SegmentArc`). **Base primitives `numeric`/`numericSpeed`/`digitalClock`/`rankAndScore` are ENGINE
|
||||
(L4GAUGE.cpp) — only need the data; `vertBar`/`segmentArcRatio`/`oneOfSeveralPixInt`/`map`/`PlayerStatus`/
|
||||
`vehicleSubSystems` are BT-specific (reconstruct + register).** Technique added: `tools/disas2.py <VA> <len>`
|
||||
(recover FPU math Ghidra drops — `FUN_004dcd94`=round, `FUN_004dcd00`=fabs) + `tools/vtdump.py <vtVA> <n>`
|
||||
(find a vtable override the assert-anchored decomp didn't export). REMAINING: `oneOfSeveralPixInt`/`map`/
|
||||
`PlayerStatus`/`vehicleSubSystems` (task #13) + the aggregate HeatSink-bank `AmbientTemperature` (`#if 0`'d 0xBBE)
|
||||
+ the Reservoir `coolantCapacity` shadow.
|
||||
|
||||
**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.
|
||||
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import struct, sys
|
||||
from capstone import *
|
||||
|
||||
PATH = r"C:\git\bt411\content\BTL4OPT.EXE"
|
||||
data = open(PATH, "rb").read()
|
||||
e_lfanew = struct.unpack_from("<I", data, 0x3C)[0]
|
||||
coff = e_lfanew + 4
|
||||
num_sec = struct.unpack_from("<H", data, coff+2)[0]
|
||||
opt_size = struct.unpack_from("<H", data, coff+16)[0]
|
||||
opt = coff + 20
|
||||
image_base = struct.unpack_from("<I", data, opt+28)[0]
|
||||
sec_tbl = opt + opt_size
|
||||
secs = []
|
||||
for i in range(num_sec):
|
||||
off = sec_tbl + i*40
|
||||
vsize, va, rawsize, rawptr = struct.unpack_from("<IIII", data, off+8)
|
||||
secs.append((va, vsize, rawptr, rawsize))
|
||||
|
||||
def va_to_off(va):
|
||||
rva = va - image_base
|
||||
for sva, vsize, rawptr, rawsize in secs:
|
||||
if sva <= rva < sva + max(vsize, rawsize):
|
||||
return rawptr + (rva - sva)
|
||||
return None
|
||||
|
||||
def read_va(va, n):
|
||||
o = va_to_off(va)
|
||||
return data[o:o+n] if o is not None else None
|
||||
|
||||
def fconst(va):
|
||||
b = read_va(va, 4)
|
||||
return struct.unpack("<f", b)[0] if b else None
|
||||
|
||||
FN = int(sys.argv[1], 16)
|
||||
LEN = int(sys.argv[2], 16) if len(sys.argv) > 2 else 0x140
|
||||
code = read_va(FN, LEN)
|
||||
md = Cs(CS_ARCH_X86, CS_MODE_32); md.detail = True
|
||||
known = {"0x408328":"SinCos","0x4dcd94":"Round(ftol ST0)","0x40954c":"Quat->Euler",
|
||||
"0x4700bc":"NumericDisplay::ctor","0x4700ac":"nameCopy","0x442aec":"bitMapBin.Get",
|
||||
"0x442c12":"bitMapBin.Release","0x444818":"GraphicGauge::ctor","0x474855":"GaugeConnDirectOf::ctor",
|
||||
"0x4c2ff8":"tiledBlit","0x402298":"operator new","0x4dcdec":"Round64"}
|
||||
print(f"=== disasm @0x{FN:x} len=0x{LEN:x} ===")
|
||||
for ins in md.disasm(code, FN):
|
||||
line = f"0x{ins.address:x}: {ins.mnemonic:<7} {ins.op_str}"
|
||||
for tok in ins.op_str.replace("[", " ").replace("]", " ").split():
|
||||
if tok.startswith("0x"):
|
||||
try:
|
||||
a = int(tok, 16)
|
||||
if 0x4b0000 <= a <= 0x520000:
|
||||
fv = fconst(a)
|
||||
if fv is not None:
|
||||
line += f" ; [0x{a:x}]={fv:g}f"
|
||||
except ValueError:
|
||||
pass
|
||||
if ins.mnemonic == "call" and ins.op_str in known:
|
||||
line += f" ; {known[ins.op_str]}"
|
||||
print(line)
|
||||
if ins.mnemonic == "ret":
|
||||
break
|
||||
@@ -0,0 +1,30 @@
|
||||
import struct, sys
|
||||
PATH = r"C:\git\bt411\content\BTL4OPT.EXE"
|
||||
data = open(PATH, "rb").read()
|
||||
e_lfanew = struct.unpack_from("<I", data, 0x3C)[0]
|
||||
coff = e_lfanew + 4
|
||||
num_sec = struct.unpack_from("<H", data, coff+2)[0]
|
||||
opt_size = struct.unpack_from("<H", data, coff+16)[0]
|
||||
opt = coff + 20
|
||||
image_base = struct.unpack_from("<I", data, opt+28)[0]
|
||||
sec_tbl = opt + opt_size
|
||||
secs = []
|
||||
for i in range(num_sec):
|
||||
off = sec_tbl + i*40
|
||||
vsize, va, rawsize, rawptr = struct.unpack_from("<IIII", data, off+8)
|
||||
secs.append((va, vsize, rawptr, rawsize))
|
||||
def va_to_off(va):
|
||||
rva = va - image_base
|
||||
for sva, vsize, rawptr, rawsize in secs:
|
||||
if sva <= rva < sva + max(vsize, rawsize):
|
||||
return rawptr + (rva - sva)
|
||||
return None
|
||||
VT = int(sys.argv[1], 16)
|
||||
N = int(sys.argv[2]) if len(sys.argv) > 2 else 32
|
||||
o = va_to_off(VT)
|
||||
print(f"vtable @0x{VT:x}:")
|
||||
for i in range(N):
|
||||
fp = struct.unpack_from("<I", data, o + i*4)[0]
|
||||
tag = ""
|
||||
if 0x4c2f94 <= fp <= 0x4c6771: tag = " <-- BT btl4gaug fn"
|
||||
print(f" [+0x{i*4:02x}] slot{i:2d} = 0x{fp:x}{tag}")
|
||||
Reference in New Issue
Block a user