From d4975796a8253d19c7e538f1502bcf851ef4aa4e Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 13 Nov 2026 16:05:47 +0100 Subject: [PATCH] fix: find a scenery's image and timetables whatever the case of their names The //$i image and a trainset's timetable are looked up through CaseInsensitivePaths, so on Linux they show when the scenery names them in other letter case than the disk has. A timetable written with backslashes is found there too, and with it the train number of a Hasler tape. --- StarterNG.Tests/ScenarioPresentationTests.cs | 14 ++++++++++ StarterNG.Tests/SceneryRepositoryTests.cs | 17 ++++++++++++ StarterNG.Tests/TapeLaunchTests.cs | 11 ++++++++ .../Sceneries/SceneryImageLocator.cs | 26 +++++++++++-------- .../Sceneries/TimetableLocator.cs | 18 ++++++++----- 5 files changed, 68 insertions(+), 18 deletions(-) diff --git a/StarterNG.Tests/ScenarioPresentationTests.cs b/StarterNG.Tests/ScenarioPresentationTests.cs index 70b3c2f..8448790 100644 --- a/StarterNG.Tests/ScenarioPresentationTests.cs +++ b/StarterNG.Tests/ScenarioPresentationTests.cs @@ -182,6 +182,20 @@ public class TimetableLocatorTests Assert.NotNull(new TimetableLocator(files).Resolve(scenery, "r1.txt")); } + [Theory] + [InlineData(@"rozklady\tgs3657")] + [InlineData("ROZKLADY/TGS3657.TXT")] + public void A_timetable_named_in_other_case_or_with_backslashes_is_found_as_on_disk(string name) + { + var files = new InMemoryFileSystem(caseSensitive: true) + .WithFile(TestInstallation.At("scenery", "Braniewo.scn"), "//$n Braniewo") + .WithFile(TestInstallation.At("scenery", "Rozklady", "TGS3657.txt"), "rozkład"); + var scenery = Assert.Single(new TestInstallation(files).Sceneries.LoadAll()); + + Assert.Equal(TestInstallation.At("scenery", "Rozklady", "TGS3657.txt"), + new TimetableLocator(files).Resolve(scenery, name)); + } + [Fact] public void The_formats_none_and_a_missing_file_both_resolve_to_nothing() { diff --git a/StarterNG.Tests/SceneryRepositoryTests.cs b/StarterNG.Tests/SceneryRepositoryTests.cs index 08dbfcb..44fd753 100644 --- a/StarterNG.Tests/SceneryRepositoryTests.cs +++ b/StarterNG.Tests/SceneryRepositoryTests.cs @@ -89,6 +89,23 @@ public class SceneryRepositoryTests Assert.Equal(Path.Combine("game", "scenery", "images", "kato.jpg"), image); } + [Theory] + [InlineData("calkowo_v2_kopalniaA.jpg", "game/scenery/images/calkowo_v2_kopalniaA.jpg")] + [InlineData(@"Images\Calkowo_V2_KopalniaA.JPG", "game/scenery/images/calkowo_v2_kopalniaA.jpg")] + [InlineData("CALKOWO_V2.JPG", "game/scenery/Calkowo_v2.jpg")] + public void The_preview_image_is_found_whatever_the_case_it_is_named_in(string named, string onDisk) + { + var files = new InMemoryFileSystem(caseSensitive: true) + .WithFile("game/scenery/calkowo.scn", "//$i " + named) + .WithFile("game/scenery/images/calkowo_v2_kopalniaA.jpg", "binary") + .WithFile("game/scenery/Calkowo_v2.jpg", "binary"); + var scenery = Assert.Single(Build(files, out _).LoadAll()); + + string? image = new SceneryImageLocator(files).Resolve(scenery); + + Assert.Equal(Path.GetFullPath(onDisk), Path.GetFullPath(image!)); + } + [Fact] public void A_scenery_without_an_image_directive_resolves_to_nothing() { diff --git a/StarterNG.Tests/TapeLaunchTests.cs b/StarterNG.Tests/TapeLaunchTests.cs index a1285fa..ae1689f 100644 --- a/StarterNG.Tests/TapeLaunchTests.cs +++ b/StarterNG.Tests/TapeLaunchTests.cs @@ -130,6 +130,17 @@ public class TapeLaunchTests Assert.Null(new StarterNG.Infrastructure.Sceneries.TimetableLocator(new InMemoryFileSystem()) .TrainNumber(new StarterNG.Domain.Sceneries.Scenery(TestInstallation.At("scenery", "td.scn"), ""), name)); + [Fact] + public void The_train_number_comes_from_a_timetable_named_in_other_case_with_backslashes() + { + var files = new InMemoryFileSystem(caseSensitive: true) + .WithFile(TestInstallation.At("scenery", "Baltyk", "EIE1531.txt"), "[ Rodzaj i numer pociagu | EIE1531 ]\r\n"); + + Assert.Equal("EIE1531", new StarterNG.Infrastructure.Sceneries.TimetableLocator(files) + .TrainNumber(new StarterNG.Domain.Sceneries.Scenery(TestInstallation.At("scenery", "td.scn"), ""), + @"baltyk\eie1531")); + } + [Fact] public void A_timetable_that_is_not_there_gives_no_train_number() => Assert.Null(new StarterNG.Infrastructure.Sceneries.TimetableLocator(new InMemoryFileSystem()) diff --git a/StarterNG/Infrastructure/Sceneries/SceneryImageLocator.cs b/StarterNG/Infrastructure/Sceneries/SceneryImageLocator.cs index dba90fe..df266a7 100644 --- a/StarterNG/Infrastructure/Sceneries/SceneryImageLocator.cs +++ b/StarterNG/Infrastructure/Sceneries/SceneryImageLocator.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.IO; +using System.Linq; using StarterNG.Application.Abstractions; using StarterNG.Domain.Sceneries; @@ -8,11 +9,13 @@ namespace StarterNG.Infrastructure.Sceneries; public sealed class SceneryImageLocator { private readonly IFileSystem _files; + private readonly CaseInsensitivePaths _names; private readonly Dictionary _cache = new(); public SceneryImageLocator(IFileSystem files) { _files = files; + _names = new CaseInsensitivePaths(files); } public string? Resolve(Scenery scenery) @@ -34,22 +37,23 @@ public sealed class SceneryImageLocator return null; string name = scenery.ImageName.Replace('\\', '/').Trim(); + if (Path.IsPathFullyQualified(name)) + return _names.Find("", name); + if (_files.FileExists(name)) + return name; + string sceneryDir = Path.GetDirectoryName(scenery.Path) ?? "."; string root = Path.GetDirectoryName(sceneryDir) ?? "."; - string[] candidates = + (string Folder, string Name)[] candidates = { - name, - Path.Combine(root, name), - Path.Combine(sceneryDir, name), - Path.Combine(sceneryDir, "images", name), - Path.Combine(root, "scenery", "images", name) + (root, name), + (sceneryDir, name), + (sceneryDir, "images/" + name), + (root, "scenery/images/" + name) }; - foreach (string candidate in candidates) - if (_files.FileExists(candidate)) - return candidate; - - return null; + return candidates.Select(candidate => _names.Find(candidate.Folder, candidate.Name)) + .FirstOrDefault(found => found is not null); } } diff --git a/StarterNG/Infrastructure/Sceneries/TimetableLocator.cs b/StarterNG/Infrastructure/Sceneries/TimetableLocator.cs index d309b47..35a7614 100644 --- a/StarterNG/Infrastructure/Sceneries/TimetableLocator.cs +++ b/StarterNG/Infrastructure/Sceneries/TimetableLocator.cs @@ -17,10 +17,12 @@ public sealed class TimetableLocator RegexOptions.Multiline | RegexOptions.IgnoreCase); private readonly IFileSystem _files; + private readonly CaseInsensitivePaths _names; public TimetableLocator(IFileSystem files) { _files = files; + _names = new CaseInsensitivePaths(files); } public string? Resolve(Scenery scenery, string? name) @@ -30,17 +32,19 @@ public sealed class TimetableLocator string sceneryDir = Path.GetDirectoryName(scenery.Path) ?? "."; string root = Path.GetDirectoryName(sceneryDir) ?? "."; + name = name.Replace('\\', '/'); - string[] candidates = + (string Folder, string Name)[] candidates = { - Path.Combine(root, "timetables", name + ".txt"), - Path.Combine(sceneryDir, name + ".txt"), - Path.Combine(root, "scenario", name + ".txt"), - Path.Combine(root, "timetables", name), - Path.Combine(sceneryDir, name) + (root, "timetables/" + name + ".txt"), + (sceneryDir, name + ".txt"), + (root, "scenario/" + name + ".txt"), + (root, "timetables/" + name), + (sceneryDir, name) }; - return candidates.FirstOrDefault(_files.FileExists); + return candidates.Select(candidate => _names.Find(candidate.Folder, candidate.Name)) + .FirstOrDefault(found => found is not null); } /// The number the timetable gives its train; null with no timetable or no such heading in it.