mirror of
https://github.com/MaSzyna-EU07/StarterNG.git
synced 2026-10-16 23:49:19 +02:00
Tell apart the halves of a unit that share one skin
The ET42-026 dresses both halves in 112E-026 and the scenery tells them apart by the model. Textures were keyed by the skin alone, so the B half became a second A: not part of the set, and listed on its own. A shared skin now gets the model in its id, vehicles are looked up by skin and model, and the vehicle cache is rebuilt for the new ids.
This commit is contained in:
@@ -117,4 +117,34 @@ public class VehicleCatalogTests
|
||||
/// <summary>Builds an entry through the real parser, for catalogue-level tests.</summary>
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ public sealed class Consist : IReadOnlyList<ConsistItem>
|
||||
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<ConsistItem>
|
||||
}
|
||||
|
||||
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<ConsistItem>
|
||||
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<ConsistItem>
|
||||
}
|
||||
|
||||
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<ConsistItem>
|
||||
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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -27,6 +27,9 @@ 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();
|
||||
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// The texture a vehicle wears: by the skin, and where one skin dresses several
|
||||
/// models, by the model too - the scenery names both.
|
||||
/// </summary>
|
||||
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))
|
||||
|
||||
@@ -8,6 +8,9 @@ public sealed class TexturesTxtParser
|
||||
{
|
||||
private const string DeriveCategory = "*";
|
||||
|
||||
/// <summary>Goes into the vehicle cache stamp, so a change to what is parsed rebuilds the cache.</summary>
|
||||
public const int Version = 2;
|
||||
|
||||
public VehicleEntry? Parse(string directory, IReadOnlyList<string> 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) ||
|
||||
|
||||
@@ -79,7 +79,8 @@ public sealed class VehicleCacheRepository : IVehicleRepository
|
||||
/// </summary>
|
||||
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())
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"]));
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user