Turn multi-car units as a whole when orientation is randomised or a unit swapped

Randomising flipped only the card's flag, so a multi-car unit was exported
with its cars in reverse order but each still facing forward - coupled back
to front. A swap copied the flag without turning the new cars. Both now turn
flag, cars and order together, as Flip does, and a locked unit as one.
This commit is contained in:
maj00r
2026-10-04 13:54:51 +02:00
parent c1c47f746e
commit d510a55740
5 changed files with 133 additions and 19 deletions

View File

@@ -20,7 +20,7 @@ public class ConsistCrewTests
installation.Vehicles.Load(installation.Library.Vehicles); installation.Vehicles.Load(installation.Library.Vehicles);
_catalog = installation.Library.Vehicles; _catalog = installation.Library.Vehicles;
_consist = new Consist(_catalog, new VehicleInfo(_catalog)); _consist = new Consist(_catalog, new VehicleInfo(_catalog, installation.Physics));
} }
[Fact] [Fact]

View File

@@ -0,0 +1,82 @@
using StarterNG.Classes;
using StarterNG.Domain;
using StarterNG.Domain.Vehicles;
using StarterNG.Tests.Fakes;
namespace StarterNG.Tests;
public class ConsistOrientationTests
{
private readonly VehicleCatalog _catalog;
private readonly TestInstallation _installation;
public ConsistOrientationTests()
{
var installation = new TestInstallation(new InMemoryFileSystem()
.WithFile(TestInstallation.At("dynamic", "pkp", "et40", "textures.txt"),
"!=e\n^2\net40-10-a.mat=et40a,ET40-A\net40-10-b.mat=et40b,ET40-B\n" +
"et40-16-a.mat=et40a,ET40-A\net40-16-b.mat=et40b,ET40-B"));
installation.Vehicles.Load(installation.Library.Vehicles);
_catalog = installation.Library.Vehicles;
_installation = installation;
}
[Fact]
public void Randomly_turned_unit_comes_out_like_a_flipped_one()
{
var flipped = NewConsist(out var unit);
flipped.Flip(unit);
var randomized = NewConsist(out _);
randomized.RandomizeOrientation(new AlwaysTurn());
Assert.Equal(Describe(flipped), Describe(randomized));
}
[Fact]
public void A_turned_unit_has_its_cars_reversed_and_each_one_turned()
{
var consist = NewConsist(out _);
consist.RandomizeOrientation(new AlwaysTurn());
// B leads, A trails, and both face backwards - not A-B reordered but facing forward.
Assert.Equal(new[] { "et40-10-b -1", "et40-10-a -1" }, Describe(consist));
}
[Fact]
public void A_unit_put_in_place_of_a_turned_one_faces_the_same_way()
{
var consist = NewConsist(out var unit);
consist.Flip(unit);
consist.Replace(0, Unit(consist, "et40-16-a"));
Assert.Equal(new[] { "et40-16-b -1", "et40-16-a -1" }, Describe(consist));
}
private Consist NewConsist(out ConsistItem unit)
{
var consist = new Consist(_catalog, new VehicleInfo(_catalog, _installation.Physics));
unit = Unit(consist, "et40-10-a");
consist.Insert(0, unit);
return consist;
}
private ConsistItem Unit(Consist consist, string lead) => new()
{
Cars = _catalog.ResolveSet(_catalog.TextureForSkin(lead)!)!
.Select(texture => consist.MakeDynamic(texture, null))
.ToList(),
Grouped = true
};
// The export order, each car with the way it faces.
private static string[] Describe(Consist consist) =>
consist.Flatten().Select(car => $"{car.SkinFile} {(car.Offset < 0 ? -1 : 1)}").ToArray();
private sealed class AlwaysTurn : Random
{
public override int Next(int maxValue) => 0;
}
}

View File

