Explicit Layout Structs
All native game classes are represented as explicit layout structs. This is fundamental to achieving 1:1 memory mapping with the game’s objects.
Basic Structure
namespace FFXIVClientStructs.FFXIV.Component.GUI;
[StructLayout(LayoutKind.Explicit, Size = 0xC0)]
public unsafe partial struct AtkResNode : ICreatable {
[FieldOffset(0x20)] public AtkResNode* ParentNode;
[FieldOffset(0x28)] public AtkResNode* PrevSiblingNode;
[FieldOffset(0x30)] public AtkResNode* NextSiblingNode;
[FieldOffset(0x38)] public AtkResNode* ChildNode;
}
Key attributes:
[StructLayout(LayoutKind.Explicit, Size = 0x...)] - Defines the struct size and explicit layout
[FieldOffset(0x...)] - Required for every field, defines exact memory position
unsafe - Required if the struct has pointer fields
partial - Required if using source generators
If you cannot determine the exact size, use your best estimate. The size should match the native class size as closely as possible.
Field Types
Allowed Types
Field types can (generally) only be types that the runtime considers unmanaged:
- Primitive integer/float types (
byte, int, float, double, etc.)
- Enums
- Pointers (
AtkResNode*, Character*, etc.)
- Fixed-size arrays (via
FixedSizeArray)
- Structs that only contain unmanaged fields
[StructLayout(LayoutKind.Explicit, Size = 0x2370)]
public unsafe partial struct Character {
// ✅ Primitive types
[FieldOffset(0x22E8)] public float Alpha;
[FieldOffset(0x2338)] public uint NameId;
// ✅ Enums
[FieldOffset(0x2364)] public CharacterModes Mode;
// ✅ Pointers
[FieldOffset(0x22F8)] public Companion* CompanionObject;
// ✅ Nested structs
[FieldOffset(0x630)] public EmoteController EmoteController;
[FieldOffset(0x670)] public MountContainer Mount;
}
Managed types like string, List<T>, or Dictionary<K,V> cannot be used as struct fields. Use pointers to native equivalents instead.
Pointer Fields
Pointers are used extensively throughout the library:
[StructLayout(LayoutKind.Explicit, Size = 0x238)]
public unsafe partial struct AtkUnitBase : ICreatable {
[FieldOffset(0xC8)] public AtkResNode* RootNode;
[FieldOffset(0xD0)] public AtkCollisionNode* WindowCollisionNode;
[FieldOffset(0xD8)] public AtkCollisionNode* WindowHeaderCollisionNode;
[FieldOffset(0xF0)] public AtkResNode* CursorTarget;
[FieldOffset(0xF8)] public AtkResNode* FocusNode;
}
Always check for null before dereferencing:
if (unitBase->RootNode != null) {
var x = unitBase->RootNode->X;
}
Fixed-Size Arrays
C# fixed size arrays are not the same as C++ fixed size arrays. FFXIVClientStructs uses Inline Arrays via the FixedSizeArray type.
Array Syntax
Native fixed size arrays like this in C++:
AtkResNode resNodeArray[10];
Are defined using the FixedSizeArrayX<> type:
[FixedSizeArray]
internal FixedSizeArray10<AtkResNode> _resNodeArray;
The source generator creates a Span<T> accessor for the array with the correct size.
Accessing Array Elements
// Access via Span
for (int i = 0; i < component->ResNodeArray.Length; i++) {
var node = component->ResNodeArray[i];
// Use node...
}
// Or use span features
var firstNode = component->ResNodeArray[0];
var slice = component->ResNodeArray.Slice(0, 5);
Inline C Strings
C strings embedded in structs are defined similarly:
// C++ definition
char freeCompanyTag[7];
// C# equivalent
[FieldOffset(0x2300), FixedSizeArray(isString: true)]
internal FixedSizeArray7<byte> _freeCompanyTag;
The generator creates string accessor properties:
/// <inheritdoc cref="_freeCompanyTag" />
public string FreeCompanyTagString
{
get => global::System.Text.Encoding.UTF8.GetString(
global::System.Runtime.InteropServices.MemoryMarshal
.CreateReadOnlySpanFromNullTerminated(
(byte*)global::System.Runtime.CompilerServices.Unsafe
.AsPointer(ref _freeCompanyTag[0])));
set
{
if (global::System.Text.Encoding.UTF8.GetByteCount(value) > 7 - 1)
{
InteropGenerator.Runtime.ThrowHelper
.ThrowStringSizeTooLarge("FreeCompanyTagString", 7);
}
global::System.Text.Encoding.UTF8.GetBytes(value.AsSpan(), _freeCompanyTag);
_freeCompanyTag[6] = 0;
}
}
String arrays automatically handle UTF-8 encoding/decoding and null-termination.
BitFields
Bitfields are used to pack multiple boolean or small integer values into a single byte or word:
[BitField<bool>(nameof(IsPartyMember), 0)]
[BitField<bool>(nameof(IsAllianceMember), 1)]
[BitField<bool>(nameof(IsFriend), 2)]
[FieldOffset(0x1CE2)] public byte RelationFlags;
// Generated properties
public partial bool IsPartyMember { get; set; }
public partial bool IsAllianceMember { get; set; }
public partial bool IsFriend { get; set; }
For complex flags:
[BitField<AtkUnitBaseVisibilityState>(nameof(VisibilityState), 20, 4)]
[BitField<AtkUnitBaseVisibilityState>(nameof(AppliedVisibilityState), 24, 4)]
[BitField<AtkUnitBaseLoadState>(nameof(LoadState), 28, 4)]
[FieldOffset(0x198)] public uint Flags198;
// Extracts 4 bits starting at bit 20
public partial AtkUnitBaseVisibilityState VisibilityState { get; set; }
Inheritance
C++ inheritance is represented using attributes and explicit offsets:
// Client::Game::Character::Character
// Client::Game::Object::GameObject
// Client::Game::Character::CharacterData
[GenerateInterop(isInherited: true)]
[Inherits<GameObject>, Inherits<CharacterData>]
[StructLayout(LayoutKind.Explicit, Size = 0x2370)]
public unsafe partial struct Character {
// GameObject fields are at offset 0x0
// CharacterData fields follow GameObject
// Character-specific fields start after base classes
}
The [Inherits<T>] attribute tells the generator to include base class fields and methods in the derived struct.
Virtual Tables
Structs with virtual functions have a virtual table pointer at offset 0:
[VirtualTable("48 8D 05 ?? ?? ?? ?? 48 89 07 48 8D 8F ?? ?? ?? ??", 3)]
public unsafe partial struct Character {
// VirtualTable pointer is implicitly at offset 0x0
[FieldOffset(0x0)] public CharacterVirtualTable* VirtualTable;
}
The generator creates a corresponding VirtualTable struct:
[StructLayout(LayoutKind.Explicit)]
public unsafe struct CharacterVirtualTable
{
[FieldOffset(624)]
public delegate* unmanaged<Character*, StatusManager*> GetStatusManager;
}
See Signature Resolution for details on how virtual tables are resolved.
STD Collections
The library provides wrappers for C++ STL collections:
[FieldOffset(0x180)] public StdVector<CStringPointer> CachedAtkValueStrings;
[FieldOffset(0x...)] public StdMap<uint, SomeValue> SomeMap;
These wrappers allow safe access to native collection data without copying.
Generic Pointers
C# doesn’t allow pointer types in generics. The library uses Pointer<T> as a workaround:
// C++ equivalent: std::vector<AtkResNode*>
[FieldOffset(0xE0), FixedSizeArray]
internal FixedSizeArray2<Pointer<AtkResNode>> _additionalMoveableNodes;
Pointer<T> implicitly converts to T*:
Pointer<AtkResNode> ptr = component->_additionalMoveableNodes[0];
AtkResNode* node = ptr; // Implicit conversion
if (node != null) {
// Use node...
}
You may need explicit conversions when working with collections of pointers.
Best Practices
Always Mark Structs Unsafe
// ✅ Good - struct is marked unsafe
public unsafe partial struct AtkResNode { }
// ❌ Avoid - marking individual members unsafe
public partial struct AtkResNode {
public unsafe AtkResNode* Parent;
}
Always Make Generator Structs Partial
// ✅ Required for generators
public unsafe partial struct Character { }
// ❌ Won't work with generators
public unsafe struct Character { }
Use Exact Offsets
Always verify field offsets match the native structure:
// Offsets must be exact to maintain memory alignment
[FieldOffset(0x20)] public AtkResNode* ParentNode; // Not 0x1F or 0x21!
[FieldOffset(0x28)] public AtkResNode* PrevSiblingNode;
Document Field Sources
Add comments indicating where information came from:
[BitField<bool>(nameof(IsSwimming), 5)]
// found in Client::Game::Event::EventSceneModuleUsualImpl.IsSwimming
[FieldOffset(0x628)] public byte Flags628;