railroader-physics-optimizer/ConsistFreezer.cs
Seton Carmichael 5414fd8979 Initial commit - Physics Overhaul v0.1.1
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
2026-06-16 12:24:56 -04:00

106 lines
3.9 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 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; }
internal static void FlushFrameCounts()
{
LastAtRestCars = _frameAtRestCars;
LastDistanceCars = _frameDistanceCars;
_frameAtRestCars = 0;
_frameDistanceCars = 0;
}
// Called from ShouldSkipTickPatch. Internal so the patch class (same file) can reach it.
internal static bool ShouldAutoFreeze(IntegrationSet set)
{
if (!AutoFreezeEnabled) 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))
{
__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())
{
ConsistFreezer._frameAtRestCars += __instance.NumberOfCars;
__result = true;
return false;
}
__result = ConsistFreezer.ShouldAutoFreeze(__instance);
if (__result)
ConsistFreezer._frameDistanceCars += __instance.NumberOfCars;
return false; // always override — stock getter never runs
}
}