Files
BT411/context/gauges-hud.md
0bcba26213 KB: land the EXPERIENCE-LEVELS decode from glass-cockpit (Cyd)
Brings Cyd's experience-level / simulation-mode research onto master (the
KB-decode portion of glass-cockpit 4e01e83; the bundled BT410 source
manifest is left for the full glass-cockpit merge).

- context/experience-levels.md (NEW): the egg per-pilot 'experience' field
  (novice/standard/veteran/expert) is the pod's SIMULATION-FIDELITY tier,
  decoded end-to-end. FUN_004c0bc8 reads btMission->experienceLevel(+0xe4)
  and fans it into the +0x25c/+0x260/+0x26c/+0x270/+0x274 flag block:
  +0x25c 'sim live' (novice lockout: jams/searchlight/powersub), +0x260 the
  HEAT-MODEL master switch (veteran+expert; FUN_004ad7d4), +0x274 raw level
  (FUN_004ac9c8 = ==0 novice predicate; the valve/advanced-cockpit lockout).
  4.0->4.10 drift: the viewscreen hunting-aid gate is GONE in 4.10 (HUD reads
  none of these flags), and movement heat is veteran+expert not expert-only.
- btplayer.cpp/.hpp: CORRECTED comments/labels -- the +0x25c..+0x274 block is
  seeded from btMission experienceLevel/advancedDamageOn, NOT the scenario
  role's returnFromDeath; the 'roleClassIndex/showKills' names are scoring-era
  mislabels. Code logic UNCHANGED (comment-only): the ctor still reads
  scenarioRole (STAND-IN, defaults 2 = veteran) pending the wiring task.
- Corrections swept into gauges-hud.md (ROOKIE->novice lockout),
  open-questions.md (player+0x260/0x274 semantics now PINNED),
  pod-hardware.md, subsystems.md, decomp-reference.md; CLAUDE.md router row.

No code-logic change -> no rebuild needed (comment + markdown only). The
runtime wiring (seed from BTMission::ExperienceLevel()) remains TODO.

Co-Authored-By: Cyd <cyd@falloutshelterarcade.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 23:20:51 -05:00

246 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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]
## Radar view-wedge tracks the torso twist (Gitea issue #1 — FIXED 2026-07-17) [T2]
The SECTOR radar's view cone (`MapDisplay::DrawViewWedge` @004c2484, btl4rdr.cpp) reads
`viewHorizontalRotation@0x31C`, fed by a per-frame `GaugeConnectionDirectOf<Radian>` the ctor
wires from the mech's Torso. The two helpers were NULL stubs, so the connection was never
created and the wedge sat at heading 0 (the reported "cone does not track the twist").
Reconstructed from the binary: `FindSubObject` (FUN_0041f98c) = a SUBSYSTEM-ROSTER walk
(count @+0x124 / array @+0x128) matching the streamed name (sub+0xd4) case-insensitively
(FUN_004d4b58 = tolower-strcmp) — the "Torso" sub-object IS the roster Torso subsystem;
`GetHorizontalRotation` = torso+0x1D8 = `Torso::currentTwist` (layout-locked), reached via the
existing task-#56 bridge `BTGetTorsoTwistAddr` (Radian ≡ {Scalar angle}, exact reinterpret).
VERIFIED live (MadCat, Standard mode Q/E): wedge rot tracks twist 0→−2.21 rad in lockstep, and
the SEMANTIC test passes — body turned away, torso twisted back onto the enemy → reticle goes
green AND the wedge points at the enemy's blip while the scope stays body-fixed. ⚠ Test with a
TWISTING mech: the Blackhawk's torso is FIXED (limits ±0.01°) — its wedge authentically never
moves. Diag: `BT_RADAR_LOG` ([radar] ctor probe + 1 Hz [radar-wedge] rot trace). [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 == 0` (2026-07-18 correction: +0x274 = the egg EXPERIENCE level,
so this is the NOVICE lockout, not a "ROOKIE role" — see [[experience-levels]]; task #12's
`BTPlayerRoleLocksAdvanced` bridge; bring-up seeding = 2 ≈ veteran = 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 01200 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, botY3·tickMajor0.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 WeaponState**
@0x350 == 2) 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. **FIX 2026-07-18 [T2]:** the pip's "loaded" was a port
approximation reading `rechargeLevel ≥ 1` — correct for emitters (charge-driven) but
**statically 1.0 on projectile weapons** (MissileLauncher/autocannon never write it — see
§Launcher-panel above), so missile/AC pips were **permanently lit and never blinked on fire**.
Now reads the AUTHENTIC attr 0x1C = the weaponAlarm StateIndicator level (`WeaponStatePtr`
`GaugeAlarm54::LevelPtr`), compared `== stateConst2` (the const the binary itself stores in
AddWeapon). The state cycles Loaded(2)↔Firing(0)/Loading(3)/Jammed(5) for BOTH families, so a
missile pip now momentarily drops when fired, exactly like the emitter's. Verified live
(Blackhawk, `BT_AUTOFIRE=1 BT_AF_MISSILE=1`): both SRM6 pips toggle 2↔0/5 on each salvo. The
authentically-static `rechargeLevel` recharge DIAL (§Launcher-panel) is untouched.
- **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.)
Player CALLSIGN labels (kill/damage feed, radar/target tags, score display) are 1bpp name
BITMAPS from the egg, not text — format, renderers, and the operator-console generator:
[[multiplayer]] §OPERATOR-SET CALLSIGNS.
## 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.