Files
BT411/context/gauges-hud.md
T
arcattackandClaude Opus 5 48d47ef806 #45 review pass: correct three claims the fix's own comments got wrong
An adversarial review of the change returned NO BLOCKERS but caught three
statements that were wrong or unverifiable.  All three are corrections to my own
comments/docs, not behaviour changes -- the verified binary is unchanged.

1. The phantom-kill note had the ORDERING backwards.  It claimed the owner's
   record "overwrites it on the next record -- the phantom stops being visible".
   The engine's event priorities run the other way: an inbound update record is
   posted at UpdateEventPriority == MaxEventPriority and drained by
   ExecuteBackgroundTask, while the rerouted score message sits at
   EntityManagerEventPriority == DefaultEventPriority and is popped later -- so
   the correction usually lands FIRST and the phantom AFTER it.  The artifact is
   BOUNDED, not masked: on the killer's pod only, that victim's KILLS reads +1
   until the next record, i.e. <= one 2s heartbeat instead of "until the victim
   next scores or dies".  Swept in btplayer.cpp, KD_SCOREBOARD_PLAN.md and
   RECONCILE.md.

2. The corpus figures were not reproducible, and were mis-tiered.  I cited
   "125 of 125 SCORE type=2 over 255 node-logs" and "0 of 8800 DMG rows" -- true
   when measured, but the corpus is append-only and this fix's OWN verification
   runs grew it from ~255 to 425 files, so nobody can re-derive them.  Replaced
   everywhere with invariant RATIOS that hold at any corpus size, re-measured
   here:
     * 0 of 18818 DMG rows are inst=R (damage is applied master-side only)
     * 439 of 439 DEATH inst=R rows read killer=0:0 killdmg=0.000, against
       0 of 225 inst=M rows (sole lastInflictingID writer: mech.cpp:746)
   Also re-tagged T1 -> T2: log-corpus field evidence is T2 per the CLAUDE.md
   tier table.  Lesson recorded in the banner: cite ratios, not counts, for a
   corpus your own rigs append to.

3. The recordLength guard's justification was wrong (and the plan's risk 5 with
   it).  A short legacy record does NOT desync the stream -- both sides advance
   by the WRITER's stamped recordLength and no reader asserts a length.  The real
   hazard is a read PAST THE ALLOCATION: an inbound update message lives in a
   per-event heap block sized from messageLength, so on a TRAILING legacy record
   the two new fields would be read off the end of it.

Plus: the record struct's own field comment still said "deathCount -- the DEATHS
column", the exact wrong-field belief #45 removed, sitting on the wire struct.

Filed the review's other finding on #60: a SECOND decomp export gap, index
jumping FUN_0049fe80 (ends 0x49ffc8) -> FUN_004a1232, 4714 unexported bytes
containing Mech::TakeDamageMessageHandler @0x4a0230 -- the producer of every
score and death message in the game.  Verified independently against
functions_index.tsv before posting.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 22:15:06 -05:00

59 KiB
Raw Blame History

