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>
This commit is contained in:
2026-07-21 11:21:20 -04:00
co-authored by Claude Opus 4.8
parent b37677cbc1
commit 5d6fd277c9
14 changed files with 294 additions and 166 deletions
+7 -8
View File
@@ -1,21 +1,20 @@
#!/usr/bin/env python3
"""Verify (or derive) SUWSF AOB patterns against a decrypted GB4 dump.
"""Verify (or derive) SUWSF AOB patterns against a decrypted game dump.
- Reads patch/SUWSF.ini, extracts every [Patch:*] Pattern, and reports how many
- 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 GB4 (see notes printed).
- 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] [SUWSF.ini]
python3 tools/aob.py <dump.bin> [games/<Game>/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"
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__), "..", "patch", "SUWSF.ini")
os.path.join(os.path.dirname(__file__), "..", "games", "GundamBreaker4", "SUWSF.ini")
def parse_pattern(pat):
@@ -65,7 +64,7 @@ def load_patches(ini_path):
def main():
if not os.path.exists(DUMP):
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()
+19 -13
View File
@@ -1,21 +1,27 @@
#!/usr/bin/env bash
# Build the drop-in release zip from committed sources + vendored SUWSF.
# Output: dist/GB4_UltraWide_Fix.zip (attach to the GitHub Release)
# Build a drop-in release zip for one game.
# ./tools/build_release.sh GundamBreaker4
# Output: dist/<Game>_UltraWide_Fix.zip (attach to the GitHub Release)
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$HERE"
V="${1:-vendor/SUWSF-x64}"
for f in "$V/SUWSF.asi" "$V/dsound.dll" "$V/LICENSE" patch/SUWSF.ini; do
GAME="${1:?usage: build_release.sh <GameName>}"
GDIR="games/$GAME"
V="vendor/SUWSF-x64"
[ -d "$GDIR" ] || { echo "[!] no such game: $GDIR"; exit 1; }
for f in "$V/SUWSF.asi" "$V/dsound.dll" "$V/LICENSE" "$GDIR/SUWSF.ini"; do
[ -f "$f" ] || { echo "[!] missing $f"; exit 1; }
done
rm -rf dist/GB4_UltraWide_Fix dist/GB4_UltraWide_Fix.zip
mkdir -p dist/GB4_UltraWide_Fix
cp "$V/SUWSF.asi" "$V/dsound.dll" patch/SUWSF.ini dist/GB4_UltraWide_Fix/
cp "$V/LICENSE" dist/GB4_UltraWide_Fix/SUWSF-LICENSE.txt
cp README.md THIRD_PARTY.md dist/GB4_UltraWide_Fix/ 2>/dev/null || true
( cd dist && zip -r -q GB4_UltraWide_Fix.zip GB4_UltraWide_Fix )
echo "[+] Built dist/GB4_UltraWide_Fix.zip"
( cd dist && sha256sum GB4_UltraWide_Fix.zip )
unzip -l dist/GB4_UltraWide_Fix.zip
OUT="dist/${GAME}_UltraWide_Fix"
rm -rf "$OUT" "$OUT.zip"
mkdir -p "$OUT"
cp "$V/SUWSF.asi" "$V/dsound.dll" "$GDIR/SUWSF.ini" "$OUT/"
cp "$V/LICENSE" "$OUT/SUWSF-LICENSE.txt"
cp "$GDIR/README.md" "$OUT/README.md" 2>/dev/null || true
cp THIRD_PARTY.md "$OUT/" 2>/dev/null || true
( cd dist && zip -r -q "${GAME}_UltraWide_Fix.zip" "${GAME}_UltraWide_Fix" )
echo "[+] Built dist/${GAME}_UltraWide_Fix.zip"
( cd dist && sha256sum "${GAME}_UltraWide_Fix.zip" )
unzip -l "dist/${GAME}_UltraWide_Fix.zip"
+9 -8
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Dump the DECRYPTED GB4-Win64-Shipping.exe image from a running (Proton) process.
"""Dump a DRM-DECRYPTED game image from a running (Proton) process.
Why: the on-disk exe is SteamStub-encrypted (.text entropy 8.0). The real UE4
code only exists in memory after the stub decrypts it at launch. This grabs that
@@ -8,7 +8,7 @@ memory so we can verify/derive the exact SUWSF byte patterns offline.
Usage:
# 1. Launch Gundam Breaker 4 (get to the main menu / lobby).
# 2. Run:
python3 tools/dump_decrypted.py
python3 tools/dump_decrypted.py <ExeName.exe> [pid]
# -> writes scratch dump + region metadata, then prints AOB match report.
If you get "Operation not permitted", either run with sudo, or temporarily:
@@ -16,8 +16,8 @@ If you get "Operation not permitted", either run with sudo, or temporarily:
"""
import ctypes, ctypes.util, os, re, sys, json, struct, glob
MODULE_HINT = "GB4-Win64-Shipping.exe"
OUTDIR = os.environ.get("GB4_DUMP_DIR", "/tmp/claude-1000/-home-ckoch-Documents-Development-BG4-UltraWide-fix/1a8ebba4-6fbe-45ae-a873-0e69d867e0db/scratchpad/gb4dump")
MODULE_HINT = next((a for a in sys.argv[1:] if a.lower().endswith(".exe")), "GB4-Win64-Shipping.exe")
OUTDIR = os.environ.get("UWP_DUMP_DIR", os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "dumps"))
libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True)
@@ -109,18 +109,19 @@ def main():
meta["regions"].append({"start": start, "end": end, "rva": rva,
"perms": perms, "size": size, "got": len(data)})
print(f" dumped 0x{start:x}-0x{end:x} {perms} rva=0x{rva:x} ({len(data)} bytes)")
dump_path = os.path.join(OUTDIR, "gb4_decrypted.bin")
stem = os.path.splitext(os.path.basename(MODULE_HINT))[0]
dump_path = os.path.join(OUTDIR, f"{stem}_decrypted.bin")
with open(dump_path, "wb") as f:
f.write(blob)
with open(os.path.join(OUTDIR, "gb4_dump_meta.json"), "w") as f:
with open(os.path.join(OUTDIR, f"{stem}_meta.json"), "w") as f:
json.dump(meta, f, indent=2)
print(f"[+] wrote {dump_path} ({len(blob)} bytes, rva-aligned)")
print(f"[+] wrote {os.path.join(OUTDIR, 'gb4_dump_meta.json')}")
print(f"[+] wrote {os.path.join(OUTDIR, stem + '_meta.json')}")
# quick sanity: is it decrypted? look for the UE4 version string / a UTF-16 hint
if b"++UE4+Release-4.27" in blob or b"CalculateProjectionMatrix" in blob:
print("[+] decrypted UE4 code confirmed in dump.")
print("\nNext: python3 tools/aob.py (verifies SUWSF patterns against this dump)")
print(f"\nNext: python3 tools/aob.py {dump_path} games/<Game>/SUWSF.ini")
if __name__ == "__main__":
+10 -8
View File
@@ -7,11 +7,12 @@
#
# Usage:
# ./tools/publish.sh [REPO_NAME] [--private]
# (defaults: repo name "GB4-UltraWide-Fix", public)
# (defaults: repo name "UltraWidePatches", public)
# GAME=GundamBreaker4 selects which game's zip to attach (default: GundamBreaker4)
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"; cd "$HERE"
REPO="${1:-GB4-UltraWide-Fix}"
REPO="${1:-UltraWidePatches}"
VIS="--public"; [ "${2:-}" = "--private" ] && VIS="--private"
TAG="v1.0.0"
@@ -21,8 +22,9 @@ OWNER="$(gh api user -q .login)"
echo "[+] Publishing as: $OWNER repo: $REPO $VIS"
# 1. build the release artifact fresh
bash tools/build_release.sh >/dev/null
echo "[+] Built dist/GB4_UltraWide_Fix.zip"
GAME="${GAME:-GundamBreaker4}"
bash tools/build_release.sh "$GAME" >/dev/null
echo "[+] Built dist/${GAME}_UltraWide_Fix.zip"
# 2. create the repo (skip if it already exists) and set as origin
if gh repo view "$OWNER/$REPO" >/dev/null 2>&1; then
@@ -30,7 +32,7 @@ if gh repo view "$OWNER/$REPO" >/dev/null 2>&1; then
git remote get-url origin >/dev/null 2>&1 || git remote add origin "https://github.com/$OWNER/$REPO.git"
else
gh repo create "$OWNER/$REPO" $VIS --source=. --remote=origin \
--description="UltraWide (32:9/21:9) fix for Gundam Breaker 4 — Hor+ FOV via SUWSF. Proton-ready."
--description="Ultrawide (32:9/21:9) fixes for PC games — Hor+ FOV via SUWSF. Proton-first."
fi
# 3. push branch + tag
@@ -39,10 +41,10 @@ git push origin "$TAG"
# 4. create the release with notes + zip asset (idempotent)
if gh release view "$TAG" -R "$OWNER/$REPO" >/dev/null 2>&1; then
gh release upload "$TAG" dist/GB4_UltraWide_Fix.zip -R "$OWNER/$REPO" --clobber
gh release upload "$TAG" "dist/${GAME}_UltraWide_Fix.zip" -R "$OWNER/$REPO" --clobber
else
gh release create "$TAG" dist/GB4_UltraWide_Fix.zip \
-R "$OWNER/$REPO" -t "GB4 UltraWide Fix $TAG" -F RELEASE_NOTES.md
gh release create "$TAG" "dist/${GAME}_UltraWide_Fix.zip" \
-R "$OWNER/$REPO" -t "$TAG" -F RELEASE_NOTES.md
fi
echo
+5 -3
View File
@@ -1,8 +1,10 @@
#!/usr/bin/env bash
# Reads the newest Proton log for GB4 (appid 1672500) and extracts SUWSF + DLL-load evidence.
# Reads the newest Proton log for a game and extracts SUWSF + DLL-load evidence.
# Usage: ./tools/read_protonlog.sh <APPID> (e.g. 1672500 for Gundam Breaker 4)
set -uo pipefail
LOG=$(ls -t "$HOME"/steam-1672500.log "$HOME"/.steam/steam/logs/steam-1672500.log 2>/dev/null | head -1)
[ -z "${LOG:-}" ] && { echo "[!] No steam-1672500.log found. Add PROTON_LOG=1 to launch options and relaunch."; exit 1; }
APPID="${1:?usage: read_protonlog.sh <APPID>}"
LOG=$(ls -t "$HOME"/steam-$APPID.log "$HOME"/.steam/steam/logs/steam-$APPID.log 2>/dev/null | head -1)
[ -z "${LOG:-}" ] && { echo "[!] No steam-$APPID.log found. Add PROTON_LOG=1 to launch options and relaunch."; exit 1; }
echo "[+] Log: $LOG ($(stat -c%s "$LOG") bytes, modified $(stat -c%y "$LOG"))"
echo "=== Did our native dsound.dll load? ==="
grep -iE "dsound\.dll|SUWSF\.asi|Ultimate" "$LOG" | grep -iE "load|builtin|native|override" | head