mirror of
https://github.com/MaSzyna-EU07/StarterNG.git
synced 2026-11-06 05:38:35 +01: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();
|
KeyboardConfig.Instance.Save();
|
||||||
};
|
};
|
||||||
|
|
||||||
var splash = new SplashWindow();
|
SplashWindow? splash = null;
|
||||||
desktop.MainWindow = splash;
|
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||||
splash.Show();
|
|
||||||
|
|
||||||
// Progress<T> marshals callbacks back to this (UI) thread.
|
|
||||||
var progress = new Progress<LoadStatus>(splash.Report);
|
|
||||||
|
|
||||||
Task.Run(async () =>
|
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);
|
GameData.Instance.Load(progress);
|
||||||
await Task.Delay(400); // let the completed bar be visible briefly
|
|
||||||
}).ContinueWith(_ =>
|
}).ContinueWith(_ =>
|
||||||
{
|
{
|
||||||
Dispatcher.UIThread.Post(() =>
|
Dispatcher.UIThread.Post(() =>
|
||||||
{
|
{
|
||||||
var main = new MainWindow();
|
var main = new MainWindow();
|
||||||
desktop.MainWindow = main; // keep a main window before closing the splash
|
desktop.MainWindow = main;
|
||||||
main.Show();
|
main.Show();
|
||||||
splash.Close();
|
splash?.Close();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace StarterNG.Classes;
|
namespace StarterNG.Classes;
|
||||||
|
|
||||||
@@ -66,15 +67,26 @@ public sealed class GameData
|
|||||||
// (length / mass / couplings) resolve without a UI-thread directory scan.
|
// (length / mass / couplings) resolve without a UI-thread directory scan.
|
||||||
Physics.PreloadIndex();
|
Physics.PreloadIndex();
|
||||||
|
|
||||||
// Phase 2 - sceneries
|
// Phase 2 - sceneries (parallelized)
|
||||||
foreach (string file in scnFiles)
|
var sceneryLock = new object();
|
||||||
{
|
var doneCount = done;
|
||||||
progress?.Report(new LoadStatus((double)done / total, LoadPhase.Sceneries,
|
Parallel.ForEach(scnFiles, new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount },
|
||||||
Path.GetFileName(file)));
|
file =>
|
||||||
try { Sceneries.Add(new Scenery(file)); }
|
{
|
||||||
catch { /* skip unreadable scenery */ }
|
try
|
||||||
done++;
|
{
|
||||||
}
|
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));
|
progress?.Report(new LoadStatus(1.0, LoadPhase.Done, null));
|
||||||
Loaded = true;
|
Loaded = true;
|
||||||
@@ -84,11 +96,19 @@ public sealed class GameData
|
|||||||
public void ReloadSceneries(string sceneryDir = "scenery/")
|
public void ReloadSceneries(string sceneryDir = "scenery/")
|
||||||
{
|
{
|
||||||
Sceneries.Clear();
|
Sceneries.Clear();
|
||||||
foreach (string file in EnumerateScenery(sceneryDir))
|
var scnFiles = EnumerateScenery(sceneryDir);
|
||||||
{
|
var sceneryLock = new object();
|
||||||
try { Sceneries.Add(new Scenery(file)); }
|
Parallel.ForEach(scnFiles, new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount },
|
||||||
catch { /* skip */ }
|
file =>
|
||||||
}
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var scenery = new Scenery(file);
|
||||||
|
lock (sceneryLock)
|
||||||
|
Sceneries.Add(scenery);
|
||||||
|
}
|
||||||
|
catch { /* skip */ }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<string> EnumerateScenery(string sceneryDir)
|
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}"
|
Background="{StaticResource CarbonPanelBg}"
|
||||||
BorderBrush="{StaticResource CarbonBorderLight}"
|
BorderBrush="{StaticResource CarbonBorderLight}"
|
||||||
BorderThickness="0,0,0,1">
|
BorderThickness="0,0,0,1">
|
||||||
<Grid RowDefinitions="32,48">
|
<Grid RowDefinitions="48">
|
||||||
|
|
||||||
<!-- Row 0: title / caption strip. The transparent background spans the
|
<!-- Logo + navigation (left) and the right cluster (right) -->
|
||||||
whole strip so PointerPressed -> BeginMoveDrag drags the window from
|
<!-- Drag-to-move handled by PointerPressed on parent grid -->
|
||||||
anywhere on it (the log button and the OS caption buttons handle their
|
<Grid ColumnDefinitions="Auto,Auto,*,Auto"
|
||||||
own clicks). The OS min/max/close buttons render at its top-right, so
|
Background="Transparent"
|
||||||
~140px is reserved on the right for them. -->
|
PointerPressed="TitleBar_OnPointerPressed">
|
||||||
<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 -->
|
<!-- Logo -->
|
||||||
<Image Grid.Column="0" Source="/Assets/Logo/maszyna_white.png"
|
<Image Grid.Column="0" Source="/Assets/Logo/maszyna_white.png"
|
||||||
@@ -70,6 +54,10 @@
|
|||||||
Tag="settings"
|
Tag="settings"
|
||||||
IsCheckedChanged="Nav_OnCheckedChanged"
|
IsCheckedChanged="Nav_OnCheckedChanged"
|
||||||
Content="{Binding [NavSettings], Source={x:Static starterNg:App.Loc}}" />
|
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"
|
<Button x:Name="HelpButton" Classes="Basic" Width="48" Cursor="Hand"
|
||||||
ToolTip.Tip="{Binding [Help], Source={x:Static starterNg:App.Loc}}"
|
ToolTip.Tip="{Binding [Help], Source={x:Static starterNg:App.Loc}}"
|
||||||
Click="HelpButton_OnClick"
|
Click="HelpButton_OnClick"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"profiles": {
|
"profiles": {
|
||||||
"StarterNG": {
|
"StarterNG": {
|
||||||
"commandName": "Project",
|
"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:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:starterNg="clr-namespace:StarterNG"
|
xmlns:starterNg="clr-namespace:StarterNG"
|
||||||
|
xmlns:infra="clr-namespace:StarterNG.Infrastructure"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="StarterNG.Views.Scenarios"
|
x:Class="StarterNG.Views.Scenarios"
|
||||||
x:CompileBindings="True">
|
x:CompileBindings="True">
|
||||||
@@ -28,7 +29,7 @@
|
|||||||
OnContent="{Binding [ShowArchival], Source={x:Static starterNg:App.Loc}}"
|
OnContent="{Binding [ShowArchival], Source={x:Static starterNg:App.Loc}}"
|
||||||
OffContent="{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">
|
<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"
|
Click="ReloadSceneries_OnClick"
|
||||||
Content="{Binding [ReloadSceneries], Source={x:Static starterNg:App.Loc}}" />
|
Content="{Binding [ReloadSceneries], Source={x:Static starterNg:App.Loc}}" />
|
||||||
<Button Classes="Flat" FontSize="11" Padding="8,2" Cursor="Hand"
|
<Button Classes="Flat" FontSize="11" Padding="8,2" Cursor="Hand"
|
||||||
@@ -158,15 +159,16 @@
|
|||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem Header="{Binding [Timetable], Source={x:Static starterNg:App.Loc}}">
|
<TabItem Header="{Binding [Timetable], Source={x:Static starterNg:App.Loc}}">
|
||||||
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
|
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
|
||||||
<TextBlock Name="timetableContent" Margin="4" FontFamily="Consolas, monospace"
|
<TextBlock Name="timetableContent" Margin="4"
|
||||||
FontSize="12" />
|
FontFamily="{x:Static infra:MonoFont.Family}"
|
||||||
|
FontSize="12" TextWrapping="NoWrap" />
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem Name="infoTab" Header="{Binding [TabInfo], Source={x:Static starterNg:App.Loc}}">
|
<TabItem Name="infoTab" Header="{Binding [TabInfo], Source={x:Static starterNg:App.Loc}}">
|
||||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
<TextBox Name="faultListBox" Margin="4" IsReadOnly="True"
|
<TextBox Name="faultListBox" Margin="4" IsReadOnly="True"
|
||||||
AcceptsReturn="True" TextWrapping="Wrap"
|
AcceptsReturn="True" TextWrapping="Wrap"
|
||||||
FontFamily="Consolas, monospace" FontSize="12" />
|
FontFamily="{x:Static infra:MonoFont.Family}" FontSize="12" />
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
</TabControl>
|
</TabControl>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
@@ -488,18 +489,26 @@ public partial class Scenarios : UserControl
|
|||||||
ShowSceneryInfo(AppState.Instance.CurrentScenery);
|
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;
|
reloadButton.IsEnabled = false;
|
||||||
GameData.Instance.ReloadSceneries();
|
try
|
||||||
Sceneries = GameData.Instance.Sceneries;
|
|
||||||
BuildSceneryTree();
|
|
||||||
if (!string.IsNullOrEmpty(selected))
|
|
||||||
{
|
{
|
||||||
StarterNG.Classes.Settings.Instance.LastScenery =
|
string? selected = AppState.Instance.CurrentScenery?.Path;
|
||||||
Path.GetFileNameWithoutExtension(selected);
|
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)
|
private void OpenSceneryDir_OnClick(object? sender, RoutedEventArgs e)
|
||||||
|
|||||||
Reference in New Issue
Block a user