commit dc28d603928419620d87a265b6c01bb5cceaaea7 Author: ckoch Date: Mon Jul 20 20:21:23 2026 -0400 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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..443be91 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# Python +.venv/ +__pycache__/ +*.pyc + +# Build artifacts (release zip is generated by tools/build_release.sh and attached to the Release) +/dist/ + +# Upstream zip (extracted binaries in vendor/SUWSF-x64/ are committed; the raw zip is not) +/vendor/SUWSF-x64.zip + +# Runtime dumps / scratch +gb4dump/ +*.bin +*.bak-bg4uw diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..173b257 --- /dev/null +++ b/LICENSE @@ -0,0 +1,26 @@ +MIT License + +Copyright (c) 2026 progkoch + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--------------------------------------------------------------------------- +This license covers the original work in this repository: the SUWSF patch +definitions (patch/SUWSF.ini), the installer (install.sh), and the tools/. +Bundled third-party components are under their own licenses; see THIRD_PARTY.md. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7dcbab5 --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +# Gundam Breaker 4 — UltraWide Fix + +UltraWide (32:9) fix for **Gundam Breaker 4** on PC, targeting the **Samsung 49" Odyssey G93SC** (5120×1440). Works on **Linux/Proton** and Windows. + +## What it fixes + +At 32:9 the game keeps a fixed *horizontal* FOV, so a wider display just **zooms the camera in** instead of showing more. This forces the camera to keep *vertical* FOV constant and expand horizontally (**Hor+**), and disables the pillarbox/letterbox clamp — the correct ultrawide behavior. + +## How it works (and why it has to work this way) + +- GB4 is **Unreal Engine 4.27**. The classic fix is to set `AspectRatioAxisConstraint = MaintainYFOV`, but doing it in `Engine.ini` **reverts once a mission loads** — the game re-sets the camera constraint per view at runtime. +- The shipping exe (`GB4-Win64-Shipping.exe`) is wrapped in **SteamStub DRM**: `.text` is encrypted on disk (entropy 8.0), so a static hex patch of the file is impossible. The real code only exists decrypted in memory. +- So the fix is a **runtime memory patch** via [SUWSF](https://github.com/PhantomGamers/SUWSF) (Somewhat Universal Widescreen Fix) loaded by [Ultimate ASI Loader](https://github.com/ThirteenAG/Ultimate-ASI-Loader). SUWSF scans the *decrypted* image at launch and rewrites the instruction that selects the FOV axis — surviving into missions. + +## Install (Linux / Proton) + +```bash +./install.sh +``` + +This drops `SUWSF.asi`, `dsound.dll` (ASI loader), and `SUWSF.ini` next to the shipping exe, and adds the `MaintainYFOV` tweak to the Proton-prefix `Engine.ini` (fixes menus/lobby instantly; SUWSF covers missions). It **never modifies the exe or any game file** and keeps an `Engine.ini.bak-bg4uw` backup. + +**REQUIRED on Proton** — add this to GB4's **Steam → Properties → Launch Options**, or the ASI loader is ignored and the fix only works in the lobby: + +``` +WINEDLLOVERRIDES="dsound=n,b" %command% +``` + +Then set the in-game resolution to **5120×1440** fullscreen. (If `dsound` ever collides, `LOADER_NAME=version.dll ./install.sh` with `version=n,b` works too.) + +> ✅ Confirmed working in gameplay at 5120×1440 (32:9) on Proton — Hor+ FOV holds through missions. + +### Install (Windows) +Extract `dist/GB4_UltraWide_Fix.zip` into `...\GB4\Binaries\Win64\` (next to `GB4-Win64-Shipping.exe`). + +### Uninstall +```bash +./install.sh uninstall +``` + +## Status & the one open step + +The byte patterns in [`patch/SUWSF.ini`](patch/SUWSF.ini) are **candidate UE4 patterns** proven on other UE4 titles (Trepang2, Lies of P). Because the DRM encrypts the exe, they can only be verified against GB4's **decrypted memory at runtime**. If they hit (likely — same engine family), you're done. If a patch reports no change: + +```bash +# with the game running at the main menu: +python3 tools/dump_decrypted.py # dumps decrypted image via /proc//mem +python3 tools/aob.py # reports which patterns matched (OK / MISS / MANY) +``` + +Send me the `aob.py` output and I'll derive GB4's exact pattern from the dump. + +## Layout + +| Path | What | +|---|---| +| `patch/SUWSF.ini` | The ultrawide patch definitions (HOR+, pillarbox, fallbacks) | +| `install.sh` | Proton/Linux installer + uninstaller | +| `tools/dump_decrypted.py` | Dumps the decrypted exe image from the running process | +| `tools/aob.py` | Verifies/derives AOB patterns against a dump | +| `tools/find_projfunc.py` | Static fingerprint scan (confirmed the DRM encryption) | +| `dist/GB4_UltraWide_Fix.zip` | Drop-in bundle for manual/Windows install | +| `vendor/SUWSF-x64/` | Upstream SUWSF 2.3.0 + Ultimate ASI Loader | + +## Credits +[SUWSF](https://github.com/PhantomGamers/SUWSF) by PhantomGamers · [Ultimate ASI Loader](https://github.com/ThirteenAG/Ultimate-ASI-Loader) by ThirteenAG · AspectRatioAxisConstraint / pillarbox patterns adapted from [Lyall's UltrawidePatches](https://codeberg.org/Lyall/UltrawidePatches). diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md new file mode 100644 index 0000000..594a2ea --- /dev/null +++ b/RELEASE_NOTES.md @@ -0,0 +1,27 @@ +# Gundam Breaker 4 — UltraWide Fix v1.0.0 + +Native-feeling **32:9 / 21:9 ultrawide** support for **Gundam Breaker 4** (PC). Forces **Hor+ FOV** (you see *more*, the camera doesn't zoom in) and disables pillarboxing. Confirmed working in gameplay at **5120×1440** on Linux/Proton (Samsung 49" Odyssey G93SC). + +## Install (Linux / Proton) +1. Extract `GB4_UltraWide_Fix.zip` into `…/GB4/Binaries/Win64/` (next to `GB4-Win64-Shipping.exe`), or run `./install.sh` from the repo. +2. **Required:** add to GB4's **Steam → Properties → Launch Options**: + ``` + WINEDLLOVERRIDES="dsound=n,b" %command% + ``` + Without this, Proton ignores the loader and the fix only works in the lobby. +3. Set the in-game resolution to your display's native ultrawide res, fullscreen. + +## Install (Windows) +Extract `GB4_UltraWide_Fix.zip` into `…\GB4\Binaries\Win64\`. No launch option needed. + +## How it works +GB4 is Unreal Engine 4.27 and its shipping exe is SteamStub-encrypted, so a static exe patch isn't possible. This uses [SUWSF](https://github.com/PhantomGamers/SUWSF) + [Ultimate ASI Loader](https://github.com/ThirteenAG/Ultimate-ASI-Loader) to patch the decrypted UE4 `AspectRatioAxisConstraint` in memory at runtime — which is why it holds through missions (the `Engine.ini` trick alone reverts in-mission). + +## Notes +- Do not use the HOR+ patch at 16:9 or narrower. +- You must own the game. This does not modify or circumvent any game copy protection. +- Uninstall: delete `SUWSF.asi`, `SUWSF.ini`, `dsound.dll` from `Binaries/Win64` (or `./install.sh uninstall`). + +**Checksum (GB4_UltraWide_Fix.zip):** `c1df7e963003f6b5c0583104908be0430e2b2ae1c5087c014ecf14c06b1cc7d3` + +Credits: SUWSF by PhantomGamers · Ultimate ASI Loader by ThirteenAG · patterns adapted from Lyall's UltrawidePatches. diff --git a/THIRD_PARTY.md b/THIRD_PARTY.md new file mode 100644 index 0000000..cbcfabe --- /dev/null +++ b/THIRD_PARTY.md @@ -0,0 +1,29 @@ +# Third-Party Components + +This project bundles the following components, redistributed under their +respective licenses. This ultrawide fix is a configuration + installer around +them; all runtime patching is performed by SUWSF. + +## SUWSF (Somewhat Universal Widescreen Fix) +- Author: PhantomGamers +- Source: https://github.com/PhantomGamers/SUWSF +- License: MIT (see `vendor/SUWSF-x64/LICENSE`) +- Files: `vendor/SUWSF-x64/SUWSF.asi` +- Version bundled: 2.3.0 + +## Ultimate ASI Loader +- Author: ThirteenAG +- Source: https://github.com/ThirteenAG/Ultimate-ASI-Loader +- License: MIT +- Files: `vendor/SUWSF-x64/dsound.dll` (renamed proxy; adapts by filename) + +## Patch patterns +The `AspectRatioAxisConstraint` (HOR+) and `ConstrainAspectRatio` (pillarbox) +byte patterns in `patch/SUWSF.ini` are adapted from the UE4 fixes in +[Lyall's UltrawidePatches](https://codeberg.org/Lyall/UltrawidePatches) +(notably Trepang2 and Lies of P), which are provided for community use with SUWSF. + +## Note +This project does not modify, crack, or circumvent any game copy protection. +SUWSF applies runtime aspect-ratio/FOV adjustments in memory for the benefit of +owners playing on ultrawide displays. You must own the game. diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..0dab88e --- /dev/null +++ b/install.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +# ============================================================================ +# Gundam Breaker 4 - UltraWide Fix installer (Linux / Proton) +# Installs SUWSF + Ultimate ASI Loader + our GB4 SUWSF.ini next to the +# shipping exe. Idempotent. Use ./install.sh uninstall to remove. +# ============================================================================ +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +LOADER_NAME="${LOADER_NAME:-dsound.dll}" # override: LOADER_NAME=version.dll ./install.sh + +# --- locate GB4 shipping exe dir across Steam libraries ----------------------- +find_gamedir() { + local cand + for base in \ + "$HOME/.steam/steam" "$HOME/.local/share/Steam" \ + "/media/$USER/Data/steam" "/media/$USER"/*/steam \ + "$HOME/.var/app/com.valvesoftware.Steam/.local/share/Steam"; do + cand="$base/steamapps/common/GBBBB/GB4/Binaries/Win64" + [ -f "$cand/GB4-Win64-Shipping.exe" ] && { echo "$cand"; return 0; } + done + # brute-force fallback + cand=$(find /home /media /mnt -maxdepth 8 -name GB4-Win64-Shipping.exe 2>/dev/null | head -1) + [ -n "$cand" ] && { dirname "$cand"; return 0; } + return 1 +} + +GAMEDIR="${GB4_WIN64_DIR:-$(find_gamedir || true)}" +if [ -z "${GAMEDIR:-}" ] || [ ! -f "$GAMEDIR/GB4-Win64-Shipping.exe" ]; then + echo "[!] Could not find GB4-Win64-Shipping.exe." + echo " Set it manually: GB4_WIN64_DIR=/path/to/GB4/Binaries/Win64 ./install.sh" + exit 1 +fi +echo "[+] Game dir: $GAMEDIR" + +ASI="$HERE/vendor/SUWSF-x64/SUWSF.asi" +LOADER_SRC="$HERE/vendor/SUWSF-x64/dsound.dll" # Ultimate ASI Loader (adapts by filename) +INI="$HERE/patch/SUWSF.ini" +for f in "$ASI" "$LOADER_SRC" "$INI"; do + [ -f "$f" ] || { echo "[!] missing $f"; exit 1; } +done + +# --- locate the Proton prefix Engine.ini (for the immediate menu/lobby win) --- +find_engine_ini() { + local cand + for base in \ + "/media/$USER/Data/steam" "$HOME/.local/share/Steam" \ + "$HOME/.steam/steam" "/media/$USER"/*/steam; do + cand="$base/steamapps/compatdata/1672500/pfx/drive_c/users/steamuser/AppData/Local/GB4/Saved/Config/WindowsNoEditor/Engine.ini" + [ -f "$cand" ] && { echo "$cand"; return 0; } + done + return 1 +} +MARK_BEGIN="; >>> BG4 UltraWide Fix (MaintainYFOV) >>>" +MARK_END="; <<< BG4 UltraWide Fix <<<" + +apply_engine_ini() { + local ini; ini="$(find_engine_ini || true)" + [ -z "$ini" ] && { echo "[i] Engine.ini not found (launch the game once first); skipping menu-level win."; return; } + if grep -qF "$MARK_BEGIN" "$ini"; then echo "[i] Engine.ini tweak already present."; return; fi + cp -v "$ini" "$ini.bak-bg4uw" + { echo ""; echo "$MARK_BEGIN"; echo "[/Script/Engine.LocalPlayer]"; + echo "AspectRatioAxisConstraint=AspectRatio_MaintainYFOV"; echo "$MARK_END"; } >> "$ini" + echo "[+] Engine.ini: added MaintainYFOV (fixes menus/lobby immediately; SUWSF covers missions)." +} + +remove_engine_ini() { + local ini; ini="$(find_engine_ini || true)" + [ -z "$ini" ] && return + if grep -qF "$MARK_BEGIN" "$ini"; then + sed -i "/$(printf '%s' "$MARK_BEGIN" | sed 's/[][\/.*^$]/\\&/g')/,/$(printf '%s' "$MARK_END" | sed 's/[][\/.*^$]/\\&/g')/d" "$ini" + echo "[+] Engine.ini: removed BG4 UltraWide block." + fi +} + +uninstall() { + echo "[+] Uninstalling..." + for f in SUWSF.asi SUWSF.ini dsound.dll dinput8.dll version.dll winmm.dll; do + [ -f "$GAMEDIR/$f" ] && { rm -v "$GAMEDIR/$f"; } + done + remove_engine_ini + echo "[+] Done. (game files untouched; nothing was ever modified in the exe)" +} + +if [ "${1:-}" = "uninstall" ]; then uninstall; exit 0; fi + +# --- install ------------------------------------------------------------------ +cp -v "$ASI" "$GAMEDIR/SUWSF.asi" +cp -v "$INI" "$GAMEDIR/SUWSF.ini" +cp -v "$LOADER_SRC" "$GAMEDIR/$LOADER_NAME" +apply_engine_ini + +echo +echo "[+] Installed: SUWSF.asi, SUWSF.ini, $LOADER_NAME" +echo +echo "NEXT (Steam):" +echo " 1. If the game does not apply the fix, add this to GB4's Steam" +echo " Launch Options (right-click game > Properties > Launch Options):" +echo " WINEDLLOVERRIDES=\"${LOADER_NAME%.dll}=n,b\" %command%" +echo " 2. Set the in-game resolution to 5120x1440 (fullscreen)." +echo " 3. Launch. Camera should widen (Hor+) instead of zooming in." +echo +echo "If nothing changes, the runtime AOB may differ for GB4. Run:" +echo " python3 tools/dump_decrypted.py (while game is at main menu)" +echo " python3 tools/aob.py (reports which patterns matched)" +echo +echo "To try a different loader name: LOADER_NAME=version.dll ./install.sh" +echo "To remove everything: ./install.sh uninstall" diff --git a/patch/SUWSF.ini b/patch/SUWSF.ini new file mode 100644 index 0000000..92b7552 --- /dev/null +++ b/patch/SUWSF.ini @@ -0,0 +1,71 @@ +[UserSettings] +; ========================================================================== +; Gundam Breaker 4 - UltraWide Fix (SUWSF patch) +; Target display: Samsung 49" Odyssey G93SC (5120x1440, 32:9) +; Engine: Unreal Engine 4.27 | Exe is SteamStub-encrypted -> runtime patch +; ========================================================================== +; Your screen resolution used in patch calculations. "auto" reads primary +; display; set explicitly if auto misreads under Proton/DPI scaling. +Resolution="5120x1440" +; Globally enable or disable all patches (default: true) +Enabled=true + +; -------------------------------------------------------------------------- +; WARNING: PATCHES BELOW. These are CANDIDATE UE4 patterns adapted from +; known-good UE4 fixes (Trepang2 / Lies of P). Because GB4's .text is +; SteamStub-encrypted on disk we cannot verify the exact bytes statically; +; SUWSF scans the DECRYPTED image in memory at runtime, so if GB4's UE4.27 +; codegen matches these generic patterns they apply directly. +; +; If a patch reports 0 matches (see SUWSF log / no visual change), run +; tools/dump_decrypted.sh while the game is running and send me the dump so +; I can derive GB4's exact pattern. Each patch is independent - a miss on one +; does not stop the others. +; -------------------------------------------------------------------------- + +; [1] HOR+ FOV -- the primary fix. +; Forces ULocalPlayer AspectRatioAxisConstraint to 0 (AspectRatio_MaintainYFOV). +; This makes the camera keep VERTICAL FOV constant and expand horizontally +; (Hor+), removing the zoomed-in feel at 32:9. Do NOT use at 16:9 or narrower. +; Value BA 00 00 00 00 = "mov edx, 0" ; 90 90 90 = pad nops. +[Patch:AspectRatioAxisConstraint] +Enabled=true +Pattern="0F ?? ?? ?? 0F ?? ?? ?? ?? ?? 41 ?? ?? ?? ?? ?? 00 00 48 ?? ?? ?? ?? 00 00 4C ?? ??" +Offset=10 +Value="BA 00 00 00 00 90 90 90" +ValueType="byte" +Match="1" +Module="auto" + +; [2] Disable pillarboxing/letterboxing (bConstrainAspectRatio = 0). +; Prevents the game clamping the render to 16:9 with black side bars. +[Patch:ConstrainAspectRatio] +Enabled=true +Pattern="33 ?? ?? 83 ?? 02 31 ?? ?? 0F ?? ?? ?? ?? 00 00" +Offset=0 +Value="90 90 90 90 90 90" +ValueType="byte" +Match="1" +Module="auto" + +; [3] OPTIONAL fallback: some UE builds hardcode 16:9 (1.777778) as a float +; (39 8E E3 3F) or (3B 8E E3 3F) and clamp to it. Disabled by default because +; it can double-apply with patch [1]. Enable ONLY if [1] misses and you still +; see a squeezed/zoomed image. Writes your true aspect ratio in its place. +[Patch:HardcodedAspect_16by9_a] +Enabled=false +Pattern="39 8E E3 3F" +Offset=0 +Value="aspectratio" +ValueType="float" +Match="all" +Module="auto" + +[Patch:HardcodedAspect_16by9_b] +Enabled=false +Pattern="3B 8E E3 3F" +Offset=0 +Value="aspectratio" +ValueType="float" +Match="all" +Module="auto" diff --git a/tools/aob.py b/tools/aob.py new file mode 100755 index 0000000..b313b9d --- /dev/null +++ b/tools/aob.py @@ -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() diff --git a/tools/build_release.sh b/tools/build_release.sh new file mode 100755 index 0000000..bc66485 --- /dev/null +++ b/tools/build_release.sh @@ -0,0 +1,21 @@ +#!/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) +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 + [ -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 diff --git a/tools/dump_decrypted.py b/tools/dump_decrypted.py new file mode 100755 index 0000000..dcdcccf --- /dev/null +++ b/tools/dump_decrypted.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +"""Dump the DECRYPTED GB4-Win64-Shipping.exe 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 +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 + # -> writes scratch dump + region metadata, then prints AOB match report. + +If you get "Operation not permitted", either run with sudo, or temporarily: + echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope (reset to 1 after) +""" +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") + +libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True) + + +class iovec(ctypes.Structure): + _fields_ = [("iov_base", ctypes.c_void_p), ("iov_len", ctypes.c_size_t)] + + +libc.process_vm_readv.restype = ctypes.c_ssize_t +libc.process_vm_readv.argtypes = [ctypes.c_int, ctypes.POINTER(iovec), ctypes.c_ulong, + ctypes.POINTER(iovec), ctypes.c_ulong, ctypes.c_ulong] + + +def find_pid(): + if len(sys.argv) > 1 and sys.argv[1].isdigit(): + return int(sys.argv[1]) + for p in glob.glob("/proc/[0-9]*"): + try: + with open(f"{p}/cmdline", "rb") as f: + cl = f.read().replace(b"\0", b" ").decode("utf-8", "replace") + if MODULE_HINT in cl: + return int(os.path.basename(p)) + except OSError: + continue + return None + + +def read_mem(pid, addr, size): + buf = ctypes.create_string_buffer(size) + local = iovec(ctypes.cast(buf, ctypes.c_void_p), size) + remote = iovec(ctypes.c_void_p(addr), size) + n = libc.process_vm_readv(pid, ctypes.byref(local), 1, ctypes.byref(remote), 1, 0) + if n < 0: + # fallback: /proc/pid/mem pread + try: + with open(f"/proc/{pid}/mem", "rb", 0) as m: + m.seek(addr) + return m.read(size) + except OSError as e: + raise OSError(f"read fail @0x{addr:x}: {os.strerror(ctypes.get_errno())} / {e}") + return buf.raw[:n] + + +def module_regions(pid): + """Return list of (start,end,perms,path) VMAs backed by the module exe.""" + regs = [] + modpath = None + with open(f"/proc/{pid}/maps") as f: + for line in f: + m = re.match(r"([0-9a-f]+)-([0-9a-f]+) (\S{4}) \S+ \S+ \S+ *(.*)", line) + if not m: + continue + start, end, perms, path = int(m[1], 16), int(m[2], 16), m[3], m[4] + if MODULE_HINT in path: + if modpath is None: + modpath = path + regs.append((start, end, perms, path)) + return modpath, regs + + +def main(): + pid = find_pid() + if not pid: + print(f"[!] No running process matching '{MODULE_HINT}'. Launch the game first.") + sys.exit(1) + print(f"[+] pid = {pid}") + modpath, regs = module_regions(pid) + if not regs: + print("[!] Module not mapped yet. Reach the main menu, then retry.") + sys.exit(1) + base = min(r[0] for r in regs) + print(f"[+] module base = 0x{base:x} ({modpath})") + os.makedirs(OUTDIR, exist_ok=True) + meta = {"pid": pid, "module": modpath, "base": base, "regions": []} + blob = bytearray() + for start, end, perms, path in regs: + size = end - start + try: + data = read_mem(pid, start, size) + except OSError as e: + print(f" skip 0x{start:x}-0x{end:x} {perms}: {e}") + continue + rva = start - base + # pad blob so file offset == rva (sparse-ish, capped) + if rva >= 0 and rva < 0x20000000: + if len(blob) < rva: + blob.extend(b"\x00" * (rva - len(blob))) + blob[rva:rva + len(data)] = data + 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") + with open(dump_path, "wb") as f: + f.write(blob) + with open(os.path.join(OUTDIR, "gb4_dump_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')}") + + # 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)") + + +if __name__ == "__main__": + main() diff --git a/tools/find_projfunc.py b/tools/find_projfunc.py new file mode 100755 index 0000000..01bca88 --- /dev/null +++ b/tools/find_projfunc.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +"""Locate FMinimalViewInfo::CalculateProjectionMatrixGivenView in GB4-Win64-Shipping.exe. + +Fingerprint: the function computes max(0.001f, FOV) * (PI/360) — so it +references both float constants 0.001f and pi/360 within a short span. +We find all RIP-relative references to those constants and cluster them. +""" +import pefile, struct, math, sys + +EXE = sys.argv[1] if len(sys.argv) > 1 else \ + "/media/ckoch/Data/steam/steamapps/common/GBBBB/GB4/Binaries/Win64/GB4-Win64-Shipping.exe" + +pe = pefile.PE(EXE, fast_load=True) +base = pe.OPTIONAL_HEADER.ImageBase + +sections = {} +for s in pe.sections: + name = s.Name.rstrip(b"\0").decode() + sections[name] = (s.VirtualAddress, s.SizeOfRawData, s.get_data()) + print(f"section {name:8s} va=0x{base+s.VirtualAddress:x} rawsize=0x{s.SizeOfRawData:x}") + +text_rva, _, text = sections[".text"] + +# --- find constant locations anywhere outside .text (rdata usually) --- +consts = { + "0.001f": struct.pack("/dev/null | head -1) +[ -z "${LOG:-}" ] && { echo "[!] No steam-1672500.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 +echo "=== SUWSF patch results ===" +grep -iE "Searching for patches|Found patch|patches found| matches|No pattern found|skipping patch|Patches disabled|INITIALIZED" "$LOG" | head -60 +echo "=== (if both sections empty, the ASI did not inject: check WINEDLLOVERRIDES) ===" diff --git a/vendor/SUWSF-x64/LICENSE b/vendor/SUWSF-x64/LICENSE new file mode 100644 index 0000000..995a847 --- /dev/null +++ b/vendor/SUWSF-x64/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 PhantomGamers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/SUWSF-x64/README.md b/vendor/SUWSF-x64/README.md new file mode 100644 index 0000000..580cd3f --- /dev/null +++ b/vendor/SUWSF-x64/README.md @@ -0,0 +1,113 @@ +# SUWSF + +Somewhat Universal Widescreen Fix is intended to enable widescreen aspect ratios (e.g. 21:9, 32:9, 48:9) in games where it is unsupported. + +## WARNING IT IS NOT RECOMMENDED TO USE THIS FIX IN GAMES THAT CONTAIN ANTICHEAT + +## Installation + +- **Ensure you have the latest [64bit VC Redist](https://aka.ms/vs/17/release/vc_redist.x64.exe) or [32bit VC Redist](https://aka.ms/vs/17/release/vc_redist.x86.exe) installed.** +- Download [the latest release](https://github.com/phantomgamers/suwsf/releases/latest) +- For 32bit (x86) games download SUWSF-x86.zip, for 64bit (x86_64) games download SUWSF-x64.zip. +- Extract it so that `SUWSF.asi`, `SUWSF.ini`, and `dsound.dll` are in the same folder as the game executable that you are trying to fix. +- If `dsound.dll` would collide with a file included in the game try a different name on [this list](https://github.com/ThirteenAG/Ultimate-ASI-Loader#description) which is marked as supporting the architecture (x86 or x64) of the game you are trying to fix. + +Note that it must be alongside the actual game executable, it will not work if it is only placed alongside a game's launcher. + +## Uninstall + +To uninstall, delete the `SUWSF.asi`, `SUWSF.ini`, and `dsound.dll` files that you extracted to the game executable location. + +## Configuration + +In `SUWSF.ini` you can configure the following settings: + +### **[UserSettings]** + +A group of settings specific to the user. + +#### **Resolution** + +The user's screen resolution that should be used in patch calculations. + +Valid options: A screen resolution (e.g. "3440x1440"), "auto" (automatically detect from primary screen, may be affected by DPI scaling) +Default: "auto" + +### **[Patch]** + +A group of settings related to a patch. Patches can optionally have names (e.g. `[Patch:AspectRatio]` or `[Patch:FOV]`). Names do not matter as long as they contain the word `Patch`. +As many patch groups as needed can be used. + +#### **Enabled** + +Whether a patch should be enabled. Use this for optional patches that users can choose to enable or disable. + +Valid options: "true", "false" +Default: "true" + +#### **Pattern** + +This should be set to a pattern that matches the bytes you would like to patch. + +Commonly this will be 16/9 in bytes, which can either be `39 8E E3 3F` or `3B 8E E3 3F` depending on the game. + +Wildcards are supported for bytes that can vary between game versions (e.g. `39 8E ?? 3F`). + +#### **Offset** + +The offset from the pattern match that should be written. Use this if you need to include bytes before the thing you are trying to patch. +e.g. to get to `39` in `90 90 39 8E E3 3F` set to 2 + +Default: 0 + +#### **Value** + +The value to write to the matched bytes. + +If `ValueType` is `float` then this can contain numbers or included variables. This can also contain an expression! +If `ValueType` is `byte` then this should contain an array of bytes! e.g. `90 90 90 90` + +Included variables: +`aspectratio` - This will be replaced with the user's screen's aspect ratio. +`width` - This will be replaced with the user's screen's width. +`height` - This will be replaced with the user's screen's height. + +Example: +`Value="width/height"` + +#### **ValueType** + +What type the Value setting contains. + +Valid options: "float", "int", "byte" +Default: `float` + +#### **Match** + +Which match should be targeted. Useful for cases where a pattern can have multiple matches but only one should be changed. + +Valid options: number of match (starting from 1), "last", "all" +**Note that `last` might match different bytes than the last match in a hex editor.** This is because SUWSF works from memory while a hex editor works on the file on disk. +Default: "all" + + +#### **Module** + +Which file should be targeted by patch. Useful for cases where the patch needs to be applied to a file other than the main game executable. (e.g. "UnityPlayer.dll") + +Valid options: "auto" (detects main game executable name), any string. +Default: "auto" + +## Troubleshooting + +If you get an error such as "Unable to load SUWSF.asi. Error:126", ensure you have the latest [64bit VC Redist](https://aka.ms/vs/17/release/vc_redist.x64.exe) or [32bit VC Redist](https://aka.ms/vs/17/release/vc_redist.x86.exe) installed depending on which version of SUWSF you are trying to use. + +If this does not work for you, try renaming `dsound.dll` to `dinput8.dll` or any other name on [this list](https://github.com/ThirteenAG/Ultimate-ASI-Loader#description) and see if the problem remains. + +`version.dll` is a good alternative to try. + +## Credits + +- [@ThirteenAG](https://github.com/ThirteenAG) for [Hooking.Patterns](https://github.com/ThirteenAG/Hooking.Patterns), [IniReader](https://github.com/ThirteenAG/IniReader) and [UltimateASILoader](https://github.com/ThirteenAG/Ultimate-ASI-Loader) +- [@sergey-shandar](https://github.com/sergey-shandar) for [getboost](https://github.com/sergey-shandar/getboost) +- [@codeplea](https://github.com/codeplea) for [tinyexpr](https://github.com/codeplea/tinyexpr) \ No newline at end of file diff --git a/vendor/SUWSF-x64/SUWSF.asi b/vendor/SUWSF-x64/SUWSF.asi new file mode 100644 index 0000000..e415cbe Binary files /dev/null and b/vendor/SUWSF-x64/SUWSF.asi differ diff --git a/vendor/SUWSF-x64/SUWSF.ini b/vendor/SUWSF-x64/SUWSF.ini new file mode 100644 index 0000000..173312e --- /dev/null +++ b/vendor/SUWSF-x64/SUWSF.ini @@ -0,0 +1,25 @@ +[UserSettings] +; Set to your screen resolution (e.g. "3440x1440") (default: "auto") +Resolution="auto" +; Globally enable or disable patches (default: true) +Enabled=true + +; WARNING: PATCHES BELOW. DO NOT MODIFY UNLESS YOU KNOW WHAT YOU ARE DOING. +; Normally users should not need to edit this + +; This is the default example patch but any number of these can be added and named anything as long as they contain the word "Patch" +[Patch:AspectRatio] +; Whether this patch should be enabled (default: true) +Enabled=true +; Byte pattern to search for. E.g. "39 8E E3 3F" (16/9 as float). Wildcards accepted (use ?? for bytes that can change) +Pattern="39 8E E3 3F" +; Offset in number of bytes to target. e.g. to get to "39" in "90 90 39 8E E3 3F" set to 2 (default: 0) +Offset=0 +; Value to write. Numbers or variables (aspectratio, width, height) are accepted. Bytes are accepted if ValueType="byte" +Value="aspectratio" +; Type of value. Accepted values are: "float", "double", "int", "byte" (default: "float") +ValueType="float" +; Which match to write to. Accepted values are: number of match (starting from 1), last, all. (default: "all") +Match="all" +; Which file to search for pattern in. "auto" detects process name automatically (default: "auto") +Module="auto" diff --git a/vendor/SUWSF-x64/dsound.dll b/vendor/SUWSF-x64/dsound.dll new file mode 100644 index 0000000..e6d882d Binary files /dev/null and b/vendor/SUWSF-x64/dsound.dll differ