The weapon panel's btjoy trigger-config display (ConfigMapGauge) never rendered because the reconstruction labelled @004c6ee0 as "SetColor(int)" and, finding no caller, concluded the gauge was authentically dormant (task #6), gating it behind a BT_CONFIGMAP dev env. The user challenged this (the PROGRAM/TRIGGER CONFIG button implies the display visualizes your config). Re-verification found the old analysis failed twice: the "no caller" search looked for direct calls to a VIRTUAL, and its slot math used the wrong vtable copy. The real ConfigMapGauge vtable @0051a1b8, matched against the T0 GaugeBase virtual roster (GAUGE.h), pins @004c6ee0 at slot 9 (+0x24) == GaugeBase::LinkToEntity, and @0x94 is the linkedEntity -- the Execute gate is "not yet linked", not "disabled". The engine broadcasts LinkToEntity(viewpointEntity) to every gauge at viewpoint bind (APP.cpp:1277 -> GaugeRenderer::LinkToEntity GAUGREND.cpp:3011), arming the gate -- the joystick DOES render in the shipped game, showing per-trigger mapping state (solid = mapped, matching the DOSBox reference). Port fix: SetColor(int color) -> LinkToEntity(Entity*), color -> linkedEntity, BT_CONFIGMAP dev enable deleted (the authentic path lights it). Render-verified with no env override: the joystick + mapped-trigger lamp draw on weapon panels. KB swept per the correction mandate: gauges-hud.md, decomp-reference.md, open-questions.md, GAUGE_COMPOSITE.md, VEHICLE_SUBSYSTEMS.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
545 lines
46 KiB
Markdown
545 lines
46 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** = the L4GLASSWIN geometry ×0.5 with the same address banks (Heat 0x2F, Mfd2 0x27,
|
||
Comm 0x37, Mfd1 0x0F, Mfd3 0x07 red 8-btn; radar rails 0x10-0x15/0x18-0x1D + bottom
|
||
{0x16,0x17,0x1F,0x1E} yellow; flight 0x38-0x3F/0x40-0x47 blue, labeled). Full rect = hit target;
|
||
the surface draws OVER it so only the lamp edge shows (the PaintGlass painter trick). Mouse:
|
||
main WndProc `WM_L/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 → `gBTGaugeCockpit`): `BT_GLASS_PANELS=1`
|
||
(Cyd's per-display windows) stands cockpit down > explicit `BT_COCKPIT=1` > `BT_DEV_GAUGES_WINDOW`
|
||
(separate window) / `BT_DEV_GAUGES_DOCK` (legacy inset) opt-out > **cockpit default**.
|
||
`BT_COCKPIT=0` forces the dock-bottom strip. `-res W H` = the WORLD VIEW size (canvas clamped to
|
||
the work area). Green tint tunable via `BT_COCKPIT_TINT=RRGGBB` (default `0x27E8`). Labels are a
|
||
lazy GDI-baked MANAGED atlas (survives device reset). Renders in ALL builds; only the PadRIO
|
||
click/lamp seam is BT_GLASS-gated. Full detail: `docs/GAUGE_COMPOSITE.md`.
|
||
|
||
## MP gauge-window FREEZE = dangling bindings + permanent SEH disable (Gitea #12, 2026-07-19) [T2 log-convicted]
|
||
The live-MP "dev-gauges window froze entirely mid-session" incident (issue #12) is NOT a
|
||
swap-chain/present bug: `scratchpad/incident_2157/operator_1.log` shows a burst of
|
||
`[gauge-fault] '<Widget>' Execute FAULTED -> gauge DISABLED` (PilotList, HeadingPointer,
|
||
MapDisplay, SectorDisplay, TwoPartBars, NumericDisplayScalar …) exactly while ALL THREE mechs
|
||
were being RE-CREATED at mission launch (second `[zonebuild]`/`[cyl]` set at new addresses —
|
||
the session had a pre-launch host drop/rejoin; a normal 2-player LAN launch on the same build
|
||
did not re-stream and did not freeze). The gauge tree had been built (lazily) during the
|
||
LOBBY phase against the FIRST-stream mechs; the re-stream freed them; every gauge Execute then
|
||
AV'd on its dangling [[attribute-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]
|
||
One ROW PER PILOT in the mission (2-player MP = 2 rows — not duplicate displays). KILLS =
|
||
`killCount` (the victim's ScoreMessageHandler credits the shooter cross-player, works for both
|
||
rows); DEATHS = `Player::deathCount`: engine-inits to **−2** (PLAYER.cpp:759), the LOCAL
|
||
vehicle-acquire branch zeroes it (btplayer.cpp:1118), then VehicleDead(-1) ++s per death. A
|
||
REMOTE player's Player object never runs the local acquire → −2 +1 spawn increment = **−1
|
||
locked**. **RESOLVED 2026-07-12 [T2]:** deaths now tally per node from LOCALLY OBSERVED events
|
||
(the same model as the cross-pod KILLS credit) — a replicant's once-per-death transition calls
|
||
`BTPlayerCountObservedDeath` on its owning player's local copy (replicant-gated: the master's
|
||
own VehicleDead path counts its node), and `BTPilotDeaths` clamps the −2/−1 pre-acquire seed
|
||
to 0 for display. Own row counts correctly as before.
|
||
|
||
## Launcher-panel recharge dial — 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.
|
||
|
||
## 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).
|
||
**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 binary also hangs the PNAMEx.bgf
|
||
player-name mesh on the 3D marker here (3D chain still deferred).
|
||
- **Simple-X mode** (PrimaryHudOn off, mask 0x20): the minimal reticle — a small green cross
|
||
(±0.02..0.08 arms) riding the aim translate (ctor @4689-4705 [T1]) — swapped for the full HUD
|
||
by Draw's state switch.
|
||
- **Target HOTBOX** (attr 0xB HotBoxVector) = a rectangle hugging the projected extents — x±4
|
||
around the hotbox point, +1/−11.5 vertical (K=2.8145 baked projection; the port uses the live
|
||
per-axis projection) — switching to the left/right edge ARROW past ±1.6 or behind.
|
||
- Reticle state Off/On + `PrimaryHudOn` (mask 0x20) picks full HUD vs the "simple X" list;
|
||
the aim group SetMatrix-translates by `Reticle::reticlePosition` (screen −1..+1 [T0]) every
|
||
frame — but **NOT by torso twist** (task #58 CORRECTION): the VIEW is torso-mounted (the eye
|
||
hangs off jointtorso → jointeye → siteeyepoint), so the crosshair stays SCREEN-CENTERED through
|
||
a twist — screen center IS the boresight; the twist reads on the bottom tape/compass/radar
|
||
wedge instead. The reticlePosition writer is un-exported (one xref binary-wide: the read-side
|
||
lookup part_014.c:5132); its coherent use is the FIXED-torso free-aim channel (mech+0x36c
|
||
[T4]). The old "translates to the torso boresight" wording here was the falsified
|
||
body-mounted-view model — see [[combat-damage]] Targeting for the full re-correction.
|
||
This recovery also CONFIRMS the HUD attr-table ids 4/5/6/8/0xA/0xB/0xC/0xD name↔use pairings
|
||
(hud.hpp had flagged them uncertain; 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). Deferred: PNAME1-8.bgf 3D marker chain. (The canopy shell
|
||
is now authentic and shows by default — see [[cockpit-view]]; `BT_HIDE_COCKPIT=1` hides it.)
|
||
|
||
Player CALLSIGN labels (kill/damage feed, radar/target tags, score display) are 1bpp name
|
||
BITMAPS from the egg, not text — format, renderers, and the operator-console generator:
|
||
[[multiplayer]] §OPERATOR-SET CALLSIGNS.
|
||
|
||
## Key Relationships
|
||
- Full history: `docs/GAUGE_COMPOSITE.md`; reticle recovery: `phases/phase-02-dpl2d-reticle.md`.
|
||
- Uses: [[attribute-pointer]] + [[reconstruction-gotchas]]; reads [[subsystems]] state.
|
||
- Renders on: [[pod-hardware]] MFD surfaces.
|