mirror of
https://github.com/MaSzyna-EU07/StarterNG.git
synced 2026-11-15 20:44:12 +01:00
Compare commits
2 Commits
fix/linux-
...
fix/reshad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e2dbf9eac | ||
|
|
91c496ab48 |
@@ -1,119 +0,0 @@
|
||||
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,8 +97,6 @@ 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>
|
||||
@@ -275,9 +273,6 @@ 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) =>
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
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,20 +21,6 @@ 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,19 +65,6 @@ 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()
|
||||
{
|
||||
@@ -215,31 +202,4 @@ 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,20 +182,6 @@ 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()
|
||||
{
|
||||
@@ -209,26 +195,3 @@ 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,23 +89,6 @@ 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, _ => true);
|
||||
var scenery = TestScenery.Parser().Parse("scenery/x.scn", scn, _ => new IncludeLookup(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 => name is "tory.scm" or "budynek.inc").Faults;
|
||||
var faults = TestScenery.Parser().Parse("scenery/x.scn", scn, name => new IncludeLookup(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_in_other_letter_case_is_no_fault()
|
||||
public void An_include_on_disk_only_in_other_letter_case_is_what_linux_misses()
|
||||
{
|
||||
// The simulator finds the file whatever the case of its letters, on Linux too.
|
||||
// The simulator folds the name to lower case and opens it so.
|
||||
var files = new InMemoryFileSystem(caseSensitive: true)
|
||||
.WithLegacyFile("game/scenery/braniewo_szeroki.scn",
|
||||
"include braniewo/base.ctr end\ninclude Braniewo/Tory.SCM end\n")
|
||||
@@ -357,20 +357,21 @@ public class ScenerySyntaxTests
|
||||
.WithFile("game/scenery/braniewo/tory.scm", "");
|
||||
var repository = TestScenery.Repository(files);
|
||||
|
||||
Assert.Empty(repository.Load("game/scenery/braniewo_szeroki.scn")!.Faults);
|
||||
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)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[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)
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void A_missing_terrain_file_is_no_fault_while_the_scenery_has_its_own_sbt(bool sbt)
|
||||
{
|
||||
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/" + sbtOnDisk, "EU07");
|
||||
files.WithFile("game/scenery/lk2_sandbox.sbt", "EU07");
|
||||
var repository = TestScenery.Repository(files);
|
||||
|
||||
var scenery = repository.Load("game/scenery/Lk2_Sandbox.scn")!;
|
||||
|
||||
@@ -39,24 +39,6 @@ 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()
|
||||
{
|
||||
|
||||
45
StarterNG.Tests/SoftwareRenderTests.cs
Normal file
45
StarterNG.Tests/SoftwareRenderTests.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using Avalonia;
|
||||
using StarterNG.Infrastructure;
|
||||
|
||||
namespace StarterNG.Tests;
|
||||
|
||||
public class SoftwareRenderTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("--software-render")]
|
||||
[InlineData("--Software-Render")]
|
||||
public void The_switch_asks_for_it_in_any_case(string arg)
|
||||
{
|
||||
Assert.True(SoftwareRender.AskedIn(new[] { "--other", arg }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Without_the_switch_the_graphics_card_draws()
|
||||
{
|
||||
Assert.False(SoftwareRender.AskedIn(Array.Empty<string>()));
|
||||
Assert.False(SoftwareRender.AskedIn(new[] { "--software" }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Windows_draws_on_the_processor_into_a_plain_window_surface()
|
||||
{
|
||||
var options = SoftwareRender.Win32Options;
|
||||
|
||||
Assert.Equal(new[] { Win32RenderingMode.Software }, options.RenderingMode);
|
||||
Assert.Equal(new[] { Win32CompositionMode.RedirectionSurface }, options.CompositionMode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void X11_draws_on_the_processor()
|
||||
{
|
||||
Assert.Equal(new[] { X11RenderingMode.Software }, SoftwareRender.X11Options.RenderingMode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Both_platforms_options_go_to_the_same_builder()
|
||||
{
|
||||
var builder = AppBuilder.Configure<Avalonia.Application>();
|
||||
|
||||
Assert.Same(builder, SoftwareRender.ApplyTo(builder));
|
||||
}
|
||||
}
|
||||
98
StarterNG.Tests/SystemGraphicsLibrariesTests.cs
Normal file
98
StarterNG.Tests/SystemGraphicsLibrariesTests.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using StarterNG.Infrastructure;
|
||||
|
||||
namespace StarterNG.Tests;
|
||||
|
||||
public class SystemGraphicsLibrariesTests
|
||||
{
|
||||
private const string System32 = @"C:\Windows\System32";
|
||||
private const string Game = @"E:\SteamLibrary\steamapps\common\MaSzyna";
|
||||
|
||||
private static readonly IntPtr Dxgi = new(0x1000);
|
||||
private static readonly IntPtr D3d11 = new(0x2000);
|
||||
|
||||
private static SystemGraphicsLibraries Take(Func<string, bool> beside, Dictionary<string, IntPtr> system)
|
||||
{
|
||||
return SystemGraphicsLibraries.Take(System32, Game, beside, Load);
|
||||
|
||||
bool Load(string path, out IntPtr handle)
|
||||
{
|
||||
handle = system.GetValueOrDefault(Path.GetFileName(path));
|
||||
return handle != IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
private static SystemGraphicsLibraries WithDxgiAndD3d11(Func<string, bool> beside) =>
|
||||
Take(beside, new Dictionary<string, IntPtr> { ["dxgi.dll"] = Dxgi, ["d3d11.dll"] = D3d11 });
|
||||
|
||||
[Fact]
|
||||
public void An_import_by_name_gets_the_system_library_with_or_without_its_extension()
|
||||
{
|
||||
var taken = WithDxgiAndD3d11(_ => false);
|
||||
|
||||
Assert.Equal(Dxgi, taken.Resolve("dxgi"));
|
||||
Assert.Equal(Dxgi, taken.Resolve("dxgi.dll"));
|
||||
Assert.Equal(D3d11, taken.Resolve("D3D11.DLL"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Other_libraries_and_ones_the_system_lacks_are_left_to_the_usual_search()
|
||||
{
|
||||
var taken = WithDxgiAndD3d11(_ => false);
|
||||
|
||||
Assert.Equal(IntPtr.Zero, taken.Resolve("user32.dll"));
|
||||
Assert.Equal(IntPtr.Zero, taken.Resolve("opengl32"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void They_are_loaded_from_the_system_folder_dxgi_first()
|
||||
{
|
||||
var asked = new List<string>();
|
||||
SystemGraphicsLibraries.Take(System32, Game, _ => false, Load);
|
||||
|
||||
Assert.Equal(SystemGraphicsLibraries.Names.Select(n => Path.Combine(System32, n + ".dll")), asked);
|
||||
Assert.Equal("dxgi", SystemGraphicsLibraries.Names[0]);
|
||||
|
||||
bool Load(string path, out IntPtr handle)
|
||||
{
|
||||
asked.Add(path);
|
||||
handle = IntPtr.Zero;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_wrapper_beside_the_starter_is_noted()
|
||||
{
|
||||
var taken = WithDxgiAndD3d11(path => path == Path.Combine(Game, "dxgi.dll"));
|
||||
|
||||
Assert.Equal(new[] { "dxgi.dll" }, taken.Shadowed);
|
||||
Assert.Contains("dxgi.dll", taken.Note);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void With_nothing_beside_the_starter_there_is_nothing_to_note()
|
||||
{
|
||||
var taken = WithDxgiAndD3d11(_ => false);
|
||||
|
||||
Assert.Empty(taken.Shadowed);
|
||||
Assert.Null(taken.Note);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Only_windows_has_system_libraries_to_take()
|
||||
{
|
||||
var taken = SystemGraphicsLibraries.TakeOnWindows();
|
||||
|
||||
Assert.Equal(OperatingSystem.IsWindows(), taken is not null);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void An_assembly_with_a_resolver_of_its_own_keeps_it()
|
||||
{
|
||||
var taken = WithDxgiAndD3d11(_ => false);
|
||||
var assembly = typeof(SystemGraphicsLibrariesTests).Assembly;
|
||||
|
||||
Assert.True(taken.ServeTo(assembly));
|
||||
Assert.False(taken.ServeTo(assembly));
|
||||
}
|
||||
}
|
||||
@@ -130,17 +130,6 @@ 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())
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
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 })));
|
||||
}
|
||||
@@ -7,9 +7,6 @@ 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,7 +56,6 @@ 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();
|
||||
@@ -120,8 +119,6 @@ public sealed class AppServices
|
||||
|
||||
public TimetableLocator Timetables { get; }
|
||||
|
||||
public AttachmentLocator Attachments { get; }
|
||||
|
||||
public LoadWeightsRepository LoadWeights { get; }
|
||||
|
||||
public GameLibrary Library { get; }
|
||||
|
||||
@@ -61,6 +61,13 @@ 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,6 +5,12 @@ 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
|
||||
@@ -29,17 +35,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="opensInclude">
|
||||
/// Whether the simulator opens an include, named as the include names it; null
|
||||
/// <param name="findInclude">
|
||||
/// What the simulator makes of an include, named as the include names it; null
|
||||
/// leaves includes unchecked.
|
||||
/// </param>
|
||||
public static Result Scan(string text, string file, Func<string, bool>? opensInclude = null)
|
||||
public static Result Scan(string text, string file, Func<string, IncludeLookup>? findInclude = 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, opensInclude, asSimulator: true);
|
||||
var simulator = new Reading(tokens, lines, findInclude, asSimulator: true);
|
||||
simulator.Run();
|
||||
var author = new Reading(tokens, lines, null, asSimulator: false);
|
||||
author.Run();
|
||||
@@ -110,7 +116,7 @@ public static class ScenerySyntax
|
||||
}
|
||||
|
||||
private sealed class Reading(
|
||||
List<SceneryCode.Token> tokens, SceneryLines lines, Func<string, bool>? opensInclude, bool asSimulator)
|
||||
List<SceneryCode.Token> tokens, SceneryLines lines, Func<string, IncludeLookup>? findInclude, bool asSimulator)
|
||||
{
|
||||
public readonly List<(int Start, int End, string Name)> Trainsets = new();
|
||||
public readonly List<(int, int)> Loose = new();
|
||||
@@ -369,8 +375,13 @@ public static class ScenerySyntax
|
||||
return stop < tokens.Count && tokens[stop].Is("end") ? stop : stop - 1;
|
||||
}
|
||||
|
||||
if (opensInclude is not null && Plain(name) && !opensInclude(name))
|
||||
Fault(SceneryFaultKind.MissingInclude, token.Index, token: name);
|
||||
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);
|
||||
}
|
||||
|
||||
// The simulator takes its parameters up to the word "end", written just so,
|
||||
// whatever comes before it.
|
||||
|
||||
@@ -10,9 +10,6 @@ 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);
|
||||
@@ -91,18 +88,8 @@ 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, new EnumerationOptions
|
||||
{
|
||||
IgnoreInaccessible = false,
|
||||
AttributesToSkip = 0,
|
||||
MatchType = MatchType.Win32,
|
||||
MatchCasing = MatchCasing.CaseInsensitive
|
||||
})
|
||||
: Array.Empty<string>();
|
||||
Directory.Exists(path) ? Directory.GetFiles(path, searchPattern) : 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) =>
|
||||
@@ -112,8 +99,7 @@ public sealed class PhysicalFileSystem : IFileSystem
|
||||
RecurseSubdirectories = true,
|
||||
IgnoreInaccessible = true,
|
||||
AttributesToSkip = 0,
|
||||
MatchType = MatchType.Win32,
|
||||
MatchCasing = MatchCasing.CaseInsensitive
|
||||
MatchType = MatchType.Win32
|
||||
})
|
||||
: Array.Empty<string>();
|
||||
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
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,21 +5,16 @@ 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 = FindSource(new PhysicalFileSystem(), Directory.GetCurrentDirectory(), logoKey, sceneryName,
|
||||
Random.Shared);
|
||||
string? src = ResolveSource(logoKey) ?? ResolveSource(sceneryName) ?? PickRandom();
|
||||
if (src is null) return;
|
||||
|
||||
string destDir = Path.Combine(Directory.GetCurrentDirectory(), "textures");
|
||||
@@ -40,36 +35,32 @@ public static class LoadingScreen
|
||||
}
|
||||
}
|
||||
|
||||
/// <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)
|
||||
private static string? ResolveSource(string? key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key)) return null;
|
||||
key = Path.GetFileNameWithoutExtension(key.Trim());
|
||||
if (key.Length == 0) return null;
|
||||
return Extensions.Select(ext => names.Find(dir, key + ext)).FirstOrDefault(path => path is not null);
|
||||
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;
|
||||
}
|
||||
|
||||
private static string? PickRandom(IFileSystem files, string dir, Random random)
|
||||
private static string? PickRandom()
|
||||
{
|
||||
var logos = files.GetFiles(dir, "logo*.*")
|
||||
.Where(f => Extensions.Contains(Path.GetExtension(f), StringComparer.OrdinalIgnoreCase))
|
||||
.Order(StringComparer.Ordinal)
|
||||
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";
|
||||
})
|
||||
.ToArray();
|
||||
if (logos.Length == 0) return null;
|
||||
return logos[random.Next(logos.Length)];
|
||||
if (files.Length == 0) return null;
|
||||
return files[Random.Shared.Next(files.Length)];
|
||||
}
|
||||
|
||||
private static void WriteBmp(string sourcePath, string destPath)
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
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,6 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using StarterNG.Application.Abstractions;
|
||||
using StarterNG.Domain.Sceneries;
|
||||
|
||||
@@ -9,13 +8,11 @@ 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)
|
||||
@@ -37,23 +34,22 @@ 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 Folder, string Name)[] candidates =
|
||||
string[] candidates =
|
||||
{
|
||||
(root, name),
|
||||
(sceneryDir, name),
|
||||
(sceneryDir, "images/" + name),
|
||||
(root, "scenery/images/" + name)
|
||||
name,
|
||||
Path.Combine(root, name),
|
||||
Path.Combine(sceneryDir, name),
|
||||
Path.Combine(sceneryDir, "images", name),
|
||||
Path.Combine(root, "scenery", "images", name)
|
||||
};
|
||||
|
||||
return candidates.Select(candidate => _names.Find(candidate.Folder, candidate.Name))
|
||||
.FirstOrDefault(found => found is not null);
|
||||
foreach (string candidate in candidates)
|
||||
if (_files.FileExists(candidate))
|
||||
return candidate;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,11 +20,11 @@ public sealed class SceneryParser
|
||||
_random = random;
|
||||
}
|
||||
|
||||
/// <param name="opensInclude">
|
||||
/// Whether the simulator opens an include, as the include names it; null leaves
|
||||
/// <param name="findInclude">
|
||||
/// What the simulator makes of an include, as the include names it; null leaves
|
||||
/// includes unchecked.
|
||||
/// </param>
|
||||
public Scenery Parse(string path, string content, Func<string, bool>? opensInclude = null)
|
||||
public Scenery Parse(string path, string content, Func<string, IncludeLookup>? findInclude = 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), opensInclude);
|
||||
var syntax = ScenerySyntax.Scan(content, System.IO.Path.GetFileName(path), findInclude);
|
||||
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 CaseInsensitivePaths _names;
|
||||
private readonly ConcurrentDictionary<(string, bool), IReadOnlyList<string>> _listings = new();
|
||||
|
||||
public SceneryRepository(IFileSystem files, IGamePaths paths, IDiagnosticsLog log, SceneryParser parser)
|
||||
{
|
||||
@@ -31,12 +31,11 @@ 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)
|
||||
{
|
||||
_names.Forget();
|
||||
_listings.Clear();
|
||||
var files = SceneryFiles().ToList();
|
||||
var loaded = new ConcurrentBag<Scenery>();
|
||||
int total = Math.Max(1, files.Count);
|
||||
@@ -59,7 +58,7 @@ public sealed class SceneryRepository : ISceneryRepository
|
||||
|
||||
public Scenery? Load(string path)
|
||||
{
|
||||
_names.Forget();
|
||||
_listings.Clear();
|
||||
return LoadOne(path);
|
||||
}
|
||||
|
||||
@@ -80,8 +79,10 @@ 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");
|
||||
bool OpensInclude(string name) =>
|
||||
Opens(name) || (binaryTerrain && SimulatorNames.Lower(name).Contains("_ter.scm"));
|
||||
IncludeLookup FindInclude(string name) =>
|
||||
Opens(name) || (binaryTerrain && SimulatorNames.Lower(name).Contains("_ter.scm"))
|
||||
? new IncludeLookup(true)
|
||||
: new IncludeLookup(false, ResolveInclude(name));
|
||||
|
||||
(string File, string Text)? ReadInclude(string name)
|
||||
{
|
||||
@@ -98,7 +99,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, OpensInclude);
|
||||
var scenery = _parser.Parse(path, text, FindInclude);
|
||||
scenery.HasCompanionTimetable = binaryTerrain;
|
||||
scenery.Sources = sources;
|
||||
scenery.TextEncoding = ownEncoding;
|
||||
@@ -113,11 +114,11 @@ public sealed class SceneryRepository : ISceneryRepository
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private bool Opens(string name) => ResolveInclude(name) is not null;
|
||||
private bool Opens(string name) =>
|
||||
_files.FileExists(Path.Combine(_paths.Scenery, SimulatorNames.Lower(name.Replace('\\', '/'))));
|
||||
|
||||
/// <summary>
|
||||
/// An include's file as it is named on disk, from the scenery folder, whatever
|
||||
@@ -125,7 +126,44 @@ 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) => _names.Resolve(_paths.Scenery, name);
|
||||
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 IEnumerable<string> SceneryFiles() =>
|
||||
_files.GetFiles(_paths.Scenery, "*.scn")
|
||||
|
||||
@@ -14,14 +14,12 @@ 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;
|
||||
}
|
||||
|
||||
@@ -31,11 +29,12 @@ 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(_names.Find(sceneryDir, $"i18n/{id}_{BaseLanguage}.json"));
|
||||
TryLoad(Path.Combine(i18nDir, $"{id}_{BaseLanguage}.json"));
|
||||
if (!string.Equals(language, BaseLanguage, StringComparison.OrdinalIgnoreCase))
|
||||
TryLoad(_names.Find(sceneryDir, $"i18n/{id}_{language}.json"));
|
||||
TryLoad(Path.Combine(i18nDir, $"{id}_{language}.json"));
|
||||
}
|
||||
|
||||
public string Translate(string? text)
|
||||
@@ -51,9 +50,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 (path is null)
|
||||
if (!_files.FileExists(path))
|
||||
return;
|
||||
|
||||
try
|
||||
|
||||
@@ -17,12 +17,10 @@ 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)
|
||||
@@ -32,19 +30,17 @@ public sealed class TimetableLocator
|
||||
|
||||
string sceneryDir = Path.GetDirectoryName(scenery.Path) ?? ".";
|
||||
string root = Path.GetDirectoryName(sceneryDir) ?? ".";
|
||||
name = name.Replace('\\', '/');
|
||||
|
||||
(string Folder, string Name)[] candidates =
|
||||
string[] candidates =
|
||||
{
|
||||
(root, "timetables/" + name + ".txt"),
|
||||
(sceneryDir, name + ".txt"),
|
||||
(root, "scenario/" + name + ".txt"),
|
||||
(root, "timetables/" + name),
|
||||
(sceneryDir, name)
|
||||
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)
|
||||
};
|
||||
|
||||
return candidates.Select(candidate => _names.Find(candidate.Folder, candidate.Name))
|
||||
.FirstOrDefault(found => found is not null);
|
||||
return candidates.FirstOrDefault(_files.FileExists);
|
||||
}
|
||||
|
||||
/// <summary>The number the timetable gives its train; null with no timetable or no such heading in it.</summary>
|
||||
|
||||
32
StarterNG/Infrastructure/SoftwareRender.cs
Normal file
32
StarterNG/Infrastructure/SoftwareRender.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Avalonia;
|
||||
|
||||
namespace StarterNG.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// Drawn by the processor instead of the graphics card, for a machine on which the
|
||||
/// window comes up empty. Each platform reads only its own options, so both are given.
|
||||
/// </summary>
|
||||
public static class SoftwareRender
|
||||
{
|
||||
public const string Switch = "--software-render";
|
||||
|
||||
public static bool AskedIn(IEnumerable<string> args) =>
|
||||
args.Any(a => string.Equals(a, Switch, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
public static Win32PlatformOptions Win32Options => new()
|
||||
{
|
||||
RenderingMode = new[] { Win32RenderingMode.Software },
|
||||
CompositionMode = new[] { Win32CompositionMode.RedirectionSurface }
|
||||
};
|
||||
|
||||
public static X11PlatformOptions X11Options => new()
|
||||
{
|
||||
RenderingMode = new[] { X11RenderingMode.Software }
|
||||
};
|
||||
|
||||
public static AppBuilder ApplyTo(AppBuilder builder) =>
|
||||
builder.With(Win32Options).With(X11Options);
|
||||
}
|
||||
90
StarterNG/Infrastructure/SystemGraphicsLibraries.cs
Normal file
90
StarterNG/Infrastructure/SystemGraphicsLibraries.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace StarterNG.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// The starter sits in the simulator's folder, and so do the graphics wrappers a player
|
||||
/// puts there for the simulator - ReShade and the like, named after the system library
|
||||
/// they stand in for. Windows looks in the program's own folder first, so the starter's
|
||||
/// window drew through them too, and with some of them it drew nothing at all.
|
||||
/// Loaded from System32 by full path before anything asks for them, these names are
|
||||
/// taken: a native library that asks by name gets the system one. .NET looks beside the
|
||||
/// program by full path before it asks by name, so the window code's own imports are
|
||||
/// handed the same libraries. The simulator is a process of its own and still finds its
|
||||
/// wrappers.
|
||||
/// Elsewhere there is nothing of the kind to keep out. Under Wine or Proton the system
|
||||
/// folder is the prefix's, so what is taken there is what would have been taken anyway.
|
||||
/// </summary>
|
||||
public sealed class SystemGraphicsLibraries
|
||||
{
|
||||
public delegate bool Loader(string path, out IntPtr handle);
|
||||
|
||||
// dxgi ahead of d3d11, which asks for it.
|
||||
public static readonly IReadOnlyList<string> Names = new[] { "dxgi", "d3d11", "d3d9", "opengl32" };
|
||||
|
||||
private readonly Dictionary<string, IntPtr> _loaded = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly List<string> _shadowed = new();
|
||||
|
||||
private SystemGraphicsLibraries()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>The files of these names that lay beside the starter.</summary>
|
||||
public IReadOnlyList<string> Shadowed => _shadowed;
|
||||
|
||||
/// <summary>What goes to the log about them, or null when none lay there.</summary>
|
||||
public string? Note => _shadowed.Count > 0
|
||||
? $"Graphics libraries beside the starter, left to the simulator: {string.Join(", ", _shadowed)}"
|
||||
: null;
|
||||
|
||||
/// <summary>The libraries of this Windows, or null on any other system.</summary>
|
||||
public static SystemGraphicsLibraries? TakeOnWindows() =>
|
||||
OperatingSystem.IsWindows()
|
||||
? Take(Environment.SystemDirectory, AppContext.BaseDirectory, File.Exists, NativeLibrary.TryLoad)
|
||||
: null;
|
||||
|
||||
public static SystemGraphicsLibraries Take(string systemDirectory, string ownDirectory,
|
||||
Func<string, bool> exists, Loader load)
|
||||
{
|
||||
var taken = new SystemGraphicsLibraries();
|
||||
foreach (string name in Names)
|
||||
{
|
||||
string file = name + ".dll";
|
||||
if (exists(Path.Combine(ownDirectory, file)))
|
||||
taken._shadowed.Add(file);
|
||||
if (load(Path.Combine(systemDirectory, file), out IntPtr handle))
|
||||
taken._loaded[name] = handle;
|
||||
}
|
||||
return taken;
|
||||
}
|
||||
|
||||
/// <summary>The system library for an import of one of these names; zero leaves it to the usual search.</summary>
|
||||
public IntPtr Resolve(string libraryName)
|
||||
{
|
||||
string name = libraryName.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)
|
||||
? libraryName[..^4]
|
||||
: libraryName;
|
||||
return _loaded.TryGetValue(name, out IntPtr handle) ? handle : IntPtr.Zero;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The assembly's imports of these names get the system libraries. False when it
|
||||
/// has a resolver of its own already; that one decides.
|
||||
/// </summary>
|
||||
public bool ServeTo(Assembly importer)
|
||||
{
|
||||
try
|
||||
{
|
||||
NativeLibrary.SetDllImportResolver(importer, (name, _, _) => Resolve(name));
|
||||
return true;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,6 @@ public sealed class FizPhysicsRepository : IPhysicsRepository
|
||||
};
|
||||
|
||||
private readonly IFileSystem _files;
|
||||
private readonly CaseInsensitivePaths _names;
|
||||
private readonly IGamePaths _paths;
|
||||
private readonly IDiagnosticsLog _log;
|
||||
|
||||
@@ -39,7 +38,6 @@ public sealed class FizPhysicsRepository : IPhysicsRepository
|
||||
public FizPhysicsRepository(IFileSystem files, IGamePaths paths, IDiagnosticsLog log)
|
||||
{
|
||||
_files = files;
|
||||
_names = new CaseInsensitivePaths(files);
|
||||
_paths = paths;
|
||||
_log = log;
|
||||
}
|
||||
@@ -167,9 +165,8 @@ public sealed class FizPhysicsRepository : IPhysicsRepository
|
||||
while (i < tokens.Count && !tokens[i].Equals("end", StringComparison.OrdinalIgnoreCase))
|
||||
includeParameters.Add(Resolve(tokens[i++], parameters));
|
||||
|
||||
// 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);
|
||||
Parse(physics, Path.Combine(directory, includeFile), directory, includeParameters.ToArray(),
|
||||
depth + 1);
|
||||
pendingKey = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -10,53 +10,44 @@ 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>();
|
||||
try
|
||||
{
|
||||
foreach (var texture in catalog.Textures)
|
||||
{
|
||||
string directory = texture.Directory.Replace('\\', '/').TrimEnd('/');
|
||||
|
||||
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(Under(directory, texture.Model), ModelExtensions))
|
||||
lines.Add($"# no model: {texture.Directory}{texture.Model}");
|
||||
}
|
||||
}
|
||||
finally
|
||||
foreach (var texture in catalog.Textures)
|
||||
{
|
||||
// The folders are listed once for the whole catalog, and not kept past it.
|
||||
_names.Forget();
|
||||
string directory = Path.Combine(
|
||||
_paths.Dynamic,
|
||||
texture.Directory.Replace('/', Path.DirectorySeparatorChar).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.Model) &&
|
||||
!ExistsWithAnyExtension(Path.Combine(directory, texture.Model), ModelExtensions))
|
||||
lines.Add($"# no model: {texture.Directory}{texture.Model}");
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
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)
|
||||
private bool ExistsWithAnyExtension(string path, string[] extensions)
|
||||
{
|
||||
if (_names.Resolve(_paths.Dynamic, name) is not null)
|
||||
if (_files.FileExists(path))
|
||||
return true;
|
||||
|
||||
foreach (string extension in extensions)
|
||||
if (_names.Resolve(_paths.Dynamic, Path.ChangeExtension(name, extension)) is not null)
|
||||
if (_files.FileExists(Path.ChangeExtension(path, extension)))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -10,12 +10,22 @@ namespace StarterNG;
|
||||
static class Program
|
||||
{
|
||||
|
||||
private static bool _softwareRender;
|
||||
|
||||
[STAThread]
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
// Ahead of everything that could draw: what lies beside the starter is the simulator's.
|
||||
var graphics = SystemGraphicsLibraries.TakeOnWindows();
|
||||
graphics?.ServeTo(typeof(Win32PlatformOptions).Assembly);
|
||||
|
||||
_softwareRender = SoftwareRender.AskedIn(args);
|
||||
|
||||
// Composition root first: everything below, including the crash handlers,
|
||||
// resolves its collaborators from this graph.
|
||||
AppServices.Initialize();
|
||||
if (graphics?.Note is { } note)
|
||||
Diagnostics.Log(note);
|
||||
|
||||
AppDomain.CurrentDomain.UnhandledException += (_, e) =>
|
||||
{
|
||||
@@ -63,9 +73,13 @@ static class Program
|
||||
};
|
||||
}
|
||||
|
||||
private static AppBuilder WithSoftwareRenderIfAsked(this AppBuilder builder) =>
|
||||
_softwareRender ? SoftwareRender.ApplyTo(builder) : builder;
|
||||
|
||||
public static AppBuilder BuildAvaloniaApp()
|
||||
=> AppBuilder.Configure<App>()
|
||||
.UsePlatformDetect()
|
||||
.WithSoftwareRenderIfAsked()
|
||||
.WithInterFont()
|
||||
.WithDiagnosticsLogging()
|
||||
.AfterSetup(_ => LogUiExceptions());
|
||||
|
||||
@@ -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.6</VersionPrefix>
|
||||
<VersionPrefix>1.5</VersionPrefix>
|
||||
<AssemblyName>Starter</AssemblyName>
|
||||
<Company>eu07.pl</Company>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
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;
|
||||
|
||||
@@ -19,7 +17,13 @@ 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(
|
||||
Path.GetFullPath(TextureFolder.Of(AppServices.Current.Files, AppServices.Current.Paths, texture))));
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -492,18 +492,20 @@ 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 = AppServices.Current.Attachments.Resolve(att) is not null;
|
||||
btn.IsEnabled = File.Exists(Path.GetFullPath(att.FilePath));
|
||||
ToolTip.SetShowOnDisabled(btn, true);
|
||||
btn.Click += (_, _) => OpenAttachment(att);
|
||||
btn.Click += (_, _) => OpenAttachment(att.FilePath);
|
||||
attachmentsPanel.Children.Add(btn);
|
||||
}
|
||||
}
|
||||
|
||||
private static void OpenAttachment(SceneryAttachment attachment)
|
||||
private static void OpenAttachment(string relativePath)
|
||||
{
|
||||
if (AppServices.Current.Attachments.Resolve(attachment) is not { } found)
|
||||
string full = Path.GetFullPath(relativePath);
|
||||
if (!File.Exists(full))
|
||||
full = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), relativePath));
|
||||
if (!File.Exists(full))
|
||||
return;
|
||||
string full = Path.GetFullPath(found);
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo(full) { UseShellExecute = true });
|
||||
|
||||
@@ -742,6 +742,7 @@
|
||||
<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,6 +772,7 @@
|
||||
<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,6 +742,7 @@
|
||||
<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,6 +769,7 @@
|
||||
<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,6 +742,7 @@
|
||||
<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