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.
This commit is contained in:
maj00r
2026-11-13 16:05:47 +01:00
parent 48af11b4b2
commit d4975796a8
5 changed files with 68 additions and 18 deletions

View File

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

View File

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

View File

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

View File

@@ -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<string, string?> _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);
}
}

View File

@@ -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);
}
/// <summary>The number the timetable gives its train; null with no timetable or no such heading in it.</summary>