Skip to main content

Overview

Native game classes are represented as explicit layout structs that map 1:1 in memory with the game’s objects. This guide covers how to properly define and add new structs.

Basic Struct Template

namespace FFXIVClientStructs.FFXIV.Component.GUI;

// Component::GUI::AtkResNode
[GenerateInterop]
[StructLayout(LayoutKind.Explicit, Size = 0xA8)]
public unsafe partial struct AtkResNode {
    [FieldOffset(0x20)] public AtkResNode* ParentNode;
    [FieldOffset(0x28)] public AtkResNode* PrevSiblingNode;
    [FieldOffset(0x30)] public AtkResNode* NextSiblingNode;
    [FieldOffset(0x38)] public AtkResNode* ChildNode;
}

Required Attributes

StructLayout

Always required - Defines the memory layout:
[StructLayout(LayoutKind.Explicit, Size = 0xA8)]
  • LayoutKind.Explicit: Required for precise memory mapping
  • Size: Total size of the struct in bytes (hexadecimal)

GenerateInterop

Required when using the interop generator:
[GenerateInterop]  // Basic usage
[GenerateInterop(isInherited: true)]  // If other structs inherit from this
Use isInherited: true when:
  • Other structs will inherit from this struct
  • The struct is used as a base class

Namespace and Naming

Namespace Convention

Follow the game’s C++ namespace hierarchy:
// Client::Game::Character::Character
namespace FFXIVClientStructs.FFXIV.Client.Game.Character;

// Component::GUI::AtkUnitBase
namespace FFXIVClientStructs.FFXIV.Component.GUI;

// Client::System::Framework::Framework
namespace FFXIVClientStructs.FFXIV.Client.System.Framework;

Struct Naming

  1. Use Official RTTI Names: Check the classinformer.csv for official names
  2. Match C++ Name: Use the exact name from RTTI (without namespace)
  3. Descriptive Names: For new classes, choose clear, descriptive names

Class Hierarchy Comments

Always include comments showing the C++ hierarchy:
// Client::Game::Character::Character
//   Client::Game::Object::GameObject
//   Client::Game::Character::CharacterData
public unsafe partial struct Character { }
Format:
  • First line: The struct’s full C++ path
  • Indented lines: Base classes or interfaces
  • Optional: Constructor signature or notes

Struct Modifiers

unsafe Keyword

Mark the struct unsafe if it contains:
  • Pointer fields (SomeType*)
  • Function pointers
  • Fixed-size arrays
public unsafe partial struct Character { }

partial Keyword

Always use partial when:
  • Using [GenerateInterop]
  • Using [MemberFunction], [VirtualFunction], etc.
  • The source generator will add code to the struct
public unsafe partial struct Character { }

Inheritance

Single Inheritance

[GenerateInterop(isInherited: true)]
[Inherits<AtkEventListener>]
[StructLayout(LayoutKind.Explicit, Size = 0x238)]
public unsafe partial struct AtkUnitBase { }

Multiple Inheritance

C++ supports multiple inheritance. Represent it with multiple [Inherits<>] attributes:
[GenerateInterop(isInherited: true)]
[Inherits<GameObject>, Inherits<CharacterData>]
[StructLayout(LayoutKind.Explicit, Size = 0x2370)]
public unsafe partial struct Character { }
Important: The order of [Inherits<>] attributes matters - it should match the C++ inheritance order.

Field Definitions

FieldOffset Attribute

Every field must have a FieldOffset:
[FieldOffset(0x20)] public AtkResNode* ParentNode;
[FieldOffset(0x28)] public AtkResNode* PrevSiblingNode;

Field Types

Allowed types (must be unmanaged):
  • Primitive types: byte, short, int, long, float, double, etc.
  • Enums
  • Pointers: SomeType*
  • Structs containing only unmanaged types
  • Fixed-size arrays (using FixedSizeArray types)
Not allowed:
  • string (use byte* or FixedSizeArray for strings)
  • Managed types
  • Arrays without FixedSizeArray

Fixed-Size Arrays

For native arrays like AtkResNode resNodeArray[10]:
[FieldOffset(0x100), FixedSizeArray] 
internal FixedSizeArray10<AtkResNode> _resNodeArray;
This generates a Span property for accessing the array:
public Span<AtkResNode> ResNodeArray => _resNodeArray;

Inline C Strings

For inline character arrays like char name[64]:
[FieldOffset(0x30), FixedSizeArray(isString: true)] 
internal FixedSizeArray64<byte> _name;
With isString: true, the generator creates helper properties:
public Span<byte> Name => _name;  // Raw bytes
public string NameString { get; set; }  // String conversion helper

BitFields

For packed boolean flags or small values:
[BitField<bool>(nameof(IsOffhandDrawn), 0)]
[FieldOffset(0x1980)] public byte WeaponFlags;

[BitField<bool>(nameof(IsPartyMember), 0)]
[BitField<bool>(nameof(IsAllianceMember), 1)]
[BitField<bool>(nameof(IsFriend), 2)]
[FieldOffset(0x1CE2)] public byte RelationFlags;
Format: [BitField<TType>(nameof(PropertyName), bitIndex)]

Complete Examples

Simple Struct

namespace FFXIVClientStructs.FFXIV.Client.Game.Object;

