Skip to main content

Overview

Testing is crucial for ensuring your struct definitions and function signatures work correctly. This guide covers building the project, testing signatures, and verifying your changes.

Building the Project

Prerequisites

  • .NET SDK: Install the version required by the project
  • Visual Studio or Rider (recommended) or VS Code
  • Git: For version control

Clone and Build

git clone https://github.com/aers/FFXIVClientStructs.git
cd FFXIVClientStructs/source
dotnet build FFXIVClientStructs.sln

Build Configuration

# Debug build (default)
dotnet build

# Release build
dotnet build -c Release

# Rebuild everything
dotnet build --no-incremental

Common Build Errors

Missing FieldOffset

Error: Field 'MyField' requires [FieldOffset] attribute
Solution: Add [FieldOffset(0xXX)] to every field.

Invalid Signature Format

Warning IGS001: Signature format invalid
Solution: Ensure signatures use ?? for wildcards and 2 hex digits per byte.

Type Not Unmanaged

Error: Type 'string' cannot be used in explicit layout struct
Solution: Use byte* or FixedSizeArray instead of managed types.

Missing partial Keyword

Error: Method with [MemberFunction] must be partial
Solution: Add partial keyword to the method.

Project Structure

The solution contains several projects:

FFXIVClientStructs

The main library with all struct and function definitions. Location: source/FFXIVClientStructs/

InteropGenerator

Source generator that creates function wrappers. Location: source/InteropGenerator/

InteropGenerator.Runtime

Runtime support library with attributes and resolver. Location: source/InteropGenerator.Runtime/

InteropGenerator.Tests

Unit tests for the source generator. Location: source/InteropGenerator.Tests/ Run tests:
cd source/InteropGenerator.Tests
dotnet test

FFXIVClientStructs.ResolverTester

Test project for verifying signatures resolve correctly at runtime. Location: source/FFXIVClientStructs.ResolverTester/

FFXIVClientStructs.StdContainerTester

Test project for STD container wrappers (vector, map, etc.). Location: source/FFXIVClientStructs.StdContainerTester/

Testing Struct Layouts

Verify Field Offsets

In your IDE, check the generated code to ensure field offsets are correct.

Using Visual Studio/Rider

  1. Build the project
  2. Navigate to your struct definition
  3. Right-click on the struct name → Go to Implementation or Generated Files
  4. Review the generated code

Memory Size Verification

Ensure the struct size matches the native size:
[StructLayout(LayoutKind.Explicit, Size = 0x2370)]
public unsafe partial struct Character { }
Verify:
Console.WriteLine($"Character size: 0x{sizeof(Character):X}");
// Should output: Character size: 0x2370

Check for Overlaps

Be careful not to overlap fields unintentionally:
// ❌ Wrong - Fields overlap
[FieldOffset(0x20)] public int Field1;
[FieldOffset(0x22)] public int Field2;  // Overlaps with Field1!

// ✓ Correct
[FieldOffset(0x20)] public int Field1;
[FieldOffset(0x24)] public int Field2;  // int is 4 bytes

Testing Signatures

Using ResolverTester

The ResolverTester project tests signature resolution against a running FFXIV process.

Setup

  1. Start FFXIV
  2. Open source/FFXIVClientStructs.ResolverTester/Program.cs
  3. Add code to test your signatures

Example Test Code

using FFXIVClientStructs.FFXIV.Client.System.Framework;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using System;

namespace FFXIVClientStructs.ResolverTester;

public class Program
{
    public static void Main(string[] args)
    {
        // Initialize resolver
        InteropGenerator.Runtime.Resolver.GetInstance.Setup();
        FFXIVClientStructs.Interop.Generated.Addresses.Register();
        InteropGenerator.Runtime.Resolver.GetInstance.Resolve();
        
        Console.WriteLine("Testing signatures...");
        
        // Test Framework singleton
        var framework = Framework.Instance();
        if (framework == null)
        {
            Console.WriteLine("❌ Framework.Instance failed!");
        }
        else
        {
            Console.WriteLine($"✓ Framework found at 0x{(nint)framework:X}");
        }
        
        // Test that the struct size seems reasonable
        Console.WriteLine($"  Framework size: 0x{sizeof(Framework):X}");
        
        Console.WriteLine("\nPress any key to exit...");
        Console.ReadKey();
    }
}

