From 532875fc7b4f8946722aa26311b5e8bde693538a Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 11 Nov 2026 00:57:57 +0100 Subject: [PATCH] Black box: the queries of the user's train worked out once a read, lights as the driver checks them - The stations, the signals passed, the horn points, the couplings, the consists and the timetable progress of the user's train are kept until a read adds something; the views asked for them every frame, which on a long session with many trains took the window down. A read that adds nothing no longer counts as a change. - The lights are checked against the lamps the vehicles at the ends have (written by the simulator from now on), as its driver checks them: Pc 1 without the upper headlight, with the auxiliary lamps, Pc 5 with the rear-end signals. Only while the user drives: the autopilot's lights are its own. - A train off the platform counts at both ends: its head past the stop point as well as its tail before the platform's end. - A recording without the couplings keeps the timetable for any vehicle of the user. - A signal passed at danger is ringed on the map only once it was. - In the tapes window a black box opens on a click or Enter, not when the arrow keys go over it. - The viewer's screenshot of a window not laid out saves nothing and still closes. --- Eu07.BlackBox.Viewer/Screenshot.cs | 3 +- Eu07.BlackBox.Views/BlackBoxStrings.cs | 4 +- Eu07.BlackBox.Views/CabPanel.cs | 15 ++-- Eu07.BlackBox.Views/MapView.cs | 4 +- Eu07.BlackBox/BlackBoxReader.cs | 6 +- Eu07.BlackBox/DrivingSummary.cs | 18 ++--- Eu07.BlackBox/HeadCodes.cs | 79 +++++++++++++++++-- Eu07.BlackBox/Recording.cs | 64 +++++++++++---- Eu07.BlackBox/Records.cs | 12 ++- Eu07.BlackBox/TimetableProgress.cs | 5 +- StarterNG.Tests/BlackBox/BlackBoxFile.cs | 8 +- .../BlackBox/BlackBoxSignalsTests.cs | 56 +++++++++++++ StarterNG.Tests/BlackBox/PlatformTests.cs | 16 ++++ StarterNG/Views/Tapes/HaslerTapeWindow.cs | 16 ++-- 14 files changed, 254 insertions(+), 52 deletions(-) diff --git a/Eu07.BlackBox.Viewer/Screenshot.cs b/Eu07.BlackBox.Viewer/Screenshot.cs index a982db2..0b8e146 100644 --- a/Eu07.BlackBox.Viewer/Screenshot.cs +++ b/Eu07.BlackBox.Viewer/Screenshot.cs @@ -16,7 +16,8 @@ internal static class Screenshot window.FollowTrain = follow; // A few frames for the layout and the first render. await Task.Delay(1000); - if (window.Content is Control content) + // Not laid out (a minimised window): nothing to save, and the viewer still closes. + if (window.Content is Control content && content.Bounds.Width >= 1 && content.Bounds.Height >= 1) { var size = new PixelSize((int)content.Bounds.Width, (int)content.Bounds.Height); using var bitmap = new RenderTargetBitmap(size); diff --git a/Eu07.BlackBox.Views/BlackBoxStrings.cs b/Eu07.BlackBox.Views/BlackBoxStrings.cs index 06f150f..d897004 100644 --- a/Eu07.BlackBox.Views/BlackBoxStrings.cs +++ b/Eu07.BlackBox.Views/BlackBoxStrings.cs @@ -148,7 +148,7 @@ public sealed class DefaultBlackBoxStrings : IBlackBoxStrings ["TtStopPast"] = "{0:0.0} m past", ["TtStopExact"] = "on the spot", ["TtOnPlatform"] = "along the platform", - ["TtOffPlatform"] = "the tail {0:0.0} m off the platform", + ["TtOffPlatform"] = "{0:0.0} m of the train off the platform", ["TtTechnical"] = "technical stop", ["TtStanding"] = "standing", ["EventStopped"] = "Stop at {0}, {1}", @@ -347,7 +347,7 @@ public sealed class DefaultBlackBoxStrings : IBlackBoxStrings ["TtStopPast"] = "{0:0.0} m za miejscem", ["TtStopExact"] = "w punkt", ["TtOnPlatform"] = "cały przy peronie", - ["TtOffPlatform"] = "koniec składu {0:0.0} m poza peronem", + ["TtOffPlatform"] = "{0:0.0} m składu poza peronem", ["TtTechnical"] = "postój techniczny", ["TtStanding"] = "stoi", ["EventStopped"] = "Postój: {0}, {1}", diff --git a/Eu07.BlackBox.Views/CabPanel.cs b/Eu07.BlackBox.Views/CabPanel.cs index a5a9e45..3cfb307 100644 --- a/Eu07.BlackBox.Views/CabPanel.cs +++ b/Eu07.BlackBox.Views/CabPanel.cs @@ -130,19 +130,20 @@ public sealed class CabPanel : Control double lightsX = Pad + 3 * third + 4; context.DrawText(MapView.Text(Strings["CabLights"], Palette.DimText, 10), new Point(lightsX, top)); double faceX = lightsX; - foreach (var (key, lights, required) in new[] + foreach (var (key, lights, required, right) in new[] { - ("CabLightsHead", s.LightsHead, HeadCodes.RequiredHead(s)), - ("CabLightsTail", s.LightsTail, HeadCodes.RequiredTail(s)) + ("CabLightsHead", s.LightsHead, HeadCodes.RequiredHead(s), HeadCodes.HeadRight(s)), + ("CabLightsTail", s.LightsTail, HeadCodes.RequiredTail(s), HeadCodes.TailRight(s)) }) { DrawFace(context, lights, new Point(faceX + 18, top + 24)); - // the end, and the signal it shows: green as it is to be, red otherwise with the one it is to show + // the end, and the signal it shows: green as it is to be (with the lamps the vehicle has), red otherwise + // with the one it is to show var shown = HeadCodes.Of(lights); - var text = required is { } want && want != shown + var text = right == false && required is { } want ? $"{HeadCodes.Name(shown)} ≠ {HeadCodes.Name(want)}" - : HeadCodes.Name(shown); - var brush = required is null ? Palette.FaintText : required == shown ? Palette.Good : Palette.Bad; + : right == true && required is { } met ? HeadCodes.Name(met) : HeadCodes.Name(shown); + var brush = right is null ? Palette.FaintText : right.Value ? Palette.Good : Palette.Bad; var label = MapView.Text($"{Strings[key]} {text}", brush, 9); context.DrawText(label, new Point(faceX + 18 - label.Width / 2, top + 34)); faceX += 62; diff --git a/Eu07.BlackBox.Views/MapView.cs b/Eu07.BlackBox.Views/MapView.cs index 791647f..189aa40 100644 --- a/Eu07.BlackBox.Views/MapView.cs +++ b/Eu07.BlackBox.Views/MapView.cs @@ -456,12 +456,14 @@ public sealed class MapView : Control var danger = new Pen(Palette.Over, 2.5); foreach (var passing in recording.UserPassings()) { + if (passing.Time > _time) + continue; var p = ToScreen(passing.Position.X, passing.Position.Z); if (!Visible(p)) continue; if (passing.AtDanger) context.DrawEllipse(null, danger, p, 10, 10); - else if (passing.Time <= _time) + else context.DrawEllipse(null, passed, p, 7, 7); } } diff --git a/Eu07.BlackBox/BlackBoxReader.cs b/Eu07.BlackBox/BlackBoxReader.cs index f241da5..417b0b0 100644 --- a/Eu07.BlackBox/BlackBoxReader.cs +++ b/Eu07.BlackBox/BlackBoxReader.cs @@ -81,7 +81,8 @@ public sealed class BlackBoxReader } _offset += at; - Recording.Changed(); + if (read > 0) + Recording.Changed(); return read; } @@ -161,6 +162,9 @@ public sealed class BlackBoxReader // What the driver went by and the lights asked for came later still. if (r.Remaining >= 5) sample = sample with { Movement = (Movement)r.U8(), LightHintHead = (short)r.U16(), LightHintTail = (short)r.U16() }; + // The lamps the vehicles at the ends have, which a pattern is checked against, later again. + if (r.Remaining >= 4) + sample = sample with { EquippedHead = (Lights)r.U16(), EquippedTail = (Lights)r.U16() }; rec.AddCab(sample); break; } diff --git a/Eu07.BlackBox/DrivingSummary.cs b/Eu07.BlackBox/DrivingSummary.cs index a95f8f2..71e9095 100644 --- a/Eu07.BlackBox/DrivingSummary.cs +++ b/Eu07.BlackBox/DrivingSummary.cs @@ -124,15 +124,16 @@ public sealed record DrivingSummary( else if (sample.Driving == Driver.Human && sample.AbsSpeedKmh > Rounding) driven += step; - // the light signals at both ends, while the train moves - if (sample.AbsSpeedKmh > 1 && HeadCodes.Right(sample) is { } right) + // the light signals at both ends, while the user drives the train and it moves: the autopilot's are its own + if (sample.Driving == Driver.Human && sample.AbsSpeedKmh > 1 && + HeadCodes.HeadRight(sample) is { } headRight && HeadCodes.TailRight(sample) is { } tailRight) { lightsJudged += step; - if (!right) + if (!headRight || !tailRight) { lightsWrong += step; var fault = new LightsFault(before.Time, sample.Time, HeadCodes.Of(sample.LightsHead), HeadCodes.RequiredHead(sample)!.Value, - HeadCodes.Of(sample.LightsTail), HeadCodes.RequiredTail(sample)!.Value); + !headRight, HeadCodes.Of(sample.LightsTail), HeadCodes.RequiredTail(sample)!.Value, !tailRight); if (faults.Count > 0 && faults[^1].SameAs(fault) && fault.Start - faults[^1].End < 2) faults[^1] = faults[^1] with { End = sample.Time }; else @@ -186,11 +187,10 @@ public sealed record DrivingSummary( } /// A stretch the user's train moved with other light signals than it was to show, at its head and its tail. -public sealed record LightsFault(double Start, double End, HeadCode Head, HeadCode HeadRequired, HeadCode Tail, HeadCode TailRequired) +public sealed record LightsFault(double Start, double End, HeadCode Head, HeadCode HeadRequired, bool HeadWrong, + HeadCode Tail, HeadCode TailRequired, bool TailWrong) { - public bool HeadWrong => Head != HeadRequired; - public bool TailWrong => Tail != TailRequired; - public bool SameAs(LightsFault other) => - Head == other.Head && HeadRequired == other.HeadRequired && Tail == other.Tail && TailRequired == other.TailRequired; + Head == other.Head && HeadRequired == other.HeadRequired && HeadWrong == other.HeadWrong && + Tail == other.Tail && TailRequired == other.TailRequired && TailWrong == other.TailWrong; } diff --git a/Eu07.BlackBox/HeadCodes.cs b/Eu07.BlackBox/HeadCodes.cs index e4657f4..ad482bf 100644 --- a/Eu07.BlackBox/HeadCodes.cs +++ b/Eu07.BlackBox/HeadCodes.cs @@ -70,12 +70,81 @@ public static class HeadCodes _ => null }; - /// Whether both ends showed what they were to while the train moved; null when nothing tells it. - public static bool? Right(CabSample sample) + /// Whether the head of the train showed what it was to; null when nothing tells it. + public static bool? HeadRight(CabSample sample) => sample.Movement switch { - if (RequiredHead(sample) is not { } head || RequiredTail(sample) is not { } tail) - return null; - return Of(sample.LightsHead) == head && Of(sample.LightsTail) == tail; + Movement.Train => Meets(sample.LightsHead, sample.EquippedHead, sample.LightHintHead, HeadCode.Pc1), + Movement.Shunting => Shows(HeadCode.Tb1, sample.LightsHead, sample.EquippedHead), + _ => null + }; + + /// Whether the tail of the train showed what it was to; null when nothing tells it. + public static bool? TailRight(CabSample sample) => sample.Movement switch + { + Movement.Train => Meets(sample.LightsTail, sample.EquippedTail, sample.LightHintTail, HeadCode.Pc5), + Movement.Shunting => Shows(HeadCode.Tb1, sample.LightsTail, sample.EquippedTail), + _ => null + }; + + /// Whether both ends showed what they were to; null when nothing tells it. + public static bool? Right(CabSample sample) => + HeadRight(sample) is { } head && TailRight(sample) is { } tail ? head && tail : null; + + private const Lights HighBeams = Lights.HighBeamLeft | Lights.HighBeamRight; + private const Lights Auxiliary = Lights.AuxiliaryLeft | Lights.AuxiliaryRight; + + // the pattern asked for at an end, the usual one when none is: the driver sets one asked for as it is + private static bool Meets(Lights lit, Lights? equipped, short hint, HeadCode usual) + { + if (hint < 0) + return Shows(usual, lit, equipped); + var pattern = (Lights)hint; + if (usual == HeadCode.Pc1 && pattern == (Lights.RedMarkerLeft | Lights.HeadlightRight | Lights.HeadlightUpper)) + return Shows(HeadCode.Pc2, lit, equipped); + return (lit & ~HighBeams) == (equipped is { } lamps ? pattern & lamps : pattern); + } + + /// + /// Whether the lamps lit at an end show the signal, as the simulator's driver checks it against the lamps + /// the vehicle has there: a vehicle without the upper headlight shows Pc 1 with the two others, one with + /// auxiliary lamps may show them instead, and one with no lamps for it the rear-end signals. A recording + /// made before the lamps were written has the pattern alone. The high beams tell nothing. + /// + public static bool Shows(HeadCode code, Lights lit, Lights? equipped) + { + if (equipped is not { } lamps) + return Of(lit) == code; + lit &= ~HighBeams; + bool auxiliary = (lamps & Auxiliary) != 0; + switch (code) + { + case HeadCode.Pc1: + { + var pattern = lamps & (Lights.HeadlightLeft | Lights.HeadlightRight | Lights.HeadlightUpper); + var fallback = lamps & (Auxiliary | Lights.HeadlightUpper); + return (pattern != 0 && lit == pattern) || (auxiliary && lit == fallback) || + (pattern == 0 && fallback == 0 && lit == Lights.RearEndSignals); + } + case HeadCode.Pc2: + { + var pattern = lamps & (Lights.RedMarkerLeft | Lights.HeadlightRight | Lights.HeadlightUpper); + var fallback = lamps & (Lights.RedMarkerLeft | Lights.AuxiliaryRight | Lights.HeadlightUpper); + return lit == pattern || (auxiliary && lit == fallback); + } + case HeadCode.Pc5: + { + var pattern = lamps & (Lights.RedMarkerLeft | Lights.RedMarkerRight); + var fallback = lamps & Lights.RearEndSignals; + return (pattern != 0 && lit == pattern) || (fallback != 0 && lit == fallback); + } + case HeadCode.Tb1: + // one headlight, of those there are + return lit == (lamps & Lights.HeadlightLeft) || lit == (lamps & Lights.HeadlightRight); + case HeadCode.Off: + return lit == Lights.None; + default: + return false; + } } /// The name of a signal as the rules give it. diff --git a/Eu07.BlackBox/Recording.cs b/Eu07.BlackBox/Recording.cs index c9694e2..c731aca 100644 --- a/Eu07.BlackBox/Recording.cs +++ b/Eu07.BlackBox/Recording.cs @@ -91,7 +91,27 @@ public sealed class Recording // ----- filled by the reader ----- - internal void Changed() => ++Revision; + internal void Changed() + { + ++Revision; + _memo.Clear(); + _consists.Clear(); + } + + // What the questions about the user's train worked out, kept until a read adds something: the views ask them every frame. + private readonly Dictionary _memo = new(); + private readonly Dictionary<(int Vehicle, int Coupling), IReadOnlySet> _consists = new(); + private static readonly object UserStationsKey = new(), UserPassingsKey = new(), UserHornChecksKey = new(), UserCouplingsKey = new(); + + /// What make gives, worked out once until the recording changes. + internal T Memo(object key, Func make) where T : class + { + if (_memo.TryGetValue(key, out var kept)) + return (T)kept; + var made = make(); + _memo[key] = made; + return made; + } private void Saw(double time) => _lastTime = Math.Max(_lastTime, time); @@ -371,8 +391,8 @@ public sealed class Recording public IReadOnlyList AutopilotSpans() => CabSpans(s => s.Driving == Driver.Ai); /// The signals the user's train passed: those passed by the vehicle the samples are of at the time. - public IEnumerable UserPassings() => - _passings.Where(p => IsUserTrain(p.Time + 0.5, p.Vehicle, p.Position, Math.Max(0, p.Overrun) + 250)); + public IReadOnlyList UserPassings() => + Memo(UserPassingsKey, () => _passings.Where(p => IsUserTrain(p.Time + 0.5, p.Vehicle, p.Position, Math.Max(0, p.Overrun) + 250)).ToList()); /// /// Whether a vehicle was of the user's train at t. The records of a train give the @@ -393,12 +413,22 @@ public sealed class Recording /// The vehicles coupled into one train with the one given at t, itself included. public IReadOnlySet ConsistAt(int vehicle, double t) { - // What each coupler held at t: the couplings in the order written, each telling both ends. + // The trains change only at a coupling: the consist is the same until the next one. + int last = LastAtOrBefore(_couplings, t, c => c.Time); + if (_consists.TryGetValue((vehicle, last), out var kept)) + return kept; + var consist = Consist(vehicle, last); + _consists[(vehicle, last)] = consist; + return consist; + } + + private HashSet Consist(int vehicle, int last) + { + // What each coupler held after the coupling given: the couplings in the order written, each telling both ends. var held = new Dictionary<(int Vehicle, int Side), (int Other, int OtherSide)>(); - foreach (var change in _couplings) + for (int i = 0; i <= last; i++) { - if (change.Time > t) - break; + var change = _couplings[i]; if (held.Remove((change.Vehicle, change.Side), out var before)) held.Remove((before.Other, before.OtherSide)); if (change.Other != 0 && (change.Flags & CouplingFlags.Coupler) != 0) @@ -432,10 +462,11 @@ public sealed class Recording _couplings.Where(c => c.Time > 0.01 && (c.Coupled || c.Uncoupled || c.Flags != c.PreviousFlags)); /// The couplings the user's train took part in, before or after them. - public IEnumerable UserCouplings() => - CouplingEvents().Where(c => CabAt(c.Time) is { } s && c.Time - s.Time <= 1.5 && - (ConsistAt(s.Vehicle, c.Time - 0.01).Contains(c.Vehicle) || - ConsistAt(s.Vehicle, c.Time + 0.01).Contains(c.Vehicle))); + public IReadOnlyList UserCouplings() => + Memo(UserCouplingsKey, () => CouplingEvents().Where(c => CabAt(c.Time) is { } s && c.Time - s.Time <= 1.5 && + (ConsistAt(s.Vehicle, c.Time - 0.01).Contains(c.Vehicle) || + ConsistAt(s.Vehicle, c.Time + 0.01).Contains(c.Vehicle))) + .ToList()); /// /// The timetable of the train the user was in at t: the last one written by then, else the first; none when the @@ -444,18 +475,21 @@ public sealed class Recording public Timetable? TimetableAt(double t) { var timetable = LastAtOrBefore(_timetables, t, x => x.Time) is var i and >= 0 ? _timetables[i] : _timetables.FirstOrDefault(); - if (timetable is null || CabAt(t) is not { } sample || sample.Vehicle == timetable.Vehicle) + // A recording made before the couplings were written can't tell the trains apart: the timetable stays. + if (timetable is null || CabAt(t) is not { } sample || sample.Vehicle == timetable.Vehicle || _couplings.Count == 0) return timetable; // another vehicle of the same train (the other cab, a car of it) keeps the timetable return ConsistAt(sample.Vehicle, t).Contains(timetable.Vehicle) ? timetable : null; } /// The stations of the timetable of the user's train, as it came to them. - public IEnumerable UserStations() => - _stations.Where(s => IsUserTrain(s.Time, s.Vehicle, s.Position, Math.Max(s.TrainLength, 50) + 150)); + public IReadOnlyList UserStations() => + Memo(UserStationsKey, () => _stations.Where(s => IsUserTrain(s.Time, s.Vehicle, s.Position, Math.Max(s.TrainLength, 50) + 150)).ToList()); /// The horn points the user's train passed, and whether the horn sounded there. - public IEnumerable UserHornChecks() + public IReadOnlyList UserHornChecks() => Memo(UserHornChecksKey, () => HornChecks().ToList()); + + private IEnumerable HornChecks() { foreach (var request in _hornRequests) { diff --git a/Eu07.BlackBox/Records.cs b/Eu07.BlackBox/Records.cs index 00f4cc4..89d08d7 100644 --- a/Eu07.BlackBox/Records.cs +++ b/Eu07.BlackBox/Records.cs @@ -240,13 +240,16 @@ public sealed record StationEvent(double Time, int Vehicle, StationKind Kind, in /// Whether the whole train stood along the platform, when its length is known: as the driver takes it, the length of /// the trains a W32 is for is the room it has there too. /// - public bool? OnPlatform => Platform != 0 ? Distance >= -2 && Distance + TrainLength <= PlatformLength + 2 : null; + public bool? OnPlatform => Platform != 0 ? OffPlatform <= PlatformTolerance : null; + + /// How far off the platform a train may stand and still count as along it [m]. + public const double PlatformTolerance = 2; /// The length of the platform [m], without the digit of its side. public double PlatformLength => Platforms.Length(Platform); - /// How far the tail stood off the platform [m], 0 along it. - public double OffPlatform => Platform != 0 ? Math.Max(0, Distance + TrainLength - PlatformLength) : 0; + /// How much of the train stood off the platform [m]: the head past the stop point, the tail before the platform's end; 0 along it. + public double OffPlatform => Platform != 0 ? Math.Max(0, -Distance) + Math.Max(0, Distance + TrainLength - PlatformLength) : 0; } public enum CameraMode : byte @@ -315,7 +318,8 @@ public readonly record struct CabSample(double Time, int Vehicle, Position Posit float MainReservoir, float Voltage, float EngineRpm, Lights LightsHead, Lights LightsTail, sbyte CabEnd = 0, SecurityFlags Security = SecurityFlags.None, float LateralAcceleration = 0, - Movement Movement = Movement.Unknown, short LightHintHead = -1, short LightHintTail = -1) + Movement Movement = Movement.Unknown, short LightHintHead = -1, short LightHintTail = -1, + Lights? EquippedHead = null, Lights? EquippedTail = null) { public bool Has(CabDevices device) => (Devices & device) == device; public bool Has(SecurityFlags flag) => (Security & flag) == flag; diff --git a/Eu07.BlackBox/TimetableProgress.cs b/Eu07.BlackBox/TimetableProgress.cs index 179a60b..e759dde 100644 --- a/Eu07.BlackBox/TimetableProgress.cs +++ b/Eu07.BlackBox/TimetableProgress.cs @@ -66,7 +66,10 @@ public sealed record StationProgress( recording.TimetableAt(t) is { } timetable ? Of(recording, timetable) : Array.Empty(); /// The rows of a timetable of the recording, each with what the train did there. - public static IReadOnlyList Of(Recording recording, Timetable timetable) + public static IReadOnlyList Of(Recording recording, Timetable timetable) => + recording.Memo((typeof(StationProgress), timetable), () => Work(recording, timetable)); + + private static IReadOnlyList Work(Recording recording, Timetable timetable) { // The records of this train: from its timetable on, until another one is written. A train given its next // timetable may come to the first station of it at once, and a recording made before the simulator wrote the diff --git a/StarterNG.Tests/BlackBox/BlackBoxFile.cs b/StarterNG.Tests/BlackBox/BlackBoxFile.cs index e2cab25..1daaf52 100644 --- a/StarterNG.Tests/BlackBox/BlackBoxFile.cs +++ b/StarterNG.Tests/BlackBox/BlackBoxFile.cs @@ -108,7 +108,7 @@ internal sealed class BlackBoxFile 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) => + short hintHead = -1, short hintTail = -1, ushort? equippedHead = null, ushort? equippedTail = null) => Record(0x20, w => { w.Write(time); @@ -154,6 +154,12 @@ internal sealed class BlackBoxFile w.Write(kind); w.Write(hintHead); w.Write(hintTail); + // the lamps the vehicles at the ends have: a recording made before them ends above + if (equippedHead is { } head) + { + w.Write(head); + w.Write(equippedTail ?? head); + } } } }); diff --git a/StarterNG.Tests/BlackBox/BlackBoxSignalsTests.cs b/StarterNG.Tests/BlackBox/BlackBoxSignalsTests.cs index f58aeea..1a565bf 100644 --- a/StarterNG.Tests/BlackBox/BlackBoxSignalsTests.cs +++ b/StarterNG.Tests/BlackBox/BlackBoxSignalsTests.cs @@ -68,6 +68,62 @@ public class HeadCodeTests Assert.False(fault.TailWrong); } + private static Recording DriveWith(ushort head, ushort tail, ushort equippedHead, ushort equippedTail, byte movement = 1, byte driving = 1) + { + var f = new BlackBoxFile().Info(); + for (double t = 0; t < 30; t += 0.5) + f.Cab(t, 1, 1000 + t * 10, 40, driving: driving, lightsHead: head, lightsTail: tail, movement: movement, + equippedHead: equippedHead, equippedTail: equippedTail); + return BlackBoxReader.Read(f.End(30).Stream()); + } + + [Fact] + public void The_lamps_the_vehicles_have_are_read() + { + var sample = DriveWith(Pc1, Pc5, 0x1B5, 0x22).Cab[0]; + Assert.Equal((Lights)0x1B5, sample.EquippedHead); + Assert.Equal((Lights)0x22, sample.EquippedTail); + } + + [Theory] + // without the upper headlight Pc 1 is the two others + [InlineData(0x11, 0x11)] + // with auxiliary lamps, those and the upper headlight + [InlineData(0x184, 0x1B5)] + // a vehicle with no lamps for it: the rear-end signals + [InlineData(0x40, 0x40)] + public void Pc1_is_what_the_vehicle_can_show_of_it(ushort lit, ushort equipped) + { + var summary = DriveWith(lit, Pc5, equipped, Pc5).Summarize(); + Assert.True(summary.LightsJudgedSeconds > 25); + Assert.Empty(summary.LightsFaults); + } + + [Fact] + public void A_tail_with_the_rear_end_signals_only_shows_Pc5_with_them() + { + Assert.Empty(DriveWith(Pc1, 0x40, Pc1, 0x40).Summarize().LightsFaults); + var fault = Assert.Single(DriveWith(Pc1, 0x00, Pc1, 0x40).Summarize().LightsFaults); + Assert.False(fault.HeadWrong); + Assert.True(fault.TailWrong); + } + + [Fact] + public void A_vehicle_with_all_the_lamps_is_to_light_all_three_for_Pc1() + { + var fault = Assert.Single(DriveWith(0x11, Pc5, 0x35, Pc5).Summarize().LightsFaults); + Assert.True(fault.HeadWrong); + Assert.Equal(HeadCode.Pc1, fault.HeadRequired); + } + + [Fact] + public void The_lights_the_autopilot_sets_are_not_the_users() + { + var summary = DriveWith(Tb1, Tb1, Pc1, Pc5, driving: 2).Summarize(); + Assert.Equal(0, summary.LightsJudgedSeconds); + Assert.Empty(summary.LightsFaults); + } + [Fact] public void An_older_recording_tells_nothing_of_the_signals() { diff --git a/StarterNG.Tests/BlackBox/PlatformTests.cs b/StarterNG.Tests/BlackBox/PlatformTests.cs index da29b7c..2b4a773 100644 --- a/StarterNG.Tests/BlackBox/PlatformTests.cs +++ b/StarterNG.Tests/BlackBox/PlatformTests.cs @@ -31,6 +31,22 @@ public class PlatformTests Assert.Equal(201, stop.PlatformLength); } + [Fact] + public void A_train_whose_head_stands_past_the_stop_point_is_off_the_platform_by_that_much() + { + var stop = Stop(-5f, 129.8f, 201); + Assert.False(stop.OnPlatform); + Assert.Equal(5, stop.OffPlatform, 1); + } + + [Fact] + public void A_metre_off_still_counts_as_along_the_platform() + { + var stop = Stop(-1f, 129.8f, 201); + Assert.True(stop.OnPlatform); + Assert.Equal(1, stop.OffPlatform, 1); + } + [Fact] public void A_train_within_the_platform_is_along_it() { diff --git a/StarterNG/Views/Tapes/HaslerTapeWindow.cs b/StarterNG/Views/Tapes/HaslerTapeWindow.cs index 21c867a..fe308c4 100644 --- a/StarterNG/Views/Tapes/HaslerTapeWindow.cs +++ b/StarterNG/Views/Tapes/HaslerTapeWindow.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; +using Avalonia.Input; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Media.Imaging; @@ -105,14 +106,19 @@ public sealed class HaslerTapeWindow : Window ToolTip.SetTip(_startClock, App.Loc["HaslerStartClockTip"]); ScrollViewer.SetHorizontalScrollBarVisibility(_list, ScrollBarVisibility.Disabled); + _list.KeyDown += async (_, e) => + { + if (e.Key == Key.Enter && _list.SelectedItem is TreeViewItem { Tag: BlackBoxRecording box }) + { + e.Handled = true; + await OpenBlackBoxAsync(box); + } + }; _list.SelectionChanged += async (_, _) => { - // A black box opens in a window of its own; the tape shown stays. - if (_list.SelectedItem is TreeViewItem { Tag: BlackBoxRecording box }) - { - await OpenBlackBoxAsync(box); + // A black box opens in a window of its own on a click or Enter, not when the arrows go over it; the tape shown stays. + if (_list.SelectedItem is TreeViewItem { Tag: BlackBoxRecording }) return; - } // A session stands for its first tape, the player's own if it has one. if (_list.SelectedItem is TreeViewItem { Tag: TapeSession } session) session.IsExpanded = true;