From e9b04e1f329f8d2ae1510e68259af152b6233304 Mon Sep 17 00:00:00 2001 From: uberhalit Date: Sun, 7 Apr 2019 18:06:06 +0200 Subject: [PATCH] added disable cam adjust option added code cave generator for cam adjust patternscan generates its own masks from pattern now some housekeeping updated readme --- README.md | 44 ++- SekiroFpsUnlockAndMore.sln.DotSettings.user | 6 + SekiroFpsUnlockAndMore/CodeCaveGenerator.cs | 342 ++++++++++++++++++ SekiroFpsUnlockAndMore/GameData.cs | 105 ++++-- SekiroFpsUnlockAndMore/MainWindow.xaml | 11 +- SekiroFpsUnlockAndMore/MainWindow.xaml.cs | 212 +++++++---- SekiroFpsUnlockAndMore/PatternScan.cs | 107 ++---- .../Properties/AssemblyInfo.cs | 6 +- .../SekiroFpsUnlockAndMore.csproj | 1 + SekiroFpsUnlockAndMore/SettingsService.cs | 3 + 10 files changed, 653 insertions(+), 184 deletions(-) create mode 100644 SekiroFpsUnlockAndMore.sln.DotSettings.user create mode 100644 SekiroFpsUnlockAndMore/CodeCaveGenerator.cs diff --git a/README.md b/README.md index 3979610..6edf2f0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Sekiro FPS Unlocker and more -A small utility to remove frame rate limit, add custom resolutions with 21/9 widescreen support, change field of view (FOV), borderless window mode and various game modifications for [Sekiro: Shadows Die Twice](https://www.sekirothegame.com/) written in C#. -Patches games memory while running, does not modify any game files. Works with every game version (legit steam & oh-not-so-legit), should work with all future updates. Also available [Nexus Mods](https://www.nexusmods.com/sekiro/mods/13/). +A small utility to remove frame rate limit, add custom resolutions with 21/9 widescreen support, change field of view (FOV), borderless window mode, display and log stats (OBS), diSable camera auto adjust on movement and various game modifications for [Sekiro: Shadows Die Twice](https://www.sekirothegame.com/) written in C#. +Patches games memory while running, does not modify any game files. Works with every game version (legit steam & oh-not-so-legit), should work with all future updates. Also available on [Nexus Mods](https://www.nexusmods.com/sekiro/mods/13/). ## Download @@ -19,6 +19,7 @@ Patches games memory while running, does not modify any game files. Works with e * add a custom resolution, 21/9 widescreen supported (will overwrite the default 1920x1080 / 1280x720 resolution, HUD limited to 16/9) * increase and decrease field of view (FOV) * set the game to borderless window mode +* disable camera auto rotate adjustment on movement (annoying for mouse users) * display hidden counters such as death/kill count and optionally log them to file to display in OBS * game modifications * global game speed modifier @@ -29,12 +30,12 @@ Patches games memory while running, does not modify any game files. Works with e ## Usage -The following graphical guide has to be done if you want to unlock the game's framerate or play on a highter refresh rate in fullscreen. If you do not wish to use that feature you can scoll down further to the guides on all other features. The graphic setup has to be done only once but as the patcher hot-patches the memory **you have to start the patcher every time you want use any of its features**. +The following graphical guide has to be done if you want to unlock the game's framerate or play on a higher refresh rate in fullscreen. If you do not wish to use that feature you can scroll down further to the guides on all other features. The graphic setup has to be done only once but as the patcher hot-patches the memory **you have to start the patcher every time you want use any of its features**. The game enforces VSYNC and forces 60 Hz in fullscreen even on 144 Hz monitors so we have to override these. -#### TL;DR Nvidia: Use Nvidia Control Panel to set 'Vsync' to 'Off' and 'Preferred Refreshrate' to 'Highest available' on a Sekiro Profile. Troubleshoot: delete the (premade) Sekiro profile, add a new profile by stating the full file path to sekiro.exe and try again. If Preferred Refreshrate is missing or game still locks to 60fps see the guide further down on Nvidia Profile Inspector. -#### TL;DR AMD: Use Radeon Settings to set 'Wait for Vertical Refresh' to 'Enhanced Sync' on a Sekiro profile. Start Sekiro in windowed mode and switch to fullscreen once ingame. +#### TL;DR Nvidia: Use Nvidia Control Panel to set 'Vsync' to 'Off' and 'Preferred Refreshrate' to 'Highest available' on a Sekiro Profile. Troubleshoot: delete the (premade) Sekiro profile, add a new profile by stating the full file path to sekiro.exe and try again. If Preferred Refreshrate is missing or game still locks to 60fps see the guide further down on Nvidia Profile Inspector an general troubleshooting. +#### TL;DR AMD: Use Radeon Settings to set 'Wait for Vertical Refresh' to 'Enhanced Sync' on a Sekiro profile. Start Sekiro in windowed mode and switch to fullscreen once ingame. Troubleshoot: see the guide further down below. #### 60 Hz monitors: disable VSYNC via driver (use 'Enhanced Sync' on AMD) and use fullscreen, see guide below #### high refresh rate monitors: use borderless or force monitor to always use highest available refresh rate and then use fullscreen, see guide below @@ -73,7 +74,7 @@ The game enforces VSYNC and forces 60 Hz in fullscreen even on 144 Hz monitors s ### Follow these steps on AMD: 1. Right click on Desktop -> `Display settings` -2. Scroll down an click `Advanced Display Settings -> Display Adapter Properties` +2. Scroll down anD click `Advanced Display Settings -> Display Adapter Properties` 3. **Switch to `Monitor` tab and make sure your monitor is set to the highest Refresh rate possible:** 4. [![Make sure your monitor is set to the highest Refresh rate possible](https://camo.githubusercontent.com/8ba71a0b512eb68509f7e7506a92a78f3cd47537/68747470733a2f2f692e696d6775722e636f6d2f61774b4862774d2e706e67)](#) 5. Open Radeon Settings @@ -102,6 +103,24 @@ The game enforces VSYNC and forces 60 Hz in fullscreen even on 144 Hz monitors s 10. Start the game and set it to Fullscreen 11. Enjoy perfectly tearing free variable high refresh rates without VSYNC +### Troubleshooting: +* Make sure you followed the appreciate steps and didn't skip any (especially not the deletion of the Sekiro profile!) +* Try disabling `Fullscreen optimization` for Sekiro: right mouse click on `sekiro.exe -> Compatibility-> tick 'Disable fullscreen optimizations'` +* Try adding the whole game folder and `Sekiro FPS Unlocker and more` to your antivirus's exclusion list +* Try disabling `Steam Broadcast` (streaming via overlay) +* Do a clean reinstall of your graphic driver: + 1. Download latest graphic drivers for your GPU + 2. Download [DDU](https://www.guru3d.com/files-get/display-driver-uninstaller-download,1.html) + 3. Disconnect internet so windows update won't auto-install minimal driver as soon as you uninstall them + 4. Boot into safe mode + 5. Completely uninstall graphic drivers and all utilities using DDU + 6. Reboot + 7. Install the latest driver + 8. Reconnect internet +* Close and disable all screen recording and streaming applications +* Close and disable all overlays +* Close and disable all performance "booster" programs and alike + ### To add a custom resolution 1. Start the game 2. Start `Sekiro FPS Unlocker and more`, set you desired resolution and enable it by ticking the check box @@ -137,6 +156,7 @@ The game enforces VSYNC and forces 60 Hz in fullscreen even on 144 Hz monitors s 2. Load up your save game 3. Start `Sekiro FPS Unlocker and more` and expand `Game modifications` 4. Set your desired values and then tick the checkbox you'd wish to enable +5. Be aware that player and game speed modifications can potentially crash the game in certain cutscenes and NPC interactions ## Preview @@ -156,7 +176,7 @@ The game enforces VSYNC and forces 60 Hz in fullscreen even on 144 Hz monitors s ## Building -Use Visual Studio 2017 to build +Use Visual Studio 2017 or newer to build ## Contributing @@ -169,6 +189,7 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file ## Credits * Me_TheCat for his contribution to log stats and display them in OBS +* Cielos for some camera adjustment offsets * Zullie the Witch#7202 for game speed and player speed modifier offsets * jackfuste for FOV offset and basic running speed fix * TyChii93#2376 for AMD and widescreen testing @@ -176,15 +197,20 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file ## Limitations -* the game has forced VSYNC so unlocking the frame rate when your monitor has 60Hz will do nothing. You'll have to disable VSYNC in Nvidia Control Panel or AMD Radeon Settings first +* the game has forced VSYNC so unlocking the frame rate when your monitor has 60Hz will do nothing. You'll have to disable VSYNC in Nvidia Control Panel or AMD Radeon Settings first, see Usage * in fullscreen the game forces the monitor to 60 Hz so you'll have to handle this with driver override too, see Usage * your monitor has to support your custom resolution otherwise it won't show up correctly -* due to how the game renders altering HUD is limited to 16/9 even on 21/9 resolutions +* due to how the game renders altering HUD is limited to 16:9 even on 21:9 resolutions * Player speed modification needs a loaded save before it can be activated +* Player and game speed modification can potentially crash the game in certain cutscenes and NPC interactions, use with caution * the hotkey won't work if the game runs in exclusive, true fullscreen mode ## Version History +* v1.2.1 (2019-04-07) + * Added an option to disable automatic camera rotation adjust on movement (thanks to Cielos for some offsets) + * Added +25% FOV option + * Improved initial load time and patching speed * v1.2.0 (2019-04-02) * Added stats (kills & deaths for now) with an option to log them to file for display in OBS (thanks to Me_TheCat for his contribution) * Player speed modifier will stick between quicktravel now diff --git a/SekiroFpsUnlockAndMore.sln.DotSettings.user b/SekiroFpsUnlockAndMore.sln.DotSettings.user new file mode 100644 index 0000000..708f005 --- /dev/null +++ b/SekiroFpsUnlockAndMore.sln.DotSettings.user @@ -0,0 +1,6 @@ + + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="PATTERN_" Suffix="" Style="AA_BB" /> + <Policy Inspect="True" Prefix="WS_" Suffix="" Style="AA_BB"><ExtraRule Prefix="" Suffix="" Style="AA_BB" /></Policy> + 2 \ No newline at end of file diff --git a/SekiroFpsUnlockAndMore/CodeCaveGenerator.cs b/SekiroFpsUnlockAndMore/CodeCaveGenerator.cs new file mode 100644 index 0000000..a5f27d0 --- /dev/null +++ b/SekiroFpsUnlockAndMore/CodeCaveGenerator.cs @@ -0,0 +1,342 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace SekiroFpsUnlockAndMore +{ + class CodeCaveGenerator + { + private class CodeCave + { + internal readonly long InstructionAddress; + internal readonly int OverwriteLength; + internal readonly long CodeCaveAddress; + internal bool Active; + private byte[] _originalInstructionset; + internal byte[] OriginalInstructionset + { + get => _originalInstructionset; + set + { + if (_originalInstructionset == null) + _originalInstructionset = value; + } + } + + internal CodeCave(long instructionAddress, int overwriteLength, long codeCaveAddress) + { + InstructionAddress = instructionAddress; + OverwriteLength = overwriteLength; + CodeCaveAddress = codeCaveAddress; + Active = false; + _originalInstructionset = null; + } + } + + private Dictionary _codeCaves; + private static IntPtr _hProcess; + private static long _lpBaseAddress; + + /// + /// Initialize functionality to create and manage code caves in given process's memory. + /// + /// The handle to the process, needs all access flag. + /// The base address of the process. + internal CodeCaveGenerator(IntPtr hProcess, long lpBaseAddress) + { + _codeCaves = new Dictionary(); + _hProcess = hProcess; + _lpBaseAddress = lpBaseAddress; + } + + /// + /// Creates a new code cave with a unique name. + /// + /// The unique name of the code cave. Used to enable and disable caves. + /// The address of the instruction that should be overwritten with jump. + /// The length of the opcodes from lpInstructionAddress that should be overwritten with NOP, must be at least 5 bytes. + /// The shellcode to place inside the code cave. + /// If overwritten instructions should be copied to the end of the code cave. + /// True if code cave has been successfully created and is ready to be activated, false otherwise. + internal bool CreateNewCodeCave(string szCaveName, long lpInstructionAddress, int dwOverwriteLength, byte[] cbCodeInject, bool bCopyOverwrittenInstructions = false) + { + if (_codeCaves.ContainsKey(szCaveName)) + { + DeactivateCodeCaveByName(szCaveName); + _codeCaves.Remove(szCaveName); + } + long caveAddress = CreateCodeCaveForInstruction(_hProcess, _lpBaseAddress, lpInstructionAddress, dwOverwriteLength, cbCodeInject, bCopyOverwrittenInstructions); + if (caveAddress < 0) + return false; + _codeCaves.Add(szCaveName, new CodeCave(lpInstructionAddress, dwOverwriteLength, caveAddress)); + return true; + } + + /// + /// Activates a code cave by name. + /// + /// The unique name of the code cave. + /// True if code cave could be activated. + internal bool ActivateCodeCaveByName(string szCaveName) + { + if (!_codeCaves.ContainsKey(szCaveName) || _codeCaves[szCaveName].Active) + return false; + + byte[] originalInstructionset = ActivateCodeCaveForInstruction(_hProcess, _codeCaves[szCaveName].InstructionAddress, _codeCaves[szCaveName].OverwriteLength, _codeCaves[szCaveName].CodeCaveAddress); + if (originalInstructionset.Length != _codeCaves[szCaveName].OverwriteLength) + return false; + _codeCaves[szCaveName].OriginalInstructionset = originalInstructionset; + _codeCaves[szCaveName].Active = true; + return true; + } + + /// + /// Deactivates a code cave by name. + /// + /// The unique name of the code cave. + /// True if code cave could be deactivated. + internal bool DeactivateCodeCaveByName(string szCaveName) + { + if (!_codeCaves.ContainsKey(szCaveName) || !_codeCaves[szCaveName].Active || _codeCaves[szCaveName].InstructionAddress < 0 || _codeCaves[szCaveName].OriginalInstructionset == null) + return false; + + if (!WriteProcessMemory(_hProcess, _codeCaves[szCaveName].InstructionAddress, _codeCaves[szCaveName].OriginalInstructionset, (ulong) _codeCaves[szCaveName].OriginalInstructionset.Length, out IntPtr lpNumberOfBytesWritten) || lpNumberOfBytesWritten.ToInt32() != _codeCaves[szCaveName].OriginalInstructionset.Length) + { + MainWindow.LogToFile("Could not disable code cave in CodeCaveGenerator()!"); + return false; + } + _codeCaves[szCaveName].Active = false; + return true; + } + + /// + /// Gets the code cave address of an already created cave by name. + /// + /// The unique name of the code cave. + /// The address of the code cave, -1 if none found. + internal long GetCodeCaveAddressByName(string szCaveName) + { + if (!_codeCaves.ContainsKey(szCaveName) || _codeCaves[szCaveName].CodeCaveAddress == 0) + return -1; + return _codeCaves[szCaveName].CodeCaveAddress; + } + + /// + /// Clears all saved code caves. Does not deactivate them. + /// + internal void ClearCodeCaves() + { + _codeCaves.Clear(); + } + + /// + /// Creates a code cave to inject code within given process's memory in reach of given instruction address. + /// Does not activate the code cave yet. + /// + /// The handle to the process, needs all access flag. + /// The base address of the process. + /// The address of the instruction that should be overwritten with jump. + /// The length of the opcodes from lpInstructionAddress that should be overwritten with NOP, must be at least 5 bytes. + /// The shellcode to place inside the code cave. + /// If overwritten instructions should be copied to the end of the code cave. + /// Uses a relative 5 bytes jump instruction, will fail if there is no free memory within signed integer range (4bytes) from lpInstructionAddress. + /// The address of the beginning of the code cave, -1 if operation failed. + private static long CreateCodeCaveForInstruction(IntPtr hProcess, long lpBaseAddress, long lpInstructionAddress, int dwOverwriteLength, byte[] cbCodeInject, bool bCopyOverwrittenInstructions = false) + { + if (IntPtr.Size != 8) + throw new Exception("Only x64 is supported!"); + + if (dwOverwriteLength < 5) + throw new Exception("dwOverwriteLength must be at least 5 bytes!"); + + // read instructions to replace with jump + byte[] cbOriginalInstructionset = new byte[dwOverwriteLength]; + if (!ReadProcessMemory(hProcess, lpInstructionAddress, cbOriginalInstructionset, (ulong)dwOverwriteLength, out IntPtr lpNumberOfBytesRead) || lpNumberOfBytesRead.ToInt32() != dwOverwriteLength) + { + MainWindow.LogToFile("Failed to read original instruction set in CodeCaveGenerator()!"); + return -1; + } + + SYSTEM_INFO si = new SYSTEM_INFO(); + GetSystemInfo(out si); + + // find lowest and highest possible address in jump range from lpInstructionAddress + int iMinimalCaveSize = 8 * (int)Math.Round((cbCodeInject.Length + dwOverwriteLength + 6) / 8.0); // nearest size rounded to 8 bytes + if (iMinimalCaveSize < 32) iMinimalCaveSize = 32; + long lpMinimalJmpAddress = lpInstructionAddress + 6 - 0x70000000; // Int32.MinValue + a little buffer overhead + long lpMaximumJmpAddress = lpInstructionAddress + 0x70000000 - iMinimalCaveSize; // Int32.MaxValue + a little buffer overhead + if (lpMinimalJmpAddress < si.lpMinimumApplicationAddress) lpMinimalJmpAddress = si.lpMinimumApplicationAddress; + if (lpMaximumJmpAddress > si.lpMaximumApplicationAddress - iMinimalCaveSize) lpMaximumJmpAddress = si.lpMaximumApplicationAddress - iMinimalCaveSize; + + // determine lowest possible memory block we could allocate for cave assuming base address lies at the beginning of a memory block + long lpPreferredAddress = lpBaseAddress - (int)((lpBaseAddress - lpMinimalJmpAddress) / si.dwAllocationGranularity) * si.dwAllocationGranularity; + + // find lowest useable memory block and allocate it + long lpAllocationAddress = 0; + MEMORY_BASIC_INFORMATION64 mbi = new MEMORY_BASIC_INFORMATION64(); + while (lpPreferredAddress < lpMaximumJmpAddress) + { + if (VirtualQueryEx(hProcess, new IntPtr(lpPreferredAddress), out mbi, MEMORY_BASIC_INFORMATION64_LENGTH) != MEMORY_BASIC_INFORMATION64_LENGTH) + break; + + if (mbi.State == MEM_FREE && mbi.RegionSize > (ulong)iMinimalCaveSize) + { + lpAllocationAddress = VirtualAllocEx(hProcess, new IntPtr((long)mbi.BaseAddress), (uint)iMinimalCaveSize, ALLOCATIONTYPE_RESERVE | ALLOCATIONTYPE_COMMIT, MEMORYPROTECTION_EXECUTEREADWRITE); + if (lpAllocationAddress > 0) + break; + } + lpPreferredAddress = (long)mbi.BaseAddress + si.dwAllocationGranularity; + } + if (lpAllocationAddress == 0) + { + MainWindow.LogToFile("No usable memory region found or failed to allocate memory for code cave in CodeCaveGenerator()!"); + return -1; + } + + // calculate jump from cave to back to where we came from + uint lpRelativePointerFromCave = (uint)((lpInstructionAddress + dwOverwriteLength) - (lpAllocationAddress + cbCodeInject.Length + (bCopyOverwrittenInstructions ? dwOverwriteLength : 0))) - 5; + + // generate jump instruction + byte[] cbJumpInstruction = new byte[] { 0xE9 }; // JMP relative to RIP + byte[] cbJumpFromCaveInstruction = cbJumpInstruction.Concat(BitConverter.GetBytes(lpRelativePointerFromCave)).ToArray(); + + // generate instructions for cave + byte[] cbCaveInstructions = cbCodeInject; + if (bCopyOverwrittenInstructions) + cbCaveInstructions = cbCaveInstructions.Concat(cbOriginalInstructionset).ToArray(); + cbCaveInstructions = cbCaveInstructions.Concat(cbJumpFromCaveInstruction).ToArray(); + + // fill code cave with instructions + if (!WriteProcessMemory(hProcess, lpAllocationAddress, cbCaveInstructions, (ulong)cbCaveInstructions.Length, out IntPtr lpNumberOfBytesWritten) || lpNumberOfBytesWritten.ToInt32() != cbCaveInstructions.Length) + { + MainWindow.LogToFile("Failed to fill code cave in CodeCaveGenerator()!"); + return -1; + } + + return lpAllocationAddress; + } + + /// + /// Creates a minimal code cave to inject code within given process's memory at given instruction address. + /// Uses a relative 5 bytes jump instruction, will fail if there is no free memory within signed integer range (4bytes) from lpInstructionAddress. + /// + /// The handle to the process, needs all access flag. + /// The address of the instruction that will be overwritten with jump. + /// The length of the opcodes from lpInstructionAddress that should be overwritten with NOP, must be at least 5 bytes. + /// The address of the previously created code cave to where the jump should lead to. + /// The overwritten instructions. + private static byte[] ActivateCodeCaveForInstruction(IntPtr hProcess, long lpInstructionAddress, int dwOverwriteLength, long lpCaveAddress) + { + // read instructions to replace with jump + byte[] cbOriginalInstructionset = new byte[dwOverwriteLength]; + if (!ReadProcessMemory(hProcess, lpInstructionAddress, cbOriginalInstructionset, (ulong)dwOverwriteLength, out IntPtr lpNumberOfBytesRead) || lpNumberOfBytesRead.ToInt32() != dwOverwriteLength) + { + MainWindow.LogToFile("Failed to read original instruction set in CodeCaveGenerator()!"); + return null; + } + + // calculate relative jump offset + uint lpRelativePointerToCave = (uint)(lpCaveAddress - lpInstructionAddress) - 5; + + // generate jump instruction and fill rest with NOPs + byte[] cbJumpInstruction = new byte[] { 0xE9 }; // JMP relative to RIP + byte[] cbJumpToCaveInstruction = cbJumpInstruction.Concat(BitConverter.GetBytes(lpRelativePointerToCave)).ToArray(); + if (dwOverwriteLength > 5) + { + byte[] cbOverwrite = new byte[dwOverwriteLength - 5]; + for (int i = 0; i < cbOverwrite.Length; i++) + cbOverwrite[i] = 0x90; // NOP + cbJumpToCaveInstruction = cbJumpToCaveInstruction.Concat(cbOverwrite).ToArray(); + } + + // write jump instruction to target address + if (!WriteProcessMemory(hProcess, lpInstructionAddress, cbJumpToCaveInstruction, (ulong)cbJumpToCaveInstruction.Length, out IntPtr lpNumberOfBytesWritten) || lpNumberOfBytesWritten.ToInt32() != cbJumpToCaveInstruction.Length) + { + MainWindow.LogToFile("Failed to overwrite target instructions in CodeCaveGenerator()!"); + return null; + } + + return cbOriginalInstructionset; + } + + #region WINAPI + + private const uint ALLOCATIONTYPE_COMMIT = 0x1000; + private const uint ALLOCATIONTYPE_RESERVE = 0x2000; + private const uint MEMORYPROTECTION_EXECUTEREADWRITE = 0x40; + private const int MEMORY_BASIC_INFORMATION64_LENGTH = 48; + private const int MEM_FREE = 0x10000; + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern IntPtr OpenProcess( + UInt32 dwDesiredAccess, + Boolean bInheritHandle, + UInt32 dwProcessId); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern Boolean CloseHandle(IntPtr hObject); + + [DllImport("kernel32.dll", SetLastError = false)] + private static extern void GetSystemInfo(out SYSTEM_INFO lpSystemInfo); + + [StructLayout(LayoutKind.Sequential)] + internal struct SYSTEM_INFO + { + public UInt16 wProcessorArchitecture; + public UInt16 wReserved; + public UInt32 dwPageSize; + public Int64 lpMinimumApplicationAddress; + public Int64 lpMaximumApplicationAddress; + public IntPtr dwActiveProcessorMask; + public UInt32 dwNumberOfProcessors; + public UInt32 dwProcessorType; + public UInt32 dwAllocationGranularity; + public UInt16 wProcessorLevel; + public UInt16 wProcessorRevision; + } + + [DllImport("kernel32.dll")] + private static extern Int32 VirtualQueryEx(IntPtr hProcess, IntPtr lpAddress, out MEMORY_BASIC_INFORMATION64 lpBuffer, uint dwLength); + + [StructLayout(LayoutKind.Sequential)] + internal struct MEMORY_BASIC_INFORMATION64 + { + public UInt64 BaseAddress; + public UInt64 AllocationBase; + public Int32 AllocationProtect; + public Int32 __alignment1; + public UInt64 RegionSize; + public Int32 State; + public Int32 Protect; + public Int32 Type; + public Int32 __alignment2; + } + + [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)] + private static extern Int64 VirtualAllocEx( + IntPtr hProcess, + IntPtr lpAddress, + uint dwSize, + UInt32 flAllocationType, + UInt32 flProtect); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern Boolean ReadProcessMemory( + IntPtr hProcess, + Int64 lpBaseAddress, + [Out] Byte[] lpBuffer, + UInt64 dwSize, + out IntPtr lpNumberOfBytesRead); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool WriteProcessMemory( + IntPtr hProcess, + Int64 lpBaseAddress, + [In, Out] Byte[] lpBuffer, + UInt64 dwSize, + out IntPtr lpNumberOfBytesWritten); + + #endregion + } +} diff --git a/SekiroFpsUnlockAndMore/GameData.cs b/SekiroFpsUnlockAndMore/GameData.cs index d93b518..02e5d09 100644 --- a/SekiroFpsUnlockAndMore/GameData.cs +++ b/SekiroFpsUnlockAndMore/GameData.cs @@ -17,11 +17,9 @@ namespace SekiroFpsUnlockAndMore 000000014116168D | C743 18 8988883C | mov dword ptr ds:[rbx+18],3C888889 | fFrameTick 0000000141161694 | 4C:89AB 70020000 | mov qword ptr ds:[rbx+270],r13 | */ - internal const string PATTERN_FRAMELOCK = "88 88 3C 4C 89 AB"; // 88 88 3C 4C 89 AB // first byte (last in mem) can can be 88/90 instead of 89 due to precision loss on floating point numbers - internal const string PATTERN_FRAMELOCK_MASK = "xxxxxx"; // mask for frame rate limiter signature scanning + internal const string PATTERN_FRAMELOCK = "88 88 3C 4C 89 AB"; // first byte (last in mem) can can be 88/90 instead of 89 due to precision loss on floating point numbers internal const int PATTERN_FRAMELOCK_OFFSET = -1; // offset to byte array from found position - internal const string PATTERN_FRAMELOCK_FUZZY = "C7 43 00 00 00 00 00 4C 89 AB"; // C7 43 ?? ?? ?? ?? ?? 4C 89 AB - internal const string PATTERN_FRAMELOCK_FUZZY_MASK = "xx?????xxx"; + internal const string PATTERN_FRAMELOCK_FUZZY = "C7 43 ?? ?? ?? ?? ?? 4C 89 AB"; internal const int PATTERN_FRAMELOCK_FUZZY_OFFSET = 3; @@ -30,8 +28,7 @@ namespace SekiroFpsUnlockAndMore fFrametimeCriticalRunningSpeed should be roughly half the frame rate: 30 @ 60FPS limit, 50 @ 100FPS limit... 00000001407D4E08 | F3:0F5905 90309202 | mulss xmm0,dword ptr ds:[1430F7EA0] | pFrametimeRunningSpeed->fFrametimeCriticalRunningSpeed */ - internal const string PATTERN_FRAMELOCK_SPEED_FIX = "F3 0F 59 05 00 30 92 02"; // F3 0F 59 05 ?? 30 92 02 - internal const string PATTERN_FRAMELOCK_SPEED_FIX_MASK = "xxxx?xxx"; + internal const string PATTERN_FRAMELOCK_SPEED_FIX = "F3 0F 59 05 ?? 30 92 02"; internal const int PATTERN_FRAMELOCK_SPEED_FIX_OFFSET = 4; /** 00000001430F7E10 @@ -39,7 +36,7 @@ namespace SekiroFpsUnlockAndMore Value: Value resolve in float table from pFrametimeRunningSpeed->fFrametimeCriticalRunningSpeed Hardcoded cause lazy -> if anyone knows how the table is calculated then tell me and I'll buy you a beer */ - internal static Dictionary PATCH_FRAMELOCK_SPEED_FIX_MATRIX = new Dictionary + internal static readonly Dictionary PATCH_FRAMELOCK_SPEED_FIX_MATRIX = new Dictionary { { new byte[1] {0x68}, 15f }, { new byte[1] {0x6C}, 16f }, @@ -77,7 +74,7 @@ namespace SekiroFpsUnlockAndMore { new byte[1] {0xEC}, 144f }, { new byte[1] {0xF0}, 150f } }; - internal static byte[] PATCH_FRAMELOCK_SPEED_FIX_DISABLE = new byte[1] { 0x90 }; // 30f + internal static readonly byte[] PATCH_FRAMELOCK_SPEED_FIX_DISABLE = new byte[1] { 0x90 }; // 30f /// /// Finds closest valid speed fix value for a frame rate limit. /// @@ -103,8 +100,7 @@ namespace SekiroFpsUnlockAndMore 000000014114AC8E | 0F57C9 | xorps xmm1,xmm1 | 000000014114AC91 | 8915 8D147D02 | mov dword ptr ds:[14391C124],edx | pCurrentResolutionHeight->iInternalGameHeight */ - internal const string PATTERN_RESOLUTION_POINTER = "0F 57 D2 89 0D 00 00 00 00 0F 57 C9"; // 0F 57 D2 89 0D ?? ?? ?? ?? 0F 57 C9 - internal const string PATTERN_RESOLUTION_POINTER_MASK = "xxxxx????xxx"; + internal const string PATTERN_RESOLUTION_POINTER = "0F 57 D2 89 0D ?? ?? ?? ?? 0F 57 C9"; internal const int PATTERN_RESOLUTION_POINTER_OFFSET = 3; internal const int PATTERN_RESOLUTION_POINTER_INSTRUCTION_LENGTH = 6; @@ -115,7 +111,6 @@ namespace SekiroFpsUnlockAndMore */ internal const string PATTERN_RESOLUTION_DEFAULT = "80 07 00 00 38 04 00 00"; // 1920x1080 internal const string PATTERN_RESOLUTION_DEFAULT_720 = "40 06 00 00 84 03 00 00"; // 1280x720 - internal const string PATTERN_RESOLUTION_DEFAULT_MASK = "xxxxxxxx"; internal static byte[] PATCH_RESOLUTION_DEFAULT_DISABLE = new byte[8] { 0x80, 0x07, 0x00, 0x00, 0x38, 0x04, 0x00, 0x00 }; internal static byte[] PATCH_RESOLUTION_DEFAULT_DISABLE_720 = new byte[8] { 0x40, 0x06, 0x00, 0x00, 0x84, 0x03, 0x00, 0x00 }; @@ -125,8 +120,7 @@ namespace SekiroFpsUnlockAndMore 000000014012967A | 74 47 | je sekiro.1401296C3 | conditional jump 000000014012967C | 47:8B94C7 1C020000 | mov r10d,dword ptr ds:[r15+r8*8+21C] | start of long resolution scaling calculation method within jump */ - internal const string PATTERN_RESOLUTION_SCALING_FIX = "47 47 8B 94 C7 1C 02 00 00"; // 47 47 8B 94 C7 1C 02 00 00 - internal const string PATTERN_RESOLUTION_SCALING_FIX_MASK = "xxxxxxxxx"; + internal const string PATTERN_RESOLUTION_SCALING_FIX = "47 47 8B 94 C7 1C 02 00 00"; internal const int PATTERN_RESOLUTION_SCALING_FIX_OFFSET = -1; internal static byte[] PATCH_RESOLUTION_SCALING_FIX_ENABLE = new byte[1] { 0xEB }; // JMP internal static byte[] PATCH_RESOLUTION_SCALING_FIX_DISABLE = new byte[1] { 0x74 }; // JE @@ -138,15 +132,14 @@ namespace SekiroFpsUnlockAndMore 000000014073954C | F3:0F590D 0CE79B02 | mulss xmm1,dword ptr ds:[1430F7C60] | pFovTableEntry->fFov */ // credits to 'jackfuste' for original offset - internal const string PATTERN_FOVSETTING = "F3 0F 10 08 F3 0F 59 0D 00 00 9B 02"; // F3 0F 10 08 F3 0F 59 0D ?? ?? 9B 02 - internal const string PATTERN_FOVSETTING_MASK = "xxxxxxxx??xx"; + internal const string PATTERN_FOVSETTING = "F3 0F 10 08 F3 0F 59 0D ?? ?? 9B 02"; internal const int PATTERN_FOVSETTING_OFFSET = 8; /** 00000001430F7C60 Key: Patch to pFovTableEntry (last 2 bytes) Value: Value resolve in float table from pFovTableEntry->fFov */ - internal static Dictionary PATCH_FOVSETTING_MATRIX = new Dictionary + internal static readonly Dictionary PATCH_FOVSETTING_MATRIX = new Dictionary { { new byte[2] {0x00, 0xE7}, "- 50%" }, { new byte[2] {0x04, 0xE7}, "- 10%" }, @@ -156,7 +149,7 @@ namespace SekiroFpsUnlockAndMore { new byte[2] {0x18, 0xE7}, "+ 75%" }, { new byte[2] {0x1C, 0xE7}, "+ 90%" } }; - internal static byte[] PATCH_FOVSETTING_DISABLE = new byte[2] { 0x0C, 0xE7 }; // + 0% + internal static readonly byte[] PATCH_FOVSETTING_DISABLE = new byte[2] { 0x0C, 0xE7 }; // + 0% /** @@ -170,12 +163,12 @@ namespace SekiroFpsUnlockAndMore 00000001407AACB6 | 8B88 90000000 | mov ecx,dword ptr ds:[rax+90] | offset pPlayerStats->iPlayerDeaths */ // credits to 'Me_TheCat' for original offset - internal const string PATTERN_PLAYER_DEATHS = "0F B6 48 00 88 8B 00 00 00 00 48 8B 05 00 00 00 00 8B 88 00 00 00 00 89 8B 00 00 00 00 48 8B 05 00 00 00 00 8B 88 00 00 00 00"; // 0F B6 48 ?? 88 8B ?? ?? 00 00 48 8B 05 ?? ?? ?? ?? 8B 88 ?? ?? 00 00 89 8B ?? ?? 00 00 48 8B 05 ?? ?? ?? ?? 8B 88 ?? ?? 00 00 - internal const string PATTERN_PLAYER_DEATHS_MASK = "xxx?xx??xxxxx????xx??xxxx??xxxxx????xx??xx"; + internal const string PATTERN_PLAYER_DEATHS = "0F B6 48 ?? 88 8B ?? ?? 00 00 48 8B 05 ?? ?? ?? ?? 8B 88 ?? ?? 00 00 89 8B ?? ?? 00 00 48 8B 05 ?? ?? ?? ?? 8B 88 ?? ?? 00 00"; internal const int PATTERN_PLAYER_DEATHS_OFFSET = 29; internal const int PATTERN_PLAYER_DEATHS_INSTRUCTION_LENGTH = 7; internal const int PATTERN_PLAYER_DEATHS_POINTER_OFFSET_OFFSET = 9; + /** Reference pointer pTotalKills to iTotalKills, does not get updated on every kill but mostly on every 2nd, includes own player deaths... 0000000141151838 | 48:8D0D A9A5B302 | lea rcx,qword ptr ds:[143C8BDE8] | pTotalKills->iTotalKills @@ -183,8 +176,7 @@ namespace SekiroFpsUnlockAndMore 0000000141151842 | C3 | ret | */ // credits to 'Me_TheCat' for original offset - internal const string PATTERN_TOTAL_KILLS = "48 8D 0D 00 00 00 00 89 14 81 C3"; // 48 8D 0D ?? ?? ?? ?? 89 14 81 C3 - internal const string PATTERN_TOTAL_KILLS_MASK = "xxx????xxxx"; + internal const string PATTERN_TOTAL_KILLS = "48 8D 0D ?? ?? ?? ?? 89 14 81 C3"; internal const int PATTERN_TOTAL_KILLS_INSTRUCTION_LENGTH = 7; @@ -195,8 +187,7 @@ namespace SekiroFpsUnlockAndMore 0000000141149E96 | F3:0F5988 68020000 | mulss xmm1,dword ptr ds:[rax+268] | */ // credits to 'Zullie the Witch' for original offset - internal const string PATTERN_TIMESCALE = "48 8B 05 00 00 00 00 F3 0F 10 88 00 00 00 00 F3 0F"; // 48 8B 05 ?? ?? ?? ?? F3 0F 10 88 ?? ?? ?? ?? F3 0F - internal const string PATTERN_TIMESCALE_MASK = "xxx????xxxx????xx"; + internal const string PATTERN_TIMESCALE = "48 8B 05 ?? ?? ?? ?? F3 0F 10 88 ?? ?? ?? ?? F3 0F"; internal const int PATTERN_TIMESCALE_INSTRUCTION_LENGTH = 7; internal const int PATTERN_TIMESCALE_POINTER_OFFSET_OFFSET = 11; @@ -209,12 +200,76 @@ namespace SekiroFpsUnlockAndMore 00000001406BF1E3 | 8B17 | mov edx,dword ptr ds:[rdi] | */ // credits to 'Zullie the Witch' for original offset - internal const string PATTERN_TIMESCALE_PLAYER = "48 8B 1D 00 00 00 00 48 85 DB 74 3C 8B 17"; // 48 8B 1D ?? ?? ?? ?? 48 85 DB 74 3C 8B 17 - internal const string PATTERN_TIMESCALE_PLAYER_MASK = "xxx????xxxxxxx"; + internal const string PATTERN_TIMESCALE_PLAYER = "48 8B 1D ?? ?? ?? ?? 48 85 DB 74 3C 8B 17"; internal const int PATTERN_TIMESCALE_PLAYER_INSTRUCTION_LENGTH = 7; internal const int PATTERN_TIMESCALE_POINTER2_OFFSET = 0x88; internal const int PATTERN_TIMESCALE_POINTER3_OFFSET = 0x1FF8; internal const int PATTERN_TIMESCALE_POINTER4_OFFSET = 0x28; internal const int PATTERN_TIMESCALE_POINTER5_OFFSET = 0xD00; + + + /** + Controls camera pitch. xmm4 holds new pitch from a calculation while rps+170 holds current one from mouse so we overwrite xmm4 with the old pitch value + 000000014073AF26 | 0F29A5 70080000 | movaps xmmword ptr ss:[rbp+870],xmm4 | code inject overwrite from here + 000000014073AF2D | 0F29A5 80080000 | movaps xmmword ptr ss:[rbp+880],xmm4 | jump back here from code inject + 000000014073AF34 | 0F29A6 70010000 | movaps xmmword ptr ds:[rsi+170],xmm4 | camPitch, newCamPitch + 000000014073AF3B | EB 1C | jmp sekiro.14073AF59 | + 000000014073AF3D | F3:0F108E 74010000 | movss xmm1,dword ptr ds:[rsi+174] | + */ + internal const string PATTERN_CAMADJUST_PITCH = "0F 29 ?? ?? ?? 00 00 0F 29 ?? ?? ?? 00 00 0F 29 ?? ?? ?? 00 00 EB ?? F3"; + internal const int INJECT_CAMADJUST_PITCH_OVERWRITE_LENGTH = 7; + internal static readonly byte[] INJECT_CAMADJUST_PITCH_SHELLCODE = new byte[] + { + 0x0F, 0x28, 0xA6, 0x70, 0x01, 0x00, 0x00, // movaps xmm4,xmmword ptr ds:[rsi+170] + 0x0F, 0x29, 0xA5, 0x70, 0x08, 0x00, 0x00 // movaps xmmword ptr ss:[rbp+870],xmm4 + }; + /** + Controls automatic camera yaw adjust on move on Z-axis. xmm0 holds new yaw while rsi+174 holds current one prior movement so we overwrite xmm0 with the old yaw value + 000000014073AF4C | E8 6F60FFFF | call sekiro.140730FC0 | + 000000014073AF51 | F3:0F1186 74010000 | movss dword ptr ds:[rsi+174],xmm0 | camYaw, newCamYaw | code inject overwrite from here + 000000014073AF59 | 80BE A3020000 00 | cmp byte ptr ds:[rsi+2A3],0 | jump back here from code inject + 000000014073AF60 | 0F84 2F020000 | je sekiro.14073B195 | + */ + internal const string PATTERN_CAMADJUST_YAW_Z = "E8 ?? ?? ?? ?? F3 ?? ?? ?? ?? ?? 00 00 80 ?? ?? ?? 00 00 00 0F 84"; + internal const int PATTERN_CAMADJUST_YAW_Z_OFFSET = 5; + internal const int INJECT_CAMADJUST_YAW_Z_OVERWRITE_LENGTH = 8; + internal static readonly byte[] INJECT_CAMADJUST_YAW_Z_SHELLCODE = new byte[] + { + 0xF3, 0x0F, 0x10, 0x86, 0x74, 0x01, 0x00, 0x00, // movss xmm0,dword ptr ds:[rsi+174] + 0xF3, 0x0F, 0x11, 0x86, 0x74, 0x01, 0x00, 0x00 // movss dword ptr ds:[rsi+174],xmm0 + }; + /** + Controls automatic camera pitch adjust on move on XY-axis. Pointer in rax holds new pitch while rsi+170 holds current one prior movement so we overwrite xmm0 with the old pitch value and then overwrite [rax] with xmm0 + 000000014073B476 | F3:0F1000 | movss xmm0,dword ptr ds:[rax] | newCamPitch | code inject overwrite from here + 000000014073B47A | F3:0F1186 70010000 | movss dword ptr ds:[rsi+170],xmm0 | camPitch + 000000014073B482 | F3:0F1085 E4120000 | movss xmm0,dword ptr ss:[rbp+12E4] | jump back here from code inject + 000000014073B48A | E8 91BDFFFF | call sekiro.140737220 | + 000000014073B48F | 0F28D0 | movaps xmm2,xmm0 | + */ + // thanks to 'Cielos' for original offset + internal const string PATTERN_CAMADJUST_PITCH_XY = "F3 ?? ?? ?? F3 ?? ?? ?? ?? ?? 00 00 F3 ?? ?? ?? ?? ?? ?? ?? E8 ?? ?? ?? ?? 0F"; + internal const int INJECT_CAMADJUST_PITCH_XY_OVERWRITE_LENGTH = 12; + internal static readonly byte[] INJECT_CAMADJUST_PITCH_XY_SHELLCODE = new byte[] + { + 0xF3, 0x0F, 0x10, 0x86, 0x70, 0x01, 0x00, 0x00, // movss xmm0,dword ptr ds:[rsi+170] + 0xF3, 0x0F, 0x11, 0x00, // movss dword ptr ds:[rax],xmm0 + 0xF3, 0x0F, 0x10, 0x00, // movss xmm0,dword ptr ds:[rax] + 0xF3, 0x0F, 0x11, 0x86, 0x70, 0x01, 0x00, 0x00 // movss dword ptr ds:[rsi+170],xmm0 + }; + /** + Controls automatic camera yaw adjust on move on XY-axis. xmm0 new yaw while rsi+174 holds current one prior movement so we overwrite xmm0 with the old yaw value + 000000014073B564 | E8 B7BCFFFF | call sekiro.140737220 | + 000000014073B569 | F3:0F1186 74010000 | movss dword ptr ds:[rsi+174],xmm0 | camYaw, newCamYaw | code inject overwrite from here + 000000014073B571 | E9 9A020000 | jmp sekiro.14073B810 | jump back here from code inject + */ + // thanks to 'Cielos' for original offset + internal const string PATTERN_CAMADJUST_YAW_XY = "E8 ?? ?? ?? ?? F3 0F 11 86 74 01 00 00 E9"; + internal const int PATTERN_CAMADJUST_YAW_XY_OFFSET = 5; + internal const int INJECT_CAMADJUST_YAW_XY_OVERWRITE_LENGTH = 8; + internal static readonly byte[] INJECT_CAMADJUST_YAW_XY_SHELLCODE = new byte[] + { + 0xF3, 0x0F, 0x10, 0x86, 0x74, 0x01, 0x00, 0x00, // movss xmm0,dword ptr ds:[rsi+174] + 0xF3, 0x0F, 0x11, 0x86, 0x74, 0x01, 0x00, 0x00 // movss dword ptr ds:[rsi+174],xmm0 + }; } } diff --git a/SekiroFpsUnlockAndMore/MainWindow.xaml b/SekiroFpsUnlockAndMore/MainWindow.xaml index cf53931..e10b5f1 100644 --- a/SekiroFpsUnlockAndMore/MainWindow.xaml +++ b/SekiroFpsUnlockAndMore/MainWindow.xaml @@ -5,7 +5,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:SekiroFpsUnlockAndMore" mc:Ignorable="d" - Title="Sekiro FPS Unlocker and more v1.2.0" Width="Auto" Height="Auto" SizeToContent="WidthAndHeight" ResizeMode="CanMinimize" Loaded="Window_Loaded" Closing="Window_Closing"> + Title="Sekiro FPS Unlocker and more v1.2.1" Width="Auto" Height="Auto" SizeToContent="WidthAndHeight" ResizeMode="CanMinimize" Loaded="Window_Loaded" Closing="Window_Closing"> @@ -28,12 +28,13 @@ + - +