Build and Run

cd source/FFXIVClientStructs.ResolverTester
dotnet build
dotnet run
Note: The game must be running for the resolver to work.

Interpreting Results

Success:
✓ Framework found at 0x7FF6A1B2C3D0
  Framework size: 0x3000
Failure:
❌ Framework.Instance failed!
If failed:
  1. Check the signature is correct
  2. Verify the signature is unique (use IDA/Ghidra)
  3. Ensure the offset is correct for StaticAddress
  4. Check that the game version matches your binary

Testing Function Resolution

Test that function signatures resolve:
using FFXIVClientStructs.FFXIV.Component.GUI;

// Test member function resolution
var uiModule = UIModule.Instance();
if (uiModule != null)
{
    Console.WriteLine($"✓ UIModule found at 0x{(nint)uiModule:X}");
    
    // Try calling a function
    var addon = uiModule->GetAddonByName("CharacterStatus"u8);
    if (addon != null)
    {
        Console.WriteLine($"✓ GetAddonByName works! Addon at 0x{(nint)addon:X}");
    }
}

Signature Resolution Reports

The resolver can generate reports of failed signatures:
var resolver = InteropGenerator.Runtime.Resolver.GetInstance;
resolver.Setup();
FFXIVClientStructs.Interop.Generated.Addresses.Register();

var result = resolver.Resolve();

Console.WriteLine($"Total addresses: {result.Total}");
Console.WriteLine($"Resolved: {result.Resolved}");
Console.WriteLine($"Failed: {result.Failed}");

if (result.FailedAddresses.Any())
{
    Console.WriteLine("\nFailed signatures:");
    foreach (var addr in result.FailedAddresses)
    {
        Console.WriteLine($"  - {addr.Name}");
        Console.WriteLine($"    Signature: {addr.Signature}");
    }
}

Testing Function Behavior

Using Dalamud Plugin

The best way to test functions is with a Dalamud plugin:
using Dalamud.Plugin;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Object;

public class TestPlugin : IDalamudPlugin
{
    public void Initialize(DalamudPluginInterface pluginInterface)
    {
        // Test character functions
        unsafe
        {
            var localPlayer = (Character*)ClientState.LocalPlayer.Address;
            if (localPlayer != null)
            {
                // Test GetTargetId
                var targetId = localPlayer->GetTargetId();
                PluginLog.Info($"Target ID: {targetId}");
                
                // Test HasStatus
                var hasSprint = localPlayer->HasStatus(50);
                PluginLog.Info($"Has Sprint: {hasSprint}");
                
                // Test virtual function
                var statusMgr = localPlayer->GetStatusManager();
                if (statusMgr != null)
                {
                    PluginLog.Info("✓ GetStatusManager works!");
                }
            }
        }
    }
}

Verify Return Values

Check that functions return sensible values:
var gameObject = GetSomeGameObject();

// Test GetName
var name = gameObject->GetName();
var nameStr = Marshal.PtrToStringUTF8((nint)name.StringPtr);
Console.WriteLine($"Name: {nameStr}");  // Should be a real name

// Test GetObjectKind  
var kind = gameObject->GetObjectKind();
Console.WriteLine($"Kind: {kind}");  // Should be valid enum value

// Test GetRadius
var radius = gameObject->GetRadius();
Console.WriteLine($"Radius: {radius}");  // Should be > 0 and reasonable

Testing Virtual Functions

Verify VTable Index

Ensure the virtual function index is correct:
// In IDA/Ghidra, check the vtable:
// vf0:  Destructor
// vf1:  GetGameObjectId     <- Index 1
// vf2:  GetObjectKind       <- Index 2
// vf3:  ???
// vf4:  GetIsTargetable     <- Index 4

[VirtualFunction(1)]
public partial GameObjectId GetGameObjectId();

[VirtualFunction(4)]
public partial bool GetIsTargetable();

