using System; using System.Collections.Generic; using System.Linq; using System.Numerics; namespace Eu07.BlackBox; /// A span of time, start included, end not. public readonly record struct TimeSpanRange(double Start, double End) { public double Length => End - Start; public bool Contains(double t) => t >= Start && t < End; } /// /// What a black box holds, as far as it has been read: the map, the vehicles and what /// happened to them over the time of the recording, which runs from 0 in seconds of /// simulation. The lists grow as more of a file still written is read; the queries /// answer for any moment of the time read so far. /// public sealed class Recording { private readonly List _tracks = new(); private readonly Dictionary> _trackIndex = new(); private readonly List _signals = new(); private readonly Dictionary _signalIndex = new(); private readonly Dictionary _vehicles = new(); private readonly List _cab = new(); private readonly List _looks = new(); private readonly Dictionary> _history = new(); private readonly List _switches = new(); private readonly Dictionary> _switchIndex = new(); private readonly List _aspects = new(); private readonly Dictionary> _aspectIndex = new(); private readonly List _commands = new(); private readonly List _driverChanges = new(); private readonly List _passings = new(); private readonly List _userVehicles = new(); private readonly List _clocks = new(); private readonly List _stopPoints = new(); private readonly List _hornPoints = new(); private readonly List _timetables = new(); private readonly List _stations = new(); private readonly List _hornRequests = new(); private readonly List _cameras = new(); private readonly List _couplings = new(); private double _lastTime; public int Version { get; internal set; } public RecordingInfo? Info { get; internal set; } /// The time of the end record: null while the simulator still writes, or if it didn't end the recording. public double? EndTime { get; internal set; } public bool Complete => EndTime is not null; /// Records of types this reader doesn't know, passed over. public int Unknown { get; internal set; } /// Records shorter than their type says, passed over. public int Malformed { get; internal set; } /// Goes up with every read that added something, for views to tell they have to look again. public int Revision { get; private set; } public IReadOnlyList Tracks => _tracks; public IReadOnlyList Signals => _signals; public IReadOnlyDictionary Vehicles => _vehicles; public IReadOnlyList Cab => _cab; public IReadOnlyList Looks => _looks; public IReadOnlyList Switches => _switches; public IReadOnlyList Aspects => _aspects; public IReadOnlyList Commands => _commands; public IReadOnlyList DriverChanges => _driverChanges; public IReadOnlyList Passings => _passings; public IReadOnlyList UserVehicles => _userVehicles; public IReadOnlyList Clocks => _clocks; public IReadOnlyList StopPoints => _stopPoints; public IReadOnlyList HornPoints => _hornPoints; public IReadOnlyList Timetables => _timetables; public IReadOnlyList Stations => _stations; public IReadOnlyList HornRequests => _hornRequests; public IReadOnlyList Cameras => _cameras; public IReadOnlyList Couplings => _couplings; /// The time the recording covers so far. public double Duration => Math.Max(EndTime ?? 0, _lastTime); /// The seconds between two looks at the vehicles. public double LookInterval => Info is { VehiclesInterval: > 0 } info ? info.VehiclesInterval : 1.0; // ----- filled by the reader ----- 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); internal void AddTrack(MapTrack track) { _tracks.Add(track); if (!_trackIndex.TryGetValue(track.Index, out var ways)) _trackIndex[track.Index] = ways = new List(1); ways.Add(track); } internal void AddSignal(MapSignal signal) { _signals.Add(signal); _signalIndex[signal.Index] = signal; } internal void AddVehicle(VehicleInfo vehicle) => _vehicles[vehicle.Id] = vehicle; internal void RemoveVehicle(double time, int id) { Saw(time); if (_vehicles.TryGetValue(id, out var vehicle)) vehicle.RemovedAt = time; } internal void AddCab(CabSample sample) { Saw(sample.Time); _cab.Add(sample); } internal void AddLook(VehicleLook look) { Saw(look.Time); _looks.Add(look); foreach (var state in look.States) { if (!_history.TryGetValue(state.Id, out var history)) _history[state.Id] = history = new List<(double, VehicleState)>(); history.Add((look.Time, state)); } } internal void AddSwitch(SwitchChange change) { Saw(change.Time); _switches.Add(change); if (!_switchIndex.TryGetValue(change.Track, out var list)) _switchIndex[change.Track] = list = new List(); list.Add(change); } internal void AddAspect(AspectChange change) { Saw(change.Time); _aspects.Add(change); if (!_aspectIndex.TryGetValue(change.Signal, out var list)) _aspectIndex[change.Signal] = list = new List(); list.Add(change); } internal void AddCommand(CommandEntry command) { Saw(command.Time); _commands.Add(command); } internal void AddDriverChange(DriverChange change) { Saw(change.Time); _driverChanges.Add(change); } internal void AddPassing(SignalPassing passing) { Saw(passing.Time); _passings.Add(passing); } internal void AddUserVehicle(UserVehicleChange change) { Saw(change.Time); _userVehicles.Add(change); } internal void AddClock(ClockEntry clock) { Saw(clock.Time); _clocks.Add(clock); } internal void AddStopPoint(MapStopPoint point) => _stopPoints.Add(point); internal void AddHornPoint(MapHornPoint point) => _hornPoints.Add(point); internal void AddTimetable(Timetable timetable) { Saw(timetable.Time); _timetables.Add(timetable); } internal void AddStation(StationEvent station) { Saw(station.Time); _stations.Add(station); } internal void AddCamera(CameraState camera) { Saw(camera.Time); _cameras.Add(camera); } internal void AddCoupling(CouplingChange change) { Saw(change.Time); _couplings.Add(change); } internal void AddHornRequest(HornRequest request) { Saw(request.Time); _hornRequests.Add(request); } // ----- queries ----- public IReadOnlyList TrackWays(int index) => _trackIndex.TryGetValue(index, out var ways) ? ways : Array.Empty(); public MapSignal? Signal(int index) => _signalIndex.GetValueOrDefault(index); public VehicleInfo? Vehicle(int id) => _vehicles.GetValueOrDefault(id); /// The last of a list sorted by time at or before t: -1 if none is. private static int LastAtOrBefore(IReadOnlyList list, double t, Func time) { int low = 0, high = list.Count - 1, found = -1; while (low <= high) { int middle = low + (high - low) / 2; if (time(list[middle]) <= t) { found = middle; low = middle + 1; } else high = middle - 1; } return found; } /// The index of the sample of the user's train at or before t, -1 before the first. public int CabIndexAt(double t) => LastAtOrBefore(_cab, t, s => s.Time); /// The sample of the user's train at or before t. public CabSample? CabAt(double t) => CabIndexAt(t) is var i and >= 0 ? _cab[i] : null; /// /// Where the user's train was at t, between the two samples around it. Null before /// the first sample, and in a gap: the user out of any vehicle. /// public Position? CabPositionAt(double t) { int i = CabIndexAt(t); if (i < 0) return null; var before = _cab[i]; if (i + 1 >= _cab.Count) return t - before.Time <= 1.0 ? before.Position : null; var after = _cab[i + 1]; if (after.Vehicle != before.Vehicle || after.Time - before.Time > 1.0) return t - before.Time <= 1.0 ? before.Position : null; double f = (t - before.Time) / Math.Max(1e-9, after.Time - before.Time); return Position.Lerp(before.Position, after.Position, Math.Clamp(f, 0, 1)); } /// /// Every vehicle on the scenery at t, where it was. A vehicle is listed only when it /// moved, so between two listings far apart it stood at the first place until the /// look before the second. /// public IReadOnlyList VehiclesAt(double t) { var states = new List(_history.Count); double interval = LookInterval; foreach (var (id, history) in _history) { if (_vehicles.TryGetValue(id, out var info) && info.RemovedAt is { } removed && removed <= t) continue; int i = LastAtOrBefore(history, t, entry => entry.Time); if (i < 0) continue; var (time, state) = history[i]; if (i + 1 < history.Count) { var (nextTime, next) = history[i + 1]; double from = Math.Max(time, nextTime - interval * 1.5); if (t > from) state = Lerp(state, next, (t - from) / Math.Max(1e-9, nextTime - from)); } states.Add(state); } return states; } private static VehicleState Lerp(VehicleState a, VehicleState b, double f) { float k = (float)Math.Clamp(f, 0, 1); float turn = b.Heading - a.Heading; while (turn > MathF.PI) turn -= 2 * MathF.PI; while (turn < -MathF.PI) turn += 2 * MathF.PI; return a with { Position = Vector3.Lerp(a.Position, b.Position, k), Heading = a.Heading + turn * k, SpeedKmh = a.SpeedKmh + (b.SpeedKmh - a.SpeedKmh) * k, Driving = k < 0.5f ? a.Driving : b.Driving }; } /// The aspect a signal showed at t, null before it was first seen. public AspectChange? AspectAt(int signal, double t) => _aspectIndex.TryGetValue(signal, out var list) && LastAtOrBefore(list, t, c => c.Time) is var i and >= 0 ? list[i] : null; /// The position of a switch at t, -1 if unknown. public int SwitchAt(int track, double t) => _switchIndex.TryGetValue(track, out var list) && LastAtOrBefore(list, t, c => c.Time) is var i and >= 0 ? list[i].State : -1; /// The time of day of the simulation at t. public TimeSpan ClockAt(double t) { int i = LastAtOrBefore(_clocks, t, c => c.Time); double second = i >= 0 ? _clocks[i].SecondOfDay + (t - _clocks[i].Time) : (Info?.SecondOfDay ?? 0) + t; second %= 86400; if (second < 0) second += 86400; return TimeSpan.FromSeconds(second); } /// The vehicle the user was in at t, and whether in its cab; null before it was known. public UserVehicleChange? UserVehicleAt(double t) => LastAtOrBefore(_userVehicles, t, c => c.Time) is var i and >= 0 ? _userVehicles[i] : null; /// The stretches of the samples of the user's train the condition holds over. public IReadOnlyList CabSpans(Func condition) { var spans = new List(); double? start = null; double gap = (Info is { CabInterval: > 0 } info ? info.CabInterval : 0.1) * 5; for (int i = 0; i < _cab.Count; i++) { var sample = _cab[i]; bool holds = condition(sample); bool broken = i > 0 && sample.Time - _cab[i - 1].Time > gap; if (start is { } open && (!holds || broken)) { spans.Add(new TimeSpanRange(open, broken ? _cab[i - 1].Time : sample.Time)); start = null; } if (holds && start is null) start = sample.Time; } if (start is { } last && _cab.Count > 0) spans.Add(new TimeSpanRange(last, _cab[^1].Time)); return spans; } /// When the AI drove the user's train: the autopilot. 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 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 /// vehicle at its head, which is not the one the user is in when the train is driven /// from its other end or from a car in the middle: those count when the place the /// record is about lies within reach of the user's vehicle. /// public bool IsUserTrain(double t, int vehicle, Position place, double reach) { if (CabAt(t) is not { } s || t - s.Time > 1.5) return false; if (s.Vehicle == vehicle) return true; // The couplings tell the train exactly; a recording made before them has the distance only. return CouplingsWritten ? ConsistAt(s.Vehicle, t).Contains(vehicle) : s.Position.DistanceTo(place) <= reach; } /// /// Whether the couplings were written: a recording made since has the movement in its cab samples, and one with /// no train of more than one vehicle has no coupling at all. /// private bool CouplingsWritten => _couplings.Count > 0 || (_cab.Count > 0 && _cab[0].Movement != Movement.Unknown); /// The vehicles coupled into one train with the one given at t, itself included. public IReadOnlySet ConsistAt(int vehicle, double t) { // 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)>(); for (int i = 0; i <= last; i++) { 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) { held[(change.Vehicle, change.Side)] = (change.Other, change.OtherSide); held[(change.Other, change.OtherSide)] = (change.Vehicle, change.Side); } } var consist = new HashSet { vehicle }; var open = new Stack(); open.Push(vehicle); while (open.Count > 0) { int at = open.Pop(); for (int side = 0; side < 2; side++) if (held.TryGetValue((at, side), out var next) && consist.Add(next.Other)) open.Push(next.Other); } return consist; } /// Where the user looked from at t, null before it was known. public CameraState? CameraAt(double t) => LastAtOrBefore(_cameras, t, c => c.Time) is var i and >= 0 ? _cameras[i] : null; /// /// The couplings made and undone after the start: those at t = 0 are what the trains /// were made of, not something done. /// public IEnumerable CouplingEvents() => _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 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 /// user had gone over to a vehicle of another consist, a train without a timetable or one standing on its own. /// public Timetable? TimetableAt(double t) { var timetable = LastAtOrBefore(_timetables, t, x => x.Time) is var i and >= 0 ? _timetables[i] : _timetables.FirstOrDefault(); // 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 || !CouplingsWritten) 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 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 IReadOnlyList UserHornChecks() => Memo(UserHornChecksKey, () => HornChecks().ToList()); private IEnumerable HornChecks() { foreach (var request in _hornRequests) { if (!IsUserTrain(request.Time, request.Vehicle, request.Position, 300)) continue; bool autopilot = CabAt(request.Time) is { Driving: Driver.Ai }; yield return new HornCheck(request, HornSounded(request), autopilot); } } /// The time given to sound the horn after a horn point, beyond how long it is to sound. public const double HornGrace = 8; /// Whether the horn of the user's train sounded from just before the horn point until a while after. private bool HornSounded(HornRequest request) { double until = request.Time + Math.Max(request.Duration, 1) + HornGrace; for (int i = Math.Max(0, CabIndexAt(request.Time - 2)); i < _cab.Count && _cab[i].Time <= until; i++) if (_cab[i].Has(CabDevices.Horn)) return true; return false; } /// When the train protection braked the user's train: the alerter or the cab signalling not acknowledged. public IReadOnlyList SecurityBrakings() => CabSpans(s => s.Has(SecurityFlags.Braking)); /// The commands the user gave in a vehicle, presses only. public IEnumerable UserCommands() => _commands.Where(c => c.Pressed && c.Vehicle != 0); public DrivingSummary Summarize(float tolerance = DrivingSummary.DefaultTolerance) => DrivingSummary.Of(this, tolerance); } /// A horn point the user's train passed: whether the horn sounded, and whether the autopilot drove then. public sealed record HornCheck(HornRequest Request, bool Sounded, bool Autopilot);