railroader-setons-special-s.../src/Modules/QuickActions/TrainReadout.cs
seton 5187c03ebe QuickActions: outer-ring cut/couple actions and consist hover wheel
Adds end-gear couple/air/cut on the vanilla pie, plus a consist wheel
for set-lead, brakes, bleed, air, and idle without opening extra windows.
2026-09-11 15:19:23 -04:00

169 lines
5 KiB
C#

using System.Collections.Generic;
using Game.Messages;
using HarmonyLib;
using Model;
using Model.AI;
using Track.Search;
using UnityEngine;
namespace S3.Modules.QuickActions;
static class TrainReadout
{
public readonly struct Snapshot
{
public readonly int Cars;
public readonly float LengthFt;
public readonly float Tons;
public readonly float RatedTeLbf;
public readonly float CurrentTeLbf;
public readonly float HereLbf;
public readonly float NeedLbf;
public readonly float WeightMarkLbf;
public readonly bool HasWaypoint;
public readonly bool CanMakeIt;
public Snapshot(
int cars, float lengthFt, float tons,
float ratedTeLbf, float currentTeLbf, float hereLbf, float needLbf, float weightMarkLbf,
bool hasWaypoint, bool canMakeIt)
{
Cars = cars;
LengthFt = lengthFt;
Tons = tons;
RatedTeLbf = ratedTeLbf;
CurrentTeLbf = currentTeLbf;
HereLbf = hereLbf;
NeedLbf = needLbf;
WeightMarkLbf = weightMarkLbf;
HasWaypoint = hasWaypoint;
CanMakeIt = canMakeIt;
}
public string StatsBlock()
{
return $"{LengthFt:0} ft\n{Tons:0} T";
}
public static string FormatTe(float lbf)
{
if (lbf >= 1000f) return $"{lbf / 1000f:0.0}k lbf";
return $"{lbf:0} lbf";
}
}
public static Snapshot Measure(Car origin)
{
int cars = 0;
float meters = 0f;
float pounds = 0f;
float rated = 0f;
float current = 0f;
float gravity = 0f;
BaseLocomotive? loco = null;
try
{
foreach (Car c in origin.EnumerateCoupled())
{
cars++;
meters += c.carLength;
pounds += c.Weight;
gravity += c.GravityForce;
if (c is BaseLocomotive l)
{
rated += l.RatedTractiveEffort;
current += Mathf.Abs(l.TractiveEffort);
loco ??= l;
}
}
}
catch { /* measured what we could */ }
float tons = pounds / 2000f;
float weightMark = tons * 20f;
float here = Mathf.Abs(gravity);
float need = 0f;
bool hasWp = false;
if (TryWaypointNeed(origin, loco, tons, out float routeNeed, out bool wp) && wp)
{
hasWp = true;
need = routeNeed;
}
bool can = rated + 0.5f >= Mathf.Max(here, need);
return new Snapshot(
cars, meters * 3.28084f, tons,
rated, current, here, need, weightMark,
hasWp, can);
}
static bool TryWaypointNeed(Car origin, BaseLocomotive? first, float tons, out float need, out bool hasWaypoint)
{
need = 0f;
hasWaypoint = false;
try
{
foreach (Car c in origin.EnumerateCoupled())
{
if (c is not BaseLocomotive l) continue;
var planner = l.AutoEngineerPlanner;
if (planner == null) continue;
object? raw = Traverse.Create(planner).Field("_orders").GetValue();
if (raw is not Orders orders) continue;
if (orders.Mode != AutoEngineerMode.Waypoint || !orders.Waypoint.HasValue)
continue;
hasWaypoint = true;
if (TryRouteGrade(planner, out float gradePct))
need = tons * 20f * Mathf.Max(0f, gradePct);
else
need = Mathf.Abs(first != null ? SumGravity(origin) : 0f);
return true;
}
}
catch { /* no waypoint */ }
return false;
}
static float SumGravity(Car origin)
{
float n = 0f;
try
{
foreach (Car c in origin.EnumerateCoupled())
n += c.GravityForce;
}
catch { /* */ }
return n;
}
static bool TryRouteGrade(AutoEngineerPlanner planner, out float maxAdversePct)
{
maxAdversePct = 0f;
try
{
object? raw = Traverse.Create(planner).Field("_route").GetValue();
if (raw is not List<RouteSearch.Step> route || route.Count < 2)
return false;
Vector3 prev = route[0].Position;
for (int i = 1; i < route.Count; i++)
{
Vector3 p = route[i].Position;
Vector3 d = p - prev;
float horiz = new Vector2(d.x, d.z).magnitude;
if (horiz < 0.5f)
{
prev = p;
continue;
}
float pct = d.y / horiz * 100f;
if (pct > maxAdversePct) maxAdversePct = pct;
prev = p;
}
return true;
}
catch
{
return false;
}
}
}