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.
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:
- Virtual table pointer is at offset 0 of the struct
- Virtual function pointers are at
vtable + (index * 8)
- 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
- Signature - Pattern to find the instruction
- Offset - Where to read the address from in the matched bytes
- 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
- Setup - Initialize the resolver with optional module base and cache
- Register - Register all addresses from the library
- 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
- 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.
Initial Resolution
- Without cache: 2-5 seconds (depends on signature count)
- With cache: Near-instant (< 100ms)
- 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.