mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
fix unlock fps
This commit is contained in:
@@ -44,7 +44,7 @@
|
||||
<CornerRadius x:Key="CompatCornerRadiusSmall">2</CornerRadius>
|
||||
<!-- OpenPaneLength -->
|
||||
<x:Double x:Key="CompatSplitViewOpenPaneLength">212</x:Double>
|
||||
<x:Double x:Key="CompatSplitViewOpenPaneLength2">268</x:Double>
|
||||
<x:Double x:Key="CompatSplitViewOpenPaneLength2">284</x:Double>
|
||||
<GridLength x:Key="CompatGridLength2">268</GridLength>
|
||||
|
||||
<x:Double x:Key="HomeAdaptiveCardHeight">180</x:Double>
|
||||
|
||||
@@ -6,14 +6,14 @@ namespace Snap.Hutao.Core.IO;
|
||||
/// <summary>
|
||||
/// 流复制状态
|
||||
/// </summary>
|
||||
internal sealed class StreamCopyState
|
||||
internal sealed class StreamCopyStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的流复制状态
|
||||
/// </summary>
|
||||
/// <param name="bytesCopied">已复制字节</param>
|
||||
/// <param name="totalBytes">总字节数</param>
|
||||
public StreamCopyState(long bytesCopied, long totalBytes)
|
||||
public StreamCopyStatus(long bytesCopied, long totalBytes)
|
||||
{
|
||||
BytesCopied = bytesCopied;
|
||||
TotalBytes = totalBytes;
|
||||
@@ -38,7 +38,7 @@ internal sealed class StreamCopyWorker
|
||||
/// </summary>
|
||||
/// <param name="progress">进度</param>
|
||||
/// <returns>任务</returns>
|
||||
public async Task CopyAsync(IProgress<StreamCopyState> progress)
|
||||
public async Task CopyAsync(IProgress<StreamCopyStatus> progress)
|
||||
{
|
||||
long totalBytesRead = 0;
|
||||
int bytesRead;
|
||||
|
||||
@@ -17,6 +17,9 @@ CreateRemoteThread
|
||||
CreateToolhelp32Snapshot
|
||||
GetModuleHandleW
|
||||
GetProcAddress
|
||||
K32EnumProcessModules
|
||||
K32GetModuleBaseName
|
||||
K32GetModuleInformation
|
||||
Module32First
|
||||
Module32Next
|
||||
ReadProcessMemory
|
||||
|
||||
@@ -1428,6 +1428,51 @@ namespace Snap.Hutao.Resource.Localization {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 在查找必要的模块时遇到问题:无法读取任何模块,可能是保护驱动已经加载完成 的本地化字符串。
|
||||
/// </summary>
|
||||
internal static string ServiceGameUnlockerFindModuleNoModuleFound {
|
||||
get {
|
||||
return ResourceManager.GetString("ServiceGameUnlockerFindModuleNoModuleFound", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 在查找必要的模块时遇到问题:查找模块超时 的本地化字符串。
|
||||
/// </summary>
|
||||
internal static string ServiceGameUnlockerFindModuleTimeLimitExeeded {
|
||||
get {
|
||||
return ResourceManager.GetString("ServiceGameUnlockerFindModuleTimeLimitExeeded", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 在匹配内存时遇到问题:无法匹配到期望的内容 的本地化字符串。
|
||||
/// </summary>
|
||||
internal static string ServiceGameUnlockerInterestedPatternNotFound {
|
||||
get {
|
||||
return ResourceManager.GetString("ServiceGameUnlockerInterestedPatternNotFound", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 在读取必要的模块内存时遇到问题:无法将模块内存复制到指定位置 的本地化字符串。
|
||||
/// </summary>
|
||||
internal static string ServiceGameUnlockerReadModuleMemoryCopyVirtualMemoryFailed {
|
||||
get {
|
||||
return ResourceManager.GetString("ServiceGameUnlockerReadModuleMemoryCopyVirtualMemoryFailed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 在读取游戏进程内存时遇到问题:无法读取到指定地址的有效值 的本地化字符串。
|
||||
/// </summary>
|
||||
internal static string ServiceGameUnlockerReadProcessMemoryPointerAddressFailed {
|
||||
get {
|
||||
return ResourceManager.GetString("ServiceGameUnlockerReadProcessMemoryPointerAddressFailed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 无法找到缓存的元数据文件 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -1572,6 +1617,15 @@ namespace Snap.Hutao.Resource.Localization {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 选择账号或直接启动 的本地化字符串。
|
||||
/// </summary>
|
||||
internal static string ViewCardLaunchGameSelectAccountPlaceholder {
|
||||
get {
|
||||
return ResourceManager.GetString("ViewCardLaunchGameSelectAccountPlaceholder", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 等级 的本地化字符串。
|
||||
/// </summary>
|
||||
|
||||
@@ -2019,4 +2019,19 @@
|
||||
<data name="ViewCardLaunchGameSelectAccountPlaceholder" xml:space="preserve">
|
||||
<value>选择账号或直接启动</value>
|
||||
</data>
|
||||
<data name="ServiceGameUnlockerFindModuleNoModuleFound" xml:space="preserve">
|
||||
<value>在查找必要的模块时遇到问题:无法读取任何模块,可能是保护驱动已经加载完成</value>
|
||||
</data>
|
||||
<data name="ServiceGameUnlockerFindModuleTimeLimitExeeded" xml:space="preserve">
|
||||
<value>在查找必要的模块时遇到问题:查找模块超时</value>
|
||||
</data>
|
||||
<data name="ServiceGameUnlockerInterestedPatternNotFound" xml:space="preserve">
|
||||
<value>在匹配内存时遇到问题:无法匹配到期望的内容</value>
|
||||
</data>
|
||||
<data name="ServiceGameUnlockerReadModuleMemoryCopyVirtualMemoryFailed" xml:space="preserve">
|
||||
<value>在读取必要的模块内存时遇到问题:无法将模块内存复制到指定位置</value>
|
||||
</data>
|
||||
<data name="ServiceGameUnlockerReadProcessMemoryPointerAddressFailed" xml:space="preserve">
|
||||
<value>在读取游戏进程内存时遇到问题:无法读取到指定地址的有效值</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -289,7 +289,7 @@ internal sealed partial class GameService : IGameService
|
||||
|
||||
if (isAdvancedOptionsAllowed && launchOptions.UnlockFps)
|
||||
{
|
||||
await ProcessInterop.UnlockFpsAsync(serviceProvider, game).ConfigureAwait(false);
|
||||
await ProcessInterop.UnlockFpsAsync(serviceProvider, game, default).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -54,12 +54,14 @@ internal static class ProcessInterop
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">服务提供器</param>
|
||||
/// <param name="game">游戏进程</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>任务</returns>
|
||||
public static Task UnlockFpsAsync(IServiceProvider serviceProvider, Process game)
|
||||
public static Task UnlockFpsAsync(IServiceProvider serviceProvider, Process game, CancellationToken token)
|
||||
{
|
||||
IGameFpsUnlocker unlocker = serviceProvider.CreateInstance<GameFpsUnlocker>(game);
|
||||
UnlockTimingOptions options = new(100, 20000, 3000);
|
||||
return unlocker.UnlockAsync(options);
|
||||
Progress<UnlockerStatus> progress = new(); // TODO: do something.
|
||||
return unlocker.UnlockAsync(options, progress, token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Unlocker;
|
||||
|
||||
/// <summary>
|
||||
/// 查找模块结果
|
||||
/// </summary>
|
||||
internal enum FindModuleResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 成功
|
||||
/// </summary>
|
||||
Ok,
|
||||
|
||||
/// <summary>
|
||||
/// 超时
|
||||
/// </summary>
|
||||
TimeLimitExeeded,
|
||||
|
||||
/// <summary>
|
||||
/// 模块尚未加载
|
||||
/// </summary>
|
||||
ModuleNotLoaded,
|
||||
|
||||
/// <summary>
|
||||
/// 没有模块,保护驱动已加载,无法读取
|
||||
/// </summary>
|
||||
NoModuleFound,
|
||||
}
|
||||
@@ -4,13 +4,11 @@
|
||||
using Snap.Hutao.Core.Diagnostics;
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
using Snap.Hutao.Win32;
|
||||
using System.Buffers.Binary;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.System.Diagnostics.ToolHelp;
|
||||
using Windows.Win32.System.ProcessStatus;
|
||||
using static Windows.Win32.PInvoke;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Unlocker;
|
||||
@@ -26,8 +24,7 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
|
||||
private readonly LaunchOptions launchOptions;
|
||||
private readonly ILogger<GameFpsUnlocker> logger;
|
||||
|
||||
private nuint fpsAddress;
|
||||
private bool isValid = true;
|
||||
private UnlockerStatus status = new();
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的 <see cref="GameFpsUnlocker"/> 对象,
|
||||
@@ -47,30 +44,31 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task UnlockAsync(UnlockTimingOptions options)
|
||||
public async Task UnlockAsync(UnlockTimingOptions options, IProgress<UnlockerStatus> progress, CancellationToken token = default)
|
||||
{
|
||||
Verify.Operation(isValid, "This Unlocker is invalid");
|
||||
Verify.Operation(status.IsUnlockerValid, "This Unlocker is invalid");
|
||||
|
||||
GameModuleEntryInfo moduleEntryInfo = await FindModuleAsync(options.FindModuleDelay, options.FindModuleLimit).ConfigureAwait(false);
|
||||
Must.Argument(moduleEntryInfo.HasValue, "读取游戏内存失败");
|
||||
(FindModuleResult result, GameModule moduleEntryInfo) = await FindModuleAsync(options.FindModuleDelay, options.FindModuleLimit).ConfigureAwait(false);
|
||||
Verify.Operation(result != FindModuleResult.TimeLimitExeeded, SH.ServiceGameUnlockerFindModuleTimeLimitExeeded);
|
||||
Verify.Operation(result != FindModuleResult.NoModuleFound, SH.ServiceGameUnlockerFindModuleNoModuleFound);
|
||||
|
||||
// Read UnityPlayer.dll
|
||||
UnsafeTryReadModuleMemoryFindFpsAddress(moduleEntryInfo);
|
||||
UnsafeFindFpsAddress(moduleEntryInfo);
|
||||
|
||||
// When player switch between scenes, we have to re adjust the fps
|
||||
// So we keep a loop here
|
||||
await LoopAdjustFpsAsync(options.AdjustFpsDelay).ConfigureAwait(false);
|
||||
await LoopAdjustFpsAsync(options.AdjustFpsDelay, progress, token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static unsafe bool UnsafeReadModulesMemory(Process process, in GameModuleEntryInfo moduleEntryInfo, out VirtualMemory memory)
|
||||
private static unsafe bool UnsafeReadModulesMemory(Process process, in GameModule moduleEntryInfo, out VirtualMemory memory)
|
||||
{
|
||||
MODULEENTRY32 unityPlayer = moduleEntryInfo.UnityPlayer;
|
||||
MODULEENTRY32 userAssembly = moduleEntryInfo.UserAssembly;
|
||||
ref readonly Module unityPlayer = ref moduleEntryInfo.UnityPlayer;
|
||||
ref readonly Module userAssembly = ref moduleEntryInfo.UserAssembly;
|
||||
|
||||
memory = new VirtualMemory(unityPlayer.modBaseSize + userAssembly.modBaseSize);
|
||||
memory = new VirtualMemory(unityPlayer.Size + userAssembly.Size);
|
||||
byte* lpBuffer = (byte*)memory.Pointer;
|
||||
return ReadProcessMemory((HANDLE)process.Handle, unityPlayer.modBaseAddr, lpBuffer, unityPlayer.modBaseSize, default)
|
||||
&& ReadProcessMemory((HANDLE)process.Handle, userAssembly.modBaseAddr, lpBuffer + unityPlayer.modBaseSize, userAssembly.modBaseSize, default);
|
||||
return ReadProcessMemory((HANDLE)process.Handle, (void*)unityPlayer.Address, lpBuffer, unityPlayer.Size, default)
|
||||
&& ReadProcessMemory((HANDLE)process.Handle, (void*)userAssembly.Address, lpBuffer + unityPlayer.Size, userAssembly.Size, default);
|
||||
}
|
||||
|
||||
private static unsafe bool UnsafeReadProcessMemory(Process process, nuint baseAddress, out nuint value)
|
||||
@@ -79,40 +77,66 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
|
||||
bool result = ReadProcessMemory((HANDLE)process.Handle, (void*)baseAddress, (byte*)&temp, 8, default);
|
||||
if (!result)
|
||||
{
|
||||
ThrowHelper.InvalidOperation("读取进程内存失败", null);
|
||||
ThrowHelper.InvalidOperation(SH.ServiceGameUnlockerReadProcessMemoryPointerAddressFailed, null);
|
||||
}
|
||||
|
||||
value = (nuint)temp;
|
||||
return result;
|
||||
}
|
||||
|
||||
private static unsafe bool UnsafeWriteProcessMemory(Process process, nuint baseAddress, int write)
|
||||
private static unsafe bool UnsafeWriteProcessMemory(Process process, nuint baseAddress, int value)
|
||||
{
|
||||
return WriteProcessMemory((HANDLE)process.Handle, (void*)baseAddress, &write, sizeof(int), default);
|
||||
return WriteProcessMemory((HANDLE)process.Handle, (void*)baseAddress, &value, sizeof(int), default);
|
||||
}
|
||||
|
||||
private static unsafe MODULEENTRY32 UnsafeFindModule(int processId, in ReadOnlySpan<byte> moduleName)
|
||||
private static unsafe FindModuleResult UnsafeTryFindModule(in HANDLE hProcess, in ReadOnlySpan<char> moduleName, out Module module)
|
||||
{
|
||||
CREATE_TOOLHELP_SNAPSHOT_FLAGS flags = CREATE_TOOLHELP_SNAPSHOT_FLAGS.TH32CS_SNAPMODULE | CREATE_TOOLHELP_SNAPSHOT_FLAGS.TH32CS_SNAPMODULE32;
|
||||
HANDLE snapshot = CreateToolhelp32Snapshot(flags, (uint)processId);
|
||||
try
|
||||
HMODULE[] buffer = new HMODULE[128];
|
||||
uint actualSize = 0;
|
||||
fixed (HMODULE* pBuffer = buffer)
|
||||
{
|
||||
Marshal.ThrowExceptionForHR(Marshal.GetLastPInvokeError());
|
||||
foreach (MODULEENTRY32 entry in StructMarshal.EnumerateModuleEntry32(snapshot))
|
||||
if (!K32EnumProcessModules(hProcess, pBuffer, unchecked((uint)(buffer.Length * sizeof(HMODULE))), out actualSize))
|
||||
{
|
||||
ReadOnlySpan<byte> szModuleNameLocal = MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)&entry.szModule);
|
||||
if (entry.th32ProcessID == processId && szModuleNameLocal.SequenceEqual(moduleName))
|
||||
Marshal.ThrowExceptionForHR(Marshal.GetLastPInvokeError());
|
||||
}
|
||||
}
|
||||
|
||||
if (actualSize == 0)
|
||||
{
|
||||
module = default!;
|
||||
return FindModuleResult.NoModuleFound;
|
||||
}
|
||||
|
||||
Span<HMODULE> modules = new(buffer, 0, unchecked((int)(actualSize / sizeof(HMODULE))));
|
||||
|
||||
foreach (ref readonly HMODULE hModule in modules)
|
||||
{
|
||||
char[] baseName = new char[256];
|
||||
fixed (char* lpBaseName = baseName)
|
||||
{
|
||||
if (K32GetModuleBaseName(hProcess, hModule, lpBaseName, 256) == 0)
|
||||
{
|
||||
return entry;
|
||||
continue;
|
||||
}
|
||||
|
||||
ReadOnlySpan<char> szModuleName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(lpBaseName);
|
||||
if (!szModuleName.SequenceEqual(moduleName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
finally
|
||||
{
|
||||
CloseHandle(snapshot);
|
||||
if (!K32GetModuleInformation(hProcess, hModule, out MODULEINFO moduleInfo, unchecked((uint)sizeof(MODULEINFO))))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
module = new((nuint)moduleInfo.lpBaseOfDll, moduleInfo.SizeOfImage);
|
||||
return FindModuleResult.Ok;
|
||||
}
|
||||
|
||||
module = default;
|
||||
return FindModuleResult.ModuleNotLoaded;
|
||||
}
|
||||
|
||||
private static int IndexOfPattern(in ReadOnlySpan<byte> memory)
|
||||
@@ -136,30 +160,43 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static unsafe GameModuleEntryInfo UnsafeGetGameModuleEntryInfo(int processId)
|
||||
private static unsafe FindModuleResult UnsafeGetGameModuleInfo(in HANDLE hProcess, out GameModule info)
|
||||
{
|
||||
MODULEENTRY32 unityPlayer = UnsafeFindModule(processId, "UnityPlayer.dll"u8);
|
||||
MODULEENTRY32 userAssembly = UnsafeFindModule(processId, "UserAssembly.dll"u8);
|
||||
FindModuleResult unityPlayerResult = UnsafeTryFindModule(hProcess, "UnityPlayer.dll", out Module unityPlayer);
|
||||
FindModuleResult userAssemblyResult = UnsafeTryFindModule(hProcess, "UserAssembly.dll", out Module userAssembly);
|
||||
|
||||
if (unityPlayer.modBaseSize != 0 && userAssembly.modBaseSize != 0)
|
||||
if (unityPlayerResult == FindModuleResult.Ok && userAssemblyResult == FindModuleResult.Ok)
|
||||
{
|
||||
return new(unityPlayer, userAssembly);
|
||||
info = new(unityPlayer, userAssembly);
|
||||
return FindModuleResult.Ok;
|
||||
}
|
||||
|
||||
return default;
|
||||
if (unityPlayerResult == FindModuleResult.NoModuleFound || userAssemblyResult == FindModuleResult.NoModuleFound)
|
||||
{
|
||||
info = default;
|
||||
return FindModuleResult.NoModuleFound;
|
||||
}
|
||||
|
||||
info = default;
|
||||
return FindModuleResult.ModuleNotLoaded;
|
||||
}
|
||||
|
||||
private async Task<GameModuleEntryInfo> FindModuleAsync(TimeSpan findModuleDelay, TimeSpan findModuleLimit)
|
||||
private async Task<ValueResult<FindModuleResult, GameModule>> FindModuleAsync(TimeSpan findModuleDelay, TimeSpan findModuleLimit)
|
||||
{
|
||||
ValueStopwatch watch = ValueStopwatch.StartNew();
|
||||
using (PeriodicTimer timer = new(findModuleDelay))
|
||||
{
|
||||
while (await timer.WaitForNextTickAsync().ConfigureAwait(false))
|
||||
{
|
||||
GameModuleEntryInfo moduleInfo = UnsafeGetGameModuleEntryInfo(gameProcess.Id);
|
||||
if (moduleInfo.HasValue)
|
||||
FindModuleResult result = UnsafeGetGameModuleInfo((HANDLE)gameProcess.Handle, out GameModule gameModule);
|
||||
if (result == FindModuleResult.Ok)
|
||||
{
|
||||
return moduleInfo;
|
||||
return new(FindModuleResult.Ok, gameModule);
|
||||
}
|
||||
|
||||
if (result == FindModuleResult.NoModuleFound)
|
||||
{
|
||||
return new(FindModuleResult.NoModuleFound, default);
|
||||
}
|
||||
|
||||
if (watch.GetElapsedTime() > findModuleLimit)
|
||||
@@ -169,83 +206,94 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
return new(FindModuleResult.TimeLimitExeeded, default);
|
||||
}
|
||||
|
||||
private async Task LoopAdjustFpsAsync(TimeSpan adjustFpsDelay)
|
||||
private async Task LoopAdjustFpsAsync(TimeSpan adjustFpsDelay, IProgress<UnlockerStatus> progress, CancellationToken token)
|
||||
{
|
||||
using (PeriodicTimer timer = new(adjustFpsDelay))
|
||||
{
|
||||
while (await timer.WaitForNextTickAsync().ConfigureAwait(false))
|
||||
while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false))
|
||||
{
|
||||
if (!gameProcess.HasExited && fpsAddress != 0)
|
||||
if (!gameProcess.HasExited && status.FpsAddress != 0U)
|
||||
{
|
||||
UnsafeWriteProcessMemory(gameProcess, fpsAddress, launchOptions.TargetFps);
|
||||
UnsafeWriteProcessMemory(gameProcess, status.FpsAddress, launchOptions.TargetFps);
|
||||
progress.Report(status);
|
||||
}
|
||||
else
|
||||
{
|
||||
isValid = false;
|
||||
fpsAddress = 0;
|
||||
status.IsUnlockerValid = false;
|
||||
status.FpsAddress = 0;
|
||||
progress.Report(status);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void UnsafeTryReadModuleMemoryFindFpsAddress(in GameModuleEntryInfo moduleEntryInfo)
|
||||
private unsafe void UnsafeFindFpsAddress(in GameModule moduleEntryInfo)
|
||||
{
|
||||
bool readOk = UnsafeReadModulesMemory(gameProcess, moduleEntryInfo, out VirtualMemory localMemory);
|
||||
Verify.Operation(readOk, "读取内存失败");
|
||||
Verify.Operation(readOk, SH.ServiceGameUnlockerReadModuleMemoryCopyVirtualMemoryFailed);
|
||||
|
||||
using (localMemory)
|
||||
{
|
||||
int offset = IndexOfPattern(localMemory.GetBuffer()[(int)moduleEntryInfo.UnityPlayer.modBaseSize..]);
|
||||
Must.Range(offset >= 0, "未匹配到FPS字节");
|
||||
int offset = IndexOfPattern(localMemory.GetBuffer()[(int)moduleEntryInfo.UnityPlayer.Size..]);
|
||||
Must.Range(offset >= 0, SH.ServiceGameUnlockerInterestedPatternNotFound);
|
||||
|
||||
byte* pLocalMemory = (byte*)localMemory.Pointer;
|
||||
MODULEENTRY32 unityPlayer = moduleEntryInfo.UnityPlayer;
|
||||
MODULEENTRY32 userAssembly = moduleEntryInfo.UserAssembly;
|
||||
ref readonly Module unityPlayer = ref moduleEntryInfo.UnityPlayer;
|
||||
ref readonly Module userAssembly = ref moduleEntryInfo.UserAssembly;
|
||||
|
||||
nuint localMemoryUnityPlayerAddress = (nuint)pLocalMemory;
|
||||
nuint localMemoryUserAssemblyAddress = localMemoryUnityPlayerAddress + unityPlayer.modBaseSize;
|
||||
nuint localMemoryUserAssemblyAddress = localMemoryUnityPlayerAddress + unityPlayer.Size;
|
||||
|
||||
nuint rip = localMemoryUserAssemblyAddress + (uint)offset;
|
||||
rip += *(uint*)(rip + 1) + 5;
|
||||
rip += *(uint*)(rip + 3) + 7;
|
||||
|
||||
nuint address = (nuint)userAssembly.modBaseAddr + (rip - localMemoryUserAssemblyAddress);
|
||||
logger.LogInformation("Game Process handle: {handle}", gameProcess.Handle);
|
||||
nuint address = userAssembly.Address + (rip - localMemoryUserAssemblyAddress);
|
||||
|
||||
nuint ptr = 0;
|
||||
SpinWait.SpinUntil(() => UnsafeReadProcessMemory(gameProcess, address, out ptr) && ptr != 0);
|
||||
|
||||
logger.LogInformation("UnsafeReadProcessMemory succeed {addr:x8}", ptr);
|
||||
rip = ptr - (nuint)unityPlayer.modBaseAddr + localMemoryUnityPlayerAddress;
|
||||
logger.LogInformation("UnityPlayer addr: {up:x8}, rip addr: {rip:x8}", localMemoryUnityPlayerAddress, rip);
|
||||
rip = ptr - unityPlayer.Address + localMemoryUnityPlayerAddress;
|
||||
while (*(byte*)rip == 0xE8 || *(byte*)rip == 0xE9)
|
||||
{
|
||||
rip += (nuint)(*(int*)(rip + 1) + 5);
|
||||
logger.LogInformation("rip ? addr: {rip:x8}", rip);
|
||||
}
|
||||
|
||||
nuint localMemoryActualAddress = rip + *(uint*)(rip + 2) + 6;
|
||||
nuint actualOffset = localMemoryActualAddress - localMemoryUnityPlayerAddress;
|
||||
fpsAddress = (nuint)(unityPlayer.modBaseAddr + actualOffset);
|
||||
logger.LogInformation("UnsafeTryReadModuleMemoryFindFpsAddress finished");
|
||||
status.FpsAddress = unityPlayer.Address + actualOffset;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly struct GameModuleEntryInfo
|
||||
private readonly struct GameModule
|
||||
{
|
||||
public readonly bool HasValue = false;
|
||||
public readonly MODULEENTRY32 UnityPlayer;
|
||||
public readonly MODULEENTRY32 UserAssembly;
|
||||
public readonly Module UnityPlayer;
|
||||
public readonly Module UserAssembly;
|
||||
|
||||
public GameModuleEntryInfo(MODULEENTRY32 unityPlayer, MODULEENTRY32 userAssembly)
|
||||
public GameModule(in Module unityPlayer, in Module userAssembly)
|
||||
{
|
||||
HasValue = true;
|
||||
UnityPlayer = unityPlayer;
|
||||
UserAssembly = userAssembly;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly struct Module
|
||||
{
|
||||
public readonly bool HasValue = false;
|
||||
public readonly nuint Address;
|
||||
public readonly uint Size;
|
||||
|
||||
public Module(nuint address, uint size)
|
||||
{
|
||||
HasValue = true;
|
||||
Address = address;
|
||||
Size = size;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,8 @@ internal interface IGameFpsUnlocker
|
||||
/// 异步的解锁帧数限制
|
||||
/// </summary>
|
||||
/// <param name="options">选项</param>
|
||||
/// <param name="progress">进度</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>解锁的结果</returns>
|
||||
Task UnlockAsync(UnlockTimingOptions options);
|
||||
Task UnlockAsync(UnlockTimingOptions options, IProgress<UnlockerStatus> progress, CancellationToken token = default);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.Abstraction;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Unlocker;
|
||||
|
||||
/// <summary>
|
||||
/// 解锁状态
|
||||
/// </summary>
|
||||
internal sealed class UnlockerStatus : ICloneable<UnlockerStatus>
|
||||
{
|
||||
/// <summary>
|
||||
/// 状态描述
|
||||
/// </summary>
|
||||
public string Description { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 查找模块状态
|
||||
/// </summary>
|
||||
public FindModuleResult FindModuleState { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前解锁器是否有效
|
||||
/// </summary>
|
||||
public bool IsUnlockerValid { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// FPS 字节地址
|
||||
/// </summary>
|
||||
public nuint FpsAddress { get; set; }
|
||||
|
||||
public UnlockerStatus Clone()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,12 @@
|
||||
xmlns:cwucont="using:CommunityToolkit.WinUI.UI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:shc="using:Snap.Hutao.Control"
|
||||
xmlns:shci="using:Snap.Hutao.Control.Image"
|
||||
xmlns:shcm="using:Snap.Hutao.Control.Markup"
|
||||
xmlns:shcp="using:Snap.Hutao.Control.Panel"
|
||||
xmlns:shvc="using:Snap.Hutao.View.Control"
|
||||
xmlns:shvconv="using:Snap.Hutao.View.Converter"
|
||||
xmlns:shvcoont="using:Snap.Hutao.View.Control"
|
||||
xmlns:shvg="using:Snap.Hutao.ViewModel.GachaLog"
|
||||
d:DataContext="{d:DesignInstance shvg:TypedWishSummary}"
|
||||
mc:Ignorable="d">
|
||||
@@ -19,15 +21,35 @@
|
||||
<SolidColorBrush x:Key="PurpleBrush" Color="#FFA156E0"/>
|
||||
<SolidColorBrush x:Key="OrangeBrush" Color="#FFBC6932"/>
|
||||
|
||||
<shvconv:Int32ToGradientColorConverter x:Key="Int32ToGradientColorConverter" MaximumValue="{Binding GuaranteeOrangeThreshold}"/>
|
||||
|
||||
<shc:BindingProxy x:Key="SummaryProxy" DataContext="{Binding}"/>
|
||||
|
||||
<DataTemplate x:Key="OrangeListTemplate" d:DataType="shvg:SummaryItem">
|
||||
<Grid Margin="0,4,4,0" Background="Transparent">
|
||||
<Grid Margin="0,4,0,0" Background="Transparent">
|
||||
<ToolTipService.ToolTip>
|
||||
<TextBlock Text="{Binding TimeFormatted}"/>
|
||||
</ToolTipService.ToolTip>
|
||||
|
||||
<Border Style="{StaticResource BorderCardStyle}">
|
||||
<ProgressBar
|
||||
MinHeight="32"
|
||||
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
|
||||
CornerRadius="{StaticResource CompatCornerRadius}"
|
||||
Maximum="{Binding Path=DataContext.GuaranteeOrangeThreshold, Source={StaticResource SummaryProxy}}"
|
||||
Opacity="0.2"
|
||||
Value="{Binding LastPull}">
|
||||
<ProgressBar.Foreground>
|
||||
<SolidColorBrush Color="{Binding LastPull, Converter={StaticResource Int32ToGradientColorConverter}}"/>
|
||||
</ProgressBar.Foreground>
|
||||
</ProgressBar>
|
||||
</Border>
|
||||
|
||||
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
|
||||
<shci:CachedImage
|
||||
Width="32"
|
||||
Height="32"
|
||||
CornerRadius="{ThemeResource ControlCornerRadius}"
|
||||
Source="{Binding Icon}"/>
|
||||
<TextBlock
|
||||
Margin="8,0,0,0"
|
||||
@@ -55,6 +77,7 @@
|
||||
|
||||
<TextBlock
|
||||
Width="24"
|
||||
Margin="0,0,8,0"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource BodyTextBlockStyle}"
|
||||
Text="{Binding LastPull}"
|
||||
@@ -76,7 +99,7 @@
|
||||
Style="{StaticResource BorderCardStyle}"
|
||||
ToolTipService.ToolTip="{Binding TimeFormatted}">
|
||||
<StackPanel>
|
||||
<shvc:ItemIcon
|
||||
<shvcoont:ItemIcon
|
||||
Width="40"
|
||||
Height="40"
|
||||
Icon="{Binding Icon}"
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using Snap.Hutao.Control;
|
||||
using Snap.Hutao.Win32;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Snap.Hutao.View.Converter;
|
||||
|
||||
/// <summary>
|
||||
/// Int32 转 色阶颜色
|
||||
/// </summary>
|
||||
internal sealed class Int32ToGradientColorConverter : DependencyObject, IValueConverter
|
||||
{
|
||||
private static readonly DependencyProperty MaximumProperty = Property<Int32ToGradientColorConverter>.Depend(nameof(Maximum), StructMarshal.Color(0xFFFF4949));
|
||||
private static readonly DependencyProperty MinimumProperty = Property<Int32ToGradientColorConverter>.Depend(nameof(Minimum), StructMarshal.Color(0xFF48FF7A));
|
||||
private static readonly DependencyProperty MaximumValueProperty = Property<Int32ToGradientColorConverter>.Depend(nameof(MaximumValue), 90);
|
||||
private static readonly DependencyProperty MinimumValueProperty = Property<Int32ToGradientColorConverter>.Depend(nameof(MinimumValue), 1);
|
||||
|
||||
/// <summary>
|
||||
/// 最小颜色
|
||||
/// </summary>
|
||||
public Color Minimum
|
||||
{
|
||||
get => (Color)GetValue(MinimumProperty);
|
||||
set => SetValue(MinimumProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 最大颜色
|
||||
/// </summary>
|
||||
public Color Maximum
|
||||
{
|
||||
get => (Color)GetValue(MaximumProperty);
|
||||
set => SetValue(MaximumProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 最小值
|
||||
/// </summary>
|
||||
public int MinimumValue
|
||||
{
|
||||
get => (int)GetValue(MinimumValueProperty);
|
||||
set => SetValue(MinimumValueProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 最大值
|
||||
/// </summary>
|
||||
public int MaximumValue
|
||||
{
|
||||
get => (int)GetValue(MaximumValueProperty);
|
||||
set => SetValue(MaximumValueProperty, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
double n = (value != null ? (int)value : MinimumValue) - MinimumValue;
|
||||
int step = MaximumValue - MinimumValue;
|
||||
double a = Minimum.A + ((Maximum.A - Minimum.A) * n / step);
|
||||
double r = Minimum.R + ((Maximum.R - Minimum.R) * n / step);
|
||||
double g = Minimum.G + ((Maximum.G - Minimum.G) * n / step);
|
||||
double b = Minimum.B + ((Maximum.B - Minimum.B) * n / step);
|
||||
|
||||
Unsafe.SkipInit(out Color color);
|
||||
color.A = (byte)a;
|
||||
color.R = (byte)r;
|
||||
color.G = (byte)g;
|
||||
color.B = (byte)b;
|
||||
return color;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ internal sealed class Int32ToVisibilityRevertConverter : IValueConverter
|
||||
/// <inheritdoc/>
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
return (int)value == 0 ? Visibility.Visible : Visibility.Collapsed;
|
||||
return value != null && (int)value == 0 ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -41,13 +41,15 @@
|
||||
Style="{StaticResource SubtitleTextBlockStyle}"
|
||||
Text="{Binding FinishDescription}"/>
|
||||
|
||||
<CommandBar Grid.Column="1" DefaultLabelPosition="Right">
|
||||
|
||||
<CommandBar
|
||||
Grid.Column="1"
|
||||
Margin="16,0,0,0"
|
||||
DefaultLabelPosition="Right">
|
||||
<CommandBar.Content>
|
||||
<AutoSuggestBox
|
||||
Width="240"
|
||||
Height="36"
|
||||
Margin="12,6,6,0"
|
||||
Margin="3,6,6,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalContentAlignment="Center"
|
||||
PlaceholderText="{shcm:ResourceString Name=ViewPageAchievementSearchPlaceholder}"
|
||||
@@ -123,6 +125,11 @@
|
||||
ItemsSource="{Binding AchievementGoals}"
|
||||
SelectedItem="{Binding SelectedAchievementGoal, Mode=TwoWay}"
|
||||
SelectionMode="Single">
|
||||
<ListView.ItemContainerStyle>
|
||||
<Style BasedOn="{StaticResource DefaultListViewItemStyle}" TargetType="ListViewItem">
|
||||
<Setter Property="Margin" Value="0,0,6,0"/>
|
||||
</Style>
|
||||
</ListView.ItemContainerStyle>
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid Margin="0,6">
|
||||
@@ -157,7 +164,7 @@
|
||||
<SplitView.Content>
|
||||
<ScrollViewer Padding="0,0,16,0">
|
||||
<ItemsControl
|
||||
Margin="16,0,0,16"
|
||||
Margin="8,0,0,16"
|
||||
ItemsPanel="{StaticResource ItemsStackPanelTemplate}"
|
||||
ItemsSource="{Binding Achievements}">
|
||||
<ItemsControl.ItemContainerTransitions>
|
||||
@@ -229,6 +236,7 @@
|
||||
</SplitView.Content>
|
||||
</SplitView>
|
||||
</Grid>
|
||||
|
||||
<Grid Visibility="{Binding SelectedArchive, Converter={StaticResource EmptyObjectToVisibilityRevertConverter}}">
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<Image
|
||||
|
||||
@@ -120,7 +120,7 @@ internal sealed partial class WelcomeViewModel : ObservableObject
|
||||
private readonly HttpClient httpClient;
|
||||
private readonly string fileName;
|
||||
private readonly string fileUrl;
|
||||
private readonly Progress<StreamCopyState> progress;
|
||||
private readonly Progress<StreamCopyStatus> progress;
|
||||
private string description = SH.ViewModelWelcomeDownloadSummaryDefault;
|
||||
private double progressValue;
|
||||
private long updateCount;
|
||||
@@ -196,7 +196,7 @@ internal sealed partial class WelcomeViewModel : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateProgressStatus(StreamCopyState status)
|
||||
private void UpdateProgressStatus(StreamCopyStatus status)
|
||||
{
|
||||
if (Interlocked.Increment(ref updateCount) % 40 == 0)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user