@@ -256,9 +256,9 @@ public sealed class Consist : IReadOnlyList<ConsistItem>
} }
/// <summary> /// <summary>
/// Puts item in place of the vehicle at index. Its crew moves over where the /// Puts item in place of the vehicle at index, facing the same way. Its crew
/// replacement can take it - a driver only into something powered - and the /// moves over where the replacement can take it - a driver only into something
/// rest is staffed as a newly added vehicle would be. /// powered - and the rest is staffed as a newly added vehicle would be.
/// </summary> /// </summary>
public void Replace(int index, ConsistItem item) public void Replace(int index, ConsistItem item)
{ {
@@ -266,6 +266,10 @@ public sealed class Consist : IReadOnlyList<ConsistItem>
_items[index] = item; _items[index] = item;
Staff(item, index); 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; var crew = replaced.Driver;
if (crew == eDriverType.Passenger || if (crew == eDriverType.Passenger ||
(crew is eDriverType.Headdriver or eDriverType.Reardriver && IsPowered(item))) (crew is eDriverType.Headdriver or eDriverType.Reardriver && IsPowered(item)))
@@ -339,6 +343,16 @@ public sealed class Consist : IReadOnlyList<ConsistItem>
int index = _items.IndexOf(item); int index = _items.IndexOf(item);
if (index < 0) return; 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; int first = index;
while (first > 0 && HoldsTail(_items[first - 1])) while (first > 0 && HoldsTail(_items[first - 1]))
first--; first--;
@@ -347,6 +361,17 @@ public sealed class Consist : IReadOnlyList<ConsistItem>
while (last + 1 < _items.Count && HoldsTail(_items[last])) while (last + 1 < _items.Count && HoldsTail(_items[last]))
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++) for (int k = first; k <= last; k++)
{ {
var card = _items[k]; var card = _items[k];
@@ -357,9 +382,6 @@ public sealed class Consist : IReadOnlyList<ConsistItem>
if (last > first) if (last > first)
_items.Reverse(first, last - first + 1); _items.Reverse(first, last - first + 1);
AutoConnectAll();
Raise();
} }
public void CycleDriver(ConsistItem item) public void CycleDriver(ConsistItem item)
@@ -492,9 +514,15 @@ public sealed class Consist : IReadOnlyList<ConsistItem>
if (start < 0) start = 0; if (start < 0) start = 0;
if (start >= _items.Count) return; if (start >= _items.Count) return;
for (int i = start; i < _items.Count; i++) // 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) if (rng.Next(2) == 0)
_items[i].Flipped = !_items[i].Flipped; Turn(first, last);
i = last + 1;
}
AutoConnectAll(); AutoConnectAll();
Raise(); Raise();

View File

@@ -1,14 +1,19 @@
using StarterNG.Classes; using StarterNG.Classes;
using StarterNG.Domain.Vehicles; using StarterNG.Domain.Vehicles;
using StarterNG.Application; using StarterNG.Application.Abstractions;
namespace StarterNG.Domain; namespace StarterNG.Domain;
public sealed class VehicleInfo public sealed class VehicleInfo
{ {
private readonly VehicleCatalog _db; private readonly VehicleCatalog _db;
private readonly IPhysicsRepository _physics;
public VehicleInfo(VehicleCatalog db) => _db = db; public VehicleInfo(VehicleCatalog db, IPhysicsRepository physics)
{
_db = db;
_physics = physics;
}
public static string ClassOf(VehicleTexture t) => t.ResolvedClass; public static string ClassOf(VehicleTexture t) => t.ResolvedClass;
@@ -23,14 +28,14 @@ public sealed class VehicleInfo
TextureFor(car) is { } t ? CategoryOf(t) : null; TextureFor(car) is { } t ? CategoryOf(t) : null;
public VehiclePhysics? PhysicsFor(VehicleTexture texture) => public VehiclePhysics? PhysicsFor(VehicleTexture texture) =>
AppServices.Current.Physics.For(texture.Directory, texture.Model) _physics.For(texture.Directory, texture.Model)
?? AppServices.Current.Physics.For(texture.Directory, texture.Skinfile); ?? _physics.For(texture.Directory, texture.Skinfile);
public VehiclePhysics? PhysicsFor(Dynamic car) public VehiclePhysics? PhysicsFor(Dynamic car)
{ {
string? dbModel = TextureFor(car)?.Model; string? dbModel = TextureFor(car)?.Model;
return AppServices.Current.Physics.For(car.DataFolder, dbModel) return _physics.For(car.DataFolder, dbModel)
?? AppServices.Current.Physics.For(car.DataFolder, car.MmdFile) ?? _physics.For(car.DataFolder, car.MmdFile)
?? AppServices.Current.Physics.For(car.DataFolder, car.SkinFile); ?? _physics.For(car.DataFolder, car.SkinFile);
} }
} }

View File

@@ -56,7 +56,7 @@ public partial class Depot : UserControl
public Depot() public Depot()
{ {
_info = new VehicleInfo(_db); _info = new VehicleInfo(_db, AppServices.Current.Physics);
_consist = new Consist(_db, _info); _consist = new Consist(_db, _info);
_cargo = new Cargo(_info, _rng); _cargo = new Cargo(_info, _rng);
_consist.Changed += RebuildConsist; _consist.Changed += RebuildConsist;
@@ -572,8 +572,7 @@ public partial class Depot : UserControl
var item = new ConsistItem var item = new ConsistItem
{ {
Cars = unit.Select(NewVehicle).ToList(), Cars = unit.Select(NewVehicle).ToList(),
Grouped = unit.Count > 1, Grouped = unit.Count > 1
Flipped = _consist.Selected.Flipped
}; };
_consist.Replace(i, item); _consist.Replace(i, item);
_consist.Selected = item; _consist.Selected = item;