railroader-setons-special-s.../src/Modules/QuickActions/EndGearActions.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

275 lines
8.2 KiB
C#

using System.Collections.Generic;
using Game.Messages;
using Game.State;
using Model;
using S3.Core;
namespace S3.Modules.QuickActions;
static class EndGearActions
{
public static bool TryNeighbor(Car car, Car.LogicalEnd end, out Car other, out Car.LogicalEnd otherEnd)
{
otherEnd = end == Car.LogicalEnd.A ? Car.LogicalEnd.B : Car.LogicalEnd.A;
other = null!;
try
{
return car.TryGetAdjacentCar(end, out other) && other != null;
}
catch
{
other = null!;
return false;
}
}
public static bool CanDisconnectAll(Car car, Car.LogicalEnd end)
{
return car[end].IsCoupled;
}
public static bool CanToggleCouple(Car car, Car.LogicalEnd end)
{
if (car[end].IsCoupled) return true;
return TryNeighbor(car, end, out _, out _);
}
public static bool CanToggleAir(Car car, Car.LogicalEnd end)
{
if (car[end].IsAirConnected) return true;
return car[end].IsCoupled && TryNeighbor(car, end, out _, out _);
}
public static void ToggleCouple(Car car, Car.LogicalEnd end)
{
bool couple = !car[end].IsCoupled;
bool hasNeighbor = TryNeighbor(car, end, out Car other, out Car.LogicalEnd otherEnd);
if (couple && !hasNeighbor) return;
try
{
car.ApplyEndGearChange(end, Car.EndGearStateKey.IsCoupled, couple);
if (hasNeighbor)
other.ApplyEndGearChange(otherEnd, Car.EndGearStateKey.IsCoupled, couple);
}
catch (System.Exception e)
{
Log.Warn($"[quickactions] couple toggle failed: {e.Message}");
}
}
public static void ToggleAir(Car car, Car.LogicalEnd end)
{
bool connect = !car[end].IsAirConnected;
if (connect && !car[end].IsCoupled) return;
Car? other = null;
try { other = car.CoupledTo(end) ?? car.AirConnectedTo(end); }
catch { /* fall through */ }
if (other == null && !TryNeighbor(car, end, out other, out _))
return;
if (other == null) return;
try
{
var msg = new SetGladhandsConnected(car.id, other.id, connect);
if (!StateManager.CheckAuthorizedToSendMessage(msg)) return;
StateManager.ApplyLocal(msg);
}
catch (System.Exception e)
{
Log.Warn($"[quickactions] air toggle failed: {e.Message}");
}
}
public static void ToggleCock(Car car, Car.LogicalEnd end)
{
float next = car[end].IsAnglecockOpen ? 0f : 1f;
try
{
car.ApplyEndGearChange(end, Car.EndGearStateKey.Anglecock, next);
if (car[end].IsCoupled && TryNeighbor(car, end, out Car other, out Car.LogicalEnd otherEnd))
other.ApplyEndGearChange(otherEnd, Car.EndGearStateKey.Anglecock, next);
}
catch (System.Exception e)
{
Log.Warn($"[quickactions] anglecock toggle failed: {e.Message}");
}
}
public static string Preview(Car car, Car.LogicalEnd end, HoverSlot slot)
{
return slot switch
{
HoverSlot.Cut => CutPreview(CountCut(car, end)),
_ => "",
};
}
static string CutPreview(int n)
{
if (n <= 0) return "";
return $"Cut out {n} Cars";
}
public static int CountCut(Car car, Car.LogicalEnd end)
{
if (!TryNeighbor(car, end, out Car other, out _))
return 0;
return CollectAway(other, car).Count;
}
public static void DisconnectAll(Car car, Car.LogicalEnd end)
{
if (!TryNeighbor(car, end, out Car other, out Car.LogicalEnd otherEnd))
return;
List<Car> detached = CollectAway(other, car);
var s = QuickActionsModule.Settings;
try
{
car.ApplyEndGearChange(end, Car.EndGearStateKey.Anglecock, 0f);
other.ApplyEndGearChange(otherEnd, Car.EndGearStateKey.Anglecock, 0f);
}
catch (System.Exception e)
{
Log.Warn($"[quickactions] drop-all anglecock failed: {e.Message}");
}
if (car[end].IsAirConnected)
{
try
{
var msg = new SetGladhandsConnected(car.id, other.id, false);
if (StateManager.CheckAuthorizedToSendMessage(msg))
StateManager.ApplyLocal(msg);
}
catch (System.Exception e)
{
Log.Warn($"[quickactions] drop-all air failed: {e.Message}");
}
}
try
{
car.ApplyEndGearChange(end, Car.EndGearStateKey.IsCoupled, false);
other.ApplyEndGearChange(otherEnd, Car.EndGearStateKey.IsCoupled, false);
}
catch (System.Exception e)
{
Log.Warn($"[quickactions] drop-all couple failed: {e.Message}");
}
if (!s.dropHandbrakeOnCut) return;
foreach (Car c in detached)
{
if (s.dropHandbrakeExcludeLocos && c is BaseLocomotive) continue;
try { c.SetHandbrake(true); }
catch (System.Exception e)
{
Log.Warn($"[quickactions] drop-all handbrake {c.DisplayName}: {e.Message}");
}
}
}
public static bool IsMadeUp(Car car, Car.LogicalEnd end)
{
try
{
var g = car[end];
return g.IsCoupled && g.IsAirConnected && g.IsAnglecockOpen;
}
catch { return false; }
}
public static bool CanToggleJoint(Car car, Car.LogicalEnd end)
{
try
{
if (car[end].IsCoupled) return true;
return TryNeighbor(car, end, out _, out _);
}
catch { return false; }
}
public static void ToggleJoint(Car car, Car.LogicalEnd end)
{
if (IsMadeUp(car, end)) DisconnectAll(car, end);
else MakeUp(car, end);
}
public static void MakeUp(Car car, Car.LogicalEnd end)
{
bool hasNeighbor = TryNeighbor(car, end, out Car other, out Car.LogicalEnd otherEnd);
if (!hasNeighbor)
{
try { if (!car[end].IsCoupled) return; }
catch { return; }
}
try
{
if (!car[end].IsCoupled)
{
car.ApplyEndGearChange(end, Car.EndGearStateKey.IsCoupled, true);
if (hasNeighbor)
other.ApplyEndGearChange(otherEnd, Car.EndGearStateKey.IsCoupled, true);
}
}
catch (System.Exception e)
{
Log.Warn($"[quickactions] make-up couple failed: {e.Message}");
}
Car? airOther = null;
try { airOther = car.CoupledTo(end) ?? other; }
catch { airOther = other; }
if (airOther != null)
{
try
{
if (!car[end].IsAirConnected)
{
var msg = new SetGladhandsConnected(car.id, airOther.id, true);
if (StateManager.CheckAuthorizedToSendMessage(msg))
StateManager.ApplyLocal(msg);
}
}
catch (System.Exception e)
{
Log.Warn($"[quickactions] make-up air failed: {e.Message}");
}
}
try
{
car.ApplyEndGearChange(end, Car.EndGearStateKey.Anglecock, 1f);
if (hasNeighbor)
other.ApplyEndGearChange(otherEnd, Car.EndGearStateKey.Anglecock, 1f);
}
catch (System.Exception e)
{
Log.Warn($"[quickactions] make-up Anglecock failed: {e.Message}");
}
}
static List<Car> CollectAway(Car start, Car blocked)
{
var list = new List<Car>();
var seen = new HashSet<Car> { blocked };
var stack = new Stack<Car>();
stack.Push(start);
while (stack.Count > 0)
{
Car c = stack.Pop();
if (!seen.Add(c)) continue;
list.Add(c);
if (TryNeighbor(c, Car.LogicalEnd.A, out Car a, out _) && !seen.Contains(a))
stack.Push(a);
if (TryNeighbor(c, Car.LogicalEnd.B, out Car b, out _) && !seen.Contains(b))
stack.Push(b);
}
return list;
}
}