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.
This commit is contained in:
maj00r
2026-10-04 19:07:23 +02:00
parent e5c46d23ea
commit 8c046cfed4
10 changed files with 75 additions and 39 deletions

View File

@@ -134,17 +134,31 @@ public class VehicleCatalogTests
{ {
var catalog = SharedSkinPair(); var catalog = SharedSkinPair();
Assert.Equal("ET42-026-A", catalog.TextureFor("112E-026", "112E-A")!.TextureMini); Assert.Equal("ET42-026-A", catalog.TextureFor(@"PKP\ET42_V2", "112E-026", "112E-A")!.TextureMini);
Assert.Equal("ET42-026-B", catalog.TextureFor("112E-026", "112E-B")!.TextureMini); Assert.Equal("ET42-026-B", catalog.TextureFor(@"PKP\ET42_V2", "112E-026", "112E-B")!.TextureMini);
} }
[Fact] [Fact]
public void The_second_half_on_a_shared_skin_follows_the_first_in_its_set() public void The_second_half_on_a_shared_skin_follows_the_first_in_its_set()
{ {
var catalog = SharedSkinPair(); 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.True(catalog.IsSetFollower(b));
Assert.Equal(new[] { "112E-A", "112E-B" }, catalog.ResolveSet(b)!.Select(t => t.Model)); 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);
}
} }

View File

@@ -60,7 +60,7 @@ public sealed class Consist : IReadOnlyList<ConsistItem>
var cars = trainset.Vehicles.Select(v => var cars = trainset.Vehicles.Select(v =>
{ {
var c = v.Clone(); 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; return c;
}).ToList(); }).ToList();
@@ -86,7 +86,7 @@ public sealed class Consist : IReadOnlyList<ConsistItem>
} }
VehicleSet? set = null; 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); _db.SetByTextureUuid.TryGetValue(uuid, out set);
if (set?.TextureRefs is { Count: > 1 }) if (set?.TextureRefs is { Count: > 1 })
@@ -97,7 +97,7 @@ public sealed class Consist : IReadOnlyList<ConsistItem>
int j = i; int j = i;
while (j < cars.Count && group.Count < set.TextureRefs.Count) 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)) && members.Contains(u))
group.Add(cars[j++]); group.Add(cars[j++]);
else else
@@ -285,7 +285,7 @@ public sealed class Consist : IReadOnlyList<ConsistItem>
} }
private bool IsPowered(ConsistItem item) => 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)); VehicleInfo.IsPoweredCategory(VehicleInfo.CategoryOf(texture));
public void MoveLeft(ConsistItem item) public void MoveLeft(ConsistItem item)
@@ -611,8 +611,8 @@ public sealed class Consist : IReadOnlyList<ConsistItem>
if (IsUnitCar(a) && IsUnitCar(b) && UnitKey(a) == UnitKey(b)) if (IsUnitCar(a) && IsUnitCar(b) && UnitKey(a) == UnitKey(b))
return true; return true;
string? ua = _db.TextureFor(a.SkinFile, a.MmdFile)?.Uuid; string? ua = _db.TextureFor(a.DataFolder, a.SkinFile, a.MmdFile)?.Uuid;
string? ub = _db.TextureFor(b.SkinFile, b.MmdFile)?.Uuid; string? ub = _db.TextureFor(b.DataFolder, b.SkinFile, b.MmdFile)?.Uuid;
if (string.IsNullOrEmpty(ua) || string.IsNullOrEmpty(ub)) if (string.IsNullOrEmpty(ua) || string.IsNullOrEmpty(ub))
return false; return false;

View File

@@ -68,7 +68,7 @@ public sealed class TexRandomizer
var item = consist[i]; var item = consist[i];
if (item.Cars.Count == 0) continue; 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)) if (current != null && _db.IsSetFollower(current))
continue; continue;
@@ -128,13 +128,13 @@ public sealed class TexRandomizer
if (index > 0) 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)) if (prev != null && !string.IsNullOrEmpty(prev.ResolvedClass))
return prev.ResolvedClass; return prev.ResolvedClass;
} }
if (index + 1 < consist.Count) 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)) if (next != null && !string.IsNullOrEmpty(next.ResolvedClass))
return next.ResolvedClass; return next.ResolvedClass;
} }

