railroader-physics-optimizer/ConsistFreezer.cs
Seton Carmichael 766822d15a release: 0.1.2 - debug visualization, loco exclusions, tuned defaults
New in 0.1.2:
- Debug color overlay: tint cars by physics state (yellow=frozen,
  cyan=fast-path, magenta=full-accuracy), individual toggles per state
- Exclude locomotives from Physics Optimizer and Auto Freeze
- Debug stats line in overlay showing live counts per state
- Resync quality 1/16 option added

Defaults tuned for new installs:
- Resync quality: 1/4 -> 1/8
- Speed threshold: ~0.7 mph -> 0.2 mph
- Overlay opacity: 100% -> 75%

Rename: Physics Overhaul -> Physics Optimizer throughout
2026-06-16 22:14:34 -04:00

129 lines
5 KiB
C#

using System.Collections.Generic;
using HarmonyLib;
using Model;
using Model.Physics;
using UnityEngine;
namespace RailroaderPhysicsOverhaul;
public static class ConsistFreezer
{
// Manual freeze — explicit /rpf freeze command
static readonly HashSet<uint> _frozenSetIds = new();
public static bool IsFrozen(uint setId) => _frozenSetIds.Contains(setId);
public static bool Freeze(uint setId) => _frozenSetIds.Add(setId);
public static bool Unfreeze(uint setId) => _frozenSetIds.Remove(setId);
public static void ClearAll() => _frozenSetIds.Clear();
// 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)
static float AutoFreezeDistanceSq => AutoFreezeDistance * AutoFreezeDistance;
// Per-tick frozen-car counters — flushed to Last* by FlushFrameCounts().
// internal so ShouldSkipTickPatch (same file, different class) can increment them.
internal static int _frameAtRestCars;
internal static int _frameDistanceCars;
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;
float distSqThresh = AutoFreezeDistanceSq;
float speedThresh = AutoFreezeSpeedThreshold;
foreach (Car car in set.Cars)
{
Transform body = car.BodyTransform;
if (body == null) continue;
// If ANY car is within distance → don't freeze the consist.
if ((body.position - camPos).sqrMagnitude < distSqThresh) return false;
// If ANY car is moving above threshold → don't freeze.
if (Mathf.Abs(car.velocity) >= speedThresh) return false;
}
// Every car is both far and slow — safe to skip the Verlet tick.
return true;
}
}
// Completely replaces the stock ShouldSkipTick getter.
// Priority order:
// 1. Manual freeze → always skip
// 2. ForceActive → never skip (profiling bypass)
// 3. AllCarsAtRest → stock optimizer behaviour, replicated explicitly
// 4. Auto-freeze → our distance + speed tier
[HarmonyPatch(typeof(IntegrationSet), "get_ShouldSkipTick")]
static class ShouldSkipTickPatch
{
static bool Prefix(IntegrationSet __instance, ref bool __result)
{
if (ConsistFreezer.IsFrozen(__instance.Id))
{
foreach (Car c in __instance.Cars) ConsistFreezer.FrozenCars.Add(c);
__result = true;
return false;
}
if (PhysicsTimer.ForceActive)
{
__result = false;
return false;
}
// Replicate stock optimizer behaviour explicitly so it works with or without
// 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;
}
__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
}
}