diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/DependencyInjection.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/DependencyInjection.cs index c28606c0..85199c9c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/DependencyInjection.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/DependencyInjection.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using CommunityToolkit.Mvvm.Messaging; +using Snap.Hutao.Core.Logging; using Snap.Hutao.Service; using System.Globalization; using Windows.Globalization; @@ -22,7 +23,7 @@ internal static class DependencyInjection ServiceProvider serviceProvider = new ServiceCollection() // Microsoft extension - .AddLogging(builder => builder.AddDebug()) + .AddLogging(builder => builder.AddUnconditionalDebug()) .AddMemoryCache() // Hutao extensions diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Logging/DebugLogger.cs b/src/Snap.Hutao/Snap.Hutao/Core/Logging/DebugLogger.cs new file mode 100644 index 00000000..bef2323b --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Logging/DebugLogger.cs @@ -0,0 +1,69 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System.Diagnostics; + +namespace Snap.Hutao.Core.Logging; + +/// +/// A logger that writes messages in the debug output window only when a debugger is attached. +/// +internal sealed class DebugLogger : ILogger +{ + private readonly string name; + + /// + /// Initializes a new instance of the class. + /// + /// The name of the logger. + public DebugLogger(string name) + { + this.name = name; + } + + /// + public IDisposable BeginScope(TState state) + where TState : notnull + { + return NullScope.Instance; + } + + /// + public bool IsEnabled(LogLevel logLevel) + { + // If the filter is null, everything is enabled + return logLevel != LogLevel.None; + } + + /// + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + if (!IsEnabled(logLevel)) + { + return; + } + + ArgumentNullException.ThrowIfNull(formatter); + + string message = formatter(state, exception); + + if (string.IsNullOrEmpty(message)) + { + return; + } + + message = $"{logLevel}: {message}"; + + if (exception != null) + { + message += Environment.NewLine + Environment.NewLine + exception; + } + + DebugWriteLine(message, name); + } + + private static void DebugWriteLine(string message, string name) + { + Debug.WriteLine(message, category: name); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Logging/DebugLoggerFactoryExtensions.cs b/src/Snap.Hutao/Snap.Hutao/Core/Logging/DebugLoggerFactoryExtensions.cs new file mode 100644 index 00000000..852a90c9 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Logging/DebugLoggerFactoryExtensions.cs @@ -0,0 +1,24 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Snap.Hutao.Core.Logging; + +/// +/// Extension methods for the class. +/// +internal static class DebugLoggerFactoryExtensions +{ + /// + /// Adds a debug logger named 'Debug' to the factory. + /// + /// The extension method argument. + /// builder + public static ILoggingBuilder AddUnconditionalDebug(this ILoggingBuilder builder) + { + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + + return builder; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Logging/DebugLoggerProvider.cs b/src/Snap.Hutao/Snap.Hutao/Core/Logging/DebugLoggerProvider.cs new file mode 100644 index 00000000..2cf5dd5e --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Logging/DebugLoggerProvider.cs @@ -0,0 +1,22 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Core.Logging; + +/// +/// The provider for the . +/// +[ProviderAlias("Debug")] +internal sealed class DebugLoggerProvider : ILoggerProvider +{ + /// + public ILogger CreateLogger(string name) + { + return new DebugLogger(name); + } + + /// + public void Dispose() + { + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Logging/NullScope.cs b/src/Snap.Hutao/Snap.Hutao/Core/Logging/NullScope.cs new file mode 100644 index 00000000..65db7224 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Logging/NullScope.cs @@ -0,0 +1,24 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Core.Logging; + +/// +/// An empty scope without any logic +/// +internal sealed class NullScope : IDisposable +{ + private NullScope() + { + } + + /// + /// 实例 + /// + public static NullScope Instance { get; } = new NullScope(); + + /// + public void Dispose() + { + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Setting/Feature.cs b/src/Snap.Hutao/Snap.Hutao/Core/Setting/Feature.cs new file mode 100644 index 00000000..d30c1f03 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Setting/Feature.cs @@ -0,0 +1,55 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using CommunityToolkit.Mvvm.ComponentModel; + +namespace Snap.Hutao.Core.Setting; + +/// +/// 功能 +/// +internal sealed class Feature : ObservableObject +{ + private readonly string displayName; + private readonly string description; + private readonly string settingKey; + private readonly bool defaultValue; + + /// + /// 构造一个新的功能 + /// + /// 显示名称 + /// 描述 + /// 键 + /// 默认值 + public Feature(string displayName, string description, string settingKey, bool defaultValue) + { + this.displayName = displayName; + this.description = description; + this.settingKey = settingKey; + this.defaultValue = defaultValue; + } + + /// + /// 显示名称 + /// + public string DisplayName { get => displayName; } + + /// + /// 描述 + /// + public string Description { get => description; } + + /// + /// 值 + /// + public bool Value + { + get => LocalSetting.Get(settingKey, defaultValue); + set + { + LocalSetting.Set(settingKey, value); + OnPropertyChanged(nameof(Value)); + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Setting/FeatureOptions.cs b/src/Snap.Hutao/Snap.Hutao/Core/Setting/FeatureOptions.cs new file mode 100644 index 00000000..46f5e053 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Setting/FeatureOptions.cs @@ -0,0 +1,33 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System.Collections; + +namespace Snap.Hutao.Core.Setting; + +/// +/// 功能选项 +/// +internal sealed class FeatureOptions : IReadOnlyCollection +{ + /// + /// 启用实时便笺无感验证 + /// + public Feature IsDailyNoteSlientVerificationEnabled { get; } = new("IsDailyNoteSlientVerificationEnabled", "启用实时便笺无感验证", "IsDailyNoteSlientVerificationEnabled", true); + + /// + public int Count { get => 1; } + + /// + public IEnumerator GetEnumerator() + { + // TODO: Use source generator + yield return IsDailyNoteSlientVerificationEnabled; + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } +} \ 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 2e9fc3cb..ef8b2b15 100644 --- a/src/Snap.Hutao/Snap.Hutao/NativeMethods.txt +++ b/src/Snap.Hutao/Snap.Hutao/NativeMethods.txt @@ -32,7 +32,9 @@ Module32First Module32Next ReadProcessMemory SetEvent +VirtualAlloc VirtualAllocEx +VirtualFree VirtualFreeEx WaitForSingleObject WriteProcessMemory 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 9661578d..d7036464 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/GameFpsUnlocker.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/GameFpsUnlocker.cs @@ -5,6 +5,7 @@ using Snap.Hutao.Core.Diagnostics; using Snap.Hutao.Win32; using System.Diagnostics; using System.Runtime.InteropServices; +using System.Text; using Windows.Win32.Foundation; using Windows.Win32.System.Diagnostics.ToolHelp; using static Windows.Win32.PInvoke; @@ -14,12 +15,15 @@ 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 { private readonly Process gameProcess; private readonly LaunchOptions launchOptions; + private readonly ILogger logger; private nuint fpsAddress; private bool isValid = true; @@ -37,31 +41,43 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker public GameFpsUnlocker(IServiceProvider serviceProvider, Process gameProcess) { launchOptions = serviceProvider.GetRequiredService(); + logger = serviceProvider.GetRequiredService>(); this.gameProcess = gameProcess; } /// public async Task UnlockAsync(TimeSpan findModuleDelay, TimeSpan findModuleLimit, TimeSpan adjustFpsDelay) { + logger.LogInformation("UnlockAsync called"); Verify.Operation(isValid, "This Unlocker is invalid"); - MODULEENTRY32 unityPlayer = await FindModuleAsync(findModuleDelay, findModuleLimit).ConfigureAwait(false); + GameModuleEntryInfo moduleEntryInfo = await FindModuleAsync(findModuleDelay, findModuleLimit).ConfigureAwait(false); + Must.Argument(moduleEntryInfo.HasValue, "读取游戏内存失败"); // Read UnityPlayer.dll - UnsafeTryReadModuleMemoryFindFpsAddress(unityPlayer); + UnsafeTryReadModuleMemoryFindFpsAddress(moduleEntryInfo); // When player switch between scenes, we have to re adjust the fps // So we keep a loop here await LoopAdjustFpsAsync(adjustFpsDelay).ConfigureAwait(false); } - private static unsafe bool UnsafeReadModuleMemory(Process process, MODULEENTRY32 entry, out Span memory) + private static unsafe bool UnsafeReadModulesMemory(Process process, GameModuleEntryInfo moduleEntryInfo, out VirtualMemory memory) { - memory = new byte[entry.modBaseSize]; + MODULEENTRY32 unityPlayer = moduleEntryInfo.UnityPlayer; + MODULEENTRY32 userAssembly = moduleEntryInfo.UserAssembly; - fixed (byte* lpBuffer = memory) + memory = new VirtualMemory(unityPlayer.modBaseSize + userAssembly.modBaseSize); + 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); + } + + private static unsafe bool UnsafeReadProcessMemory(Process process, nuint offset, out nuint value) + { + fixed (nuint* pValue = &value) { - return ReadProcessMemory((HANDLE)process.Handle, entry.modBaseAddr, lpBuffer, entry.modBaseSize, null); + return ReadProcessMemory((HANDLE)process.Handle, (void*)offset, &pValue, unchecked((uint)sizeof(nuint))); } } @@ -93,24 +109,62 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker } } - private async Task FindModuleAsync(TimeSpan findModuleDelay, TimeSpan findModuleLimit) + private static int FindPatternOffsetImplmentation(ReadOnlySpan memory) { - ValueStopwatch watch = ValueStopwatch.StartNew(); + // E8 ?? ?? ?? ?? 85 C0 7E 07 E8 ?? ?? ?? ?? EB 05 + int second = 0; + ReadOnlySpan secondPart = new byte[] { 0x85, 0xC0, 0x7E, 0x07, 0xE8, }; + ReadOnlySpan thirdPart = new byte[] { 0xEB, 0x05, }; - while (true) + while (second >= 0 && second < memory.Length) { - MODULEENTRY32 module = UnsafeFindModule(gameProcess.Id, "UnityPlayer.dll"u8); - if (!StructMarshal.IsDefault(module)) + second += memory[second..].IndexOf(secondPart); + if (memory[second - 5].Equals(0xE8) && memory.Slice(second + 9, 2).SequenceEqual(thirdPart)) { - return module; + return second - 5; } - if (watch.GetElapsedTime() > findModuleLimit) - { - break; - } + second += 5; + } - await Task.Delay(findModuleDelay).ConfigureAwait(false); + return -1; + } + + private unsafe GameModuleEntryInfo UnsafeGetGameModuleEntryInfo(int processId) + { + logger.LogInformation("UnsafeGetGameModuleEntryInfo called"); + + MODULEENTRY32 unityPlayer = UnsafeFindModule(processId, "UnityPlayer.dll"u8); + MODULEENTRY32 userAssembly = UnsafeFindModule(processId, "UserAssembly.dll"u8); + + if (unityPlayer.modBaseSize != 0 && userAssembly.modBaseSize != 0) + { + return new(unityPlayer, userAssembly); + } + + return default; + } + + private async Task FindModuleAsync(TimeSpan findModuleDelay, TimeSpan findModuleLimit) + { + logger.LogInformation("FindModuleAsync called"); + + ValueStopwatch watch = ValueStopwatch.StartNew(); + using (PeriodicTimer timer = new(findModuleDelay)) + { + while (await timer.WaitForNextTickAsync().ConfigureAwait(false)) + { + GameModuleEntryInfo moduleInfo = UnsafeGetGameModuleEntryInfo(gameProcess.Id); + if (moduleInfo.HasValue) + { + return moduleInfo; + } + + if (watch.GetElapsedTime() > findModuleLimit) + { + break; + } + } } return default; @@ -118,6 +172,8 @@ 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)) @@ -136,24 +192,80 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker } } - private unsafe void UnsafeTryReadModuleMemoryFindFpsAddress(MODULEENTRY32 unityPlayer) + private unsafe void UnsafeTryReadModuleMemoryFindFpsAddress(GameModuleEntryInfo moduleEntryInfo) { - bool readOk = UnsafeReadModuleMemory(gameProcess, unityPlayer, out Span memory); + logger.LogInformation("UnsafeTryReadModuleMemoryFindFpsAddress called"); + + bool readOk = UnsafeReadModulesMemory(gameProcess, moduleEntryInfo, out VirtualMemory localMemory); Verify.Operation(readOk, "读取内存失败"); - // Find FPS offset - // 7F 0F jg 0x11 - // 8B 05 ? ? ? ? mov eax, dword ptr[rip+?] - int adr = memory.IndexOf(new byte[] { 0x7F, 0x0F, 0x8B, 0x05 }); - - Must.Range(adr >= 0, $"未匹配到FPS字节"); - - fixed (byte* pSpan = memory) + using (localMemory) { - int rip = adr + 2; - int rel = *(int*)(pSpan + rip + 2); // Unsafe.ReadUnaligned(ref image[rip + 2]); - int ofs = rip + rel + 6; - fpsAddress = (nuint)(unityPlayer.modBaseAddr + ofs); + int offset = FindPatternOffsetImplmentation(localMemory.GetBuffer().Slice(unchecked((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 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"); + } + + Thread.Sleep(100); + } + + logger.LogInformation("ptr {addr}", ptr); + rip = ptr - (nuint)unityPlayer.modBaseAddr + localMemoryUnityPlayerAddress; + + while (*(byte*)rip == 0xE8 || *(byte*)rip == 0xE9) + { + rip += *(uint*)(rip + 1) + 5; + } + + nuint localMemoryActualAddress = rip + *(uint*)(rip + 2) + 6; + nuint actualOffset = localMemoryActualAddress - localMemoryUnityPlayerAddress; + fpsAddress = (nuint)(unityPlayer.modBaseAddr + actualOffset); + } + } + + private readonly struct GameModuleEntryInfo + { + public readonly bool HasValue = false; + public readonly MODULEENTRY32 UnityPlayer; + public readonly MODULEENTRY32 UserAssembly; + + public GameModuleEntryInfo(MODULEENTRY32 unityPlayer, MODULEENTRY32 userAssembly) + { + HasValue = true; + UnityPlayer = unityPlayer; + UserAssembly = userAssembly; } } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/VirtualMemory.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/VirtualMemory.cs new file mode 100644 index 00000000..945364f3 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/VirtualMemory.cs @@ -0,0 +1,54 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Windows.Win32.System.Memory; +using static Windows.Win32.PInvoke; + +namespace Snap.Hutao.Service.Game.Unlocker; + +/// +/// NativeMemory.AllocZeroed wrapper +/// +internal readonly unsafe struct VirtualMemory : IDisposable +{ + /// + /// 缓冲区地址 + /// + public readonly void* Pointer; + + /// + /// 长度 + /// + public readonly uint Length; + + /// + /// 构造一个新的本地内存 + /// + /// 长度 + public unsafe VirtualMemory(uint dwSize) + { + Length = dwSize; + VIRTUAL_ALLOCATION_TYPE commitAndReserve = VIRTUAL_ALLOCATION_TYPE.MEM_COMMIT | VIRTUAL_ALLOCATION_TYPE.MEM_RESERVE; + Pointer = VirtualAlloc(default, dwSize, commitAndReserve, PAGE_PROTECTION_FLAGS.PAGE_READWRITE); + } + + public static unsafe implicit operator Span(VirtualMemory memory) + { + return memory.GetBuffer(); + } + + /// + /// 获取缓冲区 + /// + /// 内存 + public unsafe Span GetBuffer() + { + return new Span(Pointer, (int)Length); + } + + /// + public void Dispose() + { + VirtualFree(Pointer, 0, VIRTUAL_FREE_TYPE.MEM_RELEASE); + } +} \ 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 a093e214..a1f684f4 100644 --- a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj +++ b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj @@ -253,7 +253,6 @@ - all runtime; build; native; contentfiles; analyzers; buildtransitive