Skip to main content

Overview

Dalamud provides a powerful hooking system that allows you to intercept calls to game functions, inspect their parameters, modify their behavior, or call the original function. The hooking system supports multiple backends (Reloaded and MinHook) and various hooking strategies.
Hooking is a powerful but dangerous feature. Incorrect hooks can crash the game or cause undefined behavior. Always test thoroughly and handle errors gracefully.

Creating Hooks

Hook from Address

The most common way to create a hook is from a memory address:
using Dalamud.Hooking;
using Dalamud.Plugin.Services;

public class MyPlugin
{
    private delegate nint MyFunctionDelegate(nint param1, int param2);
    private Hook<MyFunctionDelegate>? myHook;
    
    public void Initialize(IGameInteropProvider interop, nint address)
    {
        // Create hook from address
        this.myHook = interop.HookFromAddress<MyFunctionDelegate>(
            address,
            this.MyDetour
        );
        
        // Enable the hook
        this.myHook.Enable();
    }
    
    private nint MyDetour(nint param1, int param2)
    {
        // Your custom code here
        PluginLog.Information($"Function called with: {param1:X}, {param2}");
        
        // Call the original function
        return this.myHook!.Original(param1, param2);
    }
}

Hook from Signature

You can create a hook directly from a signature without manually scanning:
public void InitializeFromSignature(IGameInteropProvider interop)
{
    var signature = "E8 ?? ?? ?? ?? 48 8B 4C 24 ?? 48 85 C9";
    
    this.myHook = interop.HookFromSignature<MyFunctionDelegate>(
        signature,
        this.MyDetour
    );
    
    this.myHook.Enable();
}

Hook from Symbol

For hooking exported functions from DLLs:
public void HookWinApi(IGameInteropProvider interop)
{
    var sendHook = interop.HookFromSymbol<SendDelegate>(
        "ws2_32.dll",
        "send",
        this.SendDetour
    );
    
    sendHook.Enable();
}

Using Signature Attributes

The recommended approach is to use the [Signature] attribute with IGameInteropProvider.InitializeFromAttributes():
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;

public class MyHooks
{
    private delegate void SomeGameFunctionDelegate(nint a1, int a2);
    
    [Signature(
        "E8 ?? ?? ?? ?? 48 8B 4C 24 ??",
        DetourName = nameof(SomeGameFunctionDetour)
    )]
    private Hook<SomeGameFunctionDelegate>? someGameFunctionHook;
    
    public void Initialize(IGameInteropProvider interop)
    {
        // This will automatically create and assign the hook
        interop.InitializeFromAttributes(this);
        
        // Enable all hooks
        this.someGameFunctionHook?.Enable();
    }
    
    private void SomeGameFunctionDetour(nint a1, int a2)
    {
        PluginLog.Debug($"Hooked function called: {a1:X}, {a2}");
        this.someGameFunctionHook!.Original(a1, a2);
    }
    
    public void Dispose()
    {
        this.someGameFunctionHook?.Dispose();
    }
}

Hook Lifecycle

Enabling and Disabling

// Enable the hook
myHook.Enable();

// Check if enabled
if (myHook.IsEnabled)
{
    // Hook is active
}

// Temporarily disable
myHook.Disable();

// Re-enable
myHook.Enable();

Disposal

Always dispose hooks when you’re done:
public void Dispose()
{
    // Dispose automatically disables the hook
    this.myHook?.Dispose();
}

Calling Original Functions

You have two ways to call the original function:
private nint MyDetour(nint param1, int param2)
{
    // Standard way - throws if disposed
    var result = this.myHook!.Original(param1, param2);
    
    // Dispose-safe way - works even after disposal
    var result2 = this.myHook!.OriginalDisposeSafe(param1, param2);
    
    return result;
}
Use OriginalDisposeSafe when you might need to call the original function during or after cleanup.

Assembly Hooks

For advanced scenarios, you can inject raw assembly code:
using Dalamud.Hooking;

public class AssemblyHookExample
{
    private AsmHook? myAsmHook;
    
    public void CreateAsmHook(nint address)
    {
        // Assembly instructions to execute
        string[] assembly = 
        {
            "use64",
            "push rax",
            "mov rax, 0x123456",
            "pop rax",
            "ret"
        };
        
        this.myAsmHook = new AsmHook(
            address,
            assembly,
            "MyAsmHook",
            AsmHookBehaviour.ExecuteFirst
        );
        
        this.myAsmHook.Enable();
    }
}

Assembly Hook Behaviors

  • ExecuteFirst: Your assembly runs before the original code
  • ExecuteAfter: Your assembly runs after the original code
  • DoNotExecuteOriginal: Only your assembly runs (dangerous!)
Assembly hooks are extremely low-level and require deep understanding of x64 assembly and calling conventions. Use with extreme caution.

Hook Backends

Reloaded (Default)

The default backend, recommended for most use cases:
var hook = interop.HookFromAddress<MyDelegate>(
    address,
    detour,
    IGameInteropProvider.HookBackend.Reloaded
);

MinHook

An alternative backend for compatibility:
var hook = interop.HookFromAddress<MyDelegate>(
    address,
    detour,
    IGameInteropProvider.HookBackend.MinHook
);
Only use MinHook if you’ve confirmed that Reloaded doesn’t work for your specific case. The Reloaded backend is more reliable in most scenarios.

Best Practices

Error Handling

Always wrap detour logic in try-catch blocks:
private nint MyDetour(nint param1, int param2)
{
    try
    {
        // Your logic here
        PluginLog.Debug($"Called with {param1:X}");
    }
    catch (Exception ex)
    {
        PluginLog.Error(ex, "Error in hook detour");
    }
    
    // Always call original
    return this.myHook!.Original(param1, param2);
}

State Management

Be careful with shared state in detours:
private readonly object lockObject = new();
private int callCount = 0;

private nint MyDetour(nint param1, int param2)
{
    lock (this.lockObject)
    {
        this.callCount++;
    }
    
    return this.myHook!.Original(param1, param2);
}

Performance

Minimize work in frequently-called hooks:
private nint HighFrequencyDetour(nint param1, int param2)
{
    // Bad: Heavy logging in hot path
    // PluginLog.Information($"Called {DateTime.Now}");
    
    // Good: Conditional logging
    if (this.debugMode)
    {
        PluginLog.Debug($"Called with {param1:X}");
    }
    
    return this.myHook!.Original(param1, param2);
}

Common Patterns

Pre-Processing

private void PreProcessDetour(nint data, int size)
{
    // Modify parameters before calling original
    if (size > 1024)
    {
        PluginLog.Warning("Large size detected, clamping");
        size = 1024;
    }
    
    this.myHook!.Original(data, size);
}

Post-Processing

private int PostProcessDetour(nint data)
{
    // Call original first
    var result = this.myHook!.Original(data);
    
    // Process result
    if (result < 0)
    {
        PluginLog.Error($"Function returned error: {result}");
    }
    
    return result;
}

Conditional Execution

private void ConditionalDetour(nint param1, int param2)
{
    if (this.shouldBlock)
    {
        // Don't call original - block the function
        PluginLog.Info("Function blocked");
        return;
    }
    
    this.myHook!.Original(param1, param2);
}

Hook Properties

You can inspect hook properties:
PluginLog.Info($"Hook address: {myHook.Address:X}");
PluginLog.Info($"Hook enabled: {myHook.IsEnabled}");
PluginLog.Info($"Hook disposed: {myHook.IsDisposed}");
PluginLog.Info($"Backend: {myHook.BackendName}");

Build docs developers (and LLMs) love