Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
efaab8bd32 | ||
|
|
b97a8a73b1 | ||
|
|
59ee13de9d | ||
|
|
ed2e0016ba | ||
|
|
40d057ac6e | ||
|
|
cbcc53c2e9 | ||
|
|
b9e68fd284 | ||
|
|
ae994c27e6 | ||
|
|
4cd18fb9f4 | ||
|
|
bd9fdc50f6 | ||
|
|
3525c79dba | ||
|
|
8bbc1cfac8 | ||
|
|
e21c6c3c44 | ||
|
|
b6ab1bf5ab | ||
|
|
9bdb4284d6 | ||
|
|
5d6fd277c9 | ||
|
|
b37677cbc1 |
@@ -10,6 +10,11 @@ __pycache__/
|
|||||||
/vendor/SUWSF-x64.zip
|
/vendor/SUWSF-x64.zip
|
||||||
|
|
||||||
# Runtime dumps / scratch
|
# Runtime dumps / scratch
|
||||||
|
/dumps/
|
||||||
gb4dump/
|
gb4dump/
|
||||||
*.bin
|
*.bin
|
||||||
*.bak-bg4uw
|
*.bak-bg4uw
|
||||||
|
|
||||||
|
# .NET plugin build output
|
||||||
|
**/plugin/bin/
|
||||||
|
**/plugin/obj/
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -1,66 +1,63 @@
|
|||||||
# 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
|
Fixes use whichever approach the engine calls for:
|
||||||
|
|
||||||
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.
|
- **SUWSF + Ultimate ASI Loader** — runtime memory patching, for Unreal titles (and anything DRM-wrapped, where static patching is impossible).
|
||||||
|
- **BepInEx plugin** — a proper C# mod, for Unity titles with the Mono backend.
|
||||||
|
|
||||||
## How it works (and why it has to work this way)
|
Most force **Hor+ FOV** (a wider screen shows you *more* rather than zooming in) and fix UI that assumes 16:9. Some games already render correctly and only need UI work — [Burglin' Gnomes](games/BurglinGnomes/) is one, and the fix deliberately leaves its camera alone.
|
||||||
|
|
||||||
- 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.
|
## Supported games
|
||||||
- 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)
|
| Game | Engine | Method | Status |
|
||||||
|
|---|---|---|---|
|
||||||
|
| [Gundam Breaker 4](games/GundamBreaker4/) | Unreal Engine 4.27 | SUWSF | ✅ Confirmed working at 5120×1440 |
|
||||||
|
| [Burglin' Gnomes](games/BurglinGnomes/) | Unity 6 (Mono) | BepInEx plugin | ✅ Confirmed working at 5120×1440 |
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
```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` | Patch definitions (SUWSF games) |
|
||||||
| `install.sh` | Proton/Linux installer + uninstaller |
|
| `games/<Game>/plugin/` | C# plugin source + prebuilt DLL (BepInEx games) |
|
||||||
| `tools/dump_decrypted.py` | Dumps the decrypted exe image from the running process |
|
| `games/<Game>/game.conf` | Game metadata (appid, exe, paths, launch option) |
|
||||||
| `tools/aob.py` | Verifies/derives AOB patterns against a dump |
|
| `install.sh` | Generic installer / uninstaller |
|
||||||
| `tools/find_projfunc.py` | Static fingerprint scan (confirmed the DRM encryption) |
|
| `tools/build_release.sh` | Builds a per-game drop-in release zip |
|
||||||
| `dist/GB4_UltraWide_Fix.zip` | Drop-in bundle for manual/Windows install |
|
| `tools/publish.sh` | Pushes + cuts a Gitea release |
|
||||||
| `vendor/SUWSF-x64/` | Upstream SUWSF 2.3.0 + Ultimate ASI Loader |
|
| `tools/dump_decrypted.py` | Dumps a DRM-decrypted image from a running process |
|
||||||
|
| `tools/aob.py` | Verifies/derives byte patterns against a dump |
|
||||||
|
| `tools/read_protonlog.sh` | Confirms ASI injection + per-patch match counts |
|
||||||
|
| `tools/unity_diag.cs`, `tools/unity_uitune.cs` | UnityExplorer console diagnostics/tuners |
|
||||||
|
| `vendor/SUWSF-x64/` | Upstream SUWSF 2.3.0 + Ultimate ASI Loader (MIT) |
|
||||||
|
| `vendor/BepInEx_win_x64_*.zip` | Upstream BepInEx 5.4.23.5 (LGPL-2.1) |
|
||||||
|
|
||||||
## 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. Set `METHOD=` in `game.conf` to `suwsf` or `bepinex`.
|
||||||
|
3. **Unreal:** start from the UE4 patterns in [`games/GundamBreaker4/SUWSF.ini`](games/GundamBreaker4/SUWSF.ini) — they match across many UE4 titles. If a pattern misses, use `tools/dump_decrypted.py` + `tools/aob.py` to derive the exact bytes.
|
||||||
|
4. **Unity:** install BepInEx + UnityExplorer and inspect live with [`tools/unity_diag.cs`](tools/unity_diag.cs) / [`tools/unity_uitune.cs`](tools/unity_uitune.cs), then write a plugin (copy `games/BurglinGnomes/plugin/`).
|
||||||
|
|
||||||
|
## 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
@@ -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://git.lazypugs.com/ckoch/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.
|
||||||
|
|||||||
+16
-1
@@ -17,9 +17,24 @@ them; all runtime patching is performed by SUWSF.
|
|||||||
- License: MIT
|
- License: MIT
|
||||||
- Files: `vendor/SUWSF-x64/dsound.dll` (renamed proxy; adapts by filename)
|
- Files: `vendor/SUWSF-x64/dsound.dll` (renamed proxy; adapts by filename)
|
||||||
|
|
||||||
|
## BepInEx
|
||||||
|
- Author: BepInEx contributors
|
||||||
|
- Source: https://github.com/BepInEx/BepInEx
|
||||||
|
- License: LGPL-2.1
|
||||||
|
- Files: `vendor/BepInEx_win_x64_5.4.23.5.zip` (redistributed unmodified)
|
||||||
|
- Used by Unity games (e.g. Burglin' Gnomes) to load the C# fix plugin.
|
||||||
|
|
||||||
|
## UnityExplorer (development only)
|
||||||
|
- Author: sinai-dev
|
||||||
|
- Source: https://github.com/sinai-dev/UnityExplorer
|
||||||
|
- License: GPL-3.0
|
||||||
|
- Files: `vendor/UnityExplorer.BepInEx5.Mono.zip`
|
||||||
|
- Used only for runtime inspection while developing a fix; it is NOT part of any
|
||||||
|
release package and is not required by end users.
|
||||||
|
|
||||||
## 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.
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,98 @@
|
|||||||
|
# Burglin' Gnomes — UltraWide Fix
|
||||||
|
|
||||||
|
✅ **Confirmed working at 5120×1440 (32:9) on Proton.**
|
||||||
|
|
||||||
|
- **Engine:** Unity 6 (6000.0.62f1), Mono backend, URP + Cinemachine
|
||||||
|
- **Steam AppID:** 3844970
|
||||||
|
- **Executable:** `Gnomium.exe`
|
||||||
|
- **Method:** BepInEx plugin (no DRM, no memory patching needed)
|
||||||
|
|
||||||
|
## What it fixes — and what it deliberately doesn't
|
||||||
|
|
||||||
|
**The camera is already correct.** It's *orthographic* with a constant `orthographicSize` of 5.0, so a wider display genuinely shows more world (visible width 17.8 → 35.6 going 16:9 → 32:9). That's textbook Hor+, and this fix does not touch it. `fieldOfView` reads 60 at every resolution simply because orthographic cameras don't use it — a red herring if you go looking.
|
||||||
|
|
||||||
|
**The problem is entirely UI**, in two separate places needing two different levers:
|
||||||
|
|
||||||
|
| Canvas | Problem | Lever |
|
||||||
|
|---|---|---|
|
||||||
|
| HUD (`ScreenSpaceOverlay` + `ScaleWithScreenSize`) | Ships `matchWidthOrHeight = 0.453` (width-biased). At 5120px that yields scaleFactor **1.948** — a ballooned HUD with overlapping bottom/centre text. | `match = 1` (scale by height) + raise `referenceResolution.y`, shrinking the HUD *and* giving the layout more logical room |
|
||||||
|
| In-world labels (`WorldSpace`, e.g. `NamePlate`) | At 32:9 you see twice as much world, so far more labels are on screen and can collide | Canvas `transform.localScale` |
|
||||||
|
|
||||||
|
> **Gotcha:** `CanvasScaler.scaleFactor` does nothing on a WorldSpace canvas — `CanvasScaler.HandleWorldCanvas()` overwrites it with `dynamicPixelsPerUnit` every frame. Scaling the transform is the only thing that works.
|
||||||
|
|
||||||
|
## Install (Linux / Proton)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./install.sh BurglinGnomes
|
||||||
|
```
|
||||||
|
|
||||||
|
Then **required** — Steam → Properties → Launch Options:
|
||||||
|
|
||||||
|
```
|
||||||
|
WINEDLLOVERRIDES="winhttp=n,b" %command%
|
||||||
|
```
|
||||||
|
|
||||||
|
Without it Proton ignores BepInEx's `winhttp.dll` loader and the plugin never runs.
|
||||||
|
|
||||||
|
## Install (Windows)
|
||||||
|
|
||||||
|
Extract the release zip into the game folder (next to `Gnomium.exe`). No launch option needed.
|
||||||
|
|
||||||
|
## Tuning
|
||||||
|
|
||||||
|
Config is generated on first launch:
|
||||||
|
|
||||||
|
```
|
||||||
|
<game>/BepInEx/config/com.programmingpug.burglingnomes.ultrawidefix.cfg
|
||||||
|
```
|
||||||
|
|
||||||
|
**Edits apply live (~2s) — no restart.** BepInEx doesn't reload config on its own, so the plugin watches the file's mtime and calls `Config.Reload()` itself.
|
||||||
|
|
||||||
|
| Setting | Default | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `HudScale` | `0.8` | HUD size. Lower = smaller HUD *and* more layout room (cures overlapping text) |
|
||||||
|
| `WorldLabelScale` | `0.8` | In-world nameplate size |
|
||||||
|
| `MatchWidthOrHeight` | `1.0` | **Leave at 1.** Lower re-introduces width scaling and cramps the layout vertically |
|
||||||
|
| `DesignHeight` | `1080` | The game's design height; don't change |
|
||||||
|
| `Verbose` | `false` | Log every canvas adjustment |
|
||||||
|
|
||||||
|
`HudScale` → resulting HUD scale at 5120×1440 (stock is 1.333):
|
||||||
|
|
||||||
|
| HudScale | 1.0 | 0.85 | **0.8** | 0.6 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| scale | 1.333 | 1.133 | **1.067** | 0.800 |
|
||||||
|
|
||||||
|
## Verifying it works
|
||||||
|
|
||||||
|
```
|
||||||
|
<game>/BepInEx/LogOutput.log
|
||||||
|
```
|
||||||
|
|
||||||
|
That's the plugin's log — **not** UnityExplorer's in-game Log window, which only shows Unity `Debug.Log`. You should see:
|
||||||
|
|
||||||
|
```
|
||||||
|
Burglin' Gnomes UltraWide Fix 1.0.0 loaded.
|
||||||
|
HUD 'Canvas' ref (1920.00, 1080.00) -> (1920.00, 1350.00) match=1 => scale=1.067
|
||||||
|
World 'NamePlate' localScale (1.00, 1.00, 1.00) -> (0.80, 0.80, 0.80)
|
||||||
|
```
|
||||||
|
|
||||||
|
If a line is absent, that adjustment didn't happen.
|
||||||
|
|
||||||
|
## Uninstall
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./install.sh BurglinGnomes uninstall
|
||||||
|
```
|
||||||
|
|
||||||
|
Removes the loader and plugin. `BepInEx/` is left in place in case other mods use it.
|
||||||
|
|
||||||
|
## Building the plugin from source
|
||||||
|
|
||||||
|
Prebuilt `BurglinGnomes.UltraWideFix.dll` ships here, so this is only needed if you change it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd plugin
|
||||||
|
dotnet build -c Release -p:GameDir="/path/to/Burglin' Gnomes"
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires the .NET SDK; it references the game's own assemblies and redistributes none of them.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Burglin' Gnomes — metadata for ../../install.sh
|
||||||
|
# NOTE: this game uses a DIFFERENT fix method than the SUWSF/UE4 games.
|
||||||
|
TITLE="Burglin' Gnomes"
|
||||||
|
APPID=3844970
|
||||||
|
EXE="Gnomium.exe"
|
||||||
|
COMMON_SUBDIR="Burglin' Gnomes"
|
||||||
|
ENGINE="Unity 6 (6000.0.62f1), Mono backend, URP + Cinemachine"
|
||||||
|
|
||||||
|
# Fix method: BepInEx plugin (not SUWSF — no DRM, managed C# assemblies)
|
||||||
|
METHOD="bepinex"
|
||||||
|
LOADER="winhttp.dll" # BepInEx's Doorstop proxy
|
||||||
|
PLUGIN_DLL="BurglinGnomes.UltraWideFix.dll"
|
||||||
|
LAUNCH_OPTION='WINEDLLOVERRIDES="winhttp=n,b" %command%'
|
||||||
|
|
||||||
|
PREFIX_INI="" # no config-level tweak identified yet
|
||||||
|
STATUS="Confirmed working at 5120x1440 (32:9) — HUD + nameplate scaling"
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
using BepInEx;
|
||||||
|
using BepInEx.Configuration;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.UI;
|
||||||
|
using UnityEngine.SceneManagement;
|
||||||
|
|
||||||
|
namespace BurglinGnomes.UltraWideFix
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Ultrawide UI fix for Burglin' Gnomes (Unity 6, Mono).
|
||||||
|
///
|
||||||
|
/// The camera needs no patching: it is orthographic with a constant
|
||||||
|
/// orthographicSize (5.0), so a wider display already shows proportionally
|
||||||
|
/// more world (visible width 17.8 -> 35.6 going 16:9 -> 32:9). That is
|
||||||
|
/// correct Hor+ behaviour.
|
||||||
|
///
|
||||||
|
/// The actual problem is UI scaling:
|
||||||
|
/// * HUD canvases (ScreenSpaceOverlay + ScaleWithScreenSize) ship
|
||||||
|
/// matchWidthOrHeight = 0.453, which is width-biased. At 5120px wide that
|
||||||
|
/// yields scaleFactor 1.948 and a ballooned HUD. Matching HEIGHT (1.0)
|
||||||
|
/// gives 1.333, and raising referenceResolution.y shrinks it further
|
||||||
|
/// while giving the layout MORE logical room (cures overlapping text).
|
||||||
|
/// * World-space label canvases (NamePlate, ...) are NOT affected by
|
||||||
|
/// CanvasScaler.scaleFactor - CanvasScaler.HandleWorldCanvas() overwrites
|
||||||
|
/// it with dynamicPixelsPerUnit every frame. Their size comes from the
|
||||||
|
/// canvas transform's localScale, so that is what we scale.
|
||||||
|
/// </summary>
|
||||||
|
[BepInPlugin(PluginGuid, PluginName, PluginVersion)]
|
||||||
|
public class UltraWideFixPlugin : BaseUnityPlugin
|
||||||
|
{
|
||||||
|
public const string PluginGuid = "com.programmingpug.burglingnomes.ultrawidefix";
|
||||||
|
public const string PluginName = "Burglin' Gnomes UltraWide Fix";
|
||||||
|
public const string PluginVersion = "1.0.0";
|
||||||
|
|
||||||
|
private ConfigEntry<bool> _enabled;
|
||||||
|
private ConfigEntry<float> _match;
|
||||||
|
private ConfigEntry<float> _hudScale;
|
||||||
|
private ConfigEntry<float> _worldScale;
|
||||||
|
private ConfigEntry<float> _designHeight;
|
||||||
|
private ConfigEntry<bool> _verbose;
|
||||||
|
|
||||||
|
// Baselines captured once per canvas so repeated applies never compound.
|
||||||
|
private readonly System.Collections.Generic.Dictionary<int, Vector2> _refBaseline
|
||||||
|
= new System.Collections.Generic.Dictionary<int, Vector2>();
|
||||||
|
private readonly System.Collections.Generic.Dictionary<int, Vector3> _scaleBaseline
|
||||||
|
= new System.Collections.Generic.Dictionary<int, Vector3>();
|
||||||
|
|
||||||
|
private float _timer;
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
_enabled = Config.Bind("General", "Enabled", true,
|
||||||
|
"Master switch for the ultrawide UI fix.");
|
||||||
|
_match = Config.Bind("HUD", "MatchWidthOrHeight", 1.0f,
|
||||||
|
new ConfigDescription(
|
||||||
|
"CanvasScaler match for HUD canvases. 0 = scale by width, 1 = by height. " +
|
||||||
|
"Keep at 1 for ultrawide; lower values re-introduce width scaling and " +
|
||||||
|
"cramp the layout vertically.",
|
||||||
|
new AcceptableValueRange<float>(0f, 1f)));
|
||||||
|
_hudScale = Config.Bind("HUD", "HudScale", 0.8f,
|
||||||
|
new ConfigDescription(
|
||||||
|
"HUD size multiplier. Below 1 shrinks the HUD AND gives the layout more " +
|
||||||
|
"logical room, which is what cures overlapping text. 1 = stock size.",
|
||||||
|
new AcceptableValueRange<float>(0.4f, 1.5f)));
|
||||||
|
_designHeight = Config.Bind("HUD", "DesignHeight", 1080f,
|
||||||
|
"The game's design reference height. Do not change unless you know why.");
|
||||||
|
_worldScale = Config.Bind("World", "WorldLabelScale", 0.8f,
|
||||||
|
new ConfigDescription(
|
||||||
|
"Size multiplier for in-world labels (nameplates). At 32:9 you see twice " +
|
||||||
|
"as much world, so more labels are on screen and can collide; lower this " +
|
||||||
|
"to shrink them. 1 = stock size.",
|
||||||
|
new AcceptableValueRange<float>(0.3f, 1.5f)));
|
||||||
|
_verbose = Config.Bind("Debug", "Verbose", false,
|
||||||
|
"Log every canvas adjustment.");
|
||||||
|
|
||||||
|
SceneManager.sceneLoaded += (_, __) => Apply();
|
||||||
|
Logger.LogInfo($"{PluginName} {PluginVersion} loaded.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Start() => Apply();
|
||||||
|
|
||||||
|
// Canvases are created lazily (menus, popups), so re-apply periodically.
|
||||||
|
// We also re-read the config file each tick: BepInEx does NOT pick up
|
||||||
|
// external edits on its own, so without this you'd have to restart the
|
||||||
|
// game after every tweak.
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
_timer += Time.unscaledDeltaTime;
|
||||||
|
if (_timer < 2f) return;
|
||||||
|
_timer = 0f;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var stamp = System.IO.File.GetLastWriteTimeUtc(Config.ConfigFilePath);
|
||||||
|
if (stamp != _configStamp)
|
||||||
|
{
|
||||||
|
_configStamp = stamp;
|
||||||
|
Config.Reload();
|
||||||
|
Logger.LogInfo($"Config reloaded: HudScale={_hudScale.Value} " +
|
||||||
|
$"WorldLabelScale={_worldScale.Value} " +
|
||||||
|
$"Match={_match.Value} Enabled={_enabled.Value}");
|
||||||
|
// values changed -> re-derive from baselines on this pass
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (System.Exception e)
|
||||||
|
{
|
||||||
|
Logger.LogWarning($"Config reload failed (check the .cfg syntax): {e.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
private System.DateTime _configStamp;
|
||||||
|
|
||||||
|
private void Apply()
|
||||||
|
{
|
||||||
|
if (!_enabled.Value) return;
|
||||||
|
|
||||||
|
foreach (var cs in FindObjectsOfType<CanvasScaler>())
|
||||||
|
{
|
||||||
|
var canvas = cs.GetComponent<Canvas>();
|
||||||
|
if (canvas == null) continue;
|
||||||
|
int id = cs.GetInstanceID();
|
||||||
|
|
||||||
|
if (canvas.renderMode == RenderMode.WorldSpace)
|
||||||
|
{
|
||||||
|
if (Mathf.Approximately(_worldScale.Value, 1f)) continue;
|
||||||
|
|
||||||
|
var cur = cs.transform.localScale;
|
||||||
|
// Games hide world-space UI by zeroing its scale. Never cache a
|
||||||
|
// zero baseline: if we did, multiplying it would pin the canvas
|
||||||
|
// to zero forever and the UI could never reappear.
|
||||||
|
if (!_scaleBaseline.TryGetValue(id, out var base3))
|
||||||
|
{
|
||||||
|
if (cur.x <= 0.0001f) continue; // hidden right now; revisit later
|
||||||
|
base3 = cur;
|
||||||
|
_scaleBaseline[id] = base3;
|
||||||
|
}
|
||||||
|
// Respect the game hiding it: leave zeroed canvases alone.
|
||||||
|
if (cur.x <= 0.0001f) continue;
|
||||||
|
|
||||||
|
var want = base3 * _worldScale.Value;
|
||||||
|
if (cs.transform.localScale != want)
|
||||||
|
{
|
||||||
|
cs.transform.localScale = want;
|
||||||
|
if (_verbose.Value)
|
||||||
|
Logger.LogInfo($"World '{cs.name}' localScale {base3} -> {want}");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cs.uiScaleMode != CanvasScaler.ScaleMode.ScaleWithScreenSize) continue;
|
||||||
|
|
||||||
|
if (!_refBaseline.TryGetValue(id, out var baseRef))
|
||||||
|
{
|
||||||
|
baseRef = cs.referenceResolution;
|
||||||
|
_refBaseline[id] = baseRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
float targetH = _designHeight.Value / Mathf.Max(0.01f, _hudScale.Value);
|
||||||
|
var targetRef = new Vector2(baseRef.x, targetH);
|
||||||
|
|
||||||
|
if (!Mathf.Approximately(cs.matchWidthOrHeight, _match.Value))
|
||||||
|
cs.matchWidthOrHeight = _match.Value;
|
||||||
|
if (cs.referenceResolution != targetRef)
|
||||||
|
{
|
||||||
|
cs.referenceResolution = targetRef;
|
||||||
|
// expected scale now; Canvas.scaleFactor lags a frame
|
||||||
|
float exp = Mathf.Pow(Screen.width / targetRef.x, 1f - _match.Value) *
|
||||||
|
Mathf.Pow(Screen.height / targetRef.y, _match.Value);
|
||||||
|
Logger.LogInfo($"HUD '{cs.name}' ref {baseRef} -> {targetRef} " +
|
||||||
|
$"match={_match.Value} => scale={exp:F3}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.1</TargetFramework>
|
||||||
|
<AssemblyName>BurglinGnomes.UltraWideFix</AssemblyName>
|
||||||
|
<RootNamespace>BurglinGnomes.UltraWideFix</RootNamespace>
|
||||||
|
<Version>1.0.0</Version>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<!-- Plugin references the game's own assemblies; nothing is redistributed. -->
|
||||||
|
<AssemblySearchPaths>$(AssemblySearchPaths);$(GameManaged)</AssemblySearchPaths>
|
||||||
|
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
|
||||||
|
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
|
||||||
|
<NoWarn>$(NoWarn);CS0436</NoWarn>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="UltraWideFix.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
GameDir must point at the Burglin' Gnomes install. Override on the CLI:
|
||||||
|
dotnet build -c Release -p:GameDir="/path/to/Burglin' Gnomes"
|
||||||
|
-->
|
||||||
|
<PropertyGroup>
|
||||||
|
<GameDir Condition="'$(GameDir)' == ''">/media/ckoch/Data/steam/steamapps/common/Burglin' Gnomes</GameDir>
|
||||||
|
<GameManaged>$(GameDir)/Gnomium_Data/Managed</GameManaged>
|
||||||
|
<BepInExCore>$(GameDir)/BepInEx/core</BepInExCore>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="BepInEx">
|
||||||
|
<HintPath>$(BepInExCore)/BepInEx.dll</HintPath>
|
||||||
|
<Private>false</Private>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="0Harmony">
|
||||||
|
<HintPath>$(BepInExCore)/0Harmony.dll</HintPath>
|
||||||
|
<Private>false</Private>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="UnityEngine">
|
||||||
|
<HintPath>$(GameManaged)/UnityEngine.dll</HintPath>
|
||||||
|
<Private>false</Private>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="UnityEngine.CoreModule">
|
||||||
|
<HintPath>$(GameManaged)/UnityEngine.CoreModule.dll</HintPath>
|
||||||
|
<Private>false</Private>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="UnityEngine.UI">
|
||||||
|
<HintPath>$(GameManaged)/UnityEngine.UI.dll</HintPath>
|
||||||
|
<Private>false</Private>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="UnityEngine.UIModule">
|
||||||
|
<HintPath>$(GameManaged)/UnityEngine.UIModule.dll</HintPath>
|
||||||
|
<Private>false</Private>
|
||||||
|
</Reference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -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.
|
||||||
@@ -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"
|
||||||
+139
-69
@@ -1,108 +1,178 @@
|
|||||||
#!/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() {
|
METHOD="${METHOD:-suwsf}"
|
||||||
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
|
if [ "$ACTION" = "uninstall" ]; then
|
||||||
|
echo "[+] Uninstalling..."
|
||||||
|
if [ "$METHOD" = "bepinex" ]; then
|
||||||
|
rm -vf "$GAMEDIR/winhttp.dll" "$GAMEDIR/doorstop_config.ini" "$GAMEDIR/.doorstop_version" \
|
||||||
|
"$GAMEDIR/changelog.txt" "$GAMEDIR/BepInEx/plugins/$PLUGIN_DLL"
|
||||||
|
# leave BepInEx/ itself: the user may have other mods installed
|
||||||
|
echo "[i] Left BepInEx/ in place (other mods may rely on it). Delete it manually if unused."
|
||||||
|
else
|
||||||
|
for f in SUWSF.asi SUWSF.ini dsound.dll dinput8.dll version.dll winmm.dll; do
|
||||||
|
[ -f "$GAMEDIR/$f" ] && rm -v "$GAMEDIR/$f"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
remove_ini
|
||||||
|
echo "[+] Done. No game file was ever modified."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
# --- install ------------------------------------------------------------------
|
# --- install ------------------------------------------------------------------
|
||||||
cp -v "$ASI" "$GAMEDIR/SUWSF.asi"
|
if [ "$METHOD" = "bepinex" ]; then
|
||||||
cp -v "$INI" "$GAMEDIR/SUWSF.ini"
|
BEPZIP=$(ls "$HERE"/vendor/BepInEx_win_x64_*.zip 2>/dev/null | head -1)
|
||||||
cp -v "$LOADER_SRC" "$GAMEDIR/$LOADER_NAME"
|
PLUGIN="$GAMES_DIR/$GAME/$PLUGIN_DLL"
|
||||||
apply_engine_ini
|
[ -f "$BEPZIP" ] || { echo "[!] missing BepInEx zip in vendor/"; exit 1; }
|
||||||
|
[ -f "$PLUGIN" ] || { echo "[!] missing prebuilt plugin: $PLUGIN"; exit 1; }
|
||||||
|
|
||||||
echo
|
if [ -f "$GAMEDIR/winhttp.dll" ]; then
|
||||||
echo "[+] Installed: SUWSF.asi, SUWSF.ini, $LOADER_NAME"
|
echo "[i] BepInEx already present; updating plugin only."
|
||||||
echo
|
else
|
||||||
echo "NEXT (Steam):"
|
command -v unzip >/dev/null || { echo "[!] unzip required"; exit 1; }
|
||||||
echo " 1. If the game does not apply the fix, add this to GB4's Steam"
|
unzip -o -q "$BEPZIP" -d "$GAMEDIR"
|
||||||
echo " Launch Options (right-click game > Properties > Launch Options):"
|
echo "[+] Installed BepInEx ($(basename "$BEPZIP"))"
|
||||||
echo " WINEDLLOVERRIDES=\"${LOADER_NAME%.dll}=n,b\" %command%"
|
fi
|
||||||
echo " 2. Set the in-game resolution to 5120x1440 (fullscreen)."
|
mkdir -p "$GAMEDIR/BepInEx/plugins"
|
||||||
echo " 3. Launch. Camera should widen (Hor+) instead of zooming in."
|
cp -v "$PLUGIN" "$GAMEDIR/BepInEx/plugins/"
|
||||||
echo
|
else
|
||||||
echo "If nothing changes, the runtime AOB may differ for GB4. Run:"
|
ASI="$HERE/vendor/SUWSF-x64/SUWSF.asi"
|
||||||
echo " python3 tools/dump_decrypted.py (while game is at main menu)"
|
LOADER_SRC="$HERE/vendor/SUWSF-x64/dsound.dll" # Ultimate ASI Loader (adapts by filename)
|
||||||
echo " python3 tools/aob.py (reports which patterns matched)"
|
INI="$GAMES_DIR/$GAME/SUWSF.ini"
|
||||||
echo
|
for f in "$ASI" "$LOADER_SRC" "$INI"; do
|
||||||
echo "To try a different loader name: LOADER_NAME=version.dll ./install.sh"
|
[ -f "$f" ] || { echo "[!] missing $f"; exit 1; }
|
||||||
echo "To remove everything: ./install.sh uninstall"
|
done
|
||||||
|
|
||||||
|
cp -v "$ASI" "$GAMEDIR/SUWSF.asi"
|
||||||
|
cp -v "$INI" "$GAMEDIR/SUWSF.ini"
|
||||||
|
cp -v "$LOADER_SRC" "$GAMEDIR/$LOADER"
|
||||||
|
fi
|
||||||
|
apply_ini
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
[+] Installed ($METHOD): $([ "$METHOD" = "bepinex" ] && echo "BepInEx + $PLUGIN_DLL + $LOADER" || echo "SUWSF.asi, SUWSF.ini, $LOADER")
|
||||||
|
|
||||||
|
NEXT (Proton) — REQUIRED, or the loader is ignored and the fix won't apply in gameplay.
|
||||||
|
Steam > right-click ${TITLE:-$GAME} > Properties > Launch Options:
|
||||||
|
${LAUNCH_OPTION:-WINEDLLOVERRIDES="${LOADER%.dll}=n,b" %command%}
|
||||||
|
|
||||||
|
Then set the in-game resolution to your native ultrawide res, fullscreen.
|
||||||
|
|
||||||
|
Tuning (METHOD=bepinex games):
|
||||||
|
Config is generated on first launch at
|
||||||
|
<game>/BepInEx/config/*.cfg (edits apply live, ~2s, no restart)
|
||||||
|
Plugin log (proves whether it applied):
|
||||||
|
<game>/BepInEx/LogOutput.log
|
||||||
|
|
||||||
|
Troubleshooting:
|
||||||
|
Different loader name: LOADER_NAME=version.dll ./install.sh $GAME
|
||||||
|
Diagnose injection: add PROTON_LOG=1 to launch options, then
|
||||||
|
./tools/read_protonlog.sh $APPID
|
||||||
|
Remove everything: ./install.sh $GAME uninstall
|
||||||
|
EOF
|
||||||
|
|||||||
+7
-8
@@ -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()
|
||||||
|
|||||||
+40
-15
@@ -1,21 +1,46 @@
|
|||||||
#!/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. Handles both fix methods.
|
||||||
# Output: dist/GB4_UltraWide_Fix.zip (attach to the GitHub Release)
|
# ./tools/build_release.sh GundamBreaker4 (METHOD=suwsf)
|
||||||
|
# ./tools/build_release.sh BurglinGnomes (METHOD=bepinex)
|
||||||
|
# Output: dist/<Game>_UltraWide_Fix.zip
|
||||||
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"
|
||||||
[ -f "$f" ] || { echo "[!] missing $f"; exit 1; }
|
[ -d "$GDIR" ] || { echo "[!] no such game: $GDIR"; exit 1; }
|
||||||
done
|
[ -f "$GDIR/game.conf" ] || { echo "[!] missing $GDIR/game.conf"; exit 1; }
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "$GDIR/game.conf"
|
||||||
|
METHOD="${METHOD:-suwsf}"
|
||||||
|
|
||||||
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 README.md THIRD_PARTY.md dist/GB4_UltraWide_Fix/ 2>/dev/null || true
|
if [ "$METHOD" = "bepinex" ]; then
|
||||||
( cd dist && zip -r -q GB4_UltraWide_Fix.zip GB4_UltraWide_Fix )
|
BEPZIP=$(ls vendor/BepInEx_win_x64_*.zip 2>/dev/null | head -1)
|
||||||
echo "[+] Built dist/GB4_UltraWide_Fix.zip"
|
[ -f "$BEPZIP" ] || { echo "[!] missing BepInEx zip in vendor/"; exit 1; }
|
||||||
( cd dist && sha256sum GB4_UltraWide_Fix.zip )
|
[ -f "$GDIR/$PLUGIN_DLL" ] || { echo "[!] missing prebuilt plugin $GDIR/$PLUGIN_DLL"; exit 1; }
|
||||||
unzip -l dist/GB4_UltraWide_Fix.zip
|
command -v unzip >/dev/null || { echo "[!] unzip required"; exit 1; }
|
||||||
|
unzip -o -q "$BEPZIP" -d "$OUT"
|
||||||
|
mkdir -p "$OUT/BepInEx/plugins"
|
||||||
|
cp "$GDIR/$PLUGIN_DLL" "$OUT/BepInEx/plugins/"
|
||||||
|
echo "[+] Packaged BepInEx + $PLUGIN_DLL"
|
||||||
|
else
|
||||||
|
V="vendor/SUWSF-x64"
|
||||||
|
for f in "$V/SUWSF.asi" "$V/dsound.dll" "$V/LICENSE" "$GDIR/SUWSF.ini"; do
|
||||||
|
[ -f "$f" ] || { echo "[!] missing $f"; exit 1; }
|
||||||
|
done
|
||||||
|
cp "$V/SUWSF.asi" "$V/dsound.dll" "$GDIR/SUWSF.ini" "$OUT/"
|
||||||
|
cp "$V/LICENSE" "$OUT/SUWSF-LICENSE.txt"
|
||||||
|
echo "[+] Packaged SUWSF + patch ini"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cp "$GDIR/README.md" "$OUT/README.md" 2>/dev/null || true
|
||||||
|
cp THIRD_PARTY.md LICENSE "$OUT/" 2>/dev/null || true
|
||||||
|
|
||||||
|
( cd dist && zip -r -q "${GAME}_UltraWide_Fix.zip" "${GAME}_UltraWide_Fix" )
|
||||||
|
echo "[+] Built dist/${GAME}_UltraWide_Fix.zip"
|
||||||
|
( cd dist && sha256sum "${GAME}_UltraWide_Fix.zip" )
|
||||||
|
|||||||
@@ -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__":
|
||||||
|
|||||||
Executable
+73
@@ -0,0 +1,73 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Publish a release to Gitea (git.lazypugs.com).
|
||||||
|
#
|
||||||
|
# Auth: needs a Gitea token with 'write:repository'. Never hardcode it.
|
||||||
|
# export GITEA_TOKEN=... # from your shell / password manager
|
||||||
|
# ./tools/publish.sh # or: GAME=GundamBreaker4 TAG=v1.0.1 ./tools/publish.sh
|
||||||
|
#
|
||||||
|
# Steps: build the game's zip -> push branch+tag -> create release -> upload asset.
|
||||||
|
set -euo pipefail
|
||||||
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"; cd "$HERE"
|
||||||
|
|
||||||
|
HOST="${GITEA_HOST:-https://git.lazypugs.com}"
|
||||||
|
OWNER="${GITEA_OWNER:-ckoch}"
|
||||||
|
REPO="${GITEA_REPO:-UltraWidePatches}"
|
||||||
|
GAME="${GAME:-GundamBreaker4}"
|
||||||
|
TAG="${TAG:-v1.0.0}"
|
||||||
|
NOTES="${NOTES:-games/$GAME/README.md}"
|
||||||
|
[ -f "$NOTES" ] || NOTES="RELEASE_NOTES.md"
|
||||||
|
API="$HOST/api/v1"
|
||||||
|
|
||||||
|
[ -n "${GITEA_TOKEN:-}" ] || { echo "[!] GITEA_TOKEN is not set. Create one at $HOST/user/settings/applications (scope: write:repository)"; exit 1; }
|
||||||
|
AUTH=(-H "Authorization: token $GITEA_TOKEN")
|
||||||
|
ZIPNAME="${GAME}_UltraWide_Fix.zip"
|
||||||
|
|
||||||
|
# 1. build the artifact -- do NOT swallow output; a failure here used to exit
|
||||||
|
# the whole script silently because stdout went to /dev/null under `set -e`.
|
||||||
|
if ! bash tools/build_release.sh "$GAME"; then
|
||||||
|
echo "[!] build_release.sh failed for game '$GAME' (see above)"; exit 1
|
||||||
|
fi
|
||||||
|
ZIP="dist/$ZIPNAME"
|
||||||
|
[ -f "$ZIP" ] || { echo "[!] build produced no $ZIP"; exit 1; }
|
||||||
|
echo "[+] Built $ZIP"
|
||||||
|
|
||||||
|
# 2. push code + tag
|
||||||
|
git push origin main
|
||||||
|
if ! git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
|
||||||
|
git tag -a "$TAG" -m "$TAG"
|
||||||
|
echo "[+] Created tag $TAG at $(git rev-parse --short HEAD)"
|
||||||
|
fi
|
||||||
|
git push origin "$TAG" 2>&1 | tail -1
|
||||||
|
|
||||||
|
# 3. create the release (or reuse if it already exists)
|
||||||
|
REL_ID=$(curl -sf "${AUTH[@]}" "$API/repos/$OWNER/$REPO/releases/tags/$TAG" 2>/dev/null \
|
||||||
|
| python3 -c "import sys,json;print(json.load(sys.stdin)['id'])" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ -z "$REL_ID" ]; then
|
||||||
|
BODY=$(python3 -c "import json,sys;print(json.dumps(open(sys.argv[1]).read()))" "$NOTES")
|
||||||
|
REL_ID=$(curl -sf "${AUTH[@]}" -H "Content-Type: application/json" \
|
||||||
|
-d "{\"tag_name\":\"$TAG\",\"name\":\"$TAG\",\"body\":$BODY}" \
|
||||||
|
"$API/repos/$OWNER/$REPO/releases" \
|
||||||
|
| python3 -c "import sys,json;print(json.load(sys.stdin)['id'])")
|
||||||
|
echo "[+] Created release $TAG (id=$REL_ID)"
|
||||||
|
else
|
||||||
|
echo "[i] Release $TAG exists (id=$REL_ID); replacing asset"
|
||||||
|
OLD=$(curl -sf "${AUTH[@]}" "$API/repos/$OWNER/$REPO/releases/$REL_ID" \
|
||||||
|
| ZIPNAME="$ZIPNAME" python3 -c "
|
||||||
|
import sys, json, os
|
||||||
|
d = json.load(sys.stdin)
|
||||||
|
want = os.environ['ZIPNAME']
|
||||||
|
print(next((str(a['id']) for a in d.get('assets', []) if a['name'] == want), ''))" 2>/dev/null || true)
|
||||||
|
if [ -n "$OLD" ]; then
|
||||||
|
curl -sf -X DELETE "${AUTH[@]}" "$API/repos/$OWNER/$REPO/releases/$REL_ID/assets/$OLD" >/dev/null \
|
||||||
|
&& echo "[i] removed previous asset"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. upload the zip
|
||||||
|
curl -sf "${AUTH[@]}" -F "attachment=@$ZIP" \
|
||||||
|
"$API/repos/$OWNER/$REPO/releases/$REL_ID/assets?name=$ZIPNAME" >/dev/null
|
||||||
|
echo "[+] Uploaded $ZIPNAME"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "[+] Done: $HOST/$OWNER/$REPO/releases/tag/$TAG"
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
// UnityExplorer C# console diagnostic — paste into the C# Console tab, hit Run.
|
||||||
|
// Run once at ultrawide and once at 16:9, then compare.
|
||||||
|
//
|
||||||
|
// NOTE: Burglin' Gnomes uses an ORTHOGRAPHIC camera, so fieldOfView is meaningless.
|
||||||
|
// The governing value is orthographicSize (half the visible world HEIGHT):
|
||||||
|
// visible height = 2 * orthographicSize
|
||||||
|
// visible width = visible height * aspect
|
||||||
|
// If orthographicSize is CONSTANT across resolutions -> already Hor+ (correct).
|
||||||
|
// If it SHRINKS at wider aspect -> the game holds world-width constant (Vert- lock).
|
||||||
|
var sb = new System.Text.StringBuilder();
|
||||||
|
|
||||||
|
sb.AppendLine($"SCREEN {UnityEngine.Screen.width}x{UnityEngine.Screen.height} " +
|
||||||
|
$"aspect={(float)UnityEngine.Screen.width / UnityEngine.Screen.height:F4} " +
|
||||||
|
$"full={UnityEngine.Screen.fullScreenMode}");
|
||||||
|
|
||||||
|
foreach (var c in UnityEngine.Object.FindObjectsOfType<UnityEngine.Camera>())
|
||||||
|
{
|
||||||
|
if (c.orthographic)
|
||||||
|
sb.AppendLine($"CAM {c.name} ORTHO size={c.orthographicSize:F4} aspect={c.aspect:F4} " +
|
||||||
|
$"=> visibleW={2f * c.orthographicSize * c.aspect:F3} visibleH={2f * c.orthographicSize:F3} " +
|
||||||
|
$"depth={c.depth} enabled={c.enabled}");
|
||||||
|
else
|
||||||
|
sb.AppendLine($"CAM {c.name} PERSP fov={c.fieldOfView:F2} aspect={c.aspect:F4} " +
|
||||||
|
$"phys={c.usePhysicalProperties} gate={c.gateFit} depth={c.depth} enabled={c.enabled}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Cinemachine (any version) via reflection: may drive ortho size each frame ---
|
||||||
|
var bf = System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic |
|
||||||
|
System.Reflection.BindingFlags.Instance;
|
||||||
|
foreach (var mb in UnityEngine.Object.FindObjectsOfType<UnityEngine.MonoBehaviour>())
|
||||||
|
{
|
||||||
|
var t = mb.GetType();
|
||||||
|
if (t.FullName == null || t.FullName.IndexOf("Cinemachine", System.StringComparison.OrdinalIgnoreCase) < 0)
|
||||||
|
continue;
|
||||||
|
object lens = null;
|
||||||
|
foreach (var name in new[] { "Lens", "m_Lens" })
|
||||||
|
{
|
||||||
|
var p = t.GetProperty(name, bf); if (p != null) { lens = p.GetValue(mb); break; }
|
||||||
|
var f = t.GetField(name, bf); if (f != null) { lens = f.GetValue(mb); break; }
|
||||||
|
}
|
||||||
|
if (lens != null)
|
||||||
|
{
|
||||||
|
var lt = lens.GetType();
|
||||||
|
object osize = null, fov = null;
|
||||||
|
foreach (var n in new[] { "OrthographicSize", "m_OrthographicSize" })
|
||||||
|
{ var f = lt.GetField(n, bf); if (f != null) { osize = f.GetValue(lens); break; } }
|
||||||
|
foreach (var n in new[] { "FieldOfView", "m_FieldOfView" })
|
||||||
|
{ var f = lt.GetField(n, bf); if (f != null) { fov = f.GetValue(lens); break; } }
|
||||||
|
sb.AppendLine($"CM {t.Name} '{mb.name}' lensOrthoSize={osize} lensFOV={fov} enabled={mb.enabled}");
|
||||||
|
}
|
||||||
|
else sb.AppendLine($"CM {t.Name} '{mb.name}' (no lens) enabled={mb.enabled}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- uGUI canvas scalers: match=0 scales by WIDTH (bad at ultrawide), 1 = by HEIGHT ---
|
||||||
|
foreach (var cs in UnityEngine.Object.FindObjectsOfType<UnityEngine.UI.CanvasScaler>())
|
||||||
|
sb.AppendLine($"UI {cs.name} mode={cs.uiScaleMode} ref={cs.referenceResolution} " +
|
||||||
|
$"match={cs.matchWidthOrHeight} screenMatch={cs.screenMatchMode} scaleFactor={cs.scaleFactor}");
|
||||||
|
|
||||||
|
UnityExplorer.ExplorerCore.Log(sb.ToString());
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// LIVE FIX TEST — paste into UnityExplorer's C# Console while running at ultrawide.
|
||||||
|
// Applies both candidate fixes immediately so you can see the result on screen.
|
||||||
|
// Nothing is persisted: relaunching the game reverts everything.
|
||||||
|
|
||||||
|
var log = new System.Text.StringBuilder();
|
||||||
|
|
||||||
|
// ---- FIX 1: HUD scaling ----------------------------------------------------
|
||||||
|
// CanvasScaler.matchWidthOrHeight: 0 = scale by WIDTH, 1 = scale by HEIGHT.
|
||||||
|
// The game ships 0.453 (width-biased), which balloons the HUD at 5120px wide.
|
||||||
|
// Matching HEIGHT keeps the HUD the same physical size as it is at 16:9.
|
||||||
|
foreach (var cs in UnityEngine.Object.FindObjectsOfType<UnityEngine.UI.CanvasScaler>())
|
||||||
|
{
|
||||||
|
log.AppendLine($"UI {cs.name}: match {cs.matchWidthOrHeight} -> 1.0");
|
||||||
|
cs.matchWidthOrHeight = 1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- FIX 2: orthographic view width ---------------------------------------
|
||||||
|
// For an ortho camera, visible height = 2*orthographicSize and width follows
|
||||||
|
// aspect. If the game shrank orthographicSize at ultrawide, restoring the 16:9
|
||||||
|
// value gives back the vertical view (true Hor+).
|
||||||
|
// Set TARGET_ORTHO to the size you measured at 1920x1080; leave null to only report.
|
||||||
|
float? TARGET_ORTHO = null; // e.g. 5.5f
|
||||||
|
|
||||||
|
foreach (var c in UnityEngine.Object.FindObjectsOfType<UnityEngine.Camera>())
|
||||||
|
{
|
||||||
|
if (!c.orthographic) continue;
|
||||||
|
log.AppendLine($"CAM {c.name}: orthoSize={c.orthographicSize:F4} aspect={c.aspect:F4} " +
|
||||||
|
$"visibleW={2f * c.orthographicSize * c.aspect:F3} visibleH={2f * c.orthographicSize:F3}");
|
||||||
|
if (TARGET_ORTHO.HasValue)
|
||||||
|
{
|
||||||
|
c.orthographicSize = TARGET_ORTHO.Value;
|
||||||
|
log.AppendLine($" -> forced orthoSize={TARGET_ORTHO.Value}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UnityExplorer.ExplorerCore.Log(log.ToString());
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
// LIVE UI TUNER — paste into UnityExplorer's C# Console. Edit knobs, Run, look, repeat.
|
||||||
|
// Nothing persists across a relaunch.
|
||||||
|
//
|
||||||
|
// TWO canvas families, each needing a DIFFERENT lever:
|
||||||
|
//
|
||||||
|
// 1. ScreenSpaceOverlay + ScaleWithScreenSize ("Canvas", "Canvas in front")
|
||||||
|
// The HUD. Lever = matchWidthOrHeight + referenceResolution.
|
||||||
|
// KEEP MATCH AT 1.0 — lowering it re-introduces width-based scaling and
|
||||||
|
// undoes the shrink (match .8 + ref 1350 => 1.281, vs match 1 => 1.067).
|
||||||
|
//
|
||||||
|
// 2. WorldSpace canvases ("NamePlate", etc.)
|
||||||
|
// In-world labels. CanvasScaler.scaleFactor DOES NOT WORK here: for a
|
||||||
|
// WorldSpace canvas, CanvasScaler.HandleWorldCanvas() overwrites
|
||||||
|
// scaleFactor with dynamicPixelsPerUnit every frame. The real lever is
|
||||||
|
// the canvas transform's localScale.
|
||||||
|
//
|
||||||
|
// NOTE: Canvas.scaleFactor is recomputed in CanvasScaler.Update(), so reading it
|
||||||
|
// right after writing returns LAST frame's value. We print the mathematically
|
||||||
|
// expected scale instead, and echo BEFORE state (which is accurate) each run.
|
||||||
|
|
||||||
|
// ---- KNOBS -----------------------------------------------------------------
|
||||||
|
float MATCH = 1.0f; // HUD. Leave at 1.0.
|
||||||
|
float HUD_SCALE = 0.85f; // HUD. <1 = smaller HUD + more layout room.
|
||||||
|
float WORLD_SCALE = 0f; // In-world labels. 0 = don't touch.
|
||||||
|
// Absolute multiplier vs the BASELINE printed below.
|
||||||
|
// Run once with 0 to read baseline, then e.g. 0.8f.
|
||||||
|
bool LIST_ONLY = false; // true = report only, change nothing
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
var log = new System.Text.StringBuilder();
|
||||||
|
log.AppendLine($"KNOBS match={MATCH} hudScale={HUD_SCALE} worldScale={WORLD_SCALE} listOnly={LIST_ONLY}");
|
||||||
|
log.AppendLine($"SCREEN {UnityEngine.Screen.width}x{UnityEngine.Screen.height} " +
|
||||||
|
$"aspect={(float)UnityEngine.Screen.width / UnityEngine.Screen.height:F4}");
|
||||||
|
|
||||||
|
foreach (var cs in UnityEngine.Object.FindObjectsOfType<UnityEngine.UI.CanvasScaler>())
|
||||||
|
{
|
||||||
|
var cv = cs.GetComponent<UnityEngine.Canvas>();
|
||||||
|
if (cv == null) continue;
|
||||||
|
|
||||||
|
if (cv.renderMode == UnityEngine.RenderMode.WorldSpace)
|
||||||
|
{
|
||||||
|
var ls = cv.transform.localScale;
|
||||||
|
log.AppendLine($"WORLD {cs.name}: localScale={ls} (dynPPU={cs.dynamicPixelsPerUnit})");
|
||||||
|
if (!LIST_ONLY && WORLD_SCALE > 0f)
|
||||||
|
{
|
||||||
|
// Store the pristine scale on first touch so repeated runs don't compound.
|
||||||
|
var marker = cv.transform.Find("__uwp_baseline");
|
||||||
|
float bx;
|
||||||
|
if (marker == null)
|
||||||
|
{
|
||||||
|
var go = new UnityEngine.GameObject("__uwp_baseline");
|
||||||
|
go.transform.SetParent(cv.transform, false);
|
||||||
|
go.transform.localScale = ls; // stash baseline here
|
||||||
|
bx = ls.x;
|
||||||
|
}
|
||||||
|
else bx = marker.localScale.x;
|
||||||
|
|
||||||
|
cv.transform.localScale = new UnityEngine.Vector3(bx, bx, bx) * WORLD_SCALE;
|
||||||
|
log.AppendLine($" APPLIED localScale baseline={bx:F5} -> {cv.transform.localScale}");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.AppendLine($"BEFORE {cs.name} [{cs.uiScaleMode}/{cv.renderMode}] ref={cs.referenceResolution} " +
|
||||||
|
$"match={cs.matchWidthOrHeight}");
|
||||||
|
|
||||||
|
if (!LIST_ONLY && cs.uiScaleMode == UnityEngine.UI.CanvasScaler.ScaleMode.ScaleWithScreenSize)
|
||||||
|
{
|
||||||
|
cs.matchWidthOrHeight = MATCH;
|
||||||
|
if (HUD_SCALE > 0f && System.Math.Abs(HUD_SCALE - 1f) > 0.0001f)
|
||||||
|
cs.referenceResolution = new UnityEngine.Vector2(cs.referenceResolution.x, 1080f / HUD_SCALE);
|
||||||
|
|
||||||
|
// expected scale (accurate NOW; Canvas.scaleFactor lags by a frame)
|
||||||
|
var r = cs.referenceResolution;
|
||||||
|
double exp = System.Math.Pow(UnityEngine.Screen.width / r.x, 1.0 - MATCH) *
|
||||||
|
System.Math.Pow(UnityEngine.Screen.height / r.y, MATCH);
|
||||||
|
log.AppendLine($" APPLIED ref={r} match={MATCH} => expectedScale={exp:F4} " +
|
||||||
|
$"logicalCanvas={UnityEngine.Screen.width / exp:F0}x{UnityEngine.Screen.height / exp:F0}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UnityExplorer.ExplorerCore.Log(log.ToString());
|
||||||
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Reference in New Issue
Block a user