mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
fix #815
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -665,6 +665,27 @@
|
||||
<data name="ServiceGameFileOperationExceptionMessage" xml:space="preserve">
|
||||
<value>游戏文件操作失败:{0}</value>
|
||||
</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">
|
||||
<value>选择游戏本体</value>
|
||||
</data>
|
||||
|
||||
@@ -22,8 +22,6 @@ namespace Snap.Hutao.Service.AvatarInfo.Factory;
|
||||
[HighQuality]
|
||||
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 DateTimeOffset showcaseRefreshTime;
|
||||
private readonly DateTimeOffset gameRecordRefreshTime;
|
||||
|
||||
@@ -230,7 +230,7 @@ internal sealed partial class GameService : IGameService
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask LaunchAsync()
|
||||
public async ValueTask LaunchAsync(IProgress<LaunchStatus> progress)
|
||||
{
|
||||
if (IsGameRunning())
|
||||
{
|
||||
@@ -240,21 +240,37 @@ internal sealed partial class GameService : IGameService
|
||||
string gamePath = appOptions.GamePath;
|
||||
ArgumentException.ThrowIfNullOrEmpty(gamePath);
|
||||
|
||||
progress.Report(new(LaunchPhase.ProcessInitializing, SH.ServiceGameLaunchPhaseProcessInitializing));
|
||||
using (Process game = ProcessInterop.InitializeGameProcess(launchOptions, gamePath))
|
||||
{
|
||||
try
|
||||
{
|
||||
bool isFirstInstance = Interlocked.Increment(ref runningGamesCounter) == 1;
|
||||
|
||||
game.Start();
|
||||
progress.Report(new(LaunchPhase.ProcessStarted, SH.ServiceGameLaunchPhaseProcessStarted));
|
||||
|
||||
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
|
||||
{
|
||||
progress.Report(new(LaunchPhase.WaitingForExit, SH.ServiceGameLaunchPhaseWaitingProcessExit));
|
||||
await game.WaitForExitAsync().ConfigureAwait(false);
|
||||
progress.Report(new(LaunchPhase.ProcessExited, SH.ServiceGameLaunchPhaseProcessExited));
|
||||
}
|
||||
}
|
||||
finally
|
||||
|
||||
@@ -50,11 +50,7 @@ internal interface IGameService
|
||||
/// <returns>是否正在运行</returns>
|
||||
bool IsGameRunning();
|
||||
|
||||
/// <summary>
|
||||
/// 异步启动
|
||||
/// </summary>
|
||||
/// <returns>任务</returns>
|
||||
ValueTask LaunchAsync();
|
||||
ValueTask LaunchAsync(IProgress<LaunchStatus> progress);
|
||||
|
||||
/// <summary>
|
||||
/// 异步修改游戏账号名称
|
||||
|
||||
15
src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchPhase.cs
Normal file
15
src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchPhase.cs
Normal 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,
|
||||
}
|
||||
31
src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchStatus.cs
Normal file
31
src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchStatus.cs
Normal 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; }
|
||||
}
|
||||
@@ -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); }
|
||||
}
|
||||
@@ -49,19 +49,12 @@ internal static class ProcessInterop
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解锁帧率
|
||||
/// </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)
|
||||
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> progress = new(); // TODO: do something.
|
||||
return unlocker.UnlockAsync(options, progress, token);
|
||||
Progress<UnlockerStatus> lockerProgress = new(unlockStatus => progress.Report(FromUnlockStatus(unlockStatus)));
|
||||
return unlocker.UnlockAsync(options, lockerProgress, token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -92,8 +85,7 @@ internal static class ProcessInterop
|
||||
/// </summary>
|
||||
/// <param name="hProcess">进程句柄</param>
|
||||
/// <param name="libraryPathu8">库的路径,不包含'\0'</param>
|
||||
[SuppressMessage("", "SH002")]
|
||||
public static unsafe void LoadLibraryAndInject(HANDLE hProcess, ReadOnlySpan<byte> libraryPathu8)
|
||||
public static unsafe void LoadLibraryAndInject(in HANDLE hProcess, in ReadOnlySpan<byte> libraryPathu8)
|
||||
{
|
||||
HINSTANCE hKernelDll = GetModuleHandle("kernel32.dll");
|
||||
Marshal.ThrowExceptionForHR(Marshal.GetLastPInvokeError());
|
||||
@@ -132,8 +124,7 @@ internal static class ProcessInterop
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("", "SH002")]
|
||||
private static unsafe FARPROC GetProcAddress(HINSTANCE hModule, ReadOnlySpan<byte> lpProcName)
|
||||
private static unsafe FARPROC GetProcAddress(in HINSTANCE hModule, in ReadOnlySpan<byte> lpProcName)
|
||||
{
|
||||
fixed (byte* lpProcNameLocal = lpProcName)
|
||||
{
|
||||
@@ -141,12 +132,23 @@ internal static class ProcessInterop
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("", "SH002")]
|
||||
private static unsafe BOOL WriteProcessMemory(HANDLE hProcess, void* lpBaseAddress, ReadOnlySpan<byte> buffer)
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
|
||||
|
||||
// Read UnityPlayer.dll
|
||||
UnsafeFindFpsAddress(moduleEntryInfo);
|
||||
progress.Report(status);
|
||||
|
||||
// When player switch between scenes, we have to re adjust the fps
|
||||
// So we keep a loop here
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Service.Game.Unlocker;
|
||||
/// <summary>
|
||||
/// 解锁状态
|
||||
/// </summary>
|
||||
internal sealed class UnlockerStatus : ICloneable<UnlockerStatus>
|
||||
internal sealed class UnlockerStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// 状态描述
|
||||
@@ -29,9 +29,4 @@ internal sealed class UnlockerStatus : ICloneable<UnlockerStatus>
|
||||
/// FPS 字节地址
|
||||
/// </summary>
|
||||
public nuint FpsAddress { get; set; }
|
||||
|
||||
public UnlockerStatus Clone()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,12 @@
|
||||
<Pivot>
|
||||
<Pivot.RightHeader>
|
||||
<CommandBar DefaultLabelPosition="Right">
|
||||
<CommandBar.Content>
|
||||
<TextBlock
|
||||
Margin="12,14,12,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding LaunchStatusOptions.LaunchStatus.Description}"/>
|
||||
</CommandBar.Content>
|
||||
<AppBarButton
|
||||
Command="{Binding OpenScreenshotFolderCommand}"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
|
||||
@@ -93,11 +93,11 @@ internal sealed class AvatarView : INameIconSide, ICalculableSource<ICalculableA
|
||||
/// </summary>
|
||||
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>
|
||||
/// Id
|
||||
|
||||
@@ -37,6 +37,7 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
|
||||
private readonly INavigationService navigationService;
|
||||
private readonly IInfoBarService infoBarService;
|
||||
private readonly LaunchOptions launchOptions;
|
||||
private readonly LaunchStatusOptions launchStatusOptions;
|
||||
private readonly RuntimeOptions hutaoOptions;
|
||||
private readonly ResourceClient resourceClient;
|
||||
private readonly IUserService userService;
|
||||
@@ -88,6 +89,8 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
|
||||
/// </summary>
|
||||
public LaunchOptions Options { get => launchOptions; }
|
||||
|
||||
public LaunchStatusOptions LaunchStatusOptions { get => launchStatusOptions; }
|
||||
|
||||
/// <summary>
|
||||
/// 胡桃选项
|
||||
/// </summary>
|
||||
@@ -187,10 +190,10 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
|
||||
{
|
||||
// Channel changed, we need to change local file.
|
||||
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))
|
||||
{
|
||||
if (!await gameService.EnsureGameResourceAsync(SelectedScheme, progress).ConfigureAwait(false))
|
||||
if (!await gameService.EnsureGameResourceAsync(SelectedScheme, convertProgress).ConfigureAwait(false))
|
||||
{
|
||||
infoBarService.Warning(SH.ViewModelLaunchGameEnsureGameResourceFail);
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user