View File

@@ -67,14 +67,14 @@ public static class TrainsetDisplay
public static string VehicleLabel(Dynamic v, bool head = false) 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? cls = string.IsNullOrEmpty(tex?.ResolvedClass) ? null : tex!.ResolvedClass;
string? name = string.IsNullOrWhiteSpace(v.Name) ? null : v.Name; string? name = string.IsNullOrWhiteSpace(v.Name) ? null : v.Name;
return (head ? name ?? cls : cls ?? name) ?? BaseSkin(v); return (head ? name ?? cls : cls ?? name) ?? BaseSkin(v);
} }
public static bool IsPowered(Dynamic 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"; is "e" or "s" or "p" or "z" or "a";
private static string BaseSkin(Dynamic v) => private static string BaseSkin(Dynamic v) =>
@@ -206,7 +206,7 @@ public static class TrainsetDisplay
bool mu = trainset.Vehicles.Any(v => 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"; return cat is "z" or "a";
}); });
@@ -232,7 +232,7 @@ public static class TrainsetDisplay
public static string PreferredBaseName(Dynamic v, VehicleCatalog db) 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) if (tex != null)
{ {

View File

@@ -22,7 +22,7 @@ public sealed class VehicleInfo
public static bool IsPoweredCategory(string? c) => public static bool IsPoweredCategory(string? c) =>
c is "e" or "s" or "p" or "z" or "a"; 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) => public string? CategoryOf(Dynamic car) =>
TextureFor(car) is { } t ? CategoryOf(t) : null; TextureFor(car) is { } t ? CategoryOf(t) : null;

View File

@@ -27,8 +27,16 @@ public sealed class VehicleCatalog
public Dictionary<string, VehicleTexture> TextureBySkin { get; } = new(); public Dictionary<string, VehicleTexture> TextureBySkin { get; } = new();
/// <summary>For a skin shared by several models, which one dresses which.</summary> /// <summary>
private readonly Dictionary<string, VehicleTexture> _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).
/// </summary>
private readonly Dictionary<string, VehicleTexture> _textureByFolderSkinModel = new();
private readonly Dictionary<string, VehicleTexture> _textureByFolderSkin = new();
private readonly Dictionary<string, VehicleTexture> _textureBySkinModel = new();
public void BeginLoad() public void BeginLoad()
{ {
@@ -38,7 +46,9 @@ public sealed class VehicleCatalog
SetByTextureUuid.Clear(); SetByTextureUuid.Clear();
TextureByUuid.Clear(); TextureByUuid.Clear();
TextureBySkin.Clear(); TextureBySkin.Clear();
_textureBySkinAndModel.Clear(); _textureByFolderSkinModel.Clear();
_textureByFolderSkin.Clear();
_textureBySkinModel.Clear();
} }
public void Ingest(VehicleEntry entry) public void Ingest(VehicleEntry entry)
@@ -55,7 +65,9 @@ public sealed class VehicleCatalog
if (!string.IsNullOrEmpty(texture.Skinfile)) if (!string.IsNullOrEmpty(texture.Skinfile))
{ {
TextureBySkin.TryAdd(Key(texture.Skinfile), texture); 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) if (texture.Wreck)
@@ -112,21 +124,22 @@ public sealed class VehicleCatalog
return texture.TextureMini; return texture.TextureMini;
} }
public string? MiniFor(string? skinFile, string? model) => public string? MiniFor(string? dataFolder, string? skinFile, string? model) =>
TextureFor(skinFile, model) is { } texture ? ResolveMiniName(texture) : null; TextureFor(dataFolder, skinFile, model) is { } texture ? ResolveMiniName(texture) : null;
/// <summary> /// <summary>
/// The texture a vehicle wears: by the skin, and where one skin dresses several /// The texture a scenery vehicle wears, matched like the old starter did: by
/// models, by the model too - the scenery names both. /// folder, skin and model, then by less of it for a vehicle that moved.
/// </summary> /// </summary>
public VehicleTexture? TextureFor(string? skinFile, string? model) public VehicleTexture? TextureFor(string? dataFolder, string? skinFile, string? model)
{ {
if (string.IsNullOrEmpty(skinFile)) if (string.IsNullOrEmpty(skinFile))
return null; return null;
if (!string.IsNullOrEmpty(model) && if (_textureByFolderSkinModel.TryGetValue(FolderSkinModelKey(dataFolder, skinFile, model), out var texture) ||
_textureBySkinAndModel.TryGetValue(Key(skinFile, model), out var exact)) _textureByFolderSkin.TryGetValue(FolderSkinKey(dataFolder, skinFile), out texture) ||
return exact; _textureBySkinModel.TryGetValue(SkinModelKey(skinFile, model), out texture))
return texture;
return TextureForSkin(skinFile); 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) => 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()); 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);
/// <summary>"PKP\ET42_V2" in a scenery, "pkp/et42_v2/" in the catalogue.</summary>
private static string Folder(string? folder) =>
(folder ?? "").Replace('\\', '/').Trim('/').ToLowerInvariant();
public string GroupHeader(string? groupId) public string GroupHeader(string? groupId)
{ {
if (string.IsNullOrEmpty(groupId) || !GroupsById.TryGetValue(groupId, out var group)) if (string.IsNullOrEmpty(groupId) || !GroupsById.TryGetValue(groupId, out var group))

View File

@@ -254,7 +254,7 @@ public partial class Depot : UserControl
StarterNG.Infrastructure.StatsBar.Fill(trainStats, TrainsetDisplay.StatsFields( StarterNG.Infrastructure.StatsBar.Fill(trainStats, TrainsetDisplay.StatsFields(
_consist.EditingTrainset, _consist.EditingTrainset,
_info.PhysicsFor, _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)); AppServices.Current.LoadWeights.Table.WeightOf));
} }
@@ -607,7 +607,7 @@ public partial class Depot : UserControl
private void AddCopiesAfter(ConsistItem item, int count) private void AddCopiesAfter(ConsistItem item, int count)
{ {
int index = _consist.IndexOf(item); 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; return;
InsertTexturesAt(texture, index + 1, count); InsertTexturesAt(texture, index + 1, count);

View File

@@ -590,7 +590,7 @@ public sealed class VehicleBrowser
public void SelectInBrowser(Dynamic car) 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) if (texture is null)
return; return;

View File

@@ -97,7 +97,7 @@ public sealed class VehicleDetails
BuildGeneralInfo(_consist.Selected); 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); _showTextureInfo(tex, consistTexturePanel);
else else
consistTexturePanel.Children.Add(PanelNote(App.Loc["NoTextureInfo"])); consistTexturePanel.Children.Add(PanelNote(App.Loc["NoTextureInfo"]));

