Gundam Breaker 4 UltraWide Fix v1.0.0
32:9 / 21:9 ultrawide support for Gundam Breaker 4 (UE4.27) via SUWSF + Ultimate ASI Loader. Forces Hor+ FOV (AspectRatioAxisConstraint=MaintainYFOV) and disables pillarboxing, patched in decrypted memory at runtime so it holds through missions (the exe is SteamStub-encrypted; a static patch is impossible). Confirmed working in gameplay at 5120x1440 (Samsung Odyssey G93SC) on Proton. Requires WINEDLLOVERRIDES="dsound=n,b" on Proton so the loader injects. Includes: patch/SUWSF.ini, Proton installer with clean uninstall, release-zip builder, and runtime dump/verify tooling. Third-party bundles under MIT (see THIRD_PARTY.md). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Executable
+91
@@ -0,0 +1,91 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user