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
141 lines
4.7 KiB
C#
141 lines
4.7 KiB
C#
using System.Linq;
|
|
using System.Text;
|
|
using HarmonyLib;
|
|
using Model;
|
|
using Model.Physics;
|
|
using UI.Console;
|
|
|
|
namespace RailroaderPhysicsOverhaul;
|
|
|
|
/// <summary>
|
|
/// Hooks into ConsoleCommandHandler._HandleSlashCommand to add /rpf commands
|
|
/// before the game's switch block sees them.
|
|
/// </summary>
|
|
[HarmonyPatch(typeof(ConsoleCommandHandler))]
|
|
[HarmonyPatch("_HandleSlashCommand")]
|
|
static class ConsoleCommandPatch
|
|
{
|
|
static bool Prefix(string[] comps, ref string __result)
|
|
{
|
|
if (comps.Length == 0 || comps[0].ToLower() != "/rpf")
|
|
return true; // not our command, let original handle it
|
|
|
|
__result = RpfCommands.Handle(comps);
|
|
return false; // swallow — don't pass to original
|
|
}
|
|
}
|
|
|
|
static class RpfCommands
|
|
{
|
|
internal static string Handle(string[] comps)
|
|
{
|
|
if (comps.Length < 2)
|
|
return Usage();
|
|
|
|
return comps[1].ToLower() switch
|
|
{
|
|
"freeze" => Freeze(),
|
|
"unfreeze" => Unfreeze(),
|
|
"dump" => Dump(),
|
|
"timing" => PhysicsTimer.GetReport(),
|
|
"overlay" => ToggleOverlay(),
|
|
"forceactive" => ToggleForceActive(),
|
|
"lod" => SetLod(comps),
|
|
"help" => Usage(),
|
|
_ => $"Unknown subcommand '{comps[1]}'. {Usage()}",
|
|
};
|
|
}
|
|
|
|
static string Freeze()
|
|
{
|
|
var (car, set) = GetSelection();
|
|
if (car == null) return "No car selected.";
|
|
if (set == null) return $"{car.id} has no IntegrationSet.";
|
|
|
|
// Zero velocities so cars stop cleanly before we freeze the solver.
|
|
set.SetVelocity(0f, set.Cars.ToList());
|
|
bool added = ConsistFreezer.Freeze(set.Id);
|
|
return added
|
|
? $"Froze set #{set.Id} ({set.NumberOfCars} cars). Use /rpf unfreeze to release."
|
|
: $"Set #{set.Id} was already frozen.";
|
|
}
|
|
|
|
static string Unfreeze()
|
|
{
|
|
var (car, set) = GetSelection();
|
|
if (car == null) return "No car selected.";
|
|
if (set == null) return $"{car.id} has no IntegrationSet.";
|
|
|
|
bool removed = ConsistFreezer.Unfreeze(set.Id);
|
|
return removed
|
|
? $"Unfroze set #{set.Id} — solver resumed."
|
|
: $"Set #{set.Id} was not frozen.";
|
|
}
|
|
|
|
static string Dump()
|
|
{
|
|
var (car, set) = GetSelection();
|
|
if (car == null) return "No car selected.";
|
|
if (set == null) return $"{car.id} has no IntegrationSet.";
|
|
|
|
var sb = new StringBuilder();
|
|
sb.AppendLine($"=== IntegrationSet #{set.Id} | {set.NumberOfCars} cars | frozen={ConsistFreezer.IsFrozen(set.Id)} ===");
|
|
|
|
float totalWeightLbs = 0f;
|
|
int idx = 0;
|
|
foreach (Car c in set.Cars)
|
|
{
|
|
totalWeightLbs += c.Weight;
|
|
float mph = c.velocity * 2.23694f;
|
|
string coupled = c.EndGearA.IsCoupled ? "A" : "";
|
|
coupled += c.EndGearB.IsCoupled ? "B" : "";
|
|
if (coupled == "") coupled = "solo";
|
|
sb.AppendLine($" [{idx++}] {c.id} {c.DisplayName} | {mph:F1}mph | {c.Weight:F0}lb | coupled={coupled} | loc={c.WheelBoundsF.segment?.id}");
|
|
}
|
|
|
|
sb.AppendLine($"Total: {totalWeightLbs / 2000f:F0} short tons");
|
|
return sb.ToString();
|
|
}
|
|
|
|
static (Car car, IntegrationSet set) GetSelection()
|
|
{
|
|
Car car = TrainController.Shared?.SelectedCar;
|
|
return (car, car?.set);
|
|
}
|
|
|
|
static string ToggleOverlay()
|
|
{
|
|
var gui = PhysicsOverlayGUI.Instance;
|
|
if (gui == null) return "Overlay not initialized.";
|
|
gui.Visible = !gui.Visible;
|
|
return $"Overlay {(gui.Visible ? "shown" : "hidden")}.";
|
|
}
|
|
|
|
static string SetLod(string[] comps)
|
|
{
|
|
if (comps.Length < 3)
|
|
{
|
|
ConsistLOD.Enabled = !ConsistLOD.Enabled;
|
|
return $"LOD {(ConsistLOD.Enabled ? "enabled" : "disabled")}. Threshold: {ConsistLOD.DistanceThreshold:F0}m. Usage: /rpf lod <meters|off>";
|
|
}
|
|
if (comps[2].ToLower() == "off") { ConsistLOD.Enabled = false; return "LOD disabled."; }
|
|
if (float.TryParse(comps[2], out float dist) && dist > 0f)
|
|
{
|
|
ConsistLOD.DistanceThreshold = dist;
|
|
ConsistLOD.Enabled = true;
|
|
return $"LOD enabled, fast path for interior cars beyond {dist:F0}m.";
|
|
}
|
|
return "Usage: /rpf lod <meters> or /rpf lod off";
|
|
}
|
|
|
|
static string ToggleForceActive()
|
|
{
|
|
PhysicsTimer.ForceActive = !PhysicsTimer.ForceActive;
|
|
return PhysicsTimer.ForceActive
|
|
? "ForceActive ON — stock optimizer bypassed, solver always runs."
|
|
: "ForceActive OFF — stock optimizer active.";
|
|
}
|
|
|
|
static string Usage() =>
|
|
"Usage: /rpf <freeze|unfreeze|dump|timing|overlay|forceactive|lod|help>";
|
|
}
|