railroader-physics-optimizer/ConsoleCommands.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

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>";
}