Skip to main content

Overview

FFXIV uses C++ standard library containers throughout its codebase. FFXIVClientStructs provides C# wrapper types in the FFXIVClientStructs.STD namespace that mirror the memory layout and provide C#-friendly access patterns.

Available Containers

The library includes wrappers for:
  • StdVector<T> - std::vector
  • StdMap<TKey, TValue> - std::map
  • StdSet<T> - std::set
  • StdList<T> - std::list
  • StdDeque<T> - std::deque
  • StdPair<T1, T2> - std::pair
  • StdString - std::string
  • StdWString - std::wstring
  • StdSpan<T> / StdSpanReadOnly<T> - std::span

StdVector

The most commonly used container, similar to C# List<T>.

Memory Layout

[StructLayout(LayoutKind.Sequential)]
public unsafe struct StdVector<T, TMemorySpace>
    where T : unmanaged
    where TMemorySpace : IStaticMemorySpace
{
    public T* First;  // Pointer to first element
    public T* Last;   // Pointer past last element  
    public T* End;    // Pointer to end of allocation
}

Basic Usage

// Assuming you have a struct with a vector field
public unsafe partial struct MyStruct
{
    [FieldOffset(0x00)] public StdVector<uint> ItemIds;
}

// Access elements
var myStruct = GetMyStruct();
var count = myStruct->ItemIds.LongCount;

// Iterate
for (var i = 0; i < count; i++)
{
    var itemId = myStruct->ItemIds[i];
    // Process item
}

// As span
Span<uint> items = myStruct->ItemIds.AsSpan();
foreach (ref readonly var itemId in items)
{
    // Process item
}

Vector Properties and Methods

// Count
var count = vector.Count;          // int
var longCount = vector.LongCount;  // long

// Capacity
var capacity = vector.Capacity;
var longCapacity = vector.LongCapacity;

// Indexing
var first = vector[0];
var last = vector[^1];  // C# 8.0 index from end

// As Span
Span<T> span = vector.AsSpan();
Span<T> slice = vector.AsSpan(5, 10); // offset, count

// Search
var index = vector.IndexOf(value);
var contains = vector.Contains(value);

// Conversion
var array = vector.ToArray();

Modifying Vectors

Be careful when modifying game vectors - you’re changing live game memory!
// Add elements
vector.AddCopy(item);     // Copy item
vector.AddMove(ref item); // Move item

// Insert
vector.InsertCopy(index, item);
vector.InsertMove(index, ref item);

// Remove
vector.RemoveAt(index);
vector.Remove(item);
vector.Clear();

// Resize
vector.Resize(newSize);
vector.SetCapacity(newCapacity);
vector.TrimExcess();

StdMap

Dictionary-like container (red-black tree).

Memory Layout

[StructLayout(LayoutKind.Sequential, Size = 0x10)]
public unsafe struct StdMap<TKey, TValue, TMemorySpace>
    where TKey : unmanaged
    where TValue : unmanaged
    where TMemorySpace : IStaticMemorySpace
{
    public RedBlackTree<StdPair<TKey, TValue>, TKey, PairKeyExtractor<TKey, TValue>> Tree;
}

Basic Usage

// Access
if (map.TryGetValue(key, out var value, copyCtor: false))
{
    // Use value
}

// Get pointer to value
if (map.TryGetValuePointer(key, out var valuePtr))
{
    // Modify in place
    *valuePtr = newValue;
}

// Iterate
foreach (ref readonly var pair in map)
{
    var key = pair.Item1;
    var value = pair.Item2;
    // Process pair
}

Map Operations

// Count
var count = map.Count;
var longCount = map.LongCount;

// Check existence
var exists = map.ContainsKey(key);

// Add
var added = map.TryAddValueKCopyVCopy(key, value);

// Remove  
var removed = map.Remove(key);

// Clear
map.Clear();

// Keys and values
var keys = map.Keys;
var values = map.Values;

StdPair

Simple tuple-like structure:
[StructLayout(LayoutKind.Sequential)]
public struct StdPair<T1, T2> 
    where T1 : unmanaged
    where T2 : unmanaged
{
    public T1 Item1;
    public T2 Item2;
}

// Usage
var pair = new StdPair<uint, uint> 
{ 
    Item1 = 100, 
    Item2 = 200 
};

StdString

C++ std::string wrapper:
// In a struct
[FieldOffset(0x00)] public StdString Name;

// Access
var bytes = name.AsSpan();
var text = Encoding.UTF8.GetString(bytes);

// Or use ToString if available
var text = name.ToString();

Memory Spaces

Containers use memory space specifiers to handle different allocators:
// Default game allocator
StdVector<uint, StdDefaultSpace>

// Specific memory space
StdVector<uint, MyCustomMemorySpace>
Most of the time you’ll use the default:
// Type alias for default memory space
public struct StdVector<T> : StdVector<T, StdDefaultSpace>
    where T : unmanaged
{
}

Examples from the Codebase

STD Vector Implementation

From StdVector.cs:16-37:
public unsafe struct StdVector<T, TMemorySpace>
    : IStdVector<T>
    where T : unmanaged
    where TMemorySpace : IStaticMemorySpace
{
    /// <summary>
    /// The pointer to the first element. <c>null</c> if empty.
    /// </summary>
    public T* First;

    /// <summary>
    /// The pointer to next of the last element. <c>null</c> if empty.
    /// </summary>
    public T* Last;

    /// <summary>
    /// The pointer to end of allocation. <c>null</c> if empty.
    /// </summary>
    public T* End;

    public readonly void* RepresentativePointer => First;
}

