Files
ckochandClaude Opus 4.8 5d6fd277c9 Restructure into multi-game UltraWidePatches umbrella
Generalize the project from a single GB4 fix into a collection that can host
ultrawide patches for many games.

- games/<Game>/{SUWSF.ini,game.conf,README.md}; GB4 moved in as the first entry
- game.conf carries all game-specific data (appid, exe, paths, loader, launch
  option), so install.sh is fully generic: ./install.sh <Game> [uninstall]
- installer gains GAME_DIR / PREFIX_INI_FILE / LOADER_NAME overrides; verified
  install -> idempotent re-run -> uninstall leaves the tree pristine
- tools de-hardcoded: dump_decrypted.py takes an exe name, read_protonlog.sh
  takes an appid, aob.py takes a dump + game ini, build_release.sh takes a game
- README rewritten as an umbrella index with an "adding a game" guide
- LICENSE copyright -> programmingPug

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 11:21:20 -04:00

91 lines
3.0 KiB
Python
Executable File

#!/usr/bin/env python3
"""Verify (or derive) SUWSF AOB patterns against a decrypted game dump.
- Reads a game's 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 this game (see notes printed).
- many matches -> pattern too loose; tighten or set Match to a specific index.
Usage:
python3 tools/aob.py <dump.bin> [games/<Game>/SUWSF.ini]
"""
import re, sys, os
DUMP = sys.argv[1] if len(sys.argv) > 1 else ""
INI = sys.argv[2] if len(sys.argv) > 2 else \
os.path.join(os.path.dirname(__file__), "..", "games", "GundamBreaker4", "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 DUMP or 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()