Skip to main content

Fundamental Risks

FFXIVClientStructs provides direct access to native game memory. This is fundamentally unsafe and comes with significant risks:
  • Memory corruption
  • Game crashes
  • Undefined behavior
  • Save data corruption (in extreme cases)
  • Anti-cheat detection (for malicious use)
The library makes no attempt to prevent you from shooting yourself in the foot. You are responsible for safe usage.

No Marshalling, No Safety

DisableRuntimeMarshalling

The entire assembly has the DisableRuntimeMarshalling attribute enabled:
[assembly: DisableRuntimeMarshalling]
Implications:
  • No automatic type conversion
  • No bounds checking on arrays
  • No null reference protection
  • No string conversion
  • You are working directly with native memory

Unsafe Everywhere

Pointers are pervasive throughout the library:
public unsafe partial struct AtkUnitBase {
    [FieldOffset(0xC8)] public AtkResNode* RootNode;
    [FieldOffset(0xD0)] public AtkCollisionNode* WindowCollisionNode;
    [FieldOffset(0xF0)] public AtkResNode* CursorTarget;
}
Every pointer access is a potential crash if:
  • The pointer is null
  • The pointer is invalid (freed memory, wrong address)
  • The memory layout has changed (game update)
  • The object has been destroyed

Common Pitfalls

Null Pointer Dereferencing

The problem:
// ❌ DANGEROUS - no null check
var x = node->X;  // Crashes if node is null
The solution:
// ✅ Always check for null
if (node != null)
{
    var x = node->X;
}

Dangling Pointers

The problem:
AtkResNode* node = GetSomeNode();
// ... some time passes ...
// The node might have been freed by the game!
node->X = 100;  // ❌ Use after free - undefined behavior
The solution:
// ✅ Don't store pointers long-term
// Re-query when you need them
var node = GetSomeNode();
if (node != null)
{
    node->X = 100;
}
// Don't use 'node' later - it might be invalid
Native objects can be freed by the game at any time. Never store pointers across frames or game state changes.

Invalid Type Casts

The problem:
// ❌ DANGEROUS - no type checking
var textNode = (AtkTextNode*)someNode;  
textNode->SetText("Hello");  // Crashes if someNode isn't actually AtkTextNode
The solution:
// ✅ Check the node type first
if (someNode->Type == NodeType.Text)
{
    var textNode = (AtkTextNode*)someNode;
    textNode->SetText("Hello"u8);
}

// Or use the generated helper
var textNode = someNode->GetAsAtkTextNode();
if (textNode != null)
{
    textNode->SetText("Hello"u8);
}

Buffer Overflows

The problem:
// ❌ DANGEROUS - no bounds checking
fixed (byte* ptr = "This is a very long string that exceeds the buffer"u8)
{
    // Overwrites adjacent memory!
    Buffer.MemoryCopy(ptr, character->_freeCompanyTag, 7, 50);
}
The solution:
// ✅ Use the generated string property (includes bounds checking)
character->FreeCompanyTagString = "TAG";  // Throws if too long

// Or manually check bounds
var text = "TAG";
if (Encoding.UTF8.GetByteCount(text) <= 6)  // 7 - 1 for null terminator
{
    Encoding.UTF8.GetBytes(text.AsSpan(), character->_freeCompanyTag);
    character->_freeCompanyTag[6] = 0;
}

Race Conditions

The problem:
// ❌ DANGEROUS - not thread-safe
// Game thread might modify this while you're reading it
for (int i = 0; i < list->Count; i++)
{
    var item = list->Items[i];  // Might crash if Count changes
}
The solution:
// ✅ Cache values that might change
var count = list->Count;
for (int i = 0; i < count; i++)
{
    if (i >= list->Count) break;  // Safety check
    var item = list->Items[i];
}

// Better: Only access from the main game thread (via Framework.Update)
FFXIV is multi-threaded. Always assume data can change between reads unless you’re on the main game thread.

Safe Patterns

Defensive Null Checking

Always check pointers before use:
public void ProcessNode(AtkResNode* node)
{
    if (node == null) return;
    
    // Use node safely
    var x = node->X;
    var y = node->Y;
    
    // Check child pointers too
    if (node->ParentNode != null)
    {
        var parentX = node->ParentNode->X;
    }
}

Validate Before Dereferencing

public StatusManager* GetStatusManager(Character* character)
{
    if (character == null) return null;
    if (character->VirtualTable == null) return null;
    
    // Now safe to call
    return character->GetStatusManager();
}

Use Try-Catch for Critical Code

For production code, protect against crashes:
try
{
    var framework = Framework.Instance();
    if (framework != null)
    {
        var dt = framework->FrameDeltaTime;
        // Use dt...
    }
}
catch (Exception ex)
{
    // Log error and continue
    PluginLog.Error(ex, "Failed to access framework");
}
While try-catch can prevent crashes, it cannot prevent memory corruption. Use it as a last resort, not a primary safety mechanism.

Don’t Store Pointers Long-Term

public class MyPlugin
{
    // ❌ BAD - stored pointer can become invalid
    private Character* cachedPlayer;
    
    public void Update()
    {
        // Might be invalid now!
        if (cachedPlayer != null)
        {
            var hp = cachedPlayer->GetStatusManager()->CurrentHP;
        }
    }
}
public class MyPlugin
{
    // ✅ GOOD - query when needed
    public void Update()
    {
        var player = GetLocalPlayer();
        if (player != null)
        {
            var statusMgr = player->GetStatusManager();
            if (statusMgr != null)
            {
                var hp = statusMgr->CurrentHP;
            }
        }
    }
}

Validate Data Structures

