diff --git a/StarterNG.Tests/VehicleCatalogTests.cs b/StarterNG.Tests/VehicleCatalogTests.cs
index 63901e2..9c3fedb 100644
--- a/StarterNG.Tests/VehicleCatalogTests.cs
+++ b/StarterNG.Tests/VehicleCatalogTests.cs
@@ -117,4 +117,34 @@ public class VehicleCatalogTests
/// Builds an entry through the real parser, for catalogue-level tests.
private static VehicleEntry ParsedEntry(string directory, params string[] lines) =>
new StarterNG.Infrastructure.Vehicles.TexturesTxtParser().Parse(directory, lines)!;
+
+ // The ET42-026: one skin for both halves, told apart by the model.
+ private static VehicleCatalog SharedSkinPair()
+ {
+ var files = new InMemoryFileSystem()
+ .WithFile(TexturesFile("pkp", "et42_v2"),
+ "!=e,ET42-A,ET42-B\n^2\n112E-026.mat=112E-A,ET42-A,ET42-026-A\n112E-026.mat=112E-B,ET42-B,ET42-026-B");
+ var installation = new TestInstallation(files);
+ installation.Vehicles.Load(installation.Library.Vehicles);
+ return installation.Library.Vehicles;
+ }
+
+ [Fact]
+ public void A_skin_shared_by_two_models_is_found_by_the_model()
+ {
+ var catalog = SharedSkinPair();
+
+ Assert.Equal("ET42-026-A", catalog.TextureFor("112E-026", "112E-A")!.TextureMini);
+ Assert.Equal("ET42-026-B", catalog.TextureFor("112E-026", "112E-B")!.TextureMini);
+ }
+
+ [Fact]
+ public void The_second_half_on_a_shared_skin_follows_the_first_in_its_set()
+ {
+ var catalog = SharedSkinPair();
+ var b = catalog.TextureFor("112E-026", "112E-B")!;
+
+ Assert.True(catalog.IsSetFollower(b));
+ Assert.Equal(new[] { "112E-A", "112E-B" }, catalog.ResolveSet(b)!.Select(t => t.Model));
+ }
}
diff --git a/StarterNG/Domain/Consist.cs b/StarterNG/Domain/Consist.cs
index ac374e9..b67ecb9 100644
--- a/StarterNG/Domain/Consist.cs
+++ b/StarterNG/Domain/Consist.cs
@@ -60,7 +60,7 @@ public sealed class Consist : IReadOnlyList
var cars = trainset.Vehicles.Select(v =>
{
var c = v.Clone();
- c.MiniName = _db.MiniForSkin(c.SkinFile) ?? c.MiniName;
+ c.MiniName = _db.MiniFor(c.SkinFile, c.MmdFile) ?? c.MiniName;
return c;
}).ToList();
@@ -86,7 +86,7 @@ public sealed class Consist : IReadOnlyList
}
VehicleSet? set = null;
- if (_db.TextureForSkin(cars[i].SkinFile)?.Uuid is { } uuid)
+ if (_db.TextureFor(cars[i].SkinFile, cars[i].MmdFile)?.Uuid is { } uuid)
_db.SetByTextureUuid.TryGetValue(uuid, out set);
if (set?.TextureRefs is { Count: > 1 })
@@ -97,7 +97,7 @@ public sealed class Consist : IReadOnlyList
int j = i;
while (j < cars.Count && group.Count < set.TextureRefs.Count)
{
- if (_db.TextureForSkin(cars[j].SkinFile)?.Uuid is { } u
+ if (_db.TextureFor(cars[j].SkinFile, cars[j].MmdFile)?.Uuid is { } u
&& members.Contains(u))
group.Add(cars[j++]);
else
@@ -285,7 +285,7 @@ public sealed class Consist : IReadOnlyList
}
private bool IsPowered(ConsistItem item) =>
- item.Cars.Count > 0 && _db.TextureForSkin(item.Cars[0].SkinFile) is { } texture &&
+ item.Cars.Count > 0 && _db.TextureFor(item.Cars[0].SkinFile, item.Cars[0].MmdFile) is { } texture &&
VehicleInfo.IsPoweredCategory(VehicleInfo.CategoryOf(texture));
public void MoveLeft(ConsistItem item)
@@ -611,8 +611,8 @@ public sealed class Consist : IReadOnlyList
if (IsUnitCar(a) && IsUnitCar(b) && UnitKey(a) == UnitKey(b))
return true;
- string? ua = _db.TextureForSkin(a.SkinFile)?.Uuid;
- string? ub = _db.TextureForSkin(b.SkinFile)?.Uuid;
+ string? ua = _db.TextureFor(a.SkinFile, a.MmdFile)?.Uuid;
+ string? ub = _db.TextureFor(b.SkinFile, b.MmdFile)?.Uuid;
if (string.IsNullOrEmpty(ua) || string.IsNullOrEmpty(ub))
return false;
diff --git a/StarterNG/Domain/TexRandomizer.cs b/StarterNG/Domain/TexRandomizer.cs
index 450afd9..849b364 100644
--- a/StarterNG/Domain/TexRandomizer.cs
+++ b/StarterNG/Domain/TexRandomizer.cs
@@ -68,7 +68,7 @@ public sealed class TexRandomizer
var item = consist[i];
if (item.Cars.Count == 0) continue;
- var current = _db.TextureForSkin(item.Cars[0].SkinFile);
+ var current = _db.TextureFor(item.Cars[0].SkinFile, item.Cars[0].MmdFile);
if (current != null && _db.IsSetFollower(current))
continue;
@@ -128,13 +128,13 @@ public sealed class TexRandomizer
if (index > 0)
{
- var prev = _db.TextureForSkin(consist[index - 1].Cars[0].SkinFile);
+ var prev = _db.TextureFor(consist[index - 1].Cars[0].SkinFile, consist[index - 1].Cars[0].MmdFile);
if (prev != null && !string.IsNullOrEmpty(prev.ResolvedClass))
return prev.ResolvedClass;
}
if (index + 1 < consist.Count)
{
- var next = _db.TextureForSkin(consist[index + 1].Cars[0].SkinFile);
+ var next = _db.TextureFor(consist[index + 1].Cars[0].SkinFile, consist[index + 1].Cars[0].MmdFile);
if (next != null && !string.IsNullOrEmpty(next.ResolvedClass))
return next.ResolvedClass;
}
diff --git a/StarterNG/Domain/TrainsetDisplay.cs b/StarterNG/Domain/TrainsetDisplay.cs
index dbc29e6..13f9c40 100644
--- a/StarterNG/Domain/TrainsetDisplay.cs
+++ b/StarterNG/Domain/TrainsetDisplay.cs
@@ -67,14 +67,14 @@ public static class TrainsetDisplay
public static string VehicleLabel(Dynamic v, bool head = false)
{
- var tex = AppServices.Current.Library.Vehicles.TextureForSkin(v.SkinFile);
+ var tex = AppServices.Current.Library.Vehicles.TextureFor(v.SkinFile, v.MmdFile);
string? cls = string.IsNullOrEmpty(tex?.ResolvedClass) ? null : tex!.ResolvedClass;
string? name = string.IsNullOrWhiteSpace(v.Name) ? null : v.Name;
return (head ? name ?? cls : cls ?? name) ?? BaseSkin(v);
}
public static bool IsPowered(Dynamic v) =>
- AppServices.Current.Library.Vehicles.TextureForSkin(v.SkinFile)?.ResolvedCategory
+ AppServices.Current.Library.Vehicles.TextureFor(v.SkinFile, v.MmdFile)?.ResolvedCategory
is "e" or "s" or "p" or "z" or "a";
private static string BaseSkin(Dynamic v) =>
@@ -206,7 +206,7 @@ public static class TrainsetDisplay
bool mu = trainset.Vehicles.Any(v =>
{
- string? cat = db.TextureForSkin(v.SkinFile)?.ResolvedCategory;
+ string? cat = db.TextureFor(v.SkinFile, v.MmdFile)?.ResolvedCategory;
return cat is "z" or "a";
});
@@ -232,7 +232,7 @@ public static class TrainsetDisplay
public static string PreferredBaseName(Dynamic v, VehicleCatalog db)
{
- var tex = db.TextureForSkin(v.SkinFile);
+ var tex = db.TextureFor(v.SkinFile, v.MmdFile);
if (tex != null)
{
diff --git a/StarterNG/Domain/VehicleInfo.cs b/StarterNG/Domain/VehicleInfo.cs
index 537d31c..67c1c8a 100644
--- a/StarterNG/Domain/VehicleInfo.cs
+++ b/StarterNG/Domain/VehicleInfo.cs
@@ -22,7 +22,7 @@ public sealed class VehicleInfo
public static bool IsPoweredCategory(string? c) =>
c is "e" or "s" or "p" or "z" or "a";
- public VehicleTexture? TextureFor(Dynamic car) => _db.TextureForSkin(car.SkinFile);
+ public VehicleTexture? TextureFor(Dynamic car) => _db.TextureFor(car.SkinFile, car.MmdFile);
public string? CategoryOf(Dynamic car) =>
TextureFor(car) is { } t ? CategoryOf(t) : null;
diff --git a/StarterNG/Domain/Vehicles/VehicleCatalog.cs b/StarterNG/Domain/Vehicles/VehicleCatalog.cs
index 095837e..2b3108b 100644
--- a/StarterNG/Domain/Vehicles/VehicleCatalog.cs
+++ b/StarterNG/Domain/Vehicles/VehicleCatalog.cs
@@ -27,6 +27,9 @@ public sealed class VehicleCatalog
public Dictionary TextureBySkin { get; } = new();
+ /// For a skin shared by several models, which one dresses which.
+ private readonly Dictionary _textureBySkinAndModel = new();
+
public void BeginLoad()
{
Textures.Clear();
@@ -35,6 +38,7 @@ public sealed class VehicleCatalog
SetByTextureUuid.Clear();
TextureByUuid.Clear();
TextureBySkin.Clear();
+ _textureBySkinAndModel.Clear();
}
public void Ingest(VehicleEntry entry)
@@ -49,7 +53,10 @@ public sealed class VehicleCatalog
TextureByUuid[texture.Uuid] = texture;
if (!string.IsNullOrEmpty(texture.Skinfile))
- TextureBySkin.TryAdd(Path.GetFileNameWithoutExtension(texture.Skinfile).ToLowerInvariant(), texture);
+ {
+ TextureBySkin.TryAdd(Key(texture.Skinfile), texture);
+ _textureBySkinAndModel.TryAdd(Key(texture.Skinfile, texture.Model), texture);
+ }
if (texture.Wreck)
continue;
@@ -105,18 +112,38 @@ public sealed class VehicleCatalog
return texture.TextureMini;
}
- public string? MiniForSkin(string? skinFile) =>
- TextureForSkin(skinFile) is { } texture ? ResolveMiniName(texture) : null;
+ public string? MiniFor(string? skinFile, string? model) =>
+ TextureFor(skinFile, model) is { } texture ? ResolveMiniName(texture) : null;
+
+ ///
+ /// The texture a vehicle wears: by the skin, and where one skin dresses several
+ /// models, by the model too - the scenery names both.
+ ///
+ public VehicleTexture? TextureFor(string? skinFile, string? model)
+ {
+ if (string.IsNullOrEmpty(skinFile))
+ return null;
+
+ if (!string.IsNullOrEmpty(model) &&
+ _textureBySkinAndModel.TryGetValue(Key(skinFile, model), out var exact))
+ return exact;
+
+ return TextureForSkin(skinFile);
+ }
public VehicleTexture? TextureForSkin(string? skinFile)
{
if (string.IsNullOrEmpty(skinFile))
return null;
- string key = Path.GetFileNameWithoutExtension(skinFile).ToLowerInvariant();
- return TextureBySkin.TryGetValue(key, out var texture) ? texture : null;
+ return TextureBySkin.TryGetValue(Key(skinFile), out var texture) ? texture : null;
}
+ private static string Key(string skinFile) => Path.GetFileNameWithoutExtension(skinFile).ToLowerInvariant();
+
+ private static string Key(string skinFile, string? model) =>
+ Key(skinFile) + "|" + (string.IsNullOrEmpty(model) ? "" : Path.GetFileNameWithoutExtension(model).ToLowerInvariant());
+
public string GroupHeader(string? groupId)
{
if (string.IsNullOrEmpty(groupId) || !GroupsById.TryGetValue(groupId, out var group))
diff --git a/StarterNG/Infrastructure/Vehicles/TexturesTxtParser.cs b/StarterNG/Infrastructure/Vehicles/TexturesTxtParser.cs
index ba731ec..3eebfe8 100644
--- a/StarterNG/Infrastructure/Vehicles/TexturesTxtParser.cs
+++ b/StarterNG/Infrastructure/Vehicles/TexturesTxtParser.cs
@@ -8,6 +8,9 @@ public sealed class TexturesTxtParser
{
private const string DeriveCategory = "*";
+ /// Goes into the vehicle cache stamp, so a change to what is parsed rebuilds the cache.
+ public const int Version = 2;
+
public VehicleEntry? Parse(string directory, IReadOnlyList lines)
{
var entry = new VehicleEntry { Uuid = "legacy:" + directory };
@@ -133,9 +136,16 @@ public sealed class TexturesTxtParser
Implicit = true
});
+ // One skin may dress several models - the ET42-026 has a single 112E-026 for
+ // both halves - and the scenery tells them apart by the model, so the later
+ // ones are named by it as well, or they would all be the first.
+ string uuid = $"legacy:{directory}{skin}";
+ if (entry.Textures.Exists(t => string.Equals(t.Skinfile, skin, StringComparison.OrdinalIgnoreCase)))
+ uuid += "#" + lead.Model;
+
var texture = new VehicleTexture
{
- Uuid = $"legacy:{directory}{skin}",
+ Uuid = uuid,
Directory = directory,
Skinfile = skin,
Wreck = skin.Contains("wreck", StringComparison.OrdinalIgnoreCase) ||
diff --git a/StarterNG/Infrastructure/Vehicles/VehicleCacheRepository.cs b/StarterNG/Infrastructure/Vehicles/VehicleCacheRepository.cs
index 5befa5a..3e83ece 100644
--- a/StarterNG/Infrastructure/Vehicles/VehicleCacheRepository.cs
+++ b/StarterNG/Infrastructure/Vehicles/VehicleCacheRepository.cs
@@ -79,7 +79,8 @@ public sealed class VehicleCacheRepository : IVehicleRepository
///
private (string Text, int Sources) Stamp()
{
- var stamp = new StringBuilder().Append("root|").Append(Path.GetFullPath(_paths.Root)).Append('\n');
+ var stamp = new StringBuilder().Append("root|").Append(Path.GetFullPath(_paths.Root)).Append('\n')
+ .Append("parser|").Append(TexturesTxtParser.Version).Append('\n');
int sources = 0;
foreach (string path in _legacy.Sources())
diff --git a/StarterNG/Views/Depot.axaml.cs b/StarterNG/Views/Depot.axaml.cs
index 88be57d..f277a74 100644
--- a/StarterNG/Views/Depot.axaml.cs
+++ b/StarterNG/Views/Depot.axaml.cs
@@ -254,7 +254,7 @@ public partial class Depot : UserControl
StarterNG.Infrastructure.StatsBar.Fill(trainStats, TrainsetDisplay.StatsFields(
_consist.EditingTrainset,
_info.PhysicsFor,
- car => _db.TextureForSkin(car.SkinFile) is { } t ? VehicleInfo.CategoryOf(t) : null,
+ car => _db.TextureFor(car.SkinFile, car.MmdFile) is { } t ? VehicleInfo.CategoryOf(t) : null,
AppServices.Current.LoadWeights.Table.WeightOf));
}
@@ -607,7 +607,7 @@ public partial class Depot : UserControl
private void AddCopiesAfter(ConsistItem item, int count)
{
int index = _consist.IndexOf(item);
- if (index < 0 || item.Cars.Count == 0 || _db.TextureForSkin(item.Cars[0].SkinFile) is not { } texture)
+ if (index < 0 || item.Cars.Count == 0 || _db.TextureFor(item.Cars[0].SkinFile, item.Cars[0].MmdFile) is not { } texture)
return;
InsertTexturesAt(texture, index + 1, count);
diff --git a/StarterNG/Views/Depot/VehicleBrowser.cs b/StarterNG/Views/Depot/VehicleBrowser.cs
index b699225..2e0f7c7 100644
--- a/StarterNG/Views/Depot/VehicleBrowser.cs
+++ b/StarterNG/Views/Depot/VehicleBrowser.cs
@@ -590,7 +590,7 @@ public sealed class VehicleBrowser
public void SelectInBrowser(Dynamic car)
{
- var texture = _db.TextureForSkin(car.SkinFile);
+ var texture = _db.TextureFor(car.SkinFile, car.MmdFile);
if (texture is null)
return;
@@ -635,6 +635,7 @@ public sealed class VehicleBrowser
foreach (var obj in vehicleListBox.Items)
if (obj is ListBoxItem { Tag: VehicleTexture t } entry &&
string.Equals(t.Skinfile, texture.Skinfile, StringComparison.OrdinalIgnoreCase) &&
+ string.Equals(t.Model, texture.Model, StringComparison.OrdinalIgnoreCase) &&
string.Equals(t.Directory, texture.Directory, StringComparison.OrdinalIgnoreCase))
{
vehicleListBox.SelectedItem = entry;
diff --git a/StarterNG/Views/Depot/VehicleDetails.cs b/StarterNG/Views/Depot/VehicleDetails.cs
index 6ee32b7..a7d42ac 100644
--- a/StarterNG/Views/Depot/VehicleDetails.cs
+++ b/StarterNG/Views/Depot/VehicleDetails.cs
@@ -97,7 +97,7 @@ public sealed class VehicleDetails
BuildGeneralInfo(_consist.Selected);
- if (_db.TextureForSkin(_consist.ActiveCar(_consist.Selected).SkinFile) is { } tex)
+ if (_db.TextureFor(_consist.ActiveCar(_consist.Selected).SkinFile, _consist.ActiveCar(_consist.Selected).MmdFile) is { } tex)
_showTextureInfo(tex, consistTexturePanel);
else
consistTexturePanel.Children.Add(PanelNote(App.Loc["NoTextureInfo"]));
diff --git a/StarterNG/Views/Scenarios.axaml.cs b/StarterNG/Views/Scenarios.axaml.cs
index 7e8d6da..7bcad5d 100644
--- a/StarterNG/Views/Scenarios.axaml.cs
+++ b/StarterNG/Views/Scenarios.axaml.cs
@@ -363,14 +363,14 @@ public partial class Scenarios : UserControl
foreach (var v in ts.Vehicles)
{
if (string.IsNullOrEmpty(v.SkinFile)) continue;
- if (db.TextureForSkin(v.SkinFile) is null)
+ if (db.TextureFor(v.SkinFile, v.MmdFile) is null)
lines.Add($"# unknown texture: {v.Name} ({v.SkinFile})");
}
}
foreach (var v in scenery.LooseVehicles)
{
- if (db.TextureForSkin(v.SkinFile) is null)
+ if (db.TextureFor(v.SkinFile, v.MmdFile) is null)
lines.Add($"# unknown loose texture: {v.Name} ({v.SkinFile})");
}
@@ -860,7 +860,7 @@ public partial class Scenarios : UserControl
foreach (var train in trainset.Vehicles)
{
- string miniName = db.MiniForSkin(train.SkinFile) ?? train.SkinFile;
+ string miniName = db.MiniFor(train.SkinFile, train.MmdFile) ?? train.SkinFile;
if (!AppServices.Current.MiniTextures.Has(miniName) && AppServices.Current.MiniTextures.Has(train.SkinFile))
miniName = train.SkinFile;
int thumbH = AppServices.Current.Settings.LargeThumbnails ? 64 : 32;
@@ -1161,10 +1161,10 @@ public partial class Scenarios : UserControl
var db = AppServices.Current.Library.Vehicles;
StarterNG.Infrastructure.StatsBar.Fill(trainStats, TrainsetDisplay.StatsFields(
trainset,
- car => AppServices.Current.Physics.For(car.DataFolder, db.TextureForSkin(car.SkinFile)?.Model)
+ car => AppServices.Current.Physics.For(car.DataFolder, db.TextureFor(car.SkinFile, car.MmdFile)?.Model)
?? AppServices.Current.Physics.For(car.DataFolder, car.MmdFile)
?? AppServices.Current.Physics.For(car.DataFolder, car.SkinFile),
- car => db.TextureForSkin(car.SkinFile)?.ResolvedCategory));
+ car => db.TextureFor(car.SkinFile, car.MmdFile)?.ResolvedCategory));
}
private void RefreshSelectedConsist()