Distributed test zips go stale and stray. BT_EXPIRE (CMake option,
DEFAULT ON so a zip can never accidentally ship without it) makes the
exe refuse to start BT_EXPIRE_DAYS (default 14) after its build day:
boot logs the remaining window, an expired build logs + shows a
"test build expired -- ask the operator for the current build" box and
exits. btversion.h now stamps BT_BUILD_UNIX (UTC-midnight-rounded so
the only-on-change header churns once per day, not every build) +
BT_BUILD_DATE. Dev is unaffected: every build re-stamps the day.
-DBT_EXPIRE=OFF builds a non-expiring exe; mkdist reads the cache and
marks such a zip "-noexpire" + prints a warning, and the tester README
expiry note is {EXPIRE}-marker-gated like the steam lines. Second
compile-gate exception after BT_STEAM (convention updated in
glass-cockpit.md). Quality gate, not DRM -- a clock rollback defeats it.
Verified live: default build logs "test build window: 14 day(s) left";
a -DBT_EXPIRE_DAYS=0 build logs TEST BUILD EXPIRED and parks on the
explanation box (window title checked) without starting the game;
restored to 14 and rebuilt clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
212 lines
17 KiB
Markdown
212 lines
17 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.
|
|
|
|
## 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 now work; 8 dead await handler reconstruction — Gitea backlog issue;
|
|
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`** (default ON under `-platform glass`, `=0` falls
|
|
back to the single pad panel + docked gauges) — 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
|
|
- Extends: [[pod-hardware]] (RIO input path) · Uses: [[gauges-hud]] dev-gauge modes ·
|
|
Feeds: [[multiplayer]] (miniconsole hosting, Steam transport) · Build: [[build-and-run]]
|