id, title, status, source_sections, related_topics, key_terms, open_questions
id title status source_sections related_topics key_terms open_questions
gauges-hud Cockpit Gauges / MFD HUD system established docs/GAUGE_COMPOSITE.md (full map); CLAUDE.md §8
reconstruction-gotchas
decomp-reference
subsystems
pod-hardware
gauge
methodDescription
attribute-pointer
GaugeRenderer
MFD
PCC
MessageBoard LIVE 2026-07-12 (kill ticker); remaining: only strip 0 (kill) is produced -- survey the other btsmsgs.pcx strips for authentic producers
HUD attr table RESOLVED 2026-07-19 (Gitea #10 audit): re-dumped @5110b8 -- the table starts one record earlier (id 3 FlickerRate@0x1D8), every hud.hpp offset was shifted one slot; hud.hpp/CLASSMAP corrected, no behavioral impact (the reticle feeds via port globals). See GAUGE_COMPOSITE.md AUDIT finding B
SeekVoltageGraph RECONSTRUCTED 2026-07-19 (Gitea #11, was #10 finding A): full widget landed (see §SeekVoltageGraph below) -- ghosts gone steady-state (BT_PRESET_HOLD verification); remaining polish: a 1-frame transition artifact when a BT_SHOT lands on the exact page-switch frame (label BecameActive vs the graph's next rated Execute -- same lag class as the binary; self-heals next frame)
Secondary-view cycling RESOLVED 2026-07-19 (Gitea #6): the selector is the DISPLAY mode (CycleDisplayMode -> vtbl+0x4C override @4d1ae4), NOT CycleControlMode; desktop 'N' / pad RightThumb wired; pixel-verified dama->crit->heat
Upper-MFD PRESET pages RESOLVED 2026-07-19 (Gitea #9): SetPresetMode table @0051dbf0 re-decoded (little-endian -> ModeMFD bits 0-14), per-MFD pod button banks identified from the .CTL dump, desktop J/K/L cycle wired
Always-active msg-4 records IDENTIFIED 2026-07-20 (glass input audit): 0x2C = Reservoir InjectCoolant (the flush button), 0x2F/0x2E/0x2D/0x2B/0x2A/0x29 = Condenser1-6 MoveValve, 0x1A-0x1D = GeneratorA-D ToggleGeneratorOnOff (@0050fb90, unreconstructed); plus 0x13 = Mech DuckRequest (crouch), 0x28 = Mech BalanceCoolant, 0x12/0x14 = ThermalSight/Searchlight toggles -- see pod-hardware.md + docs/GLASS_COCKPIT.md 2026-07-20
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] AUDIT 2026-07-19 (Gitea #10): the full per-widget verdict table (45 rows: 34 CORRECT / 1 WRONG-filed / 6 AUTH-STATIC / 6 DEFERRED-FEED) is in docs/GAUGE_COMPOSITE.md §"AUDIT 2026-07-19" — the one WRONG was the missing SeekVoltageGraph reconstruction (the emitter/myomer eng pages' POWER curve + the authentic top-box eraser; caused the stale-ammo "SYSTEM 10 PPC" ghost). RESOLVED (Gitea #11): full reconstruction landed — §SeekVoltageGraph below. [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 (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]

Cockpit surround (the DEFAULT desktop layout, 2026-07-20) [T2]

Under BT_DEV_GAUGES, the DEFAULT is now the cockpit surround (L4VB16.cpp BTDrawCockpitPanels): the 3D world view CENTERED with the six gauge surfaces composited AROUND it in the SINGLE main window at ½ native scale (MFD 320×240, radar 240×320 portrait), plus clickable RIO button lamps — the pod-faithful arrangement (Coolant UL, Mfd2 upper-center, Comm/Hot Box UR, Mfd1/Mfd3 lower flanks, secondary/radar flush below). The pod monitors physically clip the eyeport, so the corner MFDs + top-center overlap the view edges. Mono MFDs are tinted phosphor green (radar keeps its amber palette); the world viewport is the centered view rect (BTApplyWorldViewport cockpit branch) so the reticle/HUD follow for free (dpl2d maps through the viewport), and gWindowAspect = the view rect's on-screen aspect (BTWorldAspectOf cockpit branch).

  • Layout = single source of truth: BTCockpitLayout / BTCockpitComputeLayout(canvasW,canvasH) in l4vb16.h/L4VB16.cpp, computed FROM the backbuffer (canvas) size — consumed by the window sizing (btl4main.cpp), the world viewport, the panel/button draw, the aspect, and the mouse hit-test. Constants: SCALE=0.5, OVL=44 (corner overlap), LAMP=16 (protruding lamp edge), REDCELL=64 (hidden hit depth), RAILW=26; canvas = view + (552, 548).
  • Buttons = the L4GLASSWIN geometry ×0.5 with the same address banks (Heat 0x2F, Mfd2 0x27, Comm 0x37, Mfd1 0x0F, Mfd3 0x07 red 8-btn; radar rails 0x10-0x15/0x18-0x1D + bottom {0x16,0x17,0x1F,0x1E} yellow; flight 0x38-0x3F/0x40-0x47 blue, labeled). Full rect = hit target; the surface draws OVER it so only the lamp edge shows (the PaintGlass painter trick). Mouse: main WndProc WM_L/RBUTTONBTCockpitMouseDown/Up (client→bb map, glass press/release/right- latch contract) → PadRIO::SetScreenButton (#ifdef BT_GLASS; dim/no-op in pod builds). Lamp brightness = BTLampBrightnessOf (shared inline in l4vb16.h) over PadRIO::GetLampState.
  • Env / precedence (resolved once in btl4main → gBTGaugeCockpit): BT_GLASS_PANELS=1 (Cyd's per-display windows) stands cockpit down > explicit BT_COCKPIT=1 > BT_DEV_GAUGES_WINDOW (separate window) / BT_DEV_GAUGES_DOCK (legacy inset) opt-out > cockpit default. BT_COCKPIT=0 forces the dock-bottom strip. -res W H = the WORLD VIEW size (canvas clamped to the work area). Green tint tunable via BT_COCKPIT_TINT=RRGGBB (default 0x27E8). Labels are a lazy GDI-baked MANAGED atlas (survives device reset). Renders in ALL builds; only the PadRIO click/lamp seam is BT_GLASS-gated. Full detail: docs/GAUGE_COMPOSITE.md.

MP gauge-window FREEZE = dangling bindings + permanent SEH disable (Gitea #12, 2026-07-19) [T2 log-convicted]

The live-MP "dev-gauges window froze entirely mid-session" incident (issue #12) is NOT a swap-chain/present bug: scratchpad/incident_2157/operator_1.log shows a burst of [gauge-fault] '<Widget>' Execute FAULTED -> gauge DISABLED (PilotList, HeadingPointer, MapDisplay, SectorDisplay, TwoPartBars, NumericDisplayScalar …) exactly while ALL THREE mechs were being RE-CREATED at mission launch (second [zonebuild]/[cyl] set at new addresses — the session had a pre-launch host drop/rejoin; a normal 2-player LAN launch on the same build did not re-stream and did not freeze). The gauge tree had been built (lazily) during the LOBBY phase against the FIRST-stream mechs; the re-stream freed them; every gauge Execute then AV'd on its dangling attribute-pointers and the BT_DEV_GAUGES SEH guard (Gauge::GuardedExecute, GAUGE.cpp:618) DISABLED each one — Disable(True) sets rate=0 PERMANENTLY, nothing re-enables or rebinds → the whole window static for the rest of the session (= symptoms 1+3). The AUTHENTIC engine flow handles mission transitions by Application::Shutdown → gaugeRenderer->Shutdown() → ShutdownImplementation → Remove(0) (all gauges deleted, tree lazily rebuilt next mission, APP.cpp:787/GAUGREND.cpp:3264) — the port's in-session re-stream path bypasses it. FIX LANDED (2026-07-19, awaiting human MP verify): BTL4GaugeRenderer::TearDownForViewpointRestream() (btl4grnd.cpp) performs the ENTITY-BOUND half of the engine ShutdownImplementation sequence, same order (RemoveAllAlarms → Remove(0) → entity-grid Clear), deliberately KEEPING warehouse/graphics-ports/interpreter/controls-L4Lamps (RemoveAllLamps mid-session would delete the CONTROLS-owned L4Lamps behind the buttonGroup's &lamp->automaticValue registrations — a dangling mapping; the mech-state "lamps" like AnimatedSubsystemLamp are Gauges and go with Remove(0)). Called from BTL4Application::MakeViewpointEntity before ConfigureForModel("Init", entity) whenever the viewpoint is a RE-make (that handler empirically runs once per stream — twice in the incident log, [ctrlmap] installing ×2) — the tree then rebuilds bound to the NEW mech. Log sentinel: [gauge] viewpoint re-stream: tearing the gauge tree down for rebuild. Residual exposure: Remove(0) flushes each gauge (Update(gaugeRate_A), GAUGREND.cpp:3465) before deleting it, so gauge CONNECTIONS may read the freed mech once more (plain reads; Execute stays SEH-guarded).

Per-weapon panel loop/generator lamps — 3 stacked databinding bugs (2026-07-21) [T2 human-playtest-verified]

Each SubsystemCluster (@004c8140) per-weapon MFD panel draws two image-strip lamps in the TEMP/STATUS area: cooling-loop number (btploop.pcc, frames OFF/1..6, AnimatedSubsystemLamp @004c70a4, fed by CoolingLoopConnectionBTCoolingLoopFrame) and generator letter (btpbus.pcc, frames OFF/A..D, AnimatedSourceLamp @004c7160, fed by PowerSourceConnection). These are the "4 A / 1 B / 5 D" boxes in the reference. Both rendered COMPLETELY BLANK; three independent bugs stacked (all fixed, commit e634709):

  1. Color drop — both lamp ctors dropped the bg/fg color params the binary passes (bg=0xff, fg=0, same as the sibling temp bar) and hardcoded 0,0, so OneOfSeveral::Execute did SetColor(0)+DrawBitMapOpaque(0) = black-on-black. Restored 0xff,0.
  2. shadow-field trap (gotcha #2)AnimatedSubsystemLamp/AnimatedSourceLamp each RE-DECLARED int selected; while already inheriting it from OneOfSeveral (@0xAC). The connection's &selected bound the derived shadow copy; OneOfSeveral::Execute read the base @0xAC (always 0) → every lamp stuck on frame 0 ("OFF"). Removing the redeclarations (so selected resolves to the inherited member) fixed the loop NUMBER.
  3. Attribute-table shift (gotcha #8 / attribute-pointer) — the generator lamp resolved its source via ResolveLink(AttributePointerOf(subsystem,"InputVoltage")), but the BT_DEV_GAUGES audio attribute rows shifted the chained attribute ids so AttributePointerOf no longer landed on voltageSource@0x1D0 → the link resolved to 0 (OFF) even though the master PoweredSubsystem ctor DID bind voltageSource to its Generator ([busattach] showed bound=1). Fix: new BTPowerSourceFrame(subsystem) bridge (powersub.cpp) reads the NAMED member via PoweredSubsystem::ResolveVoltageSource() and returns Generator::generatorNumber (@0x1E0), bypassing the attribute table — same pattern as BTCoolingLoopFrame. The lamp caller now passes subsystem_in (not the InputVoltage attribute slot). Lesson: gauge value feeds must read named members through a complete-type bridge, NOT AttributePointerOf+ResolveLink — the attribute table's chained ids are unstable under the audio rows. The eng-page generator-voltage bar + MyomerCluster seek-voltage graph (GeneratorVoltageConnection, evolt.pcc, btl4gau2.cpp:806/1750) were on the same dead path and are now converted the same way -> BTGeneratorVoltage(subsystem) reads Generator::MeasuredVoltage() (outputVoltage@0x1DC); [voltfeed]-verified (Myomers -> 10000V from GeneratorD). Still on the OLD path but NOT yet converted (different attribute, direct read, not confirmed broken): the GeneratorCluster + EnergyWeaponCluster bars that read AttributePointerOf(subsystem, "OutputVoltage") directly (btl4gau2.cpp:968, ~1920). Diagnostics retained (env BT_LOOP_LOG): [loopfeed] (BTCoolingLoopFrame) + [busfeed] (BTPowerSourceFrame) + [voltfeed] (BTGeneratorVoltage) print each widget's resolved value + source. HUMAN-VERIFIED live 2026-07-21 (pod build, solo ARENA1 cockpit): the loop number + generator letter boxes render correctly on the weapon panels.

Ballistic ammo count stencils out of the fire-ready dot (2026-07-21) [T1 decomp + render-verified]

The base-page missile/round count (ammoCountA, NumericDisplayInteger @0x114/this[0x45]) sits on the weapon's fire-ready "dot" — the cluster image (clusterImage @0xCC/this[0x33]) blitted by WeaponCluster::DrawWarningLamp (@004c932c) in on-colour 0xff (solid green) / off-colour 0 (black/absent), toggled by warningState when percentDone crosses the warn threshold. The count must stay legible against it, so BallisticWeaponCluster overrides DrawWarningLamp (@004c9b50): it chains the base (draws the dot) then swaps the numeric's colours via NumericDisplayInteger:: SetColors(bg,fg) (@00470ec8 → inner NumericDisplay @0x90):

  • dot ABSENT (on==0) → SetColors(0, 0xff) == green digits on black
  • dot PRESENT (on!=0) → SetColors(0xff, 0) == BLACK digits cut out of the solid green dot (the same black-on-green stencil as the loop/generator squares). SetColors ForceUpdate()s the numeric so it repaints over the freshly drawn dot on the next child-execute pass. The engine's NumericDisplay::Draw blit is SetColor(fg) + DrawBitMapOpaque(bg,…) — glyph=fg, surround=bg (opaque), so only the colour swap (not transparency) makes the cut-out. Port bug (fixed): only the base non-virtual DrawWarningLamp existed, so the count stayed green-on-black and clashed as a dark box in the green dot. Fix: made DrawWarningLamp virtual + added the ballistic override. Energy weapons (EnergyWeaponCluster, no ammo count) use the base only — unaffected.

TEMP/STATUS bar (HorizTwoPartBar) tiles a striped pattern from x=0 (2026-07-21) [T1 decomp + render-verified]

The per-weapon TEMP/STATUS bar is a HorizTwoPartBar (@004c4170, Execute @004c4340). It renders three zones along X from the interned tileImage + two colours (fillColor, backgroundColor):

  • [0, warnPix)SetColor(fillColor) + DrawTiledBitmap(tileImage) (@004c2ff8): the striped/ dotted TILE pattern (green dots). The DrawTiledBitmap color arg is ignored — the tile is blitted with its own pixels (vtbl+0x58) under the SetColor(fillColor) foreground.
  • [warnPix, valPix)fillColor solid (the green over-degrade fill; only when value > low; binary keeps the current colour, still fillColor from zone 1 — no SetColor).
  • [valPix, width)backgroundColor solid: the unfilled remainder (BLACK). COLOUR MAP (critical): the two colour params land at [this+0xA0]=fillColor (0xff green) and [this+0xA4]=backgroundColor (0 black) — caller @004c8269 pushes 0xff then 0. Green is the tile
  • over-degrade fill; black is only the remainder. The port first shipped these SWAPPED (zone 3 = fillColor) so the whole [valPix,width] remainder — most of the bar when cold (valPix small) — rendered solid green instead of black. Fixed 2026-07-21 (commit 4fbc911). warnPix = round(width*low/high), valPix = round(width*value/high) (value=CurrentTemperature, low=DegradationTemperature, high=FailureTemperature). Port bug (fixed): the Execute had been rewritten with DrawFilledRectangle starting at warnPix and NEVER used tileImage — so the bar read as a solid block "starting in the middle", not striped. The sibling VertTwoPartBar (@004c4724, eng-page vertical temp bars) was already correct (uses DrawTiledBitmap). Fix mirrored the tiled three-zone render into HorizTwoPartBar::Execute; matches the DOSBox reference (hatched fill block on the left + dotted tick scale). NOTE the horizontal/vertical bars use DIFFERENT zone colours (Horiz: zone2=bg, zone3=fill, 2 colours; Vert: zone2=fill, zone3=extra, 3 colours) — do not assume they are pure mirrors.

The secondary screen's THREE views (Damage / Critical / Heat) — mode-gated [T0/T1/T2]

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). The schematic shows the pilot's OWN mech only — there is no target-damage readout in the cockpit.

The selector is the DISPLAY mode, not the control mode (Gitea #6, RESOLVED 2026-07-19) [T1→T2]. The L4 vtable @0051e440 pins the slots: +0x4C = @004d1ae4 — dispatched by CycleDisplayModeMessageHandler (FUN_004afcac) with the new displayMode (0/1/2) — is the NotifyOfDisplayModeChange override that clears bits 18-20 and sets the mask from the table @0051dbe4 {0x40000,0x80000,0x100000}. (The old "SetControlMode @004d1ae4 switches the secondary VIEW" claim was the mislabel that kept the port's copy a never-called non-virtual.) +0x48 = @004d1acc — dispatched by CycleControlModeMessageHandler (FUN_004afbe0) — just forwards to the base RET no-op @004b048c: a BAS/MID/ADV control-mode change NEVER touches the secondary view (empirically confirmed: M cycles the CONTROL MODE gauge lamp, mask bits 18-20 unchanged, schematic stays on ARMOR DAMAGE). Authentic pod inputs (streamed type-6 .CTL EventMappings, dumped live via BT_CTRLMAP_LOG): secondary-panel button 0x15 → msg 0x15 CycleDisplayMode (the manual-p13 "'Mech status Info center", bottom left of the secondary screen: Armor/Critical/Heat Damage Schematic cycle), button 0x18 → msg 0x14 CycleControlMode (the manual-p6 mode button, top right), buttons 0x10/0x11 → ZoomIn/ZoomOut 0x12/0x13 (the map zoom ± pair). The DOS keyboard fallbacks (Keypress 0x13d/0x13e = extended F3/F4) never fire under the WinTesla VK map (VK_F3=0x72 collides with 'r', VK_F4=0x73 with 's' — the 'p'/VK_F1 collision class), so the desktop was PINNED on Damage. Port wiring (mirrors the M/ModeCycle pattern): 'N' / pad RightThumb → action DisplayCycle → gBTDisplayCycle → CycleDisplayModeNow() (mechmppr.cpp; the same body the pod button message drives). Pixel-verified live (docked gauges + BT_SHOT): ARMOR DAMAGE silhouette → CRITICAL DAMAGE subsystem list → HEAT DAMAGE colored list, mask 0x450421→0x490421→0x510421. Diags: BT_MODE_LOG, BT_VIEWCYCLE_TEST=<frame>, BT_MODECYCLE_TEST=<frame>.

The upper-MFD PRESET pages — 3 MFDs × 5 pages (Gitea #9, RESOLVED 2026-07-19) [T0/T1/T2]

The three preset-able MFDs are Mfd1 (lower left) / Mfd2 (upper center) / Mfd3 (lower right), each a PAIR of bit-planes on one physical monitor: the base Quad plane (Mfd1/2/3, masks 0x0100/0x0400/0x1000, btquad.pcx) and the engineering-page plane (Eng1/2/3, masks 0x0200/0x0800/0x2000, bteng.pcx). Mode bits (BTL4MODE.HPP [T0], bits 0-14): ModeMFD{1,2,3}{Quad,Eng1-4} = 1<<(group*5+item)fully disjoint from Mapping/NonMapping (15/16), Intercom (17) and the #6 Secondary* trio (18-20); ModeInitial puts all three MFDs on Quad. What the pages show: Quad = up to four vehicleSubSystems cluster mini-panels (the quadrants, geometry table @0x51bf34); Eng item i = the FULL-SCREEN engineering detail of the subsystem streamed onto aux screen group*4+i (sub+0x1dc; prepEngr screens 1-12: "SYSTEM NN"

  • per-class label cells + the cluster's eng child — GENERATOR SELECT A-D, POWER graph, COOLING loop, DAMAGE (MJ), ammo count …). Unpopulated screens are authored-empty per mech (Blackhawk: 9 of 12 — scr 1/2/4=PPC/Streak6/ERMed, 5-8=Sensors/Myomers/ERMed/ERMed, 9/10=Streak6/PPC; screens 3, 11, 12 empty).

SetPresetMode(group,item) @004d1b24 swaps the page: table @0051dbf0 = 15 {clear,set} pairs — set = the page's ModeMFD bit, clear = the group's other four (item 0) or all five (items 1-4). ⚠ The old reconstruction had transcribed the little-endian set column as BIG-endian dwords (0x01→0x01000000 …), so a preset press set a garbage high bit — and for group 1 items 3-4 / group 2 items 0-2 STOMPED the live NonMapping/Intercom/Secondary* bits — while the page bits never moved (the "presets unwired" defect). Fixed against section_dump.txt:72901-72908; the "does group 2 duplicate #6's secondary views?" concern is resolved: no — group 2 = MFD3, bits 10-14.

Authentic dispatch (streamed type-19 "L4" .CTL, 121 records dumped via BT_CTRLMAP_LOG): every MFD has its own 8-button RIO bank whose meanings are MODE-MASK-gated — Mfd1 = buttons 0x08-0x0F (AuxLowerLeft), Mfd2 = 0x20-0x27 (AuxUpperCenter), Mfd3 = 0x00-0x07 (AuxLowerRight). On a Quad page the bank's buttons DIRECT-SELECT the populated Eng pages (mapper EventMappings → MechRIOMapper messages Aux1Eng1-4=0x4-0x7, Aux2Eng1-4=0x9-0xC, Aux3Eng1-4=0xE-0x11 → SetPresetMode; no button is streamed for an empty screen); on an Eng page one button returns to Quad (Aux1/2/3Quad = 0x3/0x8/0xD) and the rest remap to the SHOWN subsystem (per-subsystem msgs: 0x4-0x7 = SelectGeneratorA-D, 0x8 = ToggleGeneratorMode, 0x9 = ConfigureMappables, 0x3/0xb = unjam/eject-class functions). The MechRIOMapper keyboard cases in @004d1bf0 mirror it as three key rows: 1-4=MFD1 Eng1-4/5=Quad, a s d f/g, z x c v/b — mostly claimed by the port's WASD bindings, hence dead on desktop. Port wiring: the streamed records install and fire on desktop (btinput passes the LIVE manager mask on button press, so the NUMPAD profile's 0x20-0x27 keys page MFD2 authentically); the default WASD profile adds J/K/L → actions Mfd1/2/3Cycle → gBTPresetCycle → CyclePresetModeNow(group) (btl4mppr.cpp; a port cycle-key shim — 24 mode-dependent pod buttons don't fit a keyboard — that visits exactly the pod-reachable set: Quad + populated Eng pages; the body is the authentic SetPresetMode).

Dev-composite change: BTDrawGaugeSurfaces (L4VB16.cpp) now draws the Eng1-3 planes into their sibling's cell and SKIPS any mono plane whose port channel is currently BlankColor — honoring the mode-driven reconfigure (RemapGraphicsPort) so each dev cell shows the ACTIVE page, like the pod monitor. This SUPERSEDED the 2026-07-12 "frozen-dial" scaffold (GAUGREND.cpp force-activated all 15 page bits under BT_DEV_GAUGES — removed; it made every eng screen paint over the shared Eng plane, pinning it on the highest screen). Pixel-verified live (BT_PRESET_TEST): all three MFDs page Quad → eng details → back to Quad in lockstep with the [mode] mask log; N/M un-regressed. Diags: BT_MODE_LOG ([mode] preset lines), BT_PRESET_TEST=<frame>, BT_CTRLMAP_LOG.

pilotList (Comm KILLS/DEATHS) row semantics + the 1 [T1/T2]

FIXED + RIG-VERIFIED (Gitea #43, 2026-07-24; awaiting live playtest). Field report ("never lists anyone in MP") reproduced on a 2-node rig, THREE stacked causes, all fixed:

  1. Raw-flag miscount: BuildPilotArray's count loop used the binary's raw entry+0x29 & 0x40 read on our compiled objects (databinding trap) → garbage → roster latched at 1 row. DECODED: +0x29 bit 0x40 = bit 14 of simulationFlags @+0x28 = Player:: NonScoringPlayerFlag (= Entity::NextBit, PLAYER.h:392) — the roster authentically lists SCORING players (flag cleared when the mech links its player, mech.cpp:688, all nodes). Both loops now use Player::IsScoringPlayer(); also closes a pilotIDs[1] heap overrun the undercount exposed.
  2. Latch race: the binary's build-once latch assumes 1995 synchronous pod loads; over async relay TCP the faster node ticks InterpretControls before the peer's replicants arrive (rig-proven: staggered node logged roster 1 → then peer arrival). [T3 accommodation, demand-latch precedent]: rebuild whenever the scoring CENSUS changes — also un-dangles a departed peer's freed Player from the roster. Forensic: always-on [score] pilot roster built: N scoring pilot(s) per census change.
  3. Invisible rows: LookupPlayerNameBitmap was a NULL stub + numerals are authentic signedBlankedZerosFormat (zeros draw blank) → an unselected 0/0 row rendered as a black box: invisible even with a correct roster. Wired BTPilotNameBitmap (btplayer.cpp bridge: compiled playerBitmapIndexMission::GetSmallNameBitmap, the same 64×16 egg rasters the radar labels use; replaces the binary's raw pilot+0x1e0 key read). Rig-verified: Aeolus (local, center box) + Boreas (remote, roster strip) both render with callsigns. NOTE: rows need the egg to carry name bitmaps (operator-console eggs do; bare rig eggs like MP2.EGG don't → authentic cache-miss blank box). 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. SUPERSEDED -- that "RESOLVED" claim was FALSE (corrected 2026-07-25, Gitea #45). The observed-tally design never ran: BTPlayerCountObservedDeath's only call site is replicant-gated but sits INSIDE Mech::UpdateDeathState's once-per-death transition, which a replicant never enters (it takes the mode-9 early return) -- and it could not have worked anyway, because a replicant victim carries no attribution at all (0 of 18818 DMG rows in the corpus are inst=R, which is why 439 of 439 DEATH inst=R rows read killer=0:0 against 0 of 225 inst=M rows). What actually happened: BOTH counters lived only on the OWNING pod, so every REMOTE row read 0/0 all mission -- KILLS because Entity::Dispatch reroutes a replicant's ScoreMessage to the master (ENTITY.cpp:244-251), so ++killCount lands on the killer's own machine; DEATHS because the VehicleDead(-1) handler runs on the victim's own master. Neither counter rode an update record. [T2 — stated as ratios above; an earlier draft's "125 of 125 over 255 node-logs" was true when measured but is not reproducible, because every rig run appends to the same corpus.] FIXED 2026-07-25 [T2, rig-verified]: DEATHS now reads the binary's OWN +0x280 column (BTPlayer::deathTally, our offset 0x274 -- previously declared pad_0x280 and "dead"), not Player::deathCount (which is the respawn-handshake identity, seeded -2 -- exactly what the old display clamp was hiding). Both counters replicate owner->replicant via a BTPlayer__UpdateRecord extension, so every pod shows the same numbers. See docs/KD_SCOREBOARD_PLAN.md (addendum) + docs/RECONCILE.md.

Launcher-panel recharge dial — CORRECTION 2026-07-19: it IS live (slot 17 @004b9c9c) [T1]

The weapon panels' SegmentArc270 tick ring reads MechWeapon::rechargeLevel (+0x320). The old "authentically STATIC on projectile weapons" claim was WRONG — the writer census missed the anonymous vtable slot 17 (vtbl+0x44) body @004b9c9c: rechargeLevel = (rechargeRate@0x3DC recoil@0x3E8) / rechargeRate (capstone disasm, issue #12; Ghidra never emitted it). The recovered ProjectileWeaponSimulation @004bbd04 calls it every frame in the Loading(3) and unavailable(7) alarm cases — so the launcher dial authentically ANIMATES 0→1 through each reload. (The census's four other writers stand: ctor @004b99a8, stream init @004b8fec, ResetToInitialState @004b96ec, and Emitter::ComputeOutputVoltage @004ba738 — slot 17 is the Emitter override of the SAME slot; @004b9c9c is the MechWeapon/projectile base body.) The launcher panel's other live indicators: AMMO DIGITS (AmmoBin::ammoCount via the complete-type bridges), jam/fire lamps, eject wipe.

The ballistic panel's FIRE icon = the BAY-FIRE annunciator — FIXED (2026-07-25, #47) [T1 decomp / T2 live]

BallisticWeaponCluster::Execute @004c9a38 (raw decomp): this[0x3b] = *(bin+0x18C)cookOffArmed, the field the btefire.pcc TwoState watches; while armed, this[0x3e] = (Now().ticks bin->cookOffTime@0x190) / ticksPerSecond — the cook-off countdown seconds (negative → 0 at detonation) shown by the NumericDisplayScalarTwoState beside it. The old reconstruction misread bin+0x18C as "the reload state" and bridged the icon to BTAmmoBinFeeding — so it blinked with every feed cycle and never lit on a bay fire (RajelAran: "it doesn't"). Now driven by BTAmmoBinCookOffArmed/CookOffTime complete-type bridges (ammobin.cpp). The member names firing/reloading/reloadSeconds in btl4gau2 are historic; semantically bayFireArmed / latch / secondsToDetonation.

The ENG-button ATTENTION FLASH (jam / bay fire) — BUILT (2026-07-25, #47) [T1 chain, T2 live]

The pod's authored annunciator: on a subsystem condition (TechStatusType — Destroyed 0 / Damaged 1 / CoolantLeaking 2 / Overheating 3 / AmmoBurning 4 / Jammed 5 / BadPower 6) the affected subsystem's OWN bezel buttons flash. Chain: MechTech::TechnicalAssistance (@004ad33c, per-frame) edge-scans every monitored subsystem's GetStatusFlags() bitmask (now VIRTUAL — binary slot 12) → Start/StopEntityAlarm to the gauge renderer (port: direct implementation calls) → GaugeAlarmManager::Activate(alarmModel=83 'mechalrm') → the real BTL4GaugeAlarmManager:: ReadGaugeAlarmStreamItem (btl4galm.cpp, from @004cc2fc) maps {condition, lampCode} items via the @0051cf1c vocabulary to bezel lamps (gotoEngineering = the subsystem's quad-select button; engEject/engBusMode/engCooling = the eng-page bank; per-condenser/per-generator specials) → Lamp::SetAlertState (a COUNTER — stacked alarms) → L4Lamp flushes flashFast RIO states (0x37/0x13) → the pod's physical lamps AND the glass panels (PadRIO IS the rioPointer in glass builds). The shipped 'mechalrm' table: Jammed/AmmoBurning → gotoEngineering + engEject (purge/ unjam); Destroyed → +engCooling+engBusMode; CoolantLeaking → +engCooling; BadPower → +engBusMode. Verified live: bay fire → lamp 0xD (the LRM's select button) flashes 0x37 + engEject 0xB; clears on detonation/purge. Diagnostics: BT_LAMP_LOG[techstat]/[galarm]/[lamp]. Details + the four load-bearing fixes en route: open-questions + decomp-reference §GaugeAlarm.

ConfigMapGauge (the weapon panel's trigger-config joystick) — LIVE via LinkToEntity (2026-07-21)

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. CORRECTION — the old "authentically DORMANT" claim [task #6] was WRONG. @004c6ee0 is NOT an uncalled SetColor(int): it is the virtual GaugeBase::LinkToEntity override (vtbl slot 9, +0x24 — verified against the binary vtable @0051a1b8 and the T0 GaugeBase virtual roster, GAUGE.h), and @0x94 is the linkedEntity, not a colour. The engine broadcasts LinkToEntity(viewpointEntity) to every gauge when the viewpoint binds (APP.cpp:1277 → GaugeRenderer::LinkToEntity GAUGREND.cpp:3011), arming the Execute gate — the joystick DOES render in the shipped game (matches the DOSBox reference: solid circle = the mapped trigger, hollow = unmapped). The "no caller" analysis failed twice: it looked for direct calls to a virtual, and its slot math used the wrong vtable copy. Port fix: renamed SetColor→LinkToEntity / color→linkedEntity, deleted the BT_CONFIGMAP dev enable (no longer needed — the authentic path lights it). Sampler = LBE4ControlsManager:: buttonGroup[btn].GetMapState (table DAT_00518eb4 PE-recovered); the regroup MECHANISM (ConfigureMappables/ChooseButton, task #6) was always live.

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). A heat-FAILED weapon goes DARK with no lamp — authentic (2026-07-23, closes the #30 remainder) [T1 + T2 field-captured]: BallisticWeaponCluster::Execute @004c9a38 reads exactly one weapon-state check — jammed = (weaponAlarm@0x364 == 5) for the BTEJAM lamp; its only other reads are the bin's cook-off lamp/countdown (bin+0x18C/+0x190). Weapon state 7 (unavailable: FailureHeat / destroyed / mech-disabled / bay dry) has NO presentation — dial at zero, everything dark. Field capture: a solo pilot's right SRM6 tripped FailureHeat (gate1: failHeat=1 heatLvl=2) and "just stopped working, no indication" — reported as a bug; it's the binary's behavior. Recovery: an EJECT tap revives a FailureHeat 7 while rounds remain (decomp-reference §message-tables EjectAmmo). The jam lamp lights ONLY for a true jam (5). 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 COOLANT FLUSH is LIVE (Gitea #7, 2026-07-19) [T2]: InjectCoolant (id 4, the Reservoir handler table @0x50e680, handler @4aee70) — HOLD flushes reservoir coolant through the loops (InjectCoolant @4aefa4, set%-biased via each sink's flowScale); the coolant vertBar C (Reservoir/CoolantMass = coolantLevel@0x12C, l4gauge.cfg:4526) drops live as the tank drains (BLH: 6.0 → 0 in ~0.6 s held ≈ the manual's 3-4 punches); the bluish FLUSH.PFX condensation cloud (psfx 19) spawns on the ReservoirState 0→1 edge. Desktop: 'H' HELD (action Flush). Full chain + the two ctor decode corrections it surfaced: subsystems §coolant FLUSH. Diags: BT_FLUSH_LOG, BT_FLUSH_TEST=<frame>. The MessageBoard feed needs StatusMessagePool (a NULL stub) + the per-player status queue. [T2]

SeekVoltageGraph — the eng-page POWER graph AND the top-box eraser (Gitea #11, 2026-07-19) [T1→T2]

The emitter/PPC + myomer engineering pages' "POWER" box widget (btl4gau2.cpp/.hpp; ctor @004c6798, BecameActive @004c6920, Execute @004c6934; a CLUSTER-CHILD — built by EnergyWeaponCluster @004c93b0 / MyomerCluster @004c8df4, not a config keyword). Fully reconstructed from the capstone disasm (Ghidra dropped every x87 arg; tools/disas2.py):

  • View/geometry: localView = the page's top data box (0x97,0x80)-(0x17d,0x13b) — exactly 230×187, the plot scales; view coords are view-relative (GRAPH2D.h origin = areaWithinPort.bottomLeft). Ctor sets SetOperation(Xor) — ticks/cursor erase by redraw.
  • The eraser role: BecameActive poisons the cached sample (previousVoltage@0xAC = 9999); the next Execute's change-test (sample the response at 12000 V) then runs the CLEAR @004c6be4 (SetOperation(Replace), color 0, filled 1000×1000 clipped to the box) before replotting — THIS is what erases the sibling pages' stale pixels on the shared Eng bit-plane (the #10 ghosts). Every topBox=0 PrepEngr page owns a graph; the box erase design is coherent.
  • Plot math (recovered): polyline v = 0..12000 step 1200 (_DAT_004c6bdc/_be0): x = Round(response(v)·230), y = Round(v·(1/12000)·187) — the 80-bit consts @004c6bd0/@004c6d74 are EXACTLY 1/12000. Ticks (@004c6c6c): per gear i in [*min..*max], the current gear draws a full L (axis→point→axis), others 10-px axis stubs; XOR pair moves the highlight. Cursor (@004c6c30, emitter pages only): 8×8 XOR square at the LIVE voltage (the ctor's "OutputVoltage" attr pointer). Destroyed subsystem (simulationState==1 via the BTSubsystemDestroyed bridge, powersub.cpp): centred edestryd.pcc once; revive calls own BecameActive (vtbl+0xC, slot 3 of PTR_0051a1fc — verified by vtable dump).
  • The sampler == subsystem vtbl+0x3C (slot 15), reached via the BTSeekVoltageSample dispatch bridge (emitter.cpp → myomers.cpp; databinding rule): Emitter @004bb42c = sqrt(SeekPower(v)/2.0e8) with SeekPower @004bb3f4 = damageFraction·v²·0.5·energyCoefficient (a LINE in v — the emitter graph is authentically straight); Myomers @004b8f94 = sqrt(AvailableOutput(v)·3.6/350) (steep near-vertical curve at BLH values). FUN_004dd138 = sqrt (part_015.c:4026) — the old "fabs/fp-magnitude" reading was wrong; Myomers' old best-effort name "GetSpeedReading" renamed SeekVoltageResponse.
  • The attributes: Myomers already published all 4 Seek* (@0x320-0x330). Emitter's AUTHENTIC table was recovered (binary @0x511dd4, ids 0x1D-0x25) and published (emitter.cpp): LaserOn@0x418, LaserScale@0x42C, LaserRotation@0x41C, Current/Recommended/Min/Max SeekVoltageIndex@0x3F0-0x3FC, SeekVoltage@0x400, OutputVoltage@0x414 = currentLevel (RAW volts — the cursor feed). ⚠ The MechWeapon 0x1D "OutputVoltage" PORT ALIAS (→ rechargeLevel 0..1) was RETIRED: the binary MechWeapon table ends at 0x1C, and AttributeIndexSet::Find walks lowest-id-first, so the alias SHADOWED the authentic Emitter row. Emitter renames: @0x3F8 minSeekVoltageIndex / @0x3FC maxSeekVoltageIndex (were seekStepCounter/seekVoltageCount guesses; the attr table proves the identities). PPC chains Emitter::GetAttributeIndex().
  • Verified live (BLH, autofire): page-cycling + BT_PRESET_HOLD steady-state shots — both #10 repro pairs ghost-free (SYSTEM 09 Streak → SYSTEM 10 PPC held clean; SYSTEM 02 → SYSTEM 04 clean; SYSTEM 05 → 06 Myomers clean); curves draw with moving cursors (charge cycling); exactly ONE replot per activation ([seek] log). Diags: BT_SEEK_LOG ([seek] BecameActive/replot), BT_PRESET_HOLD=<n> (freeze the #9 preset cycler after n pulses — steady-state pixel verification; the 120-frame cycle phase-locks with BT_SHOT's 90).

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]). VERDICT (Gitea #4, 2026-07-20): the "range slides in/out crazily while walking" report is AUTHENTIC behavior, not a bug [T2 measured]. Per-frame BT_RANGE_LOG traces (mech4.cpp, with an independent Möller-Trumbore cross-check BTGroundRayHitExact in btvisgnd.cpp) on scripted walks: ARENA1 garage cluster — every frame a real structure-face pick, true range legitimately hopping 40↔600 m at silhouette edges, the caret in motion 74% of frames / ~24 direction reversals per 10 s; CAVERN butte field — 806 instant >50 m jumps, 776 butte↔1200-default boundary crossings, 106/139 s windows swinging >300 m (max 1143 m), caret reversing ~54×/10 s. Every value tracks real geometry; the 500 m/s slide (authentic) then never rests. Elevation was NOT the driver (arena floor is flat y=0) — depth discontinuities are. Cyd's "ray falls through geometry" was checked: 0 fall-through frames on arena, 6/8400 (0.07%, single-frame silhouette grazes) on cavern. Bounded infidelities noted for follow-up, NOT the reported symptom: (a) butte collision (stepped YCyl tiers + cone, buttee_c.sld) is NARROWER than the sculpted buttee.bgf rock — measured a 0.27 m graze past the r=14.6 tier while visibly inside rock (terrain-march tier backstops it, error a few meters at point-blank); (b) the cavern's butteeu upper butte sections (buttee.bgf re-instanced at y=50, VideoModel-only, NO solid stream) are pick-transparent — aiming steeply up at a rock tower's top half reads through it; (c) boot transient: before the aim camera is valid, the max-range fallback designates from uninitialized ray floats → shown overshoots (~3400) and slides down for the first ~7 s (cosmetically hidden by the 1200 caret peg). Traces: scratchpad i4__walk.log; issue #4 comment has the summary. ⚠ 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 in the PORT (the port sim never called the slot-17 updater @004b9c9c — see the §Launcher-panel CORRECTION), so missile/AC pips were permanently lit and never blinked on fire. Now reads the AUTHENTIC attr 0x1C = the weaponAlarm StateIndicator level (WeaponStatePtrGaugeAlarm54::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 recharge DIAL is driven by slot 17 @004b9c9c — §Launcher-panel CORRECTION.)
  • 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 SAME lock-change block drives the PNAMEx.bgf TARGET NAME PLATE — the target's callsign floating under the crosshair. RECONSTRUCTED + RIG-VERIFIED 2026-07-24 (was "3D chain deferred"); awaiting live playtest. Mechanism, all [T1] from the Execute disassembly (reference/decomp/reticle_execute_004cdcf0.disasm.txt) — the ONLY two functions that touch the plate fields are the ctor @004cc40c and Execute @004cdcf0:
    • Mesh table = playerNameObject[] at this+0x2e8 (PNAME1 lands at +0x2ec), loaded part_014.c:4430-4444; the array is 12 slots (MAX_PLAYER_NAMES), only 8 populated.
    • Selection (@004cec98): target_mech+0x190 (the owning BTPlayer) → player+0x1e0 (playerBitmapIndex) indexes that table 1-based (the binary biases the base instead of decrementing; no bounds check). Same 1-based egg bitmapindex the Comm pilot list uses.
    • Placement (@004cdede): re-placed EVERY frame the aim moves — identity → scale 0.12 uniform → translate (K·reticlePos.x, K·reticlePos.y 0.08, 1.0), K = the x87 long double @0x4cee64 = 0.35714286 = 1/2.8 (the same 2.8 projection constant the hotbox uses). So the plate tracks the aim point, 0.08 eye-space below it — NOT pinned under screen centre.
    • Visibility = the LOCK attribute (the Scalar* at this+0x184, cached +0x188; @004cebf9-@004cec47 branches to the hide path when it reads 0) AND the target having an owning player. Reticle Off or simple-X (PrimaryHudOn clear) also hides it (@004cdd75/@004cddc1).
    • Art: the plate is a bare 1.0×0.25 quad (4:1) whose material bmap:name12_mtl etc. (content/VIDEO/MAT/BMAP.BMF) UV-addresses one shared 128×64 bitslice texmap — 4 materials × 2 v-halves = the 8 player slots. content/VIDEO/TEX/BMAP.BSL ships baked "PLAYER 1..8" fallback art (decoded 2026-07-24), but the pod OVERWRITES those texels at runtime with the egg's 1bpp CALLSIGN rasters — the original source is commented out at engine/MUNGA_L4/L4VIDEO.cpp:5682-5710 (FlushBitSliceTexturedpl_TexmapTexels2D(texmap, storage, 128, 64, 4), with the tell-tale warning "textures for player names not defined"). So the authentic plate shows the operator-set callsign, with "PLAYER n" only as the no-bitmap fallback. Material colour is a neutral (0.5,0.5,0.5) — the plate is grey-white, NOT phosphor green like the reticle glyphs.
    • PORT: same placement expressed in reticle units (÷K ⇒ 0.224 below the aim point, plate 0.336×0.084), drawn as the target player's egg callsign texture (dpl2d_DrawTexturedRect + BTGetPlayerNameTexture + BTReticleTargetPlate). Two PNAME consumers exist — this reticle plate (PNAME only), and FUN_00454a70 (class key 0x46) = the engine's CameraShipHUDRenderable, the camera/broadcast seat's followed-name banner + ranking window, which uses PNAME and PLACE1-8 as sorted [ordinal][callsign] rows and is already live as screen quads (see multiplayer camera seat). Its 1995 source is likewise commented out in L4VIDRND.cpp:2562-2876 — the literal dpl_* calls, a Rosetta stone for the binary's node API.
  • 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; the OFFSETS were re-based 2026-07-19 — the table @5110b8 starts with id 3 FlickerRate@0x1D8, so Rotation=0x1DC…CompassHeading=0x214 Scalar; hud.hpp/ CLASSMAP corrected, names/ids unchanged). The PNAME1-8.bgf target NAME PLATE is RECONSTRUCTED (2026-07-24, §Lock ring above) — no longer deferred. (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

#48 MFD ARTIFACTS -- ROOT-CAUSED + FIXED (2026-07-25) [T2 live-convicted]

The night-3 "stray blocks + misaligned lamps" on Heat/Mfd/sec: uninitialized translation-table entries leaking pixels into other displays' bit-planes. L4GraphicsPort::translationTable[256] was never ctor-initialized, and BuildSecondaryTranslation fills only the entries its BitWrangler reaches -- 2^numberOfBits: 64 for the sec plane (mask 0x3F), 4 for the overlay (0xC0) -- so entries above that stayed heap garbage. Every draw resolves color through this table, and the pixmap path indexes it with RAW PIXEL VALUES (0..255): the 480x640 radar background carries pixel index 217 -> its garbage entry's high bits (e.g. 0xFF00 = ALL EIGHT MFD planes) were written into the shared 640x480 buffer at that position -- invisible on the culprit page (the in-plane low bits happened dark), visible as bright fragments at the same coordinates on EVERY OTHER display. Convicted empirically with the new BT_PLANE_AUDIT write-site trap (L4VB16 primitives: a draw whose color carries bits outside its port's plane mask logs primitive/port/position/mask; Or/Xor ignore the mask entirely and And clears foreign planes -- all audited): 15-30 leaks/minute in a quiet solo session, PORT 'sec' ... idx 217 entry=0xffffff00 mask=0x3f, at exactly the artifact positions in the operator's screenshot. FIX: zero the table in the ctor + cycle the in-plane pattern across entries [2^bits..255] (high-index art degrades to its index-mod-2^bits colour IN-PLANE, can never leak). The 1995 binary ships the SAME 64-entry fill and relied on 6-bit art discipline -- garbage is not a preservable behaviour, so the cycle-fill is a guarded PORT deviation. Post-fix: 0 leaks over 60s on the same probe; sim3 3-pod regression clean. The tripwires are DEFAULT-ON in every build (BT_PLANE_AUDIT=0 opts out): the plane-leak trap, plus an out-of-bounds draw-start trap in buildDestPointer (release Verify is a no-op, so a wild start previously wrote silently -- now logged [plane] OOB and clamped). First detection of either class writes a GLITCH matchlog record, so if the artifact has a third cause we have not seen, the evidence lands in the round logs the operator already collects -- no env setup needed on playtest machines. (Coverage note: an in-buffer EXTENT overrun from a valid start -- a blit walking past the right/bottom edge into later rows of the same plane -- is NOT trapped; per-pixel walk checks are too hot. The plane trap still catches it whenever the color leaks planes.) SECOND real defect fixed en route: sessions configure the CAMERA seat first (cameraInit -- which builds the MISSION-REVIEW context: configure(0, sec, 0, 0x00FF, native, rgb, mrpal.pcc), a DirectColor context whose table holds full RGB565 values), and the swap to the mech re-configured WITHOUT tearing that tree down (the btl4app latch treated the mech build as "first") -- the orphaned review gauges kept executing through stale DirectColor ports. BTL4GaugeRenderer::ConfigureForModel now tears down before every rebuild (ConfigureForModel made virtual in L4GREND.h). Note the review-screen PlayerStatus gauges also read the compiled player at RAW BINARY OFFSETS (+0x1FC vehicle / +0x1C8 score / +0x1C4 alive-dead -- the databinding trap, dormant until review runs); logged in open-questions. Why "it started with the comms feature": the #43 wave registered the new gauge classes (PlayerStatus/pilotList/...) with the config interpreter, which let more of the authored page furniture parse + draw than before -- the leaking high-index pixmaps rode in with that. [T3 for this correlation detail; the leak itself and its fix are T2 live-verified.]