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:
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...
}
For serious development:
- Application Verifier (Windows) - detects memory errors
- Valgrind (Linux) - memory debugging tool
- Debug Heap - catches use-after-free earlier
Best Practices Summary
- Always check for null before dereferencing pointers
- Don’t store pointers across frames or state changes
- Validate type before casting native objects
- Check bounds before accessing arrays or buffers
- Use generated helpers (string properties, type checkers)
- Assume data can change between reads (threading)
- Wait for updates after game patches
- Log extensively in debug builds
- Test thoroughly before releasing to users
- 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.