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
|
*.user
|
||||||
.vs/
|
.vs/
|
||||||
*.suo
|
*.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
|
// 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.
|
// and moving slowly enough that physics quality there doesn't matter.
|
||||||
// This is our integrated replacement for the stock optimizer (AllCarsAtRest only).
|
// This is our integrated replacement for the stock optimizer (AllCarsAtRest only).
|
||||||
|
public static bool ExcludeLocomotives = true;
|
||||||
public static bool AutoFreezeEnabled = true;
|
public static bool AutoFreezeEnabled = true;
|
||||||
public static float AutoFreezeDistance = 200f; // meters
|
public static float AutoFreezeDistance = 200f; // meters
|
||||||
public static float AutoFreezeSpeedThreshold = 0.3f; // m/s (~0.7 mph)
|
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 LastAtRestCars { get; private set; }
|
||||||
public static int LastDistanceCars { 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()
|
internal static void FlushFrameCounts()
|
||||||
{
|
{
|
||||||
LastAtRestCars = _frameAtRestCars;
|
LastAtRestCars = _frameAtRestCars;
|
||||||
LastDistanceCars = _frameDistanceCars;
|
LastDistanceCars = _frameDistanceCars;
|
||||||
_frameAtRestCars = 0;
|
_frameAtRestCars = 0;
|
||||||
_frameDistanceCars = 0;
|
_frameDistanceCars = 0;
|
||||||
|
FrozenCars.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called from ShouldSkipTickPatch. Internal so the patch class (same file) can reach it.
|
// Called from ShouldSkipTickPatch. Internal so the patch class (same file) can reach it.
|
||||||
internal static bool ShouldAutoFreeze(IntegrationSet set)
|
internal static bool ShouldAutoFreeze(IntegrationSet set)
|
||||||
{
|
{
|
||||||
if (!AutoFreezeEnabled) return false;
|
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;
|
Camera cam = Camera.main;
|
||||||
if (cam == null) return false;
|
if (cam == null) return false;
|
||||||
Vector3 camPos = cam.transform.position;
|
Vector3 camPos = cam.transform.position;
|
||||||
|
|
@ -79,6 +92,7 @@ static class ShouldSkipTickPatch
|
||||||
{
|
{
|
||||||
if (ConsistFreezer.IsFrozen(__instance.Id))
|
if (ConsistFreezer.IsFrozen(__instance.Id))
|
||||||
{
|
{
|
||||||
|
foreach (Car c in __instance.Cars) ConsistFreezer.FrozenCars.Add(c);
|
||||||
__result = true;
|
__result = true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -93,6 +107,12 @@ static class ShouldSkipTickPatch
|
||||||
// the stock optimizer mod installed.
|
// the stock optimizer mod installed.
|
||||||
if (__instance.AllCarsAtRest())
|
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;
|
ConsistFreezer._frameAtRestCars += __instance.NumberOfCars;
|
||||||
__result = true;
|
__result = true;
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -100,7 +120,10 @@ static class ShouldSkipTickPatch
|
||||||
|
|
||||||
__result = ConsistFreezer.ShouldAutoFreeze(__instance);
|
__result = ConsistFreezer.ShouldAutoFreeze(__instance);
|
||||||
if (__result)
|
if (__result)
|
||||||
|
{
|
||||||
|
foreach (Car c in __instance.Cars) ConsistFreezer.FrozenCars.Add(c);
|
||||||
ConsistFreezer._frameDistanceCars += __instance.NumberOfCars;
|
ConsistFreezer._frameDistanceCars += __instance.NumberOfCars;
|
||||||
|
}
|
||||||
return false; // always override — stock getter never runs
|
return false; // always override — stock getter never runs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,11 @@ public static class ConsistLOD
|
||||||
{
|
{
|
||||||
public static bool Enabled = true;
|
public static bool Enabled = true;
|
||||||
public static float DistanceThreshold = 30f;
|
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;
|
public static int ResyncInterval = 4;
|
||||||
|
|
||||||
// Blacklist/whitelist — when enabled, filters which consists get LOD treatment.
|
// Blacklist/whitelist — when enabled, filters which consists get LOD treatment.
|
||||||
|
public static bool ExcludeLocomotives = true;
|
||||||
public static bool BlacklistEnabled = false;
|
public static bool BlacklistEnabled = false;
|
||||||
public static bool IsBlacklist = true; // true = listed consists skip LOD; false = only listed use LOD
|
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);
|
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 LastFastPathCount;
|
||||||
public static int LastFullPathCount;
|
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;
|
static float DistanceSq => DistanceThreshold * DistanceThreshold;
|
||||||
|
|
||||||
// Per-consist LOD eligibility cache — rebuilt lazily, flushed periodically and on settings change.
|
// 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)
|
internal static bool ShouldUseFastPath(Car car)
|
||||||
{
|
{
|
||||||
if (!Enabled) return false;
|
if (!Enabled) return false;
|
||||||
|
if (ExcludeLocomotives && car is Model.BaseLocomotive) return false;
|
||||||
if (!car.EndGearA.IsCoupled || !car.EndGearB.IsCoupled) return false;
|
if (!car.EndGearA.IsCoupled || !car.EndGearB.IsCoupled) return false;
|
||||||
if (car.BodyTransform == null) return false;
|
if (car.BodyTransform == null) return false;
|
||||||
Camera cam = Camera.main;
|
Camera cam = Camera.main;
|
||||||
|
|
@ -109,6 +115,7 @@ static class PositionWheelBoundsFrontLODPatch
|
||||||
if (!update || !ConsistLOD.ShouldUseFastPath(__instance))
|
if (!update || !ConsistLOD.ShouldUseFastPath(__instance))
|
||||||
{
|
{
|
||||||
_fullCount++;
|
_fullCount++;
|
||||||
|
ConsistLOD.FullPathCars.Add(__instance);
|
||||||
return true; // run original
|
return true; // run original
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -122,10 +129,12 @@ static class PositionWheelBoundsFrontLODPatch
|
||||||
if (isResync)
|
if (isResync)
|
||||||
{
|
{
|
||||||
_fullCount++;
|
_fullCount++;
|
||||||
|
ConsistLOD.FastPathCars.Add(__instance); // logically fast-path, just doing a scheduled sync
|
||||||
return true; // let original run this tick
|
return true; // let original run this tick
|
||||||
}
|
}
|
||||||
|
|
||||||
_fastCount++;
|
_fastCount++;
|
||||||
|
ConsistLOD.FastPathCars.Add(__instance);
|
||||||
|
|
||||||
// 1. Update track physics bounds — constraint solver and couplers need these.
|
// 1. Update track physics bounds — constraint solver and couplers need these.
|
||||||
float boundsSpan = __instance.carLength - __instance.wheelInsetF - __instance.wheelInsetR;
|
float boundsSpan = __instance.carLength - __instance.wheelInsetF - __instance.wheelInsetR;
|
||||||
|
|
@ -182,6 +191,8 @@ static class TrainControllerFixedUpdateCounterPatch
|
||||||
static void Prefix()
|
static void Prefix()
|
||||||
{
|
{
|
||||||
ConsistLOD._globalTick++;
|
ConsistLOD._globalTick++;
|
||||||
|
ConsistLOD.FastPathCars.Clear();
|
||||||
|
ConsistLOD.FullPathCars.Clear();
|
||||||
PositionWheelBoundsFrontLODPatch.ResetFrameCounts();
|
PositionWheelBoundsFrontLODPatch.ResetFrameCounts();
|
||||||
ConsistFreezer.FlushFrameCounts();
|
ConsistFreezer.FlushFrameCounts();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"Id": "RailroaderPhysicsOverhaul",
|
"Id": "RailroaderPhysicsOverhaul",
|
||||||
"DisplayName": "Physics Overhaul",
|
"DisplayName": "Physics Optimizer",
|
||||||
"Author": "seton",
|
"Author": "seton",
|
||||||
"Version": "0.1.1",
|
"Version": "0.1.2",
|
||||||
"ManagerVersion": "0.27.0",
|
"ManagerVersion": "0.27.0",
|
||||||
"GameVersionPoint": "0",
|
"GameVersionPoint": "0",
|
||||||
"AssemblyName": "RailroaderPhysicsOverhaul.dll",
|
"AssemblyName": "RailroaderPhysicsOverhaul.dll",
|
||||||
|
|
|
||||||
3
Main.cs
3
Main.cs
|
|
@ -25,6 +25,8 @@ public static class Main
|
||||||
foreach (string name in Settings.RoadNumberList)
|
foreach (string name in Settings.RoadNumberList)
|
||||||
ConsistLOD.RoadNumberList.Add(name);
|
ConsistLOD.RoadNumberList.Add(name);
|
||||||
|
|
||||||
|
ConsistLOD.ExcludeLocomotives = Settings.ExcludeLocosFromLOD;
|
||||||
|
ConsistFreezer.ExcludeLocomotives = Settings.ExcludeLocosFromFreeze;
|
||||||
ConsistFreezer.AutoFreezeEnabled = Settings.AutoFreezeEnabled;
|
ConsistFreezer.AutoFreezeEnabled = Settings.AutoFreezeEnabled;
|
||||||
ConsistFreezer.AutoFreezeDistance = Settings.AutoFreezeDistance;
|
ConsistFreezer.AutoFreezeDistance = Settings.AutoFreezeDistance;
|
||||||
ConsistFreezer.AutoFreezeSpeedThreshold = Settings.AutoFreezeSpeedThreshold;
|
ConsistFreezer.AutoFreezeSpeedThreshold = Settings.AutoFreezeSpeedThreshold;
|
||||||
|
|
@ -38,6 +40,7 @@ public static class Main
|
||||||
var overlay = go.AddComponent<PhysicsOverlayGUI>();
|
var overlay = go.AddComponent<PhysicsOverlayGUI>();
|
||||||
overlay.Visible = Settings.ShowOverlay;
|
overlay.Visible = Settings.ShowOverlay;
|
||||||
overlay.Opacity = Settings.OverlayOpacity;
|
overlay.Opacity = Settings.OverlayOpacity;
|
||||||
|
go.AddComponent<CarDebugVisualizer>();
|
||||||
|
|
||||||
// UMM settings panel callbacks.
|
// UMM settings panel callbacks.
|
||||||
modEntry.OnGUI = entry => SettingsGUI.Draw(entry, Settings);
|
modEntry.OnGUI = entry => SettingsGUI.Draw(entry, Settings);
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,27 @@
|
||||||
<OutputPath>..\..\Railroader\Mods\RailroaderPhysicsOverhaul\</OutputPath>
|
<OutputPath>..\..\Railroader\Mods\RailroaderPhysicsOverhaul\</OutputPath>
|
||||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
<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>
|
<CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>
|
||||||
|
<ModVersion>0.1.2</ModVersion>
|
||||||
</PropertyGroup>
|
</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>
|
<ItemGroup>
|
||||||
<!-- Info.json is required by UMM in the output mod folder -->
|
<!-- Info.json is required by UMM in the output mod folder -->
|
||||||
<None Include="Info.json">
|
<None Include="Info.json">
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ public class PhysicsOverlayGUI : MonoBehaviour
|
||||||
|
|
||||||
string _lodStatsStr = "";
|
string _lodStatsStr = "";
|
||||||
string _freezeStatsStr = "";
|
string _freezeStatsStr = "";
|
||||||
|
string _debugStatsStr = "";
|
||||||
int _lodStatsFrame;
|
int _lodStatsFrame;
|
||||||
|
|
||||||
GUIStyle _windowStyle;
|
GUIStyle _windowStyle;
|
||||||
|
|
@ -164,10 +165,20 @@ public class PhysicsOverlayGUI : MonoBehaviour
|
||||||
int atRest = ConsistFreezer.LastAtRestCars;
|
int atRest = ConsistFreezer.LastAtRestCars;
|
||||||
int byDist = ConsistFreezer.LastDistanceCars;
|
int byDist = ConsistFreezer.LastDistanceCars;
|
||||||
_freezeStatsStr = $"stopped:{atRest} dist/spd:{byDist}";
|
_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.Label(_lodStatsStr, _dimLabel);
|
||||||
GUILayout.EndHorizontal();
|
GUILayout.EndHorizontal();
|
||||||
|
|
||||||
|
if (_debugStatsStr.Length > 0)
|
||||||
|
GUILayout.Label(_debugStatsStr, _dimLabel);
|
||||||
|
|
||||||
// Auto-freeze row — shows cars skipping the Verlet tick each physics step
|
// Auto-freeze row — shows cars skipping the Verlet tick each physics step
|
||||||
GUILayout.BeginHorizontal();
|
GUILayout.BeginHorizontal();
|
||||||
GUILayout.Label("Auto Freeze", _dimLabel, GUILayout.Width(130f));
|
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.
|
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.
|
5. **`LocationF`/`LocationR`** are updated each tick so the segment cache never goes stale.
|
||||||
|
|
||||||
### Auto Freeze detail
|
### 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)
|
### 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.
|
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
|
## 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 |
|
| Setting | Default | Description |
|
||||||
|---------|---------|-------------|
|
|---------|---------|-------------|
|
||||||
|
|
|
||||||
58
Settings.cs
58
Settings.cs
|
|
@ -11,16 +11,25 @@ public class ModSettings : UnityModManager.ModSettings
|
||||||
{
|
{
|
||||||
public bool OptimizerEnabled = true;
|
public bool OptimizerEnabled = true;
|
||||||
public float DistanceThreshold = 30f;
|
public float DistanceThreshold = 30f;
|
||||||
public int ResyncInterval = 4;
|
public int ResyncInterval = 8;
|
||||||
public bool ShowOverlay = true;
|
public bool ShowOverlay = true;
|
||||||
|
|
||||||
// Auto-freeze: skip the Verlet tick for far+slow consists
|
// Auto-freeze: skip the Verlet tick for far+slow consists
|
||||||
public bool AutoFreezeEnabled = true;
|
public bool AutoFreezeEnabled = true;
|
||||||
public float AutoFreezeDistance = 200f; // meters
|
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.
|
// 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 BlacklistEnabled = false;
|
||||||
public bool IsBlacklist = true; // true = blacklist, false = whitelist
|
public bool IsBlacklist = true; // true = blacklist, false = whitelist
|
||||||
|
|
@ -59,8 +68,8 @@ public class ModSettings : UnityModManager.ModSettings
|
||||||
// Draws the UMM in-game settings panel.
|
// Draws the UMM in-game settings panel.
|
||||||
static class SettingsGUI
|
static class SettingsGUI
|
||||||
{
|
{
|
||||||
static readonly int[] ResyncOptions = { 1, 2, 4, 8 };
|
static readonly int[] ResyncOptions = { 1, 2, 4, 8, 16 };
|
||||||
static readonly string[] ResyncLabels = { "1/1 (max quality)", "1/2", "1/4 (default)", "1/8" };
|
static readonly string[] ResyncLabels = { "1/1 (max quality)", "1/2", "1/4", "1/8 (default)", "1/16" };
|
||||||
|
|
||||||
static string _addInput = "";
|
static string _addInput = "";
|
||||||
static string _toRemove = null; // deferred to avoid mutating list during enumeration
|
static string _toRemove = null; // deferred to avoid mutating list during enumeration
|
||||||
|
|
@ -166,6 +175,28 @@ static class SettingsGUI
|
||||||
|
|
||||||
GUILayout.Space(12f);
|
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 ────────────────────────────────────────────────────
|
// ── Road Number Filter ────────────────────────────────────────────────────
|
||||||
GUILayout.Label("<b>Road Number Filter</b>", GUILayout.ExpandWidth(false));
|
GUILayout.Label("<b>Road Number Filter</b>", GUILayout.ExpandWidth(false));
|
||||||
GUILayout.Space(4f);
|
GUILayout.Space(4f);
|
||||||
|
|
@ -285,6 +316,23 @@ static class SettingsGUI
|
||||||
changed = true;
|
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();
|
GUILayout.EndVertical();
|
||||||
|
|
||||||
if (changed)
|
if (changed)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue