13 Commits
Author SHA1 Message Date
ckochandClaude Opus 4.8 b97a8a73b1 Credit BepInEx/UnityExplorer, fix stale README refs
BepInEx install/uninstall verified end-to-end against a scratch game dir:
installs loader + plugin, uninstall removes both and leaves BepInEx/ intact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 16:34:34 -04:00
ckochandClaude Opus 4.8 59ee13de9d Burglin' Gnomes: finalize fix, docs, and BepInEx install support
Final tuned values baked in as defaults: HudScale=0.8, WorldLabelScale=0.8.

- install.sh gains METHOD=bepinex: installs BepInEx from vendor + the prebuilt
  plugin, with a matching uninstall that leaves BepInEx/ alone (other mods may
  depend on it). Success banner is now method-aware.
- Ship the prebuilt plugin DLL so users don't need a .NET SDK.
- Per-game README documenting why the camera is deliberately untouched
  (orthographic, constant orthographicSize = already Hor+), the two-canvas UI
  problem, tuning, and where the plugin log actually lives.
- Root README lists both games and both fix methods.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 16:33:56 -04:00
ckochandClaude Opus 4.8 ed2e0016ba Plugin: never cache a zero baseline for world-space canvases
The in-game log showed 'World Canvas localScale (0,0,0) -> (0,0,0)'. Games hide
world-space UI by zeroing its scale; if that zero got cached as the baseline,
multiplying it would pin the canvas to zero permanently and the UI could never
reappear. Now skips canvases that are currently zeroed - both when capturing the
baseline and when applying - so hidden UI is left alone and picked up once the
game shows it again.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 16:25:27 -04:00
ckochandClaude Opus 4.8 40d057ac6e Plugin: reload config live and always log HUD changes
BepInEx does not pick up external edits to a plugin's .cfg, so tuning values
while the game ran silently did nothing - the plugin kept the values loaded at
startup. Now watches the config file's mtime and calls Config.Reload() when it
changes, so edits apply within ~2s with no restart. Reload is wrapped in
try/catch so a malformed .cfg warns instead of spamming exceptions.

HUD adjustments now always log (not just under Verbose) with the resulting
scale, so it is obvious whether a value took effect.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 16:18:05 -04:00
ckochandClaude Opus 4.8 cbcc53c2e9 Add Burglin' Gnomes BepInEx ultrawide UI plugin
Config-driven so values are tunable without recompiling or pasting console
scripts. Camera is deliberately untouched - it is orthographic with constant
orthographicSize, which is already correct Hor+ at 32:9.

Fixes the two UI families separately, each via the lever that actually works:
- HUD (ScreenSpaceOverlay/ScaleWithScreenSize): match=1 + referenceResolution.y
  raised by HudScale, shrinking the HUD while giving the layout more room.
- World labels (WorldSpace): canvas transform localScale, because
  CanvasScaler.scaleFactor is overwritten by HandleWorldCanvas every frame.

Baselines are cached per canvas so periodic re-apply never compounds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 16:08:11 -04:00
ckochandClaude Opus 4.8 b9e68fd284 UI tuner: fix stale readback and World-Space label lever
Two defects in the tuner, both visible in the in-game logs:

1. AFTER lines read Canvas.scaleFactor immediately after writing, but that is
   recomputed in CanvasScaler.Update(), so it reported last frame's value
   (showed 1.3333 after ref was set to 1350). Now prints the mathematically
   expected scale instead.

2. WORLD_SCALE set CanvasScaler.scaleFactor on WorldSpace canvases, which
   HandleWorldCanvas() overwrites with dynamicPixelsPerUnit every frame - the
   log showed 'APPLIED scaleFactor 1 -> 0.8' followed by effectiveScale=1.0.
   World-space label size is driven by the canvas transform localScale, so it
   now scales that, stashing a baseline child so repeated runs don't compound.

Also documents that MATCH must stay 1.0: match .8 + ref 1350 yields 1.281 vs
1.067 at match 1, largely cancelling the shrink.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 16:04:56 -04:00
ckochandClaude Opus 4.8 ae994c27e6 Bake real test values into UI tuner defaults
Pasting the file verbatim was a no-op because the knobs shipped at 1.0, so
every run reported KNOBS ...=1 and changed nothing. Default to HUD_SCALE=0.85
and WORLD_SCALE=0.8 so copying the file straight from the editor actually
exercises the fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 16:00:45 -04:00
ckochandClaude Opus 4.8 4cd18fb9f4 UI tuner: handle both canvas modes, echo knob values
In-game capture revealed two canvas families: ScreenSpaceOverlay/
ScaleWithScreenSize (the HUD, ref 1920x1080 match=1) and WorldSpace/
ConstantPixelSize (NamePlate and friends). Unity ignores referenceResolution
in ConstantPixelSize mode, so the old HUD_SCALE knob could never affect the
in-world labels. Split into HUD_SCALE (referenceResolution) and WORLD_SCALE
(scaleFactor), applied per mode.

Also echoes the knob values and logs APPLIED lines, so a stale console edit is
immediately obvious instead of looking like the fix did nothing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 15:58:37 -04:00
ckochandClaude Opus 4.8 bd9fdc50f6 Fix UI tuner: scaleFactor is ignored in ScaleWithScreenSize
CanvasScaler.scaleFactor only applies to ConstantPixelSize mode, so the old
SCALE_MULT knob would have done nothing. Replaced with HUD_SCALE, implemented
via referenceResolution.y (effective scale = screenH/refH), which genuinely
shrinks the HUD while giving the layout MORE logical room - the right lever
for overlapping text.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 15:52:19 -04:00
ckochandClaude Opus 4.8 3525c79dba Burglin' Gnomes: camera confirmed correct, add live UI tuner
orthographicSize is constant at 5.0 across 1920x1080 and 5120x1440 (visible
height 10.0, width 17.8 -> 35.6). The camera is already textbook Hor+ and needs
no patch - the entire ultrawide problem is HUD scaling.

