Play back the black box of the simulator: map, cab, timeline, timetable and a summary

The simulator writes a black box of each run to physicslog/*.blackbox
(blackbox_doc.md in the maszyna repository). Three new projects read and
show it:

- Eu07.BlackBox: the reader of the records, the recording as a whole
  (the user's vehicle and consist at a time, the timetable shown, the
  progress along it with arrival and departure delays, whether the
  train stood at the platform), the light patterns Pc 1, Pc 2, Pc 5 and
  Tb 1 with the ones the run asks for, and a summary of the driving.
- Eu07.BlackBox.Views: the player window, with the scenery map (tracks,
  signals, W4 stop points with their heading and W32, the train, the
  cameras), the cab, the timeline of speed against the AI's reference,
  the events, the timetable and the summary.
- Eu07.BlackBox.Viewer: the window on its own, with options for the
  time, following the train and screenshots.

The tapes window of the starter lists the black boxes of each session
beside the Hasler tapes and opens them in the player.
This commit is contained in:
maj00r
2026-11-11 00:39:35 +01:00
parent e7d8596652
commit e530b95352
40 changed files with 6953 additions and 20 deletions

View File

@@ -0,0 +1,368 @@
using System.Numerics;
using System.Text;
using Eu07.BlackBox;
namespace StarterNG.Tests.BlackBox;
/// <summary>
/// Writes a black box the way the simulator does (blackbox_doc.md of its repository),
/// field by field, so the reader is checked against the layout and not against itself.
/// </summary>
internal sealed class BlackBoxFile
{
private readonly MemoryStream _bytes = new();
private readonly BinaryWriter _file;
public BlackBoxFile(ushort version = 1, bool header = true)
{
_file = new BinaryWriter(_bytes, Encoding.UTF8, leaveOpen: true);
if (header)
{
_file.Write("EU07BBX\0"u8);
_file.Write(version);
_file.Write((ushort)0);
}
}
public byte[] Bytes => _bytes.ToArray();
public MemoryStream Stream() => new(Bytes);
/// <summary>A record of a type, its length and what the writer puts in it.</summary>
public BlackBoxFile Record(byte type, Action<BinaryWriter> payload)
{
using var body = new MemoryStream();
using (var w = new BinaryWriter(body, Encoding.UTF8, leaveOpen: true))
payload(w);
_file.Write(type);
_file.Write((uint)body.Length);
_file.Write(body.ToArray());
return this;
}
public BlackBoxFile Info(double secondOfDay = 10 * 3600 + 34 * 60, string scenery = "td.scn") =>
Record(0x01, w =>
{
w.Write(scenery);
w.Write("2026-11-08T22:56:30");
w.Write((ushort)2026);
w.Write((ushort)312);
w.Write(secondOfDay);
w.Write("ep07-bb");
w.Write("EU07 test");
w.Write(0.1f);
w.Write(1.0f);
w.Write(60.0f);
});
public BlackBoxFile Track(uint index, string name, params Vector3[] points) =>
Track(index, name, 0, 0, points);
public BlackBoxFile Track(uint index, string name, byte kind, byte branch, params Vector3[] points) =>
Record(0x10, w =>
{
w.Write(index);
w.Write(name);
w.Write(kind);
w.Write(branch);
w.Write((byte)1);
w.Write((uint)points.Length);
foreach (var p in points)
{
w.Write(p.X);
w.Write(p.Y);
w.Write(p.Z);
}
});
public BlackBoxFile Signal(uint index, string name, double x, double y, double z) =>
Record(0x11, w =>
{
w.Write(index);
w.Write(name);
w.Write(x);
w.Write(y);
w.Write(z);
});
public BlackBoxFile Vehicle(uint id, string name, string train = "", float length = 16.3f) =>
Record(0x12, w =>
{
w.Write(id);
w.Write(name);
w.Write("303e-ep-tv");
w.Write(train);
w.Write(length);
w.Write(2.9f);
w.Write((byte)1);
});
public BlackBoxFile Removed(double time, uint id) =>
Record(0x13, w =>
{
w.Write(time);
w.Write(id);
});
public BlackBoxFile Cab(double time, uint vehicle, double z, float speed, byte driving = 1, byte flags = 0x03,
float target = 40, float nextLimit = -1, float nextDistance = -1, float brakeCylinder = 0,
uint devices = 0x0007, bool extended = true, sbyte cabEnd = 1, byte security = 0,
float lateral = 0.2f, ushort lightsHead = 0x15, ushort lightsTail = 0x22, byte? movement = null,
short hintHead = -1, short hintTail = -1) =>
Record(0x20, w =>
{
w.Write(time);
w.Write(vehicle);
w.Write(5.0);
w.Write(0.4);
w.Write(z);
w.Write(speed);
w.Write(0.1f); // acceleration
w.Write(1.5f); // odometer
w.Write(driving);
w.Write(flags);
w.Write((sbyte)3); // main controller
w.Write((sbyte)0);
w.Write((sbyte)1);
w.Write(0.0f); // train brake
w.Write(0.0f);
w.Write(brakeCylinder);
w.Write(5.0f); // brake pipe
w.Write(320.0f); // current
w.Write(target);
w.Write(nextLimit);
w.Write(nextDistance);
w.Write(0.3f);
w.Write(150.0f);
w.Write(60.0f);
w.Write(-1.0f);
w.Write(devices);
w.Write(8.5f); // main reservoir
w.Write(3000.0f); // voltage
w.Write(1100.0f); // revolutions
w.Write(lightsHead); // lights at the head of the train
w.Write(lightsTail);
// The cab, the train protection and the lateral acceleration: a recording made before them ends above.
if (extended)
{
w.Write(cabEnd);
w.Write(security);
w.Write(lateral);
// what the driver goes by and the lights asked for: a recording made before them ends above
if (movement is { } kind)
{
w.Write(kind);
w.Write(hintHead);
w.Write(hintTail);
}
}
});
public BlackBoxFile Vehicles(double time, bool keyframe, params (uint Id, float X, float Z, float Speed)[] states) =>
Record(0x21, w =>
{
w.Write(time);
w.Write((byte)(keyframe ? 1 : 0));
w.Write((uint)states.Length);
foreach (var s in states)
{
w.Write(s.Id);
w.Write(s.X);
w.Write(0.4f);
w.Write(s.Z);
w.Write(0.0f); // heading
w.Write(s.Speed);
w.Write((byte)2);
}
});
public BlackBoxFile Switch(double time, uint track, byte state) =>
Record(0x22, w =>
{
w.Write(time);
w.Write(track);
w.Write(state);
});
public BlackBoxFile Aspect(double time, uint signal, float speed1, float speed2, string command = "SetVelocity") =>
Record(0x23, w =>
{
w.Write(time);
w.Write(signal);
w.Write(speed1);
w.Write(speed2);
w.Write(command);
});
public BlackBoxFile Command(double time, uint vehicle, bool pressed, string name) =>
Record(0x30, w =>
{
w.Write(time);
w.Write(vehicle);
w.Write((byte)(pressed ? 1 : 0));
w.Write(1.0);
w.Write(2.0);
w.Write(name);
});
public BlackBoxFile Driver(double time, uint vehicle, byte driving) =>
Record(0x31, w =>
{
w.Write(time);
w.Write(vehicle);
w.Write(driving);
});
public BlackBoxFile Passed(double time, uint vehicle, uint signal, string name, float speed1, float speed,
byte flags = 0, double z = 1300) =>
Record(0x32, w =>
{
w.Write(time);
w.Write(vehicle);
w.Write(signal);
w.Write(name);
w.Write(5.0);
w.Write(0.4);
w.Write(z);
w.Write(speed1);
w.Write(-1.0f);
w.Write(speed);
w.Write(2.5f);
w.Write(flags);
});
public BlackBoxFile User(double time, uint vehicle, bool inCab, sbyte? cabEnd = null) =>
Record(0x33, w =>
{
w.Write(time);
w.Write(vehicle);
w.Write((byte)(inCab ? 1 : 0));
if (cabEnd is { } end)
w.Write(end);
});
public BlackBoxFile StopPoint(uint index, string station, double z, float stopAt, float platform, float? heading = null) =>
Record(0x14, w =>
{
w.Write(index);
w.Write(station);
w.Write(5.0);
w.Write(0.4);
w.Write(z);
w.Write(stopAt);
w.Write(platform);
if (heading is { } way)
w.Write(way);
});
public BlackBoxFile HornPoint(uint index, string name, double z, float duration = 2, float tone = 1) =>
Record(0x15, w =>
{
w.Write(index);
w.Write(name);
w.Write(5.0);
w.Write(0.4);
w.Write(z);
w.Write(duration);
w.Write(tone);
});
public BlackBoxFile Timetable(double time, uint vehicle, string train, string from, string to,
params (string Station, float Arrival, float Departure)[] rows) =>
Record(0x24, w =>
{
w.Write(time);
w.Write(vehicle);
w.Write(train);
w.Write("ROJ");
w.Write(from);
w.Write(to);
w.Write(100.0f);
w.Write((uint)rows.Length);
float km = 0;
foreach (var row in rows)
{
w.Write(row.Station);
w.Write(km += 2.5f);
w.Write(100.0f);
w.Write(row.Arrival);
w.Write(row.Departure);
w.Write((byte)0);
}
});
public BlackBoxFile Station(double time, uint vehicle, byte kind, ushort row, string station, double z,
float distance = 1, float stopAt = 0, float trainLength = 87, float platform = 150,
float arrival = -1, float departure = -1) =>
Record(0x35, w =>
{
w.Write(time);
w.Write(vehicle);
w.Write(kind);
w.Write(row);
w.Write(station);
w.Write(5.0);
w.Write(0.4);
w.Write(z);
w.Write(distance);
w.Write(stopAt);
w.Write(trainLength);
w.Write(platform);
w.Write(arrival);
w.Write(departure);
});
public BlackBoxFile HornRequest(double time, uint vehicle, double z, float duration = 2) =>
Record(0x36, w =>
{
w.Write(time);
w.Write(vehicle);
w.Write(duration);
w.Write(1.0f);
w.Write(5.0);
w.Write(0.4);
w.Write(z);
});
public BlackBoxFile Camera(double time, byte mode, uint vehicle, double z, byte view = 0, float heading = 0.5f) =>
Record(0x25, w =>
{
w.Write(time);
w.Write(mode);
w.Write(view);
w.Write(vehicle);
w.Write(5.0);
w.Write(2.4);
w.Write(z);
w.Write(heading);
w.Write(-0.1f);
});
public BlackBoxFile Coupling(double time, uint vehicle, byte side, uint other, byte otherSide, byte flags,
byte previousFlags, uint previousOther) =>
Record(0x37, w =>
{
w.Write(time);
w.Write(vehicle);
w.Write(side);
w.Write(other);
w.Write(otherSide);
w.Write(flags);
w.Write(previousFlags);
w.Write(previousOther);
w.Write(5.0);
w.Write(0.4);
w.Write(1150.0);
});
public BlackBoxFile Clock(double time, double secondOfDay) =>
Record(0x34, w =>
{
w.Write(time);
w.Write((ushort)2026);
w.Write((ushort)312);
w.Write(secondOfDay);
});
public BlackBoxFile End(double time) => Record(0x3f, w => w.Write(time));
}

View File

@@ -0,0 +1,253 @@
using Eu07.BlackBox;
namespace StarterNG.Tests.BlackBox;
/// <summary>
/// A journey of two timetables one after another: the stations, the horn points, the train
/// protection, the camera and the couplings, as the simulator writes them, and what the
/// recording tells of them.
/// </summary>
public class BlackBoxJourneyTests
{
private const uint Head = 1, Car = 2, Tail = 3, Stranger = 9;
private const byte Coupled = 0x83;
/// <summary>
/// The user in vehicle 1 of a unit 1-2-3, the clock at 10:34 (634 min). ROJ1 stands at A, stops at B
/// at t = 60 until 70, passes C at 100 without stopping; ROJ2 from t = 110 stops at D at 115.
/// </summary>
private static Recording Journey()
{
var f = new BlackBoxFile().Info()
.StopPoint(0, "Stacja_B#1", 1300, 10, 150)
.StopPoint(1, "Stacja_B#2", 1310, 10, 150)
.HornPoint(0, "w6_1", 1200)
.User(0, Head, true, -1)
.Coupling(0, Head, 1, Car, 0, Coupled, 0, 0)
.Coupling(0, Car, 1, Tail, 0, Coupled, 0, 0)
.Camera(0, 0, Head, 1150)
.Timetable(0, Head, "ROJ1", "A", "C", ("A", -1, 634), ("Stacja_B", 635, 635.5f), ("C", 637.5f, 638))
.Station(1, Head, 4, 1, "A", 1150);
for (double t = 0; t <= 130; t += 0.5)
{
float speed = t is >= 55 and < 70 || t >= 112 && t < 120 ? 0 : 40;
uint devices = 0x0007 | (t is >= 22 and < 24 ? 0x0200u : 0);
byte security = (byte)(t is >= 30 and < 32 ? 0x11 : 0x01);
f.Cab(t, Head, 1150 + t * 10, speed, devices: devices, security: security, lateral: t is >= 45 and < 48 ? 1.2f : 0.1f);
}
f.HornRequest(20, Head, 1200)
.HornRequest(40, Head, 1550)
.Camera(50, 2, 0, 1650)
.Station(60, Head, 1, 2, "Stacja_B#2", 1300)
.Station(60, Stranger, 1, 2, "Stacja_B#1", 9000)
.Coupling(80, Car, 1, 0, 0, 0, Coupled, Tail)
.Station(100, Head, 3, 3, "C", 2150)
.Timetable(110, Head, "ROJ2", "C", "D", ("C", -1, 638), ("D", 640, -1))
.Station(115, Head, 1, 2, "D", 2300);
return BlackBoxReader.Read(f.End(130).Stream());
}
[Fact]
public void The_new_records_are_read_field_by_field()
{
var r = Journey();
Assert.Equal(0, r.Unknown);
Assert.Equal(0, r.Malformed);
Assert.Equal(2, r.StopPoints.Count);
Assert.Equal("Stacja_B#2", r.StopPoints[1].Station);
Assert.Equal(10f, r.StopPoints[1].StopAt);
Assert.Equal(150f, r.StopPoints[1].Platform);
Assert.Equal("w6_1", Assert.Single(r.HornPoints).Name);
Assert.Equal(2, r.Timetables.Count);
Assert.Equal("ROJ2", r.Timetables[1].Train);
Assert.Equal(640f, r.Timetables[1].Row(2)!.Arrival);
Assert.False(r.Timetables[1].Row(2)!.Maintenance);
Assert.Equal(5, r.Stations.Count);
Assert.Equal(StationKind.Missed, r.Stations.Single(s => s.Row == 3 && s.Vehicle == Head).Kind);
Assert.Equal(2, r.HornRequests.Count);
Assert.Equal(2, r.Cameras.Count);
Assert.Equal(CameraMode.Free, r.Cameras[1].Mode);
Assert.Equal(-0.1f, r.Cameras[1].Pitch);
Assert.Equal(3, r.Couplings.Count);
Assert.True(r.Couplings[2].Uncoupled);
Assert.Equal(-1, Assert.Single(r.UserVehicles).CabEnd);
var cab = r.Cab[0];
Assert.Equal(1, cab.CabEnd);
Assert.Equal(SecurityFlags.Vigilance, cab.Security);
Assert.Equal(0.1f, cab.LateralAcceleration);
Assert.Equal(Lights.HeadlightLeft | Lights.HeadlightUpper | Lights.HeadlightRight, cab.LightsHead);
}
[Fact]
public void A_cab_written_before_the_protection_and_the_lateral_acceleration_is_still_read()
{
var r = BlackBoxReader.Read(new BlackBoxFile().Info().Cab(0, Head, 1150, 40, extended: false).User(0, Head, true).End(1).Stream());
var cab = Assert.Single(r.Cab);
Assert.Equal(0, cab.CabEnd);
Assert.Equal(SecurityFlags.None, cab.Security);
Assert.Equal(0f, cab.LateralAcceleration);
Assert.Equal(0, Assert.Single(r.UserVehicles).CabEnd);
Assert.Equal(0, r.Malformed);
}
[Fact]
public void The_couplings_tell_the_train_at_any_moment()
{
var r = Journey();
Assert.Equal(new HashSet<int> { 1, 2, 3 }, r.ConsistAt((int)Head, 79));
Assert.Equal(new HashSet<int> { 1, 2 }, r.ConsistAt((int)Head, 81));
Assert.Equal(new HashSet<int> { 3 }, r.ConsistAt((int)Tail, 81));
// What the trains were made of at the start is no coupling done.
var change = Assert.Single(r.CouplingEvents());
Assert.Equal(80, change.Time);
Assert.Single(r.UserCouplings());
}
[Fact]
public void The_stations_of_someone_else_s_train_are_not_the_user_s()
{
var r = Journey();
Assert.DoesNotContain(r.UserStations(), s => s.Vehicle == Stranger);
Assert.Equal(4, r.UserStations().Count());
}
[Fact]
public void The_timetable_shown_is_the_one_the_train_had_then()
{
var r = Journey();
Assert.Equal("ROJ1", r.TimetableAt(100)!.Train);
Assert.Equal("ROJ2", r.TimetableAt(110)!.Train);
var first = StationProgress.Of(r, 50);
Assert.Equal(3, first.Count);
Assert.Equal(StationOutcome.Stopped, first[1].Outcome);
Assert.Equal(60, first[1].ArrivedAt);
Assert.Equal(70, first[1].DepartedAt);
Assert.Equal(0, first[1].ArrivalDelay!.Value, 2);
Assert.Equal(-1 / 3.0, first[1].DepartureDelay!.Value, 2);
Assert.Equal(StationOutcome.Missed, first[2].Outcome);
Assert.Equal(StationOutcome.Stopped, StationProgress.Of(r, 120)[1].Outcome);
}
[Fact]
public void The_delay_follows_the_train_from_station_to_station()
{
var r = Journey();
Assert.Null(StationProgress.DelayAt(r, 0.5));
var standing = StationProgress.DelayAt(r, 65)!;
Assert.True(standing.Standing);
Assert.Equal("Stacja_B", standing.Station);
Assert.Equal(0, standing.Minutes, 2);
var left = StationProgress.DelayAt(r, 80)!;
Assert.False(left.Standing);
Assert.Equal(-1 / 3.0, left.Minutes, 2);
// At D, arrived at 10:35:55 for 10:40: early, and nothing to depart by.
var second = StationProgress.DelayAt(r, 118)!;
Assert.Equal("ROJ2", second.Train);
Assert.True(second.Minutes < -4);
// Left B at 10:35:10, C planned 10:37:30: by 10:35:50 (t = 110 of ROJ1's time) not yet overdue.
Assert.Equal(-1 / 3.0, StationProgress.DelayAt(r, 90)!.Minutes, 2);
var delays = StationProgress.StationDelays(r, 80);
Assert.Equal(-1 / 3.0, delays["Stacja B"], 2);
}
[Fact]
public void The_names_of_the_stations_are_shown_without_what_tells_their_stop_points_apart()
{
Assert.Equal("Bałtyk Główny", StationNames.Display("Bałtyk_Główny#2"));
Assert.Equal("Turów", StationNames.Display("Turów"));
Assert.Equal("Psie Pole po", StationNames.Display("Psie_Pole_po#"));
}
[Fact]
public void The_horn_counts_when_it_sounded_after_the_horn_point()
{
var checks = Journey().UserHornChecks().ToList();
Assert.Equal(2, checks.Count);
Assert.True(checks[0].Sounded);
Assert.False(checks[1].Sounded);
}
[Fact]
public void The_camera_is_known_from_its_first_record()
{
var r = Journey();
Assert.Null(r.CameraAt(-1));
Assert.Equal(CameraMode.Cab, r.CameraAt(49)!.Value.Mode);
Assert.Equal(CameraMode.Free, r.CameraAt(51)!.Value.Mode);
}
[Fact]
public void The_summary_counts_the_stops_of_every_timetable_the_horns_and_the_protection()
{
var summary = Journey().Summarize();
Assert.Equal(2, summary.Stops);
Assert.Equal(1, summary.StopsMissed);
Assert.Equal(2, summary.HornPoints);
Assert.Equal(1, summary.HornsMissed);
Assert.Equal(1, summary.SecurityBrakings);
// Taken over half a second, as the lateral acceleration jumps from sample to sample.
Assert.Equal(1.2f, summary.MaxLateralAcceleration, 3);
}
}
public class TrainDelayTests
{
[Fact]
public void A_train_held_on_the_way_grows_late_by_the_time_of_the_next_station()
{
// Clock 10:34 at t = 0. Departs A at 10:35 (t = 60) on time, held short of B planned at 10:37.
var f = new BlackBoxFile().Info()
.Timetable(0, 1, "ROJ9", "A", "B", ("A", -1, 635), ("B", 637, 638))
.Station(1, 1, 1, 1, "A", 1150);
for (double t = 0; t <= 400; t += 0.5)
f.Cab(t, 1, 1150 + Math.Max(0, Math.Min(t, 90) - 60) * 10, t is > 60 and < 90 ? 40 : 0);
var r = BlackBoxReader.Read(f.End(400).Stream());
Assert.Equal(0, StationProgress.DelayAt(r, 100)!.Minutes, 1);
// 10:40 at t = 360: three minutes past B's arrival, and not there.
var held = StationProgress.DelayAt(r, 360)!;
Assert.False(held.Standing);
Assert.Equal(3, held.Minutes, 2);
}
}
/// <summary>A train given its next timetable, one recording holding both.</summary>
public class NextTimetableTests
{
[Fact]
public void A_station_written_just_before_the_next_timetable_goes_with_it()
{
// ROJ1 stops at A and ends at B; at B it is given ROJ2, which starts there. A recording made before
// the simulator wrote the timetable first has B of ROJ2 half a second ahead of ROJ2 itself (here left behind,
// which on ROJ1 would mark A missed).
var f = new BlackBoxFile().Info()
.Timetable(0, 1, "ROJ1", "A", "B", ("A", -1, 635), ("B", 637, -1))
.Station(1, 1, 1, 1, "A", 1150)
.Station(50, 1, 1, 2, "B", 1150)
.Station(99.5, 1, 3, 1, "B", 1150)
.Timetable(100, 1, "ROJ2", "B", "C", ("B", -1, 640), ("C", 645, -1));
for (double t = 0; t <= 120; t += 0.5)
f.Cab(t, 1, 1150, 0);
var r = BlackBoxReader.Read(f.End(120).Stream());
var first = StationProgress.Of(r, r.Timetables[0]);
Assert.Equal(StationOutcome.Stopped, first[0].Outcome);
Assert.Equal(1, first[0].Event!.Time);
var second = StationProgress.Of(r, r.Timetables[1]);
Assert.Equal(StationOutcome.Missed, second[0].Outcome);
Assert.Equal(99.5, second[0].Event!.Time);
}
}

View File

@@ -0,0 +1,96 @@
using StarterNG.Domain.Tapes;
using StarterNG.Tests.Fakes;
namespace StarterNG.Tests.BlackBox;
public class BlackBoxListingTests
{
private static readonly DateTime Launched = new(2025, 12, 31, 23, 0, 0, DateTimeKind.Utc);
private static string Log(string name) => TestInstallation.At("physicslog", name);
[Fact]
public void Each_run_keeps_its_own_black_box_next_to_its_tapes()
{
var files = new InMemoryFileSystem()
.WithFile(Log("ra.dat"), "t")
.WithFile(Log("td_2025-12-31_23-00-05.blackbox"), "b")
.WithFile(Log("td_2026-01-01_00-00-05.blackbox"), "b");
var installation = new TestInstallation(files);
var tapes = installation.Tapes;
installation.Clock.Now = Launched;
tapes.RecordLaunch("td.scn", TimeSpan.Zero, Array.Empty<TapeVehicle>(), "ra", new[] { "ra" }, "A");
installation.Clock.Now = Launched.AddHours(1);
tapes.RecordLaunch("td.scn", TimeSpan.Zero, Array.Empty<TapeVehicle>(), "ra", new[] { "ra" }, "B");
files.WrittenAt(Log("td_2025-12-31_23-00-05.blackbox"), Launched.AddMinutes(30))
.WrittenAt(Log("td_2026-01-01_00-00-05.blackbox"), Launched.AddMinutes(90))
.WrittenAt(Log("ra.dat"), Launched.AddMinutes(90));
var sessions = tapes.Sessions();
Assert.Equal(new[] { "B", "A" }, sessions.Select(session => session.Launch!.SceneryName));
Assert.EndsWith("00-00-05.blackbox", Assert.Single(sessions[0].BlackBoxes).Path);
Assert.Single(sessions[0].Tapes);
// The tape of the first run was written over by the second; its black box was not.
Assert.EndsWith("23-00-05.blackbox", Assert.Single(sessions[1].BlackBoxes).Path);
Assert.Empty(sessions[1].Tapes);
Assert.Equal(Launched.AddMinutes(30), sessions[1].LastWrittenUtc);
}
[Fact]
public void A_black_box_is_not_taken_for_a_tape()
{
var installation = new TestInstallation(new InMemoryFileSystem()
.WithFile(Log("ra.dat"), "t")
.WithFile(Log("td_2026-03-01_12-00-00.blackbox"), "b"));
var session = Assert.Single(installation.Tapes.Sessions());
Assert.Equal("ra", Assert.Single(session.Tapes).Vehicle);
Assert.Single(session.BlackBoxes);
Assert.Equal("ra", Assert.Single(installation.Tapes.List()).Vehicle);
}
[Fact]
public void Black_boxes_with_no_launch_noted_go_with_the_tapes_written_about_the_same_time()
{
var early = new DateTime(2026, 3, 1, 12, 0, 0, DateTimeKind.Utc);
var installation = new TestInstallation(new InMemoryFileSystem()
.WithFile(Log("a.dat"), "t")
.WithFile(Log("td_2026-03-01_11-00-00.blackbox"), "b")
.WithFile(Log("td_2026-03-01_12-30-00.blackbox"), "b")
.WrittenAt(Log("a.dat"), early)
.WrittenAt(Log("td_2026-03-01_11-00-00.blackbox"), early.AddSeconds(2))
.WrittenAt(Log("td_2026-03-01_12-30-00.blackbox"), early.AddHours(1)));
var sessions = installation.Tapes.Sessions();
Assert.Equal(2, sessions.Count);
Assert.All(sessions, session => Assert.Null(session.Launch));
// Newest first: the black box on its own, then the run with the tape.
Assert.Empty(sessions[0].Tapes);
Assert.EndsWith("12-30-00.blackbox", Assert.Single(sessions[0].BlackBoxes).Path);
Assert.Single(sessions[1].Tapes);
Assert.EndsWith("11-00-00.blackbox", Assert.Single(sessions[1].BlackBoxes).Path);
Assert.Equal(early, sessions[1].StartedUtc);
}
[Fact]
public void The_black_box_of_a_running_session_is_listed_while_it_is_written()
{
var files = new InMemoryFileSystem();
var installation = new TestInstallation(files);
installation.Clock.Now = Launched;
var tapes = installation.Tapes;
tapes.RecordLaunch("td.scn", TimeSpan.Zero, Array.Empty<TapeVehicle>(), "ra");
files.WithFile(Log("td_2025-12-31_23-00-20.blackbox"), "b")
.WrittenAt(Log("td_2025-12-31_23-00-20.blackbox"), Launched.AddMinutes(5));
installation.SimulatorRunning = true;
var running = Assert.Single(tapes.Sessions());
Assert.True(running.Running);
Assert.Empty(running.Tapes);
Assert.Single(running.BlackBoxes);
}
}

View File

@@ -0,0 +1,229 @@
using System.Numerics;
using Eu07.BlackBox;
namespace StarterNG.Tests.BlackBox;
public class BlackBoxReaderTests
{
private static BlackBoxFile Session() => new BlackBoxFile()
.Info()
.Track(0, "start", new Vector3(5, 0.4f, 1100), new Vector3(5, 0.4f, 1200))
.Track(1, "zwr01", 1, 0, new Vector3(5, 0.4f, 1200), new Vector3(5, 0.4f, 1234))
.Track(1, "zwr01", 1, 1, new Vector3(5, 0.4f, 1200), new Vector3(8, 0.4f, 1234))
.Signal(0, "stacja1_a", 2.5, 3.0, 1300)
.Vehicle(1, "ep07-bb", "ROJ65113")
.Vehicle(2, "bdnu")
.Switch(0, 1, 0)
.Aspect(0, 0, 0, -1)
.Clock(0, 10 * 3600 + 34 * 60)
.User(0, 1, true)
.Vehicles(0, true, (1, 5, 1150, 0), (2, 5, 1130, 0))
.Cab(0.1, 1, 1150, 0)
.Command(1.0, 1, true, "aidriverenable")
.Command(1.1, 1, false, "aidriverenable")
.Driver(1.0, 1, 2)
.Aspect(2.0, 0, 40, -1)
.Switch(3.0, 1, 1)
.Passed(4.0, 1, 0, "stacja1_a_sem", 40, 38.5f, 0x01)
.Removed(5.0, 2)
.End(6.0);
[Fact]
public void Every_record_of_a_session_is_read_as_the_simulator_wrote_it()
{
var r = BlackBoxReader.Read(Session().Stream());
Assert.Equal(1, r.Version);
Assert.Equal("td.scn", r.Info!.Scenery);
Assert.Equal(new TimeSpan(10, 34, 0), r.Info.StartClock);
Assert.Equal(60f, r.Info.KeyframeInterval);
Assert.Equal(3, r.Tracks.Count);
Assert.Equal(2, r.TrackWays(1).Count);
Assert.Equal(TrackKind.SwitchBranch, r.TrackWays(1)[1].Kind);
Assert.Equal(new Vector3(8, 0.4f, 1234), r.TrackWays(1)[1].Points[1]);
Assert.Equal("stacja1_a", r.Signal(0)!.Name);
Assert.Equal(1300, r.Signal(0)!.Position.Z);
Assert.Equal("ROJ65113", r.Vehicle(1)!.Train);
Assert.Equal(5.0, r.Vehicle(2)!.RemovedAt);
var cab = Assert.Single(r.Cab);
Assert.Equal(1, cab.Vehicle);
Assert.Equal(new Position(5, 0.4, 1150), cab.Position);
Assert.Equal(Driver.Human, cab.Driving);
Assert.True(cab.AiValid);
Assert.Equal(3, cab.MainController);
Assert.Equal(5.0f, cab.BrakePipe);
Assert.Equal(40f, cab.Ai.TargetKmh);
Assert.Equal(60f, cab.Ai.TimetableVmaxKmh);
Assert.False(cab.Ai.HasNextLimit);
Assert.Equal(CabDevices.Battery | CabDevices.MainBreaker | CabDevices.PantographFront, cab.Devices);
Assert.True(cab.Has(CabDevices.MainBreaker));
Assert.False(cab.Has(CabDevices.Compressor));
Assert.Equal(8.5f, cab.MainReservoir);
Assert.Equal(3000f, cab.Voltage);
Assert.Equal(1100f, cab.EngineRpm);
Assert.Equal(Lights.HeadlightLeft | Lights.HeadlightUpper | Lights.HeadlightRight, cab.LightsHead);
Assert.Equal(Lights.RedMarkerLeft | Lights.RedMarkerRight, cab.LightsTail);
Assert.Equal(2, r.Commands.Count);
Assert.Equal("aidriverenable", r.Commands[0].Name);
Assert.True(r.Commands[0].Pressed);
Assert.Equal(2.0, r.Commands[0].Param2);
Assert.Equal(Driver.Ai, Assert.Single(r.DriverChanges).Driving);
var passed = Assert.Single(r.Passings);
Assert.Equal("stacja1_a_sem", passed.Event);
Assert.Equal(PassingFlags.Shunt, passed.Flags);
Assert.Equal(38.5f, passed.SpeedKmh);
Assert.False(passed.AtDanger);
Assert.True(r.Complete);
Assert.Equal(6.0, r.Duration);
Assert.Equal(0, r.Unknown);
Assert.Equal(0, r.Malformed);
}
[Fact]
public void The_switches_and_the_aspects_answer_for_any_moment()
{
var r = BlackBoxReader.Read(Session().Stream());
Assert.Equal(0, r.SwitchAt(1, 2.9));
Assert.Equal(1, r.SwitchAt(1, 3.0));
Assert.Equal(-1, r.SwitchAt(7, 3.0));
Assert.Equal(0f, r.AspectAt(0, 1.9)!.Value.Speed1);
Assert.Equal(40f, r.AspectAt(0, 2.0)!.Value.Speed1);
Assert.Null(r.AspectAt(3, 2.0));
}
[Fact]
public void A_file_still_written_is_read_on_from_where_the_last_read_stopped()
{
var bytes = Session().Bytes;
var whole = BlackBoxReader.Read(new MemoryStream(bytes));
// Cut at every byte: a record cut short waits for the rest, nothing is read twice.
for (int cut = 0; cut <= bytes.Length; cut++)
{
var reader = new BlackBoxReader();
reader.ReadMore(new MemoryStream(bytes, 0, cut));
Assert.False(reader.Recording.Complete && cut < bytes.Length);
reader.ReadMore(new MemoryStream(bytes));
Assert.Equal(whole.Tracks.Count, reader.Recording.Tracks.Count);
Assert.Equal(whole.Commands.Count, reader.Recording.Commands.Count);
Assert.Equal(whole.Cab.Count, reader.Recording.Cab.Count);
Assert.Equal(whole.Aspects.Count, reader.Recording.Aspects.Count);
Assert.True(reader.Recording.Complete);
Assert.Equal(bytes.Length, reader.Offset);
}
}
[Fact]
public void A_record_of_a_later_version_is_passed_over_by_its_length()
{
var file = new BlackBoxFile().Info()
.Record(0x7e, w => w.Write(new byte[40]))
.Command(1.0, 1, true, "hornhighactivate")
.End(2.0);
var r = BlackBoxReader.Read(file.Stream());
Assert.Equal(1, r.Unknown);
Assert.Equal("hornhighactivate", Assert.Single(r.Commands).Name);
Assert.True(r.Complete);
}
[Fact]
public void A_record_shorter_than_its_type_is_passed_over_and_counted()
{
var file = new BlackBoxFile().Record(0x20, w => w.Write(1.0)).Clock(1.0, 3600).End(2.0);
var r = BlackBoxReader.Read(file.Stream());
Assert.Empty(r.Cab);
Assert.Equal(1, r.Malformed);
Assert.Single(r.Clocks);
}
[Fact]
public void Texts_longer_than_a_byte_can_count_and_letters_beyond_ascii_read_back()
{
string name = "żółw_" + new string('x', 300);
var r = BlackBoxReader.Read(new BlackBoxFile().Signal(0, name, 0, 0, 0).Signal(1, "", 1, 1, 1).Stream());
Assert.Equal(name, r.Signal(0)!.Name);
Assert.Equal("", r.Signal(1)!.Name);
}
[Fact]
public void Something_else_than_a_black_box_or_a_version_not_known_is_refused()
{
Assert.Throws<InvalidDataException>(() => BlackBoxReader.Read(new MemoryStream("Time[s] Velocity[m/s]\r\n"u8.ToArray())));
Assert.Throws<InvalidDataException>(() => BlackBoxReader.Read(new BlackBoxFile(version: 2).Stream()));
}
[Fact]
public void A_file_written_anew_under_the_reader_is_told_so()
{
var reader = new BlackBoxReader();
reader.ReadMore(Session().Stream());
Assert.Throws<InvalidDataException>(() => reader.ReadMore(new BlackBoxFile().Info().Stream()));
}
[Fact]
public void A_vehicle_listed_only_when_it_moved_stands_until_the_look_before_it_was_seen_moving()
{
var r = BlackBoxReader.Read(new BlackBoxFile()
.Info()
.Vehicle(2, "et22")
.Vehicles(0, true, (2, 0, 0, 0))
.Vehicles(60, true, (2, 0, 0, 0))
.Vehicles(90, false, (2, 0, 10, 36))
.Vehicles(91, false, (2, 0, 20, 36))
.Stream());
Assert.Equal(0f, r.VehiclesAt(30).Single().Position.Z);
// Not listed from 60 to 90: it stood until 88.5, the look interval and a half before.
Assert.Equal(0f, r.VehiclesAt(88).Single().Position.Z);
Assert.InRange(r.VehiclesAt(89.25).Single().Position.Z, 4.9f, 5.1f);
Assert.InRange(r.VehiclesAt(90.5).Single().Position.Z, 14.9f, 15.1f);
Assert.Equal(20f, r.VehiclesAt(200).Single().Position.Z);
}
[Fact]
public void A_vehicle_removed_is_on_the_map_no_more()
{
var r = BlackBoxReader.Read(Session().Stream());
Assert.Equal(2, r.VehiclesAt(4.9).Count);
Assert.Equal(1, Assert.Single(r.VehiclesAt(5.0)).Id);
}
[Fact]
public void The_clock_goes_on_from_the_last_tick_and_over_midnight()
{
var r = BlackBoxReader.Read(new BlackBoxFile()
.Info(secondOfDay: 86390)
.Clock(0, 86390)
.Clock(1, 86391)
.Stream());
Assert.Equal(new TimeSpan(23, 59, 51), r.ClockAt(1));
Assert.Equal(86395.5, r.ClockAt(5.5).TotalSeconds, 3);
Assert.Equal(1.0, r.ClockAt(11).TotalSeconds, 3);
}
[Fact]
public void The_position_of_the_train_runs_between_its_samples_and_not_across_a_gap()
{
var r = BlackBoxReader.Read(new BlackBoxFile()
.Cab(0.0, 1, 100, 36)
.Cab(0.1, 1, 101, 36)
.Cab(5.0, 1, 200, 36)
.Stream());
Assert.Equal(100.5, r.CabPositionAt(0.05)!.Value.Z, 6);
Assert.Null(r.CabPositionAt(3.0));
Assert.Null(r.CabPositionAt(-1));
}
}

View File

@@ -0,0 +1,125 @@
using System.Linq;
using Eu07.BlackBox;
namespace StarterNG.Tests.BlackBox;
/// <summary>The light signals at the head and the tail of the user's train, against those it was to show.</summary>
public class HeadCodeTests
{
[Theory]
[InlineData(Lights.HeadlightLeft | Lights.HeadlightRight | Lights.HeadlightUpper, HeadCode.Pc1)]
[InlineData(Lights.HeadlightLeft | Lights.HeadlightRight | Lights.HeadlightUpper | Lights.HighBeamLeft | Lights.HighBeamRight, HeadCode.Pc1)]
[InlineData(Lights.RedMarkerLeft | Lights.HeadlightRight | Lights.HeadlightUpper, HeadCode.Pc2)]
[InlineData(Lights.RedMarkerLeft | Lights.RedMarkerRight, HeadCode.Pc5)]
[InlineData(Lights.RearEndSignals, HeadCode.Pc5)]
[InlineData(Lights.HeadlightRight, HeadCode.Tb1)]
[InlineData(Lights.HeadlightLeft, HeadCode.Tb1)]
[InlineData(Lights.None, HeadCode.Off)]
[InlineData(Lights.HeadlightLeft | Lights.HeadlightRight, HeadCode.Other)]
public void The_lamps_lit_tell_the_signal(Lights lights, HeadCode code) => Assert.Equal(code, HeadCodes.Of(lights));
private const ushort Pc1 = 0x15, Pc5 = 0x22, Tb1 = 0x10;
private static Recording Drive(params (double From, double To, ushort Head, ushort Tail, byte Movement, short HintHead)[] stretches)
{
var f = new BlackBoxFile().Info();
foreach (var (from, to, head, tail, movement, hint) in stretches)
for (double t = from; t < to; t += 0.5)
f.Cab(t, 1, 1000 + t * 10, 40, lightsHead: head, lightsTail: tail, movement: movement, hintHead: hint);
return BlackBoxReader.Read(f.End(stretches[^1].To).Stream());
}
[Fact]
public void A_train_run_is_to_show_Pc1_and_Pc5()
{
var summary = Drive((0, 60, Pc1, Pc5, 1, -1)).Summarize();
Assert.True(summary.LightsJudgedSeconds > 55);
Assert.Equal(0, summary.WrongLightsSeconds);
Assert.Empty(summary.LightsFaults);
}
[Fact]
public void Shunting_lights_on_a_train_run_count_as_wrong_once_for_the_whole_stretch()
{
var summary = Drive((0, 30, Pc1, Pc5, 1, -1), (30, 60, Tb1, Tb1, 1, -1), (60, 90, Pc1, Pc5, 1, -1)).Summarize();
Assert.Equal(30, summary.WrongLightsSeconds, 0);
var fault = Assert.Single(summary.LightsFaults);
Assert.Equal(HeadCode.Tb1, fault.Head);
Assert.Equal(HeadCode.Pc1, fault.HeadRequired);
Assert.True(fault.HeadWrong && fault.TailWrong);
}
[Fact]
public void Shunting_is_to_show_Tb1()
{
var summary = Drive((0, 30, Tb1, Tb1, 2, -1), (30, 40, Pc1, Pc5, 2, -1)).Summarize();
var fault = Assert.Single(summary.LightsFaults);
Assert.Equal(HeadCode.Tb1, fault.HeadRequired);
}
[Fact]
public void The_pattern_the_timetable_asks_for_is_the_one_to_show()
{
// Pc 2 asked for at the head: the left red marker, the right headlight and the upper one.
const short pc2 = 0x16;
var summary = Drive((0, 30, Pc1, Pc5, 1, pc2)).Summarize();
var fault = Assert.Single(summary.LightsFaults);
Assert.Equal(HeadCode.Pc2, fault.HeadRequired);
Assert.False(fault.TailWrong);
}
[Fact]
public void An_older_recording_tells_nothing_of_the_signals()
{
var f = new BlackBoxFile().Info();
for (double t = 0; t < 30; t += 0.5)
f.Cab(t, 1, 1000 + t * 10, 40, lightsHead: Tb1);
var summary = BlackBoxReader.Read(f.End(30).Stream()).Summarize();
Assert.Equal(0, summary.LightsJudgedSeconds);
}
}
/// <summary>The user going over to another vehicle: the timetable of the train is the one of the train the user is in.</summary>
public class UserVehicleTests
{
private const byte Coupled = 0x83;
[Fact]
public void Another_vehicle_of_the_same_train_keeps_its_timetable_another_train_has_none()
{
// The user in vehicle 1 of the unit 1-2 with ROJ1; at t = 50 over to the other cab, at t = 100 into vehicle 9, on its own.
var f = new BlackBoxFile().Info()
.Coupling(0, 1, 1, 2, 0, Coupled, 0, 0)
.Timetable(0, 1, "ROJ1", "A", "B", ("A", -1, 635), ("B", 640, -1));
for (double t = 0; t < 150; t += 0.5)
f.Cab(t, t < 50 ? 1u : t < 100 ? 2u : 9u, 1000, 0);
var r = BlackBoxReader.Read(f.End(150).Stream());
Assert.Equal("ROJ1", r.TimetableAt(20)!.Train);
Assert.Equal("ROJ1", r.TimetableAt(70)!.Train);
Assert.Null(r.TimetableAt(120));
}
}
/// <summary>The departures and the platforms in the summary.</summary>
public class StationSummaryTests
{
[Fact]
public void The_summary_tells_the_largest_delay_leaving_and_the_stops_off_the_platform()
{
// Clock 10:34 at t = 0, leaves A at once. Stops at B (10:36 to 10:37) from t = 120, leaves at t = 240 (10:38): a minute late.
var f = new BlackBoxFile().Info()
.Timetable(0, 1, "ROJ1", "A", "C", ("A", -1, 634), ("B", 636, 637), ("C", 640, -1))
.Station(1, 1, 1, 1, "A", 1000)
.Station(120, 1, 1, 2, "Stacja_B", 1000, distance: 74.7f, trainLength: 129.8f, platform: 201);
for (double t = 0; t < 300; t += 0.5)
f.Cab(t, 1, 1000, t is >= 120 and < 240 or < 2 ? 0 : 40);
var r = BlackBoxReader.Read(f.End(300).Stream());
var summary = r.Summarize();
Assert.Equal(1, summary.MaxDepartureDelayMinutes!.Value, 1);
var off = Assert.Single(summary.OffPlatform);
Assert.Equal("B", off.Station);
Assert.Equal(3.5, off.Metres, 1);
}
}

View File

@@ -0,0 +1,98 @@
using Eu07.BlackBox;
namespace StarterNG.Tests.BlackBox;
public class DrivingSummaryTests
{
private static Recording Drive(Func<BlackBoxFile, BlackBoxFile> more)
{
var file = new BlackBoxFile().Info();
return BlackBoxReader.Read(more(file).Stream());
}
/// <summary>Ten samples a second from t, at a speed against an AI holding target.</summary>
private static BlackBoxFile Stretch(BlackBoxFile file, double from, double seconds, float speed, float target,
byte driving = 1, byte flags = 0x03)
{
for (int i = 0; i < seconds * 10; i++)
file.Cab(from + i * 0.1, 1, 1000 + (from + i * 0.1) * speed / 3.6, speed, driving, flags, target);
return file;
}
[Theory]
[InlineData(40f, 40f, SpeedJudgement.Within)]
[InlineData(40.4f, 40f, SpeedJudgement.Within)]
[InlineData(44f, 40f, SpeedJudgement.Slightly)]
[InlineData(46f, 40f, SpeedJudgement.Over)]
[InlineData(-46f, 40f, SpeedJudgement.Over)]
public void A_sample_is_judged_by_how_much_faster_than_the_AI_it_went(float speed, float target, SpeedJudgement expected)
{
var r = Drive(f => f.Cab(0, 1, 0, speed, target: target));
Assert.Equal(expected, DrivingSummary.Judge(r.Cab[0]));
}
[Fact]
public void Without_the_AI_worked_out_or_with_the_AI_driving_there_is_nothing_to_judge()
{
var r = Drive(f => f.Cab(0, 1, 0, 80, flags: 0x01, target: 40).Cab(0.1, 1, 0, 80, driving: 2, target: 40));
Assert.Equal(SpeedJudgement.NoData, DrivingSummary.Judge(r.Cab[0]));
Assert.Equal(SpeedJudgement.Autopilot, DrivingSummary.Judge(r.Cab[1]));
}
[Fact]
public void The_summary_counts_the_time_over_the_AI_the_autopilot_and_the_signals_passed_at_danger()
{
var r = Drive(f =>
{
Stretch(f, 0, 10, 40, 40);
Stretch(f, 10, 10, 50, 40);
Stretch(f, 20, 10, 43, 40);
Stretch(f, 30, 10, 60, 60, driving: 2);
f.Passed(15, 1, 0, "s1", 0, 50, 0x02);
f.Passed(25, 1, 1, "s2", 40, 43);
// Another train, on a track of its own far away.
f.Passed(25, 7, 2, "someone else's", 0, 20, 0x02, z: 9000);
f.Command(5, 1, true, "mastercontrollerincrease");
f.Command(5.1, 1, false, "mastercontrollerincrease");
f.Command(6, 0, true, "timejump");
return f.Driver(30, 1, 2);
});
var summary = r.Summarize();
Assert.Equal(20, summary.OverAiSeconds, 0);
Assert.Equal(10, summary.WellOverAiSeconds, 0);
Assert.Equal(10, summary.MaxOverAiKmh, 3);
Assert.InRange(summary.MaxOverAiAt, 10, 20);
Assert.Equal(10, summary.AutopilotSeconds, 0);
Assert.Equal(30, summary.DrivenSeconds, 0);
Assert.Equal(60, summary.MaxSpeedKmh);
Assert.Equal(2, summary.SignalsPassed);
Assert.Equal("s1", Assert.Single(summary.PassedAtDanger).Event);
Assert.Equal(1, summary.Commands);
Assert.Equal(1, summary.AutopilotSwitches);
// 10 s at 40, 50, 43 and 60 km/h.
Assert.InRange(summary.DistanceKm, 0.530, 0.540);
}
[Fact]
public void The_autopilot_is_told_as_spans_of_time()
{
var r = Drive(f =>
{
Stretch(f, 0, 5, 40, 40);
Stretch(f, 5, 5, 40, 40, driving: 2);
Stretch(f, 10, 5, 40, 40);
return Stretch(f, 15, 2, 40, 40, driving: 2);
});
var spans = r.AutopilotSpans();
Assert.Equal(2, spans.Count);
Assert.Equal(5, spans[0].Start, 3);
Assert.Equal(10, spans[0].End, 3);
Assert.Equal(15, spans[1].Start, 3);
}
}

View File

@@ -0,0 +1,81 @@
using System.IO;
using Eu07.BlackBox;
namespace StarterNG.Tests.BlackBox;
/// <summary>The second value of a stop point as the driver reads it: all of it the metres, the last digit also the side.</summary>
public class PlatformTests
{
[Theory]
[InlineData(201f, 201, 1)]
[InlineData(242f, 242, 2)]
[InlineData(553f, 553, 3)]
[InlineData(150f, 150, 0)]
[InlineData(-151f, 151, 1)]
public void The_whole_value_is_metres_and_the_last_digit_the_side(float value, double length, int side)
{
Assert.Equal(length, Platforms.Length(value));
Assert.Equal(side, Platforms.Side(value));
}
private static StationEvent Stop(float distance, float length, float platform) =>
new(0, 1, StationKind.Stopped, 1, "Swoszowice", new Position(0, 0, 0), distance, 63.2f, length, platform, 600, 601);
[Fact]
public void A_train_whose_tail_reaches_past_the_platform_of_201_is_off_it()
{
// Two EN57 at Swoszowice: the head 74.7 m before the W4, the tail 204.5 m, the platform 201 m.
var stop = Stop(74.7f, 129.8f, 201);
Assert.False(stop.OnPlatform);
Assert.Equal(3.5, stop.OffPlatform, 1);
Assert.Equal(201, stop.PlatformLength);
}
[Fact]
public void A_train_within_the_platform_is_along_it()
{
var stop = Stop(63.2f, 129.8f, 201);
Assert.True(stop.OnPlatform);
Assert.Equal(0, stop.OffPlatform);
}
}
/// <summary>The way a stop point faces and the W32, as the simulator writes them.</summary>
public class StopPointRecordTests
{
[Fact]
public void A_stop_point_tells_the_way_of_the_trains_it_is_for()
{
var recording = BlackBoxReader.Read(new MemoryStream(new BlackBoxFile().Info().StopPoint(0, "Swoszowice", 100, 201, 201, 1.25f).End(10).Bytes));
var point = Assert.Single(recording.StopPoints);
Assert.True(point.HasHeading);
Assert.Equal(1.25f, point.Heading);
}
[Fact]
public void A_stop_point_of_an_older_recording_has_no_way()
{
var recording = BlackBoxReader.Read(new MemoryStream(new BlackBoxFile().Info().StopPoint(0, "Swoszowice", 100, 201, 201).End(10).Bytes));
Assert.False(Assert.Single(recording.StopPoints).HasHeading);
}
[Fact]
public void A_stop_point_on_tracks_of_both_ways_has_no_way()
{
var recording = BlackBoxReader.Read(new MemoryStream(new BlackBoxFile().Info().StopPoint(0, "Swoszowice", 100, 201, 201, float.NaN).End(10).Bytes));
Assert.False(Assert.Single(recording.StopPoints).HasHeading);
}
[Fact]
public void A_W32_is_for_the_trains_shorter_than_its_length_and_that_is_their_room()
{
var recording = BlackBoxReader.Read(new MemoryStream(new BlackBoxFile().Info().StopPoint(0, "Tumlin_po", 100, -1, -121).End(10).Bytes));
var point = Assert.Single(recording.StopPoints);
Assert.True(point.ForShortTrains);
Assert.False(point.HasPlatform);
Assert.Equal(121, point.PlatformLength);
var stop = new StationEvent(0, 1, StationKind.Stopped, 1, "Tumlin_po", new Position(0, 0, 0), 60, 50, 64.9f, -121, 600, 601);
Assert.False(stop.OnPlatform);
Assert.Equal(3.9, stop.OffPlatform, 1);
}
}

View File

@@ -20,6 +20,7 @@
<ItemGroup>
<ProjectReference Include="..\StarterNG\StarterNG.csproj" />
<ProjectReference Include="..\Eu07.BlackBox\Eu07.BlackBox.csproj" />
</ItemGroup>
</Project>