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();
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);
}
}

View File

@@ -60,7 +60,7 @@ public sealed class Consist : IReadOnlyList<ConsistItem>
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<ConsistItem>
}
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<ConsistItem>
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<ConsistItem>
}
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<ConsistItem>
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;

View File

@@ -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;
}

View File

@@ -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)
{

View File

@@ -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;

View File

@@ -27,8 +27,16 @@ public sealed class VehicleCatalog
public Dictionary<string, VehicleTexture> TextureBySkin { get; } = new();
/// <summary>For a skin shared by several models, which one dresses which.</summary>
private readonly Dictionary<string, VehicleTexture> _textureBySkinAndModel = new();
/// <summary>
/// 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()
{
@@ -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;
/// <summary>
/// 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.
/// </summary>
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);
/// <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)
{
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(
_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);

View File

@@ -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;

View File

@@ -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"]));

View File

@@ -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()