Shows the selected cut as a fan of cards with waybill, notes, and couple/handbrake/locate actions. WaypointQueue cuts appear as dividers when that mod is present.
693 lines
22 KiB
C#
693 lines
22 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using HarmonyLib;
|
|
using Model;
|
|
using Model.Definition;
|
|
using Model.Ops;
|
|
using S3.Modules.Popout;
|
|
using Track;
|
|
using UnityEngine;
|
|
|
|
namespace S3.Modules.CarCards;
|
|
|
|
enum WaypointAction
|
|
{
|
|
Cut,
|
|
Drop,
|
|
Pickup,
|
|
Couple,
|
|
}
|
|
|
|
sealed class WaypointDivider
|
|
{
|
|
public int Number;
|
|
public Color Color;
|
|
public WaypointAction Action;
|
|
public string LeftId = "";
|
|
public string RightId = "";
|
|
/// <summary>
|
|
/// When true, LeftId is the end car and RightId is the next car inward.
|
|
/// The divider sits on the outer face of LeftId, away from RightId.
|
|
/// </summary>
|
|
public bool Outer;
|
|
public string WaypointId = "";
|
|
public bool HasPosition;
|
|
public Vector3 Position;
|
|
public Quaternion Rotation = Quaternion.identity;
|
|
public string Headline = "";
|
|
public string Detail = "";
|
|
}
|
|
|
|
static class WaypointCutSim
|
|
{
|
|
const float NearCoupleM = 250f;
|
|
|
|
public static void Fill(IReadOnlyList<CardViewModel> cards, List<WaypointDivider> into, out string signature)
|
|
{
|
|
into.Clear();
|
|
signature = "";
|
|
if (cards == null || cards.Count == 0) return;
|
|
if (!CarCardsModule.Settings.showWaypointCuts) return;
|
|
if (!WaypointQueueBridge.IsInstalled) return;
|
|
|
|
BaseLocomotive? loco = LocoWithQueue(cards);
|
|
if (loco == null) return;
|
|
if (!WaypointQueueBridge.TryGetSnapshot(loco.id, out var snaps) || snaps.Count == 0)
|
|
return;
|
|
|
|
Color color = MapWaypointSystem.ColorForLoco(loco.id);
|
|
var present = new HashSet<string>(StringComparer.Ordinal);
|
|
string seedId = "";
|
|
for (int i = 0; i < cards.Count; i++)
|
|
{
|
|
string id = cards[i].Id;
|
|
if (string.IsNullOrEmpty(id)) continue;
|
|
present.Add(id);
|
|
if (seedId.Length == 0) seedId = id;
|
|
}
|
|
if (present.Count == 0) return;
|
|
|
|
// Couple-walk order (A-end to B-end), not the camera fan. View reverse
|
|
// only affects where dividers are drawn, not which cars they sit between.
|
|
var remaining = IdsOf(seedId);
|
|
remaining.RemoveAll(id => !present.Contains(id));
|
|
if (remaining.Count == 0)
|
|
{
|
|
remaining = new List<string>(present.Count);
|
|
for (int i = 0; i < cards.Count; i++)
|
|
{
|
|
string id = cards[i].Id;
|
|
if (!string.IsNullOrEmpty(id))
|
|
remaining.Add(id);
|
|
}
|
|
}
|
|
|
|
var parked = new List<ParkedCut>();
|
|
var sig = new System.Text.StringBuilder(loco.id);
|
|
sig.Append(':').Append(snaps.Count);
|
|
if (remaining.Count > 0)
|
|
sig.Append(':').Append(remaining[0]).Append('-').Append(remaining[remaining.Count - 1]);
|
|
|
|
for (int w = 0; w < snaps.Count; w++)
|
|
{
|
|
var wp = snaps[w];
|
|
sig.Append('|').Append(wp.Number)
|
|
.Append(':').Append(wp.CouplingSearchMode)
|
|
.Append(':').Append(wp.UncouplingMode)
|
|
.Append(':').Append(wp.NumberOfCarsToCut)
|
|
.Append(':').Append(wp.CoupleToCarId);
|
|
|
|
if (!Apply(wp, remaining, parked, present, into, color))
|
|
break;
|
|
}
|
|
|
|
signature = sig.ToString();
|
|
}
|
|
|
|
static bool Apply(
|
|
WaypointQueueBridge.WqWaypointSnap wp,
|
|
List<string> remaining,
|
|
List<ParkedCut> parked,
|
|
HashSet<string> present,
|
|
List<WaypointDivider> into,
|
|
Color color)
|
|
{
|
|
if (remaining.Count == 0) return false;
|
|
|
|
bool coupling = WaypointQueueBridge.Coupling(wp);
|
|
string coupleId = CoupleId(wp);
|
|
|
|
if (coupling && string.IsNullOrEmpty(coupleId)
|
|
&& string.Equals(wp.CouplingSearchMode, "Nearest", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
if (!TryResolveNearest(wp, remaining, parked, out coupleId))
|
|
return false;
|
|
}
|
|
|
|
if (coupling && string.IsNullOrEmpty(coupleId)
|
|
&& string.Equals(wp.CouplingSearchMode, "SpecificCar", StringComparison.OrdinalIgnoreCase))
|
|
coupleId = wp.CouplingSearchResultCarId ?? "";
|
|
|
|
if (Is(wp.UncouplingMode, "ByDestinationArea")
|
|
|| Is(wp.UncouplingMode, "ByDestinationIndustry")
|
|
|| Is(wp.UncouplingMode, "ByDestinationTrack"))
|
|
return true;
|
|
|
|
if (coupling && wp.Pickup && Is(wp.UncouplingMode, "ByCount") && wp.NumberOfCarsToCut > 0)
|
|
{
|
|
if (string.IsNullOrEmpty(coupleId)) return false;
|
|
bool onTrain = remaining.Contains(coupleId);
|
|
if (!TryCoupleEnd(remaining, wp, coupleId, out _, out bool atStart))
|
|
return false;
|
|
if (!onTrain)
|
|
AddEndDivider(into, wp, color, WaypointAction.Pickup, remaining, atStart);
|
|
if (!TryMergeForeign(remaining, coupleId, atStart, wp.NumberOfCarsToCut, parked, wp))
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
if (coupling && wp.Dropoff && Is(wp.UncouplingMode, "ByCount") && wp.NumberOfCarsToCut > 0)
|
|
{
|
|
if (string.IsNullOrEmpty(coupleId)) return false;
|
|
if (!TryCoupleEnd(remaining, wp, coupleId, out _, out bool atStart))
|
|
return false;
|
|
if (!TryCutCount(remaining, wp.NumberOfCarsToCut, fromStart: atStart, parked, wp, into, color, WaypointAction.Drop, present))
|
|
return remaining.Count > 0;
|
|
return true;
|
|
}
|
|
|
|
if (coupling && Is(wp.UncouplingMode, "BySpecificCar"))
|
|
return true;
|
|
|
|
if (coupling)
|
|
{
|
|
if (string.IsNullOrEmpty(coupleId)) return false;
|
|
bool onTrain = remaining.Contains(coupleId);
|
|
if (!TryCoupleEnd(remaining, wp, coupleId, out _, out bool atStart))
|
|
return false;
|
|
if (!onTrain)
|
|
AddEndDivider(into, wp, color, WaypointAction.Couple, remaining, atStart);
|
|
if (!TryMergeForeign(remaining, coupleId, atStart, keep: -1, parked, wp))
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
if (Is(wp.UncouplingMode, "AllExceptLocomotives"))
|
|
{
|
|
CutAllExceptLocos(remaining, parked, wp, into, color, present);
|
|
return remaining.Count > 0;
|
|
}
|
|
|
|
if (Is(wp.UncouplingMode, "ByCount"))
|
|
{
|
|
bool fromStart = !TakeFromLast(remaining, wp);
|
|
TryCutCount(remaining, wp.NumberOfCarsToCut, fromStart, parked, wp, into, color, WaypointAction.Cut, present);
|
|
return remaining.Count > 0;
|
|
}
|
|
|
|
if (Is(wp.UncouplingMode, "BySpecificCar"))
|
|
{
|
|
string spec = wp.UncouplingSearchResultCarId ?? "";
|
|
if (string.IsNullOrEmpty(spec)) return true;
|
|
int idx = remaining.IndexOf(spec);
|
|
if (idx < 0) return true;
|
|
bool fromStart = !TakeFromLast(remaining, wp);
|
|
int n = fromStart ? idx + 1 : remaining.Count - idx;
|
|
TryCutCount(remaining, n, fromStart, parked, wp, into, color, WaypointAction.Cut, present);
|
|
return remaining.Count > 0;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static string CoupleId(WaypointQueueBridge.WqWaypointSnap wp)
|
|
{
|
|
if (!string.IsNullOrEmpty(wp.CoupleToCarId)) return wp.CoupleToCarId;
|
|
if (!string.IsNullOrEmpty(wp.CouplingSearchResultCarId)) return wp.CouplingSearchResultCarId;
|
|
return "";
|
|
}
|
|
|
|
static bool TryCutCount(
|
|
List<string> remaining,
|
|
int n,
|
|
bool fromStart,
|
|
List<ParkedCut> parked,
|
|
WaypointQueueBridge.WqWaypointSnap wp,
|
|
List<WaypointDivider> into,
|
|
Color color,
|
|
WaypointAction action,
|
|
HashSet<string> present)
|
|
{
|
|
if (n <= 0 || n >= remaining.Count) return false;
|
|
string left;
|
|
string right;
|
|
List<string> cut;
|
|
if (fromStart)
|
|
{
|
|
left = remaining[n - 1];
|
|
right = remaining[n];
|
|
cut = remaining.GetRange(0, n);
|
|
remaining.RemoveRange(0, n);
|
|
}
|
|
else
|
|
{
|
|
int keep = remaining.Count - n;
|
|
left = remaining[keep - 1];
|
|
right = remaining[keep];
|
|
cut = remaining.GetRange(keep, n);
|
|
remaining.RemoveRange(keep, n);
|
|
}
|
|
|
|
Park(parked, cut, wp);
|
|
if (present.Contains(left) || present.Contains(right))
|
|
{
|
|
into.Add(DividerFrom(wp, color, action, left, right));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void CutAllExceptLocos(
|
|
List<string> remaining,
|
|
List<ParkedCut> parked,
|
|
WaypointQueueBridge.WqWaypointSnap wp,
|
|
List<WaypointDivider> into,
|
|
Color color,
|
|
HashSet<string> present)
|
|
{
|
|
var keep = new List<string>();
|
|
var cut = new List<string>();
|
|
for (int i = 0; i < remaining.Count; i++)
|
|
{
|
|
if (IsLocoType(remaining[i])) keep.Add(remaining[i]);
|
|
else cut.Add(remaining[i]);
|
|
}
|
|
if (cut.Count == 0) return;
|
|
|
|
for (int i = 0; i < remaining.Count - 1; i++)
|
|
{
|
|
bool a = IsLocoType(remaining[i]);
|
|
bool b = IsLocoType(remaining[i + 1]);
|
|
if (a == b) continue;
|
|
string left = remaining[i];
|
|
string right = remaining[i + 1];
|
|
if (!present.Contains(left) && !present.Contains(right)) continue;
|
|
into.Add(DividerFrom(wp, color, WaypointAction.Cut, left, right));
|
|
}
|
|
|
|
Park(parked, cut, wp);
|
|
remaining.Clear();
|
|
remaining.AddRange(keep);
|
|
}
|
|
|
|
static void AddEndDivider(
|
|
List<WaypointDivider> into,
|
|
WaypointQueueBridge.WqWaypointSnap wp,
|
|
Color color,
|
|
WaypointAction action,
|
|
List<string> remaining,
|
|
bool atStart)
|
|
{
|
|
if (remaining.Count == 0) return;
|
|
string end;
|
|
string inward;
|
|
if (atStart)
|
|
{
|
|
end = remaining[0];
|
|
inward = remaining.Count > 1 ? remaining[1] : "";
|
|
}
|
|
else
|
|
{
|
|
end = remaining[remaining.Count - 1];
|
|
inward = remaining.Count > 1 ? remaining[remaining.Count - 2] : "";
|
|
}
|
|
var d = DividerFrom(wp, color, action, end, inward);
|
|
d.Outer = true;
|
|
into.Add(d);
|
|
}
|
|
|
|
static WaypointDivider DividerFrom(
|
|
WaypointQueueBridge.WqWaypointSnap wp,
|
|
Color color,
|
|
WaypointAction action,
|
|
string leftId,
|
|
string rightId)
|
|
{
|
|
return new WaypointDivider
|
|
{
|
|
Number = wp.Number,
|
|
Color = color,
|
|
Action = action,
|
|
LeftId = leftId,
|
|
RightId = rightId,
|
|
WaypointId = wp.Id ?? "",
|
|
HasPosition = wp.HasPosition,
|
|
Position = wp.Position,
|
|
Rotation = wp.Rotation,
|
|
Headline = Headline(action, wp),
|
|
Detail = DetailLines(wp, action),
|
|
};
|
|
}
|
|
|
|
static string Headline(WaypointAction action, WaypointQueueBridge.WqWaypointSnap wp)
|
|
{
|
|
int n = wp.NumberOfCarsToCut;
|
|
return action switch
|
|
{
|
|
WaypointAction.Pickup => n > 0 ? $"Pickup {n}" : "Pickup",
|
|
WaypointAction.Drop => n > 0 ? $"Drop {n}" : "Drop",
|
|
WaypointAction.Couple => "Couple",
|
|
_ => n > 0 ? $"Cut {n}" : "Cut",
|
|
};
|
|
}
|
|
|
|
static string DetailLines(WaypointQueueBridge.WqWaypointSnap wp, WaypointAction action)
|
|
{
|
|
var lines = new List<string>();
|
|
if (!string.IsNullOrEmpty(wp.Name))
|
|
lines.Add(wp.Name);
|
|
string place = PlaceName(wp);
|
|
if (!string.IsNullOrEmpty(place))
|
|
lines.Add(place);
|
|
string couple = CarMark(wp.CoupleToCarId);
|
|
if (string.IsNullOrEmpty(couple))
|
|
couple = CarMark(wp.CouplingSearchResultCarId);
|
|
if (!string.IsNullOrEmpty(couple) && action != WaypointAction.Cut)
|
|
lines.Add("To " + couple);
|
|
if (action == WaypointAction.Cut && wp.NumberOfCarsToCut > 0)
|
|
lines.Add(wp.CountFromNearest ? "Nearest end" : "Furthest end");
|
|
if (wp.WillWait)
|
|
lines.Add(wp.WaitMinutes > 0 ? $"Wait {wp.WaitMinutes} min" : "Wait");
|
|
if (wp.WillRefuel)
|
|
lines.Add(string.IsNullOrEmpty(wp.RefuelLoad) ? "Refuel" : "Refuel " + wp.RefuelLoad);
|
|
if (!string.IsNullOrEmpty(wp.Notes))
|
|
lines.Add(wp.Notes);
|
|
return string.Join("\n", lines);
|
|
}
|
|
|
|
static string PlaceName(WaypointQueueBridge.WqWaypointSnap wp)
|
|
{
|
|
if (!string.IsNullOrEmpty(wp.AreaName))
|
|
return wp.AreaName;
|
|
if (!wp.HasPosition) return "";
|
|
try
|
|
{
|
|
var ops = OpsController.Shared;
|
|
if (ops == null) return "";
|
|
Area? area = ops.ClosestAreaForGamePosition(wp.Position);
|
|
return area != null ? area.name : "";
|
|
}
|
|
catch { return ""; }
|
|
}
|
|
|
|
static string CarMark(string id)
|
|
{
|
|
if (string.IsNullOrEmpty(id)) return "";
|
|
Car? c = ConsistBinder.Find(id);
|
|
if (c == null) return "";
|
|
try
|
|
{
|
|
return string.IsNullOrEmpty(c.DisplayName) ? id : c.DisplayName;
|
|
}
|
|
catch { return id; }
|
|
}
|
|
|
|
static bool TryCoupleEnd(
|
|
List<string> remaining,
|
|
WaypointQueueBridge.WqWaypointSnap wp,
|
|
string coupleId,
|
|
out string endId,
|
|
out bool atStart)
|
|
{
|
|
endId = "";
|
|
atStart = false;
|
|
if (remaining.Count == 0) return false;
|
|
int already = remaining.IndexOf(coupleId);
|
|
if (already >= 0)
|
|
{
|
|
atStart = already * 2 < remaining.Count;
|
|
endId = coupleId;
|
|
return true;
|
|
}
|
|
|
|
Vector3 hint = wp.HasPosition ? wp.Position : default;
|
|
if (TryGamePosId(coupleId, out Vector3 couplePos))
|
|
hint = couplePos;
|
|
else if (!wp.HasPosition)
|
|
return false;
|
|
|
|
atStart = !NearIsLast(remaining, hint);
|
|
endId = atStart ? remaining[0] : remaining[remaining.Count - 1];
|
|
return true;
|
|
}
|
|
|
|
static bool TryMergeForeign(
|
|
List<string> remaining,
|
|
string coupleId,
|
|
bool atStart,
|
|
int keep,
|
|
List<ParkedCut> parked,
|
|
WaypointQueueBridge.WqWaypointSnap wp)
|
|
{
|
|
if (remaining.Contains(coupleId)) return true;
|
|
var foreign = IdsOf(coupleId);
|
|
if (foreign.Count == 0) return false;
|
|
foreign.RemoveAll(remaining.Contains);
|
|
int ci = foreign.IndexOf(coupleId);
|
|
if (ci < 0)
|
|
{
|
|
foreign.Insert(0, coupleId);
|
|
ci = 0;
|
|
}
|
|
if (ci != 0 && ci != foreign.Count - 1)
|
|
return false;
|
|
if (ci == 0 && atStart) foreign.Reverse();
|
|
if (ci != 0 && !atStart) foreign.Reverse();
|
|
|
|
if (keep >= 0 && keep < foreign.Count)
|
|
{
|
|
List<string> extra;
|
|
List<string> take;
|
|
if (atStart)
|
|
{
|
|
int drop = foreign.Count - keep;
|
|
extra = foreign.GetRange(0, drop);
|
|
take = foreign.GetRange(drop, keep);
|
|
}
|
|
else
|
|
{
|
|
take = foreign.GetRange(0, keep);
|
|
extra = foreign.GetRange(keep, foreign.Count - keep);
|
|
}
|
|
if (extra.Count > 0) Park(parked, extra, wp);
|
|
foreign = take;
|
|
}
|
|
|
|
if (atStart) remaining.InsertRange(0, foreign);
|
|
else remaining.AddRange(foreign);
|
|
ForgetParked(parked, foreign);
|
|
return true;
|
|
}
|
|
|
|
static bool TryResolveNearest(
|
|
WaypointQueueBridge.WqWaypointSnap wp,
|
|
List<string> remaining,
|
|
List<ParkedCut> parked,
|
|
out string coupleId)
|
|
{
|
|
coupleId = "";
|
|
if (!wp.HasPosition) return false;
|
|
Vector3 wpPos = wp.Position;
|
|
var remainingSet = new HashSet<string>(remaining);
|
|
|
|
string bestParked = "";
|
|
float bestParkedD = NearCoupleM;
|
|
for (int i = 0; i < parked.Count; i++)
|
|
{
|
|
var group = parked[i];
|
|
if (!TryLiveGroup(group.Ids, remainingSet, out List<string> live, out float dist, wpPos))
|
|
continue;
|
|
if (dist >= bestParkedD) continue;
|
|
bestParkedD = dist;
|
|
bestParked = ClosestId(live, wpPos);
|
|
}
|
|
if (!string.IsNullOrEmpty(bestParked))
|
|
{
|
|
coupleId = bestParked;
|
|
return true;
|
|
}
|
|
|
|
if (TryPollNearby(wpPos, remainingSet, out string pollId))
|
|
{
|
|
coupleId = pollId;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool TryLiveGroup(
|
|
List<string> ids,
|
|
HashSet<string> remaining,
|
|
out List<string> live,
|
|
out float dist,
|
|
Vector3 wpPos)
|
|
{
|
|
live = new List<string>();
|
|
dist = float.MaxValue;
|
|
string seed = "";
|
|
for (int i = 0; i < ids.Count; i++)
|
|
{
|
|
if (remaining.Contains(ids[i])) continue;
|
|
if (ConsistBinder.Find(ids[i]) == null) continue;
|
|
seed = ids[i];
|
|
break;
|
|
}
|
|
if (string.IsNullOrEmpty(seed)) return false;
|
|
live = IdsOf(seed);
|
|
if (live.Count == 0) return false;
|
|
bool overlap = false;
|
|
for (int i = 0; i < ids.Count; i++)
|
|
{
|
|
if (live.Contains(ids[i])) { overlap = true; break; }
|
|
}
|
|
if (!overlap) return false;
|
|
string closest = ClosestId(live, wpPos);
|
|
if (!TryGamePosId(closest, out Vector3 p)) return false;
|
|
dist = Vector3.Distance(p, wpPos);
|
|
return dist < NearCoupleM;
|
|
}
|
|
|
|
static bool TryPollNearby(Vector3 wpPos, HashSet<string> remaining, out string id)
|
|
{
|
|
id = "";
|
|
TrainController? tc = null;
|
|
try { tc = TrainController.Shared; } catch { }
|
|
if (tc?.Cars == null) return false;
|
|
float best = NearCoupleM;
|
|
foreach (Car c in tc.Cars)
|
|
{
|
|
if (c == null || remaining.Contains(c.id)) continue;
|
|
bool a = false, b = false;
|
|
try { a = c[Car.LogicalEnd.A].IsCoupled; } catch { }
|
|
try { b = c[Car.LogicalEnd.B].IsCoupled; } catch { }
|
|
if (a && b) continue;
|
|
if (!TryGamePos(c, out Vector3 p)) continue;
|
|
float d = Vector3.Distance(p, wpPos);
|
|
if (d >= best) continue;
|
|
best = d;
|
|
id = c.id;
|
|
}
|
|
return !string.IsNullOrEmpty(id);
|
|
}
|
|
|
|
static void Park(List<ParkedCut> parked, List<string> ids, WaypointQueueBridge.WqWaypointSnap wp)
|
|
{
|
|
if (ids == null || ids.Count == 0) return;
|
|
var copy = new List<string>(ids);
|
|
Vector3 pos = wp.HasPosition ? wp.Position : default;
|
|
if (!wp.HasPosition)
|
|
{
|
|
for (int i = 0; i < copy.Count; i++)
|
|
{
|
|
if (TryGamePosId(copy[i], out pos)) break;
|
|
}
|
|
}
|
|
parked.Add(new ParkedCut { Ids = copy, Pos = pos });
|
|
}
|
|
|
|
static void ForgetParked(List<ParkedCut> parked, List<string> taken)
|
|
{
|
|
if (taken.Count == 0) return;
|
|
var set = new HashSet<string>(taken);
|
|
for (int i = parked.Count - 1; i >= 0; i--)
|
|
{
|
|
parked[i].Ids.RemoveAll(set.Contains);
|
|
if (parked[i].Ids.Count == 0) parked.RemoveAt(i);
|
|
}
|
|
}
|
|
|
|
static bool TakeFromLast(List<string> remaining, WaypointQueueBridge.WqWaypointSnap wp)
|
|
{
|
|
bool nearLast = NearIsLast(remaining, wp.HasPosition ? wp.Position : default);
|
|
return wp.CountFromNearest ? nearLast : !nearLast;
|
|
}
|
|
|
|
static bool NearIsLast(List<string> remaining, Vector3 pos)
|
|
{
|
|
if (remaining.Count < 2) return true;
|
|
if (!TryGamePosId(remaining[0], out Vector3 a)) return true;
|
|
if (!TryGamePosId(remaining[remaining.Count - 1], out Vector3 b)) return true;
|
|
return Vector3.Distance(b, pos) <= Vector3.Distance(a, pos);
|
|
}
|
|
|
|
static List<string> IdsOf(string seedId)
|
|
{
|
|
var ids = new List<string>();
|
|
Car? seed = ConsistBinder.Find(seedId);
|
|
if (seed == null) return ids;
|
|
try
|
|
{
|
|
foreach (Car c in seed.EnumerateCoupled())
|
|
if (c != null) ids.Add(c.id);
|
|
}
|
|
catch { }
|
|
if (ids.Count == 0) ids.Add(seedId);
|
|
return ids;
|
|
}
|
|
|
|
static string ClosestId(List<string> ids, Vector3 pos)
|
|
{
|
|
string best = ids[0];
|
|
float bestD = float.MaxValue;
|
|
for (int i = 0; i < ids.Count; i++)
|
|
{
|
|
if (!TryGamePosId(ids[i], out Vector3 p)) continue;
|
|
float d = Vector3.Distance(p, pos);
|
|
if (d >= bestD) continue;
|
|
bestD = d;
|
|
best = ids[i];
|
|
}
|
|
return best;
|
|
}
|
|
|
|
static bool TryGamePosId(string id, out Vector3 pos)
|
|
{
|
|
pos = default;
|
|
Car? c = ConsistBinder.Find(id);
|
|
return c != null && TryGamePos(c, out pos);
|
|
}
|
|
|
|
static bool TryGamePos(Car car, out Vector3 pos)
|
|
{
|
|
pos = default;
|
|
try
|
|
{
|
|
if (car == null || Graph.Shared == null) return false;
|
|
pos = Graph.Shared.GetPosition(car.WheelBoundsA);
|
|
return true;
|
|
}
|
|
catch { return false; }
|
|
}
|
|
|
|
static bool IsLocoType(string id)
|
|
{
|
|
Car? c = ConsistBinder.Find(id);
|
|
if (c == null) return false;
|
|
try
|
|
{
|
|
if (c.IsLocomotive) return true;
|
|
var a = c.Archetype;
|
|
return a == CarArchetype.LocomotiveDiesel
|
|
|| a == CarArchetype.LocomotiveSteam
|
|
|| a == CarArchetype.Tender;
|
|
}
|
|
catch { return c is BaseLocomotive; }
|
|
}
|
|
|
|
static BaseLocomotive? LocoWithQueue(IReadOnlyList<CardViewModel> cards)
|
|
{
|
|
BaseLocomotive? lead = null;
|
|
BaseLocomotive? withQ = null;
|
|
for (int i = 0; i < cards.Count; i++)
|
|
{
|
|
if (cards[i].Car is not BaseLocomotive loco) continue;
|
|
lead ??= loco;
|
|
if (!WaypointQueueBridge.TryGetSnapshot(loco.id, out var snaps) || snaps.Count == 0)
|
|
continue;
|
|
bool mu = false;
|
|
try { mu = Traverse.Create(loco).Property<bool>("IsMuEnabled").Value; }
|
|
catch { }
|
|
if (!mu) return loco;
|
|
withQ ??= loco;
|
|
}
|
|
return withQ ?? lead;
|
|
}
|
|
|
|
static bool Is(string value, string name) =>
|
|
string.Equals(value, name, StringComparison.OrdinalIgnoreCase);
|
|
|
|
sealed class ParkedCut
|
|
{
|
|
public List<string> Ids = new();
|
|
public Vector3 Pos;
|
|
}
|
|
}
|