#!/usr/bin/env python3 """Verify (or derive) SUWSF AOB patterns against a decrypted GB4 dump. - Reads patch/SUWSF.ini, extracts every [Patch:*] Pattern, and reports how many matches each has in the dump produced by dump_decrypted.py. - 1 clean match -> pattern is good, ship it. - 0 matches -> pattern needs deriving for GB4 (see notes printed). - many matches -> pattern too loose; tighten or set Match to a specific index. Usage: python3 tools/aob.py [dump.bin] [SUWSF.ini] """ import re, sys, os DUMP = sys.argv[1] if len(sys.argv) > 1 else \ "/tmp/claude-1000/-home-ckoch-Documents-Development-BG4-UltraWide-fix/1a8ebba4-6fbe-45ae-a873-0e69d867e0db/scratchpad/gb4dump/gb4_decrypted.bin" INI = sys.argv[2] if len(sys.argv) > 2 else \ os.path.join(os.path.dirname(__file__), "..", "patch", "SUWSF.ini") def parse_pattern(pat): """'0F ?? 3F' -> (regex bytes). ?? = wildcard byte.""" out = bytearray() mask = [] for tok in pat.split(): if tok in ("??", "?"): out.append(0) mask.append(False) else: out.append(int(tok, 16)) mask.append(True) # build a regex over raw bytes rx = b"" for b, m in zip(out, mask): rx += re.escape(bytes([b])) if m else b"[\\x00-\\xff]" return re.compile(rx, re.DOTALL) def find_all(data, rx, limit=50): hits, pos = [], 0 while len(hits) < limit: m = rx.search(data, pos) if not m: break hits.append(m.start()) pos = m.start() + 1 return hits def load_patches(ini_path): patches = [] cur = None with open(ini_path) as f: for raw in f: line = raw.strip() if line.startswith("[") and "Patch" in line: cur = {"name": line.strip("[]")} patches.append(cur) elif line.startswith("[") : cur = None elif cur is not None and "=" in line and not line.startswith(";"): k, _, v = line.partition("=") cur[k.strip()] = v.strip().strip('"') return patches def main(): if not os.path.exists(DUMP): print(f"[!] dump not found: {DUMP}\n Run tools/dump_decrypted.py while the game is running.") sys.exit(1) data = open(DUMP, "rb").read() print(f"[+] dump: {len(data):,} bytes") patches = load_patches(os.path.abspath(INI)) print(f"[+] {len(patches)} patches in {os.path.abspath(INI)}\n") for p in patches: pat = p.get("Pattern") if not pat: continue rx = parse_pattern(pat) hits = find_all(data, rx) enabled = p.get("Enabled", "true").lower() status = "OK " if len(hits) == 1 else ("MISS" if not hits else "MANY") print(f"[{status}] {p['name']:32s} enabled={enabled:5s} matches={len(hits)} {pat[:40]}...") for h in hits[:4]: ctx = data[h:h + 24].hex(" ") print(f" @rva 0x{h:x}: {ctx}") print("\nLegend: OK=exactly 1 match (ship it) | MISS=derive new pattern | MANY=tighten/Match=n") if __name__ == "__main__": main()