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:
+246
-3
@@ -385,9 +385,10 @@ on the pod monitor. This reuses the plasma-window CPU→window pattern and sides
|
||||
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()`). Default ON under
|
||||
`-platform glass` (preset putenv in `btl4main.cpp`); `=0` restores the legacy single pad panel
|
||||
+ docked gauge strip. Wired at three seams:
|
||||
**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
|
||||
@@ -439,3 +440,245 @@ Verified after the fix (`BT_GLASS_LOG`): `[glass] 'Heat MFD' port=Heat mask=0x40
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user