gauges: reconstruct headingPointer -- the rotating compass + heading readout [widget recon 2]

The visible money-shot: a green compass needle that rotates with the mech's
facing plus a numeric heading in whole degrees (render-verified: the needle
swung and the number went 247 -> 182 as the mech turned).

Full class reconstruction from the binary (Make/ctor/dtor/TestInstance/
ShowInstance/BecameActive/Execute). Ghidra dropped the x87 float math feeding
Execute's needle endpoints and the ctor's NumericDisplay centering, so it was
recovered by disassembling BTL4OPT.EXE with capstone (scratchpad/disas_hp.py):
the needle is a radial line from innerRadius(20) to outerRadius(39) at the
heading angle, endpoints rounded half-up; the readout is round(360 - deg).

Two corrections vs the map:
- The header ctor was 12 args; the binary's is 14 -- widened it. Fields @0x98/
  @0x9C are the needle's inner/outer RADIUS (Execute multiplies them by sin/cos),
  not "color/spacing" as the old field names implied; renamed accordingly.
- ENGINE-CONVENTION FIX: the binary read the heading from EulerAngles index [0],
  but the WinTesla EulerAngles(Quaternion) decomposition is ambiguous for a
  yawing mech (the quaternion double-cover flips it to a pitch=roll=pi branch as
  yaw sweeps past +/-pi -> the needle spins erratically). Switched to
  YawPitchRoll, whose yaw-first .yaw is the clean continuous heading
  (pitch=roll~0). Faithful to the binary's intent (heading), correct for this
  engine's decomposition.

renderer->GetLinkedEntity() resolves the viewpoint mech (a GetViewpointEntity
fallback is kept belt-and-braces). Deps (NumericDisplay/GraphicsViewRecord/
GraphicGauge) are real engine classes -> no /FORCE risk; verified no unresolved
HeadingPointer externals, no parse desync, combat un-regressed (TARGET
DESTROYED, 0 crashes). Details + the disasm technique: docs/GAUGE_COMPOSITE.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-06 16:05:47 -05:00
co-authored by Claude Opus 4.8
parent 547f25e043
commit e869b00181
4 changed files with 290 additions and 40 deletions
+26 -3
View File
@@ -257,9 +257,32 @@ Execute→palette-push runs end-to-end; combat un-regressed (TARGET DESTROYED, 0
schematic zones are now tinted by the REAL subsystem `currentTemperature` (stable tint since temp is stable —
the visible-animation win is `headingPointer` next). **Method established for every remaining widget.**
**Next increments (priority order from the map):** `headingPointer` (rotating compass + numeric heading
reconstruct from prose `btl4gaug.cpp:644`, reads owner quaternion directly, no attribute table/connection
needed — the visible money-shot); then `cmArmor`/`colorMapperMultiArmor` (armor schematic, needs the
**✅ Increment 2 — `headingPointer` (HeadingPointer) DONE — the rotating compass + live heading readout.**
The visible money-shot: a green needle that rotates with the mech's facing + a numeric heading in whole
degrees (render-verified: needle swung down-left→right and the number went 247°→182° as the mech turned).
Full class reconstruction from the binary (Make/ctor/dtor/TestInstance/ShowInstance/BecameActive/Execute) —
the Ghidra pseudo-C for Execute@004c5914 + ctor@004c562c had the x87 endpoint math dropped, so it was
**recovered by disassembling BTL4OPT.EXE** (capstone, `scratchpad/disas_hp.py`). Key facts learned:
- **The header ctor arity was wrong (12 vs the binary's 14).** Widened it; fields `@0x98`/`@0x9C` are the
needle's inner/outer RADIUS (Execute does `fild [@0x98] ; fmul sin`), not "color/spacing" as named. The
needle is a radial line innerRadius(20)..outerRadius(39) at the heading angle; endpoints round half-up
(`(int)(r*trig + 0.5f)` == the binary's `fadd 0.5 ; _ftol`). The readout = `round(360 - normalize(deg))`.
- **⭐ ENGINE-CONVENTION FIX (empirical, as the map's data-binding agent flagged): the binary read the
heading from `EulerAngles` index [0] (`.pitch`), but the WinTesla MUNGA `EulerAngles(Quaternion)`
decomposition is AMBIGUOUS for a yawing mech** — the quaternion double-cover flips it to a `pitch=roll=π`
branch as the yaw sweeps past ±π, so `euler.yaw` jumps discontinuously (needle spins erratically). Switched
to **`YawPitchRoll`** (yaw applied first → `.yaw` is the clean continuous heading, `pitch=roll≈0`
throughout). This is a legitimate port of the binary's INTENT, not a stand-in: the original engine's euler
put the yaw in [0]; WinTesla's puts it in a different, ambiguous slot, so read the unambiguous decomposition.
- `renderer->GetLinkedEntity()` (Renderer::GetLinkedEntity, RENDERER.h:374) resolves the viewpoint mech
(the entitySocket IS wired — the map's NULL-socket worry didn't materialize; a `GetViewpointEntity()`
fallback is kept belt-and-braces). Deps (`NumericDisplay`, `GraphicsViewRecord`, `GraphicGauge`) are all
real engine classes, so no compounding `/FORCE` risk. Verified: no unresolved `HeadingPointer` externals,
no parse desync, combat un-regressed (TARGET DESTROYED, 0 crashes). **TECHNIQUE (reusable): recover an x87
float computation Ghidra dropped by PE-parsing + capstone-disassembling the fn (`scratchpad/disas_hp.py`) —
read the `fild/fmul/fadd` stream + the `.data` float pool.**
**Next increments (priority order from the map):** `cmArmor`/`colorMapperMultiArmor` (armor schematic, needs the
`ArmorZoneConnection`/`MultiArmorConnection` classes reconstructed); then the attribute-table wave (`vertBar`/
`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