Operator decision: the player base is not on Windows 11, so the RGB keyboard
mirror should not be shipped or promised. The feature is NOT deleted -- Cyd's
implementation stays intact behind the gate for whoever wants it -- it is
simply off by default and invisible:
* PadRIO gate flips from opt-out (BT_KEYLIGHT=0 disables) to OPT-IN
(BT_KEYLIGHT=1 enables). Default boots produce zero keylight lines, and a
stub build and a real build now behave identically for players: nothing.
* Removed from every user-facing artifact: the README paragraph, the
CONTROLS.md section, the CONTROLS.html footer line.
* environ.ini template: the entry stays (it is the discoverable home for the
opt-in) but now says OFF-unless-1, Windows 11 22H2+, and why it is
unshipped.
* mkdist's cut-time stub warning removed -- it existed to protect a README
promise that no longer exists.
* context/glass-cockpit.md records the decision, the opt-in, and the
build-time SDK stub gate in one place.
VERIFIED: default boot logs zero keylight lines; BT_KEYLIGHT=1 engages the
feature (the stub on this machine: its one honest log line + the key map).
All three console suites pass; checkctx CLEAN.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
560 lines
42 KiB
Markdown
560 lines
42 KiB
Markdown
---
|
||
id: glass-cockpit
|
||
title: "Glass Cockpit — the desktop developer layer (BT_GLASS / BT_STEAM)"
|
||
status: living
|
||
source_sections: "plan 2026-07-17; CMakeLists.txt gates; BT412 prior art (reference only)"
|
||
related_topics: [pod-hardware, build-and-run, multiplayer, gauges-hud]
|
||
key_terms: [PadRIO, miniconsole, glass-cockpit]
|
||
open_questions: []
|
||
---
|
||
# Glass Cockpit — the desktop developer layer
|
||
|
||
**Purpose:** developer tooling so game developers can test their work WITHOUT booting a Tesla II
|
||
pod — and stay far enough out of the game that nothing a real pod requires can break. NOT a
|
||
consumer product (GDI-grade UI is the ceiling). Pod fidelity outranks desktop convenience in
|
||
every trade-off.
|
||
|
||
## ONE unified build (2026-07-21; the compile-gate split is RETIRED)
|
||
|
||
The pod/glass/steam **compile** split (three build dirs, three exes) is gone — it caused
|
||
version skew (a stale steam exe shipped 2 days behind), tripled build time, and the
|
||
"clicks silently dead in the wrong build" incident. Now **everything compiles into the one
|
||
`build/` exe** and behavior is selected at RUNTIME:
|
||
|
||
| Runtime gate | Arms |
|
||
|---|---|
|
||
| `BT_PLATFORM=glass` | the desktop-cockpit profile: `L4CONTROLS=PAD` → PadRIO (clickable cockpit buttons, bindings.txt keymap), plasma window, and the miniconsole menu on a zero-arg launch |
|
||
| `BT_STEAM_NET=1` | the Steam transport (FakeIP/SDR; degrades to Winsock without the Steam client). `steam_api.dll` is **DELAY-LOADED** (`/DELAYLOAD` + delayimp) — machines without the DLL run everything else |
|
||
| (nothing) | **GLASS is the desktop DEFAULT (2026-07-22)** — a bare boot gets PadRIO/bindings.txt (and the menu on zero args). The old accidental-DEV trap ("the old keymap again") is closed: every launcher that forgot the env used to boot DEV (play bats, steam bat, the operator console — each needed the same one-line fix). |
|
||
| `BT_PLATFORM=dev` | the legacy DEV profile (keyboard-only, the bring-up drive bridge) — explicit opt-in for debugging |
|
||
| `BT_PLATFORM=pod` | the cabinet profile. **CONTRACT: the real pod's SETENV.BAT must set this** — the old "bare boot == cabinet shape" assumption no longer holds. |
|
||
|
||
- The `BT_GLASS` macro is now ALWAYS defined (a seam marker only — the `#ifdef` sites remain
|
||
as documentation of the layer boundary). Every gated code path was audited: all are
|
||
instance-guarded (`PadRIO::activeInstance`, `steamActive`) or env-gated at runtime.
|
||
- **`BT_STEAM` stays a COMPILE gate (the LICENSE gate, default OFF, restored 2026-07-21):**
|
||
the Steamworks SDK terms bar a public release with Steam inside, so a release configure
|
||
must be able to exclude it entirely — no SDK headers, no `steam_api.lib`, no DLL. The dev
|
||
checkout configures `build/` once with `-DBT_STEAM=ON`. `mkdist.py` reads the answer from
|
||
`build/CMakeCache.txt`: steam-OFF zips carry no `play_steam.bat`, no `steam_api.dll`
|
||
(including a STALE one from an earlier ON build), a README with the steam section stripped,
|
||
and a `-nosteam` name suffix so the two flavors never clobber. Verified both flavors:
|
||
OFF exe has no steam bits + `BT_STEAM_NET=1` is inert; ON exe degrades to Winsock.
|
||
- **Menu gate (amended 2026-07-22):** the miniconsole menu runs on a zero-arg GLASS boot —
|
||
and glass is now the desktop default, so a bare double-click of btl4.exe opens the menu
|
||
(the desired desktop UX). Cabinet protection moved from "bare boot never menus" to the
|
||
**SETENV.BAT contract**: the real pod sets `BT_PLATFORM=pod` (no menu, wait-for-egg);
|
||
`BT_PLATFORM=dev` also keeps the old bare-boot shape.
|
||
- Verified matrix (2026-07-21): bare boot ≡ old pod exe (same DEV profile, same egg-load path);
|
||
glass + scripted click → ConfigureMappables end-to-end; exe runs WITHOUT steam_api.dll
|
||
present; glass zero-arg → menu; 2-node loopback MP (145 ticks each); `BT_STEAM_NET=1`
|
||
without Steam → "staying on Winsock" + game runs.
|
||
- **Convention:** every `BT_*` name is a RUNTIME env gate (`BTEnvOn`) — with TWO exceptions,
|
||
both compile gates: `BT_STEAM` (the license gate above) and **`BT_EXPIRE` (2026-07-22, the
|
||
tester-build expiry tripwire)** — default ON, the exe refuses to start `BT_EXPIRE_DAYS`
|
||
(default 14) after its build DAY (`btversion.h BT_BUILD_UNIX`, UTC-midnight-rounded so the
|
||
only-on-change header churns once per day, not per build). Boot logs `[boot] test build
|
||
window: N day(s) left`; expired = log + MessageBox + exit. Disable: `-DBT_EXPIRE=OFF`
|
||
(mkdist marks such a zip `-noexpire` + warns; README expiry note is `{EXPIRE}`-marker-gated).
|
||
Quality gate, not DRM — a clock rollback defeats it. Verified both paths live
|
||
(`-DBT_EXPIRE_DAYS=0` force-expire run). Do not add another compile gate without updating
|
||
this file.
|
||
- No weak-linkage/optional symbols — `/FORCE` turns unresolved externals into runtime AVs
|
||
([[reconstruction-gotchas]]); everything links always, so this class of trap no longer varies
|
||
by build flavor.
|
||
|
||
## Layout modes — ONE resolver (2026-07-26) [T2 four-mode boot-verified]
|
||
|
||
Where the five MFDs + radar go is a SEPARATE axis from the platform profile. Under a glass boot
|
||
(`BT_DEV_GAUGES` set) `btl4main.cpp` resolves it **once**, right after the profile putenvs land,
|
||
into `glassLayout`; the boot banner, the pad-panel decision and the window-sizing block all read
|
||
that one answer. Precedence, highest first:
|
||
|
||
| Env | Layout | Buttons live in |
|
||
|---|---|---|
|
||
| `BT_GLASS_PANELS`≠0 | per-display cockpit windows (the "exploded" view, `L4GLASSWIN`) | each display's own window |
|
||
| `BT_DEV_GAUGES_WINDOW` | the legacy separate MFD window | the single combined pad panel |
|
||
| `BT_DEV_GAUGES_DOCK` | the docked bottom gauge strip | the single combined pad panel |
|
||
| `BT_COCKPIT=0` | ...also the docked strip (the documented opt-out) | the single combined pad panel |
|
||
| (nothing) | **COCKPIT SURROUND — the glass default** | the main window (D3D-drawn lamps) |
|
||
|
||
The banner now names the live one: `[boot] platform profile: GLASS (PadRIO + plasma window)
|
||
[secondary displays: cockpit surround]`.
|
||
|
||
⚠ **Two bugs this replaced** (both were live until 2026-07-26): the mode was decided in TWO places
|
||
with duplicated precedence (the glass profile block picked `BT_PAD_PANEL`/`BT_GLASS_PANELS`; the
|
||
sizing block re-derived cockpit-vs-dock), and the banner read NEITHER — it announced "per-display
|
||
cockpit windows [BT_GLASS_PANELS]" for every glass boot, surround included. Worse, the profile
|
||
block turned `BT_COCKPIT=0` into `BT_GLASS_PANELS=1`, so the docked strip it is documented to
|
||
select was **unreachable under the glass profile**. `BT_GLASS_PANELS` is now explicit-only, and
|
||
dock/window modes auto-raise `BT_PAD_PANEL` so the 72-button field always has a home.
|
||
|
||
## ONE button-bank geometry — L4RIOBANK (2026-07-26) [T2 click-verified both modes]
|
||
|
||
The RIO button field around a pod display is drawn TWICE — composited into the main window
|
||
(surround, `L4VB16.cpp`) and one window per display (exploded, `L4GLASSWIN.cpp`) — and each
|
||
renderer used to carry its own copy of the geometry. They drifted badly: the same MFD button was
|
||
a 156×138 under-glass target in the exploded window and a **76×24 sliver entirely outside the
|
||
glass** in the surround. `engine/MUNGA_L4/L4RIOBANK.cpp` is now that geometry, once; both
|
||
renderers are consumers, and placement (WHERE each display goes) stays with each renderer because
|
||
that genuinely differs.
|
||
|
||
**The under-glass rule** (ported from RP412 `L4MFDVIEW`, which took it from the pod): a button
|
||
reaches **half the glass** in BEHIND the display and only a **lamp strip** clears the edge. The
|
||
renderer paints buttons first, imagery over them — so the lamp reads as a slim bar, practically
|
||
the whole display is the press target, and nothing is occluded (the picture wins every pixel it
|
||
covers). The strip scales off the display's SHORT axis against the native 480 (an MFD is 640×480
|
||
landscape, the map 480×640 portrait — both short-axis 480) with a 6px floor, so a half-scale
|
||
surround lamp is still a bar and not a smudge. The MFD per-column nudge against the unevenly
|
||
spaced DISPLAY/PROGRAM legends (`mfdColumnDX`) moved here too, so both renderers inherit the
|
||
same alignment.
|
||
|
||
⚠ **The map's foot row is pushed FIRST**, deliberately: the side columns now reach half the map
|
||
each, so they cover the foot band too, and the hit test takes the first match — foot-first is
|
||
what keeps 0x16/0x17/0x1F/0x1E reachable. That class of bug is why the verification below tests
|
||
first-hit reachability, not mere presence.
|
||
|
||
**Verified 2026-07-26 [T2]:** `BT_RIOBANK_LOG=1` dumps every bank's rects;
|
||
`scratchpad/checkbank.py` reports the per-bank census and proves no address is SHADOWED (has a
|
||
point no earlier button covers); `scratchpad/clickbank.py` then posts a real click at every
|
||
button's centre. Both modes: **72/72 addresses placed, zero shadowed, zero duplicated, 72/72
|
||
dispatched** (`[cockpit] CLICK` / `[glasswin] CLICK`), process survived 144 posted messages per
|
||
mode. Pod + dev profiles boot and simulate un-regressed. Surround BT_SHOT confirms the legends
|
||
are clear and the lamps read as edge strips.
|
||
|
||
## The cockpit SCALES — uniform-fit letterbox + `-fit` (2026-07-26) [T2 live]
|
||
|
||
The canvas is a fixed size and D3D9 stretched it into whatever the client area was, so a window
|
||
dragged to a different shape **squashed the instruments** — the projection was aspect-corrected
|
||
(task #20) but the panels, lamps and MFD glass were not. Now the canvas is fitted at ONE uniform
|
||
scale, centred, leftover black: the cockpit scales both ways and never distorts, whatever the
|
||
window.
|
||
|
||
- `BTCockpitFitRect(clientW, clientH, RECT*)` — integer math throughout, so the rect is stable
|
||
frame to frame (a float scale wobbles a pixel and the bars shimmer during a drag).
|
||
- D3D9 does the scaling at Present via a **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 whether that
|
||
happened; when 0 the old full-client stretch stands, so an MSAA run still works.
|
||
- Both present sites take the rect (scene + the wait-screen overlay, which paints into the same
|
||
backbuffer). `WM_SIZE` invalidates so the bars repaint black — a COPY present only writes its
|
||
dest rect, so stale bar content would otherwise persist.
|
||
- **The click transform had to follow.** `BTCockpitMouseDown` mapped client→canvas against the
|
||
full client; under the letterbox that drifts the hit test off every button by the bar width.
|
||
It now runs the identical fit (and a click on a bar hits nothing).
|
||
- **World aspect** under the letterbox is the view rect's OWN aspect — the client no longer
|
||
enters into it.
|
||
- **`-fit`** (alias `-windowed-fullscreen`): borderless `WS_POPUP` over the monitor, canvas
|
||
letterboxed inside. Verified on a 3440×1440 ultrawide.
|
||
|
||
⚠ **Ordering trap found live:** the letterbox flag was first set at device creation, but the
|
||
first `WM_SIZE` arrives BEFORE the device exists — a `-fit` boot logged `aspect=3.14` (the
|
||
stretch formula) and applied it on frame 1, because nothing resizes the window again. The INTENT
|
||
is now decided in `btl4main` alongside `gBTGaugeCockpit`; `L4VIDEO` only confirms or withdraws it.
|
||
|
||
⚠ **Capture note:** `PrintWindow` returns an all-black frame for the borderless `-fit` window
|
||
(it works for the normal chrome'd one). Use a screen-region grab (`scratchpad/shot.ps1`) for
|
||
`-fit`, and don't read a black PrintWindow there as a render failure — it isn't.
|
||
|
||
Verified: wide (1684×661) and tall (744×961) drags both letterbox with zero distortion; **72/72
|
||
buttons still dispatch through the transform at 0.62× scale with a 389px bar**; exploded / dock /
|
||
pod / dev all keep DISCARD and boot un-regressed.
|
||
|
||
## Player-tunable displays — scale, map placement, environ.ini (2026-07-26) [T2 live]
|
||
|
||
The pod bolted its instruments down at one size; a desktop panel has room to trade viewscreen for
|
||
instrument, so the player scales them. All resolved ONCE (`BTCkResolvedSizes`, L4VB16.cpp) because
|
||
the surround recomputes its layout every frame.
|
||
|
||
| Env | Effect |
|
||
|---|---|
|
||
| `BT_MFD_SCALE` | all five MFDs, % of pod size (25-200, default 100) |
|
||
| `BT_MFD_SCALE_UL/_UC/_UR/_LL/_LR` | one display, overriding the group |
|
||
| `BT_RADAR_SCALE` | the portrait map |
|
||
| `BT_RADAR_POS` | `CENTER` (default) / `LEFT` / `RIGHT` / `MIDLEFT` / `MIDRIGHT` (+ `BOTTOM*`, `*CENTER` spellings) |
|
||
|
||
**The bands derive from the resolved sizes** — that is why the sizes could not stay constants: the
|
||
surround 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`, since it overlaps the view), and
|
||
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` exactly; 150% gives `L436 R436 T343 B448`;
|
||
80% + MIDLEFT gives `L256 R212 T175 B148`.
|
||
|
||
⚠ **A corner map goes flush to the CANVAS edge**, not the view edge, and the lower MFD whose corner
|
||
it took sits beside it. Measuring off the view edge (first cut) overlapped them by the band width —
|
||
232px of map drawn over the Right Weapons MFD.
|
||
|
||
**Map legend grid — MEASURED, not inherited [T2].** The side columns must line up with the six
|
||
cells the map paints beside them (⊕MAP / ⊖MAP / IR / crouch / searchlight / display-mode =
|
||
0x10-0x15 — the imagery independently confirms our address map). `scratchpad/measurelegend.py` over
|
||
a native 480×640 capture gives **top 3, cell 102, pitch 107** on the 640 span. RP412's map is
|
||
`13 + 6×102 @ pitch 105` — same cell height, different top and pitch, so **its numbers do not
|
||
transfer**; ours are ours. Our old even division (`displayH/6 + 1 = 107`) had the pitch right by
|
||
luck and sat 3px high of the labels. Top and bottom are now scaled separately and subtracted, so
|
||
rounding cannot drift the buttons out of step with the labels down the column.
|
||
|
||
**`environ.ini` (the settings file) [T2].** ⚠ It was read ~300 lines into `WinMain`, AFTER the
|
||
platform-profile block had 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 worked as a real env var. It also `putenv`'d comment lines verbatim, which would
|
||
have turned a shipped commented-out option into a variable literally named `#BT_MFD_SCALE`. Now:
|
||
loaded immediately after the first-breath boot line, comments and blanks skipped, and **the real
|
||
environment WINS** so a launcher `.bat` overrides the file rather than fighting it. A documented
|
||
default is written on first run when absent — the `bindings.txt` convention: untracked, so an
|
||
extract-over-top upgrade never clobbers a player's settings, and every option ships commented out
|
||
so a fresh install behaves exactly as before. Verified all three: first run writes it and applies
|
||
0 settings; uncommented file settings drive the layout (130% group + 60% UC + MIDRIGHT); a real
|
||
`BT_MFD_SCALE=90` overrides the file's 130 while the file's other 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 un-regressed.
|
||
|
||
## Dead-button backlog CLOSED — DuckRequest was the last (2026-07-26) [T2 live]
|
||
|
||
The 2026-07-20 audit's "8 buttons dispatch a streamed message with no reconstructed handler" is
|
||
finished. ⚠ Only ONE of the eight was actually still missing when this pass started — generator
|
||
on/off, ToggleSeekVoltage, EjectAmmo, ToggleCooling and BalanceCoolant had all landed between
|
||
07-20 and 07-25 while `pod-hardware.md` / `open-questions.md` still called them dead
|
||
(`docs/INPUT_PATH_AUDIT.md` had already flagged the census as "stale in both directions").
|
||
**Check the code, not the census.**
|
||
|
||
**`Mech::DuckRequest` @0049fa00 (id 0x1a, RIO 0x13 — the manual's CROUCH button).** The binary's
|
||
entire body is two lines: press-only (`msg+0xc > 0`), then `duckState`(`mech+0x398`) = 1. It is a
|
||
one-shot REQUEST flag, not a posture toggle — the handler never clears it and the only other
|
||
writer in the whole binary is the mech reset (part_012.c:9439, the same reset that zeroes
|
||
`incomingLock`). Registered in `Mech::MessageHandlerEntries`; `[duck]` log line on press.
|
||
|
||
**Its consumer is a DATABINDING, not code [T1] — which is why the flag looked pointless.**
|
||
`duckState` has ZERO readers anywhere in the decomp, because it is published as attribute 0x37 and
|
||
consumed through the gauge system: `content/GAUGE/L4GAUGE.CFG` runs a 3-frame `bduck.pcc`
|
||
`oneOfSeveralPixInt` widget bound to `DuckState` on the map's legend column ("crouch mode: button
|
||
4"). Verified live: pressing 0x13 turns that legend icon grey→orange. **Do not invent a crouch
|
||
pose to "finish" this** — the flag plus the indicator IS the code side.
|
||
|
||
Bonus confirmation: those legend widgets sit at gauge offsets 537/430/322/215/108 — a **107
|
||
pitch**, independently corroborating the map legend grid measured from pixels in §Player-tunable
|
||
displays.
|
||
|
||
## THE KEYBOARD IS THE BUTTON BOARD — new default keymap (2026-07-26) [T2]
|
||
|
||
Ported from RP412's bindings design (user decision: make it the DEFAULT, not an alternate). The
|
||
letter and number rows are the MFD button banks **laid out where they sit on the panel**, and
|
||
flight moves to the NUMPAD so the board stays free:
|
||
|
||
| Keys | Bank |
|
||
|---|---|
|
||
| `1 2 3 4` / `Q W E R` | Heat / coolant 0x2F-0x2C / 0x2B-0x28 |
|
||
| `5 6 7 8` / `T Y U I` | Engineering (Mfd2) 0x27-0x24 / 0x23-0x20 |
|
||
| `9 0 - =` / `O P [ ]` | Comm / target hotbox 0x37-0x34 / 0x33-0x30 |
|
||
| `A S D F` / `Z X C V` | Left Weapons (Mfd1) 0x0F-0x0C / 0x0B-0x08 |
|
||
| `H J K L` / `N M , .` | Right Weapons (Mfd3) 0x07-0x04 / 0x03-0x00 |
|
||
| `F1`-`F6` / `F7`-`F12` | the map's two columns 0x10-0x15 / 0x18-0x1D |
|
||
| numpad | 8/2/4/6 stick, 7/9 pedals, 5 all-stop, 0/1/3/. the four fire buttons |
|
||
| `Shift`/`Ctrl` · `Alt` · arrows · `` ` `` | throttle lever · reverse · look/torso-centre · view |
|
||
|
||
`G` and `B` stay unbound — the physical gap between the lower clusters. **Coverage: 61 of 72
|
||
addresses on the keyboard, and the 11 absent ones are exactly those with no authored function**
|
||
(0x16/0x17/0x1E/0x1F column gaps, 0x38-0x3E intercom/door). All 72 remain clickable.
|
||
|
||
It lands on BT's addresses better than it deserves to: `1-4`+`QWER` are the ENTIRE coolant system
|
||
(Condensers 1-6, flush, balance), `F6`/`F7` the display/control-mode cycles, `F9`-`F12` Generators
|
||
A-D, `F4` the crouch button.
|
||
|
||
⚠ **What it costs, deliberately.** A bound key is removed from the authentic 1995 typed-hotkey
|
||
channel to stop 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.
|
||
|
||
⚠ **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 and the view toggle lives on `` ` `` alone. Without this they would have fired BOTH.
|
||
|
||
`content/CONTROLS.MAP` was rewritten to mirror the board (90 bindings, 0 parse complaints) so the
|
||
glass/pod/dev boots still feel identical — the 2026-07-21 settlement. An existing `bindings.txt`
|
||
is never overwritten, so a player only meets this board by deleting theirs.
|
||
|
||
## Hat labels corrected — 0x42 is TORSO CENTER [T1, streamed mapping]
|
||
|
||
`docs/INPUT_PATH_AUDIT.md` flagged the 0x41-0x44 labels as wrong and it was right. Settled from
|
||
`BT_CTRLMAP_LOG`: **0x42 → subsys 17 (Torso), attrID 14** — not a look at all; the other three go
|
||
to the mapper's look trio (0x44 → attrID 10 LookLeft, 0x43 → 11 LookRight, 0x41 → 12 LookBehind).
|
||
The shipped `.RES` has no "TORSO CENTER" string (its names are LookBehind/Down/Forward/Left/Right),
|
||
so the audit's phrasing was loose but its substance correct. Swept the three UI tables
|
||
(`L4GLASSWIN`, `L4PADPANEL`, `L4VB16`): 0x42 is now "Torso Ctr", 0x41/0x43/0x44 "Look Bk/R/L".
|
||
|
||
## RGB keyboard lamp mirror — L4KEYLIGHT (2026-07-26) [T2 live, claimed a real board]
|
||
|
||
Ported from RP412 (itself vRIO's `KeyboardLampMirror`). Keys bound to a lamp address in
|
||
`bindings.txt` glow with the panel palette through **Windows Dynamic Lighting** — yellow for the
|
||
map's side columns (0x10-0x1F), red for the rest — flashing in step with the on-screen buttons
|
||
(its `LampLevel` copy matches the FIXED `BTLampBrightnessOf`). Per-key boards light each bound
|
||
key; zone-lit boards mirror the strongest lamp board-wide. All WinRT runs on a private worker
|
||
thread. Gate: **OPT-IN since 2026-07-26** -- `BT_KEYLIGHT=1` enables (operator decision: the
|
||
player base is not on Windows 11, so the feature was unshipped -- removed from README/
|
||
CONTROLS.* -- rather than promised); default is OFF and produces zero log lines. Also note
|
||
the CMakeLists BUILD gate: an SDK older than 10.0.22000 compiles a dormant five-function
|
||
stub (this build machine does), so shipping the real mirror requires a newer-SDK build.
|
||
No Dynamic Lighting at runtime = one log line, then dormant.
|
||
|
||
⚠ **RP412's packing hazard does NOT apply here.** It compiles that 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
|
||
(`/std:c++17 /permissive-`, per-file in CMakeLists, since the project otherwise builds C++14
|
||
/permissive). The scalars-only interface is kept anyway so the isolation survives if packing is
|
||
ever added.
|
||
|
||
Wired in `PadRIO`: map from `bindings.keyBindings` (ActionButton binds only, first-binding-wins
|
||
per key), fed from `PadRIO::SetLamp`, stopped in the dtor (which hands the LEDs back to Windows).
|
||
Verified: `[keylight] mirroring 25 bound key(s)` + claimed this machine's 24-zone keyboard;
|
||
`BT_KEYLIGHT=0` and the pod profile produce zero keylight lines and run clean.
|
||
|
||
## Lamp flash decode was wrong — fixed (2026-07-26) [T1, from the L4RIO.h enum]
|
||
|
||
`BTLampBrightnessOf` (l4vb16.h) returned `max(state1, state2)` and blanked to 0 on the alternate
|
||
phase. The engine's own enum [T0] says otherwise: bits 0-1 = flash mode, bits 2-3 = **state 1**
|
||
brightness, bits 4-5 = **state 2** brightness (0 off / 1 dim / 3 bright) — solid shows state 1,
|
||
flashing ALTERNATES the two at 500/250/125 ms. The old formula agrees only when one state is Off,
|
||
which is why it survived: the Panic lamp is `flashFast+state1Off+state2Bright` and L4LAMP's other
|
||
pulse is off/dim. But **`L4LAMP.cpp:252` commands `flashFast + state1Dim + state2Bright`** — a
|
||
dim→bright pulse that rendered as a hard bright→off blink. RP412's `L4MFDVIEW::LampLevel` has the
|
||
faithful formula; that is now ours. THREE copies of the decode existed (l4vb16.h, L4GLASSWIN,
|
||
L4PADPANEL), all three with the bug; the two locals now forward to the one inline.
|
||
|
||
## The pre-mission WAITING SCREEN (2026-07-22) [T2]
|
||
Every pre-run state (seat wait, roster full, WaitingForEgg, LoadingMission, WaitingForLaunch)
|
||
paints an animated overlay -- Consolas green text + a 12-segment spinner phased on
|
||
GetTickCount/80 (`BTWaitScreenPaintDC`, L4VIDEO.cpp) -- so a joining player never sees "Not
|
||
Responding" or a frozen frame. Two paint paths: PRE-DEVICE (relay seat wait, L4NET
|
||
`RelayWaitTick`) paints the window DC directly (nothing else presents -- safe); ONCE THE
|
||
DEVICE EXISTS (`DPLRenderer::ExecuteIdle`) it must NOT paint after Present (the black D3D
|
||
frame alternating with GDI = the load-time flicker, user-reported) -- and the backbuffer
|
||
refuses `GetDC` (D3DERR_INVALIDCALL without D3DPRESENTFLAG_LOCKABLE_BACKBUFFER, which we
|
||
won't force game-wide), so the overlay GDI-paints into a cached OFFSCREEN PLAIN surface
|
||
(`mWaitOverlaySurface`, GetDC always legal there) and `StretchRect`s onto the backbuffer
|
||
BEFORE Present. D3DPOOL_DEFAULT: released at every device-Reset site. One-shot boot log
|
||
`[waitscreen] paint path:` says which path is live. Verified: 14 rapid PrintWindow frames
|
||
all carry the text (was alternating black).
|
||
|
||
## PadRIO gotcha: release-on-disconnect (issue #24, 2026-07-23) [T3 -- awaiting live verify]
|
||
XInput button emission is EDGE-based (`held != was_held` vs `previousPadButtons`). The
|
||
disconnect path zeroed `previousPadButtons` WITHOUT emitting release edges -- any button held
|
||
at the moment a controller (wrapper) died stayed pressed game-side FOREVER, and the zeroing
|
||
also erased the memory a reconnect would have released from. Field signature: PS3-pad-in-
|
||
xbox-mode wrapper died mid-hold -> look-view latched (stuck side view + pitch-down, no
|
||
recovery; keyboard-only run clean). Fix: on the connected->disconnected transition, emit
|
||
`EmitButton(address, 0)` for every binding whose mask was held, THEN forget. Not reproducible
|
||
headless (needs real hardware unplug) -- logic fix, awaiting the reporter's controller.
|
||
|
||
## Direct-axis release edge (issue #36, 2026-07-23) [T2 idle-verified; awaiting reporter]
|
||
The pad/joy DIRECT axis write was gated on `raw != 0` -- centering inside the deadzone stopped
|
||
writing and the channel LATCHED (fatal on spring-less channels: the Turn composite kept the
|
||
mech turning after release). Fix: per-binding previous-value arrays (`previousPadAxisRaw` /
|
||
`previousJoyAxisRaw`); the outside->inside transition writes 0 ONCE then yields the channel
|
||
(composition preserved; slew bindings excluded -- a slewed lever stays put by design). Plus
|
||
the #24 rule, axis edition: disconnect-while-deflected releases direct channels (Throttle
|
||
keeps the last lever position -- freeze beats all-stop mid-fight).
|
||
|
||
## CONVENTION: bindings.txt format is a COMPATIBILITY SURFACE (2026-07-24)
|
||
Players customize `contentindings.txt` (keys, pad, joystick/HOTAS rows) and the file is
|
||
NOT shipped in the zip (untracked -> survives extract-over-top upgrades). That only works if
|
||
the grammar stays stable across versions, so:
|
||
- **Never rename or repurpose** existing verbs (`key`/`pad`/`padaxis`/`joydev`/`joyaxis`/
|
||
`joybutton`/`joyhat`), channel names (`Throttle`/`JoystickX`/`JoystickY`/`LeftPedal`/
|
||
`RightPedal`/`Turn`), pad/axis token names, or option words (`invert`/`slew`/`deflect`/
|
||
`set`/`deadzone`). A parse change that rejects a previously-valid line silently breaks a
|
||
player's setup mid-upgrade.
|
||
- **New features = NEW row types or NEW trailing options** (the parser already skips unknown
|
||
lines with a logged warning -- old builds degrade gracefully on newer files too).
|
||
- Semantics of existing rows must stay put (e.g. deflect = spring, slew = lever, direct pad
|
||
axis = outside-deadzone-owns + release edge). If a semantic MUST change, gate it behind a
|
||
new option word and keep the bare form as-was.
|
||
- The BT_JOYCONFIG wizard writes only between its `# >>> BT_JOYCONFIG` markers -- keep that
|
||
contract so hand edits and wizard reruns coexist.
|
||
|
||
## Generic joysticks / HOTAS / pedals -- DirectInput 8 layer (2026-07-23) [T2 rig-verified; awaiting stick hardware]
|
||
Tester ask: configure non-Xbox controllers. **`L4JOY.cpp/.h`** (all builds, lazy-init): DI8
|
||
enumeration of every non-XInput game device (up to 4), `c_dfDIJoystick2` polling with
|
||
`DIPROP_RANGE` ±32767 + reacquire-on-loss + 3 s hot-plug re-enum, normalized
|
||
`BTJoyDeviceState` {8 axes -1..1, 32 buttons, 4 POV hats}. **XInput exclusion** = the
|
||
documented wbem-free RawInput check (device path contains `IG_` → VID/PID blacklist vs
|
||
`guidProduct.Data1`) — LIVE-verified skipping a real XBOX360 pad. **Bindings**
|
||
(L4PADBINDINGS): `joydev <slot> [name-substr]` + `joyaxis <X|Y|Z|RX|RY|RZ|SL0|SL1> axis
|
||
<channel> [invert] [slew r] [deadzone f]` + `joybutton <0-31>` + `joyhat <0-3> <dir>` — rows
|
||
attach to the last joydev slot; unnamed slot binds attached device #slot. **PadRIO poll**
|
||
runs them through the SAME channel/event machinery (per-binding prev arrays → detach releases
|
||
automatically, the #24 rule); a direct joyaxis on Throttle maps full travel [-1,1]→[0,1] and
|
||
writes every frame (a lever OWNS the channel; slewHeld stays clear so the GAIT DETENT still
|
||
snaps a lever parked in the walk/run dead band). **Wizard** `BT_JOYCONFIG=1`
|
||
(`BTJoyConfigWizard`, console UI, `joyconfig.bat`): baselines all axes (a HOTAS lever rests
|
||
anywhere), captures the dominant mover per prompt (0.45 threshold, 250 ms hold, already-
|
||
assigned axes excluded), derives invert from the move direction (throttle from FINAL
|
||
position), auto-adds the hat look cluster, and rewrites bindings.txt between
|
||
`# >>> BT_JOYCONFIG` markers (rest of the file preserved). Sign conventions: JoystickX + =
|
||
twist right, JoystickY + = aim up (wizard binds flight-style forward=down via invert), Turn +
|
||
= right. Legacy `L4DINPUT.cpp DIJoystick` (L4CONTROLS=DIJOYSTICK, 1995 `Joystick` iface)
|
||
left untouched. Verified: grammar accept/reject by line, no-device + XInput-only polling
|
||
clean 30 s in-mission, zero DI cost with no joy rows. NOT yet verified: real stick axes
|
||
(no hardware here) — needs a tester with a stick.
|
||
|
||
## Plan of record (2026-07-17)
|
||
|
||
Working branch `glass-cockpit` (from master `e2c21c4`). Steps: (1) gates+scaffolding [THIS],
|
||
(2) PadRIO glass cockpit, (3) miniconsole self-launch, (4) Steam transport+lobby. Full plan:
|
||
the approved plan file; running detail: `docs/GLASS_COCKPIT.md`. The retired BT412 trees
|
||
(`C:\VWE\BT412`; local branch `local-abandoned-20260717`) are read-only reference — no code is
|
||
copied from them.
|
||
|
||
## Status
|
||
|
||
- **Step 1 (gates + scaffolding): DONE 2026-07-17.** CMake options + defines; 3-config
|
||
configure matrix + negative test + defaults build verified.
|
||
- **Step 2a (.CTL id audit): DONE 2026-07-17 [T2].** The streamed `.CTL` positional ids landed
|
||
every `MechControlsMapper` record ONE MEMBER LATE (our chain starts at 2, the binary's at 3)
|
||
— a latent REAL-POD bug (RIO throttle would drive pedalsPosition), masked on dev boxes by the
|
||
keyboard bridge. Fixed ungated: ids pinned to binary numbering + id-2 pad + static_assert
|
||
locks (`mechmppr.hpp/.cpp`); torso/weapon chains were already aligned. Permanent env-gated
|
||
diagnostic: `BT_CTRLMAP_LOG=1` dumps every streamed record's resolved member offset
|
||
(`L4CTRL.cpp CreateStreamedMappings`). Full evidence: `docs/GLASS_COCKPIT.md` §2a.
|
||
- **Step 2 (PadRIO glass cockpit): DONE 2026-07-17 [T2].** `RIOBase` seam (`L4RIO.h`,
|
||
unconditional) + gated `L4CONTROLS=PAD` → `PadRIO` (`L4PADRIO.*` XInput+keyboard,
|
||
`L4PADBINDINGS.*` `content\bindings.txt`); bridge stand-downs auto-yield to a live device
|
||
(`BTRIODevicePresent`, `BT_KEY_BRIDGE` unset=auto/0=off/1=on); on-screen panel `L4PADPANEL.*`
|
||
follows **vRIO** (`C:\VWE\vrio` CockpitLayout/PanelCanvas — 104 controls incl. the two hex
|
||
keypads → real KeyEvents; right-click latch); desktop plasma `L4PLASMAWIN.*`
|
||
(`L4PLASMA=SCREEN`, buffer is top-down); `-platform glass` preset + run.cmd token. Secondary
|
||
displays reuse the EXISTING dev-gauge modes (dock / `BT_DEV_GAUGES_WINDOW=1` window /
|
||
`_DOCK=1` overlay — [[gauges-hud]]); no new MFD code, no `L4VB16.cpp` exposure.
|
||
**Button-trap guard:** base `ConfigureMappableMessageHandler` no longer `abort()`s by default
|
||
(`BT_BUTTON_TRAP=1` restores) — each `[FAIL]` log = a missing L4 override to reconstruct.
|
||
Verified: authentic-path input (engine push → streamed `.CTL` → mapper), panel-click
|
||
survival, 2-node loopback MP un-regressed on the pod build. Detail: `docs/GLASS_COCKPIT.md`.
|
||
- **Trigger-config + one-keymap (2026-07-21, HUMAN-VERIFIED live).** The cockpit-surround
|
||
clicks only work when a `PadRIO` exists, and PadRIO only exists under the **GLASS platform
|
||
profile** (`BT_PLATFORM=glass` → `L4CONTROLS=PAD`); a plain launch falls to the DEV profile
|
||
(KEYBOARD) and `PadRIO::SetScreenButton` no-ops — every cockpit click silently discarded
|
||
(the "held PROGRAM, nothing happened" incident: `[cockpit] CLICK addr=0x8` logged but
|
||
ConfigureMappables id 9 never dispatched). `play_solo.bat` now sets `BT_PLATFORM=glass`
|
||
with the glass exe. The full regroup loop (hold PROGRAM → Mapping-mode flip suppresses
|
||
fire → fire-key taps toggle the binding → joystick-display dot moves → release commits)
|
||
is verified live. Diagnostics: `[cfgmap]` ENTER/EXIT (BT_FIRE_LOG, mechweap.cpp) +
|
||
`BT_BTNTEST="addr,on,off"` scripted screen-button harness (L4PADRIO.cpp Poll). **Keymap:**
|
||
which binding engine runs follows the same profile split — GLASS = PadRIO/`bindings.txt`,
|
||
DEV/pod = btinput/`CONTROLS.MAP` — which made the felt keymap flip with the boot flavor.
|
||
Settled 2026-07-21 (commit babf5b1): CONTROLS.MAP + the compiled-in default now mirror the
|
||
bindings.txt layout (numpad aim cluster, arrows=throttle/pedals, **Alt=reverse 0x3F**,
|
||
NumPad5=AllStop, Shift=throttle) so every boot flavor feels identical.
|
||
- **Step 3 (miniconsole): DONE 2026-07-17 [T2].** `game/glass/btl4fe.*` (menu + console-shape
|
||
egg writer, golden-tested vs MP.EGG) + `btl4console.*` (the marshal: a worker-thread console
|
||
CLIENT speaking the btconsole.py wire verbatim; `content\marshal.log`). ZERO engine hooks —
|
||
the engine runs the stock console ladder. Per-mission process relaunch; `BT_FE_*` env arms the
|
||
marshal (menu relaunches clear it). Verified: solo cycle (menu→mission→60s clock→stop→menu)
|
||
and menu-hosted 2-node LAN.
|
||
- **Step 4 (Steam): CODE COMPLETE 2026-07-18 [T2 single-machine].** See [[steam-networking]] —
|
||
the wire seam + `L4STEAMNET` identity-token transport + `btl4lobby`. Live 2-account
|
||
cross-machine session pending (`docs/STEAM_TEST.md`).
|
||
- **Input-coverage audit + keyboard reconciliation: DONE 2026-07-20 [T2].** Root cause of the
|
||
dead panel: `MechRIOMapper`'s message ids were chained one enum too high (0x19-0x2a) — the
|
||
binary RIO table @0051dd30 re-registers the BASE aux/zoom ids 3..0x13 (Hotbox 0x1a); fixed
|
||
ungated + static_assert-locked (`btl4mppr.hpp`) → all 24 MFD-bank + 2 zoom buttons live
|
||
(52/72 panel buttons worked at the time; the 8 dead ones are ALL wired as of 2026-07-26 —
|
||
`DuckRequest` was the last, see §Dead-button backlog closed; 12 have no streamed mapping
|
||
authored, authentically inert). **Keyboard = the ~20 core
|
||
gameplay actions on the CONTROLS.MAP keys; the PANEL covers every pod address by click**
|
||
(right-click = hold latch) — the merged default profile lives in `L4PADBINDINGS.cpp`
|
||
(auto-writes `content\bindings.txt`; W/S/A/D/Q/E/R/F/X drive, 1-4/Space/Ctrl fire, M/N/H/C =
|
||
pod buttons 0x18/0x15/0x2C/0x2F, `/V view toggle + J/K/L preset cycles hardcoded in PadRIO).
|
||
Bound keys are SUPPRESSED from the typed 1995 channel (`PadRIO::SuppressKey`, the btinput
|
||
pattern — ended the glass double-dispatch: 'w'=pilot-select, numpad/F-key key-up aliases);
|
||
unbound keys keep their authentic meanings (5/z presets, t-o pilot select, +/- zoom).
|
||
In BAS control mode the pod turns with the STICK (Q/E) and pedals are inactive — authentic;
|
||
A/D turn in MID/ADV. Arrow keys mirror the drive cluster (UP/DOWN = the W/S throttle lever,
|
||
LEFT/RIGHT = the A/D pedals). Full audit + census: `docs/GLASS_COCKPIT.md` §2026-07-20.
|
||
- **2026-07-20 live "keymap regression" report: STALE EXE [T2].** The human-reported glass
|
||
regressions (arrows/lever/detent dead, V/J/K/L/N inert, ground-fire) were a
|
||
`build-glass/Release/btl4.exe` built BEFORE 5dd3536 landed — relaunched without rebuilding.
|
||
No input-code change was needed; a clean rebuild + full SendInput re-verification (lever
|
||
holds + zero stop, MID turns, V toggle shot-pair, level boresight, pod smoke) closed every
|
||
symptom, and V's edge is now [T2]. `BT_MPPR_TRACE` now prints mode/pedals/stick/turn/elev.
|
||
Procedural rule: rebuild every live build dir after landing an input/engine commit.
|
||
Detail: `docs/GLASS_COCKPIT.md` §2026-07-20 (later).
|
||
- **2026-07-20 human re-test: GAIT DETENT ported to the PadRIO lever [T2].** The remaining
|
||
"our detent is missing" (mixed walk/run mode + repeated accel sample) was the gait DEAD BAND
|
||
([[locomotion]]): the pod-build keyboard lever snaps out of the walk/run hysteresis band at
|
||
key-rest (mech4.cpp GAIT DETENT), and the PadRIO throttle slew lacked it. Ported faithfully:
|
||
mech4.cpp publishes the band in lever units (`gBTGaitDetentLo/Hi` seam, +0.002 engage
|
||
margin), `L4PADRIO.cpp` applies the identical nearer-edge snap when no throttle slew key/pad
|
||
axis is active. Verified: parks at 0.414/0.465 snap to 0.358/0.504 (band = demand
|
||
22.02-30.87), no at-rest sample in-band, exactly ONE up-shift + ONE down-shift clip across a
|
||
full graduated sweep, zero-stop + pod build unaffected. Detail: `docs/GLASS_COCKPIT.md`
|
||
§2026-07-20 (later still).
|
||
A/D turn in MID/ADV. Full audit + census: `docs/GLASS_COCKPIT.md` §2026-07-20.
|
||
- **Per-display cockpit windows (`BT_GLASS_PANELS`): DONE 2026-07-20 [T2 startup-verified].**
|
||
The `-platform glass` preset now breaks the secondary displays out of the single combined
|
||
panel/strip into ONE desktop window PER pod display, each carrying that display's surface
|
||
WITH its RIO button bank placed around it, arranged pod-faithfully around the main view
|
||
(`engine/MUNGA_L4/L4GLASSWIN.*`, new TU). Windows: **Heat (UL) / Mfd2 (UC) / Comm (UR) /
|
||
Mfd1 (LL) / Mfd3 (LR)** each with 8 RED MFD buttons split 4 above / 4 below the surface;
|
||
**Secondary/Radar** (portrait `sec`) with the 12 YELLOW Secondary buttons split left/right;
|
||
a **Flight Controls** window for the no-display banks (throttle 0x38-3F, joystick/fire
|
||
0x40-47, panic). Each window is a GDI window (like [[gauges-hud]]'s pad panel + the plasma
|
||
window) — the surface is CPU-expanded from the shared gauge buffer
|
||
(`SVGA16::ExpandPlaneToBGRA`, no D3D) and `StretchDIBits`'d in, so the **whole D3D
|
||
dev-composite path (dock / separate window / overlay) stands down** while these are up
|
||
(`BTDrawGaugeInset`/`BTGaugeWindowRenderAndPresent` early-return; no `gBTGaugeDockBottom`
|
||
strip). Runtime gate **`BT_GLASS_PANELS`** (⚠ was "default ON under `-platform glass`" — NO
|
||
LONGER TRUE; the surround is the glass default and panels are explicit-only, see §Layout modes
|
||
below) — read by `L4PADRIO` (which panel to create),
|
||
`L4VB16` (suppress compositing), and `btl4main` (skip the world-window dock strip). The
|
||
display↔bank map (spine of the feature): Heat 0x28-2F, Mfd2 0x20-27, Comm 0x30-37,
|
||
Mfd1 0x08-0F, Mfd3 0x00-07, sec 0x10-1B ([[pod-hardware]] §Bank→MFD). Verified: build +
|
||
bounded glass launch — `[glasswin] per-display cockpit up (7 windows…)`, no crash, full
|
||
mission loop un-regressed; surfaces carry LIVE gauge content (`BT_GLASS_LOG` = per-surface
|
||
resolve/pixel diagnostic). Layout constants (surface sizes, exact button sub-positions,
|
||
yellow left/right split) are marked tunable in `L4GLASSWIN.cpp`. Detail: `docs/GLASS_COCKPIT.md`.
|
||
⚠ **Bug found + fixed during bring-up:** surfaces were blank because `application`/`ghWnd` are
|
||
duplicate-defined globals that bind non-deterministically under `/FORCE` — the fresh
|
||
`L4GLASSWIN` TU read `application == NULL` → gauge renderer NULL. Fix: reach the
|
||
renderer/window via `BTResolveGaugeRenderer()`/`BTResolveMainWindow()`, DEFINED in
|
||
`game/btl4main.cpp` off its real `btl4App`/`hWnd` pointers (never the globals directly). See
|
||
[[reconstruction-gotchas]] §6 duplicate-GLOBAL corollary.
|
||
|
||
- **Glass per-display panel button click-crash (Gitea #18): FIXED 2026-07-20 [T2].** Clicking a
|
||
per-display Engineering panel button (0x21 reported) on an **Eng page** hard-crashed — cdb: a
|
||
wild call through an uninitialised pointer (`eip=cdcdcdcd` debug / `0x01048748` release) with
|
||
**zero btl4 frames above `Receiver::Receive`**. Root cause was NOT a `/FORCE` unresolved
|
||
external (the glass link log carried only the known-benign `mech3.obj` `CreateStreamedSubsystem`
|
||
set) but a **dense message-handler-table GAP slot** ([[reconstruction-gotchas]] §11): the
|
||
streamed Eng-page `.CTL` dispatches subsystem msg **id 3 / 0xb** to whatever subsystem is shown,
|
||
and a **weapon (Emitter)** registers only PoweredSubsystem 4-8 + MechWeapon 9-10 — so id 3 is a
|
||
gap below `entryCount`, and `Receiver::MessageHandlerSet::Build`'s `new HandlerEntry[]` left it
|
||
as heap garbage → `(this->*garbage)(msg)`. The 1995 binary has the identical non-zeroing
|
||
`new[]` and survived only on fresh-heap-zero luck (id 3 on a weapon was always meant to be
|
||
IGNORED — it's a Condenser/Reservoir action in a different subsystem branch; the uniform
|
||
Eng-page button template makes buttons for unimplemented actions authentically inert). The new
|
||
per-display windows just made the id reachable live. **Fix (engine, class-wide, faithful):**
|
||
`Build` null-inits every slot (`entryID=0/entryName=""/entryHandler=NullHandler`) before
|
||
copying inherited / placing supplied, so a gap is deterministically NullHandler → `Receive`
|
||
drops it — the authentic "Receiver ignores unhandled messages", not a glass-path guard. Fixes
|
||
the entire dead-button class (#14) at once. Verified: click-soaked every MFD bank
|
||
(Engineering/L+R Weapons/Heat/Comm, ~130 clicks through Quad↔Eng page cycles) under cdb — ZERO
|
||
AVs, and the reconstructed mapper preset selects still fire (`[mode] preset (1,1)/(2,1)…`); pod
|
||
build + solo un-regressed. The `[glasswin] CLICK` forensics log (names the button before
|
||
dispatch) landed with this. Detail: `engine/MUNGA/RECEIVER.cpp` Build.
|
||
|
||
## Bindings migration (board-2, 2026-07-26)
|
||
|
||
`bindings.txt` files without the `# bindings-board 2` marker are migrated ONCE at load
|
||
(`PadBindingProfile::MigrateBindingsFile`, L4PADBINDINGS.cpp): rows matching the union of every
|
||
HISTORICAL default set (85 canonical rows, c2ee729..1cda880) are dropped as "the old board, not a
|
||
choice"; player-authored rows are carried and win over the new defaults (the losing default is
|
||
emitted commented `# (yours wins)`); the joystick wizard's marker-delimited section is preserved
|
||
verbatim; the original is saved as `bindings.old.txt`. Idempotent after (marker check). Operator
|
||
decision: eliminate the two-population fork the preserved-file convention had created -- there is
|
||
no "old layout" world anymore, only per-row customization. Restoring bindings.old.txt just
|
||
re-migrates: deliberate. Verified end-to-end with the real exe: carry/stand-down/wizard/backup on
|
||
an old-world file, byte-identical idempotency on reboot, marker-headed fresh generation.
|
||
|
||
## Key Relationships
|
||
- Extends: [[pod-hardware]] (RIO input path) · Uses: [[gauges-hud]] dev-gauge modes ·
|
||
Feeds: [[multiplayer]] (miniconsole hosting, Steam transport) · Build: [[build-and-run]]
|