Replicates the RP412 cockpit line into BT411's own architecture -- porting the geometry and keeping our renderers, so BT_SHOT single-frame verification stays intact. MODE RESOLVER. Where the secondary displays go was decided in TWO places with duplicated precedence, and the boot banner read NEITHER -- it announced "per-display cockpit windows" for every glass boot, surround included. The split had also broken BT_COCKPIT=0: documented as the dock-bottom opt-out, the profile block converted it to BT_GLASS_PANELS=1, so the docked strip was UNREACHABLE under the glass profile. One resolver now, consumed by the banner, the pad-panel decision and the window sizing; BT_GLASS_PANELS is explicit-only; dock/window modes auto-raise BT_PAD_PANEL so the button field always has a home. L4RIOBANK -- one button geometry. Both renderers carried their own copy and had drifted: an MFD button was 156x138 reaching under the glass in the exploded window and a 76x24 sliver entirely OUTSIDE the glass in the surround. One module owns it now, both are consumers, placement stays per-renderer. The pod's under-glass rule (RP412 L4MFDVIEW): reach half the glass in behind the display, leave a lamp strip clearing the edge, paint buttons first and imagery over -- so the lamp reads as a bar and practically the whole display is the press target. The strip scales off the display's SHORT axis (the map is portrait) with a readable floor. Retired L4GLASSWIN's three local placers and seven layout constants. LAMP FLASH DECODE was wrong [T1]. BTLampBrightnessOf returned max(state1, state2) and blanked on the alternate phase; RIO::LampState (L4RIO.h [T0]) says solid shows state 1 and flashing ALTERNATES the two. Agrees only when one state is Off -- true for the Panic lamp, which is why it survived -- but L4LAMP.cpp:252 commands flashFast + state1Dim + state2Bright, a dim->bright pulse that rendered as a hard bright->off blink. Three copies existed (l4vb16.h, L4GLASSWIN, L4PADPANEL), all three wrong; the two locals now forward to the one fixed inline. THE COCKPIT SCALES. The fixed canvas was stretched into whatever the client area was, so a window dragged to a different shape squashed the instruments (the projection was aspect-corrected in task #20; the panels never were). Now one uniform scale, centred, leftover black. D3D9 applies it as a Present destination rect, which DISCARD forbids -- so the WINDOWED swap effect becomes COPY when the surround is up and multisampling is off. The click mapping had to follow (mapping against the full client drifts the hit test off every button by the bar width), as did the world aspect (under a uniform scale it is the view rect's own). -fit / -windowed-fullscreen: borderless over the monitor. - ordering trap: the first WM_SIZE beats the device, so a -fit boot logged aspect=3.14 and applied it on frame 1. The letterbox INTENT is decided in btl4main; L4VIDEO only confirms or withdraws it. PLAYER-TUNABLE DISPLAYS. BT_MFD_SCALE (+ _UL/_UC/_UR/_LL/_LR), BT_RADAR_SCALE, BT_RADAR_POS (CENTER/LEFT/RIGHT/MIDLEFT/MIDRIGHT). The surround BANDS derive from the resolved sizes -- that is why the sizes could not stay constants: the band a display hangs in has to grow with it or the canvas clips it. 100% reproduces the historical L276 R276 T223 B336 exactly. A corner map goes flush to the CANVAS edge and the lower MFD slides beside it (measuring off the view edge overlapped them by 232px). MAP LEGEND GRID -- measured, not inherited. scratchpad/measurelegend.py over a native capture: top 3, cell 102, pitch 107 of 640. RP412's map is 13 + 6x102 @ 105 -- same cell height, different top and pitch, so its numbers do NOT transfer. Our old even division had the pitch right by luck and sat 3px high of the labels. environ.ini. It was read ~300 lines into WinMain, AFTER the platform-profile block had run its getenv()s -- so every setting the profile reads was silently ignored FROM THE FILE and only worked as a real env var. It also putenv()'d comments verbatim. Now loaded immediately after the first-breath line, comments skipped, the real environment WINS over the file, and a fully documented default is written on first run (the bindings.txt convention: untracked, so extract-over-top never clobbers a player's settings). VERIFICATION HARNESS (new, reusable): BT_RIOBANK_LOG=1 dumps every bank; checkbank.py proves no address is SHADOWED (an address whose rect is covered by earlier buttons is dead however big it looks -- the overlapping under-glass banks make that a live hazard); clickbank.py posts a real click at every button centre. Verified: 72/72 placed, 0 shadowed, 72/72 dispatched in BOTH modes, after a resize, at 150%/135%, and at 75%+BOTTOMRIGHT; wide/tall drags and -fit undistorted on a 3440x1440; exploded/dock/pod/dev un-regressed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
60 KiB
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 |
|
|
|
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)
- The config
content/GAUGE/l4gauge.cfg— a text file the engineGaugeInterpreterparses (BuildConfigurationFile → Initialize → GetProcedureBody → ParsePrimitive). Each mech's gauge tree is built from the labelGetGameModel()+"Init"(e.g.bhk1Init), which invokes shared blocks (MechInit,Secondary1) thatconfigurethe ports and call gauge primitives. [T1] - Widgets — each
keyword(...)in the config resolves to aMethodDescriptioninBTL4MethodDescription[](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] - 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 throughParseAttribute → FindSubsystem (stricmp GetName()) → GetAttributePointer. A class PUBLISHES an attribute via a<Name>AttributeIDenum +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::Executeaborts. /FORCEtrap — a prose-only slot AVs on first call.- Databinding trap — never raw-read owner offsets; use a bridge (
BTGetSubsystemAuxScreen, …). - ReconStream no-op — use
DEBUG_STREAMfor logs, notDebugStream. - 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)inl4vb16.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 =
L4RIOBANK(2026-07-26: the ONE geometry, shared with the exploded windows — see glass-cockpit §ONE button-bank geometry) at half scale, same address banks (Heat 0x2F, Mfd2 0x27, Comm 0x37, Mfd1 0x0F, Mfd3 0x07 red 8-btn; radar columns 0x10-0x15/0x18-0x1D + foot {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 strip shows (the PaintGlass painter trick). ⚠ Before that date the surround's MFD lamps sat ENTIRELY outside the glass on a 24px band — a 76×24 target where the exploded window gave 156×138; both now reach half the glass. Mouse: main WndProcWM_L/RBUTTON→BTCockpitMouseDown/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) overPadRIO::GetLampState. - Env / precedence (resolved ONCE in btl4main →
glassLayout→gBTGaugeCockpit; the full table lives in glass-cockpit §Layout modes):BT_GLASS_PANELS≠0 (the per-display windows) stands cockpit down >BT_DEV_GAUGES_WINDOW(separate window) >BT_DEV_GAUGES_DOCK(legacy inset) >BT_COCKPIT=0(also the dock strip) > cockpit surround default. ⚠ Before 2026-07-26BT_COCKPIT=0actually landed on the per-display windows — the dock strip was unreachable under glass.-res W H= the WORLD VIEW size (canvas clamped to the work area). Green tint tunable viaBT_COCKPIT_TINT=RRGGBB(default0x27E8). 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 CoolingLoopConnection→BTCoolingLoopFrame) 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):
- 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, soOneOfSeveral::ExecutedidSetColor(0)+DrawBitMapOpaque(0)= black-on-black. Restored0xff,0. - shadow-field trap (gotcha #2) —
AnimatedSubsystemLamp/AnimatedSourceLampeach RE-DECLAREDint selected;while already inheriting it fromOneOfSeveral(@0xAC). The connection's&selectedbound the derived shadow copy;OneOfSeveral::Executeread the base @0xAC (always 0) → every lamp stuck on frame 0 ("OFF"). Removing the redeclarations (soselectedresolves to the inherited member) fixed the loop NUMBER. - Attribute-table shift (gotcha #8 / attribute-pointer) — the generator lamp resolved its
source via
ResolveLink(AttributePointerOf(subsystem,"InputVoltage")), but theBT_DEV_GAUGESaudio attribute rows shifted the chained attribute ids soAttributePointerOfno longer landed onvoltageSource@0x1D0→ the link resolved to 0 (OFF) even though the masterPoweredSubsystemctor DID bindvoltageSourceto its Generator ([busattach]showed bound=1). Fix: newBTPowerSourceFrame(subsystem)bridge (powersub.cpp) reads the NAMED member viaPoweredSubsystem::ResolveVoltageSource()and returnsGenerator::generatorNumber(@0x1E0), bypassing the attribute table — same pattern asBTCoolingLoopFrame. The lamp caller now passessubsystem_in(not theInputVoltageattribute slot). Lesson: gauge value feeds must read named members through a complete-type bridge, NOTAttributePointerOf+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)readsGenerator::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 readAttributePointerOf(subsystem, "OutputVoltage")directly (btl4gau2.cpp:968, ~1920). Diagnostics retained (envBT_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).SetColorsForceUpdate()s the numeric so it repaints over the freshly drawn dot on the next child-execute pass. The engine'sNumericDisplay::Drawblit isSetColor(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-virtualDrawWarningLampexisted, so the count stayed green-on-black and clashed as a dark box in the green dot. Fix: madeDrawWarningLampvirtual + 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). TheDrawTiledBitmapcolorarg is ignored — the tile is blitted with its own pixels (vtbl+0x58) under the SetColor(fillColor) foreground.[warnPix, valPix)—fillColorsolid (the green over-degrade fill; only whenvalue > low; binary keeps the current colour, stillfillColorfrom zone 1 — no SetColor).[valPix, width)—backgroundColorsolid: 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 pushes0xffthen0. 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 (commit4fbc911).warnPix = round(width*low/high),valPix = round(width*value/high)(value=CurrentTemperature, low=DegradationTemperature, high=FailureTemperature). Port bug (fixed): the Execute had been rewritten withDrawFilledRectanglestarting atwarnPixand NEVER usedtileImage— so the bar read as a solid block "starting in the middle", not striped. The siblingVertTwoPartBar(@004c4724, eng-page vertical temp bars) was already correct (usesDrawTiledBitmap). Fix mirrored the tiled three-zone render intoHorizTwoPartBar::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:
- Raw-flag miscount:
BuildPilotArray's count loop used the binary's rawentry+0x29 & 0x40read on our compiled objects (databinding trap) → garbage → roster latched at 1 row. DECODED: +0x29 bit 0x40 = bit 14 ofsimulationFlags@+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 usePlayer::IsScoringPlayer(); also closes apilotIDs[1]heap overrun the undercount exposed. - 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. - Invisible rows:
LookupPlayerNameBitmapwas a NULL stub + numerals are authenticsignedBlankedZerosFormat(zeros draw blank) → an unselected 0/0 row rendered as a black box: invisible even with a correct roster. WiredBTPilotNameBitmap(btplayer.cpp bridge: compiledplayerBitmapIndex→Mission::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 INSIDEMech::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 18818DMGrows in the corpus areinst=R, which is why 439 of 439DEATH inst=Rrows readkiller=0:0against 0 of 225inst=Mrows). What actually happened: BOTH counters lived only on the OWNING pod, so every REMOTE row read 0/0 all mission -- KILLS becauseEntity::Dispatchreroutes a replicant's ScoreMessage to the master (ENTITY.cpp:244-251), so++killCountlands on the killer's own machine; DEATHS because theVehicleDead(-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+0x280column (BTPlayer::deathTally, our offset 0x274 -- previously declaredpad_0x280and "dead"), notPlayer::deathCount(which is the respawn-handshake identity, seeded -2 -- exactly what the old display clamp was hiding). Both counters replicate owner->replicant via aBTPlayer__UpdateRecordextension, so every pod shows the same numbers. Seedocs/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::Findwalks 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 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]).
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_LOGtraces (mech4.cpp, with an independent Möller-Trumbore cross-checkBTGroundRayHitExactin 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'sbutteeuupper 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 →shownovershoots (~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, 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 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 readingrechargeLevel ≥ 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 (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 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[]atthis+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 eggbitmapindexthe 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*atthis+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_mtletc. (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.BSLships 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 atengine/MUNGA_L4/L4VIDEO.cpp:5682-5710(FlushBitSliceTexture→dpl_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), andFUN_00454a70(class key 0x46) = the engine'sCameraShipHUDRenderable, 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 inL4VIDRND.cpp:2562-2876— the literaldpl_*calls, a Rosetta stone for the binary's node API.
- Mesh table =
- 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 byReticle::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=1hides 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.
#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.]