From 8c046cfed4a43e829b466db173f9d28a4e3878e6 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 4 Oct 2026 19:07:23 +0200 Subject: [PATCH] Match scenery vehicles by folder too, as the old starter did The old starter looked a vehicle up by folder, skin and model. The folder was left out here, and 39 skin names recur across folders - "red" is a Clio, an Audi and a crane - so a vehicle could get another folder's livery. --- StarterNG.Tests/VehicleCatalogTests.cs | 20 +++++++-- StarterNG/Domain/Consist.cs | 12 +++--- StarterNG/Domain/TexRandomizer.cs | 6 +-- StarterNG/Domain/TrainsetDisplay.cs | 8 ++-- StarterNG/Domain/VehicleInfo.cs | 2 +- StarterNG/Domain/Vehicles/VehicleCatalog.cs | 48 +++++++++++++++------ StarterNG/Views/Depot.axaml.cs | 4 +- StarterNG/Views/Depot/VehicleBrowser.cs | 2 +- StarterNG/Views/Depot/VehicleDetails.cs | 2 +- StarterNG/Views/Scenarios.axaml.cs | 10 ++--- 10 files changed, 75 insertions(+), 39 deletions(-) diff --git a/StarterNG.Tests/VehicleCatalogTests.cs b/StarterNG.Tests/VehicleCatalogTests.cs index 9c3fedb..19b9201 100644 --- a/StarterNG.Tests/VehicleCatalogTests.cs +++ b/StarterNG.Tests/VehicleCatalogTests.cs @@ -134,17 +134,31 @@ public class VehicleCatalogTests { 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); + Assert.Equal("ET42-026-A", catalog.TextureFor(@"PKP\ET42_V2", "112E-026", "112E-A")!.TextureMini); + Assert.Equal("ET42-026-B", catalog.TextureFor(@"PKP\ET42_V2", "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")!; + var b = catalog.TextureFor(@"PKP\ET42_V2", "112E-026", "112E-B")!; Assert.True(catalog.IsSetFollower(b)); Assert.Equal(new[] { "112E-A", "112E-B" }, catalog.ResolveSet(b)!.Select(t => t.Model)); } + + [Fact] + public void A_skin_name_used_in_two_folders_is_found_by_the_folder() + { + var files = new InMemoryFileSystem() + .WithFile(TexturesFile("road", "clio"), "red.mat=clio,CLIO") + .WithFile(TexturesFile("industrial", "wls50_v1"), "red.mat=wls50,WLS50"); + var installation = new TestInstallation(files); + installation.Vehicles.Load(installation.Library.Vehicles); + var catalog = installation.Library.Vehicles; + + Assert.Equal("clio", catalog.TextureFor(@"road\clio", "red", "clio")!.Model); + Assert.Equal("wls50", catalog.TextureFor("industrial/wls50_v1", "red", "wls50.mmd")!.Model); + } } diff --git a/StarterNG/Domain/Consist.cs b/StarterNG/Domain/Consist.cs index b67ecb9..4009545 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.MiniFor(c.SkinFile, c.MmdFile) ?? c.MiniName; + c.MiniName = _db.MiniFor(c.DataFolder, c.SkinFile, c.MmdFile) ?? c.MiniName; return c; }).ToList(); @@ -86,7 +86,7 @@ public sealed class Consist : IReadOnlyList } VehicleSet? set = null; - if (_db.TextureFor(cars[i].SkinFile, cars[i].MmdFile)?.Uuid is { } uuid) + 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 }) @@ -97,7 +97,7 @@ public sealed class Consist : IReadOnlyList int j = i; while (j < cars.Count && group.Count < set.TextureRefs.Count) { - if (_db.TextureFor(cars[j].SkinFile, cars[j].MmdFile)?.Uuid is { } u + if (_db.TextureFor(cars[j].DataFolder, 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.TextureFor(item.Cars[0].SkinFile, item.Cars[0].MmdFile) is { } texture && + 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) @@ -611,8 +611,8 @@ public sealed class Consist : IReadOnlyList if (IsUnitCar(a) && IsUnitCar(b) && UnitKey(a) == UnitKey(b)) return true; - string? ua = _db.TextureFor(a.SkinFile, a.MmdFile)?.Uuid; - string? ub = _db.TextureFor(b.SkinFile, b.MmdFile)?.Uuid; + 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; diff --git a/StarterNG/Domain/TexRandomizer.cs b/StarterNG/Domain/TexRandomizer.cs index 849b364..23b4206 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.TextureFor(item.Cars[0].SkinFile, item.Cars[0].MmdFile); + var current = _db.TextureFor(item.Cars[0].DataFolder, 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.TextureFor(consist[index - 1].Cars[0].SkinFile, consist[index - 1].Cars[0].MmdFile); + var prev = _db.TextureFor(consist[index - 1].Cars[0].DataFolder, 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.TextureFor(consist[index + 1].Cars[0].SkinFile, consist[index + 1].Cars[0].MmdFile); + var next = _db.TextureFor(consist[index + 1].Cars[0].DataFolder, 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 13f9c40..1877da9 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.TextureFor(v.SkinFile, v.MmdFile); + var tex = AppServices.Current.Library.Vehicles.TextureFor(v.DataFolder, 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.TextureFor(v.SkinFile, v.MmdFile)?.ResolvedCategory + AppServices.Current.Library.Vehicles.TextureFor(v.DataFolder, 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.TextureFor(v.SkinFile, v.MmdFile)?.ResolvedCategory; + string? cat = db.TextureFor(v.DataFolder, 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.TextureFor(v.SkinFile, v.MmdFile); + var tex = db.TextureFor(v.DataFolder, v.SkinFile, v.MmdFile); if (tex != null) { diff --git a/StarterNG/Domain/VehicleInfo.cs b/StarterNG/Domain/VehicleInfo.cs index 67c1c8a..a88019d 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.TextureFor(car.SkinFile, car.MmdFile); + public VehicleTexture? TextureFor(Dynamic car) => _db.TextureFor(car.DataFolder, 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 2b3108b..8c8098b 100644 --- a/StarterNG/Domain/Vehicles/VehicleCatalog.cs +++ b/StarterNG/Domain/Vehicles/VehicleCatalog.cs @@ -27,8 +27,16 @@ 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(); + /// + /// The way the old starter matched a scenery vehicle: folder, skin and model. + /// A skin name recurs across folders ("red" for a Clio and a crane), and one + /// skin may dress several models (both halves of the ET42-026). + /// + private readonly Dictionary _textureByFolderSkinModel = new(); + + private readonly Dictionary _textureByFolderSkin = new(); + + private readonly Dictionary _textureBySkinModel = new(); public void BeginLoad() { @@ -38,7 +46,9 @@ public sealed class VehicleCatalog SetByTextureUuid.Clear(); TextureByUuid.Clear(); TextureBySkin.Clear(); - _textureBySkinAndModel.Clear(); + _textureByFolderSkinModel.Clear(); + _textureByFolderSkin.Clear(); + _textureBySkinModel.Clear(); } public void Ingest(VehicleEntry entry) @@ -55,7 +65,9 @@ public sealed class VehicleCatalog if (!string.IsNullOrEmpty(texture.Skinfile)) { TextureBySkin.TryAdd(Key(texture.Skinfile), texture); - _textureBySkinAndModel.TryAdd(Key(texture.Skinfile, texture.Model), texture); + _textureByFolderSkinModel.TryAdd(FolderSkinModelKey(texture.Directory, texture.Skinfile, texture.Model), texture); + _textureByFolderSkin.TryAdd(FolderSkinKey(texture.Directory, texture.Skinfile), texture); + _textureBySkinModel.TryAdd(SkinModelKey(texture.Skinfile, texture.Model), texture); } if (texture.Wreck) @@ -112,21 +124,22 @@ public sealed class VehicleCatalog return texture.TextureMini; } - public string? MiniFor(string? skinFile, string? model) => - TextureFor(skinFile, model) is { } texture ? ResolveMiniName(texture) : null; + public string? MiniFor(string? dataFolder, string? skinFile, string? model) => + TextureFor(dataFolder, 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. + /// The texture a scenery vehicle wears, matched like the old starter did: by + /// folder, skin and model, then by less of it for a vehicle that moved. /// - public VehicleTexture? TextureFor(string? skinFile, string? model) + public VehicleTexture? TextureFor(string? dataFolder, string? skinFile, string? model) { if (string.IsNullOrEmpty(skinFile)) return null; - if (!string.IsNullOrEmpty(model) && - _textureBySkinAndModel.TryGetValue(Key(skinFile, model), out var exact)) - return exact; + if (_textureByFolderSkinModel.TryGetValue(FolderSkinModelKey(dataFolder, skinFile, model), out var texture) || + _textureByFolderSkin.TryGetValue(FolderSkinKey(dataFolder, skinFile), out texture) || + _textureBySkinModel.TryGetValue(SkinModelKey(skinFile, model), out texture)) + return texture; return TextureForSkin(skinFile); } @@ -141,9 +154,18 @@ public sealed class VehicleCatalog private static string Key(string skinFile) => Path.GetFileNameWithoutExtension(skinFile).ToLowerInvariant(); - private static string Key(string skinFile, string? model) => + private static string SkinModelKey(string skinFile, string? model) => Key(skinFile) + "|" + (string.IsNullOrEmpty(model) ? "" : Path.GetFileNameWithoutExtension(model).ToLowerInvariant()); + private static string FolderSkinKey(string? folder, string skinFile) => Folder(folder) + "|" + Key(skinFile); + + private static string FolderSkinModelKey(string? folder, string skinFile, string? model) => + Folder(folder) + "|" + SkinModelKey(skinFile, model); + + /// "PKP\ET42_V2" in a scenery, "pkp/et42_v2/" in the catalogue. + private static string Folder(string? folder) => + (folder ?? "").Replace('\\', '/').Trim('/').ToLowerInvariant(); + public string GroupHeader(string? groupId) { if (string.IsNullOrEmpty(groupId) || !GroupsById.TryGetValue(groupId, out var group)) diff --git a/StarterNG/Views/Depot.axaml.cs b/StarterNG/Views/Depot.axaml.cs index f277a74..af9f5fe 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.TextureFor(car.SkinFile, car.MmdFile) is { } t ? VehicleInfo.CategoryOf(t) : null, + car => _db.TextureFor(car.DataFolder, 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.TextureFor(item.Cars[0].SkinFile, item.Cars[0].MmdFile) is not { } texture) + if (index < 0 || item.Cars.Count == 0 || _db.TextureFor(item.Cars[0].DataFolder, 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 2e0f7c7..b597cfd 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.TextureFor(car.SkinFile, car.MmdFile); + var texture = _db.TextureFor(car.DataFolder, car.SkinFile, car.MmdFile); if (texture is null) return; diff --git a/StarterNG/Views/Depot/VehicleDetails.cs b/StarterNG/Views/Depot/VehicleDetails.cs index a7d42ac..d185269 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.TextureFor(_consist.ActiveCar(_consist.Selected).SkinFile, _consist.ActiveCar(_consist.Selected).MmdFile) is { } tex) + if (_db.TextureFor(_consist.ActiveCar(_consist.Selected).DataFolder, _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 7bcad5d..f4f52fd 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.TextureFor(v.SkinFile, v.MmdFile) is null) + if (db.TextureFor(v.DataFolder, v.SkinFile, v.MmdFile) is null) lines.Add($"# unknown texture: {v.Name} ({v.SkinFile})"); } } foreach (var v in scenery.LooseVehicles) { - if (db.TextureFor(v.SkinFile, v.MmdFile) is null) + if (db.TextureFor(v.DataFolder, 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.MiniFor(train.SkinFile, train.MmdFile) ?? train.SkinFile; + string miniName = db.MiniFor(train.DataFolder, 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.TextureFor(car.SkinFile, car.MmdFile)?.Model) + car => AppServices.Current.Physics.For(car.DataFolder, db.TextureFor(car.DataFolder, car.SkinFile, car.MmdFile)?.Model) ?? AppServices.Current.Physics.For(car.DataFolder, car.MmdFile) ?? AppServices.Current.Physics.For(car.DataFolder, car.SkinFile), - car => db.TextureFor(car.SkinFile, car.MmdFile)?.ResolvedCategory)); + car => db.TextureFor(car.DataFolder, car.SkinFile, car.MmdFile)?.ResolvedCategory)); } private void RefreshSelectedConsist()