Skip to main content

Overview

C++ fixed-size arrays are common in game structures, but C# fixed-size arrays have limitations. FFXIVClientStructs uses inline arrays (C# 12) with source generators to provide type-safe, properly sized array access.

The Problem

C++ and C# handle inline arrays differently:
// C++ - array embedded directly in struct
struct Example {
    AtkResNode nodes[10];      // 10 nodes inline
    char name[32];             // 32 bytes inline
};
// C# fixed arrays - limited to primitives
public unsafe struct Example {
    public fixed int values[10];  // Only works for primitives!
    // Cannot do: public fixed AtkResNode nodes[10];
}

FixedSizeArray Solution

The library uses generated inline array types (FixedSizeArray1<T> through FixedSizeArray10000<T>) with the [FixedSizeArray] attribute:
[FieldOffset(0x00), FixedSizeArray] 
internal FixedSizeArray10<AtkResNode> _nodes;

Basic Usage

Defining Fixed Arrays

public unsafe partial struct AtkUnitList
{
    [FieldOffset(0x8), FixedSizeArray] 
    internal FixedSizeArray256<Pointer<AtkUnitBase>> _entries;
}

Accessing Elements

Fixed arrays are accessed through Span<T>:
// Access via indexer
var firstEntry = unitList._entries[0];
var lastEntry = unitList._entries[255];

// Iterate
for (int i = 0; i < 256; i++)
{
    var entry = unitList._entries[i];
    if (entry.Value != null)
    {
        // Process entry
    }
}

// As span
Span<Pointer<AtkUnitBase>> entries = unitList._entries;
foreach (ref var entry in entries)
{
    if (entry.Value != null)
    {
        // Process entry
    }
}

String Arrays

For inline C string buffers, use isString: true:
[FieldOffset(0x2300), FixedSizeArray(isString: true)] 
internal FixedSizeArray7<byte> _freeCompanyTag;
This generates a string property:
public string FreeCompanyTagString
{
    get => Encoding.UTF8.GetString(
        MemoryMarshal.CreateReadOnlySpanFromNullTerminated(
            (byte*)Unsafe.AsPointer(ref _freeCompanyTag[0])));
    set
    {
        if (Encoding.UTF8.GetByteCount(value) > 7 - 1)
        {
            ThrowHelper.ThrowStringSizeTooLarge("FreeCompanyTagString", 7);
        }
        Encoding.UTF8.GetBytes(value.AsSpan(), _freeCompanyTag);
        _freeCompanyTag[6] = 0; // null terminator
    }
}

Examples from the Codebase

UI Components

From AtkUnitList.cs:8:
[FieldOffset(0x8), FixedSizeArray] 
internal FixedSizeArray256<Pointer<AtkUnitBase>> _entries;

// Usage
var entries = atkUnitList->_entries;
for (var i = 0; i < 256; i++)
{
    var addon = entries[i].Value;
    if (addon != null)
    {
        // Process addon
    }
}

Component Nodes

From AtkUldComponentDataWindow.cs:7:
[FieldOffset(0x0C), FixedSizeArray] 
internal FixedSizeArray8<uint> _nodes;
From AtkUldComponentDataMap.cs:7:
[FieldOffset(0x0C), FixedSizeArray] 
internal FixedSizeArray10<uint> _nodes;

Large Arrays

From AtkStage.cs:45-49:
// 10,000 event pool
[FieldOffset(0x878), FixedSizeArray] 
internal FixedSizeArray10000<AtkEvent> _atkEventPool;

// String buffer
[FieldOffset(0x75C10), FixedSizeArray(isString: true)] 
internal FixedSizeArray384<byte> _formatCStringBuffer;

UI3D Module

From UI3DModule.cs:12-28:
[FieldOffset(0x20), FixedSizeArray] 
internal FixedSizeArray819<ObjectInfo> _objectInfos;

[FieldOffset(0x13340), FixedSizeArray] 
internal FixedSizeArray819<Pointer<ObjectInfo>> _sortedObjectInfoPointers;

[FieldOffset(0x14CE0), FixedSizeArray] 
internal FixedSizeArray50<Pointer<ObjectInfo>> _namePlateObjectInfoPointers;

[FieldOffset(0x14E98), FixedSizeArray] 
internal FixedSizeArray50<GameObjectId> _namePlateObjectIds;

AtkModule

From AtkModule.cs:51-52:
[FieldOffset(0x8288), FixedSizeArray(isString: true)] 
internal FixedSizeArray16<byte> _currentUIScene;

[FieldOffset(0x8298), FixedSizeArray(isString: true)] 
internal FixedSizeArray16<byte> _loadingUIScene;

// Usage
var currentScene = Encoding.UTF8.GetString(
    atkModule->CurrentUISceneString);

Character Data

