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:
@@ -28,6 +28,20 @@ internal static class ThrowHelper
|
||||
throw new OperationCanceledException(message, inner);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 无效操作
|
||||
/// </summary>
|
||||
/// <param name="message">消息</param>
|
||||
/// <param name="inner">内部错误</param>
|
||||
/// <returns>nothing</returns>
|
||||
/// <exception cref="InvalidOperationException">无效操作异常</exception>
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static InvalidOperationException InvalidOperation(string message, Exception? inner)
|
||||
{
|
||||
throw new InvalidOperationException(message, inner);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 游戏文件操作失败
|
||||
/// </summary>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -51,6 +51,7 @@ GetForegroundWindow
|
||||
GetWindowPlacement
|
||||
GetWindowThreadProcessId
|
||||
SetForegroundWindow
|
||||
ReleaseDC
|
||||
|
||||
// WinRT
|
||||
IMemoryBufferByteAccess
|
||||
@@ -58,12 +58,8 @@ internal static class ProcessInterop
|
||||
public static Task UnlockFpsAsync(IServiceProvider serviceProvider, Process game)
|
||||
{
|
||||
IGameFpsUnlocker unlocker = serviceProvider.CreateInstance<GameFpsUnlocker>(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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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;
|
||||
/// <summary>
|
||||
/// 游戏帧率解锁器
|
||||
/// Credit to https://github.com/34736384/genshin-fps-unlock
|
||||
///
|
||||
/// TODO: Save memory alloc on GameModuleEntryInfo
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal sealed class GameFpsUnlocker : IGameFpsUnlocker
|
||||
@@ -46,12 +47,11 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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<byte> moduleName)
|
||||
@@ -109,7 +114,7 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
|
||||
}
|
||||
}
|
||||
|
||||
private static int FindPatternOffsetImplmentation(ReadOnlySpan<byte> memory)
|
||||
private static int IndexOfPattern(in ReadOnlySpan<byte> 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<GameModuleEntryInfo> 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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,9 +12,7 @@ internal interface IGameFpsUnlocker
|
||||
/// <summary>
|
||||
/// 异步的解锁帧数限制
|
||||
/// </summary>
|
||||
/// <param name="findModuleDelay">每次查找UnityPlayer的延时,推荐100毫秒</param>
|
||||
/// <param name="findModuleLimit">查找UnityPlayer的最大阈值,推荐10000毫秒</param>
|
||||
/// <param name="adjustFpsDelay">每次循环调整的间隔时间,推荐2000毫秒</param>
|
||||
/// <param name="options">选项</param>
|
||||
/// <returns>解锁的结果</returns>
|
||||
Task UnlockAsync(TimeSpan findModuleDelay, TimeSpan findModuleLimit, TimeSpan adjustFpsDelay);
|
||||
Task UnlockAsync(UnlockTimingOptions options);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Unlocker;
|
||||
|
||||
/// <summary>
|
||||
/// 解锁时机选项
|
||||
/// </summary>
|
||||
internal readonly struct UnlockTimingOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 每次查找 Module 的延时
|
||||
/// </summary>
|
||||
public readonly TimeSpan FindModuleDelay;
|
||||
|
||||
/// <summary>
|
||||
/// 查找 Module 的最大时间阈值
|
||||
/// </summary>
|
||||
public readonly TimeSpan FindModuleLimit;
|
||||
|
||||
/// <summary>
|
||||
/// 每次循环调整的间隔时间
|
||||
/// </summary>
|
||||
public readonly TimeSpan AdjustFpsDelay;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的解锁器选项
|
||||
/// </summary>
|
||||
/// <param name="findModuleDelayMilliseconds">每次查找UnityPlayer的延时,推荐100毫秒</param>
|
||||
/// <param name="findModuleLimitMilliseconds">查找UnityPlayer的最大阈值,推荐10000毫秒</param>
|
||||
/// <param name="adjustFpsDelayMilliseconds">每次循环调整的间隔时间,推荐2000毫秒</param>
|
||||
public UnlockTimingOptions(int findModuleDelayMilliseconds, int findModuleLimitMilliseconds, int adjustFpsDelayMilliseconds)
|
||||
{
|
||||
FindModuleDelay = TimeSpan.FromMilliseconds(findModuleDelayMilliseconds);
|
||||
FindModuleLimit = TimeSpan.FromMilliseconds(findModuleLimitMilliseconds);
|
||||
AdjustFpsDelay = TimeSpan.FromMilliseconds(adjustFpsDelayMilliseconds);
|
||||
}
|
||||
}
|
||||
@@ -258,7 +258,7 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.VisualStudio.Validation" Version="17.6.11" />
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.2.221-beta">
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.2.252-beta">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
Reference in New Issue
Block a user