Skip to main content
ExcelGenerator is a command-line tool that generates C# struct definitions for FINAL FANTASY XIV Excel sheets. It reads schema files and game data to create type-safe accessors for game data sheets, automatically handling field layouts, arrays, nested structs, and bit fields.

Overview

ExcelGenerator bridges Excel sheet schemas with C# code by:
  • Reading YAML schema definitions from EXDSchema repository
  • Analyzing Excel header files (.exh) from game data
  • Generating properly aligned C# structs with [StructLayout]
  • Creating nested types for arrays and sub-structures
  • Handling packed boolean bit fields
  • Applying [GenerateInterop] for complex types
The output is production-ready C# code in the FFXIVClientStructs.FFXIV.Component.Exd.Sheets namespace.

Installation

ExcelGenerator is built from source:
cd source/ExcelGenerator
dotnet build -c Release

Usage

Basic Usage

dotnet run --project source/ExcelGenerator/ExcelGenerator.csproj -- \
  "C:/Steam/steamapps/common/FINAL FANTASY XIV Online/" \
  "../FFXIVClientStructs/FFXIV/Component/Exd/Sheets/" \
  "./Definitions"

Command Line Arguments

Argument 1: Game Path (required)

Path to FFXIV installation directory:
"C:/Steam/steamapps/common/FINAL FANTASY XIV Online/"
"/Applications/FINAL FANTASY XIV ONLINE.app/Contents/Resources/"
"D:/Games/SquareEnix/FINAL FANTASY XIV - A Realm Reborn/"
Must contain game/sqpack directory with Excel data.

Argument 2: Output Path (optional)

Directory for generated .cs files:
"../FFXIVClientStructs/FFXIV/Component/Exd/Sheets/"
Default: ..\..\..\FFXIVClientStructs\FFXIV\Component\Exd\Sheets\ Created automatically if it doesn’t exist.

Argument 3: Schema Path (optional)

Path to schema YAML files:
"./Definitions"
Default: Auto-updates from EXDSchema repository If not provided, ExcelGenerator will:
  1. Try to update from remote repository
  2. Fall back to latest local schema if update fails

Examples

Minimal invocation (uses defaults):
dotnet run -- "C:/FFXIV/"
Full specification:
dotnet run -- \
  "C:/FFXIV/" \
  "./output/sheets/" \
  "./schemas/"
Using relative paths:
cd source/ExcelGenerator
dotnet run -- \
  "../../../game/" \
  "../../FFXIVClientStructs/FFXIV/Component/Exd/Sheets/"

Schema Format

Schemas are YAML files defining sheet structure:

Basic Schema

Item.yml:
version: 1
fields:
  - name: Name
    type: str
  - name: Icon
    type: u16
  - name: ItemLevel
    type: u16
  - name: Rarity
    type: u8
  - name: EquipSlotCategory
    type: u8

Field Types

  • Scalars: u8, u16, u32, u64, i8, i16, i32, i64, f32, bool, str
  • Arrays: Defined with count parameter
  • Structs: Nested field definitions
  • Packed Bools: Automatically detected from .exh column types

Array Fields

fields:
  - name: BaseParams
    type: struct
    count: 6
    fields:
      - name: BaseParam
        type: u8
      - name: Value
        type: i16
Generates:
public struct BaseParamsStruct {
    public byte BaseParam;
    public short Value;
}

[FixedSizeArray<BaseParamsStruct>(6)]
private FixedSizeArray6<BaseParamsStruct> _baseParams;

Nested Structs

fields:
  - name: ClassJobCategory
    type: struct
    fields:
      - name: Id
        type: u8
      - name: Flags
        type: u32
Generates:
public struct ClassJobCategoryStruct {
    public byte Id;
    public uint Flags;
}

public ClassJobCategoryStruct ClassJobCategory;

Generated Code Structure

Simple Struct

For Action.yml:
// <auto-generated/>
namespace FFXIVClientStructs.FFXIV.Component.Exd.Sheets;

[StructLayout(LayoutKind.Explicit, Size = 0x2C)]
public unsafe partial struct Action {
    [FieldOffset(0x00)] public Utf8String Name;
    [FieldOffset(0x08)] public ushort Icon;
    [FieldOffset(0x0A)] public byte ActionCategory;
    [FieldOffset(0x0B)] public byte ClassJob;
    [FieldOffset(0x0C)] public ushort Range;
    [FieldOffset(0x0E)] public ushort EffectRange;
}

