From a65f3752557907d49c86c44c11f3fdd3e59799a7 Mon Sep 17 00:00:00 2001 From: Macocian Alexandru Victor Date: Sat, 27 Jan 2024 20:34:47 +0100 Subject: [PATCH] Release 0.9.9.9 (#548) Closes #547 Closes #546 Closes #536 --- Daybreak.Installer/Program.cs | 16 ---- Daybreak/Controls/Buttons/BackButton.xaml | 6 +- Daybreak/Controls/DropDownButton.xaml | 76 +++++++++++++++++ Daybreak/Controls/DropDownButton.xaml.cs | 60 ++++++++++++++ .../Controls/DropDownButtonContextMenu.xaml | 29 +++++++ .../DropDownButtonContextMenu.xaml.cs | 32 ++++++++ Daybreak/Controls/Glyphs/ArrowGlyph.xaml | 15 ++++ Daybreak/Controls/Glyphs/ArrowGlyph.xaml.cs | 13 +++ .../Controls/LivingEntityContextMenu.xaml.cs | 2 +- Daybreak/Controls/PlayerContextMenu.xaml.cs | 2 +- .../Templates/LaunchButtonTemplate.xaml | 19 +++-- .../Templates/LaunchButtonTemplate.xaml.cs | 82 +++++++++++++++---- Daybreak/Daybreak.csproj | 2 +- Daybreak/Models/ApplicationLauncherContext.cs | 1 + Daybreak/Models/GWCA/ConnectionContext.cs | 4 +- .../GuildWarsApplicationLaunchContext.cs | 1 + .../LaunchConfigurationWithCredentials.cs | 10 +++ Daybreak/Models/LauncherViewContext.cs | 21 +++++ .../ApplicationLauncher.cs | 20 +++-- Daybreak/Services/GWCA/GWCAClient.cs | 12 ++- Daybreak/Services/GWCA/GWCAInjector.cs | 3 +- Daybreak/Services/GWCA/IGWCAClient.cs | 3 +- Daybreak/Services/Scanner/GWCAMemoryReader.cs | 41 +++++++--- .../Services/Scanner/GuildwarsMemoryCache.cs | 2 +- .../Scanner/IGuildwarsMemoryReader.cs | 3 +- Daybreak/Utils/DependencyObjectExtensions.cs | 25 ++++++ Daybreak/Utils/NativeMethods.cs | 1 - Daybreak/Views/LauncherView.xaml | 32 +++++--- Daybreak/Views/LauncherView.xaml.cs | 80 +++++++++++++----- 29 files changed, 499 insertions(+), 114 deletions(-) create mode 100644 Daybreak/Controls/DropDownButton.xaml create mode 100644 Daybreak/Controls/DropDownButton.xaml.cs create mode 100644 Daybreak/Controls/DropDownButtonContextMenu.xaml create mode 100644 Daybreak/Controls/DropDownButtonContextMenu.xaml.cs create mode 100644 Daybreak/Controls/Glyphs/ArrowGlyph.xaml create mode 100644 Daybreak/Controls/Glyphs/ArrowGlyph.xaml.cs create mode 100644 Daybreak/Models/LauncherViewContext.cs create mode 100644 Daybreak/Utils/DependencyObjectExtensions.cs diff --git a/Daybreak.Installer/Program.cs b/Daybreak.Installer/Program.cs index 48b1aa5b..ed1a3a6e 100644 --- a/Daybreak.Installer/Program.cs +++ b/Daybreak.Installer/Program.cs @@ -113,22 +113,6 @@ static void RenderProgressBar(int currentStep, int totalSteps, int barSize) return; } -Console.WriteLine("Deleting browser caches"); -try -{ - Directory.Delete("BrowserData", true); -} -catch(Exception) -{ -} -try -{ - Directory.Delete("Daybreak.exe.WebView2", true); -} -catch(Exception) -{ -} - Console.WriteLine("Launching application"); var process = new Process { diff --git a/Daybreak/Controls/Buttons/BackButton.xaml b/Daybreak/Controls/Buttons/BackButton.xaml index 43488abc..50ff8f42 100644 --- a/Daybreak/Controls/Buttons/BackButton.xaml +++ b/Daybreak/Controls/Buttons/BackButton.xaml @@ -4,6 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:Daybreak.Controls.Buttons" + xmlns:glyphs="clr-namespace:Daybreak.Controls.Glyphs" mc:Ignorable="d" x:Name="_this" Cursor="Hand" @@ -14,10 +15,7 @@ Clicked="HighlightButton_Clicked"> - - - + diff --git a/Daybreak/Controls/DropDownButton.xaml b/Daybreak/Controls/DropDownButton.xaml new file mode 100644 index 00000000..97cba942 --- /dev/null +++ b/Daybreak/Controls/DropDownButton.xaml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Daybreak/Controls/DropDownButton.xaml.cs b/Daybreak/Controls/DropDownButton.xaml.cs new file mode 100644 index 00000000..2e05c058 --- /dev/null +++ b/Daybreak/Controls/DropDownButton.xaml.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Extensions; +using System.Windows.Input; + +namespace Daybreak.Controls; +/// +/// Interaction logic for DropdownButton.xaml +/// +public partial class DropDownButton : UserControl +{ + [GenerateDependencyProperty] + private DataTemplate itemTemplate = default!; + + [GenerateDependencyProperty] + private object selectedItem = default!; + + [GenerateDependencyProperty] + private IEnumerable items = default!; + + [GenerateDependencyProperty(InitialValue = true)] + private bool clickEnabled = true; + + public event EventHandler Clicked = default!; + public event EventHandler SelectionChanged = default!; + + public DropDownButton() + { + this.InitializeComponent(); + } + + private void MainButton_Clicked(object sender, EventArgs e) + { + this.Clicked?.Invoke(this, this.SelectedItem); + } + + private void ArrowButton_Clicked(object sender, EventArgs e) + { + var contextMenu = this.ContextMenu; + contextMenu.PlacementTarget = this; + contextMenu.IsOpen = true; + } + + private void DropDownButtonContextMenu_ItemClicked(object _, object e) + { + this.SelectedItem = e; + this.ContextMenu.IsOpen = false; + this.SelectionChanged?.Invoke(this, e); + } + + private void IgnoreRightMouseButton(object sender, MouseButtonEventArgs e) + { + if (e.ChangedButton is MouseButton.Right) + { + e.Handled = true; + } + } +} diff --git a/Daybreak/Controls/DropDownButtonContextMenu.xaml b/Daybreak/Controls/DropDownButtonContextMenu.xaml new file mode 100644 index 00000000..ff82cbac --- /dev/null +++ b/Daybreak/Controls/DropDownButtonContextMenu.xaml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + diff --git a/Daybreak/Controls/DropDownButtonContextMenu.xaml.cs b/Daybreak/Controls/DropDownButtonContextMenu.xaml.cs new file mode 100644 index 00000000..dc5e6f72 --- /dev/null +++ b/Daybreak/Controls/DropDownButtonContextMenu.xaml.cs @@ -0,0 +1,32 @@ +using Daybreak.Controls.Buttons; +using System; +using System.Collections; +using System.Extensions; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Extensions; + +namespace Daybreak.Controls; +/// +/// Interaction logic for DropDownButtonContextMenu.xaml +/// +public partial class DropDownButtonContextMenu : UserControl +{ + [GenerateDependencyProperty] + private DataTemplate itemTemplate = default!; + + [GenerateDependencyProperty] + private IEnumerable items = default!; + + public event EventHandler ItemClicked = default!; + + public DropDownButtonContextMenu() + { + this.InitializeComponent(); + } + + private void HighlightButton_Clicked(object sender, EventArgs e) + { + this.ItemClicked?.Invoke(this, sender.Cast().DataContext); + } +} diff --git a/Daybreak/Controls/Glyphs/ArrowGlyph.xaml b/Daybreak/Controls/Glyphs/ArrowGlyph.xaml new file mode 100644 index 00000000..ca961464 --- /dev/null +++ b/Daybreak/Controls/Glyphs/ArrowGlyph.xaml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/Daybreak/Controls/Glyphs/ArrowGlyph.xaml.cs b/Daybreak/Controls/Glyphs/ArrowGlyph.xaml.cs new file mode 100644 index 00000000..1f031430 --- /dev/null +++ b/Daybreak/Controls/Glyphs/ArrowGlyph.xaml.cs @@ -0,0 +1,13 @@ +using System.Windows.Controls; + +namespace Daybreak.Controls.Glyphs; +/// +/// Interaction logic for ArrowGlyph.xaml +/// +public partial class ArrowGlyph : UserControl +{ + public ArrowGlyph() + { + this.InitializeComponent(); + } +} diff --git a/Daybreak/Controls/LivingEntityContextMenu.xaml.cs b/Daybreak/Controls/LivingEntityContextMenu.xaml.cs index d1b78e7b..6b9348cb 100644 --- a/Daybreak/Controls/LivingEntityContextMenu.xaml.cs +++ b/Daybreak/Controls/LivingEntityContextMenu.xaml.cs @@ -58,7 +58,7 @@ private async void LivingEntityContextMenu_DataContextChanged(object sender, Sys this.PrimaryProfessionVisible = false; } - await this.guildwarsMemoryReader.EnsureInitialized(context.GuildWarsApplicationLaunchContext!.GuildWarsProcess, CancellationToken.None); + await this.guildwarsMemoryReader.EnsureInitialized(context.GuildWarsApplicationLaunchContext!.ProcessId, CancellationToken.None); this.EntityName = await this.guildwarsMemoryReader.GetEntityName(context.LivingEntity!, CancellationToken.None).ConfigureAwait(true); } diff --git a/Daybreak/Controls/PlayerContextMenu.xaml.cs b/Daybreak/Controls/PlayerContextMenu.xaml.cs index 3a741923..4fd27481 100644 --- a/Daybreak/Controls/PlayerContextMenu.xaml.cs +++ b/Daybreak/Controls/PlayerContextMenu.xaml.cs @@ -43,7 +43,7 @@ private async void PlayerContextMenu_DataContextChanged(object sender, System.Wi return; } - await this.guildwarsMemoryReader.EnsureInitialized(context.GuildWarsApplicationLaunchContext!.GuildWarsProcess, CancellationToken.None); + await this.guildwarsMemoryReader.EnsureInitialized(context.GuildWarsApplicationLaunchContext!.ProcessId, CancellationToken.None); this.PlayerName = await this.guildwarsMemoryReader.GetEntityName(context.Player!, CancellationToken.None).ConfigureAwait(true); } diff --git a/Daybreak/Controls/Templates/LaunchButtonTemplate.xaml b/Daybreak/Controls/Templates/LaunchButtonTemplate.xaml index 94541bfd..121b3a27 100644 --- a/Daybreak/Controls/Templates/LaunchButtonTemplate.xaml +++ b/Daybreak/Controls/Templates/LaunchButtonTemplate.xaml @@ -8,6 +8,7 @@ xmlns:configs="clr-namespace:Daybreak.Models.LaunchConfigurations" xmlns:converters="clr-namespace:Daybreak.Converters" mc:Ignorable="d" + DataContextChanged="UserControl_DataContextChanged" Loaded="UserControl_Loaded" Unloaded="UserControl_Unloaded" x:Name="_this" @@ -25,7 +26,7 @@ FontSize="22" Foreground="{StaticResource MahApps.Brushes.ThemeForeground}" Visibility="{Binding ElementName=_this, Path=GameRunning, Mode=OneWay, Converter={StaticResource InverseBooleanToVisibilityConverter}}"/> - - + TextWrapping="Wrap" + HorizontalAlignment="Center"/> @@ -45,14 +47,15 @@ - - + diff --git a/Daybreak/Controls/Templates/LaunchButtonTemplate.xaml.cs b/Daybreak/Controls/Templates/LaunchButtonTemplate.xaml.cs index a5874789..9cf930a8 100644 --- a/Daybreak/Controls/Templates/LaunchButtonTemplate.xaml.cs +++ b/Daybreak/Controls/Templates/LaunchButtonTemplate.xaml.cs @@ -1,7 +1,12 @@ using Daybreak.Configuration.Options; +using Daybreak.Controls.Buttons; +using Daybreak.Models; using Daybreak.Models.LaunchConfigurations; using Daybreak.Services.ApplicationLauncher; +using Daybreak.Services.GWCA; using Daybreak.Services.LaunchConfigurations; +using Daybreak.Services.Scanner; +using Daybreak.Utils; using Microsoft.Extensions.DependencyInjection; using Slim.Attributes; using System; @@ -9,6 +14,7 @@ using System.Core.Extensions; using System.Threading; using System.Threading.Tasks; +using System.Windows; using System.Windows.Controls; using System.Windows.Extensions; @@ -20,6 +26,7 @@ public partial class LaunchButtonTemplate : UserControl { private static readonly TimeSpan CheckGameDelay = TimeSpan.FromSeconds(1); + private readonly IGuildwarsMemoryReader guildwarsMemoryReader; private readonly ILaunchConfigurationService launchConfigurationService; private readonly IApplicationLauncher applicationLauncher; private readonly ILiveOptions liveOptions; @@ -33,10 +40,12 @@ public partial class LaunchButtonTemplate : UserControl private CancellationTokenSource? tokenSource; public LaunchButtonTemplate( + IGuildwarsMemoryReader guildwarsMemoryReader, ILaunchConfigurationService launchConfigurationService, IApplicationLauncher applicationLauncher, ILiveOptions liveOptions) { + this.guildwarsMemoryReader = guildwarsMemoryReader.ThrowIfNull(); this.launchConfigurationService = launchConfigurationService.ThrowIfNull(); this.applicationLauncher = applicationLauncher.ThrowIfNull(); this.liveOptions = liveOptions.ThrowIfNull(); @@ -46,47 +55,86 @@ public LaunchButtonTemplate( [DoNotInject] public LaunchButtonTemplate() :this( + Launch.Launcher.Instance.ApplicationServiceProvider.GetRequiredService(), Launch.Launcher.Instance.ApplicationServiceProvider.GetRequiredService(), Launch.Launcher.Instance.ApplicationServiceProvider.GetRequiredService(), Launch.Launcher.Instance.ApplicationServiceProvider.GetRequiredService>()) { } - private void UserControl_Loaded(object sender, System.Windows.RoutedEventArgs e) + private void UserControl_Loaded(object sender, RoutedEventArgs e) { this.tokenSource?.Dispose(); this.tokenSource = new CancellationTokenSource(); - this.CheckGameState(this.tokenSource.Token); + this.PeriodicallyCheckGameState(this.tokenSource.Token); } - private void UserControl_Unloaded(object sender, System.Windows.RoutedEventArgs e) + private void UserControl_Unloaded(object sender, RoutedEventArgs e) { this.tokenSource?.Dispose(); this.tokenSource = default; } - private async void CheckGameState(CancellationToken cancellationToken) + private async void UserControl_DataContextChanged(object _, DependencyPropertyChangedEventArgs e) + { + await this.CheckGameState(this.tokenSource?.Token ?? CancellationToken.None); + } + + private async void PeriodicallyCheckGameState(CancellationToken cancellationToken) { while (!cancellationToken.IsCancellationRequested) { - if (this.DataContext is not LaunchConfigurationWithCredentials config) - { - await Task.Delay(CheckGameDelay, cancellationToken); - continue; - } - - if (this.applicationLauncher.GetGuildwarsProcess(config) is null) - { - this.GameRunning = false; - } - else + // The following if case is needed due to a fault in the ContentPresenter not passing the DataContext to the content it presents + if (this.FindParent()?.DataContext is LauncherViewContext parentContext && + this.DataContext != parentContext) { - this.GameRunning = true; + this.DataContext = parentContext; } + await this.CheckGameState(cancellationToken); + await Task.Delay(CheckGameDelay, cancellationToken); + } + } + + private async Task CheckGameState(CancellationToken cancellationToken) + { + if (this.DataContext is not LauncherViewContext launcherViewContext || + launcherViewContext.Configuration is null) + { + return; + } + + if (this.applicationLauncher.GetGuildwarsProcess(launcherViewContext.Configuration) is not GuildWarsApplicationLaunchContext context) + { + this.GameRunning = false; this.CanShowFocusView = this.liveOptions.Value.Enabled && this.GameRunning; + launcherViewContext.CanLaunch = true; + return; + } - await Task.Delay(CheckGameDelay, cancellationToken); + try + { + await this.guildwarsMemoryReader.EnsureInitialized(context.ProcessId, cancellationToken); } + catch + { + this.GameRunning = false; + this.CanShowFocusView = this.liveOptions.Value.Enabled && this.GameRunning; + launcherViewContext.CanLaunch = false; + return; + } + + var loginInfo = await this.guildwarsMemoryReader.ReadLoginData(cancellationToken); + if (loginInfo?.Email != context.LaunchConfiguration.Credentials?.Username) + { + this.GameRunning = false; + this.CanShowFocusView = this.liveOptions.Value.Enabled && this.GameRunning; + launcherViewContext.CanLaunch = false; + return; + } + + this.GameRunning = true; + launcherViewContext.CanLaunch = true; + this.CanShowFocusView = this.liveOptions.Value.Enabled && this.GameRunning; } } diff --git a/Daybreak/Daybreak.csproj b/Daybreak/Daybreak.csproj index b3b9eb9c..12ea3da1 100644 --- a/Daybreak/Daybreak.csproj +++ b/Daybreak/Daybreak.csproj @@ -11,7 +11,7 @@ preview Daybreak.ico true - 0.9.9.8 + 0.9.9.9 true cfb2a489-db80-448d-a969-80270f314c46 True diff --git a/Daybreak/Models/ApplicationLauncherContext.cs b/Daybreak/Models/ApplicationLauncherContext.cs index c3a9ce7a..2515189e 100644 --- a/Daybreak/Models/ApplicationLauncherContext.cs +++ b/Daybreak/Models/ApplicationLauncherContext.cs @@ -5,4 +5,5 @@ public readonly struct ApplicationLauncherContext { public string ExecutablePath { get; init; } public Process Process { get; init; } + public uint ProcessId { get; init; } } diff --git a/Daybreak/Models/GWCA/ConnectionContext.cs b/Daybreak/Models/GWCA/ConnectionContext.cs index 11b7c9aa..739d0f38 100644 --- a/Daybreak/Models/GWCA/ConnectionContext.cs +++ b/Daybreak/Models/GWCA/ConnectionContext.cs @@ -3,9 +3,11 @@ public readonly struct ConnectionContext { public readonly int Port; + public readonly uint ProcessId; - public ConnectionContext(int port) + public ConnectionContext(int port, uint processId) { this.Port = port; + this.ProcessId = processId; } } diff --git a/Daybreak/Models/LaunchConfigurations/GuildWarsApplicationLaunchContext.cs b/Daybreak/Models/LaunchConfigurations/GuildWarsApplicationLaunchContext.cs index bba6b3b4..b206a8f5 100644 --- a/Daybreak/Models/LaunchConfigurations/GuildWarsApplicationLaunchContext.cs +++ b/Daybreak/Models/LaunchConfigurations/GuildWarsApplicationLaunchContext.cs @@ -7,6 +7,7 @@ public sealed record GuildWarsApplicationLaunchContext : IEquatable this.canLaunch; + set + { + this.canLaunch = value; + this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.CanLaunch))); + } + } +} diff --git a/Daybreak/Services/ApplicationLauncher/ApplicationLauncher.cs b/Daybreak/Services/ApplicationLauncher/ApplicationLauncher.cs index 8cd1e9c0..fbd8aacc 100644 --- a/Daybreak/Services/ApplicationLauncher/ApplicationLauncher.cs +++ b/Daybreak/Services/ApplicationLauncher/ApplicationLauncher.cs @@ -93,7 +93,7 @@ public ApplicationLauncher( } - return new GuildWarsApplicationLaunchContext { LaunchConfiguration = launchConfigurationWithCredentials, GuildWarsProcess = gwProcess }; + return new GuildWarsApplicationLaunchContext { LaunchConfiguration = launchConfigurationWithCredentials, GuildWarsProcess = gwProcess, ProcessId = (uint)gwProcess.Id }; } public void RestartDaybreak() @@ -200,7 +200,7 @@ public void RestartDaybreakAsNormalUser() } }; - var applicationLauncherContext = new ApplicationLauncherContext { Process = process, ExecutablePath = executable }; + var applicationLauncherContext = new ApplicationLauncherContext { Process = process, ExecutablePath = executable, ProcessId = 0 }; foreach(var mod in disabledmods) { using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(this.launcherOptions.Value.ModStartupTimeout)); @@ -276,7 +276,7 @@ public void RestartDaybreakAsNormalUser() } // Reset launch context with the launched process - applicationLauncherContext = new ApplicationLauncherContext { ExecutablePath = executable, Process = process }; + applicationLauncherContext = new ApplicationLauncherContext { ExecutablePath = executable, Process = process, ProcessId = (uint)pId }; foreach (var mod in mods) { using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(this.launcherOptions.Value.ModStartupTimeout)); @@ -479,25 +479,30 @@ private bool SetRegistryGuildwarsPath(string path) return Process.GetProcessesByName(ProcessName) .Select(Process => { - var AssociatedConfiguration = launchConfigurationWithCredentials.FirstOrDefault(c => ConfigurationMatchesProcess(c, Process)); - return (Process, AssociatedConfiguration); + (var AssociatedConfiguration, _, var ProcessId) = launchConfigurationWithCredentials + .Select(c => (c, ConfigurationMatchesProcess(c, Process, out var processId), processId)) + .FirstOrDefault(c => c.Item2); + return (Process, AssociatedConfiguration, ProcessId); }) .Where(tuple => tuple.AssociatedConfiguration is not null) .Select(tuple => new GuildWarsApplicationLaunchContext { GuildWarsProcess = tuple.Process, - LaunchConfiguration = tuple.AssociatedConfiguration + LaunchConfiguration = tuple.AssociatedConfiguration!, + ProcessId = tuple.ProcessId }); } - private static bool ConfigurationMatchesProcess(LaunchConfigurationWithCredentials launchConfigurationWithCredentials, Process process) + private static bool ConfigurationMatchesProcess(LaunchConfigurationWithCredentials launchConfigurationWithCredentials, Process process, out uint processId) { try { + processId = (uint)process.Id; return launchConfigurationWithCredentials.ExecutablePath == process.MainModule?.FileName; } catch (Win32Exception ex) when (ex.Message.Contains("Access is denied") || ex.Message.Contains("Only part of a ReadProcessMemory or WriteProcessMemory request was completed.")) { + processId = 0; /* * The process is running elevated. There is no way to use the standard C# libraries * to figure out what is the path of the running process. @@ -522,6 +527,7 @@ private static bool ConfigurationMatchesProcess(LaunchConfigurationWithCredentia if (QueryFullProcessImageName(maybeDesiredProcessHandle, 0, nameBuffer, ref size) && nameBuffer.ToString() == launchConfigurationWithCredentials.ExecutablePath) { + processId = pe32.th32ProcessID; CloseHandle(maybeDesiredProcessHandle); return true; } diff --git a/Daybreak/Services/GWCA/GWCAClient.cs b/Daybreak/Services/GWCA/GWCAClient.cs index 8ebc9494..55ca2a0a 100644 --- a/Daybreak/Services/GWCA/GWCAClient.cs +++ b/Daybreak/Services/GWCA/GWCAClient.cs @@ -2,7 +2,6 @@ using Microsoft.Extensions.Logging; using System; using System.Core.Extensions; -using System.Diagnostics; using System.Extensions; using System.Linq; using System.Net; @@ -53,9 +52,8 @@ public async Task CheckAlive(ConnectionContext connectionContext, Cancella } } - public async Task Connect(Process process, CancellationToken cancellationToken) + public async Task Connect(uint processId, CancellationToken cancellationToken) { - process.ThrowIfNull(); var listeners = IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpListeners() .Where(i => i.Port >= PortRange.MinRange && i.Port < PortRange.MaxRange && i.Address.ToString() == IPAddress.Any.ToString()); foreach(var listener in listeners) @@ -71,19 +69,19 @@ public async Task CheckAlive(ConnectionContext connectionContext, Cancella } var id = await response.Content.ReadAsStringAsync(cancellationToken); - if (!int.TryParse(id, out var processId)) + if (!int.TryParse(id, out var targetId)) { scopedLogger.LogInformation("Received response is not an integer. Continuing"); continue; } - if (processId != process.Id) + if (targetId != processId) { scopedLogger.LogInformation("Received response does not match desired process id. Continuing"); continue; } - return new ConnectionContext(listener.Port); + return new ConnectionContext(listener.Port, processId); } catch (Exception e) when (e is TaskCanceledException or TimeoutException) { @@ -97,6 +95,6 @@ public async Task CheckAlive(ConnectionContext connectionContext, Cancella public async Task GetAsync(ConnectionContext connectionContext, string subPath, CancellationToken cancellationToken) { var scopedLogger = this.logger.CreateScopedLogger(nameof(this.GetAsync), subPath); - return await this.httpClient.GetAsync($"{UrlTemplate.Replace(PortPlaceholder, connectionContext.Port.ToString())}/{subPath}"); + return await this.httpClient.GetAsync($"{UrlTemplate.Replace(PortPlaceholder, connectionContext.Port.ToString())}/{subPath}", cancellationToken); } } diff --git a/Daybreak/Services/GWCA/GWCAInjector.cs b/Daybreak/Services/GWCA/GWCAInjector.cs index da6e99f4..062794e4 100644 --- a/Daybreak/Services/GWCA/GWCAInjector.cs +++ b/Daybreak/Services/GWCA/GWCAInjector.cs @@ -4,7 +4,6 @@ using Daybreak.Services.Notifications; using System.Collections.Generic; using System.Core.Extensions; -using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -61,7 +60,7 @@ public async Task OnGuildWarsStarted(ApplicationLauncherContext applicationLaunc ConnectionContext? connectionContext = default; for(var i = 0; i < MaxRetries; i++) { - if (await this.gwcaClient.Connect(applicationLauncherContext.Process, cancellationToken) is ConnectionContext newContext) + if (await this.gwcaClient.Connect(applicationLauncherContext.ProcessId, cancellationToken) is ConnectionContext newContext) { connectionContext = newContext; break; diff --git a/Daybreak/Services/GWCA/IGWCAClient.cs b/Daybreak/Services/GWCA/IGWCAClient.cs index 89f9ab54..6cac55aa 100644 --- a/Daybreak/Services/GWCA/IGWCAClient.cs +++ b/Daybreak/Services/GWCA/IGWCAClient.cs @@ -1,5 +1,4 @@ using Daybreak.Models.GWCA; -using System.Diagnostics; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -7,7 +6,7 @@ namespace Daybreak.Services.GWCA; public interface IGWCAClient { - Task Connect(Process process, CancellationToken cancellationToken); + Task Connect(uint processId, CancellationToken cancellationToken); Task CheckAlive(ConnectionContext connectionContext, CancellationToken cancellationToken); diff --git a/Daybreak/Services/Scanner/GWCAMemoryReader.cs b/Daybreak/Services/Scanner/GWCAMemoryReader.cs index d7c84782..41967611 100644 --- a/Daybreak/Services/Scanner/GWCAMemoryReader.cs +++ b/Daybreak/Services/Scanner/GWCAMemoryReader.cs @@ -4,7 +4,6 @@ using Microsoft.Extensions.Logging; using System; using System.Core.Extensions; -using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using System.Extensions; @@ -15,19 +14,19 @@ using System.Windows; using Daybreak.Utils; using System.Text.RegularExpressions; -using SharpNav; -using SharpNav.Geometry; using Daybreak.Services.Pathfinding; namespace Daybreak.Services.Scanner; -public sealed class GWCAMemoryReader : IGuildwarsMemoryReader +public sealed partial class GWCAMemoryReader : IGuildwarsMemoryReader { - private static readonly Regex ItemNameColorRegex = new(@"|", RegexOptions.Compiled); + private static readonly Regex ItemNameColorRegex = GenerateItemNameColorRegex(); private readonly IPathfinder pathfinder; private readonly IGWCAClient client; private readonly ILogger logger; + private bool faulty = false; + private ConnectionContext? connectionContextCache; public GWCAMemoryReader( @@ -40,14 +39,19 @@ public GWCAMemoryReader( this.logger = logger.ThrowIfNull(); } - public async Task EnsureInitialized(Process process, CancellationToken cancellationToken) + public async Task EnsureInitialized(uint processId, CancellationToken cancellationToken) { - process.ThrowIfNull(); + if (this.faulty is false && + this.connectionContextCache.HasValue && + this.connectionContextCache.Value.ProcessId == processId) + { + return; + } - var maybeConnectionContext = await this.client.Connect(process, cancellationToken); + var maybeConnectionContext = await this.client.Connect(processId, cancellationToken); if (maybeConnectionContext is not ConnectionContext) { - throw new InvalidOperationException($"Unable to connect to desired process {process.Id}"); + throw new InvalidOperationException($"Unable to connect to desired process {processId}"); } this.connectionContextCache = maybeConnectionContext; @@ -108,6 +112,7 @@ public async Task EnsureInitialized(Process process, CancellationToken cancellat catch(Exception ex) { scopedLogger.LogError(ex, "Encountered exception while parsing response"); + this.faulty = true; } return default; @@ -152,6 +157,7 @@ public async Task EnsureInitialized(Process process, CancellationToken cancellat catch (Exception ex) { scopedLogger.LogError(ex, "Encountered exception while parsing response"); + this.faulty = true; } return default; @@ -190,6 +196,7 @@ public async Task EnsureInitialized(Process process, CancellationToken cancellat catch (Exception ex) { scopedLogger.LogError(ex, "Encountered exception while parsing response"); + this.faulty = true; } return default; @@ -227,6 +234,7 @@ public async Task EnsureInitialized(Process process, CancellationToken cancellat catch (Exception ex) { scopedLogger.LogError(ex, "Encountered exception while parsing response"); + this.faulty = true; } return default; @@ -292,6 +300,7 @@ public async Task EnsureInitialized(Process process, CancellationToken cancellat catch (Exception ex) { scopedLogger.LogError(ex, "Encountered exception while parsing response"); + this.faulty = true; } return default; @@ -329,6 +338,7 @@ public async Task EnsureInitialized(Process process, CancellationToken cancellat catch (Exception ex) { scopedLogger.LogError(ex, "Encountered exception while parsing response"); + this.faulty = true; } return default; @@ -367,6 +377,7 @@ public async Task EnsureInitialized(Process process, CancellationToken cancellat catch (Exception ex) { scopedLogger.LogError(ex, "Encountered exception while parsing response"); + this.faulty = true; } return default; @@ -418,6 +429,7 @@ public async Task EnsureInitialized(Process process, CancellationToken cancellat catch (Exception ex) { scopedLogger.LogError(ex, "Encountered exception while parsing response"); + this.faulty = true; } return default; @@ -472,6 +484,7 @@ public async Task EnsureInitialized(Process process, CancellationToken cancellat catch (Exception ex) { scopedLogger.LogError(ex, "Encountered exception while parsing response"); + this.faulty = true; } return default; @@ -517,6 +530,7 @@ public async Task EnsureInitialized(Process process, CancellationToken cancellat catch (Exception ex) { scopedLogger.LogError(ex, "Encountered exception while parsing response"); + this.faulty = true; } return default; @@ -572,6 +586,7 @@ public async Task EnsureInitialized(Process process, CancellationToken cancellat catch (Exception ex) { scopedLogger.LogError(ex, "Encountered exception while parsing response"); + this.faulty = true; } return default; @@ -609,6 +624,7 @@ public async Task EnsureInitialized(Process process, CancellationToken cancellat catch (Exception ex) { scopedLogger.LogError(ex, "Encountered exception while parsing response"); + this.faulty = true; } return default; @@ -647,6 +663,7 @@ public async Task EnsureInitialized(Process process, CancellationToken cancellat catch (Exception ex) { scopedLogger.LogError(ex, "Encountered exception while parsing response"); + this.faulty = true; } return default; @@ -654,6 +671,7 @@ public async Task EnsureInitialized(Process process, CancellationToken cancellat public void Stop() { + this.connectionContextCache = default; } private static IBagContent ParsePayload(BagContentPayload bagContentPayload) @@ -888,7 +906,7 @@ private static List> BuildFinalAdjacencyList(List trapezoid var adjacencyList = new List>(); for (var i = 0; i < trapezoids.Count; i++) { - adjacencyList.Add(originalAdjacencyList[i].ToList()); + adjacencyList.Add([.. originalAdjacencyList[i]]); } for (var i = 0; i < computedPathingMaps.Count; i++) @@ -957,4 +975,7 @@ private static Rect GetBoundingRectangle(Point[] points) return new Rect(minX, minY, maxX - minX, maxY - minY); } + + [GeneratedRegex(@"|", RegexOptions.Compiled)] + private static partial Regex GenerateItemNameColorRegex(); } diff --git a/Daybreak/Services/Scanner/GuildwarsMemoryCache.cs b/Daybreak/Services/Scanner/GuildwarsMemoryCache.cs index 327879a4..4855c42a 100644 --- a/Daybreak/Services/Scanner/GuildwarsMemoryCache.cs +++ b/Daybreak/Services/Scanner/GuildwarsMemoryCache.cs @@ -36,7 +36,7 @@ public GuildwarsMemoryCache( public async Task EnsureInitialized(GuildWarsApplicationLaunchContext context, CancellationToken cancellationToken) { - await this.guildwarsMemoryReader.EnsureInitialized(context.ThrowIfNull().GuildWarsProcess.ThrowIfNull(), cancellationToken); + await this.guildwarsMemoryReader.EnsureInitialized(context.ThrowIfNull().ProcessId, cancellationToken); } public Task ReadGameData(CancellationToken cancellationToken) diff --git a/Daybreak/Services/Scanner/IGuildwarsMemoryReader.cs b/Daybreak/Services/Scanner/IGuildwarsMemoryReader.cs index 6b33f97f..36d231d7 100644 --- a/Daybreak/Services/Scanner/IGuildwarsMemoryReader.cs +++ b/Daybreak/Services/Scanner/IGuildwarsMemoryReader.cs @@ -1,6 +1,5 @@ using Daybreak.Models.Guildwars; using System.Collections.Generic; -using System.Diagnostics; using System.Threading; using System.Threading.Tasks; @@ -8,7 +7,7 @@ namespace Daybreak.Services.Scanner; public interface IGuildwarsMemoryReader { - Task EnsureInitialized(Process process, CancellationToken cancellationToken); + Task EnsureInitialized(uint processId, CancellationToken cancellationToken); Task ReadLoginData(CancellationToken cancellationToken); Task ReadGameData(CancellationToken cancellationToken); Task ReadPathingData(CancellationToken cancellationToken); diff --git a/Daybreak/Utils/DependencyObjectExtensions.cs b/Daybreak/Utils/DependencyObjectExtensions.cs new file mode 100644 index 00000000..bc83a3d7 --- /dev/null +++ b/Daybreak/Utils/DependencyObjectExtensions.cs @@ -0,0 +1,25 @@ +using System.Windows.Media; +using System.Windows; + +namespace Daybreak.Utils; +public static class DependencyObjectExtensions +{ + public static T? FindParent(this DependencyObject child) where T : DependencyObject + { + var parentObject = VisualTreeHelper.GetParent(child); + if (parentObject is null) + { + return null; + } + + var parent = parentObject as T; + if (parent is not null) + { + return parent; + } + else + { + return FindParent(parentObject); + } + } +} diff --git a/Daybreak/Utils/NativeMethods.cs b/Daybreak/Utils/NativeMethods.cs index 669beefc..7d97d936 100644 --- a/Daybreak/Utils/NativeMethods.cs +++ b/Daybreak/Utils/NativeMethods.cs @@ -2,7 +2,6 @@ using System.Runtime.InteropServices; using System.Runtime.InteropServices.ComTypes; using System.Text; -using static Daybreak.Utils.NativeMethods; namespace Daybreak.Utils; diff --git a/Daybreak/Views/LauncherView.xaml b/Daybreak/Views/LauncherView.xaml index 0f3fe3fc..19ec854d 100644 --- a/Daybreak/Views/LauncherView.xaml +++ b/Daybreak/Views/LauncherView.xaml @@ -9,6 +9,7 @@ xmlns:converters="clr-namespace:Daybreak.Converters" xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls" xmlns:templates="clr-namespace:Daybreak.Controls.Templates" + xmlns:controls="clr-namespace:Daybreak.Controls" mc:Ignorable="d" x:Name="_this" Loaded="StartupView_Loaded" @@ -18,17 +19,22 @@ - - - - - - - + + + + + + + + + + + diff --git a/Daybreak/Views/LauncherView.xaml.cs b/Daybreak/Views/LauncherView.xaml.cs index be29657f..48030bd6 100644 --- a/Daybreak/Views/LauncherView.xaml.cs +++ b/Daybreak/Views/LauncherView.xaml.cs @@ -1,4 +1,5 @@ using Daybreak.Configuration.Options; +using Daybreak.Models; using Daybreak.Models.LaunchConfigurations; using Daybreak.Models.Onboarding; using Daybreak.Services.ApplicationLauncher; @@ -9,7 +10,6 @@ using Daybreak.Services.Onboarding; using Daybreak.Services.Screens; using System; -using System.CodeDom.Compiler; using System.Collections.ObjectModel; using System.Configuration; using System.Core.Extensions; @@ -38,14 +38,15 @@ public partial class LauncherView : UserControl private readonly IViewManager viewManager; private readonly IScreenManager screenManager; private readonly ILiveOptions focusViewOptions; + private readonly CancellationTokenSource cancellationTokenSource = new(); [GenerateDependencyProperty] - private LaunchConfigurationWithCredentials latestConfiguration = default!; + private LauncherViewContext latestConfiguration = default!; [GenerateDependencyProperty] - private bool loading; + private bool canLaunch; - public ObservableCollection LaunchConfigurations { get; } = []; + public ObservableCollection LaunchConfigurations { get; } = []; public LauncherView( IMenuService menuService, @@ -68,7 +69,7 @@ public LauncherView( this.InitializeComponent(); } - private void CheckOnboardingState() + private bool IsOnboarded() { var onboardingStage = this.onboardingService.CheckOnboardingStage(); if (onboardingStage is LauncherOnboardingStage.Default) @@ -79,64 +80,103 @@ private void CheckOnboardingState() if (onboardingStage is LauncherOnboardingStage.NeedsCredentials or LauncherOnboardingStage.NeedsExecutable or LauncherOnboardingStage.NeedsConfiguration) { this.viewManager.ShowView(onboardingStage); - return; + return false; } + + return true; } private void RetrieveLaunchConfigurations() { - this.LaunchConfigurations.ClearAnd().AddRange(this.launchConfigurationService.GetLaunchConfigurations()); - this.LatestConfiguration = this.launchConfigurationService.GetLastLaunchConfigurationWithCredentials(); + var latestLaunchConfiguration = this.launchConfigurationService.GetLastLaunchConfigurationWithCredentials(); + this.LaunchConfigurations.ClearAnd().AddRange(this.launchConfigurationService.GetLaunchConfigurations().Select(c => new LauncherViewContext { Configuration = c, CanLaunch = false })); + this.LatestConfiguration = this.LaunchConfigurations.FirstOrDefault(c => c.Configuration?.Equals(latestLaunchConfiguration) is true); + } + + private async void PeriodicallyCheckSelectedConfigState() + { + while (!this.cancellationTokenSource.IsCancellationRequested) + { + await this.Dispatcher.InvokeAsync(() => this.CanLaunch = this.LatestConfiguration?.CanLaunch ?? false); + await Task.Delay(TimeSpan.FromSeconds(1), this.cancellationTokenSource.Token); + } } private void StartupView_Loaded(object sender, RoutedEventArgs e) { - this.CheckOnboardingState(); + if (!this.IsOnboarded()) + { + return; + } + this.RetrieveLaunchConfigurations(); + this.PeriodicallyCheckSelectedConfigState(); } private void StartupView_Unloaded(object sender, RoutedEventArgs e) { + this.cancellationTokenSource?.Cancel(); + this.cancellationTokenSource?.Dispose(); + } + + private async void DropDownButton_SelectionChanged(object _, object e) + { + if (e is not LauncherViewContext context) + { + return; + } + + if (this.LatestConfiguration is null || + this.LatestConfiguration.CanLaunch is false) + { + await this.Dispatcher.InvokeAsync(() => this.CanLaunch = false); + } + else + { + await this.Dispatcher.InvokeAsync(() => this.CanLaunch = true); + } } - private async void SplitButton_Click(object sender, RoutedEventArgs e) + private async void DropDownButton_Clicked(object _, object e) { - await this.Dispatcher.InvokeAsync(() => this.Loading = true); - if (this.LatestConfiguration is null) + await this.Dispatcher.InvokeAsync(() => this.CanLaunch = true); + if (this.LatestConfiguration is null || + this.LatestConfiguration.CanLaunch is false) { - await this.Dispatcher.InvokeAsync(() => this.Loading = false); + await this.Dispatcher.InvokeAsync(() => this.CanLaunch = false); return; } var launchingTask = await new TaskFactory().StartNew(async () => { var latestConfig = await this.Dispatcher.InvokeAsync(() => this.LatestConfiguration); - if (this.applicationLauncher.GetGuildwarsProcess(latestConfig) is GuildWarsApplicationLaunchContext context) + if (this.applicationLauncher.GetGuildwarsProcess(latestConfig.Configuration!) is GuildWarsApplicationLaunchContext context) { // Detected already running guildwars process - await this.Dispatcher.InvokeAsync(() => this.Loading = false); + await this.Dispatcher.InvokeAsync(() => this.CanLaunch = false); if (this.focusViewOptions.Value.Enabled) { this.menuService.CloseMenu(); this.viewManager.ShowView(context); } + this.launchConfigurationService.SetLastLaunchConfigurationWithCredentials(latestConfig.Configuration!); return; } try { - var launchedContext = await this.applicationLauncher.LaunchGuildwars(latestConfig); + var launchedContext = await this.applicationLauncher.LaunchGuildwars(latestConfig.Configuration!); if (launchedContext is null) { - await this.Dispatcher.InvokeAsync(() => this.Loading = false); + await this.Dispatcher.InvokeAsync(() => this.CanLaunch = false); return; } - this.launchConfigurationService.SetLastLaunchConfigurationWithCredentials(latestConfig); + this.launchConfigurationService.SetLastLaunchConfigurationWithCredentials(latestConfig.Configuration!); if (this.focusViewOptions.Value.Enabled) { - await this.Dispatcher.InvokeAsync(() => this.Loading = false); + await this.Dispatcher.InvokeAsync(() => this.CanLaunch = false); this.menuService.CloseMenu(); this.viewManager.ShowView(launchedContext); } @@ -147,6 +187,6 @@ private async void SplitButton_Click(object sender, RoutedEventArgs e) }, TaskCreationOptions.LongRunning); await launchingTask; - await this.Dispatcher.InvokeAsync(() => this.Loading = false); + await this.Dispatcher.InvokeAsync(() => this.CanLaunch = false); } }