Features: - ConsistLOD: fast-path Bezier position update for distant cars (PositionAccuracy matched, LocationF/R synced, OnPosition fired, culler sphere kept current) - ConsistFreezer: per-consist auto-freeze replacing stock all-or-nothing optimizer - PhysicsTimer: stacked ring buffers (render, FixedUpdate, Tick, PosCars) + report - PhysicsOverlayGUI: draggable stacked-area chart, reference lines always visible, ON/OFF toggles with persistence, opacity control - Settings: JSON persistence, full UMM settings panel with auto-save - ConsoleCommands: /rpf with freeze/unfreeze/dump/timing/overlay/forceactive/lod
301 lines
12 KiB
C#
301 lines
12 KiB
C#
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<string>();
|
|
|
|
// 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<ModSettings>(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("<b>Physics Optimizer</b>", 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("<b>Auto Freeze</b> (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("<b>Road Number Filter</b>", 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("<b>Profiler Overlay</b>", 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();
|
|
}
|
|
}
|