Test Virtual Calls

unsafe
{
    var gameObject = GetGameObject();
    
    // Call virtual function
    var objectId = gameObject->GetGameObjectId();
    Console.WriteLine($"Object ID: {objectId}");
    
    // Verify the vtable pointer exists
    if (gameObject->VirtualTable != null)
    {
        Console.WriteLine($"✓ VTable at 0x{(nint)gameObject->VirtualTable:X}");
    }
}

Testing Arrays

FixedSizeArray

Test that array access works:
public unsafe partial struct TestStruct
{
    [FieldOffset(0x00), FixedSizeArray] 
    internal FixedSizeArray10<int> _values;
}

// Test
var test = new TestStruct();
for (int i = 0; i < 10; i++)
{
    test.Values[i] = i * 10;
}

Console.WriteLine($"Value[5]: {test.Values[5]}");  // Should be 50

String Arrays

[FieldOffset(0x30), FixedSizeArray(isString: true)] 
internal FixedSizeArray64<byte> _name;

// Test writing
var testStruct = new TestStruct();
testStruct.NameString = "Test Name";

// Test reading
var name = testStruct.NameString;
Console.WriteLine($"Name: {name}");  // Should be "Test Name"

Testing in Production

Dalamud Plugin Testing

  1. Create a test Dalamud plugin
  2. Reference your local build of FFXIVClientStructs
  3. Test struct access and function calls
  4. Verify behavior in various game scenarios

Beta Testing

  1. Ask community members to test your changes
  2. Test across different game content (duties, overworld, etc.)
  3. Monitor for crashes or incorrect behavior
  4. Gather feedback on API usability

Automated Testing

Unit Tests

The InteropGenerator.Tests project has unit tests for the generator:
cd source/InteropGenerator.Tests
dotnet test
These tests verify:
  • Generator produces correct code
  • Attributes are parsed correctly
  • Analyzers catch errors

CI/CD

The repository uses GitHub Actions for CI:
  • Builds on every commit
  • Runs unit tests
  • Checks code formatting
Ensure your PR passes CI before submitting.

Common Testing Issues

Signature Doesn’t Resolve

Symptoms: Function returns null or throws exception Solutions:
  1. Check signature uniqueness in IDA/Ghidra
  2. Verify signature format (use ??, not ?)
  3. Test with ResolverTester
  4. Check game version matches your reverse engineering binary

Wrong Function Called

Symptoms: Function has unexpected side effects Solutions:
  1. Verify signature points to correct function
  2. Check parameter types and order
  3. Ensure calling convention is correct (should be automatic)
  4. Verify virtual function index

Crashes

Symptoms: Game crashes when calling function Solutions:
  1. Check parameter types match native function
  2. Verify struct pointer is valid (not null)
  3. Ensure you’re not passing managed types
  4. Check that pointers point to valid memory
  5. Verify function signature is correct

Incorrect Return Values

Symptoms: Function returns garbage or wrong data Solutions:
  1. Check return type matches native function
  2. Verify you’re calling the right function
  3. Ensure struct layout is correct
  4. Check for calling convention issues

Best Practices

  1. Test Early: Build and test as you add definitions
  2. Test Thoroughly: Don’t just test happy paths
  3. Test In-Game: Runtime testing is essential
  4. Test Edge Cases: Null pointers, invalid IDs, etc.
  5. Document Behavior: Note any quirks or special cases
  6. Use ResolverTester: Verify signatures before committing
  7. Get Feedback: Have others test your changes

Checklist Before Submitting PR

  • Project builds without errors
  • All signatures are tested with ResolverTester
  • Functions tested in-game (via Dalamud plugin or tool)
  • Return values verified as sensible
  • No crashes when calling functions
  • Field offsets verified against IDA/Ghidra
  • Struct size is correct
  • Code follows style guidelines
  • Comments added for complex/uncertain parts
  • No unrelated changes included

Getting Help

  • GitHub Issues: Report bugs or ask questions
  • Discord: Dalamud Discord for community support
  • PR Comments: Ask maintainers for feedback

Next Steps

Build docs developers (and LLMs) love