Files
BT411/scratchpad/keysweep.py
T
arcattackandClaude Opus 5 b517c4cb30 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
2026-07-24 20:48:31 -05:00

130 lines
4.5 KiB
Python

"""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))