diff --git a/Dalamud.Boot/xivfixes.cpp b/Dalamud.Boot/xivfixes.cpp index fd4f28d91..952b33802 100644 --- a/Dalamud.Boot/xivfixes.cpp +++ b/Dalamud.Boot/xivfixes.cpp @@ -180,8 +180,23 @@ static TFnGetInputDeviceManager* GetGetInputDeviceManager(HWND hwnd) { void xivfixes::prevent_devicechange_crashes(bool bApply) { static const char* LogTag = "[xivfixes:prevent_devicechange_crashes]"; - static std::optional> s_hookCreateWindowExA; - static std::optional s_hookWndProc; + + // We hook RegisterClassExA, since if the game has already launched (inject mode), the very crash we're trying to fix cannot happen at that point. + static std::optional> s_hookRegisterClassExA; + static WNDPROC s_pfnGameWndProc = nullptr; + + // We're intentionally leaking memory for this one. + static const auto s_pfnBinder = static_cast(VirtualAlloc(nullptr, 64, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE)); + static const auto s_pfnAlternativeWndProc = static_cast([](HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) -> LRESULT { + if (uMsg == WM_DEVICECHANGE && wParam == DBT_DEVNODES_CHANGED) { + if (!GetGetInputDeviceManager(hWnd)()) { + logging::I("{} WndProc(0x{:X}, WM_DEVICECHANGE, DBT_DEVNODES_CHANGED, {}) called but the game does not have InputDeviceManager initialized; doing nothing.", LogTag, reinterpret_cast(hWnd), lParam); + return 0; + } + } + + return s_pfnGameWndProc(hWnd, uMsg, wParam, lParam); + }); if (bApply) { if (!g_startInfo.BootEnabledGameFixes.contains("prevent_devicechange_crashes")) { @@ -189,47 +204,38 @@ void xivfixes::prevent_devicechange_crashes(bool bApply) { return; } - s_hookCreateWindowExA.emplace("user32.dll!CreateWindowExA (prevent_devicechange_crashes)", "user32.dll", "CreateWindowExA", 0); - s_hookCreateWindowExA->set_detour([](DWORD dwExStyle, LPCSTR lpClassName, LPCSTR lpWindowName, DWORD dwStyle, int X, int Y, int nWidth, int nHeight, HWND hWndParent, HMENU hMenu, HINSTANCE hInstance, LPVOID lpParam)->HWND { - const auto hWnd = s_hookCreateWindowExA->call_original(dwExStyle, lpClassName, lpWindowName, dwStyle, X, Y, nWidth, nHeight, hWndParent, hMenu, hInstance, lpParam); + s_hookRegisterClassExA.emplace("user32.dll!RegisterClassExA (prevent_devicechange_crashes)", "user32.dll", "RegisterClassExA", 0); + s_hookRegisterClassExA->set_detour([](const WNDCLASSEXA* pWndClassExA)->ATOM { + // If this RegisterClassExA isn't initiated by the game executable, we do not handle it. + if (pWndClassExA->hInstance != GetModuleHandleW(nullptr)) + return s_hookRegisterClassExA->call_original(pWndClassExA); - if (!hWnd - || hInstance != g_hGameInstance - || 0 != strcmp(lpClassName, "FFXIVGAME")) - return hWnd; + // If this RegisterClassExA isn't about FFXIVGAME, the game's main window, we do not handle it. + if (strncmp(pWndClassExA->lpszClassName, "FFXIVGAME", 10) != 0) + return s_hookRegisterClassExA->call_original(pWndClassExA); - logging::I(R"({} CreateWindow(0x{:08X}, "{}", "{}", 0x{:08X}, {}, {}, {}, {}, 0x{:X}, 0x{:X}, 0x{:X}, 0x{:X}) called; unhooking CreateWindowExA and hooking WndProc.)", - LogTag, dwExStyle, lpClassName, lpWindowName, dwStyle, X, Y, nWidth, nHeight, reinterpret_cast(hWndParent), reinterpret_cast(hMenu), reinterpret_cast(hInstance), reinterpret_cast(lpParam)); + // push qword ptr [rip+1] + // ret + // + memcpy(s_pfnBinder, "\xFF\x35\x01\x00\x00\x00\xC3", 7); + *reinterpret_cast(reinterpret_cast(s_pfnBinder) + 7) = s_pfnAlternativeWndProc; + + s_pfnGameWndProc = pWndClassExA->lpfnWndProc; - s_hookWndProc.emplace("FFXIVGAME:WndProc (prevent_devicechange_crashes)", hWnd); - s_hookWndProc->set_detour([](HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) -> LRESULT { - - if (uMsg == WM_DEVICECHANGE && wParam == DBT_DEVNODES_CHANGED) { - if (!GetGetInputDeviceManager(hWnd)()) { - logging::I("{} WndProc(0x{:X}, WM_DEVICECHANGE, DBT_DEVNODES_CHANGED, {}) called but the game does not have InputDeviceManager initialized; doing nothing.", LogTag, reinterpret_cast(hWnd), lParam); - return 0; - } - } - - return s_hookWndProc->call_original(hWnd, uMsg, wParam, lParam); - }); - - return hWnd; + WNDCLASSEXA wndClassExA = *pWndClassExA; + wndClassExA.lpfnWndProc = s_pfnBinder; + return s_hookRegisterClassExA->call_original(&wndClassExA); }); logging::I("{} Enable", LogTag); } else { - if (s_hookCreateWindowExA) { - logging::I("{} Disable CreateWindowExA", LogTag); - s_hookCreateWindowExA.reset(); + if (s_hookRegisterClassExA) { + logging::I("{} Disable RegisterClassExA", LogTag); + s_hookRegisterClassExA.reset(); } - // This will effectively revert any other WndProc alterations, including Dalamud. - if (s_hookWndProc) { - logging::I("{} Disable WndProc", LogTag); - s_hookWndProc.reset(); - } + *reinterpret_cast(reinterpret_cast(s_pfnBinder) + 7) = s_pfnGameWndProc; } } diff --git a/Dalamud/Game/Gui/Internal/DalamudIME.cs b/Dalamud/Game/Gui/Internal/DalamudIME.cs index e16692d43..811e25997 100644 --- a/Dalamud/Game/Gui/Internal/DalamudIME.cs +++ b/Dalamud/Game/Gui/Internal/DalamudIME.cs @@ -10,7 +10,7 @@ using Dalamud.Hooking; using Dalamud.Interface.Internal; using Dalamud.Logging.Internal; using ImGuiNET; - +using PInvoke; using static Dalamud.NativeFunctions; namespace Dalamud.Game.Gui.Internal @@ -23,24 +23,14 @@ namespace Dalamud.Game.Gui.Internal { private static readonly ModuleLog Log = new("IME"); - private IntPtr interfaceHandle; - private IntPtr wndProcPtr; - private IntPtr oldWndProcPtr; - private WndProcDelegate wndProcDelegate; private AsmHook imguiTextInputCursorHook; private Vector2* cursorPos; - /// - /// Initializes a new instance of the class. - /// - /// Tag. [ServiceManager.ServiceConstructor] private DalamudIME() { } - private delegate long WndProcDelegate(IntPtr hWnd, uint msg, ulong wParam, long lParam); - /// /// Gets a value indicating whether the module is enabled. /// @@ -64,81 +54,23 @@ namespace Dalamud.Game.Gui.Internal /// public void Dispose() { - if (this.oldWndProcPtr != IntPtr.Zero) - { - SetWindowLongPtrW(this.interfaceHandle, WindowLongType.WndProc, this.oldWndProcPtr); - this.oldWndProcPtr = IntPtr.Zero; - } - this.imguiTextInputCursorHook?.Dispose(); Marshal.FreeHGlobal((IntPtr)this.cursorPos); } /// - /// Get the position of the cursor. + /// Processes window messages. /// - /// The position of the cursor. - internal Vector2 GetCursorPos() - { - return new Vector2(this.cursorPos->X, this.cursorPos->Y); - } - - [ServiceManager.CallWhenServicesReady] - private void ContinueConstruction(InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene) + /// Handle of the window. + /// Type of window message. + /// wParam. + /// lParam. + /// Return value, if not doing further processing. + public unsafe IntPtr? ProcessWndProcW(IntPtr hWnd, User32.WindowMessage msg, void* wParam, void* lParam) { try { - this.wndProcDelegate = this.WndProcDetour; - this.interfaceHandle = interfaceManagerWithScene.Manager.WindowHandlePtr; - this.wndProcPtr = Marshal.GetFunctionPointerForDelegate(this.wndProcDelegate); - this.oldWndProcPtr = SetWindowLongPtrW(this.interfaceHandle, WindowLongType.WndProc, this.wndProcPtr); - - var module = Process.GetCurrentProcess().Modules.Cast().First(m => m.ModuleName == "cimgui.dll"); - var scanner = new SigScanner(module); - var cursorDrawingPtr = scanner.ScanModule("F3 0F 11 75 ?? 0F 28 CF"); - Log.Debug($"Found cursorDrawingPtr at {cursorDrawingPtr:X}"); - - this.cursorPos = (Vector2*)Marshal.AllocHGlobal(sizeof(Vector2)); - this.cursorPos->X = 0f; - this.cursorPos->Y = 0f; - - var asm = new[] - { - "use64", - $"push rax", - $"mov rax, {(IntPtr)this.cursorPos + sizeof(float)}", - $"movss [rax],xmm7", - $"mov rax, {(IntPtr)this.cursorPos}", - $"movss [rax],xmm6", - $"pop rax", - }; - - Log.Debug($"Asm Code:\n{string.Join("\n", asm)}"); - this.imguiTextInputCursorHook = new AsmHook(cursorDrawingPtr, asm, "ImguiTextInputCursorHook"); - this.imguiTextInputCursorHook?.Enable(); - - this.IsEnabled = true; - Log.Information("Enabled!"); - } - catch (Exception ex) - { - Log.Information(ex, "Enable failed"); - } - } - - private void ToggleWindow(bool visible) - { - if (visible) - Service.Get().OpenImeWindow(); - else - Service.Get().CloseImeWindow(); - } - - private long WndProcDetour(IntPtr hWnd, uint msg, ulong wParam, long lParam) - { - try - { - if (hWnd == this.interfaceHandle && ImGui.GetCurrentContext() != IntPtr.Zero && ImGui.GetIO().WantTextInput) + if (ImGui.GetCurrentContext() != IntPtr.Zero && ImGui.GetIO().WantTextInput) { var io = ImGui.GetIO(); var wmsg = (WindowsMessage)msg; @@ -146,17 +78,17 @@ namespace Dalamud.Game.Gui.Internal switch (wmsg) { case WindowsMessage.WM_IME_NOTIFY: - switch ((IMECommand)wParam) + switch ((IMECommand)(IntPtr)wParam) { case IMECommand.ChangeCandidate: this.ToggleWindow(true); if (hWnd == IntPtr.Zero) - return 0; + return IntPtr.Zero; var hIMC = ImmGetContext(hWnd); if (hIMC == IntPtr.Zero) - return 0; + return IntPtr.Zero; var size = ImmGetCandidateListW(hIMC, 0, IntPtr.Zero, 0); if (size == 0) @@ -225,11 +157,11 @@ namespace Dalamud.Game.Gui.Internal break; case WindowsMessage.WM_IME_COMPOSITION: - if ((lParam & (long)IMEComposition.ResultStr) > 0) + if (((long)(IntPtr)lParam & (long)IMEComposition.ResultStr) > 0) { var hIMC = ImmGetContext(hWnd); if (hIMC == IntPtr.Zero) - return 0; + return IntPtr.Zero; var dwSize = ImmGetCompositionStringW(hIMC, IMEComposition.ResultStr, IntPtr.Zero, 0); var unmanagedPointer = Marshal.AllocHGlobal((int)dwSize); @@ -249,11 +181,11 @@ namespace Dalamud.Game.Gui.Internal } if (((long)(IMEComposition.CompStr | IMEComposition.CompAttr | IMEComposition.CompClause | - IMEComposition.CompReadAttr | IMEComposition.CompReadClause | IMEComposition.CompReadStr) & lParam) > 0) + IMEComposition.CompReadAttr | IMEComposition.CompReadClause | IMEComposition.CompReadStr) & (long)(IntPtr)lParam) > 0) { var hIMC = ImmGetContext(hWnd); if (hIMC == IntPtr.Zero) - return 0; + return IntPtr.Zero; var dwSize = ImmGetCompositionStringW(hIMC, IMEComposition.CompStr, IntPtr.Zero, 0); var unmanagedPointer = Marshal.AllocHGlobal((int)dwSize); @@ -281,7 +213,62 @@ namespace Dalamud.Game.Gui.Internal Log.Error(ex, "Prevented a crash in an IME hook"); } - return CallWindowProcW(this.oldWndProcPtr, hWnd, msg, wParam, lParam); + return null; + } + + /// + /// Get the position of the cursor. + /// + /// The position of the cursor. + internal Vector2 GetCursorPos() + { + return new Vector2(this.cursorPos->X, this.cursorPos->Y); + } + + [ServiceManager.CallWhenServicesReady] + private void ContinueConstruction(InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene) + { + try + { + var module = Process.GetCurrentProcess().Modules.Cast().First(m => m.ModuleName == "cimgui.dll"); + var scanner = new SigScanner(module); + var cursorDrawingPtr = scanner.ScanModule("F3 0F 11 75 ?? 0F 28 CF"); + Log.Debug($"Found cursorDrawingPtr at {cursorDrawingPtr:X}"); + + this.cursorPos = (Vector2*)Marshal.AllocHGlobal(sizeof(Vector2)); + this.cursorPos->X = 0f; + this.cursorPos->Y = 0f; + + var asm = new[] + { + "use64", + $"push rax", + $"mov rax, {(IntPtr)this.cursorPos + sizeof(float)}", + $"movss [rax],xmm7", + $"mov rax, {(IntPtr)this.cursorPos}", + $"movss [rax],xmm6", + $"pop rax", + }; + + Log.Debug($"Asm Code:\n{string.Join("\n", asm)}"); + this.imguiTextInputCursorHook = new AsmHook(cursorDrawingPtr, asm, "ImguiTextInputCursorHook"); + this.imguiTextInputCursorHook?.Enable(); + + this.IsEnabled = true; + Log.Information("Enabled!"); + } + catch (Exception ex) + { + Log.Information(ex, "Enable failed"); + } + } + + private void ToggleWindow(bool visible) + { + if (visible) + Service.Get().OpenImeWindow(); + else + Service.Get().CloseImeWindow(); } } } diff --git a/Dalamud/Game/Network/Internal/WinSockHandlers.cs b/Dalamud/Game/Network/Internal/WinSockHandlers.cs index ed9d635a3..26b620433 100644 --- a/Dalamud/Game/Network/Internal/WinSockHandlers.cs +++ b/Dalamud/Game/Network/Internal/WinSockHandlers.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Net.Sockets; using System.Runtime.InteropServices; @@ -17,7 +18,7 @@ namespace Dalamud.Game.Network.Internal [ServiceManager.ServiceConstructor] private WinSockHandlers() { - this.ws2SocketHook = Hook.FromSymbol("ws2_32.dll", "socket", this.OnSocket, true); + this.ws2SocketHook = Hook.FromImport(Process.GetCurrentProcess().MainModule, "ws2_32.dll", "socket", 23, this.OnSocket); this.ws2SocketHook?.Enable(); } diff --git a/Dalamud/Hooking/Hook.cs b/Dalamud/Hooking/Hook.cs index 9c1cbaa06..39821f847 100644 --- a/Dalamud/Hooking/Hook.cs +++ b/Dalamud/Hooking/Hook.cs @@ -1,6 +1,8 @@ using System; +using System.Diagnostics; +using System.IO; using System.Reflection; - +using System.Runtime.InteropServices; using Dalamud.Configuration.Internal; using Dalamud.Hooking.Internal; using Dalamud.Memory; @@ -13,12 +15,14 @@ namespace Dalamud.Hooking /// This class is basically a thin wrapper around the LocalHook type to provide helper functions. /// /// Delegate type to represents a function prototype. This must be the same prototype as original function do. - public sealed class Hook : IDisposable, IDalamudHook where T : Delegate + public class Hook : IDisposable, IDalamudHook where T : Delegate { + private const ulong IMAGE_ORDINAL_FLAG64 = 0x8000000000000000; + private const uint IMAGE_ORDINAL_FLAG32 = 0x80000000; + private readonly IntPtr address; - private readonly Reloaded.Hooks.Definitions.IHook hookImpl; - private readonly MinSharp.Hook minHookImpl; - private readonly bool isMinHook; + + private readonly Hook? compatHookImpl; /// /// Initializes a new instance of the class. @@ -26,6 +30,7 @@ namespace Dalamud.Hooking /// /// A memory address to install a hook. /// Callback function. Delegate must have a same original function prototype. + [Obsolete("Use Hook.FromAddress instead.")] public Hook(IntPtr address, T detour) : this(address, detour, false, Assembly.GetCallingAssembly()) { @@ -39,42 +44,29 @@ namespace Dalamud.Hooking /// A memory address to install a hook. /// Callback function. Delegate must have a same original function prototype. /// Use the MinHook hooking library instead of Reloaded. + [Obsolete("Use Hook.FromAddress instead.")] public Hook(IntPtr address, T detour, bool useMinHook) : this(address, detour, useMinHook, Assembly.GetCallingAssembly()) { } + /// + /// Initializes a new instance of the class. + /// + /// A memory address to install a hook. + internal Hook(IntPtr address) + { + this.address = address; + } + + [Obsolete("Use Hook.FromAddress instead.")] private Hook(IntPtr address, T detour, bool useMinHook, Assembly callingAssembly) { address = HookManager.FollowJmp(address); - this.isMinHook = !EnvironmentConfiguration.DalamudForceReloaded && (EnvironmentConfiguration.DalamudForceMinHook || useMinHook); - - var hasOtherHooks = HookManager.Originals.ContainsKey(address); - if (!hasOtherHooks) - { - MemoryHelper.ReadRaw(address, 0x32, out var original); - HookManager.Originals[address] = original; - } - - this.address = address; - if (this.isMinHook) - { - if (!HookManager.MultiHookTracker.TryGetValue(address, out var indexList)) - indexList = HookManager.MultiHookTracker[address] = new(); - - var index = (ulong)indexList.Count; - - this.minHookImpl = new MinSharp.Hook(address, detour, index); - - // Add afterwards, so the hookIdent starts at 0. - indexList.Add(this); - } + if (useMinHook) + this.compatHookImpl = new MinHookHook(address, detour, callingAssembly); else - { - this.hookImpl = ReloadedHooks.Instance.CreateHook(detour, address.ToInt64()); - } - - HookManager.TrackedHooks.TryAdd(Guid.NewGuid(), new HookInfo(this, detour, callingAssembly)); + this.compatHookImpl = new ReloadedHook(address, detour, callingAssembly); } /// @@ -94,40 +86,12 @@ namespace Dalamud.Hooking /// Gets a delegate function that can be used to call the actual function as if function is not hooked yet. /// /// Hook is already disposed. - public T Original - { - get - { - this.CheckDisposed(); - if (this.isMinHook) - { - return this.minHookImpl.Original; - } - else - { - return this.hookImpl.OriginalFunction; - } - } - } + public virtual T Original => this.compatHookImpl != null ? this.compatHookImpl!.Original : throw new NotImplementedException(); /// /// Gets a value indicating whether or not the hook is enabled. /// - public bool IsEnabled - { - get - { - this.CheckDisposed(); - if (this.isMinHook) - { - return this.minHookImpl.Enabled; - } - else - { - return this.hookImpl.IsHookEnabled; - } - } - } + public virtual bool IsEnabled => this.compatHookImpl != null ? this.compatHookImpl!.IsEnabled : throw new NotImplementedException(); /// /// Gets a value indicating whether or not the hook has been disposed. @@ -135,15 +99,80 @@ namespace Dalamud.Hooking public bool IsDisposed { get; private set; } /// - public string BackendName - { - get - { - if (this.isMinHook) - return "MinHook"; + public virtual string BackendName => this.compatHookImpl != null ? this.compatHookImpl!.BackendName : throw new NotImplementedException(); - return "Reloaded"; + /// + /// Creates a hook by rewriting import table address. + /// + /// A memory address to install a hook. + /// Callback function. Delegate must have a same original function prototype. + /// The hook with the supplied parameters. + public static unsafe Hook FromFunctionPointerVariable(IntPtr address, T detour) + { + return new FunctionPointerVariableHook(address, detour, Assembly.GetCallingAssembly()); + } + + /// + /// Creates a hook by rewriting import table address. + /// + /// Module to check for. + /// Name of the DLL, including the extension. + /// Decorated name of the function. + /// Hint or ordinal. 0 to unspecify. + /// Callback function. Delegate must have a same original function prototype. + /// The hook with the supplied parameters. + public static unsafe Hook FromImport(ProcessModule module, string moduleName, string functionName, uint hintOrOrdinal, T detour) + { + var pDos = (PeHeader.IMAGE_DOS_HEADER*)module.BaseAddress; + var pNt = (PeHeader.IMAGE_FILE_HEADER*)(module.BaseAddress + (int)pDos->e_lfanew + 4); + var isPe64 = pNt->SizeOfOptionalHeader == Marshal.SizeOf(); + PeHeader.IMAGE_DATA_DIRECTORY* pDataDirectory; + if (isPe64) + { + var pOpt = (PeHeader.IMAGE_OPTIONAL_HEADER64*)(module.BaseAddress + (int)pDos->e_lfanew + 4 + Marshal.SizeOf()); + pDataDirectory = &pOpt->ImportTable; } + else + { + var pOpt = (PeHeader.IMAGE_OPTIONAL_HEADER32*)(module.BaseAddress + (int)pDos->e_lfanew + 4 + Marshal.SizeOf()); + pDataDirectory = &pOpt->ImportTable; + } + + var moduleNameLowerWithNullTerminator = (moduleName + "\0").ToLowerInvariant(); + foreach (ref var importDescriptor in new Span( + (PeHeader.IMAGE_IMPORT_DESCRIPTOR*)(module.BaseAddress + (int)pDataDirectory->VirtualAddress), + (int)(pDataDirectory->Size / Marshal.SizeOf()))) + { + // Having all zero values signals the end of the table. We didn't find anything. + if (importDescriptor.Characteristics == 0) + throw new MissingMethodException("Specified dll not found"); + + // Skip invalid entries, just in case. + if (importDescriptor.Name == 0) + continue; + + // Name must be contained in this directory. + if (importDescriptor.Name < pDataDirectory->VirtualAddress) + continue; + var currentDllNameWithNullTerminator = Marshal.PtrToStringUTF8( + module.BaseAddress + (int)importDescriptor.Name, + (int)Math.Min(pDataDirectory->Size + pDataDirectory->VirtualAddress - importDescriptor.Name, moduleNameLowerWithNullTerminator.Length)); + + // Is this entry about the DLL that we're looking for? (Case insensitive) + if (currentDllNameWithNullTerminator.ToLowerInvariant() != moduleNameLowerWithNullTerminator) + continue; + + if (isPe64) + { + return new FunctionPointerVariableHook(FromImportHelper64(module.BaseAddress, ref importDescriptor, ref *pDataDirectory, functionName, hintOrOrdinal), detour, Assembly.GetCallingAssembly()); + } + else + { + return new FunctionPointerVariableHook(FromImportHelper32(module.BaseAddress, ref importDescriptor, ref *pDataDirectory, functionName, hintOrOrdinal), detour, Assembly.GetCallingAssembly()); + } + } + + throw new MissingMethodException("Specified dll not found"); } /// @@ -177,28 +206,40 @@ namespace Dalamud.Hooking if (procAddress == IntPtr.Zero) throw new Exception($"Could not get the address of {moduleName}::{exportName}"); - return new Hook(procAddress, detour, useMinHook); + procAddress = HookManager.FollowJmp(procAddress); + if (useMinHook) + return new MinHookHook(procAddress, detour, Assembly.GetCallingAssembly()); + else + return new ReloadedHook(procAddress, detour, Assembly.GetCallingAssembly()); + } + + /// + /// Creates a hook. Hooking address is inferred by calling to GetProcAddress() function. + /// The hook is not activated until Enable() method is called. + /// Please do not use MinHook unless you have thoroughly troubleshot why Reloaded does not work. + /// + /// A memory address to install a hook. + /// Callback function. Delegate must have a same original function prototype. + /// Use the MinHook hooking library instead of Reloaded. + /// The hook with the supplied parameters. + public static Hook FromAddress(IntPtr procAddress, T detour, bool useMinHook = false) + { + procAddress = HookManager.FollowJmp(procAddress); + if (useMinHook) + return new MinHookHook(procAddress, detour, Assembly.GetCallingAssembly()); + else + return new ReloadedHook(procAddress, detour, Assembly.GetCallingAssembly()); } /// /// Remove a hook from the current process. /// - public void Dispose() + public virtual void Dispose() { if (this.IsDisposed) return; - if (this.isMinHook) - { - this.minHookImpl.Dispose(); - - var index = HookManager.MultiHookTracker[this.address].IndexOf(this); - HookManager.MultiHookTracker[this.address][index] = null; - } - else - { - this.Disable(); - } + this.compatHookImpl?.Dispose(); this.IsDisposed = true; } @@ -206,60 +247,134 @@ namespace Dalamud.Hooking /// /// Starts intercepting a call to the function. /// - public void Enable() + public virtual void Enable() { - this.CheckDisposed(); - - if (this.isMinHook) - { - if (!this.minHookImpl.Enabled) - { - this.minHookImpl.Enable(); - } - } + if (this.compatHookImpl != null) + this.compatHookImpl.Enable(); else - { - if (!this.hookImpl.IsHookActivated) - this.hookImpl.Activate(); - - if (!this.hookImpl.IsHookEnabled) - this.hookImpl.Enable(); - } + throw new NotImplementedException(); } /// /// Stops intercepting a call to the function. /// - public void Disable() + public virtual void Disable() { - this.CheckDisposed(); - - if (this.isMinHook) - { - if (this.minHookImpl.Enabled) - { - this.minHookImpl.Disable(); - } - } + if (this.compatHookImpl != null) + this.compatHookImpl.Disable(); else - { - if (!this.hookImpl.IsHookActivated) - return; - - if (this.hookImpl.IsHookEnabled) - this.hookImpl.Disable(); - } + throw new NotImplementedException(); } /// /// Check if this object has been disposed already. /// - private void CheckDisposed() + protected void CheckDisposed() { if (this.IsDisposed) { throw new ObjectDisposedException(message: "Hook is already disposed", null); } } + + private static unsafe IntPtr FromImportHelper32(IntPtr baseAddress, ref PeHeader.IMAGE_IMPORT_DESCRIPTOR desc, ref PeHeader.IMAGE_DATA_DIRECTORY dir, string functionName, uint hintOrOrdinal) + { + var importLookupsOversizedSpan = new Span((uint*)(baseAddress + (int)desc.OriginalFirstThunk), (int)((dir.Size - desc.OriginalFirstThunk) / Marshal.SizeOf())); + var importAddressesOversizedSpan = new Span((uint*)(baseAddress + (int)desc.FirstThunk), (int)((dir.Size - desc.FirstThunk) / Marshal.SizeOf())); + + var functionNameWithNullTerminator = functionName + "\0"; + for (int i = 0, i_ = Math.Min(importLookupsOversizedSpan.Length, importAddressesOversizedSpan.Length); i < i_ && importLookupsOversizedSpan[i] != 0 && importAddressesOversizedSpan[i] != 0; i++) + { + var importLookup = importLookupsOversizedSpan[i]; + + // Is this entry importing by ordinals? A lot of socket functions are the case. + if ((importLookup & IMAGE_ORDINAL_FLAG32) != 0) + { + var ordinal = importLookup & ~IMAGE_ORDINAL_FLAG32; + + // Is this the entry? + if (hintOrOrdinal == 0 || ordinal != hintOrOrdinal) + continue; + + // Is this entry not importing by ordinals, and are we using hint exclusively to find the entry? + } + else + { + var hint = Marshal.ReadInt16(baseAddress + (int)importLookup); + + if (functionName.Length > 0) + { + // Is this the entry? + if (hint != hintOrOrdinal) + continue; + } + else + { + // Name must be contained in this directory. + var currentFunctionNameWithNullTerminator = Marshal.PtrToStringUTF8( + baseAddress + (int)importLookup + 2, + (int)Math.Min(dir.VirtualAddress + dir.Size - (uint)baseAddress - importLookup - 2, (uint)functionNameWithNullTerminator.Length)); + + // Is this entry about the function that we're looking for? + if (currentFunctionNameWithNullTerminator != functionNameWithNullTerminator) + continue; + } + } + + return baseAddress + (int)desc.FirstThunk + (i * Marshal.SizeOf()); + } + + throw new MissingMethodException("Specified method not found"); + } + + private static unsafe IntPtr FromImportHelper64(IntPtr baseAddress, ref PeHeader.IMAGE_IMPORT_DESCRIPTOR desc, ref PeHeader.IMAGE_DATA_DIRECTORY dir, string functionName, uint hintOrOrdinal) + { + var importLookupsOversizedSpan = new Span((ulong*)(baseAddress + (int)desc.OriginalFirstThunk), (int)((dir.Size - desc.OriginalFirstThunk) / Marshal.SizeOf())); + var importAddressesOversizedSpan = new Span((ulong*)(baseAddress + (int)desc.FirstThunk), (int)((dir.Size - desc.FirstThunk) / Marshal.SizeOf())); + + var functionNameWithNullTerminator = functionName + "\0"; + for (int i = 0, i_ = Math.Min(importLookupsOversizedSpan.Length, importAddressesOversizedSpan.Length); i < i_ && importLookupsOversizedSpan[i] != 0 && importAddressesOversizedSpan[i] != 0; i++) + { + var importLookup = importLookupsOversizedSpan[i]; + + // Is this entry importing by ordinals? A lot of socket functions are the case. + if ((importLookup & IMAGE_ORDINAL_FLAG64) != 0) + { + var ordinal = importLookup & ~IMAGE_ORDINAL_FLAG64; + + // Is this the entry? + if (hintOrOrdinal == 0 || ordinal != hintOrOrdinal) + continue; + + // Is this entry not importing by ordinals, and are we using hint exclusively to find the entry? + } + else + { + var hint = Marshal.ReadInt16(baseAddress + (int)importLookup); + + if (functionName.Length == 0) + { + // Is this the entry? + if (hint != hintOrOrdinal) + continue; + } + else + { + // Name must be contained in this directory. + var currentFunctionNameWithNullTerminator = Marshal.PtrToStringUTF8( + baseAddress + (int)importLookup + 2, + (int)Math.Min((ulong)dir.VirtualAddress + dir.Size - (ulong)baseAddress - importLookup - 2, (ulong)functionNameWithNullTerminator.Length)); + + // Is this entry about the function that we're looking for? + if (currentFunctionNameWithNullTerminator != functionNameWithNullTerminator) + continue; + } + } + + return baseAddress + (int)desc.FirstThunk + (i * Marshal.SizeOf()); + } + + throw new MissingMethodException("Specified method not found"); + } } } diff --git a/Dalamud/Hooking/Internal/FunctionPointerVariableHook.cs b/Dalamud/Hooking/Internal/FunctionPointerVariableHook.cs new file mode 100644 index 000000000..9d1c289d8 --- /dev/null +++ b/Dalamud/Hooking/Internal/FunctionPointerVariableHook.cs @@ -0,0 +1,117 @@ +using System; +using System.ComponentModel; +using System.Reflection; +using System.Runtime.InteropServices; +using Dalamud.Memory; + +namespace Dalamud.Hooking.Internal +{ + /// + /// Manages a hook with MinHook. + /// + /// Delegate type to represents a function prototype. This must be the same prototype as original function do. + internal class FunctionPointerVariableHook : Hook where T : Delegate + { + private readonly IntPtr pfnOriginal; + private readonly T originalDelegate; + private readonly T detourDelegate; + + private bool enabled = false; + + /// + /// Initializes a new instance of the class. + /// + /// A memory address to install a hook. + /// Callback function. Delegate must have a same original function prototype. + /// Calling assembly. + internal FunctionPointerVariableHook(IntPtr address, T detour, Assembly callingAssembly) + : base(address) + { + var hasOtherHooks = HookManager.Originals.ContainsKey(this.Address); + if (!hasOtherHooks) + { + MemoryHelper.ReadRaw(this.Address, 0x32, out var original); + HookManager.Originals[this.Address] = original; + } + + if (!HookManager.MultiHookTracker.TryGetValue(this.Address, out var indexList)) + indexList = HookManager.MultiHookTracker[this.Address] = new(); + + this.pfnOriginal = Marshal.ReadIntPtr(this.Address); + this.originalDelegate = Marshal.GetDelegateForFunctionPointer(this.pfnOriginal); + this.detourDelegate = detour; + + // Add afterwards, so the hookIdent starts at 0. + indexList.Add(this); + + HookManager.TrackedHooks.TryAdd(Guid.NewGuid(), new HookInfo(this, detour, callingAssembly)); + } + + /// + public override T Original + { + get + { + this.CheckDisposed(); + return this.originalDelegate; + } + } + + /// + public override bool IsEnabled + { + get + { + this.CheckDisposed(); + return this.enabled; + } + } + + /// + public override string BackendName => "MinHook"; + + /// + public override void Dispose() + { + if (this.IsDisposed) + return; + + this.Disable(); + + var index = HookManager.MultiHookTracker[this.Address].IndexOf(this); + HookManager.MultiHookTracker[this.Address][index] = null; + + base.Dispose(); + } + + /// + public override void Enable() + { + this.CheckDisposed(); + + if (!this.enabled) + { + if (!NativeFunctions.VirtualProtect(this.Address, (UIntPtr)Marshal.SizeOf(), MemoryProtection.ExecuteReadWrite, out var oldProtect)) + throw new Win32Exception(Marshal.GetLastWin32Error()); + + Marshal.WriteIntPtr(this.Address, Marshal.GetFunctionPointerForDelegate(this.detourDelegate)); + NativeFunctions.VirtualProtect(this.Address, (UIntPtr)Marshal.SizeOf(), oldProtect, out _); + } + } + + /// + public override void Disable() + { + this.CheckDisposed(); + + if (this.enabled) + { + if (!NativeFunctions.VirtualProtect(this.Address, (UIntPtr)Marshal.SizeOf(), MemoryProtection.ExecuteReadWrite, out var oldProtect)) + throw new Win32Exception(Marshal.GetLastWin32Error()); + + Marshal.WriteIntPtr(this.Address, this.pfnOriginal); + NativeFunctions.VirtualProtect(this.Address, (UIntPtr)Marshal.SizeOf(), oldProtect, out _); + } + } + } +} diff --git a/Dalamud/Hooking/Internal/MinHookHook.cs b/Dalamud/Hooking/Internal/MinHookHook.cs new file mode 100644 index 000000000..563b137a1 --- /dev/null +++ b/Dalamud/Hooking/Internal/MinHookHook.cs @@ -0,0 +1,104 @@ +using System; +using System.Reflection; + +using Dalamud.Memory; + +namespace Dalamud.Hooking.Internal +{ + /// + /// Manages a hook with MinHook. + /// + /// Delegate type to represents a function prototype. This must be the same prototype as original function do. + internal class MinHookHook : Hook where T : Delegate + { + private readonly MinSharp.Hook minHookImpl; + + /// + /// Initializes a new instance of the class. + /// + /// A memory address to install a hook. + /// Callback function. Delegate must have a same original function prototype. + /// Calling assembly. + internal MinHookHook(IntPtr address, T detour, Assembly callingAssembly) + : base(address) + { + var hasOtherHooks = HookManager.Originals.ContainsKey(this.Address); + if (!hasOtherHooks) + { + MemoryHelper.ReadRaw(this.Address, 0x32, out var original); + HookManager.Originals[this.Address] = original; + } + + if (!HookManager.MultiHookTracker.TryGetValue(this.Address, out var indexList)) + indexList = HookManager.MultiHookTracker[this.Address] = new(); + + var index = (ulong)indexList.Count; + + this.minHookImpl = new MinSharp.Hook(this.Address, detour, index); + + // Add afterwards, so the hookIdent starts at 0. + indexList.Add(this); + + HookManager.TrackedHooks.TryAdd(Guid.NewGuid(), new HookInfo(this, detour, callingAssembly)); + } + + /// + public override T Original + { + get + { + this.CheckDisposed(); + return this.minHookImpl.Original; + } + } + + /// + public override bool IsEnabled + { + get + { + this.CheckDisposed(); + return this.minHookImpl.Enabled; + } + } + + /// + public override string BackendName => "MinHook"; + + /// + public override void Dispose() + { + if (this.IsDisposed) + return; + + this.minHookImpl.Dispose(); + + var index = HookManager.MultiHookTracker[this.Address].IndexOf(this); + HookManager.MultiHookTracker[this.Address][index] = null; + + base.Dispose(); + } + + /// + public override void Enable() + { + this.CheckDisposed(); + + if (!this.minHookImpl.Enabled) + { + this.minHookImpl.Enable(); + } + } + + /// + public override void Disable() + { + this.CheckDisposed(); + + if (this.minHookImpl.Enabled) + { + this.minHookImpl.Disable(); + } + } + } +} diff --git a/Dalamud/Hooking/Internal/PeHeader.cs b/Dalamud/Hooking/Internal/PeHeader.cs new file mode 100644 index 000000000..4e2987273 --- /dev/null +++ b/Dalamud/Hooking/Internal/PeHeader.cs @@ -0,0 +1,396 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +#pragma warning disable +namespace Dalamud.Hooking.Internal +{ + internal class PeHeader + { + public struct IMAGE_DOS_HEADER + { + public UInt16 e_magic; + public UInt16 e_cblp; + public UInt16 e_cp; + public UInt16 e_crlc; + public UInt16 e_cparhdr; + public UInt16 e_minalloc; + public UInt16 e_maxalloc; + public UInt16 e_ss; + public UInt16 e_sp; + public UInt16 e_csum; + public UInt16 e_ip; + public UInt16 e_cs; + public UInt16 e_lfarlc; + public UInt16 e_ovno; + public UInt16 e_res_0; + public UInt16 e_res_1; + public UInt16 e_res_2; + public UInt16 e_res_3; + public UInt16 e_oemid; + public UInt16 e_oeminfo; + public UInt16 e_res2_0; + public UInt16 e_res2_1; + public UInt16 e_res2_2; + public UInt16 e_res2_3; + public UInt16 e_res2_4; + public UInt16 e_res2_5; + public UInt16 e_res2_6; + public UInt16 e_res2_7; + public UInt16 e_res2_8; + public UInt16 e_res2_9; + public UInt32 e_lfanew; + } + + [StructLayout(LayoutKind.Sequential)] + public struct IMAGE_DATA_DIRECTORY + { + public UInt32 VirtualAddress; + public UInt32 Size; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct IMAGE_OPTIONAL_HEADER32 + { + public UInt16 Magic; + public Byte MajorLinkerVersion; + public Byte MinorLinkerVersion; + public UInt32 SizeOfCode; + public UInt32 SizeOfInitializedData; + public UInt32 SizeOfUninitializedData; + public UInt32 AddressOfEntryPoint; + public UInt32 BaseOfCode; + public UInt32 BaseOfData; + public UInt32 ImageBase; + public UInt32 SectionAlignment; + public UInt32 FileAlignment; + public UInt16 MajorOperatingSystemVersion; + public UInt16 MinorOperatingSystemVersion; + public UInt16 MajorImageVersion; + public UInt16 MinorImageVersion; + public UInt16 MajorSubsystemVersion; + public UInt16 MinorSubsystemVersion; + public UInt32 Win32VersionValue; + public UInt32 SizeOfImage; + public UInt32 SizeOfHeaders; + public UInt32 CheckSum; + public UInt16 Subsystem; + public UInt16 DllCharacteristics; + public UInt32 SizeOfStackReserve; + public UInt32 SizeOfStackCommit; + public UInt32 SizeOfHeapReserve; + public UInt32 SizeOfHeapCommit; + public UInt32 LoaderFlags; + public UInt32 NumberOfRvaAndSizes; + + public IMAGE_DATA_DIRECTORY ExportTable; + public IMAGE_DATA_DIRECTORY ImportTable; + public IMAGE_DATA_DIRECTORY ResourceTable; + public IMAGE_DATA_DIRECTORY ExceptionTable; + public IMAGE_DATA_DIRECTORY CertificateTable; + public IMAGE_DATA_DIRECTORY BaseRelocationTable; + public IMAGE_DATA_DIRECTORY Debug; + public IMAGE_DATA_DIRECTORY Architecture; + public IMAGE_DATA_DIRECTORY GlobalPtr; + public IMAGE_DATA_DIRECTORY TLSTable; + public IMAGE_DATA_DIRECTORY LoadConfigTable; + public IMAGE_DATA_DIRECTORY BoundImport; + public IMAGE_DATA_DIRECTORY IAT; + public IMAGE_DATA_DIRECTORY DelayImportDescriptor; + public IMAGE_DATA_DIRECTORY CLRRuntimeHeader; + public IMAGE_DATA_DIRECTORY Reserved; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct IMAGE_OPTIONAL_HEADER64 + { + public UInt16 Magic; + public Byte MajorLinkerVersion; + public Byte MinorLinkerVersion; + public UInt32 SizeOfCode; + public UInt32 SizeOfInitializedData; + public UInt32 SizeOfUninitializedData; + public UInt32 AddressOfEntryPoint; + public UInt32 BaseOfCode; + public UInt64 ImageBase; + public UInt32 SectionAlignment; + public UInt32 FileAlignment; + public UInt16 MajorOperatingSystemVersion; + public UInt16 MinorOperatingSystemVersion; + public UInt16 MajorImageVersion; + public UInt16 MinorImageVersion; + public UInt16 MajorSubsystemVersion; + public UInt16 MinorSubsystemVersion; + public UInt32 Win32VersionValue; + public UInt32 SizeOfImage; + public UInt32 SizeOfHeaders; + public UInt32 CheckSum; + public UInt16 Subsystem; + public UInt16 DllCharacteristics; + public UInt64 SizeOfStackReserve; + public UInt64 SizeOfStackCommit; + public UInt64 SizeOfHeapReserve; + public UInt64 SizeOfHeapCommit; + public UInt32 LoaderFlags; + public UInt32 NumberOfRvaAndSizes; + + public IMAGE_DATA_DIRECTORY ExportTable; + public IMAGE_DATA_DIRECTORY ImportTable; + public IMAGE_DATA_DIRECTORY ResourceTable; + public IMAGE_DATA_DIRECTORY ExceptionTable; + public IMAGE_DATA_DIRECTORY CertificateTable; + public IMAGE_DATA_DIRECTORY BaseRelocationTable; + public IMAGE_DATA_DIRECTORY Debug; + public IMAGE_DATA_DIRECTORY Architecture; + public IMAGE_DATA_DIRECTORY GlobalPtr; + public IMAGE_DATA_DIRECTORY TLSTable; + public IMAGE_DATA_DIRECTORY LoadConfigTable; + public IMAGE_DATA_DIRECTORY BoundImport; + public IMAGE_DATA_DIRECTORY IAT; + public IMAGE_DATA_DIRECTORY DelayImportDescriptor; + public IMAGE_DATA_DIRECTORY CLRRuntimeHeader; + public IMAGE_DATA_DIRECTORY Reserved; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct IMAGE_FILE_HEADER + { + public UInt16 Machine; + public UInt16 NumberOfSections; + public UInt32 TimeDateStamp; + public UInt32 PointerToSymbolTable; + public UInt32 NumberOfSymbols; + public UInt16 SizeOfOptionalHeader; + public UInt16 Characteristics; + } + + [StructLayout(LayoutKind.Explicit)] + public struct IMAGE_SECTION_HEADER + { + [FieldOffset(0)] + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] + public char[] Name; + [FieldOffset(8)] + public UInt32 VirtualSize; + [FieldOffset(12)] + public UInt32 VirtualAddress; + [FieldOffset(16)] + public UInt32 SizeOfRawData; + [FieldOffset(20)] + public UInt32 PointerToRawData; + [FieldOffset(24)] + public UInt32 PointerToRelocations; + [FieldOffset(28)] + public UInt32 PointerToLinenumbers; + [FieldOffset(32)] + public UInt16 NumberOfRelocations; + [FieldOffset(34)] + public UInt16 NumberOfLinenumbers; + [FieldOffset(36)] + public DataSectionFlags Characteristics; + + public string Section + { + get { return new string(Name); } + } + } + + [Flags] + public enum DataSectionFlags : uint + { + /// + /// Reserved for future use. + /// + TypeReg = 0x00000000, + /// + /// Reserved for future use. + /// + TypeDsect = 0x00000001, + /// + /// Reserved for future use. + /// + TypeNoLoad = 0x00000002, + /// + /// Reserved for future use. + /// + TypeGroup = 0x00000004, + /// + /// The section should not be padded to the next boundary. This flag is obsolete and is replaced by IMAGE_SCN_ALIGN_1BYTES. This is valid only for object files. + /// + TypeNoPadded = 0x00000008, + /// + /// Reserved for future use. + /// + TypeCopy = 0x00000010, + /// + /// The section contains executable code. + /// + ContentCode = 0x00000020, + /// + /// The section contains initialized data. + /// + ContentInitializedData = 0x00000040, + /// + /// The section contains uninitialized data. + /// + ContentUninitializedData = 0x00000080, + /// + /// Reserved for future use. + /// + LinkOther = 0x00000100, + /// + /// The section contains comments or other information. The .drectve section has this type. This is valid for object files only. + /// + LinkInfo = 0x00000200, + /// + /// Reserved for future use. + /// + TypeOver = 0x00000400, + /// + /// The section will not become part of the image. This is valid only for object files. + /// + LinkRemove = 0x00000800, + /// + /// The section contains COMDAT data. For more information, see section 5.5.6, COMDAT Sections (Object Only). This is valid only for object files. + /// + LinkComDat = 0x00001000, + /// + /// Reset speculative exceptions handling bits in the TLB entries for this section. + /// + NoDeferSpecExceptions = 0x00004000, + /// + /// The section contains data referenced through the global pointer (GP). + /// + RelativeGP = 0x00008000, + /// + /// Reserved for future use. + /// + MemPurgeable = 0x00020000, + /// + /// Reserved for future use. + /// + Memory16Bit = 0x00020000, + /// + /// Reserved for future use. + /// + MemoryLocked = 0x00040000, + /// + /// Reserved for future use. + /// + MemoryPreload = 0x00080000, + /// + /// Align data on a 1-byte boundary. Valid only for object files. + /// + Align1Bytes = 0x00100000, + /// + /// Align data on a 2-byte boundary. Valid only for object files. + /// + Align2Bytes = 0x00200000, + /// + /// Align data on a 4-byte boundary. Valid only for object files. + /// + Align4Bytes = 0x00300000, + /// + /// Align data on an 8-byte boundary. Valid only for object files. + /// + Align8Bytes = 0x00400000, + /// + /// Align data on a 16-byte boundary. Valid only for object files. + /// + Align16Bytes = 0x00500000, + /// + /// Align data on a 32-byte boundary. Valid only for object files. + /// + Align32Bytes = 0x00600000, + /// + /// Align data on a 64-byte boundary. Valid only for object files. + /// + Align64Bytes = 0x00700000, + /// + /// Align data on a 128-byte boundary. Valid only for object files. + /// + Align128Bytes = 0x00800000, + /// + /// Align data on a 256-byte boundary. Valid only for object files. + /// + Align256Bytes = 0x00900000, + /// + /// Align data on a 512-byte boundary. Valid only for object files. + /// + Align512Bytes = 0x00A00000, + /// + /// Align data on a 1024-byte boundary. Valid only for object files. + /// + Align1024Bytes = 0x00B00000, + /// + /// Align data on a 2048-byte boundary. Valid only for object files. + /// + Align2048Bytes = 0x00C00000, + /// + /// Align data on a 4096-byte boundary. Valid only for object files. + /// + Align4096Bytes = 0x00D00000, + /// + /// Align data on an 8192-byte boundary. Valid only for object files. + /// + Align8192Bytes = 0x00E00000, + /// + /// The section contains extended relocations. + /// + LinkExtendedRelocationOverflow = 0x01000000, + /// + /// The section can be discarded as needed. + /// + MemoryDiscardable = 0x02000000, + /// + /// The section cannot be cached. + /// + MemoryNotCached = 0x04000000, + /// + /// The section is not pageable. + /// + MemoryNotPaged = 0x08000000, + /// + /// The section can be shared in memory. + /// + MemoryShared = 0x10000000, + /// + /// The section can be executed as code. + /// + MemoryExecute = 0x20000000, + /// + /// The section can be read. + /// + MemoryRead = 0x40000000, + /// + /// The section can be written to. + /// + MemoryWrite = 0x80000000 + } + + [StructLayout(LayoutKind.Explicit)] + public struct IMAGE_IMPORT_DESCRIPTOR + { + [FieldOffset(0)] + public uint Characteristics; + + [FieldOffset(0)] + public uint OriginalFirstThunk; + + [FieldOffset(4)] + public uint TimeDateStamp; + + [FieldOffset(8)] + public uint ForwarderChain; + + [FieldOffset(12)] + public uint Name; + + [FieldOffset(16)] + public uint FirstThunk; + } + } +} diff --git a/Dalamud/Hooking/Internal/ReloadedHook.cs b/Dalamud/Hooking/Internal/ReloadedHook.cs new file mode 100644 index 000000000..4b3acd16d --- /dev/null +++ b/Dalamud/Hooking/Internal/ReloadedHook.cs @@ -0,0 +1,92 @@ +using System; +using System.Reflection; + +using Dalamud.Memory; +using Reloaded.Hooks; + +namespace Dalamud.Hooking.Internal +{ + internal class ReloadedHook : Hook where T : Delegate + { + private readonly Reloaded.Hooks.Definitions.IHook hookImpl; + + /// + /// Initializes a new instance of the class. + /// + /// A memory address to install a hook. + /// Callback function. Delegate must have a same original function prototype. + /// Calling assembly. + internal ReloadedHook(IntPtr address, T detour, Assembly callingAssembly) + : base(address) + { + var hasOtherHooks = HookManager.Originals.ContainsKey(this.Address); + if (!hasOtherHooks) + { + MemoryHelper.ReadRaw(this.Address, 0x32, out var original); + HookManager.Originals[this.Address] = original; + } + + this.hookImpl = ReloadedHooks.Instance.CreateHook(detour, address.ToInt64()); + + HookManager.TrackedHooks.TryAdd(Guid.NewGuid(), new HookInfo(this, detour, callingAssembly)); + } + + /// + public override T Original + { + get + { + this.CheckDisposed(); + return this.hookImpl.OriginalFunction; + } + } + + /// + public override bool IsEnabled + { + get + { + this.CheckDisposed(); + return this.hookImpl.IsHookEnabled; + } + } + + /// + public override string BackendName => "Reloaded"; + + /// + public override void Dispose() + { + if (this.IsDisposed) + return; + + this.Disable(); + + base.Dispose(); + } + + /// + public override void Enable() + { + this.CheckDisposed(); + + if (!this.hookImpl.IsHookActivated) + this.hookImpl.Activate(); + + if (!this.hookImpl.IsHookEnabled) + this.hookImpl.Enable(); + } + + /// + public override void Disable() + { + this.CheckDisposed(); + + if (!this.hookImpl.IsHookActivated) + return; + + if (this.hookImpl.IsHookEnabled) + this.hookImpl.Disable(); + } + } +} diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index e6a7e37c2..fb548b331 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -11,6 +11,7 @@ using Dalamud.Configuration.Internal; using Dalamud.Game; using Dalamud.Game.ClientState.GamePad; using Dalamud.Game.ClientState.Keys; +using Dalamud.Game.Gui.Internal; using Dalamud.Game.Internal.DXGI; using Dalamud.Hooking; using Dalamud.Interface.GameFonts; @@ -27,6 +28,7 @@ using Serilog; using SharpDX.Direct3D11; // general dev notes, here because it's easiest + /* * - Hooking ResizeBuffers seemed to be unnecessary, though I'm not sure why. Left out for now since it seems to work without it. * - We may want to build our ImGui command list in a thread to keep it divorced from present. We'd still have to block in present to @@ -52,18 +54,17 @@ namespace Dalamud.Interface.Internal private const ushort Fallback1Codepoint = 0x3013; // Geta mark; FFXIV uses this to indicate that a glyph is missing. private const ushort Fallback2Codepoint = '-'; // FFXIV uses dash if Geta mark is unavailable. - private readonly string rtssPath; - private readonly HashSet glyphRequests = new(); private readonly Dictionary loadedFontInfo = new(); private readonly ManualResetEvent fontBuildSignal; private readonly SwapChainVtableResolver address; + private readonly Hook dispatchMessageWHook; + private readonly Hook setCursorHook; private RawDX11Scene? scene; private Hook? presentHook; private Hook? resizeBuffersHook; - private Hook? setCursorHook; // can't access imgui IO before first present call private bool lastWantCapture = false; @@ -72,31 +73,16 @@ namespace Dalamud.Interface.Internal private bool isOverrideGameCursor = true; [ServiceManager.ServiceConstructor] - private InterfaceManager() + private InterfaceManager(SigScanner sigScanner) { + this.dispatchMessageWHook = Hook.FromImport( + Process.GetCurrentProcess().MainModule, "user32.dll", "DispatchMessageW", 0, this.DispatchMessageWDetour); + this.setCursorHook = Hook.FromImport( + Process.GetCurrentProcess().MainModule, "user32.dll", "SetCursor", 0, this.SetCursorDetour); + this.fontBuildSignal = new ManualResetEvent(false); this.address = new SwapChainVtableResolver(); - - try - { - var rtss = NativeFunctions.GetModuleHandleW("RTSSHooks64.dll"); - - if (rtss != IntPtr.Zero) - { - var fileName = new StringBuilder(255); - _ = NativeFunctions.GetModuleFileNameW(rtss, fileName, fileName.Capacity); - this.rtssPath = fileName.ToString(); - Log.Verbose($"RTSS at {this.rtssPath}"); - - if (!NativeFunctions.FreeLibrary(rtss)) - throw new Win32Exception(); - } - } - catch (Exception e) - { - Log.Error(e, "RTSS Free failed"); - } } [UnmanagedFunctionPointer(CallingConvention.ThisCall)] @@ -105,10 +91,17 @@ namespace Dalamud.Interface.Internal [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate IntPtr ResizeBuffersDelegate(IntPtr swapChain, uint bufferCount, uint width, uint height, uint newFormat, uint swapChainFlags); - [UnmanagedFunctionPointer(CallingConvention.ThisCall)] + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + private delegate int CreateDXGIFactoryDelegate(Guid riid, out IntPtr ppFactory); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + private delegate int IDXGIFactory_CreateSwapChainDelegate(IntPtr pFactory, IntPtr pDevice, IntPtr pDesc, out IntPtr ppSwapChain); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate IntPtr SetCursorDelegate(IntPtr hCursor); - private delegate void InstallRTSSHook(); + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + private delegate IntPtr DispatchMessageWDelegate(ref User32.MSG msg); /// /// This event gets called each frame to facilitate ImGui drawing. @@ -245,6 +238,11 @@ namespace Dalamud.Interface.Internal /// public bool IsBuildingFontsBeforeAtlasBuild => this.isRebuildingFonts && !this.fontBuildSignal.WaitOne(0); + /// + /// Gets a value indicating the native handle of the game main window. + /// + public IntPtr GameWindowHandle { get; private set; } + /// /// Dispose of managed and unmanaged resources. /// @@ -260,9 +258,10 @@ namespace Dalamud.Interface.Internal Thread.Sleep(500); this.scene?.Dispose(); - this.setCursorHook?.Dispose(); + this.setCursorHook.Dispose(); this.presentHook?.Dispose(); this.resizeBuffersHook?.Dispose(); + this.dispatchMessageWHook.Dispose(); } #nullable enable @@ -428,6 +427,132 @@ namespace Dalamud.Interface.Internal Util.Fatal($"One or more files required by XIVLauncher were not found.\nPlease restart and report this error if it occurs again.\n\n{path}", "Error"); } + private void InitScene(IntPtr swapChain) + { + RawDX11Scene newScene; + using (Timings.Start("IM Scene Init")) + { + try + { + newScene = new RawDX11Scene(swapChain); + } + catch (DllNotFoundException ex) + { + Service.ProvideException(ex); + Log.Error(ex, "Could not load ImGui dependencies."); + + var res = PInvoke.User32.MessageBox( + IntPtr.Zero, + "Dalamud plugins require the Microsoft Visual C++ Redistributable to be installed.\nPlease install the runtime from the official Microsoft website or disable Dalamud.\n\nDo you want to download the redistributable now?", + "Dalamud Error", + User32.MessageBoxOptions.MB_YESNO | User32.MessageBoxOptions.MB_TOPMOST | User32.MessageBoxOptions.MB_ICONERROR); + + if (res == User32.MessageBoxResult.IDYES) + { + var psi = new ProcessStartInfo + { + FileName = "https://aka.ms/vs/16/release/vc_redist.x64.exe", + UseShellExecute = true, + }; + Process.Start(psi); + } + + Environment.Exit(-1); + + // Doesn't reach here, but to make the compiler not complain + return; + } + + var startInfo = Service.Get(); + var configuration = Service.Get(); + + var iniFileInfo = new FileInfo(Path.Combine(Path.GetDirectoryName(startInfo.ConfigurationPath), "dalamudUI.ini")); + + try + { + if (iniFileInfo.Length > 1200000) + { + Log.Warning("dalamudUI.ini was over 1mb, deleting"); + iniFileInfo.CopyTo(Path.Combine(iniFileInfo.DirectoryName, $"dalamudUI-{DateTimeOffset.Now.ToUnixTimeSeconds()}.ini")); + iniFileInfo.Delete(); + } + } + catch (Exception ex) + { + Log.Error(ex, "Could not delete dalamudUI.ini"); + } + + newScene.UpdateCursor = this.isOverrideGameCursor; + newScene.ImGuiIniPath = iniFileInfo.FullName; + newScene.OnBuildUI += this.Display; + newScene.OnNewInputFrame += this.OnNewInputFrame; + + StyleModel.TransferOldModels(); + + if (configuration.SavedStyles == null || configuration.SavedStyles.All(x => x.Name != StyleModelV1.DalamudStandard.Name)) + { + configuration.SavedStyles = new List { StyleModelV1.DalamudStandard, StyleModelV1.DalamudClassic }; + configuration.ChosenStyle = StyleModelV1.DalamudStandard.Name; + } + else if (configuration.SavedStyles.Count == 1) + { + configuration.SavedStyles.Add(StyleModelV1.DalamudClassic); + } + else if (configuration.SavedStyles[1].Name != StyleModelV1.DalamudClassic.Name) + { + configuration.SavedStyles.Insert(1, StyleModelV1.DalamudClassic); + } + + configuration.SavedStyles[0] = StyleModelV1.DalamudStandard; + configuration.SavedStyles[1] = StyleModelV1.DalamudClassic; + + var style = configuration.SavedStyles.FirstOrDefault(x => x.Name == configuration.ChosenStyle); + if (style == null) + { + style = StyleModelV1.DalamudStandard; + configuration.ChosenStyle = style.Name; + configuration.Save(); + } + + style.Apply(); + + ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale; + + this.SetupFonts(); + + if (!configuration.IsDocking) + { + ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.DockingEnable; + } + else + { + ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.DockingEnable; + } + + // NOTE (Chiv) Toggle gamepad navigation via setting + if (!configuration.IsGamepadNavigationEnabled) + { + ImGui.GetIO().BackendFlags &= ~ImGuiBackendFlags.HasGamepad; + ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.NavEnableSetMousePos; + } + else + { + ImGui.GetIO().BackendFlags |= ImGuiBackendFlags.HasGamepad; + ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.NavEnableSetMousePos; + } + + // NOTE (Chiv) Explicitly deactivate on dalamud boot + ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.NavEnableGamepad; + + ImGuiHelpers.MainViewport = ImGui.GetMainViewport(); + + Log.Information("[IM] Scene & ImGui setup OK!"); + } + + this.scene = newScene; + Service.Provide(new(this)); + } + /* * NOTE(goat): When hooking ReShade DXGISwapChain::runtime_present, this is missing the syncInterval arg. * Seems to work fine regardless, I guess, so whatever. @@ -438,125 +563,7 @@ namespace Dalamud.Interface.Internal return this.presentHook!.Original(swapChain, syncInterval, presentFlags); if (this.scene == null) - { - using (Timings.Start("IM Scene Init")) - { - try - { - this.scene = new RawDX11Scene(swapChain); - } - catch (DllNotFoundException ex) - { - Service.ProvideException(ex); - Log.Error(ex, "Could not load ImGui dependencies."); - - var res = PInvoke.User32.MessageBox( - IntPtr.Zero, - "Dalamud plugins require the Microsoft Visual C++ Redistributable to be installed.\nPlease install the runtime from the official Microsoft website or disable Dalamud.\n\nDo you want to download the redistributable now?", - "Dalamud Error", - User32.MessageBoxOptions.MB_YESNO | User32.MessageBoxOptions.MB_TOPMOST | User32.MessageBoxOptions.MB_ICONERROR); - - if (res == User32.MessageBoxResult.IDYES) - { - var psi = new ProcessStartInfo - { - FileName = "https://aka.ms/vs/16/release/vc_redist.x64.exe", - UseShellExecute = true, - }; - Process.Start(psi); - } - - Environment.Exit(-1); - } - - var startInfo = Service.Get(); - var configuration = Service.Get(); - - var iniFileInfo = new FileInfo(Path.Combine(Path.GetDirectoryName(startInfo.ConfigurationPath), "dalamudUI.ini")); - - try - { - if (iniFileInfo.Length > 1200000) - { - Log.Warning("dalamudUI.ini was over 1mb, deleting"); - iniFileInfo.CopyTo(Path.Combine(iniFileInfo.DirectoryName, $"dalamudUI-{DateTimeOffset.Now.ToUnixTimeSeconds()}.ini")); - iniFileInfo.Delete(); - } - } - catch (Exception ex) - { - Log.Error(ex, "Could not delete dalamudUI.ini"); - } - - this.scene.UpdateCursor = this.isOverrideGameCursor; - this.scene.ImGuiIniPath = iniFileInfo.FullName; - this.scene.OnBuildUI += this.Display; - this.scene.OnNewInputFrame += this.OnNewInputFrame; - - StyleModel.TransferOldModels(); - - if (configuration.SavedStyles == null || configuration.SavedStyles.All(x => x.Name != StyleModelV1.DalamudStandard.Name)) - { - configuration.SavedStyles = new List { StyleModelV1.DalamudStandard, StyleModelV1.DalamudClassic }; - configuration.ChosenStyle = StyleModelV1.DalamudStandard.Name; - } - else if (configuration.SavedStyles.Count == 1) - { - configuration.SavedStyles.Add(StyleModelV1.DalamudClassic); - } - else if (configuration.SavedStyles[1].Name != StyleModelV1.DalamudClassic.Name) - { - configuration.SavedStyles.Insert(1, StyleModelV1.DalamudClassic); - } - - configuration.SavedStyles[0] = StyleModelV1.DalamudStandard; - configuration.SavedStyles[1] = StyleModelV1.DalamudClassic; - - var style = configuration.SavedStyles.FirstOrDefault(x => x.Name == configuration.ChosenStyle); - if (style == null) - { - style = StyleModelV1.DalamudStandard; - configuration.ChosenStyle = style.Name; - configuration.Save(); - } - - style.Apply(); - - ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale; - - this.SetupFonts(); - - if (!configuration.IsDocking) - { - ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.DockingEnable; - } - else - { - ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.DockingEnable; - } - - // NOTE (Chiv) Toggle gamepad navigation via setting - if (!configuration.IsGamepadNavigationEnabled) - { - ImGui.GetIO().BackendFlags &= ~ImGuiBackendFlags.HasGamepad; - ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.NavEnableSetMousePos; - } - else - { - ImGui.GetIO().BackendFlags |= ImGuiBackendFlags.HasGamepad; - ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.NavEnableSetMousePos; - } - - // NOTE (Chiv) Explicitly deactivate on dalamud boot - ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.NavEnableGamepad; - - ImGuiHelpers.MainViewport = ImGui.GetMainViewport(); - - Log.Information("[IM] Scene & ImGui setup OK!"); - - Service.Get().RunOnFrameworkThread(() => Service.Provide(new(this))); - } - } + this.InitScene(swapChain); if (this.address.IsReshade) { @@ -975,44 +982,34 @@ namespace Dalamud.Interface.Internal this.address.Setup(sigScanner); framework.RunOnFrameworkThread(() => { - this.setCursorHook = Hook.FromSymbol("user32.dll", "SetCursor", this.SetCursorDetour, true)!; + while ((this.GameWindowHandle = NativeFunctions.FindWindowEx(IntPtr.Zero, this.GameWindowHandle, "FFXIVGAME", IntPtr.Zero)) != IntPtr.Zero) + { + _ = User32.GetWindowThreadProcessId(this.GameWindowHandle, out var pid); + + if (pid == Environment.ProcessId && User32.IsWindowVisible(this.GameWindowHandle)) + break; + } + this.presentHook = new Hook(this.address.Present, this.PresentDetour); this.resizeBuffersHook = new Hook(this.address.ResizeBuffers, this.ResizeBuffersDetour); Log.Verbose("===== S W A P C H A I N ====="); - Log.Verbose($"SetCursor address 0x{this.setCursorHook!.Address.ToInt64():X}"); Log.Verbose($"Present address 0x{this.presentHook!.Address.ToInt64():X}"); Log.Verbose($"ResizeBuffers address 0x{this.resizeBuffersHook!.Address.ToInt64():X}"); this.setCursorHook.Enable(); this.presentHook.Enable(); this.resizeBuffersHook.Enable(); - - try - { - if (!string.IsNullOrEmpty(this.rtssPath)) - { - NativeFunctions.LoadLibraryW(this.rtssPath); - var rtssModule = NativeFunctions.GetModuleHandleW("RTSSHooks64.dll"); - var installAddr = NativeFunctions.GetProcAddress(rtssModule, "InstallRTSSHook"); - - Log.Debug("Installing RTSS hook"); - Marshal.GetDelegateForFunctionPointer(installAddr).Invoke(); - Log.Debug("RTSS hook OK!"); - } - } - catch (Exception ex) - { - Log.Error(ex, "Could not reload RTSS"); - } + this.dispatchMessageWHook.Enable(); }); } private void Disable() { - this.setCursorHook?.Disable(); + this.setCursorHook.Disable(); this.presentHook?.Disable(); this.resizeBuffersHook?.Disable(); + this.dispatchMessageWHook.Disable(); } // This is intended to only be called as a handler attached to scene.OnNewRenderFrame @@ -1059,6 +1056,23 @@ namespace Dalamud.Interface.Internal this.isRebuildingFonts = false; } + private unsafe IntPtr DispatchMessageWDetour(ref User32.MSG msg) + { + if (msg.hwnd == this.GameWindowHandle && this.scene != null) + { + var ime = Service.GetNullable(); + var res = ime?.ProcessWndProcW(msg.hwnd, msg.message, (void*)msg.wParam, (void*)msg.lParam); + if (res != null) + return res.Value; + + res = this.scene.ProcessWndProcW(msg.hwnd, msg.message, (void*)msg.wParam, (void*)msg.lParam); + if (res != null) + return res.Value; + } + + return this.dispatchMessageWHook.Original(ref msg); + } + private IntPtr ResizeBuffersDetour(IntPtr swapChain, uint bufferCount, uint width, uint height, uint newFormat, uint swapChainFlags) { #if DEBUG diff --git a/Dalamud/NativeFunctions.cs b/Dalamud/NativeFunctions.cs index 5b03eeb33..05e2feabc 100644 --- a/Dalamud/NativeFunctions.cs +++ b/Dalamud/NativeFunctions.cs @@ -1906,6 +1906,21 @@ namespace Dalamud /// public int ClientPointers; } + + /// + /// Finds window according to conditions. + /// + /// Handle to parent window. + /// Window to search after. + /// Name of class. + /// Name of window. + /// Found window, or null. + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern IntPtr FindWindowEx( + IntPtr parentHandle, + IntPtr childAfter, + string className, + IntPtr windowTitle); } /// diff --git a/lib/ImGuiScene b/lib/ImGuiScene index de1a512bc..5da5fb742 160000 --- a/lib/ImGuiScene +++ b/lib/ImGuiScene @@ -1 +1 @@ -Subproject commit de1a512bc77eed279f9909a3a233068a51a57b21 +Subproject commit 5da5fb742375aba55026f3beeef05dfe876a21bd