mirror of
https://github.com/MaSzyna-EU07/StarterNG.git
synced 2026-10-25 02:27:36 +02:00
A driver in cab 1 goes into the first car of a unit and one in cab 2 into the last - the ET42-A has only cab 1, the ET42-B only cab 2. The crew is the unit's: the card, the details and replacing a vehicle all set it there, rather than in whichever car happened to be selected.
730 lines
24 KiB
C#
730 lines
24 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
using StarterNG.Classes;
|
|
using StarterNG.Domain.Sceneries;
|
|
using StarterNG.Domain.Vehicles;
|
|
using StarterNG.Application;
|
|
|
|
namespace StarterNG.Domain;
|
|
|
|
public sealed class Consist : IReadOnlyList<ConsistItem>
|
|
{
|
|
private static readonly string[] CarSuffixes = { "sa", "sb", "ra", "rb", "s" };
|
|
|
|
private readonly VehicleCatalog _db;
|
|
private readonly VehicleInfo _info;
|
|
private readonly List<ConsistItem> _items = new();
|
|
private int _nameCounter;
|
|
|
|
public Consist(VehicleCatalog db, VehicleInfo info)
|
|
{
|
|
_db = db;
|
|
_info = info;
|
|
}
|
|
|
|
public event Action? Changed;
|
|
|
|
public Trainset? EditingTrainset { get; set; }
|
|
|
|
public ConsistItem? Selected { get; set; }
|
|
|
|
public Dynamic? SelectedCar { get; set; }
|
|
|
|
public int Count => _items.Count;
|
|
|
|
/// <summary>Every vehicle, a multi-unit counted car by car - <see cref="Count"/> counts units.</summary>
|
|
public int VehicleCount => _items.Sum(i => i.Cars.Count);
|
|
|
|
public ConsistItem this[int index]
|
|
{
|
|
get => _items[index];
|
|
set => _items[index] = value;
|
|
}
|
|
|
|
public IEnumerator<ConsistItem> GetEnumerator() => _items.GetEnumerator();
|
|
|
|
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
|
|
|
public int IndexOf(ConsistItem item) => _items.IndexOf(item);
|
|
|
|
public void Raise() => Changed?.Invoke();
|
|
|
|
public void LoadFrom(Trainset trainset)
|
|
{
|
|
EditingTrainset = trainset;
|
|
_items.Clear();
|
|
|
|
var cars = trainset.Vehicles.Select(v =>
|
|
{
|
|
var c = v.Clone();
|
|
c.MiniName = _db.MiniFor(c.DataFolder, c.SkinFile, c.MmdFile) ?? c.MiniName;
|
|
return c;
|
|
}).ToList();
|
|
|
|
int i = 0;
|
|
while (i < cars.Count)
|
|
{
|
|
if (HoldsNext(cars[i]))
|
|
{
|
|
var locked = new List<Dynamic> { cars[i] };
|
|
int j = i;
|
|
while (j < cars.Count - 1 && HoldsNext(cars[j]))
|
|
locked.Add(cars[++j]);
|
|
|
|
_items.Add(new ConsistItem
|
|
{
|
|
Cars = locked,
|
|
Grouped = true,
|
|
Flipped = locked[0].Offset < 0,
|
|
Driver = UnitDriver(locked)
|
|
});
|
|
i = j + 1;
|
|
continue;
|
|
}
|
|
|
|
VehicleSet? set = null;
|
|
if (_db.TextureFor(cars[i].DataFolder, cars[i].SkinFile, cars[i].MmdFile)?.Uuid is { } uuid)
|
|
_db.SetByTextureUuid.TryGetValue(uuid, out set);
|
|
|
|
if (set?.TextureRefs is { Count: > 1 })
|
|
{
|
|
var members = new HashSet<string>(set.TextureRefs.Where(r => !string.IsNullOrEmpty(r)));
|
|
var group = new List<Dynamic>();
|
|
|
|
int j = i;
|
|
while (j < cars.Count && group.Count < set.TextureRefs.Count)
|
|
{
|
|
if (_db.TextureFor(cars[j].DataFolder, cars[j].SkinFile, cars[j].MmdFile)?.Uuid is { } u
|
|
&& members.Contains(u))
|
|
group.Add(cars[j++]);
|
|
else
|
|
break;
|
|
}
|
|
|
|
if (group.Count >= 2)
|
|
{
|
|
_items.Add(new ConsistItem
|
|
{
|
|
Cars = group,
|
|
Grouped = true,
|
|
Flipped = group[0].Offset < 0,
|
|
Driver = UnitDriver(group)
|
|
});
|
|
i = j;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (IsUnitCar(cars[i]))
|
|
{
|
|
string key = UnitKey(cars[i]);
|
|
var group = new List<Dynamic> { cars[i] };
|
|
int j = i + 1;
|
|
while (j < cars.Count && IsUnitCar(cars[j]) && UnitKey(cars[j]) == key)
|
|
group.Add(cars[j++]);
|
|
|
|
if (group.Count >= 2)
|
|
{
|
|
_items.Add(new ConsistItem
|
|
{
|
|
Cars = group,
|
|
Grouped = true,
|
|
Flipped = group[0].Offset < 0,
|
|
Driver = UnitDriver(group)
|
|
});
|
|
i = j;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
_items.Add(new ConsistItem
|
|
{
|
|
Cars = new List<Dynamic> { cars[i] },
|
|
Grouped = false,
|
|
Flipped = cars[i].Offset < 0,
|
|
Driver = cars[i].DriverType
|
|
});
|
|
i++;
|
|
}
|
|
|
|
Selected = _items.FirstOrDefault();
|
|
SyncStartingVehicle();
|
|
Raise();
|
|
}
|
|
|
|
public List<Dynamic> Flatten() => _items.SelectMany(RunningOrder).ToList();
|
|
|
|
/// <summary>A card's cars in the order they run, which is the order the export writes.</summary>
|
|
public static IEnumerable<Dynamic> RunningOrder(ConsistItem item) =>
|
|
item.Flipped ? Enumerable.Reverse(item.Cars) : item.Cars;
|
|
|
|
public void SyncStartingVehicle()
|
|
{
|
|
if (Selected is not { Cars.Count: > 0 } item)
|
|
return;
|
|
|
|
var driven = item.Cars.FirstOrDefault(c =>
|
|
c.DriverType is eDriverType.Headdriver or eDriverType.Reardriver);
|
|
AppServices.Current.State.StartingVehicleName = (driven ?? item.Cars[0]).Name;
|
|
}
|
|
|
|
public Dynamic MakeDynamic(VehicleTexture texture, Scenery? scenery)
|
|
{
|
|
string skin = Base(texture.Skinfile);
|
|
return new Dynamic
|
|
{
|
|
RangeMax = -1,
|
|
RangeMin = 0,
|
|
Name = MakeUniqueName(skin, scenery),
|
|
DataFolder = StripDynamicPrefix(texture.Directory),
|
|
SkinFile = skin,
|
|
MmdFile = string.IsNullOrEmpty(texture.Model) ? skin : texture.Model!,
|
|
Offset = 0f,
|
|
DriverType = eDriverType.Nobody,
|
|
Coupling = new Coupling { Flags = Coupling.Mechanical | Coupling.BrakePipe },
|
|
HasVelocity = true,
|
|
MiniName = _db.ResolveMiniName(texture)
|
|
};
|
|
}
|
|
|
|
private string MakeUniqueName(string baseName, Scenery? scenery)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(baseName)) baseName = "vehicle";
|
|
baseName = baseName.Replace(' ', '_');
|
|
|
|
var used = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (var item in _items)
|
|
foreach (var c in item.Cars)
|
|
if (!string.IsNullOrEmpty(c.Name))
|
|
used.Add(c.Name!);
|
|
|
|
if (scenery != null)
|
|
{
|
|
foreach (string n in scenery.LooseVehicleNames)
|
|
used.Add(n);
|
|
foreach (var ts in scenery.Trainsets)
|
|
{
|
|
if (ReferenceEquals(ts, EditingTrainset)) continue;
|
|
foreach (var v in ts.Vehicles)
|
|
if (!string.IsNullOrEmpty(v.Name))
|
|
used.Add(v.Name!);
|
|
}
|
|
}
|
|
|
|
string candidate;
|
|
do
|
|
candidate = $"{baseName}_{++_nameCounter}";
|
|
while (used.Contains(candidate));
|
|
return candidate;
|
|
}
|
|
|
|
private static string StripDynamicPrefix(string directory)
|
|
{
|
|
string d = directory.Replace('\\', '/').TrimEnd('/');
|
|
const string prefix = "dynamic/";
|
|
if (d.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
|
d = d[prefix.Length..];
|
|
return d;
|
|
}
|
|
|
|
public void Insert(int at, ConsistItem item) =>
|
|
_items.Insert(Math.Clamp(at, 0, _items.Count), item);
|
|
|
|
/// <summary>
|
|
/// Crew for a vehicle joining at position: a driver in cab A when it is powered
|
|
/// and either leads or the consist has no driver yet, nobody otherwise.
|
|
/// </summary>
|
|
public void Staff(ConsistItem item, int position)
|
|
{
|
|
item.Driver = eDriverType.Nobody;
|
|
foreach (var car in item.Cars)
|
|
car.DriverType = eDriverType.Nobody;
|
|
|
|
if (!IsPowered(item))
|
|
return;
|
|
|
|
bool staffed = _items.Any(other => !ReferenceEquals(other, item) &&
|
|
other.Driver is eDriverType.Headdriver or eDriverType.Reardriver);
|
|
if (position == 0 || !staffed)
|
|
Crew(item, eDriverType.Headdriver);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Puts item in place of the vehicle at index, facing the same way. Its crew
|
|
/// moves over where the replacement can take it - a driver only into something
|
|
/// powered - and the rest is staffed as a newly added vehicle would be.
|
|
/// </summary>
|
|
public void Replace(int index, ConsistItem item)
|
|
{
|
|
var replaced = _items[index];
|
|
_items[index] = item;
|
|
Staff(item, index);
|
|
|
|
// Facing the way the replaced vehicle did - cars and all, as Flip would.
|
|
if (item.Flipped != replaced.Flipped)
|
|
Turn(index, index);
|
|
|
|
var crew = replaced.Driver;
|
|
if (crew == eDriverType.Passenger ||
|
|
(crew is eDriverType.Headdriver or eDriverType.Reardriver && IsPowered(item)))
|
|
Crew(item, crew);
|
|
}
|
|
|
|
// The crew lives on the cars - they are what is shown and exported - and the
|
|
// card only mirrors it. A unit has one crew, in the car that has the cab.
|
|
private static void Crew(ConsistItem item, eDriverType crew)
|
|
{
|
|
if (item.Cars.Count == 0)
|
|
return;
|
|
|
|
foreach (var car in item.Cars)
|
|
car.DriverType = eDriverType.Nobody;
|
|
CrewCar(item, crew).DriverType = crew;
|
|
item.Driver = crew;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Where a unit's crew sits: cab 1 is at the front of its first car and cab 2
|
|
/// at the back of its last - the ET42-A has only cab 1, the ET42-B only cab 2.
|
|
/// The unit's own order, so turning it round takes the cabs along.
|
|
/// </summary>
|
|
public static Dynamic CrewCar(ConsistItem item, eDriverType crew) =>
|
|
crew == eDriverType.Reardriver ? item.Cars[^1] : item.Cars[0];
|
|
|
|
/// <summary>Puts the crew into the unit, in the car its cab is in.</summary>
|
|
public void SetCrew(ConsistItem item, eDriverType crew)
|
|
{
|
|
Crew(item, crew);
|
|
if (ReferenceEquals(Selected, item))
|
|
SyncStartingVehicle();
|
|
Raise();
|
|
AppServices.Current.State.NotifyChanged();
|
|
}
|
|
|
|
private bool IsPowered(ConsistItem item) =>
|
|
item.Cars.Count > 0 && _db.TextureFor(item.Cars[0].DataFolder, item.Cars[0].SkinFile, item.Cars[0].MmdFile) is { } texture &&
|
|
VehicleInfo.IsPoweredCategory(VehicleInfo.CategoryOf(texture));
|
|
|
|
public void MoveLeft(ConsistItem item)
|
|
{
|
|
int i = _items.IndexOf(item);
|
|
if (i > 0)
|
|
Move(i, i - 1);
|
|
}
|
|
|
|
public void MoveRight(ConsistItem item)
|
|
{
|
|
int i = _items.IndexOf(item);
|
|
if (i >= 0 && i < _items.Count - 1)
|
|
Move(i, i + 1);
|
|
}
|
|
|
|
public void Move(int from, int to)
|
|
{
|
|
if (from < 0 || from >= _items.Count) return;
|
|
|
|
var previousHead = _items[0];
|
|
var item = _items[from];
|
|
_items.RemoveAt(from);
|
|
to = Math.Clamp(to, 0, _items.Count);
|
|
_items.Insert(to, item);
|
|
|
|
HandOverLead(previousHead);
|
|
Selected = item;
|
|
AutoConnectAll();
|
|
Raise();
|
|
}
|
|
|
|
/// <summary>
|
|
/// A locomotive that comes to the head of the train takes over the driver of
|
|
/// the one it put behind, which is left unstaffed: the train is driven from its
|
|
/// front, from cab A.
|
|
/// </summary>
|
|
private void HandOverLead(ConsistItem previousHead)
|
|
{
|
|
var head = _items[0];
|
|
if (ReferenceEquals(head, previousHead) || !IsPowered(head) ||
|
|
previousHead.Driver is not (eDriverType.Headdriver or eDriverType.Reardriver))
|
|
return;
|
|
|
|
previousHead.Driver = eDriverType.Nobody;
|
|
foreach (var car in previousHead.Cars)
|
|
car.DriverType = eDriverType.Nobody;
|
|
|
|
Crew(head, eDriverType.Headdriver);
|
|
}
|
|
|
|
public void Remove(ConsistItem item)
|
|
{
|
|
int gap = _items.IndexOf(item);
|
|
_items.Remove(item);
|
|
|
|
// The car now at the end still holds the coupler of the one taken off it.
|
|
if (gap == _items.Count)
|
|
FreeTail();
|
|
|
|
if (ReferenceEquals(Selected, item))
|
|
Selected = _items.Count == 0
|
|
? null
|
|
: _items[Math.Clamp(gap, 0, _items.Count - 1)];
|
|
|
|
Raise();
|
|
}
|
|
|
|
public void Flip(ConsistItem item)
|
|
{
|
|
int index = _items.IndexOf(item);
|
|
if (index < 0) return;
|
|
|
|
var (first, last) = UnitAround(index);
|
|
Turn(first, last);
|
|
|
|
AutoConnectAll();
|
|
Raise();
|
|
}
|
|
|
|
/// <summary>The cards locked together with the one at index, as the range they span.</summary>
|
|
private (int First, int Last) UnitAround(int index)
|
|
{
|
|
int first = index;
|
|
while (first > 0 && HoldsTail(_items[first - 1]))
|
|
first--;
|
|
|
|
int last = index;
|
|
while (last + 1 < _items.Count && HoldsTail(_items[last]))
|
|
last++;
|
|
|
|
return (first, last);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Turns the cards first..last end for end - the flag on each card, the cars it
|
|
/// holds and the order of the cards. All three or nothing: the export writes the
|
|
/// cars in card order and each car with its own orientation, so a flag turned
|
|
/// alone couples a multi-car unit back to front.
|
|
/// </summary>
|
|
private void Turn(int first, int last)
|
|
{
|
|
for (int k = first; k <= last; k++)
|
|
{
|
|
var card = _items[k];
|
|
card.Flipped = !card.Flipped;
|
|
foreach (var car in card.Cars)
|
|
car.Offset = car.Offset >= 0 ? -1f : 0f;
|
|
}
|
|
|
|
if (last > first)
|
|
_items.Reverse(first, last - first + 1);
|
|
}
|
|
|
|
public void CycleDriver(ConsistItem item) =>
|
|
SetCrew(item, item.Driver switch
|
|
{
|
|
eDriverType.Nobody => eDriverType.Headdriver,
|
|
eDriverType.Headdriver => eDriverType.Reardriver,
|
|
eDriverType.Reardriver => eDriverType.Passenger,
|
|
_ => eDriverType.Nobody
|
|
});
|
|
|
|
public void Split(ConsistItem item)
|
|
{
|
|
int i = _items.IndexOf(item);
|
|
if (i < 0) return;
|
|
|
|
var order = RunningOrder(item).ToList();
|
|
|
|
_items.RemoveAt(i);
|
|
for (int c = 0; c < order.Count; c++)
|
|
{
|
|
_items.Insert(i + c, new ConsistItem
|
|
{
|
|
Cars = new List<Dynamic> { order[c] },
|
|
Grouped = false,
|
|
// Each car's own way - a unit joined from both ways splits back into them.
|
|
Flipped = IsTurned(order[c]),
|
|
Driver = order[c].DriverType
|
|
});
|
|
}
|
|
Selected = _items[i];
|
|
Raise();
|
|
}
|
|
|
|
public void Join(ConsistItem item)
|
|
{
|
|
int index = _items.IndexOf(item);
|
|
if (index < 0) return;
|
|
|
|
int first = index;
|
|
while (first > 0 && HoldsTail(_items[first - 1]))
|
|
first--;
|
|
|
|
int last = index;
|
|
|
|
bool inUnit = first < index || HoldsTail(_items[index]);
|
|
if (!inUnit && last + 1 < _items.Count)
|
|
{
|
|
var card = _items[last];
|
|
if (card.Cars.Count > 0)
|
|
TailCar(card).Coupling.Set(Coupling.WorkshopLock, true);
|
|
last++;
|
|
}
|
|
|
|
while (last + 1 < _items.Count && HoldsTail(_items[last]))
|
|
last++;
|
|
|
|
if (last == first) return;
|
|
|
|
// The cars as they run, each card read the way it faces, so joining vehicles
|
|
// that face different ways leaves every car where and how it was. The unit
|
|
// keeps the first card's flag; its cars are stored against that flag.
|
|
var cars = new List<Dynamic>();
|
|
var driver = eDriverType.Nobody;
|
|
for (int k = first; k <= last; k++)
|
|
{
|
|
var card = _items[k];
|
|
cars.AddRange(RunningOrder(card));
|
|
if (driver == eDriverType.Nobody)
|
|
driver = card.Driver;
|
|
}
|
|
|
|
bool flipped = _items[first].Flipped;
|
|
if (flipped)
|
|
cars.Reverse();
|
|
|
|
var joined = new ConsistItem
|
|
{
|
|
Cars = cars,
|
|
Grouped = true,
|
|
Flipped = flipped,
|
|
Driver = driver
|
|
};
|
|
|
|
_items.RemoveRange(first, last - first + 1);
|
|
_items.Insert(first, joined);
|
|
Selected = joined;
|
|
|
|
AutoConnectAll();
|
|
Raise();
|
|
}
|
|
|
|
public void RandomizeOrder(Random rng)
|
|
{
|
|
int start = Selected != null ? _items.IndexOf(Selected) : 0;
|
|
if (start < 0) start = 0;
|
|
|
|
var indices = new List<int>();
|
|
for (int i = start; i < _items.Count; i++)
|
|
{
|
|
if (_items[i].Grouped) continue;
|
|
string? cat = _info.CategoryOf(_items[i].Cars[0]);
|
|
if (cat is { Length: 1 } && char.IsUpper(cat[0]))
|
|
indices.Add(i);
|
|
}
|
|
|
|
if (indices.Count < 2) return;
|
|
|
|
var items = indices.Select(i => _items[i]).ToList();
|
|
for (int i = items.Count - 1; i > 0; i--)
|
|
{
|
|
int j = rng.Next(i + 1);
|
|
(items[i], items[j]) = (items[j], items[i]);
|
|
}
|
|
for (int k = 0; k < indices.Count; k++)
|
|
_items[indices[k]] = items[k];
|
|
|
|
AutoConnectAll();
|
|
Raise();
|
|
}
|
|
|
|
public void RandomizeOrientation(Random rng)
|
|
{
|
|
int start = Selected != null ? _items.IndexOf(Selected) : 0;
|
|
if (start < 0) start = 0;
|
|
if (start >= _items.Count) return;
|
|
|
|
// Unit by unit, as Flip turns them: half a locked unit turned on its own
|
|
// would be coupled back to front.
|
|
for (int i = start; i < _items.Count;)
|
|
{
|
|
var (first, last) = UnitAround(i);
|
|
if (rng.Next(2) == 0)
|
|
Turn(first, last);
|
|
i = last + 1;
|
|
}
|
|
|
|
AutoConnectAll();
|
|
Raise();
|
|
}
|
|
|
|
/// <summary>
|
|
/// The Auto button: couplers picked afresh for the whole consist, hand-set ones
|
|
/// included, so the back of the train is 0 again.
|
|
/// </summary>
|
|
public void AutoCouple()
|
|
{
|
|
foreach (var car in Flatten())
|
|
car.CouplerSetByHand = false;
|
|
|
|
AutoConnectAll();
|
|
}
|
|
|
|
public void AutoConnectAll()
|
|
{
|
|
var flat = Flatten();
|
|
|
|
for (int i = 0; i < flat.Count - 1; i++)
|
|
{
|
|
var left = flat[i];
|
|
var right = flat[i + 1];
|
|
bool lf = IsTurned(left);
|
|
bool rf = IsTurned(right);
|
|
|
|
var lp = _info.PhysicsFor(left);
|
|
var rp = _info.PhysicsFor(right);
|
|
|
|
int leftMax = lp == null ? 3 : (lf ? lp.AllowedFlagA : lp.AllowedFlagB);
|
|
int rightMax = rp == null ? 3 : (rf ? rp.AllowedFlagB : rp.AllowedFlagA);
|
|
int common = leftMax & rightMax;
|
|
|
|
string lct = lp == null ? "" : (lf ? lp.ControlTypeA : lp.ControlTypeB);
|
|
string rct = rp == null ? "" : (rf ? rp.ControlTypeB : rp.ControlTypeA);
|
|
if ((common & Coupling.ControlMU) != 0 &&
|
|
!string.Equals(lct, rct, StringComparison.OrdinalIgnoreCase))
|
|
common &= ~Coupling.ControlMU;
|
|
|
|
left.Coupling.Flags = left.Coupling.Locked ? -common : common;
|
|
left.CouplerSetByHand = false;
|
|
}
|
|
|
|
FreeTail();
|
|
}
|
|
|
|
public bool CanFormUnit(ConsistItem left, ConsistItem right)
|
|
{
|
|
if (left.Cars.Count == 0 || right.Cars.Count == 0)
|
|
return false;
|
|
|
|
var tail = TailCar(left);
|
|
var head = HeadCar(right);
|
|
|
|
var lp = _info.PhysicsFor(tail);
|
|
var rp = _info.PhysicsFor(head);
|
|
int leftMax = lp == null ? 3 : (IsTurned(tail) ? lp.AllowedFlagA : lp.AllowedFlagB);
|
|
int rightMax = rp == null ? 3 : (IsTurned(head) ? rp.AllowedFlagB : rp.AllowedFlagA);
|
|
|
|
return (leftMax & rightMax & Coupling.WorkshopLock) != 0 || SameSet(tail, head);
|
|
}
|
|
|
|
public bool SameSet(Dynamic a, Dynamic b)
|
|
{
|
|
if (IsUnitCar(a) && IsUnitCar(b) && UnitKey(a) == UnitKey(b))
|
|
return true;
|
|
|
|
string? ua = _db.TextureFor(a.DataFolder, a.SkinFile, a.MmdFile)?.Uuid;
|
|
string? ub = _db.TextureFor(b.DataFolder, b.SkinFile, b.MmdFile)?.Uuid;
|
|
if (string.IsNullOrEmpty(ua) || string.IsNullOrEmpty(ub))
|
|
return false;
|
|
|
|
return _db.SetByTextureUuid.TryGetValue(ua!, out var sa) &&
|
|
_db.SetByTextureUuid.TryGetValue(ub!, out var sb) &&
|
|
ReferenceEquals(sa, sb) && sa.TextureRefs.Count > 1;
|
|
}
|
|
|
|
public Dynamic ActiveCar(ConsistItem item) =>
|
|
SelectedCar != null && item.Cars.Contains(SelectedCar) ? SelectedCar : item.Cars[0];
|
|
|
|
public static IEnumerable<Dynamic> UnitTargets(ConsistItem item) => item.Cars;
|
|
|
|
public IEnumerable<Dynamic> MemberTargets(ConsistItem item) =>
|
|
item.Cars.Count == 1 ? item.Cars : new[] { ActiveCar(item) };
|
|
|
|
public static Dynamic TailCar(ConsistItem item) =>
|
|
item.Flipped ? item.Cars[0] : item.Cars[^1];
|
|
|
|
public static Dynamic HeadCar(ConsistItem item) =>
|
|
item.Flipped ? item.Cars[^1] : item.Cars[0];
|
|
|
|
/// <summary>
|
|
/// Clears the coupler at the back of the consist, unless it was set in the
|
|
/// editor. The simulator hangs the end signals only on a last vehicle whose
|
|
/// coupler is 0 - the code does nothing else at the back of a train - and every
|
|
/// new car comes with 3. Its parameters, the brake setting among them, stay.
|
|
/// </summary>
|
|
private void FreeTail()
|
|
{
|
|
if (_items.Count == 0 || _items[^1].Cars.Count == 0)
|
|
return;
|
|
|
|
var tail = TailCar(_items[^1]);
|
|
if (!tail.CouplerSetByHand)
|
|
tail.Coupling.Flags = 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether a car runs turned. Its own sign is the truth - it is what the export
|
|
/// writes - and decides which coupler is at which end; a card's flag agrees
|
|
/// with it except in a unit joined from vehicles facing different ways.
|
|
/// </summary>
|
|
public static bool IsTurned(Dynamic car) => car.Offset < 0;
|
|
|
|
public static bool HoldsTail(ConsistItem item) =>
|
|
item.Cars.Count > 0 && HoldsNext(TailCar(item));
|
|
|
|
public static bool HoldsNext(Dynamic car) =>
|
|
car.Coupling.Has(Coupling.WorkshopLock) || car.Coupling.Locked;
|
|
|
|
public static eDriverType UnitDriver(IReadOnlyList<Dynamic> group) =>
|
|
group.Select(c => c.DriverType)
|
|
.Where(d => d != eDriverType.Nobody)
|
|
.DefaultIfEmpty(eDriverType.Nobody)
|
|
.First();
|
|
|
|
public static string Base(string skinOrName) => System.IO.Path.GetFileNameWithoutExtension(skinOrName);
|
|
|
|
public static string UnitKey(Dynamic d) => StripCarSuffix(Base(d.SkinFile));
|
|
|
|
public static bool IsUnitCar(Dynamic d) => UnitKey(d) != Base(d.SkinFile);
|
|
|
|
private static string StripCarSuffix(string name)
|
|
{
|
|
foreach (string suf in CarSuffixes)
|
|
{
|
|
if (name.Length > suf.Length &&
|
|
name.EndsWith(suf, StringComparison.OrdinalIgnoreCase) &&
|
|
char.IsDigit(name[name.Length - suf.Length - 1]))
|
|
return name[..^suf.Length];
|
|
}
|
|
return name;
|
|
}
|
|
|
|
public static string UnitLabel(ConsistItem item)
|
|
{
|
|
string key = UnitKey(item.Cars[0]);
|
|
return item.Cars.Count > 1 ? $"{key} [{item.Cars.Count}]" : key;
|
|
}
|
|
|
|
public static int GetWagonNumber(Dynamic car)
|
|
{
|
|
string name = car.Name ?? "";
|
|
int hash = name.LastIndexOf('#');
|
|
if (hash >= 0 && hash + 1 < name.Length && name[(hash + 1)..].All(char.IsDigit)
|
|
&& int.TryParse(name[(hash + 1)..], out int n))
|
|
return n;
|
|
return 0;
|
|
}
|
|
|
|
public void SetWagonNumber(Dynamic car, int number)
|
|
{
|
|
string name = car.Name ?? "";
|
|
int hash = name.LastIndexOf('#');
|
|
string baseName = (hash >= 0 && hash + 1 < name.Length && name[(hash + 1)..].All(char.IsDigit))
|
|
? name[..hash]
|
|
: name;
|
|
car.Name = number > 0 ? $"{baseName}#{number}" : baseName;
|
|
Raise();
|
|
}
|
|
}
|