diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredLayoutState.cs b/src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredLayoutState.cs index 70515422..bb44b651 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredLayoutState.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredLayoutState.cs @@ -62,6 +62,7 @@ internal sealed class UniformStaggeredLayoutState } } + [SuppressMessage("", "SH007")] internal UniformStaggeredColumnLayout GetColumnLayout(int columnIndex) { this.columnLayout.TryGetValue(columnIndex, out UniformStaggeredColumnLayout? columnLayout); @@ -116,7 +117,7 @@ internal sealed class UniformStaggeredLayoutState desiredHeight = estimatedHeight; } - if (Math.Abs(desiredHeight - lastAverageHeight) < 5) // Why 5? + if (Math.Abs(desiredHeight - lastAverageHeight) < 5) { return lastAverageHeight; } diff --git a/src/Snap.Hutao/Snap.Hutao/Control/ValueConverter.cs b/src/Snap.Hutao/Snap.Hutao/Control/ValueConverter.cs index c8b322cf..facb12b7 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/ValueConverter.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/ValueConverter.cs @@ -15,22 +15,7 @@ internal abstract class ValueConverter : IValueConverter /// public object? Convert(object value, Type targetType, object parameter, string language) { -#if DEBUG - try - { - return Convert((TFrom)value); - } - catch (Exception ex) - { - Ioc.Default - .GetRequiredService>>() - .LogError(ex, "值转换器异常"); - - throw; - } -#else return Convert((TFrom)value); -#endif } /// diff --git a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteNotificationOperation.cs b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteNotificationOperation.cs index 2286856b..1c5890ce 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteNotificationOperation.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteNotificationOperation.cs @@ -24,7 +24,7 @@ internal sealed partial class DailyNoteNotificationOperation private const string ToastAttributionUnknown = "Unknown"; private readonly ITaskContext taskContext; - private readonly IGameService gameService; + private readonly IGameServiceFacade gameService; private readonly BindingClient bindingClient; private readonly DailyNoteOptions options; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryWebCacheProvider.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryWebCacheProvider.cs index 45d02c9c..635c336c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryWebCacheProvider.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryWebCacheProvider.cs @@ -19,7 +19,7 @@ namespace Snap.Hutao.Service.GachaLog.QueryProvider; [Injection(InjectAs.Transient)] internal sealed partial class GachaLogQueryWebCacheProvider : IGachaLogQueryProvider { - private readonly IGameService gameService; + private readonly IGameServiceFacade gameService; private readonly MetadataOptions metadataOptions; /// diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/GameAccountService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/GameAccountService.cs new file mode 100644 index 00000000..ff717fab --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/GameAccountService.cs @@ -0,0 +1,135 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.ExceptionService; +using Snap.Hutao.Factory.Abstraction; +using Snap.Hutao.Model.Entity; +using Snap.Hutao.View.Dialog; +using System.Collections.ObjectModel; + +namespace Snap.Hutao.Service.Game.Account; + +[ConstructorGenerated] +[Injection(InjectAs.Singleton, typeof(IGameAccountService))] +internal sealed partial class GameAccountService : IGameAccountService +{ + private readonly IContentDialogFactory contentDialogFactory; + private readonly IServiceProvider serviceProvider; + private readonly IGameDbService gameDbService; + private readonly ITaskContext taskContext; + private readonly AppOptions appOptions; + + private ObservableCollection? gameAccounts; + + public ObservableCollection GameAccountCollection + { + get => gameAccounts ??= gameDbService.GetGameAccountCollection(); + } + + public async ValueTask DetectGameAccountAsync() + { + ArgumentNullException.ThrowIfNull(gameAccounts); + + string? registrySdk = RegistryInterop.Get(); + if (!string.IsNullOrEmpty(registrySdk)) + { + GameAccount? account = null; + try + { + account = gameAccounts.SingleOrDefault(a => a.MihoyoSDK == registrySdk); + } + catch (InvalidOperationException ex) + { + ThrowHelper.UserdataCorrupted(SH.ServiceGameDetectGameAccountMultiMatched, ex); + } + + if (account is null) + { + // ContentDialog must be created by main thread. + await taskContext.SwitchToMainThreadAsync(); + LaunchGameAccountNameDialog dialog = await contentDialogFactory.CreateInstanceAsync().ConfigureAwait(false); + (bool isOk, string name) = await dialog.GetInputNameAsync().ConfigureAwait(false); + + if (isOk) + { + account = GameAccount.From(name, registrySdk); + + // sync database + await taskContext.SwitchToBackgroundAsync(); + await gameDbService.AddGameAccountAsync(account).ConfigureAwait(false); + + // sync cache + await taskContext.SwitchToMainThreadAsync(); + gameAccounts.Add(account); + } + } + + return account; + } + + return default; + } + + public GameAccount? DetectCurrentGameAccount() + { + ArgumentNullException.ThrowIfNull(gameAccounts); + + string? registrySdk = RegistryInterop.Get(); + + if (!string.IsNullOrEmpty(registrySdk)) + { + try + { + return gameAccounts.SingleOrDefault(a => a.MihoyoSDK == registrySdk); + } + catch (InvalidOperationException ex) + { + ThrowHelper.UserdataCorrupted(SH.ServiceGameDetectGameAccountMultiMatched, ex); + } + } + + return null; + } + + public bool SetGameAccount(GameAccount account) + { + if (string.IsNullOrEmpty(appOptions.PowerShellPath)) + { + ThrowHelper.RuntimeEnvironment(SH.ServiceGameRegisteryInteropPowershellNotFound, default!); + } + + return RegistryInterop.Set(account, appOptions.PowerShellPath); + } + + public void AttachGameAccountToUid(GameAccount gameAccount, string uid) + { + gameAccount.UpdateAttachUid(uid); + gameDbService.UpdateGameAccount(gameAccount); + } + + public async ValueTask ModifyGameAccountAsync(GameAccount gameAccount) + { + await taskContext.SwitchToMainThreadAsync(); + LaunchGameAccountNameDialog dialog = serviceProvider.CreateInstance(); + (bool isOk, string name) = await dialog.GetInputNameAsync().ConfigureAwait(true); + + if (isOk) + { + gameAccount.UpdateName(name); + + // sync database + await taskContext.SwitchToBackgroundAsync(); + await gameDbService.UpdateGameAccountAsync(gameAccount).ConfigureAwait(false); + } + } + + public async ValueTask RemoveGameAccountAsync(GameAccount gameAccount) + { + await taskContext.SwitchToMainThreadAsync(); + ArgumentNullException.ThrowIfNull(gameAccounts); + gameAccounts.Remove(gameAccount); + + await taskContext.SwitchToBackgroundAsync(); + await gameDbService.RemoveGameAccountByIdAsync(gameAccount.InnerId).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/IGameAccountService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/IGameAccountService.cs new file mode 100644 index 00000000..108a35a6 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/IGameAccountService.cs @@ -0,0 +1,24 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Entity; +using System.Collections.ObjectModel; + +namespace Snap.Hutao.Service.Game.Account; + +internal interface IGameAccountService +{ + ObservableCollection GameAccountCollection { get; } + + void AttachGameAccountToUid(GameAccount gameAccount, string uid); + + GameAccount? DetectCurrentGameAccount(); + + ValueTask DetectGameAccountAsync(); + + ValueTask ModifyGameAccountAsync(GameAccount gameAccount); + + ValueTask RemoveGameAccountAsync(GameAccount gameAccount); + + bool SetGameAccount(GameAccount account); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/RegistryInterop.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/RegistryInterop.cs similarity index 98% rename from src/Snap.Hutao/Snap.Hutao/Service/Game/RegistryInterop.cs rename to src/Snap.Hutao/Snap.Hutao/Service/Game/Account/RegistryInterop.cs index 97bbd4d5..29e8d129 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/RegistryInterop.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/RegistryInterop.cs @@ -9,7 +9,7 @@ using System.IO; using System.Runtime.InteropServices; using System.Text; -namespace Snap.Hutao.Service.Game; +namespace Snap.Hutao.Service.Game.Account; /// /// 注册表操作 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/ChannelOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/ChannelOptions.cs similarity index 97% rename from src/Snap.Hutao/Snap.Hutao/Service/Game/ChannelOptions.cs rename to src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/ChannelOptions.cs index 8ebb4027..5c335637 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/ChannelOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/ChannelOptions.cs @@ -3,7 +3,7 @@ using Snap.Hutao.Model.Intrinsic; -namespace Snap.Hutao.Service.Game; +namespace Snap.Hutao.Service.Game.Configuration; /// /// 多通道 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/GameChannelOptionsService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/GameChannelOptionsService.cs new file mode 100644 index 00000000..20db8658 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/GameChannelOptionsService.cs @@ -0,0 +1,95 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.ExceptionService; +using Snap.Hutao.Core.IO.Ini; +using Snap.Hutao.Service.Game.Scheme; +using System.IO; +using static Snap.Hutao.Service.Game.GameConstants; + +namespace Snap.Hutao.Service.Game.Configuration; + +[ConstructorGenerated] +[Injection(InjectAs.Singleton, typeof(IGameChannelOptionsService))] +internal sealed partial class GameChannelOptionsService : IGameChannelOptionsService +{ + private readonly AppOptions appOptions; + + public ChannelOptions GetChannelOptions() + { + string gamePath = appOptions.GamePath; + string configPath = Path.Combine(Path.GetDirectoryName(gamePath) ?? string.Empty, ConfigFileName); + bool isOversea = string.Equals(Path.GetFileName(gamePath), GenshinImpactFileName, StringComparison.OrdinalIgnoreCase); + + if (!File.Exists(configPath)) + { + return ChannelOptions.FileNotFound(isOversea, configPath); + } + + using (FileStream stream = File.OpenRead(configPath)) + { + List parameters = IniSerializer.Deserialize(stream).OfType().ToList(); + string? channel = parameters.FirstOrDefault(p => p.Key == ChannelOptions.ChannelName)?.Value; + string? subChannel = parameters.FirstOrDefault(p => p.Key == ChannelOptions.SubChannelName)?.Value; + + return new(channel, subChannel, isOversea); + } + } + + public bool SetChannelOptions(LaunchScheme scheme) + { + string gamePath = appOptions.GamePath; + string? directory = Path.GetDirectoryName(gamePath); + ArgumentException.ThrowIfNullOrEmpty(directory); + string configPath = Path.Combine(directory, ConfigFileName); + + List elements = default!; + try + { + using (FileStream readStream = File.OpenRead(configPath)) + { + elements = IniSerializer.Deserialize(readStream).ToList(); + } + } + catch (FileNotFoundException ex) + { + ThrowHelper.GameFileOperation(SH.ServiceGameSetMultiChannelConfigFileNotFound.Format(configPath), ex); + } + catch (DirectoryNotFoundException ex) + { + ThrowHelper.GameFileOperation(SH.ServiceGameSetMultiChannelConfigFileNotFound.Format(configPath), ex); + } + catch (UnauthorizedAccessException ex) + { + ThrowHelper.GameFileOperation(SH.ServiceGameSetMultiChannelUnauthorizedAccess, ex); + } + + bool changed = false; + + foreach (IniElement element in elements) + { + if (element is IniParameter parameter) + { + if (parameter.Key == "channel") + { + changed = parameter.Set(scheme.Channel.ToString("D")) || changed; + } + + if (parameter.Key == "sub_channel") + { + changed = parameter.Set(scheme.SubChannel.ToString("D")) || changed; + } + } + } + + if (changed) + { + using (FileStream writeStream = File.Create(configPath)) + { + IniSerializer.Serialize(writeStream, elements); + } + } + + return changed; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/IGameChannelOptionsService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/IGameChannelOptionsService.cs new file mode 100644 index 00000000..a07fbc9a --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/IGameChannelOptionsService.cs @@ -0,0 +1,13 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Service.Game.Scheme; + +namespace Snap.Hutao.Service.Game.Configuration; + +internal interface IGameChannelOptionsService +{ + ChannelOptions GetChannelOptions(); + + bool SetChannelOptions(LaunchScheme scheme); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/IgnoredInvalidChannelOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/IgnoredInvalidChannelOptions.cs similarity index 91% rename from src/Snap.Hutao/Snap.Hutao/Service/Game/IgnoredInvalidChannelOptions.cs rename to src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/IgnoredInvalidChannelOptions.cs index eac6d975..1d0e6266 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/IgnoredInvalidChannelOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/IgnoredInvalidChannelOptions.cs @@ -4,7 +4,7 @@ using Snap.Hutao.Model.Intrinsic; using System.Collections.Immutable; -namespace Snap.Hutao.Service.Game; +namespace Snap.Hutao.Service.Game.Configuration; internal static class IgnoredInvalidChannelOptions { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/GamePathService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/GamePathService.cs new file mode 100644 index 00000000..7f773c77 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/GamePathService.cs @@ -0,0 +1,60 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Service.Game.Locator; + +namespace Snap.Hutao.Service.Game; + +[ConstructorGenerated] +[Injection(InjectAs.Singleton, typeof(IGamePathService))] +internal sealed partial class GamePathService : IGamePathService +{ + private readonly IServiceProvider serviceProvider; + private readonly AppOptions appOptions; + + public async ValueTask> SilentGetGamePathAsync() + { + // Cannot find in setting + if (string.IsNullOrEmpty(appOptions.GamePath)) + { + IGameLocatorFactory locatorFactory = serviceProvider.GetRequiredService(); + + bool isOk; + string path; + + // Try locate by unity log + (isOk, path) = await locatorFactory + .Create(GameLocationSource.UnityLog) + .LocateGamePathAsync() + .ConfigureAwait(false); + + if (!isOk) + { + // Try locate by registry + (isOk, path) = await locatorFactory + .Create(GameLocationSource.Registry) + .LocateGamePathAsync() + .ConfigureAwait(false); + } + + if (isOk) + { + // Save result. + appOptions.GamePath = path; + } + else + { + return new(false, SH.ServiceGamePathLocateFailed); + } + } + + if (!string.IsNullOrEmpty(appOptions.GamePath)) + { + return new(true, appOptions.GamePath); + } + else + { + return new(false, default!); + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameProcessService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameProcessService.cs new file mode 100644 index 00000000..dc3e07ec --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameProcessService.cs @@ -0,0 +1,128 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core; +using Snap.Hutao.Service.Game.Unlocker; +using System.Diagnostics; +using System.IO; +using static Snap.Hutao.Service.Game.GameConstants; + +namespace Snap.Hutao.Service.Game; + +/// +/// 进程互操作 +/// +[ConstructorGenerated] +[Injection(InjectAs.Singleton, typeof(IGameProcessService))] +internal sealed partial class GameProcessService : IGameProcessService +{ + private readonly IServiceProvider serviceProvider; + private readonly RuntimeOptions runtimeOptions; + private readonly LaunchOptions launchOptions; + private readonly AppOptions appOptions; + + private volatile int runningGamesCounter; + + public bool IsGameRunning() + { + if (runningGamesCounter == 0) + { + return false; + } + + return Process.GetProcessesByName(YuanShenProcessName).Any() + || Process.GetProcessesByName(GenshinImpactProcessName).Any(); + } + + public async ValueTask LaunchAsync(IProgress progress) + { + if (IsGameRunning()) + { + return; + } + + string gamePath = appOptions.GamePath; + ArgumentException.ThrowIfNullOrEmpty(gamePath); + + progress.Report(new(LaunchPhase.ProcessInitializing, SH.ServiceGameLaunchPhaseProcessInitializing)); + using (Process game = InitializeGameProcess(launchOptions, gamePath)) + { + try + { + Interlocked.Increment(ref runningGamesCounter); + game.Start(); + progress.Report(new(LaunchPhase.ProcessStarted, SH.ServiceGameLaunchPhaseProcessStarted)); + + if (runtimeOptions.IsElevated && appOptions.IsAdvancedLaunchOptionsEnabled && launchOptions.UnlockFps) + { + progress.Report(new(LaunchPhase.UnlockingFps, SH.ServiceGameLaunchPhaseUnlockingFps)); + try + { + await UnlockFpsAsync(serviceProvider, game, progress).ConfigureAwait(false); + } + catch (InvalidOperationException) + { + // The Unlocker can't unlock the process + game.Kill(); + throw; + } + finally + { + progress.Report(new(LaunchPhase.ProcessExited, SH.ServiceGameLaunchPhaseProcessExited)); + } + } + else + { + progress.Report(new(LaunchPhase.WaitingForExit, SH.ServiceGameLaunchPhaseWaitingProcessExit)); + await game.WaitForExitAsync().ConfigureAwait(false); + progress.Report(new(LaunchPhase.ProcessExited, SH.ServiceGameLaunchPhaseProcessExited)); + } + } + finally + { + Interlocked.Decrement(ref runningGamesCounter); + } + } + } + + private static Process InitializeGameProcess(LaunchOptions options, string gamePath) + { + string commandLine = string.Empty; + + if (options.IsEnabled) + { + Must.Argument(!(options.IsBorderless && options.IsExclusive), "无边框与独占全屏选项无法同时生效"); + + // https://docs.unity.cn/cn/current/Manual/PlayerCommandLineArguments.html + // https://docs.unity3d.com/2017.4/Documentation/Manual/CommandLineArguments.html + commandLine = new CommandLineBuilder() + .AppendIf("-popupwindow", options.IsBorderless) + .AppendIf("-window-mode", options.IsExclusive, "exclusive") + .Append("-screen-fullscreen", options.IsFullScreen ? 1 : 0) + .AppendIf("-screen-width", options.IsScreenWidthEnabled, options.ScreenWidth) + .AppendIf("-screen-height", options.IsScreenHeightEnabled, options.ScreenHeight) + .AppendIf("-monitor", options.IsMonitorEnabled, options.Monitor.Value) + .ToString(); + } + + return new() + { + StartInfo = new() + { + Arguments = commandLine, + FileName = gamePath, + UseShellExecute = true, + Verb = "runas", + WorkingDirectory = Path.GetDirectoryName(gamePath), + }, + }; + } + + private static ValueTask UnlockFpsAsync(IServiceProvider serviceProvider, Process game, IProgress progress, CancellationToken token = default) + { + IGameFpsUnlocker unlocker = serviceProvider.CreateInstance(game); + UnlockTimingOptions options = new(100, 20000, 3000); + Progress lockerProgress = new(unlockStatus => progress.Report(LaunchStatus.FromUnlockStatus(unlockStatus))); + return unlocker.UnlockAsync(options, lockerProgress, token); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs deleted file mode 100644 index 25ff0bcd..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs +++ /dev/null @@ -1,395 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -using Snap.Hutao.Core; -using Snap.Hutao.Core.ExceptionService; -using Snap.Hutao.Core.IO.Ini; -using Snap.Hutao.Factory.Abstraction; -using Snap.Hutao.Model.Entity; -using Snap.Hutao.Service.Game.Locator; -using Snap.Hutao.Service.Game.Package; -using Snap.Hutao.View.Dialog; -using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher; -using Snap.Hutao.Web.Response; -using System.Collections.ObjectModel; -using System.Diagnostics; -using System.IO; -using static Snap.Hutao.Service.Game.GameConstants; - -namespace Snap.Hutao.Service.Game; - -/// -/// 游戏服务 -/// -[HighQuality] -[ConstructorGenerated] -[Injection(InjectAs.Singleton, typeof(IGameService))] -internal sealed partial class GameService : IGameService -{ - private readonly IContentDialogFactory contentDialogFactory; - private readonly PackageConverter packageConverter; - private readonly IServiceProvider serviceProvider; - private readonly IGameDbService gameDbService; - private readonly LaunchOptions launchOptions; - private readonly RuntimeOptions runtimeOptions; - private readonly ITaskContext taskContext; - private readonly AppOptions appOptions; - - private volatile int runningGamesCounter; - private ObservableCollection? gameAccounts; - - /// - public ObservableCollection GameAccountCollection - { - get => gameAccounts ??= gameDbService.GetGameAccountCollection(); - } - - /// - public async ValueTask> GetGamePathAsync() - { - // Cannot find in setting - if (string.IsNullOrEmpty(appOptions.GamePath)) - { - IGameLocatorFactory locatorFactory = serviceProvider.GetRequiredService(); - - // Try locate by unity log - ValueResult result = await locatorFactory - .Create(GameLocationSource.UnityLog) - .LocateGamePathAsync() - .ConfigureAwait(false); - - if (!result.IsOk) - { - // Try locate by registry - result = await locatorFactory - .Create(GameLocationSource.Registry) - .LocateGamePathAsync() - .ConfigureAwait(false); - } - - if (result.IsOk) - { - // Save result. - appOptions.GamePath = result.Value; - } - else - { - return new(false, SH.ServiceGamePathLocateFailed); - } - } - - if (!string.IsNullOrEmpty(appOptions.GamePath)) - { - return new(true, appOptions.GamePath); - } - else - { - return new(false, default!); - } - } - - /// - public ChannelOptions GetChannelOptions() - { - string gamePath = appOptions.GamePath; - string configPath = Path.Combine(Path.GetDirectoryName(gamePath) ?? string.Empty, ConfigFileName); - bool isOversea = string.Equals(Path.GetFileName(gamePath), GenshinImpactFileName, StringComparison.OrdinalIgnoreCase); - - if (!File.Exists(configPath)) - { - return ChannelOptions.FileNotFound(isOversea, configPath); - } - - using (FileStream stream = File.OpenRead(configPath)) - { - List parameters = IniSerializer.Deserialize(stream).OfType().ToList(); - string? channel = parameters.FirstOrDefault(p => p.Key == ChannelOptions.ChannelName)?.Value; - string? subChannel = parameters.FirstOrDefault(p => p.Key == ChannelOptions.SubChannelName)?.Value; - - return new(channel, subChannel, isOversea); - } - } - - /// - public bool SetChannelOptions(LaunchScheme scheme) - { - string gamePath = appOptions.GamePath; - string? directory = Path.GetDirectoryName(gamePath); - ArgumentException.ThrowIfNullOrEmpty(directory); - string configPath = Path.Combine(directory, ConfigFileName); - - List elements = default!; - try - { - using (FileStream readStream = File.OpenRead(configPath)) - { - elements = IniSerializer.Deserialize(readStream).ToList(); - } - } - catch (FileNotFoundException ex) - { - ThrowHelper.GameFileOperation(SH.ServiceGameSetMultiChannelConfigFileNotFound.Format(configPath), ex); - } - catch (DirectoryNotFoundException ex) - { - ThrowHelper.GameFileOperation(SH.ServiceGameSetMultiChannelConfigFileNotFound.Format(configPath), ex); - } - catch (UnauthorizedAccessException ex) - { - ThrowHelper.GameFileOperation(SH.ServiceGameSetMultiChannelUnauthorizedAccess, ex); - } - - bool changed = false; - - foreach (IniElement element in elements) - { - if (element is IniParameter parameter) - { - if (parameter.Key == "channel") - { - changed = parameter.Set(scheme.Channel.ToString("D")) || changed; - } - - if (parameter.Key == "sub_channel") - { - changed = parameter.Set(scheme.SubChannel.ToString("D")) || changed; - } - } - } - - if (changed) - { - using (FileStream writeStream = File.Create(configPath)) - { - IniSerializer.Serialize(writeStream, elements); - } - } - - return changed; - } - - /// - public async ValueTask EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress progress) - { - string gamePath = appOptions.GamePath; - string? gameFolder = Path.GetDirectoryName(gamePath); - ArgumentException.ThrowIfNullOrEmpty(gameFolder); - string gameFileName = Path.GetFileName(gamePath); - - progress.Report(new(SH.ServiceGameEnsureGameResourceQueryResourceInformation)); - Response response = await serviceProvider - .GetRequiredService() - .GetResourceAsync(launchScheme) - .ConfigureAwait(false); - - if (response.IsOk()) - { - GameResource resource = response.Data; - - if (!launchScheme.ExecutableMatches(gameFileName)) - { - bool replaced = await packageConverter - .EnsureGameResourceAsync(launchScheme, resource, gameFolder, progress) - .ConfigureAwait(false); - - if (replaced) - { - // We need to change the gamePath if we switched. - string exeName = launchScheme.IsOversea ? GenshinImpactFileName : YuanShenFileName; - - await taskContext.SwitchToMainThreadAsync(); - appOptions.GamePath = Path.Combine(gameFolder, exeName); - } - else - { - // We can't start the game - // when we failed to convert game - return false; - } - } - - await packageConverter.EnsureDeprecatedFilesAndSdkAsync(resource, gameFolder).ConfigureAwait(false); - - return true; - } - - return false; - } - - /// - public bool IsGameRunning() - { - if (runningGamesCounter == 0) - { - return false; - } - - return Process.GetProcessesByName(YuanShenProcessName).Any() - || Process.GetProcessesByName(GenshinImpactProcessName).Any(); - } - - /// - public async ValueTask LaunchAsync(IProgress progress) - { - if (IsGameRunning()) - { - return; - } - - string gamePath = appOptions.GamePath; - ArgumentException.ThrowIfNullOrEmpty(gamePath); - - progress.Report(new(LaunchPhase.ProcessInitializing, SH.ServiceGameLaunchPhaseProcessInitializing)); - using (Process game = ProcessInterop.InitializeGameProcess(launchOptions, gamePath)) - { - try - { - Interlocked.Increment(ref runningGamesCounter); - game.Start(); - progress.Report(new(LaunchPhase.ProcessStarted, SH.ServiceGameLaunchPhaseProcessStarted)); - - if (runtimeOptions.IsElevated && appOptions.IsAdvancedLaunchOptionsEnabled && launchOptions.UnlockFps) - { - progress.Report(new(LaunchPhase.UnlockingFps, SH.ServiceGameLaunchPhaseUnlockingFps)); - try - { - await ProcessInterop.UnlockFpsAsync(serviceProvider, game, progress).ConfigureAwait(false); - } - catch (InvalidOperationException) - { - // The Unlocker can't unlock the process - game.Kill(); - throw; - } - finally - { - progress.Report(new(LaunchPhase.ProcessExited, SH.ServiceGameLaunchPhaseProcessExited)); - } - } - else - { - progress.Report(new(LaunchPhase.WaitingForExit, SH.ServiceGameLaunchPhaseWaitingProcessExit)); - await game.WaitForExitAsync().ConfigureAwait(false); - progress.Report(new(LaunchPhase.ProcessExited, SH.ServiceGameLaunchPhaseProcessExited)); - } - } - finally - { - Interlocked.Decrement(ref runningGamesCounter); - } - } - } - - /// - public async ValueTask DetectGameAccountAsync() - { - ArgumentNullException.ThrowIfNull(gameAccounts); - - string? registrySdk = RegistryInterop.Get(); - if (!string.IsNullOrEmpty(registrySdk)) - { - GameAccount? account = null; - try - { - account = gameAccounts.SingleOrDefault(a => a.MihoyoSDK == registrySdk); - } - catch (InvalidOperationException ex) - { - ThrowHelper.UserdataCorrupted(SH.ServiceGameDetectGameAccountMultiMatched, ex); - } - - if (account is null) - { - // ContentDialog must be created by main thread. - await taskContext.SwitchToMainThreadAsync(); - LaunchGameAccountNameDialog dialog = await contentDialogFactory.CreateInstanceAsync().ConfigureAwait(false); - (bool isOk, string name) = await dialog.GetInputNameAsync().ConfigureAwait(false); - - if (isOk) - { - account = GameAccount.From(name, registrySdk); - - // sync database - await taskContext.SwitchToBackgroundAsync(); - await gameDbService.AddGameAccountAsync(account).ConfigureAwait(false); - - // sync cache - await taskContext.SwitchToMainThreadAsync(); - gameAccounts.Add(account); - } - } - - return account; - } - - return default; - } - - /// - public GameAccount? DetectCurrentGameAccount() - { - ArgumentNullException.ThrowIfNull(gameAccounts); - - string? registrySdk = RegistryInterop.Get(); - - if (!string.IsNullOrEmpty(registrySdk)) - { - try - { - return gameAccounts.SingleOrDefault(a => a.MihoyoSDK == registrySdk); - } - catch (InvalidOperationException ex) - { - ThrowHelper.UserdataCorrupted(SH.ServiceGameDetectGameAccountMultiMatched, ex); - } - } - - return null; - } - - /// - public bool SetGameAccount(GameAccount account) - { - if (string.IsNullOrEmpty(appOptions.PowerShellPath)) - { - ThrowHelper.RuntimeEnvironment(SH.ServiceGameRegisteryInteropPowershellNotFound, default!); - } - - return RegistryInterop.Set(account, appOptions.PowerShellPath); - } - - /// - public void AttachGameAccountToUid(GameAccount gameAccount, string uid) - { - gameAccount.UpdateAttachUid(uid); - gameDbService.UpdateGameAccount(gameAccount); - } - - /// - public async ValueTask ModifyGameAccountAsync(GameAccount gameAccount) - { - await taskContext.SwitchToMainThreadAsync(); - LaunchGameAccountNameDialog dialog = serviceProvider.CreateInstance(); - (bool isOk, string name) = await dialog.GetInputNameAsync().ConfigureAwait(true); - - if (isOk) - { - gameAccount.UpdateName(name); - - // sync database - await taskContext.SwitchToBackgroundAsync(); - await gameDbService.UpdateGameAccountAsync(gameAccount).ConfigureAwait(false); - } - } - - /// - public async ValueTask RemoveGameAccountAsync(GameAccount gameAccount) - { - await taskContext.SwitchToMainThreadAsync(); - ArgumentNullException.ThrowIfNull(gameAccounts); - gameAccounts.Remove(gameAccount); - - await taskContext.SwitchToBackgroundAsync(); - await gameDbService.RemoveGameAccountByIdAsync(gameAccount.InnerId).ConfigureAwait(false); - } -} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacade.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacade.cs new file mode 100644 index 00000000..0de8977b --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacade.cs @@ -0,0 +1,104 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Entity; +using Snap.Hutao.Service.Game.Account; +using Snap.Hutao.Service.Game.Configuration; +using Snap.Hutao.Service.Game.Package; +using Snap.Hutao.Service.Game.Scheme; +using System.Collections.ObjectModel; + +namespace Snap.Hutao.Service.Game; + +/// +/// 游戏服务 +/// +[HighQuality] +[ConstructorGenerated] +[Injection(InjectAs.Singleton, typeof(IGameServiceFacade))] +internal sealed partial class GameServiceFacade : IGameServiceFacade +{ + private readonly IGameChannelOptionsService gameChannelOptionsService; + private readonly IGameAccountService gameAccountService; + private readonly IGameProcessService gameProcessService; + private readonly IGamePackageService gamePackageService; + private readonly IGamePathService gamePathService; + + /// + public ObservableCollection GameAccountCollection + { + get => gameAccountService.GameAccountCollection; + } + + /// + public ValueTask> GetGamePathAsync() + { + return gamePathService.SilentGetGamePathAsync(); + } + + /// + public ChannelOptions GetChannelOptions() + { + return gameChannelOptionsService.GetChannelOptions(); + } + + /// + public bool SetChannelOptions(LaunchScheme scheme) + { + return gameChannelOptionsService.SetChannelOptions(scheme); + } + + /// + public ValueTask DetectGameAccountAsync() + { + return gameAccountService.DetectGameAccountAsync(); + } + + /// + public GameAccount? DetectCurrentGameAccount() + { + return gameAccountService.DetectCurrentGameAccount(); + } + + /// + public bool SetGameAccount(GameAccount account) + { + return gameAccountService.SetGameAccount(account); + } + + /// + public void AttachGameAccountToUid(GameAccount gameAccount, string uid) + { + gameAccountService.AttachGameAccountToUid(gameAccount, uid); + } + + /// + public ValueTask ModifyGameAccountAsync(GameAccount gameAccount) + { + return gameAccountService.ModifyGameAccountAsync(gameAccount); + } + + /// + public ValueTask RemoveGameAccountAsync(GameAccount gameAccount) + { + return gameAccountService.RemoveGameAccountAsync(gameAccount); + } + + /// + public bool IsGameRunning() + { + return gameProcessService.IsGameRunning(); + } + + /// + public ValueTask LaunchAsync(IProgress progress) + { + return gameProcessService.LaunchAsync(progress); + } + + /// + public ValueTask EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress progress) + { + return gamePackageService.EnsureGameResourceAsync(launchScheme, progress); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/IGamePathService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/IGamePathService.cs new file mode 100644 index 00000000..c0b09ccc --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/IGamePathService.cs @@ -0,0 +1,9 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.Game; + +internal interface IGamePathService +{ + ValueTask> SilentGetGamePathAsync(); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameProcessService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameProcessService.cs new file mode 100644 index 00000000..d2c08c07 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameProcessService.cs @@ -0,0 +1,11 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.Game; + +internal interface IGameProcessService +{ + bool IsGameRunning(); + + ValueTask LaunchAsync(IProgress progress); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameServiceFacade.cs similarity index 94% rename from src/Snap.Hutao/Snap.Hutao/Service/Game/IGameService.cs rename to src/Snap.Hutao/Snap.Hutao/Service/Game/IGameServiceFacade.cs index 19ad9808..89528ef7 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameServiceFacade.cs @@ -2,7 +2,10 @@ // Licensed under the MIT license. using Snap.Hutao.Model.Entity; +using Snap.Hutao.Service.Game.Account; +using Snap.Hutao.Service.Game.Configuration; using Snap.Hutao.Service.Game.Package; +using Snap.Hutao.Service.Game.Scheme; using System.Collections.ObjectModel; namespace Snap.Hutao.Service.Game; @@ -11,7 +14,7 @@ namespace Snap.Hutao.Service.Game; /// 游戏服务 /// [HighQuality] -internal interface IGameService +internal interface IGameServiceFacade { /// /// 游戏内账号集合 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchStatus.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchStatus.cs index 62a6ea3b..df7e3c1c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchStatus.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchStatus.cs @@ -1,6 +1,8 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Service.Game.Unlocker; + namespace Snap.Hutao.Service.Game; internal sealed class LaunchStatus @@ -14,4 +16,16 @@ internal sealed class LaunchStatus public LaunchPhase Phase { get; set; } public string Description { get; set; } -} + + public static LaunchStatus FromUnlockStatus(UnlockerStatus unlockerStatus) + { + if (unlockerStatus.FindModuleState == FindModuleResult.Ok) + { + return new(LaunchPhase.UnlockFpsSucceed, SH.ServiceGameLaunchPhaseUnlockFpsSucceed); + } + else + { + return new(LaunchPhase.UnlockFpsFailed, SH.ServiceGameLaunchPhaseUnlockFpsFailed); + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/GamePackageService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/GamePackageService.cs new file mode 100644 index 00000000..831e1c5c --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/GamePackageService.cs @@ -0,0 +1,67 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Service.Game.Scheme; +using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher; +using Snap.Hutao.Web.Response; +using System.IO; +using static Snap.Hutao.Service.Game.GameConstants; + +namespace Snap.Hutao.Service.Game.Package; + +[ConstructorGenerated] +[Injection(InjectAs.Singleton, typeof(IGamePackageService))] +internal sealed partial class GamePackageService : IGamePackageService +{ + private readonly PackageConverter packageConverter; + private readonly IServiceProvider serviceProvider; + private readonly ITaskContext taskContext; + private readonly AppOptions appOptions; + + public async ValueTask EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress progress) + { + string gamePath = appOptions.GamePath; + string? gameFolder = Path.GetDirectoryName(gamePath); + ArgumentException.ThrowIfNullOrEmpty(gameFolder); + string gameFileName = Path.GetFileName(gamePath); + + progress.Report(new(SH.ServiceGameEnsureGameResourceQueryResourceInformation)); + Response response = await serviceProvider + .GetRequiredService() + .GetResourceAsync(launchScheme) + .ConfigureAwait(false); + + if (response.IsOk()) + { + GameResource resource = response.Data; + + if (!launchScheme.ExecutableMatches(gameFileName)) + { + bool replaced = await packageConverter + .EnsureGameResourceAsync(launchScheme, resource, gameFolder, progress) + .ConfigureAwait(false); + + if (replaced) + { + // We need to change the gamePath if we switched. + string exeName = launchScheme.IsOversea ? GenshinImpactFileName : YuanShenFileName; + + await taskContext.SwitchToMainThreadAsync(); + appOptions.GamePath = Path.Combine(gameFolder, exeName); + } + else + { + // We can't start the game + // when we failed to convert game + return false; + } + } + + await packageConverter.EnsureDeprecatedFilesAndSdkAsync(resource, gameFolder).ConfigureAwait(false); + + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/IGamePackageService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/IGamePackageService.cs new file mode 100644 index 00000000..ffb3d54b --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/IGamePackageService.cs @@ -0,0 +1,11 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Service.Game.Scheme; + +namespace Snap.Hutao.Service.Game.Package; + +internal interface IGamePackageService +{ + ValueTask EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress progress); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs index a01a8f22..3e39bfd9 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs @@ -6,6 +6,7 @@ using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; using Snap.Hutao.Core.ExceptionService; using Snap.Hutao.Core.IO; using Snap.Hutao.Core.IO.Hashing; +using Snap.Hutao.Service.Game.Scheme; using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher; using System.IO; using System.IO.Compression; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/ProcessInterop.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/ProcessInterop.cs deleted file mode 100644 index 3b3dc973..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/ProcessInterop.cs +++ /dev/null @@ -1,162 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -using Snap.Hutao.Core; -using Snap.Hutao.Service.Game.Unlocker; -using System.Diagnostics; -using System.IO; -using System.Runtime.InteropServices; -using Windows.Win32.Foundation; -using Windows.Win32.System.Memory; -using Windows.Win32.System.Threading; -using static Windows.Win32.PInvoke; - -namespace Snap.Hutao.Service.Game; - -/// -/// 进程互操作 -/// -internal static class ProcessInterop -{ - /// - /// 获取初始化后的游戏进程 - /// - /// 启动选项 - /// 游戏路径 - /// 初始化后的游戏进程 - public static Process InitializeGameProcess(LaunchOptions options, string gamePath) - { - string commandLine = string.Empty; - - if (options.IsEnabled) - { - Must.Argument(!(options.IsBorderless && options.IsExclusive), "无边框与独占全屏选项无法同时生效"); - - // https://docs.unity.cn/cn/current/Manual/PlayerCommandLineArguments.html - // https://docs.unity3d.com/2017.4/Documentation/Manual/CommandLineArguments.html - commandLine = new CommandLineBuilder() - .AppendIf("-popupwindow", options.IsBorderless) - .AppendIf("-window-mode", options.IsExclusive, "exclusive") - .Append("-screen-fullscreen", options.IsFullScreen ? 1 : 0) - .AppendIf("-screen-width", options.IsScreenWidthEnabled, options.ScreenWidth) - .AppendIf("-screen-height", options.IsScreenHeightEnabled, options.ScreenHeight) - .AppendIf("-monitor", options.IsMonitorEnabled, options.Monitor.Value) - .ToString(); - } - - return new() - { - StartInfo = new() - { - Arguments = commandLine, - FileName = gamePath, - UseShellExecute = true, - Verb = "runas", - WorkingDirectory = Path.GetDirectoryName(gamePath), - }, - }; - } - - public static ValueTask UnlockFpsAsync(IServiceProvider serviceProvider, Process game, IProgress progress, CancellationToken token = default) - { - IGameFpsUnlocker unlocker = serviceProvider.CreateInstance(game); - UnlockTimingOptions options = new(100, 20000, 3000); - Progress lockerProgress = new(unlockStatus => progress.Report(FromUnlockStatus(unlockStatus))); - return unlocker.UnlockAsync(options, lockerProgress, token); - } - - /// - /// 尝试禁用mhypbase - /// - /// 游戏进程 - /// 游戏路径 - /// 是否禁用成功 - public static bool DisableProtection(Process game, string gamePath) - { - string? gameFolder = Path.GetDirectoryName(gamePath); - string mhypbaseDll = Path.Combine(gameFolder ?? string.Empty, "mhypbase.dll"); - - if (File.Exists(mhypbaseDll)) - { - using (File.OpenHandle(mhypbaseDll, share: FileShare.None)) - { - SpinWait.SpinUntil(() => game.MainWindowHandle != 0); - return true; - } - } - - return false; - } - - /// - /// 加载并注入指定路径的库 - /// - /// 进程句柄 - /// 库的路径,不包含'\0' - public static unsafe void LoadLibraryAndInject(in HANDLE hProcess, in ReadOnlySpan libraryPathu8) - { - HINSTANCE hKernelDll = GetModuleHandle("kernel32.dll"); - Marshal.ThrowExceptionForHR(Marshal.GetLastPInvokeError()); - - FARPROC pLoadLibraryA = GetProcAddress(hKernelDll, "LoadLibraryA"u8); - Marshal.ThrowExceptionForHR(Marshal.GetLastPInvokeError()); - - void* pNativeLibraryPath = default; - try - { - VIRTUAL_ALLOCATION_TYPE allocType = VIRTUAL_ALLOCATION_TYPE.MEM_RESERVE | VIRTUAL_ALLOCATION_TYPE.MEM_COMMIT; - pNativeLibraryPath = VirtualAllocEx(hProcess, default, unchecked((uint)libraryPathu8.Length + 1), allocType, PAGE_PROTECTION_FLAGS.PAGE_READWRITE); - Marshal.ThrowExceptionForHR(Marshal.GetLastPInvokeError()); - - WriteProcessMemory(hProcess, pNativeLibraryPath, libraryPathu8); - Marshal.ThrowExceptionForHR(Marshal.GetLastPInvokeError()); - - LPTHREAD_START_ROUTINE lpThreadLoadLibraryA = pLoadLibraryA.CreateDelegate(); - HANDLE hLoadLibraryAThread = default; - try - { - hLoadLibraryAThread = CreateRemoteThread(hProcess, default, 0, lpThreadLoadLibraryA, pNativeLibraryPath, 0); - Marshal.ThrowExceptionForHR(Marshal.GetLastPInvokeError()); - - WaitForSingleObject(hLoadLibraryAThread, 2000); - Marshal.ThrowExceptionForHR(Marshal.GetLastPInvokeError()); - } - finally - { - CloseHandle(hLoadLibraryAThread); - } - } - finally - { - VirtualFreeEx(hProcess, pNativeLibraryPath, 0, VIRTUAL_FREE_TYPE.MEM_RELEASE); - } - } - - private static unsafe FARPROC GetProcAddress(in HINSTANCE hModule, in ReadOnlySpan lpProcName) - { - fixed (byte* lpProcNameLocal = lpProcName) - { - return Windows.Win32.PInvoke.GetProcAddress(hModule, new PCSTR(lpProcNameLocal)); - } - } - - private static unsafe BOOL WriteProcessMemory(in HANDLE hProcess, void* lpBaseAddress, in ReadOnlySpan buffer) - { - fixed (void* lpBuffer = buffer) - { - return Windows.Win32.PInvoke.WriteProcessMemory(hProcess, lpBaseAddress, lpBuffer, unchecked((uint)buffer.Length)); - } - } - - private static LaunchStatus FromUnlockStatus(UnlockerStatus unlockerStatus) - { - if (unlockerStatus.FindModuleState == FindModuleResult.Ok) - { - return new(LaunchPhase.UnlockFpsSucceed, SH.ServiceGameLaunchPhaseUnlockFpsSucceed); - } - else - { - return new(LaunchPhase.UnlockFpsFailed, SH.ServiceGameLaunchPhaseUnlockFpsFailed); - } - } -} diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/KnownLaunchSchemes.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Scheme/KnownLaunchSchemes.cs similarity index 98% rename from src/Snap.Hutao/Snap.Hutao/Service/Game/KnownLaunchSchemes.cs rename to src/Snap.Hutao/Snap.Hutao/Service/Game/Scheme/KnownLaunchSchemes.cs index 16fac365..c1373917 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/KnownLaunchSchemes.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Scheme/KnownLaunchSchemes.cs @@ -3,7 +3,7 @@ using Snap.Hutao.Model.Intrinsic; -namespace Snap.Hutao.Service.Game; +namespace Snap.Hutao.Service.Game.Scheme; internal static class KnownLaunchSchemes { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchScheme.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Scheme/LaunchScheme.cs similarity index 94% rename from src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchScheme.cs rename to src/Snap.Hutao/Snap.Hutao/Service/Game/Scheme/LaunchScheme.cs index 5aeede35..69e5698a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchScheme.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Scheme/LaunchScheme.cs @@ -2,14 +2,15 @@ // Licensed under the MIT license. using Snap.Hutao.Model.Intrinsic; +using Snap.Hutao.Service.Game.Configuration; -namespace Snap.Hutao.Service.Game; +namespace Snap.Hutao.Service.Game.Scheme; /// /// 启动方案 /// [HighQuality] -internal partial class LaunchScheme +internal class LaunchScheme { /// /// 显示名称 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchSchemeBilibili.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Scheme/LaunchSchemeBilibili.cs similarity index 93% rename from src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchSchemeBilibili.cs rename to src/Snap.Hutao/Snap.Hutao/Service/Game/Scheme/LaunchSchemeBilibili.cs index cd5057be..1d96e21d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchSchemeBilibili.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Scheme/LaunchSchemeBilibili.cs @@ -3,7 +3,7 @@ using Snap.Hutao.Model.Intrinsic; -namespace Snap.Hutao.Service.Game; +namespace Snap.Hutao.Service.Game.Scheme; internal sealed class LaunchSchemeBilibili : LaunchScheme { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchSchemeChinese.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Scheme/LaunchSchemeChinese.cs similarity index 93% rename from src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchSchemeChinese.cs rename to src/Snap.Hutao/Snap.Hutao/Service/Game/Scheme/LaunchSchemeChinese.cs index 1e8f0e9f..e9d27c6d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchSchemeChinese.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Scheme/LaunchSchemeChinese.cs @@ -3,7 +3,7 @@ using Snap.Hutao.Model.Intrinsic; -namespace Snap.Hutao.Service.Game; +namespace Snap.Hutao.Service.Game.Scheme; internal sealed class LaunchSchemeChinese : LaunchScheme { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchSchemeOversea.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Scheme/LaunchSchemeOversea.cs similarity index 93% rename from src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchSchemeOversea.cs rename to src/Snap.Hutao/Snap.Hutao/Service/Game/Scheme/LaunchSchemeOversea.cs index 2838d82e..08b6799a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchSchemeOversea.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Scheme/LaunchSchemeOversea.cs @@ -3,7 +3,7 @@ using Snap.Hutao.Model.Intrinsic; -namespace Snap.Hutao.Service.Game; +namespace Snap.Hutao.Service.Game.Scheme; internal sealed class LaunchSchemeOversea : LaunchScheme { diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/GachaLogPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/GachaLogPage.xaml index 0f12787f..4b720db2 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/GachaLogPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/GachaLogPage.xaml @@ -382,7 +382,7 @@ Style="{StaticResource BaseTextBlockStyle}" Text="{shcm:ResourceString Name=ViewControlStatisticsCardOrangeText}"/> @@ -391,7 +391,7 @@ Style="{StaticResource BaseTextBlockStyle}" Text="{shcm:ResourceString Name=ViewControlStatisticsCardPurpleText}"/> @@ -405,7 +405,7 @@ Style="{StaticResource BaseTextBlockStyle}" Text="{shcm:ResourceString Name=ViewControlStatisticsCardOrangeText}"/> @@ -414,7 +414,7 @@ Style="{StaticResource BaseTextBlockStyle}" Text="{shcm:ResourceString Name=ViewControlStatisticsCardPurpleText}"/> @@ -423,7 +423,7 @@ Style="{StaticResource BaseTextBlockStyle}" Text="{shcm:ResourceString Name=ViewControlStatisticsCardBlueText}"/> diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs index 24dc9524..6b5704c2 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs @@ -9,7 +9,9 @@ using Snap.Hutao.Factory.Abstraction; using Snap.Hutao.Model.Entity; using Snap.Hutao.Service; using Snap.Hutao.Service.Game; +using Snap.Hutao.Service.Game.Configuration; using Snap.Hutao.Service.Game.Package; +using Snap.Hutao.Service.Game.Scheme; using Snap.Hutao.Service.Navigation; using Snap.Hutao.Service.Notification; using Snap.Hutao.Service.User; @@ -42,7 +44,7 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel private readonly RuntimeOptions hutaoOptions; private readonly IUserService userService; private readonly ITaskContext taskContext; - private readonly IGameService gameService; + private readonly IGameServiceFacade gameService; private readonly IMemoryCache memoryCache; private readonly AppOptions appOptions; diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModelSlim.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModelSlim.cs index c04150b2..aba0fd04 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModelSlim.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModelSlim.cs @@ -16,7 +16,7 @@ namespace Snap.Hutao.ViewModel.Game; [ConstructorGenerated(CallBaseConstructor = true)] internal sealed partial class LaunchGameViewModelSlim : Abstraction.ViewModelSlim { - private readonly IGameService gameService; + private readonly IGameServiceFacade gameService; private readonly ITaskContext taskContext; private readonly IInfoBarService infoBarService; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs b/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs index 4247f148..2f1f87c4 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. using Snap.Hutao.Model.Primitive; -using Snap.Hutao.Service.Game; +using Snap.Hutao.Service.Game.Scheme; using Snap.Hutao.Web.Hoyolab; namespace Snap.Hutao.Web; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/ApiOsEndpoints.cs b/src/Snap.Hutao/Snap.Hutao/Web/ApiOsEndpoints.cs index 159bb4f4..676fb754 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/ApiOsEndpoints.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/ApiOsEndpoints.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. using Snap.Hutao.Model.Primitive; -using Snap.Hutao.Service.Game; +using Snap.Hutao.Service.Game.Scheme; using Snap.Hutao.Web.Hoyolab; namespace Snap.Hutao.Web; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/ResourceClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/ResourceClient.cs index c7a319e5..738d0d08 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/ResourceClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/ResourceClient.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; -using Snap.Hutao.Service.Game; +using Snap.Hutao.Service.Game.Scheme; using Snap.Hutao.Web.Request.Builder; using Snap.Hutao.Web.Request.Builder.Abstraction; using Snap.Hutao.Web.Response;