This commit is contained in:
DismissedLight
2023-08-28 22:30:09 +08:00
committed by Lightczx
parent 0bdc5d6c54
commit cd3ce6d338
15 changed files with 202 additions and 40 deletions

View File

@@ -1536,6 +1536,69 @@ namespace Snap.Hutao.Resource.Localization {
} }
} }
/// <summary>
/// 查找类似 游戏进程已退出 的本地化字符串。
/// </summary>
internal static string ServiceGameLaunchPhaseProcessExited {
get {
return ResourceManager.GetString("ServiceGameLaunchPhaseProcessExited", resourceCulture);
}
}
/// <summary>
/// 查找类似 正在初始化游戏进程 的本地化字符串。
/// </summary>
internal static string ServiceGameLaunchPhaseProcessInitializing {
get {
return ResourceManager.GetString("ServiceGameLaunchPhaseProcessInitializing", resourceCulture);
}
}
/// <summary>
/// 查找类似 游戏进程已启动 的本地化字符串。
/// </summary>
internal static string ServiceGameLaunchPhaseProcessStarted {
get {
return ResourceManager.GetString("ServiceGameLaunchPhaseProcessStarted", resourceCulture);
}
}
/// <summary>
/// 查找类似 解锁帧率上限失败,正在结束游戏进程 的本地化字符串。
/// </summary>
internal static string ServiceGameLaunchPhaseUnlockFpsFailed {
get {
return ResourceManager.GetString("ServiceGameLaunchPhaseUnlockFpsFailed", resourceCulture);
}
}
/// <summary>
/// 查找类似 解锁帧率上限成功 的本地化字符串。
/// </summary>
internal static string ServiceGameLaunchPhaseUnlockFpsSucceed {
get {
return ResourceManager.GetString("ServiceGameLaunchPhaseUnlockFpsSucceed", resourceCulture);
}
}
/// <summary>
/// 查找类似 正在尝试解锁帧率上限 的本地化字符串。
/// </summary>
internal static string ServiceGameLaunchPhaseUnlockingFps {
get {
return ResourceManager.GetString("ServiceGameLaunchPhaseUnlockingFps", resourceCulture);
}
}
/// <summary>
/// 查找类似 等待游戏进程退出 的本地化字符串。
/// </summary>
internal static string ServiceGameLaunchPhaseWaitingProcessExit {
get {
return ResourceManager.GetString("ServiceGameLaunchPhaseWaitingProcessExit", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 选择游戏本体 的本地化字符串。 /// 查找类似 选择游戏本体 的本地化字符串。
/// </summary> /// </summary>

View File

@@ -665,6 +665,27 @@
<data name="ServiceGameFileOperationExceptionMessage" xml:space="preserve"> <data name="ServiceGameFileOperationExceptionMessage" xml:space="preserve">
<value>游戏文件操作失败:{0}</value> <value>游戏文件操作失败:{0}</value>
</data> </data>
<data name="ServiceGameLaunchPhaseProcessExited" xml:space="preserve">
<value>游戏进程已退出</value>
</data>
<data name="ServiceGameLaunchPhaseProcessInitializing" xml:space="preserve">
<value>正在初始化游戏进程</value>
</data>
<data name="ServiceGameLaunchPhaseProcessStarted" xml:space="preserve">
<value>游戏进程已启动</value>
</data>
<data name="ServiceGameLaunchPhaseUnlockFpsFailed" xml:space="preserve">
<value>解锁帧率上限失败,正在结束游戏进程</value>
</data>
<data name="ServiceGameLaunchPhaseUnlockFpsSucceed" xml:space="preserve">
<value>解锁帧率上限成功</value>
</data>
<data name="ServiceGameLaunchPhaseUnlockingFps" xml:space="preserve">
<value>正在尝试解锁帧率上限</value>
</data>
<data name="ServiceGameLaunchPhaseWaitingProcessExit" xml:space="preserve">
<value>等待游戏进程退出</value>
</data>
<data name="ServiceGameLocatorFileOpenPickerCommitText" xml:space="preserve"> <data name="ServiceGameLocatorFileOpenPickerCommitText" xml:space="preserve">
<value>选择游戏本体</value> <value>选择游戏本体</value>
</data> </data>

View File

@@ -22,8 +22,6 @@ namespace Snap.Hutao.Service.AvatarInfo.Factory;
[HighQuality] [HighQuality]
internal sealed class SummaryAvatarFactory internal sealed class SummaryAvatarFactory
{ {
private static readonly DateTimeOffset DefaultRefreshTime = new(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0));
private readonly ModelAvatarInfo avatarInfo; private readonly ModelAvatarInfo avatarInfo;
private readonly DateTimeOffset showcaseRefreshTime; private readonly DateTimeOffset showcaseRefreshTime;
private readonly DateTimeOffset gameRecordRefreshTime; private readonly DateTimeOffset gameRecordRefreshTime;

View File

@@ -230,7 +230,7 @@ internal sealed partial class GameService : IGameService
} }
/// <inheritdoc/> /// <inheritdoc/>
public async ValueTask LaunchAsync() public async ValueTask LaunchAsync(IProgress<LaunchStatus> progress)
{ {
if (IsGameRunning()) if (IsGameRunning())
{ {
@@ -240,21 +240,37 @@ internal sealed partial class GameService : IGameService
string gamePath = appOptions.GamePath; string gamePath = appOptions.GamePath;
ArgumentException.ThrowIfNullOrEmpty(gamePath); ArgumentException.ThrowIfNullOrEmpty(gamePath);
progress.Report(new(LaunchPhase.ProcessInitializing, SH.ServiceGameLaunchPhaseProcessInitializing));
using (Process game = ProcessInterop.InitializeGameProcess(launchOptions, gamePath)) using (Process game = ProcessInterop.InitializeGameProcess(launchOptions, gamePath))
{ {
try try
{ {
bool isFirstInstance = Interlocked.Increment(ref runningGamesCounter) == 1;
game.Start(); game.Start();
progress.Report(new(LaunchPhase.ProcessStarted, SH.ServiceGameLaunchPhaseProcessStarted));
if (runtimeOptions.IsElevated && appOptions.IsAdvancedLaunchOptionsEnabled && launchOptions.UnlockFps) if (runtimeOptions.IsElevated && appOptions.IsAdvancedLaunchOptionsEnabled && launchOptions.UnlockFps)
{ {
await ProcessInterop.UnlockFpsAsync(serviceProvider, game, default).ConfigureAwait(false); 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 else
{ {
progress.Report(new(LaunchPhase.WaitingForExit, SH.ServiceGameLaunchPhaseWaitingProcessExit));
await game.WaitForExitAsync().ConfigureAwait(false); await game.WaitForExitAsync().ConfigureAwait(false);
progress.Report(new(LaunchPhase.ProcessExited, SH.ServiceGameLaunchPhaseProcessExited));
} }
} }
finally finally

View File

@@ -50,11 +50,7 @@ internal interface IGameService
/// <returns>是否正在运行</returns> /// <returns>是否正在运行</returns>
bool IsGameRunning(); bool IsGameRunning();
/// <summary> ValueTask LaunchAsync(IProgress<LaunchStatus> progress);
/// 异步启动
/// </summary>
/// <returns>任务</returns>
ValueTask LaunchAsync();
/// <summary> /// <summary>
/// 异步修改游戏账号名称 /// 异步修改游戏账号名称

View File

@@ -0,0 +1,15 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Game;
internal enum LaunchPhase
{
ProcessInitializing,
ProcessStarted,
UnlockingFps,
UnlockFpsSucceed,
UnlockFpsFailed,
WaitingForExit,
ProcessExited,
}

View File

@@ -0,0 +1,31 @@
// 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.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;
internal sealed class LaunchStatus
{
public LaunchStatus(LaunchPhase phase, string description)
{
Phase = phase;
Description = description;
}
public LaunchPhase Phase { get; set; }
public string Description { get; set; }
}

View File

@@ -0,0 +1,14 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
namespace Snap.Hutao.Service.Game;
[Injection(InjectAs.Singleton)]
internal sealed class LaunchStatusOptions : ObservableObject
{
private LaunchStatus? launchStatus;
public LaunchStatus? LaunchStatus { get => launchStatus; set => SetProperty(ref launchStatus, value); }
}

View File

@@ -49,19 +49,12 @@ internal static class ProcessInterop
}; };
} }
/// <summary> public static ValueTask UnlockFpsAsync(IServiceProvider serviceProvider, Process game, IProgress<LaunchStatus> progress, CancellationToken token = default)
/// 解锁帧率
/// </summary>
/// <param name="serviceProvider">服务提供器</param>
/// <param name="game">游戏进程</param>
/// <param name="token">取消令牌</param>
/// <returns>任务</returns>
public static ValueTask UnlockFpsAsync(IServiceProvider serviceProvider, Process game, CancellationToken token)
{ {
IGameFpsUnlocker unlocker = serviceProvider.CreateInstance<GameFpsUnlocker>(game); IGameFpsUnlocker unlocker = serviceProvider.CreateInstance<GameFpsUnlocker>(game);
UnlockTimingOptions options = new(100, 20000, 3000); UnlockTimingOptions options = new(100, 20000, 3000);
Progress<UnlockerStatus> progress = new(); // TODO: do something. Progress<UnlockerStatus> lockerProgress = new(unlockStatus => progress.Report(FromUnlockStatus(unlockStatus)));
return unlocker.UnlockAsync(options, progress, token); return unlocker.UnlockAsync(options, lockerProgress, token);
} }
/// <summary> /// <summary>
@@ -92,8 +85,7 @@ internal static class ProcessInterop
/// </summary> /// </summary>
/// <param name="hProcess">进程句柄</param> /// <param name="hProcess">进程句柄</param>
/// <param name="libraryPathu8">库的路径,不包含'\0'</param> /// <param name="libraryPathu8">库的路径,不包含'\0'</param>
[SuppressMessage("", "SH002")] public static unsafe void LoadLibraryAndInject(in HANDLE hProcess, in ReadOnlySpan<byte> libraryPathu8)
public static unsafe void LoadLibraryAndInject(HANDLE hProcess, ReadOnlySpan<byte> libraryPathu8)
{ {
HINSTANCE hKernelDll = GetModuleHandle("kernel32.dll"); HINSTANCE hKernelDll = GetModuleHandle("kernel32.dll");
Marshal.ThrowExceptionForHR(Marshal.GetLastPInvokeError()); Marshal.ThrowExceptionForHR(Marshal.GetLastPInvokeError());
@@ -132,8 +124,7 @@ internal static class ProcessInterop
} }
} }
[SuppressMessage("", "SH002")] private static unsafe FARPROC GetProcAddress(in HINSTANCE hModule, in ReadOnlySpan<byte> lpProcName)
private static unsafe FARPROC GetProcAddress(HINSTANCE hModule, ReadOnlySpan<byte> lpProcName)
{ {
fixed (byte* lpProcNameLocal = lpProcName) fixed (byte* lpProcNameLocal = lpProcName)
{ {
@@ -141,12 +132,23 @@ internal static class ProcessInterop
} }
} }
[SuppressMessage("", "SH002")] private static unsafe BOOL WriteProcessMemory(in HANDLE hProcess, void* lpBaseAddress, in ReadOnlySpan<byte> buffer)
private static unsafe BOOL WriteProcessMemory(HANDLE hProcess, void* lpBaseAddress, ReadOnlySpan<byte> buffer)
{ {
fixed (void* lpBuffer = buffer) fixed (void* lpBuffer = buffer)
{ {
return Windows.Win32.PInvoke.WriteProcessMemory(hProcess, lpBaseAddress, lpBuffer, unchecked((uint)buffer.Length)); 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);
}
}
} }

View File

@@ -50,6 +50,7 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
// Read UnityPlayer.dll // Read UnityPlayer.dll
UnsafeFindFpsAddress(moduleEntryInfo); UnsafeFindFpsAddress(moduleEntryInfo);
progress.Report(status);
// When player switch between scenes, we have to re adjust the fps // When player switch between scenes, we have to re adjust the fps
// So we keep a loop here // So we keep a loop here

View File

@@ -8,7 +8,7 @@ namespace Snap.Hutao.Service.Game.Unlocker;
/// <summary> /// <summary>
/// 解锁状态 /// 解锁状态
/// </summary> /// </summary>
internal sealed class UnlockerStatus : ICloneable<UnlockerStatus> internal sealed class UnlockerStatus
{ {
/// <summary> /// <summary>
/// 状态描述 /// 状态描述
@@ -29,9 +29,4 @@ internal sealed class UnlockerStatus : ICloneable<UnlockerStatus>
/// FPS 字节地址 /// FPS 字节地址
/// </summary> /// </summary>
public nuint FpsAddress { get; set; } public nuint FpsAddress { get; set; }
public UnlockerStatus Clone()
{
throw new NotImplementedException();
}
} }

View File

@@ -34,6 +34,12 @@
<Pivot> <Pivot>
<Pivot.RightHeader> <Pivot.RightHeader>
<CommandBar DefaultLabelPosition="Right"> <CommandBar DefaultLabelPosition="Right">
<CommandBar.Content>
<TextBlock
Margin="12,14,12,0"
VerticalAlignment="Center"
Text="{Binding LaunchStatusOptions.LaunchStatus.Description}"/>
</CommandBar.Content>
<AppBarButton <AppBarButton
Command="{Binding OpenScreenshotFolderCommand}" Command="{Binding OpenScreenshotFolderCommand}"
Icon="{shcm:FontIcon Glyph=&#xED25;}" Icon="{shcm:FontIcon Glyph=&#xED25;}"

View File

@@ -93,11 +93,11 @@ internal sealed class AvatarView : INameIconSide, ICalculableSource<ICalculableA
/// </summary> /// </summary>
public uint FetterLevel { get; set; } public uint FetterLevel { get; set; }
public string ShowcaseRefreshTimeFormat { get; set; } public string ShowcaseRefreshTimeFormat { get; set; } = default!;
public string GameRecordRefreshTimeFormat { get; set; } public string GameRecordRefreshTimeFormat { get; set; } = default!;
public string CalculatorRefreshTimeFormat { get; set; } public string CalculatorRefreshTimeFormat { get; set; } = default!;
/// <summary> /// <summary>
/// Id /// Id

View File

@@ -37,6 +37,7 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
private readonly INavigationService navigationService; private readonly INavigationService navigationService;
private readonly IInfoBarService infoBarService; private readonly IInfoBarService infoBarService;
private readonly LaunchOptions launchOptions; private readonly LaunchOptions launchOptions;
private readonly LaunchStatusOptions launchStatusOptions;
private readonly RuntimeOptions hutaoOptions; private readonly RuntimeOptions hutaoOptions;
private readonly ResourceClient resourceClient; private readonly ResourceClient resourceClient;
private readonly IUserService userService; private readonly IUserService userService;
@@ -88,6 +89,8 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
/// </summary> /// </summary>
public LaunchOptions Options { get => launchOptions; } public LaunchOptions Options { get => launchOptions; }
public LaunchStatusOptions LaunchStatusOptions { get => launchStatusOptions; }
/// <summary> /// <summary>
/// 胡桃选项 /// 胡桃选项
/// </summary> /// </summary>
@@ -187,10 +190,10 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
{ {
// Channel changed, we need to change local file. // Channel changed, we need to change local file.
LaunchGamePackageConvertDialog dialog = await contentDialogFactory.CreateInstanceAsync<LaunchGamePackageConvertDialog>().ConfigureAwait(false); LaunchGamePackageConvertDialog dialog = await contentDialogFactory.CreateInstanceAsync<LaunchGamePackageConvertDialog>().ConfigureAwait(false);
IProgress<PackageReplaceStatus> progress = taskContext.CreateProgressForMainThread<PackageReplaceStatus>(state => dialog.State = state/*.Clone()*/); IProgress<PackageReplaceStatus> convertProgress = taskContext.CreateProgressForMainThread<PackageReplaceStatus>(state => dialog.State = state);
using (await dialog.BlockAsync(taskContext).ConfigureAwait(false)) using (await dialog.BlockAsync(taskContext).ConfigureAwait(false))
{ {
if (!await gameService.EnsureGameResourceAsync(SelectedScheme, progress).ConfigureAwait(false)) if (!await gameService.EnsureGameResourceAsync(SelectedScheme, convertProgress).ConfigureAwait(false))
{ {
infoBarService.Warning(SH.ViewModelLaunchGameEnsureGameResourceFail); infoBarService.Warning(SH.ViewModelLaunchGameEnsureGameResourceFail);
return; return;
@@ -207,7 +210,8 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
} }
} }
await gameService.LaunchAsync().ConfigureAwait(false); IProgress<LaunchStatus> launchProgress = taskContext.CreateProgressForMainThread<LaunchStatus>(status => launchStatusOptions.LaunchStatus = status);
await gameService.LaunchAsync(launchProgress).ConfigureAwait(false);
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -58,7 +58,7 @@ internal sealed partial class LaunchGameViewModelSlim : Abstraction.ViewModelSli
} }
} }
await gameService.LaunchAsync().ConfigureAwait(false); await gameService.LaunchAsync(new Progress<LaunchStatus>()).ConfigureAwait(false);
} }
catch (Exception ex) catch (Exception ex)
{ {