// Client::Game::Object::GameObject
[GenerateInterop(isInherited: true)]
[VirtualTable("48 8D 05 ?? ?? ?? ?? C7 81 ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? 48 8B C1", 3)]
[StructLayout(LayoutKind.Explicit, Size = 0x1A0)]
public unsafe partial struct GameObject {
    [FieldOffset(0x10)] public Vector3 DefaultPosition;
    [FieldOffset(0x20)] public float DefaultRotation;
    [FieldOffset(0x30), FixedSizeArray(isString: true)] internal FixedSizeArray64<byte> _name;
    [FieldOffset(0x78)] public uint EntityId;
    [FieldOffset(0x8C)] public ushort ObjectIndex;
    [FieldOffset(0x90)] public ObjectKind ObjectKind;
    [FieldOffset(0x100)] public DrawObject* DrawObject;
}

Struct with Inheritance

namespace FFXIVClientStructs.FFXIV.Component.GUI;

// Component::GUI::AtkUnitBase
//   Component::GUI::AtkEventListener
[GenerateInterop(isInherited: true)]
[Inherits<AtkEventListener>]
[StructLayout(LayoutKind.Explicit, Size = 0x238)]
[VirtualTable("48 8D 05 ?? ?? ?? ?? 48 8B D9 48 89 01 33 ED 48 8B 89 ?? ?? ?? ?? 8B F2", 3, 74)]
public unsafe partial struct AtkUnitBase : ICreatable {
    [FieldOffset(0x8), FixedSizeArray(isString: true)] 
    internal FixedSizeArray32<byte> _name;
    
    [FieldOffset(0x28)] public AtkUldManager UldManager;
    [FieldOffset(0xC8)] public AtkResNode* RootNode;
    [FieldOffset(0x178)] public AtkValue* AtkValues;
    [FieldOffset(0x1E2)] public ushort AtkValuesCount;
    
    [BitField<bool>(nameof(IsReady), 0)]
    [BitField<bool>(nameof(DisableUserClose), 2)]
    [FieldOffset(0x1A1)] public byte Flags1A1;
}

Struct with Multiple Inheritance

namespace FFXIVClientStructs.FFXIV.Client.Game.Character;

// Client::Game::Character::Character
//   Client::Game::Object::GameObject
//   Client::Game::Character::CharacterData
[GenerateInterop(isInherited: true)]
[Inherits<GameObject>, Inherits<CharacterData>]
[StructLayout(LayoutKind.Explicit, Size = 0x2370)]
[VirtualTable("48 8D 05 ?? ?? ?? ?? 48 89 07 48 8D 8F ?? ?? ?? ?? 48 8D 05 ?? ?? ?? ?? 48 89 87 ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 8D 8F ?? ?? ?? ?? 33 ED 48 8D 05 ?? ?? ?? ??", 3)]
public unsafe partial struct Character {
    [FieldOffset(0x600)] public MovementStateOptions MovementState;
    [FieldOffset(0x630)] public EmoteController EmoteController;
    [FieldOffset(0x670)] public MountContainer Mount;
    
    [FieldOffset(0x2300), FixedSizeArray(isString: true)] 
    internal FixedSizeArray7<byte> _freeCompanyTag;
    
    [FieldOffset(0x2308)] public GameObjectId TargetId;
    [FieldOffset(0x2350)] public ulong AccountId;
}

VirtualTable Attribute

If your struct has virtual functions, add the [VirtualTable] attribute:
[VirtualTable("48 8D 05 ?? ?? ?? ?? 48 89 07", 3)]
public unsafe partial struct MyStruct { }
Parameters:
  1. signature: The signature to find the vtable location
  2. offset: Offset within the signature where the vtable pointer is
  3. optional count: Number of virtual functions (optional)
This enables:
  • Static vtable pointer access via StaticVirtualTablePointer
  • Static function pointer access for hooking

ICreatable Interface

Add ICreatable if the struct should be creatable using game allocators:
public unsafe partial struct AtkUnitBase : ICreatable {
    [MemberFunction("E8 ?? ?? ?? ?? 33 D2 48 8D 9F")]
    public partial void Ctor();
}
This is typically used for UI objects that you might want to instantiate.

Finding Struct Information

Using IDA Pro

  1. Load the FFXIV binary in IDA Pro
  2. Import the rename database
  3. Locate the struct’s constructor or vtable
  4. Examine the RTTI information for official names
  5. Analyze memory accesses to find field offsets

Using Ghidra

  1. Load the binary in Ghidra
  2. Import the rename database script
  3. Search for known strings or function signatures
  4. Analyze cross-references to find struct usage
  5. Map out field accesses to determine offsets

Tips for Finding Offsets

  • Look for constructor code that initializes fields
  • Find functions that access the struct and trace memory offsets
  • Use known values to search memory in-game
  • Cross-reference with existing similar structs
  • Check RTTI metadata when available

Best Practices

  1. Start Small: Don’t try to map the entire struct at once
  2. Verify Offsets: Double-check field offsets in multiple functions
  3. Add Comments: Note uncertainty or alternate interpretations
  4. Test Thoroughly: Verify the struct works in practice
  5. Document Unknowns: Leave comments for unknown fields with their offsets
  6. Use Proper Types: Don’t use byte as a placeholder for unknown types

Common Mistakes

  • Forgetting [FieldOffset] on fields
  • Using managed types (like string) directly
  • Incorrect struct size calculation
  • Missing unsafe keyword
  • Missing partial keyword when using generators
  • Wrong namespace hierarchy
  • Incorrect inheritance order

Next Steps

Build docs developers (and LLMs) love