diff --git a/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/ThrowHelper.cs b/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/ThrowHelper.cs index 7bf5d25a..91bf8add 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/ThrowHelper.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/ThrowHelper.cs @@ -28,6 +28,20 @@ internal static class ThrowHelper throw new OperationCanceledException(message, inner); } + /// + /// 无效操作 + /// + /// 消息 + /// 内部错误 + /// nothing + /// 无效操作异常 + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + public static InvalidOperationException InvalidOperation(string message, Exception? inner) + { + throw new InvalidOperationException(message, inner); + } + /// /// 游戏文件操作失败 /// diff --git a/src/Snap.Hutao/Snap.Hutao/NativeMethods.json b/src/Snap.Hutao/Snap.Hutao/NativeMethods.json index f7b56332..bcd1d020 100644 --- a/src/Snap.Hutao/Snap.Hutao/NativeMethods.json +++ b/src/Snap.Hutao/Snap.Hutao/NativeMethods.json @@ -1,6 +1,5 @@ { "$schema": "https://raw.githubusercontent.com/microsoft/CsWin32/main/src/Microsoft.Windows.CsWin32/settings.schema.json", "allowMarshaling": true, - "public": true, "useSafeHandles": false } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/NativeMethods.txt b/src/Snap.Hutao/Snap.Hutao/NativeMethods.txt index ef8b2b15..236f52f3 100644 --- a/src/Snap.Hutao/Snap.Hutao/NativeMethods.txt +++ b/src/Snap.Hutao/Snap.Hutao/NativeMethods.txt @@ -51,6 +51,7 @@ GetForegroundWindow GetWindowPlacement GetWindowThreadProcessId SetForegroundWindow +ReleaseDC // WinRT IMemoryBufferByteAccess \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/ProcessInterop.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/ProcessInterop.cs index 9c4ba836..637d6281 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/ProcessInterop.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/ProcessInterop.cs @@ -58,12 +58,8 @@ internal static class ProcessInterop public static Task UnlockFpsAsync(IServiceProvider serviceProvider, Process game) { IGameFpsUnlocker unlocker = serviceProvider.CreateInstance(game); - - TimeSpan findModuleDelay = TimeSpan.FromMilliseconds(100); - TimeSpan findModuleLimit = TimeSpan.FromMilliseconds(10000); - TimeSpan adjustFpsDelay = TimeSpan.FromMilliseconds(3000); - - return unlocker.UnlockAsync(findModuleDelay, findModuleLimit, adjustFpsDelay); + UnlockTimingOptions options = new(100, 10000, 3000); + return unlocker.UnlockAsync(options); } /// diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/GameFpsUnlocker.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/GameFpsUnlocker.cs index d7036464..2a6b981d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/GameFpsUnlocker.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/GameFpsUnlocker.cs @@ -2,8 +2,11 @@ // Licensed under the MIT license. 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; @@ -15,8 +18,6 @@ namespace Snap.Hutao.Service.Game.Unlocker; /// /// 游戏帧率解锁器 /// Credit to https://github.com/34736384/genshin-fps-unlock -/// -/// TODO: Save memory alloc on GameModuleEntryInfo /// [HighQuality] internal sealed class GameFpsUnlocker : IGameFpsUnlocker @@ -46,12 +47,11 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker } /// - public async Task UnlockAsync(TimeSpan findModuleDelay, TimeSpan findModuleLimit, TimeSpan adjustFpsDelay) + public async Task UnlockAsync(UnlockTimingOptions options) { - logger.LogInformation("UnlockAsync called"); Verify.Operation(isValid, "This Unlocker is invalid"); - GameModuleEntryInfo moduleEntryInfo = await FindModuleAsync(findModuleDelay, findModuleLimit).ConfigureAwait(false); + GameModuleEntryInfo moduleEntryInfo = await FindModuleAsync(options.FindModuleDelay, options.FindModuleLimit).ConfigureAwait(false); Must.Argument(moduleEntryInfo.HasValue, "读取游戏内存失败"); // Read UnityPlayer.dll @@ -59,10 +59,10 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker // When player switch between scenes, we have to re adjust the fps // So we keep a loop here - await LoopAdjustFpsAsync(adjustFpsDelay).ConfigureAwait(false); + await LoopAdjustFpsAsync(options.AdjustFpsDelay).ConfigureAwait(false); } - private static unsafe bool UnsafeReadModulesMemory(Process process, GameModuleEntryInfo moduleEntryInfo, out VirtualMemory memory) + private static unsafe bool UnsafeReadModulesMemory(Process process, in GameModuleEntryInfo moduleEntryInfo, out VirtualMemory memory) { MODULEENTRY32 unityPlayer = moduleEntryInfo.UnityPlayer; MODULEENTRY32 userAssembly = moduleEntryInfo.UserAssembly; @@ -73,17 +73,22 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker && ReadProcessMemory((HANDLE)process.Handle, userAssembly.modBaseAddr, lpBuffer + unityPlayer.modBaseSize, userAssembly.modBaseSize, default); } - private static unsafe bool UnsafeReadProcessMemory(Process process, nuint offset, out nuint value) + private static unsafe bool UnsafeReadProcessMemory(Process process, nuint baseAddress, out nuint value) { - fixed (nuint* pValue = &value) + ulong temp = 0; + bool result = ReadProcessMemory((HANDLE)process.Handle, (void*)baseAddress, (byte*)&temp, 8, default); + if (!result) { - return ReadProcessMemory((HANDLE)process.Handle, (void*)offset, &pValue, unchecked((uint)sizeof(nuint))); + ThrowHelper.InvalidOperation("读取进程内存失败", null); } + + value = (nuint)temp; + return result; } - private static unsafe bool UnsafeWriteModuleMemory(Process process, nuint baseAddress, int write) + private static unsafe bool UnsafeWriteProcessMemory(Process process, nuint baseAddress, int write) { - return WriteProcessMemory((HANDLE)process.Handle, (void*)baseAddress, &write, sizeof(int), null); + return WriteProcessMemory((HANDLE)process.Handle, (void*)baseAddress, &write, sizeof(int), default); } private static unsafe MODULEENTRY32 UnsafeFindModule(int processId, in ReadOnlySpan moduleName) @@ -109,7 +114,7 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker } } - private static int FindPatternOffsetImplmentation(ReadOnlySpan memory) + private static int IndexOfPattern(in ReadOnlySpan memory) { // E8 ?? ?? ?? ?? 85 C0 7E 07 E8 ?? ?? ?? ?? EB 05 int second = 0; @@ -130,10 +135,8 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker return -1; } - private unsafe GameModuleEntryInfo UnsafeGetGameModuleEntryInfo(int processId) + private static unsafe GameModuleEntryInfo UnsafeGetGameModuleEntryInfo(int processId) { - logger.LogInformation("UnsafeGetGameModuleEntryInfo called"); - MODULEENTRY32 unityPlayer = UnsafeFindModule(processId, "UnityPlayer.dll"u8); MODULEENTRY32 userAssembly = UnsafeFindModule(processId, "UserAssembly.dll"u8); @@ -147,8 +150,6 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker private async Task FindModuleAsync(TimeSpan findModuleDelay, TimeSpan findModuleLimit) { - logger.LogInformation("FindModuleAsync called"); - ValueStopwatch watch = ValueStopwatch.StartNew(); using (PeriodicTimer timer = new(findModuleDelay)) { @@ -172,15 +173,13 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker private async Task LoopAdjustFpsAsync(TimeSpan adjustFpsDelay) { - logger.LogInformation("LoopAdjustFpsAsync called"); - using (PeriodicTimer timer = new(adjustFpsDelay)) { while (await timer.WaitForNextTickAsync().ConfigureAwait(false)) { if (!gameProcess.HasExited && fpsAddress != 0) { - UnsafeWriteModuleMemory(gameProcess, fpsAddress, launchOptions.TargetFps); + UnsafeWriteProcessMemory(gameProcess, fpsAddress, launchOptions.TargetFps); } else { @@ -192,66 +191,46 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker } } - private unsafe void UnsafeTryReadModuleMemoryFindFpsAddress(GameModuleEntryInfo moduleEntryInfo) + private unsafe void UnsafeTryReadModuleMemoryFindFpsAddress(in GameModuleEntryInfo moduleEntryInfo) { - logger.LogInformation("UnsafeTryReadModuleMemoryFindFpsAddress called"); - bool readOk = UnsafeReadModulesMemory(gameProcess, moduleEntryInfo, out VirtualMemory localMemory); Verify.Operation(readOk, "读取内存失败"); using (localMemory) { - int offset = FindPatternOffsetImplmentation(localMemory.GetBuffer().Slice(unchecked((int)moduleEntryInfo.UnityPlayer.modBaseSize))); + int offset = IndexOfPattern(localMemory.GetBuffer()[(int)moduleEntryInfo.UnityPlayer.modBaseSize..]); Must.Range(offset > 0, "未匹配到FPS字节"); byte* pLocalMemory = (byte*)localMemory.Pointer; MODULEENTRY32 unityPlayer = moduleEntryInfo.UnityPlayer; MODULEENTRY32 userAssembly = moduleEntryInfo.UserAssembly; - logger.LogInformation("Pattern: {bytes}", BitConverter.ToString(localMemory.GetBuffer().Slice((int)(offset + unityPlayer.modBaseSize), 16).ToArray())); // - nuint localMemoryUnityPlayerAddress = (nuint)pLocalMemory; nuint localMemoryUserAssemblyAddress = localMemoryUnityPlayerAddress + unityPlayer.modBaseSize; - { - logger.LogInformation("localMemoryUnityPlayerAddress {addr:X8}", localMemoryUnityPlayerAddress); - logger.LogInformation("localMemoryUserAssemblyAddress {addr:X8}", localMemoryUserAssemblyAddress); - logger.LogInformation("memory end at {addr:X8}", localMemoryUserAssemblyAddress + userAssembly.modBaseSize); - } 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 ptr = 0; - nuint address = rip - localMemoryUserAssemblyAddress + (nuint)userAssembly.modBaseAddr; - logger.LogInformation("UnsafeReadModuleMemory at {addr:x8}|{uaAddr:x8}", address, (nuint)userAssembly.modBaseAddr); - while (ptr == 0) - { - // Critial: The pointer here is always returning 0 - // Make this a dead loop. - if (UnsafeReadProcessMemory(gameProcess, address, out ptr)) - { - logger.LogInformation("UnsafeReadProcessMemory succeed {addr:x8}", ptr); - } - else - { - logger.LogInformation("UnsafeReadProcessMemory failed"); - } + SpinWait.SpinUntil(() => UnsafeReadProcessMemory(gameProcess, address, out ptr) && ptr != 0); - Thread.Sleep(100); - } - - logger.LogInformation("ptr {addr}", ptr); + 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); while (*(byte*)rip == 0xE8 || *(byte*)rip == 0xE9) { - rip += *(uint*)(rip + 1) + 5; + 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"); } } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/IGameFpsUnlocker.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/IGameFpsUnlocker.cs index 6cc1f46a..cbf843bf 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/IGameFpsUnlocker.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/IGameFpsUnlocker.cs @@ -12,9 +12,7 @@ internal interface IGameFpsUnlocker /// /// 异步的解锁帧数限制 /// - /// 每次查找UnityPlayer的延时,推荐100毫秒 - /// 查找UnityPlayer的最大阈值,推荐10000毫秒 - /// 每次循环调整的间隔时间,推荐2000毫秒 + /// 选项 /// 解锁的结果 - Task UnlockAsync(TimeSpan findModuleDelay, TimeSpan findModuleLimit, TimeSpan adjustFpsDelay); + Task UnlockAsync(UnlockTimingOptions options); } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/UnlockTimingOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/UnlockTimingOptions.cs new file mode 100644 index 00000000..a3a835bf --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/UnlockTimingOptions.cs @@ -0,0 +1,38 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.Game.Unlocker; + +/// +/// 解锁时机选项 +/// +internal readonly struct UnlockTimingOptions +{ + /// + /// 每次查找 Module 的延时 + /// + public readonly TimeSpan FindModuleDelay; + + /// + /// 查找 Module 的最大时间阈值 + /// + public readonly TimeSpan FindModuleLimit; + + /// + /// 每次循环调整的间隔时间 + /// + public readonly TimeSpan AdjustFpsDelay; + + /// + /// 构造一个新的解锁器选项 + /// + /// 每次查找UnityPlayer的延时,推荐100毫秒 + /// 查找UnityPlayer的最大阈值,推荐10000毫秒 + /// 每次循环调整的间隔时间,推荐2000毫秒 + public UnlockTimingOptions(int findModuleDelayMilliseconds, int findModuleLimitMilliseconds, int adjustFpsDelayMilliseconds) + { + FindModuleDelay = TimeSpan.FromMilliseconds(findModuleDelayMilliseconds); + FindModuleLimit = TimeSpan.FromMilliseconds(findModuleLimitMilliseconds); + AdjustFpsDelay = TimeSpan.FromMilliseconds(adjustFpsDelayMilliseconds); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj index a1f684f4..565ba4f7 100644 --- a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj +++ b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj @@ -258,7 +258,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive