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