mirror of
https://github.com/gwdevhub/Daybreak.git
synced 2026-07-15 15:19:57 +00:00
@@ -1,4 +1,4 @@
|
||||
name: Daybreak Version Check
|
||||
name: Daybreak Version Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -7,16 +7,19 @@ on:
|
||||
paths:
|
||||
- "Daybreak/**"
|
||||
- "Daybreak.Installer/**"
|
||||
- "Daybreak.Linux/**"
|
||||
- "Daybreak.Windows/**"
|
||||
- "Daybreak.API/**"
|
||||
- "Daybreak.Shared/**"
|
||||
- "Daybreak.Injector/**"
|
||||
|
||||
jobs:
|
||||
|
||||
check_version:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
targetplatform: [x86]
|
||||
targetplatform: [x64]
|
||||
|
||||
runs-on: windows-latest
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
Configuration: Release
|
||||
@@ -24,30 +27,31 @@ jobs:
|
||||
Actions_Allow_Unsecure_Commands: true
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install .NET Core
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: '9.x'
|
||||
- name: Install .NET Core
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: "10.x"
|
||||
|
||||
- name: Get Latest Tag
|
||||
id: getLatestTag
|
||||
uses: WyriHaximus/github-action-get-previous-tag@v1
|
||||
- name: Get Latest Tag
|
||||
id: getLatestTag
|
||||
uses: WyriHaximus/github-action-get-previous-tag@v1
|
||||
|
||||
- name: Build Daybreak project
|
||||
run: dotnet build Daybreak -c $env:Configuration --property:SolutionDir=$env:GITHUB_WORKSPACE
|
||||
- name: Build Daybreak project
|
||||
run: dotnet build Daybreak.Linux -c $env:Configuration --property:SolutionDir=$env:GITHUB_WORKSPACE
|
||||
|
||||
- name: Set version variable
|
||||
run: |
|
||||
$version = .\Scripts\GetBuildVersion.ps1
|
||||
echo "::set-env name=Version::$version"
|
||||
- name: Set version variable
|
||||
run: |
|
||||
version=$(grep '<Version>' Directory.Build.props | sed 's/.*<Version>\(.*\)<\/Version>.*/\1/')
|
||||
echo "Version=$version" >> $GITHUB_ENV
|
||||
|
||||
- name: Check version difference
|
||||
run: |
|
||||
.\Scripts\CompareVersions -currentVersion ${{ env.Version }} -lastVersion ${{ env.LatestReleaseTag }}
|
||||
env:
|
||||
LatestReleaseTag: ${{ steps.getLatestTag.outputs.tag }}
|
||||
|
||||
- name: Check version difference
|
||||
run: |
|
||||
.\Scripts\CompareVersions -currentVersion ${{ env.Version }} -lastVersion ${{ env.LatestReleaseTag }}
|
||||
env:
|
||||
LatestReleaseTag: ${{ steps.getLatestTag.outputs.tag }}
|
||||
@@ -1,5 +1,5 @@
|
||||
using Daybreak.Shared.Attributes;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization;
|
||||
using Daybreak.Shared.Attributes;
|
||||
|
||||
namespace Daybreak.Configuration.Options;
|
||||
|
||||
@@ -7,12 +7,19 @@ namespace Daybreak.Configuration.Options;
|
||||
public sealed class LauncherOptions
|
||||
{
|
||||
[JsonPropertyName(nameof(ShortcutLocation))]
|
||||
[OptionName(Name = "Shortcut Location", Description = "Location where the shortcut will be placed")]
|
||||
[OptionName(
|
||||
Name = "Shortcut Location",
|
||||
Description = "Location where the shortcut will be placed"
|
||||
)]
|
||||
[OptionSynchronizationIgnore]
|
||||
public string? ShortcutLocation { get; set; } = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
|
||||
public string? ShortcutLocation { get; set; } =
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
|
||||
|
||||
[JsonPropertyName(nameof(PlaceShortcut))]
|
||||
[OptionName(Name = "Shortcut Placed", Description = "If true, the shortcut will be placed. If false, the shortcut will be deleted")]
|
||||
[OptionName(
|
||||
Name = "Shortcut Placed",
|
||||
Description = "If true, the shortcut will be placed. If false, the shortcut will be deleted"
|
||||
)]
|
||||
public bool PlaceShortcut { get; set; }
|
||||
|
||||
[JsonPropertyName(nameof(AutoCheckUpdate))]
|
||||
@@ -20,11 +27,24 @@ public sealed class LauncherOptions
|
||||
public bool AutoCheckUpdate { get; set; } = true;
|
||||
|
||||
[JsonPropertyName(nameof(MultiLaunchSupport))]
|
||||
[OptionName(Name = "Multi-Launch Support", Description = "If true, the launcher will support multiple executables being launched at the same time")]
|
||||
[OptionName(
|
||||
Name = "Multi-Launch Support",
|
||||
Description = "If true, the launcher will support multiple executables being launched at the same time"
|
||||
)]
|
||||
public bool MultiLaunchSupport { get; set; }
|
||||
|
||||
[JsonPropertyName(nameof(ModStartupTimeout))]
|
||||
[OptionName(Name = "Mod Startup Timeout", Description = "Amount of seconds that Daybreak will wait for each mod to start-up before cancelling the tasks")]
|
||||
[OptionName(
|
||||
Name = "Mod Startup Timeout",
|
||||
Description = "Amount of seconds that Daybreak will wait for each mod to start-up before cancelling the tasks"
|
||||
)]
|
||||
[OptionRange<double>(MinValue = 30, MaxValue = 300)]
|
||||
public double ModStartupTimeout { get; set; } = 30;
|
||||
|
||||
[JsonPropertyName(nameof(SaveWindowPosition))]
|
||||
[OptionName(
|
||||
Name = "Save Window Position",
|
||||
Description = "If true, the launcher will save its position on the screen between launching"
|
||||
)]
|
||||
public bool SaveWindowPosition { get; set; }
|
||||
}
|
||||
|
||||
@@ -175,9 +175,13 @@ internal sealed class ApplicationLauncher(
|
||||
var availableExecutables = this.guildWarsExecutableManager.GetExecutableList();
|
||||
var runningInstances = this.GetGuildwarsProcesses();
|
||||
var firstAvailableExecutable = availableExecutables.FirstOrDefault(e =>
|
||||
!runningInstances.Any(p =>
|
||||
Path.GetFullPath(p.MainModule?.FileName ?? string.Empty) == Path.GetFullPath(e)
|
||||
FindFirstOrDefault(
|
||||
runningInstances,
|
||||
p =>
|
||||
Path.GetFullPath(p.MainModule?.FileName ?? string.Empty)
|
||||
== Path.GetFullPath(e)
|
||||
)
|
||||
is null
|
||||
);
|
||||
if (firstAvailableExecutable is null)
|
||||
{
|
||||
@@ -538,7 +542,7 @@ internal sealed class ApplicationLauncher(
|
||||
return this.guildWarsProcessFinder.FindProcesses(launchConfigurationWithCredentials);
|
||||
}
|
||||
|
||||
public IReadOnlyList<Process> GetGuildwarsProcesses()
|
||||
public ReadOnlyMemory<Process> GetGuildwarsProcesses()
|
||||
{
|
||||
return this.guildWarsProcessFinder.GetGuildWarsProcesses();
|
||||
}
|
||||
@@ -722,4 +726,21 @@ internal sealed class ApplicationLauncher(
|
||||
|
||||
return [argName, $"\"{argValue}\""];
|
||||
}
|
||||
|
||||
private static Process? FindFirstOrDefault(
|
||||
ReadOnlyMemory<Process> processes,
|
||||
Func<Process, bool> selector
|
||||
)
|
||||
{
|
||||
var span = processes.Span;
|
||||
for (var i = 0; i < span.Length; i++)
|
||||
{
|
||||
if (selector(span[i]))
|
||||
{
|
||||
return span[i];
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
using Daybreak.Configuration.Options;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Core.Extensions;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Daybreak.Configuration.Options;
|
||||
using Daybreak.Shared.Models;
|
||||
using Daybreak.Shared.Models.Api;
|
||||
using Daybreak.Shared.Models.LaunchConfigurations;
|
||||
@@ -9,15 +14,11 @@ using Daybreak.Shared.Services.LaunchConfigurations;
|
||||
using Daybreak.Shared.Services.Notifications;
|
||||
using Daybreak.Shared.Services.Onboarding;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Core.Extensions;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TrailBlazr.Services;
|
||||
using TrailBlazr.ViewModels;
|
||||
|
||||
namespace Daybreak.Views;
|
||||
|
||||
public sealed class LaunchViewModel(
|
||||
IViewManager viewManager,
|
||||
INotificationService notificationService,
|
||||
@@ -25,17 +26,20 @@ public sealed class LaunchViewModel(
|
||||
ILaunchConfigurationService launchConfigurationService,
|
||||
IOnboardingService onboardingService,
|
||||
IApplicationLauncher applicationLauncher,
|
||||
IOptionsMonitor<FocusViewOptions> focusViewOptions) : ViewModelBase<LaunchViewModel, LaunchView>, INotifyPropertyChanged, IDisposable
|
||||
IOptionsMonitor<FocusViewOptions> focusViewOptions
|
||||
) : ViewModelBase<LaunchViewModel, LaunchView>, INotifyPropertyChanged, IDisposable
|
||||
{
|
||||
private static readonly TimeSpan LaunchTimeout = TimeSpan.FromSeconds(10);
|
||||
|
||||
private readonly IViewManager viewManager = viewManager.ThrowIfNull();
|
||||
private readonly INotificationService notificationService = notificationService.ThrowIfNull();
|
||||
private readonly IDaybreakApiService daybreakApiService = daybreakApiService.ThrowIfNull();
|
||||
private readonly ILaunchConfigurationService launchConfigurationService = launchConfigurationService.ThrowIfNull();
|
||||
private readonly ILaunchConfigurationService launchConfigurationService =
|
||||
launchConfigurationService.ThrowIfNull();
|
||||
private readonly IOnboardingService onboardingService = onboardingService.ThrowIfNull();
|
||||
private readonly IApplicationLauncher applicationLauncher = applicationLauncher.ThrowIfNull();
|
||||
private readonly IOptionsMonitor<FocusViewOptions> focusViewOptions = focusViewOptions.ThrowIfNull();
|
||||
private readonly IOptionsMonitor<FocusViewOptions> focusViewOptions =
|
||||
focusViewOptions.ThrowIfNull();
|
||||
|
||||
private CancellationTokenSource? cancellationTokenSource;
|
||||
private string? autoLaunchConfigurationId;
|
||||
@@ -43,12 +47,12 @@ public sealed class LaunchViewModel(
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
public ObservableCollection<LauncherViewContext> LaunchConfigurations { get; } = [];
|
||||
|
||||
public LauncherViewContext? SelectedConfiguration
|
||||
{
|
||||
|
||||
public LauncherViewContext? SelectedConfiguration
|
||||
{
|
||||
get;
|
||||
private set
|
||||
{
|
||||
private set
|
||||
{
|
||||
if (field != value)
|
||||
{
|
||||
field = value;
|
||||
@@ -57,12 +61,12 @@ public sealed class LaunchViewModel(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanLaunch
|
||||
{
|
||||
|
||||
public bool CanLaunch
|
||||
{
|
||||
get;
|
||||
private set
|
||||
{
|
||||
private set
|
||||
{
|
||||
if (field != value)
|
||||
{
|
||||
field = value;
|
||||
@@ -73,19 +77,19 @@ public sealed class LaunchViewModel(
|
||||
|
||||
public bool IsLaunching { get; private set; }
|
||||
|
||||
public bool IsDropdownOpen
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
public bool IsDropdownOpen
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
if (field != value)
|
||||
{
|
||||
field = value;
|
||||
this.OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public string LaunchButtonText => this.GetLaunchButtonText();
|
||||
|
||||
public string LaunchButtonSubtext => this.GetLaunchButtonSubtext();
|
||||
@@ -94,7 +98,12 @@ public sealed class LaunchViewModel(
|
||||
{
|
||||
if (!this.IsOnboarded())
|
||||
{
|
||||
this.viewManager.ShowView<LauncherOnboardingView>((nameof(LauncherOnboardingView.Status), this.onboardingService.CheckOnboardingStage().ToString()));
|
||||
this.viewManager.ShowView<LauncherOnboardingView>(
|
||||
(
|
||||
nameof(LauncherOnboardingView.Status),
|
||||
this.onboardingService.CheckOnboardingStage().ToString()
|
||||
)
|
||||
);
|
||||
return base.ParametersSet(view, cancellationToken);
|
||||
}
|
||||
|
||||
@@ -128,14 +137,17 @@ public sealed class LaunchViewModel(
|
||||
}
|
||||
else
|
||||
{
|
||||
await this.LaunchGuildWars(this.cancellationTokenSource?.Token ?? CancellationToken.None);
|
||||
await this.LaunchGuildWars(
|
||||
this.cancellationTokenSource?.Token ?? CancellationToken.None
|
||||
);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.notificationService.NotifyError(
|
||||
title: "Launch Failed",
|
||||
description: $"Failed to launch: {ex.Message}");
|
||||
title: "Launch Failed",
|
||||
description: $"Failed to launch: {ex.Message}"
|
||||
);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -153,7 +165,7 @@ public sealed class LaunchViewModel(
|
||||
this.SelectedConfiguration = configuration;
|
||||
this.UpdateCanLaunch();
|
||||
}
|
||||
|
||||
|
||||
this.IsDropdownOpen = false;
|
||||
this.RefreshView();
|
||||
}
|
||||
@@ -179,9 +191,19 @@ public sealed class LaunchViewModel(
|
||||
|
||||
this.RetrieveLaunchConfigurations();
|
||||
var ct = this.cancellationTokenSource.Token;
|
||||
Task.Factory.StartNew(() => this.PeriodicallyCheckSelectedConfigState(ct), ct, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
if (this.autoLaunchConfigurationId is not null &&
|
||||
this.LaunchConfigurations.FirstOrDefault(c => c.Configuration?.Identifier == this.autoLaunchConfigurationId) is LauncherViewContext context)
|
||||
Task.Factory.StartNew(
|
||||
() => this.PeriodicallyCheckSelectedConfigState(ct),
|
||||
ct,
|
||||
TaskCreationOptions.LongRunning,
|
||||
TaskScheduler.Current
|
||||
);
|
||||
if (
|
||||
this.autoLaunchConfigurationId is not null
|
||||
&& this.LaunchConfigurations.FirstOrDefault(c =>
|
||||
c.Configuration?.Identifier == this.autoLaunchConfigurationId
|
||||
)
|
||||
is LauncherViewContext context
|
||||
)
|
||||
{
|
||||
this.SelectedConfiguration = context;
|
||||
Task.Run(this.LaunchSelectedConfiguration);
|
||||
@@ -216,24 +238,33 @@ public sealed class LaunchViewModel(
|
||||
throw new InvalidOperationException($"Unexpected onboarding stage {onboardingStage}");
|
||||
}
|
||||
|
||||
return onboardingStage is not (LauncherOnboardingStage.NeedsCredentials or LauncherOnboardingStage.NeedsExecutable or LauncherOnboardingStage.NeedsConfiguration);
|
||||
return onboardingStage
|
||||
is not (
|
||||
LauncherOnboardingStage.NeedsCredentials
|
||||
or LauncherOnboardingStage.NeedsExecutable
|
||||
or LauncherOnboardingStage.NeedsConfiguration
|
||||
);
|
||||
}
|
||||
|
||||
private void RetrieveLaunchConfigurations()
|
||||
{
|
||||
var latestLaunchConfiguration = this.launchConfigurationService.GetLastLaunchConfigurationWithCredentials();
|
||||
var configurations = this.launchConfigurationService.GetLaunchConfigurations()
|
||||
var latestLaunchConfiguration =
|
||||
this.launchConfigurationService.GetLastLaunchConfigurationWithCredentials();
|
||||
var configurations = this
|
||||
.launchConfigurationService.GetLaunchConfigurations()
|
||||
.Select(c => new LauncherViewContext { Configuration = c, CanLaunch = false })
|
||||
.ToList();
|
||||
|
||||
|
||||
this.LaunchConfigurations.Clear();
|
||||
foreach (var config in configurations)
|
||||
{
|
||||
this.LaunchConfigurations.Add(config);
|
||||
}
|
||||
|
||||
this.SelectedConfiguration = this.LaunchConfigurations.FirstOrDefault(c => c.Configuration?.Equals(latestLaunchConfiguration) is true)
|
||||
?? this.LaunchConfigurations.FirstOrDefault();
|
||||
|
||||
this.SelectedConfiguration =
|
||||
this.LaunchConfigurations.FirstOrDefault(c =>
|
||||
c.Configuration?.Equals(latestLaunchConfiguration) is true
|
||||
) ?? this.LaunchConfigurations.FirstOrDefault();
|
||||
}
|
||||
|
||||
private async Task PeriodicallyCheckSelectedConfigState(CancellationToken cancellationToken)
|
||||
@@ -249,12 +280,8 @@ public sealed class LaunchViewModel(
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
catch (Exception) { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,7 +318,12 @@ public sealed class LaunchViewModel(
|
||||
}
|
||||
else
|
||||
{
|
||||
this.CanLaunch = (this.SelectedConfiguration.CanKill || this.SelectedConfiguration.CanLaunch || this.SelectedConfiguration.CanAttach) && !this.IsLaunching;
|
||||
this.CanLaunch =
|
||||
(
|
||||
this.SelectedConfiguration.CanKill
|
||||
|| this.SelectedConfiguration.CanLaunch
|
||||
|| this.SelectedConfiguration.CanAttach
|
||||
) && !this.IsLaunching;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,8 +383,10 @@ public sealed class LaunchViewModel(
|
||||
return;
|
||||
}
|
||||
|
||||
this.launchConfigurationService.SetLastLaunchConfigurationWithCredentials(this.SelectedConfiguration.Configuration);
|
||||
|
||||
this.launchConfigurationService.SetLastLaunchConfigurationWithCredentials(
|
||||
this.SelectedConfiguration.Configuration
|
||||
);
|
||||
|
||||
try
|
||||
{
|
||||
if (this.SelectedConfiguration.AppContext is not null)
|
||||
@@ -361,7 +395,11 @@ public sealed class LaunchViewModel(
|
||||
}
|
||||
else if (this.SelectedConfiguration.ApiContext is not null)
|
||||
{
|
||||
await this.AttachContext(this.SelectedConfiguration, this.SelectedConfiguration.ApiContext, cancellationToken);
|
||||
await this.AttachContext(
|
||||
this.SelectedConfiguration,
|
||||
this.SelectedConfiguration.ApiContext,
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -375,11 +413,17 @@ public sealed class LaunchViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CheckGameState(LauncherViewContext launcherViewContext, bool isSelected, CancellationToken cancellationToken)
|
||||
private async Task CheckGameState(
|
||||
LauncherViewContext launcherViewContext,
|
||||
bool isSelected,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
if (launcherViewContext.Configuration is null ||
|
||||
launcherViewContext.Configuration.Credentials is null ||
|
||||
!isSelected)
|
||||
if (
|
||||
launcherViewContext.Configuration is null
|
||||
|| launcherViewContext.Configuration.Credentials is null
|
||||
|| !isSelected
|
||||
)
|
||||
{
|
||||
launcherViewContext.GameRunning = false;
|
||||
launcherViewContext.CanLaunch = false;
|
||||
@@ -390,7 +434,7 @@ public sealed class LaunchViewModel(
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.applicationLauncher.GetGuildwarsProcesses().Count == 0)
|
||||
if (this.applicationLauncher.GetGuildwarsProcesses().Length == 0)
|
||||
{
|
||||
launcherViewContext.GameRunning = false;
|
||||
launcherViewContext.CanLaunch = true;
|
||||
@@ -401,8 +445,11 @@ public sealed class LaunchViewModel(
|
||||
return;
|
||||
}
|
||||
|
||||
(var appContext, var apiContext) = await this.GetAppAndApiContext(launcherViewContext, cancellationToken);
|
||||
|
||||
(var appContext, var apiContext) = await this.GetAppAndApiContext(
|
||||
launcherViewContext,
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
// No app context means game is not running for this configuration - allow launch
|
||||
if (appContext is null)
|
||||
{
|
||||
@@ -437,32 +484,49 @@ public sealed class LaunchViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<(GuildWarsApplicationLaunchContext? AppContext, ScopedApiContext? ApiContext)> GetAppAndApiContext(LauncherViewContext launcherViewContext, CancellationToken cancellationToken)
|
||||
private async Task<(
|
||||
GuildWarsApplicationLaunchContext? AppContext,
|
||||
ScopedApiContext? ApiContext
|
||||
)> GetAppAndApiContext(
|
||||
LauncherViewContext launcherViewContext,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
if (launcherViewContext.Configuration is null ||
|
||||
launcherViewContext.Configuration.Credentials is null)
|
||||
if (
|
||||
launcherViewContext.Configuration is null
|
||||
|| launcherViewContext.Configuration.Credentials is null
|
||||
)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
// Return early if the app and api context are already initialized
|
||||
if (launcherViewContext.AppContext is not null &&
|
||||
!launcherViewContext.AppContext.GuildWarsProcess.HasExited &&
|
||||
launcherViewContext.ApiContext is not null &&
|
||||
await launcherViewContext.ApiContext.IsAvailable(cancellationToken))
|
||||
if (
|
||||
launcherViewContext.AppContext is not null
|
||||
&& !launcherViewContext.AppContext.GuildWarsProcess.HasExited
|
||||
&& launcherViewContext.ApiContext is not null
|
||||
&& await launcherViewContext.ApiContext.IsAvailable(cancellationToken)
|
||||
)
|
||||
{
|
||||
return (launcherViewContext.AppContext, launcherViewContext.ApiContext);
|
||||
}
|
||||
|
||||
var processContext = this.applicationLauncher.GetGuildwarsProcess(launcherViewContext.Configuration);
|
||||
var processContext = this.applicationLauncher.GetGuildwarsProcess(
|
||||
launcherViewContext.Configuration
|
||||
);
|
||||
if (processContext is not null)
|
||||
{
|
||||
var maybeApiContext = await this.daybreakApiService.GetDaybreakApiContext(processContext.GuildWarsProcess, cancellationToken);
|
||||
var maybeApiContext = await this.daybreakApiService.GetDaybreakApiContext(
|
||||
processContext.GuildWarsProcess,
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
// If the API is available but it does not belong to the desired user, return null
|
||||
if (maybeApiContext is not null &&
|
||||
await maybeApiContext.GetLoginInfo(cancellationToken) is LoginInfo loginInfo &&
|
||||
loginInfo.Email != launcherViewContext.Configuration.Credentials.Username)
|
||||
if (
|
||||
maybeApiContext is not null
|
||||
&& await maybeApiContext.GetLoginInfo(cancellationToken) is LoginInfo loginInfo
|
||||
&& loginInfo.Email != launcherViewContext.Configuration.Credentials.Username
|
||||
)
|
||||
{
|
||||
return (processContext, default);
|
||||
}
|
||||
@@ -470,11 +534,17 @@ public sealed class LaunchViewModel(
|
||||
return (processContext, maybeApiContext);
|
||||
}
|
||||
|
||||
var apiContext = await this.daybreakApiService.FindDaybreakApiContextByCredentials(launcherViewContext.Configuration.Credentials, cancellationToken);
|
||||
var apiContext = await this.daybreakApiService.FindDaybreakApiContextByCredentials(
|
||||
launcherViewContext.Configuration.Credentials,
|
||||
cancellationToken
|
||||
);
|
||||
return (processContext, apiContext);
|
||||
}
|
||||
|
||||
private async Task AttachContext(GuildWarsApplicationLaunchContext context, CancellationToken cancellationToken)
|
||||
private async Task AttachContext(
|
||||
GuildWarsApplicationLaunchContext context,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
if (!this.focusViewOptions.CurrentValue.Enabled)
|
||||
{
|
||||
@@ -482,25 +552,41 @@ public sealed class LaunchViewModel(
|
||||
}
|
||||
|
||||
using var notificationToken = this.notificationService.NotifyInformation(
|
||||
title: "Attaching to Guild Wars process...",
|
||||
description: "Attempting to attach to Guild Wars process");
|
||||
var apiContext = await this.daybreakApiService.AttachDaybreakApiContext(context, cancellationToken);
|
||||
title: "Attaching to Guild Wars process...",
|
||||
description: "Attempting to attach to Guild Wars process"
|
||||
);
|
||||
var apiContext = await this.daybreakApiService.AttachDaybreakApiContext(
|
||||
context,
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
if (apiContext is null)
|
||||
{
|
||||
this.notificationService.NotifyError(
|
||||
title: "Could not attach to Guild Wars",
|
||||
description: "Could not find the Api context to attach to Guild Wars. Check the logs for more details");
|
||||
description: "Could not find the Api context to attach to Guild Wars. Check the logs for more details"
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.viewManager.ShowView<FocusView>(
|
||||
(nameof(FocusView.ProcessId), context.ProcessId.ToString()),
|
||||
(nameof(FocusView.ConfigurationId), context.LaunchConfiguration.Identifier ?? throw new InvalidOperationException("LaunchConfig identifier cannot be null")));
|
||||
(
|
||||
nameof(FocusView.ConfigurationId),
|
||||
context.LaunchConfiguration.Identifier
|
||||
?? throw new InvalidOperationException(
|
||||
"LaunchConfig identifier cannot be null"
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AttachContext(LauncherViewContext launcherViewContext, ScopedApiContext apiContext, CancellationToken cancellationToken)
|
||||
private async Task AttachContext(
|
||||
LauncherViewContext launcherViewContext,
|
||||
ScopedApiContext apiContext,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
if (launcherViewContext.Configuration is null)
|
||||
{
|
||||
@@ -513,17 +599,21 @@ public sealed class LaunchViewModel(
|
||||
}
|
||||
|
||||
using var notificationToken = this.notificationService.NotifyInformation(
|
||||
title: "Attaching to Guild Wars process...",
|
||||
description: "Attempting to attach to Guild Wars process");
|
||||
title: "Attaching to Guild Wars process...",
|
||||
description: "Attempting to attach to Guild Wars process"
|
||||
);
|
||||
|
||||
var processIdResponse = await apiContext.GetProcessId(cancellationToken);
|
||||
if (processIdResponse is null ||
|
||||
Process.GetProcessById(processIdResponse.ProcessId) is not Process guildWarsProcess)
|
||||
if (
|
||||
processIdResponse is null
|
||||
|| Process.GetProcessById(processIdResponse.ProcessId) is not Process guildWarsProcess
|
||||
)
|
||||
{
|
||||
notificationToken.Cancel();
|
||||
this.notificationService.NotifyError(
|
||||
title: "Could not attach to Guild Wars",
|
||||
description: "Could not find the Api context to attach to Guild Wars. Check the logs for more details");
|
||||
description: "Could not find the Api context to attach to Guild Wars. Check the logs for more details"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -531,17 +621,31 @@ public sealed class LaunchViewModel(
|
||||
{
|
||||
ProcessId = (uint)processIdResponse.ProcessId,
|
||||
GuildWarsProcess = guildWarsProcess,
|
||||
LaunchConfiguration = launcherViewContext.Configuration
|
||||
LaunchConfiguration = launcherViewContext.Configuration,
|
||||
};
|
||||
|
||||
await this.daybreakApiService.AttachDaybreakApiContext(launchContext, apiContext, cancellationToken);
|
||||
this.launchConfigurationService.SetLastLaunchConfigurationWithCredentials(launcherViewContext.Configuration);
|
||||
await this.daybreakApiService.AttachDaybreakApiContext(
|
||||
launchContext,
|
||||
apiContext,
|
||||
cancellationToken
|
||||
);
|
||||
this.launchConfigurationService.SetLastLaunchConfigurationWithCredentials(
|
||||
launcherViewContext.Configuration
|
||||
);
|
||||
this.viewManager.ShowView<FocusView>(
|
||||
(nameof(FocusView.ProcessId), launchContext.ProcessId.ToString()),
|
||||
(nameof(FocusView.ConfigurationId), launchContext.LaunchConfiguration.Identifier ?? throw new InvalidOperationException("LaunchConfig identifier cannot be null")));
|
||||
(nameof(FocusView.ProcessId), launchContext.ProcessId.ToString()),
|
||||
(
|
||||
nameof(FocusView.ConfigurationId),
|
||||
launchContext.LaunchConfiguration.Identifier
|
||||
?? throw new InvalidOperationException("LaunchConfig identifier cannot be null")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private async Task LaunchContext(LauncherViewContext launcherViewContext, CancellationToken cancellationToken)
|
||||
private async Task LaunchContext(
|
||||
LauncherViewContext launcherViewContext,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
if (launcherViewContext.Configuration is null)
|
||||
{
|
||||
@@ -549,15 +653,20 @@ public sealed class LaunchViewModel(
|
||||
}
|
||||
|
||||
var launchNotificationToken = this.notificationService.NotifyInformation(
|
||||
title: "Launching Guild Wars...",
|
||||
description: $"Attempting to launch Guild Wars process at {launcherViewContext.Configuration.ExecutablePath}",
|
||||
expirationTime: DateTime.MaxValue);
|
||||
var launchedContext = await this.applicationLauncher.LaunchGuildwars(launcherViewContext.Configuration, cancellationToken);
|
||||
title: "Launching Guild Wars...",
|
||||
description: $"Attempting to launch Guild Wars process at {launcherViewContext.Configuration.ExecutablePath}",
|
||||
expirationTime: DateTime.MaxValue
|
||||
);
|
||||
var launchedContext = await this.applicationLauncher.LaunchGuildwars(
|
||||
launcherViewContext.Configuration,
|
||||
cancellationToken
|
||||
);
|
||||
if (launchedContext is null)
|
||||
{
|
||||
this.notificationService.NotifyError(
|
||||
title: "Could not launch Guild Wars",
|
||||
description: $"Could not launch Guild Wars at {launcherViewContext.Configuration.ExecutablePath}. Check the logs for more details");
|
||||
description: $"Could not launch Guild Wars at {launcherViewContext.Configuration.ExecutablePath}. Check the logs for more details"
|
||||
);
|
||||
launchNotificationToken.Cancel();
|
||||
return;
|
||||
}
|
||||
@@ -565,39 +674,60 @@ public sealed class LaunchViewModel(
|
||||
launcherViewContext.AppContext = launchedContext;
|
||||
launchNotificationToken.Cancel();
|
||||
this.daybreakApiService.RequestInstancesAnnouncement();
|
||||
this.launchConfigurationService.SetLastLaunchConfigurationWithCredentials(launcherViewContext.Configuration);
|
||||
this.launchConfigurationService.SetLastLaunchConfigurationWithCredentials(
|
||||
launcherViewContext.Configuration
|
||||
);
|
||||
if (!this.focusViewOptions.CurrentValue.Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var attachNotificationToken = this.notificationService.NotifyInformation(
|
||||
title: "Attaching to Guild Wars process...",
|
||||
description: "Attempting to attach to Guild Wars process");
|
||||
title: "Attaching to Guild Wars process...",
|
||||
description: "Attempting to attach to Guild Wars process"
|
||||
);
|
||||
|
||||
//Wait 1 second to allow the launched Guild Wars process to advertise itself
|
||||
var sw = Stopwatch.StartNew();
|
||||
var apiContext = await this.daybreakApiService.AttachDaybreakApiContext(launchedContext, cancellationToken);
|
||||
while (sw.Elapsed < LaunchTimeout && !cancellationToken.IsCancellationRequested && apiContext is null)
|
||||
var apiContext = await this.daybreakApiService.AttachDaybreakApiContext(
|
||||
launchedContext,
|
||||
cancellationToken
|
||||
);
|
||||
while (
|
||||
sw.Elapsed < LaunchTimeout
|
||||
&& !cancellationToken.IsCancellationRequested
|
||||
&& apiContext is null
|
||||
)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
|
||||
apiContext = await this.daybreakApiService.AttachDaybreakApiContext(launchedContext, cancellationToken);
|
||||
apiContext = await this.daybreakApiService.AttachDaybreakApiContext(
|
||||
launchedContext,
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
attachNotificationToken.Cancel();
|
||||
|
||||
if (apiContext is null)
|
||||
{
|
||||
this.notificationService.NotifyError(
|
||||
title: "Could not attach to Guild Wars",
|
||||
description: "Could not find the Api context to attach to Guild Wars. Check the logs for more details");
|
||||
description: "Could not find the Api context to attach to Guild Wars. Check the logs for more details"
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
launcherViewContext.ApiContext = apiContext;
|
||||
this.viewManager.ShowView<FocusView>(
|
||||
(nameof(FocusView.ProcessId), launchedContext.ProcessId.ToString()),
|
||||
(nameof(FocusView.ConfigurationId), launchedContext.LaunchConfiguration.Identifier ?? throw new InvalidOperationException("LaunchConfig identifier cannot be null")));
|
||||
(
|
||||
nameof(FocusView.ConfigurationId),
|
||||
launchedContext.LaunchConfiguration.Identifier
|
||||
?? throw new InvalidOperationException(
|
||||
"LaunchConfig identifier cannot be null"
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Daybreak.Linux.Configuration;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Daybreak.Linux;
|
||||
|
||||
@@ -7,45 +6,8 @@ public static partial class Launcher
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
ConfigureEnvironment();
|
||||
|
||||
var bootstrap = Launch.Launcher.SetupBootstrap();
|
||||
var platformConfiguration = new LinuxPlatformConfiguration();
|
||||
Launch.Launcher.LaunchSequence(args, bootstrap, platformConfiguration);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures environment variables needed for WebKitGTK/Photino to work
|
||||
/// reliably across different Linux display servers and GPU drivers.
|
||||
/// Variables are only set when not already defined by the user, so they
|
||||
/// can always be overridden externally.
|
||||
/// </summary>
|
||||
private static void ConfigureEnvironment()
|
||||
{
|
||||
// These environment variables must ideally be set BEFORE the process
|
||||
// starts (e.g. via the launch wrapper script) so that native libraries
|
||||
// like Mesa/EGL and libwayland see them at load time. Setting them here
|
||||
// serves as a fallback for cases where the wrapper isn't used, but may
|
||||
// not prevent all Wayland-related crashes.
|
||||
|
||||
// WebKitGTK's DMA-BUF renderer can crash or produce artifacts on
|
||||
// certain GPU driver combinations (especially Nvidia proprietary).
|
||||
SetIfUnset("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
|
||||
|
||||
// GPU-accelerated compositing in WebKitGTK can cause blank windows
|
||||
// or crashes with some Mesa/Nvidia drivers.
|
||||
SetIfUnset("WEBKIT_DISABLE_COMPOSITING_MODE", "1");
|
||||
|
||||
// Photino (via WebKitGTK + GTK3) does not fully support native Wayland.
|
||||
// Force X11 (via XWayland on Wayland sessions).
|
||||
SetIfUnset("GDK_BACKEND", "x11");
|
||||
}
|
||||
|
||||
private static void SetIfUnset(string variable, string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable(variable)))
|
||||
{
|
||||
Environment.SetEnvironmentVariable(variable, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using System.Diagnostics;
|
||||
using System.Extensions.Core;
|
||||
using Daybreak.Linux.Services.Wine;
|
||||
using Daybreak.Shared.Models.LaunchConfigurations;
|
||||
using Daybreak.Shared.Services.ApplicationLauncher;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Diagnostics;
|
||||
using System.Extensions.Core;
|
||||
|
||||
namespace Daybreak.Linux.Services.ApplicationLauncher;
|
||||
|
||||
@@ -15,24 +15,25 @@ namespace Daybreak.Linux.Services.ApplicationLauncher;
|
||||
/// </summary>
|
||||
public sealed class GuildWarsProcessFinder(
|
||||
IWinePrefixManager winePrefixManager,
|
||||
ILogger<GuildWarsProcessFinder> logger)
|
||||
: IGuildWarsProcessFinder
|
||||
ILogger<GuildWarsProcessFinder> logger
|
||||
) : IGuildWarsProcessFinder
|
||||
{
|
||||
private const string GuildWarsExeName = "Gw.exe";
|
||||
|
||||
private readonly Memory<Process> guildWarsProcesses = new(new Process[256]);
|
||||
private readonly IWinePrefixManager winePrefixManager = winePrefixManager;
|
||||
private readonly ILogger<GuildWarsProcessFinder> logger = logger;
|
||||
|
||||
public IReadOnlyList<Process> GetGuildWarsProcesses()
|
||||
public Memory<Process> GetGuildWarsProcesses()
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
var processes = new List<Process>();
|
||||
|
||||
var index = 0;
|
||||
foreach (var (pid, _) in this.ScanForGuildWarsProcesses())
|
||||
{
|
||||
try
|
||||
{
|
||||
processes.Add(Process.GetProcessById(pid));
|
||||
this.guildWarsProcesses.Span[index] = Process.GetProcessById(pid);
|
||||
index++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -40,16 +41,20 @@ public sealed class GuildWarsProcessFinder(
|
||||
}
|
||||
}
|
||||
|
||||
scopedLogger.LogDebug("Found {Count} Guild Wars process(es) under Wine", processes.Count);
|
||||
return processes;
|
||||
scopedLogger.LogDebug("Found {Count} Guild Wars process(es) under Wine", index);
|
||||
return this.guildWarsProcesses[..index];
|
||||
}
|
||||
|
||||
public GuildWarsApplicationLaunchContext? FindProcess(LaunchConfigurationWithCredentials configuration)
|
||||
public GuildWarsApplicationLaunchContext? FindProcess(
|
||||
LaunchConfigurationWithCredentials configuration
|
||||
)
|
||||
{
|
||||
return this.FindProcesses(configuration).FirstOrDefault();
|
||||
}
|
||||
|
||||
public IEnumerable<GuildWarsApplicationLaunchContext?> FindProcesses(params LaunchConfigurationWithCredentials[] configurations)
|
||||
public IEnumerable<GuildWarsApplicationLaunchContext?> FindProcesses(
|
||||
params LaunchConfigurationWithCredentials[] configurations
|
||||
)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
|
||||
@@ -57,7 +62,8 @@ public sealed class GuildWarsProcessFinder(
|
||||
{
|
||||
// Match executable path from Wine cmdline to configuration
|
||||
var matchedConfig = configurations.FirstOrDefault(c =>
|
||||
ConfigurationMatchesPath(c, executablePath));
|
||||
ConfigurationMatchesPath(c, executablePath)
|
||||
);
|
||||
|
||||
if (matchedConfig is null)
|
||||
{
|
||||
@@ -79,7 +85,7 @@ public sealed class GuildWarsProcessFinder(
|
||||
{
|
||||
GuildWarsProcess = process,
|
||||
LaunchConfiguration = matchedConfig,
|
||||
ProcessId = (uint)pid
|
||||
ProcessId = (uint)pid,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -132,7 +138,8 @@ public sealed class GuildWarsProcessFinder(
|
||||
// cmdline is null-separated; find the segment containing Gw.exe
|
||||
var segments = cmdline.Split('\0', StringSplitOptions.RemoveEmptyEntries);
|
||||
var exeSegment = segments.FirstOrDefault(s =>
|
||||
s.EndsWith(GuildWarsExeName, StringComparison.OrdinalIgnoreCase));
|
||||
s.EndsWith(GuildWarsExeName, StringComparison.OrdinalIgnoreCase)
|
||||
);
|
||||
|
||||
if (exeSegment is not null)
|
||||
{
|
||||
@@ -140,8 +147,14 @@ public sealed class GuildWarsProcessFinder(
|
||||
executablePath = WinePathToLinuxPath(exeSegment);
|
||||
}
|
||||
}
|
||||
catch (UnauthorizedAccessException) { continue; }
|
||||
catch (IOException) { continue; }
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (executablePath is not null)
|
||||
{
|
||||
@@ -157,8 +170,10 @@ public sealed class GuildWarsProcessFinder(
|
||||
private static string WinePathToLinuxPath(string winePath)
|
||||
{
|
||||
// Strip Z: prefix and convert backslashes
|
||||
if (winePath.StartsWith("Z:", StringComparison.OrdinalIgnoreCase) ||
|
||||
winePath.StartsWith("z:", StringComparison.OrdinalIgnoreCase))
|
||||
if (
|
||||
winePath.StartsWith("Z:", StringComparison.OrdinalIgnoreCase)
|
||||
|| winePath.StartsWith("z:", StringComparison.OrdinalIgnoreCase)
|
||||
)
|
||||
{
|
||||
winePath = winePath[2..];
|
||||
}
|
||||
@@ -168,7 +183,8 @@ public sealed class GuildWarsProcessFinder(
|
||||
|
||||
private static bool ConfigurationMatchesPath(
|
||||
LaunchConfigurationWithCredentials configuration,
|
||||
string executablePath)
|
||||
string executablePath
|
||||
)
|
||||
{
|
||||
if (configuration.ExecutablePath is null)
|
||||
{
|
||||
@@ -178,6 +194,7 @@ public sealed class GuildWarsProcessFinder(
|
||||
return string.Equals(
|
||||
Path.GetFullPath(configuration.ExecutablePath),
|
||||
Path.GetFullPath(executablePath),
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
StringComparison.OrdinalIgnoreCase
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,35 @@
|
||||
using Daybreak.Shared.Models.LaunchConfigurations;
|
||||
using System.Diagnostics;
|
||||
using Daybreak.Shared.Models.LaunchConfigurations;
|
||||
using Daybreak.Shared.Services.Mods;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Daybreak.Shared.Services.ApplicationLauncher;
|
||||
|
||||
public interface IApplicationLauncher
|
||||
{
|
||||
Task<bool> ReapplyMods(GuildWarsApplicationLaunchContext guildWarsApplicationLaunchContext, IEnumerable<IModService> mods, CancellationToken cancellationToken);
|
||||
Task<IEnumerable<IModService>> CheckMods(GuildWarsApplicationLaunchContext guildWarsApplicationLaunchContext, CancellationToken cancellationToken);
|
||||
IEnumerable<string> GetLoadedModules(GuildWarsApplicationLaunchContext guildWarsApplicationLaunchContext);
|
||||
GuildWarsApplicationLaunchContext? GetGuildwarsProcess(LaunchConfigurationWithCredentials launchConfigurationWithCredentials);
|
||||
IEnumerable<GuildWarsApplicationLaunchContext?> GetGuildwarsProcesses(params LaunchConfigurationWithCredentials[] launchConfigurationWithCredentials);
|
||||
IReadOnlyList<Process> GetGuildwarsProcesses();
|
||||
Task<bool> ReapplyMods(
|
||||
GuildWarsApplicationLaunchContext guildWarsApplicationLaunchContext,
|
||||
IEnumerable<IModService> mods,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
Task<IEnumerable<IModService>> CheckMods(
|
||||
GuildWarsApplicationLaunchContext guildWarsApplicationLaunchContext,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
IEnumerable<string> GetLoadedModules(
|
||||
GuildWarsApplicationLaunchContext guildWarsApplicationLaunchContext
|
||||
);
|
||||
GuildWarsApplicationLaunchContext? GetGuildwarsProcess(
|
||||
LaunchConfigurationWithCredentials launchConfigurationWithCredentials
|
||||
);
|
||||
IEnumerable<GuildWarsApplicationLaunchContext?> GetGuildwarsProcesses(
|
||||
params LaunchConfigurationWithCredentials[] launchConfigurationWithCredentials
|
||||
);
|
||||
ReadOnlyMemory<Process> GetGuildwarsProcesses();
|
||||
void KillGuildWarsProcess(GuildWarsApplicationLaunchContext guildWarsApplicationLaunchContext);
|
||||
Task<GuildWarsApplicationLaunchContext?> LaunchGuildwars(LaunchConfigurationWithCredentials launchConfigurationWithCredentials, CancellationToken cancellationToken);
|
||||
Task<GuildWarsApplicationLaunchContext?> LaunchGuildwars(
|
||||
LaunchConfigurationWithCredentials launchConfigurationWithCredentials,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
void RestartDaybreak();
|
||||
void RestartDaybreakAsAdmin();
|
||||
void RestartDaybreakAsNormalUser();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Daybreak.Shared.Models.LaunchConfigurations;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using Daybreak.Shared.Models.LaunchConfigurations;
|
||||
|
||||
namespace Daybreak.Shared.Services.ApplicationLauncher;
|
||||
|
||||
@@ -13,16 +14,20 @@ public interface IGuildWarsProcessFinder
|
||||
/// <summary>
|
||||
/// Returns all running Guild Wars processes.
|
||||
/// </summary>
|
||||
IReadOnlyList<Process> GetGuildWarsProcesses();
|
||||
Memory<Process> GetGuildWarsProcesses();
|
||||
|
||||
/// <summary>
|
||||
/// Finds a running Guild Wars process that matches the given launch configuration.
|
||||
/// Returns null if no matching process is found.
|
||||
/// </summary>
|
||||
GuildWarsApplicationLaunchContext? FindProcess(LaunchConfigurationWithCredentials configuration);
|
||||
GuildWarsApplicationLaunchContext? FindProcess(
|
||||
LaunchConfigurationWithCredentials configuration
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Finds all running Guild Wars processes that match any of the given launch configurations.
|
||||
/// </summary>
|
||||
IEnumerable<GuildWarsApplicationLaunchContext?> FindProcesses(params LaunchConfigurationWithCredentials[] configurations);
|
||||
IEnumerable<GuildWarsApplicationLaunchContext?> FindProcesses(
|
||||
params LaunchConfigurationWithCredentials[] configurations
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using Daybreak.Shared.Models.LaunchConfigurations;
|
||||
using Daybreak.Shared.Services.ApplicationLauncher;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using Daybreak.Shared.Models.LaunchConfigurations;
|
||||
using Daybreak.Shared.Services.ApplicationLauncher;
|
||||
using static Daybreak.Windows.Utils.NativeMethods;
|
||||
|
||||
namespace Daybreak.Windows.Services.ApplicationLauncher;
|
||||
@@ -16,23 +16,30 @@ public sealed class GuildWarsProcessFinder : IGuildWarsProcessFinder
|
||||
{
|
||||
private const string ProcessName = "gw";
|
||||
|
||||
public IReadOnlyList<Process> GetGuildWarsProcesses()
|
||||
public Memory<Process> GetGuildWarsProcesses()
|
||||
{
|
||||
return Process.GetProcessesByName(ProcessName);
|
||||
}
|
||||
|
||||
public GuildWarsApplicationLaunchContext? FindProcess(LaunchConfigurationWithCredentials configuration)
|
||||
public GuildWarsApplicationLaunchContext? FindProcess(
|
||||
LaunchConfigurationWithCredentials configuration
|
||||
)
|
||||
{
|
||||
return this.FindProcesses(configuration).FirstOrDefault();
|
||||
}
|
||||
|
||||
public IEnumerable<GuildWarsApplicationLaunchContext?> FindProcesses(params LaunchConfigurationWithCredentials[] configurations)
|
||||
public IEnumerable<GuildWarsApplicationLaunchContext?> FindProcesses(
|
||||
params LaunchConfigurationWithCredentials[] configurations
|
||||
)
|
||||
{
|
||||
return Process.GetProcessesByName(ProcessName)
|
||||
return Process
|
||||
.GetProcessesByName(ProcessName)
|
||||
.Select(process =>
|
||||
{
|
||||
(var AssociatedConfiguration, _, var ProcessId) = configurations
|
||||
.Select(c => (c, ConfigurationMatchesProcess(c, process, out var processId), processId))
|
||||
.Select(c =>
|
||||
(c, ConfigurationMatchesProcess(c, process, out var processId), processId)
|
||||
)
|
||||
.FirstOrDefault(c => c.Item2);
|
||||
return (Process: process, AssociatedConfiguration, ProcessId);
|
||||
})
|
||||
@@ -40,19 +47,29 @@ public sealed class GuildWarsProcessFinder : IGuildWarsProcessFinder
|
||||
.Select(tuple => new GuildWarsApplicationLaunchContext
|
||||
{
|
||||
GuildWarsProcess = tuple.Process,
|
||||
LaunchConfiguration = tuple.AssociatedConfiguration!,
|
||||
ProcessId = tuple.ProcessId
|
||||
LaunchConfiguration = tuple.AssociatedConfiguration,
|
||||
ProcessId = tuple.ProcessId,
|
||||
});
|
||||
}
|
||||
|
||||
private static bool ConfigurationMatchesProcess(LaunchConfigurationWithCredentials launchConfigurationWithCredentials, Process process, out uint processId)
|
||||
private static bool ConfigurationMatchesProcess(
|
||||
LaunchConfigurationWithCredentials launchConfigurationWithCredentials,
|
||||
Process process,
|
||||
out uint processId
|
||||
)
|
||||
{
|
||||
try
|
||||
{
|
||||
processId = (uint)process.Id;
|
||||
return launchConfigurationWithCredentials.ExecutablePath == process.MainModule?.FileName;
|
||||
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."))
|
||||
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;
|
||||
/*
|
||||
@@ -62,10 +79,7 @@ public sealed class GuildWarsProcessFinder : IGuildWarsProcessFinder
|
||||
* We create a process snapshot and compare the paths
|
||||
*/
|
||||
var hSnapshot = CreateToolhelp32Snapshot(0x00000002, 0); // TH32CS_SNAPPROCESS
|
||||
var pe32 = new ProcessEntry32
|
||||
{
|
||||
dwSize = (uint)Marshal.SizeOf<ProcessEntry32>()
|
||||
};
|
||||
var pe32 = new ProcessEntry32 { dwSize = (uint)Marshal.SizeOf<ProcessEntry32>() };
|
||||
|
||||
var nameBuffer = new StringBuilder(1024);
|
||||
if (Process32First(hSnapshot, ref pe32))
|
||||
@@ -75,9 +89,21 @@ public sealed class GuildWarsProcessFinder : IGuildWarsProcessFinder
|
||||
var size = 1024U;
|
||||
if (pe32.szExeFile == "Gw.exe")
|
||||
{
|
||||
var maybeDesiredProcessHandle = OpenProcess(ProcessAccessFlags.QueryLimitedInformation, false, pe32.th32ProcessID);
|
||||
if (QueryFullProcessImageName(maybeDesiredProcessHandle, 0, nameBuffer, ref size) &&
|
||||
nameBuffer.ToString() == launchConfigurationWithCredentials.ExecutablePath)
|
||||
var maybeDesiredProcessHandle = OpenProcess(
|
||||
ProcessAccessFlags.QueryLimitedInformation,
|
||||
false,
|
||||
pe32.th32ProcessID
|
||||
);
|
||||
if (
|
||||
QueryFullProcessImageName(
|
||||
maybeDesiredProcessHandle,
|
||||
0,
|
||||
nameBuffer,
|
||||
ref size
|
||||
)
|
||||
&& nameBuffer.ToString()
|
||||
== launchConfigurationWithCredentials.ExecutablePath
|
||||
)
|
||||
{
|
||||
processId = pe32.th32ProcessID;
|
||||
CloseHandle(maybeDesiredProcessHandle);
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
using System.Core.Extensions;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Extensions;
|
||||
using System.Extensions.Core;
|
||||
using System.Runtime.InteropServices;
|
||||
using Daybreak.Configuration.Options;
|
||||
using Daybreak.Shared.Models;
|
||||
using Daybreak.Shared.Services.Options;
|
||||
@@ -7,12 +13,6 @@ using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Photino.NET;
|
||||
using System.Core.Extensions;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Extensions;
|
||||
using System.Extensions.Core;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.Windows.Services.Screens;
|
||||
|
||||
@@ -20,11 +20,16 @@ internal sealed class ScreenManager(
|
||||
PhotinoWindow photinoWindow,
|
||||
IOptionsProvider optionsProvider,
|
||||
IOptionsMonitor<ScreenManagerOptions> liveUpdateableOptions,
|
||||
ILogger<ScreenManager> logger) : IScreenManager, IHostedService
|
||||
IOptionsMonitor<LauncherOptions> liveUpdateableLauncherOptions,
|
||||
ILogger<ScreenManager> logger
|
||||
) : IScreenManager, IHostedService
|
||||
{
|
||||
private readonly PhotinoWindow photinoWindow = photinoWindow.ThrowIfNull();
|
||||
private readonly IOptionsProvider optionsProvider = optionsProvider.ThrowIfNull();
|
||||
private readonly IOptionsMonitor<ScreenManagerOptions> liveUpdateableOptions = liveUpdateableOptions.ThrowIfNull();
|
||||
private readonly IOptionsMonitor<ScreenManagerOptions> liveUpdateableOptions =
|
||||
liveUpdateableOptions.ThrowIfNull();
|
||||
private readonly IOptionsMonitor<LauncherOptions> liveUpdateableLauncherOptions =
|
||||
liveUpdateableLauncherOptions.ThrowIfNull();
|
||||
private readonly ILogger<ScreenManager> logger = logger.ThrowIfNull();
|
||||
|
||||
public IEnumerable<Screen> Screens => GetAllScreens();
|
||||
@@ -52,11 +57,18 @@ internal sealed class ScreenManager(
|
||||
|
||||
public void SaveWindowPositionAndSize()
|
||||
{
|
||||
if (!this.liveUpdateableLauncherOptions.CurrentValue.SaveWindowPosition)
|
||||
{
|
||||
this.ResetSavedPosition();
|
||||
return;
|
||||
}
|
||||
|
||||
var position = new Rectangle(
|
||||
this.photinoWindow.Left,
|
||||
this.photinoWindow.Top,
|
||||
this.photinoWindow.Width,
|
||||
this.photinoWindow.Height);
|
||||
this.photinoWindow.Height
|
||||
);
|
||||
|
||||
var options = this.liveUpdateableOptions.CurrentValue;
|
||||
options.X = position.Left;
|
||||
@@ -86,7 +98,15 @@ internal sealed class ScreenManager(
|
||||
return false;
|
||||
}
|
||||
|
||||
NativeMethods.SetWindowPos(hwnd.Value, NativeMethods.HWND_TOP, screen.Size.Left, screen.Size.Top, screen.Size.Width, screen.Size.Height, NativeMethods.SWP_SHOWWINDOW);
|
||||
NativeMethods.SetWindowPos(
|
||||
hwnd.Value,
|
||||
NativeMethods.HWND_TOP,
|
||||
screen.Size.Left,
|
||||
screen.Size.Top,
|
||||
screen.Size.Width,
|
||||
screen.Size.Height,
|
||||
NativeMethods.SWP_SHOWWINDOW
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -96,9 +116,14 @@ internal sealed class ScreenManager(
|
||||
(int)this.liveUpdateableOptions.CurrentValue.X,
|
||||
(int)this.liveUpdateableOptions.CurrentValue.Y,
|
||||
(int)this.liveUpdateableOptions.CurrentValue.Width,
|
||||
(int)this.liveUpdateableOptions.CurrentValue.Height);
|
||||
(int)this.liveUpdateableOptions.CurrentValue.Height
|
||||
);
|
||||
|
||||
if (savedPosition.Width is 0 || savedPosition.Height is 0)
|
||||
if (
|
||||
savedPosition.Width is 0
|
||||
|| savedPosition.Height is 0
|
||||
|| !this.liveUpdateableLauncherOptions.CurrentValue.SaveWindowPosition
|
||||
)
|
||||
{
|
||||
var firstScreen = this.Screens.FirstOrDefault();
|
||||
if (firstScreen.Size.IsEmpty)
|
||||
@@ -107,10 +132,11 @@ internal sealed class ScreenManager(
|
||||
}
|
||||
|
||||
return new Rectangle(
|
||||
firstScreen.Size.X + (firstScreen.Size.Width / 4),
|
||||
firstScreen.Size.Y + (firstScreen.Size.Height / 4),
|
||||
firstScreen.Size.Width / 2,
|
||||
firstScreen.Size.Height / 2);
|
||||
firstScreen.Size.X + (firstScreen.Size.Width / 4),
|
||||
firstScreen.Size.Y + (firstScreen.Size.Height / 4),
|
||||
firstScreen.Size.Width / 2,
|
||||
firstScreen.Size.Height / 2
|
||||
);
|
||||
}
|
||||
|
||||
return savedPosition;
|
||||
@@ -127,20 +153,35 @@ internal sealed class ScreenManager(
|
||||
var screens = new List<Screen>();
|
||||
int index = 0;
|
||||
|
||||
NativeMethods.EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, (hMonitor, hdcMonitor, ref lprcMonitor, dwData) =>
|
||||
{
|
||||
var monitorInfo = new NativeMethods.MonitorInfoEx
|
||||
NativeMethods.EnumDisplayMonitors(
|
||||
IntPtr.Zero,
|
||||
IntPtr.Zero,
|
||||
(hMonitor, hdcMonitor, ref lprcMonitor, dwData) =>
|
||||
{
|
||||
CbSize = (uint)Marshal.SizeOf<NativeMethods.MonitorInfoEx>()
|
||||
};
|
||||
var monitorInfo = new NativeMethods.MonitorInfoEx
|
||||
{
|
||||
CbSize = (uint)Marshal.SizeOf<NativeMethods.MonitorInfoEx>(),
|
||||
};
|
||||
|
||||
if (NativeMethods.GetMonitorInfo(hMonitor, ref monitorInfo))
|
||||
{
|
||||
screens.Add(new Screen(index++, new Rectangle(monitorInfo.RcMonitor.Left, monitorInfo.RcMonitor.Top, monitorInfo.RcMonitor.Width, monitorInfo.RcMonitor.Height)));
|
||||
}
|
||||
if (NativeMethods.GetMonitorInfo(hMonitor, ref monitorInfo))
|
||||
{
|
||||
screens.Add(
|
||||
new Screen(
|
||||
index++,
|
||||
new Rectangle(
|
||||
monitorInfo.RcMonitor.Left,
|
||||
monitorInfo.RcMonitor.Top,
|
||||
monitorInfo.RcMonitor.Width,
|
||||
monitorInfo.RcMonitor.Height
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}, IntPtr.Zero);
|
||||
return true;
|
||||
},
|
||||
IntPtr.Zero
|
||||
);
|
||||
|
||||
return screens;
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
Write-Host "Retrieving version"
|
||||
$filepath = Get-ChildItem -Path .\Daybreak -Filter *.version
|
||||
$version = $filepath.BaseName
|
||||
Write-Host "Version: $version"
|
||||
return $version
|
||||
@@ -1,20 +0,0 @@
|
||||
Param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$version,
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$connectionString,
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$sourcePath
|
||||
)
|
||||
|
||||
# Create a new blob container
|
||||
$containerName = "v$($version.ToLower().Replace('.', '-'))" # Container names must be lowercase
|
||||
Write-Host "Creating container $($containerName)"
|
||||
az storage container create --name $containerName --connection-string $connectionString
|
||||
|
||||
Write-Host "Uploading files"
|
||||
# Upload all files from the source path to the blob container
|
||||
az storage blob upload-batch --destination $containerName --source $sourcePath --connection-string $connectionString
|
||||
|
||||
Write-Host "Setting public access to container"
|
||||
az storage container set-permission --name $containerName --public-access blob --connection-string $connectionString
|
||||
@@ -0,0 +1,14 @@
|
||||
$ErrorActionPreference = "Stop"
|
||||
$RepoRoot = Resolve-Path "$PSScriptRoot/.."
|
||||
|
||||
# Force X11 backend — Photino/WebKitGTK/GTK3 doesn't support Wayland natively
|
||||
if (-not $env:GDK_BACKEND) { $env:GDK_BACKEND = "x11" }
|
||||
|
||||
# Prevent WebKitGTK from using DMA-BUF (crashes on some Nvidia/Mesa combos)
|
||||
if (-not $env:WEBKIT_DISABLE_DMABUF_RENDERER) { $env:WEBKIT_DISABLE_DMABUF_RENDERER = "1" }
|
||||
|
||||
# Disable GPU compositing (blank windows on some drivers)
|
||||
if (-not $env:WEBKIT_DISABLE_COMPOSITING_MODE) { $env:WEBKIT_DISABLE_COMPOSITING_MODE = "1" }
|
||||
|
||||
dotnet run --project Daybreak.Linux
|
||||
|
||||
Reference in New Issue
Block a user