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>
685 lines
48 KiB
Markdown
685 lines
48 KiB
Markdown
# GLASS_COCKPIT — running ledger (desktop developer layer)
|
||
|
||
Detailed running log for the `glass-cockpit` branch work: PadRIO, on-screen panel/plasma,
|
||
miniconsole, Steam transport/lobby. Curated digest: `context/glass-cockpit.md` (read that
|
||
first); this file carries the step-by-step detail, evidence, and verification transcripts.
|
||
|
||
---
|
||
|
||
## 2026-07-17 — Step 1: branch + compile gates + scaffolding
|
||
|
||
- Branch `glass-cockpit` created from master @ `e2c21c4`.
|
||
- `CMakeLists.txt`: `option(BT_GLASS OFF)` + `option(BT_STEAM OFF)`; `BT_STEAM` without
|
||
`BT_GLASS` is a configure-time FATAL_ERROR; gates append to `BT_DEFS` (flows to all three
|
||
targets); `STATUS` line prints gate state at configure. Gated `target_sources` blocks will be
|
||
added per-TU as they land (CMake would reject nonexistent source files now).
|
||
- `.gitignore`: `build-pod/`, `build-steam/`, `content/bindings.txt`, `content/frontend.egg`,
|
||
`content/steam_appid.txt`.
|
||
- Verification (this step): defaults configure prints `BT_GLASS=OFF BT_STEAM=OFF` and builds
|
||
(purity build); `-DBT_GLASS=ON` and ON/ON configures print correct gate state; the
|
||
BT_STEAM-without-BT_GLASS negative test FATAL_ERRORs. Full 3-config BUILD matrix becomes
|
||
meaningful from step 2 (first gated code) and is re-run there.
|
||
|
||
## 2026-07-17 — Step 2a: the .CTL positional-attribute off-by-one — CONFIRMED and FIXED (ungated)
|
||
|
||
**The bug [T2, live-verified]:** the streamed `.CTL` "L4" mapping resource carries the 1995
|
||
binary's positional attribute ids (stick=3, throttle=4, ...), resolved at install time by
|
||
`Simulation::GetAttributePointer(id)` → `AttributeIndexSet::Find(id)` = `attributeIndex[id-1]`
|
||
(positional). `MechControlsMapper` chained its enum on `Subsystem::NextAttributeID == 2`, but
|
||
the binary's chain reaches 3 (one base attribute we don't publish) — so EVERY streamed mapper
|
||
record landed one member late.
|
||
|
||
**Evidence (new `BT_CTRLMAP_LOG=1` diagnostic in `LBE4ControlsManager::CreateStreamedMappings`,
|
||
`engine/MUNGA_L4/L4CTRL.cpp` — permanent, env-gated, default off).** Before the fix
|
||
(`build-pod` Debug, `-egg DEV.EGG`, dev platform):
|
||
```
|
||
rec 0 subsys 0 group 1(Joy) elem 0 attrID 3 -> +0xe4 (= throttlePosition: WRONG, stick intended)
|
||
rec 1 subsys 0 group 0(Scalar) elem 0 attrID 4 -> +0xe8 (= pedalsPosition: WRONG, throttle intended)
|
||
rec 2 subsys 0 group 3(Button) elem 63 attrID 6 -> +0xf0 (= speedDemand: WRONG, reverse intended)
|
||
rec 4-6 attrID 10/11/12 -> lookRight/lookBehind/lookDown (one late; LookLeft/Right/Behind intended)
|
||
```
|
||
Spacing attr3→attr4 was 4 bytes (Scalar) where stick→throttle must be 8 (ControlsJoystick).
|
||
The weapons chain (attrID 19 → +0x31c TriggerState) was already correct — mechweap was
|
||
pad-aligned in task #16 (gotcha #11). Torso (subsys 17) also aligned (MechSubsystem publishes
|
||
binary id 2). ONLY the mapper chain (subsys 0) was off. On the dev box the keyboard INPUT
|
||
BRIDGE (writes the mapper members directly, after the engine push) masked it; on a REAL POD the
|
||
serial RIO throttle scalar would have driven `pedalsPosition` and the stick x/y would have
|
||
driven throttle+pedals — the Phase-8 landmine prior art predicted.
|
||
|
||
**The fix (ungated, pod-correctness — user-approved 2026-07-17):** `mechmppr.hpp` pins the enum
|
||
to the binary numbering (`MechControlsMapperPadFirstAttributeID = 2`, stick=3 ... PilotArray=
|
||
0x16); `mechmppr.cpp` pads id 2 with a valid never-bound entry (the mech.cpp attrPad idiom) and
|
||
locks the base + pinned ids with `static_assert`s. `L4MechControlsMapper::TargetRangeExponent`
|
||
(chained) moves 0x16→0x17, matching the binary's own arithmetic; it is bound by name (audio
|
||
zoom-blip), so no consumer shifts.
|
||
|
||
**After the fix:** `attrID 3 -> +0xdc` (stick), `attrID 4 -> +0xe4` (throttle, 8-byte spacing
|
||
✓), `attrID 6 -> +0xec` (reverse), `10/11/12 -> +0xfc/+0x100/+0x104` (the look trio); torso +
|
||
weapons unchanged. Regression: headless forced-walk run (`BT_FORCE_THROTTLE=1`) drives normally
|
||
— `[mppr] thr=1 -> speedDemand=61.501`, gait cycles, position advances, 31/31 subsystems
|
||
simulated, no faults.
|
||
|
||
## 2026-07-17 — Steps 2b-2e: PadRIO, stand-downs, displays, preset — DONE + verified
|
||
|
||
**2b (RIOBase + PadRIO, commit ac57a47):** `L4RIO.h` splits abstract `RIOBase` (enums, the five
|
||
analog Scalars, pure `GetNextEvent`/`SetLamp`, no-op serial ops, NEW `IsOperational()`) out of
|
||
serial `RIO` (`: PCSerialPacket, RIOBase` — ctor assigns as before, zero behavioral delta);
|
||
`L4CTRL.h` `rioPointer` retyped `RIOBase*`; gated `L4CONTROLS=PAD` factory arm. NEW gated TUs
|
||
`L4PADRIO.*` (XInput+keyboard → the surface; 3s hot-plug; focus-guarded keys; per-poll
|
||
AnalogEvent heartbeat time-gated so the manager's drain loop terminates; `lampState[]`;
|
||
static `SetScreenButton` — button addresses AND the keypad space 0x50-0x6F → KeyEvents) and
|
||
`L4PADBINDINGS.*` (`content\bindings.txt`, default written on first run; deflect/slew/set axis
|
||
model; addresses validated vs ButtonCount).
|
||
|
||
**2c (stand-downs, commit 9f35a80):** engine query `BTRIODevicePresent()` (ungated —
|
||
pod-correct); both dev bridges (`mech4.cpp` attr writes, `mechmppr.cpp` BT_KEY_BRIDGE) gate
|
||
their WRITES: unset=auto / "0"=off / else on; forced harness always rides. **Definitive live
|
||
test:** OS-injected LSHIFT hold slewed PadRIO throttle 0→0.33→1.0 with `pre==thr` every frame —
|
||
the value arrives via the ENGINE push through the streamed `.CTL` binding (bridge stood down),
|
||
`speedDemand=61.501` from authentic InterpretControls.
|
||
|
||
**2d/2e (displays + preset, commits db9ac94 + 418acf0):** the on-screen panel follows **vRIO**
|
||
(`C:\VWE\vrio` — `CockpitLayout.cs`/`PanelCanvas.cs`, per Cyd): five 4x2 MFD clusters
|
||
(descending addresses), Throttle/Secondary/Screen/Joystick 1x8 columns, two 4x4 hex keypads
|
||
(0x50 internal→pilot unit, 0x60 external→operator unit), physical names, red/yellow/blue
|
||
coloration, 500/250/125ms flash, right-click latch — 104 controls. `PlasmaWindow :
|
||
Video8BitBuffered` renders the 128x32 plasma via the SAME gauge-renderer path as serial
|
||
(buffer is TOP-DOWN — the flip rendered it upside down, user-reported + fixed). MFD surfaces =
|
||
the EXISTING dev-gauge modes (no new code, no L4VB16 exposure). `-platform glass` preset +
|
||
run.cmd token. **Button-trap guard (e2c21c4 pattern):** the authentic base
|
||
`ConfigureMappableMessageHandler` FAIL trap is `abort()` under DEBUGOFF — any aux/zoom button
|
||
without its reconstructed L4 override KILLED the game on press (panel makes all reachable).
|
||
Default now log+ignore; `BT_BUTTON_TRAP=1` restores the hard trap. Verified: synthetic panel
|
||
click → 2 `[FAIL]` (press+release through panel→PadRIO→ProcessRIOEvent→streamed mapping), game
|
||
survives. NOTE: each `[FAIL]` marks a missing L4 override — reconstruction backlog, see
|
||
[[open-questions]].
|
||
|
||
**Step-2 verification matrix (all passed 2026-07-17):**
|
||
1. Purity: defaults `build-pod/` compiles (known mech3 /FORCE externals only); seam audit —
|
||
`BT_GLASS`/`BT_STEAM` mentions ONLY in CMakeLists, btl4main.cpp preset region, L4CTRL.cpp
|
||
(include+PAD arm), L4GREND.cpp (include+SCREEN branch), gated TUs, run.cmd docs.
|
||
2. Pod solo forced-walk clean (§2a).
|
||
3. **2-node loopback MP on the pod build:** A `-egg MP.EGG -net 1501` + B `-net 1601` +
|
||
`btconsole.py` relay — both nodes launch into the running mission (77/76 ticks, mechs at
|
||
their drop zones, gait/target loops live).
|
||
4. Glass build: PadRIO drives through the authentic mapper path; panel click path live; plasma
|
||
window; all boot lines present.
|
||
Deferred to the user (visual-only): panel/plasma appearance vs vRIO on screen, XInput stick
|
||
feel, `L4PADFLIP` polarity.
|
||
|
||
## 2026-07-17 — Step 3: the miniconsole — DONE + verified (commit c4ca87b)
|
||
|
||
**Architecture: the console stays a wire peer.** `game/glass/btl4fe.*` (GDI catalog menu + the
|
||
console-shape egg writer) and `btl4console.*` (the LocalConsole marshal: a worker-thread console
|
||
CLIENT speaking the `tools/btconsole.py` protocol verbatim — 1040-byte chunked
|
||
`ReceiveEggFileMessage`, 20s settle, `RunMission`×2, stay connected, own the clock,
|
||
`StopMission`(id 6)@expiry; logs `content\marshal.log`). The engine runs the 100% stock console
|
||
ladder — ZERO engine hooks; the planned `L4APP.H` setters proved unnecessary (launches pass real
|
||
argv on relaunch). WinMain: one gated block — zero-mission-arg glass launch → menu → relaunch
|
||
self with mission argv (`BT_FE_EGG/PODS/SECS` arm the marshal; `BT_FE_LOOP` returns exits to the
|
||
menu; menu relaunches CLEAR the handoff — the leak re-armed a marshal in cycle test 1).
|
||
|
||
**Verified (synthetic menu drives, all autonomous):**
|
||
- Golden egg: `frontend.egg` section sequence + line offsets IDENTICAL to `MP.EGG`; ordinal
|
||
pages verbatim; GDI name-glyph pages in MP.EGG framing (32/8 rows, 16 bytes/line).
|
||
- Solo cycle: menu → egg → relaunch → marshal → stock ladder → mission RUNS (58+ ticks) → 60s
|
||
clock → stop → menu returns with clean env.
|
||
- **HOST LAN 2-node:** peer started manually (`-net 1601`); menu-hosted launch → marshal feeds
|
||
BOTH pods (2-pilot egg, 10 chunks each) → both in-mission (65/18 ticks) → stop both → host
|
||
returns to the menu, peer idles at console-wait.
|
||
|
||
## 2026-07-17 — Step 4 pre: Steamworks SDK 1.64 vendored (commit 23c0019)
|
||
|
||
Official Valve zip (provided by Cyd at the repo root), extracted to
|
||
`extern/steamworks_sdk_164/` and trimmed to `public/` + `redistributable_bin/` + Readme
|
||
(12.5 MB); zip removed. Consumed only under `-DBT_STEAM=ON`.
|
||
|
||
## 2026-07-17/18 — Step 4: the Steam wire — CODE COMPLETE (commits e79e8fa, 48ede2f, f703bb1)
|
||
|
||
**4a (e79e8fa) — the wire seam:** file-local `BTNetSend/Recv/Accept/Close` wrappers around the
|
||
9 raw Winsock sites in `L4NET.CPP`; OFF builds are pure code motion. Regression: 2-node loopback
|
||
MP through the seam — mesh forms, full 31/31 mission on both nodes.
|
||
|
||
**4b (48ede2f) — the transport:** gated TU `L4STEAMNET` — pseudo-SOCKETs (0x5EA0xxxx) behind the
|
||
seam; the engine's TCP byte stream rides reliable-NoNagle Steam messages re-assembled into
|
||
per-connection rings (recv semantics preserved: empty→WSAEWOULDBLOCK, closed→0); seam
|
||
completions: `CheckSocket` (peer = token address), `OpenConnection` TCP_OPEN delegate,
|
||
`GetMyAddress` self-token. Install on `BT_STEAM_NET=1`; graceful degrade to Winsock at every
|
||
step. BOTH pumps needed (`SteamAPI_RunCallbacks` + sockets `RunCallbacks`).
|
||
|
||
**4c (f703bb1) — the lobby + THE FAKEIP LESSON [T2]:** prior art assumed FakeIP survives the
|
||
per-mission process relaunch — DISPROVEN live (menu process got 169.254.124.110, the mission
|
||
process got 169.254.54.234; the pod timed out connecting to its own stale address). Redesign:
|
||
roster addresses are opaque ipv4-shaped TOKENS (`169.254.77.N`, ports 1501 console / 1502 game)
|
||
minted by the host at GO and mapped to Steam IDENTITIES; connections ride
|
||
`ConnectP2P(identity, channel)` (console=0, game=1); the map hands off via env
|
||
(`BT_FE_MYFAKE`/`BT_FE_STEAMMAP`). `btl4lobby`: ISteamMatchmaking room (`btl4=1` filter),
|
||
pilots publish name/mech/color, host GO mints tokens + writes `btl4map` + `btl4go`.
|
||
|
||
**Verified single-machine (ON/ON, live Steam client):** menu → HOST STEAM LOBBY → room → GO →
|
||
map `169.254.77.1=<steamID64>` → egg roster carries the token → mission process
|
||
`[steamnet] up ... 1 roster token(s) incl. self` → pod self-matches → stock console ladder →
|
||
mission RUNS (46 ticks). 3-config build matrix green throughout. **Remaining (user hardware):**
|
||
the live 2-account cross-machine session — procedure in `docs/STEAM_TEST.md`.
|
||
|
||
## 2026-07-20 — Glass input-coverage audit + keyboard reconciliation
|
||
|
||
**Trigger:** making BT_GLASS the permanent player experience — familiar desktop keys dead, "most
|
||
panel buttons do nothing". Full audit of every input surface (bindings.txt keys, the 72-button
|
||
panel, the streamed .CTL EventMapping records, the typed 1995 channel), then the fix set.
|
||
|
||
### Finding 1 — THE panel-dead-buttons root cause: MechRIOMapper message ids shifted [T1→T2 fixed]
|
||
`MechRIOMapper`'s enum chained `Aux1QuadMessageID = L4MechControlsMapper::NextMessageID` (=0x19),
|
||
registering all 18 RIO handlers at ids 0x19-0x2a. The binary RIO table @0051dd30 RE-REGISTERS the
|
||
base aux/zoom ids **3..0x13** with the override handlers (Hotbox = 0x1a @0051de98) — that
|
||
re-registration over the base FAIL-trap ids is the whole point of the derived table. Streamed
|
||
`.CTL` events carry the binary ids, so every MFD-bank/zoom press found only the base
|
||
`ConfigureMappableMessageHandler` → `[FAIL]` (26 of 72 panel buttons dead; on a real pod with the
|
||
hard trap, an abort()). Hotbox worked by accident (sender + entry both used the same wrong 0x2a).
|
||
Fixed: ids pinned to the binary numbering + static_assert locks (`btl4mppr.hpp`). Verified live:
|
||
panel 0x0F/0x08/0x27/0x20 → `[mode] preset` lines, 0x10/0x11 zoom no longer FAIL. (Residual
|
||
fidelity gap: the binary RIO table also carries its OWN Keypress id 0x19 → @004d2514,
|
||
unreconstructed; we still register the shared L4 Keypress 0x17 — works, logged in the backlog.)
|
||
|
||
### Finding 2 — the always-active msg-4 records IDENTIFIED [T1] (closes the #9 open question)
|
||
Binary message tables decoded from section_dump (16-byte {id,name,fn,0} entries): per-receiver-
|
||
class id spaces. Roster (ARENA1 MadCat): subs 3=Reservoir, 4-9=Condenser1-6, 10-13=GeneratorA-D.
|
||
So: **0x2C = the COOLANT FLUSH button** (Reservoir InjectCoolant, WORKS), **0x2F/0x2E/0x2D/0x2B/
|
||
0x2A/0x29 = per-condenser VALVE buttons** (MoveValve, WORK), **0x1A-0x1D = Generator A-D ON/OFF**
|
||
(`ToggleGeneratorOnOff` id 4, table @0050fb90 fn @004b1ed0 — DEAD, unreconstructed). Also new:
|
||
0x13 → Mech **DuckRequest** (0x1a @0049fa00 — the manual's CROUCH button!), 0x28 → Mech
|
||
**BalanceCoolant** (0x16 @0049f728), 0x12 → ThermalSight **ToggleLamp** (id 3, table
|
||
@0x51120C fn @004b860c), 0x14 → Searchlight + Searchlight2 **ToggleLamp** (id 3, table @0x51117C
|
||
fn **@004b838c**) — ✅ both WIRED 2026-07-25 (#61); the two fn addresses were swapped here before
|
||
that (see `context/decomp-reference.md`). Weapon
|
||
Eng-page extras: msg 0x3 = **ToggleCooling** (HeatableSubsystem @0050e41c fn @004ad6f8, unwired),
|
||
msg 0xb = **ToggleSeekVoltage** (energy @004ba478) / **EjectAmmo** (ammo weapons @004bb9b8),
|
||
Myomers msg 9 = ToggleSeekVoltage (@004b8a48; body exists, unwired). Unhandled messages are
|
||
SILENT (Receiver::Receive ignores an unfound id) — the dead-button census cannot come from
|
||
[FAIL] logs alone; it comes from the BT_CTRLMAP_LOG record dump × the handler tables.
|
||
|
||
### Finding 3 — glass keyboard: stand-down + double dispatch [T2]
|
||
ALL btinput actions (drive keys, M/N/J/K/L/H/C/V/B/G/F5-F9) stand down when PadRIO owns the path
|
||
— nothing on the game side polls keys directly (BTInputPoll is the only GetAsyncKeyState
|
||
consumer). Meanwhile the typed 1995 channel stays LIVE (glass preset = `PAD,KEYBOARD`) with NO
|
||
suppression — so Cyd's old bindings double-dispatched: W (keypad row) also typed 'w' = pilot
|
||
select 0; C (fire middle) also typed 'c' = MFD3 Eng3; NUMPAD key-UPS alias to letter hotkeys
|
||
(VK_NUMPAD2=0x62='b' = MFD3 Quad, VK_F5=0x74='t' = pilot select 3). This is the historic
|
||
pre-btinput double-dispatch, back on the glass side.
|
||
|
||
### The fix set (landed)
|
||
1. `btl4mppr.hpp` — RIO id pinning (ungated, pod-correctness; the step-2a idiom).
|
||
2. `L4PADRIO.*` — typed-channel suppression tables built from the loaded bindings (+hardcoded
|
||
keys), `PadRIO::SuppressKey` consumed by a `#ifdef BT_GLASS` arm next to the btinput hook in
|
||
`L4CTRL.cpp:1506` block; V joins backtick on the view-toggle edge; J/K/L fire
|
||
`gBTPresetCycle[3]` (the same seam the dev poll feeds — no binary analog exists for a "cycle"
|
||
button; the body is the authentic SetPresetMode).
|
||
3. `L4PADBINDINGS.cpp` + `content\bindings.txt` — the MERGED default map (user decision:
|
||
keyboard = the ~20 core gameplay actions on the CONTROLS.MAP keys; the PANEL covers all 72
|
||
pod buttons by click/latch). W/S throttle, A/D pedals, Q/E twist, R/F elevation, X all-stop,
|
||
1-4/Space/Ctrl fire + LALT reverse + B look-behind, M=0x18, N=0x15, H=0x2C (flush hold),
|
||
C=0x2F (Condenser1 valve), G=0x0E (weapon-1 configure, Mfd1-Quad-gated), F5-F9=Mfd2 bank
|
||
0x27-0x24+0x22 (page-gated gen A-D/mode), numpad flight cluster kept. DISPLACED from Cyd's
|
||
map (each reachable on the panel or by editing bindings.txt): number row→secondary panel,
|
||
QWERTY row→pilot keypad (dropped), arrows→hat looks (arrows now drive), V/C/B→fire
|
||
0x47/0x46/0x45, LCTRL→throttle-down. XInput pad section untouched.
|
||
NOTE (control modes, authentic): in BAS mode the pod turns with the STICK (Q/E here) and the
|
||
pedals are inactive; A/D turn in MID/ADV — exactly the manual's behavior, unlike the desktop
|
||
bridge which re-routes A/D per mode.
|
||
4. Un-touched: CONTROLS.MAP, btinput defaults, pod serial path. `bindings.txt` is read only by
|
||
`PadBindingProfile` inside the gated PadRIO TU — the pod build never opens it.
|
||
|
||
### Verification (2026-07-20, build-glass2, ARENA1 solo, all logs in-session)
|
||
- Panel: 0x0F→preset(0,1), 0x08→(0,0), 0x27→(1,1), 0x20→(1,0), zoom no-FAIL; 0x15 display
|
||
1→2→0; 0x2C hold → `[flush] Reservoir level 6→0`; dead-class buttons (0x13/0x1A/0x12/0x14)
|
||
silent as predicted. ZERO [FAIL] lines in the whole session (was 2 per bank press).
|
||
- Keyboard: W hold → thr 0→1, speedDemand 61.501, gait advances; Q → turnDemand 0.94 (BAS);
|
||
M → `control mode -> 1`; A hold → turnDemand 1.0 (MID); Space → `[emitter] FIRED`; N →
|
||
`display notify 1`; J/K/L → presets (0,1)/(1,1)/(2,1); H → reservoir drain; C → condenser#1
|
||
valveState 1→5. BT_SHOT frame confirms visually: MID lit on the control-mode column, CRITICAL
|
||
DAMAGE schematic up, MFDs on Eng pages, heading moved. V not re-injected (user took
|
||
foreground mid-run) — code-identical to the verified backtick edge [T3].
|
||
- Suppression: typed 'w' swallowed (no [keych]), typed 't' flows ([keych] 0x74), WM_KEYUP
|
||
VK_F5 swallowed (the 't'-alias hazard).
|
||
- Un-regression: build-pod (gates OFF) compiles 0 errors; forced-walk run drives
|
||
(speedDemand=61.501, 32 subsystems, streamed ids 3→+0xdc / 4→+0xe4 unchanged).
|
||
|
||
### 2026-07-20 (later) — the "keymap regression" live report: STALE EXE, not code [T2]
|
||
|
||
**Report (human, live glass session):** arrows dead/half-dead (UP accelerated, no detent;
|
||
LEFT/RIGHT no turn), throttle not a lever, V no view toggle, J/K/L/N inert, weapons firing into
|
||
the ground. **Root cause: `build-glass/Release/btl4.exe` was built 07:47 — BEFORE commit
|
||
`5dd3536` (08:35) landed the whole audit fix set** (SuppressKey tables, V/J/K/L hardcoded keys,
|
||
the RIO-id pinning that makes 0x15/N work) — the session was relaunched without rebuilding, and
|
||
kept running the pre-audit image. `content\bindings.txt` (08:23) already WAS the merged map and
|
||
the loader parsed it fine (`[padrio] bindings loaded: 40 keys, 10 pad buttons, 5 pad axes`), so
|
||
every bindings-file symptom analysis was a red herring; the binary was simply old. The
|
||
ground-fire symptom is the same session: with the R/F/X semantic chaos of the unsuppressed typed
|
||
channel, the torso elevation had been driven down and nothing recentered it.
|
||
|
||
**Fix:** `rm build-glass/Release/btl4.exe && cmake --build build-glass --config Release` (0
|
||
`error C`). No input-code change was needed — arrows/lever/detent/V were all already correct at
|
||
HEAD. One diagnostic enrichment landed: the `BT_MPPR_TRACE` `[mppr-c]` line now also prints
|
||
`mode/pedals/stick/turn/elev`, so one env-gated line proves the whole control surface.
|
||
|
||
**Re-verification (build-glass, DEV.EGG solo, SendInput-injected keys, own PID only):**
|
||
- UP hold 0.9 s → thr 0→0.63, dmd 38.70; released → **holds** 0.63 for 3 s (persistent lever).
|
||
- DOWN hold 2.5 s → thr 0.36→0.0028→0, rev=0 throughout — **stops at zero, no reverse
|
||
crash-through** (the wire throttle is the authentic 0..1 lever; reverse is the 0x3F button).
|
||
- M → `[mode] control mode -> 1`; LEFT → pedals +1 / turnDemand +1 (CCW=left, the wire sign);
|
||
RIGHT → pedals −1 / turnDemand −1; both auto-center on release. (In BAS the pedals are
|
||
authentically inactive — turns are the stick, Q/E.)
|
||
- R/F → stick.y ±1, torso elev 0→+0.349→−0.195, holds where released (positional torso,
|
||
matches the btinput path); **boresight level at spawn (elev=0)**.
|
||
- V → `[view] COCKPIT eyepoint` ↔ `[view] external chase`, BT_SHOT pair confirms
|
||
(chase mech ↔ canopy + reticle). Closes the audit's one T3 (V edge) as [T2].
|
||
- SPACE → `[emitter] FIRED #1`. Pod build rebuilt + smoked: BT_AUTODRIVE=0.8 →
|
||
dmd=49.2008, clean log, no cross-contamination.
|
||
|
||
**Lesson (procedural):** after landing an engine/input commit, REBUILD every live build dir
|
||
before relaunching — a stale `build-glass` exe reproduces "the whole keymap is broken" exactly.
|
||
|
||
### 2026-07-20 (later still) — human re-test: GAIT DETENT ported to the PadRIO lever [T2]
|
||
|
||
**Report (fresh build):** zero-stop confirmed working, but "OUR detent is still missing — it
|
||
goes into that mixed walk/run mode and repeats the acceleration sample." That is the documented
|
||
gait DEAD BAND (locomotion.md [T1]): the SM has no stable state for a demand in
|
||
(`walkStrideLength`@0x534, `reverseSpeedMax`@0x538) — parked there it cycles 7→10→14→7 forever,
|
||
retriggering EngineShiftFwd/Rev each swing. The pod-build keyboard lever carries an
|
||
accommodation (mech4.cpp "GAIT DETENT": at KEY-REST, snap the lever out of the band to the
|
||
NEARER edge; sweeping through while held stays continuous) — the PadRIO slew channel had no
|
||
such protection, so the glass lever could park in-band.
|
||
|
||
**Port (faithful copy, not an invention):** mech4.cpp publishes the band in lever units every
|
||
driven frame through a new seam (`gBTGaitDetentLo/Hi`, same guards + the same +0.002 engage
|
||
margin; hi<=lo = unknown); `L4PADRIO.cpp Poll()` applies the IDENTICAL nearer-edge snap to the
|
||
Throttle channel when the slew is at rest (no slew key held, no pad slew axis deflected) —
|
||
same rule, same log tag, gated `BT_GAIT_TRACE`. The rest decision uses PadRIO's own held-state
|
||
(the lever's owner on glass), not gBTDrive.keyFwd/Back.
|
||
|
||
**Verified (build-glass, DEV.EGG solo, graduated UP-taps parking the lever every ~0.10 across
|
||
0..1, BT_GAIT_TRACE+BT_MPPR_TRACE):** band [0.358026, 0.50388) = demand 22.02→30.87 (the
|
||
locomotion.md Blackhawk numbers); park at 0.4137 → snapped DOWN to 0.358026, park at 0.465126 →
|
||
snapped UP to 0.50388 (both nearer-edge directions); rest-position histogram shows NO at-rest
|
||
sample inside the band; gait states: ONE walk→run clip (state 10, once) on the whole ascending
|
||
sweep, ONE run→walk (state 14, once) descending, normal 6↔7 / 12↔13 cycling between — the
|
||
accel/shift sample can fire only once per real transition. Zero-stop unaffected (thr=0 rest,
|
||
state 0). Pod build rebuilt + BT_AUTODRIVE smoke clean (the seam publish is a pure global
|
||
write on the pod path).
|
||
|
||
## 2026-07-18 — Merge origin/master (commit f889e24): btinput meets PadRIO
|
||
|
||
Master's 33-commit run landed (D1 relay/operator-console line, CONTROLS.MAP+XInput binding
|
||
engine `btinput.cpp`, camera seats, torso pitch, sign fixes, the 1995 manual, version stamping).
|
||
**The reconciliation:** btinput (ungated, dev/pod desktops) joined the stand-down convention —
|
||
`BTInputPoll` yields and `BTInputSuppressKey` claims nothing when an operational cockpit device
|
||
(serial RIO / PadRIO) owns the input path (`BTRIODevicePresent`; `BT_KEY_BRIDGE` override;
|
||
forced harness exempt). **One input system per mode:** `CONTROLS.MAP`/btinput on dev+pod
|
||
desktops, `bindings.txt`/PadRIO on glass. The D1 relay keeps its own raw sockets by design
|
||
(relay and Steam are alternative wires; the BTNet seam covers the arcade mesh both need).
|
||
Verified: 3-config build, glass stand-down live, pod forced-walk with btinput active, 2-node
|
||
loopback MP 31/31.
|
||
|
||
## 2026-07-19 — Mission-control options: the AUTHORITATIVE catalog (TeslaSuite console.ini)
|
||
|
||
Source: `C:\VWE\TeslaSuite\410console\4_10-console-extracted\Console.ini`, the Mac 4.10 operator
|
||
console's `BT::` adventure tree (the `tag=` values are the egg field values; display names are the
|
||
console's). Cross-checked vs `410console/battletech-port/BATTLETECH-PORT-SPEC.md` + `reference/
|
||
cavern.egg`. This is every option the original console exposed for a BattleTech mission.
|
||
|
||
**Mission-level** — `[mission]`: `adventure=BattleTech` (fixed), `temperature=27` (fixed),
|
||
`scenario=freeforall` (the tag is always freeforall; the RESPAWN choice is the ROLE):
|
||
- map (8): cavern, grass, rav, polar3, polar4, arena1, arena2, dbase
|
||
- time (4): morning, day, evening, night ← was day/night/morning/**dusk** (wrong: evening)
|
||
- weather (3): clear, fog (light), soup (heavy)
|
||
- role (2): Default (`model=dfltrole`, respawn) / NoReturn (`model=noreturn`, one life)
|
||
- length: seconds
|
||
|
||
**Per-pilot** — participant block:
|
||
- experience (4 [T1 game decode]): novice, standard, veteran, expert ← was hardcoded `expert`
|
||
- vehicle (18, friendly→tag): Black Hawk=blkhawk, Loki=loki, Nova=bhk1, Mad Cat=madcat, Thor=thor,
|
||
Owens=owens, Kotori=own1, Summoner=thr1, Hellbringer=lok1, Timber Wolf=mad1, Avatar=avatar,
|
||
Satyr=ava1, Sunder=sunder, Sunder V1=snd1, Vulture=vulture, Mad Dog=vul1, Mischief=lok2,
|
||
Zanin Neko=mad2 (the 18 ModelList names; matches the 2026-07-18 vehicle sweep)
|
||
- color/camo (7, display→tag): White=White, Black=Black, Brown=Brown, Red=**Crimson**, Green=Green,
|
||
Gray=**Grey**, Tan=Tan ← was White/Black/Crimson only
|
||
- badge/emblem (7, the `badge=` field): VGL, Davion, Kurita, Liao, Marik, None, Steiner ← was `VGL`
|
||
- patch/badge-color (8, the `patch=` field): Yellow, Blue, Grey, Green, Violet, Red, White, Black
|
||
← was `Yellow`
|
||
- dropzone (5, the `dropzone=` field): one, two, three, four, five ← was `one`
|
||
- advancedDamage (0/1): the advanced damage model toggle ← was hardcoded `1`
|
||
- vehicleValue: 1000 (const), loadzones=1 (const), bitmapindex (per-pilot ordinal)
|
||
|
||
---
|
||
|
||
## 2026-07-20 — Per-display cockpit windows (BT_GLASS_PANELS)
|
||
|
||
**Goal (Cyd):** stop collapsing the desktop cockpit into combined surfaces. Break each
|
||
SECONDARY display out into its OWN window carrying that display's surface WITH its RIO button
|
||
bank placed around it, arranged pod-faithfully around the main 3D view — a dev cockpit that
|
||
reads like the pod, so button/lamp/display behavior can be validated in position.
|
||
|
||
**New TU: `engine/MUNGA_L4/L4GLASSWIN.{h,cpp}`** (BT_GLASS-only; added to the CMake BT_GLASS
|
||
`target_sources`). Builds SEVEN GDI windows and positions them around the main window:
|
||
|
||
| Window | Surface (port) | RIO bank | Buttons |
|
||
|---|---|---|---|
|
||
| Heat MFD (UL) | `Heat` (0x4000) | 0x28-0x2F | 8 red, 4 above / 4 below |
|
||
| MFD 2 (UC) | `Mfd2`/`Eng2` (0x0400) | 0x20-0x27 | 8 red, 4 above / 4 below |
|
||
| Comm MFD (UR) | `Comm` (0x8000) | 0x30-0x37 | 8 red, 4 above / 4 below |
|
||
| MFD 1 (LL) | `Mfd1`/`Eng1` (0x0100) | 0x08-0x0F | 8 red, 4 above / 4 below |
|
||
| MFD 3 (LR) | `Mfd3`/`Eng3` (0x1000) | 0x00-0x07 | 8 red, 4 above / 4 below |
|
||
| Secondary/Radar | `sec` (palette, portrait) | 0x10-0x1B Secondary | 12 yellow, 6 left / 6 right |
|
||
| Flight Controls | (none) | 0x38-0x47 (+ panic/icom) | neutral, 2 columns |
|
||
|
||
**Architecture — GDI + CPU blit, NO D3D for the secondaries.** Each window is a layered,
|
||
topmost GDI window (the L4PADPANEL pattern: double-buffered paint, ~62 ms repaint timer,
|
||
left-click = momentary / right-click = latch → `PadRIO::SetScreenButton`; lamps from
|
||
`PadRIO::GetLampState`). The surface is CPU-expanded from the shared gauge pixelBuffer by the
|
||
new **`SVGA16::ExpandPlaneToBGRA(mask, paletteID, monoTint, rotateQuadrant, dst, &ow, &oh)`**
|
||
(L4VB16.cpp) — the no-D3D analog of `DrawDevSurface`: palette-LUT expand for `sec` (monoTint
|
||
< 0), mono bit-plane → tint for the MFDs; the portrait secondary CRT is unrotated by
|
||
transposing into `dst` (rotate 1/3, `BT_GAUGE_SEC_ROT`) — then `StretchDIBits` (HALFTONE) into
|
||
the surface rect. An Mfd/Eng pair shows whichever plane is the live (non-`BlankColor`) one, as
|
||
on the pod monitor. This reuses the plasma-window CPU→window pattern and sidesteps the whole
|
||
D3D dev-composite bug family (additional-swap-chain lifetime, device-state save/restore,
|
||
depth-stencil mismatch, layered strobing).
|
||
|
||
**Gate: `BT_GLASS_PANELS`** (runtime env; `BTGlassPanelsActive()`). ⚠ **Was** default-ON under
|
||
`-platform glass` (preset putenv in `btl4main.cpp`) — as of 2026-07-26 the COCKPIT SURROUND is the
|
||
glass default and this gate is **explicit-only**; the one resolver + the full precedence table are
|
||
in `context/glass-cockpit.md` §Layout modes. Wired at three seams:
|
||
- `L4PADRIO` ctor: `BTGlassPanelsActive()` → `BTGlassPanels_Create()` instead of
|
||
`BTPadPanel_Create()`; dtor destroys both (safe no-ops).
|
||
- `L4VB16`: `BTDrawGaugeInset` / `BTGaugeWindowRenderAndPresent` early-return when
|
||
`GlassPanelsOwnSurfaces()` (the per-display windows own the surfaces).
|
||
- `btl4main`: skip the `gBTGaugeDockBottom` + 1100×600 world-window forcing (world stays
|
||
normal size — no bottom strip).
|
||
|
||
**Placement.** `ComputeLayout()` rings the windows around `ghWnd` (the main game window):
|
||
top row Heat/Mfd2/Comm above it, Mfd1/Mfd3 at the lower corners, radar below-center, Flight
|
||
Controls left of the seat; clamped to the work area, with an 800×600-centered fallback before
|
||
the main window exists + a one-shot reposition once `ghWnd` comes up (in the repaint timer).
|
||
|
||
**Verified [T2 startup].** `-DBT_GLASS=ON` Debug build links (the usual `/FORCE` mech3
|
||
subsystem set is pre-existing, unrelated). Bounded `-platform glass -egg DEV.EGG` launch:
|
||
`[glasswin] per-display cockpit up (7 windows: 5 MFD + radar + flight)`, `[plasmawin] … up`,
|
||
`[padrio] XInput pad found`, no failure/abort markers, full mission loop un-regressed (killed
|
||
after 18 s). Visual layout / surface content / click-through NOT yet human-verified.
|
||
|
||
**Tunable (Cyd flagged refinements):** surface display sizes, the exact button sub-positions
|
||
around each screen, the yellow left/right split (first cut 0x10-0x15 / 0x16-0x1B), and the MFD
|
||
mono phosphor tint — all named constants at the top of `L4GLASSWIN.cpp`. Deferred: folding the
|
||
plasma window into the same arrangement; per-window draggable/saved positions; moving/resizing
|
||
the main window into a true center cell for a tighter ring.
|
||
|
||
### Blank-surface bug (found + fixed same day) — /FORCE duplicate-global race
|
||
|
||
First glass-panels run showed the buttons but BLANK display surfaces. `BT_GLASS_LOG`
|
||
diagnostics in `BlitSurface` traced it stage by stage: `application == 0x00000000` in
|
||
`L4GLASSWIN.cpp` for the ENTIRE run (call 1 → 481), so `application->GetGaugeRenderer()` was
|
||
never even reached. Yet the legacy dock path (`BTDrawGaugeSurfaces`, `L4VB16.cpp`, SAME engine
|
||
lib) logged `application=047BA9C8 gr=0C57FF78` — non-NULL.
|
||
|
||
Root cause: the 1995 headers declare free globals (`application`, `ghWnd`, …) without
|
||
`inline`/`extern`, so they are DEFINED in both `munga_engine` and `bt410_l4`. Under
|
||
`/FORCE:MULTIPLE` the linker binds each `.obj`'s reference to one copy or the other
|
||
**non-deterministically per link** — the newly-added `L4GLASSWIN.obj` bound the copy the game
|
||
never assigns (stays NULL), and a relink even flipped `L4VB16.obj`. Routing through an engine
|
||
helper is therefore unreliable (its binding is equally random).
|
||
|
||
Fix: `BTResolveGaugeRenderer()` / `BTResolveMainWindow()` (declared in `l4vb16.h`) are DEFINED
|
||
in `game/btl4main.cpp` off the launcher's OWN file-scope pointers — `btl4App` (set to `new_app`
|
||
at btl4main.cpp:631) and the global `hWnd` (the real main window). Those live in `btl4main.obj`
|
||
and are the actual assigned pointers, so the resolvers are always correct regardless of the
|
||
duplicate-symbol binding. `BlitSurface` and the placement code call the resolvers, never the
|
||
`application`/`ghWnd` globals. New durable gotcha: `context/reconstruction-gotchas.md` §6
|
||
duplicate-GLOBAL corollary.
|
||
|
||
Verified after the fix (`BT_GLASS_LOG`): `[glass] 'Heat MFD' port=Heat mask=0x4000 pal=3
|
||
enable=2 ow=640 oh=480 nonzero=22227/307200`, `[glass] 'Secondary / Radar' port=sec mask=0x3f
|
||
pal=1 enable=3 ow=480 oh=640 nonzero=26206/307200` — surfaces resolve and carry live pixels.
|
||
`BT_GLASS_LOG` left in as a permanent per-surface resolve/pixel diagnostic (throttled).
|
||
|
||
## 2026-07-26 — Layout modes get ONE resolver; button geometry gets ONE owner (L4RIOBANK)
|
||
|
||
The RP412 replication pass, step 1 of the agreed plan (port the geometry, keep our renderers).
|
||
|
||
### The mode resolver (btl4main.cpp)
|
||
Where the secondary displays go was decided in TWO places with duplicated precedence -- the glass
|
||
profile block picked `BT_PAD_PANEL`/`BT_GLASS_PANELS`, the window-sizing block re-derived
|
||
cockpit-vs-dock -- and the boot banner read NEITHER, announcing "per-display cockpit windows
|
||
[BT_GLASS_PANELS]" for every glass boot including the surround default. The split had also
|
||
broken `BT_COCKPIT=0`: documented as the dock-bottom opt-out, the profile block converted it to
|
||
`BT_GLASS_PANELS=1`, so **the docked strip was unreachable under the glass profile**.
|
||
|
||
Now: one `glassLayout` resolved after the profile putenvs land, consumed by the banner, the
|
||
pad-panel decision and the sizing block. Precedence `BT_GLASS_PANELS` > `BT_DEV_GAUGES_WINDOW` >
|
||
`BT_DEV_GAUGES_DOCK` > `BT_COCKPIT=0` > surround default; `BT_GLASS_PANELS` is explicit-only now
|
||
(it was auto-set); dock/window auto-raise `BT_PAD_PANEL` so the 72-button field always has a home.
|
||
Banner: `[boot] platform profile: GLASS (PadRIO + plasma window) [secondary displays: <mode>]`.
|
||
Verified all four modes boot to the mode they name, plus pod/dev.
|
||
|
||
### L4RIOBANK -- the shared field
|
||
`engine/MUNGA_L4/L4RIOBANK.cpp/.h` (new TU). Both renderers were carrying their own copy of the
|
||
button geometry and had drifted: an MFD button was 156x138 reaching under the glass in the
|
||
exploded window, and a **76x24 sliver entirely OUTSIDE the glass** in the surround. One module
|
||
now owns it, both are consumers, placement stays per-renderer.
|
||
|
||
The under-glass rule, from RP412 `L4MFDVIEW` (which took it from the pod): reach half the glass
|
||
in behind the display, leave a lamp strip clearing the edge, paint buttons first and imagery
|
||
over. Strip scales off the display's SHORT axis vs the native 480 (MFD 640x480 landscape, map
|
||
480x640 portrait) with a 6px floor. The MFD per-column nudge against the DISPLAY/PROGRAM legends
|
||
moved here so both renderers inherit one alignment. The map's foot row is pushed FIRST because
|
||
the side columns now cover that band and the hit test takes the first match.
|
||
|
||
Retired: `L4GLASSWIN`'s `PlaceCellAt`/`PlaceLine`/`PlaceRect` and its `RedCellH`/`EdgeProtrude`/
|
||
`RailW`/`RailSlotAdd`/`RailGap`/`RedOffsetX`/`kRedColDX` constants; the surround's inline MFD/
|
||
radar/flight button loops. `kCkRedH`/`kCkLamp` survive only as band reserves for the canvas math.
|
||
|
||
### Lamp flash decode [T1 -> fixed]
|
||
`BTLampBrightnessOf` returned `max(state1,state2)` and blanked on the alternate phase. Per the
|
||
engine's own `RIO::LampState` enum [T0] solid shows state 1 and flashing ALTERNATES state 1 and
|
||
state 2. Agrees only when one state is Off -- true for Panic (`flashFast+state1Off+state2Bright`)
|
||
which is why it survived -- but `L4LAMP.cpp:252` commands `flashFast+state1Dim+state2Bright`, a
|
||
dim->bright pulse that rendered as bright->off. Three copies existed (l4vb16.h, L4GLASSWIN,
|
||
L4PADPANEL), all three wrong; the two locals now forward to the one fixed inline.
|
||
|
||
### Verification harness (new, reusable)
|
||
- `BT_RIOBANK_LOG=1` -- one-shot dump of every bank's buttons + bounds (capped at 16 banks so the
|
||
per-frame surround caller prints one pass).
|
||
- `scratchpad/checkbank.py <log>` -- per-bank census + **first-hit reachability**: an address
|
||
whose rect is entirely covered by earlier buttons in its bank is dead however big it looks, and
|
||
the overlapping under-glass banks make that a live hazard. Reports duplicates and shadowing.
|
||
- `scratchpad/clickbank.py <log> <title> --mode surround|exploded` -- posts a real
|
||
WM_LBUTTONDOWN/UP at every button's centre; the game's own CLICK forensics prove dispatch.
|
||
|
||
Result both modes: **72/72 placed, 0 shadowed, 0 duplicated, 72/72 dispatched**, process survived
|
||
144 posted messages each. Pod + dev profiles boot and simulate un-regressed. Surround `BT_SHOT`
|
||
confirms the legends are clear and the lamps read as edge strips; exploded `PrintWindow` captures
|
||
confirm surfaces still land under their banks.
|
||
|
||
## 2026-07-26 (later) — Phase 2: the cockpit scales (uniform-fit letterbox + -fit)
|
||
|
||
RP412 replication step 2. The canvas is a FIXED size and D3D9 stretched it into the client, so a
|
||
window dragged to a different shape squashed the instruments -- the projection was aspect-corrected
|
||
back in task #20, but the panels, lamps and MFD glass never were. Now: one uniform scale, centred,
|
||
leftover black.
|
||
|
||
**Mechanism.** `BTCockpitFitRect()` (L4VB16.cpp) computes the centred uniform-scale rect in integer
|
||
math -- a float scale wobbles by a pixel between frames and the bars shimmer during a drag. D3D9
|
||
applies it as a Present **destination rect**, which `D3DSWAPEFFECT_DISCARD` forbids, so the
|
||
WINDOWED swap effect becomes `D3DSWAPEFFECT_COPY` when the surround is up and multisampling is off
|
||
(COPY cannot multisample). `gBTCockpitLetterbox` records the outcome; 0 = the old full-client
|
||
stretch, which is what every non-cockpit mode and any MSAA run still gets.
|
||
|
||
Wired at five seams:
|
||
- `L4VIDEO` device setup: swap effect + confirm/withdraw the flag.
|
||
- Both Present sites: the scene present AND the wait-screen overlay present (it paints into the
|
||
same backbuffer, so it has to land in the same rect).
|
||
- `btl4main` WM_SIZE: `InvalidateRect` -- a COPY present only writes its dest rect, so stale bar
|
||
content persists without an erase (the class brush is black).
|
||
- `BTCockpitMouseDown`: client->canvas through the SAME fit. Mapping against the full client
|
||
drifts the hit test off every button by the bar width; a click on a bar now hits nothing.
|
||
- `BTWorldAspectOf`: under the letterbox the view's on-screen aspect is its OWN -- the client
|
||
drops out of the formula entirely.
|
||
|
||
**`-fit` / `-windowed-fullscreen`** (btl4main): borderless `WS_POPUP` over the monitor rect, canvas
|
||
letterboxed inside. Unknown tokens fall through `L4Application::ParseCommandLine`'s ladder to
|
||
`return True`, so no arg-parser change was needed.
|
||
|
||
### Ordering trap (found live, fixed)
|
||
The letterbox flag was originally set at device creation -- but **the first WM_SIZE beats the
|
||
device**. A `-fit` boot on the 3440x1440 ultrawide logged `[resize] client 3440x1440 aspect=3.14`
|
||
(the stretch formula), and since nothing resizes the window again, the per-frame
|
||
`gWindowAspect != appliedAspect` check applied it on frame 1. The INTENT is now decided in
|
||
btl4main alongside `gBTGaugeCockpit` (windowed cockpit + MULTISAMPLE unset/0); L4VIDEO only
|
||
confirms it or withdraws it. Re-verified: the boot line reads `aspect=1.8`.
|
||
|
||
### Capture note (for whoever verifies this next)
|
||
`PrintWindow` returns an all-black frame for the borderless `-fit` window, while working fine for
|
||
the normal chrome'd window. Use a screen-region grab (`scratchpad/shot.ps1`) for `-fit`. The
|
||
all-black PrintWindow is NOT a render failure -- the log showed 18 sim ticks and the screen grab
|
||
showed the full cockpit.
|
||
|
||
### Verified
|
||
- Wide drag 1684x661 and tall drag 744x961: canvas uniformly scaled and centred, bars on the long
|
||
axis, ZERO distortion (captures `fit_wide.png` / `fit_tall.png`).
|
||
- `-fit` on 3440x1440: borderless, centred, bars left/right, undistorted.
|
||
- **72/72 buttons still dispatch after a resize** -- clickbank.py now maps canvas->client through
|
||
its own copy of the fit (an independent check that the two transforms agree): at fit 906x661
|
||
with a 389px left bar, all 72 `[cockpit] CLICK` lines land.
|
||
- exploded / dock / pod / dev: DISCARD retained, boot + simulate un-regressed.
|
||
|
||
## 2026-07-26 (later still) — Phase 3: player-tunable displays + the settings file
|
||
|
||
RP412 replication step 3.
|
||
|
||
### Scaling (BTCkResolvedSizes, L4VB16.cpp)
|
||
`BT_MFD_SCALE` (all five), `BT_MFD_SCALE_UL/_UC/_UR/_LL/_LR` (per display, overriding the group),
|
||
`BT_RADAR_SCALE` -- percentages of the pod size, 25-200, resolved ONCE (the surround recomputes its
|
||
layout every frame, so reading the env per frame would be silly).
|
||
|
||
The load-bearing consequence: **the surround BANDS derive from the resolved sizes**. That is why
|
||
the sizes could not stay compile-time constants -- the band a display hangs in has to grow with it
|
||
or the canvas clips it. Each band is what its largest occupant needs; a corner MFD only claims
|
||
`size - kCkOVL` because it overlaps the view; the bottom band drops to just the lower MFDs when the
|
||
map leaves that row for a side. Verified 100% reproduces the historical `L276 R276 T223 B336`
|
||
byte-for-byte, 150% gives `L436 R436 T343 B448`, 80%+MIDLEFT gives `L256 R212 T175 B148`.
|
||
|
||
The flight label blocks now hang off their OWN lower MFD's scaled height + lamp strip, instead of
|
||
the constant -- a shrunken MFD used to leave the block floating in the gap and an enlarged one to
|
||
overlap it.
|
||
|
||
### Map placement (BT_RADAR_POS)
|
||
CENTER (default) / LEFT / RIGHT / MIDLEFT / MIDRIGHT, with the BOTTOM*/`*CENTER` spellings RP
|
||
accepts. Bottom corner: the map goes flush to the canvas edge and the lower MFD whose corner it
|
||
took slides beside it. Halfway up a side: it leaves the bottom row entirely and the bottom band
|
||
shrinks accordingly.
|
||
|
||
⚠ **First cut was wrong and the capture caught it:** the corner map was placed relative to the VIEW
|
||
edge, which overlapped the neighbouring MFD by the band width -- 232px of map drawn over the Right
|
||
Weapons display. Corner placement is measured off the CANVAS edge.
|
||
|
||
### The map legend grid -- measured, not inherited
|
||
The side columns have to line up with the six cells the map imagery paints beside them. Zooming a
|
||
native Secondary/Radar capture shows those cells are exactly our address map: MAP+ 0x10, MAP- 0x11,
|
||
IR 0x12 (ThermalSight lamp), crouch 0x13 (DuckRequest), searchlight 0x14, display-mode 0x15 -- the
|
||
art independently corroborating [[pod-hardware]].
|
||
|
||
`scratchpad/measurelegend.py` scans the capture for the legend's vertical rule and for our lamp
|
||
bars, and reports both. Ours: **top 3, cell 102, pitch 107** of the 640 span. RP412's map:
|
||
`13 + 6x102 @ 105`. Same cell height, different top and pitch -- confirming the deferral call in
|
||
Phase 1 was right, the numbers do NOT transfer. Our old even division (`displayH/6 + 1`) had the
|
||
pitch right by luck and sat 3px high of the labels; the grid is now pinned to the measurement, with
|
||
top and bottom scaled SEPARATELY and subtracted (RP's trick) so rounding cannot walk the buttons
|
||
out of step with the labels down a scaled column.
|
||
|
||
### environ.ini
|
||
Two defects, both fixed:
|
||
1. **Read far too late.** It sat ~300 lines into WinMain, after the platform-profile block had
|
||
already run its getenv()s -- so every setting the profile reads (BT_PLATFORM, BT_COCKPIT,
|
||
BT_GLASS_PANELS, BT_DEV_GAUGES, L4CONTROLS...) was silently ignored FROM THE FILE and only ever
|
||
worked as a real environment variable. Now loaded immediately after the first-breath boot line.
|
||
2. **putenv()'d comments verbatim**, so a commented-out option would have become an environment
|
||
variable literally named `#BT_MFD_SCALE`. Comments (`#`, `;`) and blanks are skipped, and lines
|
||
with no `=` are counted and ignored.
|
||
|
||
Added: the real environment WINS over the file (a launcher .bat overrides rather than fights), and
|
||
a fully documented default is written on first run when the file is absent -- the bindings.txt
|
||
convention (untracked via .gitignore, so extract-over-top never clobbers a player's settings; every
|
||
option ships commented out so a fresh install behaves exactly as before).
|
||
|
||
Verified live, all three paths: first run writes 3886 bytes and applies `0 setting(s)`; uncommenting
|
||
`BT_MFD_SCALE=130` + `BT_MFD_SCALE_UC=60` + `BT_RADAR_POS=MIDRIGHT` in the FILE drives the layout
|
||
(`displays UL 130% UC 60% ... radar on the right side, centred`); and `set BT_MFD_SCALE=90` beats
|
||
the file's 130 while the file's other two settings still apply.
|
||
|
||
### Verified
|
||
- 72/72 placed, 0 shadowed, 72/72 dispatched at baseline, at 150%/135%, and at 75% + BOTTOMRIGHT.
|
||
- exploded / dock / pod / dev boot and simulate un-regressed.
|
||
- Captures: p3_big2 (150%), p3_radleft2 (bottom-left + slid MFD), p3_radmidl (side-mounted map at
|
||
80%), p3_fromini (settings taken from the file).
|
||
|
||
## 2026-07-26 (final) — Phases 4-6: the dead-button backlog, the lamp mirror, the shipped doc
|
||
|
||
### Phase 4 — Mech::DuckRequest, and a census that lied in both directions
|
||
The 2026-07-20 audit listed 8 buttons with no reconstructed handler. **Only ONE was still
|
||
missing.** Generator on/off (`powersub.cpp`), ToggleSeekVoltage (`emitter.cpp`), EjectAmmo
|
||
(`projweap.cpp`), ToggleCooling (`heatfamily_reslice.cpp`) and BalanceCoolant (`mech.cpp`) had all
|
||
landed between 07-20 and 07-25 while `pod-hardware.md` and `open-questions.md` still called them
|
||
dead. `docs/INPUT_PATH_AUDIT.md:190` had already flagged this ("census stale in both directions")
|
||
and was right. Rule for next time: **grep the reconstruction, not the prose.**
|
||
|
||
`Mech::DuckRequest` @0049fa00 (id 0x1a, RIO 0x13 -- the manual's CROUCH button). Binary body, in
|
||
full:
|
||
|
||
if (0 < *(int *)(param_2 + 0xc)) { *(undefined4 *)(param_1 + 0x398) = 1; }
|
||
|
||
Press-only; sets `duckState` (`mech+0x398`, attribute 0x37). A one-shot REQUEST flag -- the
|
||
handler never clears it and the only other writer in the entire binary is the mech reset
|
||
(part_012.c:9439, the same reset that zeroes `incomingLock`, which is how that region was already
|
||
mapped). Registered in `Mech::MessageHandlerEntries`; `[duck]` log on press.
|
||
|
||
**Why the flag has no reader, and why that is correct.** `duckState` has ZERO readers anywhere in
|
||
the decomp. It is published as an ATTRIBUTE, so its consumer is a databinding:
|
||
`content/GAUGE/L4GAUGE.CFG` runs `oneOfSeveralPixInt(E, ModeAlwaysActive, bduck.pcc, 3, 1,
|
||
DuckState)` -- "crouch mode: button 4" on the map's legend column. Verified live by capturing the
|
||
Secondary/Radar window before and after a 0x13 press: the crouch icon goes grey -> orange. So the
|
||
handler is COMPLETE; a crouch pose invented here would be a stand-in for data we have not found
|
||
(no SQUAT clip name survives in the decomp or in content/, only DuckServo01.wav in AUDIO1.RES).
|
||
|
||
Bonus: those gauge widgets sit at offsets 537/430/322/215/108 -- a **107 pitch**, independently
|
||
corroborating the map legend grid measured from pixels in Phase 3.
|
||
|
||
Still open from that census: MechRIOMapper's own Keypress @004d2514 (id 0x19).
|
||
|
||
### Phase 5 — L4KEYLIGHT (RGB keyboard lamp mirror)
|
||
Ported from RP412 (itself a port of vRIO's KeyboardLampMirror). Keys bound to a lamp address in
|
||
bindings.txt glow with the panel palette -- yellow for the map's side columns (0x10-0x1F), red for
|
||
the rest -- flashing in step with the on-screen buttons (the LampLevel copy matches the FIXED
|
||
BTLampBrightnessOf from Phase 1). Per-key boards light each bound key; zone-lit boards mirror the
|
||
strongest lamp board-wide. All WinRT runs on a private worker thread (watcher, claim, 100 ms paint
|
||
loop, repaint only on change).
|
||
|
||
**The packing hazard did NOT transfer.** RP412 compiles this file with forced default struct
|
||
packing because its engine is `/Zp1`, which would break the WinRT ABI. BT411's `BT_OPTS` is
|
||
`/permissive /W0 /wd4996 /EHsc /bigobj /MP` -- no `/Zp` -- so only the dialect flags are needed:
|
||
`set_source_files_properties(... COMPILE_OPTIONS "/std:c++17;/permissive-")`, since the project
|
||
otherwise builds C++14 /permissive. The scalars-only interface is kept regardless.
|
||
|
||
Wired in `PadRIO`: map built from `bindings.keyBindings` (ActionButton binds only, first binding
|
||
per key wins, addresses < LampCount), fed from `PadRIO::SetLamp`, stopped in the dtor (which hands
|
||
the LEDs back to Windows). Gate `BT_KEYLIGHT=0`.
|
||
|
||
Verified live: `[keylight] mirroring 25 bound key(s)` + `+ LAPTOP-767CF84B (24 zones - board-wide
|
||
mirror)` -- it claimed this machine's real keyboard. `BT_KEYLIGHT=0` -> zero keylight lines, game
|
||
fine. Pod profile (no PadRIO) -> zero lines. Exploded -> works.
|
||
|
||
### Phase 6 — the shipped controls reference
|
||
`docs/CONTROLS.md`: the keyboard table plus **the full 72-button pod map**, grouped by the display
|
||
each bank surrounds, with the coolant-valve detent warning and the eject/jam procedure. mkdist
|
||
flattens it to ASCII as `CONTROLS.txt` at the zip root (the README's own flattening idiom).
|
||
`players/README.txt` gained pointers to it, to environ.ini, to -fit, and to the RGB mirror.
|
||
Verified: `mkdist.py` writes a 5149-byte pure-ASCII CONTROLS.txt into the zip.
|