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