railroader-setons-special-s.../src/Modules/PhysicsOptimizer/PhysicsOptimizerModule.cs
seton a1fcf01125 Initial commit: S3 - Seton's Special Sauce v0.2.0
Consolidates two standalone Railroader mods into one UMM "everything mod"
with an optional-module framework. Modules are disabled by default and
toggled per-module from the S3 settings page.

Core framework:
- IModule contract plus ModuleRegistry, with each module owning a Harmony
  instance scoped by its id so only enabled modules patch the game
- Per-module flat JSON settings (SettingsStore). On this Mono runtime
  JsonUtility silently drops nested custom-class fields, so settings stay flat
- Foldout-per-module settings panel plus a detector that offers to disable the
  old standalone mods if they are still installed

Modules (moved over to parity, verified in-game):
- Physics Optimizer (was RailroaderPhysicsOverhaul): LOD fast-path and
  auto-freeze, profiler overlay, debug car tinting, /rpf console commands
- Map Popout (was RRPopout): native map detach window. Pure-UMM install that
  drops the winhttp proxy and LoadLibrary's RRPopout.dll from the mod folder.
  Native Win32 + D3D11 + Dear ImGui engine included. Fixes a latent break where
  the now-private MapBuilder.UpdateForZoom() is reached via Traverse.

Build: dotnet for the managed assembly (netstandard2.1) and CMake for the
native DLL. build-local.ps1 installs into the game, build-release.ps1 packages
the UMM drag-install zip.
2026-06-17 14:17:41 -04:00

99 lines
3.8 KiB
C#

using System;
using HarmonyLib;
using S3.Core;
using UnityEngine;
namespace S3.Modules.PhysicsOptimizer;
/// <summary>
/// S³ module wrapping the physics-CPU optimizations (LOD fast-path + auto-freeze)
/// plus the profiler overlay and debug visualizer.
///
/// Owns a Harmony instance keyed "S3.physics" and patches only its own types, so
/// the patches exist only while the module is enabled. <see cref="Settings"/> is
/// a static instance the overlay/visualizer read directly.
/// </summary>
public sealed class PhysicsOptimizerModule : IModule
{
private const string SettingsFile = "S3.physics.json";
public static PhysicsSettings Settings { get; private set; } = new();
private static Harmony? _harmony;
// Attribute-annotated patch classes the module owns. PosCarsTimerPatch is NOT
// here — it has no [HarmonyPatch] attribute and is applied dynamically by
// PhysicsTimer.TryPatchPositionCars (method name varies by game version).
private static readonly Type[] PatchTypes =
{
typeof(PositionWheelBoundsFrontLODPatch),
typeof(TrainControllerFixedUpdateCounterPatch),
typeof(ShouldSkipTickPatch),
typeof(TickTimerPatch),
typeof(FixedUpdateTimerPatch),
typeof(ConsoleCommandPatch),
};
public PhysicsOptimizerModule() => Settings = SettingsStore.Load<PhysicsSettings>(SettingsFile);
public string Id => "physics";
public string DisplayName => "Physics Optimizer";
public string Description =>
"Cuts CPU time spent on train physics (LOD fast-path + auto-freeze for far/slow consists). " +
"Includes a profiler overlay and debug car tinting. Console: /rpf";
public bool Enabled
{
get => Settings.enabled;
set => Settings.enabled = value;
}
public void OnEnable()
{
ApplySettingsToStatics();
_harmony = new Harmony("S3.physics");
foreach (Type t in PatchTypes)
_harmony.CreateClassProcessor(t).Patch();
PhysicsTimer.TryPatchPositionCars(_harmony, Main.ModEntry.Logger);
var go = new GameObject("S3.PhysicsOptimizer.Host");
UnityEngine.Object.DontDestroyOnLoad(go);
PhysicsOverlayGUI overlay = go.AddComponent<PhysicsOverlayGUI>();
overlay.Visible = Settings.ShowOverlay;
overlay.Opacity = Settings.OverlayOpacity;
go.AddComponent<CarDebugVisualizer>();
}
public void OnDisable()
{
// Reserved for live toggling. Today modules apply on next launch, so this
// is not called; when it is, unpatch via _harmony.UnpatchAll("S3.physics")
// and destroy the host GameObject.
_harmony?.UnpatchAll("S3.physics");
_harmony = null;
}
public void SaveSettings() => Persist();
internal static void Persist() => SettingsStore.Save(SettingsFile, Settings);
public void DrawSettings() => PhysicsSettingsUI.Draw();
private static void ApplySettingsToStatics()
{
ConsistLOD.Enabled = Settings.OptimizerEnabled;
ConsistLOD.DistanceThreshold = Settings.DistanceThreshold;
ConsistLOD.ResyncInterval = Settings.ResyncInterval;
ConsistLOD.BlacklistEnabled = Settings.BlacklistEnabled;
ConsistLOD.IsBlacklist = Settings.IsBlacklist;
ConsistLOD.RoadNumberList.Clear();
foreach (string name in Settings.RoadNumberList)
ConsistLOD.RoadNumberList.Add(name);
ConsistLOD.ExcludeLocomotives = Settings.ExcludeLocosFromLOD;
ConsistFreezer.ExcludeLocomotives = Settings.ExcludeLocosFromFreeze;
ConsistFreezer.AutoFreezeEnabled = Settings.AutoFreezeEnabled;
ConsistFreezer.AutoFreezeDistance = Settings.AutoFreezeDistance;
ConsistFreezer.AutoFreezeSpeedThreshold = Settings.AutoFreezeSpeedThreshold;
}
}