mirror of
https://github.com/MaSzyna-EU07/StarterNG.git
synced 2026-11-13 20:03:19 +01:00
Compare commits
17 Commits
v1.5.228
...
fix/linux-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7cd3ef8f1a | ||
|
|
1b41ef752a | ||
|
|
42f702a096 | ||
|
|
7f57160aae | ||
|
|
2efae83b57 | ||
|
|
34f2ce99e8 | ||
|
|
52408a9931 | ||
|
|
290b753f40 | ||
|
|
6279228a7d | ||
|
|
d4975796a8 | ||
|
|
48af11b4b2 | ||
|
|
744720d922 | ||
|
|
b5ba67430f | ||
|
|
c261d4b0ab | ||
|
|
3aa6a256a5 | ||
|
|
95c510b3ac | ||
|
|
da5a1efea1 |
119
StarterNG.Tests/CaseInsensitivePathsTests.cs
Normal file
119
StarterNG.Tests/CaseInsensitivePathsTests.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
using StarterNG.Infrastructure;
|
||||
using StarterNG.Tests.Fakes;
|
||||
|
||||
namespace StarterNG.Tests;
|
||||
|
||||
public class CaseInsensitivePathsTests
|
||||
{
|
||||
private static InMemoryFileSystem Disk() => new InMemoryFileSystem(caseSensitive: true)
|
||||
.WithFile("game/scenery/Braniewo/Base.ctr", "")
|
||||
.WithFile("game/scenery/Braniewo/rozklady/TGS3657.txt", "")
|
||||
.WithDirectory("game/textures/mini");
|
||||
|
||||
[Theory]
|
||||
[InlineData("braniewo/base.ctr", "Braniewo/Base.ctr")]
|
||||
[InlineData(@"BRANIEWO\Rozklady\tgs3657.TXT", "Braniewo/rozklady/TGS3657.txt")]
|
||||
[InlineData("Braniewo/Base.ctr", "Braniewo/Base.ctr")]
|
||||
[InlineData("braniewo//base.ctr", "Braniewo/Base.ctr")]
|
||||
[InlineData("/braniewo/base.ctr", "Braniewo/Base.ctr")]
|
||||
[InlineData(@"\Braniewo\Base.ctr", "Braniewo/Base.ctr")]
|
||||
public void A_name_in_any_case_is_given_as_it_is_on_disk(string name, string onDisk) =>
|
||||
Assert.Equal(onDisk, new CaseInsensitivePaths(Disk()).Resolve("game/scenery", name));
|
||||
|
||||
[Theory]
|
||||
[InlineData("braniewo/brak.ctr")]
|
||||
[InlineData("brak/base.ctr")]
|
||||
[InlineData("")]
|
||||
public void A_name_nothing_answers_to_is_not_found(string name) =>
|
||||
Assert.Null(new CaseInsensitivePaths(Disk()).Resolve("game/scenery", name));
|
||||
|
||||
[Fact]
|
||||
public void A_folder_is_found_only_when_one_is_asked_for()
|
||||
{
|
||||
var names = new CaseInsensitivePaths(Disk());
|
||||
|
||||
Assert.Equal("Braniewo/rozklady", names.Resolve("game/scenery", "braniewo/ROZKLADY", directory: true));
|
||||
Assert.Null(names.Resolve("game/scenery", "braniewo/ROZKLADY"));
|
||||
Assert.Null(names.Resolve("game/scenery", "braniewo/base.ctr", directory: true));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_name_written_as_on_disk_wins_over_one_in_other_case()
|
||||
{
|
||||
var files = Disk().WithFile("game/scenery/braniewo/base.ctr", "");
|
||||
var names = new CaseInsensitivePaths(files);
|
||||
|
||||
Assert.Equal("braniewo/base.ctr", names.Resolve("game/scenery", "braniewo/base.ctr"));
|
||||
Assert.Equal("Braniewo/Base.ctr", names.Resolve("game/scenery", "Braniewo/base.CTR"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Of_folders_named_alike_the_one_holding_the_file_is_taken()
|
||||
{
|
||||
var files = Disk().WithFile("game/scenery/braniewo/tory.scm", "");
|
||||
var names = new CaseInsensitivePaths(files);
|
||||
|
||||
Assert.Equal("Braniewo/Base.ctr", names.Resolve("game/scenery", "braniewo/base.ctr"));
|
||||
Assert.Equal("braniewo/tory.scm", names.Resolve("game/scenery", "Braniewo/Tory.scm"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_full_path_joins_the_folder_and_the_name_on_disk() =>
|
||||
Assert.Equal(Path.Combine("game/scenery", "Braniewo", "Base.ctr"),
|
||||
new CaseInsensitivePaths(Disk()).Find("game/scenery", "BRANIEWO/BASE.CTR"));
|
||||
|
||||
[Fact]
|
||||
public void A_full_path_is_found_from_its_own_root()
|
||||
{
|
||||
string scenery = Path.GetFullPath(Path.Combine("game", "scenery"));
|
||||
var files = new InMemoryFileSystem(caseSensitive: true)
|
||||
.WithFile(Path.Combine(scenery, "Braniewo", "Base.ctr"), "");
|
||||
|
||||
Assert.Equal(Path.Combine(scenery, "Braniewo", "Base.ctr"),
|
||||
new CaseInsensitivePaths(files).Find("elsewhere", Path.Combine(scenery, "BRANIEWO", "base.CTR")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_disk_that_ignores_case_gives_the_name_back_as_written()
|
||||
{
|
||||
var files = new InMemoryFileSystem().WithFile("game/scenery/Braniewo/Base.ctr", "");
|
||||
var names = new CaseInsensitivePaths(files);
|
||||
|
||||
Assert.Equal("BRANIEWO/base.CTR", names.Resolve("game/scenery", "BRANIEWO/base.CTR"));
|
||||
Assert.Null(names.Resolve("game/scenery", "braniewo/brak.ctr"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Kept_listings_hold_until_forgotten()
|
||||
{
|
||||
var files = Disk();
|
||||
var names = new CaseInsensitivePaths(files, keepListings: true);
|
||||
Assert.Null(names.Resolve("game/scenery", "braniewo/nowy.scm"));
|
||||
|
||||
files.WithFile("game/scenery/Braniewo/Nowy.scm", "");
|
||||
Assert.Null(names.Resolve("game/scenery", "braniewo/nowy.scm"));
|
||||
|
||||
names.Forget();
|
||||
Assert.Equal("Braniewo/Nowy.scm", names.Resolve("game/scenery", "braniewo/nowy.scm"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Listings_not_kept_see_a_file_added_since()
|
||||
{
|
||||
var files = Disk();
|
||||
var names = new CaseInsensitivePaths(files);
|
||||
Assert.Null(names.Resolve("game/scenery", "braniewo/nowy.scm"));
|
||||
|
||||
files.WithFile("game/scenery/Braniewo/Nowy.scm", "");
|
||||
|
||||
Assert.Equal("Braniewo/Nowy.scm", names.Resolve("game/scenery", "braniewo/nowy.scm"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_folder_that_cannot_be_listed_holds_nothing_to_find()
|
||||
{
|
||||
var files = Disk().DenyListing("game/scenery");
|
||||
|
||||
Assert.Null(new CaseInsensitivePaths(files).Resolve("game/scenery", "braniewo/base.ctr"));
|
||||
}
|
||||
}
|
||||
@@ -97,6 +97,8 @@ public sealed class InMemoryFileSystem : IFileSystem
|
||||
return this;
|
||||
}
|
||||
|
||||
public bool IgnoresCase => _comparison == StringComparison.OrdinalIgnoreCase;
|
||||
|
||||
public bool FileExists(string path) => _files.ContainsKey(Normalize(path));
|
||||
|
||||
/// <summary>A link counts as the directory it points at, as the disk has it.</summary>
|
||||
@@ -273,6 +275,9 @@ public sealed class InMemoryFileSystem : IFileSystem
|
||||
return;
|
||||
slash = key.LastIndexOf('/');
|
||||
}
|
||||
// An absolute Unix path's parent is the root, "/", which normalises to "".
|
||||
if (slash == 0)
|
||||
_directories.Add("");
|
||||
}
|
||||
|
||||
private static string Normalize(string path) =>
|
||||
|
||||
28
StarterNG.Tests/LoadingScreenLogoTests.cs
Normal file
28
StarterNG.Tests/LoadingScreenLogoTests.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using StarterNG.Infrastructure;
|
||||
using StarterNG.Tests.Fakes;
|
||||
|
||||
namespace StarterNG.Tests;
|
||||
|
||||
public class LoadingScreenLogoTests
|
||||
{
|
||||
private static InMemoryFileSystem Logos() => new InMemoryFileSystem(caseSensitive: true)
|
||||
.WithFile("game/Textures/Logo/EP09_Logo.PNG", "png")
|
||||
.WithFile("game/Textures/Logo/Braniewo.JPG", "jpg")
|
||||
.WithFile("game/Textures/Logo/Logo1.BMP", "bmp")
|
||||
.WithFile("game/Textures/Logo/notes.txt", "");
|
||||
|
||||
[Theory]
|
||||
[InlineData("ep09_logo", null, "EP09_Logo.PNG")]
|
||||
[InlineData("ep09_logo.bmp", "braniewo", "EP09_Logo.PNG")]
|
||||
[InlineData(null, "braniewo", "Braniewo.JPG")]
|
||||
[InlineData("brak", "brak", "Logo1.BMP")]
|
||||
public void The_logo_is_found_whatever_the_case_of_its_name_and_extension(string? logo, string? scenery,
|
||||
string onDisk) =>
|
||||
Assert.Equal(Path.GetFullPath(Path.Combine("game", "Textures", "Logo", onDisk)),
|
||||
Path.GetFullPath(LoadingScreen.FindSource(Logos(), "game", logo, scenery, new Random(1))!));
|
||||
|
||||
[Fact]
|
||||
public void With_no_logo_folder_there_is_no_logo() =>
|
||||
Assert.Null(LoadingScreen.FindSource(new InMemoryFileSystem(caseSensitive: true), "game", "x", "y",
|
||||
new Random(1)));
|
||||
}
|
||||
@@ -21,6 +21,20 @@ public sealed class PhysicalFileSystemTests : IDisposable
|
||||
Assert.Equal(new[] { path }, Directory.GetFiles(_root));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_pattern_lists_names_in_any_letter_case()
|
||||
{
|
||||
Directory.CreateDirectory(Path.Combine(_root, "PKP", "EP09"));
|
||||
foreach (string name in new[] { "Braniewo.SCN", "kato.scn", "notes.txt", Path.Combine("PKP", "EP09", "EP09.FIZ"),
|
||||
Path.Combine("PKP", "EP09", "ep09dumb.fiz") })
|
||||
File.WriteAllText(Path.Combine(_root, name), "");
|
||||
|
||||
Assert.Equal(new[] { "Braniewo.SCN", "kato.scn" },
|
||||
_files.GetFiles(_root, "*.scn").Select(Path.GetFileName).Order(StringComparer.Ordinal));
|
||||
Assert.Equal(new[] { "EP09.FIZ", "ep09dumb.fiz" },
|
||||
_files.GetFilesRecursive(_root, "*.fiz").Select(Path.GetFileName).Order(StringComparer.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void An_atomic_write_that_cannot_finish_leaves_what_stood_there()
|
||||
{
|
||||
|
||||
@@ -65,6 +65,32 @@ public class PhysicsAndAssetsTests
|
||||
Assert.Equal(125, physics.VMax);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void An_include_named_in_other_case_is_followed()
|
||||
{
|
||||
var installation = new TestInstallation(new InMemoryFileSystem(caseSensitive: true)
|
||||
.WithFile(Fiz("pkp", "ep07", "ep07"), "include Common.FIZ 84000 end")
|
||||
.WithFile(TestInstallation.At("dynamic", "pkp", "ep07", "common.fiz"), "Param: M=(1) Vmax=125"));
|
||||
|
||||
var physics = installation.Physics.For("pkp/ep07/", "ep07")!;
|
||||
|
||||
Assert.Equal(84000, physics.Mass);
|
||||
Assert.Equal(125, physics.VMax);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_dot_in_the_model_name_is_part_of_the_name()
|
||||
{
|
||||
// As in the game: cd/9-107_v1 holds 9-107.13.fiz, 9-107.6.fiz and more.
|
||||
var installation = new TestInstallation(new InMemoryFileSystem()
|
||||
.WithFile(Fiz("cd", "9-107_v1", "9-107.13"), "Load: LoadAccepted=coal MaxLoad=58")
|
||||
.WithFile(Fiz("cd", "9-107_v1", "9-107.6"), "Load: LoadAccepted=coal MaxLoad=55"));
|
||||
|
||||
Assert.Equal(58, installation.Physics.For("cd/9-107_v1/", "9-107.13")!.MaxLoad);
|
||||
Assert.Equal(55, installation.Physics.For("cd/9-107_v1/", "9-107.6")!.MaxLoad);
|
||||
Assert.Equal(58, installation.Physics.For("cd/9-107_v1/", "9-107.13.e3d")!.MaxLoad);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_dumb_variant_stands_in_when_the_model_has_no_fiz_of_its_own()
|
||||
{
|
||||
@@ -189,4 +215,31 @@ public class PhysicsAndAssetsTests
|
||||
|
||||
Assert.Empty(installation.MissingAssets.Scan(installation.Library.Vehicles));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_livery_with_no_folder_is_looked_for_in_dynamic_itself()
|
||||
{
|
||||
var installation = new TestInstallation(new InMemoryFileSystem(caseSensitive: true)
|
||||
.WithFile(TestInstallation.At("dynamic", "EP09.bmp"), "bitmap")
|
||||
.WithFile(TestInstallation.At("dynamic", "ep09.e3d"), "model"));
|
||||
var catalog = new StarterNG.Domain.Vehicles.VehicleCatalog(installation.MiniTextures);
|
||||
catalog.Textures.Add(new StarterNG.Domain.Vehicles.VehicleTexture { Directory = "", Skinfile = "ep09", Model = "EP09" });
|
||||
|
||||
Assert.Empty(installation.MissingAssets.Scan(catalog));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_skin_and_model_on_disk_in_other_case_are_not_reported()
|
||||
{
|
||||
var files = new InMemoryFileSystem(caseSensitive: true)
|
||||
.WithFile(TestInstallation.At("dynamic", "PKP", "EP09", "textures.txt"),
|
||||
"EP09-014.mat=ep09.e3d,ep09")
|
||||
.WithFile(TestInstallation.At("dynamic", "PKP", "EP09", "ep09-014.BMP"), "bitmap")
|
||||
.WithFile(TestInstallation.At("dynamic", "PKP", "EP09", "EP09.E3D"), "model");
|
||||
var installation = new TestInstallation(files);
|
||||
installation.Vehicles.Load(installation.Library.Vehicles);
|
||||
Assert.NotEmpty(installation.Library.Vehicles.Textures);
|
||||
|
||||
Assert.Empty(installation.MissingAssets.Scan(installation.Library.Vehicles));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
@@ -195,3 +209,26 @@ public class TimetableLocatorTests
|
||||
Assert.Null(locator.Resolve(scenery, null));
|
||||
}
|
||||
}
|
||||
|
||||
public class AttachmentLocatorTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(@"scenery\Braniewo\opis.PDF")]
|
||||
[InlineData("SCENERY/braniewo/Opis.pdf")]
|
||||
public void An_attachment_named_in_other_case_is_found_as_on_disk(string named)
|
||||
{
|
||||
var installation = new TestInstallation(new InMemoryFileSystem(caseSensitive: true)
|
||||
.WithFile(TestInstallation.At("scenery", "Braniewo", "Opis.pdf"), "%PDF"));
|
||||
|
||||
Assert.Equal(TestInstallation.At("scenery", "Braniewo", "Opis.pdf"),
|
||||
new AttachmentLocator(installation.Files, installation.Paths)
|
||||
.Resolve(new SceneryAttachment { FilePath = named }));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("scenery/brak.pdf")]
|
||||
[InlineData("")]
|
||||
public void An_attachment_that_is_not_there_is_not_found(string named) =>
|
||||
Assert.Null(new AttachmentLocator(new InMemoryFileSystem(caseSensitive: true), new TestInstallation().Paths)
|
||||
.Resolve(new SceneryAttachment { FilePath = named }));
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -273,7 +273,7 @@ public class ScenerySyntaxTests
|
||||
include koniec.scm
|
||||
""";
|
||||
|
||||
var scenery = TestScenery.Parser().Parse("scenery/x.scn", scn, _ => new IncludeLookup(true));
|
||||
var scenery = TestScenery.Parser().Parse("scenery/x.scn", scn, _ => true);
|
||||
|
||||
Assert.Empty(scenery.Faults);
|
||||
Assert.Equal("td/rozklad", Assert.Single(scenery.Trainsets).Name);
|
||||
@@ -326,7 +326,7 @@ public class ScenerySyntaxTests
|
||||
endoptional
|
||||
""";
|
||||
|
||||
var faults = TestScenery.Parser().Parse("scenery/x.scn", scn, name => new IncludeLookup(name is "tory.scm" or "budynek.inc")).Faults;
|
||||
var faults = TestScenery.Parser().Parse("scenery/x.scn", scn, name => name is "tory.scm" or "budynek.inc").Faults;
|
||||
|
||||
Assert.Equal((SceneryFaultKind.MissingInclude, "x.scn", 2, null, "brak.scm"), Seen(Assert.Single(faults)));
|
||||
}
|
||||
@@ -347,9 +347,9 @@ public class ScenerySyntaxTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void An_include_on_disk_only_in_other_letter_case_is_what_linux_misses()
|
||||
public void An_include_on_disk_in_other_letter_case_is_no_fault()
|
||||
{
|
||||
// The simulator folds the name to lower case and opens it so.
|
||||
// The simulator finds the file whatever the case of its letters, on Linux too.
|
||||
var files = new InMemoryFileSystem(caseSensitive: true)
|
||||
.WithLegacyFile("game/scenery/braniewo_szeroki.scn",
|
||||
"include braniewo/base.ctr end\ninclude Braniewo/Tory.SCM end\n")
|
||||
@@ -357,21 +357,20 @@ public class ScenerySyntaxTests
|
||||
.WithFile("game/scenery/braniewo/tory.scm", "");
|
||||
var repository = TestScenery.Repository(files);
|
||||
|
||||
var faults = repository.Load("game/scenery/braniewo_szeroki.scn")!.Faults;
|
||||
|
||||
Assert.Equal((SceneryFaultKind.IncludeLetterCase, "braniewo_szeroki.scn", 1, "Braniewo/Base.ctr", "braniewo/base.ctr"),
|
||||
Seen(Assert.Single(faults)));
|
||||
Assert.Empty(repository.Load("game/scenery/braniewo_szeroki.scn")!.Faults);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void A_missing_terrain_file_is_no_fault_while_the_scenery_has_its_own_sbt(bool sbt)
|
||||
[InlineData(null)]
|
||||
[InlineData("lk2_sandbox.sbt")]
|
||||
[InlineData("Lk2_Sandbox.SBT")]
|
||||
public void A_missing_terrain_file_is_no_fault_while_the_scenery_has_its_own_sbt(string? sbtOnDisk)
|
||||
{
|
||||
bool sbt = sbtOnDisk is not null;
|
||||
var files = new InMemoryFileSystem(caseSensitive: true)
|
||||
.WithLegacyFile("game/scenery/Lk2_Sandbox.scn", "include nmt/nmt100_mazowieckie_ter.scm end\nFirstInit\n");
|
||||
if (sbt)
|
||||
files.WithFile("game/scenery/lk2_sandbox.sbt", "EU07");
|
||||
files.WithFile("game/scenery/" + sbtOnDisk, "EU07");
|
||||
var repository = TestScenery.Repository(files);
|
||||
|
||||
var scenery = repository.Load("game/scenery/Lk2_Sandbox.scn")!;
|
||||
|
||||
@@ -39,6 +39,24 @@ public class SceneryTranslationsTests
|
||||
Assert.Equal("Polski", scenery.LocalizedDescription(translations));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_tables_are_found_whatever_the_case_of_their_names()
|
||||
{
|
||||
var files = new InMemoryFileSystem(caseSensitive: true)
|
||||
.WithFile(TestInstallation.At("scenery", "Braniewo.scn"), "//$d @brief")
|
||||
.WithFile(Path.Combine(Path.GetFullPath(TestInstallation.At("scenery")), "I18N", "braniewo_en.json"),
|
||||
"""{ "brief": "English" }""")
|
||||
.WithFile(Path.Combine(Path.GetFullPath(TestInstallation.At("scenery")), "I18N", "BRANIEWO_PL.json"),
|
||||
"""{ "brief": "Polski" }""");
|
||||
var installation = new TestInstallation(files);
|
||||
var scenery = Assert.Single(installation.Sceneries.LoadAll());
|
||||
var translations = new SceneryTranslations(files, installation.Log);
|
||||
|
||||
translations.LoadFor(scenery, "Polski");
|
||||
|
||||
Assert.Equal("Polski", scenery.LocalizedDescription(translations));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Literal_text_and_unknown_keys_are_left_alone()
|
||||
{
|
||||
|
||||
@@ -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())
|
||||
|
||||
22
StarterNG.Tests/TextureFolderTests.cs
Normal file
22
StarterNG.Tests/TextureFolderTests.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using StarterNG.Domain.Vehicles;
|
||||
using StarterNG.Infrastructure.Vehicles;
|
||||
using StarterNG.Tests.Fakes;
|
||||
|
||||
namespace StarterNG.Tests;
|
||||
|
||||
public class TextureFolderTests
|
||||
{
|
||||
private static readonly TestInstallation Installation = new(new InMemoryFileSystem(caseSensitive: true)
|
||||
.WithFile("game/dynamic/PKP/EP09_v1/textures.txt", "")
|
||||
.WithDirectory("game/Other/Folder"));
|
||||
|
||||
[Theory]
|
||||
[InlineData("pkp/ep09_v1/", "dynamic/PKP/EP09_v1")]
|
||||
[InlineData(@"PKP\EP09_V1", "dynamic/PKP/EP09_v1")]
|
||||
[InlineData("other/folder/", "Other/Folder")]
|
||||
[InlineData("pkp/brak/", "pkp/brak")]
|
||||
public void A_liverys_folder_is_found_as_it_is_named_on_disk(string named, string onDisk) =>
|
||||
Assert.Equal(Path.GetFullPath(Path.Combine("game", onDisk)),
|
||||
Path.GetFullPath(TextureFolder.Of(Installation.Files, Installation.Paths,
|
||||
new VehicleTexture { Directory = named })));
|
||||
}
|
||||
@@ -13,6 +13,6 @@ public static class AppInfo
|
||||
#if DEBUG
|
||||
text += " (DEBUG)";
|
||||
#endif
|
||||
return text + " public beta";
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@ namespace StarterNG.Application.Abstractions;
|
||||
|
||||
public interface IFileSystem
|
||||
{
|
||||
/// <summary>Whether a name finds its file whatever the case of its letters, as on Windows and macOS.</summary>
|
||||
bool IgnoresCase { get; }
|
||||
|
||||
bool FileExists(string path);
|
||||
|
||||
bool DirectoryExists(string path);
|
||||
|
||||
@@ -56,6 +56,7 @@ public sealed class AppServices
|
||||
var missingAssets = new MissingAssetScanner(files, paths);
|
||||
SceneryTexts = new SceneryTranslations(files, log);
|
||||
Timetables = new TimetableLocator(files);
|
||||
Attachments = new AttachmentLocator(files, paths);
|
||||
LoadWeights = new LoadWeightsRepository(files, paths, log);
|
||||
Library = new GameLibrary(Vehicles, Sceneries, MiniTextures, Physics, log);
|
||||
State = new AppState();
|
||||
@@ -119,6 +120,8 @@ public sealed class AppServices
|
||||
|
||||
public TimetableLocator Timetables { get; }
|
||||
|
||||
public AttachmentLocator Attachments { get; }
|
||||
|
||||
public LoadWeightsRepository LoadWeights { get; }
|
||||
|
||||
public GameLibrary Library { get; }
|
||||
|
||||
@@ -61,13 +61,6 @@ public enum SceneryFaultKind
|
||||
/// <summary>An include whose file is not there; <see cref="SceneryFault.Token"/> names it.</summary>
|
||||
MissingInclude,
|
||||
|
||||
/// <summary>
|
||||
/// An include whose file is there only under a name in other letter case: the
|
||||
/// simulator looks for it in lower case, which on Linux misses it. The
|
||||
/// <see cref="SceneryFault.Subject"/> holds the name on disk.
|
||||
/// </summary>
|
||||
IncludeLetterCase,
|
||||
|
||||
/// <summary>An include with no <c>end</c>, which swallows what follows it.</summary>
|
||||
IncludeNotClosed,
|
||||
|
||||
|
||||
@@ -5,12 +5,6 @@ using System.Linq;
|
||||
|
||||
namespace StarterNG.Domain.Sceneries;
|
||||
|
||||
/// <summary>
|
||||
/// What the simulator makes of an include: whether it opens the file - and when
|
||||
/// it does not, the file that is there under a name differing only in letter case.
|
||||
/// </summary>
|
||||
public readonly record struct IncludeLookup(bool Opens, string? OnDisk = null);
|
||||
|
||||
/// <summary>
|
||||
/// Goes through a scenery's text for the parts the starter reads - trainsets,
|
||||
/// vehicles, includes - finding where each trainset and loose vehicle is, and
|
||||
@@ -35,17 +29,17 @@ public static class ScenerySyntax
|
||||
|
||||
/// <param name="text">The scenery's text, its includes put in place.</param>
|
||||
/// <param name="file">The scenery's file name, for faults in its own text.</param>
|
||||
/// <param name="findInclude">
|
||||
/// What the simulator makes of an include, named as the include names it; null
|
||||
/// <param name="opensInclude">
|
||||
/// Whether the simulator opens an include, named as the include names it; null
|
||||
/// leaves includes unchecked.
|
||||
/// </param>
|
||||
public static Result Scan(string text, string file, Func<string, IncludeLookup>? findInclude = null)
|
||||
public static Result Scan(string text, string file, Func<string, bool>? opensInclude = null)
|
||||
{
|
||||
string code = SceneryCode.Mask(text, out int unclosed);
|
||||
var tokens = SceneryCode.Tokens(code);
|
||||
var lines = new SceneryLines(text, file);
|
||||
|
||||
var simulator = new Reading(tokens, lines, findInclude, asSimulator: true);
|
||||
var simulator = new Reading(tokens, lines, opensInclude, asSimulator: true);
|
||||
simulator.Run();
|
||||
var author = new Reading(tokens, lines, null, asSimulator: false);
|
||||
author.Run();
|
||||
@@ -116,7 +110,7 @@ public static class ScenerySyntax
|
||||
}
|
||||
|
||||
private sealed class Reading(
|
||||
List<SceneryCode.Token> tokens, SceneryLines lines, Func<string, IncludeLookup>? findInclude, bool asSimulator)
|
||||
List<SceneryCode.Token> tokens, SceneryLines lines, Func<string, bool>? opensInclude, bool asSimulator)
|
||||
{
|
||||
public readonly List<(int Start, int End, string Name)> Trainsets = new();
|
||||
public readonly List<(int, int)> Loose = new();
|
||||
@@ -375,13 +369,8 @@ public static class ScenerySyntax
|
||||
return stop < tokens.Count && tokens[stop].Is("end") ? stop : stop - 1;
|
||||
}
|
||||
|
||||
if (findInclude is not null && Plain(name) && findInclude(name) is { Opens: false } lookup)
|
||||
{
|
||||
if (lookup.OnDisk is { } onDisk)
|
||||
Fault(SceneryFaultKind.IncludeLetterCase, token.Index, onDisk, name);
|
||||
else
|
||||
Fault(SceneryFaultKind.MissingInclude, token.Index, token: name);
|
||||
}
|
||||
if (opensInclude is not null && Plain(name) && !opensInclude(name))
|
||||
Fault(SceneryFaultKind.MissingInclude, token.Index, token: name);
|
||||
|
||||
// The simulator takes its parameters up to the word "end", written just so,
|
||||
// whatever comes before it.
|
||||
|
||||
@@ -10,6 +10,9 @@ public sealed class PhysicalFileSystem : IFileSystem
|
||||
{
|
||||
public const string TempSuffix = ".starter.tmp";
|
||||
|
||||
// Their disks as they come; a Linux one tells case apart.
|
||||
public bool IgnoresCase { get; } = OperatingSystem.IsWindows() || OperatingSystem.IsMacOS();
|
||||
|
||||
public bool FileExists(string path) => File.Exists(path);
|
||||
|
||||
public bool DirectoryExists(string path) => Directory.Exists(path);
|
||||
@@ -88,8 +91,18 @@ public sealed class PhysicalFileSystem : IFileSystem
|
||||
public void CopyFile(string sourcePath, string destinationPath, bool overwrite) =>
|
||||
File.Copy(sourcePath, destinationPath, overwrite);
|
||||
|
||||
// The pattern matches names in any letter case, as on Windows: the game's data,
|
||||
// made there, has X.SCN as well as x.scn.
|
||||
public IReadOnlyList<string> GetFiles(string path, string searchPattern = "*") =>
|
||||
Directory.Exists(path) ? Directory.GetFiles(path, searchPattern) : Array.Empty<string>();
|
||||
Directory.Exists(path)
|
||||
? Directory.GetFiles(path, searchPattern, new EnumerationOptions
|
||||
{
|
||||
IgnoreInaccessible = false,
|
||||
AttributesToSkip = 0,
|
||||
MatchType = MatchType.Win32,
|
||||
MatchCasing = MatchCasing.CaseInsensitive
|
||||
})
|
||||
: Array.Empty<string>();
|
||||
|
||||
// A folder it may not enter costs what is in it, not the whole listing.
|
||||
public IReadOnlyList<string> GetFilesRecursive(string path, string searchPattern) =>
|
||||
@@ -99,7 +112,8 @@ public sealed class PhysicalFileSystem : IFileSystem
|
||||
RecurseSubdirectories = true,
|
||||
IgnoreInaccessible = true,
|
||||
AttributesToSkip = 0,
|
||||
MatchType = MatchType.Win32
|
||||
MatchType = MatchType.Win32,
|
||||
MatchCasing = MatchCasing.CaseInsensitive
|
||||
})
|
||||
: Array.Empty<string>();
|
||||
|
||||
|
||||
132
StarterNG/Infrastructure/CaseInsensitivePaths.cs
Normal file
132
StarterNG/Infrastructure/CaseInsensitivePaths.cs
Normal file
@@ -0,0 +1,132 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using StarterNG.Application.Abstractions;
|
||||
|
||||
namespace StarterNG.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// Finds a file or folder of the game's data by a name whose letters may stand in
|
||||
/// another case than on disk, as Windows, Wine and the simulator find it, and gives
|
||||
/// the name as the disk has it. The data is made on Windows: its names and the
|
||||
/// names written in it seldom agree letter for letter, which on Linux matters.
|
||||
/// </summary>
|
||||
public sealed class CaseInsensitivePaths
|
||||
{
|
||||
private readonly IFileSystem _files;
|
||||
private readonly Func<(string Folder, bool Files), ILookup<string, string>> _list;
|
||||
|
||||
/// <summary>Each folder's files and folders by name, listed once until <see cref="Forget"/>; null lists every time.</summary>
|
||||
private readonly ConcurrentDictionary<(string Folder, bool Files), ILookup<string, string>>? _listings;
|
||||
|
||||
/// <param name="keepListings">
|
||||
/// Lists each folder once, for many names looked up in data that stays as it is
|
||||
/// meanwhile, until <see cref="Forget"/>.
|
||||
/// </param>
|
||||
public CaseInsensitivePaths(IFileSystem files, bool keepListings = false)
|
||||
{
|
||||
_files = files;
|
||||
_list = List;
|
||||
if (keepListings)
|
||||
_listings = new ConcurrentDictionary<(string, bool), ILookup<string, string>>();
|
||||
}
|
||||
|
||||
/// <summary>Lets the next lookups list the folders again.</summary>
|
||||
public void Forget() => _listings?.Clear();
|
||||
|
||||
/// <summary>
|
||||
/// The file named so under <paramref name="folder"/> as it is named on disk, relative
|
||||
/// to the folder with forward slashes; null when nothing there answers to it. A name
|
||||
/// written with its letters as on disk wins over one differing only in their case;
|
||||
/// a disk that does not tell case apart gives it back as written.
|
||||
/// </summary>
|
||||
/// <param name="name">
|
||||
/// A name under the folder, with either slash, joined to it as the simulator joins
|
||||
/// it: a slash ahead of it counts for nothing.
|
||||
/// </param>
|
||||
/// <param name="directory">Looks for a folder instead of a file.</param>
|
||||
public string? Resolve(string folder, string name, bool directory = false)
|
||||
{
|
||||
string[] parts = name.Replace('\\', '/').Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length == 0)
|
||||
return null;
|
||||
|
||||
string exact = string.Join('/', parts);
|
||||
string whole = Path.Combine(folder, exact);
|
||||
if (directory ? _files.DirectoryExists(whole) : _files.FileExists(whole))
|
||||
return exact;
|
||||
// Where case does not matter, missed as written is missed in any case.
|
||||
if (_files.IgnoresCase)
|
||||
return null;
|
||||
|
||||
var found = new string[parts.Length];
|
||||
return Walk(folder, parts, 0, directory, found) ? string.Join('/', found) : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The full path of the file named so, its letters as on disk; null when there is
|
||||
/// none. A name given in full is looked up from its own root, any other under
|
||||
/// <paramref name="folder"/> as <see cref="Resolve"/> does.
|
||||
/// </summary>
|
||||
public string? Find(string folder, string name, bool directory = false)
|
||||
{
|
||||
if (Path.IsPathFullyQualified(name))
|
||||
{
|
||||
folder = Path.GetPathRoot(name)!;
|
||||
name = name[folder.Length..];
|
||||
}
|
||||
|
||||
return Resolve(folder, name, directory) is { } found
|
||||
? Path.Combine(folder, found.Replace('/', Path.DirectorySeparatorChar))
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the parts from <paramref name="at"/> on under <paramref name="current"/>. Of
|
||||
/// folders named alike in other case - two copies of the data on Linux - each is
|
||||
/// tried, the one written as on disk first.
|
||||
/// </summary>
|
||||
private bool Walk(string current, string[] parts, int at, bool directory, string[] found)
|
||||
{
|
||||
if (at == parts.Length)
|
||||
return true;
|
||||
|
||||
string part = parts[at];
|
||||
if (part is "." or "..")
|
||||
{
|
||||
found[at] = part;
|
||||
return Walk(Path.Combine(current, part), parts, at + 1, directory, found);
|
||||
}
|
||||
|
||||
bool last = at == parts.Length - 1;
|
||||
var matches = Listing(current, files: last && !directory)[part]
|
||||
.OrderBy(entry => string.Equals(Path.GetFileName(entry), part, StringComparison.Ordinal) ? 0 : 1);
|
||||
foreach (string match in matches)
|
||||
{
|
||||
found[at] = Path.GetFileName(match);
|
||||
if (Walk(match, parts, at + 1, directory, found))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private ILookup<string, string> Listing(string folder, bool files) =>
|
||||
_listings is null ? List((folder, files)) : _listings.GetOrAdd((folder, files), _list);
|
||||
|
||||
// A folder that cannot be listed holds nothing to be found.
|
||||
private ILookup<string, string> List((string Folder, bool Files) key)
|
||||
{
|
||||
var entries = Array.Empty<string>();
|
||||
try
|
||||
{
|
||||
if (_files.DirectoryExists(key.Folder))
|
||||
entries = (key.Files ? _files.GetFiles(key.Folder) : _files.GetDirectories(key.Folder)).ToArray();
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
// Nothing listed, nothing found.
|
||||
}
|
||||
return entries.ToLookup(entry => Path.GetFileName(entry), StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
@@ -5,16 +5,21 @@ using System.Runtime.InteropServices;
|
||||
using Avalonia;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Platform;
|
||||
using StarterNG.Application.Abstractions;
|
||||
using StarterNG.Infrastructure.Adapters;
|
||||
|
||||
namespace StarterNG.Infrastructure;
|
||||
|
||||
public static class LoadingScreen
|
||||
{
|
||||
private static readonly string[] Extensions = { ".bmp", ".jpg", ".jpeg", ".png" };
|
||||
|
||||
public static void Prepare(string? logoKey, string? sceneryName = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
string? src = ResolveSource(logoKey) ?? ResolveSource(sceneryName) ?? PickRandom();
|
||||
string? src = FindSource(new PhysicalFileSystem(), Directory.GetCurrentDirectory(), logoKey, sceneryName,
|
||||
Random.Shared);
|
||||
if (src is null) return;
|
||||
|
||||
string destDir = Path.Combine(Directory.GetCurrentDirectory(), "textures");
|
||||
@@ -35,32 +40,36 @@ public static class LoadingScreen
|
||||
}
|
||||
}
|
||||
|
||||
private static string? ResolveSource(string? key)
|
||||
/// <summary>
|
||||
/// The picture of textures/logo under <paramref name="root"/> to show: the trainset's,
|
||||
/// else the scenery's, else one of logo*.* at random; its name and extension in any
|
||||
/// letter case. Null when there is none.
|
||||
/// </summary>
|
||||
public static string? FindSource(IFileSystem files, string root, string? logoKey, string? sceneryName,
|
||||
Random random)
|
||||
{
|
||||
var names = new CaseInsensitivePaths(files);
|
||||
if (names.Find(root, "textures/logo", directory: true) is not { } dir)
|
||||
return null;
|
||||
return Named(names, dir, logoKey) ?? Named(names, dir, sceneryName) ?? PickRandom(files, dir, random);
|
||||
}
|
||||
|
||||
private static string? Named(CaseInsensitivePaths names, string dir, string? key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key)) return null;
|
||||
key = Path.GetFileNameWithoutExtension(key.Trim());
|
||||
string dir = Path.Combine(Directory.GetCurrentDirectory(), "textures", "logo");
|
||||
foreach (string ext in new[] { ".bmp", ".jpg", ".jpeg", ".png" })
|
||||
{
|
||||
string path = Path.Combine(dir, key + ext);
|
||||
if (File.Exists(path)) return path;
|
||||
}
|
||||
return null;
|
||||
if (key.Length == 0) return null;
|
||||
return Extensions.Select(ext => names.Find(dir, key + ext)).FirstOrDefault(path => path is not null);
|
||||
}
|
||||
|
||||
private static string? PickRandom()
|
||||
private static string? PickRandom(IFileSystem files, string dir, Random random)
|
||||
{
|
||||
string dir = Path.Combine(Directory.GetCurrentDirectory(), "textures", "logo");
|
||||
if (!Directory.Exists(dir)) return null;
|
||||
var files = Directory.GetFiles(dir, "logo*.*")
|
||||
.Where(f =>
|
||||
{
|
||||
string e = Path.GetExtension(f).ToLowerInvariant();
|
||||
return e is ".bmp" or ".jpg" or ".jpeg" or ".png";
|
||||
})
|
||||
var logos = files.GetFiles(dir, "logo*.*")
|
||||
.Where(f => Extensions.Contains(Path.GetExtension(f), StringComparer.OrdinalIgnoreCase))
|
||||
.Order(StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
if (files.Length == 0) return null;
|
||||
return files[Random.Shared.Next(files.Length)];
|
||||
if (logos.Length == 0) return null;
|
||||
return logos[random.Next(logos.Length)];
|
||||
}
|
||||
|
||||
private static void WriteBmp(string sourcePath, string destPath)
|
||||
|
||||
24
StarterNG/Infrastructure/Sceneries/AttachmentLocator.cs
Normal file
24
StarterNG/Infrastructure/Sceneries/AttachmentLocator.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using StarterNG.Application.Abstractions;
|
||||
using StarterNG.Domain.Sceneries;
|
||||
|
||||
namespace StarterNG.Infrastructure.Sceneries;
|
||||
|
||||
/// <summary>Where the file a scenery attaches (<c>//$f</c>) is on disk.</summary>
|
||||
public sealed class AttachmentLocator
|
||||
{
|
||||
private readonly IGamePaths _paths;
|
||||
private readonly CaseInsensitivePaths _names;
|
||||
|
||||
public AttachmentLocator(IFileSystem files, IGamePaths paths)
|
||||
{
|
||||
_paths = paths;
|
||||
_names = new CaseInsensitivePaths(files);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The attachment's file, named from the game's folder or in full, with its letters
|
||||
/// as on disk; null when it is not there.
|
||||
/// </summary>
|
||||
public string? Resolve(SceneryAttachment attachment) =>
|
||||
string.IsNullOrWhiteSpace(attachment.FilePath) ? null : _names.Find(_paths.Root, attachment.FilePath.Trim());
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,11 +20,11 @@ public sealed class SceneryParser
|
||||
_random = random;
|
||||
}
|
||||
|
||||
/// <param name="findInclude">
|
||||
/// What the simulator makes of an include, as the include names it; null leaves
|
||||
/// <param name="opensInclude">
|
||||
/// Whether the simulator opens an include, as the include names it; null leaves
|
||||
/// includes unchecked.
|
||||
/// </param>
|
||||
public Scenery Parse(string path, string content, Func<string, IncludeLookup>? findInclude = null)
|
||||
public Scenery Parse(string path, string content, Func<string, bool>? opensInclude = null)
|
||||
{
|
||||
string original = content;
|
||||
|
||||
@@ -38,7 +38,7 @@ public sealed class SceneryParser
|
||||
|
||||
// Read from the text with its optional includes blanked, which keeps every
|
||||
// place where it was, so a fault's line is the line in the file.
|
||||
var syntax = ScenerySyntax.Scan(content, System.IO.Path.GetFileName(path), findInclude);
|
||||
var syntax = ScenerySyntax.Scan(content, System.IO.Path.GetFileName(path), opensInclude);
|
||||
var faults = syntax.Faults.ToList();
|
||||
var inlined = SceneryIncludes.Ranges(content);
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ public sealed class SceneryRepository : ISceneryRepository
|
||||
private readonly SceneryParser _parser;
|
||||
|
||||
/// <summary>The scenery folders' files and folders, listed once per load of them all.</summary>
|
||||
private readonly ConcurrentDictionary<(string, bool), IReadOnlyList<string>> _listings = new();
|
||||
private readonly CaseInsensitivePaths _names;
|
||||
|
||||
public SceneryRepository(IFileSystem files, IGamePaths paths, IDiagnosticsLog log, SceneryParser parser)
|
||||
{
|
||||
@@ -31,11 +31,12 @@ public sealed class SceneryRepository : ISceneryRepository
|
||||
_paths = paths;
|
||||
_log = log;
|
||||
_parser = parser;
|
||||
_names = new CaseInsensitivePaths(files, keepListings: true);
|
||||
}
|
||||
|
||||
public IReadOnlyList<Scenery> LoadAll(IProgress<SceneryLoadProgress>? progress = null)
|
||||
{
|
||||
_listings.Clear();
|
||||
_names.Forget();
|
||||
var files = SceneryFiles().ToList();
|
||||
var loaded = new ConcurrentBag<Scenery>();
|
||||
int total = Math.Max(1, files.Count);
|
||||
@@ -58,7 +59,7 @@ public sealed class SceneryRepository : ISceneryRepository
|
||||
|
||||
public Scenery? Load(string path)
|
||||
{
|
||||
_listings.Clear();
|
||||
_names.Forget();
|
||||
return LoadOne(path);
|
||||
}
|
||||
|
||||
@@ -79,10 +80,8 @@ public sealed class SceneryRepository : ISceneryRepository
|
||||
// With its own .sbt the scenery's terrain comes from it, and the simulator
|
||||
// passes its terrain includes over without opening them.
|
||||
bool binaryTerrain = Opens(Path.GetFileNameWithoutExtension(path) + ".sbt");
|
||||
IncludeLookup FindInclude(string name) =>
|
||||
Opens(name) || (binaryTerrain && SimulatorNames.Lower(name).Contains("_ter.scm"))
|
||||
? new IncludeLookup(true)
|
||||
: new IncludeLookup(false, ResolveInclude(name));
|
||||
bool OpensInclude(string name) =>
|
||||
Opens(name) || (binaryTerrain && SimulatorNames.Lower(name).Contains("_ter.scm"));
|
||||
|
||||
(string File, string Text)? ReadInclude(string name)
|
||||
{
|
||||
@@ -99,7 +98,7 @@ public sealed class SceneryRepository : ISceneryRepository
|
||||
|
||||
string own = LegacyText.Decode(_files.ReadAllBytes(path), out var ownEncoding);
|
||||
string text = SceneryIncludes.Inline(own, ReadInclude);
|
||||
var scenery = _parser.Parse(path, text, FindInclude);
|
||||
var scenery = _parser.Parse(path, text, OpensInclude);
|
||||
scenery.HasCompanionTimetable = binaryTerrain;
|
||||
scenery.Sources = sources;
|
||||
scenery.TextEncoding = ownEncoding;
|
||||
@@ -114,11 +113,11 @@ public sealed class SceneryRepository : ISceneryRepository
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the simulator opens a file of the scenery folder named so: by the
|
||||
/// name in lower case, which on Linux has to be how it is named on disk.
|
||||
/// Whether the simulator opens a file of the scenery folder named so: whatever the
|
||||
/// case of its letters, on Linux too. A shade more lenient than eu07 there, which
|
||||
/// folds only the letters A to Z and of two folders named alike takes the first.
|
||||
/// </summary>
|
||||
private bool Opens(string name) =>
|
||||
_files.FileExists(Path.Combine(_paths.Scenery, SimulatorNames.Lower(name.Replace('\\', '/'))));
|
||||
private bool Opens(string name) => ResolveInclude(name) is not null;
|
||||
|
||||
/// <summary>
|
||||
/// An include's file as it is named on disk, from the scenery folder, whatever
|
||||
@@ -126,44 +125,7 @@ public sealed class SceneryRepository : ISceneryRepository
|
||||
/// copy written for a start carries in place of the include, so they come along
|
||||
/// on Linux too.
|
||||
/// </summary>
|
||||
private string? ResolveInclude(string name)
|
||||
{
|
||||
string relative = name.Replace('\\', '/');
|
||||
if (_files.FileExists(Path.Combine(_paths.Scenery, relative)))
|
||||
return relative;
|
||||
|
||||
string[] parts = relative.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||
string directory = _paths.Scenery;
|
||||
var found = new List<string>();
|
||||
for (int i = 0; i < parts.Length; i++)
|
||||
{
|
||||
if (parts[i] is "." or "..")
|
||||
{
|
||||
directory = Path.Combine(directory, parts[i]);
|
||||
found.Add(parts[i]);
|
||||
continue;
|
||||
}
|
||||
|
||||
bool last = i == parts.Length - 1;
|
||||
string? match = Listing(directory, last)
|
||||
.FirstOrDefault(entry => string.Equals(Path.GetFileName(entry), parts[i],
|
||||
StringComparison.OrdinalIgnoreCase));
|
||||
if (match is null)
|
||||
return null;
|
||||
directory = match;
|
||||
found.Add(Path.GetFileName(match));
|
||||
}
|
||||
|
||||
return string.Join('/', found);
|
||||
}
|
||||
|
||||
private IReadOnlyList<string> Listing(string directory, bool files) =>
|
||||
_listings.GetOrAdd((directory, files), key =>
|
||||
{
|
||||
if (!_files.DirectoryExists(key.Item1))
|
||||
return Array.Empty<string>();
|
||||
return key.Item2 ? _files.GetFiles(key.Item1) : _files.GetDirectories(key.Item1);
|
||||
});
|
||||
private string? ResolveInclude(string name) => _names.Resolve(_paths.Scenery, name);
|
||||
|
||||
private IEnumerable<string> SceneryFiles() =>
|
||||
_files.GetFiles(_paths.Scenery, "*.scn")
|
||||
|
||||
@@ -14,12 +14,14 @@ public sealed class SceneryTranslations : ISceneryTranslations
|
||||
private const string BaseLanguage = "en";
|
||||
|
||||
private readonly IFileSystem _files;
|
||||
private readonly CaseInsensitivePaths _names;
|
||||
private readonly IDiagnosticsLog _log;
|
||||
private readonly Dictionary<string, string> _map = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public SceneryTranslations(IFileSystem files, IDiagnosticsLog log)
|
||||
{
|
||||
_files = files;
|
||||
_names = new CaseInsensitivePaths(files);
|
||||
_log = log;
|
||||
}
|
||||
|
||||
@@ -29,12 +31,11 @@ public sealed class SceneryTranslations : ISceneryTranslations
|
||||
|
||||
string sceneryDir = Path.GetDirectoryName(Path.GetFullPath(scenery.Path)) ?? "scenery";
|
||||
string id = Path.GetFileNameWithoutExtension(scenery.Path);
|
||||
string i18nDir = Path.Combine(sceneryDir, "i18n");
|
||||
string language = NormalizeLanguage(langCode);
|
||||
|
||||
TryLoad(Path.Combine(i18nDir, $"{id}_{BaseLanguage}.json"));
|
||||
TryLoad(_names.Find(sceneryDir, $"i18n/{id}_{BaseLanguage}.json"));
|
||||
if (!string.Equals(language, BaseLanguage, StringComparison.OrdinalIgnoreCase))
|
||||
TryLoad(Path.Combine(i18nDir, $"{id}_{language}.json"));
|
||||
TryLoad(_names.Find(sceneryDir, $"i18n/{id}_{language}.json"));
|
||||
}
|
||||
|
||||
public string Translate(string? text)
|
||||
@@ -50,9 +51,9 @@ public sealed class SceneryTranslations : ISceneryTranslations
|
||||
return _map.TryGetValue(key, out string? translated) ? translated : text;
|
||||
}
|
||||
|
||||
private void TryLoad(string path)
|
||||
private void TryLoad(string? path)
|
||||
{
|
||||
if (!_files.FileExists(path))
|
||||
if (path is null)
|
||||
return;
|
||||
|
||||
try
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -23,6 +23,7 @@ public sealed class FizPhysicsRepository : IPhysicsRepository
|
||||
};
|
||||
|
||||
private readonly IFileSystem _files;
|
||||
private readonly CaseInsensitivePaths _names;
|
||||
private readonly IGamePaths _paths;
|
||||
private readonly IDiagnosticsLog _log;
|
||||
|
||||
@@ -38,6 +39,7 @@ public sealed class FizPhysicsRepository : IPhysicsRepository
|
||||
public FizPhysicsRepository(IFileSystem files, IGamePaths paths, IDiagnosticsLog log)
|
||||
{
|
||||
_files = files;
|
||||
_names = new CaseInsensitivePaths(files);
|
||||
_paths = paths;
|
||||
_log = log;
|
||||
}
|
||||
@@ -51,7 +53,7 @@ public sealed class FizPhysicsRepository : IPhysicsRepository
|
||||
if (string.IsNullOrEmpty(model))
|
||||
return null;
|
||||
|
||||
string name = Path.GetFileNameWithoutExtension(model);
|
||||
string name = Path.GetFileName(model);
|
||||
if (name.Length == 0)
|
||||
return null;
|
||||
|
||||
@@ -60,7 +62,10 @@ public sealed class FizPhysicsRepository : IPhysicsRepository
|
||||
if (_cache.TryGetValue(key, out var cached))
|
||||
return cached;
|
||||
|
||||
var physics = Read(folder, name);
|
||||
// The name as written first: in 9-107.13 the dot is part of the name, not an extension.
|
||||
string bare = Path.GetFileNameWithoutExtension(name);
|
||||
var physics = Read(folder, name)
|
||||
?? (bare.Length > 0 && bare != name ? Read(folder, bare) : null);
|
||||
_cache[key] = physics;
|
||||
return physics;
|
||||
}
|
||||
@@ -162,8 +167,9 @@ public sealed class FizPhysicsRepository : IPhysicsRepository
|
||||
while (i < tokens.Count && !tokens[i].Equals("end", StringComparison.OrdinalIgnoreCase))
|
||||
includeParameters.Add(Resolve(tokens[i++], parameters));
|
||||
|
||||
Parse(physics, Path.Combine(directory, includeFile), directory, includeParameters.ToArray(),
|
||||
depth + 1);
|
||||
// Named in the letter case of whoever wrote the include, not always that of the file.
|
||||
string included = _names.Find(directory, includeFile) ?? Path.Combine(directory, includeFile);
|
||||
Parse(physics, included, directory, includeParameters.ToArray(), depth + 1);
|
||||
pendingKey = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -10,44 +10,53 @@ public sealed class MissingAssetScanner
|
||||
private static readonly string[] TextureExtensions = { ".mat", ".bmp" };
|
||||
private static readonly string[] ModelExtensions = { ".t3d", ".e3d" };
|
||||
|
||||
private readonly IFileSystem _files;
|
||||
private readonly IGamePaths _paths;
|
||||
private readonly CaseInsensitivePaths _names;
|
||||
|
||||
public MissingAssetScanner(IFileSystem files, IGamePaths paths)
|
||||
{
|
||||
_files = files;
|
||||
_paths = paths;
|
||||
_names = new CaseInsensitivePaths(files, keepListings: true);
|
||||
}
|
||||
|
||||
public List<string> Scan(VehicleCatalog catalog)
|
||||
{
|
||||
var lines = new List<string>();
|
||||
|
||||
foreach (var texture in catalog.Textures)
|
||||
try
|
||||
{
|
||||
string directory = Path.Combine(
|
||||
_paths.Dynamic,
|
||||
texture.Directory.Replace('/', Path.DirectorySeparatorChar).TrimEnd('\\', '/'));
|
||||
foreach (var texture in catalog.Textures)
|
||||
{
|
||||
string directory = texture.Directory.Replace('\\', '/').TrimEnd('/');
|
||||
|
||||
if (!string.IsNullOrEmpty(texture.Skinfile) &&
|
||||
!ExistsWithAnyExtension(Path.Combine(directory, texture.Skinfile), TextureExtensions))
|
||||
lines.Add($"# no file: {texture.Directory}{texture.Skinfile}");
|
||||
if (!string.IsNullOrEmpty(texture.Skinfile) &&
|
||||
!ExistsWithAnyExtension(Under(directory, texture.Skinfile), TextureExtensions))
|
||||
lines.Add($"# no file: {texture.Directory}{texture.Skinfile}");
|
||||
|
||||
if (!string.IsNullOrEmpty(texture.Model) &&
|
||||
!ExistsWithAnyExtension(Path.Combine(directory, texture.Model), ModelExtensions))
|
||||
lines.Add($"# no model: {texture.Directory}{texture.Model}");
|
||||
if (!string.IsNullOrEmpty(texture.Model) &&
|
||||
!ExistsWithAnyExtension(Under(directory, texture.Model), ModelExtensions))
|
||||
lines.Add($"# no model: {texture.Directory}{texture.Model}");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// The folders are listed once for the whole catalog, and not kept past it.
|
||||
_names.Forget();
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
private bool ExistsWithAnyExtension(string path, string[] extensions)
|
||||
private static string Under(string directory, string name) =>
|
||||
directory.Length == 0 ? name : directory + "/" + name;
|
||||
|
||||
/// <summary>Whether the file is under dynamic as named, or with one of the extensions, in any letter case.</summary>
|
||||
private bool ExistsWithAnyExtension(string name, string[] extensions)
|
||||
{
|
||||
if (_files.FileExists(path))
|
||||
if (_names.Resolve(_paths.Dynamic, name) is not null)
|
||||
return true;
|
||||
|
||||
foreach (string extension in extensions)
|
||||
if (_files.FileExists(Path.ChangeExtension(path, extension)))
|
||||
if (_names.Resolve(_paths.Dynamic, Path.ChangeExtension(name, extension)) is not null)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
|
||||
24
StarterNG/Infrastructure/Vehicles/TextureFolder.cs
Normal file
24
StarterNG/Infrastructure/Vehicles/TextureFolder.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System.IO;
|
||||
using StarterNG.Application.Abstractions;
|
||||
using StarterNG.Domain.Vehicles;
|
||||
|
||||
namespace StarterNG.Infrastructure.Vehicles;
|
||||
|
||||
public static class TextureFolder
|
||||
{
|
||||
/// <summary>
|
||||
/// A livery's folder as it is named on disk: under dynamic, or from the game's folder
|
||||
/// when its name starts there, in any letter case. Where it would be when it is not there.
|
||||
/// </summary>
|
||||
public static string Of(IFileSystem files, IGamePaths paths, VehicleTexture texture)
|
||||
{
|
||||
string directory = texture.Directory.Replace('\\', '/').TrimEnd('/');
|
||||
if (directory.Length == 0)
|
||||
return paths.Dynamic;
|
||||
|
||||
var names = new CaseInsensitivePaths(files);
|
||||
return names.Find(paths.Dynamic, directory, directory: true) ??
|
||||
names.Find(paths.Root, directory, directory: true) ??
|
||||
Path.Combine(paths.Root, directory.Replace('/', Path.DirectorySeparatorChar));
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,9 @@ public partial class MainWindow : Window
|
||||
{
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
InitializeComponent();
|
||||
// The top bar lets the version go first when it runs short of room, which at the usual widths it does;
|
||||
// the title has room for it at any width.
|
||||
Title = $"{Title} {AppInfo.Version}";
|
||||
|
||||
ExtendClientAreaToDecorationsHint = true;
|
||||
ExtendClientAreaTitleBarHeightHint = TitleBarHeight;
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||
<!-- Major and minor only: CI adds the patch, the number of commits, so every
|
||||
build of main is a higher version without anyone bumping it. -->
|
||||
<VersionPrefix>1.5</VersionPrefix>
|
||||
<VersionPrefix>1.6</VersionPrefix>
|
||||
<AssemblyName>Starter</AssemblyName>
|
||||
<Company>eu07.pl</Company>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using System.IO;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input.Platform;
|
||||
using StarterNG.Application;
|
||||
using StarterNG.Domain;
|
||||
using StarterNG.Domain.Vehicles;
|
||||
using StarterNG.Infrastructure.Vehicles;
|
||||
|
||||
namespace StarterNG.Views;
|
||||
|
||||
@@ -17,13 +19,7 @@ internal static class TextureMenuItems
|
||||
if (TopLevel.GetTopLevel(owner)?.Clipboard is { } clipboard)
|
||||
await clipboard.SetTextAsync(Consist.Base(texture.Skinfile));
|
||||
});
|
||||
menu.AddItem(App.Loc["OpenTextureFolder"], () => Infrastructure.Diagnostics.OpenOrReport(FolderOf(texture)));
|
||||
}
|
||||
|
||||
private static string FolderOf(VehicleTexture texture)
|
||||
{
|
||||
string dir = Path.Combine(Directory.GetCurrentDirectory(), "dynamic",
|
||||
texture.Directory.Replace('/', Path.DirectorySeparatorChar).TrimEnd('\\', '/'));
|
||||
return Directory.Exists(dir) ? dir : Path.Combine(Directory.GetCurrentDirectory(), texture.Directory);
|
||||
menu.AddItem(App.Loc["OpenTextureFolder"], () => Infrastructure.Diagnostics.OpenOrReport(
|
||||
Path.GetFullPath(TextureFolder.Of(AppServices.Current.Files, AppServices.Current.Paths, texture))));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -492,20 +492,18 @@ public partial class Scenarios : UserControl
|
||||
btn.Classes.Add("Flat");
|
||||
ToolTip.SetTip(btn, att.FilePath);
|
||||
// A button that does nothing is no way to say the file is not there.
|
||||
btn.IsEnabled = File.Exists(Path.GetFullPath(att.FilePath));
|
||||
btn.IsEnabled = AppServices.Current.Attachments.Resolve(att) is not null;
|
||||
ToolTip.SetShowOnDisabled(btn, true);
|
||||
btn.Click += (_, _) => OpenAttachment(att.FilePath);
|
||||
btn.Click += (_, _) => OpenAttachment(att);
|
||||
attachmentsPanel.Children.Add(btn);
|
||||
}
|
||||
}
|
||||
|
||||
private static void OpenAttachment(string relativePath)
|
||||
private static void OpenAttachment(SceneryAttachment attachment)
|
||||
{
|
||||
string full = Path.GetFullPath(relativePath);
|
||||
if (!File.Exists(full))
|
||||
full = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), relativePath));
|
||||
if (!File.Exists(full))
|
||||
if (AppServices.Current.Attachments.Resolve(attachment) is not { } found)
|
||||
return;
|
||||
string full = Path.GetFullPath(found);
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo(full) { UseShellExecute = true });
|
||||
|
||||
@@ -742,7 +742,6 @@
|
||||
<String key="SyntaxVehicleOutsideTrainset">Vozidlo {0} stojí mimo soupravu: chybí záhlaví trainset?</String>
|
||||
<String key="SyntaxNotANumber">{0}: „{1}“ místo čísla</String>
|
||||
<String key="SyntaxMissingInclude">Soubor {1} nenalezen (include)</String>
|
||||
<String key="SyntaxIncludeLetterCase">Soubor {1} je na disku jako {0}: linuxový eu07 ho nenačte</String>
|
||||
<String key="SyntaxIncludeNotClosed">include {1} bez end</String>
|
||||
<String key="SyntaxTrainsetUnreadable">Soupravu {0} se nepodařilo načíst</String>
|
||||
<String key="SyntaxVehicleUnreadable">Vozidlo {0} se nepodařilo načíst</String>
|
||||
|
||||
@@ -772,7 +772,6 @@
|
||||
<String key="SyntaxVehicleOutsideTrainset">Vehicle {0} stands outside a trainset: header missing?</String>
|
||||
<String key="SyntaxNotANumber">{0}: “{1}” instead of a number</String>
|
||||
<String key="SyntaxMissingInclude">File {1} not found (include)</String>
|
||||
<String key="SyntaxIncludeLetterCase">File {1} is on disk as {0}: the Linux eu07 will not load it</String>
|
||||
<String key="SyntaxIncludeNotClosed">include {1} without end</String>
|
||||
<String key="SyntaxTrainsetUnreadable">Trainset {0} could not be read</String>
|
||||
<String key="SyntaxVehicleUnreadable">Vehicle {0} could not be read</String>
|
||||
|
||||
@@ -742,7 +742,6 @@
|
||||
<String key="SyntaxVehicleOutsideTrainset">A(z) {0} jármű szerelvényen kívül áll: hiányzik a trainset fejléc?</String>
|
||||
<String key="SyntaxNotANumber">{0}: „{1}” szám helyett</String>
|
||||
<String key="SyntaxMissingInclude">A(z) {1} fájl nem található (include)</String>
|
||||
<String key="SyntaxIncludeLetterCase">A(z) {1} fájl a lemezen {0} néven van: a linuxos eu07 nem tölti be</String>
|
||||
<String key="SyntaxIncludeNotClosed">include {1} end nélkül</String>
|
||||
<String key="SyntaxTrainsetUnreadable">A(z) {0} szerelvényt nem sikerült beolvasni</String>
|
||||
<String key="SyntaxVehicleUnreadable">A(z) {0} járművet nem sikerült beolvasni</String>
|
||||
|
||||
@@ -769,7 +769,6 @@
|
||||
<String key="SyntaxVehicleOutsideTrainset">Pojazd {0} stoi poza składem: brak nagłówka trainset?</String>
|
||||
<String key="SyntaxNotANumber">{0}: „{1}” zamiast liczby</String>
|
||||
<String key="SyntaxMissingInclude">Brak pliku {1} (include)</String>
|
||||
<String key="SyntaxIncludeLetterCase">Plik {1} jest na dysku jako {0}: linuksowy eu07 go nie wczyta</String>
|
||||
<String key="SyntaxIncludeNotClosed">include {1} bez end</String>
|
||||
<String key="SyntaxTrainsetUnreadable">Nie udało się odczytać składu {0}</String>
|
||||
<String key="SyntaxVehicleUnreadable">Nie udało się odczytać pojazdu {0}</String>
|
||||
|
||||
@@ -742,7 +742,6 @@
|
||||
<String key="SyntaxVehicleOutsideTrainset">Транспорт {0} стоит вне состава: нет заголовка trainset?</String>
|
||||
<String key="SyntaxNotANumber">{0}: «{1}» вместо числа</String>
|
||||
<String key="SyntaxMissingInclude">Файл {1} не найден (include)</String>
|
||||
<String key="SyntaxIncludeLetterCase">Файл {1} на диске назван {0}: eu07 на Linux его не загрузит</String>
|
||||
<String key="SyntaxIncludeNotClosed">include {1} без end</String>
|
||||
<String key="SyntaxTrainsetUnreadable">Не удалось прочитать состав {0}</String>
|
||||
<String key="SyntaxVehicleUnreadable">Не удалось прочитать единицу {0}</String>
|
||||
|
||||
Reference in New Issue
Block a user