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