mirror of
https://github.com/MaSzyna-EU07/StarterNG.git
synced 2026-11-14 04:05:50 +01:00
eu07 reads static terrain from files named by "terrain ... endterrain": text (.txtf) or its binary version (.btf). The scenery check now tells a terrain file of which neither is there (MissingTerrainFile). Such a file also takes the place of the scenery's .sbt, so the terrain includes after it are read after all and a missing one is told again. When the .sbt was already used before the directive - a terrain include or terrain geometry passed over, or FirstInit - the simulator passes the file over instead, which is told too (TerrainFileBehindSbt). The parser takes the lookup of files and whether there is an .sbt as one SceneryFileLookup.
436 lines
18 KiB
C#
436 lines
18 KiB
C#
using StarterNG.Domain.Sceneries;
|
|
using StarterNG.Tests.Fakes;
|
|
|
|
namespace StarterNG.Tests;
|
|
|
|
public class ScenerySyntaxTests
|
|
{
|
|
private static (SceneryFaultKind, string, int, string?, string?) Seen(SceneryFault fault) =>
|
|
(fault.Kind, fault.File, fault.Line, fault.Subject, fault.Token);
|
|
|
|
[Fact]
|
|
public void A_trainset_commented_out_is_no_trainset()
|
|
{
|
|
const string scn = """
|
|
/* trainset stary tor_1 0 0
|
|
node -1 0 st dynamic PKP/EP07 ep07 ep07 0 headdriver 3 0 enddynamic
|
|
endtrainset */
|
|
// trainset dla gracza:
|
|
trainset pociag tor_1 10 0
|
|
node -1 0 ep07-001 dynamic PKP/EP07 ep07 ep07 0 headdriver 3 0 enddynamic//koniec
|
|
endtrainset
|
|
""";
|
|
|
|
var scenery = TestScenery.Parser().Parse("scenery/x.scn", scn);
|
|
|
|
var trainset = Assert.Single(scenery.Trainsets);
|
|
Assert.Equal("pociag", trainset.Name);
|
|
Assert.Single(trainset.Vehicles);
|
|
Assert.Empty(scenery.Faults);
|
|
Assert.Contains("trainset stary", scenery.Template);
|
|
}
|
|
|
|
[Fact]
|
|
public void A_trainset_without_its_end_is_reported_and_the_next_one_read_on_its_own()
|
|
{
|
|
const string scn = """
|
|
FirstInit
|
|
trainset pierwszy tor_1 10 0
|
|
node -1 0 a dynamic PKP/EP07 ep07 ep07 0 headdriver 3 0 enddynamic
|
|
trainset drugi tor_2 10 0
|
|
node -1 0 b dynamic PKP/EP07 ep07 ep07 0 headdriver 3 0 enddynamic
|
|
endtrainset
|
|
""";
|
|
|
|
var scenery = TestScenery.Parser().Parse("scenery/x.scn", scn);
|
|
|
|
Assert.Equal((SceneryFaultKind.TrainsetNotClosed, "x.scn", 2, "pierwszy", null),
|
|
Seen(Assert.Single(scenery.Faults)));
|
|
var trainset = Assert.Single(scenery.Trainsets);
|
|
Assert.Equal("drugi", trainset.Name);
|
|
Assert.Equal("b", Assert.Single(trainset.Vehicles).Name);
|
|
// The unended one is the simulator's to read as written.
|
|
Assert.Contains("trainset pierwszy", scenery.Template);
|
|
}
|
|
|
|
[Fact]
|
|
public void A_vehicle_without_its_end_is_reported_yet_its_trainset_listed()
|
|
{
|
|
const string scn = """
|
|
trainset pociag tor_1 10 0
|
|
node -1 0 a dynamic PKP/EP07 ep07 ep07 0 headdriver 3
|
|
node -1 0 b dynamic PKP/111A 111a 111a 0 nobody 3 0 enddynamic
|
|
endtrainset
|
|
""";
|
|
|
|
var scenery = TestScenery.Parser().Parse("scenery/x.scn", scn);
|
|
|
|
var fault = Assert.Single(scenery.Faults);
|
|
Assert.Equal((SceneryFaultKind.VehicleNotClosed, "x.scn", 2, "a", null), Seen(fault));
|
|
// The simulator goes on to the next enddynamic, taking b for a's.
|
|
Assert.Equal("b", fault.Lost);
|
|
Assert.Equal(new[] { "a", "b" }, Assert.Single(scenery.Trainsets).Vehicles.Select(v => v.Name));
|
|
}
|
|
|
|
[Fact]
|
|
public void A_vehicle_with_no_load_count_takes_the_next_one_with_it()
|
|
{
|
|
const string scn = """
|
|
trainset pociag tor_1 10 0
|
|
node -1 0 a dynamic PKP/EP07 ep07 ep07 0 headdriver 3 enddynamic
|
|
node -1 0 b dynamic PKP/111A 111a 111a 0 nobody 3 0 enddynamic
|
|
endtrainset
|
|
""";
|
|
|
|
var scenery = TestScenery.Parser().Parse("scenery/x.scn", scn);
|
|
|
|
// Its enddynamic read for the count, "node" for a destination, b's for its end.
|
|
var fault = Assert.Single(scenery.Faults);
|
|
Assert.Equal((SceneryFaultKind.LoadCountMissing, 2, "a", "b"), (fault.Kind, fault.Line, fault.Subject, fault.Lost));
|
|
Assert.Equal(new[] { "a", "b" }, Assert.Single(scenery.Trainsets).Vehicles.Select(v => v.Name));
|
|
}
|
|
|
|
[Fact]
|
|
public void A_load_with_no_type_runs_on_into_the_next_trainset()
|
|
{
|
|
const string scn = """
|
|
trainset pierwszy tor_1 10 0
|
|
node -1 0 a dynamic PKP/412Z 412z 412z 0 nobody 3 40 enddynamic
|
|
endtrainset
|
|
trainset drugi tor_2 10 0
|
|
node -1 0 b dynamic PKP/EP07 ep07 ep07 0 headdriver 3 0 enddynamic
|
|
node -1 0 c dynamic PKP/111A 111a 111a 0 nobody 3 0 enddynamic
|
|
endtrainset
|
|
""";
|
|
|
|
var scenery = TestScenery.Parser().Parse("scenery/x.scn", scn);
|
|
|
|
// The second endtrainset is what ends the first trainset then.
|
|
var fault = Assert.Single(scenery.Faults);
|
|
Assert.Equal((SceneryFaultKind.LoadTypeMissing, 2, "a", "40", "endtrainset, trainset drugi, b"),
|
|
(fault.Kind, fault.Line, fault.Subject, fault.Token, fault.Lost));
|
|
Assert.Equal(new[] { "pierwszy", "drugi" }, scenery.Trainsets.Select(t => t.Name));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("0 none", "none")]
|
|
[InlineData("0 pantstate", "pantstate")]
|
|
[InlineData("0 cel Katowice", null)]
|
|
[InlineData("3 pantstate", null)]
|
|
[InlineData("12 wegiel", null)]
|
|
public void One_word_after_the_load_is_taken_for_a_destination_named_enddynamic(string load, string? word)
|
|
{
|
|
string scn = $"""
|
|
trainset pociag tor_1 10 0
|
|
node -1 0 a dynamic PKP/EP07 ep07 ep07 0 headdriver 3 {load} enddynamic
|
|
node -1 0 b dynamic PKP/111A 111a 111a 0 nobody 3 0 enddynamic
|
|
endtrainset
|
|
""";
|
|
|
|
var faults = TestScenery.Parser().Parse("scenery/x.scn", scn).Faults;
|
|
|
|
if (word is null)
|
|
Assert.Empty(faults);
|
|
else
|
|
Assert.Equal((SceneryFaultKind.WordBeforeEnd, word, "b"), (faults.Single().Kind, faults.Single().Token, faults.Single().Lost));
|
|
}
|
|
|
|
[Fact]
|
|
public void A_header_written_twice_is_told_as_such()
|
|
{
|
|
const string scn = """
|
|
trainset baltyk\mod\exe45151 chojnice301 40 0.1
|
|
//$o -Skład ozdobny
|
|
trainset baltyk\mod\exe45151 chojnice301 40 0.1
|
|
node -1 0 a dynamic PKP/EP07 ep07 ep07 0 headdriver 3 0 enddynamic
|
|
endtrainset
|
|
""";
|
|
|
|
var scenery = TestScenery.Parser().Parse("scenery/x.scn", scn);
|
|
|
|
Assert.Equal((SceneryFaultKind.TrainsetHeaderRepeated, "x.scn", 1, "baltyk\\mod\\exe45151", null),
|
|
Seen(Assert.Single(scenery.Faults)));
|
|
Assert.Single(scenery.Trainsets);
|
|
}
|
|
|
|
[Fact]
|
|
public void A_trainset_whose_header_is_swallowed_is_told_at_the_fault_that_began_it()
|
|
{
|
|
const string scn = """
|
|
trainset pierwszy tor_1 10 0
|
|
node -1 0 a dynamic PKP/EP07 ep07 ep07 0 headdriver 3 0 enddynamic
|
|
endtrainset
|
|
//trainset bez_naglowka tor_2 0 0
|
|
node -1 0 b dynamic PKP/EP07 ep07 ep07 0 headdriver 3 0 enddynamic
|
|
endtrainset
|
|
trainset trzeci tor_3 0 0
|
|
node -1 0 c1 dynamic PKP/EU07 eu07 eu07 0 headdriver 3 0 enddynamic
|
|
node -1 0 c2 dynamic PKP/111A 111a 111a 0 nobody 3 0 enddynamic
|
|
node -1 0 c3 dynamic PKP/111A 111a 111a 0 nobody 3 0 enddynamic
|
|
endtrainset
|
|
trainset czwarty tor_4 0 0
|
|
node -1 0 d dynamic PKP/EP07 ep07 ep07 0 headdriver 3 0 enddynamic
|
|
endtrainset
|
|
""";
|
|
|
|
var scenery = TestScenery.Parser().Parse("scenery/x.scn", scn);
|
|
|
|
// c2 with no trainset takes c3, and the endtrainset is left alone - all of
|
|
// it from b, where the fault is.
|
|
var fault = Assert.Single(scenery.Faults);
|
|
Assert.Equal((SceneryFaultKind.VehicleOutsideTrainset, 5, "endtrainset, trainset trzeci, c1", "trzeci"),
|
|
(fault.Kind, fault.Line, fault.Lost, fault.Unmade));
|
|
Assert.Equal(new[] { "pierwszy", "trzeci", "czwarty" }, scenery.Trainsets.Select(t => t.Name));
|
|
}
|
|
|
|
[Fact]
|
|
public void Text_where_a_number_belongs_is_named()
|
|
{
|
|
const string scn = """
|
|
trainset pociag tor_1 1O 0
|
|
node -1 0 a dynamic PKP/EP07 ep07 ep07 0,5 headdriver 3 0 enddynamic
|
|
endtrainset
|
|
""";
|
|
|
|
var faults = TestScenery.Parser().Parse("scenery/x.scn", scn).Faults;
|
|
|
|
Assert.Equal(new[]
|
|
{
|
|
(SceneryFaultKind.NotANumber, "x.scn", 1, (string?)"pociag", (string?)"1O"),
|
|
(SceneryFaultKind.NotANumber, "x.scn", 2, "a", "0,5")
|
|
}, faults.Select(Seen));
|
|
}
|
|
|
|
[Fact]
|
|
public void A_track_name_split_by_a_space_is_named_whole()
|
|
{
|
|
// "Pawonków#L" with its "ó" lost to a space.
|
|
const string scn = """
|
|
trainset none Pawonk w#L 200.0 0.0
|
|
node -1 0 a dynamic PKP/EP07 ep07 ep07 0 headdriver 3 0 enddynamic
|
|
endtrainset
|
|
""";
|
|
|
|
var faults = TestScenery.Parser().Parse("scenery/x.scn", scn).Faults;
|
|
|
|
Assert.Equal((SceneryFaultKind.TrackNameSplit, "x.scn", 1, "none", "Pawonk w#L"), Seen(Assert.Single(faults)));
|
|
}
|
|
|
|
[Fact]
|
|
public void Vehicles_left_without_their_trainset_header_are_told_so()
|
|
{
|
|
const string scn = """
|
|
//trainset 2003r\64040 Staniszcze_Male_po#2 -50 0.1
|
|
node -1 0 EN57-1542ra dynamic pkp\en57_v2 EN57-1542ra EN57RA 0 headdriver 255 0 enddynamic
|
|
endtrainset
|
|
node -1 0 auto dynamic road\ford transit transit droga-b 0 headdriver 25.0 0 enddynamic
|
|
""";
|
|
|
|
var scenery = TestScenery.Parser().Parse("scenery/x.scn", scn);
|
|
|
|
// On no track the simulator has, it drops the vehicle and goes on to the
|
|
// next enddynamic - past the endtrainset, to the car's.
|
|
var fault = Assert.Single(scenery.Faults);
|
|
Assert.Equal((SceneryFaultKind.VehicleOutsideTrainset, 2, "endtrainset, auto"), (fault.Kind, fault.Line, fault.Lost));
|
|
Assert.Equal("auto", Assert.Single(scenery.LooseVehicles).Name);
|
|
}
|
|
|
|
[Fact]
|
|
public void Structure_faults_are_found_where_the_simulator_reports_them()
|
|
{
|
|
const string scn = """
|
|
endtrainset
|
|
trainset pusty tor_1 10 0
|
|
endtrainset trainset pociag tor_1 10 0
|
|
node -1 0 a dynamic PKP/EP07 ep07 ep07 0 headdriver enddynamic
|
|
node -1 0 b dynamic PKP/EP07 ep07 ep07 0 headdriver 3 0 enddynamic
|
|
reversed
|
|
endtrainset
|
|
include dekoracje.scm // bez end
|
|
include auta.scm end
|
|
/* bez końca
|
|
""";
|
|
|
|
var faults = TestScenery.Parser().Parse("scenery/x.scn", scn).Faults.Select(f => (f.Kind, f.Line)).ToList();
|
|
|
|
Assert.Equal(new[]
|
|
{
|
|
(SceneryFaultKind.StrayEndTrainset, 1),
|
|
(SceneryFaultKind.VehicleIncomplete, 4),
|
|
(SceneryFaultKind.ReversedMisplaced, 6),
|
|
(SceneryFaultKind.IncludeNotClosed, 8),
|
|
(SceneryFaultKind.UnclosedComment, 10)
|
|
}, faults);
|
|
}
|
|
|
|
[Fact]
|
|
public void A_trainset_with_no_vehicles_is_listed_to_be_filled_in_the_depot()
|
|
{
|
|
const string scn = """
|
|
trainset td/rozklad tdo_n1734 0.0 0.1
|
|
//$o Skład gotowy na żeberku.
|
|
endtrainset
|
|
include koniec.scm
|
|
""";
|
|
|
|
var scenery = TestScenery.Parser().Parse("scenery/x.scn", scn, new SceneryFileLookup(_ => true));
|
|
|
|
Assert.Empty(scenery.Faults);
|
|
Assert.Equal("td/rozklad", Assert.Single(scenery.Trainsets).Name);
|
|
}
|
|
|
|
[Fact]
|
|
public void Reversed_may_follow_the_assignments_ahead_of_the_vehicles()
|
|
{
|
|
const string scn = """
|
|
Trainset pociag tor_1 10 0
|
|
assignment pl "Pociąg" endassignment
|
|
reversed
|
|
Node -1 0 a dynamic PKP/EP07 ep07 ep07 0 HeadDriver 3 0 EndDynamic
|
|
EndTrainset
|
|
""";
|
|
|
|
var scenery = TestScenery.Parser().Parse("scenery/x.scn", scn);
|
|
|
|
Assert.Empty(scenery.Faults);
|
|
var trainset = Assert.Single(scenery.Trainsets);
|
|
Assert.True(trainset.Reversed);
|
|
Assert.Equal(StarterNG.Classes.eDriverType.Headdriver, Assert.Single(trainset.Vehicles).DriverType);
|
|
}
|
|
|
|
[Fact]
|
|
public void A_fault_in_an_included_file_is_placed_in_that_file()
|
|
{
|
|
string text = SceneryIncludes.Inline(
|
|
"FirstInit\ninclude tomaszewo/auta.scm end\ntrainset zly tor 0\nendtrainset\n",
|
|
name => (name, "// auta\ntrainset auto road1 0 50\nnode -1 0 a1 dynamic car car car x headdriver 3 0 enddynamic\nendtrainset"));
|
|
|
|
var faults = TestScenery.Parser().Parse("scenery/x.scn", text).Faults.Select(Seen);
|
|
|
|
Assert.Equal(new[]
|
|
{
|
|
(SceneryFaultKind.NotANumber, "tomaszewo/auta.scm", 3, (string?)"a1", (string?)"x"),
|
|
(SceneryFaultKind.TrainsetIncomplete, "x.scn", 3, "zly", null)
|
|
}, faults);
|
|
}
|
|
|
|
[Fact]
|
|
public void Missing_includes_are_reported_but_optional_ones_and_templates_are_not()
|
|
{
|
|
const string scn = """
|
|
include tory.scm end
|
|
include brak.scm end // komentarz
|
|
include budynek.inc (p1) 0 0 end
|
|
include $(p1)_dom.inc end
|
|
include opcja.scm end //$optional 2,Opcja
|
|
endoptional
|
|
""";
|
|
|
|
var faults = TestScenery.Parser().Parse("scenery/x.scn", scn, new SceneryFileLookup(name => name is "tory.scm" or "budynek.inc")).Faults;
|
|
|
|
Assert.Equal((SceneryFaultKind.MissingInclude, "x.scn", 2, null, "brak.scm"), Seen(Assert.Single(faults)));
|
|
}
|
|
|
|
[Fact]
|
|
public void An_include_named_in_another_case_is_found_as_the_simulator_finds_it()
|
|
{
|
|
var files = new InMemoryFileSystem(caseSensitive: true)
|
|
.WithLegacyFile("game/scenery/x.scn", "FirstInit\ninclude Tomaszewo/Auta.SCM end\n")
|
|
.WithLegacyFile("game/scenery/tomaszewo/auta.scm",
|
|
"trainset auto road1 0 50\nnode -1 0 a1 dynamic car car car 0 headdriver 3 0 enddynamic\nendtrainset\n");
|
|
var repository = TestScenery.Repository(files);
|
|
|
|
var scenery = repository.Load("game/scenery/x.scn")!;
|
|
|
|
Assert.Empty(scenery.Faults);
|
|
Assert.Equal("tomaszewo/auta.scm", Assert.Single(scenery.Trainsets).SourceFile);
|
|
}
|
|
|
|
[Fact]
|
|
public void An_include_on_disk_in_other_letter_case_is_no_fault()
|
|
{
|
|
// 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")
|
|
.WithFile("game/scenery/Braniewo/Base.ctr", "")
|
|
.WithFile("game/scenery/braniewo/tory.scm", "");
|
|
var repository = TestScenery.Repository(files);
|
|
|
|
Assert.Empty(repository.Load("game/scenery/braniewo_szeroki.scn")!.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)
|
|
{
|
|
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");
|
|
var repository = TestScenery.Repository(files);
|
|
|
|
var scenery = repository.Load("game/scenery/Lk2_Sandbox.scn")!;
|
|
|
|
Assert.Equal(sbt, scenery.HasCompanionTimetable);
|
|
Assert.Equal(sbt ? 0 : 1, scenery.Faults.Count);
|
|
}
|
|
|
|
private static IReadOnlyList<SceneryFault> TerrainFaults(string scn, bool sbt, params string[] onDisk)
|
|
{
|
|
var files = new InMemoryFileSystem(caseSensitive: true).WithLegacyFile("game/scenery/Mapa.scn", scn);
|
|
if (sbt)
|
|
files.WithFile("game/scenery/mapa.sbt", "EU07");
|
|
foreach (string file in onDisk)
|
|
files.WithFile("game/scenery/" + file, "");
|
|
return TestScenery.Repository(files).Load("game/scenery/Mapa.scn")!.Faults;
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("Teren/Mapa.txtf")]
|
|
[InlineData("teren/mapa.btf")]
|
|
[InlineData("TEREN/MAPA.BTF")]
|
|
public void A_terrain_file_found_as_text_or_binary_is_no_fault(string onDisk) =>
|
|
Assert.Empty(TerrainFaults("terrain teren/mapa.txtf endterrain\nFirstInit\n", sbt: false, onDisk));
|
|
|
|
[Fact]
|
|
public void A_terrain_file_missing_both_ways_is_reported_but_an_sbt_named_with_it_is_not()
|
|
{
|
|
var faults = TerrainFaults("// teren\nterrain teren/mapa.btf teren/stara.sbt\nendterrain\nFirstInit\n", sbt: false);
|
|
|
|
Assert.Equal((SceneryFaultKind.MissingTerrainFile, "Mapa.scn", 2, null, "teren/mapa.btf"),
|
|
Seen(Assert.Single(faults)));
|
|
}
|
|
|
|
[Fact]
|
|
public void A_terrain_file_takes_the_place_of_the_sbt_so_terrain_includes_are_read_after_all()
|
|
{
|
|
var faults = TerrainFaults("terrain teren/mapa.txtf endterrain\ninclude nmt/brak_ter.scm end\nFirstInit\n",
|
|
sbt: true, "teren/mapa.txtf");
|
|
|
|
Assert.Equal((SceneryFaultKind.MissingInclude, "Mapa.scn", 2, null, "nmt/brak_ter.scm"),
|
|
Seen(Assert.Single(faults)));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("include nmt/brak_ter.scm end\nterrain teren/mapa.txtf endterrain\nFirstInit\n", 2)]
|
|
[InlineData("node -1 0 t1 triangles trawa\n0 0 0 0 0 0 0 0 end\nendtri\nterrain teren/mapa.txtf endterrain\nFirstInit\n", 4)]
|
|
[InlineData("node -1 -1 t1 model 0 0 0 0 teren.t3d none endmodel\nterrain teren/mapa.txtf endterrain\nFirstInit\n", 2)]
|
|
[InlineData("FirstInit\nterrain teren/mapa.txtf endterrain\n", 2)]
|
|
public void A_terrain_file_behind_what_the_sbt_already_gave_is_passed_over(string scn, int line)
|
|
{
|
|
var faults = TerrainFaults(scn, sbt: true, "teren/mapa.txtf");
|
|
|
|
Assert.Equal((SceneryFaultKind.TerrainFileBehindSbt, "Mapa.scn", line, null, "teren/mapa.txtf"),
|
|
Seen(Assert.Single(faults)));
|
|
}
|
|
|
|
[Fact]
|
|
public void A_model_not_of_the_terrain_leaves_the_sbt_free_to_give_way() =>
|
|
Assert.Empty(TerrainFaults("node 500 0 m1 model 0 0 0 0 dom.t3d none endmodel\nterrain teren/mapa.txtf endterrain\nFirstInit\n",
|
|
sbt: true, "teren/mapa.txtf"));
|
|
}
|