Play back the black box of the simulator: map, cab, timeline, timetable and a summary

The simulator writes a black box of each run to physicslog/*.blackbox
(blackbox_doc.md in the maszyna repository). Three new projects read and
show it:

- Eu07.BlackBox: the reader of the records, the recording as a whole
  (the user's vehicle and consist at a time, the timetable shown, the
  progress along it with arrival and departure delays, whether the
  train stood at the platform), the light patterns Pc 1, Pc 2, Pc 5 and
  Tb 1 with the ones the run asks for, and a summary of the driving.
- Eu07.BlackBox.Views: the player window, with the scenery map (tracks,
  signals, W4 stop points with their heading and W32, the train, the
  cameras), the cab, the timeline of speed against the AI's reference,
  the events, the timetable and the summary.
- Eu07.BlackBox.Viewer: the window on its own, with options for the
  time, following the train and screenshots.

The tapes window of the starter lists the black boxes of each session
beside the Hasler tapes and opens them in the player.
This commit is contained in:
maj00r
2026-11-11 00:39:35 +01:00
parent e7d8596652
commit e530b95352
40 changed files with 6953 additions and 20 deletions

View File

@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- The black box on its own, without the starter: opens the file given on the
command line, or one picked. -->
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
<PublishTrimmed>true</PublishTrimmed>
<IsAotCompatible>true</IsAotCompatible>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationIcon>..\StarterNG\Assets\starter.ico</ApplicationIcon>
<AssemblyName>BlackBoxViewer</AssemblyName>
<RootNamespace>Eu07.BlackBox.Viewer</RootNamespace>
<VersionPrefix>1.0</VersionPrefix>
<Company>eu07.pl</Company>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="12.1.4" />
<PackageReference Include="Avalonia.Desktop" Version="12.1.4" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="12.1.4" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="12.1.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Eu07.BlackBox.Views\Eu07.BlackBox.Views.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,109 @@
using System;
using System.IO;
using System.Linq;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Styling;
using Avalonia.Themes.Fluent;
using Eu07.BlackBox.Views;
namespace Eu07.BlackBox.Viewer;
/// <summary>
/// BlackBoxViewer [file.blackbox] [--live] [--time seconds] [--follow] [--timetable | --summary] [--screenshot out.png]
/// --live follows a file the simulator still writes; --screenshot saves the window
/// at the moment given and quits, for documentation and checks.
/// </summary>
internal static class Program
{
[STAThread]
public static int Main(string[] args)
{
return BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
}
public static AppBuilder BuildAvaloniaApp() =>
AppBuilder.Configure<ViewerApp>()
.UsePlatformDetect()
.WithInterFont();
}
internal sealed class ViewerApp : Application
{
public override void Initialize()
{
Styles.Add(new FluentTheme());
RequestedThemeVariant = ThemeVariant.Dark;
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
var options = ViewerOptions.Parse(desktop.Args ?? Array.Empty<string>());
var window = new BlackBoxWindow { WindowStartupLocation = Avalonia.Controls.WindowStartupLocation.CenterScreen };
window.Live = options.Live;
desktop.MainWindow = window;
window.Opened += async (_, _) =>
{
if (options.File is { } file)
await window.OpenAsync(Path.GetFullPath(file));
window.ShowTimetable = options.Timetable;
if (options.Summary)
window.SummaryShown = true;
if (options.Screenshot is { } shot)
{
await Screenshot.SaveAsync(window, shot, options.Time, options.Follow);
return;
}
if (options.Time is { } time)
window.SeekTo(time);
window.FollowTrain = options.Follow;
};
}
base.OnFrameworkInitializationCompleted();
}
}
internal sealed record ViewerOptions(string? File, bool Live, double? Time, string? Screenshot, bool Follow, bool Timetable,
bool Summary = false)
{
public static ViewerOptions Parse(string[] args)
{
string? file = null, screenshot = null;
double? time = null;
bool live = false, follow = false, timetable = false, summary = false;
for (int i = 0; i < args.Length; i++)
{
switch (args[i])
{
case "--live":
live = true;
break;
case "--follow":
follow = true;
break;
case "--timetable":
timetable = true;
break;
case "--summary":
summary = true;
break;
case "--time" when i + 1 < args.Length &&
double.TryParse(args[i + 1], System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture, out var t):
time = t;
++i;
break;
case "--screenshot" when i + 1 < args.Length:
screenshot = args[++i];
break;
default:
if (!args[i].StartsWith("--", StringComparison.Ordinal))
file ??= args[i];
break;
}
}
return new ViewerOptions(file, live, time, screenshot, follow, timetable, summary);
}
}

View File

@@ -0,0 +1,28 @@
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media.Imaging;
using Eu07.BlackBox.Views;
namespace Eu07.BlackBox.Viewer;
/// <summary>The window as it shows a moment, saved to a PNG; then the viewer closes.</summary>
internal static class Screenshot
{
public static async Task SaveAsync(BlackBoxWindow window, string path, double? time, bool follow)
{
if (time is { } t)
window.SeekTo(t);
window.FollowTrain = follow;
// A few frames for the layout and the first render.
await Task.Delay(1000);
if (window.Content is Control content)
{
var size = new PixelSize((int)content.Bounds.Width, (int)content.Bounds.Height);
using var bitmap = new RenderTargetBitmap(size);
bitmap.Render(content);
bitmap.Save(path, new PngBitmapEncoderOptions());
}
window.Close();
}
}

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<!-- This manifest is used on Windows only.
Don't remove it as it might cause problems with window transparency and embedded controls.
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
<assemblyIdentity version="1.0.0.0" name="Eu07.BlackBoxViewer"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on
and is designed to work with. Uncomment the appropriate elements
and Windows will automatically select the most compatible environment. -->
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>