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:
2026-07-20 20:21:23 -04:00
co-authored by Claude Opus 4.8
commit dc28d60392
17 changed files with 829 additions and 0 deletions
+15
View File
@@ -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
+26
View File
@@ -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.
+66
View File
@@ -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/<pid>/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).
+27
View File
@@ -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.
+29
View File
@@ -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.
Executable
+108
View File
@@ -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"
+71
View File
@@ -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"
Executable
+91
View File
@@ -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()
+21
View File
@@ -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
+127
View File
@@ -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()
+78
View File
@@ -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("<f", 0.001),
"pi/360": struct.pack("<f", math.pi / 360),
}
const_vas = {k: [] for k in consts}
for name, (rva, size, data) in sections.items():
if name == ".text":
continue
for key, pat in consts.items():
off = -1
while True:
off = data.find(pat, off + 1)
if off < 0:
break
va = base + rva + off
# only aligned-ish hits to cut noise
const_vas[key].append(va)
for k, v in const_vas.items():
print(f"{k}: {len(v)} occurrences in data sections")
# --- find rip-relative references in .text to any of those VAs ---
targets = {}
for k, vas in const_vas.items():
for va in vas:
targets.setdefault(va, k)
refs = [] # (text_off, va, key)
tbase = base + text_rva
n = len(text)
for i in range(n - 4):
disp = struct.unpack_from("<i", text, i)[0]
va = tbase + i + 4 + disp
k = targets.get(va)
if k:
refs.append((i, va, k))
print(f"total rip refs to candidate consts: {len(refs)}")
# cluster: find 0.001f refs with a pi/360 ref within 0x100 bytes
refs.sort()
by_key = {}
for off, va, k in refs:
by_key.setdefault(k, []).append(off)
import bisect
p360 = by_key.get("pi/360", [])
hits = []
for off in by_key.get("0.001f", []):
j = bisect.bisect_left(p360, off - 0x100)
while j < len(p360) and p360[j] < off + 0x100:
hits.append((off, p360[j]))
j += 1
print(f"\nclustered candidates (0.001f + pi/360 within 0x100):")
for a, b in hits:
print(f" 0.001f@text+0x{a:x} (va 0x{tbase+a:x}) pi/360@text+0x{b:x} (va 0x{tbase+b:x})")
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# Reads the newest Proton log for GB4 (appid 1672500) and extracts SUWSF + DLL-load evidence.
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; }
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) ==="
+21
View File
@@ -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.
+113
View File
@@ -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)
BIN
View File
Binary file not shown.
+25
View File
@@ -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"
BIN
View File
Binary file not shown.