diff --git a/src/Desktop/Fischless.Fetch/Launch/GILauncher.cs b/src/Desktop/Fischless.Fetch/Launch/GILauncher.cs
index 453aa70..8f4b7ef 100644
--- a/src/Desktop/Fischless.Fetch/Launch/GILauncher.cs
+++ b/src/Desktop/Fischless.Fetch/Launch/GILauncher.cs
@@ -183,8 +183,8 @@ public static async Task LaunchAsync(int? delayMs = null, GIRelaunchMethod relau
try
{
await new GameFpsUnlocker(gameProcess)
- .SetTargetFps(launchParameter.Fps.Value)
- .UnlockAsync(new UnlockTimingOptions(100, 20000, 3000));
+ .SetTargetFps((int)launchParameter.Fps.Value)
+ .UnlockAsync(GameFpsUnlockerOption.Default.Value);
}
catch
{
diff --git a/src/Desktop/Fischless.Fetch/Unlocker/FindModuleResult.cs b/src/Desktop/Fischless.Fetch/Unlocker/FindModuleResult.cs
deleted file mode 100644
index 3093b5c..0000000
--- a/src/Desktop/Fischless.Fetch/Unlocker/FindModuleResult.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace Fischless.Fetch.Unlocker;
-
-internal enum FindModuleResult
-{
- Ok,
- TimeLimitExeeded,
- ModuleNotLoaded,
- NoModuleFound,
-}
diff --git a/src/Desktop/Fischless.Fetch/Unlocker/GameFpsUnlocker.cs b/src/Desktop/Fischless.Fetch/Unlocker/GameFpsUnlocker.cs
index 4b3db53..0095937 100644
--- a/src/Desktop/Fischless.Fetch/Unlocker/GameFpsUnlocker.cs
+++ b/src/Desktop/Fischless.Fetch/Unlocker/GameFpsUnlocker.cs
@@ -1,393 +1,21 @@
-using Microsoft;
-using System.Diagnostics;
-using System.Diagnostics.CodeAnalysis;
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-using Vanara;
-using Vanara.PInvoke;
+using System.Diagnostics;
namespace Fischless.Fetch.Unlocker;
-///
-/// Credit to https://github.com/34736384/genshin-fps-unlock
-///
-internal sealed class GameFpsUnlocker(Process gameProcess) : IGameFpsUnlocker
+internal sealed class GameFpsUnlocker(Process gameProcess)
{
private readonly Process gameProcess = gameProcess;
- private uint targetFps;
- private readonly UnlockerStatus status = new();
+ private int targetFps;
- public GameFpsUnlocker SetTargetFps(uint targetFps)
+ public GameFpsUnlocker SetTargetFps(int targetFps)
{
this.targetFps = targetFps;
return this;
}
- ///
- public async ValueTask UnlockAsync(UnlockTimingOptions options, IProgress? progress = null, CancellationToken token = default)
+ public async Task UnlockAsync(GameFpsUnlockerOption options, CancellationTokenSource cts = null!)
{
- Verify.Operation(status.IsUnlockerValid, "This Unlocker is invalid");
-
- (FindModuleResult result, GameModule moduleEntryInfo) = await FindModuleAsync(options.FindModuleDelay, options.FindModuleLimit).ConfigureAwait(false);
- Verify.Operation(result != FindModuleResult.TimeLimitExeeded, "Error finding required modules: timeout; please retry");
- Verify.Operation(result != FindModuleResult.NoModuleFound, "Error finding required modules: could not read any module, the protection driver may have been loaded; please retry");
-
- // Read UnityPlayer.dll
- UnsafeFindFpsAddress(moduleEntryInfo);
- progress?.Report(status);
-
- // When player switch between scenes, we have to re adjust the fps
- // So we keep a loop here
- await LoopAdjustFpsAsync(options.AdjustFpsDelay, progress, token).ConfigureAwait(false);
- }
-
- private static unsafe bool UnsafeReadModulesMemory(System.Diagnostics.Process process, in GameModule 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 Kernel32X.ReadProcessMemory(process.Handle, unityPlayer.Address, memory.AsSpan()[..(int)unityPlayer.Size], out _)
- && Kernel32X.ReadProcessMemory(process.Handle, userAssembly.Address, memory.AsSpan()[(int)unityPlayer.Size..], out _);
- }
-
- private static unsafe bool UnsafeReadProcessMemory(Process process, nuint baseAddress, out nuint value)
- {
- value = 0;
- bool result = Kernel32X.ReadProcessMemory(process.Handle, baseAddress, ref value, out _);
- Verify.Operation(result, "Error reading process modules' memory: could not read valid value in given address");
- return result;
- }
-
- private static unsafe bool UnsafeWriteProcessMemory(Process process, nuint baseAddress, int value)
- {
- return Kernel32X.WriteProcessMemory(process.Handle, baseAddress, ref value, out _);
- }
-
- private static unsafe FindModuleResult UnsafeTryFindModule(in nint hProcess, in ReadOnlySpan moduleName, out Module module)
- {
- HMODULE[] buffer = new HMODULE[128];
- if (!Kernel32X.K32EnumProcessModules(hProcess, buffer, out uint actualSize))
- {
- Marshal.ThrowExceptionForHR(Marshal.GetLastPInvokeError());
- }
-
- if (actualSize == 0)
- {
- module = default!;
- return FindModuleResult.NoModuleFound;
- }
-
- foreach (ref readonly HMODULE hModule in buffer.AsSpan()[..(int)(actualSize / sizeof(HMODULE))])
- {
- char[] baseName = new char[256];
-
- if (Kernel32X.K32GetModuleBaseNameW(hProcess, hModule, baseName) == 0)
- {
- continue;
- }
-
- fixed (char* lpBaseName = baseName)
- {
- ReadOnlySpan szModuleName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(lpBaseName);
- if (!szModuleName.SequenceEqual(moduleName))
- {
- continue;
- }
- }
-
- if (!Kernel32X.K32GetModuleInformation(hProcess, hModule, out Kernel32.MODULEINFO moduleInfo))
- {
- continue;
- }
-
- module = new((nuint)moduleInfo.lpBaseOfDll, moduleInfo.SizeOfImage);
- return FindModuleResult.Ok;
- }
-
- module = default;
- return FindModuleResult.ModuleNotLoaded;
- }
-
- private static int IndexOfPattern(in ReadOnlySpan memory)
- {
- // B9 3C 00 00 00 FF 15
- ReadOnlySpan part = [0xB9, 0x3C, 0x00, 0x00, 0x00, 0xFF, 0x15];
- return memory.IndexOf(part);
- }
-
- private static FindModuleResult UnsafeGetGameModuleInfo(in nint hProcess, out GameModule info)
- {
- FindModuleResult unityPlayerResult = UnsafeTryFindModule(hProcess, "UnityPlayer.dll", out Module unityPlayer);
- FindModuleResult userAssemblyResult = UnsafeTryFindModule(hProcess, "UserAssembly.dll", out Module userAssembly);
-
- if (unityPlayerResult == FindModuleResult.Ok && userAssemblyResult == FindModuleResult.Ok)
- {
- info = new(unityPlayer, userAssembly);
- return FindModuleResult.Ok;
- }
-
- if (unityPlayerResult == FindModuleResult.NoModuleFound && userAssemblyResult == FindModuleResult.NoModuleFound)
- {
- info = default;
- return FindModuleResult.NoModuleFound;
- }
-
- info = default;
- return FindModuleResult.ModuleNotLoaded;
- }
-
- private async ValueTask> FindModuleAsync(TimeSpan findModuleDelay, TimeSpan findModuleLimit)
- {
- ValueStopwatch watch = ValueStopwatch.StartNew();
- using (PeriodicTimer timer = new(findModuleDelay))
- {
- while (await timer.WaitForNextTickAsync().ConfigureAwait(false))
- {
- FindModuleResult result = UnsafeGetGameModuleInfo(gameProcess.Handle, out GameModule gameModule);
- if (result == FindModuleResult.Ok)
- {
- return new(FindModuleResult.Ok, gameModule);
- }
-
- if (result == FindModuleResult.NoModuleFound)
- {
- return new(FindModuleResult.NoModuleFound, default);
- }
-
- if (watch.GetElapsedTime() > findModuleLimit)
- {
- break;
- }
- }
- }
-
- return new(FindModuleResult.TimeLimitExeeded, default);
- }
-
- private async ValueTask LoopAdjustFpsAsync(TimeSpan adjustFpsDelay, IProgress progress, CancellationToken token)
- {
- using (PeriodicTimer timer = new(adjustFpsDelay))
- {
- while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false))
- {
- if (!gameProcess.HasExited && status.FpsAddress != 0U)
- {
- UnsafeWriteProcessMemory(gameProcess, status.FpsAddress, (int)targetFps);
- progress?.Report(status);
- }
- else
- {
- status.IsUnlockerValid = false;
- status.FpsAddress = 0;
- progress?.Report(status);
- return;
- }
- }
- }
- }
-
- private unsafe void UnsafeFindFpsAddress(in GameModule moduleEntryInfo)
- {
- bool readOk = UnsafeReadModulesMemory(gameProcess, moduleEntryInfo, out VirtualMemory localMemory);
- Verify.Operation(readOk, "Error reading required modules' memory: could not copy module memory to destination");
-
- using (localMemory)
- {
- int offset = IndexOfPattern(localMemory.AsSpan()[(int)moduleEntryInfo.UnityPlayer.Size..]);
- Must.Range(offset >= 0, "Error matching memory pattern: no expected content");
-
- byte* pLocalMemory = (byte*)localMemory.Pointer;
- ref readonly Module unityPlayer = ref moduleEntryInfo.UnityPlayer;
- ref readonly Module userAssembly = ref moduleEntryInfo.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(gameProcess, address, out ptr) && ptr != 0);
-
- rip = ptr - unityPlayer.Address + localMemoryUnityPlayerAddress;
-
- // CALL or JMP
- while (*(byte*)rip == 0xE8 || *(byte*)rip == 0xE9)
- {
- rip += (nuint)(*(int*)(rip + 1) + 5);
- }
-
- nuint localMemoryActualAddress = rip + *(uint*)(rip + 2) + 6;
- nuint actualOffset = localMemoryActualAddress - localMemoryUnityPlayerAddress;
- status.FpsAddress = unityPlayer.Address + actualOffset;
- }
- }
-
- private readonly struct GameModule
- {
- public readonly bool HasValue = false;
- public readonly Module UnityPlayer;
- public readonly Module UserAssembly;
-
- public GameModule(in Module unityPlayer, in Module userAssembly)
- {
- HasValue = true;
- UnityPlayer = unityPlayer;
- UserAssembly = userAssembly;
- }
- }
-
- private readonly struct Module(nuint address, uint size)
- {
- public readonly bool HasValue = true;
- public readonly nuint Address = address;
- public readonly uint Size = size;
- }
-}
-
-file static class UnmanagedMemoryExtension
-{
- public static unsafe Span AsSpan(this VirtualMemory unmanagedMemory)
- {
- return new(unmanagedMemory.Pointer, (int)unmanagedMemory.Size);
- }
-}
-
-file static class Must
-{
- [MethodImpl(MethodImplOptions.NoInlining)]
- public static void Range([DoesNotReturnIf(false)] bool condition, string? message, [CallerArgumentExpression(nameof(condition))] string? parameterName = null)
- {
- if (!condition)
- {
- throw new ArgumentOutOfRangeException(parameterName, message);
- }
- }
-}
-
-file static class Kernel32X
-{
- [DebuggerStepThrough]
- public static unsafe BOOL ReadProcessMemory(nint hProcess, nuint lpBaseAddress, Span buffer, [MaybeNull] out SizeT numberOfBytesRead)
- {
- fixed (byte* lpBuffer = buffer)
- {
- return Kernel32.ReadProcessMemory(hProcess, (nint)lpBaseAddress, (nint)lpBuffer, buffer.Length, out numberOfBytesRead);
- }
- }
-
- [DebuggerStepThrough]
- public static unsafe BOOL ReadProcessMemory(nint hProcess, nuint lpBaseAddress, ref T buffer, [MaybeNull] out SizeT numberOfBytesRead)
- where T : unmanaged
- {
- fixed (T* lpBuffer = &buffer)
- {
- return Kernel32.ReadProcessMemory(hProcess, (nint)lpBaseAddress, (nint)lpBuffer, sizeof(T), out numberOfBytesRead);
- }
- }
-
- [DebuggerStepThrough]
- public static unsafe BOOL WriteProcessMemory(nint hProcess, nuint lpBaseAddress, ref readonly T buffer, out SizeT numberOfBytesWritten)
- where T : unmanaged
- {
- fixed (T* lpBuffer = &buffer)
- {
- return Kernel32.WriteProcessMemory(hProcess, (nint)lpBaseAddress, (nint)lpBuffer, (uint)sizeof(T), out numberOfBytesWritten);
- }
- }
-
- [DllImport("KERNEL32.dll", CallingConvention = CallingConvention.Winapi, ExactSpelling = true)]
- public static unsafe extern BOOL K32EnumProcessModules(HANDLE hProcess, HMODULE* lphModule, uint cb, uint* lpcbNeeded);
-
- [DebuggerStepThrough]
- public static unsafe BOOL K32EnumProcessModules(nint hProcess, Span hModules, out uint cbNeeded)
- {
- fixed (HMODULE* lphModule = hModules)
- {
- fixed (uint* lpcbNeeded = &cbNeeded)
- {
- return K32EnumProcessModules(hProcess, lphModule, (uint)(hModules.Length * sizeof(HMODULE)), lpcbNeeded);
- }
- }
- }
-
- internal readonly struct PWSTR
- {
- public readonly unsafe char* Value;
-
- public static unsafe implicit operator PWSTR(char* value) => *(PWSTR*)&value;
-
- public static unsafe implicit operator char*(PWSTR value) => *(char**)&value;
- }
-
- [DllImport("KERNEL32.dll", CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode, ExactSpelling = true)]
- public static extern uint K32GetModuleBaseNameW(HANDLE hProcess, [AllowNull] HMODULE hModule, PWSTR lpBaseName, uint nSize);
-
- [DebuggerStepThrough]
- public static unsafe uint K32GetModuleBaseNameW(HANDLE hProcess, [AllowNull] HMODULE hModule, Span baseName)
- {
- fixed (char* lpBaseName = baseName)
- {
- return K32GetModuleBaseNameW(hProcess, hModule, lpBaseName, (uint)baseName.Length);
- }
- }
-
- [DllImport("KERNEL32.dll", CallingConvention = CallingConvention.Winapi, ExactSpelling = true)]
- public static unsafe extern BOOL K32GetModuleInformation(HANDLE hProcess, HMODULE hModule, Kernel32.MODULEINFO* lpmodinfo, uint cb);
-
- [DebuggerStepThrough]
- public static unsafe BOOL K32GetModuleInformation(HANDLE hProcess, HMODULE hModule, out Kernel32.MODULEINFO modinfo)
- {
- fixed (Kernel32.MODULEINFO* lpmodinfo = &modinfo)
- {
- return K32GetModuleInformation(hProcess, hModule, lpmodinfo, (uint)sizeof(Kernel32.MODULEINFO));
- }
- }
-}
-
-internal readonly struct HMODULE
-{
- public readonly nint Value;
-}
-
-internal readonly struct ValueResult(TResult isOk, TValue value)
-{
- public readonly TResult IsOk = isOk;
-
- public readonly TValue Value = value;
-
- public void Deconstruct(out TResult isOk, out TValue value)
- {
- isOk = IsOk;
- value = Value;
- }
-}
-
-internal readonly struct ValueStopwatch
-{
- private readonly long startTimestamp;
-
- private ValueStopwatch(long startTimestamp)
- {
- this.startTimestamp = startTimestamp;
- }
-
- public bool IsActive
- {
- get => startTimestamp != 0;
- }
-
- public static ValueStopwatch StartNew()
- {
- return new(Stopwatch.GetTimestamp());
- }
-
- public TimeSpan GetElapsedTime()
- {
- return Stopwatch.GetElapsedTime(startTimestamp);
+ options.TargetFps = targetFps;
+ await Task.Run(() => GameFpsUnlockerImpl.Start(options, pid: (uint)gameProcess.Id, cts: cts));
}
}
diff --git a/src/Desktop/Fischless.Fetch/Unlocker/GameFpsUnlockerImpl.cs b/src/Desktop/Fischless.Fetch/Unlocker/GameFpsUnlockerImpl.cs
new file mode 100644
index 0000000..88fed22
--- /dev/null
+++ b/src/Desktop/Fischless.Fetch/Unlocker/GameFpsUnlockerImpl.cs
@@ -0,0 +1,238 @@
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using System.Text;
+using Vanara.PInvoke;
+
+namespace Fischless.Fetch.Unlocker;
+
+internal class GameFpsUnlockerImpl
+{
+ private const int STILL_ACTIVE = 0x00000103;
+
+ private class DeferManager(List deferredActions) : IDisposable
+ {
+ private readonly List deferredActions = deferredActions;
+
+ public DeferManager() : this([])
+ {
+ }
+
+ public void Defer(Action action)
+ {
+ deferredActions.Add(action);
+ }
+
+ public void Dispose()
+ {
+ deferredActions.ForEach(action => action?.Invoke());
+ deferredActions.Clear();
+ }
+ }
+
+ private static class Interop
+ {
+ public static string GetLastErrorAsString(Win32Error errorCode)
+ {
+ StringBuilder messageBuffer = new(256);
+ int formatResult = Kernel32.FormatMessage(
+ Kernel32.FormatMessageFlags.FORMAT_MESSAGE_FROM_SYSTEM | Kernel32.FormatMessageFlags.FORMAT_MESSAGE_IGNORE_INSERTS,
+ IntPtr.Zero,
+ (uint)errorCode,
+ 0,
+ messageBuffer,
+ (uint)messageBuffer.Capacity,
+ IntPtr.Zero
+ );
+
+ if (formatResult == NTStatus.STATUS_SUCCESS)
+ {
+ return $"Unknown error (Code {errorCode})";
+ }
+
+ return messageBuffer.ToString().Trim();
+ }
+ }
+
+ private static bool GetModule2(Kernel32.SafeHPROCESS hProcess, string moduleName, out Kernel32.MODULEENTRY32 pEntry)
+ {
+ pEntry = new Kernel32.MODULEENTRY32 { dwSize = (uint)Marshal.SizeOf() };
+ HINSTANCE[] modules = new HINSTANCE[1024];
+
+ if (!Kernel32.EnumProcessModules(hProcess, modules, (uint)(modules.Length * Marshal.SizeOf()), out uint cbNeeded))
+ {
+ return false;
+ }
+
+ Array.Resize(ref modules, (int)(cbNeeded / IntPtr.Size));
+
+ foreach (HINSTANCE module in modules)
+ {
+ StringBuilder szModuleName = new(Kernel32.MAX_PATH);
+
+ if (Kernel32.GetModuleBaseName(hProcess, module, szModuleName, (uint)szModuleName.Capacity) == 0)
+ {
+ continue;
+ }
+
+ if (moduleName != szModuleName.ToString())
+ {
+ continue;
+ }
+
+ if (Kernel32.GetModuleInformation(hProcess, module, out Kernel32.MODULEINFO modInfo, (uint)Marshal.SizeOf()))
+ {
+ pEntry.modBaseAddr = modInfo.lpBaseOfDll;
+ pEntry.modBaseSize = modInfo.SizeOfImage;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static unsafe nint PatternScan(nint module, uint dataLength, int[] pattern)
+ {
+ byte* scanBytes = (byte*)module;
+ ulong s = (ulong)pattern.Length;
+ int[] d = pattern.Select(p => p == '?' ? -1 : p).ToArray();
+
+ for (ulong i = 0ul; i < dataLength - s; ++i)
+ {
+ bool found = true;
+ for (ulong j = 0ul; j < s; ++j)
+ {
+ if (scanBytes[i + j] != d[j] && d[j] != -1)
+ {
+ found = false;
+ break;
+ }
+ }
+ if (found)
+ {
+ return IntPtr.Add(module, (int)i);
+ }
+ }
+ return IntPtr.Zero;
+ }
+
+ public static unsafe void Start(GameFpsUnlockerOption option, string? gamePath = null, uint? pid = null, CancellationTokenSource? cts = null)
+ {
+ if (!option.TargetFps.HasValue)
+ {
+ return;
+ }
+
+ if (string.IsNullOrWhiteSpace(gamePath) && pid == null)
+ {
+ return;
+ }
+
+ int targetFps = option.TargetFps.Value;
+ using DeferManager deferManager = new();
+
+ Kernel32.SafeHPROCESS hProcess = null!;
+
+ if (pid == null)
+ {
+ hProcess = Kernel32.CreateProcess(gamePath!);
+ }
+ else
+ {
+ hProcess = Kernel32.OpenProcess(new ACCESS_MASK(Kernel32.ProcessAccess.PROCESS_ALL_ACCESS), false, pid.Value);
+ }
+
+ if (hProcess.IsInvalid)
+ {
+ Debug.WriteLine($"[Unlocker] CreateProcess failed with {gamePath}");
+ return;
+ }
+ else
+ {
+ deferManager.Defer(() => { using (hProcess) { } });
+ }
+
+ int foundLimit = 0;
+ Kernel32.MODULEENTRY32 hUnityPlayer;
+ Thread.Sleep(option.FindModuleDelay);
+ while (!GetModule2(hProcess, "UnityPlayer.dll", out hUnityPlayer))
+ {
+ if (cts?.Token.IsCancellationRequested ?? false)
+ {
+ return;
+ }
+
+ foundLimit += option.FindModuleDelay;
+ if (foundLimit > option.FindModuleLimit)
+ {
+ Debug.WriteLine($"[Unlocker] GetModule2 failed in {option.FindModuleLimit} ms");
+ break;
+ }
+
+ Thread.Sleep(option.FindModuleDelay);
+ }
+
+ Debug.WriteLine($"[Unlocker] UnityPlayer: {hUnityPlayer.modBaseAddr.ToInt64()}");
+
+ nint up = Kernel32.VirtualAlloc(IntPtr.Zero, hUnityPlayer.modBaseSize, Kernel32.MEM_ALLOCATION_TYPE.MEM_COMMIT | Kernel32.MEM_ALLOCATION_TYPE.MEM_RESERVE, Kernel32.MEM_PROTECTION.PAGE_READWRITE);
+
+ if (up == IntPtr.Zero)
+ {
+ Win32Error code = Kernel32.GetLastError();
+ Debug.WriteLine($"[Unlocker] VirtualAlloc UP failed ({code}): {Interop.GetLastErrorAsString(code)}");
+ return;
+ }
+ else
+ {
+ deferManager.Defer(() => Kernel32.VirtualFree(up, hUnityPlayer.modBaseSize, Kernel32.MEM_ALLOCATION_TYPE.MEM_COMMIT | Kernel32.MEM_ALLOCATION_TYPE.MEM_RESERVE));
+ }
+
+ if (!Kernel32.ReadProcessMemory(hProcess, hUnityPlayer.modBaseAddr, up, hUnityPlayer.modBaseSize, out _))
+ {
+ Win32Error code = Kernel32.GetLastError();
+ Debug.WriteLine($"[Unlocker] ReadProcessMemory unity failed ({code}): {Interop.GetLastErrorAsString(code)}");
+ return;
+ }
+
+ Debug.WriteLine("[Unlocker] Searching for pattern...");
+
+ nint address = PatternScan(up, hUnityPlayer.modBaseSize, [0x7F, 0x0E, 0xE8, '?', '?', '?', '?', 0x66, 0x0F, 0x6E, 0xC8]);
+
+ if (address == IntPtr.Zero)
+ {
+ Debug.WriteLine("[Unlocker] outdated pattern");
+ return;
+ }
+
+ nint pfps = 0;
+ {
+ nint rip = address;
+ rip += 3;
+ rip += *(int*)(rip) + 6;
+ rip += *(int*)(rip) + 4;
+ pfps = rip - up + hUnityPlayer.modBaseAddr;
+ Debug.WriteLine($"[Unlocker] FPS Offset: {pfps}");
+ }
+
+ uint exitCode = STILL_ACTIVE;
+ while (exitCode == STILL_ACTIVE)
+ {
+ if (cts?.Token.IsCancellationRequested ?? false)
+ {
+ return;
+ }
+
+ Kernel32.GetExitCodeProcess(hProcess, out exitCode);
+ Thread.Sleep(option.FpsDelay);
+
+ int fps = 0;
+ Kernel32.ReadProcessMemory(hProcess, pfps, new IntPtr(&fps), sizeof(int), out _);
+ if (fps == -1)
+ {
+ continue;
+ }
+ if (fps != targetFps)
+ {
+ Kernel32.WriteProcessMemory(hProcess, pfps, new IntPtr(&targetFps), sizeof(int), out _);
+ }
+ }
+ }
+}
diff --git a/src/Desktop/Fischless.Fetch/Unlocker/IGameFpsUnlocker.cs b/src/Desktop/Fischless.Fetch/Unlocker/IGameFpsUnlocker.cs
deleted file mode 100644
index e9350ec..0000000
--- a/src/Desktop/Fischless.Fetch/Unlocker/IGameFpsUnlocker.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace Fischless.Fetch.Unlocker;
-
-internal interface IGameFpsUnlocker
-{
- ValueTask UnlockAsync(UnlockTimingOptions options, IProgress progress, CancellationToken token = default);
-}
diff --git a/src/Desktop/Fischless.Fetch/Unlocker/LICENSE b/src/Desktop/Fischless.Fetch/Unlocker/LICENSE
deleted file mode 100644
index a83b962..0000000
--- a/src/Desktop/Fischless.Fetch/Unlocker/LICENSE
+++ /dev/null
@@ -1,21 +0,0 @@
-MIT License
-
-Copyright (c) 2022 DGP Studio
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
diff --git a/src/Desktop/Fischless.Fetch/Unlocker/UnlockTimingOptions.cs b/src/Desktop/Fischless.Fetch/Unlocker/UnlockTimingOptions.cs
deleted file mode 100644
index 9e6584f..0000000
--- a/src/Desktop/Fischless.Fetch/Unlocker/UnlockTimingOptions.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace Fischless.Fetch.Unlocker;
-
-internal readonly struct UnlockTimingOptions(int findModuleDelayMilliseconds, int findModuleLimitMilliseconds, int adjustFpsDelayMilliseconds)
-{
- public readonly TimeSpan FindModuleDelay = TimeSpan.FromMilliseconds(findModuleDelayMilliseconds);
-
- public readonly TimeSpan FindModuleLimit = TimeSpan.FromMilliseconds(findModuleLimitMilliseconds);
-
- public readonly TimeSpan AdjustFpsDelay = TimeSpan.FromMilliseconds(adjustFpsDelayMilliseconds);
-}
diff --git a/src/Desktop/Fischless.Fetch/Unlocker/UnlockerOption.cs b/src/Desktop/Fischless.Fetch/Unlocker/UnlockerOption.cs
new file mode 100644
index 0000000..01de9f3
--- /dev/null
+++ b/src/Desktop/Fischless.Fetch/Unlocker/UnlockerOption.cs
@@ -0,0 +1,14 @@
+namespace Fischless.Fetch.Unlocker;
+
+public sealed class GameFpsUnlockerOption
+{
+ public static Lazy Default { get; } = new();
+
+ public int? TargetFps { get; set; } = 120;
+
+ public int FindModuleDelay { get; set; } = 100;
+
+ public int FindModuleLimit { get; set; } = 2000;
+
+ public int FpsDelay { get; set; } = 2000;
+}
diff --git a/src/Desktop/Fischless.Fetch/Unlocker/UnlockerStatus.cs b/src/Desktop/Fischless.Fetch/Unlocker/UnlockerStatus.cs
deleted file mode 100644
index 949ff3e..0000000
--- a/src/Desktop/Fischless.Fetch/Unlocker/UnlockerStatus.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-namespace Fischless.Fetch.Unlocker;
-
-internal sealed class UnlockerStatus
-{
- public string Description { get; set; } = default!;
-
- public FindModuleResult FindModuleState { get; set; }
-
- public bool IsUnlockerValid { get; set; } = true;
-
- public nuint FpsAddress { get; set; }
-}
diff --git a/src/Desktop/Fischless.Fetch/Unlocker/VirtualMemory.cs b/src/Desktop/Fischless.Fetch/Unlocker/VirtualMemory.cs
deleted file mode 100644
index 9f459d2..0000000
--- a/src/Desktop/Fischless.Fetch/Unlocker/VirtualMemory.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using System.Runtime.InteropServices;
-
-namespace Fischless.Fetch.Unlocker;
-
-internal readonly unsafe struct VirtualMemory(uint dwSize) : IDisposable
-{
- private readonly uint size = dwSize;
- private readonly void* pointer = NativeMemory.Alloc(dwSize);
-
- public uint Size { get => size; }
- public void* Pointer { get => pointer; }
-
- public void Dispose()
- {
- NativeMemory.Free(pointer);
- }
-}