Compare commits
1 commit
| Author | SHA1 | Date | |
|---|---|---|---|
| 766822d15a |
10 changed files with 252 additions and 14 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -3,3 +3,4 @@ bin/
|
|||
*.user
|
||||
.vs/
|
||||
*.suo
|
||||
*.zip
|
||||
|
|
|
|||
124
CarDebugVisualizer.cs
Normal file
124
CarDebugVisualizer.cs
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
using System.Collections.Generic;
|
||||
using Model;
|
||||
using UnityEngine;
|
||||
|
||||
namespace RailroaderPhysicsOverhaul;
|
||||
|
||||
[UnityEngine.DefaultExecutionOrder(10000)] // run after all game LateUpdates so our MPB is last to write
|
||||
public class CarDebugVisualizer : MonoBehaviour
|
||||
{
|
||||
public static CarDebugVisualizer Instance { get; private set; }
|
||||
public int TintedCount => _curr.Count;
|
||||
|
||||
// A 1x1 white texture fed via MPB overrides _MainTex so the shader renders
|
||||
// texture*color = white*color = solid color, regardless of the car's original texture.
|
||||
Texture2D _whiteTex;
|
||||
readonly MaterialPropertyBlock _mpb = new();
|
||||
|
||||
readonly Dictionary<Car, Renderer[]> _rendCache = new();
|
||||
readonly Dictionary<Car, Color> _curr = new();
|
||||
readonly Dictionary<Car, Color> _prev = new();
|
||||
|
||||
static readonly Color ColFrozen = new(1.00f, 0.90f, 0.00f); // yellow
|
||||
static readonly Color ColFastPath = new(0.00f, 1.00f, 1.00f); // cyan
|
||||
static readonly Color ColFullPath = new(1.00f, 0.00f, 1.00f); // magenta
|
||||
|
||||
void Awake()
|
||||
{
|
||||
Instance = this;
|
||||
_whiteTex = new Texture2D(1, 1, TextureFormat.RGBA32, false)
|
||||
{ hideFlags = HideFlags.HideAndDontSave };
|
||||
_whiteTex.SetPixel(0, 0, Color.white);
|
||||
_whiteTex.Apply();
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
ClearAll();
|
||||
if (_whiteTex) Destroy(_whiteTex);
|
||||
Instance = null;
|
||||
}
|
||||
|
||||
void LateUpdate()
|
||||
{
|
||||
_prev.Clear();
|
||||
foreach (var kv in _curr) _prev[kv.Key] = kv.Value;
|
||||
_curr.Clear();
|
||||
|
||||
if (Main.Settings.DebugHighlightFrozen)
|
||||
Collect(ConsistFreezer.FrozenCars, ColFrozen);
|
||||
if (Main.Settings.DebugHighlightFastPath)
|
||||
Collect(ConsistLOD.FastPathCars, ColFastPath);
|
||||
if (Main.Settings.DebugHighlightFullPath)
|
||||
Collect(ConsistLOD.FullPathCars, ColFullPath);
|
||||
|
||||
foreach (var (car, color) in _curr)
|
||||
ApplyTint(car, color);
|
||||
|
||||
foreach (var car in _prev.Keys)
|
||||
if (!_curr.ContainsKey(car))
|
||||
ClearTint(car);
|
||||
|
||||
if (Time.frameCount % 300 == 0)
|
||||
PurgeCache();
|
||||
}
|
||||
|
||||
void Collect(HashSet<Car> source, Color color)
|
||||
{
|
||||
foreach (var car in source)
|
||||
if (car != null && !_curr.ContainsKey(car))
|
||||
_curr[car] = color;
|
||||
}
|
||||
|
||||
void ApplyTint(Car car, Color color)
|
||||
{
|
||||
foreach (var r in GetRenderers(car))
|
||||
{
|
||||
if (r == null) continue;
|
||||
_mpb.Clear();
|
||||
_mpb.SetColor("_Color", color);
|
||||
_mpb.SetColor("_BaseColor", color);
|
||||
_mpb.SetTexture("_MainTex", _whiteTex); // built-in RP
|
||||
_mpb.SetTexture("_BaseMap", _whiteTex); // URP
|
||||
_mpb.SetTexture("_BaseColorMap", _whiteTex); // HDRP
|
||||
r.SetPropertyBlock(_mpb);
|
||||
}
|
||||
}
|
||||
|
||||
void ClearTint(Car car)
|
||||
{
|
||||
foreach (var r in GetRenderers(car))
|
||||
if (r != null) r.SetPropertyBlock(null);
|
||||
}
|
||||
|
||||
void ClearAll()
|
||||
{
|
||||
foreach (var car in _curr.Keys) ClearTint(car);
|
||||
foreach (var car in _prev.Keys) ClearTint(car);
|
||||
_curr.Clear();
|
||||
_prev.Clear();
|
||||
_rendCache.Clear();
|
||||
}
|
||||
|
||||
Renderer[] GetRenderers(Car car)
|
||||
{
|
||||
if (!_rendCache.TryGetValue(car, out var renderers) || renderers.Length == 0)
|
||||
{
|
||||
renderers = car.BodyTransform != null
|
||||
? car.BodyTransform.GetComponentsInChildren<Renderer>()
|
||||
: System.Array.Empty<Renderer>();
|
||||
// Don't cache empty results — retry next frame until the mesh loads
|
||||
if (renderers.Length > 0)
|
||||
_rendCache[car] = renderers;
|
||||
}
|
||||
return renderers;
|
||||
}
|
||||
|
||||
void PurgeCache()
|
||||
{
|
||||
var dead = new List<Car>();
|
||||
foreach (var car in _rendCache.Keys)
|
||||
if (car == null) dead.Add(car);
|
||||
foreach (var car in dead) _rendCache.Remove(car);
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ public static class ConsistFreezer
|
|||
// Auto-freeze: skip the Verlet tick for consists that are far from the camera
|
||||
// and moving slowly enough that physics quality there doesn't matter.
|
||||
// This is our integrated replacement for the stock optimizer (AllCarsAtRest only).
|
||||
public static bool ExcludeLocomotives = true;
|
||||
public static bool AutoFreezeEnabled = true;
|
||||
public static float AutoFreezeDistance = 200f; // meters
|
||||
public static float AutoFreezeSpeedThreshold = 0.3f; // m/s (~0.7 mph)
|
||||
|
|
@ -31,18 +32,30 @@ public static class ConsistFreezer
|
|||
public static int LastAtRestCars { get; private set; }
|
||||
public static int LastDistanceCars { get; private set; }
|
||||
|
||||
// Per-tick set of cars whose Verlet tick is being skipped — for debug visualization.
|
||||
public static readonly HashSet<Car> FrozenCars = new();
|
||||
|
||||
internal static void FlushFrameCounts()
|
||||
{
|
||||
LastAtRestCars = _frameAtRestCars;
|
||||
LastDistanceCars = _frameDistanceCars;
|
||||
_frameAtRestCars = 0;
|
||||
_frameDistanceCars = 0;
|
||||
FrozenCars.Clear();
|
||||
}
|
||||
|
||||
// Called from ShouldSkipTickPatch. Internal so the patch class (same file) can reach it.
|
||||
internal static bool ShouldAutoFreeze(IntegrationSet set)
|
||||
{
|
||||
if (!AutoFreezeEnabled) return false;
|
||||
// ShouldSkipTick is per-IntegrationSet — we can't freeze freight cars while keeping
|
||||
// a coupled loco ticking. If ExcludeLocomotives is on, the whole consist stays live
|
||||
// so AI waypoint queues on the loco don't get interrupted by a physics freeze.
|
||||
if (ExcludeLocomotives)
|
||||
{
|
||||
foreach (Car car in set.Cars)
|
||||
if (car is BaseLocomotive) return false;
|
||||
}
|
||||
Camera cam = Camera.main;
|
||||
if (cam == null) return false;
|
||||
Vector3 camPos = cam.transform.position;
|
||||
|
|
@ -79,6 +92,7 @@ static class ShouldSkipTickPatch
|
|||
{
|
||||
if (ConsistFreezer.IsFrozen(__instance.Id))
|
||||
{
|
||||
foreach (Car c in __instance.Cars) ConsistFreezer.FrozenCars.Add(c);
|
||||
__result = true;
|
||||
return false;
|
||||
}
|
||||
|
|
@ -93,6 +107,12 @@ static class ShouldSkipTickPatch
|
|||
// the stock optimizer mod installed.
|
||||
if (__instance.AllCarsAtRest())
|
||||
{
|
||||
if (ConsistFreezer.ExcludeLocomotives)
|
||||
{
|
||||
foreach (Car c in __instance.Cars)
|
||||
if (c is BaseLocomotive) { __result = false; return false; }
|
||||
}
|
||||
foreach (Car c in __instance.Cars) ConsistFreezer.FrozenCars.Add(c);
|
||||
ConsistFreezer._frameAtRestCars += __instance.NumberOfCars;
|
||||
__result = true;
|
||||
return false;
|
||||
|
|
@ -100,7 +120,10 @@ static class ShouldSkipTickPatch
|
|||
|
||||
__result = ConsistFreezer.ShouldAutoFreeze(__instance);
|
||||
if (__result)
|
||||
{
|
||||
foreach (Car c in __instance.Cars) ConsistFreezer.FrozenCars.Add(c);
|
||||
ConsistFreezer._frameDistanceCars += __instance.NumberOfCars;
|
||||
}
|
||||
return false; // always override — stock getter never runs
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,10 +13,11 @@ public static class ConsistLOD
|
|||
{
|
||||
public static bool Enabled = true;
|
||||
public static float DistanceThreshold = 30f;
|
||||
// Must be a power of 2 (1, 2, 4, 8). 1 = full path every tick (no dead-reckoning).
|
||||
// Must be a power of 2 (1, 2, 4, 8, 16). 1 = full path every tick (no dead-reckoning).
|
||||
public static int ResyncInterval = 4;
|
||||
|
||||
// Blacklist/whitelist — when enabled, filters which consists get LOD treatment.
|
||||
public static bool ExcludeLocomotives = true;
|
||||
public static bool BlacklistEnabled = false;
|
||||
public static bool IsBlacklist = true; // true = listed consists skip LOD; false = only listed use LOD
|
||||
public static readonly HashSet<string> RoadNumberList = new(System.StringComparer.OrdinalIgnoreCase);
|
||||
|
|
@ -25,6 +26,10 @@ public static class ConsistLOD
|
|||
public static int LastFastPathCount;
|
||||
public static int LastFullPathCount;
|
||||
|
||||
// Per-tick sets for debug visualization — cleared at FixedUpdate start, populated by the patch.
|
||||
public static readonly HashSet<Car> FastPathCars = new();
|
||||
public static readonly HashSet<Car> FullPathCars = new();
|
||||
|
||||
static float DistanceSq => DistanceThreshold * DistanceThreshold;
|
||||
|
||||
// Per-consist LOD eligibility cache — rebuilt lazily, flushed periodically and on settings change.
|
||||
|
|
@ -35,6 +40,7 @@ public static class ConsistLOD
|
|||
internal static bool ShouldUseFastPath(Car car)
|
||||
{
|
||||
if (!Enabled) return false;
|
||||
if (ExcludeLocomotives && car is Model.BaseLocomotive) return false;
|
||||
if (!car.EndGearA.IsCoupled || !car.EndGearB.IsCoupled) return false;
|
||||
if (car.BodyTransform == null) return false;
|
||||
Camera cam = Camera.main;
|
||||
|
|
@ -109,6 +115,7 @@ static class PositionWheelBoundsFrontLODPatch
|
|||
if (!update || !ConsistLOD.ShouldUseFastPath(__instance))
|
||||
{
|
||||
_fullCount++;
|
||||
ConsistLOD.FullPathCars.Add(__instance);
|
||||
return true; // run original
|
||||
}
|
||||
|
||||
|
|
@ -122,10 +129,12 @@ static class PositionWheelBoundsFrontLODPatch
|
|||
if (isResync)
|
||||
{
|
||||
_fullCount++;
|
||||
ConsistLOD.FastPathCars.Add(__instance); // logically fast-path, just doing a scheduled sync
|
||||
return true; // let original run this tick
|
||||
}
|
||||
|
||||
_fastCount++;
|
||||
ConsistLOD.FastPathCars.Add(__instance);
|
||||
|
||||
// 1. Update track physics bounds — constraint solver and couplers need these.
|
||||
float boundsSpan = __instance.carLength - __instance.wheelInsetF - __instance.wheelInsetR;
|
||||
|
|
@ -182,6 +191,8 @@ static class TrainControllerFixedUpdateCounterPatch
|
|||
static void Prefix()
|
||||
{
|
||||
ConsistLOD._globalTick++;
|
||||
ConsistLOD.FastPathCars.Clear();
|
||||
ConsistLOD.FullPathCars.Clear();
|
||||
PositionWheelBoundsFrontLODPatch.ResetFrameCounts();
|
||||
ConsistFreezer.FlushFrameCounts();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"Id": "RailroaderPhysicsOverhaul",
|
||||
"DisplayName": "Physics Overhaul",
|
||||
"DisplayName": "Physics Optimizer",
|
||||
"Author": "seton",
|
||||
"Version": "0.1.1",
|
||||
"Version": "0.1.2",
|
||||
"ManagerVersion": "0.27.0",
|
||||
"GameVersionPoint": "0",
|
||||
"AssemblyName": "RailroaderPhysicsOverhaul.dll",
|
||||
|
|
|
|||
3
Main.cs
3
Main.cs
|
|
@ -25,6 +25,8 @@ public static class Main
|
|||
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;
|
||||
|
|
@ -38,6 +40,7 @@ public static class Main
|
|||
var overlay = go.AddComponent<PhysicsOverlayGUI>();
|
||||
overlay.Visible = Settings.ShowOverlay;
|
||||
overlay.Opacity = Settings.OverlayOpacity;
|
||||
go.AddComponent<CarDebugVisualizer>();
|
||||
|
||||
// UMM settings panel callbacks.
|
||||
modEntry.OnGUI = entry => SettingsGUI.Draw(entry, Settings);
|
||||
|
|
|
|||
|
|
@ -8,10 +8,27 @@
|
|||
<OutputPath>..\..\Railroader\Mods\RailroaderPhysicsOverhaul\</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<!-- Don't copy game DLLs to output — they live in the game folder -->
|
||||
<!-- Don't copy game DLLs to output - they live in the game folder -->
|
||||
<CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>
|
||||
<ModVersion>0.1.2</ModVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Release build only: package the mod into a zip ready for Forgejo/UMM upload.
|
||||
Zip structure: RailroaderPhysicsOverhaul/Info.json + .dll
|
||||
UMM drag-and-drop install expects exactly this layout. -->
|
||||
<Target Name="PackageRelease" AfterTargets="Build" Condition="'$(Configuration)' == 'Release'">
|
||||
<PropertyGroup>
|
||||
<_PkgDir>$(MSBuildProjectDirectory)\obj\pkg\RailroaderPhysicsOverhaul\</_PkgDir>
|
||||
<_ZipOut>$(MSBuildProjectDirectory)\RailroaderPhysicsOverhaul-$(ModVersion).zip</_ZipOut>
|
||||
</PropertyGroup>
|
||||
<RemoveDir Directories="$(MSBuildProjectDirectory)\obj\pkg" />
|
||||
<MakeDir Directories="$(_PkgDir)" />
|
||||
<Copy SourceFiles="$(OutputPath)RailroaderPhysicsOverhaul.dll;$(OutputPath)Info.json"
|
||||
DestinationFolder="$(_PkgDir)" />
|
||||
<Exec Command="powershell -NoProfile -Command "Compress-Archive -Path '$(MSBuildProjectDirectory)\obj\pkg\RailroaderPhysicsOverhaul' -DestinationPath '$(_ZipOut)' -Force"" />
|
||||
<Message Text="Release zip ready: $(_ZipOut)" Importance="High" />
|
||||
</Target>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Info.json is required by UMM in the output mod folder -->
|
||||
<None Include="Info.json">
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ public class PhysicsOverlayGUI : MonoBehaviour
|
|||
|
||||
string _lodStatsStr = "";
|
||||
string _freezeStatsStr = "";
|
||||
string _debugStatsStr = "";
|
||||
int _lodStatsFrame;
|
||||
|
||||
GUIStyle _windowStyle;
|
||||
|
|
@ -164,10 +165,20 @@ public class PhysicsOverlayGUI : MonoBehaviour
|
|||
int atRest = ConsistFreezer.LastAtRestCars;
|
||||
int byDist = ConsistFreezer.LastDistanceCars;
|
||||
_freezeStatsStr = $"stopped:{atRest} dist/spd:{byDist}";
|
||||
|
||||
bool anyDbg = Main.Settings.DebugHighlightFrozen ||
|
||||
Main.Settings.DebugHighlightFastPath ||
|
||||
Main.Settings.DebugHighlightFullPath;
|
||||
_debugStatsStr = anyDbg
|
||||
? $"dbg — frozen:{ConsistFreezer.FrozenCars.Count} fast:{ConsistLOD.FastPathCars.Count} full:{ConsistLOD.FullPathCars.Count} tinted:{CarDebugVisualizer.Instance?.TintedCount ?? 0}"
|
||||
: "";
|
||||
}
|
||||
GUILayout.Label(_lodStatsStr, _dimLabel);
|
||||
GUILayout.EndHorizontal();
|
||||
|
||||
if (_debugStatsStr.Length > 0)
|
||||
GUILayout.Label(_debugStatsStr, _dimLabel);
|
||||
|
||||
// Auto-freeze row — shows cars skipping the Verlet tick each physics step
|
||||
GUILayout.BeginHorizontal();
|
||||
GUILayout.Label("Auto Freeze", _dimLabel, GUILayout.Width(130f));
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Railroader Physics Overhaul
|
||||
# Railroader Physics Optimizer
|
||||
|
||||
A [Unity Mod Manager](https://www.nexusmods.com/site/mods/21) mod for [Railroader](https://store.steampowered.com/app/1638770/Railroader/) that reduces CPU time spent on train physics without sacrificing gameplay.
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ The mod patches `Car.PositionWheelBoundsFront` with a Harmony prefix. When a car
|
|||
5. **`LocationF`/`LocationR`** are updated each tick so the segment cache never goes stale.
|
||||
|
||||
### Auto Freeze detail
|
||||
`ShouldSkipTick` is replaced. The stock implementation skips the entire solver when all cars are at rest; this mod tracks each car individually and skips cars that are far away and *very* slow, reducing wasteful simulation of parked cars.
|
||||
`ShouldSkipTick` is replaced. This mod tracks each car individually and skips cars that are far away and *very* slow, reducing wasteful simulation of parked cars.
|
||||
|
||||
### Sampling model (the "% of FixedUpdate" number)
|
||||
The profiler measures time with `Stopwatch.GetTimestamp()` (nanosecond resolution, no GC pressure). It accumulates over 60-tick windows. "% of FixedUpdate" is how much of the total physics budget `Tick()` alone consumed that window — a high percentage means Verlet integration dominates; a low percentage means air brakes, coupler forces, or position updates are the bottleneck.
|
||||
|
|
@ -78,7 +78,7 @@ Open the in-game console and type `/rpf <subcommand>`:
|
|||
|
||||
## Settings
|
||||
|
||||
Open UMM (Ctrl+F10), select **Physics Overhaul**. All settings auto-save on change.
|
||||
Open UMM (Ctrl+F10), select **Physics Optimizer**. All settings auto-save on change.
|
||||
|
||||
| Setting | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
|
|
|
|||
58
Settings.cs
58
Settings.cs
|
|
@ -11,16 +11,25 @@ public class ModSettings : UnityModManager.ModSettings
|
|||
{
|
||||
public bool OptimizerEnabled = true;
|
||||
public float DistanceThreshold = 30f;
|
||||
public int ResyncInterval = 4;
|
||||
public int ResyncInterval = 8;
|
||||
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)
|
||||
public float AutoFreezeSpeedThreshold = 0.09f; // m/s (~0.2 mph)
|
||||
|
||||
// Road number filter — serialized as a flat string array in Settings.xml.
|
||||
public float OverlayOpacity = 1.0f;
|
||||
public float OverlayOpacity = 0.75f;
|
||||
|
||||
// Locomotive exclusions
|
||||
public bool ExcludeLocosFromLOD = true;
|
||||
public bool ExcludeLocosFromFreeze = true;
|
||||
|
||||
// Debug visualization — tint cars by their current physics state.
|
||||
public bool DebugHighlightFrozen = false;
|
||||
public bool DebugHighlightFastPath = false;
|
||||
public bool DebugHighlightFullPath = false;
|
||||
|
||||
public bool BlacklistEnabled = false;
|
||||
public bool IsBlacklist = true; // true = blacklist, false = whitelist
|
||||
|
|
@ -59,8 +68,8 @@ public class ModSettings : UnityModManager.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 readonly int[] ResyncOptions = { 1, 2, 4, 8, 16 };
|
||||
static readonly string[] ResyncLabels = { "1/1 (max quality)", "1/2", "1/4", "1/8 (default)", "1/16" };
|
||||
|
||||
static string _addInput = "";
|
||||
static string _toRemove = null; // deferred to avoid mutating list during enumeration
|
||||
|
|
@ -166,6 +175,28 @@ static class SettingsGUI
|
|||
|
||||
GUILayout.Space(12f);
|
||||
|
||||
// ── Locomotive Exclusions ─────────────────────────────────────────────────
|
||||
GUILayout.Label("<b>Locomotive Exclusions</b>", GUILayout.ExpandWidth(false));
|
||||
GUILayout.Space(4f);
|
||||
|
||||
bool newExLOD = GUILayout.Toggle(s.ExcludeLocosFromLOD, " Exclude locomotives from Physics Optimizer");
|
||||
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("<b>Road Number Filter</b>", GUILayout.ExpandWidth(false));
|
||||
GUILayout.Space(4f);
|
||||
|
|
@ -285,6 +316,23 @@ static class SettingsGUI
|
|||
changed = true;
|
||||
}
|
||||
|
||||
GUILayout.Space(12f);
|
||||
|
||||
// ── Debug Visualization ───────────────────────────────────────────────────
|
||||
GUILayout.Label("<b>Debug Visualization</b>", 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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue