diff --git a/src/Snap.Hutao/Snap.Hutao/Properties/launchSettings.json b/src/Snap.Hutao/Snap.Hutao/Properties/launchSettings.json index 36a6b395..a5fb2a14 100644 --- a/src/Snap.Hutao/Snap.Hutao/Properties/launchSettings.json +++ b/src/Snap.Hutao/Snap.Hutao/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "Snap.Hutao": { "commandName": "MsixPackage", - "nativeDebugging": true, + "nativeDebugging": false, "doNotLaunchApp": false, "allowLocalNetworkLoopbackProperty": true }, diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameFileSystem.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameFileSystem.cs index 6e7529eb..1ceea43f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameFileSystem.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameFileSystem.cs @@ -1,6 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Service.Game.Scheme; using System.IO; namespace Snap.Hutao.Service.Game; @@ -38,4 +39,6 @@ internal sealed class GameFileSystem public string PCGameSDKFilePath { get => pcGameSDKFilePath ??= Path.Combine(GameDirectory, GameConstants.PCGameSDKFilePath); } public string ScreenShotDirectory { get => Path.Combine(GameDirectory, "ScreenShot"); } + + public string DataDirectory { get => Path.Combine(GameDirectory, LaunchScheme.ExecutableIsOversea(GameFileName) ? GameConstants.GenshinImpactData : GameConstants.YuanShenData); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionUnlockFpsHandler.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionUnlockFpsHandler.cs index 4e932b64..ea9c98b4 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionUnlockFpsHandler.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionUnlockFpsHandler.cs @@ -19,7 +19,12 @@ internal sealed class LaunchExecutionUnlockFpsHandler : ILaunchExecutionDelegate IProgressFactory progressFactory = context.ServiceProvider.GetRequiredService(); IProgress progress = progressFactory.CreateForMainThread(c => context.Progress.Report(LaunchStatus.FromUnlockerContext(c))); - GameFpsUnlocker unlocker = new(context.ServiceProvider, context.Process, new(100, 20000, 3000), progress); + if (!context.TryGetGameFileSystem(out GameFileSystem? gameFileSystem)) + { + return; + } + + GameFpsUnlocker unlocker = new(context.ServiceProvider, context.Process, new(gameFileSystem, 100, 20000, 3000), progress); try { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/GameFpsAddress.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/GameFpsAddress.cs index 4d87306f..ceaafabd 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/GameFpsAddress.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/GameFpsAddress.cs @@ -16,43 +16,31 @@ internal static class GameFpsAddress private const byte ASM_JMP = 0xE9; #pragma warning restore SA1310 - public static unsafe void UnsafeFindFpsAddress(GameFpsUnlockerContext state, in RequiredGameModule requiredGameModule) + public static unsafe void UnsafeFindFpsAddress(GameFpsUnlockerContext context, in RequiredRemoteModule remoteModule, in RequiredLocalModule localModule) { - bool readOk = UnsafeReadModulesMemory(state.GameProcess, requiredGameModule, out VirtualMemory localMemory); - HutaoException.ThrowIfNot(readOk, SH.ServiceGameUnlockerReadModuleMemoryCopyVirtualMemoryFailed); + int offsetToUserAssembly = IndexOfPattern(localModule.UserAssembly.AsSpan()); + HutaoException.ThrowIfNot(offsetToUserAssembly >= 0, SH.ServiceGameUnlockerInterestedPatternNotFound); - using (localMemory) + // Register Instruction Pointer + nuint rip = localModule.UserAssembly.Address + (uint)offsetToUserAssembly; + rip += 5U; + rip += (nuint)(*(int*)(rip + 2U) + 6); + + nuint remoteVirtualAddress = remoteModule.UserAssembly.Address + (rip - localModule.UserAssembly.Address); + + nuint ptr = 0; + SpinWait.SpinUntil(() => UnsafeReadProcessMemory(context.GameProcess, remoteVirtualAddress, out ptr) && ptr != 0); + + nuint localVirtualAddress = ptr - remoteModule.UnityPlayer.Address + localModule.UnityPlayer.Address; + + while (*(byte*)localVirtualAddress is ASM_CALL or ASM_JMP) { - int offset = IndexOfPattern(localMemory.AsSpan()[(int)requiredGameModule.UnityPlayer.Size..]); - HutaoException.ThrowIfNot(offset >= 0, SH.ServiceGameUnlockerInterestedPatternNotFound); - - byte* pLocalMemory = (byte*)localMemory.Pointer; - ref readonly Module unityPlayer = ref requiredGameModule.UnityPlayer; - ref readonly Module userAssembly = ref requiredGameModule.UserAssembly; - - nuint localMemoryUnityPlayerAddress = (nuint)pLocalMemory; - nuint localMemoryUserAssemblyAddress = localMemoryUnityPlayerAddress + unityPlayer.Size; - - nuint rip = localMemoryUserAssemblyAddress + (uint)offset; - rip += 5U; - rip += (nuint)(*(int*)(rip + 2U) + 6); - - nuint address = userAssembly.Address + (rip - localMemoryUserAssemblyAddress); - - nuint ptr = 0; - SpinWait.SpinUntil(() => UnsafeReadProcessMemory(state.GameProcess, address, out ptr) && ptr != 0); - - rip = ptr - unityPlayer.Address + localMemoryUnityPlayerAddress; - - while (*(byte*)rip is ASM_CALL or ASM_JMP) - { - rip += (nuint)(*(int*)(rip + 1) + 5); - } - - nuint localMemoryActualAddress = rip + *(uint*)(rip + 2) + 6; - nuint actualOffset = localMemoryActualAddress - localMemoryUnityPlayerAddress; - state.FpsAddress = unityPlayer.Address + actualOffset; + localVirtualAddress += (nuint)(*(int*)(localVirtualAddress + 1) + 5); } + + localVirtualAddress += *(uint*)(localVirtualAddress + 2) + 6; + nuint relativeVirtualAddress = localVirtualAddress - localModule.UnityPlayer.Address; + context.FpsAddress = remoteModule.UnityPlayer.Address + relativeVirtualAddress; } private static int IndexOfPattern(in ReadOnlySpan memory) @@ -62,16 +50,6 @@ internal static class GameFpsAddress return memory.IndexOf(part); } - private static unsafe bool UnsafeReadModulesMemory(Process process, in RequiredGameModule moduleEntryInfo, out VirtualMemory memory) - { - ref readonly Module unityPlayer = ref moduleEntryInfo.UnityPlayer; - ref readonly Module userAssembly = ref moduleEntryInfo.UserAssembly; - - memory = new VirtualMemory(unityPlayer.Size + userAssembly.Size); - return ReadProcessMemory(process.Handle, (void*)unityPlayer.Address, memory.AsSpan()[..(int)unityPlayer.Size], out _) - && ReadProcessMemory(process.Handle, (void*)userAssembly.Address, memory.AsSpan()[(int)unityPlayer.Size..], out _); - } - private static unsafe bool UnsafeReadProcessMemory(Process process, nuint baseAddress, out nuint value) { value = 0; 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 ec38da02..2b0cb141 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/GameFpsUnlocker.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/GameFpsUnlocker.cs @@ -4,6 +4,7 @@ using Snap.Hutao.Core.ExceptionService; using Snap.Hutao.Win32.Foundation; using System.Diagnostics; +using System.Runtime.InteropServices; using static Snap.Hutao.Win32.Kernel32; namespace Snap.Hutao.Service.Game.Unlocker; @@ -18,12 +19,12 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker private readonly LaunchOptions launchOptions; private readonly GameFpsUnlockerContext context = new(); - public GameFpsUnlocker(IServiceProvider serviceProvider, Process gameProcess, in UnlockTimingOptions options, IProgress progress) + public GameFpsUnlocker(IServiceProvider serviceProvider, Process gameProcess, in UnlockOptions options, IProgress progress) { launchOptions = serviceProvider.GetRequiredService(); context.GameProcess = gameProcess; - context.TimingOptions = options; + context.Options = options; context.Progress = progress; } @@ -31,18 +32,22 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker public async ValueTask UnlockAsync(CancellationToken token = default) { HutaoException.ThrowIfNot(context.IsUnlockerValid, "This Unlocker is invalid"); - (FindModuleResult result, RequiredGameModule gameModule) = await GameProcessModule.FindModuleAsync(context).ConfigureAwait(false); + (FindModuleResult result, RequiredRemoteModule remoteModule) = await GameProcessModule.FindModuleAsync(context).ConfigureAwait(false); HutaoException.ThrowIfNot(result != FindModuleResult.TimeLimitExeeded, SH.ServiceGameUnlockerFindModuleTimeLimitExeeded); HutaoException.ThrowIfNot(result != FindModuleResult.NoModuleFound, SH.ServiceGameUnlockerFindModuleNoModuleFound); - GameFpsAddress.UnsafeFindFpsAddress(context, gameModule); + using (RequiredLocalModule localModule = LoadRequiredLocalModule(context.Options.GameFileSystem)) + { + GameFpsAddress.UnsafeFindFpsAddress(context, remoteModule, localModule); + } + context.Report(); return context.FpsAddress != 0U; } public async ValueTask PostUnlockAsync(CancellationToken token = default) { - using (PeriodicTimer timer = new(context.TimingOptions.AdjustFpsDelay)) + using (PeriodicTimer timer = new(context.Options.AdjustFpsDelay)) { while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false)) { @@ -66,4 +71,15 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker { return WriteProcessMemory((HANDLE)process.Handle, (void*)baseAddress, ref value, out _); } + + private static RequiredLocalModule LoadRequiredLocalModule(GameFileSystem gameFileSystem) + { + string gameFoler = gameFileSystem.GameDirectory; + string dataFoler = gameFileSystem.DataDirectory; + + nint unityPlayerAddress = NativeLibrary.Load(System.IO.Path.Combine(gameFoler, "UnityPlayer.dll")); + nint userAssemblyAddress = NativeLibrary.Load(System.IO.Path.Combine(dataFoler, "Native", "UserAssembly.dll")); + + return new(unityPlayerAddress, userAssemblyAddress); + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/GameFpsUnlockerContext.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/GameFpsUnlockerContext.cs index ab674d18..dcf5778a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/GameFpsUnlockerContext.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/GameFpsUnlockerContext.cs @@ -18,7 +18,7 @@ internal sealed class GameFpsUnlockerContext public nuint FpsAddress { get; set; } - public UnlockTimingOptions TimingOptions { get; set; } + public UnlockOptions Options { get; set; } public Process GameProcess { get; set; } = default!; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/GameProcessModule.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/GameProcessModule.cs index cc63bb3d..cc9dc030 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/GameProcessModule.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/GameProcessModule.cs @@ -12,14 +12,14 @@ namespace Snap.Hutao.Service.Game.Unlocker; internal static class GameProcessModule { - public static async ValueTask> FindModuleAsync(GameFpsUnlockerContext state) + public static async ValueTask> FindModuleAsync(GameFpsUnlockerContext state) { ValueStopwatch watch = ValueStopwatch.StartNew(); - using (PeriodicTimer timer = new(state.TimingOptions.FindModuleDelay)) + using (PeriodicTimer timer = new(state.Options.FindModuleDelay)) { while (await timer.WaitForNextTickAsync().ConfigureAwait(false)) { - FindModuleResult result = UnsafeGetGameModuleInfo((HANDLE)state.GameProcess.Handle, out RequiredGameModule gameModule); + FindModuleResult result = UnsafeGetGameModuleInfo((HANDLE)state.GameProcess.Handle, out RequiredRemoteModule gameModule); if (result == FindModuleResult.Ok) { return new(FindModuleResult.Ok, gameModule); @@ -30,7 +30,7 @@ internal static class GameProcessModule return new(FindModuleResult.NoModuleFound, default); } - if (watch.GetElapsedTime() > state.TimingOptions.FindModuleLimit) + if (watch.GetElapsedTime() > state.Options.FindModuleLimit) { break; } @@ -40,7 +40,7 @@ internal static class GameProcessModule return new(FindModuleResult.TimeLimitExeeded, default); } - private static FindModuleResult UnsafeGetGameModuleInfo(in HANDLE hProcess, out RequiredGameModule info) + private static FindModuleResult UnsafeGetGameModuleInfo(in HANDLE hProcess, out RequiredRemoteModule info) { FindModuleResult unityPlayerResult = UnsafeFindModule(hProcess, "UnityPlayer.dll", out Module unityPlayer); FindModuleResult userAssemblyResult = UnsafeFindModule(hProcess, "UserAssembly.dll", out Module userAssembly); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/Module.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/Module.cs index 34f2e0a8..6a86cf04 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/Module.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/Module.cs @@ -1,6 +1,8 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using System.Runtime.CompilerServices; + namespace Snap.Hutao.Service.Game.Unlocker; internal readonly struct Module @@ -15,4 +17,9 @@ internal readonly struct Module Address = address; Size = size; } + + public unsafe Span AsSpan() + { + return new((void*)Address, (int)Size); + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/RequiredLocalModule.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/RequiredLocalModule.cs new file mode 100644 index 00000000..1ea58a8e --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/RequiredLocalModule.cs @@ -0,0 +1,35 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Win32.Foundation; +using Snap.Hutao.Win32.System.ProcessStatus; +using System.Diagnostics; +using System.Runtime.InteropServices; +using static Snap.Hutao.Win32.Kernel32; + +namespace Snap.Hutao.Service.Game.Unlocker; + +internal readonly struct RequiredLocalModule : IDisposable +{ + public readonly bool HasValue = false; + public readonly Module UnityPlayer; + public readonly Module UserAssembly; + + public RequiredLocalModule(nint unityPlayerAddress, nint userAssemblyAddress) + { + HasValue = true; + HANDLE process = Process.GetCurrentProcess().Handle; + + K32GetModuleInformation(process, unityPlayerAddress, out MODULEINFO upInfo); + UnityPlayer = new((nuint)unityPlayerAddress, upInfo.SizeOfImage); + + K32GetModuleInformation(process, userAssemblyAddress, out MODULEINFO uaInfo); + UserAssembly = new((nuint)userAssemblyAddress, uaInfo.SizeOfImage); + } + + public void Dispose() + { + NativeLibrary.Free((nint)UnityPlayer.Address); + NativeLibrary.Free((nint)UserAssembly.Address); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/RequiredGameModule.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/RequiredRemoteModule.cs similarity index 74% rename from src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/RequiredGameModule.cs rename to src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/RequiredRemoteModule.cs index bdf6c7db..5ae03e0c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/RequiredGameModule.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/RequiredRemoteModule.cs @@ -3,13 +3,13 @@ namespace Snap.Hutao.Service.Game.Unlocker; -internal readonly struct RequiredGameModule +internal readonly struct RequiredRemoteModule { public readonly bool HasValue = false; public readonly Module UnityPlayer; public readonly Module UserAssembly; - public RequiredGameModule(in Module unityPlayer, in Module userAssembly) + public RequiredRemoteModule(in Module unityPlayer, in Module userAssembly) { HasValue = true; UnityPlayer = unityPlayer; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/UnlockOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/UnlockOptions.cs new file mode 100644 index 00000000..197e8a86 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/UnlockOptions.cs @@ -0,0 +1,20 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.Game.Unlocker; + +internal readonly struct UnlockOptions +{ + public readonly GameFileSystem GameFileSystem; + public readonly TimeSpan FindModuleDelay; + public readonly TimeSpan FindModuleLimit; + public readonly TimeSpan AdjustFpsDelay; + + public UnlockOptions(GameFileSystem gameFileSystem, int findModuleDelayMilliseconds, int findModuleLimitMilliseconds, int adjustFpsDelayMilliseconds) + { + GameFileSystem = gameFileSystem; + 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/Service/Game/Unlocker/UnlockTimingOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/UnlockTimingOptions.cs deleted file mode 100644 index a3a835bf..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/UnlockTimingOptions.cs +++ /dev/null @@ -1,38 +0,0 @@ -// 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/Win32/Foundation/HMODULE.cs b/src/Snap.Hutao/Snap.Hutao/Win32/Foundation/HMODULE.cs index 5fb3a172..6b1317be 100644 --- a/src/Snap.Hutao/Snap.Hutao/Win32/Foundation/HMODULE.cs +++ b/src/Snap.Hutao/Snap.Hutao/Win32/Foundation/HMODULE.cs @@ -8,4 +8,6 @@ namespace Snap.Hutao.Win32.Foundation; internal readonly struct HMODULE { public readonly nint Value; + + public static unsafe implicit operator HMODULE(nint value) => *(HMODULE*)&value; } \ No newline at end of file