Compare commits

..

1 Commits

Author SHA1 Message Date
maj00r
dee54a34bb 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.
2026-11-13 17:46:27 +01:00
10 changed files with 168 additions and 20 deletions

View File

@@ -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"));
}

View File

@@ -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,

View File

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

View File

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

View File

@@ -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;

View File

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

View File

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

View File

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

View File

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

View File

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