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