using System; using System.IO; using System.Linq; using UnityEngine; using UnityModManagerNet; namespace RailroaderPhysicsOverhaul; [Serializable] public class ModSettings : UnityModManager.ModSettings { public bool OptimizerEnabled = true; public float DistanceThreshold = 30f; public int ResyncInterval = 4; public bool ShowOverlay = true; // Auto-freeze: skip the Verlet tick for far+slow consists public bool AutoFreezeEnabled = true; public float AutoFreezeDistance = 200f; // meters public float AutoFreezeSpeedThreshold = 0.3f; // m/s (~0.7 mph) // Road number filter — serialized as a flat string array in Settings.xml. public float OverlayOpacity = 1.0f; public bool BlacklistEnabled = false; public bool IsBlacklist = true; // true = blacklist, false = whitelist public string[] RoadNumberList = Array.Empty(); // UMM's XML serializer silently fails on Unity's Mono runtime — use JsonUtility instead. public override void Save(UnityModManager.ModEntry modEntry) { try { File.WriteAllText(Path.Combine(modEntry.Path, "Settings.json"), JsonUtility.ToJson(this, prettyPrint: true)); } catch (Exception e) { modEntry.Logger.Error($"Settings save failed: {e.Message}"); } } public static ModSettings Load(UnityModManager.ModEntry modEntry) { try { string path = Path.Combine(modEntry.Path, "Settings.json"); if (File.Exists(path)) return JsonUtility.FromJson(File.ReadAllText(path)); } catch (Exception e) { modEntry.Logger.Log($"Settings load failed, using defaults: {e.Message}"); } return new ModSettings(); } } // Draws the UMM in-game settings panel. static class SettingsGUI { static readonly int[] ResyncOptions = { 1, 2, 4, 8 }; static readonly string[] ResyncLabels = { "1/1 (max quality)", "1/2", "1/4 (default)", "1/8" }; static string _addInput = ""; static string _toRemove = null; // deferred to avoid mutating list during enumeration public static void Draw(UnityModManager.ModEntry modEntry, ModSettings s) { bool changed = false; GUILayout.BeginVertical(); // ── Physics Optimizer ───────────────────────────────────────────────────── GUILayout.Label("Physics Optimizer", GUILayout.ExpandWidth(false)); GUILayout.Space(4f); bool newEnabled = GUILayout.Toggle(s.OptimizerEnabled, " Enabled (skips expensive 3D updates for cars far from camera)"); if (newEnabled != s.OptimizerEnabled) { s.OptimizerEnabled = newEnabled; ConsistLOD.Enabled = newEnabled; changed = true; } GUILayout.Space(6f); // Distance threshold slider GUILayout.BeginHorizontal(); GUILayout.Label($"Distance threshold: {s.DistanceThreshold:F0} m", GUILayout.Width(220f)); float newDist = GUILayout.HorizontalSlider(s.DistanceThreshold, 5f, 200f, GUILayout.Width(200f)); GUILayout.EndHorizontal(); GUILayout.Label(" Cars farther than this from the camera use dead-reckoning.", GUI.skin.label); if (Mathf.Abs(newDist - s.DistanceThreshold) > 0.5f) { s.DistanceThreshold = Mathf.Round(newDist); ConsistLOD.DistanceThreshold = s.DistanceThreshold; changed = true; } GUILayout.Space(6f); // Resync quality radio buttons GUILayout.Label("Resync quality (fraction of far cars fully updated per tick):"); GUILayout.BeginHorizontal(); for (int i = 0; i < ResyncOptions.Length; i++) { bool selected = s.ResyncInterval == ResyncOptions[i]; if (GUILayout.Toggle(selected, ResyncLabels[i], GUI.skin.button, GUILayout.Width(130f)) && !selected) { s.ResyncInterval = ResyncOptions[i]; ConsistLOD.ResyncInterval = s.ResyncInterval; changed = true; } } GUILayout.EndHorizontal(); GUILayout.Label(" Lower = smoother visuals; higher = cheaper. Cost spread evenly via stagger.", GUI.skin.label); GUILayout.Space(12f); // ── Auto Freeze ─────────────────────────────────────────────────────────── GUILayout.Label("Auto Freeze (replaces stock optimizer)", GUILayout.ExpandWidth(false)); GUILayout.Space(4f); bool newAF = GUILayout.Toggle(s.AutoFreezeEnabled, " Skip Verlet tick for consists that are far away and nearly stopped"); if (newAF != s.AutoFreezeEnabled) { s.AutoFreezeEnabled = newAF; ConsistFreezer.AutoFreezeEnabled = newAF; changed = true; } if (s.AutoFreezeEnabled) { GUILayout.Space(4f); GUILayout.BeginHorizontal(); GUILayout.Label($"Freeze distance: {s.AutoFreezeDistance:F0} m", GUILayout.Width(200f)); float newFDist = GUILayout.HorizontalSlider(s.AutoFreezeDistance, 50f, 1000f, GUILayout.Width(200f)); GUILayout.EndHorizontal(); GUILayout.Label(" Consists with no car closer than this are eligible to freeze.", GUI.skin.label); if (Mathf.Abs(newFDist - s.AutoFreezeDistance) > 1f) { s.AutoFreezeDistance = Mathf.Round(newFDist); ConsistFreezer.AutoFreezeDistance = s.AutoFreezeDistance; changed = true; } GUILayout.Space(4f); GUILayout.BeginHorizontal(); GUILayout.Label($"Speed threshold: {s.AutoFreezeSpeedThreshold * 2.23694f:F1} mph", GUILayout.Width(200f)); float newSpd = GUILayout.HorizontalSlider(s.AutoFreezeSpeedThreshold * 2.23694f, 0f, 10f, GUILayout.Width(200f)); GUILayout.EndHorizontal(); GUILayout.Label(" Consist must be slower than this to be eligible.", GUI.skin.label); float newSpdMs = newSpd / 2.23694f; if (Mathf.Abs(newSpdMs - s.AutoFreezeSpeedThreshold) > 0.01f) { s.AutoFreezeSpeedThreshold = newSpdMs; ConsistFreezer.AutoFreezeSpeedThreshold = s.AutoFreezeSpeedThreshold; changed = true; } } GUILayout.Space(12f); // ── Road Number Filter ──────────────────────────────────────────────────── GUILayout.Label("Road Number Filter", GUILayout.ExpandWidth(false)); GUILayout.Space(4f); bool newBlEnabled = GUILayout.Toggle(s.BlacklistEnabled, " Enable road number filter"); if (newBlEnabled != s.BlacklistEnabled) { s.BlacklistEnabled = newBlEnabled; ConsistLOD.BlacklistEnabled = newBlEnabled; ConsistLOD.InvalidateConsistCache(); changed = true; } if (s.BlacklistEnabled) { GUILayout.Space(4f); // Mode toggle GUILayout.BeginHorizontal(); GUILayout.Label("Mode:", GUILayout.Width(45f)); if (GUILayout.Toggle(s.IsBlacklist, " Blacklist (listed consists skip optimizer)", GUILayout.ExpandWidth(false)) && !s.IsBlacklist) { s.IsBlacklist = true; ConsistLOD.IsBlacklist = true; ConsistLOD.InvalidateConsistCache(); changed = true; } GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); GUILayout.Space(45f); if (GUILayout.Toggle(!s.IsBlacklist, " Whitelist (only listed consists use optimizer)", GUILayout.ExpandWidth(false)) && s.IsBlacklist) { s.IsBlacklist = false; ConsistLOD.IsBlacklist = false; ConsistLOD.InvalidateConsistCache(); changed = true; } GUILayout.EndHorizontal(); GUILayout.Space(6f); // Add entry GUILayout.BeginHorizontal(); GUILayout.Label("Road number:", GUILayout.Width(110f)); _addInput = GUILayout.TextField(_addInput, GUILayout.Width(160f)); if (GUILayout.Button("Add", GUILayout.Width(50f))) { string trimmed = _addInput.Trim(); if (trimmed.Length > 0 && !s.RoadNumberList.Contains(trimmed)) { s.RoadNumberList = s.RoadNumberList.Append(trimmed).ToArray(); SyncList(s); changed = true; } _addInput = ""; } GUILayout.EndHorizontal(); GUILayout.Label(" Enter the full display name as shown in-game (e.g. \"UP 1234\").", GUI.skin.label); GUILayout.Space(4f); // Current entries if (s.RoadNumberList.Length == 0) { GUILayout.Label(" (no entries)", GUI.skin.label); } else { foreach (string name in s.RoadNumberList) { GUILayout.BeginHorizontal(); GUILayout.Label(name, GUILayout.Width(180f)); if (GUILayout.Button("Remove", GUILayout.Width(70f))) _toRemove = name; GUILayout.EndHorizontal(); } // Apply deferred removal (can't mutate inside foreach) if (_toRemove != null) { s.RoadNumberList = s.RoadNumberList.Where(n => n != _toRemove).ToArray(); SyncList(s); _toRemove = null; changed = true; } } } GUILayout.Space(12f); // ── Profiler Overlay ────────────────────────────────────────────────────── GUILayout.Label("Profiler Overlay", GUILayout.ExpandWidth(false)); GUILayout.Space(4f); bool newOverlay = GUILayout.Toggle(s.ShowOverlay, " Show in-game profiler overlay"); if (newOverlay != s.ShowOverlay) { s.ShowOverlay = newOverlay; if (PhysicsOverlayGUI.Instance != null) PhysicsOverlayGUI.Instance.Visible = newOverlay; changed = true; } GUILayout.Space(4f); GUILayout.BeginHorizontal(); GUILayout.Label($"Opacity: {s.OverlayOpacity * 100f:F0}%", GUILayout.Width(110f)); float newOpacity = GUILayout.HorizontalSlider(s.OverlayOpacity, 0.1f, 1.0f, GUILayout.Width(200f)); GUILayout.EndHorizontal(); if (Mathf.Abs(newOpacity - s.OverlayOpacity) > 0.01f) { s.OverlayOpacity = newOpacity; if (PhysicsOverlayGUI.Instance != null) PhysicsOverlayGUI.Instance.Opacity = s.OverlayOpacity; changed = true; } GUILayout.EndVertical(); if (changed) s.Save(modEntry); } static void SyncList(ModSettings s) { ConsistLOD.RoadNumberList.Clear(); foreach (string n in s.RoadNumberList) ConsistLOD.RoadNumberList.Add(n); ConsistLOD.InvalidateConsistCache(); } }