From cbcc53c2e96e3f094a45f35e3e5c6484e2828e3c Mon Sep 17 00:00:00 2001 From: ckoch Date: Tue, 21 Jul 2026 16:08:11 -0400 Subject: [PATCH] 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 --- .gitignore | 4 + games/BurglinGnomes/plugin/UltraWideFix.cs | 142 ++++++++++++++++++ .../BurglinGnomes/plugin/UltraWideFix.csproj | 57 +++++++ 3 files changed, 203 insertions(+) create mode 100644 games/BurglinGnomes/plugin/UltraWideFix.cs create mode 100644 games/BurglinGnomes/plugin/UltraWideFix.csproj diff --git a/.gitignore b/.gitignore index d7be2f5..8a45785 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,7 @@ __pycache__/ gb4dump/ *.bin *.bak-bg4uw + +# .NET plugin build output +**/plugin/bin/ +**/plugin/obj/ diff --git a/games/BurglinGnomes/plugin/UltraWideFix.cs b/games/BurglinGnomes/plugin/UltraWideFix.cs new file mode 100644 index 0000000..6f1100a --- /dev/null +++ b/games/BurglinGnomes/plugin/UltraWideFix.cs @@ -0,0 +1,142 @@ +using BepInEx; +using BepInEx.Configuration; +using UnityEngine; +using UnityEngine.UI; +using UnityEngine.SceneManagement; + +namespace BurglinGnomes.UltraWideFix +{ + /// + /// 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. + /// + [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 _enabled; + private ConfigEntry _match; + private ConfigEntry _hudScale; + private ConfigEntry _worldScale; + private ConfigEntry _designHeight; + private ConfigEntry _verbose; + + // Baselines captured once per canvas so repeated applies never compound. + private readonly System.Collections.Generic.Dictionary _refBaseline + = new System.Collections.Generic.Dictionary(); + private readonly System.Collections.Generic.Dictionary _scaleBaseline + = new System.Collections.Generic.Dictionary(); + + 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(0f, 1f))); + _hudScale = Config.Bind("HUD", "HudScale", 0.85f, + 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(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", 1.0f, + 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(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. + private void Update() + { + _timer += Time.unscaledDeltaTime; + if (_timer < 2f) return; + _timer = 0f; + Apply(); + } + + private void Apply() + { + if (!_enabled.Value) return; + + foreach (var cs in FindObjectsOfType()) + { + var canvas = cs.GetComponent(); + if (canvas == null) continue; + int id = cs.GetInstanceID(); + + if (canvas.renderMode == RenderMode.WorldSpace) + { + if (Mathf.Approximately(_worldScale.Value, 1f)) continue; + if (!_scaleBaseline.TryGetValue(id, out var base3)) + { + base3 = cs.transform.localScale; + _scaleBaseline[id] = base3; + } + 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; + if (_verbose.Value) + Logger.LogInfo($"HUD '{cs.name}' ref {baseRef} -> {targetRef} " + + $"match={_match.Value}"); + } + } + } + } +} diff --git a/games/BurglinGnomes/plugin/UltraWideFix.csproj b/games/BurglinGnomes/plugin/UltraWideFix.csproj new file mode 100644 index 0000000..45d4121 --- /dev/null +++ b/games/BurglinGnomes/plugin/UltraWideFix.csproj @@ -0,0 +1,57 @@ + + + + netstandard2.1 + BurglinGnomes.UltraWideFix + BurglinGnomes.UltraWideFix + 1.0.0 + latest + + $(AssemblySearchPaths);$(GameManaged) + false + true + $(NoWarn);CS0436 + + + + + + + + + /media/ckoch/Data/steam/steamapps/common/Burglin' Gnomes + $(GameDir)/Gnomium_Data/Managed + $(GameDir)/BepInEx/core + + + + + $(BepInExCore)/BepInEx.dll + false + + + $(BepInExCore)/0Harmony.dll + false + + + $(GameManaged)/UnityEngine.dll + false + + + $(GameManaged)/UnityEngine.CoreModule.dll + false + + + $(GameManaged)/UnityEngine.UI.dll + false + + + $(GameManaged)/UnityEngine.UIModule.dll + false + + + +