From 9b5848042a87922bc72e1e50d9111702399d6a7a Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 11 Nov 2026 18:21:27 +0100 Subject: [PATCH] fix(tapes): a tape written after another run began is not the launch's before it, and a session with only a black box shows no other session's tape --- .../BlackBox/BlackBoxListingTests.cs | 24 ++++++++++++ StarterNG/Application/HaslerTapes.cs | 37 +++++++++++++++---- StarterNG/Views/Tapes/HaslerTapeWindow.cs | 37 ++++++++++++++----- StarterNG/startercfg/lang/cz.xml | 1 + StarterNG/startercfg/lang/en.xml | 1 + StarterNG/startercfg/lang/hu.xml | 1 + StarterNG/startercfg/lang/pl.xml | 1 + StarterNG/startercfg/lang/ru.xml | 1 + 8 files changed, 86 insertions(+), 17 deletions(-) diff --git a/StarterNG.Tests/BlackBox/BlackBoxListingTests.cs b/StarterNG.Tests/BlackBox/BlackBoxListingTests.cs index afff660..ceed107 100644 --- a/StarterNG.Tests/BlackBox/BlackBoxListingTests.cs +++ b/StarterNG.Tests/BlackBox/BlackBoxListingTests.cs @@ -121,6 +121,30 @@ public class BlackBoxListingTests Assert.EndsWith(own, Assert.Single(launched.BlackBoxes).Path); } + [Fact] + public void A_tape_written_after_another_run_began_is_not_taken_for_the_launch_before() + { + // The starter launched Bałtyk; later a run of another scenery, not launched from it, wrote its tape and black box. + var files = new InMemoryFileSystem(); + var installation = new TestInstallation(files); + installation.Clock.Now = Launched; + installation.Tapes.RecordLaunch("baltyk.scn", TimeSpan.Zero, Array.Empty(), "ep07", new[] { "ep07" }, "Bałtyk"); + string box = Box("td", Launched.AddHours(5)); + files.WithFile(Log("ep07.dat"), "t").WrittenAt(Log("ep07.dat"), Launched.AddMinutes(50)) + .WithFile(Log("bb-su45.dat"), "t").WrittenAt(Log("bb-su45.dat"), Launched.AddHours(5).AddMinutes(8)) + .WithFile(Log(box), "b").WrittenAt(Log(box), Launched.AddHours(5).AddMinutes(8)); + + var sessions = installation.Tapes.Sessions(); + + var baltyk = Assert.Single(sessions, session => session.Launch is not null); + Assert.Equal("ep07", Assert.Single(baltyk.Tapes).Vehicle); + Assert.Empty(baltyk.BlackBoxes); + var other = Assert.Single(sessions, session => session.Launch is null); + Assert.Equal("bb-su45", Assert.Single(other.Tapes).Vehicle); + Assert.Single(other.BlackBoxes); + Assert.Null(installation.Tapes.ContextOf(other.Tapes[0]).StartClock); + } + [Fact] public void A_black_box_started_before_a_launch_is_not_taken_for_it() { diff --git a/StarterNG/Application/HaslerTapes.cs b/StarterNG/Application/HaslerTapes.cs index 34e55f0..a49baf8 100644 --- a/StarterNG/Application/HaslerTapes.cs +++ b/StarterNG/Application/HaslerTapes.cs @@ -227,7 +227,7 @@ public sealed class HaslerTapes public TapeContext ContextOf(TapeFile file) { - var launch = Launches().LastOrDefault(l => l.Covers(file.LastWriteUtc)); + var launch = LaunchOf(file, Launches(), ReadBlackBoxes()); if (launch is null) return new TapeContext(null, Recorders.Guess(null, file.Vehicle)); @@ -243,8 +243,9 @@ public sealed class HaslerTapes /// /// The runs the tapes came from, the running or latest one first. A tape belongs - /// to the last launch before it was written; one written before any known launch - /// goes with those written about the same time, as a run writes them all at its end. + /// to the last launch before it was written, unless a run the starter didn't launch + /// began between them; one with no launch goes with those written about the same + /// time, as a run writes them all at its end. /// public IReadOnlyList Sessions() { @@ -254,12 +255,11 @@ public sealed class HaslerTapes var tapes = _files.DirectoryExists(Folder) ? _files.GetFiles(Folder, "*.dat").Select(ReadTape).ToList() : new List(); - var behind = tapes.GroupBy(tape => launches.LastOrDefault(l => l.Covers(tape.LastWriteUtc))) + var boxes = ReadBlackBoxes(); + var tapeLaunch = tapes.ToDictionary(tape => tape, tape => LaunchOf(tape, launches, boxes)); + var behind = tapes.GroupBy(tape => tapeLaunch[tape]) .Where(group => group.Key is not null) .ToDictionary(group => group.Key!, group => group.ToList()); - var boxes = _files.DirectoryExists(Folder) - ? _files.GetFiles(Folder, "*.blackbox").Select(ReadBlackBox).ToList() - : new List(); var boxesBehind = boxes.GroupBy(box => LaunchOf(box, launches)) .Where(group => group.Key is not null) .ToDictionary(group => group.Key!, group => group.ToList()); @@ -279,7 +279,7 @@ public sealed class HaslerTapes } // What no launch covers goes together by when it was written: a run writes it all at its end. - var loose = tapes.Where(t => !launches.Any(l => l.Covers(t.LastWriteUtc))) + var loose = tapes.Where(t => tapeLaunch[t] is null) .Select(t => (Written: t.LastWriteUtc, Tape: (TapeFile?)t, Box: (BlackBoxRecording?)null)) .Concat(boxes.Where(b => LaunchOf(b, launches) is null) .Select(b => (Written: b.LastWriteUtc, Tape: (TapeFile?)null, Box: (BlackBoxRecording?)b))) @@ -318,6 +318,27 @@ public sealed class HaslerTapes private TapeFile ReadTape(string path) => new(path, Path.GetFileNameWithoutExtension(path), _files.GetLastWriteTimeUtc(path), _files.GetFileSize(path)); + private List ReadBlackBoxes() => + _files.DirectoryExists(Folder) + ? _files.GetFiles(Folder, "*.blackbox").Select(ReadBlackBox).ToList() + : new List(); + + /// + /// The launch a tape came from. A tape tells nothing of its run but when it was written: the last launch + /// before then, unless a black box shows another run, one the starter didn't launch, began between the two. + /// That run wrote the tape, not the launch's. + /// + public static TapeLaunch? LaunchOf(TapeFile tape, IReadOnlyList launches, IReadOnlyList boxes) + { + var launch = launches.LastOrDefault(l => l.Covers(tape.LastWriteUtc)); + if (launch is null) + return null; + bool another = boxes.Any(box => NamedBy(box.Path) is { } named + && named.StartedUtc > launch.LaunchedUtc && named.StartedUtc < tape.LastWriteUtc + && LaunchOf(box, launches) != launch); + return another ? null : launch; + } + /// /// The launch a black box came from. Unlike a tape, it says which: its name has the scenery and the moment /// the recording started. So it is the last launch before that moment, and only if that launch was of the diff --git a/StarterNG/Views/Tapes/HaslerTapeWindow.cs b/StarterNG/Views/Tapes/HaslerTapeWindow.cs index bfa1ecf..8634740 100644 --- a/StarterNG/Views/Tapes/HaslerTapeWindow.cs +++ b/StarterNG/Views/Tapes/HaslerTapeWindow.cs @@ -121,19 +121,28 @@ public sealed class HaslerTapeWindow : Window }; _list.SelectionChanged += async (_, _) => { - // 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; - var file = _list.SelectedItem switch + if (_list.SelectedItem is TreeViewItem { Tag: TapeSession } node) + node.IsExpanded = true; + // A black box opens in a window of its own on a click or Enter, not when the arrows go over it; the + // tape beside it is one of its session's, the one shown if it is. + var session = _list.SelectedItem switch { - TreeViewItem { Tag: TapeFile tape } => tape, - TreeViewItem { Tag: TapeSession { Tapes.Count: > 0 } run } => run.Tapes[0], + TreeViewItem { Tag: TapeSession run } => run, + TreeViewItem { Tag: BlackBoxRecording box } => + _sessions.FirstOrDefault(s => s.BlackBoxes.Any(b => b.Path == box.Path)), _ => null }; - if (file is null || file == _shown) + if (_list.SelectedItem is TreeViewItem { Tag: BlackBoxRecording } && session?.Tapes.Any(t => t.Path == _shown?.Path) == true) + return; + var file = _list.SelectedItem is TreeViewItem { Tag: TapeFile tape } ? tape : session?.Tapes.FirstOrDefault(); + if (file is null) + { + if (session is not null) + ShowNoTape(); + return; + } + if (file == _shown) return; ApplyContext(_tapes.ContextOf(file)); @@ -586,6 +595,16 @@ public sealed class HaslerTapeWindow : Window _recorder.SelectedIndex = context.Recorder == RecorderKind.Electronic ? 1 : 0; } + /// A session with a black box and no tape: no tape of another session left beside it. + private void ShowNoTape() + { + _shown = null; + _view.Tape = null; + _view.Mark = null; + _cursor.Text = ""; + _summary.Text = App.Loc["HaslerSessionNoTape"]; + } + private async Task ShowAsync(TapeFile file, bool scrollToEnd) { if (_loading) diff --git a/StarterNG/startercfg/lang/cz.xml b/StarterNG/startercfg/lang/cz.xml index b692ad0..4f33134 100644 --- a/StarterNG/startercfg/lang/cz.xml +++ b/StarterNG/startercfg/lang/cz.xml @@ -853,6 +853,7 @@ Černá skříňka Celá relace na mapě: jízda oproti AI, povely, návěstidla, kabina klikněte na pásku: černá skříňka v tom okamžiku + Tato relace nemá pásku, jen černou skříňku. Ve skladu: {0} Vyberte soupravu s vozidly Úroveň jen pro tuto část grafiky diff --git a/StarterNG/startercfg/lang/en.xml b/StarterNG/startercfg/lang/en.xml index e2a479b..051a8f8 100644 --- a/StarterNG/startercfg/lang/en.xml +++ b/StarterNG/startercfg/lang/en.xml @@ -1082,6 +1082,7 @@ Black box The whole session on a map: the driving against the AI, the commands, the signals, the cab click the tape: the black box at that moment + This session has no tape, only the black box. In the warehouse: {0} Pick a consist with vehicles Level for this part of the graphics only diff --git a/StarterNG/startercfg/lang/hu.xml b/StarterNG/startercfg/lang/hu.xml index 105376f..9403a73 100644 --- a/StarterNG/startercfg/lang/hu.xml +++ b/StarterNG/startercfg/lang/hu.xml @@ -853,6 +853,7 @@ Fekete doboz A teljes menet a térképen: vezetés az AI-hoz képest, parancsok, jelzők, vezetőfülke kattintson a szalagra: a fekete doboz abban a pillanatban + Ebben a munkamenetben nincs szalag, csak a fekete doboz. A raktárban: {0} Válassz járműveket tartalmazó szerelvényt Csak a grafika ezen részének szintje diff --git a/StarterNG/startercfg/lang/pl.xml b/StarterNG/startercfg/lang/pl.xml index b1b1884..82db58a 100644 --- a/StarterNG/startercfg/lang/pl.xml +++ b/StarterNG/startercfg/lang/pl.xml @@ -904,6 +904,7 @@ Czarna skrzynka Cała sesja na mapie: jazda względem AI, komendy, sygnalizatory, kabina kliknij taśmę: czarna skrzynka w tym momencie + W tej sesji nie ma taśmy, jest tylko czarna skrzynka. W magazynie: {0} Wybierz skład z pojazdami Poziom tylko tej części grafiki diff --git a/StarterNG/startercfg/lang/ru.xml b/StarterNG/startercfg/lang/ru.xml index 00d9070..8f74f8a 100644 --- a/StarterNG/startercfg/lang/ru.xml +++ b/StarterNG/startercfg/lang/ru.xml @@ -853,6 +853,7 @@ Чёрный ящик Вся сессия на карте: езда по сравнению с ИИ, команды, светофоры, кабина щёлкните по ленте: чёрный ящик в этот момент + В этом сеансе нет ленты, только чёрный ящик. На складе: {0} Выберите состав с транспортом Уровень только для этой части графики