Skip to main content

Overview

FFXIVClientStructs uses byte pattern signatures to resolve function and data locations at runtime. This allows the library to work across game updates by finding code patterns instead of using hardcoded addresses.

Initialization

For Dalamud Plugins

If you’re writing a Dalamud plugin using the built-in copy of the library, no initialization is required. Dalamud handles this automatically.

Manual Initialization

If using a standalone copy or local version:
InteropGenerator.Runtime.Resolver.GetInstance.Setup();
FFXIVClientStructs.Interop.Generated.Addresses.Register();
InteropGenerator.Runtime.Resolver.GetInstance.Resolve();
This three-step process sets up the resolver, registers all addresses, and resolves them from signatures.

Setup Parameters

The Setup() method has three optional arguments:
public void Setup(
    void* moduleBase = null,
    string? version = null,
    FileInfo? cacheFile = null
)

Module Base

The first argument allows you to pass a pointer to a copy of the FFXIV module in memory:
// Example: Use Dalamud's clean module copy
var scanner = GetService<SigScanner>();
Resolver.GetInstance.Setup((void*)scanner.SearchBase);
Purpose: Scan a fresh copy of the binary rather than one modified by active hooks from other sources.

Version String

The second argument is the key for which cache index to use from the cache file:
Resolver.GetInstance.Setup(null, "2024.01.01.0000.0000");

Cache File

The third argument takes a path to a JSON file for signature caching:
var cacheFile = new FileInfo("path/to/cache.json");
Resolver.GetInstance.Setup(null, gameVersion, cacheFile);
Benefits:
  • The resolver is relatively fast, but using the cache is near-instant
  • Speeds up subsequent launches significantly
  • Optional but recommended for production use
With caching enabled, resolution on subsequent runs is nearly instantaneous instead of taking several seconds.

Signature Format

All signatures in the library must follow strict formatting rules:
  • Use ?? for wildcard bytes (bytes that may vary)
  • Include 2 characters per byte (e.g., 48 8B not 488B)
  • Separate bytes with spaces
// ✅ Correct format
"E8 ?? ?? ?? ?? C1 E7 0C"
"48 8B 1D ?? ?? ?? ?? 8B 7C 24"

// ❌ Incorrect formats
"E8 ? ? ? ? C1 E7 0C"        // Single ? instead of ??
"488B1D????????8B7C24"        // No spaces
"E8 ?? ?? ?? ?? C1E70C"       // Inconsistent spacing
Incorrect signature format will cause resolution to fail. Always use two ? characters per wildcard byte.

Function Resolution

Member Functions

Member functions are resolved using code signatures:
[MemberFunction("E8 ?? ?? ?? ?? C1 E7 0C")]
public partial void AddEvent(
    AtkEventType eventType, 
    uint eventParam, 
    AtkEventListener* listener,
    AtkResNode* nodeParam, 
    bool isGlobalEvent
);
The generator creates resolution code and a wrapper:
public partial void AddEvent(...)
{
    if (MemberFunctionPointers.AddEvent is null)
    {
        InteropGenerator.Runtime.ThrowHelper.ThrowNullAddress(
            "MemberFunctionPointers.AddEvent", 
            "E8 ?? ?? ?? ?? C1 E7 0C"
        );
    }
    MemberFunctionPointers.AddEvent(
        (AtkResNode*)Unsafe.AsPointer(ref this), 
        eventType, 
        eventParam, 
        listener, 
        nodeParam, 
        isGlobalEvent
    );
}
The wrapper automatically passes the this pointer to the native function. Static functions do not receive a this pointer.

Virtual Functions

Virtual functions are resolved via the virtual table index:
[VirtualFunction(78)]
public partial StatusManager* GetStatusManager();
The generator creates a virtual table struct:
[StructLayout(LayoutKind.Explicit)]
public unsafe struct CharacterVirtualTable
{
    // Index 78 * 8 bytes per pointer = offset 624 (0x270)
    [FieldOffset(624)] 
    public delegate* unmanaged<Character*, StatusManager*> GetStatusManager;
}

[FieldOffset(0)] public CharacterVirtualTable* VirtualTable;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public partial StatusManager* GetStatusManager() 
    => VirtualTable->GetStatusManager(
        (Character*)Unsafe.AsPointer(ref this)
    );
How it works:
  1. Virtual table pointer is at offset 0 of the struct
  2. Virtual function pointers are at vtable + (index * 8)
  3. The wrapper calls through the function pointer with this
Virtual functions cannot be static and always include the object instance pointer. They will call the appropriate override for the actual class type.

Static Address Resolution

Static addresses locate singleton instances or global variables:
[StaticAddress("48 8B 1D ?? ?? ?? ?? 8B 7C 24", 3, isPointer: true)]
public static partial Framework* Instance();

Parameters

  1. Signature - Pattern to find the instruction
  2. Offset - Where to read the address from in the matched bytes
  3. isPointer - Whether the location contains a pointer to the data

Generated Code

public unsafe static class StaticAddressPointers
{
    public static Framework** ppInstance 
        => (Framework**)Framework.Addresses.Instance.Value;
}

public static partial Framework* Instance()
{
    if (StaticAddressPointers.ppInstance is null)
    {
        ThrowHelper.ThrowNullAddress(
            "Framework.Instance", 
            "48 8B 1D ?? ?? ?? ?? 8B 7C 24"
        );
    }
    return *StaticAddressPointers.ppInstance;
}

