using System.Linq;
using UnityEngine;
namespace S3.Modules.PhysicsOptimizer;
// Renders the Physics Optimizer settings as the body of its foldout in the S³
// settings panel. Writes both the persisted settings and the runtime statics on
// every change, then persists via PhysicsOptimizerModule.Persist().
static class PhysicsSettingsUI
{
static readonly int[] ResyncOptions = { 1, 2, 4, 8, 16 };
static readonly string[] ResyncLabels = { "1/1 (max quality)", "1/2", "1/4 (default)", "1/8", "1/16" };
static string _addInput = "";
static string _toRemove = null; // deferred to avoid mutating list during enumeration
public static void Draw()
{
PhysicsSettings s = PhysicsOptimizerModule.Settings;
bool changed = false;
GUILayout.BeginVertical();
// ── LOD Fast-Path ─────────────────────────────────────────────────────────
GUILayout.Label("LOD Fast-Path", 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);
// ── Locomotive Exclusions ─────────────────────────────────────────────────
GUILayout.Label("Locomotive Exclusions", GUILayout.ExpandWidth(false));
GUILayout.Space(4f);
bool newExLOD = GUILayout.Toggle(s.ExcludeLocosFromLOD, " Exclude locomotives from LOD fast-path");
if (newExLOD != s.ExcludeLocosFromLOD)
{
s.ExcludeLocosFromLOD = newExLOD;
ConsistLOD.ExcludeLocomotives = newExLOD;
changed = true;
}
bool newExFreeze = GUILayout.Toggle(s.ExcludeLocosFromFreeze, " Exclude locomotives from Auto Freeze");
if (newExFreeze != s.ExcludeLocosFromFreeze)
{
s.ExcludeLocosFromFreeze = newExFreeze;
ConsistFreezer.ExcludeLocomotives = newExFreeze;
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);
// ── Debug Visualization ───────────────────────────────────────────────────
GUILayout.Label("Debug Visualization", GUILayout.ExpandWidth(false));
GUILayout.Space(4f);
GUILayout.Label(" Tints car models by physics state. Useful for verifying LOD and freeze behaviour.", GUI.skin.label);
GUILayout.Space(4f);
bool newHF = GUILayout.Toggle(s.DebugHighlightFrozen, " Highlight frozen cars (yellow)");
if (newHF != s.DebugHighlightFrozen) { s.DebugHighlightFrozen = newHF; changed = true; }
bool newHFP = GUILayout.Toggle(s.DebugHighlightFastPath, " Highlight fast-path cars (cyan)");
if (newHFP != s.DebugHighlightFastPath) { s.DebugHighlightFastPath = newHFP; changed = true; }
bool newHFU = GUILayout.Toggle(s.DebugHighlightFullPath, " Highlight full-accuracy cars (magenta)");
if (newHFU != s.DebugHighlightFullPath) { s.DebugHighlightFullPath = newHFU; changed = true; }
GUILayout.EndVertical();
if (changed)
PhysicsOptimizerModule.Persist();
}
static void SyncList(PhysicsSettings s)
{
ConsistLOD.RoadNumberList.Clear();
foreach (string n in s.RoadNumberList)
ConsistLOD.RoadNumberList.Add(n);
ConsistLOD.InvalidateConsistCache();
}
}