View File

@@ -363,14 +363,14 @@ public partial class Scenarios : UserControl
foreach (var v in ts.Vehicles) foreach (var v in ts.Vehicles)
{ {
if (string.IsNullOrEmpty(v.SkinFile)) continue; 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})"); lines.Add($"# unknown texture: {v.Name} ({v.SkinFile})");
} }
} }
foreach (var v in scenery.LooseVehicles) 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})"); lines.Add($"# unknown loose texture: {v.Name} ({v.SkinFile})");
} }
@@ -860,7 +860,7 @@ public partial class Scenarios : UserControl
foreach (var train in trainset.Vehicles) 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)) if (!AppServices.Current.MiniTextures.Has(miniName) && AppServices.Current.MiniTextures.Has(train.SkinFile))
miniName = train.SkinFile; miniName = train.SkinFile;
int thumbH = AppServices.Current.Settings.LargeThumbnails ? 64 : 32; int thumbH = AppServices.Current.Settings.LargeThumbnails ? 64 : 32;
@@ -1161,10 +1161,10 @@ public partial class Scenarios : UserControl
var db = AppServices.Current.Library.Vehicles; var db = AppServices.Current.Library.Vehicles;
StarterNG.Infrastructure.StatsBar.Fill(trainStats, TrainsetDisplay.StatsFields( StarterNG.Infrastructure.StatsBar.Fill(trainStats, TrainsetDisplay.StatsFields(
trainset, 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.MmdFile)
?? AppServices.Current.Physics.For(car.DataFolder, car.SkinFile), ?? 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() private void RefreshSelectedConsist()