Pointer vs. Direct Instance

The isPointer parameter determines the resolution behavior:
// C++ - static pointer (allocated on heap)
static Framework* FrameworkInstance;
// C# - isPointer: true
[StaticAddress("...", 3, isPointer: true)]
public static partial Framework* Instance();
// Returns: *ppInstance (dereference the pointer)
// C++ - static instance (allocated in binary)
static GameMain GameMainInstance;
// C# - isPointer: false
[StaticAddress("...", 3, isPointer: false)]
public static partial GameMain* Instance();
// Returns: pInstance (pointer to the instance)

Static Address Offset

The offset parameter tells the resolver where in the instruction to find the address:
Signature: "48 8B 1D ?? ?? ?? ?? 8B 7C 24"
Bytes:      48 8B 1D XX XX XX XX 8B 7C 24
Offset:      0  1  2  3  4  5  6  7  8  9
                      ^
                      Offset = 3 (first ?? byte)
The resolver reads a relative offset from position 3 and calculates the final address.
Since instructions are variable length, you must specify the correct offset to the address bytes (usually the first ?? in the signature).

VTable Address Resolution

The [VTableAddress] attribute locates static virtual tables:
[VTableAddress("48 8D 05 ?? ?? ?? ?? 48 89 03 48 8D 83", 3)]
public unsafe partial struct AddonRetainerTaskAsk
This generates:
public static class Addresses
{
    public static readonly Address StaticVirtualTable = 
        new Address("FFXIVClientStructs.FFXIV.Client.UI.AddonRetainerTaskAsk.StaticVirtualTable", 
                    "48 8D 05 ?? ?? ?? ?? 48 89 03 ...", ...);
}

public static AddonRetainerTaskAskVirtualTable* StaticVirtualTablePointer 
    => (AddonRetainerTaskAskVirtualTable*)Addresses.StaticVirtualTable.Value;

Static Virtual Function Pointers

When both [VTableAddress] and [VirtualFunction] are present, you can get static addresses for hooking:
[VTableAddress("48 8D 05 ?? ?? ?? ?? 48 89 03", 3)]
public unsafe partial struct AddonRetainerTaskAsk
{
    [VirtualFunction(48)]
    public partial void OnSetup(uint a1, AtkValue* a2);
}

// Usage: Hook the virtual function statically
var hookAddr = (nint)AddonRetainerTaskAsk.StaticVirtualTablePointer->OnSetup;
this.onSetupHook = Hook<OnSetupDelegate>.FromAddress(hookAddr, OnSetupDetour);
This enables hooking virtual functions before any instances exist, which is useful for intercepting all calls to that virtual function.

Resolution Process

Step-by-Step

  1. Setup - Initialize the resolver with optional module base and cache
  2. Register - Register all addresses from the library
  3. Resolve - Scan memory for each signature
    • Search for the byte pattern in the module
    • Calculate the final address based on offset and instruction type
    • Store the resolved address
    • Cache the result if caching is enabled
  4. Null Checks - Each function checks if its address was resolved before calling

Error Handling

If a signature fails to resolve:
if (MemberFunctionPointers.AddEvent is null)
{
    InteropGenerator.Runtime.ThrowHelper.ThrowNullAddress(
        "MemberFunctionPointers.AddEvent", 
        "E8 ?? ?? ?? ?? C1 E7 0C"
    );
}
This throws an exception with:
  • The function name that failed
  • The signature that failed to match
  • Helpful debugging information
Calling a function whose signature failed to resolve will throw a helpful exception. Always handle potential resolution failures in production code.

Performance Considerations

Initial Resolution

  • Without cache: 2-5 seconds (depends on signature count)
  • With cache: Near-instant (< 100ms)

Runtime Performance

  • Function calls: Zero overhead after resolution
  • Null checks: Single pointer comparison (optimized by JIT)
  • Virtual calls: One extra pointer dereference vs. direct calls

Best Practices

// ✅ Cache frequently-used pointers
var framework = Framework.Instance();
if (framework != null)
{
    // Use framework multiple times without repeated null checks
    var dt = framework->FrameDeltaTime;
    var counter = framework->FrameCounter;
}

// ❌ Don't repeatedly call Instance() if not necessary
for (int i = 0; i < 1000; i++)
{
    var dt = Framework.Instance()->FrameDeltaTime; // Unnecessary null check each time
}

Signature Maintenance

Writing Good Signatures

Unique but not too specific:
// ✅ Good - enough context to be unique
"E8 ?? ?? ?? ?? C1 E7 0C 48 8B"

// ❌ Too short - might match multiple locations
"E8 ?? ?? ?? ??"

// ❌ Too specific - will break on minor code changes
"E8 12 34 56 78 C1 E7 0C 48 8B 5C 24 30 48 83 C4 28"
Include instruction boundaries:
// ✅ Complete instructions
"48 8B 1D ?? ?? ?? ?? 8B 7C 24"  
// 48 8B 1D = mov rbx, [rip+offset]
// 8B 7C 24 = mov edi, [rsp+...]

// ❌ Partial instruction
"8B 1D ?? ?? ?? ?? 8B"  // Cuts into the middle of instructions
Good signatures balance uniqueness with resilience to game updates. Include enough context to be unique but avoid hardcoded addresses or values that change frequently.

Build docs developers (and LLMs) love