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:
@@ -268,7 +268,16 @@ target_sources(munga_engine PRIVATE
|
||||
"engine/MUNGA_L4/L4RIOBANK.cpp"
|
||||
"engine/MUNGA_L4/L4GLASSWIN.cpp"
|
||||
"engine/MUNGA_L4/L4PLASMAWIN.cpp"
|
||||
"engine/MUNGA_L4/L4KEYLIGHT.cpp"
|
||||
)
|
||||
# RGB keyboard lamp mirror (Windows Dynamic Lighting). C++/WinRT needs C++17
|
||||
# AND conformance mode, while the rest of the project builds C++14 /permissive
|
||||
# -- so this ONE file gets its own flags. Its interface (l4keylight.h) is
|
||||
# scalars only, so nothing else has to change dialect. (RP412 additionally
|
||||
# forced default struct packing here because its engine is /Zp1; BT411 sets no
|
||||
# /Zp, so that part does not apply -- see the file header.)
|
||||
set_source_files_properties("engine/MUNGA_L4/L4KEYLIGHT.cpp" PROPERTIES
|
||||
COMPILE_OPTIONS "/std:c++17;/permissive-")
|
||||
target_include_directories(munga_engine BEFORE PRIVATE
|
||||
"${CMAKE_SOURCE_DIR}/engine/shim"
|
||||
"${DXSDK}/Include")
|
||||
|
||||
+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 +
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
# BattleTech 4.11 — controls reference
|
||||
|
||||
Two ways to fly: the **keyboard/pad** (the ~25 core actions) and the **cockpit panel**, where
|
||||
every one of the pod's 72 illuminated buttons is mouse-clickable exactly where it sat in the
|
||||
cabinet. Left-click presses; **right-click latches** a button held.
|
||||
|
||||
Rebind keys in `content\bindings.txt` (delete it to regenerate the defaults).
|
||||
Cockpit layout and display sizes live in `content\environ.ini` (likewise self-documenting).
|
||||
|
||||
---
|
||||
|
||||
## Keyboard
|
||||
|
||||
| Key | Action | | Key | Action |
|
||||
|---|---|---|---|---|
|
||||
| W/S or ↑/↓ | throttle lever | | X or NumPad5 | all stop |
|
||||
| A/D or ←/→ | turn pedals | | Alt | reverse thrust |
|
||||
| Q/E | torso twist | | Shift | throttle up |
|
||||
| R/F | torso aim | | NumPad 8/2/4/6 | torso aim/twist |
|
||||
| 1 or Space | main trigger | | 2 | middle thumb |
|
||||
| 3 or Ctrl | upper thumb (missiles) | | 4 | pinky |
|
||||
| V | external camera | | B | look behind |
|
||||
| M | control mode (BAS/MID/ADV) | | N | schematic |
|
||||
| J/K/L | MFD preset pages | | H (hold) | coolant flush |
|
||||
| C | cycle a coolant valve | | G (hold) + fire | regroup a weapon |
|
||||
| − / = | volume down/up | | F5–F9 | generator select / mode |
|
||||
|
||||
**You spawn in BASIC control mode.** In BASIC the stick steers and the pedal keys do nothing.
|
||||
Press **M** once for MID, where the pedals steer and the stick twists the torso — the full
|
||||
experience. If "turning doesn't work", this is why.
|
||||
|
||||
Numpad aim keys need **NumLock ON**.
|
||||
|
||||
> Control mode (BAS/MID/ADV, the M key) is *not* the menu's experience setting
|
||||
> (novice/standard/veteran/expert). That one changes the simulation — heat, damage — not your
|
||||
> controls.
|
||||
|
||||
---
|
||||
|
||||
## The cockpit panel — all 72 pod buttons
|
||||
|
||||
Each display carries the bank of buttons mounted around it in the real cabinet. Click anywhere on
|
||||
a display: the buttons reach *under* the glass, so the whole screen is a press target and only the
|
||||
lit strip at the edge shows.
|
||||
|
||||
### Heat / coolant display — the loop controls
|
||||
| Button | Action |
|
||||
|---|---|
|
||||
| `0x28` | **Balance coolant** — equalise every condenser valve |
|
||||
| `0x29`–`0x2F` | the six **condenser valves** (Cond 6…1) and the **coolant flush** (`0x2C`, hold) |
|
||||
|
||||
⚠ The valve detents run 1 - 5 - 50 (max) - **0 (CLOSED)** - back to 1. One press past max shuts
|
||||
that loop off and everything on it overheats. Boosting one loop starves the others — the supply
|
||||
is shared.
|
||||
|
||||
### Weapon and engineering displays
|
||||
| Bank | Display |
|
||||
|---|---|
|
||||
| `0x00`–`0x07` | Right Weapons (lower right) |
|
||||
| `0x08`–`0x0F` | Left Weapons (lower left) |
|
||||
| `0x20`–`0x27` | Engineering (upper centre) |
|
||||
| `0x30`–`0x37` | Comm / target hotbox (upper right) — pilot select |
|
||||
|
||||
Each display's buttons are **page-gated**: on the Quad page the outer buttons jump straight to a
|
||||
populated Eng page; on an Eng page they drive the subsystem shown — generator select A–D, bus
|
||||
mode, coolant toggle, trigger configure, and **eject** (tap to clear a jam, hold ~3 s to jettison
|
||||
a burning ammo bay).
|
||||
|
||||
### Map / radar — the side columns
|
||||
| Button | Action | | Button | Action |
|
||||
|---|---|---|---|---|
|
||||
| `0x10` | map zoom in | | `0x18` | cycle control mode (BAS/MID/ADV) |
|
||||
| `0x11` | map zoom out | | `0x1A`–`0x1D` | generator A–D on/off |
|
||||
| `0x12` | thermal / IR sight | | `0x16`,`0x17`,`0x19`,`0x1E`,`0x1F` | no function (as shipped in 1995) |
|
||||
| `0x13` | **crouch** | | | |
|
||||
| `0x14` | searchlight | | | |
|
||||
| `0x15` | cycle display mode | | | |
|
||||
|
||||
### Flight controls
|
||||
| Button | Action |
|
||||
|---|---|
|
||||
| `0x3D` | panic / config lamp |
|
||||
| `0x3F` | throttle head — reverse thrust |
|
||||
| `0x40` | main trigger |
|
||||
| `0x41`–`0x44` | hat: behind / torso centre / right / left |
|
||||
| `0x45`–`0x47` | pinky, middle thumb, upper thumb — the mappable fire buttons |
|
||||
| `0x38`–`0x3C`, `0x3E` | intercom and door lines — no function on a desktop |
|
||||
|
||||
---
|
||||
|
||||
## Controllers
|
||||
|
||||
An **Xbox controller** works out of the box.
|
||||
|
||||
**Flight sticks, HOTAS and pedals:** run `joyconfig.bat` once. A wizard asks you to move each
|
||||
control in turn — stick, twist, throttle, fire buttons — writes the bindings, and drops you into
|
||||
the solo menu to try them. It only rewrites its own section of `bindings.txt`, so hand edits
|
||||
elsewhere in the file survive.
|
||||
|
||||
**RGB keyboards** with Windows Dynamic Lighting mirror the panel: any key you have bound to a
|
||||
lamp button glows in the panel's own colours and flashes in step with the on-screen buttons. Turn
|
||||
it off with `BT_KEYLIGHT=0` in `environ.ini`.
|
||||
|
||||
---
|
||||
|
||||
## Cockpit layout
|
||||
|
||||
Set these in `content\environ.ini`:
|
||||
|
||||
- **`BT_MFD_SCALE`** (and `_UL`/`_UC`/`_UR`/`_LL`/`_LR`) — size of the five MFDs, as a percentage
|
||||
of their pod size. Turn them up if you want to actually read them while you fly.
|
||||
- **`BT_RADAR_SCALE`**, **`BT_RADAR_POS`** — size the map, and move it out of the middle of the
|
||||
road: `CENTER`, `LEFT`, `RIGHT`, `MIDLEFT`, `MIDRIGHT`.
|
||||
- **`BT_GLASS_PANELS=1`** — break every display out into its own desktop window instead of the
|
||||
composited cockpit. Good for reading one display at full size.
|
||||
|
||||
Run with **`-fit`** for a borderless window over the whole monitor. The cockpit scales to any
|
||||
window shape at one uniform scale, so nothing ever stretches — a window that isn't the cockpit's
|
||||
shape just gets black bars.
|
||||
+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.
|
||||
|
||||
@@ -0,0 +1,406 @@
|
||||
//===========================================================================//
|
||||
// L4KEYLIGHT.cpp - Windows Dynamic Lighting keyboard mirror.
|
||||
//
|
||||
// Ported from RP412's L4KEYLIGHT (itself a port of vRIO's
|
||||
// KeyboardLampMirror). Keys bound to a lamp address in bindings.txt glow
|
||||
// with the panel palette, so the keyboard reads as the pod's button field.
|
||||
//
|
||||
// COMPILED APART FROM THE REST: /std:c++17 + conformance mode, per-file in
|
||||
// CMakeLists (C++/WinRT needs both; the project builds C++14 /permissive).
|
||||
// NOTE: RP412 also had to force DEFAULT struct packing here because its
|
||||
// engine compiles /Zp1, which would break the WinRT ABI -- BT411 does NOT
|
||||
// set /Zp (checked 2026-07-26: BT_OPTS is /permissive /W0 /wd4996 /EHsc
|
||||
// /bigobj /MP), so that hazard does not apply. The scalars-only interface
|
||||
// in l4keylight.h is kept anyway: it costs nothing and keeps this TU
|
||||
// isolated if packing is ever added.
|
||||
//
|
||||
// Everything WinRT runs on a private worker thread: device watcher,
|
||||
// keyboard claiming, and a 100 ms paint loop that mirrors the lamp bytes
|
||||
// (the same flash formula as BTLampBrightnessOf, so the board and the
|
||||
// on-screen cockpit buttons blink in step).
|
||||
//===========================================================================//
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
|
||||
#include <winrt/base.h>
|
||||
#include <winrt/Windows.Foundation.h>
|
||||
#include <winrt/Windows.Foundation.Collections.h>
|
||||
#include <winrt/Windows.Devices.Enumeration.h>
|
||||
#include <winrt/Windows.Devices.Lights.h>
|
||||
#include <winrt/Windows.System.h>
|
||||
#include <winrt/Windows.UI.h>
|
||||
|
||||
#pragma comment(lib, "windowsapp.lib")
|
||||
|
||||
#include "l4keylight.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
using namespace winrt;
|
||||
using namespace winrt::Windows::Devices::Enumeration;
|
||||
using namespace winrt::Windows::Devices::Lights;
|
||||
using winrt::Windows::UI::Color;
|
||||
using winrt::Windows::System::VirtualKey;
|
||||
|
||||
struct KeyEntry
|
||||
{
|
||||
int virtualKey;
|
||||
int address;
|
||||
bool yellow;
|
||||
};
|
||||
|
||||
struct ClaimedArray
|
||||
{
|
||||
hstring id;
|
||||
LampArray array{ nullptr };
|
||||
bool perKey = false;
|
||||
bool baseCoated = false;
|
||||
};
|
||||
|
||||
std::mutex gLock;
|
||||
std::vector<KeyEntry> gMap;
|
||||
unsigned char gLamps[64] = {};
|
||||
void (*gLogger)(const char *) = nullptr;
|
||||
|
||||
std::atomic<bool> gRunning{ false };
|
||||
std::thread gWorker;
|
||||
|
||||
void Log(const char *line)
|
||||
{
|
||||
void (*logger)(const char *);
|
||||
{
|
||||
std::lock_guard<std::mutex> hold(gLock);
|
||||
logger = gLogger;
|
||||
}
|
||||
if (logger != nullptr)
|
||||
{
|
||||
logger(line);
|
||||
}
|
||||
}
|
||||
|
||||
void Logf(const char *format, ...)
|
||||
{
|
||||
char line[256];
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
_vsnprintf_s(line, sizeof(line), _TRUNCATE, format, args);
|
||||
va_end(args);
|
||||
Log(line);
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Lamp byte -> brightness level, animating the flash modes.
|
||||
// MUST match BTLampBrightnessOf (l4vb16.h) exactly, so the board and
|
||||
// the on-screen buttons blink together: solid shows state 1, flashing
|
||||
// ALTERNATES state 1 and state 2 (RIO::LampState, L4RIO.h). Copied
|
||||
// rather than included because this TU takes no engine headers.
|
||||
//---------------------------------------------------------------
|
||||
int LampLevel(int lamp_state)
|
||||
{
|
||||
int mode = lamp_state & 0x03;
|
||||
int level1 = (lamp_state >> 2) & 0x03;
|
||||
int level2 = (lamp_state >> 4) & 0x03;
|
||||
|
||||
if (mode == 0)
|
||||
{
|
||||
return level1;
|
||||
}
|
||||
|
||||
static const int half_period[4] = { 0, 500, 250, 125 };
|
||||
return ((GetTickCount() / half_period[mode]) & 1) ? level2 : level1;
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// The panel palette (vRIO's KeyboardLampMirror shades): red for
|
||||
// the banks, yellow for Secondary/Screen; the off shade keeps the
|
||||
// bound keys faintly visible so the board reads as a button field.
|
||||
//---------------------------------------------------------------
|
||||
Color Shade(int level, bool yellow)
|
||||
{
|
||||
Color color;
|
||||
color.A = 255;
|
||||
if (yellow)
|
||||
{
|
||||
if (level >= 3) { color.R = 245; color.G = 210; color.B = 60; }
|
||||
else if (level >= 1) { color.R = 140; color.G = 118; color.B = 38; }
|
||||
else { color.R = 70; color.G = 60; color.B = 24; }
|
||||
}
|
||||
else
|
||||
{
|
||||
if (level >= 3) { color.R = 230; color.G = 70; color.B = 70; }
|
||||
else if (level >= 1) { color.R = 120; color.G = 50; color.B = 50; }
|
||||
else { color.R = 64; color.G = 40; color.B = 40; }
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
bool SameColor(const Color &a, const Color &b)
|
||||
{
|
||||
return a.A == b.A && a.R == b.R && a.G == b.G && a.B == b.B;
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// The worker: watcher + claim + paint loop
|
||||
//---------------------------------------------------------------
|
||||
void Worker()
|
||||
{
|
||||
try
|
||||
{
|
||||
init_apartment();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// apartment already set on this thread; carry on
|
||||
}
|
||||
|
||||
std::mutex claimedLock;
|
||||
std::vector<ClaimedArray> claimed;
|
||||
bool anySeen = false;
|
||||
|
||||
DeviceWatcher watcher{ nullptr };
|
||||
try
|
||||
{
|
||||
watcher = DeviceInformation::CreateWatcher(LampArray::GetDeviceSelector());
|
||||
|
||||
watcher.Added([&](DeviceWatcher const &, DeviceInformation const &info)
|
||||
{
|
||||
try
|
||||
{
|
||||
LampArray array = LampArray::FromIdAsync(info.Id()).get();
|
||||
if (array == nullptr ||
|
||||
array.LampArrayKind() != LampArrayKind::Keyboard)
|
||||
{
|
||||
return; // mice / strips / cases stay untouched
|
||||
}
|
||||
anySeen = true;
|
||||
ClaimedArray entry;
|
||||
entry.id = info.Id();
|
||||
entry.array = array;
|
||||
entry.perKey = array.SupportsVirtualKeys();
|
||||
{
|
||||
std::lock_guard<std::mutex> hold(claimedLock);
|
||||
claimed.push_back(entry);
|
||||
}
|
||||
Logf(entry.perKey
|
||||
? "KeyLight: + %ls (%d LEDs, per-key)"
|
||||
: "KeyLight: + %ls (%d zones - board-wide mirror)",
|
||||
info.Name().c_str(), (int) array.LampCount());
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Log("KeyLight: could not open a lamp array");
|
||||
}
|
||||
});
|
||||
watcher.Removed([&](DeviceWatcher const &, DeviceInformationUpdate const &update)
|
||||
{
|
||||
std::lock_guard<std::mutex> hold(claimedLock);
|
||||
for (size_t i = 0; i < claimed.size(); ++i)
|
||||
{
|
||||
if (claimed[i].id == update.Id())
|
||||
{
|
||||
claimed.erase(claimed.begin() + i);
|
||||
Log("KeyLight: keyboard disconnected");
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
watcher.Updated([](DeviceWatcher const &, DeviceInformationUpdate const &)
|
||||
{
|
||||
// required for the watcher to progress
|
||||
});
|
||||
watcher.Start();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Log("KeyLight: Dynamic Lighting unavailable on this system");
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Paint loop: 100 ms cadence, repaint only on change
|
||||
//
|
||||
std::vector<Color> lastColors;
|
||||
int waited = 0;
|
||||
while (gRunning.load())
|
||||
{
|
||||
Sleep(50);
|
||||
waited += 50;
|
||||
if (waited < 100)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
waited = 0;
|
||||
|
||||
std::vector<KeyEntry> map;
|
||||
unsigned char lamps[64];
|
||||
{
|
||||
std::lock_guard<std::mutex> hold(gLock);
|
||||
map = gMap;
|
||||
memcpy(lamps, gLamps, sizeof(lamps));
|
||||
}
|
||||
|
||||
std::vector<Color> colors(map.size());
|
||||
std::vector<VirtualKey> keys(map.size());
|
||||
int bestLevel = 0;
|
||||
bool bestYellow = false;
|
||||
bool changed = (lastColors.size() != map.size());
|
||||
for (size_t i = 0; i < map.size(); ++i)
|
||||
{
|
||||
int address = map[i].address;
|
||||
int level = (address >= 0 && address < 64)
|
||||
? LampLevel(lamps[address]) : 0;
|
||||
if (level > bestLevel)
|
||||
{
|
||||
bestLevel = level;
|
||||
bestYellow = map[i].yellow;
|
||||
}
|
||||
colors[i] = Shade(level, map[i].yellow);
|
||||
keys[i] = (VirtualKey) map[i].virtualKey;
|
||||
if (!changed && !SameColor(colors[i], lastColors[i]))
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> hold(claimedLock);
|
||||
bool freshClaim = false;
|
||||
for (ClaimedArray &entry : claimed)
|
||||
{
|
||||
if (!entry.baseCoated)
|
||||
{
|
||||
freshClaim = true;
|
||||
}
|
||||
}
|
||||
if (!changed && !freshClaim)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Color aggregate = Shade(bestLevel, bestYellow);
|
||||
for (ClaimedArray &entry : claimed)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (entry.perKey)
|
||||
{
|
||||
if (!entry.baseCoated)
|
||||
{
|
||||
Color black;
|
||||
black.A = 255; black.R = 0; black.G = 0; black.B = 0;
|
||||
entry.array.SetColor(black);
|
||||
entry.baseCoated = true;
|
||||
}
|
||||
if (!map.empty())
|
||||
{
|
||||
entry.array.SetColorsForKeys(
|
||||
array_view<Color const>(colors.data(), colors.data() + colors.size()),
|
||||
array_view<VirtualKey const>(keys.data(), keys.data() + keys.size()));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
entry.baseCoated = true;
|
||||
entry.array.SetColor(aggregate);
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// device wobble; the watcher handles removal
|
||||
}
|
||||
}
|
||||
lastColors = colors;
|
||||
}
|
||||
|
||||
//
|
||||
// Releasing the arrays hands the LEDs back to Windows
|
||||
//
|
||||
try
|
||||
{
|
||||
watcher.Stop();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> hold(claimedLock);
|
||||
claimed.clear();
|
||||
}
|
||||
if (!anySeen)
|
||||
{
|
||||
Log("KeyLight: no Dynamic Lighting keyboard was found this session");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//########################################################################
|
||||
// The scalar interface (safe across the packing boundary)
|
||||
//########################################################################
|
||||
|
||||
void
|
||||
KeyLight_SetLogger(void (*logger)(const char *line))
|
||||
{
|
||||
std::lock_guard<std::mutex> hold(gLock);
|
||||
gLogger = logger;
|
||||
}
|
||||
|
||||
void
|
||||
KeyLight_SetMap(
|
||||
const int *virtual_keys,
|
||||
const int *addresses,
|
||||
const unsigned char *yellow,
|
||||
int count
|
||||
)
|
||||
{
|
||||
std::lock_guard<std::mutex> hold(gLock);
|
||||
gMap.clear();
|
||||
gMap.reserve(count);
|
||||
for (int i = 0; i < count; ++i)
|
||||
{
|
||||
KeyEntry entry;
|
||||
entry.virtualKey = virtual_keys[i];
|
||||
entry.address = addresses[i];
|
||||
entry.yellow = (yellow[i] != 0);
|
||||
gMap.push_back(entry);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
KeyLight_UpdateLamps(const unsigned char *lamp_state, int count)
|
||||
{
|
||||
if (count > 64)
|
||||
{
|
||||
count = 64;
|
||||
}
|
||||
std::lock_guard<std::mutex> hold(gLock);
|
||||
memcpy(gLamps, lamp_state, count);
|
||||
}
|
||||
|
||||
void
|
||||
KeyLight_Start()
|
||||
{
|
||||
if (gRunning.exchange(true))
|
||||
{
|
||||
return;
|
||||
}
|
||||
gWorker = std::thread(Worker);
|
||||
}
|
||||
|
||||
void
|
||||
KeyLight_Stop()
|
||||
{
|
||||
if (!gRunning.exchange(false))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (gWorker.joinable())
|
||||
{
|
||||
gWorker.join();
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "l4glasswin.h"
|
||||
#include "l4ctrl.h"
|
||||
#include "l4joy.h"
|
||||
#include "l4keylight.h" // RGB keyboard lamp mirror (scalars-only interface)
|
||||
|
||||
#include <windows.h>
|
||||
#include <xinput.h>
|
||||
@@ -63,6 +64,17 @@ int
|
||||
// XInput normalization: thumbs to -1..1 past the stock deadzone, triggers
|
||||
// to 0..1 past the stock threshold.
|
||||
//
|
||||
//
|
||||
// The lamp mirror's log sink. It cannot use DEBUG_STREAM itself: its TU
|
||||
// takes no engine headers (l4keylight.h is scalars only), so it hands us
|
||||
// finished lines instead.
|
||||
//
|
||||
static void
|
||||
KeyLightLog(const char *line)
|
||||
{
|
||||
DEBUG_STREAM << "[keylight] " << (line ? line : "") << "\n" << std::flush;
|
||||
}
|
||||
|
||||
static float
|
||||
NormalizeThumb(int value, int dead_zone)
|
||||
{
|
||||
@@ -170,6 +182,67 @@ PadRIO::PadRIO():
|
||||
flipStickAxes =
|
||||
(getenv("L4PADFLIP") != NULL && *getenv("L4PADFLIP") != '0');
|
||||
|
||||
//
|
||||
// RGB KEYBOARD LAMP MIRROR (Windows Dynamic Lighting, l4keylight.h):
|
||||
// every key bound to a lamp ADDRESS glows with the panel palette --
|
||||
// yellow for the Secondary/Screen columns (0x10-0x1F), red for the rest,
|
||||
// exactly like the on-screen buttons. Keypad binds have no lamp, so
|
||||
// they are skipped; a key bound twice takes its FIRST binding.
|
||||
// BT_KEYLIGHT=0 opts out; a machine without Dynamic Lighting logs once
|
||||
// and stays dormant.
|
||||
//
|
||||
keyLightActive = False;
|
||||
{
|
||||
const char *gate = getenv("BT_KEYLIGHT");
|
||||
if (gate == NULL || atoi(gate) != 0)
|
||||
{
|
||||
static int lightKeys[192];
|
||||
static int lightAddresses[192];
|
||||
static unsigned char lightYellow[192];
|
||||
int count = 0;
|
||||
for (int k = 0; k < bindings.keyBindingCount && count < 192; ++k)
|
||||
{
|
||||
const PadBindingProfile::KeyBinding &binding = bindings.keyBindings[k];
|
||||
if (binding.action.kind != PadBindingProfile::ActionButton)
|
||||
{
|
||||
continue; // axes/keypads carry no lamp
|
||||
}
|
||||
int address = binding.action.address;
|
||||
if (address < 0 || address >= LampCount)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Logical duplicate = False;
|
||||
for (int j = 0; j < count; ++j)
|
||||
{
|
||||
if (lightKeys[j] == binding.virtualKey)
|
||||
{
|
||||
duplicate = True; // first binding wins
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (duplicate)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
lightKeys[count] = binding.virtualKey;
|
||||
lightAddresses[count] = address;
|
||||
lightYellow[count] = (unsigned char)
|
||||
((address >= 0x10 && address <= 0x1F) ? 1 : 0);
|
||||
++count;
|
||||
}
|
||||
if (count > 0)
|
||||
{
|
||||
KeyLight_SetLogger(KeyLightLog);
|
||||
KeyLight_SetMap(lightKeys, lightAddresses, lightYellow, count);
|
||||
KeyLight_Start();
|
||||
keyLightActive = True;
|
||||
DEBUG_STREAM << "[keylight] mirroring " << count
|
||||
<< " bound key(s) onto RGB keyboards\n" << std::flush;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Never revision 0.0 -- some diagnostics print it; give the synthetic
|
||||
// board a recognizable version.
|
||||
@@ -199,6 +272,11 @@ PadRIO::PadRIO():
|
||||
|
||||
PadRIO::~PadRIO()
|
||||
{
|
||||
if (keyLightActive)
|
||||
{
|
||||
KeyLight_Stop(); // hands the LEDs back to Windows
|
||||
keyLightActive = False;
|
||||
}
|
||||
BTGlassPanels_Destroy(); // safe no-op if the glass windows were never created
|
||||
BTPadPanel_Destroy();
|
||||
if (activeInstance == this)
|
||||
@@ -1038,6 +1116,18 @@ void
|
||||
if (lampNumber >= 0 && lampNumber < LampCount)
|
||||
{
|
||||
lampState[lampNumber] = state;
|
||||
if (keyLightActive)
|
||||
{
|
||||
// The mirror keeps its own copy (its paint loop runs on a private
|
||||
// thread and must not reach into the device); the first 64
|
||||
// addresses are the whole lamp field.
|
||||
unsigned char bytes[64];
|
||||
for (int i = 0; i < 64; ++i)
|
||||
{
|
||||
bytes[i] = (unsigned char) lampState[i];
|
||||
}
|
||||
KeyLight_UpdateLamps(bytes, 64);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -143,6 +143,11 @@ protected:
|
||||
int
|
||||
lampState[LampCount];
|
||||
|
||||
// RGB keyboard lamp mirror running? (l4keylight.h; BT_KEYLIGHT=0 off,
|
||||
// also stays False when no bound key carries a lamp address)
|
||||
Logical
|
||||
keyLightActive;
|
||||
|
||||
//
|
||||
// Typed-channel suppression tables (built from the loaded bindings +
|
||||
// the hardcoded view/preset keys; see SuppressKey).
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
//===========================================================================//
|
||||
// File: l4keylight.h //
|
||||
// Project: MUNGA Brick: RGB keyboard lamp mirror //
|
||||
// Contents: Windows Dynamic Lighting bridge (vRIO's KeyboardLampMirror) //
|
||||
//---------------------------------------------------------------------------//
|
||||
// Copyright (C) 1994-1995, Virtual World Entertainment, Inc. //
|
||||
// PROPRIETARY AND CONFIDENTIAL //
|
||||
//===========================================================================//
|
||||
|
||||
#pragma once
|
||||
|
||||
//########################################################################
|
||||
// Mirrors the game-commanded RIO lamp states onto per-key RGB keyboards
|
||||
// through Windows Dynamic Lighting (the WinRT LampArray API), following
|
||||
// vRIO's KeyboardLampMirror: keys bound to lamp addresses glow with the
|
||||
// panel palette (red for the banks, yellow for the Secondary/Screen
|
||||
// columns), flash modes blink at the panel's rates, other keys are
|
||||
// blacked out so the keyboard reads as the button field. Zone-lit
|
||||
// keyboards mirror the strongest lamp board-wide. Dynamic Lighting
|
||||
// grants LED control to the FOREGROUND app - which is the game itself,
|
||||
// so no Windows settings dance is needed.
|
||||
//
|
||||
// The implementation is C++/WinRT on a private worker thread, compiled
|
||||
// with default struct packing (the engine builds /Zp1, which would
|
||||
// break the WinRT ABI) - hence this interface is scalars only. All
|
||||
// functions are safe to call when Dynamic Lighting is unavailable;
|
||||
// failures log once and the mirror stays dormant.
|
||||
//########################################################################
|
||||
|
||||
// Where the mirror's status lines go (the caller owns the sink).
|
||||
void
|
||||
KeyLight_SetLogger(void (*logger)(const char *line));
|
||||
|
||||
// The key map: parallel arrays of virtual keys, their RIO lamp
|
||||
// addresses (0x00-0x47), and whether each paints yellow (Secondary /
|
||||
// Screen columns) instead of red.
|
||||
void
|
||||
KeyLight_SetMap(
|
||||
const int *virtual_keys,
|
||||
const int *addresses,
|
||||
const unsigned char *yellow,
|
||||
int count
|
||||
);
|
||||
|
||||
// Latest game-commanded lamp bytes (indexed by RIO lamp number).
|
||||
void
|
||||
KeyLight_UpdateLamps(const unsigned char *lamp_state, int count);
|
||||
|
||||
// Claim keyboards and start painting / release the LEDs to Windows.
|
||||
void
|
||||
KeyLight_Start();
|
||||
void
|
||||
KeyLight_Stop();
|
||||
@@ -104,6 +104,18 @@ control (stick, twist, throttle, fire buttons), writes the bindings,
|
||||
and drops you into the solo menu to try them. Rebind keys in
|
||||
content\bindings.txt (regenerates with defaults if deleted).
|
||||
|
||||
FULL CONTROLS REFERENCE: see CONTROLS.txt next to this file -- every one of
|
||||
the pod's 72 cockpit buttons, what it does, and which display it sits on.
|
||||
|
||||
COCKPIT LAYOUT: content\environ.ini (written on first run, every option
|
||||
documented in the file itself) sizes the MFDs and the map, moves the map out
|
||||
of the middle of the road, or breaks each display into its own window. The
|
||||
cockpit now scales to ANY window shape without distorting -- drag it, maximise
|
||||
it, or launch with -fit for borderless full screen.
|
||||
|
||||
RGB KEYBOARDS: if yours supports Windows Dynamic Lighting, the keys you have
|
||||
bound to cockpit buttons glow in the panel's colours and flash along with it.
|
||||
|
||||
This build: {VERSION}. Private -- do not redistribute.
|
||||
{EXPIRE}Test builds STOP WORKING 14 days after they are made -- if the game
|
||||
{EXPIRE}says it has expired, just grab the operator's newest zip.
|
||||
|
||||
@@ -119,6 +119,22 @@ def main():
|
||||
lines = [l for l in lines if "{EXPIRE}" not in l]
|
||||
readme = "".join(lines)
|
||||
z.writestr(name + "/README.txt", readme)
|
||||
|
||||
# The full controls reference travels with the game, so players get the
|
||||
# 72-button panel map without the repo. Flattened to ASCII so it reads
|
||||
# correctly in Notepad -- the markdown source keeps its typography.
|
||||
if os.path.exists("docs/CONTROLS.md"):
|
||||
controls = open("docs/CONTROLS.md", encoding="utf-8").read()
|
||||
for bad, good in ((u"—", "-"), (u"–", "-"), (u"‘", "'"),
|
||||
(u"’", "'"), (u"“", '"'), (u"”", '"'),
|
||||
(u"×", "x"), (u"→", "->"), (u"…", "..."),
|
||||
(u"↑", "Up"), (u"↓", "Down"),
|
||||
(u"←", "Left"), (u"→", "Right"),
|
||||
(u"⚠", "!"), (u"·", "-")):
|
||||
controls = controls.replace(bad, good)
|
||||
controls = controls.encode("ascii", "replace").decode("ascii")
|
||||
z.writestr(name + "/CONTROLS.txt", controls)
|
||||
print(" controls reference -> CONTROLS.txt")
|
||||
for p in bats: # bats at the zip ROOT (next to
|
||||
arc = os.path.basename(p) # content\ + build\, per the guard)
|
||||
z.write(p, name + "/" + arc)
|
||||
|
||||
Reference in New Issue
Block a user