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
130 lines
4.5 KiB
Python
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))
|