From Character.cs:51:
[FieldOffset(0x2300), FixedSizeArray(isString: true)] 
internal FixedSizeArray7<byte> _freeCompanyTag;

// Access via generated property
var fcTag = character->FreeCompanyTagString;
character->FreeCompanyTagString = "NEWFC";

Havok Math

From hkSimdFloat32.cs:6:
[FieldOffset(0x00), FixedSizeArray] 
internal FixedSizeArray4<float> _f32;

Matrix Types

From Matrix4x4.cs:6:
[FieldOffset(0x00), CExporterIgnore, FixedSizeArray] 
internal FixedSizeArray16<float> _matrix;
From Matrix2x2.cs:6:
[FieldOffset(0x0), CExporterIgnore, FixedSizeArray] 
internal FixedSizeArray4<float> _matrix;

Generated Array Types

The source generator creates inline array types:
// Generated by source generators
[InlineArray(10)]
public struct FixedSizeArray10<T> where T : unmanaged
{
    private T _element0;
}

// Implicitly converts to Span<T>
public static implicit operator Span<T>(FixedSizeArray10<T> array)
    => MemoryMarshal.CreateSpan(ref array._element0, 10);

public static implicit operator ReadOnlySpan<T>(FixedSizeArray10<T> array)
    => MemoryMarshal.CreateReadOnlySpan(ref array._element0, 10);

Working with Spans

Fixed arrays implicitly convert to spans for easy manipulation:
// Array in struct
[FieldOffset(0x00), FixedSizeArray] 
internal FixedSizeArray10<int> _values;

// Implicit conversion to Span
Span<int> values = myStruct->_values;

// Span operations
values.Fill(0);
values[0] = 42;
var sum = 0;
foreach (var value in values)
{
    sum += value;
}

// Pass to methods expecting Span
ProcessValues(myStruct->_values); // Implicitly converts

void ProcessValues(Span<int> values)
{
    // Work with span
}

Memory Layout

Fixed arrays are embedded directly in the containing struct:
[StructLayout(LayoutKind.Explicit, Size = 0x100)]
public unsafe partial struct Example
{
    [FieldOffset(0x00)] public int id;
    
    // 40 bytes (10 * sizeof(int))
    [FieldOffset(0x04), FixedSizeArray] 
    internal FixedSizeArray10<int> _values;
    
    [FieldOffset(0x2C)] public float nextField;
}

// Memory layout:
// 0x00: id (4 bytes)
// 0x04: _values[0-9] (40 bytes)
// 0x2C: nextField (4 bytes)

Available Sizes

Pre-generated types exist for common sizes:
  • FixedSizeArray1<T> through FixedSizeArray256<T> (every size)
  • FixedSizeArray384<T>, FixedSizeArray512<T>
  • FixedSizeArray819<T>, FixedSizeArray942<T>
  • FixedSizeArray2048<T>
  • FixedSizeArray10000<T>
If you need a different size, add it to the generator configuration.

Common Patterns

Searching Arrays

[FieldOffset(0x30), FixedSizeArray] 
internal FixedSizeArray13<AtkUnitList> _depthLayers;

// Find specific addon
public AtkUnitBase* FindAddon(string name)
{
    var nameBytes = Encoding.UTF8.GetBytes(name + "\0");
    
    for (var layer = 0; layer < 13; layer++)
    {
        var entries = _depthLayers[layer]._entries;
        for (var i = 0; i < 256; i++)
        {
            var addon = entries[i].Value;
            if (addon != null)
            {
                var addonName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(
                    addon->Name);
                if (addonName.SequenceEqual(nameBytes))
                {
                    return addon;
                }
            }
        }
    }
    return null;
}

Copying Arrays

// Copy one array to another
Span<float> source = sourceStruct->_values;
Span<float> dest = destStruct->_values;
source.CopyTo(dest);

// Or use MemoryCopy for pointers
var source = &sourceStruct->_values;
var dest = &destStruct->_values;
Buffer.MemoryCopy(source, dest, sizeof(float) * 10, sizeof(float) * 10);

Best Practices

Always use FixedSizeArray for non-primitive types. C# fixed arrays only work with primitives.
Use isString: true for inline C string buffers to get automatic string property generation.

Naming Conventions

// Private field with underscore
[FieldOffset(0x00), FixedSizeArray] 
internal FixedSizeArray10<int> _values;

// Public property (if needed)
public Span<int> Values => _values;

// String arrays get auto-generated property
[FieldOffset(0x00), FixedSizeArray(isString: true)] 
internal FixedSizeArray32<byte> _name;
// Generates: public string NameString { get; set; }

Bounds Checking

Spans provide automatic bounds checking in debug builds:
Span<int> values = myStruct->_values; // Size 10
var value = values[9];  // OK
var value = values[10]; // IndexOutOfRangeException in debug

See Also

Build docs developers (and LLMs) love