Files
BT411/docs/GLASS_COCKPIT.md
T
CydandClaude Opus 5 5f88a4eeb2 The keyboard becomes the button board (RP412 bindings port) + a controls page
Ported RP412's bindings design and made it the DEFAULT: the letter and number
rows are the MFD button banks laid out WHERE THEY SIT ON THE PANEL, flight
moves to the numpad so the board stays free, F1-F12 are the map's two columns,
and G/B stay unbound as the physical gap between the lower clusters.  Expressed
in BT's OWN grammar (slew/deflect/set, the Turn channel) -- bindings.txt is a
documented compatibility surface and was not touched.

Coverage: 61 of 72 addresses on the keyboard, and the 11 absent ones are
exactly those the pod never wired (0x16/0x17/0x1E/0x1F column gaps, 0x38-0x3E
intercom/door).  All 72 stay clickable on the panel.

It lands on BT's addresses unreasonably well: 1-4 + QWER are the ENTIRE coolant
system (Condensers 1-6, flush, balance), F6/F7 the display and control-mode
cycles, F9-F12 Generators A-D, F4 the crouch button reconstructed earlier
today.

THE DELIBERATE COST.  A bound key is removed from the authentic 1995
typed-hotkey channel so it cannot double-dispatch, and this board binds nearly
everything -- so 5 (Quad page), z (Eng1), t/y/u/i/o (pilot select) and +/-
(target zoom) are given up.  Unbind a key to get its 1995 meaning back.
Documented in the file header, the markdown and the page.

NEW RULE: A BINDINGS ROW WINS OVER A BUILT-IN CONVENIENCE KEY
(PadRIO::KeyHasBinding).  V and J/K/L are board buttons now, and those polls
read GetAsyncKeyState directly -- without this they would have fired BOTH the
button and the built-in.  View toggle lives on BACKTICK alone.

CONTROLS.MAP rewritten to mirror the board so glass/pod/dev still feel
identical (the 2026-07-21 settlement): 90 bindings, 0 parse complaints.

HAT LABELS CORRECTED [T1].  INPUT_PATH_AUDIT flagged 0x41-0x44 and was right.
Settled from the streamed mapping (BT_CTRLMAP_LOG): elem 66 -> subsys 17
attrID 14, i.e. 0x42 is the TORSO subsystem, not a look; 0x44/0x43/0x41 are the
mapper's LookLeft/LookRight/LookBehind (attrID 10/11/12).  The .RES has no
"TORSO CENTER" string -- its names are LookBehind/Down/Forward/Left/Right -- so
the audit's wording was loose but its substance correct.  Swept L4GLASSWIN,
L4PADPANEL and L4VB16.

docs/CONTROLS.html: interactive keyboard (hover a key for its address and
meaning, MFD clusters outlined), pad diagram, panel map, the under-glass
press-target figure, radar placement thumbnails and the environ.ini table --
modelled on RP412's page, rewritten for BT's addresses and hazards.  mkdist
wraps the fragment in a doctype/charset shell and ships it beside CONTROLS.txt.

Verified: bindings.txt regenerates and loads (74 keys, 0 errors); CONTROLS.MAP
90 bindings, 0 errors; 72/72 panel addresses still dispatch; surround /
exploded / dock / pod / dev boot and simulate with zero faults.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 03:23:27 -05:00

