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.
Always have Blueprints
https://steamcommunity.com/sharedfiles/filedetails/?id=3670612683
Opus 4.7 review
ShipSelectorUIPatch.cs contains ShipSelectorDetailsUIPatch class. Confusing.___playerCareer β if the field is ever null, Harmony will throw and the menu will break.OnLoaded β if PatchAll ever 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
| Change | Reason |
|---|---|
Renamed class to ShipSelectorUIPatch |
Match the file name, less confusion |
Marked class static |
It's a Harmony patch container, never instantiated |
[HarmonyPrefix] attribute added |
Explicit, future-proof against Harmony lookup changes |
nameof(ShipSelectorUI.Open) |
Refactor-safe, compile-time checked vs string "Open" |
Null check on ___playerCareer |
Prevents NRE crashing the UI |
MinimumUpgradePoints const |
Easier to tweak, self-documenting |
Changed <= 0 to < MinimumUpgradePoints |
Consistent with the constant's intent |
Try/catch + logging in OnLoaded |
You can see in the log if the mod loaded |
ModId const exposed via ModLoader |
Single source of truth, reused in log messages |
Removed unused usings |
Cleanup (System.Reflection, Resources, SceneManagement) |
| Consistent spaces (no tabs) | Style consistency |
Opus 4.7 tips
OnLoaded and assign to a static field that the patch reads.public const string Version = "1.0.0"; and log it β useful when users report bugs.PlayerCareer directly instead of UI: If the goal is "always have blueprints," you could clamp the setter on ShipUpgradePoints itself. 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.cs
using DWS.Common.InjectionFramework;
using HarmonyLib;
using UBOAT.Game;
using UBOAT.Game.Core.Serialization;
using UnityEngine;
namespace Uboat.Mods.W1ngZ.AlwaysHaveBlueprints
{
[NonSerializedInGameState]
public class ModLoader : IUserMod
{
public const string ModId = "Uboat.Mods.W1ngZ.AlwaysHaveBlueprints";
public void OnLoaded()
{
try
{
var harmony = new Harmony(ModId);
harmony.PatchAll();
Debug.Log($"[{ModId}] Loaded and patched successfully.");
}
catch (System.Exception ex)
{
Debug.LogError($"[{ModId}] Failed to apply Harmony patches: {ex}");
}
}
}
}ShipSelectorUIPatch.cs
using HarmonyLib;
using UBOAT.Game.Sandbox;
using UBOAT.Game.UI;
using UnityEngine;
namespace Uboat.Mods.W1ngZ.AlwaysHaveBlueprints
{
/// <summary>
/// Ensures the player always has at least one ship upgrade point
/// available when opening the ship selector UI, so blueprints are
/// never locked behind missing upgrade points.
/// </summary>
[HarmonyPatch(typeof(ShipSelectorUI), nameof(ShipSelectorUI.Open))]
public static class ShipSelectorUIPatch
{
private const int MinimumUpgradePoints = 1;
[HarmonyPrefix]
private static void Prefix(PlayerCareer ___playerCareer)
{
if (___playerCareer == null)
{
Debug.LogWarning($"[{ModLoader.ModId}] PlayerCareer was null in ShipSelectorUI.Open; skipping.");
return;
}
if (___playerCareer.ShipUpgradePoints < MinimumUpgradePoints)
{
___playerCareer.ShipUpgradePoints = MinimumUpgradePoints;
}
}
}
}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
}
}
}
}
Custom Stack Limits
https://steamcommunity.com/sharedfiles/filedetails/?id=3437617137
Opus 4.7 tips
Details
TargetMethods().CallvirtβCallβGetStackLimitis a static extension method, soCallis the correct opcode (Callvirtworks but is semantically wrong and slower).ldfld) too, not just operand β safer against future edge cases (writes, field-address loads).__originalMethodparameter β no more hardcoded strings that drift out of sync with the patches.GetStackLimit(null data / null name).Settingsβ change one constant to bump all 30 limits.FieldInfo/MethodInforesolved once, not per instruction.ModLoader(usesLogErrorinstead ofLogFormatfor failures).ModLoader.csDetails
Patches.csDetails
SandboxEquipmentDataExtensions.csDetails
Settings.csDetails
StackLimitTranspiler.csDetails