With Arrays

[GenerateInterop]
[StructLayout(LayoutKind.Explicit, Size = 0x50)]
public unsafe partial struct Item {
    [FieldOffset(0x00)] public Utf8String Name;
    [FieldOffset(0x08)] public ushort Icon;
    
    [FieldOffset(0x10)]
    [FixedSizeArray<BaseParamStruct>(6)]
    private FixedSizeArray6<BaseParamStruct> _baseParams;
    
    public struct BaseParamStruct {
        public byte BaseParam;
        public short Value;
    }
}

With Bit Fields

fields:
  - name: CanBeHq
    type: bool
  - name: IsDyeable
    type: bool
  - name: IsCrestWorthy
    type: bool
Generates:
[FieldOffset(0x20)] public bool CanBeHq;    // PackedBool0 at bit 0
[FieldOffset(0x20)] public bool IsDyeable;  // PackedBool1 at bit 1
[FieldOffset(0x20)] public bool IsCrestWorthy; // PackedBool2 at bit 2

Processing Flow

1. Schema Update

using var update = new Updater(gamePath, "Definitions");
var schemaPath = update.TryUpdate();
if (schemaPath == null) {
    Console.WriteLine("Update Failed. Using Existing Schema...");
    schemaPath = update.GetLatestLocalDirectory();
}
  • Attempts to pull latest schemas from repository
  • Falls back to cached local schemas
  • Validates schema directory exists

2. Game Data Loading

using var generator = new Generator(Path.Combine(gamePath, "game", "sqpack"));
  • Initializes Lumina GameData with sqpack path
  • Loads Excel header files (.exh)
  • Provides column definitions and metadata

3. Schema Processing

For each .yml file in schema directory:
var exh = gameData.GetFile<ExcelHeaderFile>($"exd/{name}.exh");
var schema = deserializer.Deserialize(path);
  • Read YAML schema
  • Load corresponding .exh from game data
  • Match schema fields to column definitions

4. Generator Creation

For each field, create appropriate generator:
var generator = CreateGeneratorForField(fields, fieldIndex, columns, columnIndex, offset);
Generator types:
  • ScalarGenerator - Single value fields
  • ArrayGenerator - Fixed-size arrays
  • StructGenerator - Nested structures
  • BitFieldGenerator - Packed boolean fields

5. Code Generation

var fieldsBuilder = new StringBuilder();
var structsBuilder = new StringBuilder();

foreach (var generator in generators) {
    generator.WriteFields(fieldsBuilder);
    generator.WriteStructs(structsBuilder);
}
  • Build field declarations
  • Build nested struct definitions
  • Apply proper indentation
  • Add [GenerateInterop] if needed

6. File Output

var source = generator.ProcessDefinition(path, sheetName, structName, nameSpace);
File.WriteAllText(outputFile, source);

Column Type Mapping

Excel column types map to C# types:
Excel TypeC# TypeSize
StringUtf8String8 bytes
Boolbool1 byte
Int8sbyte1 byte
UInt8byte1 byte
Int16short2 bytes
UInt16ushort2 bytes
Int32int4 bytes
UInt32uint4 bytes
Float32float4 bytes
Int64long8 bytes
UInt64ulong8 bytes
PackedBool0-7boolshares 1 byte

Alignment and Sizing

Structs are aligned to 4-byte boundaries:
var structSize = columns[^1].Offset + Util.SizeOf(columns[^1].Type);
structSize = (structSize + 3) & ~3; // align up on 4
Example:
  • Last field at offset 0x1A, size 2 bytes → end at 0x1C
  • Aligned size: 0x1C (already aligned)
  • Last field at offset 0x1B, size 1 byte → end at 0x1C
  • Aligned size: 0x20 (next 4-byte boundary)

Error Handling

Schema Missing

 - schema Item missing!
Cause: No Item.yml in schema directory Fix: Update schemas or check spelling

Sheet No Longer Exists

 - sheet Item no longer exists!
Cause: Game removed the sheet, or wrong game path Fix: Verify game version and installation