735 lines
51 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# GLASS_COCKPIT — running ledger (desktop developer layer)
Detailed running log for the `glass-cockpit` branch work: PadRIO, on-screen panel/plasma,
miniconsole, Steam transport/lobby. Curated digest: `context/glass-cockpit.md` (read that
first); this file carries the step-by-step detail, evidence, and verification transcripts.
---
## 2026-07-17 — Step 1: branch + compile gates + scaffolding
- Branch `glass-cockpit` created from master @ `e2c21c4`.
- `CMakeLists.txt`: `option(BT_GLASS OFF)` + `option(BT_STEAM OFF)`; `BT_STEAM` without
`BT_GLASS` is a configure-time FATAL_ERROR; gates append to `BT_DEFS` (flows to all three
targets); `STATUS` line prints gate state at configure. Gated `target_sources` blocks will be
added per-TU as they land (CMake would reject nonexistent source files now).
- `.gitignore`: `build-pod/`, `build-steam/`, `content/bindings.txt`, `content/frontend.egg`,
`content/steam_appid.txt`.
- Verification (this step): defaults configure prints `BT_GLASS=OFF BT_STEAM=OFF` and builds
(purity build); `-DBT_GLASS=ON` and ON/ON configures print correct gate state; the
BT_STEAM-without-BT_GLASS negative test FATAL_ERRORs. Full 3-config BUILD matrix becomes
meaningful from step 2 (first gated code) and is re-run there.
## 2026-07-17 — Step 2a: the .CTL positional-attribute off-by-one — CONFIRMED and FIXED (ungated)
**The bug [T2, live-verified]:** the streamed `.CTL` "L4" mapping resource carries the 1995
binary's positional attribute ids (stick=3, throttle=4, ...), resolved at install time by
`Simulation::GetAttributePointer(id)``AttributeIndexSet::Find(id)` = `attributeIndex[id-1]`
(positional). `MechControlsMapper` chained its enum on `Subsystem::NextAttributeID == 2`, but
the binary's chain reaches 3 (one base attribute we don't publish) — so EVERY streamed mapper
record landed one member late.
**Evidence (new `BT_CTRLMAP_LOG=1` diagnostic in `LBE4ControlsManager::CreateStreamedMappings`,
`engine/MUNGA_L4/L4CTRL.cpp` — permanent, env-gated, default off).** Before the fix
(`build-pod` Debug, `-egg DEV.EGG`, dev platform):
```
rec 0 subsys 0 group 1(Joy) elem 0 attrID 3 -> +0xe4 (= throttlePosition: WRONG, stick intended)
rec 1 subsys 0 group 0(Scalar) elem 0 attrID 4 -> +0xe8 (= pedalsPosition: WRONG, throttle intended)
rec 2 subsys 0 group 3(Button) elem 63 attrID 6 -> +0xf0 (= speedDemand: WRONG, reverse intended)
rec 4-6 attrID 10/11/12 -> lookRight/lookBehind/lookDown (one late; LookLeft/Right/Behind intended)
```
Spacing attr3→attr4 was 4 bytes (Scalar) where stick→throttle must be 8 (ControlsJoystick).
The weapons chain (attrID 19 → +0x31c TriggerState) was already correct — mechweap was
pad-aligned in task #16 (gotcha #11). Torso (subsys 17) also aligned (MechSubsystem publishes
binary id 2). ONLY the mapper chain (subsys 0) was off. On the dev box the keyboard INPUT
BRIDGE (writes the mapper members directly, after the engine push) masked it; on a REAL POD the
serial RIO throttle scalar would have driven `pedalsPosition` and the stick x/y would have
driven throttle+pedals — the Phase-8 landmine prior art predicted.
**The fix (ungated, pod-correctness — user-approved 2026-07-17):** `mechmppr.hpp` pins the enum
to the binary numbering (`MechControlsMapperPadFirstAttributeID = 2`, stick=3 ... PilotArray=
0x16); `mechmppr.cpp` pads id 2 with a valid never-bound entry (the mech.cpp attrPad idiom) and
locks the base + pinned ids with `static_assert`s. `L4MechControlsMapper::TargetRangeExponent`
(chained) moves 0x16→0x17, matching the binary's own arithmetic; it is bound by name (audio
zoom-blip), so no consumer shifts.
**After the fix:** `attrID 3 -> +0xdc` (stick), `attrID 4 -> +0xe4` (throttle, 8-byte spacing
✓), `attrID 6 -> +0xec` (reverse), `10/11/12 -> +0xfc/+0x100/+0x104` (the look trio); torso +
weapons unchanged. Regression: headless forced-walk run (`BT_FORCE_THROTTLE=1`) drives normally
`[mppr] thr=1 -> speedDemand=61.501`, gait cycles, position advances, 31/31 subsystems
simulated, no faults.
## 2026-07-17 — Steps 2b-2e: PadRIO, stand-downs, displays, preset — DONE + verified
**2b (RIOBase + PadRIO, commit ac57a47):** `L4RIO.h` splits abstract `RIOBase` (enums, the five
analog Scalars, pure `GetNextEvent`/`SetLamp`, no-op serial ops, NEW `IsOperational()`) out of
serial `RIO` (`: PCSerialPacket, RIOBase` — ctor assigns as before, zero behavioral delta);
`L4CTRL.h` `rioPointer` retyped `RIOBase*`; gated `L4CONTROLS=PAD` factory arm. NEW gated TUs
`L4PADRIO.*` (XInput+keyboard → the surface; 3s hot-plug; focus-guarded keys; per-poll
AnalogEvent heartbeat time-gated so the manager's drain loop terminates; `lampState[]`;
static `SetScreenButton` — button addresses AND the keypad space 0x50-0x6F → KeyEvents) and
`L4PADBINDINGS.*` (`content\bindings.txt`, default written on first run; deflect/slew/set axis
model; addresses validated vs ButtonCount).
**2c (stand-downs, commit 9f35a80):** engine query `BTRIODevicePresent()` (ungated —
pod-correct); both dev bridges (`mech4.cpp` attr writes, `mechmppr.cpp` BT_KEY_BRIDGE) gate
their WRITES: unset=auto / "0"=off / else on; forced harness always rides. **Definitive live
test:** OS-injected LSHIFT hold slewed PadRIO throttle 0→0.33→1.0 with `pre==thr` every frame —
the value arrives via the ENGINE push through the streamed `.CTL` binding (bridge stood down),
`speedDemand=61.501` from authentic InterpretControls.
**2d/2e (displays + preset, commits db9ac94 + 418acf0):** the on-screen panel follows **vRIO**
(`C:\VWE\vrio``CockpitLayout.cs`/`PanelCanvas.cs`, per Cyd): five 4x2 MFD clusters
(descending addresses), Throttle/Secondary/Screen/Joystick 1x8 columns, two 4x4 hex keypads
(0x50 internal→pilot unit, 0x60 external→operator unit), physical names, red/yellow/blue
coloration, 500/250/125ms flash, right-click latch — 104 controls. `PlasmaWindow :
Video8BitBuffered` renders the 128x32 plasma via the SAME gauge-renderer path as serial
(buffer is TOP-DOWN — the flip rendered it upside down, user-reported + fixed). MFD surfaces =
the EXISTING dev-gauge modes (no new code, no L4VB16 exposure). `-platform glass` preset +
run.cmd token. **Button-trap guard (e2c21c4 pattern):** the authentic base
`ConfigureMappableMessageHandler` FAIL trap is `abort()` under DEBUGOFF — any aux/zoom button
without its reconstructed L4 override KILLED the game on press (panel makes all reachable).
Default now log+ignore; `BT_BUTTON_TRAP=1` restores the hard trap. Verified: synthetic panel
click → 2 `[FAIL]` (press+release through panel→PadRIO→ProcessRIOEvent→streamed mapping), game
survives. NOTE: each `[FAIL]` marks a missing L4 override — reconstruction backlog, see
[[open-questions]].
**Step-2 verification matrix (all passed 2026-07-17):**
1. Purity: defaults `build-pod/` compiles (known mech3 /FORCE externals only); seam audit —
`BT_GLASS`/`BT_STEAM` mentions ONLY in CMakeLists, btl4main.cpp preset region, L4CTRL.cpp
(include+PAD arm), L4GREND.cpp (include+SCREEN branch), gated TUs, run.cmd docs.
2. Pod solo forced-walk clean (§2a).
3. **2-node loopback MP on the pod build:** A `-egg MP.EGG -net 1501` + B `-net 1601` +
`btconsole.py` relay — both nodes launch into the running mission (77/76 ticks, mechs at
their drop zones, gait/target loops live).
4. Glass build: PadRIO drives through the authentic mapper path; panel click path live; plasma
window; all boot lines present.
Deferred to the user (visual-only): panel/plasma appearance vs vRIO on screen, XInput stick
feel, `L4PADFLIP` polarity.
## 2026-07-17 — Step 3: the miniconsole — DONE + verified (commit c4ca87b)
**Architecture: the console stays a wire peer.** `game/glass/btl4fe.*` (GDI catalog menu + the
console-shape egg writer) and `btl4console.*` (the LocalConsole marshal: a worker-thread console
CLIENT speaking the `tools/btconsole.py` protocol verbatim — 1040-byte chunked
`ReceiveEggFileMessage`, 20s settle, `RunMission`×2, stay connected, own the clock,
`StopMission`(id 6)@expiry; logs `content\marshal.log`). The engine runs the 100% stock console
ladder — ZERO engine hooks; the planned `L4APP.H` setters proved unnecessary (launches pass real
argv on relaunch). WinMain: one gated block — zero-mission-arg glass launch → menu → relaunch
self with mission argv (`BT_FE_EGG/PODS/SECS` arm the marshal; `BT_FE_LOOP` returns exits to the
menu; menu relaunches CLEAR the handoff — the leak re-armed a marshal in cycle test 1).
**Verified (synthetic menu drives, all autonomous):**
- Golden egg: `frontend.egg` section sequence + line offsets IDENTICAL to `MP.EGG`; ordinal
pages verbatim; GDI name-glyph pages in MP.EGG framing (32/8 rows, 16 bytes/line).
- Solo cycle: menu → egg → relaunch → marshal → stock ladder → mission RUNS (58+ ticks) → 60s
clock → stop → menu returns with clean env.
- **HOST LAN 2-node:** peer started manually (`-net 1601`); menu-hosted launch → marshal feeds
BOTH pods (2-pilot egg, 10 chunks each) → both in-mission (65/18 ticks) → stop both → host
returns to the menu, peer idles at console-wait.
## 2026-07-17 — Step 4 pre: Steamworks SDK 1.64 vendored (commit 23c0019)
Official Valve zip (provided by Cyd at the repo root), extracted to
`extern/steamworks_sdk_164/` and trimmed to `public/` + `redistributable_bin/` + Readme
(12.5 MB); zip removed. Consumed only under `-DBT_STEAM=ON`.
## 2026-07-17/18 — Step 4: the Steam wire — CODE COMPLETE (commits e79e8fa, 48ede2f, f703bb1)
**4a (e79e8fa) — the wire seam:** file-local `BTNetSend/Recv/Accept/Close` wrappers around the
9 raw Winsock sites in `L4NET.CPP`; OFF builds are pure code motion. Regression: 2-node loopback
MP through the seam — mesh forms, full 31/31 mission on both nodes.
**4b (48ede2f) — the transport:** gated TU `L4STEAMNET` — pseudo-SOCKETs (0x5EA0xxxx) behind the
seam; the engine's TCP byte stream rides reliable-NoNagle Steam messages re-assembled into
per-connection rings (recv semantics preserved: empty→WSAEWOULDBLOCK, closed→0); seam
completions: `CheckSocket` (peer = token address), `OpenConnection` TCP_OPEN delegate,
`GetMyAddress` self-token. Install on `BT_STEAM_NET=1`; graceful degrade to Winsock at every
step. BOTH pumps needed (`SteamAPI_RunCallbacks` + sockets `RunCallbacks`).
**4c (f703bb1) — the lobby + THE FAKEIP LESSON [T2]:** prior art assumed FakeIP survives the
per-mission process relaunch — DISPROVEN live (menu process got 169.254.124.110, the mission
process got 169.254.54.234; the pod timed out connecting to its own stale address). Redesign:
roster addresses are opaque ipv4-shaped TOKENS (`169.254.77.N`, ports 1501 console / 1502 game)
minted by the host at GO and mapped to Steam IDENTITIES; connections ride
`ConnectP2P(identity, channel)` (console=0, game=1); the map hands off via env
(`BT_FE_MYFAKE`/`BT_FE_STEAMMAP`). `btl4lobby`: ISteamMatchmaking room (`btl4=1` filter),
pilots publish name/mech/color, host GO mints tokens + writes `btl4map` + `btl4go`.
**Verified single-machine (ON/ON, live Steam client):** menu → HOST STEAM LOBBY → room → GO →
map `169.254.77.1=<steamID64>` → egg roster carries the token → mission process
`[steamnet] up ... 1 roster token(s) incl. self` → pod self-matches → stock console ladder →
mission RUNS (46 ticks). 3-config build matrix green throughout. **Remaining (user hardware):**
the live 2-account cross-machine session — procedure in `docs/STEAM_TEST.md`.
## 2026-07-20 — Glass input-coverage audit + keyboard reconciliation
**Trigger:** making BT_GLASS the permanent player experience — familiar desktop keys dead, "most
panel buttons do nothing". Full audit of every input surface (bindings.txt keys, the 72-button
panel, the streamed .CTL EventMapping records, the typed 1995 channel), then the fix set.
### Finding 1 — THE panel-dead-buttons root cause: MechRIOMapper message ids shifted [T1→T2 fixed]
`MechRIOMapper`'s enum chained `Aux1QuadMessageID = L4MechControlsMapper::NextMessageID` (=0x19),
registering all 18 RIO handlers at ids 0x19-0x2a. The binary RIO table @0051dd30 RE-REGISTERS the
base aux/zoom ids **3..0x13** with the override handlers (Hotbox = 0x1a @0051de98) — that
re-registration over the base FAIL-trap ids is the whole point of the derived table. Streamed
`.CTL` events carry the binary ids, so every MFD-bank/zoom press found only the base
`ConfigureMappableMessageHandler``[FAIL]` (26 of 72 panel buttons dead; on a real pod with the
hard trap, an abort()). Hotbox worked by accident (sender + entry both used the same wrong 0x2a).
Fixed: ids pinned to the binary numbering + static_assert locks (`btl4mppr.hpp`). Verified live:
panel 0x0F/0x08/0x27/0x20 → `[mode] preset` lines, 0x10/0x11 zoom no longer FAIL. (Residual
fidelity gap: the binary RIO table also carries its OWN Keypress id 0x19 → @004d2514,
unreconstructed; we still register the shared L4 Keypress 0x17 — works, logged in the backlog.)
### Finding 2 — the always-active msg-4 records IDENTIFIED [T1] (closes the #9 open question)
Binary message tables decoded from section_dump (16-byte {id,name,fn,0} entries): per-receiver-
class id spaces. Roster (ARENA1 MadCat): subs 3=Reservoir, 4-9=Condenser1-6, 10-13=GeneratorA-D.
So: **0x2C = the COOLANT FLUSH button** (Reservoir InjectCoolant, WORKS), **0x2F/0x2E/0x2D/0x2B/
0x2A/0x29 = per-condenser VALVE buttons** (MoveValve, WORK), **0x1A-0x1D = Generator A-D ON/OFF**
(`ToggleGeneratorOnOff` id 4, table @0050fb90 fn @004b1ed0 — DEAD, unreconstructed). Also new:
0x13 → Mech **DuckRequest** (0x1a @0049fa00 — the manual's CROUCH button!), 0x28 → Mech
**BalanceCoolant** (0x16 @0049f728), 0x12 → ThermalSight **ToggleLamp** (id 3, table
@0x51120C fn @004b860c), 0x14 → Searchlight + Searchlight2 **ToggleLamp** (id 3, table @0x51117C
fn **@004b838c**) — ✅ both WIRED 2026-07-25 (#61); the two fn addresses were swapped here before
that (see `context/decomp-reference.md`). Weapon
Eng-page extras: msg 0x3 = **ToggleCooling** (HeatableSubsystem @0050e41c fn @004ad6f8, unwired),
msg 0xb = **ToggleSeekVoltage** (energy @004ba478) / **EjectAmmo** (ammo weapons @004bb9b8),
Myomers msg 9 = ToggleSeekVoltage (@004b8a48; body exists, unwired). Unhandled messages are
SILENT (Receiver::Receive ignores an unfound id) — the dead-button census cannot come from
[FAIL] logs alone; it comes from the BT_CTRLMAP_LOG record dump × the handler tables.
### Finding 3 — glass keyboard: stand-down + double dispatch [T2]
ALL btinput actions (drive keys, M/N/J/K/L/H/C/V/B/G/F5-F9) stand down when PadRIO owns the path
— nothing on the game side polls keys directly (BTInputPoll is the only GetAsyncKeyState
consumer). Meanwhile the typed 1995 channel stays LIVE (glass preset = `PAD,KEYBOARD`) with NO
suppression — so Cyd's old bindings double-dispatched: W (keypad row) also typed 'w' = pilot
select 0; C (fire middle) also typed 'c' = MFD3 Eng3; NUMPAD key-UPS alias to letter hotkeys
(VK_NUMPAD2=0x62='b' = MFD3 Quad, VK_F5=0x74='t' = pilot select 3). This is the historic
pre-btinput double-dispatch, back on the glass side.
### The fix set (landed)
1. `btl4mppr.hpp` — RIO id pinning (ungated, pod-correctness; the step-2a idiom).
2. `L4PADRIO.*` — typed-channel suppression tables built from the loaded bindings (+hardcoded
keys), `PadRIO::SuppressKey` consumed by a `#ifdef BT_GLASS` arm next to the btinput hook in
`L4CTRL.cpp:1506` block; V joins backtick on the view-toggle edge; J/K/L fire
`gBTPresetCycle[3]` (the same seam the dev poll feeds — no binary analog exists for a "cycle"
button; the body is the authentic SetPresetMode).
3. `L4PADBINDINGS.cpp` + `content\bindings.txt` — the MERGED default map (user decision:
keyboard = the ~20 core gameplay actions on the CONTROLS.MAP keys; the PANEL covers all 72
pod buttons by click/latch). W/S throttle, A/D pedals, Q/E twist, R/F elevation, X all-stop,
1-4/Space/Ctrl fire + LALT reverse + B look-behind, M=0x18, N=0x15, H=0x2C (flush hold),
C=0x2F (Condenser1 valve), G=0x0E (weapon-1 configure, Mfd1-Quad-gated), F5-F9=Mfd2 bank
0x27-0x24+0x22 (page-gated gen A-D/mode), numpad flight cluster kept. DISPLACED from Cyd's
map (each reachable on the panel or by editing bindings.txt): number row→secondary panel,
QWERTY row→pilot keypad (dropped), arrows→hat looks (arrows now drive), V/C/B→fire
0x47/0x46/0x45, LCTRL→throttle-down. XInput pad section untouched.
NOTE (control modes, authentic): in BAS mode the pod turns with the STICK (Q/E here) and the
pedals are inactive; A/D turn in MID/ADV — exactly the manual's behavior, unlike the desktop
bridge which re-routes A/D per mode.
4. Un-touched: CONTROLS.MAP, btinput defaults, pod serial path. `bindings.txt` is read only by
`PadBindingProfile` inside the gated PadRIO TU — the pod build never opens it.
### Verification (2026-07-20, build-glass2, ARENA1 solo, all logs in-session)
- Panel: 0x0F→preset(0,1), 0x08→(0,0), 0x27→(1,1), 0x20→(1,0), zoom no-FAIL; 0x15 display
1→2→0; 0x2C hold → `[flush] Reservoir level 6→0`; dead-class buttons (0x13/0x1A/0x12/0x14)
silent as predicted. ZERO [FAIL] lines in the whole session (was 2 per bank press).
- Keyboard: W hold → thr 0→1, speedDemand 61.501, gait advances; Q → turnDemand 0.94 (BAS);
M → `control mode -> 1`; A hold → turnDemand 1.0 (MID); Space → `[emitter] FIRED`; N →
`display notify 1`; J/K/L → presets (0,1)/(1,1)/(2,1); H → reservoir drain; C → condenser#1
valveState 1→5. BT_SHOT frame confirms visually: MID lit on the control-mode column, CRITICAL
DAMAGE schematic up, MFDs on Eng pages, heading moved. V not re-injected (user took
foreground mid-run) — code-identical to the verified backtick edge [T3].
- Suppression: typed 'w' swallowed (no [keych]), typed 't' flows ([keych] 0x74), WM_KEYUP
VK_F5 swallowed (the 't'-alias hazard).
- Un-regression: build-pod (gates OFF) compiles 0 errors; forced-walk run drives
(speedDemand=61.501, 32 subsystems, streamed ids 3→+0xdc / 4→+0xe4 unchanged).
### 2026-07-20 (later) — the "keymap regression" live report: STALE EXE, not code [T2]
**Report (human, live glass session):** arrows dead/half-dead (UP accelerated, no detent;
LEFT/RIGHT no turn), throttle not a lever, V no view toggle, J/K/L/N inert, weapons firing into
the ground. **Root cause: `build-glass/Release/btl4.exe` was built 07:47 — BEFORE commit
`5dd3536` (08:35) landed the whole audit fix set** (SuppressKey tables, V/J/K/L hardcoded keys,
the RIO-id pinning that makes 0x15/N work) — the session was relaunched without rebuilding, and
kept running the pre-audit image. `content\bindings.txt` (08:23) already WAS the merged map and
the loader parsed it fine (`[padrio] bindings loaded: 40 keys, 10 pad buttons, 5 pad axes`), so
every bindings-file symptom analysis was a red herring; the binary was simply old. The
ground-fire symptom is the same session: with the R/F/X semantic chaos of the unsuppressed typed
channel, the torso elevation had been driven down and nothing recentered it.
**Fix:** `rm build-glass/Release/btl4.exe && cmake --build build-glass --config Release` (0
`error C`). No input-code change was needed — arrows/lever/detent/V were all already correct at
HEAD. One diagnostic enrichment landed: the `BT_MPPR_TRACE` `[mppr-c]` line now also prints
`mode/pedals/stick/turn/elev`, so one env-gated line proves the whole control surface.
**Re-verification (build-glass, DEV.EGG solo, SendInput-injected keys, own PID only):**
- UP hold 0.9 s → thr 0→0.63, dmd 38.70; released → **holds** 0.63 for 3 s (persistent lever).
- DOWN hold 2.5 s → thr 0.36→0.0028→0, rev=0 throughout — **stops at zero, no reverse
crash-through** (the wire throttle is the authentic 0..1 lever; reverse is the 0x3F button).
- M → `[mode] control mode -> 1`; LEFT → pedals +1 / turnDemand +1 (CCW=left, the wire sign);
RIGHT → pedals 1 / turnDemand 1; both auto-center on release. (In BAS the pedals are
authentically inactive — turns are the stick, Q/E.)
- R/F → stick.y ±1, torso elev 0→+0.349→−0.195, holds where released (positional torso,
matches the btinput path); **boresight level at spawn (elev=0)**.
- V → `[view] COCKPIT eyepoint``[view] external chase`, BT_SHOT pair confirms
(chase mech ↔ canopy + reticle). Closes the audit's one T3 (V edge) as [T2].
- SPACE → `[emitter] FIRED #1`. Pod build rebuilt + smoked: BT_AUTODRIVE=0.8 →
dmd=49.2008, clean log, no cross-contamination.
**Lesson (procedural):** after landing an engine/input commit, REBUILD every live build dir
before relaunching — a stale `build-glass` exe reproduces "the whole keymap is broken" exactly.
### 2026-07-20 (later still) — human re-test: GAIT DETENT ported to the PadRIO lever [T2]
**Report (fresh build):** zero-stop confirmed working, but "OUR detent is still missing — it
goes into that mixed walk/run mode and repeats the acceleration sample." That is the documented
gait DEAD BAND (locomotion.md [T1]): the SM has no stable state for a demand in
(`walkStrideLength`@0x534, `reverseSpeedMax`@0x538) — parked there it cycles 7→10→14→7 forever,
retriggering EngineShiftFwd/Rev each swing. The pod-build keyboard lever carries an
accommodation (mech4.cpp "GAIT DETENT": at KEY-REST, snap the lever out of the band to the
NEARER edge; sweeping through while held stays continuous) — the PadRIO slew channel had no
such protection, so the glass lever could park in-band.
**Port (faithful copy, not an invention):** mech4.cpp publishes the band in lever units every
driven frame through a new seam (`gBTGaitDetentLo/Hi`, same guards + the same +0.002 engage
margin; hi<=lo = unknown); `L4PADRIO.cpp Poll()` applies the IDENTICAL nearer-edge snap to the
Throttle channel when the slew is at rest (no slew key held, no pad slew axis deflected) —
same rule, same log tag, gated `BT_GAIT_TRACE`. The rest decision uses PadRIO's own held-state
(the lever's owner on glass), not gBTDrive.keyFwd/Back.
**Verified (build-glass, DEV.EGG solo, graduated UP-taps parking the lever every ~0.10 across
0..1, BT_GAIT_TRACE+BT_MPPR_TRACE):** band [0.358026, 0.50388) = demand 22.02→30.87 (the
locomotion.md Blackhawk numbers); park at 0.4137 → snapped DOWN to 0.358026, park at 0.465126 →
snapped UP to 0.50388 (both nearer-edge directions); rest-position histogram shows NO at-rest
sample inside the band; gait states: ONE walk→run clip (state 10, once) on the whole ascending
sweep, ONE run→walk (state 14, once) descending, normal 6↔7 / 12↔13 cycling between — the
accel/shift sample can fire only once per real transition. Zero-stop unaffected (thr=0 rest,
state 0). Pod build rebuilt + BT_AUTODRIVE smoke clean (the seam publish is a pure global
write on the pod path).
## 2026-07-18 — Merge origin/master (commit f889e24): btinput meets PadRIO
Master's 33-commit run landed (D1 relay/operator-console line, CONTROLS.MAP+XInput binding
engine `btinput.cpp`, camera seats, torso pitch, sign fixes, the 1995 manual, version stamping).
**The reconciliation:** btinput (ungated, dev/pod desktops) joined the stand-down convention —
`BTInputPoll` yields and `BTInputSuppressKey` claims nothing when an operational cockpit device
(serial RIO / PadRIO) owns the input path (`BTRIODevicePresent`; `BT_KEY_BRIDGE` override;
forced harness exempt). **One input system per mode:** `CONTROLS.MAP`/btinput on dev+pod
desktops, `bindings.txt`/PadRIO on glass. The D1 relay keeps its own raw sockets by design
(relay and Steam are alternative wires; the BTNet seam covers the arcade mesh both need).
Verified: 3-config build, glass stand-down live, pod forced-walk with btinput active, 2-node
loopback MP 31/31.
## 2026-07-19 — Mission-control options: the AUTHORITATIVE catalog (TeslaSuite console.ini)
Source: `C:\VWE\TeslaSuite\410console\4_10-console-extracted\Console.ini`, the Mac 4.10 operator
console's `BT::` adventure tree (the `tag=` values are the egg field values; display names are the
console's). Cross-checked vs `410console/battletech-port/BATTLETECH-PORT-SPEC.md` + `reference/
cavern.egg`. This is every option the original console exposed for a BattleTech mission.
**Mission-level**`[mission]`: `adventure=BattleTech` (fixed), `temperature=27` (fixed),
`scenario=freeforall` (the tag is always freeforall; the RESPAWN choice is the ROLE):
- map (8): cavern, grass, rav, polar3, polar4, arena1, arena2, dbase
- time (4): morning, day, evening, night ← was day/night/morning/**dusk** (wrong: evening)
- weather (3): clear, fog (light), soup (heavy)
- role (2): Default (`model=dfltrole`, respawn) / NoReturn (`model=noreturn`, one life)
- length: seconds
**Per-pilot** — participant block:
- experience (4 [T1 game decode]): novice, standard, veteran, expert ← was hardcoded `expert`
- vehicle (18, friendly→tag): Black Hawk=blkhawk, Loki=loki, Nova=bhk1, Mad Cat=madcat, Thor=thor,
Owens=owens, Kotori=own1, Summoner=thr1, Hellbringer=lok1, Timber Wolf=mad1, Avatar=avatar,
Satyr=ava1, Sunder=sunder, Sunder V1=snd1, Vulture=vulture, Mad Dog=vul1, Mischief=lok2,
Zanin Neko=mad2 (the 18 ModelList names; matches the 2026-07-18 vehicle sweep)
- color/camo (7, display→tag): White=White, Black=Black, Brown=Brown, Red=**Crimson**, Green=Green,
Gray=**Grey**, Tan=Tan ← was White/Black/Crimson only
- badge/emblem (7, the `badge=` field): VGL, Davion, Kurita, Liao, Marik, None, Steiner ← was `VGL`
- patch/badge-color (8, the `patch=` field): Yellow, Blue, Grey, Green, Violet, Red, White, Black
← was `Yellow`
- dropzone (5, the `dropzone=` field): one, two, three, four, five ← was `one`
- advancedDamage (0/1): the advanced damage model toggle ← was hardcoded `1`
- vehicleValue: 1000 (const), loadzones=1 (const), bitmapindex (per-pilot ordinal)
---
## 2026-07-20 — Per-display cockpit windows (BT_GLASS_PANELS)
**Goal (Cyd):** stop collapsing the desktop cockpit into combined surfaces. Break each
SECONDARY display out into its OWN window carrying that display's surface WITH its RIO button
bank placed around it, arranged pod-faithfully around the main 3D view — a dev cockpit that
reads like the pod, so button/lamp/display behavior can be validated in position.
**New TU: `engine/MUNGA_L4/L4GLASSWIN.{h,cpp}`** (BT_GLASS-only; added to the CMake BT_GLASS
`target_sources`). Builds SEVEN GDI windows and positions them around the main window:
| Window | Surface (port) | RIO bank | Buttons |
|---|---|---|---|
| Heat MFD (UL) | `Heat` (0x4000) | 0x28-0x2F | 8 red, 4 above / 4 below |
| MFD 2 (UC) | `Mfd2`/`Eng2` (0x0400) | 0x20-0x27 | 8 red, 4 above / 4 below |
| Comm MFD (UR) | `Comm` (0x8000) | 0x30-0x37 | 8 red, 4 above / 4 below |
| MFD 1 (LL) | `Mfd1`/`Eng1` (0x0100) | 0x08-0x0F | 8 red, 4 above / 4 below |
| MFD 3 (LR) | `Mfd3`/`Eng3` (0x1000) | 0x00-0x07 | 8 red, 4 above / 4 below |
| Secondary/Radar | `sec` (palette, portrait) | 0x10-0x1B Secondary | 12 yellow, 6 left / 6 right |
| Flight Controls | (none) | 0x38-0x47 (+ panic/icom) | neutral, 2 columns |
**Architecture — GDI + CPU blit, NO D3D for the secondaries.** Each window is a layered,
topmost GDI window (the L4PADPANEL pattern: double-buffered paint, ~62 ms repaint timer,
left-click = momentary / right-click = latch → `PadRIO::SetScreenButton`; lamps from
`PadRIO::GetLampState`). The surface is CPU-expanded from the shared gauge pixelBuffer by the
new **`SVGA16::ExpandPlaneToBGRA(mask, paletteID, monoTint, rotateQuadrant, dst, &ow, &oh)`**
(L4VB16.cpp) — the no-D3D analog of `DrawDevSurface`: palette-LUT expand for `sec` (monoTint
< 0), mono bit-plane → tint for the MFDs; the portrait secondary CRT is unrotated by
transposing into `dst` (rotate 1/3, `BT_GAUGE_SEC_ROT`) — then `StretchDIBits` (HALFTONE) into
the surface rect. An Mfd/Eng pair shows whichever plane is the live (non-`BlankColor`) one, as
on the pod monitor. This reuses the plasma-window CPU→window pattern and sidesteps the whole
D3D dev-composite bug family (additional-swap-chain lifetime, device-state save/restore,
depth-stencil mismatch, layered strobing).
**Gate: `BT_GLASS_PANELS`** (runtime env; `BTGlassPanelsActive()`). ⚠ **Was** default-ON under
`-platform glass` (preset putenv in `btl4main.cpp`) — as of 2026-07-26 the COCKPIT SURROUND is the
glass default and this gate is **explicit-only**; the one resolver + the full precedence table are
in `context/glass-cockpit.md` §Layout modes. Wired at three seams:
- `L4PADRIO` ctor: `BTGlassPanelsActive()``BTGlassPanels_Create()` instead of
`BTPadPanel_Create()`; dtor destroys both (safe no-ops).
- `L4VB16`: `BTDrawGaugeInset` / `BTGaugeWindowRenderAndPresent` early-return when
`GlassPanelsOwnSurfaces()` (the per-display windows own the surfaces).
- `btl4main`: skip the `gBTGaugeDockBottom` + 1100×600 world-window forcing (world stays
normal size — no bottom strip).
**Placement.** `ComputeLayout()` rings the windows around `ghWnd` (the main game window):
top row Heat/Mfd2/Comm above it, Mfd1/Mfd3 at the lower corners, radar below-center, Flight
Controls left of the seat; clamped to the work area, with an 800×600-centered fallback before
the main window exists + a one-shot reposition once `ghWnd` comes up (in the repaint timer).
**Verified [T2 startup].** `-DBT_GLASS=ON` Debug build links (the usual `/FORCE` mech3
subsystem set is pre-existing, unrelated). Bounded `-platform glass -egg DEV.EGG` launch:
`[glasswin] per-display cockpit up (7 windows: 5 MFD + radar + flight)`, `[plasmawin] … up`,
`[padrio] XInput pad found`, no failure/abort markers, full mission loop un-regressed (killed
after 18 s). Visual layout / surface content / click-through NOT yet human-verified.
**Tunable (Cyd flagged refinements):** surface display sizes, the exact button sub-positions
around each screen, the yellow left/right split (first cut 0x10-0x15 / 0x16-0x1B), and the MFD
mono phosphor tint — all named constants at the top of `L4GLASSWIN.cpp`. Deferred: folding the
plasma window into the same arrangement; per-window draggable/saved positions; moving/resizing
the main window into a true center cell for a tighter ring.
### Blank-surface bug (found + fixed same day) — /FORCE duplicate-global race
First glass-panels run showed the buttons but BLANK display surfaces. `BT_GLASS_LOG`
diagnostics in `BlitSurface` traced it stage by stage: `application == 0x00000000` in
`L4GLASSWIN.cpp` for the ENTIRE run (call 1 → 481), so `application->GetGaugeRenderer()` was
never even reached. Yet the legacy dock path (`BTDrawGaugeSurfaces`, `L4VB16.cpp`, SAME engine
lib) logged `application=047BA9C8 gr=0C57FF78` — non-NULL.
Root cause: the 1995 headers declare free globals (`application`, `ghWnd`, …) without
`inline`/`extern`, so they are DEFINED in both `munga_engine` and `bt410_l4`. Under
`/FORCE:MULTIPLE` the linker binds each `.obj`'s reference to one copy or the other
**non-deterministically per link** — the newly-added `L4GLASSWIN.obj` bound the copy the game
never assigns (stays NULL), and a relink even flipped `L4VB16.obj`. Routing through an engine
helper is therefore unreliable (its binding is equally random).
Fix: `BTResolveGaugeRenderer()` / `BTResolveMainWindow()` (declared in `l4vb16.h`) are DEFINED
in `game/btl4main.cpp` off the launcher's OWN file-scope pointers — `btl4App` (set to `new_app`
at btl4main.cpp:631) and the global `hWnd` (the real main window). Those live in `btl4main.obj`
and are the actual assigned pointers, so the resolvers are always correct regardless of the
duplicate-symbol binding. `BlitSurface` and the placement code call the resolvers, never the
`application`/`ghWnd` globals. New durable gotcha: `context/reconstruction-gotchas.md` §6
duplicate-GLOBAL corollary.
Verified after the fix (`BT_GLASS_LOG`): `[glass] 'Heat MFD' port=Heat mask=0x4000 pal=3
enable=2 ow=640 oh=480 nonzero=22227/307200`, `[glass] 'Secondary / Radar' port=sec mask=0x3f
pal=1 enable=3 ow=480 oh=640 nonzero=26206/307200` — surfaces resolve and carry live pixels.
`BT_GLASS_LOG` left in as a permanent per-surface resolve/pixel diagnostic (throttled).
## 2026-07-26 — Layout modes get ONE resolver; button geometry gets ONE owner (L4RIOBANK)
The RP412 replication pass, step 1 of the agreed plan (port the geometry, keep our renderers).
### The mode resolver (btl4main.cpp)
Where the secondary displays go was decided in TWO places with duplicated precedence -- the glass
profile block picked `BT_PAD_PANEL`/`BT_GLASS_PANELS`, the window-sizing block re-derived
cockpit-vs-dock -- and the boot banner read NEITHER, announcing "per-display cockpit windows
[BT_GLASS_PANELS]" for every glass boot including the surround default. 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**.
Now: one `glassLayout` resolved after the profile putenvs land, consumed by the banner, the
pad-panel decision and the sizing block. Precedence `BT_GLASS_PANELS` > `BT_DEV_GAUGES_WINDOW` >
`BT_DEV_GAUGES_DOCK` > `BT_COCKPIT=0` > surround default; `BT_GLASS_PANELS` is explicit-only now
(it was auto-set); dock/window auto-raise `BT_PAD_PANEL` so the 72-button field always has a home.
Banner: `[boot] platform profile: GLASS (PadRIO + plasma window) [secondary displays: <mode>]`.
Verified all four modes boot to the mode they name, plus pod/dev.
### L4RIOBANK -- the shared field
`engine/MUNGA_L4/L4RIOBANK.cpp/.h` (new TU). Both renderers were carrying their own copy of the
button geometry 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
now owns it, both are consumers, placement stays per-renderer.
The under-glass rule, from RP412 `L4MFDVIEW` (which took it from the pod): reach half the glass
in behind the display, leave a lamp strip clearing the edge, paint buttons first and imagery
over. Strip scales off the display's SHORT axis vs the native 480 (MFD 640x480 landscape, map
480x640 portrait) with a 6px floor. The MFD per-column nudge against the DISPLAY/PROGRAM legends
moved here so both renderers inherit one alignment. The map's foot row is pushed FIRST because
the side columns now cover that band and the hit test takes the first match.
Retired: `L4GLASSWIN`'s `PlaceCellAt`/`PlaceLine`/`PlaceRect` and its `RedCellH`/`EdgeProtrude`/
`RailW`/`RailSlotAdd`/`RailGap`/`RedOffsetX`/`kRedColDX` constants; the surround's inline MFD/
radar/flight button loops. `kCkRedH`/`kCkLamp` survive only as band reserves for the canvas math.
### Lamp flash decode [T1 -> fixed]
`BTLampBrightnessOf` returned `max(state1,state2)` and blanked on the alternate phase. Per the
engine's own `RIO::LampState` enum [T0] solid shows state 1 and flashing ALTERNATES state 1 and
state 2. Agrees only when one state is Off -- true for Panic (`flashFast+state1Off+state2Bright`)
which is why it survived -- but `L4LAMP.cpp:252` commands `flashFast+state1Dim+state2Bright`, a
dim->bright pulse that rendered as bright->off. Three copies existed (l4vb16.h, L4GLASSWIN,
L4PADPANEL), all three wrong; the two locals now forward to the one fixed inline.
### Verification harness (new, reusable)
- `BT_RIOBANK_LOG=1` -- one-shot dump of every bank's buttons + bounds (capped at 16 banks so the
per-frame surround caller prints one pass).
- `scratchpad/checkbank.py <log>` -- per-bank census + **first-hit reachability**: an address
whose rect is entirely covered by earlier buttons in its bank is dead however big it looks, and
the overlapping under-glass banks make that a live hazard. Reports duplicates and shadowing.
- `scratchpad/clickbank.py <log> <title> --mode surround|exploded` -- posts a real
WM_LBUTTONDOWN/UP at every button's centre; the game's own CLICK forensics prove dispatch.
Result both modes: **72/72 placed, 0 shadowed, 0 duplicated, 72/72 dispatched**, process survived
144 posted messages each. Pod + dev profiles boot and simulate un-regressed. Surround `BT_SHOT`
confirms the legends are clear and the lamps read as edge strips; exploded `PrintWindow` captures
confirm surfaces still land under their banks.
## 2026-07-26 (later) — Phase 2: the cockpit scales (uniform-fit letterbox + -fit)
RP412 replication step 2. The canvas is a FIXED size and D3D9 stretched it into the client, so a
window dragged to a different shape squashed the instruments -- the projection was aspect-corrected
back in task #20, but the panels, lamps and MFD glass never were. Now: one uniform scale, centred,
leftover black.
**Mechanism.** `BTCockpitFitRect()` (L4VB16.cpp) computes the centred uniform-scale rect in integer
math -- a float scale wobbles by a pixel between frames and the bars shimmer during a drag. D3D9
applies it as a Present **destination rect**, which `D3DSWAPEFFECT_DISCARD` forbids, so the
WINDOWED swap effect becomes `D3DSWAPEFFECT_COPY` when the surround is up and multisampling is off
(COPY cannot multisample). `gBTCockpitLetterbox` records the outcome; 0 = the old full-client
stretch, which is what every non-cockpit mode and any MSAA run still gets.
Wired at five seams:
- `L4VIDEO` device setup: swap effect + confirm/withdraw the flag.
- Both Present sites: the scene present AND the wait-screen overlay present (it paints into the
same backbuffer, so it has to land in the same rect).
- `btl4main` WM_SIZE: `InvalidateRect` -- a COPY present only writes its dest rect, so stale bar
content persists without an erase (the class brush is black).
- `BTCockpitMouseDown`: client->canvas through the SAME fit. Mapping against the full client
drifts the hit test off every button by the bar width; a click on a bar now hits nothing.
- `BTWorldAspectOf`: under the letterbox the view's on-screen aspect is its OWN -- the client
drops out of the formula entirely.
**`-fit` / `-windowed-fullscreen`** (btl4main): borderless `WS_POPUP` over the monitor rect, canvas
letterboxed inside. Unknown tokens fall through `L4Application::ParseCommandLine`'s ladder to
`return True`, so no arg-parser change was needed.
### Ordering trap (found live, fixed)
The letterbox flag was originally set at device creation -- but **the first WM_SIZE beats the
device**. A `-fit` boot on the 3440x1440 ultrawide logged `[resize] client 3440x1440 aspect=3.14`
(the stretch formula), and since nothing resizes the window again, the per-frame
`gWindowAspect != appliedAspect` check applied it on frame 1. The INTENT is now decided in
btl4main alongside `gBTGaugeCockpit` (windowed cockpit + MULTISAMPLE unset/0); L4VIDEO only
confirms it or withdraws it. Re-verified: the boot line reads `aspect=1.8`.
### Capture note (for whoever verifies this next)
`PrintWindow` returns an all-black frame for the borderless `-fit` window, while working fine for
the normal chrome'd window. Use a screen-region grab (`scratchpad/shot.ps1`) for `-fit`. The
all-black PrintWindow is NOT a render failure -- the log showed 18 sim ticks and the screen grab
showed the full cockpit.
### Verified
- Wide drag 1684x661 and tall drag 744x961: canvas uniformly scaled and centred, bars on the long
axis, ZERO distortion (captures `fit_wide.png` / `fit_tall.png`).
- `-fit` on 3440x1440: borderless, centred, bars left/right, undistorted.
- **72/72 buttons still dispatch after a resize** -- clickbank.py now maps canvas->client through
its own copy of the fit (an independent check that the two transforms agree): at fit 906x661
with a 389px left bar, all 72 `[cockpit] CLICK` lines land.
- exploded / dock / pod / dev: DISCARD retained, boot + simulate un-regressed.
## 2026-07-26 (later still) — Phase 3: player-tunable displays + the settings file
RP412 replication step 3.
### Scaling (BTCkResolvedSizes, L4VB16.cpp)
`BT_MFD_SCALE` (all five), `BT_MFD_SCALE_UL/_UC/_UR/_LL/_LR` (per display, overriding the group),
`BT_RADAR_SCALE` -- percentages of the pod size, 25-200, resolved ONCE (the surround recomputes its
layout every frame, so reading the env per frame would be silly).
The load-bearing consequence: **the surround BANDS derive from the resolved sizes**. That is why
the sizes could not stay compile-time constants -- the band a display hangs in has to grow with it
or the canvas clips it. Each band is what its largest occupant needs; a corner MFD only claims
`size - kCkOVL` because it overlaps the view; the bottom band drops to just the lower MFDs when the
map leaves that row for a side. Verified 100% reproduces the historical `L276 R276 T223 B336`
byte-for-byte, 150% gives `L436 R436 T343 B448`, 80%+MIDLEFT gives `L256 R212 T175 B148`.
The flight label blocks now hang off their OWN lower MFD's scaled height + lamp strip, instead of
the constant -- a shrunken MFD used to leave the block floating in the gap and an enlarged one to
overlap it.
### Map placement (BT_RADAR_POS)
CENTER (default) / LEFT / RIGHT / MIDLEFT / MIDRIGHT, with the BOTTOM*/`*CENTER` spellings RP
accepts. Bottom corner: the map goes flush to the canvas edge and the lower MFD whose corner it
took slides beside it. Halfway up a side: it leaves the bottom row entirely and the bottom band
shrinks accordingly.
**First cut was wrong and the capture caught it:** the corner map was placed relative to the VIEW
edge, which overlapped the neighbouring MFD by the band width -- 232px of map drawn over the Right
Weapons display. Corner placement is measured off the CANVAS edge.
### The map legend grid -- measured, not inherited
The side columns have to line up with the six cells the map imagery paints beside them. Zooming a
native Secondary/Radar capture shows those cells are exactly our address map: MAP+ 0x10, MAP- 0x11,
IR 0x12 (ThermalSight lamp), crouch 0x13 (DuckRequest), searchlight 0x14, display-mode 0x15 -- the
art independently corroborating [[pod-hardware]].
`scratchpad/measurelegend.py` scans the capture for the legend's vertical rule and for our lamp
bars, and reports both. Ours: **top 3, cell 102, pitch 107** of the 640 span. RP412's map:
`13 + 6x102 @ 105`. Same cell height, different top and pitch -- confirming the deferral call in
Phase 1 was right, the numbers do NOT transfer. Our old even division (`displayH/6 + 1`) had the
pitch right by luck and sat 3px high of the labels; the grid is now pinned to the measurement, with
top and bottom scaled SEPARATELY and subtracted (RP's trick) so rounding cannot walk the buttons
out of step with the labels down a scaled column.
### environ.ini
Two defects, both fixed:
1. **Read far too late.** It sat ~300 lines into WinMain, after the platform-profile block had
already run its getenv()s -- so every setting the profile reads (BT_PLATFORM, BT_COCKPIT,
BT_GLASS_PANELS, BT_DEV_GAUGES, L4CONTROLS...) was silently ignored FROM THE FILE and only ever
worked as a real environment variable. Now loaded immediately after the first-breath boot line.
2. **putenv()'d comments verbatim**, so a commented-out option would have become an environment
variable literally named `#BT_MFD_SCALE`. Comments (`#`, `;`) and blanks are skipped, and lines
with no `=` are counted and ignored.
Added: the real environment WINS over the file (a launcher .bat overrides rather than fights), and
a fully documented default is written on first run when the file is absent -- the bindings.txt
convention (untracked via .gitignore, so extract-over-top never clobbers a player's settings; every
option ships commented out so a fresh install behaves exactly as before).
Verified live, all three paths: first run writes 3886 bytes and applies `0 setting(s)`; uncommenting
`BT_MFD_SCALE=130` + `BT_MFD_SCALE_UC=60` + `BT_RADAR_POS=MIDRIGHT` in the FILE drives the layout
(`displays UL 130% UC 60% ... radar on the right side, centred`); and `set BT_MFD_SCALE=90` beats
the file's 130 while the file's other two settings still apply.
### Verified
- 72/72 placed, 0 shadowed, 72/72 dispatched at baseline, at 150%/135%, and at 75% + BOTTOMRIGHT.
- exploded / dock / pod / dev boot and simulate un-regressed.
- Captures: p3_big2 (150%), p3_radleft2 (bottom-left + slid MFD), p3_radmidl (side-mounted map at
80%), p3_fromini (settings taken from the file).
## 2026-07-26 (final) — Phases 4-6: the dead-button backlog, the lamp mirror, the shipped doc
### Phase 4 — Mech::DuckRequest, and a census that lied in both directions
The 2026-07-20 audit listed 8 buttons with no reconstructed handler. **Only ONE was still
missing.** Generator on/off (`powersub.cpp`), ToggleSeekVoltage (`emitter.cpp`), EjectAmmo
(`projweap.cpp`), ToggleCooling (`heatfamily_reslice.cpp`) and BalanceCoolant (`mech.cpp`) had all
landed between 07-20 and 07-25 while `pod-hardware.md` and `open-questions.md` still called them
dead. `docs/INPUT_PATH_AUDIT.md:190` had already flagged this ("census stale in both directions")
and was right. Rule for next time: **grep the reconstruction, not the prose.**
`Mech::DuckRequest` @0049fa00 (id 0x1a, RIO 0x13 -- the manual's CROUCH button). Binary body, in
full:
if (0 < *(int *)(param_2 + 0xc)) { *(undefined4 *)(param_1 + 0x398) = 1; }
Press-only; sets `duckState` (`mech+0x398`, attribute 0x37). A one-shot REQUEST flag -- the
handler never clears it and the only other writer in the entire binary is the mech reset
(part_012.c:9439, the same reset that zeroes `incomingLock`, which is how that region was already
mapped). Registered in `Mech::MessageHandlerEntries`; `[duck]` log on press.
**Why the flag has no reader, and why that is correct.** `duckState` has ZERO readers anywhere in
the decomp. It is published as an ATTRIBUTE, so its consumer is a databinding:
`content/GAUGE/L4GAUGE.CFG` runs `oneOfSeveralPixInt(E, ModeAlwaysActive, bduck.pcc, 3, 1,
DuckState)` -- "crouch mode: button 4" on the map's legend column. Verified live by capturing the
Secondary/Radar window before and after a 0x13 press: the crouch icon goes grey -> orange. So the
handler is COMPLETE; a crouch pose invented here would be a stand-in for data we have not found
(no SQUAT clip name survives in the decomp or in content/, only DuckServo01.wav in AUDIO1.RES).
Bonus: those gauge widgets sit at offsets 537/430/322/215/108 -- a **107 pitch**, independently
corroborating the map legend grid measured from pixels in Phase 3.
Still open from that census: MechRIOMapper's own Keypress @004d2514 (id 0x19).
### Phase 5 — L4KEYLIGHT (RGB keyboard lamp mirror)
Ported from RP412 (itself a port of vRIO's KeyboardLampMirror). Keys bound to a lamp address in
bindings.txt glow with the panel palette -- yellow for the map's side columns (0x10-0x1F), red for
the rest -- flashing in step with the on-screen buttons (the LampLevel copy matches the FIXED
BTLampBrightnessOf from Phase 1). Per-key boards light each bound key; zone-lit boards mirror the
strongest lamp board-wide. All WinRT runs on a private worker thread (watcher, claim, 100 ms paint
loop, repaint only on change).
**The packing hazard did NOT transfer.** RP412 compiles this file with forced default struct
packing because its engine is `/Zp1`, which would break the WinRT ABI. BT411's `BT_OPTS` is
`/permissive /W0 /wd4996 /EHsc /bigobj /MP` -- no `/Zp` -- so only the dialect flags are needed:
`set_source_files_properties(... COMPILE_OPTIONS "/std:c++17;/permissive-")`, since the project
otherwise builds C++14 /permissive. The scalars-only interface is kept regardless.
Wired in `PadRIO`: map built from `bindings.keyBindings` (ActionButton binds only, first binding
per key wins, addresses < LampCount), fed from `PadRIO::SetLamp`, stopped in the dtor (which hands
the LEDs back to Windows). Gate `BT_KEYLIGHT=0`.
Verified live: `[keylight] mirroring 25 bound key(s)` + `+ LAPTOP-767CF84B (24 zones - board-wide
mirror)` -- it claimed this machine's real keyboard. `BT_KEYLIGHT=0` -> zero keylight lines, game
fine. Pod profile (no PadRIO) -> zero lines. Exploded -> works.
### Phase 6 — the shipped controls reference
`docs/CONTROLS.md`: the keyboard table plus **the full 72-button pod map**, grouped by the display
each bank surrounds, with the coolant-valve detent warning and the eject/jam procedure. mkdist
flattens it to ASCII as `CONTROLS.txt` at the zip root (the README's own flattening idiom).
`players/README.txt` gained pointers to it, to environ.ini, to -fit, and to the RGB mirror.
Verified: `mkdist.py` writes a 5149-byte pure-ASCII CONTROLS.txt into the zip.
## 2026-07-26 (last) — the keyboard becomes the button board (RP412 bindings port)
User decision: port RP412's bindings design and make it the DEFAULT (not an opt-in alternate),
plus a BT-flavoured interactive controls page.
**The layout.** Letter and number rows = the MFD banks laid out where they sit on the panel;
flight to the numpad so the board stays free; F1-F12 = the map's two columns; G and B unbound as
the physical gap between the lower clusters. Expressed in BT's OWN grammar (slew/deflect/set,
the Turn channel) -- the bindings.txt format is a compatibility surface and was not touched.
Coverage audit: **61 of 72 addresses on the keyboard**, and the 11 absent ones are exactly the
addresses with no authored function (0x16/0x17/0x1E/0x1F, 0x38-0x3E). 0x19 is bound (F8) purely
for column completeness. All 72 stay clickable on the panel.
The scheme lands on BT's addresses unreasonably well: 1-4 + QWER are the whole coolant system
(Condensers 1-6, flush, balance), F6/F7 the display and control-mode cycles, F9-F12 Generators
A-D, F4 the crouch button reconstructed earlier today.
**The deliberate cost.** A bound key is removed from the 1995 typed-hotkey channel, and this board
binds nearly everything -- so 5 (Quad), z (Eng1), t/y/u/i/o (pilot select) and +/- (zoom) go.
Documented in the file header, the markdown and the page.
**New rule: a bindings row WINS over a built-in convenience key.** `PadRIO::KeyHasBinding` --
V and J/K/L are board buttons now, so the view-toggle and preset-cycle polls stand down for them
(they poll GetAsyncKeyState directly and would otherwise have fired BOTH). View toggle lives on
BACKTICK alone.
**CONTROLS.MAP mirrored** so glass/pod/dev still feel identical (the 2026-07-21 settlement):
90 bindings, 0 parse complaints under BT_INPUT_LOG.
### Hat labels: 0x42 is TORSO CENTER [T1]
INPUT_PATH_AUDIT flagged 0x41-0x44 as mislabelled. Settled from the streamed mapping
(BT_CTRLMAP_LOG): `elem 66 -> subsys 17 attrID 14` = the TORSO subsystem, while 0x44/0x43/0x41 go
to the mapper's look trio (attrID 10/11/12 = LookLeft/LookRight/LookBehind). The .RES itself has
no "TORSO CENTER" string -- its names are LookBehind/Down/Forward/Left/Right -- so the audit's
wording was loose but its substance right. Swept L4GLASSWIN / L4PADPANEL / L4VB16.
### The controls page
`docs/CONTROLS.html` -- interactive keyboard (hover any key for its address and meaning, with the
MFD clusters outlined), pad diagram, panel map, the under-glass press-target figure, the radar
placement thumbnails, and the environ.ini table. Modelled on RP412's page, rewritten for BT's
addresses and hazards (the coolant-valve detent warning, jams and ammo-bay fires, BASIC control
mode). mkdist wraps the fragment in a doctype/charset shell and ships it beside CONTROLS.txt.
### Verified
- bindings.txt regenerates and loads: 74 keys, 0 parse errors; CONTROLS.MAP 90 bindings, 0 errors.
- 72/72 panel addresses still dispatch on click.
- surround / exploded / dock / pod / dev: boot + simulate, zero faults.
- Dist carries CONTROLS.txt + a 46 KB CONTROLS.html with doctype and charset.