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>
This commit is contained in:
@@ -14,3 +14,7 @@ __pycache__/
|
|||||||
gb4dump/
|
gb4dump/
|
||||||
*.bin
|
*.bin
|
||||||
*.bak-bg4uw
|
*.bak-bg4uw
|
||||||
|
|
||||||
|
# .NET plugin build output
|
||||||
|
**/plugin/bin/
|
||||||
|
**/plugin/obj/
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
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.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<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", 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<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.
|
||||||
|
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<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;
|
||||||
|
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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user