Files
BT411/context/glass-cockpit.md
T
CydandClaude Opus 5 02b1b50e45 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>
2026-07-26 02:24:30 -05:00

37 KiB
Raw Blame History

id, title, status, source_sections, related_topics, key_terms, open_questions
id title status source_sections related_topics key_terms open_questions
glass-cockpit Glass Cockpit — the desktop developer layer (BT_GLASS / BT_STEAM) living plan 2026-07-17; CMakeLists.txt gates; BT412 prior art (reference only)
pod-hardware
build-and-run
multiplayer
gauges-hud
PadRIO
miniconsole
glass-cockpit

Glass Cockpit — the desktop developer layer

Purpose: developer tooling so game developers can test their work WITHOUT booting a Tesla II pod — and stay far enough out of the game that nothing a real pod requires can break. NOT a consumer product (GDI-grade UI is the ceiling). Pod fidelity outranks desktop convenience in every trade-off.

ONE unified build (2026-07-21; the compile-gate split is RETIRED)

The pod/glass/steam compile split (three build dirs, three exes) is gone — it caused version skew (a stale steam exe shipped 2 days behind), tripled build time, and the "clicks silently dead in the wrong build" incident. Now everything compiles into the one build/ exe and behavior is selected at RUNTIME:

Runtime gate Arms
BT_PLATFORM=glass the desktop-cockpit profile: L4CONTROLS=PAD → PadRIO (clickable cockpit buttons, bindings.txt keymap), plasma window, and the miniconsole menu on a zero-arg launch
BT_STEAM_NET=1 the Steam transport (FakeIP/SDR; degrades to Winsock without the Steam client). steam_api.dll is DELAY-LOADED (/DELAYLOAD + delayimp) — machines without the DLL run everything else
(nothing) GLASS is the desktop DEFAULT (2026-07-22) — a bare boot gets PadRIO/bindings.txt (and the menu on zero args). The old accidental-DEV trap ("the old keymap again") is closed: every launcher that forgot the env used to boot DEV (play bats, steam bat, the operator console — each needed the same one-line fix).
BT_PLATFORM=dev the legacy DEV profile (keyboard-only, the bring-up drive bridge) — explicit opt-in for debugging
BT_PLATFORM=pod the cabinet profile. CONTRACT: the real pod's SETENV.BAT must set this — the old "bare boot == cabinet shape" assumption no longer holds.
  • The BT_GLASS macro is now ALWAYS defined (a seam marker only — the #ifdef sites remain as documentation of the layer boundary). Every gated code path was audited: all are instance-guarded (PadRIO::activeInstance, steamActive) or env-gated at runtime.
  • BT_STEAM stays a COMPILE gate (the LICENSE gate, default OFF, restored 2026-07-21): the Steamworks SDK terms bar a public release with Steam inside, so a release configure must be able to exclude it entirely — no SDK headers, no steam_api.lib, no DLL. The dev checkout configures build/ once with -DBT_STEAM=ON. mkdist.py reads the answer from build/CMakeCache.txt: steam-OFF zips carry no play_steam.bat, no steam_api.dll (including a STALE one from an earlier ON build), a README with the steam section stripped, and a -nosteam name suffix so the two flavors never clobber. Verified both flavors: OFF exe has no steam bits + BT_STEAM_NET=1 is inert; ON exe degrades to Winsock.
  • Menu gate (amended 2026-07-22): the miniconsole menu runs on a zero-arg GLASS boot — and glass is now the desktop default, so a bare double-click of btl4.exe opens the menu (the desired desktop UX). Cabinet protection moved from "bare boot never menus" to the SETENV.BAT contract: the real pod sets BT_PLATFORM=pod (no menu, wait-for-egg); BT_PLATFORM=dev also keeps the old bare-boot shape.
  • Verified matrix (2026-07-21): bare boot ≡ old pod exe (same DEV profile, same egg-load path); glass + scripted click → ConfigureMappables end-to-end; exe runs WITHOUT steam_api.dll present; glass zero-arg → menu; 2-node loopback MP (145 ticks each); BT_STEAM_NET=1 without Steam → "staying on Winsock" + game runs.
  • Convention: every BT_* name is a RUNTIME env gate (BTEnvOn) — with TWO exceptions, both compile gates: BT_STEAM (the license gate above) and BT_EXPIRE (2026-07-22, the tester-build expiry tripwire) — default ON, the exe refuses to start BT_EXPIRE_DAYS (default 14) after its build DAY (btversion.h BT_BUILD_UNIX, UTC-midnight-rounded so the only-on-change header churns once per day, not per build). Boot logs [boot] test build window: N day(s) left; expired = log + MessageBox + exit. Disable: -DBT_EXPIRE=OFF (mkdist marks such a zip -noexpire + warns; README expiry note is {EXPIRE}-marker-gated). Quality gate, not DRM — a clock rollback defeats it. Verified both paths live (-DBT_EXPIRE_DAYS=0 force-expire run). Do not add another compile gate without updating this file.
  • No weak-linkage/optional symbols — /FORCE turns unresolved externals into runtime AVs (reconstruction-gotchas); everything links always, so this class of trap no longer varies by build flavor.

Layout modes — ONE resolver (2026-07-26) [T2 four-mode boot-verified]

Where the five MFDs + radar go is a SEPARATE axis from the platform profile. Under a glass boot (BT_DEV_GAUGES set) btl4main.cpp resolves it once, right after the profile putenvs land, into glassLayout; the boot banner, the pad-panel decision and the window-sizing block all read that one answer. Precedence, highest first:

Env Layout Buttons live in
BT_GLASS_PANELS≠0 per-display cockpit windows (the "exploded" view, L4GLASSWIN) each display's own window
BT_DEV_GAUGES_WINDOW the legacy separate MFD window the single combined pad panel
BT_DEV_GAUGES_DOCK the docked bottom gauge strip the single combined pad panel
BT_COCKPIT=0 ...also the docked strip (the documented opt-out) the single combined pad panel
(nothing) COCKPIT SURROUND — the glass default the main window (D3D-drawn lamps)

The banner now names the live one: [boot] platform profile: GLASS (PadRIO + plasma window) [secondary displays: cockpit surround].

Two bugs this replaced (both were live until 2026-07-26): the mode was decided in TWO places with duplicated precedence (the glass profile block picked BT_PAD_PANEL/BT_GLASS_PANELS; the sizing block re-derived cockpit-vs-dock), and the banner read NEITHER — it announced "per-display cockpit windows [BT_GLASS_PANELS]" for every glass boot, surround included. Worse, the profile block turned BT_COCKPIT=0 into BT_GLASS_PANELS=1, so the docked strip it is documented to select was unreachable under the glass profile. BT_GLASS_PANELS is now explicit-only, and dock/window modes auto-raise BT_PAD_PANEL so the 72-button field always has a home.

ONE button-bank geometry — L4RIOBANK (2026-07-26) [T2 click-verified both modes]

The RIO button field around a pod display is drawn TWICE — composited into the main window (surround, L4VB16.cpp) and one window per display (exploded, L4GLASSWIN.cpp) — and each renderer used to carry its own copy of the geometry. They drifted badly: the same MFD button was a 156×138 under-glass target in the exploded window and a 76×24 sliver entirely outside the glass in the surround. engine/MUNGA_L4/L4RIOBANK.cpp is now that geometry, once; both renderers are consumers, and placement (WHERE each display goes) stays with each renderer because that genuinely differs.

The under-glass rule (ported from RP412 L4MFDVIEW, which took it from the pod): a button reaches half the glass in BEHIND the display and only a lamp strip clears the edge. The renderer paints buttons first, imagery over them — so the lamp reads as a slim bar, practically the whole display is the press target, and nothing is occluded (the picture wins every pixel it covers). The strip scales off the display's SHORT axis against the native 480 (an MFD is 640×480 landscape, the map 480×640 portrait — both short-axis 480) with a 6px floor, so a half-scale surround lamp is still a bar and not a smudge. The MFD per-column nudge against the unevenly spaced DISPLAY/PROGRAM legends (mfdColumnDX) moved here too, so both renderers inherit the same alignment.

The map's foot row is pushed FIRST, deliberately: the side columns now reach half the map each, so they cover the foot band too, and the hit test takes the first match — foot-first is what keeps 0x16/0x17/0x1F/0x1E reachable. That class of bug is why the verification below tests first-hit reachability, not mere presence.

Verified 2026-07-26 [T2]: BT_RIOBANK_LOG=1 dumps every bank's rects; scratchpad/checkbank.py reports the per-bank census and proves no address is SHADOWED (has a point no earlier button covers); scratchpad/clickbank.py then posts a real click at every button's centre. Both modes: 72/72 addresses placed, zero shadowed, zero duplicated, 72/72 dispatched ([cockpit] CLICK / [glasswin] CLICK), process survived 144 posted messages per mode. Pod + dev profiles boot and simulate un-regressed. Surround BT_SHOT confirms the legends are clear and the lamps read as edge strips.

The cockpit SCALES — uniform-fit letterbox + -fit (2026-07-26) [T2 live]

The canvas is a fixed size and D3D9 stretched it into whatever the client area was, so a window dragged to a different shape squashed the instruments — the projection was aspect-corrected (task #20) but the panels, lamps and MFD glass were not. Now the canvas is fitted at ONE uniform scale, centred, leftover black: the cockpit scales both ways and never distorts, whatever the window.

  • BTCockpitFitRect(clientW, clientH, RECT*) — integer math throughout, so the rect is stable frame to frame (a float scale wobbles a pixel and the bars shimmer during a drag).
  • D3D9 does the scaling at Present via a destination rect, which D3DSWAPEFFECT_DISCARD forbids — so the WINDOWED swap effect becomes D3DSWAPEFFECT_COPY when the surround is up and multisampling is off (COPY cannot multisample). gBTCockpitLetterbox records whether that happened; when 0 the old full-client stretch stands, so an MSAA run still works.
  • Both present sites take the rect (scene + the wait-screen overlay, which paints into the same backbuffer). WM_SIZE invalidates so the bars repaint black — a COPY present only writes its dest rect, so stale bar content would otherwise persist.
  • The click transform had to follow. BTCockpitMouseDown mapped client→canvas against the full client; under the letterbox that drifts the hit test off every button by the bar width. It now runs the identical fit (and a click on a bar hits nothing).
  • World aspect under the letterbox is the view rect's OWN aspect — the client no longer enters into it.
  • -fit (alias -windowed-fullscreen): borderless WS_POPUP over the monitor, canvas letterboxed inside. Verified on a 3440×1440 ultrawide.

Ordering trap found live: the letterbox flag was first set at device creation, but the first WM_SIZE arrives BEFORE the device exists — a -fit boot logged aspect=3.14 (the stretch formula) and applied it on frame 1, because nothing resizes the window again. The INTENT is now decided in btl4main alongside gBTGaugeCockpit; L4VIDEO only confirms or withdraws it.

Capture note: PrintWindow returns an all-black frame for the borderless -fit window (it works for the normal chrome'd one). Use a screen-region grab (scratchpad/shot.ps1) for -fit, and don't read a black PrintWindow there as a render failure — it isn't.

Verified: wide (1684×661) and tall (744×961) drags both letterbox with zero distortion; 72/72 buttons still dispatch through the transform at 0.62× scale with a 389px bar; exploded / dock / pod / dev all keep DISCARD and boot un-regressed.

Player-tunable displays — scale, map placement, environ.ini (2026-07-26) [T2 live]

The pod bolted its instruments down at one size; a desktop panel has room to trade viewscreen for instrument, so the player scales them. All resolved ONCE (BTCkResolvedSizes, L4VB16.cpp) because the surround recomputes its layout every frame.

Env Effect
BT_MFD_SCALE all five MFDs, % of pod size (25-200, default 100)
BT_MFD_SCALE_UL/_UC/_UR/_LL/_LR one display, overriding the group
BT_RADAR_SCALE the portrait map
BT_RADAR_POS CENTER (default) / LEFT / RIGHT / MIDLEFT / MIDRIGHT (+ BOTTOM*, *CENTER spellings)

The bands derive from the resolved sizes — that is why the sizes could not stay constants: the surround band a display hangs in has to grow with it or the canvas clips it. Each band is what its LARGEST occupant needs (a corner MFD only claims size - kCkOVL, since it overlaps the view), and the bottom band drops to just the lower MFDs when the map leaves that row for a side. Verified: 100% reproduces the historical L276 R276 T223 B336 exactly; 150% gives L436 R436 T343 B448; 80% + MIDLEFT gives L256 R212 T175 B148.

A corner map goes flush to the CANVAS edge, not the view edge, and the lower MFD whose corner it took sits beside it. Measuring off the view edge (first cut) overlapped them by the band width — 232px of map drawn over the Right Weapons MFD.

Map legend grid — MEASURED, not inherited [T2]. The side columns must line up with the six cells the map paints beside them (⊕MAP / ⊖MAP / IR / crouch / searchlight / display-mode = 0x10-0x15 — the imagery independently confirms our address map). scratchpad/measurelegend.py over a native 480×640 capture gives top 3, cell 102, pitch 107 on the 640 span. RP412's map is 13 + 6×102 @ pitch 105 — same cell height, different top and pitch, so its numbers do not transfer; ours are ours. Our old even division (displayH/6 + 1 = 107) had the pitch right by luck and sat 3px high of the labels. Top and bottom are now scaled separately and subtracted, so rounding cannot drift the buttons out of step with the labels down the column.

environ.ini (the settings file) [T2]. ⚠ It was read ~300 lines into WinMain, AFTER the platform-profile block had run its getenvs — 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 /MPno /Zp — so only the dialect flags are needed (/std:c++17 /permissive-, per-file in CMakeLists, since the project otherwise builds C++14 /permissive). The scalars-only interface is kept anyway so the isolation survives if packing is ever added.

Wired in PadRIO: map from bindings.keyBindings (ActionButton binds only, first-binding-wins per key), fed from PadRIO::SetLamp, stopped in the dtor (which hands the LEDs back to Windows). Verified: [keylight] mirroring 25 bound key(s) + claimed this machine's 24-zone keyboard; BT_KEYLIGHT=0 and the pod profile produce zero keylight lines and run clean.

Lamp flash decode was wrong — fixed (2026-07-26) [T1, from the L4RIO.h enum]

BTLampBrightnessOf (l4vb16.h) returned max(state1, state2) and blanked to 0 on the alternate phase. The engine's own enum [T0] says otherwise: bits 0-1 = flash mode, bits 2-3 = state 1 brightness, bits 4-5 = state 2 brightness (0 off / 1 dim / 3 bright) — solid shows state 1, flashing ALTERNATES the two at 500/250/125 ms. The old formula agrees only when one state is Off, which is why it survived: the Panic lamp is flashFast+state1Off+state2Bright and L4LAMP's other pulse is off/dim. But L4LAMP.cpp:252 commands flashFast + state1Dim + state2Bright — a dim→bright pulse that rendered as a hard bright→off blink. RP412's L4MFDVIEW::LampLevel has the faithful formula; that is now ours. THREE copies of the decode existed (l4vb16.h, L4GLASSWIN, L4PADPANEL), all three with the bug; the two locals now forward to the one inline.

The pre-mission WAITING SCREEN (2026-07-22) [T2]

Every pre-run state (seat wait, roster full, WaitingForEgg, LoadingMission, WaitingForLaunch) paints an animated overlay -- Consolas green text + a 12-segment spinner phased on GetTickCount/80 (BTWaitScreenPaintDC, L4VIDEO.cpp) -- so a joining player never sees "Not Responding" or a frozen frame. Two paint paths: PRE-DEVICE (relay seat wait, L4NET RelayWaitTick) paints the window DC directly (nothing else presents -- safe); ONCE THE DEVICE EXISTS (DPLRenderer::ExecuteIdle) it must NOT paint after Present (the black D3D frame alternating with GDI = the load-time flicker, user-reported) -- and the backbuffer refuses GetDC (D3DERR_INVALIDCALL without D3DPRESENTFLAG_LOCKABLE_BACKBUFFER, which we won't force game-wide), so the overlay GDI-paints into a cached OFFSCREEN PLAIN surface (mWaitOverlaySurface, GetDC always legal there) and StretchRects onto the backbuffer BEFORE Present. D3DPOOL_DEFAULT: released at every device-Reset site. One-shot boot log [waitscreen] paint path: says which path is live. Verified: 14 rapid PrintWindow frames all carry the text (was alternating black).

PadRIO gotcha: release-on-disconnect (issue #24, 2026-07-23) [T3 -- awaiting live verify]

XInput button emission is EDGE-based (held != was_held vs previousPadButtons). The disconnect path zeroed previousPadButtons WITHOUT emitting release edges -- any button held at the moment a controller (wrapper) died stayed pressed game-side FOREVER, and the zeroing also erased the memory a reconnect would have released from. Field signature: PS3-pad-in- xbox-mode wrapper died mid-hold -> look-view latched (stuck side view + pitch-down, no recovery; keyboard-only run clean). Fix: on the connected->disconnected transition, emit EmitButton(address, 0) for every binding whose mask was held, THEN forget. Not reproducible headless (needs real hardware unplug) -- logic fix, awaiting the reporter's controller.

Direct-axis release edge (issue #36, 2026-07-23) [T2 idle-verified; awaiting reporter]

The pad/joy DIRECT axis write was gated on raw != 0 -- centering inside the deadzone stopped writing and the channel LATCHED (fatal on spring-less channels: the Turn composite kept the mech turning after release). Fix: per-binding previous-value arrays (previousPadAxisRaw / previousJoyAxisRaw); the outside->inside transition writes 0 ONCE then yields the channel (composition preserved; slew bindings excluded -- a slewed lever stays put by design). Plus the #24 rule, axis edition: disconnect-while-deflected releases direct channels (Throttle keeps the last lever position -- freeze beats all-stop mid-fight).

CONVENTION: bindings.txt format is a COMPATIBILITY SURFACE (2026-07-24)

Players customize contentindings.txt (keys, pad, joystick/HOTAS rows) and the file is NOT shipped in the zip (untracked -> survives extract-over-top upgrades). That only works if the grammar stays stable across versions, so:

  • Never rename or repurpose existing verbs (key/pad/padaxis/joydev/joyaxis/ joybutton/joyhat), channel names (Throttle/JoystickX/JoystickY/LeftPedal/ RightPedal/Turn), pad/axis token names, or option words (invert/slew/deflect/ set/deadzone). A parse change that rejects a previously-valid line silently breaks a player's setup mid-upgrade.
  • New features = NEW row types or NEW trailing options (the parser already skips unknown lines with a logged warning -- old builds degrade gracefully on newer files too).
  • Semantics of existing rows must stay put (e.g. deflect = spring, slew = lever, direct pad axis = outside-deadzone-owns + release edge). If a semantic MUST change, gate it behind a new option word and keep the bare form as-was.
  • The BT_JOYCONFIG wizard writes only between its # >>> BT_JOYCONFIG markers -- keep that contract so hand edits and wizard reruns coexist.

Generic joysticks / HOTAS / pedals -- DirectInput 8 layer (2026-07-23) [T2 rig-verified; awaiting stick hardware]

Tester ask: configure non-Xbox controllers. L4JOY.cpp/.h (all builds, lazy-init): DI8 enumeration of every non-XInput game device (up to 4), c_dfDIJoystick2 polling with DIPROP_RANGE ±32767 + reacquire-on-loss + 3 s hot-plug re-enum, normalized BTJoyDeviceState {8 axes -1..1, 32 buttons, 4 POV hats}. XInput exclusion = the documented wbem-free RawInput check (device path contains IG_ → VID/PID blacklist vs guidProduct.Data1) — LIVE-verified skipping a real XBOX360 pad. Bindings (L4PADBINDINGS): joydev <slot> [name-substr] + joyaxis <X|Y|Z|RX|RY|RZ|SL0|SL1> axis <channel> [invert] [slew r] [deadzone f] + joybutton <0-31> + joyhat <0-3> <dir> — rows attach to the last joydev slot; unnamed slot binds attached device #slot. PadRIO poll runs them through the SAME channel/event machinery (per-binding prev arrays → detach releases automatically, the #24 rule); a direct joyaxis on Throttle maps full travel [-1,1]→[0,1] and writes every frame (a lever OWNS the channel; slewHeld stays clear so the GAIT DETENT still snaps a lever parked in the walk/run dead band). Wizard BT_JOYCONFIG=1 (BTJoyConfigWizard, console UI, joyconfig.bat): baselines all axes (a HOTAS lever rests anywhere), captures the dominant mover per prompt (0.45 threshold, 250 ms hold, already- assigned axes excluded), derives invert from the move direction (throttle from FINAL position), auto-adds the hat look cluster, and rewrites bindings.txt between # >>> BT_JOYCONFIG markers (rest of the file preserved). Sign conventions: JoystickX + = twist right, JoystickY + = aim up (wizard binds flight-style forward=down via invert), Turn + = right. Legacy L4DINPUT.cpp DIJoystick (L4CONTROLS=DIJOYSTICK, 1995 Joystick iface) left untouched. Verified: grammar accept/reject by line, no-device + XInput-only polling clean 30 s in-mission, zero DI cost with no joy rows. NOT yet verified: real stick axes (no hardware here) — needs a tester with a stick.

Plan of record (2026-07-17)

Working branch glass-cockpit (from master e2c21c4). Steps: (1) gates+scaffolding [THIS], (2) PadRIO glass cockpit, (3) miniconsole self-launch, (4) Steam transport+lobby. Full plan: the approved plan file; running detail: docs/GLASS_COCKPIT.md. The retired BT412 trees (C:\VWE\BT412; local branch local-abandoned-20260717) are read-only reference — no code is copied from them.

Status

  • Step 1 (gates + scaffolding): DONE 2026-07-17. CMake options + defines; 3-config configure matrix + negative test + defaults build verified.

  • Step 2a (.CTL id audit): DONE 2026-07-17 [T2]. The streamed .CTL positional ids landed every MechControlsMapper record ONE MEMBER LATE (our chain starts at 2, the binary's at 3) — a latent REAL-POD bug (RIO throttle would drive pedalsPosition), masked on dev boxes by the keyboard bridge. Fixed ungated: ids pinned to binary numbering + id-2 pad + static_assert locks (mechmppr.hpp/.cpp); torso/weapon chains were already aligned. Permanent env-gated diagnostic: BT_CTRLMAP_LOG=1 dumps every streamed record's resolved member offset (L4CTRL.cpp CreateStreamedMappings). Full evidence: docs/GLASS_COCKPIT.md §2a.

  • Step 2 (PadRIO glass cockpit): DONE 2026-07-17 [T2]. RIOBase seam (L4RIO.h, unconditional) + gated L4CONTROLS=PADPadRIO (L4PADRIO.* XInput+keyboard, L4PADBINDINGS.* content\bindings.txt); bridge stand-downs auto-yield to a live device (BTRIODevicePresent, BT_KEY_BRIDGE unset=auto/0=off/1=on); on-screen panel L4PADPANEL.* follows vRIO (C:\VWE\vrio CockpitLayout/PanelCanvas — 104 controls incl. the two hex keypads → real KeyEvents; right-click latch); desktop plasma L4PLASMAWIN.* (L4PLASMA=SCREEN, buffer is top-down); -platform glass preset + run.cmd token. Secondary displays reuse the EXISTING dev-gauge modes (dock / BT_DEV_GAUGES_WINDOW=1 window / _DOCK=1 overlay — gauges-hud); no new MFD code, no L4VB16.cpp exposure. Button-trap guard: base ConfigureMappableMessageHandler no longer abort()s by default (BT_BUTTON_TRAP=1 restores) — each [FAIL] log = a missing L4 override to reconstruct. Verified: authentic-path input (engine push → streamed .CTL → mapper), panel-click survival, 2-node loopback MP un-regressed on the pod build. Detail: docs/GLASS_COCKPIT.md.

  • Trigger-config + one-keymap (2026-07-21, HUMAN-VERIFIED live). The cockpit-surround clicks only work when a PadRIO exists, and PadRIO only exists under the GLASS platform profile (BT_PLATFORM=glassL4CONTROLS=PAD); a plain launch falls to the DEV profile (KEYBOARD) and PadRIO::SetScreenButton no-ops — every cockpit click silently discarded (the "held PROGRAM, nothing happened" incident: [cockpit] CLICK addr=0x8 logged but ConfigureMappables id 9 never dispatched). play_solo.bat now sets BT_PLATFORM=glass with the glass exe. The full regroup loop (hold PROGRAM → Mapping-mode flip suppresses fire → fire-key taps toggle the binding → joystick-display dot moves → release commits) is verified live. Diagnostics: [cfgmap] ENTER/EXIT (BT_FIRE_LOG, mechweap.cpp) + BT_BTNTEST="addr,on,off" scripted screen-button harness (L4PADRIO.cpp Poll). Keymap: which binding engine runs follows the same profile split — GLASS = PadRIO/bindings.txt, DEV/pod = btinput/CONTROLS.MAP — which made the felt keymap flip with the boot flavor. Settled 2026-07-21 (commit babf5b1): CONTROLS.MAP + the compiled-in default now mirror the bindings.txt layout (numpad aim cluster, arrows=throttle/pedals, Alt=reverse 0x3F, NumPad5=AllStop, Shift=throttle) so every boot flavor feels identical.

  • Step 3 (miniconsole): DONE 2026-07-17 [T2]. game/glass/btl4fe.* (menu + console-shape egg writer, golden-tested vs MP.EGG) + btl4console.* (the marshal: a worker-thread console CLIENT speaking the btconsole.py wire verbatim; content\marshal.log). ZERO engine hooks — the engine runs the stock console ladder. Per-mission process relaunch; BT_FE_* env arms the marshal (menu relaunches clear it). Verified: solo cycle (menu→mission→60s clock→stop→menu) and menu-hosted 2-node LAN.

  • Step 4 (Steam): CODE COMPLETE 2026-07-18 [T2 single-machine]. See steam-networking — the wire seam + L4STEAMNET identity-token transport + btl4lobby. Live 2-account cross-machine session pending (docs/STEAM_TEST.md).

  • Input-coverage audit + keyboard reconciliation: DONE 2026-07-20 [T2]. Root cause of the dead panel: MechRIOMapper's message ids were chained one enum too high (0x19-0x2a) — the binary RIO table @0051dd30 re-registers the BASE aux/zoom ids 3..0x13 (Hotbox 0x1a); fixed ungated + static_assert-locked (btl4mppr.hpp) → all 24 MFD-bank + 2 zoom buttons live (52/72 panel buttons worked at the time; the 8 dead ones are ALL wired as of 2026-07-26 — DuckRequest was the last, see §Dead-button backlog closed; 12 have no streamed mapping authored, authentically inert). Keyboard = the ~20 core gameplay actions on the CONTROLS.MAP keys; the PANEL covers every pod address by click (right-click = hold latch) — the merged default profile lives in L4PADBINDINGS.cpp (auto-writes content\bindings.txt; W/S/A/D/Q/E/R/F/X drive, 1-4/Space/Ctrl fire, M/N/H/C = pod buttons 0x18/0x15/0x2C/0x2F, /V view toggle + J/K/L preset cycles hardcoded in PadRIO). Bound keys are SUPPRESSED from the typed 1995 channel (PadRIO::SuppressKey, the btinput pattern — ended the glass double-dispatch: 'w'=pilot-select, numpad/F-key key-up aliases); unbound keys keep their authentic meanings (5/z presets, t-o pilot select, +/- zoom). In BAS control mode the pod turns with the STICK (Q/E) and pedals are inactive — authentic; A/D turn in MID/ADV. Arrow keys mirror the drive cluster (UP/DOWN = the W/S throttle lever, LEFT/RIGHT = the A/D pedals). Full audit + census: docs/GLASS_COCKPIT.md` §2026-07-20.

  • 2026-07-20 live "keymap regression" report: STALE EXE [T2]. The human-reported glass regressions (arrows/lever/detent dead, V/J/K/L/N inert, ground-fire) were a build-glass/Release/btl4.exe built BEFORE 5dd3536 landed — relaunched without rebuilding. No input-code change was needed; a clean rebuild + full SendInput re-verification (lever holds + zero stop, MID turns, V toggle shot-pair, level boresight, pod smoke) closed every symptom, and V's edge is now [T2]. BT_MPPR_TRACE now prints mode/pedals/stick/turn/elev. Procedural rule: rebuild every live build dir after landing an input/engine commit. Detail: docs/GLASS_COCKPIT.md §2026-07-20 (later).

  • 2026-07-20 human re-test: GAIT DETENT ported to the PadRIO lever [T2]. The remaining "our detent is missing" (mixed walk/run mode + repeated accel sample) was the gait DEAD BAND (locomotion): the pod-build keyboard lever snaps out of the walk/run hysteresis band at key-rest (mech4.cpp GAIT DETENT), and the PadRIO throttle slew lacked it. Ported faithfully: mech4.cpp publishes the band in lever units (gBTGaitDetentLo/Hi seam, +0.002 engage margin), L4PADRIO.cpp applies the identical nearer-edge snap when no throttle slew key/pad axis is active. Verified: parks at 0.414/0.465 snap to 0.358/0.504 (band = demand 22.02-30.87), no at-rest sample in-band, exactly ONE up-shift + ONE down-shift clip across a full graduated sweep, zero-stop + pod build unaffected. Detail: docs/GLASS_COCKPIT.md §2026-07-20 (later still). A/D turn in MID/ADV. Full audit + census: docs/GLASS_COCKPIT.md §2026-07-20.

  • Per-display cockpit windows (BT_GLASS_PANELS): DONE 2026-07-20 [T2 startup-verified]. The -platform glass preset now breaks the secondary displays out of the single combined panel/strip into ONE desktop window PER pod display, each carrying that display's surface WITH its RIO button bank placed around it, arranged pod-faithfully around the main view (engine/MUNGA_L4/L4GLASSWIN.*, new TU). Windows: Heat (UL) / Mfd2 (UC) / Comm (UR) / Mfd1 (LL) / Mfd3 (LR) each with 8 RED MFD buttons split 4 above / 4 below the surface; Secondary/Radar (portrait sec) with the 12 YELLOW Secondary buttons split left/right; a Flight Controls window for the no-display banks (throttle 0x38-3F, joystick/fire 0x40-47, panic). Each window is a GDI window (like gauges-hud's pad panel + the plasma window) — the surface is CPU-expanded from the shared gauge buffer (SVGA16::ExpandPlaneToBGRA, no D3D) and StretchDIBits'd in, so the whole D3D dev-composite path (dock / separate window / overlay) stands down while these are up (BTDrawGaugeInset/BTGaugeWindowRenderAndPresent early-return; no gBTGaugeDockBottom strip). Runtime gate BT_GLASS_PANELS (⚠ was "default ON under -platform glass" — NO LONGER TRUE; the surround is the glass default and panels are explicit-only, see §Layout modes below) — read by L4PADRIO (which panel to create), L4VB16 (suppress compositing), and btl4main (skip the world-window dock strip). The display↔bank map (spine of the feature): Heat 0x28-2F, Mfd2 0x20-27, Comm 0x30-37, Mfd1 0x08-0F, Mfd3 0x00-07, sec 0x10-1B (pod-hardware §Bank→MFD). Verified: build + bounded glass launch — [glasswin] per-display cockpit up (7 windows…), no crash, full mission loop un-regressed; surfaces carry LIVE gauge content (BT_GLASS_LOG = per-surface resolve/pixel diagnostic). Layout constants (surface sizes, exact button sub-positions, yellow left/right split) are marked tunable in L4GLASSWIN.cpp. Detail: docs/GLASS_COCKPIT.md. ⚠ Bug found + fixed during bring-up: surfaces were blank because application/ghWnd are duplicate-defined globals that bind non-deterministically under /FORCE — the fresh L4GLASSWIN TU read application == NULL → gauge renderer NULL. Fix: reach the renderer/window via BTResolveGaugeRenderer()/BTResolveMainWindow(), DEFINED in game/btl4main.cpp off its real btl4App/hWnd pointers (never the globals directly). See reconstruction-gotchas §6 duplicate-GLOBAL corollary.

  • Glass per-display panel button click-crash (Gitea #18): FIXED 2026-07-20 [T2]. Clicking a per-display Engineering panel button (0x21 reported) on an Eng page hard-crashed — cdb: a wild call through an uninitialised pointer (eip=cdcdcdcd debug / 0x01048748 release) with zero btl4 frames above Receiver::Receive. Root cause was NOT a /FORCE unresolved external (the glass link log carried only the known-benign mech3.obj CreateStreamedSubsystem set) but a dense message-handler-table GAP slot (reconstruction-gotchas §11): the streamed Eng-page .CTL dispatches subsystem msg id 3 / 0xb to whatever subsystem is shown, and a weapon (Emitter) registers only PoweredSubsystem 4-8 + MechWeapon 9-10 — so id 3 is a gap below entryCount, and Receiver::MessageHandlerSet::Build's new HandlerEntry[] left it as heap garbage → (this->*garbage)(msg). The 1995 binary has the identical non-zeroing new[] and survived only on fresh-heap-zero luck (id 3 on a weapon was always meant to be IGNORED — it's a Condenser/Reservoir action in a different subsystem branch; the uniform Eng-page button template makes buttons for unimplemented actions authentically inert). The new per-display windows just made the id reachable live. Fix (engine, class-wide, faithful): Build null-inits every slot (entryID=0/entryName=""/entryHandler=NullHandler) before copying inherited / placing supplied, so a gap is deterministically NullHandler → Receive drops it — the authentic "Receiver ignores unhandled messages", not a glass-path guard. Fixes the entire dead-button class (#14) at once. Verified: click-soaked every MFD bank (Engineering/L+R Weapons/Heat/Comm, ~130 clicks through Quad↔Eng page cycles) under cdb — ZERO AVs, and the reconstructed mapper preset selects still fire ([mode] preset (1,1)/(2,1)…); pod build + solo un-regressed. The [glasswin] CLICK forensics log (names the button before dispatch) landed with this. Detail: engine/MUNGA/RECEIVER.cpp Build.

Key Relationships