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
}
}
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
| Container | Use Case |
|---|
StdVector | Sequential data, frequent iteration |
StdMap | Key-value lookups, sorted data |
StdSet | Unique values, membership testing |
StdList | Frequent insertions/deletions |
StdDeque | Double-ended queue operations |
StdPair | Simple 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