There are uboat mos working locally and working perfectly after passing via Claude Opus 4.7 .
All modes are working fine on my Uboat from GOG.
There are uboat mos working locally and working perfectly after passing via Claude Opus 4.7 .
All modes are working fine on my Uboat from GOG.
Brighter Nights
https://steamcommunity.com/sharedfiles/filedetails/?id=3699304305
Opus 4.7 review
| Before | After | |
|---|---|---|
Redundant [HarmonyPatch] on wrapper class |
β present | β removed |
| Duplicated mapβzoom math (night/day branches) | 2Γ copyβpaste | 1 branch, multiplier picks night/day |
Dead locals (forcedLightFadeDay/Night both = 0) |
β present | β removed |
| Night detection mixed into exposure patch | β | Extracted to dedicated NightDetector class |
| Magic numbers scattered across patches | ~15 values inline | Centralized in Config (single source of truth) |
| Before | After | |
|---|---|---|
Harmony.PatchAll exception kills mod load |
unhandled | wrapped in try/catch + log |
MapCameraController.Update patch can spam exceptions every frame |
yes | selfβdisables after first error (matches AutoExposure pattern) |
Perβframe patches all have _failed killβswitch |
only 1 of 3 | 3 of 3 |
| Logging | bare Debug.LogException |
tagged [BrighterNights] ... prefix β easier to grep logs |
| Before | After | |
|---|---|---|
| Tweak a curve / threshold | hunt through patch bodies | edit one const in Config |
| Switch enum check style | chained ` | |
| Mod identity string | hardcoded inline | BrighterNightsMod.HarmonyId constant |
Unchanged β same exposure offsets, same curve exponent, same night threshold (95Β° zenith), same 10βsecond check interval, same map pitch range. This is a pure refactor; no gameplay impact.
BrighterNights.cs) β avoids UBOAT's flaky multiβfile Roslyn compile.mod.json permissions stay minimal: ["Reflection"] β no IO needed.using list (UBOAT's namespace layout is unintuitive; trimming usings broke compilation).using directives in UBOAT mods even if they look unused β type locations are not where you'd guess (NonSerializedInGameState lives in UBOAT.Game.Core.Serialization, not the framework namespace; BrighterNights/CameraMode live in UBOAT.Game.Scene, not .Camera).BadImageFormatException on mod load = compile errors above it. Always scroll up in the log; the exception itself is just Mono refusing the broken bytes Roslyn produced.if (_failed) return; at the top, set in catch) are the right pattern for anything that runs in Update / postfix hot paths β one stale reflection cache shouldn't tank your frame rate with exception spam.BrighterNights.cs
using System;
using System.Reflection;
using DWS.Common.InjectionFramework;
using HarmonyLib;
using UBOAT.Game;
using UBOAT.Game.Scene;
using UBOAT.Game.Core.Serialization;
using UBOAT.Game.Scene.Tasks;
using UBOAT.Game.Scene.Camera;
using UnityEngine.Rendering.PostProcessing;
using UnityEngine;
using PlayWay.Water;
namespace UBOAT.Mods.brighternights
{
// =====================================================================
// Entry point
// =====================================================================
[NonSerializedInGameState]
public sealed class BrighterNightsMod : IUserMod
{
public const string HarmonyId = "jaelee1111.uboat.brighternights";
private static bool _initialized;
private static Harmony _harmony;
public void OnLoaded()
{
if (_initialized) return;
_initialized = true;
try
{
_harmony = new Harmony(HarmonyId);
_harmony.PatchAll(typeof(BrighterNightsMod).Assembly);
Debug.Log("[BrighterNights] Patches applied.");
}
catch (Exception ex)
{
Debug.LogError("[BrighterNights] Failed to apply patches:");
Debug.LogException(ex);
}
}
}
// =====================================================================
// Central tunables - tweak here, not in patch code
// =====================================================================
internal static class Config
{
// Night detection
public const float NightCheckIntervalSeconds = 10f;
public const float NightSunZenithThreshold = 95f;
// Auto-exposure boost
public const float WorldBoostNight = 1.5f;
public const float WorldBoostDay = 0.5f;
public const float MapBoostNight = 3.0f;
public const float MapBoostDay = 1.0f;
// Map zoom curve
public const float MapZoomedIn = 4000f;
public const float MapZoomedOut = 12000f;
public const float MapZoomExponent = 6.0f;
// Underwater forced fade
public const float UnderwaterFadeDay = 0f;
public const float UnderwaterFadeNight = 0f;
// Map camera tweaks
public const float MapMinDistancePreset = 25f;
public const float MapPitchElevationMin = 25f;
public const float MapPitchElevationMax = 75f;
public const float MapPitchMin = 30f;
public const float MapPitchMax = 85f;
}
// =====================================================================
// Day/Night detection (throttled reflection into TOD_Sky)
// =====================================================================
internal static class NightDetector
{
public static bool IsNight;
private static bool _reflectionCached;
private static PropertyInfo _instanceProp;
private static PropertyInfo _sunZenithProp;
private static object _skyInstance;
private static float _nextCheckTime;
private static bool _failed;
public static void Tick()
{
if (_failed) return;
float now = Time.unscaledTime;
if (now < _nextCheckTime) return;
_nextCheckTime = now + Config.NightCheckIntervalSeconds;
try
{
if (!_reflectionCached)
{
_reflectionCached = true;
Type todSkyType = AccessTools.TypeByName("TOD_Sky");
if (todSkyType != null)
{
_instanceProp = AccessTools.Property(todSkyType, "Instance");
_sunZenithProp = AccessTools.Property(todSkyType, "SunZenith");
}
}
if (_skyInstance == null || _skyInstance.Equals(null))
{
if (_instanceProp != null)
_skyInstance = _instanceProp.GetValue(null);
}
if (_skyInstance == null || _sunZenithProp == null)
{
IsNight = false;
return;
}
float sunZenith = (float)_sunZenithProp.GetValue(_skyInstance);
IsNight = sunZenith > Config.NightSunZenithThreshold;
}
catch (Exception ex)
{
Debug.LogError("[BrighterNights] NightDetector failed, disabling.");
Debug.LogException(ex);
_failed = true;
}
}
}
// =====================================================================
// Auto-exposure boost (world + map zoom curve)
// =====================================================================
[HarmonyPatch(typeof(BrighterNights), "DoUpdate")]
internal static class AutoExposurePatch
{
private static bool _failed;
[HarmonyPostfix]
private static void Postfix(AutoExposure ___autoExposure, MainCamera ___mainCamera)
{
if (_failed) return;
try
{
if (___autoExposure == null || !___mainCamera || !___mainCamera.isActiveAndEnabled)
return;
NightDetector.Tick();
bool night = NightDetector.IsNight;
bool inMap = ___mainCamera.CurrentMode == CameraMode.Map;
float boost;
if (inMap && ___mainCamera.MapController != null)
{
float elevation = ___mainCamera.MapController.CameraElevation;
float t = Mathf.Clamp01(
(Config.MapZoomedOut - elevation) /
(Config.MapZoomedOut - Config.MapZoomedIn));
float curve = Mathf.Pow(t, Config.MapZoomExponent);
boost = curve * (night ? Config.MapBoostNight : Config.MapBoostDay);
}
else
{
boost = night ? Config.WorldBoostNight : Config.WorldBoostDay;
}
___autoExposure.minLuminance.value -= boost;
}
catch (Exception ex)
{
Debug.LogError("[BrighterNights] AutoExposurePatch failed, disabling.");
Debug.LogException(ex);
_failed = true;
}
}
}
// =====================================================================
// Underwater light fade override
// =====================================================================
[HarmonyPatch(typeof(WaterMaterials), "OnProfilesChanged")]
internal static class UnderwaterLightPatch
{
[HarmonyPostfix]
private static void Postfix(ref float ___underwaterLightFadeScale)
{
___underwaterLightFadeScale = NightDetector.IsNight
? Config.UnderwaterFadeNight
: Config.UnderwaterFadeDay;
}
}
// =====================================================================
// Map camera: minimum zoom preset
// =====================================================================
[HarmonyPatch(typeof(MapCameraController), "Initialize")]
internal static class MapCameraInitializePatch
{
[HarmonyPostfix]
private static void Postfix(ref float[] ___mapDistancePresets)
{
if (___mapDistancePresets != null && ___mapDistancePresets.Length > 0)
___mapDistancePresets[0] = Config.MapMinDistancePreset;
}
}
// =====================================================================
// Map camera: pitch lerp based on elevation
// =====================================================================
[HarmonyPatch(typeof(MapCameraController), "Update")]
internal static class MapCameraUpdatePatch
{
private static bool _failed;
[HarmonyPostfix]
private static void Postfix(
MapCameraController __instance,
MainCamera ___mainCamera,
float ___cameraElevation)
{
if (_failed) return;
try
{
if (___mainCamera == null || ___mainCamera.transform == null)
return;
MapCameraController.MapDisplayMode mode = __instance.Mode;
if (mode == MapCameraController.MapDisplayMode.Headquarters ||
mode == MapCameraController.MapDisplayMode.AssignmentSelection ||
mode == MapCameraController.MapDisplayMode.AssignmentSummary ||
mode == MapCameraController.MapDisplayMode.Vacation)
{
return;
}
float lift = Mathf.Clamp01(
(___cameraElevation - Config.MapPitchElevationMin) /
(Config.MapPitchElevationMax - Config.MapPitchElevationMin));
float pitch = Mathf.Lerp(Config.MapPitchMin, Config.MapPitchMax, lift);
Vector3 euler = ___mainCamera.transform.eulerAngles;
___mainCamera.transform.eulerAngles = new Vector3(pitch, euler.y, euler.z);
}
catch (Exception ex)
{
Debug.LogError("[BrighterNights] MapCameraUpdatePatch failed, disabling.");
Debug.LogException(ex);
_failed = true;
}
}
}
}Dynamic Distance Labels
https://steamcommunity.com/sharedfiles/filedetails/?id=3405289645
Opus 4.7 recap
| # | Improvement | Impact |
|---|---|---|
| 1 | Collapsed 4 duplicated Alt/Unit branches into one XOR expression ((units == Nautical) ^ altPressed) |
~30 lines removed, single source of truth |
| 2 | Unified format-selection in helpers via ternary cascade instead of repeated if/else blocks | Less duplication, easier to tweak thresholds |
| 3 | Split Constants into its own file (Constants.cs) |
Separation of concerns |
| 4 | Renamed file KilometersToPreferredUnitsPatch.cs β UnitsUtilityPatch.cs |
Matches the class it patches |
| 5 | const instead of static readonly for true constants |
Slightly faster, signals intent |
| 6 | Added HARMONY_ID constant |
No magic strings |
| # | Improvement | Impact |
|---|---|---|
| 7 | Cached FieldInfo for UnitsUtility.locale once, instead of Traverse.Create(...).Field(...) on every call |
Removes per-frame reflection allocations |
| 8 | Removed redundant Traverse...SetValue write-backs for cache_* fields |
They were no-ops β Harmony's ref ___field already writes back automatically |
| 9 | Cache-on-miss pattern cacheM ?? (cacheM = locale["m"]) |
Locale lookup happens once, not every call |
| # | Improvement | Impact |
|---|---|---|
| 10 | ConditionalWeakTable<MapLineUI, β¦> replaces Dictionary in MapLineUIPatches |
Fixes a memory leak β destroyed MapLineUI instances are now GC'd automatically |
| 11 | sqrMagnitude > 1e-6f instead of diff != Vector2.zero |
Avoids float-equality pitfalls |
| 12 | Re-enabled error logging in the patch catch block | Silent failures previously hid bugs |
| 13 | Locale type alias (using Locale = UBOAT.Game.Core.Data.Locale;) |
Disambiguates from other Locale types in the namespace tree |
Source/
βββ Constants.cs (new)
βββ ModLoader.cs (cleaned)
βββ UnitsUtilityPatch.cs (renamed + rewritten)
βββ DynamicDistanceHelper.cs (deduplicated)
βββ MapLineUIPatches.cs (leak fixed)
SetCursorPos Y coordinate β Win32 and Unity use different Y-origins; flagged it but didn't change it since your original worked. Switch to Screen.height - y if cursor placement drifts.ProcessMetricUnits β AppendMetric to reflect what they actually do (append to a StringBuilder, not "process").Constants if you want them user-configurable later.LeftAlt/RightAlt.Camera.main lookup in MapLineUIPatches with a cached reference (Camera.main is a FindObjectWithTag call internally).[HarmonyPatch] guard via Prepare() that logs and skips if UnitsUtility.locale field is missing (future-proofing against game updates)./Bundles - no changes
Constants.cs
namespace DynamicDistanceLabels
{
public static class Constants
{
public const string MOD_VERSION = "1.1.0";
public const string MOD_TAG = "[DynamicLabels]";
public const string HARMONY_ID = "com.drexack.uboat.DynamicDistanceLabels";
// 1 km == 1/1.852 nmi
public const float KM_TO_NMI = 1f / 1.852f;
}
}DynamicDistanceHelper.cs
using Cysharp.Text;
using UBOAT.Game.Core.Data; // <-- this was missing
using Locale = UBOAT.Game.Core.Data.Locale; // <-- disambiguate
namespace DynamicDistanceLabels
{
public static class DynamicDistanceHelper
{
public static void AppendMetric(
float kilometers,
ref Utf16ValueStringBuilder sb,
Locale locale,
ref string cacheM,
ref string cacheKm)
{
if (kilometers < 1f)
{
sb.AppendFormat("{0:0}", kilometers * 1000f); // meters
sb.Append(' ');
sb.Append(cacheM ?? (cacheM = locale["m"]));
return;
}
string format =
kilometers < 5f ? "{0:0.00}" :
kilometers < 20f ? "{0:0.0}" :
"{0:0}";
sb.AppendFormat(format, kilometers);
sb.Append(' ');
sb.Append(cacheKm ?? (cacheKm = locale["km"]));
}
public static void AppendNautical(
float nauticalMiles,
ref Utf16ValueStringBuilder sb,
Locale locale,
ref string cacheCables,
ref string cacheNmi)
{
if (nauticalMiles < 1f)
{
// 1 nmi == 10 cables
sb.AppendFormat("{0:0.0}", nauticalMiles * 10f);
sb.Append(' ');
sb.Append(cacheCables ?? (cacheCables = locale["cables"]));
return;
}
string format = nauticalMiles < 20f ? "{0:0.0}" : "{0:0}";
sb.AppendFormat(format, nauticalMiles);
sb.Append(' ');
sb.Append(cacheNmi ?? (cacheNmi = locale["nmi"]));
}
}
}MapLineUIPatches.cs
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using HarmonyLib;
using UBOAT.Game.UI.Map;
using UnityEngine;
namespace DynamicDistanceLabels
{
[HarmonyPatch(typeof(MapLineUI), "PlaceLine")]
public static class MapLineUI_PlaceLine_Patch
{
private sealed class LockedLength { public float Value; }
// ConditionalWeakTable -> entries are GC'd automatically when the
// MapLineUI instance is destroyed. No manual cleanup, no leaks.
private static readonly ConditionalWeakTable<MapLineUI, LockedLength> lockedLengths =
new ConditionalWeakTable<MapLineUI, LockedLength>();
#if UNITY_STANDALONE_WIN
[DllImport("user32.dll")]
private static extern bool SetCursorPos(int X, int Y);
#endif
static void Prefix(MapLineUI __instance, ref Vector2 from, ref Vector2 targetPoint)
{
if (Input.GetKey(KeyCode.R))
{
if (!lockedLengths.TryGetValue(__instance, out var locked))
{
locked = new LockedLength { Value = Vector2.Distance(from, targetPoint) };
lockedLengths.Add(__instance, locked);
}
Vector2 diff = targetPoint - from;
if (diff.sqrMagnitude > 1e-6f)
{
targetPoint = from + diff.normalized * locked.Value;
}
#if UNITY_STANDALONE_WIN
Camera cam = Camera.main;
if (cam != null)
{
Vector3 worldEndpoint = new Vector3(targetPoint.x, 0f, targetPoint.y);
Vector3 screenPoint = cam.WorldToScreenPoint(worldEndpoint);
SetCursorPos((int)screenPoint.x, (int)screenPoint.y);
}
#endif
}
else
{
lockedLengths.Remove(__instance);
}
}
}
}ModLoader.cs
using System;
using HarmonyLib;
using UBOAT.Game; // <-- IUserMod
using UBOAT.Game.Core.Serialization;
using UnityEngine;
namespace DynamicDistanceLabels
{
[NonSerializedInGameState]
public class ModLoader : IUserMod
{
public void OnLoaded()
{
try
{
var harmony = new Harmony(Constants.HARMONY_ID);
harmony.PatchAll();
Debug.Log($"{Constants.MOD_TAG} v{Constants.MOD_VERSION} loaded. All patches applied.");
}
catch (Exception e)
{
Debug.LogError($"{Constants.MOD_TAG} Error during patching:");
Debug.LogException(e);
}
}
}
}UnitsUtilityPatch.cs
using System;
using System.Reflection;
using Cysharp.Text;
using HarmonyLib;
using UBOAT.Game.Core;
using UBOAT.Game.Core.Data;
using UnityEngine;
using Locale = UBOAT.Game.Core.Data.Locale; // <-- disambiguate
namespace DynamicDistanceLabels
{
[HarmonyPatch(typeof(UnitsUtility), "KilometersToPreferredUnits",
new[] { typeof(Utf16ValueStringBuilder), typeof(float), typeof(bool) },
new[] { ArgumentType.Ref, ArgumentType.Normal, ArgumentType.Normal })]
public static class UnitsUtilityPatch
{
// Cache the FieldInfo once; avoids per-call Traverse allocations.
private static readonly FieldInfo LocaleField =
AccessTools.Field(typeof(UnitsUtility), "locale");
static bool Prefix(
ref Utf16ValueStringBuilder stringBuilder,
float value,
ref UserSettings ___userSettings,
ref string ___cache_nmi,
ref string ___cache_cables,
ref string ___cache_km,
ref string ___cache_m)
{
try
{
var locale = (Locale)LocaleField.GetValue(null);
if (locale == null) return true; // fall through to original
bool altPressed = Input.GetKey(KeyCode.LeftAlt) || Input.GetKey(KeyCode.RightAlt);
Units units = ___userSettings.GameplaySettings.units;
// Alt toggles to the opposite unit system.
bool useNautical = (units == Units.Nautical) ^ altPressed;
if (useNautical)
{
DynamicDistanceHelper.AppendNautical(
value * Constants.KM_TO_NMI, ref stringBuilder, locale,
ref ___cache_cables, ref ___cache_nmi);
}
else
{
DynamicDistanceHelper.AppendMetric(
value, ref stringBuilder, locale,
ref ___cache_m, ref ___cache_km);
}
return false;
}
catch (Exception ex)
{
Debug.LogError($"{Constants.MOD_TAG} Exception in UnitsUtility patch: {ex}");
return true; // safe fallback to original
}
}
}
}
Always have Blueprints
https://steamcommunity.com/sharedfiles/filedetails/?id=3670612683
Opus 4.7 review
Details
ShipSelectorUIPatch.cscontainsShipSelectorDetailsUIPatchclass. Confusing.___playerCareerβ if the field is ever null, Harmony will throw and the menu will break.OnLoadedβ ifPatchAllever fails, the whole mod fails silently with no log.1β making the minimum points configurable (or at least a const) is cleaner.[HarmonyPrefix]attribute β works without it via naming, but explicit is better.ModLoader.cs.Opus 4.7 improvements
Details
ShipSelectorUIPatchstatic[HarmonyPrefix]attribute addednameof(ShipSelectorUI.Open)"Open"___playerCareerMinimumUpgradePointsconst<= 0to< MinimumUpgradePointsOnLoadedModIdconst exposed viaModLoaderusingsSystem.Reflection,Resources,SceneManagement)Opus 4.7 tips
Details
OnLoadedand assign to a static field that the patch reads.public const string Version = "1.0.0";and log it β useful when users report bugs.PlayerCareerdirectly instead of UI: If the goal is "always have blueprints," you could clamp the setter onShipUpgradePointsitself. The UI patch only triggers when the selector is opened β if some other code path checks points first, you'd miss it. UI patch is fine for now though, lower risk of side effects.ModLoader.csDetails
ShipSelectorUIPatch.csDetails