mirror of
https://github.com/MaSzyna-EU07/StarterNG.git
synced 2026-11-13 20:03:19 +01:00
feat: check the terrain files a scenery's terrain directive names
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.
This commit is contained in:
@@ -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 SceneryFileLookup(_ => 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, 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)));
|
||||
}
|
||||
@@ -378,4 +378,58 @@ public class ScenerySyntaxTests
|
||||
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"));
|
||||
}
|
||||
|
||||
@@ -61,6 +61,19 @@ public enum SceneryFaultKind
|
||||
/// <summary>An include whose file is not there; <see cref="SceneryFault.Token"/> names it.</summary>
|
||||
MissingInclude,
|
||||
|
||||
/// <summary>
|
||||
/// A terrain file named by a terrain directive whose text (.txtf) and binary
|
||||
/// file (.btf) are both missing; <see cref="SceneryFault.Token"/> names it.
|
||||
/// </summary>
|
||||
MissingTerrainFile,
|
||||
|
||||
/// <summary>
|
||||
/// A terrain file the simulator passes over: the scenery's terrain comes from its
|
||||
/// .sbt, which something before the directive was already taken from;
|
||||
/// <see cref="SceneryFault.Token"/> names it.
|
||||
/// </summary>
|
||||
TerrainFileBehindSbt,
|
||||
|
||||
/// <summary>An include with no <c>end</c>, which swallows what follows it.</summary>
|
||||
IncludeNotClosed,
|
||||
|
||||
|
||||
@@ -5,6 +5,14 @@ using System.Linq;
|
||||
|
||||
namespace StarterNG.Domain.Sceneries;
|
||||
|
||||
/// <summary>What the simulator finds of the files a scenery names.</summary>
|
||||
/// <param name="Opens">Whether it opens a file of the scenery folder, named as the scenery names it.</param>
|
||||
/// <param name="HasSbt">
|
||||
/// Whether the scenery has its own .sbt: while that is in use, its terrain includes
|
||||
/// (<c>_ter.scm</c>) and terrain geometry are passed over, the .sbt holding them.
|
||||
/// </param>
|
||||
public sealed record SceneryFileLookup(Func<string, bool> Opens, bool HasSbt = false);
|
||||
|
||||
/// <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 +37,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
|
||||
/// leaves includes unchecked.
|
||||
/// <param name="files">
|
||||
/// What the simulator finds of the files the scenery names - includes and terrain
|
||||
/// files; null leaves them unchecked.
|
||||
/// </param>
|
||||
public static Result Scan(string text, string file, Func<string, bool>? opensInclude = null)
|
||||
public static Result Scan(string text, string file, SceneryFileLookup? files = 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, files, asSimulator: true);
|
||||
simulator.Run();
|
||||
var author = new Reading(tokens, lines, null, asSimulator: false);
|
||||
author.Run();
|
||||
@@ -110,8 +118,14 @@ 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, SceneryFileLookup? files, bool asSimulator)
|
||||
{
|
||||
// Whether the scenery's .sbt is in use, and whether the simulator passed over
|
||||
// something of the text on its account already - or got past FirstInit - after
|
||||
// which a terrain file can no longer take its place.
|
||||
private bool _sbt = files?.HasSbt ?? false;
|
||||
private bool _sbtTaken;
|
||||
|
||||
public readonly List<(int Start, int End, string Name)> Trainsets = new();
|
||||
public readonly List<(int, int)> Loose = new();
|
||||
public readonly List<SceneryFault> Faults = new();
|
||||
@@ -156,12 +170,20 @@ public static class ScenerySyntax
|
||||
{
|
||||
// Not a vehicle: only its kind is stepped over, what follows is its own.
|
||||
if (i + 4 >= tokens.Count || !tokens[i + 4].Is("dynamic"))
|
||||
{
|
||||
if (_sbt && TerrainGeometry(i))
|
||||
_sbtTaken = true;
|
||||
i += 4;
|
||||
}
|
||||
else
|
||||
i = Vehicle(i);
|
||||
}
|
||||
else if (token.Is("include"))
|
||||
i = Include(i);
|
||||
else if (token.Is("terrain") && asSimulator)
|
||||
i = Terrain(i);
|
||||
else if (token.Is("firstinit"))
|
||||
_sbtTaken |= _sbt;
|
||||
}
|
||||
|
||||
if (_open is { } last)
|
||||
@@ -369,8 +391,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 (files is not null && Plain(name))
|
||||
{
|
||||
if (_sbt && SimulatorNames.Lower(name).Contains("_ter.scm"))
|
||||
_sbtTaken = true;
|
||||
else if (!files.Opens(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.
|
||||
@@ -383,6 +410,52 @@ public static class ScenerySyntax
|
||||
return last;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A terrain directive: the files it names up to <c>endterrain</c>. Of a terrain
|
||||
/// file (.txtf, .btf) the simulator takes the text or the binary file of the same
|
||||
/// name, whichever is there; an .sbt it passes over quietly when it cannot use it.
|
||||
/// </summary>
|
||||
private int Terrain(int at)
|
||||
{
|
||||
int end = at + 1;
|
||||
while (end < tokens.Count && !tokens[end].Is("endterrain"))
|
||||
end++;
|
||||
|
||||
for (int named = at + 1; named < end && files is not null; named++)
|
||||
{
|
||||
string name = tokens[named].Text;
|
||||
string lower = SimulatorNames.Lower(name);
|
||||
if (!Plain(name) || !(lower.EndsWith(".txtf") || lower.EndsWith(".btf")))
|
||||
continue;
|
||||
|
||||
string stem = name[..name.LastIndexOf('.')];
|
||||
if (!files.Opens(stem + ".txtf") && !files.Opens(stem + ".btf"))
|
||||
Fault(SceneryFaultKind.MissingTerrainFile, tokens[named].Index, token: name);
|
||||
else if (_sbtTaken)
|
||||
Fault(SceneryFaultKind.TerrainFileBehindSbt, tokens[named].Index, token: name);
|
||||
else
|
||||
// The .sbt gives way: the terrain includes from here on are read after all.
|
||||
_sbt = false;
|
||||
}
|
||||
return Math.Min(end, tokens.Count - 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the node starting at the given word is terrain geometry, which an .sbt
|
||||
/// in use holds in its place: triangles or lines, or a model shown from any distance.
|
||||
/// </summary>
|
||||
private bool TerrainGeometry(int at)
|
||||
{
|
||||
if (at + 4 >= tokens.Count)
|
||||
return false;
|
||||
var kind = tokens[at + 4];
|
||||
if (kind.Is("model"))
|
||||
return double.TryParse(tokens[at + 2].Text, NumberStyles.Float, CultureInfo.InvariantCulture,
|
||||
out double nearest) && nearest < 0;
|
||||
return kind.Is("triangles") || kind.Is("triangle_strip") || kind.Is("triangle_fan") ||
|
||||
kind.Is("lines") || kind.Is("line_strip") || kind.Is("line_loop");
|
||||
}
|
||||
|
||||
// A vehicle with no trainset around it, written as within one - offset, crew,
|
||||
// coupling - where its track, offset and crew belong.
|
||||
private bool TrainsetStyle(int at) =>
|
||||
|
||||
@@ -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
|
||||
/// includes unchecked.
|
||||
/// <param name="files">
|
||||
/// What the simulator finds of the files the scenery names; null leaves them
|
||||
/// unchecked.
|
||||
/// </param>
|
||||
public Scenery Parse(string path, string content, Func<string, bool>? opensInclude = null)
|
||||
public Scenery Parse(string path, string content, SceneryFileLookup? files = 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), files);
|
||||
var faults = syntax.Faults.ToList();
|
||||
var inlined = SceneryIncludes.Ranges(content);
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StarterNG.Application.Abstractions;
|
||||
using StarterNG.Domain;
|
||||
using StarterNG.Domain.Sceneries;
|
||||
using StarterNG.Infrastructure.Adapters;
|
||||
|
||||
@@ -78,10 +77,9 @@ public sealed class SceneryRepository : ISceneryRepository
|
||||
var encodings = new Dictionary<string, Encoding>(StringComparer.Ordinal);
|
||||
|
||||
// With its own .sbt the scenery's terrain comes from it, and the simulator
|
||||
// passes its terrain includes over without opening them.
|
||||
// passes its terrain includes over without opening them - unless a terrain
|
||||
// file takes its place.
|
||||
bool binaryTerrain = Opens(Path.GetFileNameWithoutExtension(path) + ".sbt");
|
||||
bool OpensInclude(string name) =>
|
||||
Opens(name) || (binaryTerrain && SimulatorNames.Lower(name).Contains("_ter.scm"));
|
||||
|
||||
(string File, string Text)? ReadInclude(string name)
|
||||
{
|
||||
@@ -98,7 +96,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, new SceneryFileLookup(Opens, binaryTerrain));
|
||||
scenery.HasCompanionTimetable = binaryTerrain;
|
||||
scenery.Sources = sources;
|
||||
scenery.TextEncoding = ownEncoding;
|
||||
|
||||
@@ -742,6 +742,8 @@
|
||||
<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="SyntaxMissingTerrainFile">Soubor terénu {1} nenalezen, ani .txtf, ani .btf (terrain)</String>
|
||||
<String key="SyntaxTerrainFileBehindSbt">Soubor terénu {1} ignorován: terén už pochází ze souboru .sbt scenérie</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,8 @@
|
||||
<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="SyntaxMissingTerrainFile">Terrain file {1} not found, neither .txtf nor .btf (terrain)</String>
|
||||
<String key="SyntaxTerrainFileBehindSbt">Terrain file {1} ignored: the terrain already comes from the .sbt of the scenery</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,8 @@
|
||||
<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="SyntaxMissingTerrainFile">A(z) {1} terepfájl nem található, sem .txtf, sem .btf (terrain)</String>
|
||||
<String key="SyntaxTerrainFileBehindSbt">A(z) {1} terepfájl kimarad: a terep már a díszlet .sbt fájljából jön</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,8 @@
|
||||
<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="SyntaxMissingTerrainFile">Brak pliku terenu {1}, ani .txtf, ani .btf (terrain)</String>
|
||||
<String key="SyntaxTerrainFileBehindSbt">Plik terenu {1} pominięty: teren pochodzi już z pliku .sbt scenerii</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,8 @@
|
||||
<String key="SyntaxVehicleOutsideTrainset">Транспорт {0} стоит вне состава: нет заголовка trainset?</String>
|
||||
<String key="SyntaxNotANumber">{0}: «{1}» вместо числа</String>
|
||||
<String key="SyntaxMissingInclude">Файл {1} не найден (include)</String>
|
||||
<String key="SyntaxMissingTerrainFile">Файл рельефа {1} не найден, ни .txtf, ни .btf (terrain)</String>
|
||||
<String key="SyntaxTerrainFileBehindSbt">Файл рельефа {1} пропущен: рельеф уже берётся из файла .sbt сценария</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