Vector Properties

From StdVector.cs:48-78:
public int Count
{
    readonly get => checked((int)LongCount);
    set => Resize(value);
}

public long LongCount
{
    readonly get => Last - First;
    set => Resize(value);
}

public int Capacity
{
    readonly get => checked((int)LongCapacity);
    set => SetCapacity(value);
}

public long LongCapacity
{
    readonly get => End - First;
    set => SetCapacity(value);
}

public readonly ref T this[long index] 
    => ref First[CheckedIndex(index < 0 ? LongCount - ~index : index)];

public readonly ref T this[int index] 
    => ref this[(long)index];

StdMap Implementation

From StdMap.cs:9-34:
public unsafe struct StdMap<TKey, TValue, TMemorySpace>
    : IStdMap<TKey, TValue>
    where TKey : unmanaged
    where TValue : unmanaged
    where TMemorySpace : IStaticMemorySpace
{
    public RedBlackTree<StdPair<TKey, TValue>, TKey, PairKeyExtractor<TKey, TValue>> Tree;

    public readonly int Count => checked((int)Tree.LongCount);
    public readonly long LongCount => Tree.LongCount;

    public readonly IStdMap<TKey, TValue>.KeyCollection Keys 
        => new(Tree.Pointer);

    public readonly IStdMap<TKey, TValue>.ValueCollection Values 
        => new(Tree.Pointer);

    public ref TValue this[in TKey key]
    {
        get
        {
            var loc = Tree.FindLowerBound(key);
            if (loc.KeyEquals(key))
                return ref loc.Bound->_Myval.Item2;
            // ... handle missing key
        }
    }
}

Working with Pointers in Containers

C# doesn’t allow pointer types in generics, so use Pointer<T>:
// Instead of: StdVector<AtkUnitBase*>
StdVector<Pointer<AtkUnitBase>>

// Pointer<T> implicitly converts to T*
var entry = vector[0];
AtkUnitBase* addon = entry; // Implicit conversion

// Or use .Value
AtkUnitBase* addon = entry.Value;
Example from AtkUnitList.cs:8:
[FieldOffset(0x8), FixedSizeArray] 
internal FixedSizeArray256<Pointer<AtkUnitBase>> _entries;

// Access
for (var i = 0; i < 256; i++)
{
    var entryPtr = _entries[i];
    AtkUnitBase* addon = entryPtr.Value; // or entryPtr implicitly
    if (addon != null)
    {
        // Use addon
    }
}

Performance Considerations

Direct Pointer Access

For performance-critical code, use pointers directly:
// Fast - direct pointer iteration
var ptr = vector.First;
var end = vector.Last;
while (ptr < end)
{
    // Process *ptr
    ptr++;
}

// Slower - indexer with bounds checking
for (var i = 0; i < vector.Count; i++)
{
    var item = vector[i];
}

// Good balance - span iteration
Span<T> span = vector.AsSpan();
foreach (ref readonly var item in span)
{
    // Process item
}

Avoid Allocations

// Bad - allocates array
var array = vector.ToArray();
foreach (var item in array) { }

// Good - no allocation
Span<T> span = vector.AsSpan();
foreach (ref readonly var item in span) { }

// Good - direct pointer access
for (var ptr = vector.First; ptr < vector.Last; ptr++)
{
    // Process *ptr
}

Common Patterns

Safe Iteration

// Check for null/empty
if (vector.First != null && vector.LongCount > 0)
{
    foreach (ref readonly var item in vector.AsSpan())
    {
        // Process item
    }
}

Finding Elements

// Using LINQ-like methods
var index = vector.LongFindIndex(x => x.Id == targetId);
if (index >= 0)
{
    var found = vector[index];
}

// Manual search
for (var i = 0L; i < vector.LongCount; i++)
{
    if (vector[i].Id == targetId)
    {
        // Found it
        break;
    }
}

Map Lookups

// Preferred - no exceptions
if (map.TryGetValue(key, out var value, copyCtor: false))
{
    // Use value
}

// Get pointer for in-place modification
if (map.TryGetValuePointer(key, out var ptr))
{
    ptr->SomeField = newValue;
}

Best Practices

Never modify game containers unless you know exactly what you’re doing. Corruption can crash the game.
Use AsSpan() for safe, efficient iteration with automatic bounds checking.

When to Use Each Container

ContainerUse Case
StdVectorSequential data, frequent iteration
StdMapKey-value lookups, sorted data
StdSetUnique values, membership testing
StdListFrequent insertions/deletions
StdDequeDouble-ended queue operations
StdPairSimple key-value or coordinate pairs

Reading vs Writing

// Reading - generally safe
var count = vector.LongCount;
var item = vector[0];
var exists = map.ContainsKey(key);

// Writing - be very careful!
vector.AddCopy(newItem);     // Modifies game memory!
map.Remove(key);             // Can break game logic!
vector.Clear();              // Dangerous!

See Also

  • Fixed Arrays - For fixed-size inline arrays
  • String Handling - For StdString usage
  • STD implementation files in FFXIVClientStructs/STD/

Build docs developers (and LLMs) love