CanvasScaler match=0.453 gave scaleFactor 1.948 (ballooned HUD); match=1 gives
1.333 with logical height exactly 1080 (the design height). Adds a live tuner
with independent match / scaleFactor / referenceResolution knobs to chase the
remaining text overlap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 15:51:38 -04:00
ckochandClaude Opus 4.8 8bbc1cfac8 Correct Burglin' Gnomes diagnosis: camera is ORTHOGRAPHIC
fieldOfView is meaningless here (reads 60 at every resolution because nothing
uses it). The governing value is orthographicSize. Diagnostic now reports
orthoSize plus computed visible width/height, and checks Cinemachine lens
ortho size too.

Also confirmed the HUD cause: CanvasScaler match=0.453 (width-biased) with a
1920x1080 reference, which balloons the HUD at 5120px wide. Added a live
fix-test script to try match=1 and an orthoSize override in-game.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 15:31:05 -04:00
ckochandClaude Opus 4.8 e21c6c3c44 Add UnityExplorer camera/UI diagnostic script
Reflection-based so it compiles across Cinemachine versions. Dumps cameras,
Cinemachine lens FOV, and CanvasScaler settings to identify what drives FOV
and what breaks the HUD at ultrawide.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 15:06:02 -04:00
ckochandClaude Opus 4.8 b6ab1bf5ab Scaffold Burglin' Gnomes (Unity 6 Mono) as second target
Recon: Unity 6000.0.62f1, Mono backend (readable C# assemblies), URP +
Cinemachine, no DRM. No FOV logic in game code, so the horizontal-FOV lock is
a serialized camera setting - needs runtime inspection via BepInEx/UnityExplorer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 13:16:18 -04:00
14 changed files with 607 additions and 21 deletions
+4
View File
@@ -14,3 +14,7 @@ __pycache__/
gb4dump/
*.bin
*.bak-bg4uw
# .NET plugin build output
**/plugin/bin/
**/plugin/obj/
+18 -8
View File
@@ -2,13 +2,19 @@
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**.
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).
Fixes use whichever approach the engine calls for:
- **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.
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.
## Supported games
| Game | Engine | Status |
|---|---|---|
| [Gundam Breaker 4](games/GundamBreaker4/) | Unreal Engine 4.27 | ✅ Confirmed working in gameplay at 5120×1440 |
| 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
@@ -32,21 +38,25 @@ Config-file tweaks (e.g. UE4's `AspectRatioAxisConstraint=MaintainYFOV`) typical
| Path | What |
|---|---|
| `games/<Game>/SUWSF.ini` | That game's patch definitions |
| `games/<Game>/SUWSF.ini` | Patch definitions (SUWSF games) |
| `games/<Game>/plugin/` | C# plugin source + prebuilt DLL (BepInEx games) |
| `games/<Game>/game.conf` | Game metadata (appid, exe, paths, launch option) |
| `install.sh` | Generic installer / uninstaller |
| `tools/build_release.sh` | Builds a per-game drop-in release zip |
| `tools/publish.sh` | Pushes + cuts a GitHub release |
| `tools/publish.sh` | Pushes + cuts a Gitea release |
| `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) |
## Adding a game
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.
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
+15
View File
@@ -17,6 +17,21 @@ them; all runtime patching is performed by SUWSF.
- License: MIT
- 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
The `AspectRatioAxisConstraint` (HOR+) and `ConstrainAspectRatio` (pillarbox)
byte patterns in `games/*/SUWSF.ini` are adapted from the UE4 fixes in
Binary file not shown.
+98
View File
@@ -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.
+16
View File
@@ -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"
+177
View File
@@ -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>
+33 -1
View File
@@ -105,17 +105,42 @@ remove_ini() {
fi
}
METHOD="${METHOD:-suwsf}"
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 ------------------------------------------------------------------
if [ "$METHOD" = "bepinex" ]; then
BEPZIP=$(ls "$HERE"/vendor/BepInEx_win_x64_*.zip 2>/dev/null | head -1)
PLUGIN="$GAMES_DIR/$GAME/$PLUGIN_DLL"
[ -f "$BEPZIP" ] || { echo "[!] missing BepInEx zip in vendor/"; exit 1; }
[ -f "$PLUGIN" ] || { echo "[!] missing prebuilt plugin: $PLUGIN"; exit 1; }
if [ -f "$GAMEDIR/winhttp.dll" ]; then
echo "[i] BepInEx already present; updating plugin only."
else
command -v unzip >/dev/null || { echo "[!] unzip required"; exit 1; }
unzip -o -q "$BEPZIP" -d "$GAMEDIR"
echo "[+] Installed BepInEx ($(basename "$BEPZIP"))"
fi
mkdir -p "$GAMEDIR/BepInEx/plugins"
cp -v "$PLUGIN" "$GAMEDIR/BepInEx/plugins/"
else
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"
@@ -126,11 +151,12 @@ 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: SUWSF.asi, SUWSF.ini, $LOADER
[+] 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:
@@ -138,6 +164,12 @@ NEXT (Proton) — REQUIRED, or the loader is ignored and the fix won't apply in
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
+59
View File
@@ -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());
+36
View File
@@ -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());
+82
View File
@@ -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());
Binary file not shown.
Binary file not shown.