diff --git a/Dalamud/Hooking/Internal/CallHook.cs b/Dalamud/Hooking/Internal/CallHook.cs
index c9b5562ba..5b438b5a8 100644
--- a/Dalamud/Hooking/Internal/CallHook.cs
+++ b/Dalamud/Hooking/Internal/CallHook.cs
@@ -15,10 +15,10 @@ namespace Dalamud.Hooking.Internal;
/// Only the specific callsite hooked is modified, if the game calls the virtual function from other locations this hook will not be triggered.
///
/// Delegate signature for this hook.
-internal class CallHook : IDisposable where T : Delegate
+internal class CallHook : IDalamudHook where T : Delegate
{
private readonly Reloaded.Hooks.AsmHook asmHook;
-
+
private T? detour;
private bool activated;
@@ -29,7 +29,10 @@ internal class CallHook : IDisposable where T : Delegate
/// Delegate to invoke.
internal CallHook(nint address, T detour)
{
+ ArgumentNullException.ThrowIfNull(detour);
+
this.detour = detour;
+ this.Address = address;
var detourPtr = Marshal.GetFunctionPointerForDelegate(this.detour);
var code = new[]
@@ -38,14 +41,14 @@ internal class CallHook : IDisposable where T : Delegate
$"mov rax, 0x{detourPtr:X8}",
"call rax",
};
-
+
var opt = new AsmHookOptions
{
PreferRelativeJump = true,
Behaviour = Reloaded.Hooks.Definitions.Enums.AsmHookBehaviour.DoNotExecuteOriginal,
MaxOpcodeSize = 5,
};
-
+
this.asmHook = new Reloaded.Hooks.AsmHook(code, (nuint)address, opt);
}
@@ -53,7 +56,16 @@ internal class CallHook : IDisposable where T : Delegate
/// Gets a value indicating whether or not the hook is enabled.
///
public bool IsEnabled => this.asmHook.IsEnabled;
-
+
+ ///
+ public IntPtr Address { get; }
+
+ ///
+ public string BackendName => "Reloaded AsmHook";
+
+ ///
+ public bool IsDisposed => this.detour == null;
+
///
/// Starts intercepting a call to the function.
///
@@ -65,7 +77,7 @@ internal class CallHook : IDisposable where T : Delegate
this.asmHook.Activate();
return;
}
-
+
this.asmHook.Enable();
}
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs
index b24587d6c..ec5f12d6e 100644
--- a/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs
@@ -1,6 +1,15 @@
-using System.Runtime.InteropServices;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Dalamud.Game;
+using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Hooking;
+using Dalamud.Hooking.Internal;
+
+using FFXIVClientStructs.FFXIV.Component.GUI;
+
using ImGuiNET;
using PInvoke;
using Serilog;
@@ -10,23 +19,46 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
///
/// Widget for displaying hook information.
///
-internal class HookWidget : IDataWindowWidget
+internal unsafe class HookWidget : IDataWindowWidget
{
+ private readonly List hookStressTestList = [];
+
private Hook? messageBoxMinHook;
private bool hookUseMinHook;
-
+
+ private int hookStressTestCount = 0;
+ private int hookStressTestMax = 1000;
+ private int hookStressTestWait = 100;
+ private int hookStressTestMaxDegreeOfParallelism = 10;
+ private StressTestHookTarget hookStressTestHookTarget = StressTestHookTarget.Random;
+ private bool hookStressTestRunning = false;
+
+ private MessageBoxWDelegate? messageBoxWOriginal;
+ private AddonFinalizeDelegate? addonFinalizeOriginal;
+
+ private AddonLifecycleAddressResolver? address;
+
private delegate int MessageBoxWDelegate(
IntPtr hWnd,
[MarshalAs(UnmanagedType.LPWStr)] string text,
[MarshalAs(UnmanagedType.LPWStr)] string caption,
NativeFunctions.MessageBoxType type);
-
+
+ private delegate void AddonFinalizeDelegate(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase);
+
+ private enum StressTestHookTarget
+ {
+ MessageBoxW,
+ AddonFinalize,
+ Random,
+ }
+
///
- public string DisplayName { get; init; } = "Hook";
+ public string DisplayName { get; init; } = "Hook";
///
public string[]? CommandShortcuts { get; init; } = { "hook" };
-
+
///
public bool Ready { get; set; }
@@ -34,6 +66,9 @@ internal class HookWidget : IDataWindowWidget
public void Load()
{
this.Ready = true;
+
+ this.address = new AddonLifecycleAddressResolver();
+ this.address.Setup(Service.Get());
}
///
@@ -41,7 +76,9 @@ internal class HookWidget : IDataWindowWidget
{
try
{
- ImGui.Checkbox("Use MinHook", ref this.hookUseMinHook);
+ ImGui.Checkbox("Use MinHook (only for regular hooks, AsmHook is Reloaded-only)", ref this.hookUseMinHook);
+
+ ImGui.Separator();
if (ImGui.Button("Create"))
this.messageBoxMinHook = Hook.FromSymbol("User32", "MessageBoxW", this.MessageBoxWDetour, this.hookUseMinHook);
@@ -66,18 +103,94 @@ internal class HookWidget : IDataWindowWidget
if (this.messageBoxMinHook != null)
ImGui.Text("Enabled: " + this.messageBoxMinHook?.IsEnabled);
+
+ ImGui.Separator();
+
+ ImGui.BeginDisabled(this.hookStressTestRunning);
+ ImGui.Text("Stress Test");
+
+ if (ImGui.InputInt("Max", ref this.hookStressTestMax))
+ this.hookStressTestCount = 0;
+
+ ImGui.InputInt("Wait (ms)", ref this.hookStressTestWait);
+ ImGui.InputInt("Max Degree of Parallelism", ref this.hookStressTestMaxDegreeOfParallelism);
+
+ if (ImGui.BeginCombo("Target", HookTargetToString(this.hookStressTestHookTarget)))
+ {
+ foreach (var target in Enum.GetValues())
+ {
+ if (ImGui.Selectable(HookTargetToString(target), this.hookStressTestHookTarget == target))
+ this.hookStressTestHookTarget = target;
+ }
+
+ ImGui.EndCombo();
+ }
+
+ if (ImGui.Button("Stress Test"))
+ {
+ Task.Run(() =>
+ {
+ this.hookStressTestRunning = true;
+ this.hookStressTestCount = 0;
+ Parallel.For(
+ 0,
+ this.hookStressTestMax,
+ new ParallelOptions
+ {
+ MaxDegreeOfParallelism = this.hookStressTestMaxDegreeOfParallelism,
+ },
+ _ =>
+ {
+ this.hookStressTestList.Add(this.HookTarget(this.hookStressTestHookTarget));
+ this.hookStressTestCount++;
+ Thread.Sleep(this.hookStressTestWait);
+ });
+ }).ContinueWith(t =>
+ {
+ if (t.IsFaulted)
+ {
+ Log.Error(t.Exception, "Stress test failed");
+ }
+ else
+ {
+ Log.Information("Stress test completed");
+ }
+
+ this.hookStressTestRunning = false;
+ this.hookStressTestList.ForEach(hook =>
+ {
+ hook.Dispose();
+ });
+ this.hookStressTestList.Clear();
+ });
+ }
+
+ ImGui.EndDisabled();
+
+ ImGui.TextUnformatted("Status: " + (this.hookStressTestRunning ? "Running" : "Idle"));
+ ImGui.ProgressBar(this.hookStressTestCount / (float)this.hookStressTestMax, new System.Numerics.Vector2(0, 0), $"{this.hookStressTestCount}/{this.hookStressTestMax}");
}
catch (Exception ex)
{
- Log.Error(ex, "MinHook error caught");
+ Log.Error(ex, "Hook error caught");
}
}
-
+
+ private static string HookTargetToString(StressTestHookTarget target)
+ {
+ return target switch
+ {
+ StressTestHookTarget.MessageBoxW => "MessageBoxW (Hook)",
+ StressTestHookTarget.AddonFinalize => "AddonFinalize (Hook)",
+ _ => target.ToString(),
+ };
+ }
+
private int MessageBoxWDetour(IntPtr hwnd, string text, string caption, NativeFunctions.MessageBoxType type)
{
Log.Information("[DATAHOOK] {Hwnd} {Text} {Caption} {Type}", hwnd, text, caption, type);
- var result = this.messageBoxMinHook!.Original(hwnd, "Cause Access Violation?", caption, NativeFunctions.MessageBoxType.YesNo);
+ var result = this.messageBoxWOriginal!(hwnd, "Cause Access Violation?", caption, NativeFunctions.MessageBoxType.YesNo);
if (result == (int)User32.MessageBoxResult.IDYES)
{
@@ -86,4 +199,52 @@ internal class HookWidget : IDataWindowWidget
return result;
}
+
+ private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase)
+ {
+ Log.Information("OnAddonFinalize");
+ this.addonFinalizeOriginal!(unitManager, atkUnitBase);
+ }
+
+ private void OnAddonUpdate(AtkUnitBase* thisPtr, float delta)
+ {
+ Log.Information("OnAddonUpdate");
+ }
+
+ private IDalamudHook HookMessageBoxW()
+ {
+ var hook = Hook.FromSymbol(
+ "User32",
+ "MessageBoxW",
+ this.MessageBoxWDetour,
+ this.hookUseMinHook);
+
+ this.messageBoxWOriginal = hook.Original;
+ hook.Enable();
+ return hook;
+ }
+
+ private IDalamudHook HookAddonFinalize()
+ {
+ var hook = Hook.FromAddress(this.address!.AddonFinalize, this.OnAddonFinalize);
+
+ this.addonFinalizeOriginal = hook.Original;
+ hook.Enable();
+ return hook;
+ }
+
+ private IDalamudHook HookTarget(StressTestHookTarget target)
+ {
+ if (target == StressTestHookTarget.Random)
+ {
+ target = (StressTestHookTarget)Random.Shared.Next(0, 2);
+ }
+
+ return target switch
+ {
+ StressTestHookTarget.MessageBoxW => this.HookMessageBoxW(),
+ StressTestHookTarget.AddonFinalize => this.HookAddonFinalize(),
+ _ => throw new ArgumentOutOfRangeException(nameof(target), target, null),
+ };
+ }
}