mirror of
https://github.com/MaSzyna-EU07/StarterNG.git
synced 2026-11-11 16:57:11 +01:00
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.
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
}
|
||||
|
||||
/// <summary>A stretch the user's train moved with other light signals than it was to show, at its head and its tail.</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -70,12 +70,81 @@ public static class HeadCodes
|
||||
_ => null
|
||||
};
|
||||
|
||||
/// <summary>Whether both ends showed what they were to while the train moved; null when nothing tells it.</summary>
|
||||
public static bool? Right(CabSample sample)
|
||||
/// <summary>Whether the head of the train showed what it was to; null when nothing tells it.</summary>
|
||||
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
|
||||
};
|
||||
|
||||
/// <summary>Whether the tail of the train showed what it was to; null when nothing tells it.</summary>
|
||||
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
|
||||
};
|
||||
|
||||
/// <summary>Whether both ends showed what they were to; null when nothing tells it.</summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The name of a signal as the rules give it.</summary>
|
||||
|
||||
@@ -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<object, object> _memo = new();
|
||||
private readonly Dictionary<(int Vehicle, int Coupling), IReadOnlySet<int>> _consists = new();
|
||||
private static readonly object UserStationsKey = new(), UserPassingsKey = new(), UserHornChecksKey = new(), UserCouplingsKey = new();
|
||||
|
||||
/// <summary>What make gives, worked out once until the recording changes.</summary>
|
||||
internal T Memo<T>(object key, Func<T> 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<TimeSpanRange> AutopilotSpans() => CabSpans(s => s.Driving == Driver.Ai);
|
||||
|
||||
/// <summary>The signals the user's train passed: those passed by the vehicle the samples are of at the time.</summary>
|
||||
public IEnumerable<SignalPassing> UserPassings() =>
|
||||
_passings.Where(p => IsUserTrain(p.Time + 0.5, p.Vehicle, p.Position, Math.Max(0, p.Overrun) + 250));
|
||||
public IReadOnlyList<SignalPassing> UserPassings() =>
|
||||
Memo(UserPassingsKey, () => _passings.Where(p => IsUserTrain(p.Time + 0.5, p.Vehicle, p.Position, Math.Max(0, p.Overrun) + 250)).ToList());
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// <summary>The vehicles coupled into one train with the one given at t, itself included.</summary>
|
||||
public IReadOnlySet<int> 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<int> 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));
|
||||
|
||||
/// <summary>The couplings the user's train took part in, before or after them.</summary>
|
||||
public IEnumerable<CouplingChange> 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<CouplingChange> 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());
|
||||
|
||||
/// <summary>
|
||||
/// 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;
|
||||
}
|
||||
|
||||
/// <summary>The stations of the timetable of the user's train, as it came to them.</summary>
|
||||
public IEnumerable<StationEvent> UserStations() =>
|
||||
_stations.Where(s => IsUserTrain(s.Time, s.Vehicle, s.Position, Math.Max(s.TrainLength, 50) + 150));
|
||||
public IReadOnlyList<StationEvent> UserStations() =>
|
||||
Memo(UserStationsKey, () => _stations.Where(s => IsUserTrain(s.Time, s.Vehicle, s.Position, Math.Max(s.TrainLength, 50) + 150)).ToList());
|
||||
|
||||
/// <summary>The horn points the user's train passed, and whether the horn sounded there.</summary>
|
||||
public IEnumerable<HornCheck> UserHornChecks()
|
||||
public IReadOnlyList<HornCheck> UserHornChecks() => Memo(UserHornChecksKey, () => HornChecks().ToList());
|
||||
|
||||
private IEnumerable<HornCheck> HornChecks()
|
||||
{
|
||||
foreach (var request in _hornRequests)
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
/// </summary>
|
||||
public bool? OnPlatform => Platform != 0 ? Distance >= -2 && Distance + TrainLength <= PlatformLength + 2 : null;
|
||||
public bool? OnPlatform => Platform != 0 ? OffPlatform <= PlatformTolerance : null;
|
||||
|
||||
/// <summary>How far off the platform a train may stand and still count as along it [m].</summary>
|
||||
public const double PlatformTolerance = 2;
|
||||
|
||||
/// <summary>The length of the platform [m], without the digit of its side.</summary>
|
||||
public double PlatformLength => Platforms.Length(Platform);
|
||||
|
||||
/// <summary>How far the tail stood off the platform [m], 0 along it.</summary>
|
||||
public double OffPlatform => Platform != 0 ? Math.Max(0, Distance + TrainLength - PlatformLength) : 0;
|
||||
/// <summary>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.</summary>
|
||||
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;
|
||||
|
||||
@@ -66,7 +66,10 @@ public sealed record StationProgress(
|
||||
recording.TimetableAt(t) is { } timetable ? Of(recording, timetable) : Array.Empty<StationProgress>();
|
||||
|
||||
/// <summary>The rows of a timetable of the recording, each with what the train did there.</summary>
|
||||
public static IReadOnlyList<StationProgress> Of(Recording recording, Timetable timetable)
|
||||
public static IReadOnlyList<StationProgress> Of(Recording recording, Timetable timetable) =>
|
||||
recording.Memo((typeof(StationProgress), timetable), () => Work(recording, timetable));
|
||||
|
||||
private static IReadOnlyList<StationProgress> 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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user