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
+1
View File
@@ -10,6 +10,7 @@ __pycache__/
/vendor/SUWSF-x64.zip /vendor/SUWSF-x64.zip
# Runtime dumps / scratch # Runtime dumps / scratch
/dumps/
gb4dump/ gb4dump/
*.bin *.bin
*.bak-bg4uw *.bak-bg4uw
+2 -2
View File
@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2026 progkoch Copyright (c) 2026 programmingPug
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -22,5 +22,5 @@ SOFTWARE.
--------------------------------------------------------------------------- ---------------------------------------------------------------------------
This license covers the original work in this repository: the SUWSF patch This license covers the original work in this repository: the SUWSF patch
definitions (patch/SUWSF.ini), the installer (install.sh), and the tools/. definitions (games/*/SUWSF.ini), the installer (install.sh), and the tools/.
Bundled third-party components are under their own licenses; see THIRD_PARTY.md. Bundled third-party components are under their own licenses; see THIRD_PARTY.md.
+35 -48
View File
@@ -1,66 +1,53 @@
# Gundam Breaker 4 — UltraWide Fix # UltraWidePatches
UltraWide (32:9) fix for **Gundam Breaker 4** on PC, targeting the **Samsung 49" Odyssey G93SC** (5120×1440). Works on **Linux/Proton** and Windows. Ultrawide (**32:9 / 21:9**) fixes for PC games that ship with poor or no ultrawide support — built and tested on a **Samsung 49" Odyssey G93SC (5120×1440)**, and **Proton/Linux-first**.
## What it fixes Most of these force **Hor+ FOV** (a wider screen shows you *more*, instead of zooming the camera in) and disable pillarboxing, by patching the game in memory at runtime with [SUWSF](https://github.com/PhantomGamers/SUWSF) + [Ultimate ASI Loader](https://github.com/ThirteenAG/Ultimate-ASI-Loader).
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. ## Supported games
## How it works (and why it has to work this way) | Game | Engine | Status |
|---|---|---|
| [Gundam Breaker 4](games/GundamBreaker4/) | Unreal Engine 4.27 | ✅ Confirmed working in gameplay at 5120×1440 |
- 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. ## Install
- 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 ```bash
./install.sh ./install.sh # list available patches
./install.sh GundamBreaker4 # install one
./install.sh GundamBreaker4 uninstall
``` ```
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. The installer finds the game across your Steam libraries, drops the loader + patch next to the executable, and applies any config-level tweak (with a backup). **It never modifies game files.**
**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: > **Proton users:** each game needs a `WINEDLLOVERRIDES` launch option or the loader is silently ignored — the installer prints the exact line for that game. This is the single most common reason a fix "doesn't work."
``` Windows users can extract a release zip straight into the game's exe folder; no launch option needed.
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.) ## Why runtime patching
> ✅ Confirmed working in gameplay at 5120×1440 (32:9) on Proton — Hor+ FOV holds through missions. Config-file tweaks (e.g. UE4's `AspectRatioAxisConstraint=MaintainYFOV`) typically fix menus but **revert once gameplay loads**, because the game re-sets the camera per view. Many shipping executables are also DRM-encrypted on disk (GB4's is SteamStub-wrapped, `.text` entropy 8.0), which makes static exe patching impossible. Patching the *decrypted image in memory* solves both problems at once.
### Install (Windows) ## Repo layout
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 | | Path | What |
|---|---| |---|---|
| `patch/SUWSF.ini` | The ultrawide patch definitions (HOR+, pillarbox, fallbacks) | | `games/<Game>/SUWSF.ini` | That game's patch definitions |
| `install.sh` | Proton/Linux installer + uninstaller | | `games/<Game>/game.conf` | Game metadata (appid, exe, paths, launch option) |
| `tools/dump_decrypted.py` | Dumps the decrypted exe image from the running process | | `install.sh` | Generic installer / uninstaller |
| `tools/aob.py` | Verifies/derives AOB patterns against a dump | | `tools/build_release.sh` | Builds a per-game drop-in release zip |
| `tools/find_projfunc.py` | Static fingerprint scan (confirmed the DRM encryption) | | `tools/publish.sh` | Pushes + cuts a GitHub release |
| `dist/GB4_UltraWide_Fix.zip` | Drop-in bundle for manual/Windows install | | `tools/dump_decrypted.py` | Dumps a DRM-decrypted image from a running process |
| `vendor/SUWSF-x64/` | Upstream SUWSF 2.3.0 + Ultimate ASI Loader | | `tools/aob.py` | Verifies/derives byte patterns against a dump |
| `tools/read_protonlog.sh` | Confirms ASI injection + per-patch match counts |
| `vendor/SUWSF-x64/` | Upstream SUWSF 2.3.0 + Ultimate ASI Loader (MIT) |
## Credits ## Adding a game
[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).
1. `mkdir games/<GameName>` with a `game.conf` (copy GB4's as a template) and a `SUWSF.ini`.
2. Start from the known UE4 patterns in [`games/GundamBreaker4/SUWSF.ini`](games/GundamBreaker4/SUWSF.ini) — they match across many UE4 titles.
3. If a pattern misses, use `tools/dump_decrypted.py` + `tools/aob.py` to derive the game's exact bytes.
## License
MIT — see [LICENSE](LICENSE). Bundled third-party components (SUWSF, Ultimate ASI Loader) are MIT and credited in [THIRD_PARTY.md](THIRD_PARTY.md). You must own the games; nothing here modifies or circumvents copy protection.
+10 -10
View File
@@ -1,27 +1,27 @@
# Gundam Breaker 4 — UltraWide Fix v1.0.0 # 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). Part of [UltraWidePatches](https://github.com/programmingPug/UltraWidePatches).
Ultrawide **32:9 / 21:9** support for **Gundam Breaker 4** (PC). Forces **Hor+ FOV** — a wider screen shows you *more*, instead of zooming the camera in — and disables pillarboxing. Confirmed working in gameplay at **5120×1440** on Linux/Proton (Samsung 49" Odyssey G93SC).
## Install (Linux / Proton) ## 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. 1. Extract the zip into `…/GB4/Binaries/Win64/` (next to `GB4-Win64-Shipping.exe`), or run `./install.sh GundamBreaker4` from the repo.
2. **Required:** add to GB4's **Steam → Properties → Launch Options**: 2. **Required** Steam → Properties → Launch Options:
``` ```
WINEDLLOVERRIDES="dsound=n,b" %command% WINEDLLOVERRIDES="dsound=n,b" %command%
``` ```
Without this, Proton ignores the loader and the fix only works in the lobby. 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. 3. Set the in-game resolution to your native ultrawide res, fullscreen.
## Install (Windows) ## Install (Windows)
Extract `GB4_UltraWide_Fix.zip` into `…\GB4\Binaries\Win64\`. No launch option needed. Extract the zip into `…\GB4\Binaries\Win64\`. No launch option needed.
## How it works ## 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). 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 ## Notes
- Do not use the HOR+ patch at 16:9 or narrower. - Do not enable the HOR+ patch at 16:9 or narrower.
- You must own the game. This does not modify or circumvent any game copy protection. - You must own the game. Nothing here modifies or circumvents copy protection.
- Uninstall: delete `SUWSF.asi`, `SUWSF.ini`, `dsound.dll` from `Binaries/Win64` (or `./install.sh uninstall`). - Uninstall: `./install.sh GundamBreaker4 uninstall`, or delete `SUWSF.asi`, `SUWSF.ini`, `dsound.dll`.
**Checksum (GB4_UltraWide_Fix.zip):** `c1df7e963003f6b5c0583104908be0430e2b2ae1c5087c014ecf14c06b1cc7d3`
Credits: SUWSF by PhantomGamers · Ultimate ASI Loader by ThirteenAG · patterns adapted from Lyall's UltrawidePatches. Credits: SUWSF by PhantomGamers · Ultimate ASI Loader by ThirteenAG · patterns adapted from Lyall's UltrawidePatches.
+1 -1
View File
@@ -19,7 +19,7 @@ them; all runtime patching is performed by SUWSF.
## Patch patterns ## Patch patterns
The `AspectRatioAxisConstraint` (HOR+) and `ConstrainAspectRatio` (pillarbox) The `AspectRatioAxisConstraint` (HOR+) and `ConstrainAspectRatio` (pillarbox)
byte patterns in `patch/SUWSF.ini` are adapted from the UE4 fixes in byte patterns in `games/*/SUWSF.ini` are adapted from the UE4 fixes in
[Lyall's UltrawidePatches](https://codeberg.org/Lyall/UltrawidePatches) [Lyall's UltrawidePatches](https://codeberg.org/Lyall/UltrawidePatches)
(notably Trepang2 and Lies of P), which are provided for community use with SUWSF. (notably Trepang2 and Lies of P), which are provided for community use with SUWSF.
+64
View File
@@ -0,0 +1,64 @@
# Gundam Breaker 4 — UltraWide Fix
**Confirmed working in gameplay at 5120×1440 (32:9) on Proton.**
Forces **Hor+ FOV** so an ultrawide display shows *more of the battlefield* instead of zooming the camera in, and disables pillarboxing.
- **Engine:** Unreal Engine 4.27
- **Steam AppID:** 1672500
- **Executable:** `GB4-Win64-Shipping.exe` (SteamStub-encrypted → runtime patch required)
## Install (Linux / Proton)
```bash
./install.sh GundamBreaker4
```
Then **required** — Steam → right-click *Gundam Breaker 4* → Properties → Launch Options:
```
WINEDLLOVERRIDES="dsound=n,b" %command%
```
Without it, Proton ignores the native `dsound.dll` and the fix appears to work **only in the lobby** (that part is the `Engine.ini` tweak) while reverting in missions.
Finally, set the in-game resolution to **5120×1440**, fullscreen.
## Install (Windows)
Extract the release zip into `…\GB4\Binaries\Win64\` (next to `GB4-Win64-Shipping.exe`). No launch option needed.
## Uninstall
```bash
./install.sh GundamBreaker4 uninstall
```
## What it patches
| Patch | Effect |
|---|---|
| `AspectRatioAxisConstraint` | Forces `MaintainYFOV` (0) → Hor+ FOV. **Do not use at 16:9 or narrower.** |
| `ConstrainAspectRatio` | NOPs the pillarbox/letterbox clamp |
| `HardcodedAspect_16by9_*` | Optional fallbacks, disabled by default |
The patterns are UE4-generic (adapted from Trepang2 / Lies of P in [Lyall's UltrawidePatches](https://codeberg.org/Lyall/UltrawidePatches)) and matched GB4's 4.27 code as-is.
## Troubleshooting
Add `PROTON_LOG=1` to the launch options, relaunch, then:
```bash
./tools/read_protonlog.sh 1672500
```
That reports whether the ASI injected and how many matches each pattern found. If a pattern shows `No pattern found`, derive the exact bytes:
```bash
python3 tools/dump_decrypted.py GB4-Win64-Shipping.exe # while the game runs
python3 tools/aob.py <dump> games/GundamBreaker4/SUWSF.ini
```
## Known / not yet verified
- HUD/UI scaling and cutscene framing at 32:9 have not been formally checked.
+28
View File
@@ -0,0 +1,28 @@
# Game metadata consumed by ../../install.sh
# Everything game-specific lives here so the installer stays generic.
TITLE="Gundam Breaker 4"
APPID=1672500
# Executable to locate, and its path under steamapps/common/
EXE="GB4-Win64-Shipping.exe"
COMMON_SUBDIR="GBBBB/GB4/Binaries/Win64"
# Ultimate ASI Loader proxy name. On Proton this REQUIRES the matching
# WINEDLLOVERRIDES launch option (see LAUNCH_OPTION below).
LOADER="dsound.dll"
# Engine .ini inside the Proton prefix, relative to compatdata/<APPID>/pfx/
# Leave empty to skip the config-level tweak.
PREFIX_INI="drive_c/users/steamuser/AppData/Local/GB4/Saved/Config/WindowsNoEditor/Engine.ini"
# Block appended to PREFIX_INI. Fixes menus/lobby immediately; the runtime
# SUWSF patch is what carries it through gameplay.
read -r -d '' INI_BLOCK <<'EOF' || true
[/Script/Engine.LocalPlayer]
AspectRatioAxisConstraint=AspectRatio_MaintainYFOV
EOF
LAUNCH_OPTION='WINEDLLOVERRIDES="dsound=n,b" %command%'
ENGINE="Unreal Engine 4.27"
STATUS="Confirmed working in gameplay at 5120x1440 (32:9) on Proton"
+103 -65
View File
@@ -1,108 +1,146 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# ============================================================================ # ============================================================================
# Gundam Breaker 4 - UltraWide Fix installer (Linux / Proton) # UltraWidePatches - installer (Linux / Proton, also usable on Windows via
# Installs SUWSF + Ultimate ASI Loader + our GB4 SUWSF.ini next to the # manual extraction of the release zip)
# shipping exe. Idempotent. Use ./install.sh uninstall to remove. #
# Usage:
# ./install.sh # list available games
# ./install.sh GundamBreaker4 # install that game's patch
# ./install.sh GundamBreaker4 uninstall
#
# Overrides:
# GAME_DIR=/path/to/Binaries/Win64 ./install.sh <Game> # nonstandard install
# PREFIX_INI_FILE=/path/to/Engine.ini ./install.sh <Game> # nonstandard prefix
# LOADER_NAME=version.dll ./install.sh <Game> # loader collision
# ============================================================================ # ============================================================================
set -euo pipefail set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOADER_NAME="${LOADER_NAME:-dsound.dll}" # override: LOADER_NAME=version.dll ./install.sh GAMES_DIR="$HERE/games"
# --- locate GB4 shipping exe dir across Steam libraries ----------------------- list_games() {
echo "Available patches:"
for d in "$GAMES_DIR"/*/; do
[ -f "$d/game.conf" ] || continue
( TITLE=""; STATUS=""; . "$d/game.conf" >/dev/null 2>&1 || true
printf " %-22s %s\n" "$(basename "$d")" "${TITLE:-}${STATUS:-}" )
done
echo
echo "Usage: ./install.sh <GameName> [uninstall]"
}
GAME="${1:-}"
ACTION="${2:-install}"
if [ -z "$GAME" ]; then list_games; exit 0; fi
GAME_CONF="$GAMES_DIR/$GAME/game.conf"
if [ ! -f "$GAME_CONF" ]; then
echo "[!] Unknown game: $GAME"; echo; list_games; exit 1
fi
# shellcheck disable=SC1090
. "$GAME_CONF"
LOADER="${LOADER_NAME:-${LOADER:-dsound.dll}}"
echo "=== ${TITLE:-$GAME} ==="
# --- locate the game's exe directory across Steam libraries -------------------
find_gamedir() { find_gamedir() {
local cand local cand
for base in \ for base in \
"$HOME/.steam/steam" "$HOME/.local/share/Steam" \ "$HOME/.steam/steam" "$HOME/.local/share/Steam" \
"/media/$USER/Data/steam" "/media/$USER"/*/steam \ "/media/$USER"/*/steam "/media/$USER"/*/SteamLibrary \
"$HOME/.var/app/com.valvesoftware.Steam/.local/share/Steam"; do "$HOME/.var/app/com.valvesoftware.Steam/.local/share/Steam"; do
cand="$base/steamapps/common/GBBBB/GB4/Binaries/Win64" cand="$base/steamapps/common/$COMMON_SUBDIR"
[ -f "$cand/GB4-Win64-Shipping.exe" ] && { echo "$cand"; return 0; } [ -f "$cand/$EXE" ] && { echo "$cand"; return 0; }
done done
# brute-force fallback cand=$(find /home /media /mnt -maxdepth 8 -name "$EXE" 2>/dev/null | head -1)
cand=$(find /home /media /mnt -maxdepth 8 -name GB4-Win64-Shipping.exe 2>/dev/null | head -1)
[ -n "$cand" ] && { dirname "$cand"; return 0; } [ -n "$cand" ] && { dirname "$cand"; return 0; }
return 1 return 1
} }
GAMEDIR="${GB4_WIN64_DIR:-$(find_gamedir || true)}" GAMEDIR="${GAME_DIR:-$(find_gamedir || true)}"
if [ -z "${GAMEDIR:-}" ] || [ ! -f "$GAMEDIR/GB4-Win64-Shipping.exe" ]; then if [ -z "${GAMEDIR:-}" ] || [ ! -f "$GAMEDIR/$EXE" ]; then
echo "[!] Could not find GB4-Win64-Shipping.exe." echo "[!] Could not find $EXE."
echo " Set it manually: GB4_WIN64_DIR=/path/to/GB4/Binaries/Win64 ./install.sh" echo " Set it manually: GAME_DIR=/path/to/exe/dir ./install.sh $GAME"
exit 1 exit 1
fi fi
echo "[+] Game dir: $GAMEDIR" echo "[+] Game dir: $GAMEDIR"
ASI="$HERE/vendor/SUWSF-x64/SUWSF.asi" # --- locate the Proton prefix ini (optional, per-game) ------------------------
LOADER_SRC="$HERE/vendor/SUWSF-x64/dsound.dll" # Ultimate ASI Loader (adapts by filename) find_prefix_ini() {
INI="$HERE/patch/SUWSF.ini" [ -n "${PREFIX_INI_FILE:-}" ] && { [ -f "$PREFIX_INI_FILE" ] && echo "$PREFIX_INI_FILE" && return 0; return 1; }
for f in "$ASI" "$LOADER_SRC" "$INI"; do [ -z "${PREFIX_INI:-}" ] && return 1
[ -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 local cand
for base in \ for base in \
"/media/$USER/Data/steam" "$HOME/.local/share/Steam" \ "$HOME/.steam/steam" "$HOME/.local/share/Steam" \
"$HOME/.steam/steam" "/media/$USER"/*/steam; do "/media/$USER"/*/steam "/media/$USER"/*/SteamLibrary \
cand="$base/steamapps/compatdata/1672500/pfx/drive_c/users/steamuser/AppData/Local/GB4/Saved/Config/WindowsNoEditor/Engine.ini" "$HOME/.var/app/com.valvesoftware.Steam/.local/share/Steam"; do
cand="$base/steamapps/compatdata/$APPID/pfx/$PREFIX_INI"
[ -f "$cand" ] && { echo "$cand"; return 0; } [ -f "$cand" ] && { echo "$cand"; return 0; }
done done
return 1 return 1
} }
MARK_BEGIN="; >>> BG4 UltraWide Fix (MaintainYFOV) >>>" MARK_BEGIN="; >>> UltraWidePatches ($GAME) >>>"
MARK_END="; <<< BG4 UltraWide Fix <<<" MARK_END="; <<< UltraWidePatches ($GAME) <<<"
apply_engine_ini() { apply_ini() {
local ini; ini="$(find_engine_ini || true)" [ -z "${INI_BLOCK:-}" ] && return
[ -z "$ini" ] && { echo "[i] Engine.ini not found (launch the game once first); skipping menu-level win."; return; } local ini; ini="$(find_prefix_ini || true)"
if grep -qF "$MARK_BEGIN" "$ini"; then echo "[i] Engine.ini tweak already present."; return; fi [ -z "$ini" ] && { echo "[i] Prefix ini not found (launch the game once first); skipping config tweak."; return; }
cp -v "$ini" "$ini.bak-bg4uw" if grep -qF "$MARK_BEGIN" "$ini"; then echo "[i] Config tweak already present."; return; fi
{ echo ""; echo "$MARK_BEGIN"; echo "[/Script/Engine.LocalPlayer]"; cp "$ini" "$ini.bak-uwp"
echo "AspectRatioAxisConstraint=AspectRatio_MaintainYFOV"; echo "$MARK_END"; } >> "$ini" { echo ""; echo "$MARK_BEGIN"; echo "$INI_BLOCK"; echo "$MARK_END"; } >> "$ini"
echo "[+] Engine.ini: added MaintainYFOV (fixes menus/lobby immediately; SUWSF covers missions)." echo "[+] Config tweak applied (backup: $(basename "$ini").bak-uwp)"
} }
remove_engine_ini() { remove_ini() {
local ini; ini="$(find_engine_ini || true)" local ini; ini="$(find_prefix_ini || true)"
[ -z "$ini" ] && return [ -z "$ini" ] && return
if grep -qF "$MARK_BEGIN" "$ini"; then 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" local esc_b esc_e
echo "[+] Engine.ini: removed BG4 UltraWide block." esc_b=$(printf '%s' "$MARK_BEGIN" | sed 's/[][\/.*^$]/\\&/g')
esc_e=$(printf '%s' "$MARK_END" | sed 's/[][\/.*^$]/\\&/g')
sed -i "/$esc_b/,/$esc_e/d" "$ini"
echo "[+] Config tweak removed."
fi fi
} }
uninstall() { if [ "$ACTION" = "uninstall" ]; then
echo "[+] Uninstalling..." echo "[+] Uninstalling..."
for f in SUWSF.asi SUWSF.ini dsound.dll dinput8.dll version.dll winmm.dll; do for f in SUWSF.asi SUWSF.ini dsound.dll dinput8.dll version.dll winmm.dll; do
[ -f "$GAMEDIR/$f" ] && { rm -v "$GAMEDIR/$f"; } [ -f "$GAMEDIR/$f" ] && rm -v "$GAMEDIR/$f"
done done
remove_engine_ini remove_ini
echo "[+] Done. (game files untouched; nothing was ever modified in the exe)" echo "[+] Done. No game file was ever modified."
} exit 0
fi
if [ "${1:-}" = "uninstall" ]; then uninstall; exit 0; fi
# --- install ------------------------------------------------------------------ # --- install ------------------------------------------------------------------
ASI="$HERE/vendor/SUWSF-x64/SUWSF.asi"
LOADER_SRC="$HERE/vendor/SUWSF-x64/dsound.dll" # Ultimate ASI Loader (adapts by filename)
INI="$GAMES_DIR/$GAME/SUWSF.ini"
for f in "$ASI" "$LOADER_SRC" "$INI"; do
[ -f "$f" ] || { echo "[!] missing $f"; exit 1; }
done
cp -v "$ASI" "$GAMEDIR/SUWSF.asi" cp -v "$ASI" "$GAMEDIR/SUWSF.asi"
cp -v "$INI" "$GAMEDIR/SUWSF.ini" cp -v "$INI" "$GAMEDIR/SUWSF.ini"
cp -v "$LOADER_SRC" "$GAMEDIR/$LOADER_NAME" cp -v "$LOADER_SRC" "$GAMEDIR/$LOADER"
apply_engine_ini apply_ini
echo cat <<EOF
echo "[+] Installed: SUWSF.asi, SUWSF.ini, $LOADER_NAME"
echo [+] Installed: SUWSF.asi, SUWSF.ini, $LOADER
echo "NEXT (Steam):"
echo " 1. If the game does not apply the fix, add this to GB4's Steam" NEXT (Proton) — REQUIRED, or the loader is ignored and the fix won't apply in gameplay.
echo " Launch Options (right-click game > Properties > Launch Options):" Steam > right-click ${TITLE:-$GAME} > Properties > Launch Options:
echo " WINEDLLOVERRIDES=\"${LOADER_NAME%.dll}=n,b\" %command%" ${LAUNCH_OPTION:-WINEDLLOVERRIDES="${LOADER%.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." Then set the in-game resolution to your native ultrawide res, fullscreen.
echo
echo "If nothing changes, the runtime AOB may differ for GB4. Run:" Troubleshooting:
echo " python3 tools/dump_decrypted.py (while game is at main menu)" Different loader name: LOADER_NAME=version.dll ./install.sh $GAME
echo " python3 tools/aob.py (reports which patterns matched)" Diagnose injection: add PROTON_LOG=1 to launch options, then
echo ./tools/read_protonlog.sh $APPID
echo "To try a different loader name: LOADER_NAME=version.dll ./install.sh" Remove everything: ./install.sh $GAME uninstall
echo "To remove everything: ./install.sh uninstall" EOF
+7 -8
View File
@@ -1,21 +1,20 @@
#!/usr/bin/env python3 #!/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. matches each has in the dump produced by dump_decrypted.py.
- 1 clean match -> pattern is good, ship it. - 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. - many matches -> pattern too loose; tighten or set Match to a specific index.
Usage: Usage:
python3 tools/aob.py [dump.bin] [SUWSF.ini] python3 tools/aob.py <dump.bin> [games/<Game>/SUWSF.ini]
""" """
import re, sys, os import re, sys, os
DUMP = sys.argv[1] if len(sys.argv) > 1 else \ 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 \ 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): def parse_pattern(pat):
@@ -65,7 +64,7 @@ def load_patches(ini_path):
def main(): 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.") print(f"[!] dump not found: {DUMP}\n Run tools/dump_decrypted.py while the game is running.")
sys.exit(1) sys.exit(1)
data = open(DUMP, "rb").read() data = open(DUMP, "rb").read()
+19 -13
View File
@@ -1,21 +1,27 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Build the drop-in release zip from committed sources + vendored SUWSF. # Build a drop-in release zip for one game.
# Output: dist/GB4_UltraWide_Fix.zip (attach to the GitHub Release) # ./tools/build_release.sh GundamBreaker4
# Output: dist/<Game>_UltraWide_Fix.zip (attach to the GitHub Release)
set -euo pipefail set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$HERE" cd "$HERE"
V="${1:-vendor/SUWSF-x64}" GAME="${1:?usage: build_release.sh <GameName>}"
for f in "$V/SUWSF.asi" "$V/dsound.dll" "$V/LICENSE" patch/SUWSF.ini; do 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; } [ -f "$f" ] || { echo "[!] missing $f"; exit 1; }
done done
rm -rf dist/GB4_UltraWide_Fix dist/GB4_UltraWide_Fix.zip OUT="dist/${GAME}_UltraWide_Fix"
mkdir -p dist/GB4_UltraWide_Fix rm -rf "$OUT" "$OUT.zip"
cp "$V/SUWSF.asi" "$V/dsound.dll" patch/SUWSF.ini dist/GB4_UltraWide_Fix/ mkdir -p "$OUT"
cp "$V/LICENSE" dist/GB4_UltraWide_Fix/SUWSF-LICENSE.txt cp "$V/SUWSF.asi" "$V/dsound.dll" "$GDIR/SUWSF.ini" "$OUT/"
cp README.md THIRD_PARTY.md dist/GB4_UltraWide_Fix/ 2>/dev/null || true cp "$V/LICENSE" "$OUT/SUWSF-LICENSE.txt"
( cd dist && zip -r -q GB4_UltraWide_Fix.zip GB4_UltraWide_Fix ) cp "$GDIR/README.md" "$OUT/README.md" 2>/dev/null || true
echo "[+] Built dist/GB4_UltraWide_Fix.zip" cp THIRD_PARTY.md "$OUT/" 2>/dev/null || true
( cd dist && sha256sum GB4_UltraWide_Fix.zip ) ( cd dist && zip -r -q "${GAME}_UltraWide_Fix.zip" "${GAME}_UltraWide_Fix" )
unzip -l dist/GB4_UltraWide_Fix.zip 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 #!/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 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 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: Usage:
# 1. Launch Gundam Breaker 4 (get to the main menu / lobby). # 1. Launch Gundam Breaker 4 (get to the main menu / lobby).
# 2. Run: # 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. # -> writes scratch dump + region metadata, then prints AOB match report.
If you get "Operation not permitted", either run with sudo, or temporarily: 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 import ctypes, ctypes.util, os, re, sys, json, struct, glob
MODULE_HINT = "GB4-Win64-Shipping.exe" MODULE_HINT = next((a for a in sys.argv[1:] if a.lower().endswith(".exe")), "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") 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) 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, meta["regions"].append({"start": start, "end": end, "rva": rva,
"perms": perms, "size": size, "got": len(data)}) "perms": perms, "size": size, "got": len(data)})
print(f" dumped 0x{start:x}-0x{end:x} {perms} rva=0x{rva:x} ({len(data)} bytes)") 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: with open(dump_path, "wb") as f:
f.write(blob) 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) json.dump(meta, f, indent=2)
print(f"[+] wrote {dump_path} ({len(blob)} bytes, rva-aligned)") 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 # 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: if b"++UE4+Release-4.27" in blob or b"CalculateProjectionMatrix" in blob:
print("[+] decrypted UE4 code confirmed in dump.") 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__": if __name__ == "__main__":
+10 -8
View File
@@ -7,11 +7,12 @@
# #
# Usage: # Usage:
# ./tools/publish.sh [REPO_NAME] [--private] # ./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 set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"; cd "$HERE" HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"; cd "$HERE"
REPO="${1:-GB4-UltraWide-Fix}" REPO="${1:-UltraWidePatches}"
VIS="--public"; [ "${2:-}" = "--private" ] && VIS="--private" VIS="--public"; [ "${2:-}" = "--private" ] && VIS="--private"
TAG="v1.0.0" TAG="v1.0.0"
@@ -21,8 +22,9 @@ OWNER="$(gh api user -q .login)"
echo "[+] Publishing as: $OWNER repo: $REPO $VIS" echo "[+] Publishing as: $OWNER repo: $REPO $VIS"
# 1. build the release artifact fresh # 1. build the release artifact fresh
bash tools/build_release.sh >/dev/null GAME="${GAME:-GundamBreaker4}"
echo "[+] Built dist/GB4_UltraWide_Fix.zip" 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 # 2. create the repo (skip if it already exists) and set as origin
if gh repo view "$OWNER/$REPO" >/dev/null 2>&1; then 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" git remote get-url origin >/dev/null 2>&1 || git remote add origin "https://github.com/$OWNER/$REPO.git"
else else
gh repo create "$OWNER/$REPO" $VIS --source=. --remote=origin \ 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 fi
# 3. push branch + tag # 3. push branch + tag
@@ -39,10 +41,10 @@ git push origin "$TAG"
# 4. create the release with notes + zip asset (idempotent) # 4. create the release with notes + zip asset (idempotent)
if gh release view "$TAG" -R "$OWNER/$REPO" >/dev/null 2>&1; then 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 else
gh release create "$TAG" dist/GB4_UltraWide_Fix.zip \ gh release create "$TAG" "dist/${GAME}_UltraWide_Fix.zip" \
-R "$OWNER/$REPO" -t "GB4 UltraWide Fix $TAG" -F RELEASE_NOTES.md -R "$OWNER/$REPO" -t "$TAG" -F RELEASE_NOTES.md
fi fi
echo echo
+5 -3
View File
@@ -1,8 +1,10 @@
#!/usr/bin/env bash #!/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 set -uo pipefail
LOG=$(ls -t "$HOME"/steam-1672500.log "$HOME"/.steam/steam/logs/steam-1672500.log 2>/dev/null | head -1) APPID="${1:?usage: read_protonlog.sh <APPID>}"
[ -z "${LOG:-}" ] && { echo "[!] No steam-1672500.log found. Add PROTON_LOG=1 to launch options and relaunch."; exit 1; } 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 "[+] Log: $LOG ($(stat -c%s "$LOG") bytes, modified $(stat -c%y "$LOG"))"
echo "=== Did our native dsound.dll load? ===" echo "=== Did our native dsound.dll load? ==="
grep -iE "dsound\.dll|SUWSF\.asi|Ultimate" "$LOG" | grep -iE "load|builtin|native|override" | head grep -iE "dsound\.dll|SUWSF\.asi|Ultimate" "$LOG" | grep -iE "load|builtin|native|override" | head