Generation Failed

 -> Failed to Generate Source for 'Item (./Definitions/Item.yml)'
Cause: Schema/column mismatch or unsupported field type Fix: Check console for exception details, verify schema validity

Unknown Field Type

ArgumentException: Unknown field type: CustomType in column: 0x10 UInt32
Cause: Schema uses unsupported field type Fix: Use standard types (u8, u16, u32, i8, i16, i32, f32, bool, str, struct)

Configuration

Updater Settings

Modify Updater.cs for custom schema sources:
public Updater(string gamePath, string localPath) {
    // Configure repository URL
    // Set update behavior
    // Define caching strategy
}

Generator Options

Customize output in Generator.cs:
var nameSpace = "FFXIVClientStructs.FFXIV.Component.Exd.Sheets";
// Change to your namespace

Dependencies

NuGet Packages

  • Lumina 7.0.0 - FFXIV game data reading
  • YamlDotNet 16.3.0 - YAML schema parsing

Project References

  • InteropGenerator.Runtime (for [GenerateInterop] attribute)

Build Configuration

<PropertyGroup>
  <OutputType>Exe</OutputType>
  <TargetFramework>net10.0</TargetFramework>
  <Nullable>enable</Nullable>
</PropertyGroup>

Troubleshooting

Game Path Not Found

Problem: DirectoryNotFoundException: GamePath not Found Solution: Provide valid FFXIV installation path as first argument

Schema Update Fails

Problem: Network error or repository unavailable Solution: Generator falls back to local schemas automatically. Pre-cache schemas for offline use:
git clone https://github.com/xivapi/EXDSchema.git Definitions

Incorrect Field Offsets

Problem: Generated fields at wrong offsets Cause: Schema out of sync with game version Solution: Update schemas to match current game patch:
cd Definitions
git pull origin main

Missing Nested Structs

Problem: Nested struct types not generated Cause: Schema doesn’t define struct fields Fix: Add nested field definitions:
- name: MyField
  type: struct
  fields:
    - name: SubField1
      type: u32

Performance Issues

Problem: Slow generation Expected: 200-300 sheets in 10-30 seconds If slower:
  • Check disk I/O (sqpack reading)
  • Verify game files aren’t on network drive
  • Monitor memory usage with complex schemas

Advanced Usage

Partial Generation

Generate specific sheets only:
var allowedSheets = new[] { "Item", "Action", "Status" };
foreach (var path in Directory.EnumerateFiles(schemaPath, "*.yml")) {
    var sheetName = Path.GetFileNameWithoutExtension(path);
    if (!allowedSheets.Contains(sheetName)) continue;
    // ... process
}

Custom Type Mappings

Extend Util.cs for custom type conversions:
public static string GetCSharpType(ExcelColumnDataType type) {
    return type switch {
        ExcelColumnDataType.String => "Utf8String",
        ExcelColumnDataType.CustomType => "MyCustomType",
        _ => "uint"
    };
}

Integration with Build

Run ExcelGenerator as pre-build step:
<Target Name="GenerateExcelSheets" BeforeTargets="BeforeBuild">
  <Exec Command="dotnet run --project $(SolutionDir)source/ExcelGenerator -- $(GamePath)"/>
</Target>

Output Example

Full generated file for a simple sheet:
// <auto-generated/>
namespace FFXIVClientStructs.FFXIV.Component.Exd.Sheets;

[GenerateInterop]
[StructLayout(LayoutKind.Explicit, Size = 0x18)]
public unsafe partial struct ContentFinderCondition {
    [FieldOffset(0x00)] public Utf8String Name;
    [FieldOffset(0x08)] public byte ContentType;
    [FieldOffset(0x09)] public byte ContentMemberType;
    [FieldOffset(0x0A)] public ushort TerritoryType;
    [FieldOffset(0x0C)] public ushort ItemLevelRequired;
    [FieldOffset(0x0E)] public byte AllowUndersized;
    [FieldOffset(0x0F)] public bool HighEndDuty;
    
    [FieldOffset(0x10)]
    [FixedSizeArray<byte>(4)]
    private FixedSizeArray4<byte> _acceptClassJobCategory;
}

See Also

Build docs developers (and LLMs) love