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