mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
introducing game service facade
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -15,22 +15,7 @@ internal abstract class ValueConverter<TFrom, TTo> : IValueConverter
|
||||
/// <inheritdoc/>
|
||||
public object? Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
#if DEBUG
|
||||
try
|
||||
{
|
||||
return Convert((TFrom)value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Ioc.Default
|
||||
.GetRequiredService<ILogger<ValueConverter<TFrom, TTo>>>()
|
||||
.LogError(ex, "值转换器异常");
|
||||
|
||||
throw;
|
||||
}
|
||||
#else
|
||||
return Convert((TFrom)value);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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<GameAccount>? gameAccounts;
|
||||
|
||||
public ObservableCollection<GameAccount> GameAccountCollection
|
||||
{
|
||||
get => gameAccounts ??= gameDbService.GetGameAccountCollection();
|
||||
}
|
||||
|
||||
public async ValueTask<GameAccount?> 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<LaunchGameAccountNameDialog>().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<LaunchGameAccountNameDialog>();
|
||||
(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);
|
||||
}
|
||||
}
|
||||
@@ -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<GameAccount> GameAccountCollection { get; }
|
||||
|
||||
void AttachGameAccountToUid(GameAccount gameAccount, string uid);
|
||||
|
||||
GameAccount? DetectCurrentGameAccount();
|
||||
|
||||
ValueTask<GameAccount?> DetectGameAccountAsync();
|
||||
|
||||
ValueTask ModifyGameAccountAsync(GameAccount gameAccount);
|
||||
|
||||
ValueTask RemoveGameAccountAsync(GameAccount gameAccount);
|
||||
|
||||
bool SetGameAccount(GameAccount account);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 注册表操作
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
using Snap.Hutao.Model.Intrinsic;
|
||||
|
||||
namespace Snap.Hutao.Service.Game;
|
||||
namespace Snap.Hutao.Service.Game.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// 多通道
|
||||
@@ -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<IniParameter> parameters = IniSerializer.Deserialize(stream).OfType<IniParameter>().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<IniElement> 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
60
src/Snap.Hutao/Snap.Hutao/Service/Game/GamePathService.cs
Normal file
60
src/Snap.Hutao/Snap.Hutao/Service/Game/GamePathService.cs
Normal file
@@ -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<ValueResult<bool, string>> SilentGetGamePathAsync()
|
||||
{
|
||||
// Cannot find in setting
|
||||
if (string.IsNullOrEmpty(appOptions.GamePath))
|
||||
{
|
||||
IGameLocatorFactory locatorFactory = serviceProvider.GetRequiredService<IGameLocatorFactory>();
|
||||
|
||||
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!);
|
||||
}
|
||||
}
|
||||
}
|
||||
128
src/Snap.Hutao/Snap.Hutao/Service/Game/GameProcessService.cs
Normal file
128
src/Snap.Hutao/Snap.Hutao/Service/Game/GameProcessService.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 进程互操作
|
||||
/// </summary>
|
||||
[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<LaunchStatus> 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<LaunchStatus> progress, CancellationToken token = default)
|
||||
{
|
||||
IGameFpsUnlocker unlocker = serviceProvider.CreateInstance<GameFpsUnlocker>(game);
|
||||
UnlockTimingOptions options = new(100, 20000, 3000);
|
||||
Progress<UnlockerStatus> lockerProgress = new(unlockStatus => progress.Report(LaunchStatus.FromUnlockStatus(unlockStatus)));
|
||||
return unlocker.UnlockAsync(options, lockerProgress, token);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 游戏服务
|
||||
/// </summary>
|
||||
[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<GameAccount>? gameAccounts;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ObservableCollection<GameAccount> GameAccountCollection
|
||||
{
|
||||
get => gameAccounts ??= gameDbService.GetGameAccountCollection();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<ValueResult<bool, string>> GetGamePathAsync()
|
||||
{
|
||||
// Cannot find in setting
|
||||
if (string.IsNullOrEmpty(appOptions.GamePath))
|
||||
{
|
||||
IGameLocatorFactory locatorFactory = serviceProvider.GetRequiredService<IGameLocatorFactory>();
|
||||
|
||||
// Try locate by unity log
|
||||
ValueResult<bool, string> 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!);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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<IniParameter> parameters = IniSerializer.Deserialize(stream).OfType<IniParameter>().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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool SetChannelOptions(LaunchScheme scheme)
|
||||
{
|
||||
string gamePath = appOptions.GamePath;
|
||||
string? directory = Path.GetDirectoryName(gamePath);
|
||||
ArgumentException.ThrowIfNullOrEmpty(directory);
|
||||
string configPath = Path.Combine(directory, ConfigFileName);
|
||||
|
||||
List<IniElement> 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;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress)
|
||||
{
|
||||
string gamePath = appOptions.GamePath;
|
||||
string? gameFolder = Path.GetDirectoryName(gamePath);
|
||||
ArgumentException.ThrowIfNullOrEmpty(gameFolder);
|
||||
string gameFileName = Path.GetFileName(gamePath);
|
||||
|
||||
progress.Report(new(SH.ServiceGameEnsureGameResourceQueryResourceInformation));
|
||||
Response<GameResource> response = await serviceProvider
|
||||
.GetRequiredService<ResourceClient>()
|
||||
.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;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsGameRunning()
|
||||
{
|
||||
if (runningGamesCounter == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Process.GetProcessesByName(YuanShenProcessName).Any()
|
||||
|| Process.GetProcessesByName(GenshinImpactProcessName).Any();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask LaunchAsync(IProgress<LaunchStatus> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<GameAccount?> 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<LaunchGameAccountNameDialog>().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;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool SetGameAccount(GameAccount account)
|
||||
{
|
||||
if (string.IsNullOrEmpty(appOptions.PowerShellPath))
|
||||
{
|
||||
ThrowHelper.RuntimeEnvironment(SH.ServiceGameRegisteryInteropPowershellNotFound, default!);
|
||||
}
|
||||
|
||||
return RegistryInterop.Set(account, appOptions.PowerShellPath);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void AttachGameAccountToUid(GameAccount gameAccount, string uid)
|
||||
{
|
||||
gameAccount.UpdateAttachUid(uid);
|
||||
gameDbService.UpdateGameAccount(gameAccount);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask ModifyGameAccountAsync(GameAccount gameAccount)
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
LaunchGameAccountNameDialog dialog = serviceProvider.CreateInstance<LaunchGameAccountNameDialog>();
|
||||
(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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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);
|
||||
}
|
||||
}
|
||||
104
src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacade.cs
Normal file
104
src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacade.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 游戏服务
|
||||
/// </summary>
|
||||
[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;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ObservableCollection<GameAccount> GameAccountCollection
|
||||
{
|
||||
get => gameAccountService.GameAccountCollection;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<ValueResult<bool, string>> GetGamePathAsync()
|
||||
{
|
||||
return gamePathService.SilentGetGamePathAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ChannelOptions GetChannelOptions()
|
||||
{
|
||||
return gameChannelOptionsService.GetChannelOptions();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool SetChannelOptions(LaunchScheme scheme)
|
||||
{
|
||||
return gameChannelOptionsService.SetChannelOptions(scheme);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<GameAccount?> DetectGameAccountAsync()
|
||||
{
|
||||
return gameAccountService.DetectGameAccountAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public GameAccount? DetectCurrentGameAccount()
|
||||
{
|
||||
return gameAccountService.DetectCurrentGameAccount();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool SetGameAccount(GameAccount account)
|
||||
{
|
||||
return gameAccountService.SetGameAccount(account);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void AttachGameAccountToUid(GameAccount gameAccount, string uid)
|
||||
{
|
||||
gameAccountService.AttachGameAccountToUid(gameAccount, uid);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask ModifyGameAccountAsync(GameAccount gameAccount)
|
||||
{
|
||||
return gameAccountService.ModifyGameAccountAsync(gameAccount);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask RemoveGameAccountAsync(GameAccount gameAccount)
|
||||
{
|
||||
return gameAccountService.RemoveGameAccountAsync(gameAccount);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsGameRunning()
|
||||
{
|
||||
return gameProcessService.IsGameRunning();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask LaunchAsync(IProgress<LaunchStatus> progress)
|
||||
{
|
||||
return gameProcessService.LaunchAsync(progress);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress)
|
||||
{
|
||||
return gamePackageService.EnsureGameResourceAsync(launchScheme, progress);
|
||||
}
|
||||
}
|
||||
@@ -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<ValueResult<bool, string>> SilentGetGamePathAsync();
|
||||
}
|
||||
@@ -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<LaunchStatus> progress);
|
||||
}
|
||||
@@ -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;
|
||||
/// 游戏服务
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal interface IGameService
|
||||
internal interface IGameServiceFacade
|
||||
{
|
||||
/// <summary>
|
||||
/// 游戏内账号集合
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress)
|
||||
{
|
||||
string gamePath = appOptions.GamePath;
|
||||
string? gameFolder = Path.GetDirectoryName(gamePath);
|
||||
ArgumentException.ThrowIfNullOrEmpty(gameFolder);
|
||||
string gameFileName = Path.GetFileName(gamePath);
|
||||
|
||||
progress.Report(new(SH.ServiceGameEnsureGameResourceQueryResourceInformation));
|
||||
Response<GameResource> response = await serviceProvider
|
||||
.GetRequiredService<ResourceClient>()
|
||||
.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;
|
||||
}
|
||||
}
|
||||
@@ -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<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 进程互操作
|
||||
/// </summary>
|
||||
internal static class ProcessInterop
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取初始化后的游戏进程
|
||||
/// </summary>
|
||||
/// <param name="options">启动选项</param>
|
||||
/// <param name="gamePath">游戏路径</param>
|
||||
/// <returns>初始化后的游戏进程</returns>
|
||||
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<LaunchStatus> progress, CancellationToken token = default)
|
||||
{
|
||||
IGameFpsUnlocker unlocker = serviceProvider.CreateInstance<GameFpsUnlocker>(game);
|
||||
UnlockTimingOptions options = new(100, 20000, 3000);
|
||||
Progress<UnlockerStatus> lockerProgress = new(unlockStatus => progress.Report(FromUnlockStatus(unlockStatus)));
|
||||
return unlocker.UnlockAsync(options, lockerProgress, token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试禁用mhypbase
|
||||
/// </summary>
|
||||
/// <param name="game">游戏进程</param>
|
||||
/// <param name="gamePath">游戏路径</param>
|
||||
/// <returns>是否禁用成功</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载并注入指定路径的库
|
||||
/// </summary>
|
||||
/// <param name="hProcess">进程句柄</param>
|
||||
/// <param name="libraryPathu8">库的路径,不包含'\0'</param>
|
||||
public static unsafe void LoadLibraryAndInject(in HANDLE hProcess, in ReadOnlySpan<byte> 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<LPTHREAD_START_ROUTINE>();
|
||||
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<byte> 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<byte> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
using Snap.Hutao.Model.Intrinsic;
|
||||
|
||||
namespace Snap.Hutao.Service.Game;
|
||||
namespace Snap.Hutao.Service.Game.Scheme;
|
||||
|
||||
internal static class KnownLaunchSchemes
|
||||
{
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 启动方案
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal partial class LaunchScheme
|
||||
internal class LaunchScheme
|
||||
{
|
||||
/// <summary>
|
||||
/// 显示名称
|
||||
@@ -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
|
||||
{
|
||||
@@ -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
|
||||
{
|
||||
@@ -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
|
||||
{
|
||||
@@ -382,7 +382,7 @@
|
||||
Style="{StaticResource BaseTextBlockStyle}"
|
||||
Text="{shcm:ResourceString Name=ViewControlStatisticsCardOrangeText}"/>
|
||||
<GridView
|
||||
ItemTemplate="{StaticResource HistoryWishItemTemplate}"
|
||||
ItemTemplate="{StaticResource HistoryWishGridTemplate}"
|
||||
ItemsSource="{Binding Statistics.OrangeAvatars}"
|
||||
SelectionMode="None"/>
|
||||
|
||||
@@ -391,7 +391,7 @@
|
||||
Style="{StaticResource BaseTextBlockStyle}"
|
||||
Text="{shcm:ResourceString Name=ViewControlStatisticsCardPurpleText}"/>
|
||||
<GridView
|
||||
ItemTemplate="{StaticResource HistoryWishItemTemplate}"
|
||||
ItemTemplate="{StaticResource HistoryWishGridTemplate}"
|
||||
ItemsSource="{Binding Statistics.PurpleAvatars}"
|
||||
SelectionMode="None"/>
|
||||
</StackPanel>
|
||||
@@ -405,7 +405,7 @@
|
||||
Style="{StaticResource BaseTextBlockStyle}"
|
||||
Text="{shcm:ResourceString Name=ViewControlStatisticsCardOrangeText}"/>
|
||||
<GridView
|
||||
ItemTemplate="{StaticResource HistoryWishItemTemplate}"
|
||||
ItemTemplate="{StaticResource HistoryWishGridTemplate}"
|
||||
ItemsSource="{Binding Statistics.OrangeWeapons}"
|
||||
SelectionMode="None"/>
|
||||
|
||||
@@ -414,7 +414,7 @@
|
||||
Style="{StaticResource BaseTextBlockStyle}"
|
||||
Text="{shcm:ResourceString Name=ViewControlStatisticsCardPurpleText}"/>
|
||||
<GridView
|
||||
ItemTemplate="{StaticResource HistoryWishItemTemplate}"
|
||||
ItemTemplate="{StaticResource HistoryWishGridTemplate}"
|
||||
ItemsSource="{Binding Statistics.PurpleWeapons}"
|
||||
SelectionMode="None"/>
|
||||
|
||||
@@ -423,7 +423,7 @@
|
||||
Style="{StaticResource BaseTextBlockStyle}"
|
||||
Text="{shcm:ResourceString Name=ViewControlStatisticsCardBlueText}"/>
|
||||
<GridView
|
||||
ItemTemplate="{StaticResource HistoryWishItemTemplate}"
|
||||
ItemTemplate="{StaticResource HistoryWishGridTemplate}"
|
||||
ItemsSource="{Binding Statistics.BlueWeapons}"
|
||||
SelectionMode="None"/>
|
||||
</StackPanel>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace Snap.Hutao.ViewModel.Game;
|
||||
[ConstructorGenerated(CallBaseConstructor = true)]
|
||||
internal sealed partial class LaunchGameViewModelSlim : Abstraction.ViewModelSlim<View.Page.LaunchGamePage>
|
||||
{
|
||||
private readonly IGameService gameService;
|
||||
private readonly IGameServiceFacade gameService;
|
||||
private readonly ITaskContext taskContext;
|
||||
private readonly IInfoBarService infoBarService;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user