The MadCat torso twists, the view turns with it, and targeting follows. Three reconstruction fronts closed: THE ELECTRICAL WATCHDOG CHAIN (why the torso never powered up): - PowerWatcher::UpdateWatch reconstructed (@004b181c, the REAL registered Performance -- PTR @0050f5fc; Ghidra missed the fn start): the watchdog MIRRORS the watched subsystem's electrical level (+0x278), brownout downgrade when gen output <= minVoltage% x rated. @004b1804 relabeled ResetToInitialState (slot 10) -- the old "Simulation" tag was wrong. - The factory watcher-CONNECT pass reconstructed (vtable slot +0x38, @004aee2c/@004b1a40 byte-identical, recovered from raw exe bytes): watchedLink.Add(roster[watchedSubsystem]) on the master node. Was the SubProxy::Start() no-op -- every watchdog sat at 0 forever. - MinVoltageScale = 0.01 (a 10-byte x87 literal @0x4b1924; was 1.0f = permanent brownout) and PowerWatcher's Derivation chains its REAL base HeatWatcher (the HeatableSubsystem stand-in broke IsDerivedFrom for the whole Torso/Searchlight/ThermalSight family). - KB correction swept: derivation tag 0x50e604 = HEATWATCHER (not "HeatSink"); the btl4gaug heat-widget gate now tests it via the BTIsHeatWatcher bridge. THE CROSSHAIR (task #58 forensics, 6-agent workflow + live probes): - The VIEW is TORSO-MOUNTED: jointtorso -> jointeye -> siteeyepoint in every twist-capable .SKL; the camera + canopy ride the same hinge subtree through HingeRenderable's live matrix-stack compose -- ALREADY WORKING in the port. The crosshair stays screen-centered (center IS the boresight); the twist reads on the tape carets/compass/radar. - The real bug was the port's gBTAimX = tan(twist) slew (the falsified "body-mounted view" model): the camera already carried the twist, so the crosshair counter-slid to hull-forward and the fire ray with it. Deleted; the pick ray inherits the twist from the yawing eye basis. - Two instrumentation traps documented (chase-eye-as-default-camera, BT_FORCE_TORSO clobbering real joints -> the hook now only fills unresolved ones); an over-correcting explicit eye compose was added on those false readings and retired the same day. CONTROLS + REPLICATION: - Q/E spring-center on release (the axis is a twist-RATE demand; the old hold-deflection model drifted forever); X also zeroes the axis and pulses the authentic torso Recenter (@004b6918). M cycles control mode via the real CycleControlMode body. - Torso update-record DIRECTION fixed: engine truth is Write=serialize / Read=apply; @004b6a78 is the READ (was mislabeled Write) and the missing WRITE @004b6a1c recovered from raw disasm (recordLength 0x1C, twist/vel/rate at +0x10/14/18) -- kills the replicant's 0xCDCDCDCD -140-degree ghost twist. - Marching-ghost desync: 4 Standing-case guards zero stale reverse cycleSpeed (negative cadence passed the <= ZeroSpeed stop gate). - Kill credit rerouted to the OBSERVED killer (lastInflictingID -> killer's player link) -- kills count, target K/D populates. KB: subsystems.md (watcher chain), multiplayer.md (record direction), combat-damage.md + gauges-hud.md + cockpit-view.md (torso-mounted view re-correction), decomp-reference.md (new addresses + tag fix), open-questions.md (dead capability-roster loops 2-4, snapshot CD read). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
216 lines
17 KiB
Markdown
216 lines
17 KiB
Markdown
---
|
||
id: gauges-hud
|
||
title: "Cockpit Gauges / MFD HUD system"
|
||
status: established
|
||
source_sections: "docs/GAUGE_COMPOSITE.md (full map); CLAUDE.md §8"
|
||
related_topics: [reconstruction-gotchas, decomp-reference, subsystems, pod-hardware]
|
||
key_terms: [gauge, methodDescription, attribute-pointer, GaugeRenderer, MFD, PCC]
|
||
open_questions:
|
||
- "MessageBoard LIVE 2026-07-12 (kill ticker); remaining: only strip 0 (kill) is produced -- survey the other btsmsgs.pcx strips for authentic producers"
|
||
- "HUD binary attr table @005110c0 offsets conflict with the port HUD layout on 3 slots (0x1D8/0x1EC/0x1F8) -- re-dump before publishing"
|
||
- "Secondary-view cycling (Damage/Critical/Heat) unreachable from the desktop keyboard: the mapper's 0x13d/0x13e cases are dead DOS F3/F4 codes (VK_F3/F4 = 0x72/0x73 collide with 'r'/'s') -- wire a free key to CycleControlMode"
|
||
- "MP DEATHS resolved 2026-07-12 (observed-death tally + display clamp); remaining: verify multi-death tallies stay in sync across a long session (GAUGE_COMPOSITE.md)"
|
||
---
|
||
|
||
# Cockpit Gauges / MFD HUD
|
||
|
||
The pod's cockpit instruments: the secondary MFD (radar/heat/comm), the 5 mono MFDs, and the
|
||
engineering screens. Full map + per-widget history in **`docs/GAUGE_COMPOSITE.md`**. **The gauge
|
||
system is COMPLETE** at the registration+binding level: all 50 config attribute bindings resolve
|
||
(0 NULL) and every config gauge primitive is registered + built (0 parse-skips). [T2]
|
||
|
||
## Architecture (three layers)
|
||
1. **The config** `content/GAUGE/l4gauge.cfg` — a text file the engine `GaugeInterpreter` parses
|
||
(`BuildConfigurationFile → Initialize → GetProcedureBody → ParsePrimitive`). Each mech's gauge
|
||
tree is built from the label `GetGameModel()+"Init"` (e.g. `bhk1Init`), which invokes shared
|
||
blocks (`MechInit`, `Secondary1`) that `configure` the ports and call gauge primitives. [T1]
|
||
2. **Widgets** — each `keyword(...)` in the config resolves to a `MethodDescription` in
|
||
`BTL4MethodDescription[]` (btl4grnd.cpp). An UNREGISTERED keyword is **parse-skipped** (never
|
||
built). Base primitives (`numeric`/`digitalClock`/`rankAndScore`/`vertBar`-base/`segmentArc`)
|
||
are ENGINE (L4GAUGE.cpp); BT-specific ones (`vertBar`/`map`/`pilotList`/`GeneratorCluster`/
|
||
`sectorDisplay`/`prepEngr`/`messageBoard`/`vehicleSubSystems`/the ColorMapper family) are
|
||
reconstructed + registered. [T2]
|
||
3. **Data binding** — gauges bind to game state by NAME via the engine [[attribute-pointer]]
|
||
system: the config's `Subsystem/Attribute` (e.g. `HeatSink/CurrentTemperature`) resolves
|
||
through `ParseAttribute → FindSubsystem (stricmp GetName()) → GetAttributePointer`. A class
|
||
PUBLISHES an attribute via a `<Name>AttributeID` enum + `static AttributePointers[]`
|
||
(`ATTRIBUTE_ENTRY`) + `GetAttributeIndex()` chained to its parent. ⚠ DENSE-TABLE HAZARD (see
|
||
[[reconstruction-gotchas]] §11). [T2]
|
||
|
||
## Reconstructing a widget — the recipe
|
||
`MethodDescription Class::methodDescription = { "keyword", Class::Make, { ParameterDescription
|
||
rows } };` + a line in `BTL4MethodDescription[]` before `&BTL4ChainToPrevious`. The static
|
||
`Make(int port, Vector2DOf<int> pos, Entity*, GaugeRenderer*)` reads `methodDescription.parameterList[]`,
|
||
allocs (`operator new(binSize)` or plain `new`), placement-news the ctor. Then ctor/dtor/
|
||
TestInstance/BecameActive/Execute. **Gotchas that bite EVERY widget:**
|
||
- Container-Execute must override (§10 gotchas) — else `Gauge::Execute` aborts.
|
||
- `/FORCE` trap — a prose-only slot AVs on first call.
|
||
- Databinding trap — never raw-read owner offsets; use a bridge (`BTGetSubsystemAuxScreen`, …).
|
||
- ReconStream no-op — use `DEBUG_STREAM` for logs, not `DebugStream`.
|
||
- Lazy build — wait for the gauge window before concluding "not built" (`BT_GAUGE_SKIP_LOG`). [T2]
|
||
|
||
## The completed widgets (this project's gauge wave)
|
||
- **Attributes published:** HeatSink table (Degradation/Failure/NormalizedPressure/CoolantMassLeakRate/
|
||
ValveSetting/…), PoweredSubsystem (InputVoltage), MechWeapon (OutputVoltage), Mech (LinearSpeed,
|
||
radar Position/Quaternion), Sensor (RadarPercent), **AggregateHeatSink (AmbientTemperature=300 —
|
||
the LAST NULL)**. [T2]
|
||
- **Widgets reconstructed + registered:** ColorMapper family (cmHeat/cmCrit/cmArmor/multiArmor),
|
||
headingPointer, vertBar (VertTwoPartBar), segmentArcRatio, oneOfSeveralPixInt, map (radar),
|
||
PlayerStatus, vehicleSubSystems (the engineering cluster panels), LeakGauge, vertNormalSlider,
|
||
**pilotList** (Comm KILLS/DEATHS), **GeneratorCluster**, **SectorDisplay** (radar SECTOR X/Z
|
||
read-out — live), **PrepEngrScreen** (12 engineering-screen label overlays), **MessageBoard**
|
||
(comm ticker — deferred-empty). [T2]
|
||
- **Condenser valve gauge:** ValveSetting→coolantFlowScale reads the authentic **1/N**
|
||
(`RecomputeCondenserValves`, FUN_0049f788, was a no-op stub). [T2]
|
||
|
||
## Dev composite (off-pod)
|
||
`BT_DEV_GAUGES` renders the 6 pod [[MFD]] surfaces in a separate 960×384 window (bit-plane masks
|
||
over one shared `SVGA16` pixelBuffer; `SVGA16::DrawDevSurface`). On the POD they come from
|
||
`SETENV.BAT`/`L4GAUGE` on the real multi-adapter hardware (`FindBestAdapterIndices`/`BuildWindows`,
|
||
intact). The `overlay` port (SectorDisplay lives there) shares the `sec` physical surface via a
|
||
different bit-plane (0x00C0). [T2]
|
||
|
||
## The secondary screen's THREE views (Damage / Critical / Heat) — mode-gated [T0/T1]
|
||
The `sec` port stacks three mode-gated mech-schematic layers at offset (50,0) over the
|
||
always-on radar/heading/speed/messageBoard (`Secondary1`): **Damage** (`ModeSecondaryDamage`,
|
||
`<mech>dama.pcc` + `colorMapArmor`/`colorMapperMultiArmor` — 4 silhouettes front/left/right/back,
|
||
pixel-plane ids 60-63, per-DAMAGE-ZONE `dz_*` tint through the adpal→adpal2 ramp), **Critical**
|
||
(`ModeSecondaryCritical`, `<mech>crit.pcc` + the cmCrit per-SUBSYSTEM list), **Heat**
|
||
(`ModeSecondaryHeat` + cmHeat). Mode bits (BTL4MODE.HPP, `nextModeBit`=0 [T0]): Mapping=0x8000,
|
||
NonMapping=0x10000, Intercom=0x20000, **SecondaryDamage=0x40000, SecondaryCritical=0x80000,
|
||
SecondaryHeat=0x100000** (bits 18-20). `ModeInitial` includes **SecondaryDamage** → the ARMOR
|
||
view is the default-on layer (our port creates `BTL4ModeManager(ModeInitial)`, btl4app.cpp:303).
|
||
`L4MechControlsMapper::SetControlMode` @004d1ae4 is the SELECTOR — its mask table
|
||
{0x40000,0x80000,0x100000} clears bits 18-20 and sets one — despite the "control mode" name it
|
||
switches the secondary VIEW. ⚠ Desktop gap: the Keypress cases `0x13d`/`0x13e`
|
||
(CycleControlMode/CycleDisplayMode) are the DOS Tesla extended F3/F4 codes and never fire under
|
||
the WinTesla VK map (VK_F3=0x72 collides with 'r', VK_F4=0x73 with 's' — the same collision
|
||
class as the documented 'p'/VK_F1 drop), so the desktop is PINNED on the Damage view. The
|
||
schematic shows the pilot's OWN mech only — there is no target-damage readout in the cockpit.
|
||
|
||
## pilotList (Comm KILLS/DEATHS) row semantics + the −1 [T1/T2]
|
||
One ROW PER PILOT in the mission (2-player MP = 2 rows — not duplicate displays). KILLS =
|
||
`killCount` (the victim's ScoreMessageHandler credits the shooter cross-player, works for both
|
||
rows); DEATHS = `Player::deathCount`: engine-inits to **−2** (PLAYER.cpp:759), the LOCAL
|
||
vehicle-acquire branch zeroes it (btplayer.cpp:1118), then VehicleDead(-1) ++s per death. A
|
||
REMOTE player's Player object never runs the local acquire → −2 +1 spawn increment = **−1
|
||
locked**. **RESOLVED 2026-07-12 [T2]:** deaths now tally per node from LOCALLY OBSERVED events
|
||
(the same model as the cross-pod KILLS credit) — a replicant's once-per-death transition calls
|
||
`BTPlayerCountObservedDeath` on its owning player's local copy (replicant-gated: the master's
|
||
own VehicleDead path counts its node), and `BTPilotDeaths` clamps the −2/−1 pre-acquire seed
|
||
to 0 for display. Own row counts correctly as before.
|
||
|
||
## Launcher-panel recharge dial — authentically STATIC [T1, writer census]
|
||
The weapon panels' SegmentArc270 tick ring reads MechWeapon::rechargeLevel (+0x320). A FULL
|
||
writer census of the binary (all decomp spellings: `0x320) =`, `+ 800) =`, `[200] =`) finds
|
||
exactly four writers in the weapon family: the ctor (@004b99a8, 1.0), the stream init
|
||
(@004b8fec), ResetToInitialState (@004b96ec, 1.0), and the ONE live writer —
|
||
`Emitter::ComputeOutputVoltage` @004ba738, reached only through the Emitter vtable. Projectile
|
||
weapons (MissileLauncher/autocannon) never write it: their dial draws full ONCE and never moves
|
||
on the pod. The launcher panel's live indicators are the AMMO DIGITS (AmmoBin::ammoCount via
|
||
the complete-type bridges, 2026-07-12 — the old raw bin+0x180/0x18c reads were layout garbage),
|
||
the jam/fire lamps and the eject wipe. Do NOT "fix" the static ring — it is the authentic look.
|
||
|
||
## ConfigMapGauge (the weapon panel's regroup lamp column) — authentically DORMANT
|
||
The per-weapon btjoy.pcc joystick image + 4 cm_* state lamps (off/other/only/both) showing,
|
||
for each mappable fire button (Pinky/ThumbLow/Trigger/ThumbHigh), whether THIS panel's weapon
|
||
is bound to it. **The shipped binary never enables it [T1, task #6]:** no caller of SetColor
|
||
@004c6ee0 exists, so color==0 and Execute early-outs. The state loop is reconstructed
|
||
(btl4gau2.cpp; table DAT_00518eb4 PE-recovered; sampler = LBE4ControlsManager::
|
||
buttonGroup[btn].GetMapState — NOT the ModeManager, the old guard note was wrong) behind the
|
||
PORT dev enable `BT_CONFIGMAP=1`. The regroup MECHANISM itself (ConfigureMappables/
|
||
ChooseButton, task #6) is live regardless of the gauge.
|
||
|
||
## Authentically-static (do NOT "fix")
|
||
Degradation/Failure temps are fixed markers; AmbientTemperature 300; CoolantMassLeakRate 0 on a
|
||
pristine mech (damage-gated); cmArmor/cmCrit all-green (undamaged solo player — BT is PvP-only);
|
||
RANK 1 solo; MessageBoard empty (no status messages exist in bring-up).
|
||
**CORRECTION (task #10, 2026-07-11):** the old "Heat MFD is authentically NEAR-STATIC /
|
||
currentTemperature ~77 rounds to zero" claim was computed at the DEGENERATE bring-up heat scale.
|
||
At the authentic 1e7-unit economy (tasks #9-#10) the Heat MFD is fully dynamic: weapons run
|
||
77→2000 (the authored failure threshold), condensers/generators run 100-1400 under sustained
|
||
fire, and the bank plateaus ~600 — temperatures, heatLoad and the heat alarms all animate. [T2]
|
||
|
||
## Remaining = DATA FEEDS, not widgets (deferred systems)
|
||
**✅ The condenser valve CONTROL is LIVE (task #13, 2026-07-11) [T2]:** `MoveValve` (id 4, the
|
||
Condenser handler table @0x50E52C — exactly one entry) registered + guarded by the REAL
|
||
FUN_004ac9c8 = `player+0x274 roleClassIndex == 0` (the ROOKIE-role lockout; task #12's
|
||
`BTPlayerRoleLocksAdvanced` bridge — the old "game-mode flag / likely off in a basic mission"
|
||
hedge is superseded: bring-up role = 2 = UNLOCKED, verified live: press → valveState 1→5 →
|
||
flow redistribution 1/6 → 5/10). Desktop: 'C' cycles the selected condenser (BT_VALVE_SLOT).
|
||
The **MessageBoard** feed needs **StatusMessagePool** (a NULL stub) + the per-player status
|
||
queue. `SeekVoltageGraph`'s 4 Seek* attrs are a cluster-child (not config-called). [T2]
|
||
|
||
## Cockpit HUD reticle (main screen, inside view) — LIVE
|
||
`BTReticleRenderable` (0x358 bytes, ctor @004cc40c) draws over the finished 3D frame in cockpit
|
||
view only, via the recovered **dpl2d** 2D display-list API (recorders @0x487f34-0x488630; opcode
|
||
map + coordinate model in `phases/phase-02-dpl2d-reticle.md`; port: `game/reconstructed/dpl2d.cpp`).
|
||
Geometry is the ctor's hardcoded calibration (originX 0.35, originY 0.25, scaleY 0.5, right range
|
||
ladder 0..1200 m, bottom TORSO-TWIST tape (NOT a heading tape — stale wording swept task #58; see
|
||
the tape entry below), center cross + dot; tick ladders via FUN_004cd938). The
|
||
range caret binds to the live target range (`BTSetHudTargetRange`, fed by mech4's targeting step).
|
||
**Weapon pips:** the build loop (part_014.c:5386) registers EVERY subsystem
|
||
`IsDerivedFrom(0x511830 = MechWeapon)` — lasers, PPCs AND missile launchers (BLH = 7 pips) — via
|
||
`AddWeapon` @004cdac0 (verified store map in btl4vid.hpp).
|
||
|
||
**The per-frame `Execute` @004cdcf0 is RECOVERED (task #37, capstone disasm via
|
||
`tools/disas2.py` — the full annotated read: the task-37 commit + btl4vid.cpp comments) [T1],
|
||
and every instrument is now live [T2]:**
|
||
- **Right ladder** = range 0–1200 m: a YELLOW width-2 BAR from ladder-top to the caret + a
|
||
**GREEN width-1 caret triangle** (ctor @4550-4551 sets green/1 AFTER the yellow bar call —
|
||
transcription color bug caught by a period reference screenshot, 2026-07-09; same for the
|
||
bottom bowtie carets @4569-4570); pegs at 1200 with no target; the DISPLAYED range slides at
|
||
**500 m/s** toward the true pick range (HudSimulation :5652 [T1]). ⚠ A period pod screenshot
|
||
(`C:\git\image.webp`, likely a DIFFERENT pod revision — its crosshair is ~2.5× taller with
|
||
arrowhead arms, NOT our binary's ±0.04..0.16 program) structurally CONFIRMS our layout: yellow
|
||
bar + green caret + colored pip dots on the right ladder, bottom tape + green bowtie, rotating
|
||
compass circle bottom-left, and a mid-ladder range reading with no lock (the world-pick
|
||
terrain range). Our glyph constants remain [T1] from OUR 4.10 binary. **The weapon pips sit on
|
||
this same ladder at each weapon's authored max-range mark** — caret below a pip = that weapon
|
||
reaches the target. BLH authored data (live dump): 3× ER-M laser red @**500 m** (two stacked
|
||
at one column; PipExtendedRange=1), 2× missile amber (0.6,0.4,0) @**800 m**, 2× PPC blue
|
||
@**900 m** — so the 7 pips read as 3 weapon-SYSTEM groups. (The engineering-panel "RANGE 500M"
|
||
labels are panel text; the authoritative reach is the streamed WeaponRange.)
|
||
- **Bottom 21-tick tape = the TORSO-TWIST indicator** (NOT a heading tape): deflection line +
|
||
carets at `∓(span/2)·(RotationOfTorsoHorizontal / HorizontalTorsoLimit)` (HUD attrs 4/5/6).
|
||
Fixed-torso BLH: centred (authentic static).
|
||
- **Circle-with-stem = the COMPASS** (HUD attr 0xD CompassHeading, rad→deg rotation) at
|
||
`(botX, botY−3·tickMajor−0.03)`; the **THREAT trail** (attr 0xC ThreatVector) draws inside its
|
||
rotated frame: 0.05-unit attack-direction marks, fresh <2 s red, expiring at 6 s, 1 s blink tick.
|
||
Port feed: the player's TakeDamage handler pushes the impact direction.
|
||
- **Pips** (composed into subB6, master-called): hidden when the weapon's DAMAGE state == 1
|
||
(destroyed, attr 1); LIT (A) when the FIRE-CYCLE state == 2 (loaded, attr 0x1c; port source
|
||
rechargeLevel ≥ 1) else dark ring (B, charging); filtered by `weaponMode & elementMask&0xF`
|
||
(the weapon-GROUP bits Front/Rear/Left/Right). **Range plays NO part** — Execute never reads
|
||
the stored TargetWithinRange slots.
|
||
- **Lock ring** = subB9 (ring+cross) at frame centre, **SPINNING 4°/frame** while the Lock attr
|
||
(0xA) is up. The Lock PRODUCER is the authentic HudSimulation rule (part_013.c:5619-5634 [T1],
|
||
wired task #38): lock requires a target AND your own HUD's host zone damage < **0.75** (a
|
||
shot-up targeting computer loses lock) AND the targeted zone's damage < **1.0** (whole-mech
|
||
target checks zone 0 — so a wreck's dead zone can't re-lock). The hotbox stays visible without
|
||
lock (box = HotBoxVector, ring = Lock — separate signals). The binary also hangs the PNAMEx.bgf
|
||
player-name mesh on the 3D marker here (3D chain still deferred).
|
||
- **Simple-X mode** (PrimaryHudOn off, mask 0x20): the minimal reticle — a small green cross
|
||
(±0.02..0.08 arms) riding the aim translate (ctor @4689-4705 [T1]) — swapped for the full HUD
|
||
by Draw's state switch.
|
||
- **Target HOTBOX** (attr 0xB HotBoxVector) = a rectangle hugging the projected extents — x±4
|
||
around the hotbox point, +1/−11.5 vertical (K=2.8145 baked projection; the port uses the live
|
||
per-axis projection) — switching to the left/right edge ARROW past ±1.6 or behind.
|
||
- Reticle state Off/On + `PrimaryHudOn` (mask 0x20) picks full HUD vs the "simple X" list;
|
||
the aim group SetMatrix-translates by `Reticle::reticlePosition` (screen −1..+1 [T0]) every
|
||
frame — but **NOT by torso twist** (task #58 CORRECTION): the VIEW is torso-mounted (the eye
|
||
hangs off jointtorso → jointeye → siteeyepoint), so the crosshair stays SCREEN-CENTERED through
|
||
a twist — screen center IS the boresight; the twist reads on the bottom tape/compass/radar
|
||
wedge instead. The reticlePosition writer is un-exported (one xref binary-wide: the read-side
|
||
lookup part_014.c:5132); its coherent use is the FIXED-torso free-aim channel (mech+0x36c
|
||
[T4]). The old "translates to the torso boresight" wording here was the falsified
|
||
body-mounted-view model — see [[combat-damage]] Targeting for the full re-correction.
|
||
This recovery also CONFIRMS the HUD attr-table ids 4/5/6/8/0xA/0xB/0xC/0xD name↔use pairings
|
||
(hud.hpp had flagged them uncertain). Deferred: PNAME1-8.bgf 3D marker chain. (The canopy shell
|
||
is now authentic and shows by default — see [[cockpit-view]]; `BT_HIDE_COCKPIT=1` hides it.)
|
||
|
||
## Key Relationships
|
||
- Full history: `docs/GAUGE_COMPOSITE.md`; reticle recovery: `phases/phase-02-dpl2d-reticle.md`.
|
||
- Uses: [[attribute-pointer]] + [[reconstruction-gotchas]]; reads [[subsystems]] state.
|
||
- Renders on: [[pod-hardware]] MFD surfaces.
|