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>
712 lines
60 KiB
Markdown
712 lines
60 KiB
Markdown
---
|
||
id: gauges-hud
|
||
title: "Cockpit Gauges / MFD HUD system"
|
||
status: established
|
||
source_sections: "docs/GAUGE_COMPOSITE.md (full map); CLAUDE.md §8"
|
||
related_topics: [reconstruction-gotchas, decomp-reference, subsystems, pod-hardware]
|
||
key_terms: [gauge, methodDescription, attribute-pointer, GaugeRenderer, MFD, PCC]
|
||
open_questions:
|
||
- "MessageBoard LIVE 2026-07-12 (kill ticker); remaining: only strip 0 (kill) is produced -- survey the other btsmsgs.pcx strips for authentic producers"
|
||
- "HUD 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** = `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 WndProc `WM_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) over `PadRIO::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-26 `BT_COCKPIT=0` actually 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 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-pointer]]s 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):
|
||
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 `playerBitmapIndex` → `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 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 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_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, 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 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 (`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[]` 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` (`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), 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
|
||
- 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.]
|