RGB keyboard lamp mirror + the shipped controls reference
KEYBOARD LAMP MIRROR (L4KEYLIGHT). Ported from RP412, itself a port of 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: watcher, claim, and a 100 ms paint loop that repaints only on change. RP412's packing hazard does NOT transfer: it forces default struct packing there because its engine is /Zp1, which would break the WinRT ABI. BT411's BT_OPTS carries no /Zp, so only the dialect flags are needed -- /std:c++17 /permissive- per-file, 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 built 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). BT_KEYLIGHT=0 opts out; a machine without Dynamic Lighting logs once and stays dormant. Verified live: claimed this machine's 24-zone keyboard and mirrors 25 bound keys; BT_KEYLIGHT=0 and the pod profile produce zero keylight lines and run clean. SHIPPED CONTROLS REFERENCE. docs/CONTROLS.md carries the keyboard table plus the full 72-BUTTON POD MAP, grouped by the display each bank surrounds, with the coolant-valve detent warning (1-5-50-CLOSED, one press past max shuts the loop) and the jam/eject procedure. mkdist flattens it to ASCII as CONTROLS.txt at the zip root, the README's own idiom. players/README.txt gained pointers to it, to environ.ini, to -fit, and to the RGB mirror. KB: context/glass-cockpit.md and docs/GLASS_COCKPIT.md updated for the whole branch -- layout modes, L4RIOBANK and the under-glass rule, the letterbox fit and its ordering trap, player-tunable displays, the measured legend grid, the closed dead-button backlog, and this mirror. Verified: five-mode regression (surround/exploded/dock/pod/dev) boots and simulates with zero faults; mkdist writes a 5149-byte pure-ASCII CONTROLS.txt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+211
-4
@@ -63,6 +63,211 @@ version skew (a stale steam exe shipped 2 days behind), tripled build time, and
|
||||
([[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.
|
||||
|
||||
## 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: `BT_KEYLIGHT=0` opts out; no Dynamic Lighting = 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
|
||||
@@ -199,8 +404,9 @@ copied from them.
|
||||
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 now work; 8 dead await handler reconstruction — Gitea backlog issue;
|
||||
12 have no streamed mapping authored, authentically inert). **Keyboard = the ~20 core
|
||||
(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 =
|
||||
@@ -243,8 +449,9 @@ copied from them.
|
||||
(`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`** (default ON under `-platform glass`, `=0` falls
|
||||
back to the single pad panel + docked gauges) — read by `L4PADRIO` (which panel to create),
|
||||
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 +
|
||||
|
||||
Reference in New Issue
Block a user