Before iterating or accessing complex structures:
public void ProcessResNodes(AtkUnitBase* unitBase)
{
    if (unitBase == null) return;
    if (unitBase->RootNode == null) return;
    if (unitBase->UldManager.NodeListCount > 1000) return;  // Sanity check
    
    // Now safer to process
    for (int i = 0; i < unitBase->UldManager.NodeListCount; i++)
    {
        if (i >= unitBase->UldManager.NodeListCount) break;
        var node = unitBase->UldManager.NodeList[i];
        if (node == null) continue;
        
        ProcessNode(node);
    }
}

String Safety

No C# Strings in Interop

Because DisableRuntimeMarshalling is enabled:
// ❌ NOT ALLOWED - string requires marshalling
public partial void NativeFunction(string text);

// ✅ Use byte* instead
public partial void NativeFunction(byte* text);

// ✅ Or use generated overloads
[GenerateStringOverloads]
public partial void NativeFunction(byte* text);
// Creates: NativeFunction(string text)
// Creates: NativeFunction(ReadOnlySpan<byte> text)

Reading Native Strings

Never return C# strings from native pointers:
// ❌ BAD - makes assumptions about lifetime
public string GetName() {
    return Marshal.PtrToStringUTF8((nint)namePtr);
}

// ✅ GOOD - return pointer, let caller decide
public byte* GetName() {
    return namePtr;
}

// Caller handles conversion if needed
var namePtr = obj->GetName();
if (namePtr != null) {
    var name = MemoryHelper.ReadStringNullTerminated((nint)namePtr);
}
Why? Native string pointers might:
  • Point to temporary memory
  • Be freed by the game
  • Be modified by the game
  • Have unexpected lifetime

Writing Native Strings

Use the generated string properties with bounds checking:
// ✅ Safe - includes bounds checking
character->FreeCompanyTagString = "TAG";

// ✅ Safe - uses generated overload with bounds checking
manager->GetAddonByName("CharacterStatus");

// ⚠️ Manual - you must ensure bounds
fixed (byte* ptr = "TAG"u8)
{
    if (ptr != null && Encoding.UTF8.GetByteCount("TAG") < 7)
    {
        Buffer.MemoryCopy(ptr, character->_freeCompanyTag, 7, 4);
    }
}

Game Updates and Struct Changes

Signature Resolution Failures

After a game update, signatures may fail to resolve:
try
{
    var framework = Framework.Instance();
}
catch (Exception ex)
{
    // Signature might have failed to resolve
    PluginLog.Error("Framework.Instance signature failed");
    return;
}

Struct Layout Changes

Field offsets can change between patches:
// Before patch: correct offset
[FieldOffset(0x22E8)] public float Alpha;

// After patch: offset might be wrong!
// Reading/writing will access wrong memory
character->Alpha = 1.0f;  // Might corrupt unrelated data
After major game updates, wait for FFXIVClientStructs to be updated. Using outdated struct definitions will cause crashes and corruption.

Version Checking

For production plugins:
public void Initialize()
{
    var expectedVersion = "2024.01.01.0000.0000";
    var actualVersion = GetGameVersion();
    
    if (actualVersion != expectedVersion)
    {
        PluginLog.Warning(
            $"Game version mismatch. Expected {expectedVersion}, got {actualVersion}"
        );
        PluginLog.Warning("Plugin may not work correctly. Waiting for update.");
        isEnabled = false;
    }
}

Memory Corruption Symptoms

How to recognize you’ve corrupted memory:

Immediate Crashes

Access Violation (0xC0000005) at address 0x00000000
Access Violation reading address 0xDDDDDDDD
These indicate:
  • Null pointer dereference
  • Use-after-free (freed memory is filled with 0xDD in debug builds)
  • Invalid pointer

Delayed Crashes

Memory corruption might not crash immediately:
// Corrupt some memory
node->X = *(float*)0x12345678;  // Random value

// Game seems fine for a while...
// Then crashes later when it uses that corrupted data

Data Corruption

  • UI elements in wrong positions
  • Characters with wrong equipment
  • Stats showing incorrect values
  • Broken textures or models
If you see strange behavior after using FFXIVClientStructs, assume memory corruption until proven otherwise. Restart the game immediately.

Debugging Tips

Use Debug Builds

Debug builds provide better error messages:
<PropertyGroup>
    <Configuration>Debug</Configuration>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

Add Logging

public void ProcessNode(AtkResNode* node)
{
    PluginLog.Verbose($"ProcessNode: node={((nint)node):X}");
    
    if (node == null)
    {
        PluginLog.Warning("ProcessNode: node is null");
        return;
    }
    
    PluginLog.Verbose($"ProcessNode: type={node->Type}, X={node->X}, Y={node->Y}");
}

Validate Assumptions

public void ProcessCharacter(Character* character)
{
    Debug.Assert(character != null, "character is null");
    Debug.Assert(character->VirtualTable != null, "vtable is null");
    Debug.Assert(
        character->Mode >= 0 && character->Mode < CharacterModes.Max,
        $"Invalid mode: {character->Mode}"
    );
    
    // Continue with processing...
}

Use Memory Validation Tools

For serious development:
  • Application Verifier (Windows) - detects memory errors
  • Valgrind (Linux) - memory debugging tool
  • Debug Heap - catches use-after-free earlier

Best Practices Summary

  1. Always check for null before dereferencing pointers
  2. Don’t store pointers across frames or state changes
  3. Validate type before casting native objects
  4. Check bounds before accessing arrays or buffers
  5. Use generated helpers (string properties, type checkers)
  6. Assume data can change between reads (threading)
  7. Wait for updates after game patches
  8. Log extensively in debug builds
  9. Test thoroughly before releasing to users
  10. Expect crashes and handle them gracefully
When in doubt, add more null checks. The performance cost is negligible compared to the cost of a crash.

Build docs developers (and LLMs) love