From 4235a2f6d35b12005b500dab4744e86602deb01a Mon Sep 17 00:00:00 2001 From: Slacker <288981238+Slacker86@users.noreply.github.com> Date: Fri, 29 May 2026 17:40:22 +0200 Subject: [PATCH 01/94] PD2Launcherv2: Remove bogus Services directory from the project --- PD2Launcherv2/PD2Launcherv2.csproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/PD2Launcherv2/PD2Launcherv2.csproj b/PD2Launcherv2/PD2Launcherv2.csproj index 4ac15617..d6a29256 100644 --- a/PD2Launcherv2/PD2Launcherv2.csproj +++ b/PD2Launcherv2/PD2Launcherv2.csproj @@ -117,10 +117,6 @@ - - - - From 0c2128e146c809a4a07d38cdcba04da21f8d5ce6 Mon Sep 17 00:00:00 2001 From: Slacker <288981238+Slacker86@users.noreply.github.com> Date: Fri, 29 May 2026 17:40:22 +0200 Subject: [PATCH 02/94] PD2Launcherv2: Decouple IsDisableUpdates from Messages.ConfigurationChangeMessage ...where it clearly doesn't belong, and put it in its own LauncherOptionsChangeMessage. Additionally, extract 'disableAutoUpdate' from PD2Shared.Models.LauncherArgs and place it in its own PD2Shared.Models.LauncherOptions. Finally, update OptionsView to place 'DisableAutoUpdate' under its own category: 'Launcher'. --- PD2Launcherv2/App.xaml.cs | 4 +- PD2Launcherv2/MainWindow.xaml.cs | 40 ++++++++++++++----- .../Messages/ConfigurationChangeMessage.cs | 1 - .../Messages/LauncherOptionsChangeMessage.cs | 7 ++++ PD2Launcherv2/ViewModels/AboutViewModel.cs | 13 ++---- PD2Launcherv2/ViewModels/OptionsViewModel.cs | 32 ++++++++++++--- PD2Launcherv2/Views/OptionsView.xaml | 10 ++++- PD2Shared/GameUpdateManager.cs | 4 +- PD2Shared/Models/AllSettings.cs | 1 + PD2Shared/Models/LauncherArgs.cs | 1 - PD2Shared/Models/LauncherOptions.cs | 7 ++++ PD2Shared/Models/StorageKey.cs | 1 + PD2Shared/Storage/LocalStorage.cs | 7 ++++ SteamPD2/Program.cs | 4 +- 14 files changed, 98 insertions(+), 34 deletions(-) create mode 100644 PD2Launcherv2/Messages/LauncherOptionsChangeMessage.cs create mode 100644 PD2Shared/Models/LauncherOptions.cs diff --git a/PD2Launcherv2/App.xaml.cs b/PD2Launcherv2/App.xaml.cs index eb007f6a..926258f8 100644 --- a/PD2Launcherv2/App.xaml.cs +++ b/PD2Launcherv2/App.xaml.cs @@ -110,8 +110,8 @@ protected override async void OnStartup(StartupEventArgs e) await filterHelpers.CheckAndUpdateFilterAsync(selected); } - var launcherArgs = localStorage.LoadSection(StorageKey.LauncherArgs); - if (!launcherArgs.disableAutoUpdate) + var launcherOptions = localStorage.LoadSection(StorageKey.LauncherOptions); + if (!launcherOptions.DisableAutoUpdate) { try { diff --git a/PD2Launcherv2/MainWindow.xaml.cs b/PD2Launcherv2/MainWindow.xaml.cs index 13c4a325..7ba65a0b 100644 --- a/PD2Launcherv2/MainWindow.xaml.cs +++ b/PD2Launcherv2/MainWindow.xaml.cs @@ -93,6 +93,21 @@ public Visibility CustomVisibility } } + private bool _isDisableUpdates; + public bool IsDisableUpdates + { + get => _isDisableUpdates; + set + { + if (_isDisableUpdates != value) + { + _isDisableUpdates = value; + UpdatesNotificationVisibility = value ? Visibility.Visible : Visibility.Collapsed; + OnPropertyChanged(nameof(IsDisableUpdates)); + } + } + } + private Visibility _updatesNotificationVisibility = Visibility.Collapsed; public Visibility UpdatesNotificationVisibility { @@ -108,7 +123,6 @@ public Visibility UpdatesNotificationVisibility } public List NewsItems { get; set; } - public bool IsDisableUpdates { get; private set; } public ICommand OpenOptionsCommand { get; private set; } public ICommand OpenLootCommand { get; private set; } public ICommand OpenAboutCommand { get; private set; } @@ -133,10 +147,12 @@ public MainWindow() EnsureWindowIsVisible(); Loaded += MainWindow_Loaded; LoadConfiguration(); + LoadOptions(); // Registering to receive NavigationMessage Messenger.Default.Register(this, OnNavigationMessageReceived); Messenger.Default.Register(this, OnConfigurationChanged); + Messenger.Default.Register(this, OnLauncherOptionsChanged); DataContext = this; this.Closed += MainWindow_Closed; @@ -251,8 +267,8 @@ private async void PlayButton_Click(object sender, RoutedEventArgs e) bool isUpdated = await _filterHelpers.CheckAndUpdateFilterAsync(selectedAuthorAndFilter); } - LauncherArgs launcherArgs = _localStorage.LoadSection(StorageKey.LauncherArgs); - if (!launcherArgs.disableAutoUpdate) + LauncherOptions launcherOptions = _localStorage.LoadSection(StorageKey.LauncherOptions); + if (!launcherOptions.DisableAutoUpdate) { try { @@ -402,13 +418,14 @@ private void TextBlock_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) private void LoadConfiguration() { var fileUpdateModel = _localStorage.LoadSection(StorageKey.FileUpdateModel); - var launcherArgs = _localStorage.LoadSection(StorageKey.LauncherArgs); IsBeta = fileUpdateModel?.FilePath == "Beta"; IsCustom = fileUpdateModel?.FilePath == "Custom"; - IsDisableUpdates = launcherArgs?.disableAutoUpdate == true; + } - // Use property to control visibility - UpdatesNotificationVisibility = IsDisableUpdates ? Visibility.Visible : Visibility.Collapsed; + private void LoadOptions() + { + var launcherOptions = _localStorage.LoadSection(StorageKey.LauncherOptions); + IsDisableUpdates = launcherOptions?.DisableAutoUpdate == true; } private void OnConfigurationChanged(ConfigurationChangeMessage message) @@ -417,8 +434,12 @@ private void OnConfigurationChanged(ConfigurationChangeMessage message) OnPropertyChanged(nameof(IsBeta)); IsCustom = message.IsCustom; OnPropertyChanged(nameof(IsCustom)); - // Use property to control visibility - UpdatesNotificationVisibility = message.IsDisableUpdates ? Visibility.Visible : Visibility.Collapsed; + } + + private void OnLauncherOptionsChanged(LauncherOptionsChangeMessage message) + { + IsDisableUpdates = message.DisableAutoUpdate; + OnPropertyChanged(nameof(IsDisableUpdates)); } private async void MainWindow_Loaded(object sender, RoutedEventArgs e) @@ -548,6 +569,7 @@ public void InitializeDefaultSettings(ILocalStorage localStorage) _localStorage.InitializeIfNotExists(StorageKey.FileUpdateModel, new FileUpdateModel()); _localStorage.InitializeIfNotExists(StorageKey.DdrawOptions, new DdrawOptions()); _localStorage.InitializeIfNotExists(StorageKey.LauncherArgs, new LauncherArgs()); + _localStorage.InitializeIfNotExists(StorageKey.LauncherOptions, new LauncherOptions()); _localStorage.InitializeIfNotExists(StorageKey.SelectedAuthorAndFilter, new SelectedAuthorAndFilter()); _localStorage.InitializeIfNotExists(StorageKey.Pd2AuthorList, new Pd2AuthorList()); _localStorage.InitializeIfNotExists(StorageKey.News, new News()); diff --git a/PD2Launcherv2/Messages/ConfigurationChangeMessage.cs b/PD2Launcherv2/Messages/ConfigurationChangeMessage.cs index 518d8a1e..5eb308fc 100644 --- a/PD2Launcherv2/Messages/ConfigurationChangeMessage.cs +++ b/PD2Launcherv2/Messages/ConfigurationChangeMessage.cs @@ -5,6 +5,5 @@ public class ConfigurationChangeMessage { public bool IsBeta { get; set; } public bool IsCustom { get; set; } - public bool IsDisableUpdates { get; set; } } } \ No newline at end of file diff --git a/PD2Launcherv2/Messages/LauncherOptionsChangeMessage.cs b/PD2Launcherv2/Messages/LauncherOptionsChangeMessage.cs new file mode 100644 index 00000000..8c647dab --- /dev/null +++ b/PD2Launcherv2/Messages/LauncherOptionsChangeMessage.cs @@ -0,0 +1,7 @@ +namespace PD2Launcherv2.Messages +{ + public class LauncherOptionsChangeMessage + { + public bool DisableAutoUpdate { get; set; } + } +} diff --git a/PD2Launcherv2/ViewModels/AboutViewModel.cs b/PD2Launcherv2/ViewModels/AboutViewModel.cs index f3134998..620585b6 100644 --- a/PD2Launcherv2/ViewModels/AboutViewModel.cs +++ b/PD2Launcherv2/ViewModels/AboutViewModel.cs @@ -74,8 +74,7 @@ public void ProdBucketAssign() FilePath = "Live" }; _localStorage.Update(StorageKey.FileUpdateModel, fileUpdateModel); - var launcherArgs = _localStorage.LoadSection(StorageKey.LauncherArgs); - Messenger.Default.Send(new ConfigurationChangeMessage { IsBeta = false , IsCustom = false, IsDisableUpdates = launcherArgs.disableAutoUpdate}); + Messenger.Default.Send(new ConfigurationChangeMessage { IsBeta = false , IsCustom = false }); Debug.WriteLine("end ProdBucketAssign\n"); Messenger.Default.Send(new NavigationMessage { Action = NavigationAction.GoBack }); } @@ -88,9 +87,8 @@ public void BetaBucketAssign() FilePath = "Beta" }; _localStorage.Update(StorageKey.FileUpdateModel, fileUpdateModel); - var launcherArgs = _localStorage.LoadSection(StorageKey.LauncherArgs); - Messenger.Default.Send(new ConfigurationChangeMessage { IsBeta = true , IsCustom = false , IsDisableUpdates = launcherArgs.disableAutoUpdate }); + Messenger.Default.Send(new ConfigurationChangeMessage { IsBeta = true , IsCustom = false }); Debug.WriteLine("end BetaBucketAssign \n"); Messenger.Default.Send(new NavigationMessage { Action = NavigationAction.GoBack }); } @@ -118,15 +116,12 @@ private void CustomBucketAssign() _localStorage.Update(StorageKey.FileUpdateModel, fileUpdateModel); - var launcherArgs = _localStorage.LoadSection(StorageKey.LauncherArgs); - Messenger.Default.Send(new ConfigurationChangeMessage { IsBeta = false, - IsCustom = false, - IsDisableUpdates = launcherArgs.disableAutoUpdate + IsCustom = false }); - Messenger.Default.Send(new ConfigurationChangeMessage { IsBeta = false, IsCustom = true, IsDisableUpdates = launcherArgs.disableAutoUpdate }); + Messenger.Default.Send(new ConfigurationChangeMessage { IsBeta = false, IsCustom = true }); Debug.WriteLine("end SetCustomEnvironment\n"); Messenger.Default.Send(new NavigationMessage { Action = NavigationAction.GoBack }); } diff --git a/PD2Launcherv2/ViewModels/OptionsViewModel.cs b/PD2Launcherv2/ViewModels/OptionsViewModel.cs index f6de3757..8905a671 100644 --- a/PD2Launcherv2/ViewModels/OptionsViewModel.cs +++ b/PD2Launcherv2/ViewModels/OptionsViewModel.cs @@ -40,6 +40,7 @@ public OptionsViewModel(ILocalStorage localStorage) MinFpsPickerItems = Constants.MinFpsPickerItems(); ShaderPickerItems = Constants.ShaderPickerItems(); LoadLauncherArgs(); + LoadLauncherOptions(); LoadDDrawStorage(); DealWithLoadingModeComboBox(_localStorage); CloseCommand = new RelayCommand(CloseView); @@ -513,9 +514,7 @@ public bool AutoUpdate Debug.WriteLine($"Set _autoUpdate {value}"); _autoUpdate = value; OnPropertyChanged(); - var fileUpdateMode = _localStorage.LoadSection(StorageKey.FileUpdateModel); - bool amIBeta = fileUpdateMode.FilePath.Equals("Beta"); - Messenger.Default.Send(new ConfigurationChangeMessage { IsDisableUpdates = value, IsBeta = amIBeta }); + Messenger.Default.Send(new LauncherOptionsChangeMessage { DisableAutoUpdate = value }); } } } @@ -600,11 +599,21 @@ private void LoadLauncherArgs() IsDdrawSelected = launcherArgs.graphics; SkipToBnet = launcherArgs.skiptobnet; SndBkg = launcherArgs.sndbkg; - AutoUpdate = launcherArgs.disableAutoUpdate; } Debug.WriteLine("end LoadLauncherArgs\n"); } + private void LoadLauncherOptions() + { + Debug.WriteLine("\nStart LoadLauncherOptions"); + LauncherOptions launcherOptions = _localStorage.LoadSection(StorageKey.LauncherOptions); + if (launcherOptions != null) + { + AutoUpdate = launcherOptions.DisableAutoUpdate; + } + Debug.WriteLine("end LoadLauncherOptions\n"); + } + private void UpdateLauncherArgsStorage() { Debug.WriteLine("\nStart UpdateLauncherArgsStorage"); @@ -613,13 +622,23 @@ private void UpdateLauncherArgsStorage() // Again, assuming true represents "ddraw" graphics = IsDdrawSelected, skiptobnet = SkipToBnet, - sndbkg = SndBkg, - disableAutoUpdate = AutoUpdate, + sndbkg = SndBkg }; _localStorage.Update(StorageKey.LauncherArgs, launcherArgs); Debug.WriteLine("end UpdateLauncherArgsStorage\n"); } + private void UpdateLauncherOptionsStorage() + { + Debug.WriteLine("\nStart UpdateLauncherOptionsStorage"); + var launcherOptions = new LauncherOptions + { + DisableAutoUpdate = AutoUpdate + }; + _localStorage.Update(StorageKey.LauncherOptions, launcherOptions); + Debug.WriteLine("end UpdateLauncherOptionsStorage\n"); + } + private void LoadDDrawCheckBoxOptions() { DdrawOptions dDrawOptions = _localStorage.LoadSection(StorageKey.DdrawOptions); @@ -769,6 +788,7 @@ private void CloseView() { //save LauncherArgs Storage UpdateLauncherArgsStorage(); + UpdateLauncherOptionsStorage(); //save UpdateDDrawStorage(); //write ddrawstorage to .ini diff --git a/PD2Launcherv2/Views/OptionsView.xaml b/PD2Launcherv2/Views/OptionsView.xaml index da50f113..b8790116 100644 --- a/PD2Launcherv2/Views/OptionsView.xaml +++ b/PD2Launcherv2/Views/OptionsView.xaml @@ -67,9 +67,15 @@ + + + + + FontFamily="{StaticResource BlizzMedium}" Foreground="{StaticResource GoldBrush}" IsChecked="{Binding AutoUpdate, Mode=TwoWay}" + FontSize="12" Margin="10,10,10,0"/> diff --git a/PD2Shared/GameUpdateManager.cs b/PD2Shared/GameUpdateManager.cs index 3714000a..2c8f23e8 100644 --- a/PD2Shared/GameUpdateManager.cs +++ b/PD2Shared/GameUpdateManager.cs @@ -32,8 +32,8 @@ public static async Task RunUpdateAndLaunchAsync( } } - var launcherArgs = storage.LoadSection(StorageKey.LauncherArgs); - if (launcherArgs != null && launcherArgs.disableAutoUpdate) + var launcherOptions = storage.LoadSection(StorageKey.LauncherOptions); + if (launcherOptions?.DisableAutoUpdate == true) { gameLauncher.LaunchGame(storage); return; diff --git a/PD2Shared/Models/AllSettings.cs b/PD2Shared/Models/AllSettings.cs index a1f263cb..89370155 100644 --- a/PD2Shared/Models/AllSettings.cs +++ b/PD2Shared/Models/AllSettings.cs @@ -8,6 +8,7 @@ public class AllSettings public SelectedAuthorAndFilter SelectedAuthorAndFilter { get; set; } public Pd2AuthorList Pd2AuthorList { get; set; } public LauncherArgs LauncherArgs { get; set; } + public LauncherOptions LauncherOptions { get; set; } public News News { get; set; } public WindowPositionModel WindowPosition { get; set; } public ResetInfo ResetInfo { get; set; } diff --git a/PD2Shared/Models/LauncherArgs.cs b/PD2Shared/Models/LauncherArgs.cs index be281248..ce24ed86 100644 --- a/PD2Shared/Models/LauncherArgs.cs +++ b/PD2Shared/Models/LauncherArgs.cs @@ -5,6 +5,5 @@ public class LauncherArgs public bool graphics { get; set; } = false; public bool skiptobnet { get; set; } = true; public bool sndbkg { get; set; } = false; - public bool disableAutoUpdate { get; set; } = false; } } \ No newline at end of file diff --git a/PD2Shared/Models/LauncherOptions.cs b/PD2Shared/Models/LauncherOptions.cs new file mode 100644 index 00000000..52ef5cbd --- /dev/null +++ b/PD2Shared/Models/LauncherOptions.cs @@ -0,0 +1,7 @@ +namespace PD2Shared.Models +{ + public class LauncherOptions + { + public bool DisableAutoUpdate { get; set; } = false; + } +} diff --git a/PD2Shared/Models/StorageKey.cs b/PD2Shared/Models/StorageKey.cs index cbbec05a..8c2e96c5 100644 --- a/PD2Shared/Models/StorageKey.cs +++ b/PD2Shared/Models/StorageKey.cs @@ -4,6 +4,7 @@ namespace PD2Shared.Models public enum StorageKey { LauncherArgs, + LauncherOptions, DdrawOptions, FileUpdateModel, Pd2AuthorList, diff --git a/PD2Shared/Storage/LocalStorage.cs b/PD2Shared/Storage/LocalStorage.cs index 051c1df6..ee9c0388 100644 --- a/PD2Shared/Storage/LocalStorage.cs +++ b/PD2Shared/Storage/LocalStorage.cs @@ -68,6 +68,9 @@ public void Update(StorageKey key, T value) where T : class case StorageKey.LauncherArgs: settings.LauncherArgs = value as LauncherArgs ?? new LauncherArgs(); break; + case StorageKey.LauncherOptions: + settings.LauncherOptions = value as LauncherOptions ?? new LauncherOptions(); + break; case StorageKey.DdrawOptions: settings.DdrawOptions = value as DdrawOptions ?? new DdrawOptions(); break; @@ -128,6 +131,7 @@ public T LoadSection(StorageKey key) where T : class return key switch { StorageKey.LauncherArgs => settings.LauncherArgs as T ?? Activator.CreateInstance(), + StorageKey.LauncherOptions => settings.LauncherOptions as T ?? Activator.CreateInstance(), StorageKey.DdrawOptions => settings.DdrawOptions as T ?? Activator.CreateInstance(), StorageKey.FileUpdateModel => settings.FileUpdateModel as T ?? Activator.CreateInstance(), StorageKey.Pd2AuthorList => settings.Pd2AuthorList as T ?? Activator.CreateInstance(), @@ -160,6 +164,9 @@ public T LoadSection(StorageKey key) where T : class case StorageKey.LauncherArgs: settings.LauncherArgs = defaultValue as LauncherArgs ?? new LauncherArgs(); break; + case StorageKey.LauncherOptions: + settings.LauncherOptions = defaultValue as LauncherOptions ?? new LauncherOptions(); + break; case StorageKey.DdrawOptions: settings.DdrawOptions = defaultValue as DdrawOptions ?? new DdrawOptions(); break; diff --git a/SteamPD2/Program.cs b/SteamPD2/Program.cs index 17368fa0..8936d986 100644 --- a/SteamPD2/Program.cs +++ b/SteamPD2/Program.cs @@ -36,8 +36,8 @@ static async Task Run(string[] args) var filterHelpers = new FilterHelpers(new HttpClient(), localStorage); var launchGameHelpers = new LaunchGameHelpers(); - var launcherArgs = localStorage.LoadSection(StorageKey.LauncherArgs); - if (launcherArgs?.disableAutoUpdate == true) + var launcherOptions = localStorage.LoadSection(StorageKey.LauncherOptions); + if (launcherOptions?.DisableAutoUpdate == true) { Log("disableAutoUpdate is enabled. Skipping all update checks."); launchGameHelpers.LaunchGame(localStorage); From 04159a7fa35ae44b2ed44ca3a76c81a0f81f10d8 Mon Sep 17 00:00:00 2001 From: Slacker <288981238+Slacker86@users.noreply.github.com> Date: Fri, 29 May 2026 17:40:22 +0200 Subject: [PATCH 03/94] PD2Launcherv2: Colors: Introduce Red* set of colors and derived brushes and use them immediately --- PD2Launcherv2/Resources/Styles/Colors.xaml | 6 ++++++ PD2Launcherv2/Views/OptionsView.xaml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/PD2Launcherv2/Resources/Styles/Colors.xaml b/PD2Launcherv2/Resources/Styles/Colors.xaml index 54690ae6..7e8b2172 100644 --- a/PD2Launcherv2/Resources/Styles/Colors.xaml +++ b/PD2Launcherv2/Resources/Styles/Colors.xaml @@ -5,10 +5,16 @@ #FFA78E65 #FFB59E76 #FF947A56 + #FFA7656D + #FFB5767D + #FF945661 #007759 + + + #998363 #b39a74 diff --git a/PD2Launcherv2/Views/OptionsView.xaml b/PD2Launcherv2/Views/OptionsView.xaml index b8790116..87fe141e 100644 --- a/PD2Launcherv2/Views/OptionsView.xaml +++ b/PD2Launcherv2/Views/OptionsView.xaml @@ -76,7 +76,7 @@ - + From d66199442886143fac248bb7956deb16e23c853a Mon Sep 17 00:00:00 2001 From: Slacker <288981238+Slacker86@users.noreply.github.com> Date: Fri, 29 May 2026 22:57:14 +0200 Subject: [PATCH 04/94] PD2Launcherv2: Views.OptionsView: Put the D2GL/3dfx note under the right checkbox and space the groupboxes more evenly --- PD2Launcherv2/Views/OptionsView.xaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/PD2Launcherv2/Views/OptionsView.xaml b/PD2Launcherv2/Views/OptionsView.xaml index 87fe141e..bb4aac70 100644 --- a/PD2Launcherv2/Views/OptionsView.xaml +++ b/PD2Launcherv2/Views/OptionsView.xaml @@ -45,14 +45,14 @@ + - + FontSize="14" HorizontalAlignment="Left" Margin="10,5,0,0"/> - - @@ -71,7 +71,7 @@ + FontSize="14" Margin="10,5,0,0"> Date: Fri, 29 May 2026 17:40:22 +0200 Subject: [PATCH 05/94] PD2Launcherv2, PD2Shared: Add ForceSoftwareRenderer to LauncherOptions ...for toggling software rendering under WPF --- PD2Launcherv2/MainWindow.xaml.cs | 18 +++++++++++++ .../Messages/LauncherOptionsChangeMessage.cs | 1 + PD2Launcherv2/ViewModels/OptionsViewModel.cs | 27 ++++++++++++++++++- PD2Launcherv2/Views/OptionsView.xaml | 3 +++ PD2Shared/Models/LauncherOptions.cs | 1 + 5 files changed, 49 insertions(+), 1 deletion(-) diff --git a/PD2Launcherv2/MainWindow.xaml.cs b/PD2Launcherv2/MainWindow.xaml.cs index 7ba65a0b..6d0ff2e6 100644 --- a/PD2Launcherv2/MainWindow.xaml.cs +++ b/PD2Launcherv2/MainWindow.xaml.cs @@ -93,6 +93,21 @@ public Visibility CustomVisibility } } + private bool _forceSoftwareRenderer; + public bool ForceSoftwareRenderer + { + get => _forceSoftwareRenderer; + set + { + if (_forceSoftwareRenderer != value) + { + _forceSoftwareRenderer = value; + System.Windows.Media.RenderOptions.ProcessRenderMode = _forceSoftwareRenderer ? System.Windows.Interop.RenderMode.SoftwareOnly : System.Windows.Interop.RenderMode.Default; + OnPropertyChanged(nameof(ForceSoftwareRenderer)); + } + } + } + private bool _isDisableUpdates; public bool IsDisableUpdates { @@ -425,6 +440,7 @@ private void LoadConfiguration() private void LoadOptions() { var launcherOptions = _localStorage.LoadSection(StorageKey.LauncherOptions); + ForceSoftwareRenderer = launcherOptions?.ForceSoftwareRenderer == true; IsDisableUpdates = launcherOptions?.DisableAutoUpdate == true; } @@ -438,6 +454,8 @@ private void OnConfigurationChanged(ConfigurationChangeMessage message) private void OnLauncherOptionsChanged(LauncherOptionsChangeMessage message) { + ForceSoftwareRenderer = message.ForceSoftwareRenderer; + OnPropertyChanged(nameof(ForceSoftwareRenderer)); IsDisableUpdates = message.DisableAutoUpdate; OnPropertyChanged(nameof(IsDisableUpdates)); } diff --git a/PD2Launcherv2/Messages/LauncherOptionsChangeMessage.cs b/PD2Launcherv2/Messages/LauncherOptionsChangeMessage.cs index 8c647dab..a74c20de 100644 --- a/PD2Launcherv2/Messages/LauncherOptionsChangeMessage.cs +++ b/PD2Launcherv2/Messages/LauncherOptionsChangeMessage.cs @@ -2,6 +2,7 @@ { public class LauncherOptionsChangeMessage { + public bool ForceSoftwareRenderer { get; set; } public bool DisableAutoUpdate { get; set; } } } diff --git a/PD2Launcherv2/ViewModels/OptionsViewModel.cs b/PD2Launcherv2/ViewModels/OptionsViewModel.cs index 8905a671..03a1637d 100644 --- a/PD2Launcherv2/ViewModels/OptionsViewModel.cs +++ b/PD2Launcherv2/ViewModels/OptionsViewModel.cs @@ -503,6 +503,25 @@ public bool SkipToBnet } } + private bool _forceSoftwareRederer; + public bool ForceSoftwareRenderer + { + get => _forceSoftwareRederer; + set + { + if (_forceSoftwareRederer != value) + { + _forceSoftwareRederer = value; + OnPropertyChanged(); + Messenger.Default.Send(new LauncherOptionsChangeMessage + { + ForceSoftwareRenderer = value, + DisableAutoUpdate = AutoUpdate + }); + } + } + } + private bool _autoUpdate; public bool AutoUpdate { @@ -514,7 +533,11 @@ public bool AutoUpdate Debug.WriteLine($"Set _autoUpdate {value}"); _autoUpdate = value; OnPropertyChanged(); - Messenger.Default.Send(new LauncherOptionsChangeMessage { DisableAutoUpdate = value }); + Messenger.Default.Send(new LauncherOptionsChangeMessage + { + ForceSoftwareRenderer = ForceSoftwareRenderer, + DisableAutoUpdate = value + }); } } } @@ -609,6 +632,7 @@ private void LoadLauncherOptions() LauncherOptions launcherOptions = _localStorage.LoadSection(StorageKey.LauncherOptions); if (launcherOptions != null) { + ForceSoftwareRenderer = launcherOptions.ForceSoftwareRenderer; AutoUpdate = launcherOptions.DisableAutoUpdate; } Debug.WriteLine("end LoadLauncherOptions\n"); @@ -633,6 +657,7 @@ private void UpdateLauncherOptionsStorage() Debug.WriteLine("\nStart UpdateLauncherOptionsStorage"); var launcherOptions = new LauncherOptions { + ForceSoftwareRenderer = ForceSoftwareRenderer, DisableAutoUpdate = AutoUpdate }; _localStorage.Update(StorageKey.LauncherOptions, launcherOptions); diff --git a/PD2Launcherv2/Views/OptionsView.xaml b/PD2Launcherv2/Views/OptionsView.xaml index bb4aac70..efedbc9d 100644 --- a/PD2Launcherv2/Views/OptionsView.xaml +++ b/PD2Launcherv2/Views/OptionsView.xaml @@ -73,6 +73,9 @@ FontFamily="{StaticResource BlizzMedium}" Foreground="{StaticResource GoldLighterBrush}" FontSize="14" Margin="10,5,0,0"> + diff --git a/PD2Shared/Models/LauncherOptions.cs b/PD2Shared/Models/LauncherOptions.cs index 52ef5cbd..9ee4298a 100644 --- a/PD2Shared/Models/LauncherOptions.cs +++ b/PD2Shared/Models/LauncherOptions.cs @@ -2,6 +2,7 @@ { public class LauncherOptions { + public bool ForceSoftwareRenderer { get; set; } = false; public bool DisableAutoUpdate { get; set; } = false; } } From 2f6b456480b4a9115689b98208d8af983f9f170f Mon Sep 17 00:00:00 2001 From: Slacker <288981238+Slacker86@users.noreply.github.com> Date: Fri, 29 May 2026 17:40:22 +0200 Subject: [PATCH 06/94] PD2Shared: ILocalStorage: Introduce LoadSectionIfExists() ...which, unlike dependent LoadSection(), returns null for an absent section instead of a default-constructed object. --- PD2Shared/Interfaces/ILocalStorage.cs | 4 +++- PD2Shared/Storage/LocalStorage.cs | 27 ++++++++++++++++----------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/PD2Shared/Interfaces/ILocalStorage.cs b/PD2Shared/Interfaces/ILocalStorage.cs index 62427860..7bf27e82 100644 --- a/PD2Shared/Interfaces/ILocalStorage.cs +++ b/PD2Shared/Interfaces/ILocalStorage.cs @@ -12,8 +12,10 @@ public interface ILocalStorage //save a setting bucket by keyname void Update(StorageKey key, T value) where T : class; + T? LoadSectionIfExists(StorageKey key) where T : class; + //load a setting bucket by keyname - T LoadSection(StorageKey key) where T : class; + T LoadSection(StorageKey key) where T : class, new(); void InitializeIfNotExists(StorageKey key, T defaultValue) where T : class, new(); } diff --git a/PD2Shared/Storage/LocalStorage.cs b/PD2Shared/Storage/LocalStorage.cs index ee9c0388..c8dc168f 100644 --- a/PD2Shared/Storage/LocalStorage.cs +++ b/PD2Shared/Storage/LocalStorage.cs @@ -124,25 +124,30 @@ public void Update(StorageKey key, T value) where T : class } } - public T LoadSection(StorageKey key) where T : class + public T? LoadSectionIfExists(StorageKey key) where T : class { var settings = Load(); return key switch { - StorageKey.LauncherArgs => settings.LauncherArgs as T ?? Activator.CreateInstance(), - StorageKey.LauncherOptions => settings.LauncherOptions as T ?? Activator.CreateInstance(), - StorageKey.DdrawOptions => settings.DdrawOptions as T ?? Activator.CreateInstance(), - StorageKey.FileUpdateModel => settings.FileUpdateModel as T ?? Activator.CreateInstance(), - StorageKey.Pd2AuthorList => settings.Pd2AuthorList as T ?? Activator.CreateInstance(), - StorageKey.SelectedAuthorAndFilter => settings.SelectedAuthorAndFilter as T ?? Activator.CreateInstance(), - StorageKey.WindowPosition => settings.WindowPosition as T ?? Activator.CreateInstance(), - StorageKey.News => settings.News as T ?? Activator.CreateInstance(), - StorageKey.ResetInfo => settings.ResetInfo as T ?? Activator.CreateInstance(), - _ => Activator.CreateInstance() + StorageKey.LauncherArgs => settings.LauncherArgs as T, + StorageKey.LauncherOptions => settings.LauncherOptions as T, + StorageKey.DdrawOptions => settings.DdrawOptions as T, + StorageKey.FileUpdateModel => settings.FileUpdateModel as T, + StorageKey.Pd2AuthorList => settings.Pd2AuthorList as T, + StorageKey.SelectedAuthorAndFilter => settings.SelectedAuthorAndFilter as T, + StorageKey.WindowPosition => settings.WindowPosition as T, + StorageKey.News => settings.News as T, + StorageKey.ResetInfo => settings.ResetInfo as T, + _ => default }; } + public T LoadSection(StorageKey key) where T : class, new() + { + return LoadSectionIfExists(key) ?? new(); + } + public void InitializeIfNotExists(StorageKey key, T defaultValue) where T : class, new() { if (!Directory.Exists(_storageDirectory)) From 95e031c65413b33d73c4fa0a7003cf8bbced844d Mon Sep 17 00:00:00 2001 From: Slacker <288981238+Slacker86@users.noreply.github.com> Date: Tue, 21 Jul 2026 11:01:14 +0200 Subject: [PATCH 07/94] PD2Launcherv2: Get rid of PD2Launcherv2.Constants class ...as it turns out to be merely a copy of PD2Shared.Constants. --- PD2Launcherv2/Constants.cs | 145 ------------------- PD2Launcherv2/Helpers/DDrawHelpers.cs | 2 +- PD2Launcherv2/ViewModels/OptionsViewModel.cs | 2 +- 3 files changed, 2 insertions(+), 147 deletions(-) delete mode 100644 PD2Launcherv2/Constants.cs diff --git a/PD2Launcherv2/Constants.cs b/PD2Launcherv2/Constants.cs deleted file mode 100644 index 0585acc7..00000000 --- a/PD2Launcherv2/Constants.cs +++ /dev/null @@ -1,145 +0,0 @@ -using PD2Shared.Models; - -namespace ProjectDiablo2Launcherv2 -{ - public static class Constants - { - public static readonly List excludedFiles = new List - { - "D2.LNG", "BnetLog.txt", "ProjectDiablo.cfg", "ddraw.ini", "default.filter", "loot.filter", "UI.ini", "d2gl.yaml", - }; - - public static class Ddraw - { - public const string IniFileName = "ddraw.ini"; - - public static DdrawOptions DefaultDdrawOptions => new() - { - Fullscreen = true, - MaxFps = "60", - Shader = "Shaders\\xbr\\xbr-lv2-noblend.glsl", - PosX = -32000, - PosY = -32000, - Renderer = "opengl", - SaveSettings = "1", - Resizeable = true, - MaxGameTicks = "-2", - HandleMouse = true, - Hook = "4", - MinFps = "0", - SingleCpu = true - }; - } - - public static class LocalStorage - { - public const string LauncherArgsKey = "LauncherArgs"; - public const string DdrawOptionsKey = "DdrawOptions"; - public const string FileUpdateModelKey = "FileUpdateModel"; - public const string AuthorAndFilterKey = "AuthorAndFilterKey"; - } - - public static List MaxFpsPickerItems() - { - return new List - { - new DisplayValuePair { DisplayValue = "60 (default)", ActualValue = "60" }, - new DisplayValuePair { DisplayValue = "-1 (screen rate)", ActualValue = "-1" }, - new DisplayValuePair { DisplayValue = "unlimited", ActualValue = "0" }, - new DisplayValuePair { DisplayValue = "n = cap", ActualValue = "n" } - }; - } - - public static List ModePickerItems() - { - return new List - { - new DisplayValuePair { DisplayValue = "Fullscreen (default)", ActualValue = "fullscreen" }, - new DisplayValuePair { DisplayValue = "Windowed", ActualValue = "windowed" }, - new DisplayValuePair { DisplayValue = "Borderless", ActualValue = "borderless" } - }; - } - - public static List MaxGameTicksPickerItems() - { - return new List - { - new DisplayValuePair { DisplayValue = "-2 Default Refresh Rate", ActualValue = "-2" }, - new DisplayValuePair { DisplayValue = "-1 (disable)", ActualValue = "-1" }, - new DisplayValuePair { DisplayValue = "0 emulate 60hz", ActualValue = "0" }, - new DisplayValuePair { DisplayValue = "60", ActualValue = "60" }, - new DisplayValuePair { DisplayValue = "30", ActualValue = "30" }, - new DisplayValuePair { DisplayValue = "25", ActualValue = "25" }, - new DisplayValuePair { DisplayValue = "20", ActualValue = "20" }, - new DisplayValuePair { DisplayValue = "15", ActualValue = "15" } - }; - } - - public static List SaveWindowPositionPickerItems() - { - return new List - { - new DisplayValuePair { DisplayValue = "global (default)", ActualValue = "1" }, - new DisplayValuePair { DisplayValue = "game specific", ActualValue = "2" }, - new DisplayValuePair { DisplayValue = "disable", ActualValue = "0" } - }; - } - - public static List RendererPickerItems() - { - return new List - { - new DisplayValuePair { DisplayValue = "OpenGL (default)", ActualValue = "opengl" }, - new DisplayValuePair { DisplayValue = "Auto", ActualValue = "auto" }, - new DisplayValuePair { DisplayValue = "direct3d9", ActualValue = "direct3d9" }, - new DisplayValuePair { DisplayValue = "GDI", ActualValue = "gdi" } - }; - } - - public static List HookPickerItems() - { - return new List - { - new DisplayValuePair { DisplayValue = "all modules (default)", ActualValue = "4" }, - new DisplayValuePair { DisplayValue = "IAT hooking", ActualValue = "1" }, - new DisplayValuePair { DisplayValue = "microsoft detours", ActualValue = "2" }, - new DisplayValuePair { DisplayValue = "IAT+detours", ActualValue = "3" }, - new DisplayValuePair { DisplayValue = "disable", ActualValue = "0" } - }; - } - - public static List MinFpsPickerItems() - { - return new List - { - new DisplayValuePair { DisplayValue = "0 (default)", ActualValue = "0" }, - new DisplayValuePair { DisplayValue = "-1 (use max fps)", ActualValue = "-1" }, - new DisplayValuePair { DisplayValue = "5", ActualValue = "5" }, - new DisplayValuePair { DisplayValue = "10", ActualValue = "10" } - }; - } - - public static List ShaderPickerItems() - { - return new List - { - new DisplayValuePair { DisplayValue = "xbr-lv2-noblend (default)", ActualValue = "Shaders\\xbr\\xbr-lv2-noblend.glsl" }, - new DisplayValuePair { DisplayValue = "xbr-lv2", ActualValue = "Shaders\\xbr-lv2.glsl" }, - new DisplayValuePair { DisplayValue = "xbr-lv3", ActualValue = "Shaders\\xbr\\xbr-lv3.glsl" }, - new DisplayValuePair { DisplayValue = "xbrz-freescale", ActualValue = "Shaders\\xbrz-freescale.glsl" }, - new DisplayValuePair { DisplayValue = "4xbrz", ActualValue = "Shaders\\xbrz\\4xbrz.glsl" }, - new DisplayValuePair { DisplayValue = "5xbrz", ActualValue = "Shaders\\xbrz\\5xbrz.glsl" }, - new DisplayValuePair { DisplayValue = "6xbrz", ActualValue = "Shaders\\xbrz\\6xbrz.glsl" }, - new DisplayValuePair { DisplayValue = "aa-shader-4.0", ActualValue = "Shaders\\anti-aliasing\\aa-shader-4.0.glsl" }, - new DisplayValuePair { DisplayValue = "advanced-aa", ActualValue = "Shaders\\anti-aliasing\\advanced-aa.glsl" }, - new DisplayValuePair { DisplayValue = "reverse-aa", ActualValue = "Shaders\\anti-aliasing\\reverse-aa.glsl" }, - new DisplayValuePair { DisplayValue = "bilinear", ActualValue = "Shaders\\bilinear.glsl" }, - new DisplayValuePair { DisplayValue = "bright", ActualValue = "Shaders\\bright.glsl" }, - new DisplayValuePair { DisplayValue = "crt-lottes-fast", ActualValue = "Shaders\\crt-lottes-fast.glsl" }, - new DisplayValuePair { DisplayValue = "cubic", ActualValue = "Shaders\\cubic.glsl" }, - new DisplayValuePair { DisplayValue = "scanline", ActualValue = "Shaders\\scanline.glsl" }, - new DisplayValuePair { DisplayValue = "simple-sharp-bilinear", ActualValue = "Shaders\\simple-sharp-bilinear.glsl" } - }; - } - } -} \ No newline at end of file diff --git a/PD2Launcherv2/Helpers/DDrawHelpers.cs b/PD2Launcherv2/Helpers/DDrawHelpers.cs index 34b5700e..95a6bbbb 100644 --- a/PD2Launcherv2/Helpers/DDrawHelpers.cs +++ b/PD2Launcherv2/Helpers/DDrawHelpers.cs @@ -1,8 +1,8 @@  using MadMilkman.Ini; +using PD2Shared; using PD2Shared.Interfaces; using PD2Shared.Models; -using ProjectDiablo2Launcherv2; using System.Diagnostics; using System.IO; diff --git a/PD2Launcherv2/ViewModels/OptionsViewModel.cs b/PD2Launcherv2/ViewModels/OptionsViewModel.cs index 03a1637d..3557e34b 100644 --- a/PD2Launcherv2/ViewModels/OptionsViewModel.cs +++ b/PD2Launcherv2/ViewModels/OptionsViewModel.cs @@ -2,9 +2,9 @@ using GalaSoft.MvvmLight.Messaging; using PD2Launcherv2.Enums; using PD2Launcherv2.Helpers; +using PD2Shared; using PD2Shared.Interfaces; using PD2Shared.Models; -using ProjectDiablo2Launcherv2; using System.Windows; using System.Diagnostics; using PD2Launcherv2.Messages; From 2f1ef81809e5a2130e6ae30ed0c1a1ea4e9aeaf1 Mon Sep 17 00:00:00 2001 From: Slacker <288981238+Slacker86@users.noreply.github.com> Date: Fri, 29 May 2026 17:40:22 +0200 Subject: [PATCH 08/94] PD2Launcherv2: Introduce Utils.MsgBox --- PD2Launcherv2/MainWindow.xaml.cs | 1 + PD2Launcherv2/Utils/MsgBox.cs | 133 +++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 PD2Launcherv2/Utils/MsgBox.cs diff --git a/PD2Launcherv2/MainWindow.xaml.cs b/PD2Launcherv2/MainWindow.xaml.cs index 6d0ff2e6..8a0a4362 100644 --- a/PD2Launcherv2/MainWindow.xaml.cs +++ b/PD2Launcherv2/MainWindow.xaml.cs @@ -186,6 +186,7 @@ public MainWindow() _localStorage.Update(StorageKey.FileUpdateModel, storeUpdate); } + this.Title = MsgBox.DefaultDialogTitle; // Don't try to update launcher in debug mode // TEST diff --git a/PD2Launcherv2/Utils/MsgBox.cs b/PD2Launcherv2/Utils/MsgBox.cs new file mode 100644 index 00000000..a0776730 --- /dev/null +++ b/PD2Launcherv2/Utils/MsgBox.cs @@ -0,0 +1,133 @@ +using System.Text; +using System.Windows; + +namespace PD2Launcherv2.Utils +{ + public static class MsgBox + { + public const string DefaultDialogTitle = "Project Diablo 2 Launcher"; + + private static MessageBoxResult ShowWrapper(string messageBoxText, MessageBoxImage icon, MessageBoxButton button, MessageBoxResult defaultResult = MessageBoxResult.None) + { + if (App.Current.MainWindow != null) + { + return MessageBox.Show(App.Current.MainWindow, messageBoxText, DefaultDialogTitle, button, icon, defaultResult); + } + else + { + return MessageBox.Show(messageBoxText, DefaultDialogTitle, button, icon, defaultResult); + } + } + + public static MessageBoxResult Info(string messageBoxText, MessageBoxButton button = MessageBoxButton.OK, MessageBoxResult defaultResult = MessageBoxResult.None) + { + return ShowWrapper(messageBoxText, MessageBoxImage.Information, button, defaultResult); + } + + public static MessageBoxResult Warn(string messageBoxText, MessageBoxButton button = MessageBoxButton.OK, MessageBoxResult defaultResult = MessageBoxResult.None) + { + return ShowWrapper(messageBoxText, MessageBoxImage.Warning, button, defaultResult); + } + + public static MessageBoxResult Error(string messageBoxText, MessageBoxButton button = MessageBoxButton.OK, MessageBoxResult defaultResult = MessageBoxResult.None) + { + return ShowWrapper(messageBoxText, MessageBoxImage.Error, button, defaultResult); + } + + public static MessageBoxResult Exception( + Exception? exception, + string? messageBoxText = null, + MessageBoxImage icon = MessageBoxImage.Error, + MessageBoxButton button = MessageBoxButton.OK, + MessageBoxResult defaultResult = MessageBoxResult.None) + { + if (exception == null && messageBoxText == null) + { + throw new ArgumentNullException($"Method '{nameof(Exception)}' cannot be called with both: '{nameof(exception)}' and '{nameof(messageBoxText)}' being null.", (Exception)null!); + } + + // Available options: + // + // = = = = = = = = = = = + // + // [message/exception] + // + // = = = = = = = = = = = + // + // [message] + // + // [exception] + // + // = = = = = = = = = = = + // + // [message/exception] + // + // --- + // + // [exception] + // + // [exception] + // + // [...] + // + // = = = = = = = = = = = + // + // Conclusion: Add "---" at entry index 1 where total entry count >= 3 + + int entryCount = messageBoxText != null ? 1 : 0; + + for (var currentException = exception!; currentException != null; currentException = currentException.InnerException) + { + if (currentException is AggregateException) + { + // Ignore AggregateException exception itself, only its InnerExceptions count + continue; + } + + if (++entryCount >= 3) + { + // This much should be enough + break; + } + } + + StringBuilder sb = new(messageBoxText); + + var lastMessage = messageBoxText; + var currEntryIdx = lastMessage == null ? 0 : 1; + + for (var currentException = exception!; currentException != null; currentException = currentException.InnerException) + { + if (currentException is AggregateException) + { + // Ignore AggregateException exception itself, only its InnerExceptions contain useful messages + continue; + } + + if (lastMessage == currentException.Message) + { + // Skip duplicate messages + continue; + } + + if (sb.Length > 0) + { + if (entryCount >= 3 && currEntryIdx == 1) + { + sb.Append("\n\n---\n\n"); + } + else + { + sb.Append("\n\n"); + } + } + + sb.Append(currentException.Message); + lastMessage = currentException.Message; + ++currEntryIdx; + } + + return ShowWrapper(sb.ToString(), icon, button, defaultResult); + } + } +} From 0fb35b09c0c50abbd9c5d37f89a66a7bc2eb0614 Mon Sep 17 00:00:00 2001 From: Slacker <288981238+Slacker86@users.noreply.github.com> Date: Fri, 29 May 2026 17:40:22 +0200 Subject: [PATCH 09/94] PD2Shared: Introduce Utils.Env ...with platform/filesystem helpers. (This will go against the established dependency injection pattern). --- PD2Shared/Utils/Env.cs | 101 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 PD2Shared/Utils/Env.cs diff --git a/PD2Shared/Utils/Env.cs b/PD2Shared/Utils/Env.cs new file mode 100644 index 00000000..3240a42d --- /dev/null +++ b/PD2Shared/Utils/Env.cs @@ -0,0 +1,101 @@ +namespace PD2Shared.Utils +{ + public static class Env + { + static Env() + { + // In theory this could return null (https://learn.microsoft.com/en-us/dotnet/api/system.environment.processpath#remarks), + // but it's unlikely on Windows as a running process should have an associated handle open, preventing the file from being altered. + ProcessPath = Environment.ProcessPath!; + + ProcessFileName = Path.GetFileName(ProcessPath); + // ProcessPath will never be null, empty nor a root directory + ProcessDirPath = Path.GetDirectoryName(ProcessPath)!; + } + + public static string ProcessFileName { get; } + public static string ProcessPath { get; } + public static string ProcessDirPath { get; } + + public static string GetCwd() + { + // While this method isn't much on its own, it's used for consistency + return Environment.CurrentDirectory; + } + + public static string GetLauncherFilesRootDirPath() + { + return Path.Combine(GetCwd(), "launcher.files"); + } + + public static Exception? CheckIfDirectoryIsWritable(string directoryPath) + { + var filePath = Path.Combine(directoryPath, Path.GetRandomFileName()); + + try + { + using var stream = new FileStream( + filePath, + FileMode.CreateNew, + FileAccess.Write, + FileShare.None, + bufferSize: 0, + FileOptions.DeleteOnClose); + } + catch(Exception ex) + { + return ex; + } + + return null; + } + + public static void EnsureDirectoryExists(string fileFullPath) + { + ArgumentNullException.ThrowIfNull(fileFullPath, nameof(fileFullPath)); + + if (!Path.IsPathFullyQualified(fileFullPath)) + { + throw new ArgumentException("Must be a fully qualified path", nameof(fileFullPath)); + } + + string? dirPath = Path.GetDirectoryName(fileFullPath); + + // Skip when in root directory + if (dirPath != null) + { + Directory.CreateDirectory(dirPath); + } + } + + public static async Task FileExistsAsync(string path) + { + return await Task.Run(() => + { + return File.Exists(path); + }).ConfigureAwait(false); + } + + public static Tuple TryGetFileSize(string path) + { + long? fileSize = null; + Exception? exception = null; + + try + { + fileSize = new FileInfo(path).Length; + } + catch (Exception ex) + { + exception = ex; + } + + return Tuple.Create(fileSize, exception); + } + + public static async Task> TryGetFileSizeAsync(string path) + { + return await Task.Run(() => TryGetFileSize(path)).ConfigureAwait(false); + } + } +} From cb8e67e698a76f70ac597b458ed482d21dea7988 Mon Sep 17 00:00:00 2001 From: Slacker <288981238+Slacker86@users.noreply.github.com> Date: Fri, 29 May 2026 17:40:22 +0200 Subject: [PATCH 10/94] PD2Shared: Add Serilog and its selected sinks as dependencies --- PD2Shared/PD2Shared.csproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/PD2Shared/PD2Shared.csproj b/PD2Shared/PD2Shared.csproj index 9963de93..932cbd98 100644 --- a/PD2Shared/PD2Shared.csproj +++ b/PD2Shared/PD2Shared.csproj @@ -13,6 +13,10 @@ + + + + From 8c48f540597c3958f603ac8f9b46add65236e7ee Mon Sep 17 00:00:00 2001 From: Slacker <288981238+Slacker86@users.noreply.github.com> Date: Fri, 29 May 2026 17:40:22 +0200 Subject: [PATCH 11/94] PD2Shared: Introduce Constants.VersionString ...and render it dynamically in MainWindow --- PD2Launcherv2/MainWindow.xaml | 2 +- PD2Launcherv2/MainWindow.xaml.cs | 1 + PD2Shared/Constants.cs | 3 +++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/PD2Launcherv2/MainWindow.xaml b/PD2Launcherv2/MainWindow.xaml index 9d00b23c..32141a84 100644 --- a/PD2Launcherv2/MainWindow.xaml +++ b/PD2Launcherv2/MainWindow.xaml @@ -224,7 +224,7 @@ - Date: Fri, 24 Jul 2026 08:26:31 +0200 Subject: [PATCH 12/94] eqpublish.ps1: Update accordingly after introducing Constants.VersionString --- eqpublish.ps1 | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/eqpublish.ps1 b/eqpublish.ps1 index e85e70c4..5b173713 100644 --- a/eqpublish.ps1 +++ b/eqpublish.ps1 @@ -4,27 +4,27 @@ $buildFailed = $false Push-Location $PSScriptRoot try { - $mainWindowPath = Join-Path ` + $constantsPath = Join-Path ` $PSScriptRoot ` - "PD2Launcherv2\MainWindow.xaml" + "PD2Shared\Constants.cs" - if (-not (Test-Path $mainWindowPath -PathType Leaf)) { - throw "Could not find MainWindow.xaml at: $mainWindowPath" + if (-not (Test-Path $constantsPath -PathType Leaf)) { + throw "Could not find Constants.cs at: $constantsPath" } - $xamlContent = Get-Content $mainWindowPath -Raw + $csContent = Get-Content $constantsPath -Raw # Accepts versions such as: # v 2.14.1 # v 2.14.AWS1 # v 2.14.SUCCESS $versionMatch = [regex]::Match( - $xamlContent, - 'Text="v\s*([^"]+)"' + $csContent, + 'VersionString\s*=\s*"([^"]+)"' ) if (-not $versionMatch.Success) { - throw "Could not find version text in MainWindow.xaml" + throw "Could not find version text in Constants.cs" } $version = $versionMatch.Groups[1].Value.Trim() From 634b3a8de027a7c2af3b5db6b968a7a2f6031312 Mon Sep 17 00:00:00 2001 From: Slacker <288981238+Slacker86@users.noreply.github.com> Date: Fri, 29 May 2026 17:40:22 +0200 Subject: [PATCH 13/94] PD2Shared: Introduce Logging namespace and incorporate logging into the application This includes bootstrapping a Serilog-based logger at application boot and winding it down gracefully at shutdown. Additionally, allow starting the launcher with the '--console' switch to pop up a classic Windows Console Host console window for outputting the log, apart from having it simultaneously written to a file. Due to how inflexible this console window is, it's primarily usefulness is when debugging and thus enabled by default in debug builds. Moreover, provide own set of logging routines to replace the stock Serilog ones, supplied as a set of extension methods to the Serilog.ILogger interface and contained in the ILoggerEx class. These routines provide a uniform output with caller names and better exception logging. The class LoggerStatic provides additional convenience properties for working with the logger and is meant to be used as a "static using" together with the aforementioned ILoggerEx. Additionally, bring a few utility classes based on Utils.TimedDisposable, including: * LoggedScope -- which can be used to log time spent in a given scope. * LoggedRoutine -- which is even more specialized and logs time spent in the current routine. --- PD2Launcherv2/App.xaml.cs | 17 +++ PD2Shared/Logging/ILoggerEx.cs | 107 +++++++++++++++ PD2Shared/Logging/LoggedRoutine.cs | 23 ++++ PD2Shared/Logging/LoggedScope.cs | 23 ++++ PD2Shared/Logging/Logging.cs | 212 +++++++++++++++++++++++++++++ PD2Shared/Logging/LoggingStatic.cs | 14 ++ PD2Shared/Utils/TimedDisposable.cs | 42 ++++++ 7 files changed, 438 insertions(+) create mode 100644 PD2Shared/Logging/ILoggerEx.cs create mode 100644 PD2Shared/Logging/LoggedRoutine.cs create mode 100644 PD2Shared/Logging/LoggedScope.cs create mode 100644 PD2Shared/Logging/Logging.cs create mode 100644 PD2Shared/Logging/LoggingStatic.cs create mode 100644 PD2Shared/Utils/TimedDisposable.cs diff --git a/PD2Launcherv2/App.xaml.cs b/PD2Launcherv2/App.xaml.cs index 926258f8..f6692a4e 100644 --- a/PD2Launcherv2/App.xaml.cs +++ b/PD2Launcherv2/App.xaml.cs @@ -10,6 +10,8 @@ using System.Net.Http; using System.Text; using System.Windows; +using PD2Shared.Logging; +using static PD2Shared.Logging.LoggingStatic; namespace PD2Launcherv2 { @@ -136,12 +138,27 @@ protected override async void OnStartup(StartupEventArgs e) return; } + // This is a bit meh, but let's keep the convention and make this case-insensitive + var createConsole = e.Args.Any(arg => arg.Equals("--console", StringComparison.OrdinalIgnoreCase)); + + // This is not expected to throw + Logging.SetUp(createConsole); + + L.CallerInformation($"Using up to {Environment.ProcessorCount} concurrent task(s)"); + // Normal UI mode base.OnStartup(e); var mainWindow = _serviceProvider.GetService(); mainWindow?.Show(); } + protected override void OnExit(ExitEventArgs e) + { + Logging.ShutDown(e.ApplicationExitCode); + + base.OnExit(e); + } + // Handle non-UI thread exceptions private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { diff --git a/PD2Shared/Logging/ILoggerEx.cs b/PD2Shared/Logging/ILoggerEx.cs new file mode 100644 index 00000000..ada31bfa --- /dev/null +++ b/PD2Shared/Logging/ILoggerEx.cs @@ -0,0 +1,107 @@ +using Serilog; +using System.Runtime.CompilerServices; + +namespace PD2Shared.Logging +{ + public static class ILoggerEx + { + private static readonly string SeparatorLine = "- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"; + + private static readonly object _lastExceptionStackTraceLock = new(); + private static string? _lastExceptionStackTrace = null; + + private static void CallerWrite(this ILogger logger, Serilog.Events.LogEventLevel logEventLevel, Exception? exception, string messageTemplate, string callerName, object?[]? propertyValues) + { + if (exception != null) + { + // This can have moderate impact on the logger's performance, but should help de-clutter the log. + lock (_lastExceptionStackTraceLock) + { + if (_lastExceptionStackTrace == exception.StackTrace) + { + messageTemplate = messageTemplate + Environment.NewLine + + exception.GetType() + ": " + exception.Message + Environment.NewLine + + " "; + exception = null; + } + else + { + _lastExceptionStackTrace = exception.StackTrace; + } + } + } + + logger.Write(logEventLevel, exception, $"{(callerName + "()"),-30} " + messageTemplate, propertyValues); + } + + public static void CallerWrite(this ILogger logger, Serilog.Events.LogEventLevel logEventLevel, string messageTemplate, object?[]? propertyValues = null, [CallerMemberName] string callerName = "?") + { + CallerWrite(logger, logEventLevel, exception: null, messageTemplate, callerName, propertyValues); + } + + public static void CallerVerbose(this ILogger logger, string messageTemplate, object?[]? propertyValues = null, [CallerMemberName] string callerName = "?") + { + CallerWrite(logger, Serilog.Events.LogEventLevel.Verbose, exception: null, messageTemplate, callerName, propertyValues); + } + + public static void CallerVerbose(this ILogger logger, Exception? exception, string messageTemplate, object?[]? propertyValues = null, [CallerMemberName] string callerName = "?") + { + CallerWrite(logger, Serilog.Events.LogEventLevel.Verbose, exception, messageTemplate, callerName, propertyValues); + } + + public static void CallerDebug(this ILogger logger, string messageTemplate, object?[]? propertyValues = null, [CallerMemberName] string callerName = "?") + { + CallerWrite(logger, Serilog.Events.LogEventLevel.Debug, exception: null, messageTemplate, callerName, propertyValues); + } + + public static void CallerDebug(this ILogger logger, Exception? exception, string messageTemplate, object?[]? propertyValues = null, [CallerMemberName] string callerName = "?") + { + CallerWrite(logger, Serilog.Events.LogEventLevel.Debug, exception, messageTemplate, callerName, propertyValues); + } + + public static void CallerInformation(this ILogger logger, string messageTemplate, object?[]? propertyValues = null, [CallerMemberName] string callerName = "?") + { + CallerWrite(logger, Serilog.Events.LogEventLevel.Information, exception: null, messageTemplate, callerName, propertyValues); + } + + public static void CallerInformation(this ILogger logger, Exception? exception, string messageTemplate, object?[]? propertyValues = null, [CallerMemberName] string callerName = "?") + { + CallerWrite(logger, Serilog.Events.LogEventLevel.Information, exception, messageTemplate, callerName, propertyValues); + } + + public static void CallerWarning(this ILogger logger, string messageTemplate, object?[]? propertyValues = null, [CallerMemberName] string callerName = "?") + { + CallerWrite(logger, Serilog.Events.LogEventLevel.Warning, exception: null, messageTemplate, callerName, propertyValues); + } + + public static void CallerWarning(this ILogger logger, Exception? exception, string messageTemplate, object?[]? propertyValues = null, [CallerMemberName] string callerName = "?") + { + CallerWrite(logger, Serilog.Events.LogEventLevel.Warning, exception, messageTemplate, callerName, propertyValues); + } + + public static void CallerError(this ILogger logger, string messageTemplate, object?[]? propertyValues = null, [CallerMemberName] string callerName = "?") + { + CallerWrite(logger, Serilog.Events.LogEventLevel.Error, exception: null, messageTemplate, callerName, propertyValues); + } + + public static void CallerError(this ILogger logger, Exception? exception, string messageTemplate, object?[]? propertyValues = null, [CallerMemberName] string callerName = "?") + { + CallerWrite(logger, Serilog.Events.LogEventLevel.Error, exception, messageTemplate, callerName, propertyValues); + } + + public static void CallerFatal(this ILogger logger, string messageTemplate, object?[]? propertyValues = null, [CallerMemberName] string callerName = "?") + { + CallerWrite(logger, Serilog.Events.LogEventLevel.Fatal, exception: null, messageTemplate, callerName, propertyValues); + } + + public static void CallerFatal(this ILogger logger, Exception? exception, string messageTemplate, object?[]? propertyValues = null, [CallerMemberName] string callerName = "?") + { + CallerWrite(logger, Serilog.Events.LogEventLevel.Fatal, exception, messageTemplate, callerName, propertyValues); + } + + public static void Separator(this ILogger logger, Serilog.Events.LogEventLevel logEventLevel = Serilog.Events.LogEventLevel.Information) + { + logger.Write(logEventLevel, SeparatorLine); + } + } +} diff --git a/PD2Shared/Logging/LoggedRoutine.cs b/PD2Shared/Logging/LoggedRoutine.cs new file mode 100644 index 00000000..1341fe92 --- /dev/null +++ b/PD2Shared/Logging/LoggedRoutine.cs @@ -0,0 +1,23 @@ +using System.Runtime.CompilerServices; +using PD2Shared.Utils; +using static PD2Shared.Logging.LoggingStatic; + +namespace PD2Shared.Logging +{ + public class LoggedRoutine : TimedDisposable + { + public LoggedRoutine( + Serilog.Events.LogEventLevel logEventLevel = Serilog.Events.LogEventLevel.Information, + [CallerMemberName] string callerName = null!) + : base(() => + { + L.CallerWrite(logEventLevel, $">>", propertyValues: null, callerName); + }, + timeSpan => + { + L.CallerWrite(logEventLevel, $"<< {timeSpan}", propertyValues: null, callerName); + }) + { + } + } +} diff --git a/PD2Shared/Logging/LoggedScope.cs b/PD2Shared/Logging/LoggedScope.cs new file mode 100644 index 00000000..c674eff5 --- /dev/null +++ b/PD2Shared/Logging/LoggedScope.cs @@ -0,0 +1,23 @@ +using System.Runtime.CompilerServices; +using PD2Shared.Utils; +using static PD2Shared.Logging.LoggingStatic; + +namespace PD2Shared.Logging +{ + public class LoggedScope : TimedDisposable + { + public LoggedScope(string message, + Serilog.Events.LogEventLevel logEventLevel = Serilog.Events.LogEventLevel.Information, + [CallerMemberName] string callerName = null!) + : base(() => + { + L.CallerWrite(logEventLevel, $"> {message}", propertyValues: null, callerName); + }, + timeSpan => + { + L.CallerWrite(logEventLevel, $"< {message} Finished in {timeSpan}.", propertyValues: null, callerName); + }) + { + } + } +} diff --git a/PD2Shared/Logging/Logging.cs b/PD2Shared/Logging/Logging.cs new file mode 100644 index 00000000..d503ccca --- /dev/null +++ b/PD2Shared/Logging/Logging.cs @@ -0,0 +1,212 @@ +using System.Runtime.InteropServices; +using System.Text; +using Serilog; +using PD2Shared.Utils; +using static PD2Shared.Logging.LoggingStatic; + +namespace PD2Shared.Logging +{ + public static class Logging + { + private static class DllImports + { + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool AllocConsole(); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool FreeConsole(); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool SetConsoleOutputCP([In] uint wCodePageID); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool SetConsoleTitle(string lpConsoleTitle); + + [DllImport("kernel32.dll")] + public static extern IntPtr GetConsoleWindow(); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow); + } + + private const string ConsoleOutputTemplate = "{Timestamp:HH:mm:ss.fff} {Level:u1}{Level:u1} {Message:lj}{NewLine}{Exception}"; + private const string FileOutputTemplate = "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {Level:u1}{Level:u1} {Message:lj}{NewLine}{Exception}"; + + private static readonly object _logSyncObject = new(); + + private static readonly string _logFileName; + private static readonly string _logDirPath; + private static readonly string _logPath; + + private static readonly string _previousLogFileName; + private static readonly string _previousLogPath; + + private static bool _alreadySetUp = false; + private static bool _consoleCreated = false; + + static Logging() + { + // Name the log file after the actual executable filename + var processFileName = Path.GetFileName(Env.ProcessPath); + var stem = Path.GetFileNameWithoutExtension(processFileName); + + _logFileName = string.Concat(stem, ".log"); + _previousLogFileName = string.Concat(stem, ".previous", ".log"); + + // Since GetLauncherFilesRootDirPath() depends on GetCwd(), cache this + _logDirPath = Env.GetLauncherFilesRootDirPath(); + + _logPath = Path.Combine(Env.GetLauncherFilesRootDirPath(), _logFileName); + _previousLogPath = Path.Combine(Env.GetLauncherFilesRootDirPath(), _previousLogFileName); + } + + public static string LogPath { get => _logPath; } + public static string LogDirPath { get => _logDirPath; } + public static string LogFileName { get => _logFileName; } + + public static void SetUp(bool createConsole) + { +#if DEBUG + createConsole = true; +#endif + + if (_alreadySetUp) + { + throw new InvalidOperationException("The log has been already set up."); + } + + if (createConsole) + { + if (!DllImports.AllocConsole()) + { + throw new InvalidOperationException($"{nameof(DllImports.AllocConsole)}() failed."); + } + + // Force using UTF-8 as the output code page + if (!DllImports.SetConsoleOutputCP((uint)Encoding.UTF8.CodePage)) + { + throw new InvalidOperationException($"{nameof(DllImports.SetConsoleOutputCP)}() failed."); + } + + if (!DllImports.SetConsoleTitle("Log")) + { + throw new InvalidOperationException($"{nameof(DllImports.SetConsoleTitle)}() failed."); + } + + IntPtr consoleHwnd = DllImports.GetConsoleWindow(); + + if (consoleHwnd == IntPtr.Zero) + { + throw new InvalidOperationException($"{nameof(DllImports.GetConsoleWindow)}() failed."); + } + + // Don't bother with a full set of constants, this is for debugging purposes only + const int SW_SHOWMAXIMIZED = 3; + + if (!DllImports.ShowWindowAsync(consoleHwnd, SW_SHOWMAXIMIZED)) + { + throw new InvalidOperationException($"{nameof(DllImports.ShowWindowAsync)}() failed."); + } + + _consoleCreated = true; + } + +#if DEBUG + // Since ILoggingFailureListener doesn't help much on sink initialization failure as of Serilog 4.4.0, resort to this: + Serilog.Debugging.SelfLog.Enable(message => + { + lock (_logSyncObject) + { + Console.Error.WriteLine(message); + } + }); +#endif + + // Rotate the log file manually, *sigh*... + // + // (Serilog blindly appends to an existing file and doesn't support simple file rotation unless files reach their configured limits). + (var _, var logRotateEx) = TryRotateLogFile(_logPath, _previousLogPath); + + var loggerConfiguration = new LoggerConfiguration() + .MinimumLevel.Verbose() + .WriteTo.Async(c => c.File(path: _logPath, outputTemplate: FileOutputTemplate, restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Debug)); + + if (createConsole) + { + loggerConfiguration.WriteTo.Async(c => c.Console(syncRoot: _logSyncObject, outputTemplate: ConsoleOutputTemplate)); + } + + Log.Logger = loggerConfiguration.CreateLogger(); + + // Should be able to tell here if the sink wasn't created successfully. + // Sadly, Serilog doesn't support any of that as of 4.4.0. Instead, it handles any exceptions internally and writes them to SelfLog, as per: + // https://github.com/serilog/serilog/wiki/Reliability#configuration. + +#if DEBUG + // And now disable self-logging past sink creation as this excessive output won't be of much use. + Serilog.Debugging.SelfLog.Disable(); +#endif + + L.CallerInformation($"{Env.ProcessFileName} {Constants.VersionString}"); + L.CallerInformation($"Process path: '{Env.ProcessPath}'"); + L.CallerInformation($"Current working directory: '{Env.GetCwd()}'"); + L.CallerInformation($"Log file path: '{_logPath}'"); + + if (logRotateEx != null) + { + L.CallerError(logRotateEx, $"{nameof(TryRotateLogFile)}() threw"); + } + + _alreadySetUp = true; + } + + private static bool RotateLogFile(string logPath, string previousLogPath) + { + if (File.Exists(logPath)) + { + File.Move(logPath, previousLogPath, overwrite: true); + return true; + } + + return false; + } + + private static Tuple TryRotateLogFile(string logPath, string previousLogPath) + { + bool? res; + + try + { + res = RotateLogFile(logPath, previousLogPath); + } + catch (Exception ex) + { + return Tuple.Create((bool?)null, (Exception?)ex); + } + + return Tuple.Create(res, (Exception?)null); + } + + public static async void ShutDown(int exitCode) + { + if (!_alreadySetUp) + { + return; + } + + L.CallerInformation($"Exiting with {exitCode} exit code..."); + L.CallerInformation("Closing logger..."); + await Log.CloseAndFlushAsync(); + + if (_consoleCreated) + { + DllImports.FreeConsole(); + } + } + } +} diff --git a/PD2Shared/Logging/LoggingStatic.cs b/PD2Shared/Logging/LoggingStatic.cs new file mode 100644 index 00000000..9e1f29e5 --- /dev/null +++ b/PD2Shared/Logging/LoggingStatic.cs @@ -0,0 +1,14 @@ +namespace PD2Shared.Logging +{ + public static class LoggingStatic + { + // A convenience property to be combined with ILoggerEx + public static Serilog.ILogger L { get => Serilog.Log.Logger; } + + // A convenience method for constructing arrays from "params" to be easily passed to ILoggerEx methods + public static object?[]? ExplicitArray(params object?[] args) + { + return args; + } + } +} diff --git a/PD2Shared/Utils/TimedDisposable.cs b/PD2Shared/Utils/TimedDisposable.cs new file mode 100644 index 00000000..b407e920 --- /dev/null +++ b/PD2Shared/Utils/TimedDisposable.cs @@ -0,0 +1,42 @@ +using System.Diagnostics; + +namespace PD2Shared.Utils +{ + public class TimedDisposable : IDisposable + { + private bool _disposed = false; + + private readonly Stopwatch _stopwatch; + private readonly Action _onDisposeAction; + + public TimedDisposable(Action onConstructAction, Action onDisposeAction) + { + _onDisposeAction = onDisposeAction; + _stopwatch = Stopwatch.StartNew(); + + onConstructAction(); + } + + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (disposing) + { + _onDisposeAction(_stopwatch.Elapsed); + } + + _disposed = true; + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} From 4529485d4918aefa15c7e770287f41eb903adb58 Mon Sep 17 00:00:00 2001 From: Slacker <288981238+Slacker86@users.noreply.github.com> Date: Fri, 29 May 2026 17:40:22 +0200 Subject: [PATCH 14/94] PD2Launcherv2: Improve unhandled exception handlers ...by utilizing Logging and Utils.MsgBox. Also make sure these handlers do terminate the application in the end. --- PD2Launcherv2/App.xaml.cs | 68 ++++++++++++++------------------------- 1 file changed, 24 insertions(+), 44 deletions(-) diff --git a/PD2Launcherv2/App.xaml.cs b/PD2Launcherv2/App.xaml.cs index f6692a4e..89ca8f68 100644 --- a/PD2Launcherv2/App.xaml.cs +++ b/PD2Launcherv2/App.xaml.cs @@ -10,9 +10,12 @@ using System.Net.Http; using System.Text; using System.Windows; +using PD2Launcherv2.Utils; using PD2Shared.Logging; using static PD2Shared.Logging.LoggingStatic; +[assembly: System.Runtime.CompilerServices.RuntimeCompatibilityAttribute(WrapNonExceptionThrows = true)] + namespace PD2Launcherv2 { /// @@ -162,14 +165,33 @@ protected override void OnExit(ExitEventArgs e) // Handle non-UI thread exceptions private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { - LogException(e.ExceptionObject as Exception); + // Blindly cast the object to Exception thanks to RuntimeCompatibilityAttribute (https://learn.microsoft.com/en-us/dotnet/api/system.unhandledexceptioneventargs.exceptionobject#remarks) + var ex = (Exception)e.ExceptionObject; + + L.Fatal(ex, "Unhandled exception"); + + this.Dispatcher.Invoke(() => + { + MsgBox.Exception(ex, "Unhandled exception:"); + + // Unlike DispatcherUnhandledException, WER will still kick in. + // However, due to reliance on task asynchronous programming model, it is very unlikely that this handler will ever be used. + this.Shutdown(1); + }); } // Handle UI thread exceptions private void App_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e) { - LogException(e.Exception); + var ex = e.Exception; + + L.Fatal(ex, "Unhandled exception"); + MsgBox.Exception(ex, "Unhandled exception:"); + e.Handled = true; // Prevent application from crashing + + // ...yet shut it down on our own terms now, knowing that we have prevented Windows Error Reporting from kicking in. + this.Shutdown(1); } private void CleanUpTempStorageFiles() @@ -191,47 +213,5 @@ private void CleanUpTempStorageFiles() } } } - - private void LogException(Exception ex) - { - if (ex == null) return; - - string logPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory); - - string logFile = Path.Combine(logPath, $"pd2launcher_error__{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.txt"); - - // Use StackTrace to get more detailed info about where the exception occurred - var stackTrace = new StackTrace(ex, true); - var frame = stackTrace.GetFrames()?.FirstOrDefault(); - var method = frame?.GetMethod(); - var declaringType = method?.DeclaringType; - var methodName = method?.Name; - - // Prepare the log entry - var sb = new StringBuilder(); - sb.AppendLine("=============================================================================="); - sb.AppendLine($"Timestamp: {DateTime.Now}"); - sb.AppendLine("Exception Class:"); - sb.AppendLine(declaringType?.FullName ?? "N/A"); - sb.AppendLine("Exception Method:"); - sb.AppendLine($"{methodName ?? "N/A"}"); - sb.AppendLine("Exception Message:"); - sb.AppendLine(ex.Message); - sb.AppendLine("Stack Trace:"); - sb.AppendLine(ex.StackTrace); - - // Include inner exception details if available - if (ex.InnerException != null) - { - sb.AppendLine("Inner Exception Message:"); - sb.AppendLine(ex.InnerException.Message); - sb.AppendLine("Inner Exception Stack Trace:"); - sb.AppendLine(ex.InnerException.StackTrace); - } - sb.AppendLine("=============================================================================="); - - // Append the log entry to the file - File.AppendAllText(logFile, sb.ToString()); - } } } \ No newline at end of file From 870c97899bb505babe3430914c7842ad79096890 Mon Sep 17 00:00:00 2001 From: Slacker <288981238+Slacker86@users.noreply.github.com> Date: Fri, 29 May 2026 17:40:22 +0200 Subject: [PATCH 15/94] PD2Launcherv2: Perform sanity checks on boot --- PD2Launcherv2/App.xaml.cs | 48 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/PD2Launcherv2/App.xaml.cs b/PD2Launcherv2/App.xaml.cs index 89ca8f68..5c58a490 100644 --- a/PD2Launcherv2/App.xaml.cs +++ b/PD2Launcherv2/App.xaml.cs @@ -13,6 +13,7 @@ using PD2Launcherv2.Utils; using PD2Shared.Logging; using static PD2Shared.Logging.LoggingStatic; +using PD2Shared.Utils; [assembly: System.Runtime.CompilerServices.RuntimeCompatibilityAttribute(WrapNonExceptionThrows = true)] @@ -149,6 +150,14 @@ protected override async void OnStartup(StartupEventArgs e) L.CallerInformation($"Using up to {Environment.ProcessorCount} concurrent task(s)"); + if (!PerformSanityChecks()) + { + L.CallerInformation($"{nameof(PerformSanityChecks)}() failed and user declined to continue."); + + this.Shutdown(1); + return; + } + // Normal UI mode base.OnStartup(e); var mainWindow = _serviceProvider.GetService(); @@ -213,5 +222,44 @@ private void CleanUpTempStorageFiles() } } } + + private static bool PerformSanityChecks() + { + bool CheckIfDirIsWritable(string dirPath, string errorPattern) + { + var ex = Env.CheckIfDirectoryIsWritable(dirPath); + + if (ex != null) + { + var messagePart = string.Format(errorPattern, dirPath); + + L.CallerWarning(ex, $"{nameof(Env.CheckIfDirectoryIsWritable)}() failed for path '{dirPath}'"); + + if (MsgBox.Warn( + messagePart + "\n" + + "This can lead to unexpected issues.\n\n" + + "Do you want to continue?", + MessageBoxButton.YesNo, + MessageBoxResult.No) == MessageBoxResult.No) + { + return false; + } + else + { + L.CallerWarning("User ignored sanity check."); + } + } + else + { + L.CallerDebug($"Directory '{dirPath}' is writable."); + } + + return true; + } + + return + CheckIfDirIsWritable(Env.ProcessDirPath, "The directory '{0}', where the launcher's files reside, does not appear to be writable.") && + CheckIfDirIsWritable(Env.GetCwd(), "The working directory '{0}' does not appear to be writable."); + } } } \ No newline at end of file From fb459dcaf970654775d2e86b76e7c56fb77628d1 Mon Sep 17 00:00:00 2001 From: Slacker <288981238+Slacker86@users.noreply.github.com> Date: Fri, 29 May 2026 17:40:22 +0200 Subject: [PATCH 16/94] PD2Shared: Introduce Utils.Wine (This will go against the established dependency injection pattern). --- PD2Shared/Utils/Wine.cs | 45 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 PD2Shared/Utils/Wine.cs diff --git a/PD2Shared/Utils/Wine.cs b/PD2Shared/Utils/Wine.cs new file mode 100644 index 00000000..ded5df5b --- /dev/null +++ b/PD2Shared/Utils/Wine.cs @@ -0,0 +1,45 @@ +using System.Runtime.InteropServices; +using PD2Shared.Logging; +using static PD2Shared.Logging.LoggingStatic; + +namespace PD2Shared.Utils +{ + public static class Wine + { + private static class DllImports + { + [DllImport("ntdll.dll", CharSet = CharSet.Ansi)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", + "CA2101:Specify marshaling for P/Invoke string arguments", + Justification = "UTF-8 strings are expected, thus CharSet.Ansi is the correct choice.")] + public static extern string wine_get_version(); + } + + private static readonly bool _runningUnderWine = false; + private static readonly Version? _wineVersion = null; + + static Wine() + { + string versionString; + + try + { + versionString = DllImports.wine_get_version(); + } + catch (EntryPointNotFoundException) + { + return; + } + + _runningUnderWine = true; + + if (!Version.TryParse(versionString, out _wineVersion)) + { + L.CallerError($"Failed to parse Wine {nameof(versionString)}: '{versionString}'"); + } + } + + public static Version? WineVersion { get => _wineVersion; } + public static bool IsRunningUnderWine { get => _runningUnderWine; } + } +} From e68809ce830a11533a410c8d110a8a5282ee92ae Mon Sep 17 00:00:00 2001 From: Slacker <288981238+Slacker86@users.noreply.github.com> Date: Fri, 29 May 2026 17:40:22 +0200 Subject: [PATCH 17/94] PD2Shared: Make LocalStorage.Update() more robust ...by not tripping on File.Replace() when destinationFileName is missing --- PD2Shared/Storage/LocalStorage.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PD2Shared/Storage/LocalStorage.cs b/PD2Shared/Storage/LocalStorage.cs index c8dc168f..3738bfaf 100644 --- a/PD2Shared/Storage/LocalStorage.cs +++ b/PD2Shared/Storage/LocalStorage.cs @@ -110,7 +110,7 @@ public void Update(StorageKey key, T value) where T : class if (IsValidJson(json)) { File.WriteAllText(tempFilePath, json); - File.Replace(tempFilePath, filePath, null); + File.Move(tempFilePath, filePath, overwrite: true); Debug.WriteLine("Settings updated successfully."); } else From b17fc553978d76b22fb8ece314ee5cb7251576b0 Mon Sep 17 00:00:00 2001 From: Slacker <288981238+Slacker86@users.noreply.github.com> Date: Fri, 29 May 2026 17:40:22 +0200 Subject: [PATCH 18/94] PD2Launcherv2: MainWindow: Hide MainFrame in the designer --- PD2Launcherv2/MainWindow.xaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/PD2Launcherv2/MainWindow.xaml b/PD2Launcherv2/MainWindow.xaml index 32141a84..e24cef09 100644 --- a/PD2Launcherv2/MainWindow.xaml +++ b/PD2Launcherv2/MainWindow.xaml @@ -2,6 +2,9 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:PD2Launcherv2.CustomControl" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + mc:Ignorable="d" Title="Project Diablo 2 Launcher" Height="600" Width="800" WindowStyle="None" AllowsTransparency="True" Background="Transparent"> @@ -244,6 +247,6 @@ Visibility="{Binding UpdatesNotificationVisibility}" /> - + From a8dcf97591050c63a452aaba83d9223ddc2920f4 Mon Sep 17 00:00:00 2001 From: Slacker <288981238+Slacker86@users.noreply.github.com> Date: Fri, 29 May 2026 17:40:22 +0200 Subject: [PATCH 19/94] PD2Launcherv2: MainWindow: Show an indicator if running under Wine --- PD2Launcherv2/MainWindow.xaml | 3 +++ PD2Launcherv2/MainWindow.xaml.cs | 10 ++++++++++ PD2Launcherv2/PD2Launcherv2.csproj | 2 ++ PD2Launcherv2/Resources/Images/winelogo16.png | Bin 0 -> 700 bytes 4 files changed, 15 insertions(+) create mode 100644 PD2Launcherv2/Resources/Images/winelogo16.png diff --git a/PD2Launcherv2/MainWindow.xaml b/PD2Launcherv2/MainWindow.xaml index e24cef09..33817778 100644 --- a/PD2Launcherv2/MainWindow.xaml +++ b/PD2Launcherv2/MainWindow.xaml @@ -246,6 +246,9 @@ Margin="50,5,0,135" Visibility="{Binding UpdatesNotificationVisibility}" /> + + + diff --git a/PD2Launcherv2/MainWindow.xaml.cs b/PD2Launcherv2/MainWindow.xaml.cs index dcfebc31..b4b2082b 100644 --- a/PD2Launcherv2/MainWindow.xaml.cs +++ b/PD2Launcherv2/MainWindow.xaml.cs @@ -18,6 +18,7 @@ using System.Windows.Navigation; using System.Windows.Threading; using System.IO; +using PD2Shared.Utils; namespace PD2Launcherv2 { @@ -189,6 +190,15 @@ public MainWindow() this.Title = MsgBox.DefaultDialogTitle; this.VersionText.Text = PD2Shared.Constants.VersionString; + if (Wine.IsRunningUnderWine) + { + WineLogo16Image.ToolTip = Wine.WineVersion != null ? $"Wine {Wine.WineVersion} detected" : "Undetermined Wine version"; + } + else + { + WineLogo16Image.Visibility = Visibility.Hidden; + } + // Don't try to update launcher in debug mode // TEST diff --git a/PD2Launcherv2/PD2Launcherv2.csproj b/PD2Launcherv2/PD2Launcherv2.csproj index d6a29256..1afca9a5 100644 --- a/PD2Launcherv2/PD2Launcherv2.csproj +++ b/PD2Launcherv2/PD2Launcherv2.csproj @@ -59,6 +59,7 @@ + @@ -115,6 +116,7 @@ + diff --git a/PD2Launcherv2/Resources/Images/winelogo16.png b/PD2Launcherv2/Resources/Images/winelogo16.png new file mode 100644 index 0000000000000000000000000000000000000000..8d4fce561a8a4ab8bc4c065fcc86fe50e9178fa5 GIT binary patch literal 700 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jPK-BC>eK@{Ea{HEjtmSN z`?>!lvI6-E$sR$z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBDB+hH z;hE;^%b*2hb1<+lN-=;;U<6`2Mrk;30I z?gLL3#}JFtYbW_<3kQlEeP8|glBe-xi!86JA|V>8EUc!>mMn7FnCH6T?IEs5Vd1eM zbIO9JNLexd<=7>7rFF+gZjPpe-kTl*vwcpqTw%H0vSj9&`R4CSwO6W$?LB??;qjew zm_PKlSGD>)`*`xhiH+Ak7I&^nl+Zn1$Pzg%I@Njps_WbGR;-Hpb+^BoamwQsKKr~}?%bJ(yzQ$mI5ha3y_X|v zB{7Sw;dw1%$?=})Tb5s+ni8~g{p)ho^UUJO*K2;CyEkM0YF-}(iH*x%x4mA*$r{-< zL51;sS?!!4d7+x<>NnLa`)B+Q(lI&x*6h!Vk_AC)o9<>Dc(FCpjq?;Q%Lc_S3T4^r z?nVATBh+)ywMVhCTJDQ*@_vEuccmkg84kSKe08yNuC@4=JsTW2-UwVW`1|9_r$e8f z#RJ3H#Mk&{AF3+oZ@m%q*;2-J7XKr7ga{1+#9Tq;* zrZAsck{X@NAuy@=;H~p#U#Y)3q#PP*{%f1Uuan&_HAbbDrB`J+`^{YcZ!k8Lv#t4d z=4a%qoBHhg9AaPleS7^Y=%#^Qi<6_{BqqN={fJ$075*%RtpC2hW%_XYh^=yokR34I O89ZJ6T-G@yGywn)wi~Db literal 0 HcmV?d00001 From 244e2b202ddf3c0af8acbb23bb71fa95ea16dda2 Mon Sep 17 00:00:00 2001 From: Slacker <288981238+Slacker86@users.noreply.github.com> Date: Fri, 29 May 2026 17:40:22 +0200 Subject: [PATCH 20/94] PD2Launcherv2: MainWindow: Offer applying launcher options for Wine compatibility during first run --- PD2Launcherv2/MainWindow.xaml.cs | 37 ++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/PD2Launcherv2/MainWindow.xaml.cs b/PD2Launcherv2/MainWindow.xaml.cs index b4b2082b..724648f8 100644 --- a/PD2Launcherv2/MainWindow.xaml.cs +++ b/PD2Launcherv2/MainWindow.xaml.cs @@ -18,6 +18,7 @@ using System.Windows.Navigation; using System.Windows.Threading; using System.IO; +using PD2Launcherv2.Utils; using PD2Shared.Utils; namespace PD2Launcherv2 @@ -596,6 +597,42 @@ private void ShowErrorMessage(string message) public void InitializeDefaultSettings(ILocalStorage localStorage) { + // Since LauncherOptions has been recently added, expect existing config files (aka LocalStorage) + // to be missing that section. Since the whole LocalStorage implementation is a bit sketchy and the below + // initialization doesn't even work -- perform only this specific manual step for now. + // + // Keep in mind that LocalStorage.Update() will still rotate and rewrite the entire file. *sigh* + if (_localStorage.LoadSectionIfExists(StorageKey.LauncherOptions) == null) + { + bool localStorageUpdated = false; + + if (Wine.IsRunningUnderWine) + { + if (MsgBox.Info( + "It appears to be the first time the launcher has been run.\n" + + "Additionally, it's running under Wine.\n" + + "\n" + + "Would you like to set up launcher options for maximum Wine compatibility?\n" + + "(This can be re-adjusted in the Options menu at any time).", + MessageBoxButton.YesNo, + MessageBoxResult.Yes) == MessageBoxResult.Yes) + { + localStorage.Update(StorageKey.LauncherOptions, new LauncherOptions() + { + ForceSoftwareRenderer = true + }); + + localStorageUpdated = true; + } + } + + if (!localStorageUpdated) + { + localStorage.Update(StorageKey.LauncherOptions, new LauncherOptions()); + } + } + + // None of the below logic works as expected _localStorage.InitializeIfNotExists(StorageKey.FileUpdateModel, new FileUpdateModel()); _localStorage.InitializeIfNotExists(StorageKey.DdrawOptions, new DdrawOptions()); _localStorage.InitializeIfNotExists(StorageKey.LauncherArgs, new LauncherArgs()); From d523331ab4886cd96d0796ae51ca67d14eef55d6 Mon Sep 17 00:00:00 2001 From: Slacker <288981238+Slacker86@users.noreply.github.com> Date: Fri, 29 May 2026 17:40:22 +0200 Subject: [PATCH 21/94] PD2Shared: Utils.Wine: Add means of configuring Wine for PD2 --- PD2Shared/Utils/Wine.cs | 101 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/PD2Shared/Utils/Wine.cs b/PD2Shared/Utils/Wine.cs index ded5df5b..61f6cbc3 100644 --- a/PD2Shared/Utils/Wine.cs +++ b/PD2Shared/Utils/Wine.cs @@ -1,4 +1,5 @@ using System.Runtime.InteropServices; +using Microsoft.Win32; using PD2Shared.Logging; using static PD2Shared.Logging.LoggingStatic; @@ -15,6 +16,39 @@ private static class DllImports public static extern string wine_get_version(); } + const string HkcuExeKeyPath = @"Software\Wine\AppDefaults\Game.exe"; + const string HkcuDllOverridesKeyPath = HkcuExeKeyPath + @"\DllOverrides"; + + // MSVC2019 runtime + private static readonly string[] Msvc2019Libs = { + "api-ms-win-crt-conio-l1-1-0", + "api-ms-win-crt-heap-l1-1-0", + "api-ms-win-crt-locale-l1-1-0", + "api-ms-win-crt-math-l1-1-0", + "api-ms-win-crt-private-l1-1-0", + "api-ms-win-crt-runtime-l1-1-0", + "api-ms-win-crt-stdio-l1-1-0", + "api-ms-win-crt-time-l1-1-0", + "atl140", + "concrt140", + "msvcp140", + "msvcp140_1", + "msvcp140_2", + "msvcp140_atomic_wait", + "msvcp140_codecvt_ids", + "ucrtbase", + "vcamp140", + "vccorlib140", + "vcomp140", + "vcruntime140", + "vcruntime140_1" + }; + + public class WineException : Exception + { + public WineException(string? message, Exception? innerException = null) : base(message, innerException) { } + } + private static readonly bool _runningUnderWine = false; private static readonly Version? _wineVersion = null; @@ -41,5 +75,72 @@ static Wine() public static Version? WineVersion { get => _wineVersion; } public static bool IsRunningUnderWine { get => _runningUnderWine; } + + public static void ApplyWineConfiguration() + { + // What needs to be set: + // + // [HKEY_CURRENT_USER\Software\Wine\AppDefaults\Game.exe] + // ; Workaround for InitializeCriticalSection() in Wine>=9.9 (won't help if running >=9.5, which introduced the unconditional API change) + // "Version"="win7" + // + // [HKEY_CURRENT_USER\Software\Wine\AppDefaults\Game.exe\DllOverrides] + // ; Provided DDraw wrapper + // "ddraw"="native" + // ; MSVC2019 runtime + // "*api-ms-win-crt-conio-l1-1-0"="native,builtin" + // "*api-ms-win-crt-heap-l1-1-0"="native,builtin" + // "*api-ms-win-crt-locale-l1-1-0"="native,builtin" + // "*api-ms-win-crt-math-l1-1-0"="native,builtin" + // "*api-ms-win-crt-private-l1-1-0"="native,builtin" + // "*api-ms-win-crt-runtime-l1-1-0"="native,builtin" + // "*api-ms-win-crt-stdio-l1-1-0"="native,builtin" + // "*api-ms-win-crt-time-l1-1-0"="native,builtin" + // "*atl140"="native,builtin" + // "*concrt140"="native,builtin" + // "*msvcp140"="native,builtin" + // "*msvcp140_1"="native,builtin" + // "*msvcp140_2"="native,builtin" + // "*msvcp140_atomic_wait"="native,builtin" + // "*msvcp140_codecvt_ids"="native,builtin" + // "*ucrtbase"="native,builtin" + // "*vcamp140"="native,builtin" + // "*vccorlib140"="native,builtin" + // "*vcomp140"="native,builtin" + // "*vcruntime140"="native,builtin" + // "*vcruntime140_1"="native,builtin" + + using (var key = Registry.CurrentUser.CreateSubKey(HkcuExeKeyPath, writable: true)) + { + if (key == null) + { + throw new WineException($"Failed to create registry key: '{Registry.CurrentUser}\\{HkcuExeKeyPath}'"); + } + + key.SetValue("Version", "win7", RegistryValueKind.String); + } + + using (var key = Registry.CurrentUser.CreateSubKey(HkcuDllOverridesKeyPath, writable: true)) + { + if (key == null) + { + throw new WineException($"Failed to create registry key: '{Registry.CurrentUser}\\{HkcuDllOverridesKeyPath}'"); + } + + // Provided DDraw wrapper + key.SetValue("ddraw", "native", RegistryValueKind.String); + + // MSVC2019 runtime libs + foreach (var libName in Msvc2019Libs) + { + key.SetValue($"*{libName}", "native,builtin", RegistryValueKind.String); + } + } + } + + public static void RemoveWineConfiguration() + { + Registry.CurrentUser.DeleteSubKeyTree(HkcuExeKeyPath, throwOnMissingSubKey: false); + } } } From 8325c01237b0ff1dfe3a2d6b1648ddea749ceb9a Mon Sep 17 00:00:00 2001 From: Slacker <288981238+Slacker86@users.noreply.github.com> Date: Fri, 29 May 2026 17:40:22 +0200 Subject: [PATCH 22/94] PD2Launcherv2: OptionsView: Remove dead code --- PD2Launcherv2/Views/OptionsView.xaml.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/PD2Launcherv2/Views/OptionsView.xaml.cs b/PD2Launcherv2/Views/OptionsView.xaml.cs index a1dfda27..c160c511 100644 --- a/PD2Launcherv2/Views/OptionsView.xaml.cs +++ b/PD2Launcherv2/Views/OptionsView.xaml.cs @@ -13,10 +13,5 @@ public OptionsView() InitializeComponent(); DataContext = App.Resolve(); } - - private void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) - { - - } } } \ No newline at end of file From edc03154f719dadd4323e8ce0efd174950d346cc Mon Sep 17 00:00:00 2001 From: Slacker <288981238+Slacker86@users.noreply.github.com> Date: Fri, 29 May 2026 17:40:22 +0200 Subject: [PATCH 23/94] PD2Launcherv2: View(Model)s/OptionsView(Model): Allow applying/removing Wine configuration for PD2 ...in place of 'Windows permissions' when running under Wine --- PD2Launcherv2/PD2Launcherv2.csproj | 2 + PD2Launcherv2/Resources/Images/winelogo32.png | Bin 0 -> 1613 bytes PD2Launcherv2/ViewModels/OptionsViewModel.cs | 48 ++++++++++++++++++ PD2Launcherv2/Views/OptionsView.xaml | 4 +- PD2Launcherv2/Views/OptionsView.xaml.cs | 20 +++++++- 5 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 PD2Launcherv2/Resources/Images/winelogo32.png diff --git a/PD2Launcherv2/PD2Launcherv2.csproj b/PD2Launcherv2/PD2Launcherv2.csproj index 1afca9a5..d65a178e 100644 --- a/PD2Launcherv2/PD2Launcherv2.csproj +++ b/PD2Launcherv2/PD2Launcherv2.csproj @@ -60,6 +60,7 @@ + @@ -117,6 +118,7 @@ + diff --git a/PD2Launcherv2/Resources/Images/winelogo32.png b/PD2Launcherv2/Resources/Images/winelogo32.png new file mode 100644 index 0000000000000000000000000000000000000000..450a1111a5bbed9598af9a8ff52ed8b617f6853e GIT binary patch literal 1613 zcmV-T2D15yP)z@;j|==^1poj5AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUyPGwk0W=%~1DgXcg2mk~D8UO_V00031002G#8vq3W00031002e- zC;$Th000310ssI4695AM000310RR9100000003YB00031003YB0004Z1>ffY00nVL zL_t(o!@ZYXY!p=#$N%@<`PiLpcV|DkZ3SBkm==VlH47q7uUcindYHTJ3_dgc`I;w{+Y6?Ci|!%p6~;A-+)C#gp9J zc{ulfe>vyed(QwXFvBnu!!VSUXt1R^9zT9OD1=4l`W9%Gb?B$-G z9(yUee5n9Jh>HOB6VX{Bdcri#SZ{AH>+S7jzVAmRNqUlq&H;FVh`wB^uC!D~&Usxp z9M0r&xo*Z-i>7He`uqE{j4=j4cU^ZC=X_Hv7Q30tUD@-zzg;{bgrz9TLb+VNcKY<`ec5a_W*CN3l?cRQu`NV&MborHM08Y^<=-vK zy0U)#`X4iyjN*Bos;a8kwQHB&+1XhG;GuXtz8Sz!RRD-+I}v?FL_q)%+qOk2l?vo? zIbBuNAm?0dYikRQjEqnq5D2xlwoU+ecqsr&Ey6AU-vDUxJWtHe&uio3hRG%c!WTFmpj zn5JooU@(}No}LcH<8fk)ZCPBqvJ{*+abm3yqS)8hH?w!|-gT9Q1$}CCbbao^h0p_Y za}kMi)l*f+Qxtchre zqU+iu7sFU#tYUI*JB+zC7cMM_wzfP;(wY?m0H`ng@=N1jQkOqTPBM8k?xz&WqaXR~q^h> zZZ}gD4gj82_I*`8Qz$I#F$}}MBQ4pjHZU-t&dg5V3grWB*`uikpw{E+%4Ym9}@t0^VpjkB;YwkR(48~ ze9vqq6Sb@|6eUntC>7F{6d(ll`7l_Nh_9pLSEjvBfiIo!UfqnNBq`kv=`XFnNS zMFc$a^zK4!Z8%UW71`(yBXn)@>RYe9`i4=}BJ_Rlm2F#hWF6bdrc#$nrBX?jgsix( z#|H<`WF5zmw{G1Uf9%o6Lj&(#Jq)0#0P@Vt%@2l#zTV&2(HZXO=wP;O`@Zi1U?La{ zhU@C;E7WB8%Syzy**8kX~$zmTZ3kF&}G79Ahjo00000 LNkvXXu0mjfTl2QP literal 0 HcmV?d00001 diff --git a/PD2Launcherv2/ViewModels/OptionsViewModel.cs b/PD2Launcherv2/ViewModels/OptionsViewModel.cs index 3557e34b..97707bfd 100644 --- a/PD2Launcherv2/ViewModels/OptionsViewModel.cs +++ b/PD2Launcherv2/ViewModels/OptionsViewModel.cs @@ -8,6 +8,10 @@ using System.Windows; using System.Diagnostics; using PD2Launcherv2.Messages; +using PD2Launcherv2.Utils; +using PD2Shared.Logging; +using static PD2Shared.Logging.LoggingStatic; +using PD2Shared.Utils; namespace PD2Launcherv2.ViewModels { @@ -137,6 +141,28 @@ private void ToggleAdvancedOptions() private void SetWindowsPermissions() { + if (Wine.IsRunningUnderWine) + { + try + { + Wine.ApplyWineConfiguration(); + + MsgBox.Info("Configuration applied successfully."); + } + catch (Wine.WineException ex) + { + L.CallerError(ex.InnerException, ex.Message); + MsgBox.Exception(ex, "Failed to apply configuration:"); + } + catch (Exception ex) + { + L.CallerError(ex, "Failed to apply configuration."); + MsgBox.Exception(ex, "Failed to apply configuration:"); + } + + return; + } + var startInfo = new ProcessStartInfo() { FileName = "powershell.exe", @@ -148,6 +174,28 @@ private void SetWindowsPermissions() private void RemoveWindowsPermissions() { + if (Wine.IsRunningUnderWine) + { + try + { + Wine.RemoveWineConfiguration(); + + MsgBox.Info("Configuration removed successfully."); + } + catch (Wine.WineException ex) + { + L.CallerError(ex.InnerException, ex.Message); + MsgBox.Exception(ex, "Failed to remove configuration:"); + } + catch (Exception ex) + { + L.CallerError(ex, "Failed to remove configuration."); + MsgBox.Exception(ex, "Failed to remove configuration:"); + } + + return; + } + var startInfo = new ProcessStartInfo() { FileName = "powershell.exe", diff --git a/PD2Launcherv2/Views/OptionsView.xaml b/PD2Launcherv2/Views/OptionsView.xaml index efedbc9d..7bf3ff2b 100644 --- a/PD2Launcherv2/Views/OptionsView.xaml +++ b/PD2Launcherv2/Views/OptionsView.xaml @@ -15,8 +15,9 @@ - + + + (); + + // It's much easier to have this set up here than via the model + if (Wine.IsRunningUnderWine) + { + WindowsPermissionsText.Text = "Wine configuration"; + SetWindowsPermissions.Content = "Apply"; + SetWindowsPermissions.ToolTip = null; + RemoveWindowsPermissions.Content = "Remove"; + RemoveWindowsPermissions.ToolTip = null; + } + else + { + WineLogo32Image.Visibility = System.Windows.Visibility.Collapsed; + WineLogo32ImageCopy.Visibility = System.Windows.Visibility.Collapsed; + } } } } \ No newline at end of file From 4a1f1e8d07ef01b2c953f0fec04ee4a25ac667e0 Mon Sep 17 00:00:00 2001 From: Slacker <288981238+Slacker86@users.noreply.github.com> Date: Fri, 29 May 2026 17:40:22 +0200 Subject: [PATCH 24/94] PD2Launcherv2: MainWindow: Offer applying PD2-specific Wine configuration during first run --- PD2Launcherv2/MainWindow.xaml.cs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/PD2Launcherv2/MainWindow.xaml.cs b/PD2Launcherv2/MainWindow.xaml.cs index 724648f8..e5e956df 100644 --- a/PD2Launcherv2/MainWindow.xaml.cs +++ b/PD2Launcherv2/MainWindow.xaml.cs @@ -19,6 +19,8 @@ using System.Windows.Threading; using System.IO; using PD2Launcherv2.Utils; +using PD2Shared.Logging; +using static PD2Shared.Logging.LoggingStatic; using PD2Shared.Utils; namespace PD2Launcherv2 @@ -624,6 +626,28 @@ public void InitializeDefaultSettings(ILocalStorage localStorage) localStorageUpdated = true; } + + if (MsgBox.Info( + "Would you like to apply PD2-specific Wine configuration?\n" + + "(This can be re-adjusted in the Options menu at any time).", + MessageBoxButton.YesNo, + MessageBoxResult.Yes) == MessageBoxResult.Yes) + { + try + { + Wine.ApplyWineConfiguration(); + } + catch (Wine.WineException ex) + { + L.CallerError(ex.InnerException, ex.Message); + MsgBox.Exception(ex, "Failed to apply Wine configuration:"); + } + catch (Exception ex) + { + L.CallerError(ex, "Failed to apply Wine configuration."); + MsgBox.Exception(ex, "Failed to apply Wine configuration:"); + } + } } if (!localStorageUpdated) From 3411bd6e007a471f5161969d64fa7031c1af4e44 Mon Sep 17 00:00:00 2001 From: Slacker <288981238+Slacker86@users.noreply.github.com> Date: Fri, 29 May 2026 17:40:22 +0200 Subject: [PATCH 25/94] PD2Launcherv2, PD2Shared: Add UseHttp2 to LauncherOptions ...for requesting using HTTP/2 or newer protocol for certain operations --- PD2Launcherv2/MainWindow.xaml.cs | 21 +++++++++++++++- .../Messages/LauncherOptionsChangeMessage.cs | 1 + PD2Launcherv2/ViewModels/OptionsViewModel.cs | 24 +++++++++++++++++++ PD2Launcherv2/Views/OptionsView.xaml | 3 +++ PD2Shared/Models/LauncherOptions.cs | 1 + 5 files changed, 49 insertions(+), 1 deletion(-) diff --git a/PD2Launcherv2/MainWindow.xaml.cs b/PD2Launcherv2/MainWindow.xaml.cs index e5e956df..d4291e76 100644 --- a/PD2Launcherv2/MainWindow.xaml.cs +++ b/PD2Launcherv2/MainWindow.xaml.cs @@ -112,6 +112,20 @@ public bool ForceSoftwareRenderer } } + private bool _useHttp2; + public bool UseHttp2 + { + get => _useHttp2; + set + { + if (_useHttp2 != value) + { + _useHttp2 = value; + OnPropertyChanged(nameof(UseHttp2)); + } + } + } + private bool _isDisableUpdates; public bool IsDisableUpdates { @@ -456,6 +470,7 @@ private void LoadOptions() { var launcherOptions = _localStorage.LoadSection(StorageKey.LauncherOptions); ForceSoftwareRenderer = launcherOptions?.ForceSoftwareRenderer == true; + UseHttp2 = launcherOptions?.UseHttp2 == true; IsDisableUpdates = launcherOptions?.DisableAutoUpdate == true; } @@ -471,6 +486,8 @@ private void OnLauncherOptionsChanged(LauncherOptionsChangeMessage message) { ForceSoftwareRenderer = message.ForceSoftwareRenderer; OnPropertyChanged(nameof(ForceSoftwareRenderer)); + UseHttp2 = message.UseHttp2; + OnPropertyChanged(nameof(UseHttp2)); IsDisableUpdates = message.DisableAutoUpdate; OnPropertyChanged(nameof(IsDisableUpdates)); } @@ -621,7 +638,9 @@ public void InitializeDefaultSettings(ILocalStorage localStorage) { localStorage.Update(StorageKey.LauncherOptions, new LauncherOptions() { - ForceSoftwareRenderer = true + ForceSoftwareRenderer = true, + // HTTP/2 performance in Wine is currently subpar + UseHttp2 = false }); localStorageUpdated = true; diff --git a/PD2Launcherv2/Messages/LauncherOptionsChangeMessage.cs b/PD2Launcherv2/Messages/LauncherOptionsChangeMessage.cs index a74c20de..f5c9817f 100644 --- a/PD2Launcherv2/Messages/LauncherOptionsChangeMessage.cs +++ b/PD2Launcherv2/Messages/LauncherOptionsChangeMessage.cs @@ -3,6 +3,7 @@ public class LauncherOptionsChangeMessage { public bool ForceSoftwareRenderer { get; set; } + public bool UseHttp2 { get; set; } public bool DisableAutoUpdate { get; set; } } } diff --git a/PD2Launcherv2/ViewModels/OptionsViewModel.cs b/PD2Launcherv2/ViewModels/OptionsViewModel.cs index 97707bfd..c8c9a222 100644 --- a/PD2Launcherv2/ViewModels/OptionsViewModel.cs +++ b/PD2Launcherv2/ViewModels/OptionsViewModel.cs @@ -564,6 +564,27 @@ public bool ForceSoftwareRenderer Messenger.Default.Send(new LauncherOptionsChangeMessage { ForceSoftwareRenderer = value, + UseHttp2 = UseHttp2, + DisableAutoUpdate = AutoUpdate + }); + } + } + } + + private bool _useHttp2; + public bool UseHttp2 + { + get => _useHttp2; + set + { + if (_useHttp2 != value) + { + _useHttp2 = value; + OnPropertyChanged(); + Messenger.Default.Send(new LauncherOptionsChangeMessage + { + ForceSoftwareRenderer = ForceSoftwareRenderer, + UseHttp2 = value, DisableAutoUpdate = AutoUpdate }); } @@ -584,6 +605,7 @@ public bool AutoUpdate Messenger.Default.Send(new LauncherOptionsChangeMessage { ForceSoftwareRenderer = ForceSoftwareRenderer, + UseHttp2 = UseHttp2, DisableAutoUpdate = value }); } @@ -681,6 +703,7 @@ private void LoadLauncherOptions() if (launcherOptions != null) { ForceSoftwareRenderer = launcherOptions.ForceSoftwareRenderer; + UseHttp2 = launcherOptions.UseHttp2; AutoUpdate = launcherOptions.DisableAutoUpdate; } Debug.WriteLine("end LoadLauncherOptions\n"); @@ -706,6 +729,7 @@ private void UpdateLauncherOptionsStorage() var launcherOptions = new LauncherOptions { ForceSoftwareRenderer = ForceSoftwareRenderer, + UseHttp2 = UseHttp2, DisableAutoUpdate = AutoUpdate }; _localStorage.Update(StorageKey.LauncherOptions, launcherOptions); diff --git a/PD2Launcherv2/Views/OptionsView.xaml b/PD2Launcherv2/Views/OptionsView.xaml index 7bf3ff2b..9310a942 100644 --- a/PD2Launcherv2/Views/OptionsView.xaml +++ b/PD2Launcherv2/Views/OptionsView.xaml @@ -78,6 +78,9 @@ + diff --git a/PD2Shared/Models/LauncherOptions.cs b/PD2Shared/Models/LauncherOptions.cs index 9ee4298a..5de7dbd1 100644 --- a/PD2Shared/Models/LauncherOptions.cs +++ b/PD2Shared/Models/LauncherOptions.cs @@ -3,6 +3,7 @@ public class LauncherOptions { public bool ForceSoftwareRenderer { get; set; } = false; + public bool UseHttp2 { get; set; } = false; public bool DisableAutoUpdate { get; set; } = false; } } From 4e12731f06e58a5a7554c7006b565ac0476a3722 Mon Sep 17 00:00:00 2001 From: Slacker <288981238+Slacker86@users.noreply.github.com> Date: Fri, 29 May 2026 17:40:22 +0200 Subject: [PATCH 26/94] PD2Launcherv2: MainWindow: Remove an odd callback trigger in CloseButton_Click() --- PD2Launcherv2/MainWindow.xaml.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/PD2Launcherv2/MainWindow.xaml.cs b/PD2Launcherv2/MainWindow.xaml.cs index d4291e76..cc287f10 100644 --- a/PD2Launcherv2/MainWindow.xaml.cs +++ b/PD2Launcherv2/MainWindow.xaml.cs @@ -442,7 +442,6 @@ private void DonateButton_Click(object sender, RoutedEventArgs e) private void CloseButton_Click(object sender, RoutedEventArgs e) { - MainWindow_Closed(sender, e); this.Close(); } From add387c2381743f2e09405e59bab62a154a43b7c Mon Sep 17 00:00:00 2001 From: Slacker <288981238+Slacker86@users.noreply.github.com> Date: Fri, 29 May 2026 17:40:22 +0200 Subject: [PATCH 27/94] PD2Launcherv2: Update Resources/Images/empty_button.png ...with one that has less contrasting edges --- .../Resources/Images/empty_button.png | Bin 11336 -> 11964 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/PD2Launcherv2/Resources/Images/empty_button.png b/PD2Launcherv2/Resources/Images/empty_button.png index ce584199b4d67953d838b9b0deb874a908305ab0..616ca2eb84eab56a8fa9685b1f0e1ff494c3ad85 100644 GIT binary patch literal 11964 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 KMNUMnLSTX&|3b?E From ccbbd378b5a7a9997190b0d6c073bb002283edd8 Mon Sep 17 00:00:00 2001 From: Slacker <288981238+Slacker86@users.noreply.github.com> Date: Fri, 29 May 2026 17:40:22 +0200 Subject: [PATCH 28/94] PD2Launcherv2: Resources/Images: Update image exif tags with explicit 96x96 DPI resolution across the board ...due to WPF relying on image metadata when computing output sizes of raster images. --- PD2Launcherv2/Resources/Images/bg1.jpg | Bin 264187 -> 264183 bytes PD2Launcherv2/Resources/Images/bg2.jpg | Bin 256223 -> 256219 bytes PD2Launcherv2/Resources/Images/bg_plain.jpg | Bin 76904 -> 76901 bytes PD2Launcherv2/Resources/Images/btn_beta.jpg | Bin 14220 -> 14219 bytes PD2Launcherv2/Resources/Images/btn_live.jpg | Bin 13915 -> 13913 bytes PD2Launcherv2/Resources/Images/btn_more.jpg | Bin 1437 -> 1435 bytes .../Resources/Images/btn_more_pressed.jpg | Bin 1411 -> 1409 bytes .../Resources/Images/btn_no_updates3.jpg | Bin 18505 -> 18504 bytes PD2Launcherv2/Resources/Images/btn_switch.jpg | Bin 9782 -> 9780 bytes .../Resources/Images/btn_switch_pressed.jpg | Bin 9797 -> 9795 bytes .../Resources/Images/checkbox_checked.png | Bin 855 -> 945 bytes .../Resources/Images/checkbox_unchecked.png | Bin 852 -> 942 bytes PD2Launcherv2/Resources/Images/close.jpg | Bin 9933 -> 9931 bytes .../Resources/Images/close_pressed.jpg | Bin 9436 -> 9433 bytes .../Resources/Images/custom_notif.jpg | Bin 14844 -> 14843 bytes PD2Launcherv2/Resources/Images/donate.jpg | Bin 7759 -> 7847 bytes .../Resources/Images/donate_pressed.jpg | Bin 7565 -> 7653 bytes PD2Launcherv2/Resources/Images/logo.gif | Bin 482560 -> 485721 bytes PD2Launcherv2/Resources/Images/loot.jpg | Bin 19830 -> 19828 bytes .../Resources/Images/loot_pressed.jpg | Bin 19653 -> 19652 bytes PD2Launcherv2/Resources/Images/minimize.jpg | Bin 9567 -> 9566 bytes .../Resources/Images/minimize_pressed.jpg | Bin 9111 -> 9108 bytes PD2Launcherv2/Resources/Images/options.jpg | Bin 21975 -> 21974 bytes .../Resources/Images/options_pressed.jpg | Bin 21905 -> 21903 bytes PD2Launcherv2/Resources/Images/play.jpg | Bin 21891 -> 21890 bytes .../Resources/Images/play_pressed.jpg | Bin 21755 -> 21754 bytes PD2Launcherv2/Resources/Images/thin_next.jpg | Bin 11373 -> 11370 bytes PD2Launcherv2/Resources/Images/thin_prev.jpg | Bin 11446 -> 11443 bytes .../Resources/Images/updating_disabled.jpg | Bin 19084 -> 19136 bytes 29 files changed, 0 insertions(+), 0 deletions(-) diff --git a/PD2Launcherv2/Resources/Images/bg1.jpg b/PD2Launcherv2/Resources/Images/bg1.jpg index 786a82f12f3c91b2aaa8d6080d93bd8a4dee92a6..9474768d20d1e6a0f137ea99e03381f041d3ec3e 100644 GIT binary patch delta 151 zcmey}FYvuzfbIW{{}1_pO=JsKoYbDlz{0@5zzD?4fcOFsCjc>s52hyt%ZV~D7#SFu zS{ayF8JQ>;np&A!S{WNp+^jS4p@ga&qcj66L<6H5qXq*rn9an%z{ChMm{D)D9Ami< bC)hj&CWeIOdBW}Ugc*UDY5P23=GZR)8%Q1~ delta 152 zcmey~FYvoxfbIW{{}1{9O=JsKoY_fObpT`yAAGy^L{nz4jYgMk^$W@2DqVuTsSShG2cQIL}pteb&}Az^b1*L7_GOjQ@^ delta 129 zcmcbab32Fa|Be3-*}hCo#XF3UYFKfE6$?cx-Os Hx~>fXOY9n( diff --git a/PD2Launcherv2/Resources/Images/btn_more.jpg b/PD2Launcherv2/Resources/Images/btn_more.jpg index 4a502f24a9682a210e266dbfd1c6c2c6d8761660..e2d465b6d9b2f3d9c0865f3fc7a79e1c870cdce1 100644 GIT binary patch delta 50 zcmbQsJ)2wN|Be3-7zDiBJlz-=85tQ8fRN$;Lq@KN5(_zjOa?|EPMCNoZnF|&H46ZY C%nlR) delta 50 zcmbQuJ(pYJ|Be3-7zDiBJlz-=85tQofRN$;Lq?v75(_zjOa?|E_Lz7mZnGR?EeimJ C2o1CV diff --git a/PD2Launcherv2/Resources/Images/btn_more_pressed.jpg b/PD2Launcherv2/Resources/Images/btn_more_pressed.jpg index 085a5260dfc97befa538f40792a2c511d2420075..974b234897d927b68550379c26fd473bced8dc85 100644 GIT binary patch delta 50 zcmZqXZseBuf8+lH1_3WOPd5feMn;ANAY}OekdbSm#6nIWlYtS46DA&t+pNTx$N~U@ Cqz%RZ delta 50 zcmZqVZswNwf8+lH1_3WOPd5feMn(n?AY}OekdbGi#6nIWlYtS4JtiKC+bqYJ%mM&< C;0(f$_uyMz;Sq{y*gSFp+Jt`jz%f1{MYe21X!$0i@&@IKYsBApt51rY9bdpLkDf z;#afDx{MO4IgHW_tPp9&5=IRMW-yzHfq{t;W*B45<}Ai;c1DKHUpbnYIl+c7Ffk-- JZsa*(4FD?q8o&Sm delta 136 zcmX>xf$`)7Mz;Sq{y*gSIFW6#`sMaa1{MYe21X!$0i+ZdIKYtWPXhykx&Q-|o_Iih z;seo%U(D4R7}yw@8JK~}85!7sgcJiSn9abL$0!YFmrV{}6lbj4oWa=5zWEDBGc%_L OSQQh4$L0o}6V?C@3LS3% diff --git a/PD2Launcherv2/Resources/Images/btn_switch.jpg b/PD2Launcherv2/Resources/Images/btn_switch.jpg index dac743fa57dac33c84981185d1b854d9524494f1..425406eebfbab8ddf86f5396e8dd13433826c475 100644 GIT binary patch delta 125 zcmdnyv&Dz)|Be3-S@uk1o2-7NJ(Gcjfq{V$h+hCHIR*|eWMD{uih}8h2jnN-6Px(e zY_cw+glZ0>Gy^L{nz4jYgMk^$W@2DqVuTsSShG2c@irSLST_R`L&9c1ju)~3{PY*P delta 129 zcmdnuv(1O?|Be3-S@un2o2-7hJ(Gcjfq{V$h+hCH1qKc<o#XF-e%+U04rc(@Yw9b H@j?~=`GXp3 diff --git a/PD2Launcherv2/Resources/Images/btn_switch_pressed.jpg b/PD2Launcherv2/Resources/Images/btn_switch_pressed.jpg index 56eb9412561844ae2c826953b6e7285a9da34f92..f00732ae4c73ddf01005ec6c418779bd6a8ef3f6 100644 GIT binary patch delta 125 zcmX@=bJ&ON|Be3-Sr$!Xo2-7NJ(Gcjfq{V$h+hCHIR*|eWMD{uih}8h2jnN-6Px(e zY_cw+glZ0>Gy^L{nz4jYgMk^$W@2DqVuTsSShG2c@gN%~ST_R`L&9bg4na8p`~McS delta 129 zcmX@?bJU0J|Be3-S(Z#>o2-7hJ(Gcjfq{V$h+hCH1qKc<o#XF9%SS604rc(@Yrm` HAt(m`_?{VM diff --git a/PD2Launcherv2/Resources/Images/checkbox_checked.png b/PD2Launcherv2/Resources/Images/checkbox_checked.png index 8ffff52945f16a8c55b2a7384ef978af37023d34..b645c3b6b1611105a8ff811878c56efb0c42f066 100644 GIT binary patch delta 99 zcmcc4wvl~;cLal9YJ_K+uP=iZ0|NsG0}G=R11pfl2*h@b(r~sLqXq*rSe%K0fk_z3 X2C0I81Rw?Cf$4JA`_DIe@-YJdMqvta delta 10 RcmdnUew}TC_r|3>%m5d<1J(cl diff --git a/PD2Launcherv2/Resources/Images/checkbox_unchecked.png b/PD2Launcherv2/Resources/Images/checkbox_unchecked.png index ed801a3d4b5fe4d230267a819abd21150172f2f3..d2f822f00b9253e95e324ba6e985a566e2e8c42b 100644 GIT binary patch delta 99 zcmcb@wvK&5Yy^W}YJ_K+uP=iZ0|NsG0}G=R11pfl2*h@b(r~sLqXq*rSe%K0fk_z3 X2C0I81Rw?Cf$4JA`_DH<|6~FHOuP$v delta 10 RcmZ3-euZs9?8fcinE)B-1l|Au diff --git a/PD2Launcherv2/Resources/Images/close.jpg b/PD2Launcherv2/Resources/Images/close.jpg index 9a7add3fb4e6d2e64cae873010a49859ac898c10..1a2bc53b1e3e2c351bbd16604a28ef49c5b5d526 100644 GIT binary patch delta 128 zcmX@>d)k-n|Be3-SsqPfo2-7NJ(Gcjfq{V$h+hCHIR*|eWMD{uih}9M55?se87AHn zoA}jivM!^9Y7V0`11m%YV+o@M12dS-#K6GB2s4kdW^)$fPc}}l0YD29Hs^38$^igg C%oqOv delta 129 zcmX@@d)Alj|Be3-S)NQ}o2-7hJ(Gcjfq{V$h+hCH1qKc<o#XF{$%6y04rc*@YtNi HkthcMB%2y` diff --git a/PD2Launcherv2/Resources/Images/close_pressed.jpg b/PD2Launcherv2/Resources/Images/close_pressed.jpg index a26d6aa3ba31d8c97df78af99e38ae6aeaa3d5f8..3fa701e7771c8457c432902204d0188c12139adf 100644 GIT binary patch delta 143 zcmccPdDD~a|Be3-Sqdhyg)2^K&tza>U|?Vb;$=X50f-ZT7{mwD6YJzf7>o>z46FCO|Be3-S&ApJg)2^M&tza>U|?Vb;uS!Ag@Nl&0|SG)0F0jOASNfyU}Ruq zU}a!vWo)QmU|?lzYGq;uR6KE`&cxeNs!EK~46Hyd&Gy^L{nz4jYgMk^$W@2DqVuTsSShG2cv679EVe@15W@b*XAq-3m37hk| Gd-VV>of=jE delta 134 zcmexe{HK`h|Be3-+3!zeo2-7hJ(Gcjfq{V$h+hCH1qKc<o#XFRb;7 M#Ne?xkGoe7022Tm1poj5 diff --git a/PD2Launcherv2/Resources/Images/donate.jpg b/PD2Launcherv2/Resources/Images/donate.jpg index 092af6185aff4a41e4ab266f434f3b2d53cd9846..6e3f625aa397dbe0e46d6c9f475a3f313ba4abf2 100644 GIT binary patch delta 136 zcmX?av)s1+|Be3-7zDiBJlz-=85tQ8fRN$;LxwQdip(?y247zWEd~Y#4h9xRDF#*` zixG(J7^UHCH%1KxX0SLD0|S#Vlnqh^0SQ0~!~@g+Z!0pTGgzF7fq_XF$_A-|fCL}~;(_V^w;7xnn3+HTL^1=xM)fpV0FM$9 A4gdfE delta 29 jcmaEA-D@rK|Hl6Z3<6$mo^A|`j0_BnK*+E$r&1OGqu~gd diff --git a/PD2Launcherv2/Resources/Images/logo.gif b/PD2Launcherv2/Resources/Images/logo.gif index b2e2af112b2db69ab8d627062569337da116040e..b74faef22a90ddf0f56f9924c7cf2f07719e1fd7 100644 GIT binary patch delta 3248 zcmZqJC42Ld>;?_ydd2_T5xxNmE{P?HK-$K>q98FjJGDe1DK$Ma&sP2Y?)~ZtnJKpF z;ikR@z6H*y8JQkcMXAA6ej&+K*~ykE>h^YAHWgMCxdpkYC5Z|ZxjA{oRu#7Di7EL> zsa8NXNLXK80j#7X+g9B(H!(fc%F4AOGc6=PzdlF7&{)sZR2^taQA(Oskc%7C3?R=| zJ)@+gz)D}gyu4hm+*mKaC|%#s($Z4jz)0W7NEfI=x41H|B(Xv_uUHvq0+#~V442g6 z1*I0}=ahns3(dhtjDf65=j{7D}9hR zkh}#9EU*wTv_MRF@Pph;8mK2tIYc$dw&1n|kNs4%1&{r7u?4p!c>Iss(Kuvq+k!&_ zP?&W4$+3lW`>AUS4*P+YknVpRX5kjb>lLcmkJlF5jw8{2GHoHzeyZDo&3=d_B>Eql zL3qUod4)Rm6S4)j6G^e3d|ODdpBA=Y*blda6#rwGLP!8^3vPu}u%8H9s9--uwh(PU zjch@+AK4Nrgg>g`a2^+;EuESNY(SR-Tgr_8|NZ^*``6DO-@kqR^7+%p5AW;Wy?yig z)yo&ppFMr@_|d}$_wU`kbNklK8`rO0y>j`|#S7=pojr5<)X5Xaj~zX7_|U-v`}ghL zvwPRh9ox5U-LiSp#trM&tzENv)yfsimn~hgc+tWI^XJW-Gkey|8PlgtoicgS#0mX< zy*=GsogM9Mtu4(>jScm6wKdgMl@;Y>r6t8hg$4EbdAT{+S(zE>X{jm6Nr?&Zaj`Mc zQIQehVWA;?_y?b@ZR&MeJGgxik@GXgOa5HkZY3lOscG28Yd!t7H+0mi%$zyJUM diff --git a/PD2Launcherv2/Resources/Images/loot.jpg b/PD2Launcherv2/Resources/Images/loot.jpg index aa0bf543fe863f20def2061e1ea2292a82c1a955..20c67945039a7c247256812f604cd03306c6fb8a 100644 GIT binary patch delta 130 zcmex1i}A}WMz;Sq{y*fHG?8tx`jz%f1{MYe21X!$0i@&@IKYsBApt51rYB#Nm1AU> zcu#ENSF_2wj1sCjjM5CO5EYCij2aBgU^Wv20}~_6JjR;MS&SRGIKc)0ElAj`!V}~O E0Ihf!BLDyZ delta 131 zcmew|i}BkmMz;Sq{y*fHGLdbv`sMaa1{MYe21X!$0i+ZdIKYtWPXhykx&Q-|o_Iih z;seo%U(D4R7}yw@8JK~}85!7sgcJiSn9abL$0!YFmrV{}6lbj4oWZz}i_-(FfRVvt Jvl35`BLH^C8l(UK diff --git a/PD2Launcherv2/Resources/Images/loot_pressed.jpg b/PD2Launcherv2/Resources/Images/loot_pressed.jpg index 10ad03491a03ab5d07c22a1866033ac1b443ee87..ba9a19aed78c849565612a735783ff5e108f3ae6 100644 GIT binary patch delta 134 zcmX>)lkvz*Mz;Sq{y*eMn8-F+{YrZ#0}BHK10xW>08(-c9AL=6kN_10(-RNKPrN5K z@vGTnT}BDj97bsdR){oX38MxBGnmc9z`(=^GmNoja~9)oHb#ccsT|eJoM1zMmLzO` I$$iHG0PI^DkN^Mx delta 136 zcmX>ylkw08$DJ9AL=xr-6Y%U4Q{fPdp$$ z@qy^XFXrkD3~Y?d49q~~j0|i*LW+SE%w}NBW0Z!o%O(dfiZj-2&S3n_wmF5PnwirB OtcsDrWAh8{I}QNNe;q#n diff --git a/PD2Launcherv2/Resources/Images/minimize.jpg b/PD2Launcherv2/Resources/Images/minimize.jpg index 3e730488d2ae2668b034fdc7dbfe21d1a290173a..fa2df52c5ef683fc431707348c19a4d6fdcd8b2b 100644 GIT binary patch delta 132 zcmccbbG}%~-;y!N3e=GchnQF~SUEtl6B!sLIU9u(_Y5nwb-92+)#*&B7c$ FvH-1+7z+RZ delta 134 zcmccTb>EBa|Be3-S-K~(O;*3$p2@(%z`(!=#4mu90s{vaa{XywU{DuefYK8W$WMGA zI`NCSIs*e6BQpatP&p$58<3D0gU2|b(=F7Rhc*Uu~aj2dVp0i MGI(qj;_#6L0FT=mx&QzG diff --git a/PD2Launcherv2/Resources/Images/minimize_pressed.jpg b/PD2Launcherv2/Resources/Images/minimize_pressed.jpg index 752c879b96a2d9eb46872b9a1937b6dd8e3986c5..d76aad2f3304c66c79f218ef963269ca16bcb2c6 100644 GIT binary patch delta 145 zcmbR4KE<8w|Be3-S;Qu?g)2^K&tza>U|?Vb;$=X50f-ZT7{mwDlM968L>U;242%q{ z3=FM|4HXOwtc;DVOpPaQ)|vQFLRF4Ynt>IfgHer9gMk^$W@2DqVuYE@sJB^;@jsBi US(YW8nGU|?Vb;uS!Ag@Nl&0|SG)0F0hkCog7XU}Rur zU}$A*s9<1VWo&F^Y7A65aih+}+fu4ZjM5COKrYY(MlD7S24*muiGhKM5oR)@(Pk;e X|4f^uSkjp}J;3se3?7@i*tui?PedCd diff --git a/PD2Launcherv2/Resources/Images/options.jpg b/PD2Launcherv2/Resources/Images/options.jpg index ba5ba8c49a7a3677a3296194236d403fcbdc3e59..9184b4236c408e736daac5e5fe2c85f814cb4a47 100644 GIT binary patch delta 137 zcmcbj0~GMb5=8Rg3STi Lld##GcYy~0Q@R=D delta 137 zcmcb%n(_K-Mz;Sq{y*egKap*+`sMaa1{MYe21X!$0i+ZdIKYtWPXhykx&Q-|o@^*9 zH}SsM#4qOR3=C|H%nZyx^^6Q`KthUv70hN}%wv>>v&$w2Fp4wQZO&ly4^vAC*BjA z_|AHz!y((Da1O6108$DJ9AL=xr-6Y%U4Q{fPiB;r zn|NPr;umvu1_m}pW(H=UdPW8|AR)!T3T87f<}pgc*=3Uh7{wXuHfJy{<>vGND_~^s K*eu3d=>Y%$KpAiV diff --git a/PD2Launcherv2/Resources/Images/play.jpg b/PD2Launcherv2/Resources/Images/play.jpg index 7d8d4b46cf8834bd61cb74ceca09107d3795bec1..01ca09ca76444f5751838149b7af18e39c3e4ea9 100644 GIT binary patch delta 134 zcmZo(&DgY>k?sGD{|`BnCbCUdztWz`z{0@5zzD=IfRr2q2N*IiBtS*M^uz=56Yq&l z{AxB?mr+7Bhf$h=6(Y@8!l=Q(3}!PiFfcK~3}dX>oW=N;osnU4I%hRAC)g06B?+5f I^UUx70K5Pi00000 delta 136 zcmZo#&Dgw}k?sGD{|`BnC$ddezucb5z{0@5zzD=IfRq9Q2N-hwX<%Sb7hr(W6A#Ew zd>}gUi@7=j0~;eV12a%LBLf?dkYZp3vl$rk7^UItvdICA;*52hGZ_D}Z%*T^X6Ez& Ot72sE*!+rTh6ezgWE`&m diff --git a/PD2Launcherv2/Resources/Images/play_pressed.jpg b/PD2Launcherv2/Resources/Images/play_pressed.jpg index 0e1ab20fb26809bfacc6651351d32625fe2a44d2..017e7d1f2d9688d19ba8c207d5cdadc090f81bfd 100644 GIT binary patch delta 134 zcmeyplJVC{Mz;Sq{y*e&naDO-{YrZ#0}BHK10xW>08(-c9AL=6kN_10(-RNKPrN5K z@vGTnT}BDj97bsdR){oX38MxBGnmc9z`(=^GmNoja~9)$c1DKHo}A6hoM1zMmLzPx I%A@8103t6M5&!@I delta 137 zcmeyhlJWOSMz;Sq{y*e&oyay>{c?LI0}BHK10xW>08$DJ9AL=xr-6Y%U4Q{fPfnDV zn|NPr;umvu1_m}pW(H=UdPW8|AR)!T3T87f<}pgc*=3Uh7{wXuHfJ#2XW#6>+04x8 P0anGx;Ia7%kD3Pn0Qnq( diff --git a/PD2Launcherv2/Resources/Images/thin_next.jpg b/PD2Launcherv2/Resources/Images/thin_next.jpg index 9bad28b7a2af0a1d17a29fbb3a5460c5c6a218e2..9949ae3bb7209f5f34bd8106720f5c4bd4a918d3 100644 GIT binary patch delta 145 zcmaDG@hXDt|Be3-ndePp3s;=fp2@(%z`(!=#LIyA0uUzvF^CVQCpQSni83%485kK@ z8JJlam@0rsV=Dv8iJNsMK9o?EW0YoKh3H^ZW7J?^2D6zM7?>DgCNt`7mSb!O@;5JK SPG{x>n+vodVY4WEgem|Nni=>2 delta 143 zcmaDA@iv0(|Be3-nHNrE3s;=jp2@(%z`(!=#4CXK3Io@l1_lOo0T?~8PF~E&z{tSL zz|6|PQ~^X9TNzjal}_BKGx4^RsuH6#11pdVG=WizQGU}A)s%xJV(im{z( V^CISSW=;>VJR^h0W+Ct diff --git a/PD2Launcherv2/Resources/Images/thin_prev.jpg b/PD2Launcherv2/Resources/Images/thin_prev.jpg index 987284375a0fafccebcd053e2d5cf07fd78b0873..166458433d1a6407884a0cee8dd44fe704e8af59 100644 GIT binary patch delta 143 zcmdlMxjB;U|Be3-nYT@33s;=fp2@(%z`(!=#LIyA0uUzvF^CVQC)UY}Fc=va8CV&Z zSs9orfJh@NQ-g_{btXQPP?ckpW?+S=XH;XTQ-|oDbx0-picM Q%n3FXXnw+GUG`2@0LAng0{{R3 delta 143 zcmdlSxh<0I|Be3-nRiZP3s;=jp2@(%z`(!=#4CXK3Io@l1_lOo0T?~8PF~E&z{tSL zz|6|PQ~^X9S(zFDl}_BKGx4^RsuH6#11pdVG=WizQGU}A)s%xJV(ig7;E V=3UI`%$y!zc}510&06f8ssP;Y9FPD2 diff --git a/PD2Launcherv2/Resources/Images/updating_disabled.jpg b/PD2Launcherv2/Resources/Images/updating_disabled.jpg index 2f1344df709295dd84f42a2fdc7555f1e5609589..8602ab492437d821d0cfdb599c1b5f58b55419a0 100644 GIT binary patch delta 116 zcmeB~%6MQZW7Pi}{~t1hxmIMRF);Z0GH5X{FmN!iFbXj+16hnf$iOJYzzSwFFxW9l m!`W_(8c;P%Kv@V#08&653}AYrS~W8#M1+wcVe=~HM{WRHj1RN` delta 64 zcmX>wm9b|kquT!){~t0axmIMRF);Z0GH5X{FmNz1G72#;16hnf2nHLYs+l=Gpducd J?=e4e0{|5(4aooi From 7d0abb8cb270e6d6745c9f8237a94faf61c5b892 Mon Sep 17 00:00:00 2001 From: Slacker <288981238+Slacker86@users.noreply.github.com> Date: Fri, 29 May 2026 17:40:22 +0200 Subject: [PATCH 29/94] PD2Launcherv2: Resources/Images: Remove bogus PhysicalPixel (PixelsPerUnitX/Y, PixelUnits) PNG tags from affected files ...to not trip up WPF even more --- .../Resources/Images/checkbox_checked.png | Bin 945 -> 924 bytes .../Resources/Images/checkbox_unchecked.png | Bin 942 -> 921 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/PD2Launcherv2/Resources/Images/checkbox_checked.png b/PD2Launcherv2/Resources/Images/checkbox_checked.png index b645c3b6b1611105a8ff811878c56efb0c42f066..0d4493d81d18234a2e63e1029ec8be22e2406ea4 100644 GIT binary patch delta 10 RcmdnUK8JmR%0|lyW&jkr19boZ delta 29 icmbQkzL9-`iV$akM`SSrgPt-7Ggd6MFWabC%?to Date: Fri, 29 May 2026 17:40:22 +0200 Subject: [PATCH 30/94] PD2Launcherv2: ControlStyles: Update ImageFillProgressBarStyle visuals ...not to obscure the underlying image too much --- PD2Launcherv2/Resources/Styles/ControlStyles.xaml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/PD2Launcherv2/Resources/Styles/ControlStyles.xaml b/PD2Launcherv2/Resources/Styles/ControlStyles.xaml index 00bb549f..5a75cb57 100644 --- a/PD2Launcherv2/Resources/Styles/ControlStyles.xaml +++ b/PD2Launcherv2/Resources/Styles/ControlStyles.xaml @@ -109,14 +109,17 @@ + + + + + + + + - + + + + diff --git a/PD2Launcherv2/Resources/Styles/ControlStyles.xaml b/PD2Launcherv2/Resources/Styles/ControlStyles.xaml index d41992cc..2fc14d2e 100644 --- a/PD2Launcherv2/Resources/Styles/ControlStyles.xaml +++ b/PD2Launcherv2/Resources/Styles/ControlStyles.xaml @@ -63,18 +63,6 @@ - - @@ -286,9 +286,9 @@ @@ -633,7 +633,7 @@ - + @@ -642,10 +642,10 @@ - + - + @@ -661,14 +661,14 @@ From 84b9d9b4d11288373f7f013c0d2391423cf6ebf4 Mon Sep 17 00:00:00 2001 From: Slacker <288981238+Slacker86@users.noreply.github.com> Date: Tue, 14 Jul 2026 19:35:32 +0200 Subject: [PATCH 87/94] PD2Shared: Wine.ApplyWineConfiguration(): Also enable 'EmulateModeset' ...to disable changing the actual display resolution --- PD2Shared/Utils/Wine.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/PD2Shared/Utils/Wine.cs b/PD2Shared/Utils/Wine.cs index 9ab4c6e7..0de08b40 100644 --- a/PD2Shared/Utils/Wine.cs +++ b/PD2Shared/Utils/Wine.cs @@ -29,6 +29,7 @@ private static class DllImports const string HkcuExeKeyPath = @"Software\Wine\AppDefaults\Game.exe"; const string HkcuDllOverridesKeyPath = HkcuExeKeyPath + @"\DllOverrides"; + const string HkcuX11KeyPath = HkcuExeKeyPath + @"\X11 Driver"; // MSVC2019 runtime private static readonly string[] Msvc2019Libs = { @@ -148,6 +149,11 @@ public static void ApplyWineConfiguration() // "*vcomp140"="native,builtin" // "*vcruntime140"="native,builtin" // "*vcruntime140_1"="native,builtin" + // + // [HKEY_CURRENT_USER\Software\Wine\AppDefaults\Game.exe\X11 Driver] + // ; Emulate modesetting -- don't change the actual display resolution + // ; Available since Wine 9.22 + // "EmulateModeset"="Y" using (var key = Registry.CurrentUser.CreateSubKey(HkcuExeKeyPath, writable: true)) { @@ -175,6 +181,17 @@ public static void ApplyWineConfiguration() key.SetValue($"*{libName}", "native,builtin", RegistryValueKind.String); } } + + using (var key = Registry.CurrentUser.CreateSubKey(HkcuX11KeyPath, writable: true)) + { + if (key == null) + { + throw new WineException($"Failed to create registry key: '{Registry.CurrentUser}\\{HkcuX11KeyPath}'"); + } + + // Emulate modesetting + key.SetValue("EmulateModeset", "Y", RegistryValueKind.String); + } } public static void RemoveWineConfiguration() From 4a2047e81d4bf1e00d244f5146a1d37413a47770 Mon Sep 17 00:00:00 2001 From: Slacker <288981238+Slacker86@users.noreply.github.com> Date: Wed, 15 Jul 2026 22:59:42 +0200 Subject: [PATCH 88/94] PD2Launcherv2: Add 'Close after launch' checkbox to the launcher ...that will allow the launcher to gracefully quit after the game has been started and running for more than 10 seconds and the launcher window hasn't seen any interaction during that time. This is a compromise approach as without an explicit signal from the game itself it is impossible to determine whether it was successfully bootstrapped or not. Additionally: * ILaunchGameHelpers, LaunchGameHelpers: Add means of assigning an event handler to the Process.Exited event * LauncherOptions: Add AutoCloseAfterLaunch property to allow storing this setting --- PD2Launcherv2/MainWindow.xaml | 23 +- PD2Launcherv2/MainWindow.xaml.cs | 248 +++++++++++++++++- .../Resources/Styles/ControlStyles.xaml | 18 ++ PD2Shared/Helpers/LaunchGameHelpers.cs | 41 +-- PD2Shared/Interfaces/ILaunchGameHelpers.cs | 4 +- PD2Shared/Models/LauncherOptions.cs | 1 + 6 files changed, 300 insertions(+), 35 deletions(-) diff --git a/PD2Launcherv2/MainWindow.xaml b/PD2Launcherv2/MainWindow.xaml index c51a86f6..8014b71c 100644 --- a/PD2Launcherv2/MainWindow.xaml +++ b/PD2Launcherv2/MainWindow.xaml @@ -153,7 +153,7 @@ - + + + + + @@ -300,13 +317,13 @@ Source="pack://application:,,,/Resources/Images/btn_no_updates3.jpg" Stretch="Fill" HorizontalAlignment="Left" VerticalAlignment="Bottom" - Margin="50,0,0,130" + Margin="50,0,0,132" Visibility="{Binding UpdatesNotificationVisibility}" /> - + diff --git a/PD2Launcherv2/MainWindow.xaml.cs b/PD2Launcherv2/MainWindow.xaml.cs index bdb59ef0..944f8472 100644 --- a/PD2Launcherv2/MainWindow.xaml.cs +++ b/PD2Launcherv2/MainWindow.xaml.cs @@ -78,6 +78,17 @@ private enum KeyComboDown private bool _suppressRendererChangedMessages = false; + // Auto-close + private static readonly TimeSpan AutoCloseTimeSpan = TimeSpan.FromSeconds(10); + + private readonly object _autoCloseLock = new(); + private bool _autoCloseActive = false; + private Process? _autoCloseGameProcess = null; + private readonly EventWaitHandle _autoCloseThreadInterrupt = new(initialState: false, EventResetMode.ManualReset); + private Thread? _autoCloseThread = null; + private readonly Stopwatch _autoCloseProgressUpdateStopwatch = new(); + private DispatcherTimer? _autoCloseProgressUpdateTimer = null; + private bool _isBeta; public bool IsBeta { @@ -181,6 +192,21 @@ public bool IsDisableUpdates } } + private bool _autoCloseAfterLaunch; + public bool AutoCloseAfterLaunch + { + get => _autoCloseAfterLaunch; + set + { + if (_autoCloseAfterLaunch != value) + { + _autoCloseAfterLaunch = value; + AutoCloseCheckBox.IsChecked = _autoCloseAfterLaunch; + OnPropertyChanged(nameof(AutoCloseAfterLaunch)); + } + } + } + private Visibility _updatesNotificationVisibility = Visibility.Collapsed; public Visibility UpdatesNotificationVisibility { @@ -270,6 +296,10 @@ public MainWindow() WineLogo16Image.Visibility = Visibility.Hidden; } + // Auto-close + AutoCloseResetProgress(); + InputManager.Current.PostProcessInput += AutoClosePostProcessInput; + // Don't try to update launcher in debug mode // TEST @@ -279,6 +309,7 @@ public MainWindow() CheckForUpdates(); #endif } + private void OnNavigationMessageReceived(NavigationMessage message) { Overlay.Visibility = Visibility.Collapsed; @@ -584,24 +615,41 @@ void HandleFatalGameFileUpdateException(string cause, string effect) return; } - UpdatePlayButtonText("Launching..."); - try { + UpdatePlayButtonText("Launching..."); + if (Process.GetProcessesByName("Game").Any()) { MsgBox.Warn("Game is already running."); return; } - _launchGameHelpers.LaunchGame(_localStorage); + bool useAutoClose = AutoCloseAfterLaunch; + Process gameProcess; + + try + { + gameProcess = _launchGameHelpers.LaunchGame(_localStorage, useAutoClose ? AutoCloseGameProcessExited : null); + } + catch (Exception ex) + { + L.CallerError(ex, $"{nameof(_launchGameHelpers.LaunchGame)}() threw"); + MsgBox.Exception(ex, "Failed to launch the game:"); + + return; + } + + if (useAutoClose) + { + AutoCloseBegin(gameProcess); + } + else + { + gameProcess.Dispose(); + } await Task.Delay(TimeSpan.FromSeconds(1.5)); } - catch (Exception ex) - { - L.CallerError(ex, $"{nameof(LaunchGameHelpers.LaunchGame)}() threw"); - MsgBox.Exception(ex, "Failed to launch the game:"); - } } finally { @@ -646,6 +694,11 @@ private void Window_Closing(object sender, CancelEventArgs e) _closePending = true; } + + if (AutoCloseAbort()) + { + L.CallerDebug($"Auto-close aborted due to window closing."); + } } private void CheckKeys(KeyboardDevice kd) @@ -687,6 +740,11 @@ private void Window_IsKeyboardFocusWithinChanged(object sender, DependencyProper private void Window_Activated(object sender, EventArgs e) { + if (AutoCloseAbort()) + { + L.CallerDebug($"Auto-close aborted due to window activation."); + } + // Attempt to refocus when closing a modal dialog to get keyboard focus back if (!this.IsKeyboardFocusWithin) { @@ -1007,10 +1065,12 @@ private void LoadConfiguration() private void LoadOptions() { - var launcherOptions = _localStorage.LoadSection(StorageKey.LauncherOptions); - ForceSoftwareRenderer = launcherOptions?.ForceSoftwareRenderer == true; - UseHttp2 = launcherOptions?.UseHttp2 == true; - IsDisableUpdates = launcherOptions?.DisableAutoUpdate == true; + LauncherOptions launcherOptions = _localStorage.LoadSection(StorageKey.LauncherOptions); + + ForceSoftwareRenderer = launcherOptions.ForceSoftwareRenderer; + UseHttp2 = launcherOptions.UseHttp2; + IsDisableUpdates = launcherOptions.DisableAutoUpdate; + AutoCloseAfterLaunch = launcherOptions.AutoCloseAfterLaunch; } private void OnConfigurationChanged(ConfigurationChangeMessage message) @@ -1163,6 +1223,15 @@ private void MainWindow_Closed(object sender, EventArgs e) Debug.WriteLine($"\n\n Saving window position: Left = {this.Left}, Top = {this.Top} \n\n"); _localStorage.Update(StorageKey.WindowPosition, windowPosition); + + // Usage of these local values instead of LocalStorage directly might lead to discrepancy + _localStorage.Update(StorageKey.LauncherOptions, new LauncherOptions + { + ForceSoftwareRenderer = this.ForceSoftwareRenderer, + UseHttp2 = this.UseHttp2, + DisableAutoUpdate = this.IsDisableUpdates, + AutoCloseAfterLaunch = this.AutoCloseAfterLaunch + }); } private void EnsureWindowIsVisible() @@ -1568,5 +1637,160 @@ private async void GoToLogButton_Click(object sender, RoutedEventArgs e) MsgBox.Exception(ex, "Failed to navigate to the log file:", MessageBoxImage.Warning); } } + + private void AutoCloseBegin(Process gameProcess) + { + lock (_autoCloseLock) + { + if (_autoCloseActive) + { + return; + } + + if (gameProcess.HasExited) + { + // If the process has already terminated -- bail + return; + } + + _autoCloseGameProcess = gameProcess; + + _autoCloseProgressUpdateStopwatch.Restart(); + _autoCloseProgressUpdateTimer = new DispatcherTimer( + TimeSpan.FromMilliseconds(UpdateThrottle.DefaultIntervalMilliseconds), + DispatcherPriority.Render, + AutoCloseProgressUpdateTimer, + this.Dispatcher + ); + _autoCloseProgressUpdateTimer.Start(); + + _autoCloseThreadInterrupt.Reset(); + _autoCloseThread = new Thread(AutoCloseThread); + _autoCloseThread.Start(); + + _autoCloseActive = true; + + L.CallerDebug($"Auto-closing in {AutoCloseTimeSpan}..."); + } + } + + private bool AutoCloseAbort(bool joinThread = true) + { + Thread? autoCloseThread = null; + + lock (_autoCloseLock) + { + if (!_autoCloseActive) + { + return false; + } + + _autoCloseThreadInterrupt.Set(); + if (joinThread) + { + autoCloseThread = _autoCloseThread; + } + _autoCloseThread = null; + + _autoCloseProgressUpdateTimer!.Stop(); + _autoCloseProgressUpdateTimer = null; + + this.Dispatcher.Invoke(AutoCloseResetProgress); + + _autoCloseGameProcess!.Exited -= AutoCloseGameProcessExited; + _autoCloseGameProcess = null; + + _autoCloseActive = false; + } + + autoCloseThread?.Join(); + + return true; + } + + private void AutoCloseResetProgress() + { + AutoCloseProgressBar.Value = 0; + } + + private void AutoCloseGameProcessExited(object? sender, EventArgs e) + { + // This will be called from a thread pool + + if (!AutoCloseAbort()) + { + // If not active yet -- just bail. + // AutoCloseBegin() will also bail if the process has terminated already. + return; + } + + if (sender is Process process) + { + L.CallerDebug($"Auto-close aborted due to game process terminating with {process.ExitCode} exit code."); + + process.Dispose(); + } + else + { + L.CallerDebug($"Auto-close aborted due to game process terminating."); + } + + this.Dispatcher.Invoke(() => + { + // Try to bring the window into foreground + this.Topmost = true; + this.Activate(); + this.Topmost = false; + }); + } + + private void AutoClosePostProcessInput(object sender, ProcessInputEventArgs e) + { + if (e.StagingItem.Input is MouseButtonEventArgs m) + { + // Minimize number of different events being handled here + if (m.ButtonState == MouseButtonState.Pressed && m.RoutedEvent.RoutingStrategy == RoutingStrategy.Tunnel) + { + if (AutoCloseAbort()) + { + L.CallerDebug($"Auto-close aborted due to mouse down event."); + } + } + } + else if (e.StagingItem.Input is KeyEventArgs k) + { + // Minimize number of different events being handled here + if (k.IsDown && !k.IsRepeat && k.RoutedEvent.RoutingStrategy == RoutingStrategy.Tunnel) + { + if (AutoCloseAbort()) + { + L.CallerDebug($"Auto-close aborted due to key down event."); + } + } + } + } + + private void AutoCloseThread() + { + if (_autoCloseThreadInterrupt.WaitOne(AutoCloseTimeSpan)) + { + return; + } + else + { + if (AutoCloseAbort(joinThread: false)) + { + L.CallerDebug("Auto-closing..."); + this.Dispatcher.Invoke(this.Close); + } + } + } + + private void AutoCloseProgressUpdateTimer(object? sender, EventArgs e) + { + double progress = Math.Max(0, AutoCloseTimeSpan.Ticks - _autoCloseProgressUpdateStopwatch.Elapsed.Ticks) / (double)AutoCloseTimeSpan.Ticks; + + AutoCloseProgressBar.Value = progress * AutoCloseProgressBar.Maximum; + } } } \ No newline at end of file diff --git a/PD2Launcherv2/Resources/Styles/ControlStyles.xaml b/PD2Launcherv2/Resources/Styles/ControlStyles.xaml index c1b5bfe7..8e2fb359 100644 --- a/PD2Launcherv2/Resources/Styles/ControlStyles.xaml +++ b/PD2Launcherv2/Resources/Styles/ControlStyles.xaml @@ -333,6 +333,24 @@ + + -