mirror of
https://github.com/MaSzyna-EU07/StarterNG.git
synced 2026-10-18 08:29:19 +02:00
Load sceneries in parallel and skip the splash on a fast start
Scenery parsing runs on Parallel.ForEach and the reload button does it off the UI thread. The splash window only appears when loading passes 300ms, so a warm start goes straight to the main window. The log button moves into the navigation row, dropping the separate caption strip; timetable and info use the shared MonoFont family instead of a hard-coded Consolas.
This commit is contained in:
@@ -75,25 +75,35 @@ public partial class App : Application
|
||||
KeyboardConfig.Instance.Save();
|
||||
};
|
||||
|
||||
var splash = new SplashWindow();
|
||||
desktop.MainWindow = splash;
|
||||
splash.Show();
|
||||
|
||||
// Progress<T> marshals callbacks back to this (UI) thread.
|
||||
var progress = new Progress<LoadStatus>(splash.Report);
|
||||
SplashWindow? splash = null;
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
var progress = new Progress<LoadStatus>(status =>
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
// Show splash only if loading takes more than 300ms
|
||||
if (sw.ElapsedMilliseconds > 300 && splash == null)
|
||||
{
|
||||
splash = new SplashWindow();
|
||||
desktop.MainWindow = splash;
|
||||
splash.Show();
|
||||
}
|
||||
splash?.Report(status);
|
||||
});
|
||||
});
|
||||
|
||||
GameData.Instance.Load(progress);
|
||||
await Task.Delay(400); // let the completed bar be visible briefly
|
||||
}).ContinueWith(_ =>
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
var main = new MainWindow();
|
||||
desktop.MainWindow = main; // keep a main window before closing the splash
|
||||
desktop.MainWindow = main;
|
||||
main.Show();
|
||||
splash.Close();
|
||||
splash?.Close();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StarterNG.Classes;
|
||||
|
||||
@@ -66,15 +67,26 @@ public sealed class GameData
|
||||
// (length / mass / couplings) resolve without a UI-thread directory scan.
|
||||
Physics.PreloadIndex();
|
||||
|
||||
// Phase 2 - sceneries
|
||||
foreach (string file in scnFiles)
|
||||
{
|
||||
progress?.Report(new LoadStatus((double)done / total, LoadPhase.Sceneries,
|
||||
Path.GetFileName(file)));
|
||||
try { Sceneries.Add(new Scenery(file)); }
|
||||
catch { /* skip unreadable scenery */ }
|
||||
done++;
|
||||
}
|
||||
// Phase 2 - sceneries (parallelized)
|
||||
var sceneryLock = new object();
|
||||
var doneCount = done;
|
||||
Parallel.ForEach(scnFiles, new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount },
|
||||
file =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var scenery = new Scenery(file);
|
||||
lock (sceneryLock)
|
||||
{
|
||||
Sceneries.Add(scenery);
|
||||
doneCount++;
|
||||
progress?.Report(new LoadStatus((double)doneCount / total, LoadPhase.Sceneries,
|
||||
Path.GetFileName(file)));
|
||||
}
|
||||
}
|
||||
catch { /* skip unreadable scenery */ }
|
||||
});
|
||||
done = doneCount;
|
||||
|
||||
progress?.Report(new LoadStatus(1.0, LoadPhase.Done, null));
|
||||
Loaded = true;
|
||||
@@ -84,11 +96,19 @@ public sealed class GameData
|
||||
public void ReloadSceneries(string sceneryDir = "scenery/")
|
||||
{
|
||||
Sceneries.Clear();
|
||||
foreach (string file in EnumerateScenery(sceneryDir))
|
||||
{
|
||||
try { Sceneries.Add(new Scenery(file)); }
|
||||
catch { /* skip */ }
|
||||
}
|
||||
var scnFiles = EnumerateScenery(sceneryDir);
|
||||
var sceneryLock = new object();
|
||||
Parallel.ForEach(scnFiles, new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount },
|
||||
file =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var scenery = new Scenery(file);
|
||||
lock (sceneryLock)
|
||||
Sceneries.Add(scenery);
|
||||
}
|
||||
catch { /* skip */ }
|
||||
});
|
||||
}
|
||||
|
||||
private static List<string> EnumerateScenery(string sceneryDir)
|
||||
|
||||
55
StarterNG/Infrastructure/MonoFont.cs
Normal file
55
StarterNG/Infrastructure/MonoFont.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace StarterNG.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// A fixed-width font family that actually exists on this machine.
|
||||
/// Avalonia treats the extra names in "Consolas, monospace" as glyph fallbacks,
|
||||
/// not as family alternatives, and "monospace" is a fontconfig alias it never
|
||||
/// resolves — so that stack silently renders in the proportional UI font
|
||||
/// everywhere except Windows. The timetable and the fault/preview boxes align
|
||||
/// their columns with spaces, so they need a real monospace family: probe the
|
||||
/// installed ones once and keep the first match.
|
||||
/// </summary>
|
||||
public static class MonoFont
|
||||
{
|
||||
private static readonly string[] Preferred =
|
||||
{
|
||||
"Consolas", // Windows
|
||||
"Menlo", // macOS
|
||||
"SF Mono",
|
||||
"DejaVu Sans Mono", // most Linux distros
|
||||
"Liberation Mono",
|
||||
"Noto Sans Mono",
|
||||
"Ubuntu Mono",
|
||||
"Courier New"
|
||||
};
|
||||
|
||||
public static FontFamily Family { get; } = Resolve();
|
||||
|
||||
private static FontFamily Resolve()
|
||||
{
|
||||
try
|
||||
{
|
||||
var installed = new HashSet<string>(
|
||||
FontManager.Current.SystemFonts.Select(f => f.Name),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (string name in Preferred)
|
||||
if (installed.Contains(name))
|
||||
return new FontFamily(name);
|
||||
|
||||
// None of the usual suspects: take any family that calls itself Mono.
|
||||
string? anyMono = installed.FirstOrDefault(
|
||||
n => n.Contains("Mono", StringComparison.OrdinalIgnoreCase));
|
||||
if (anyMono is not null)
|
||||
return new FontFamily(anyMono);
|
||||
}
|
||||
catch { /* no font manager yet - fall back to the default family */ }
|
||||
|
||||
return FontFamily.Default;
|
||||
}
|
||||
}
|
||||
@@ -28,29 +28,13 @@
|
||||
Background="{StaticResource CarbonPanelBg}"
|
||||
BorderBrush="{StaticResource CarbonBorderLight}"
|
||||
BorderThickness="0,0,0,1">
|
||||
<Grid RowDefinitions="32,48">
|
||||
<Grid RowDefinitions="48">
|
||||
|
||||
<!-- Row 0: title / caption strip. The transparent background spans the
|
||||
whole strip so PointerPressed -> BeginMoveDrag drags the window from
|
||||
anywhere on it (the log button and the OS caption buttons handle their
|
||||
own clicks). The OS min/max/close buttons render at its top-right, so
|
||||
~140px is reserved on the right for them. -->
|
||||
<Grid Grid.Row="0" Background="Transparent"
|
||||
PointerPressed="TitleBar_OnPointerPressed"
|
||||
ColumnDefinitions="*,Auto">
|
||||
<TextBlock Grid.Column="0" Text="Starter MaSzyna" VerticalAlignment="Center"
|
||||
Margin="14,0,0,0" FontSize="11"
|
||||
Foreground="{StaticResource CarbonFgDim}" />
|
||||
|
||||
<Button Grid.Column="1" Classes="Basic" Click="openLastLogClick"
|
||||
VerticalAlignment="Stretch" Margin="0,0,140,0" Padding="10,0" Cursor="Hand"
|
||||
ToolTip.Tip="{Binding [ShowLatestLogTooltip], Source={x:Static starterNg:App.Loc}}">
|
||||
<materialIcons:MaterialIcon Kind="FileDocument" Height="16" Width="16" />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- Row 1: logo + navigation (left) and the right cluster (right) -->
|
||||
<Grid Grid.Row="1" ColumnDefinitions="Auto,Auto,*,Auto">
|
||||
<!-- Logo + navigation (left) and the right cluster (right) -->
|
||||
<!-- Drag-to-move handled by PointerPressed on parent grid -->
|
||||
<Grid ColumnDefinitions="Auto,Auto,*,Auto"
|
||||
Background="Transparent"
|
||||
PointerPressed="TitleBar_OnPointerPressed">
|
||||
|
||||
<!-- Logo -->
|
||||
<Image Grid.Column="0" Source="/Assets/Logo/maszyna_white.png"
|
||||
@@ -70,6 +54,10 @@
|
||||
Tag="settings"
|
||||
IsCheckedChanged="Nav_OnCheckedChanged"
|
||||
Content="{Binding [NavSettings], Source={x:Static starterNg:App.Loc}}" />
|
||||
<Button Classes="Basic" Click="openLastLogClick" VerticalAlignment="Center" Padding="10,0" Cursor="Hand"
|
||||
ToolTip.Tip="{Binding [ShowLatestLogTooltip], Source={x:Static starterNg:App.Loc}}">
|
||||
<materialIcons:MaterialIcon Kind="FileDocument" Height="16" Width="16" />
|
||||
</Button>
|
||||
<Button x:Name="HelpButton" Classes="Basic" Width="48" Cursor="Hand"
|
||||
ToolTip.Tip="{Binding [Help], Source={x:Static starterNg:App.Loc}}"
|
||||
Click="HelpButton_OnClick"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"profiles": {
|
||||
"StarterNG": {
|
||||
"commandName": "Project",
|
||||
"workingDirectory": "E:\\Hirek\\Games\\Steam\\steamapps\\common\\MaSzyna"
|
||||
"workingDirectory": "/home/maj00r/.local/share/Steam/steamapps/common/MaSzyna/"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:starterNg="clr-namespace:StarterNG"
|
||||
xmlns:infra="clr-namespace:StarterNG.Infrastructure"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="StarterNG.Views.Scenarios"
|
||||
x:CompileBindings="True">
|
||||
@@ -28,7 +29,7 @@
|
||||
OnContent="{Binding [ShowArchival], Source={x:Static starterNg:App.Loc}}"
|
||||
OffContent="{Binding [ShowArchival], Source={x:Static starterNg:App.Loc}}" />
|
||||
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Spacing="8" Margin="2,4,0,0">
|
||||
<Button Classes="Flat" FontSize="11" Padding="8,2" Cursor="Hand"
|
||||
<Button x:Name="reloadButton" Classes="Flat" FontSize="11" Padding="8,2" Cursor="Hand"
|
||||
Click="ReloadSceneries_OnClick"
|
||||
Content="{Binding [ReloadSceneries], Source={x:Static starterNg:App.Loc}}" />
|
||||
<Button Classes="Flat" FontSize="11" Padding="8,2" Cursor="Hand"
|
||||
@@ -158,15 +159,16 @@
|
||||
</TabItem>
|
||||
<TabItem Header="{Binding [Timetable], Source={x:Static starterNg:App.Loc}}">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
|
||||
<TextBlock Name="timetableContent" Margin="4" FontFamily="Consolas, monospace"
|
||||
FontSize="12" />
|
||||
<TextBlock Name="timetableContent" Margin="4"
|
||||
FontFamily="{x:Static infra:MonoFont.Family}"
|
||||
FontSize="12" TextWrapping="NoWrap" />
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
<TabItem Name="infoTab" Header="{Binding [TabInfo], Source={x:Static starterNg:App.Loc}}">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<TextBox Name="faultListBox" Margin="4" IsReadOnly="True"
|
||||
AcceptsReturn="True" TextWrapping="Wrap"
|
||||
FontFamily="Consolas, monospace" FontSize="12" />
|
||||
FontFamily="{x:Static infra:MonoFont.Family}" FontSize="12" />
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
@@ -488,18 +489,26 @@ public partial class Scenarios : UserControl
|
||||
ShowSceneryInfo(AppState.Instance.CurrentScenery);
|
||||
}
|
||||
|
||||
private void ReloadSceneries_OnClick(object? sender, RoutedEventArgs e)
|
||||
private async void ReloadSceneries_OnClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
string? selected = AppState.Instance.CurrentScenery?.Path;
|
||||
GameData.Instance.ReloadSceneries();
|
||||
Sceneries = GameData.Instance.Sceneries;
|
||||
BuildSceneryTree();
|
||||
if (!string.IsNullOrEmpty(selected))
|
||||
reloadButton.IsEnabled = false;
|
||||
try
|
||||
{
|
||||
StarterNG.Classes.Settings.Instance.LastScenery =
|
||||
Path.GetFileNameWithoutExtension(selected);
|
||||
string? selected = AppState.Instance.CurrentScenery?.Path;
|
||||
await Task.Run(() => GameData.Instance.ReloadSceneries());
|
||||
Sceneries = GameData.Instance.Sceneries;
|
||||
BuildSceneryTree();
|
||||
if (!string.IsNullOrEmpty(selected))
|
||||
{
|
||||
StarterNG.Classes.Settings.Instance.LastScenery =
|
||||
Path.GetFileNameWithoutExtension(selected);
|
||||
}
|
||||
RestoreLastScenery();
|
||||
}
|
||||
finally
|
||||
{
|
||||
reloadButton.IsEnabled = true;
|
||||
}
|
||||
RestoreLastScenery();
|
||||
}
|
||||
|
||||
private void OpenSceneryDir_OnClick(object? sender, RoutedEventArgs e)
|
||||
|
||||
Reference in New Issue
Block a user