Overview
FFXIVClientStructs is a companion library that provides C# definitions for FFXIV’s internal structures, functions, and UI elements. It enables type-safe, structured access to game memory instead of working with raw pointers and offsets.
FFXIVClientStructs is automatically included with Dalamud and doesn’t require separate installation.
What is FFXIVClientStructs?
FFXIVClientStructs is a reverse-engineered mapping of FFXIV’s internal C++ classes and structures to C#. It provides:
- Struct definitions: C# representations of game structures
- Function signatures: Native function declarations
- Type safety: Compile-time checking instead of raw memory access
- Documentation: Field names and structure documentation
Basic Usage
Accessing Game Singletons
Many game systems are accessible via singleton instances:
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI;
public unsafe void AccessGameData()
{
// Get the main framework instance
var framework = Framework.Instance();
if (framework == null)
{
PluginLog.Error("Framework not available");
return;
}
// Access game managers
var inventoryManager = InventoryManager.Instance();
if (inventoryManager != null)
{
var itemCount = inventoryManager->GetInventoryItemCount(1);
PluginLog.Info($"Items in inventory: {itemCount}");
}
// Access UI manager
var uiModule = UIModule.Instance();
if (uiModule != null)
{
PluginLog.Info("UI Module available");
}
}
Working with Structures
Access structure fields directly:
using FFXIVClientStructs.FFXIV.Client.Game.Object;
public unsafe void ReadGameObject(GameObject* obj)
{
if (obj == null) return;
// Access fields
var objectId = obj->ObjectID;
var position = obj->Position;
var rotation = obj->Rotation;
PluginLog.Info($"Object ID: {objectId:X}");
PluginLog.Info($"Position: ({position.X}, {position.Y}, {position.Z})");
PluginLog.Info($"Rotation: {rotation}");
// Check object kind
var kind = obj->ObjectKind;
if (kind == ObjectKind.Player)
{
// Cast to more specific type
var character = (Character*)obj;
var level = character->CharacterData.Level;
PluginLog.Info($"Player level: {level}");
}
}
Calling Game Functions
ClientStructs provides function signatures you can call:
using FFXIVClientStructs.FFXIV.Client.Game;
public unsafe void UseAction()
{
var actionManager = ActionManager.Instance();
if (actionManager == null) return;
// Use an action
uint actionId = 3; // Sprint
var result = actionManager->UseAction(
ActionType.Action,
actionId,
targetId: 0xE0000000 // Self
);
if (result)
{
PluginLog.Info("Action used successfully");
}
}
Common Structures
Framework
The main game framework:
using FFXIVClientStructs.FFXIV.Client.System.Framework;
public unsafe void UseFramework()
{
var fw = Framework.Instance();
if (fw == null) return;
// Get current server time
var serverTime = fw->ServerTime;
PluginLog.Info($"Server time: {serverTime}");
// Check if in cutscene
// var inCutscene = fw->InCutscene;
}
GameObject and Character
Game objects and characters:
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
public unsafe void InspectCharacter(GameObject* obj)
{
if (obj == null || obj->ObjectKind != ObjectKind.Player)
return;
var character = (Character*)obj;
// Character data
var data = character->CharacterData;
PluginLog.Info($"Name: {obj->GetName()}");
PluginLog.Info($"Level: {data.Level}");
PluginLog.Info($"ClassJob: {data.ClassJob}");
// Status
var hp = character->CharacterData.Health;
var maxHp = character->CharacterData.MaxHealth;
var mp = character->CharacterData.Mana;
PluginLog.Info($"HP: {hp}/{maxHp}");
PluginLog.Info($"MP: {mp}");
// Mount info
var mountId = character->Mount.MountId;
if (mountId != 0)
{
PluginLog.Info($"Mounted on: {mountId}");
}
}
UI Components
Accessing UI addons (windows):
using FFXIVClientStructs.FFXIV.Component.GUI;
using FFXIVClientStructs.FFXIV.Client.UI;
public unsafe void AccessUI()
{
var uiModule = UIModule.Instance();
if (uiModule == null) return;
// Get a specific addon
var nameplate = uiModule->GetRaptureAtkModule()->GetAddonByName("NamePlate");
if (nameplate != null)
{
PluginLog.Info($"NamePlate visible: {nameplate->IsVisible}");
}
// Access addon as specific type
var inventory = (AtkUnitBase*)uiModule->GetRaptureAtkModule()
->GetAddonByName("Inventory");
if (inventory != null && inventory->IsVisible)
{
PluginLog.Info("Inventory is open");
// Access UI nodes
var rootNode = inventory->RootNode;
if (rootNode != null)
{
PluginLog.Info($"Root node type: {rootNode->Type}");
}
}
}
Inventory
Accessing inventory data:
using FFXIVClientStructs.FFXIV.Client.Game;
public unsafe void CheckInventory()
{
var inventoryManager = InventoryManager.Instance();
if (inventoryManager == null) return;
// Get inventory container
var inventory = inventoryManager->GetInventoryContainer(
InventoryType.Inventory1
);
if (inventory == null) return;
// Iterate items
for (int i = 0; i < inventory->Size; i++)
{
var item = inventory->GetInventorySlot(i);
if (item == null || item->ItemID == 0)
continue;
PluginLog.Info($"Slot {i}: Item {item->ItemID}, Qty: {item->Quantity}");
PluginLog.Info($" HQ: {item->Flags.HasFlag(InventoryItem.ItemFlags.HighQuality)}");
}
}
Utility Extensions
Dalamud provides extension methods for common ClientStruct types:
Utf8String Extensions
using FFXIVClientStructs.FFXIV.Client.System.String;
using Dalamud.Utility;
public unsafe void HandleUtf8String(Utf8String* str)
{
if (str == null) return;
// Convert to C# string
var text = str->ToString();
PluginLog.Info($"Text: {text}");
// Check if empty
if (str->IsEmpty)
{
PluginLog.Info("String is empty");
}
}
Utf8String extensions assume standard ClientStructs. If you use custom ClientStructs builds, these extensions may break.
Vector Extensions
using FFXIVClientStructs.FFXIV.Client.Graphics;
using Dalamud.Utility.Numerics;
using System.Numerics;
public void ConvertVectors()
{
// FFXIVClientStructs Vector3 to System.Numerics.Vector3
var csVector = new FFXIVClientStructs.FFXIV.Common.Math.Vector3
{
X = 100f,
Y = 50f,
Z = 200f
};
// Convert using extensions
var systemVector = new Vector3(csVector.X, csVector.Y, csVector.Z);
// Calculate distance
var distance = systemVector.Length();
PluginLog.Info($"Distance from origin: {distance}");
}
Working with Pointers
Null Checking
Always check for null pointers:
public unsafe void SafeAccess()
{
var manager = InventoryManager.Instance();
if (manager == null)
{
PluginLog.Warning("Manager not available");
return;
}
var container = manager->GetInventoryContainer(InventoryType.Inventory1);
if (container == null)
{
PluginLog.Warning("Container not available");
return;
}
// Safe to use now
var size = container->Size;
}
Pointer Arithmetic
public unsafe void IterateArray(AtkComponentNode** nodes, int count)
{
if (nodes == null) return;
for (int i = 0; i < count; i++)
{
var node = nodes[i];
if (node == null) continue;
// Access node data
var component = node->Component;
if (component != null)
{
// Process component
}
}
}
Casting Pointers
using FFXIVClientStructs.FFXIV.Component.GUI;
public unsafe void CastNodes(AtkResNode* node)
{
if (node == null) return;
switch (node->Type)
{
case NodeType.Image:
var imageNode = (AtkImageNode*)node;
PluginLog.Info($"Image: {imageNode->PartId}");
break;
case NodeType.Text:
var textNode = (AtkTextNode*)node;
var text = textNode->NodeText.ToString();
PluginLog.Info($"Text: {text}");
break;
case NodeType.Component:
var componentNode = (AtkComponentNode*)node;
var component = componentNode->Component;
PluginLog.Info($"Component: {component->UldManager.NodeListCount} nodes");
break;
}
}
Memory Safety
Fixed Buffers
Some structures use fixed buffers:
public unsafe void ReadFixedBuffer()
{
// Example structure with fixed buffer
// fixed byte Name[64];
var obj = GetSomeObject();
if (obj == null) return;
// Access fixed buffer
var nameSpan = new ReadOnlySpan<byte>(obj->Name, 64);
// Find null terminator
var nullIndex = nameSpan.IndexOf((byte)0);
if (nullIndex >= 0)
{
nameSpan = nameSpan[..nullIndex];
}
// Convert to string
var name = System.Text.Encoding.UTF8.GetString(nameSpan);
PluginLog.Info($"Name: {name}");
}
Structure Validation
public unsafe bool IsValidStructure<T>(T* ptr) where T : unmanaged
{
if (ptr == null)
return false;
// Check if pointer is in valid memory range
var address = (nint)ptr;
// Basic sanity checks
if (address < 0x10000) // Null-ish
return false;
if (address > 0x7FFFFFFFFFFF) // Out of user space
return false;
return true;
}
ClientStructs Version
You can check the ClientStructs version:
using FFXIVClientStructs;
public void CheckVersion()
{
var version = ThisAssembly.Git.Commits;
PluginLog.Info($"ClientStructs commits: {version}");
// From Dalamud's version info
var gitHash = Dalamud.Utility.Versioning.GetGitHashClientStructs();
PluginLog.Info($"ClientStructs hash: {gitHash}");
}
Best Practices
Always Use Unsafe Context
ClientStructs requires unsafe code:
// In .csproj
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
Check for Null
Always validate pointers before use:
public unsafe void AlwaysCheckNull()
{
var instance = SomeManager.Instance();
if (instance == null)
{
// Handle gracefully
return;
}
// Safe to use
}
Avoid Storing Pointers
Don’t store pointers long-term - they can become invalid:
// Bad: Storing pointer
private unsafe GameObject* storedObject;
// Good: Store object ID and re-fetch
private uint storedObjectId;
public unsafe GameObject* GetObject()
{
// Re-fetch each time
var objectTable = ObjectTable.Instance();
return objectTable->GetObjectById(this.storedObjectId);
}
Use with Game Services
Combine ClientStructs with Dalamud services:
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
public class CombinedExample
{
private readonly IObjectTable objectTable;
public CombinedExample(IObjectTable objectTable)
{
this.objectTable = objectTable;
}
public unsafe void UseServices()
{
// Use Dalamud service for high-level access
var player = this.objectTable.FirstOrDefault(o => o.ObjectKind == Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player);
if (player != null)
{
// Get ClientStruct pointer for low-level access
var playerPtr = (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)player.Address;
if (playerPtr != null)
{
// Access fields not exposed by Dalamud
var internalData = playerPtr->CharacterData;
}
}
}
}
Common Patterns
Singleton Access Pattern
public unsafe class GameDataAccessor
{
public void RefreshData()
{
var framework = Framework.Instance();
if (framework == null) return;
var inventory = InventoryManager.Instance();
if (inventory == null) return;
var ui = UIModule.Instance();
if (ui == null) return;
// Use instances
}
}
UI Addon Pattern
public unsafe bool TryGetAddon<T>(string name, out T* addon) where T : unmanaged
{
addon = null;
var uiModule = UIModule.Instance();
if (uiModule == null) return false;
var atkModule = uiModule->GetRaptureAtkModule();
if (atkModule == null) return false;
var baseAddon = atkModule->GetAddonByName(name);
if (baseAddon == null) return false;
addon = (T*)baseAddon;
return true;
}