Input-path audit: 206 paths traced, 22 dead -- plan doc only, no code changes
13-agent read-only audit (6 tracers -> adversarial verifiers -> triage), validated against 2,294 streamed control-mapping records decoded from all 18 mechs in BTL4.RES plus the live 121-record install dump. Verdict: 22 dead dispatch routes, 11 player-visible, 8 documentation defects, 2 entirely dormant input layers (CONTROLS.MAP and the whole DirectInput grammar). RETRACTS MY OWN DIAGNOSIS from the reverse-thrust fix earlier today: streamed BUTTON mappings ARE consumed (L4CTRL.cpp:2254-2262 / :2327-2336, and ctrlmap.log:32 shows reverse's record resolving to +0xec). The AddOrErase Fail bodies are the config-session base traps, overridden by MechRIOMapper. So 'reconstruct AddOrErase' would clear 0 of 2,294 records. The false claim is in pod-hardware.md:121-135 AND in mechmppr.cpp:894-919 -- both owed a sweep, and reverseThrust now has two writers (unknown #2 decides which to keep). The 'how did it break' question is therefore reopened; leading candidate is the unhandled WM_SYSKEYDOWN/SC_KEYMENU menu-modal loop on a held ALT. Biggest new finding: a default-constructed Receiver::MessageHandlerSet is a silent blackhole -- Sensor/Avionics alone kills 108 records (6 cockpit buttons + F5-F9). One accessor in sensor.cpp is the best findings-cleared/risk ratio. Worse-than-dead: target designation writes raw +0x284 on a modern-layout object, possibly out of bounds, reachable by pressing 't' -- flagged as a playtest safety note. Also lands scratchpad/keysweep.py: the empirical counterpart (presses every bound key, records what actually moves). Not yet run -- needs the machine idle. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg
This commit is contained in:
co-authored by
Claude Opus 5
parent
6e810d99cc
commit
b517c4cb30
@@ -83,7 +83,7 @@ precise than anything you can infer.
|
||||
| Term / acronym definitions | `reference/glossary.yaml` |
|
||||
| The ORIGINAL 1995 player manual (controls, per-mech stats, coolant loops) | `reference/manual/Tesla40_BT_manual.pdf` (+ alignment audit in `context/pod-hardware.md` §Manual) |
|
||||
| The complete verbatim detail (fallback) | `docs/PROGRESS_LOG.md` (the old 2236-line CLAUDE.md) |
|
||||
| Detailed running ledgers | `docs/RECONCILE.md`, `docs/GAUGE_COMPOSITE.md`, `docs/HARD_PROBLEMS.md`, `docs/SUBSYS_PLAN.md`, `docs/P3_LOCOMOTION.md`, `docs/RESOURCE_AUDIT.md`, `docs/VEHICLE_SUBSYSTEMS.md`, `docs/BGF_FORMAT.md`, `docs/ASSET_PIPELINE.md`, `docs/BT_SOURCE_STATUS.md`, `docs/WAVE_PLAN.md`, `docs/GLASS_COCKPIT.md`, `docs/REVOLVING_DOOR_PLAN.md` |
|
||||
| Detailed running ledgers | `docs/RECONCILE.md`, `docs/GAUGE_COMPOSITE.md`, `docs/HARD_PROBLEMS.md`, `docs/SUBSYS_PLAN.md`, `docs/P3_LOCOMOTION.md`, `docs/RESOURCE_AUDIT.md`, `docs/VEHICLE_SUBSYSTEMS.md`, `docs/BGF_FORMAT.md`, `docs/ASSET_PIPELINE.md`, `docs/BT_SOURCE_STATUS.md`, `docs/WAVE_PLAN.md`, `docs/GLASS_COCKPIT.md`, `docs/REVOLVING_DOOR_PLAN.md`, `docs/INPUT_PATH_AUDIT.md` |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
# Input Path Audit — findings + fix triage
|
||||
|
||||
**Status: PLANNED. No code changed.** Written 2026-07-24 from a read-only 13-agent audit
|
||||
(6 tracers -> adversarial verifiers -> triage). 206 input paths traced end to end and
|
||||
cross-checked against 2,294 streamed control-mapping records decoded from all 18 mechs in
|
||||
`content/BTL4.RES`, plus the live 121-record install dump in `content/ctrlmap.log`.
|
||||
Provenance: workflow `wf_628e421a-b0e`.
|
||||
|
||||
Act on this AFTER the 2026-07-24 playtest. Work order is in section 6, split into
|
||||
safe-in-one-boot vs must-wait.
|
||||
|
||||
---
|
||||
|
||||
## READ THIS FIRST — a retraction of my own diagnosis
|
||||
|
||||
While fixing reverse thrust earlier today I asserted that **streamed BUTTON mappings are never
|
||||
consumed** because `MechControlsMapper::AddOrErase` is a `Fail` stub. **That is false.** All six
|
||||
auditors and every adversarial verifier converged on the refutation independently:
|
||||
|
||||
* `LBE4ControlsManager::CreateStreamedMappings` installs button records itself
|
||||
(`engine/MUNGA_L4/L4CTRL.cpp:2254-2262` direct, `:2327-2336` event).
|
||||
* `content/ctrlmap.log:32` shows reverse thrust's own record installing and resolving:
|
||||
`rec 2 ... elem 63 ... attrID 6 -> +0xec` = `MechControlsMapper::reverseThrust`.
|
||||
* The `Fail` bodies at `mechmppr.cpp:1477-1500` are the authentic base traps for the
|
||||
*config-session* vtable slots (+0x40/+0x44) and are overridden with real bodies by
|
||||
`MechRIOMapper` (`btl4mppr.cpp:1275-1295`) -- which is the mapper actually installed.
|
||||
|
||||
So "reconstruct the AddOrErase pair" would clear **0 of 2,294 records**. The false claim is now
|
||||
written into `context/pod-hardware.md:121-135` AND into the source as a 25-line justifying
|
||||
comment at `mechmppr.cpp:894-919`; **both must be swept before they justify another stand-in**
|
||||
(CLAUDE.md correction mandate).
|
||||
|
||||
Consequences to settle:
|
||||
1. **The reverse fix works** (empirically: hold LALT -> `rev=1`, `speedDemand -61.5`) but
|
||||
`reverseThrust` now has **two writers**. Unknown #2 in section 5 is the exact experiment that
|
||||
decides whether the `[T3]` accommodation should be deleted outright.
|
||||
2. **The original "how did it break" answer is therefore still open.** The strongest surviving
|
||||
candidate is unknown #3: nothing in `engine/` or `game/` handles `WM_SYSKEYDOWN` /
|
||||
`WM_SYSCHAR` / `SC_KEYMENU`, so a held ALT reaches `DefWindowProc`'s menu-modal loop. That
|
||||
would also explain a *previously working* Alt going dead when window/message handling changed.
|
||||
Test it before believing any story.
|
||||
|
||||
## Safety note for tonight's playtest
|
||||
|
||||
Section 2 finding #1: target designation (the 8 Comm-bank buttons `0x30-0x37` and typed
|
||||
`t y u i o`) performs three raw 1995-offset writes `*(int*)((int)pilotArray[0] + 0x284)` on a
|
||||
modern-layout object (`mechmppr.cpp:1344`, `:1364`, `:1420`). If `sizeof(BTPlayer) < 0x288` this
|
||||
is an **out-of-bounds write** -- i.e. heap corruption reachable by pressing `t`. Unknown #1 is the
|
||||
one-line test that settles it. Designation is already broken either way, so **nothing is lost by
|
||||
avoiding `t`/`y`/`u`/`i`/`o` and the Comm bank tonight**; if an unexplained crash shows up, this
|
||||
is the first suspect.
|
||||
|
||||
---
|
||||
|
||||
# BATTLETECH PORT — INPUT PATH AUDIT + FIX TRIAGE
|
||||
**Scope:** desktop GLASS profile (the shipped default, `btl4main.cpp:428-429` → `L4CONTROLS=PAD,KEYBOARD`). Static analysis only; nothing launched.
|
||||
|
||||
---
|
||||
|
||||
## 1. HEADLINE
|
||||
|
||||
134 bound-or-clickable input paths were traced end to end (40 `key` rows + 15 `pad`/`padaxis` rows + 7 hardcoded keys in `content/bindings.txt`, plus all 72 pod RIO addresses `0x00-0x47`, every one of which is mouse-clickable), cross-checked against 2,294 streamed `"L4"` control-mapping records decoded statically from all 18 mechs in `content/BTL4.RES` and against the live 121-record install dump in `content/ctrlmap.log`. **22 dispatch routes are DEAD** (no consumer, or a consumer that provably cannot act), **11 of them a player would notice tonight**, plus **8 documentation defects that read as dead** and **2 entire input layers that are dormant on every shipped launch** (`content/CONTROLS.MAP` and the whole DirectInput/HOTAS grammar). Critically, **the audit brief's own root cause is wrong and must be retracted**: streamed BUTTON mappings *are* consumed — by the engine, at `engine/MUNGA_L4/L4CTRL.cpp:2254-2262` (direct) and `:2327-2336` (event) — and `content/ctrlmap.log:32` proves reverse thrust's record installed and resolved (`rec 2 ... elem 63 ... attrID 6 -> +0xec` = `MechControlsMapper::reverseThrust`). `MechControlsMapper::AddOrErase` is the config-session regroup toggle on a different vtable slot, and the live mapper `MechRIOMapper` overrides it with real bodies (`game/reconstructed/btl4mppr.cpp:1275-1295`), so "reconstruct the AddOrErase pair" would clear **zero** of 2,294 records. The real dead-input generators are four different mechanisms, listed in §4. One finding is worse than dead: the target hotbox performs a raw 4-byte write at `pilot+0x284` on a modern-layout object, reachable by pressing `t`.
|
||||
|
||||
---
|
||||
|
||||
## 2. DEAD AND A PLAYER WOULD NOTICE
|
||||
|
||||
Ranked by impact. **Reverse thrust `0x3F` (key LALT / pad B) is FIXED** and confirmed live (`mechmppr.cpp:912-919` + streamed `rec 2`) — it is listed in §5 only because the *fix's stated cause is false* and it now has two writers.
|
||||
|
||||
| # | Input | What it should do | Why it is dead | Fix | How to verify |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | **Comm bank `0x30`-`0x37`** (8 upper-right MFD buttons) **+ typed `t y u i o`** | Designate a pilot as your target → radar target box, callsign plate, hunting arrow, kill attribution | Handler runs but its whole effect is three raw 1995-offset writes `*(int*)((int)pilotArray[0] + 0x284) = target` (`game/reconstructed/mechmppr.cpp:1344`, `:1364`, `:1420`); in our layout that is not `BTPlayer::objectiveMech`, whose **only** typed writer is `objectiveMech = 0` in the ctor (`btplayer.cpp:1425`) — and it may be past the end of the object | **S** (neuter) / **M** (bridge) | Add `DEBUG_STREAM << sizeof(BTPlayer) << " " << offsetof(BTPlayer,objectiveMech)` at boot — it will not be `0x288`/`0x284`. In-game: click `0x30` and confirm `GetObjectiveMech()` stays 0 and no radar target box appears |
|
||||
| 2 | **MFD2 ENG1 page bank** — `0x27 0x26 0x25 0x24 0x22 0x21` = **keys F5-F9** on that page | Route Avionics power to generator A-D, gen mode, coolant cut | The receiver is `Sensor` (roster name "Avionics", streamed `subsys 14`) and `Receiver::MessageHandlerSet Sensor::MessageHandlers;` is **default-constructed** (`sensor.cpp:103-104`, handed to `DefaultData` at `:129`) → `entryCount 0`, no parent chain → `Find()` returns `NullHandler` for **every** id (`engine/MUNGA/RECEIVER.h:376-380, 398-403`) and `Receiver::Receive` drops it silently. Verified: `content/ctrlmap.log:93-98` sends msgs 3-8 to `subsys 14`; the identical bank on ENG2 (`subsys 15` Myomers) works because `myomers.cpp:88-100` chains `PoweredSubsystem::GetMessageHandlers()`. **108 records across 18 mechs** | **S** | Press `K` until MFD2 shows an ENG page, then F5 with `BT_FIRE_LOG=1`: expect `[gensel] Avionics -> GeneratorA (tapped)` (`powersub.cpp:569-571`). Today: nothing |
|
||||
| 3 | **pad LT / RT (turn pedals)** — and every pad-only session | Steer the legs | Not a wiring gap: the pedals are live (`L4PADRIO.cpp:710-715` → `L4CTRL.cpp:1438-1446` → `btl4mppr.cpp:1337-1340` → `:1399 pedalsPosition`) but only *Standard/Veteran* read them (`mechmppr.cpp:1034`, `:1061`); the mapper boots in **Basic** (`mechmppr.cpp:361`) — and **the pad has no binding for `0x18` CycleControlMode**. Verified: `content/bindings.txt:74-89` binds only `0x40 0x3F 0x47 0x42 0x44 0x43 0x41`; no `0x18`, no `0x15` | **S** | `BT_MPPR_TRACE=1`, squeeze LT on a pad-only launch: `pedals=` moves, `turn=0`. Add `pad BACK button 0x18`, press it → `[mode] control mode -> 1`, then LT yaws |
|
||||
| 4 | **Fire dropout: releasing one alias of a held fire button** (`1`+`SPACE` on `0x40`; `3`+`LCTRL`+`RCTRL` on `0x47`; pad A/X on the same) | Two keys on one address = two wires to one button | Edge state is per **binding**, not per **address**: `previousKeyHeld[k]` is indexed by binding index and `EmitButton(addr, held)` fires on every per-binding edge (`engine/MUNGA_L4/L4PADRIO.cpp:580-595`; same shape in the pad loop `:665-685`). The release writes `-(addr+1)` (`L4CTRL.cpp:2635-2647`) and `ControlsUpdateManager::Update` then suppresses the redundant press, so fire stays **off** until the held key is released and re-pressed | **M** (shared plumbing — **defer**) | `BT_FIRE_LOG=1`: hold LCTRL (missiles firing), tap `3`, release `3` → fire stops with LCTRL still down |
|
||||
| 5 | **`0x1A`-`0x1D`** Generator A-D ON/OFF (4 radar-rail buttons) | Take a generator off line (heat/power management) | `Generator : HeatSink` (`powersub.hpp:320-322`) and **no `Generator::GetMessageHandlers()` exists** — `powersub.cpp:1014` resolves to HeatSink's set `{id 3 ToggleCooling}`, so streamed `msg 4` (`ctrlmap.log:44-47`) finds nothing. `ToggleGeneratorOnOff` @`004b1ed0` is unreconstructed (0 hits in `game/`) | **M** | Click `0x1A`: today zero log. After: generator output voltage → 0 and its ENG-page bar follows |
|
||||
| 6 | **`0x13`** CROUCH (manual p8, a full section) | Duck to present a smaller target | Streamed `subsys -1 msg 0x1a` (`ctrlmap.log:40`) resolves to the Mech (`ENTITY.cpp:608-615`), but `Mech::MessageHandlerEntries` has exactly three entries — `TakeDamage`, `PlayerLink`, `BalanceCoolant` (`mech.cpp:463-467`). No id `0x1a`. Assets exist (`Mech::duckState` attr 0x37 `mech.cpp:861`, SQUAT clips, `DuckServo01.wav`) | **M** (reconstruct @`0049fa00`) | Click `0x13` on a stationary mech: expect the squat clip + `duckState 0→1` |
|
||||
| 7 | **Torso recenter from the keyboard** (`0x42` is pad/mouse only) | Center a twisted torso — the manual's anti-disorientation control (p9) | `0x42` itself is **LIVE** (streamed `subsys 17 attr 14 -> +0x208` = `Torso::centerCommand`, consumed `torso.cpp:636-640`) but `content/bindings.txt:83,86` bind it only on `pad Y`/`DPAD_UP`. The port's keyboard path (`gBTTorsoRecenter`, `mech4.cpp:2875`) is fed from the zeroed `gBTInput` and its consumer sits inside the stood-down bridge (`mechmppr.cpp:854-863`, gate `:655-660`) | **S** | Twist with `E`, press `X`: `currentTwist` does not return to 0. Add `key X button 0x42` (two rows on one VK both fire — `L4PADRIO.cpp:580-629` iterates all bindings) |
|
||||
| 8 | **Any clickable button latches if the mouse-up is lost** (worst on `0x3F` reverse, `0x40` trigger) | A momentary click always releases | Neither WndProc handles `WM_CAPTURECHANGED` or `WM_KILLFOCUS` — grep over `engine/` + `game/` returns **zero** hits. Alt-tab or a system dialog between press and release leaves `gCkPressed` set and `SetScreenButton(a,0)` never runs | **M** — trap: `btl4main.cpp:137` calls `ReleaseCapture()` itself, which re-enters `WM_CAPTURECHANGED`; guard on `gCkPressed != -1` (it is cleared before `ReleaseCapture`) | Click-hold `0x3F`, alt-tab, release outside the window: `BT_PAD_LOG=1` shows no release line and `rev` stays 1 |
|
||||
| 9 | **Every HOTAS / flight stick / rudder** (whole `joydev/joyaxis/joybutton/joyhat` grammar) | Drive the pod channels from a DirectInput device | `content/bindings.txt` is 89 lines and has **zero** joy rows; the joy poll is gated on `joyAxisBindingCount>0 \|\| …` (`L4PADRIO.cpp:775-777`) so `BTJoyInit/BTJoyPoll` are never called. `PadBindingProfile::Load` writes the default only when the file is **absent** (`L4PADBINDINGS.cpp:708-713`), and the file is gitignored (`.gitignore:23`) — so the zip ships **this machine's stale file** | **S** now (ship without it / refresh it) · **M** later (version marker + additive merge) | `content/jstest.log`: `bindings loaded: 40 keys, 10 pad buttons, 5 pad axes, 0 joy axes…`; no `[joy]` line exists in any log in `content/` |
|
||||
| 10 | **`joyconfig.bat` re-runs the wizard after it finishes** | Run once, then play | `BT_JOYCONFIG` is never cleared (`game/btl4main.cpp:287-291`), and the front end relaunches the exe inheriting the environment (`btl4console.cpp:138-174` clears only `BT_FE_EGG/PODS/SECS/LOOP`) → the mission process re-enters the wizard and blocks on `_getch()` behind the 3D window | **S** (1 line) | Run `joyconfig.bat`, finish it, watch for a second `=== BattleTech joystick setup ===` banner |
|
||||
| 11 | **`0x14`** Searchlight (+Searchlight2) | Turn the light on for night maps | `Receiver::MessageHandlerSet Searchlight::MessageHandlers;` default-constructed (`searchlight.cpp:50-51`, used `:72`) — the `ToggleLamp` body at `:158` is unreachable. **And wiring it changes nothing visible**: the 1995 binary writes `commandedOn@0x1DC` while the simulation reads `requestedOn@0x1E0` (`open-questions.md:183-200`), faithfully preserved at `searchlight.cpp:189` | **S** to wire (no visible payoff) · **L** to make the light work (needs a fidelity decision) | After wiring, `ToggleLamp` logs twice per press and `lightState` still stays 0 — that is the correct, faithful outcome. **`docs/GLASS_COCKPIT.md:192-194` calling this "the quick win" is misleading** |
|
||||
| 12 | **`0x12`** Thermal sight | IR view | Two layers dead: `ThermalSight::MessageHandlers` default-constructed (`thermalsight.cpp:52-53`) **and** no body — `ToggleGlobalThermalVision() {}` is an explicit no-op (`thermalsight.hpp:153`), plus the thermal render path is unported | **L** | Not testable until both land |
|
||||
| 13 | **Numpad cluster with NumLock OFF** | Aim the torso | `L4PADRIO.cpp:583-584` reads `VK_NUMPAD8..9` verbatim; with NumLock off Windows delivers `VK_UP`/`VK_DOWN`/`VK_LEFT`/`VK_RIGHT`, which `bindings.txt:26,27,30,31` map to **throttle and pedals** — "aim up" accelerates the mech. `NUMPAD5/7/9` → `VK_CLEAR/HOME/PRIOR`, unbound | **S** (startup warning) · **M** (extended-key flag) | NumLock off, hold NUMPAD8 with `BT_MPPR_TRACE=1`: `thr=` rises |
|
||||
| 14 | **Duplicate rows on one axis add instead of aliasing** (`W`+`UP`+`LSHIFT` = 3× lever rate; `A`+`LEFT`+`NUMPAD7` = 3× pedal) | One lever, one rate | `slewDelta[channel] += rate*dt` per **binding** with no per-channel fold (`L4PADRIO.cpp:604-620`). End state is identical (clamped `:956-961`, spring uses max `:157-168`) — only the ramp rate multiplies, so LSHIFT+W doubles throttle rate | **S** but same file as #4 — **bundle and defer** | `BT_GAIT_TRACE=1`, hold `W` alone vs `W`+`UP`, compare lever delta/second |
|
||||
|
||||
### 2b. Reads as dead but is authentic — **documentation defects only** (fix the words, not the code)
|
||||
|
||||
| Input | Reality | Where the words are wrong |
|
||||
|---|---|---|
|
||||
| **keys `2` / `4`** (`0x46` / `0x45`) | **LIVE-by-design.** No mech's `.CTL` authors these two addresses (I independently confirmed the absence across all 18 tables) because they are the pod's **spare mappable** buttons (`buttonType == MAPBTN`, `L4CTRL.cpp:126-128`), filled by the regroup session: `G`→`0x0E`→`ConfigureMappables` (`mechweap.cpp:190-229`) → `EnterConfiguration` temp-maps 0x40/0x45/0x46/0x47 (`btl4mppr.cpp:773-784`) → `ChooseButton` → `MechRIOMapper::AddOrErase` real body (`btl4mppr.cpp:1275-1295`). Runtime-observed: `content/btn2.log:377/534` `[cfgmap] ENTER/EXIT`, `content/cfg.log:741` `[rio-map] toggle btn=0x40 … LEFT`. **Do not seed a default group — that would diverge from the pod.** | `players/README.txt:49-50` lists them as working triggers; `content/bindings.txt:48` files them under "fire groups" |
|
||||
| **F6 / F8** | Page-gated, not dead: no mask-`0x20` record on any mech, but `elem 0x26 → msg 5` / `0x24 → msg 7` under the ENG masks (`ctrlmap.log:94,96,101,103,109,111,117,119`) with real `SelectGeneratorB/D` bodies | `bindings.txt:66-67` + `L4PADBINDINGS.cpp:248-249` imply Quad-page meaning |
|
||||
| **F9** | On the Quad page it opens a **weapon regroup session** (`msg 9`) on 11 of 18 mechs and is absent on 7 (lok1/lok2/loki/mad2/madcat/owens/vulture). Gen-mode (`msg 8`) is ENG-page only. A *held* F9 makes all fire mappings dormant | same comment block |
|
||||
| **`-` / `=`** | Volume works (`mech4.cpp:2756-2771`, direct poll) but neither key is in `bindings.txt`, so `PadRIO::BuildKeySuppression` never claims them and the typed char also reaches `case '-': ZoomTargetRangeOut()` (`btl4mppr.cpp:876`). `-` lowers volume **and** zooms out; there is no key for zoom in | undocumented |
|
||||
| **"Shift ... throttle up"** | Only `LSHIFT` is bound (`bindings.txt:45`); `RSHIFT` is inert. Same for `RALT` vs reverse | `players/README.txt:48` |
|
||||
| **typed `5`** = MFD1 Quad | Swallowed: `key NUMPAD5` sets `suppressChar['5']` via the `VK_NUMPAD0..9 → '0'+n` branch I verified at `L4PADRIO.cpp:246-249` | `bindings.txt:18` promises it |
|
||||
| **`content/CONTROLS.MAP`** (47 rows, tracked & shipped) | Never opened on the shipped profile — `btinput.cpp:773-789` memsets `gBTInput` and returns before `LoadBindings()`. Live proof `content/btn2.log:58 [input] binding engine standing down` | Its own header invites the player to edit it |
|
||||
| **A/D/arrows do not turn** | Authentic: Basic steers with the stick, Standard/Veteran with the pedals | Correctly documented (`README.txt:52-56`) — leave it |
|
||||
|
||||
---
|
||||
|
||||
## 3. DEAD BUT COSMETIC / UNREACHABLE IN PRACTICE
|
||||
|
||||
Recorded so nobody re-discovers them:
|
||||
|
||||
- **Not pod buttons at all, yet drawn clickable:** `0x16` `0x17` `0x1E` = `LampTesla1/2/3`, solid-state **relay outputs** written by `L4APP.cpp:885-911` (they even blink); `0x1F` is named by neither the button nor lamp enum (`L4CTRL.h:448-495`, `:273-281`); `0x38` = `LampIcomDecRelay` (with a stale `//ButtonPanic=0x38` comment at `L4CTRL.cpp:112` — Panic moved to `0x3D`); `0x39`-`0x3B` icom + `0x3C` door, whose entire registration block is `#if 0`-ed (`L4ICOM.cpp:902-949`); `0x3E` = `LampFloor`. Placed at `L4VB16.cpp:306-309` / `L4GLASSWIN.cpp:267-273`.
|
||||
- **`0x19`** Secondary8 — a real illuminated pod button with no authored record on any mech. Authentic.
|
||||
- **`0x3D` "Panic"** — its only registration is a lamp under mask `0x200000` (`ModeEject`, `game/original/BT_L4/BTL4MODE.HPP:73`), which can never be set because `Mech::GetMissionReviewMode()` is a `return 0` stub (`btstubs.cpp:101-104`). Drawn **non-inert and labeled**, so it looks functional. Same stub also makes the pilot keypad (`keyboardGroup[KeyboardPilot]`, registered under `0x200000` at `btl4mppr.cpp:1341-1342`) permanently undeliverable → the 32 panel keypad buttons `0x50`-`0x6F` are inert in normal play.
|
||||
- **Page-gated, correct:** MFD1 Quad `0x0A`/`0x0B`; MFD3 Quad `0x00`-`0x03`; MFD2 Quad `0x24`/`0x26`. Unauthored **pages**: MFD1 Eng3 (`0x8`), MFD3 Eng3/Eng4 (`0x2000`/`0x4000`).
|
||||
- **Live but keyboard-unreachable (mouse-only):** `0x28` BalanceCoolant (handler landed, `mech.cpp:467`+`:476-487`); `0x10`/`0x11` radar zoom; `0x0B`/`0x23`/`0x03` ammo EJECT; `0x43`/`0x44` side looks; condensers 2-6 (`0x29`-`0x2E`).
|
||||
- **Grammar targets nothing uses:** `keypad <unit> <key>` rows (0 rows shipped; consumer gated on `0x200000`); mapper `msg 0x16 ToggleVoiceAssist` (registered, no record targets it, and it raw-reads `GetMech()+0x190` — leave unbound).
|
||||
- **Authentic 1995 typo, do not "fix":** `L4CTRL.cpp:1716-1721` publishes `ButtonThrottle1 - 1` (`0x3E`) on press. Harmless — every consumer tests `>0`/`<1`. Legacy DirectInput path only.
|
||||
- **Unregistered `Mech` message ids with no button:** `0x15` RealMaxSpeed @`0049f604`, `0x17` SetBurningState @`0049f674`, `0x18` ClearBurningState @`0049f700`, `0x19` EjectPilot @`0049f854`. `EjectPilot` is the manual's punch-out (-1000 score) and has no implementation at all — track in `open-questions.md`, not here.
|
||||
- **`ScalarLeftPedal`/`RightPedal` are absent from every streamed table** — correct: the mapper's ctor registers them (`btl4mppr.cpp:1337-1340`). "Not in the `.CTL`" never implies dead.
|
||||
|
||||
---
|
||||
|
||||
## 4. THE SYSTEMIC CAUSES (ranked)
|
||||
|
||||
**First, the retraction.** The brief's step 3 — *"streamed BUTTON mappings are never consumed: both `MechControlsMapper::AddOrErase` overloads are `Fail` stubs"* — is false, and all six auditors plus every adversarial verifier converged on that independently. `LBE4ControlsManager::CreateStreamedMappings` installs button records itself (`L4CTRL.cpp:2254-2262` / `:2327-2336`); the `Fail` bodies at `mechmppr.cpp:1477-1500` are the authentic base traps for the *config-session* vtable slots (+0x40/+0x44), overridden by real bodies in `MechRIOMapper` (`btl4mppr.cpp:1275-1295`) — the mapper `btl4app.cpp:586/628` actually installs. **Reconstructing that pair would clear 0 of 2,294 records.** The false claim is written into `context/pod-hardware.md:121-135` *and* into the source as a 25-line justifying comment at `mechmppr.cpp:894-919`; both must be swept before they justify another stand-in.
|
||||
|
||||
| Rank | Mechanism | Findings explained | The ONE fix |
|
||||
|---|---|---|---|
|
||||
| **1** | **A default-constructed `Receiver::MessageHandlerSet` is a total blackhole.** `RECEIVER.h:376-380` leaves `entryCount = 0` with no parent chain; `Find()` returns `NullHandler` for **every** id (`:398-403`) and `Receiver::Receive` drops it with **no log, no `[FAIL]`, no counter** (`RECEIVER.cpp:99-105`) | **3 dead surfaces = 116 records** (Sensor/Avionics 108, Searchlight, ThermalSight) and 4 more latent: `gyro.cpp:205`, `mechtech.cpp:148`, `messmgr.cpp:102`, `torso.cpp:141` | Give each class a `GetMessageHandlers()` function-local static chained to its base — the exact idiom at `myomers.cpp:88-100` / `powersub.cpp:131-152`. Lint: `grep -rn -B1 '^\t[A-Za-z0-9_]*::MessageHandlers;' game/reconstructed/*.cpp` |
|
||||
| **2** | **Raw 1995 byte offsets inside a reconstructed handler.** The handler *runs*, so nothing logs — it just reads/writes the wrong address. Our layout is provably not the binary's (`btplayer.hpp` declares `levelFlag270@0x270` **before** `advancedDamageOn2@0x268`; the Entity base is ~`0x1BC` vs the binary's `0x300`) | The **largest** dead surface (hotbox, 8 buttons + 5 typed keys) plus latent siblings: `btl4gau3.cpp:772,795,800`, `mechmppr.cpp:581,1391-1392` | Typed bridge functions in the complete-type TU — the precedent already exists two files away (`btplayer.cpp:1542`, `:1775`) — plus `static_assert` on every offset a bridge replaces. Sweep with `grep '\*(int \*)((int)' game/reconstructed` |
|
||||
| **3** | **Three parallel desktop input layers with a stand-down line, and the setters sit on the opposite side of it from the consumers.** `btinput.cpp:773-789` memsets `gBTInput` whenever `BTRIODevicePresent()`, which `PadRIO::IsOperational()` makes unconditionally true (`L4PADRIO.h:44-46`); `mech4.cpp:2733+` derives ~12 globals from that zeroed struct; `mechmppr.cpp:648-870` consumes several of them **inside** a block gated on `!BTRIODevicePresent()` (`:655-660`) | All 47 `CONTROLS.MAP` rows, the torso/elevation recenter (#7), `gBTLookBehind`/`gBTPresetCycle`/`gBTGenSelKey`/fire-level channels, **and every `BT_*_TEST` harness** (`mech4.cpp:5352-5610`) | Retire one layer. Cheapest honest step: stop shipping `CONTROLS.MAP`, delete `btinput`'s port-side action channels, and declare `content/bindings.txt` the single surface. Until then: **only `BT_BTNTEST` (`L4PADRIO.cpp:393-420`) injects at the real seam — use it for all input verification** |
|
||||
| **4** | **Edge state keyed per binding, not per RIO address** — two independent loops (`L4PADRIO.cpp:580-595` keys, `:665-685` pad, `:789-837` joy/hat) each keep their own state and all call `EmitButton` on shared addresses | Fire dropout on `0x40`/`0x47`, cross-device cancel (pad A vs key 1), early release on the duplicated `0x42`/`0x43`/`0x44` pad rows, additive axis rates | One per-address hold count inside `PadRIO::EmitButton` (`L4PADRIO.cpp:341`): emit press on 0→1, release on 1→0. Fixes keyboard + pad + joy + hat + panel clicks at once, and hardens the `gBTReverseHeld` latch |
|
||||
| **5** | **Silent failure by design + invisible gating.** An unmapped RIO address logs nothing; an unhandled message logs nothing; a click off the glass profile logs nothing; and the `BTCockpitBtn::inert` flag — documented at `l4vb16.h:40` as "no authored mapping → drawn greyed" — is populated for **exactly one block of 8** (`L4VB16.cpp:316`) and left `0` for the other 64 (I verified every `CkPushBtn` call) | Why every one of these survived playtesting, plus the whole page-gating class (F6/F8/F9, MFD1 `0x0A/0x0B`, MFD3 `0x00-0x03`) and the Basic-mode pedal cliff | See §7 — three log lines and one `CurrentFilterStatus` call |
|
||||
|
||||
**Best ratio of findings-cleared to risk: cause #1, applied to `Sensor` alone.** One function-local-static accessor in `sensor.cpp`, copied from `myomers.cpp:88-100`, revives 6 cockpit buttons and keys F5-F9 on the MFD2 ENG1 page = **108 of the 2,294 shipped records**, touches no shared input plumbing, cannot regress anything currently working (the set is empty today, so nothing depends on its contents), and is verifiable in one boot with one keypress. Cause #4 has the widest reach but touches every input device at once — that is exactly the change you do *not* make on playtest day.
|
||||
|
||||
---
|
||||
|
||||
## 5. UNKNOWNS — the test script
|
||||
|
||||
| # | Question | Exact test | What answers it |
|
||||
|---|---|---|---|
|
||||
| 1 | **Is the `+0x284` hotbox write out of bounds** (heap corruption) or merely the wrong member? | Add one boot line printing `sizeof(BTPlayer)` and `offsetof(BTPlayer, objectiveMech)`, or a deliberate `static_assert(offsetof(BTPlayer,objectiveMech)==0x284)` | `sizeof < 0x288` ⇒ **out of bounds**; every hotbox click and every `t/y/u/i/o` press corrupts the heap. `ChooseDefaultPilot` has **no caller**, so spawn is safe; `UpdateCurrentPilot` is guarded only by `pilotArrayBuilt && pilotArray[0]!=0`, and `BuildPilotArray()` runs every frame (`btl4mppr.cpp:405`) |
|
||||
| 2 | **Was the reverse fix necessary?** Two writers now exist for `reverseThrust` | Build with `mechmppr.cpp:912-919` `#if 0`'d; `BT_MPPR_TRACE=1 BT_PAD_LOG=1`, throttle to ~0.5, hold LALT | `rev=64` ⇒ the streamed record is the live consumer and the accommodation is redundant (delete it, correct the comment). `rev=0` ⇒ something upstream of `ProcessRIOEvent` eats `0x3F` and the real cause is still unknown |
|
||||
| 3 | **Does a held `LALT` stall the message pump?** (leading unproven candidate for the original report) | Hold LALT for 3 s with `BT_MPPR_TRACE=1` and watch whether the 0.5 s trace lines keep arriving; compare `BT_BTNTEST=0x3F,600,1200` | No `WM_SYSKEYDOWN`/`WM_SYSCHAR`/`WM_SYSCOMMAND`/`SC_KEYMENU` handler exists anywhere in `engine/`+`game/`, so ALT reaches `DefWindowProc` and its menu-modal loop. If the trace pauses, swallow `SC_KEYMENU` in `btl4main.cpp`'s WndProc and/or move reverse off ALT |
|
||||
| 4 | **Per-mech coverage.** Every record-based verdict rests on ONE mech (bhk1, 121 records — and `content/ctrlmap.log:1` was captured on the **DEV** profile) | One boot per mech with `BT_CTRLMAP_LOG=1`; `grep '^\[ctrlmap\] rec'`; diff against the bhk1 baseline. **Re-run tonight's checks under `BT_PLATFORM=glass`** | Settles whether any chassis authors `0x45`/`0x46`, whether `0x19` is unmapped everywhere, and the F9 7-of-18 split |
|
||||
| 5 | **Does the regroup session reach `0x45`/`0x46`?** (`0x40` has been observed: `content/cfg.log:741`) | `BT_FIRE_LOG=1`, hold `G` on the MFD1 Quad page, tap `2` | Expect `[cfgmap] ChooseButton toggle btn 0x46` then `[rio-map] toggle btn=0x46 … JOINED`; release `G` and tap `2` → the weapon fires |
|
||||
| 6 | **Do the pedals actually reach `turnDemand`?** No `pedals=` line exists in any of the 509 logs | `BT_MPPR_TRACE=1`, press `M` once, hold `LEFT` (and squeeze LT) | `pedals=1 turn=1` confirms the `LeftPedal → pedalsPosition → turnDemand` hop (currently T1-static only) |
|
||||
| 7 | **Can `J`/`K`/`L` park you on an unauthored page** (whole bank dead)? | `BT_MODE_LOG=1 BT_CTRLMAP_LOG=1`, cycle every page on every mech, cross-check each resulting mask against the record dump | Any mask that never appears in the dump is a dead-bank page. Fix: make `CyclePresetModeNow`'s `populated[]` test `buttonGroup[bankHi].CurrentFilterStatus(pageMask)` instead of the roster's `auxScreenNumber` |
|
||||
| 8 | **Can `L4CONTROLS` silently downgrade to the 9-record Thrustmaster stream?** Both `MOUSE` and `DIJOYSTICK` set `PrimaryThrustMaster` (`L4CTRL.cpp:816`, `:851`) and the **last** matching token wins | `set L4CONTROLS=PAD,KEYBOARD,MOUSE` and read `btl4.log` | `[ctrlmap] installing 'Thrustmaster' table: 9 mappings` ⇒ throttle, reverse, all looks, torso center, every preset/zoom/mode/coolant/generator record vanish at once. Make `PrimaryRIO` non-demotable |
|
||||
| 9 | **Is the pilot keypad reachable in normal play?** | Click a panel keypad key `0x50-0x5F` with `BT_KEY_LOG=1` in a mission and in mission review | Registered only under `0x200000` (`btl4mppr.cpp:1341-1342`) and `GetMissionReviewMode()` is a `return 0` stub ⇒ expect nothing in both cases |
|
||||
|
||||
---
|
||||
|
||||
## 6. RECOMMENDED ORDER OF WORK
|
||||
|
||||
### SAFE BEFORE TONIGHT — small, isolated, each verifiable in one boot
|
||||
Do them in this order; stop at any point and what you have is still shippable.
|
||||
|
||||
1. **Neuter the three raw `+0x284` writes** (`mechmppr.cpp:1344`, `:1364`, `:1420`) — comment them out or guard them behind the offset check. This removes a possible out-of-bounds write reachable by pressing `t` and **changes nothing a player can see**, because designation is already broken. Do *not* implement the typed bridge tonight: that would newly enable target designation and surface untested downstream consumers (`btl4rdr.cpp:1188`, `btplayer.cpp:712` kill attribution).
|
||||
2. **`Sensor::GetMessageHandlers()`** — one function-local static chained to `PoweredSubsystem::GetMessageHandlers()`, passed to `Sensor::DefaultData` (`sensor.cpp:129-134`). Revives 6 buttons + F5-F9 on the MFD2 ENG1 page. Verify: `K` to an ENG page, F5 → `[gensel] Avionics -> GeneratorA`.
|
||||
3. **`pad BACK button 0x18`** (and optionally `pad START button 0x15`) — a pad-only player can finally leave Basic and the triggers work.
|
||||
4. **`key X button 0x42`** and **`key NUMPAD5 button 0x42`** — keyboard torso recenter.
|
||||
> For 3 and 4: edit **both** `content/bindings.txt` *and* `defaultProfileText` in `L4PADBINDINGS.cpp:206-275`, and change nothing else in the rows, so "delete the file to regenerate" stays lossless.
|
||||
5. **Clear `BT_JOYCONFIG`** after the wizard (`SetEnvironmentVariableA("BT_JOYCONFIG", NULL);` at `btl4main.cpp:290`, plus the clear-list in `BTFE_RelaunchSelfAndExit`). Stops joyconfig users hitting an apparent hang.
|
||||
6. **Ship-time, no code:** `content/bindings.txt` is gitignored, so the zip is packaging **this machine's stale 89-line file with zero joy rows**. Either omit it (the game regenerates the current default, joy grammar and `Turn` channel included) or regenerate it here first. Do this even if you skip everything else — it is the difference between "a HOTAS does nothing" and "a HOTAS works after `joyconfig.bat`".
|
||||
7. **`inert = 1`** for the confirmed-dead panel addresses in `BTCockpitComputeLayout` — `0x12 0x13 0x14 0x16 0x17 0x19 0x1A 0x1B 0x1C 0x1D 0x1E 0x1F`, and flip `thrInert[5]` (`0x3D` "Panic") from 0 to 1 (`L4VB16.cpp:316`). Data-only, and it stops testers filing the same reports.
|
||||
8. **Words:** `players/README.txt` — keys `2`/`4` are *spare, empty until you assign a weapon with `G`*; "Left Shift"; reverse needs *Alt **with the throttle up***; target select is `t y u i o` or the radar hotbox; radar zoom / coolant balance / ammo EJECT are mouse-only (press `J`/`K`/`L` to page first). `content/bindings.txt` header + `L4PADBINDINGS.cpp:248-254` — F6/F8 are inert on the Quad page, F9 is a regroup hold there. `content/CONTROLS.MAP` — a one-line banner: *NOT USED by the shipped launchers; edit `content\bindings.txt`*.
|
||||
|
||||
### MUST WAIT — touches shared input plumbing or needs new decomp
|
||||
- Per-address hold count in `PadRIO::EmitButton` (fixes the fire dropout, cross-device cancel, and the duplicated pad rows). Highest reach, highest blast radius — **not tonight**.
|
||||
- Per-channel axis alias fold (max, not sum) — same file, bundle with the above.
|
||||
- Focus guard for the pad and joy blocks (`L4PADRIO.cpp:662-763`, `:775-911` never consult `ProcessHasFocus()`, and `L4JOY` opens with `DISCL_BACKGROUND`). Relevant tonight only as an operator warning: **alt-tabbing with a trigger or stick held keeps driving the mech.**
|
||||
- `WM_CAPTURECHANGED`/`WM_KILLFOCUS` release, with the `ReleaseCapture` re-entrancy guard.
|
||||
- Deleting the reverse `[T3]` accommodation — only after unknown #2.
|
||||
- `Generator::ToggleGeneratorOnOff` (@`004b1ed0`) and `Mech::DuckRequest` (@`0049fa00`) reconstructions. Note the trap: `msg 4` is `SelectGeneratorA` on `PoweredSubsystem` — chain Generator's set to **HeatSink's**, never PoweredSubsystem's, or those four buttons will do the *wrong* thing instead of nothing.
|
||||
- ThermalSight (handler + body + renderer).
|
||||
- Suppression-table changes (`-`/`=`, the numpad-digit branch) — they alter the typed 1995 channel; pair with binding `PAGEUP`/`PAGEDOWN` to `0x10`/`0x11` so zoom does not simply disappear.
|
||||
- `bindings.txt` version marker + additive merge; `joydev`/token-ceiling parser fixes; DirectInput range read-back and hot-plug re-enumeration; wizard hat-slot bug.
|
||||
- Retiring `btinput`/`CONTROLS.MAP`; the hotbox typed bridge; `EnterControlsMode` / eject (Phase 8).
|
||||
|
||||
---
|
||||
|
||||
## 7. REGRESSION GUARD
|
||||
|
||||
The whole class exists because **three failure modes produce zero output**. Each has a cheap, already-supported fix.
|
||||
|
||||
1. **Unhandled-message trace — the single highest-value line in this report.** In `Receiver::Receive` (`RECEIVER.cpp:99-105`), when `Find()` returns `NullHandler`, log once per `(class, id)` pair using the receiver's `Derivation` name (already available — it is what `ClassDerivations` carries). **This one line would have printed Sensor/Avionics, Searchlight, ThermalSight, crouch and all four generators on the first boot** — 5 of the 11 player-visible findings, and it will catch the next one. ~5 lines, `getenv`-gated or always-on.
|
||||
2. **Bound-address consumer census at startup.** At the end of `CreateStreamedMappings`, walk the loaded `PadBindingProfile`'s button addresses and, for each, call the engine's existing `buttonGroup[addr].CurrentFilterStatus(mask)` (`CONTROLS.h:263-264`, `CONTROLS.cpp:153-176`) across the masks the game can actually be in; log one line per address with zero instances: `[input] WARNING: bound button 0x46 (key 2) has no consumer on this mech`. Catches the reverse-thrust *shape* (a bound address with nothing behind it) plus keys 2/4 and F6/F8 — at boot, on every mech, for free.
|
||||
3. **Drive `BTCockpitBtn::inert` from `CurrentFilterStatus` at draw time** instead of the hardcoded `0`. The field is already documented for exactly this purpose (`l4vb16.h:40`) and the engine already computes the answer. It greys unauthored addresses **and** tracks MFD page gating automatically — a dead button becomes visible in a screenshot, and reverse thrust would have drawn grey the day it broke. Do the data-only version tonight (§6.7), the computed version after.
|
||||
4. **Promote two existing lines to always-on:** `[ctrlmap] installing '<name>' table: N mappings` (`btl4app.cpp:672`) — catches the Thrustmaster downgrade instantly; and add `[cockpit] CLICK addr=0x.. DISCARDED (no RIO device — not the glass profile)` in `BTCockpitMouseDown`, which turns the repeat-offender "held PROGRAM, nothing happened" trap into a log line.
|
||||
5. **Promote the auditors' `BTL4.RES` decoder to `tools/`.** Records are 24 bytes (`controlsGroup, controlsElement, mappingType, subsystemID, modeMask, attributeID|messageID` per `CONTROLS.h:783-803`); type-19 `ControlMappingStream` per mech, validated against `content/ctrlmap.log` at 121/121 records with zero mismatches. It makes "is this button mapped on this mech, on this page?" a two-second static query instead of a per-mech log hunt — and it already produced the authoritative answer, identical on all 18 mechs: **MAPPED** = `0x00-0x15, 0x18, 0x1A-0x1D, 0x20-0x2F, 0x3F, 0x40-0x44, 0x47`; **NEVER MAPPED** = `0x16, 0x17, 0x19, 0x1E, 0x1F, 0x30-0x3E, 0x45, 0x46`.
|
||||
|
||||
### KB sweep owed in the same commits (per the CLAUDE.md correction mandate)
|
||||
`context/pod-hardware.md:121-135` (false AddOrErase premise — also in `mechmppr.cpp:894-919`, `docs/GLASS_COCKPIT.md` §2a); `context/pod-hardware.md:50-61` (`0x28` BalanceCoolant, weapon `msg 0xb`, Myomers `msg 9` are all LIVE since 2026-07-21/23); the hat labels `0x41-0x44` (the shipped RES says `0x41` lookBehind, **`0x42` TORSO CENTER**, `0x43` lookRight, `0x44` lookLeft — fix `content/CONTROLS.MAP:20-22`, `L4PADBINDINGS.cpp:159-160`, `pod-hardware.md`); `context/open-questions.md:172-182` (census stale in both directions — `0x28`/ToggleCooling/ToggleSeekVoltage/EjectAmmo now live, hotbox never listed); `btl4mppr.cpp:1332` calls `0x200000` "ModePreset1" — the surviving T0 header names it **ModeEject** (`game/original/BT_L4/BTL4MODE.HPP:74`, which is authoritative and not yet cited by any context file); `searchlight.cpp:51`/`hud.cpp:282` table-address citations are +4 and mis-paired (real: Searchlight `@0x51117C → @004b838c`, ThermalSight `@0x51120C → @004b860c`); stale `//ButtonPanic=0x38` at `L4CTRL.cpp:112`.
|
||||
@@ -0,0 +1,129 @@
|
||||
"""EMPIRICAL input sweep: press every key bound in content/bindings.txt, one at a
|
||||
time, and record what the game log actually does in response.
|
||||
|
||||
The static audit finds bindings with no consumer by reading code; this proves it
|
||||
at runtime. A key that produces NO log delta and NO channel movement is a
|
||||
candidate dead binding (cross-check against the audit before believing it -- some
|
||||
live effects are silent).
|
||||
|
||||
Usage: python keysweep.py [--hold 1.2] [--only W,S,LALT]
|
||||
|
||||
SAFETY: launches ONE btl4.exe and kills only that PID. Do NOT run while a human
|
||||
is play-testing on this machine (shared GPU + the log would interleave).
|
||||
"""
|
||||
import ctypes
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
KEYEVENTF_KEYUP = 0x0002
|
||||
user32 = ctypes.windll.user32
|
||||
|
||||
# name -> VK, mirroring engine/MUNGA_L4/L4PADBINDINGS.cpp's table (lines 30-60)
|
||||
VK = {
|
||||
"LSHIFT": 0xA0, "RSHIFT": 0xA1, "LCTRL": 0xA2, "RCTRL": 0xA3,
|
||||
"LALT": 0xA4, "RALT": 0xA5, "SPACE": 0x20, "ENTER": 0x0D, "TAB": 0x09,
|
||||
"ESC": 0x1B, "UP": 0x26, "DOWN": 0x28, "LEFT": 0x25, "RIGHT": 0x27,
|
||||
"NUMPAD0": 0x60, "NUMPAD1": 0x61, "NUMPAD2": 0x62, "NUMPAD3": 0x63,
|
||||
"NUMPAD4": 0x64, "NUMPAD5": 0x65, "NUMPAD6": 0x66, "NUMPAD7": 0x67,
|
||||
"NUMPAD8": 0x68, "NUMPAD9": 0x69, "NUMPADPLUS": 0x6B,
|
||||
"NUMPADMINUS": 0x6D, "NUMPADSTAR": 0x6A, "NUMPADSLASH": 0x6F,
|
||||
"NUMPADDOT": 0x6E, "BACKTICK": 0xC0,
|
||||
}
|
||||
for c in "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789":
|
||||
VK.setdefault(c, ord(c))
|
||||
for n in range(1, 13):
|
||||
VK["F%d" % n] = 0x70 + (n - 1)
|
||||
|
||||
REPO = r"C:\git\bt411"
|
||||
CONTENT = os.path.join(REPO, "content")
|
||||
EXE = os.path.join(REPO, "build", "Release", "btl4.exe")
|
||||
LOG = os.path.join(REPO, "scratchpad_keysweep.log")
|
||||
|
||||
hold_s = 1.2
|
||||
only = None
|
||||
for i, a in enumerate(sys.argv):
|
||||
if a == "--hold":
|
||||
hold_s = float(sys.argv[i + 1])
|
||||
if a == "--only":
|
||||
only = [s.strip().upper() for s in sys.argv[i + 1].split(",")]
|
||||
|
||||
|
||||
def parse_bindings():
|
||||
rows = []
|
||||
path = os.path.join(CONTENT, "bindings.txt")
|
||||
for line in open(path, encoding="utf-8", errors="replace"):
|
||||
s = line.split("#")[0].strip()
|
||||
if not s.lower().startswith("key "):
|
||||
continue
|
||||
parts = s.split()
|
||||
if len(parts) < 3:
|
||||
continue
|
||||
name = parts[1].upper()
|
||||
rows.append((name, " ".join(parts[2:])))
|
||||
return rows
|
||||
|
||||
|
||||
rows = parse_bindings()
|
||||
if only:
|
||||
rows = [r for r in rows if r[0] in only]
|
||||
print("bindings to sweep: %d" % len(rows))
|
||||
|
||||
if os.path.exists(LOG):
|
||||
os.remove(LOG)
|
||||
|
||||
env = dict(os.environ)
|
||||
env.update({
|
||||
"BT_START_INSIDE": "1",
|
||||
"BT_KEY_NOFOCUS": "1",
|
||||
"BT_MPPR_TRACE": "1", # mapper demand each 0.5s
|
||||
"BT_PAD_LOG": "1", # [padwrite] button/axis writes
|
||||
"BT_KEY_LOG": "1", # [look] + key diagnostics
|
||||
"BT_MODE_LOG": "1", # [mode] preset/control-mode changes
|
||||
"BT_LOG": LOG,
|
||||
})
|
||||
proc = subprocess.Popen([EXE, "-egg", "LAST.EGG"], cwd=CONTENT, env=env)
|
||||
print("pid", proc.pid)
|
||||
|
||||
results = []
|
||||
try:
|
||||
deadline = time.time() + 200
|
||||
while time.time() < deadline:
|
||||
if os.path.exists(LOG) and "[mppr-c]" in open(LOG, errors="replace").read():
|
||||
break
|
||||
time.sleep(2)
|
||||
else:
|
||||
print("FAIL: never reached the mission")
|
||||
sys.exit(2)
|
||||
time.sleep(2)
|
||||
|
||||
for name, target in rows:
|
||||
vk = VK.get(name)
|
||||
if vk is None:
|
||||
results.append((name, target, "NO-VK", 0))
|
||||
continue
|
||||
before = len(open(LOG, errors="replace").read())
|
||||
user32.keybd_event(vk, 0, 0, 0)
|
||||
time.sleep(hold_s)
|
||||
user32.keybd_event(vk, 0, KEYEVENTF_KEYUP, 0)
|
||||
time.sleep(0.6)
|
||||
txt = open(LOG, errors="replace").read()
|
||||
delta = txt[before:]
|
||||
# ignore the always-on heartbeat lines so only real reactions count
|
||||
noise = re.compile(r"\[(tick|loadphase|waitscreen|relay-stats|sync|gait|gaitSM|drive|mppr)\]")
|
||||
reacted = [l for l in delta.splitlines()
|
||||
if l.strip() and not noise.search(l)]
|
||||
# a mapper-demand change also counts as a reaction
|
||||
dem = re.findall(r"\[mppr-c\] thr=([-\d.]+) rev=(\d+).*?dmd=([-\d.]+)", delta)
|
||||
moved = len(set(dem)) > 1
|
||||
results.append((name, target, "; ".join(reacted[:2])[:90] or ("demand moved" if moved else ""), len(reacted) + (1 if moved else 0)))
|
||||
print(" %-10s %-34s %s" % (name, target, results[-1][2] or "(silent)"))
|
||||
finally:
|
||||
proc.terminate()
|
||||
|
||||
print("\n==== SILENT (candidate dead bindings; cross-check the static audit) ====")
|
||||
for name, target, note, n in results:
|
||||
if n == 0:
|
||||
print(" %-10s %s" % (name, target))
|
||||
Reference in New Issue
Block a user