Only this pageAll pages
Powered by GitBook
1 of 36

XIV Dev Wiki

Loading...

Loading...

Game Data

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Data Files

Loading...

Loading...

Loading...

Network

Loading...

Loading...

Loading...

Game Internals

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

File Formats

Documentation for files located within the games internal filesystem.

Excel

Excel is the relational data storage system that the game uses to store information, similarly to a relational database. The structure of EXL, EXH and EXD files are documented here.

Timelines

A timeline consists of a list of items, and a list of clips.

Items represent the creation of an emitter, while clips represent other actions, such as deleting particles. Although clips are not well understood, the are probably used for things such as hiding a vfx when a weapon is sheathed.

Clips

Clips must be triggered from items using the ClipNumber parameter. Usually, items which trigger clips will have EmitterIndex = -1.

Items are also where emitters are attached to binders and effectors using BinderIndex, EmitterIndex, and EffectorIndex. Items which were originally used for a cutscene or background will usually have BinderIndex = -1, which will cause them to not be rendered on player characters, so creating a binder and setting BinderIndex = 0 can fix this.

Object Life

Life controls how long a particle lives, and can be somewhat counter-intuitive. Life = -1 means that the emitter lives forever, so it's animations will only trigger once. If you want an emitter to continuously loop (that is, die and get re-created), set it a value greater than -1.

Channels

A channel is a specific connection to a game server: lobby, chat and world.

Lobby

Chat

World

Chat Log (.log)

World

(TODO)

Emitters

Emitters, as their name suggests, emit objects (either other emitters, or particles). They are triggered through timeline items, and can be moved in 3D space just like any other object. Unlike particles, emitters themselves are not visible. Emitters can also trigger a sound to play.

When an emitter creates another object, it can influence the properties of its child (such as color, scale, or position). Whether or not an emitter will influence its children is determined by parameters such as InfluenceCoordScale.

There are several types of emitters:

  • Point

  • Cone

  • Cone Model (like a cone, but is subdivided across its axes)

  • Sphere

  • Sphere Model

  • Model (any model, where objects can be emitted on its vertices)

Visual Effects

Documentation for the VFX system used by FFXIV

(TODO)

Particles

Particles are the visible component of a VFX, and are created exclusively by emitters. They are similar to emitters in that they can be positioned in 3D space.

There are several types of particles:

  • Decal

  • Decal Ring

  • Disc

  • Laser

  • Light Model

  • Line

  • Model

  • Parameter

  • Polygon

  • Polyline (line made of triangles)

  • Powder (used for small particles, such as the dust which some weapons create)

  • Windmill

  • Quad (a 1x1 square)

Particles can have textures mapped onto them using several different types of nodes (covered later).

Simple Animations

Sometimes, a particle does not require complex animations, and can use the SimpleAnimations node instead (in order for it to work, the SimpleAnimationsEnabled attribute must be True) This is often used for Powder particles, which often have very basic animations due to their size and number.

Particle Textures

There are several types of texture nodes which can be applied to a particle. Most texture nodes will have a TextureIndex. This determines which texture will be used as a color, normal map, etc. TextureColor1 is especially important as it has MaskTextureIndex, which determines which parts of the particle will be transparent.

Most texture nodes will also have a UVSetIndex (UV Sets are discussed later).

UV Sets

UV Sets are contained within particles, and are used to manipulate textures. This primarily involves scaling, rotating, and offsetting a texture. For example, most aura or fire VFXs will use a UV Set to animate a texture so that it "scrolls" across the particle, creating the illusion of a moving flame or energy aura.

Effectors

Effectors apply miscellaneous effects to emitters, particles, or the camera. In order to be used, they must either be triggered through a timeline item, or an emitter.

There are several types of effectors:

  • Point light

  • Directional light

  • Radial Blur

  • Black hole

  • Camera quake (if you've ever been annoyed by the screen shake from Holy, this is why)

Coordinate System

Few things to note:

  • Just like Super Mario 64, y component of coordinate represents altitude. Think z component as depth as in Z-buffer.

(TODO)

RSF

RSF is very similar to RSV except it works on file level.

Pseudocode

The procedure to generate a RSF file is as follows:

def create_rsf(plaintext):
    deflated_bytes = zlib.compress(plaintext, level=9)
    
    # Server stores this part and will send it to the client when zoning-in
    header = deflated_bytes[:64]
    
    # Client stores this part
    body = deflated_bytes[64:]

Binders

Binders determine where a vfx should be created, and its tracking behavior (should it follow a moving character).

There are several types of binders:

  • Point (the most common, attach to a character, for example)

  • Camera

  • Spline

  • Linear

Binders can be attached to caster or target, and have several "property" fields. Properties have the attribute BinderPointId, which allows a vfx to be bound to a specific point on a model (such as at the hilt of a sword). Binder points are specified in the skeleton's .mdl file, although this is not well-understood.

Actions

(TODO)

Global Parameters

These are global settings applied to the entire VFX. Parameters such as Revised Scale and Revised Color can be used to adjust the size and color of an entire VFX without having to manually change the values of each individual component.

Also of note are the DrawLayer parameter, which controls when the VFX is drawn (such as BeforeSky or BeforeCloud), and the Clipbox settings.

Character Data Files

Contains information about files that are specific to each character.

Data files are xxx.dat files generally located in the XIV client folder and sub folders at <..>\Documents\My Games\FINAL FANTASY XIV - A Realm Reborn\.

The FFXIV Character Creator saves presets, when saving a character appearance at the end of character creation, in a file called FFXIV_CHARA_XX.dat where XX is the preset slot number from 01 to 40.

All data related to characters that have been used on the current machine is saved in a folder called FFXIV_CHRxxxx where xxxx is a unique ID for the character.

This folder contains *.dat files for character settings, hotbars and UI configuration, macros and so on, as well as a log folder containing the chat logs.

Ping

Internal ping calculation

  1. The client uses to generate an arbitrary timestamp, which it converts roughly into milliseconds by dividing it by 10,000.

  2. An IPC message is sent by the client which includes the timestamp encoded as a 32-bit integer value.

  3. An IPC message is sent back by the server with the same timestamp.

  4. The client generates a new timestamp, divides it by 10,000, and then computes the difference between them to get the RTT for the connection.

Pseudocode

Remarks

Because the server copies the same timestamp into the response message as is in the request message, ping calculation can be done in a stateless manner with respect to the client.

RSV

RSV is a placeholder value used for masking the actual value until the player is actually zoned in.

The packet sent from the server upon entering the instance is as follows:

Pseudocode

Links

SqexArg

Encoded Process Argument

Upon launching the game, the retail launcher passes handful of information (e.g. login token, client language, etc...) to the game that usually looks like this:

Encoding Process

  • Encrypt UTF-8 plain text using Blowfish; you need to pad the buffer with zero as needed

    • Key is GetTickCount() & 0xFFFF_0000

    • Block cipher mode of operation is ECB (yes, you read it right.)

  • Convert encrypted bytes to

  • Format it as //**sqex0003{base64}{checksum}**//

Checksum

Pseudocode

Character data folder

*[Last updated FFXIV 6.58]

This folder, called FFXIV_CHRxxxx where xxxx is a unique ID for the character, contains all of the client side data related to one or another of the characters played.

Following is a list of the data files found in this folder.

Note: most of the information in here is taken from without doublechekcing for now.

TODO: figure out the format for each file.

  • ACQ.DAT: The list of recent /tell contacts. Binary format

  • ADDON.DAT: This stores your UI settings such as HUD layouts and window sizes. Binary format

  • COMMON.DAT: This stores your Character Settings. Plain text format.

  • CONTROL0.DAT: This stores Keyboard/Mouse Settings. Plain text format.

  • CONTROL1.DAT: This stores Gamepay Settings. Plain text format.

  • GEARSET.DAT: This contains your Gear sets. Binary format.

  • GS.DAT: Unknown. Binary format.

  • HOTBAR.DAT: This contains the contents of your Hotbars, but not their positions or such. Binary format.

  • ITEMFDR.DAT: Item search index, used when searching for similar items. Binary format.

  • ITEMODR.DAT: This contains Inventory and Retainer item sort positions. Binary format.

  • KEYBIND.DAT: This contains your Keybinds. Binary format.

  • LOGFLTR.DAT: This contains your Chat log filters. Binary format.

  • MACRO.DAT: This is your Macros. Binary format.

  • UISAVE.DAT: This contains timers such as the retainer venture timer, possibly other stuff. Binary format.

  • : The game writes your chat history to disk each time its internal buffer overflows, which potentially allows you to retrieve chat history in longer play sessions.

Character Creator Presets
Character Data Folder
void SendPingRequest() {
    int64_t t;
    PingRequest req;
    QueryPerformanceCounter(&t);
    req.timestamp = (int32_t)(t / 10000);
    SendMessage(req);
}

int32_t HandlePingResponse(PingResponse resp) {
    int64_t currT;
    int32_t prevMs = resp.timestamp;
    QueryPerformanceCounter(&currT);
    int32_t currMs = (int32_t)(currT / 10000);
    return currMs - prevMs;
}
QueryPerformanceCounter
// sent for each rsv string
struct RsvPkt {
  UInt32 value_size;    // len(without_nul_char(value))
  char key_str[32];     // null terminated
  char value_str[1024]; // null terminated
}
void OnRsvPktreceived(RsvPkt pkt) {
  // e.g.) "_rsv_29752_-1_1_C0_0Action"
  var key = ReadCString(pkt.key_str);
  // e.g.) "Alternative End"
  var val = ReadCString(pkt.value_str);

  rsvMap[key] = val;
}

string GetActionName(int row) {
  var val = table[row, col];

  if (rsvMap.Contains(val)) {
    var key = val;
    return rsvMap[key];
  } else {
    return val;
  }
}
https://github.com/NotAdam/Lumina/blob/0530a97d21/src/Lumina/Excel/RSV/RsvProvider.cs
//**sqex0003VGhpcyBpcyBqdXN0IGZvciBleGFtcGxlIHB1cnBvc2U7IGl0IGFjdHVhbGx5IG5ldmVyIGxvb2sgbGlrZSB0aGlzA**//
char[] ChecksumTable = {
    'f', 'X', '1', 'p', 'G', 't', 'd', 'S',
    '5', 'C', 'A', 'P', '4', '_', 'V', 'L'
};

char GetChecksum(uint key) {
    // mask the nibble we're looking for
    var value = key & 0x000F_0000;

    return ChecksumTable[value >> 16];
}
Base64URL
this reddit post
"log" folder

Weather

In most zones, weather is pseudorandom and can be derived from the current time. Each zone has a weather table, which contains the possible weather states for the zone and the stacked probability of that weather being active.

Target value

The value that the pseudorandom number generator produces is referred to as the target value. This value can be calculated deterministically for any given instant. It will always be in the range [0, 99].

Calculation

Given a UNIX timestamp unixSeconds:

int32_t CalculateTarget(int32_t unixSeconds) {
    // Get Eorzea hour for weather start
    int32_t bell = unixSeconds / 175;

    // For these calculations, 16:00 is 0, 00:00 is 8 and 08:00 is 16.
    // This rotates the time accordingly and holds onto the leftover time
    // to add back in later on.
    uint32_t increment = ((uint32_t)(bell + 8 - (bell % 8))) % 24;

    // Take Eorzea days since the unix epoch
    uint32_t totalDays = (uint32_t)(unixSeconds / 4200);

    uint32_t calcBase = (totalDays * 0x64) + increment;
    uint32_t step1 = (calcBase << 0xB) ^ calcBase;
    uint32_t step2 = (step1 >> 8) ^ step1;
    return (int32_t)(step2 % 0x64);
}

Weather lookup

Once the target value has been calculated, the WeatherRate row for the current TerritoryType is retrieved. The rates of each entry in the row are accumulated until the target value is less than the current value of the rate accumulator. At this point, the current weather rate entry is selected, and the corresponding weather ID denotes the Weather row that should be used.

Retrieving the weather for a zone by zone name

Territories can be queried by name using their PlaceName attribute. Territories have a WeatherRate that describes the weather rate row they use. Using the process described above, the current Weather being used can be determined.

Remarks

Some zones, such as the original version of Diadem, use the WeatherChange message to control what weather the player experiences. It is not known what zones currently use this message for weather.

Schedulers

Every VFX has exactly 1 Scheduler.

Schedulers control when Timelines are started, and have 2 components: a list of items, and a list of triggers. A scheduler can have an arbitrary number of items, but must have 12 triggers. Both items and triggers follow the same format:

bool Enabled
int StartTime
int TimelineIndex

Triggers

What the triggers represent is not understood, but they are probably each for a specific type of action, such as sheathing a weapon, getting hit, etc. Unused triggers have TimelineIndex = -1.

Items

Items function like triggers, but will start automatically without any additional input. If you are having trouble viewing a VFX, such as one which was originally used for a cutscene, it can often be useful to disable all the triggers, and instead exclusively use items to start timelines.

Animation Lock

Animation lock is an internal timer that player has to wait certain amount of time before they are allowed to use any actions.

Animation lock is an internal timer that player has to wait certain amount of time before they are allowed to use any actions.

Basics

  • Timer counts toward zero. (i.e. it represents remaining time)

  • Many GCDs have 0.1s animation lock. This timer is while casting. It is resumed after player finishes casting on client side.

  • oGCDs and instant skills have 0.5s initial animation lock.

  • Server can also set animation lock in response to actions used from the client. (usually by the packet commonly called "effect packet") However, it simply discards any previous timer running. Therefore, game does not compensate for player's ping. (i.e. time already spent during initial animation lock)

  • You can't target ground while animation lock is active.

When those three above combined, it can greatly deter game-play experience in certain condition. This will be discussed below.

Properties

  • Some actions like limit break, foods and duty actions may lock for different amount of time. For example, movement skill locks for 0.8 seconds and items have 2.0 seconds.

Clipping GCD

Since animation lock from the server overwrites curent animation lock to new value, it is this reason why double weaving between GCDs extremely unliable when player is connected to the datacentre across continent.

Hardcasting Swiftcast

  • If the client fail to receive response during initial animation lock, client is allowed to use next action. TODO.

While Swiftcast is one of notorious offender, this property is not limited to Swiftcast only. For example, Red Mage's Dualcast just works same as hardcasting Swiftcast.

Cancelling Animation Lock

TODO: rescue lb3

TODO: it is possible to cancel oGCD 0.6s animlock by letting GCD overwrites it on high ping environment. (something something related to hardcasting swiftcast)

Welcome

Welcome to the XIV Dev Wiki. The idea is for this to be a single place where everything is located in a easy to access and searchable manner. Meta discussion and questions should be via issues on the GitHub repo that this documentation is pulled from: .

Final Fantasy XIV © 2010-2022 SQUARE ENIX CO., LTD. All Rights Reserved. We are not affiliated with SQUARE ENIX CO., LTD. in any way.

https://github.com/xivdev/docs/

Character Creator Preset

The character creator saves it's presets in binairy files called FFXIV_CHARA_XX.dat where XX is the preset slot number. These files are located in the <..>\Documents\My Games\FINAL FANTASY XIV - A Realm Reborn\ folder.

Note: The investigation into this format has been made in a very cursory manner. There is no garantie of accuracy.

Binary format details

[Last updated: FFXIV 7.0 Benchmark]

The file is read in little endian format.

The file contains 4 sections:

  • A 16 bytes header

  • A 32 bytes data block

  • A 40 bytes/characters comment section

  • A 124 bytes of 0x00 at the end of the file

Here is a sample file, displayed in hexadecimal, from the 7.0 Benchmark Character Creator: The colors represent the data sections related to this other view: The mechanisms to understand this view are detailed in the next sections.

File header

The 16 bytes of file header are split into 4 sections:

  • A 32 bits file header @0x00, in orange. Always 0x2013FF14.

  • A 32 bits version ID @0x04. Only the LSB seems to be used. Here in green, 7(0x00000007).

  • A 32 bits assumed checksum value @0x08. Here in red, 0x77225B1E.

  • A 32 bits padding of 0x00000000 @0x0C, in purple.

Data

The actual preset data follows. Using ImHex's pattern syntax, here is the actual character data, in order. The full pattern parser file for imHex is available here and can be opened in any text editor or ImHex.

Knowing the following data sizes (See full pattern file for the full enum values):

Race = u8 enum;
Gender = u8 enum;
Age = u8 enum;
Subrace = u8 enum;
FFXIVid = u8;
FFXIVColor = u8;
FaceFeature = u8 bitfield;
EyeShape = u8;
LightDarkColor = u8;
FacePaint = u8;

Notes:

  • FaceFeature is a bitfield where each bit is considered a boolean for a specific feature. For exemple bits 6 (0x40) and 5 (0x20) represent left and right tattoos for Hyur and will be 1 when the tattos is visible.

  • EyeShape, LightDarkColor and FacePaint use the 7th bit (MSB, 0x08) of the u8 as a boolean to for exemple, switch between "light" or "dark" color, or "reversed" facepaint.

These are all of the data saved in the file:

    Race race;
    Gender gender;
    Age age;
    u8 height_pct;
    Subrace subrace;
    FFXIVid face_id;
    FFXIVid hair;
    match (race){
        (Race::Hrothgar): bool enable_fur_pattern;
        (_): bool enable_hair_highlights;
    }
    FFXIVColor skin_tone;
    FFXIVColor right_eye_color;
    FFXIVColor hair_color;
    match (race){
        (Race::Hrothgar): FFXIVColor pattern_fur_color;
        (_): FFXIVColor hair_highlights_color;
    }
    FaceFeatures facial_features;
    match (race){
        (Race::AuRa): FFXIVColor limbal_ring_color;
        (Subrace::Wildwood): FFXIVColor ear_clasp_color;
        (Subrace::Keepers_of_the_Moon & Gender::female): FFXIVColor ear_clasp_color;
        (_): FFXIVColor tattoo_color;
    }
    FFXIVid eyebrows;
    FFXIVColor left_eye_color;
    EyeShape eyes_shape;
    FFXIVid nose;
    FFXIVid jaw;
    FFXIVid mouth;
    match (race){
        (Race::Hrothgar): FFXIVid fur_pattern_id;
        (_): LightDarkColor lips_color;
    }
    match (race){
        (Race::Hyur | Race::Roegadyn): u8 muscle_tone_pct;
        (Race::Miquote | Race::AuRa | Race::Hrothgar): u8 tail_length_pct;
        (Race::Lalafell | Race::Elzen | Race::Viera): u8 ear_length_pct;
        (_): u8 _tbd;
    }
    FFXIVid tail_shape;
    u8 bust_pct;
    FacePaint face_paint;
    LightDarkColor face_paint_color;
    FFXIVid voice;
    u8 _space2;

Sections containing match (...){} are single bytes that take on different meanings depending on, usually, the character's Race.

Comment

From 0x3000 to 0x5008 included are the 40 characters (bytes) available as a comment when saving the preset.

Empty space

The rest of the file is empty space. No usage has been found yet for it.

Community Projects

Various open source resources and projects

This is a list of various open source resources and projects related to FFXIV. Please leave an issue if your project is listed here and you'd like to remove it.

Programs

XIVLauncher

link

language

C#

A popular custom launcher with extra features. Good reference for the login flow (including Steam/Free Trial accounts) and the patching system.

Dalamud

link

language

C#

A plugin framework and API, meant to be used with XIVLauncher. Almost all official plugins are open source.

Libraries

Lumina

link

language

C#

Library for reading and interacting with FFXIV game files and Excel data.

SaintCoinach

link

language

C#

Another library for reading and interacting with game files/Excel data. Provides community maintained metadata on Excel data types.

Ironworks

link

language

Rust

Yet another library for game data, but in Rust.

kobold

link

language

TypeScript

Another library for game data, in TypeScript.

FFXIVClientStructs

link

language

C#, Python

A C# library containing community-maintained structs in the client, and Python scripts to help reverse engineer the game binary in IDA/Ghidra.

Tools

SaintCoinach.Cmd

link

language

C#

A command line interface for SaintCoinach. Features commands to export raw game files, the UI images, and music.

Godbert

link

language

C#

A GUI interface for browsing Excel data and viewing territories, powered by SaintCoinach.

FFXIV Explorer

link

language

Java

A GUI for browsing the game files. You may be interested in this fork adding more features.

Websites & APIs

Thaliak

link

source

An API for game versions and patch information.

ResLogger2

link

source

A list of file paths in the game files, crowdsourced via a Dalamud plugin.

XIVAPI

link

source

A general-purpose API for FFXIV. Most useful if you can't embed a library into your application or need Lodestone information.

Networking

Sapphire

link

language

C++

A server emulator. Not always up to date, but still a great reference on many things.

Machina

link

language

C#

A general library for packet capture with specialized FFXIV functionality.

Velcro

link

language

Go, C#

A set of tools to capture network data and store it into an SQLite database.

Nari

link

language

Python

A library to parse ACT logs.

ZiPatch

ZiPatch(.patch) is a file format used to update online games made by SQEX.

We'll only cover version 3 for now.

Intro

This patch format has been extant since FFXIV 1.0, but has had a few modifications since then, especially from 2.0 onward.

A patch file name generally starts with either H or D, for HIST and DIFF, respectively, followed by a version string, such as 2013.08.09.0001.0000. This is optionally followed by a lowercase letter (starting at 'a') in case the patch file corresponding to that version string would be larger than 1.5GB for HIST files. e.g.: H2013.08.09.0001.0000a.patch, D2019.05.29.0000.0000.patch.

Patch Naming Convention

Patch files follow the following naming convention:

<D|H><YEAR>.<MONTH>.<DAY>.<PART>.<REVISION>.patch

Where D|H indicates that a patch is a delta patch or a history patch respectively.

Some example patch names are as follows:

H2017.06.06.0000.0001a.patch
H2017.06.06.0000.0001b.patch
D2017.07.11.0000.0001.patch
D2019.04.16.0000.0000.patch

Notes

  • Byte order, unless explicitly mentioned, is big endian order. That is, 0x00AB_CDEF in hexadecimal will be encoded as 00 AB CD EF in file - not EF CD AB 00 which is little endian.

File Signature

All patch files start with 91 5A 49 50 41 54 43 48 0D 0A 1A 0A.

Chunks

The ZiPatch signature is followed by a contiguous array of chunks with the following structure

struct ChunkEntry
{
    ChunkHeader Header;
    Chunk Payload; // TODO: explain this
    UInt32BE Crc32;
}
[StructLayout(LayoutKind.Explicit, Size = 8)]
struct ChunkHeader
{
    [FieldOffset(0)] UInt32BE Size;
    [FieldOffset(4)] char Name[4];
}

Where Header.Size indicates the size of Chunk.Payload, and Header.Name is a 4-character string indicating the type of the chunk, e.g., "FHDR", "APLY", and "SQPK".

Following Header, there's a Chunk describing the chunk contents, which will be detailed in the following pages. Following the chunk payload, there is a CRC32 of Header.Name with Payload.

This contiguous array of chunks terminates with a chunk of Name "EOF_".

FHDR - File Header

[StructLayout(LayoutKind.Explicit, Size = 256)]
struct FhdrChunk
{
    [FieldOffset(0x2)] byte Version; // ZIPATCH version (3)
    [FieldOffset(0x4)] char Name[4];
    // TODO...
    [FieldOffset(0x20)] UInt32BE DepotHash; // also observable in url
}

FHDR is always the first chunk in the file, containing a few unused fields and some information about the current patch file (such as number of commands, etc.)

FhdrChunk.Name has been observed to be HIST for history patches, or DIFF for delta patches.

APLY - Apply Option

[StructLayout(LayoutKind.Explicit, Size = 12)]
struct AplyChunk
{
    // 1 -> Ignore Missing; 2 -> Ignore Mismatch
    [FieldOffset(0x0)] UInt32BE Option;
    // Believed to be length (in bytes) of Value, but it doesn't currently change
    [FieldOffset(0x4)] UInt32BE Reserved;
    // Non-zero Value sets Option to true for this .patch file
    [FieldOffset(0x8)] UInt32BE Value;
}

The two currently known Options, Ignore Missing and Ignore Mismatch are normally set to false via two APLY chunks set right after the FHDR. This means that the official patcher fails if a file it expects there is missing or some data it expects to be there is not.

ADIR - Add Directory

[StructLayout(LayoutKind.Explicit)]
struct AdirChunk
{
    [FieldOffset(0x0)] UInt32BE PathLength;
    [FieldOffset(0x4)] char Path[PathLength]; // TODO: This isn't valid C#
}

Creates directory at Path. Path is relative to the game folder plus the patch type being applied (boot or game).

e.g.: If Path = "movie\ex1", and we are patching 'game' at "C:\Games\SquareEnix\FINAL FANTASY XIV - A Realm Reborn\", we would create the folder "ex1" at location "C:\Games\SquareEnix\FINAL FANTASY XIV - A Realm Reborn\game\movie\"

DELD - Delete Directory

[StructLayout(LayoutKind.Explicit)]
struct DeldChunk
{
    [FieldOffset(0x0)] UInt32BE PathLength;
    [FieldOffset(0x4)] char Path[PathLength]; // TODO: This isn't valid C#
}

Similar to ADIR, but the folder at Path is deleted instead. It only deletes empty folders.

SQPK

_EOF

Indicates no chunks should be processed after this.

[StructLayout(LayoutKind.Explicit, Size = 32)]
struct EofChunk
{
    // zero padded
}

Links

  • ZiPatch File Structure

  • https://github.com/goatcorp/FFXIVQuickLauncher/tree/master/XIVLauncher.PatchInstaller/ZiPatch

SQPK
https://github.com/goatcorp/FFXIVQuickLauncher
https://github.com/goatcorp/Dalamud
https://github.com/NotAdam/Lumina
https://github.com/xivapi/SaintCoinach
https://github.com/ackwell/ironworks
https://github.com/ackwell/kobold
https://github.com/aers/FFXIVClientStructs
https://github.com/xivapi/SaintCoinach
https://github.com/xivapi/SaintCoinach
https://bitbucket.org/Ioncannon/ffxiv-explorer/src
http://thaliak.xiv.dev/
https://github.com/avafloww/Thaliak
https://rl2.perchbird.dev/
https://github.com/lmcintyre/ResLogger2
https://xivapi.com/
https://github.com/xivapi/xivapi.com
https://github.com/SapphireServer/Sapphire
https://github.com/ravahn/machina
https://github.com/velcro-xiv/velcro
https://github.com/xivlogs/nari

SQPK

Primitives

[StructLayout(LayoutKind.Explicit, Size = 5)]
struct SqpkChunk
{
    [FieldOffset(0x0)] Uint32BE Size;
    [FieldOffset(0x4)] SqpkOperation Opcode;
    [FieldOffset(0x5)] SqpkPayload Payload;
}

enum SqpkOperation : byte
{
    A, // Add data. Used to overwrite blocks in dat files. Can also delete old data
    D, // Delete data. Used to delete blocks (overwrite with 0x00) in dat files
    E, // Expand data. Used to insert empty blocks into dat files
    F, // File operations. Adding files, deleting files, etc.
    H, // Header Update. Updates headers of dat or index files
    I, // Index Add/Delete. Present in patch files but unused
    X, // Patch Info. Present in patch files but unused
    T, // Target Info. Present in patch files but only one field is used
}

SqpkChunk is a 'container' chunk which executes one of several operations in SqpkOperation. These will be detailed in the following subsections.

Important to note that any fields prepended with "Block" will mean that the number is in blocks, or units of 128-bytes. e.g., BlockOffset means that the offset in bytes would be calculated by BlockOffset << 7 or BlockOffset * 128.

These operations often contain fields indicating which file the operation will apply to. These adhere to the following structure:

[StructLayout(LayoutKind.Explicit, Size = 8)]
struct SqpkFile
{
    [FieldOffset(0x0)] UInt16BE MainId;
    [FieldOffset(0x2)] UInt16BE SubId;
    [FieldOffset(0x4)] UInt32BE FileId;

    // Expansion => (byte)(SubId >> 8);
    // DatFileName => $"{MainId:x2}{SubId:x4}.win32.dat{FileId}"
    // IndexFileName => $"{MainId:x2}{SubId:x4}.win32.index{(FileId == 0 ? string.Empty : FileId.ToString())}"
}

When referring to block headers, the following struct will be relevant:

[StructLayout(LayoutKind.Explicit, Size = 128)]
struct BlockHeader
{
    [FieldOffset(0x0)] UInt32LE Size;
    [FieldOffset(0x4)] UInt32LE Type;
    [FieldOffset(0x8)] UInt32LE FileSize;
    [FieldOffset(0xC)] UInt32LE TotalBlocks;
    [FieldOffset(0x10)] UInt32LE UsedBlocks;
}
struct BlockCount {
  [FieldOffset(0x00)] UInt32BE Value;

  UInt32 InBytes => Value * 128;
}
struct SqpkFilePath {
  [FieldOffset(0x00)] UInt16BE Main;
  [FieldOffset(0x02)] UInt32BE Sub;
  [FieldOffset(0x04)] UInt32BE File;
}
struct SqpkFileRange {
  [FieldOffset(0x00)] BlockCount Offset;
  [FieldOffset(0x04)] BlockCount Count;
}

Types

Type A - Add Data

[StructLayout(LayoutKind.Explicit)]
struct SqpkAddData
{
    [FieldOffset(0x0)] byte reserved[3];
    [FieldOffset(0x3)] SqpkFile File; // Dat file
    [FieldOffset(0xB)] UInt32BE BlockOffset;
    [FieldOffset(0xF)] UInt32BE BlockCount;
    [FieldOffset(0x13)] UInt32BE BlockDeleteCount;
    [FieldOffset(0x17)] byte Data[BlockCount << 7]; // TODO: Not valid C#
}

This operation writes SqpkAddData.Data to the dat File, at BlockOffset. It will then write BlockDeleteCount blocks of empty (0x00) 128-bytes.

Type D - Delete Data

[StructLayout(LayoutKind.Explicit, Size = 19)]
struct SqpkDeleteData
{
    [FieldOffset(0x0)] byte reserved[3];
    [FieldOffset(0x3)] SqpkFile File; // Dat file
    [FieldOffset(0xB)] UInt32BE BlockOffset;
    [FieldOffset(0xF)] UInt32BE BlockCount;
}

This operation writes BlockCount empty (0x00) 128-byte blocks at BlockOffset, with the first of those blocks containing a BlockHeader struct with:Size = 128, Type = FileSize = UsedBlocks = 0, TotalBlocks = BlockCount

If Delete Data encounters EOF while writing or seeking to BlockOffset, it will fail.

Type E - Expand Data

Expand Data follows the same structure as Delete Data, but the behaviour differs. When Expand Data seeks past the end of file or writes past the end of file, it does not fail, since this is its intended use.

Type F - File Operation

Pseudocode

Uint64 AlignBlockSize(UInt64 size) {
  return (size + 0x8F) & 0xFFFFFF80;
}

[StructLayout(Size = 0x1B)]
struct FileHeader
{
  [FieldOffset(0x00)] byte Operation;
  // padding: [u8; 2]
  [FieldOffset(0x03)] UInt64BE Offset;
  [FieldOffset(0x0B)] UInt64BE Size;
  [FieldOffset(0x13)] UInt32BE FilePathSize;
  [FieldOffset(0x17)] UInt16BE ExpansionId;
  // padding: [u8; 2]
}

[StructLayout(Size = 0x10)]
struct FileBlockHeader
{
  [FieldOffset(0x00)] UInt32LE Size;
  // padding: [u8; 4]
  [FieldOffset(0x08)] UInt32LE CompressedSize;
  [FieldOffset(0x0C)] UInt32LE DecompressedSize;

  bool IsBlockCompressed => CompressedSize != 32000;

  long BlockSize => IsBlockCompressed switch {
    true => CompressedSize,
    false => DecompressedSize,
  };

  long AlignedBlockSize => AlignBlockSize(BlockSize);
}
FileChunk:
+------------+==========+~~~~~~~~~~~~~~~~+
| FileHeader | FilePath | SqpkFileBlocks |
+------------+==========+~~~~~~~~~~~~~~~~+

Notes:

  • FilePath is a null terminated utf-8 string. Length of the string is designated by FileHeader.FilePathSize

SqpkFileBlocks:
+-------------+==============+=======+-------------+==============+
| BlkHeader#0 | BlkPayload#0 | pad#0 | BlkHeader#1 | BlkPayload#1 |
+-------------+==============+=======+-------------+==============+
 <------- BlockSize#0 ------->        <---- BlockSize#1 ---------->
 <------- AlignedBlockSize#0 -------> <---- AlignedBlockSize#1 ----
+=======+~~~~~+-------------+==============+=======+
| pad#1 | ... | BlkHeader#N | BlkPayload#N | pad#N |
+=======+~~~~~+-------------+==============+=======+
         <------- BlockSize#N ------>
 ------>       <-------- AlignedBlockSize#N ------->

Notes:

  • If blkHeader.Compressed is true then the payload is a deflated stream.

  • If blkHeader.Compressed is false then the payload is a raw bytes stream.

  • Yes, AlignedBlockSize is very dodgy. Apparently they are rounded up to next multiple of 128 except they're not.

  • There are no good ways to determine how many blocks are actually encoded in SqpkFileBlocks.

    • Usually the last block won't be compressed but keeping track of payload.remaining() and stop if it's all read will be more robust against a forged file.

void ApplyChunk(FileHeader header)
{
  var filePath = ReadCString(header.FilePathSize);
  var file = OpenFile(filePath);

  file.SeekTo(header.Offset);

  // assuming:
  // chunkCurrent = beginning of blkHeader#0
  // chunkEnd     = end of pad#N
  var (chunkCurrent, chunkEnd) = GetChunkPosition();
  while chunkCurrent < chunkEnd {
    patchFile.SeekTo(chunkCurrent);

    var blockHeader = ReadBlockHeader();
    var payload = ReadBlockPayload(header.BlockSize);

    var stream = blockHeader.IsCompressed switch {
      true => new DeflateStream(payload, CompressionMode.Decompress);
      false => payload,
    };

    stream.CopyTo(file);

    chunkCurrent += blockHeader.AlignedBlockSize;
  }
}

Type H - Header Update

enum FileKind : byte
{
  Dat = b'D',   // id.platform.datN
  Index = b'I', // id.platform.indexN
}

enum HeaderKind : byte
{
  Version = b'V',
  Data = b'D',
  Index = b'I',
}

struct HeaderChunkHeader
{
  [FieldOffset(0x00)] FileKind FileKind;
  [FieldOffset(0x01)] HeaderKind HeaderKind;
  [FieldOffset(0x03)] SqpkFilePath Path;
}
+-------------------+---------------------+
| HeaderChunkHeader | Payload: [u8; 1024] |
+-------------------+---------------------+

Pseudocode

void ApplyChunk(HeaderChunkHeader header, byte payload[1024])
{
  var path = ResolvePath(header.Path, header.FileKind);
  var file = OpenFile(path);

  var writeOffset = header.HeaderKind switch {
    HeaderKind.Version => 0,
    HeaderKind.Data => 1024,
    HeaderKind.Index => 1024,
  };

  file.Write(writeOffset, payload);
}

Type I - Index Update

This is currently no-op.

Type X - Patch Info

This is currently no-op.

Type T - Target Info

struct TargetHeader
{
  // ..platform: ps3, ps4, win32...
}

Packet Structure

Game packet structures

Game packets are composed of 3 distinct parts, the packet header, the segment header and a (sometimes) optional IPC header.

While 'IPC' is incorrect terminology given the usage, SE calls it IPC so the naming has been preserved.

Packet Header

Field
Description

Segment Header

Field
Description

Segment Types

Type
Description

IPC Header

Only present when the parent segment type is set to SEGMENTTYPE_IPC.

Field
Description

Decoding Packets

Decoding packets is reasonably simple and assuming you have a buffer that you write data in to for each connection, it's something like the following:

A lot of detail is omitted for brevity, but it's generally pretty straightforward.

A more comprehensive example of packet parsing can be found in Sapphire:

  • (and some container-level parsing)

struct FFXIVARR_PACKET_HEADER
{
  uint64_t magic[2];
  uint64_t timestamp;
  uint32_t size;
  uint16_t connectionType;
  uint16_t segmentCount;
  uint8_t unknown_20;
  uint8_t isCompressed;
  uint32_t unknown_24;
};

magic

A magic value that identifies a packet. This is FF14ARR if you read it both in its hexadecimal and ASCII representation at the same time.

timestamp

The number of milliseconds since the unix epoch when the packet was sent.

size

The size of the entire packet including its segments and data.

connectionType

The connection type. This will be 1 for zone channel connections and 2 for chat. This is only sent on the initial connection now, previously this was sent with every packet but that is no longer the case.

unknown_20

Alignment, most likely.

isCompressed

Whether the segments + remaining data is compressed. The header is always uncompressed. This data is compressed with zlib and there is no header - default compression settings.

unknown_24

Alignment, most likely.

struct FFXIVARR_PACKET_SEGMENT_HEADER
{
  uint32_t size;
  uint32_t source_actor;
  uint32_t target_actor;
  uint16_t type;
  uint16_t padding;
};

size

The size of this segment and its data (if any).

source_actor

The actor ID of the actor who effectively caused this packet to be sent. For example, if another player casts an action, the source_actor field will contain their actor ID.

target_actor

The actor ID of the actor who is affected by the packet. This isn't used consistently, but the same logical rules apply as source_actor.

type

The type of segment, see below for more detail. Based on the value of this field indicates what kind of data you'd expect to find after the segment header.

paddding

Alignment.

enum FFXIVARR_SEGMENT_TYPE
{
  SEGMENTTYPE_SESSIONINIT = 1,
  SEGMENTTYPE_IPC = 3,
  SEGMENTTYPE_KEEPALIVE = 7,
  //SEGMENTTYPE_RESPONSE = 8,
  SEGMENTTYPE_ENCRYPTIONINIT = 9,
};

SEGMENTTYPE_SESSIONINIT

Used to login to a world or chat server. The packet that has a segment that has a type set to this will contain a correct connectionType set in the packet header. Use this to record what kind of connection it is. Example implementation is in Sapphire.

SEGMENTTYPE_IPC

Used for segments that contain data that should be handled by the packet router for the associated channel. Chat messages, using actions and etc. will always be sent via this segment type and there will be a FFXIVARR_IPC_HEADER immediately following the segment header.

SEGMENTTYPE_KEEPALIVE

TODO: can't remember where this is actually used - lobby? As a note, world (and chat?) use IPCs to ping/pong. Because reasons.

SEGMENTTYPE_ENCRYPTIONINIT

Used to initialize Blowfish for the lobby channel. The client sends a packet to lobby with it's key phrase which is then used to """secure""" a lobby session (spoiler alert: it's not secure).

struct FFXIVARR_IPC_HEADER
{
  uint16_t reserved;
  uint16_t type;
  uint16_t padding;
  uint16_t serverId;
  uint32_t timestamp;
  uint32_t padding1;
};

reserved

type

This will contain the opcode of the packet which identifies which hanadler the packet data following this packet should go.

padding

Potentially data here and not padding but it's probably not that important. Right?

serverId

TODO: write about retail server architecture.

timestamp

A Unix timestamp in seconds since the epoch. Not really sure why this exists here but it does and that's what it has in it.

padding1

Alignment.

if buf.size < sizeof(FFXIVARR_PACKET_HEADER):
    return

header = &buf[0] as FFXIVARR_PACKET_HEADER:

if buf.size < header.size:
    return

data = slice buf from sizeof(FFXIVARR_PACKET_HEADER) to end of buf

if buf.isCompressed:
    data = zlib_inflate(data)

pos = 0
while true:
    segment = &data[pos] as FFXIVARR_PACKET_SEGMENT_HEADER

    if segment.size >= buf.size
        burn them

    if segment.size >= data.size:
        also burn them

    pos = segment.size

    seg_hdr_size = sizeof(FFXIVARR_PACKET_SEGMENT_HEADER)

    if segment.type == SEGMENTTYPE_IPC:
        ipc_size = segment.size - seg_hdr_size
        ipc_data = slice segment from seg_hdr_size to ipc_size

        ipc_hdr = &ipc_data[0] as FFXIVARR_IPC_HEADER
        ipc_hdr_size = sizeof(FFXIVARR_IPC_HEADER)

        packet_data = slice ipc_data from ipc_hdr_size to remaining size
        process_channel_packet(ipc_hdr.type, packet_data)

    // other segment types depend on the type of channel, but it's more of the same
Packet container
Packet parsing

AVFX Files

This page contains the file structure itself. What each of the structures listed here are used for can be on other pages.

AVFX files are organized as a nested series of blocks, with each block following the format:

Name: 4 bytes
Size: 4 bytes
Contents: [Size] bytes

The contents of a block can further contain other blocks. It is also worth noting that the name of a block is reversed from its actual meaning. For example, the "top-level" block for an AVFX file is "XFVA"(AVFX reversed). Names which are fewer than 4 character are also padded out to 4 bytes. Blocks are also padded out to be 4-aligned, so even if Sizeis 3, there will be an extra 00 before the next block starts.

Overview

Note: the names of the blocks are reversed for readability. Also, the notation TmLn[], for example, simply means several sequential TmLn blocks, one directly after the other. There is no "list" structure in AVFX files.

The structure is also abridged for brevity, the full list of parameters can be found at the link below.

AVFX:
    Ver: the AVFX version used?
    
    ...parameters....
    
    ScCn: number of schedulers in this file (always 1)
    TlCn: number of timelines
    EmCn: number of emitters
    PrCn: number of particles
    EfCn: number of effectors
    BdCn: number of binders
    TxCn: number of textures
    MdCn: number of models
    
    Schd: a scheduler block
    TmLn[]: a list of timeline blocks
    Emit[]: a list of emitter blocks
    Ptcl[]: a list of particle blocks
    Efct[]: a list of effector blocks
    Bind[]: a list of binder blocks
    Tex[]: a list of texture blocks
    Modl[]: a list of model blocks

(Full structure)

Scheduler (Schd)

Schd:
    ItCn: item count
    TrCn: trigger count, always 12
    Item[]: a list of items
    Trgr[]: a list of triggers, always 12 Trgr blocks

Items and Triggers

The way Item and Trgr blocks are organized is somewhat unintuitive. Both of them have an identical structure:

Item or Trgr:
    bEna: enabled
    StTm: start time
    TlNo: timeline index

However, each subsequent Item/Trgr will have the data of the previous appended to it. For example:

Item:
    bEna
    StTm
    TlNo

Item:
    bEna : from item #0
    StTm : from item #0
    TlNo : from item #0
    bEna
    StTm
    TlNo

  Item:
    bEna : from item #0
    StTm : from item #0
    TlNo : from item #0
    bEna : from item #1
    StTm : from item #1
    TlNo : from item #1
    bEna
    StTm
    TlNo

For this reason, the easiest way to read a list of Item is to take the last one, and split it into 3-block chunks.

Timeline (TmLn)

TmLn:
    ...parameters...

    TICn: number of items
    CpCn: number of clips

    Item[]: list of item blocks
    Clip[]: list of clip blocks

(Full structure)

Timeline Items

Timeline items have this structure, and are similar to scheduler items in that each item contains the data of the previous ones as well.

Timeline clips

Timeline clips are different in that the data they contain is not organized into blocks, but is rather one continuous 164-byte:

4-byte string, reversed
4 4-byte ints
4 4-byte floats
4 32-byte strings

Emitter (Emit)

Emit:
    SdNm: the path to a sound file (.sdm)

    ...parameters....

    PrCn: number of particles
    EmCn: number of emitters

    ...animation curves...

    ItEm[]: list of emitter items
    ItPr[]: list of particles items

    Data: depends on the emitter type

(Full structure)

The Data block contains information relevant to the emitter's type (specified in the EVT parameter). Depending on the type, the Data block may not exist at all (structures).

Emitter/Particle Items

ItEm and ItPr blocks share the same basic structure, but follow the same pattern as items and triggers within schedulers, where previous items' data is appended to each subsequent one. However, in an emitter, all of the ItEm data is included in the ItPr data. As an example:

ItEm:
    [ItEm data #0]

ItEm:
    [ItEm data #0]
    [ItEm data #1]

ItEm:
    [ItEm data #0]
    [ItEm data #1]
    [ItEm data #2]

ItPr:
    [ItEm data #0]
    [ItEm data #1]
    [ItEm data #2]
    [ItPr data #0]

ItPr:
    [ItEm data #0]
    [ItEm data #1]
    [ItEm data #2]
    [ItPr data #0]
    [ItPr data #1]

Particle (Ptcl)

Ptcl:

    ...parameters...

    UvSN: number of UV Sets

    ...more parameters...

    ...animation curves...

    Smpl: Simple animations (optional)

    UVSet[]: a list of UVSet blocks

    Data: depends on particle type

    TC1: texture color 1
    TC2: texture color 2 (optional)
    TC3: (optional)
    TC4: (optional)
    TN: texture normal
    TR: texture reflection
    TD: texture distortion
    TP: texture palette

(Full structure)

Like with emitters, the contents of the Data block depends on the particle type (parameter PrVT ). Some particle types do not contain a Data block (structures).

  • Simple animation structure

  • UV Set structure

  • Texture color 1 structure

  • Texture color 2, 3, and 4

  • Texture normal

  • Texture reflection

  • Texture distortion

  • Texture palette

Particle Simple Animations

The Smpl block contains 2 unique parameters: Cols and Frms. Cols is 16 bytes, where each 4 bytes represent an rgba color (each byte is one channel). Frms is 8 bytes, where each 2 bytes is an integer.

Effector (Efct)

Efct:
    ...parameters...

    Data

(Full structure)

As with emitters and particles, the Data block (structures) depends on the type of effector, and may not exist.

Binder (Bind)

Bind:
    ...parameters...

    PrpS: Start binder properties
    Prp1: binder properties 1
    Prp2: binder properties 2
    PrpG: Goal binder properties

    Data

(Full structure)

Data is, once again, dependent on the type of binder, and may not exist (structures). Each of the binder properties have this structure.

Texture (Tex)

Texture blocks are simply paths to atex files within the game's internal file structure.

Model (Modl)

The embedded models have 4 possible blocks within them: VNum , VDrw , VEmt, and VIdx . All 4 of the blocks can be in the same model, however VNum and VEmt are always paired, as are VIdx and VDrw .

VIdx

This is a list of 2-byte integers, where each 3 integers represents a triangle in the model. The integers themselves are the indexes of vertices within VDrw .

VDrw

VDrw is a list of vertices, where each vertex is 36 bytes long with the following format:

4 2-byte floats: position
4 1-byte ints: normal
4 1-byte ints: tangent
4 1-byte ints: color
4 2-byte floats: uv1
4 2-byte floats: uv2

VEmt

This is a list of 28-byte vertices, with the following format:

3 4-byte floats: position
3 4-byte floats: normal
4 1-byte ints: color

VNum

This is a list of 2-byte integers, where each integer corresponds to a vertex in VEmt (so the length of elements in VNum always equals the number in VEmt).

Curves

Curves have the following structure:

[Curve Name]:
    ....parameters...

    Keys

(Full structure)

The name of a curve varies, but some examples are X ,RGB , SclA , etc. They are used to animate motion over time. Keys is a single block which contains the information on the shape of the curve. Every 16 bytes in Keys corresponds to a single keyframe, with the following format:

2-byte integer: time
2-byte integer: type
4-byte float: X
4-byte float: Y
4-byte float: Z

SqPack

Everything SqPack: indexes, dat files

SqPack(s) are formed from the following concepts, in which order 'matters':

  1. Data Files

Repositories

Repositories are essentially a collection of categories and there's not much to know here. As of writing, there's 4, one for the base game (ffxiv) and one for each expansion (ex1, ex2, etc.). Consider the following directory structure:

Each folder inside sqpack/ is it's own repository. As an aside, when the game tries to access files, it distinguishes which repository to find a file in by parsing the file path and pulling out 2 segments, the repository name and the category.

For example:

Path
Repository
Category

The first two paths explicitly define their repository in their file path, and it's always the second segment - if it's omitted it defaults to the ffxiv repository. Category is always the first segment and must be present for a path to resolve.

Categories

Categories are just logical separations of game data. The following categories exist:

ID
Name
Notes

Every game path will start with one of the above names and it defines which index to search to find a file.

SqPack Files

Indexes and regular data files are effectively SqPack files and subsequently have a common header, SqPackHeader. It looks like the following:

Indexes (and data) starts at size so you want to seek to the value of size before you read anything out of the file.

Reading Index Data

The index data is located directly after the header and depends on which variant of index file you load. The retail client ships with both variants of index files, which we'll mostly refer to as index and index2 to make the difference obvious. Contrary to the retail client shipping with both index variants, benchmarks only ship with index2 files. The reason is unknown.

Immediately following the SqPackHeader there's a SqPackIndexHeader (which is only present in index files):

The actual SqPackIndexHeader is 0x400bytes large, but for the purposes of this, we're only interested in the first 16 bytes. From the indexDataOffset and indexDataSize, you can determine where to start reading from and how many index elements exist inside an index. indexDataOffset is an absolute offset to where the index data is located, and indexDataSize is the collective size of every IndexHashTableEntry that's in a file. This entry is slightly different in the case of index2 files, so we'll generally cover the two with a focus on index1 files for now.

Reading Index

There's a couple notable differences between the C++ and C# version, so we'll just explain the C++ version and the latter will make sense too.

The hash is a u64 that contains two u32s: the lower bits are the filename CRC32, the higher bits are the folder CRC32.

Generally speaking, calculating a hash works like this:

  1. Convert the path to lowercase

  2. Find the last instance of / and split the string with the last / existing in the first group. The filename needs to have no directory separators

  3. Calculate the CRC32 of both path segments

  4. Join both CRC32s into a u64, eg. directoryHash << 32 | filenameHash

The dataFileId is to identify which file (on disk) contains the file. Larger categories are split across multiple files (each is capped at 2,000,000,000 bytes, or 2 GB), so this is used to distinguish between 020000.win32.dat0 and 020000.win32.dat1 for example, where dataFileId would be 0 and 1 respectively for files located in either dat.

The offset is the absolute number of 8 byte aligned segments that the file is located at within a specific dat file. In a given dat file, a file is located at offset * 0x8 which gives you the absolute offset to start reading a file from.

Reading Index2

The main difference between index and index2 is that the entire path is encoded into one CRC32 hash and does not split the path by folder and filename. Outside of that, everything is identical to index.

sqpack/
    ex1/
    ex2/
    ex3/
    ffxiv/

bg/ex3/01_nvt_n4/twn/n4t1/bgparts/n4t1_a1_chr03.mdl

ex3

bg

music/ex2/bgm_ex2_system_title.scd

ex2

music

chara/weapon/w0501/obj/body/b0018/vfx/texture/uv_cryst_128s.atex

ffxiv

chara

0

common

Contains basic data like fonts, vulgar words dictionary, shader input textures

1

bgcommon

Contains textures, models, environments and collision that are shared between territories

2

bg

Contains layouts, definitions, collision, models and textures for specific territories

3

cut

Contains cutscene animations and definitions

4

chara

Contains models, textures and definition files for all humans/demihumans/monsters

5

shader

Contains compiled shaders

6

ui

Contains UI layouts and textures

7

sound

Contains sound effects

8

vfx

Contains textures and VFX definition files(AVFX)

9

ui_script

No index/dat in retail client, likely leftover from silverlight

A

exd

Contains Excel List, Header and Data files

B

game_script

Contains compiled Lua scripts (5.1) for quests, cutscenes and battles

C

music

Contains BGM

12

sqpack_test

Category missing in retail client/no files.

13

debug

Category missing in retail client/no files.

enum PlatformId : uint8_t
{
    Win32,
    PS3,
    PS4
};

// https://github.com/SapphireServer/Sapphire/blob/develop/deps/datReader/SqPack.cpp#L5
struct SqPackHeader
{
    char magic[0x8];
    PlatformId platformId;
    uint8_t padding0[3];
    uint32_t size;
    uint32_t version;
    uint32_t type;
};
public enum PlatformId : byte
{
    Win32,
    PS3,
    PS4
}

// https://github.com/NotAdam/Lumina/blob/master/Lumina/Data/Structs/SqPackHeader.cs
[StructLayout( LayoutKind.Sequential )]
public unsafe struct SqPackHeader
{
    public fixed byte magic[8];
    public PlatformId platformId;
    public fixed byte __unknown[3];
    public UInt32 size;
    public UInt32 version;
    public UInt32 type;
}
struct SqPackIndexHeader
{
    uint32_t size;
    uint32_t type;
    uint32_t indexDataOffset;
    uint32_t indexDataSize;
};
[StructLayout( LayoutKind.Sequential )]
public unsafe struct SqPackIndexHeader
{
    public UInt32 size;
    public UInt32 version;
    public UInt32 indexDataOffset;
    public UInt32 indexDataSize;
}
struct IndexHashTableEntry
{
    uint64_t hash;
    uint32_t unknown : 1;
    uint32_t dataFileId : 3;
    uint32_t offset : 28;
    uint32_t _padding;
};
[StructLayout( LayoutKind.Sequential )]
public struct IndexHashTableEntry
{
    public UInt64 hash;
    public UInt32 data;
    private UInt32 _padding;

    public byte DataFileId => (byte) ( ( data & 0b1110 ) >> 1 );

    public uint Offset => (uint) ( data & ~0xF ) * 0x08;
}
struct Index2HashTableEntry
{
    uint32_t hash;
    uint32_t unknown : 1;
    uint32_t dataFileId : 3;
    uint32_t offset : 28;
};
[StructLayout( LayoutKind.Sequential )]
public struct Index2HashTableEntry
{
    public UInt32 hash;
    public UInt32 data;

    public byte DataFileId => (byte) ( ( data & 0b1110 ) >> 1 );

    public uint Offset => (uint) ( data & ~0xF ) * 0x08;
}
Repositories
Categories
File Indexes

Excel

Excel is a binary data format for storing relational data. Contains info for EXL, EXH and EXD files.

Excel is a binary data format for storing relational data. It's composed of 3 different files:

  1. Excel List (.exl) is used to provide the excel index for the game, and allows certain sheets to be accessed by an ID rather than a name.

  2. Excel Header (.exh) is used to define the data layout, variant, segmentation and available languages in the event of localisation being present.

  3. Excel Data (.exd) is the raw data itself and just contains an offset table and then data for each row. The structure of this file changes depending on which variant is defined in the header.

Excel List (.exl)

Probably the simplest format in the entire game given it's just a text file.

The first line of the file is its 'header' and defines its type, EXLT and it's version following that. Each sequential line after the 'header' is a path, relative to the exd/ category, and its immutable ID. The immutable ID is optional and in cases where not relevant (eg. quest dialogue files) it's value is -1.

For example, here's an example of the content of a list file:

EXLT,2
Achievement,209
Action,4
content/DeepDungeon2Achievement,-1
content/DeepDungeon2Gacha,-1

From these entries, you can build something like the following:

ID
Relative Path/Sheet Name
Header Path

209

Achievement

exd/achievement.exh

4

Action

exd/action.exh

content/DeepDungeon2Achievement

exd/content/deepdungeon2achievement.exh

content/DeepDungeon2Gacha

exd/content/deepdungeon2gacha.exh

From here, the only thing you can do is parse headers as you will not be able to accurately figure out data paths without first reading the header due to language and row segmentation.

As a note, any excel entry in this file with an ID set get their header pre-cached by the game on startup.

Excel Header (.exh)

The Excel Header defines the schema and how you read data files. The first structure in this file is it's header which contains information you'll need to parse the rest of the file.

This file in its entirely is in big endian. You will need to convert this file to little endian on applicable systems.

For example, in C#, BitConverter.IsLittleEndian will return whether or not a conversion needs to take place. See the Lumina source code for this file for a working example.

struct ExhHeader
{
  char magic[0x4];
  uint16_t unknown;
  uint16_t dataOffset;
  uint16_t columnCount;
  uint16_t pageCount;
  uint16_t languageCount;
  uint16_t unknown1;
  uint8_t u2;
  uint8_t variant;
  uint16_t u3;
  uint32_t rowCount;
  uint32_t u4[2];
};
[StructLayout( LayoutKind.Sequential )]
public unsafe struct ExcelHeaderHeader
{
    public fixed byte Magic[4];
    // todo: not sure? maybe?
    public ushort Version;
    public ushort DataOffset;
    public ushort ColumnCount;
    public ushort PageCount;
    public ushort LanguageCount;
    private ushort _unknown1;
    private byte _unknown2;
    public ExcelVariant Variant;
    private ushort _unknown3;
    public uint RowCount;
    private fixed uint U4[2];
}

Magic

The magic is always EXHF. If it's not, the file is probably not the file you're trying to read.

Data Offset

DataOffset isn't relevant to this file at all, but is required when loading certain data from Excel Data files such as strings. It points to the end of the fixed size data of a row. For example, a row can be made of a bunch of integral types which have a known length at compile time, however a string length is variable. This offset allows you to then seek to the end of the row and access any additional data that may be on the end. If this is weird, don't worry, because it'll make sense later.

Column Count

ColumnCount is how many columns exist within the file and is the first thing you'll read after the header. In other words, you'll need to read sizeof( ColumnDefinition ) * ColumnCount immediately after the header.

enum ExcelColumnDataType : uint16_t
{
    String = 0x0,
    Bool = 0x1,
    Int8 = 0x2,
    UInt8 = 0x3,
    Int16 = 0x4,
    UInt16 = 0x5,
    Int32 = 0x6,
    UInt32 = 0x7,
    Float32 = 0x9,
    Int64 = 0xA,
    UInt64 = 0xB,

    // 0 is read like data & 1, 1 is like data & 2, 2 = data & 4, etc...
    PackedBool0 = 0x19,
    PackedBool1 = 0x1A,
    PackedBool2 = 0x1B,
    PackedBool3 = 0x1C,
    PackedBool4 = 0x1D,
    PackedBool5 = 0x1E,
    PackedBool6 = 0x1F,
    PackedBool7 = 0x20,
};
struct ExcelColumnDefinition
{
    ExcelColumnDataType type;
    uint16_t offset;
};
[StructLayout( LayoutKind.Sequential )]
public struct ExcelColumnDefinition
{
    public ExcelColumnDataType Type;
    public ushort Offset;
}

Offset is relative to the row offset, which you don't have until you read the offset table from a data file.

Type is the type of data stored. Most don't have any special handling outside of String and PackedBoolX but we'll cover that later.

Page Count

PageCount is the number of 'pages' an excel data sheet is split into. Many sheets only have a single page and every row will exist in a single page, however larger sheets such as Quest or Item have many pages. This information is another structure and immediately follows the column definition data.

struct ExcelDataPagination
{
    uint32_t startId;
    uint32_t rowCount;
}
[StructLayout( LayoutKind.Sequential )]
public struct ExcelDataPagination
{
    public uint StartId;
    public uint RowCount;
}

startId is the row id where a page starts. You'll need this to build file paths for data files.

rowCount is how many parent rows a sheet contains. As a quick example before we cover it more in depth later is that given a row id, you can calculate which page a row is on with row >= startId && row < startId + rowCount - 1.

Language Count

Finally, the last thing you'll need to read out of a header file is the languages. These are needed to generate paths to data files along with the paging information.

enum Language : uint16_t
{
    None,
    // ja
    Japanese,
    // en
    English,
    // de
    German,
    // fr
    French,
    // chs
    ChineseSimplified,
    // cht
    ChineseTraditional,
    // ko
    Korean
}

You can read out the languages as is.

Variant

enum ExcelVariant : uint8_t
{
    Unknown,
    Default,
    SubRows
}

Used when reading data. Default requires no extra processing and you can just iterate over the offset list inside a data file. SubRows makes each row contain it's own rows. As a better example, it's like having a compound key on a database table. Instead of one column being the identifier for a row, you have two instead.

Row Count

This is the total count of all rows across every page. The game uses this field when it internally queries for a row count.

Generating Data File Paths

Once you have the 3 pieces of critical information from the headers, namely:

  1. The column count

  2. The page count

  3. The languages

You have everything you need to find data files! How exciting. The files follow one of 2 path formats, which makes this pretty easy. Make sure not to discard the data from the header, there's still some other information you'll need from it.

If the no language is set, your paths follow the following format:

exd/<name>_<page.startId>.exd

Otherwise, if one or many languages are set (as in, not None), the following format applies:

exd/<name>_<page.startId>_<languageCode>.exd

This should be relatively obvious, but here's a few examples:

Name
Page Start ID
Language
Path

Item

0

English (en)

exd/item_0_en.exd

Item

10000

Japanese (ja)

exd/item_10000_ja.exd

Mount

0

French (fr)

exd/mount_0_fr.exd

Quest

65536

German (de)

exd/quest_65536_de.exd

The above also applies for quest sheets and so on which exist in subfolders.

Excel Data (.exd)

The data file contains a single page of sheet data and as mentioned before, you will need the header file to read data correctly.

Similarly to Excel Headers, excel data files are entirely in big endian and will need to be converted to little endian on applicable systems.

struct ExcelDataHeader
{
  char magic[0x4];
  uint16_t version;
  uint16_t unknown1;
  uint32_t indexSize;
  uint32_t unknown2[5];
};
[StructLayout( LayoutKind.Sequential )]
public unsafe struct ExcelDataHeader
{
    public fixed byte Magic[4];
    public UInt16 Version;
    private UInt16 U1;
    public UInt32 IndexSize;
    private fixed UInt16 U2[10];
}

The value of magic should always be EXDF.

indexSize is how large the row offset index is, in terms of total size. To convert that to a number of entries, you'd do indexSize / sizeof( ExcelDataOffset ).

Data Offset Entries

Immediately following the ExcelDataHeader, the root row offsets are stored. The reason it's called the 'root row offset' is because on variant 2 sheets (or sheets with subrows), this offset won't point to data that can be read following the column data.

struct ExcelDataOffset
{
    uint32_t rowId;
    uint32_t offset;
};
[StructLayout( LayoutKind.Sequential )]
public struct ExcelDataOffset
{
    public UInt32 RowId;
    public UInt32 Offset;
}

rowId is the absolute row id, so a simple way to map these in whatever you're doing is to convert the list of data offsets in this file to a key value map, where the key is the rowId and the value is the offset. Then you can directly index rowIds on any given data page.

offset is the absolute offset to where the data is located in the file. It can be used as is.

Row Header

Once you seek to a row offset by following the offset list after the header, the first thing you'll encounter is the row header.

struct ExcelDataRowHeader
{
    uint32_t dataSize;
    uint16_t rowCount;
};
[StructLayout(LayoutKind.Sequential)]
public struct ExcelDataRowHeader
{
    public uint DataSize;
    public ushort RowCount;
}

The dataSize is the entire size of the row, including any data for subrows (if they exist). You can use this field to copy out the exact amount of data for a row (or subrows) to then later parse if you choose not to do it in place.

rowCount is always 1 on variant 1 sheets and you can ignore that field entirely if you choose to. However, on variant 2 sheets, the rowCount is how many subrows belong to a row.

Therefore, on variant 1 sheets, immediately after the row header is your row data. You can then read columns directly out of the data given a column offset that you read from the header.

On the other hand, variant 2 sheets, following the header is the first subrow, or subrow 0. You can calculate the offset to n subrow with the following:

rowOffset + 6 + ( subRowId * header.dataOffset + 2 * ( subRowId + 1 ) );

To clear up a few things:

  1. The rowOffset is the offset from the offset list.

  2. rowOffset + 6 is the skipping the size of the row header. You could just store the offset once you read the row header and use that.

  3. header.dataOffset points to the end of the raw data of the row, or is basically the 'fixed' size of a row, not including strings. This lets you seek to the end of a row and immediately go to the next subrow.

There are some intricacies to reading certain types of columns which we'll cover in the next section.

Reading Row Data

Reading data out of a row depends a bit on the type of the column that you're reading. Most don't require any additional logic and you can read them out of the row and use them as is.

All of the types are as follows:

enum ExcelColumnDataType : uint16_t
{
    String = 0x0,
    Bool = 0x1,
    Int8 = 0x2,
    UInt8 = 0x3,
    Int16 = 0x4,
    UInt16 = 0x5,
    Int32 = 0x6,
    UInt32 = 0x7,
    Float32 = 0x9,
    Int64 = 0xA,
    UInt64 = 0xB,

    // 0 is read like data & 1, 1 is like data & 2, 2 = data & 4, etc...
    PackedBool0 = 0x19,
    PackedBool1 = 0x1A,
    PackedBool2 = 0x1B,
    PackedBool3 = 0x1C,
    PackedBool4 = 0x1D,
    PackedBool5 = 0x1E,
    PackedBool6 = 0x1F,
    PackedBool7 = 0x20,
};

String

As mentioned previously, variable length data is stored past the end of the fixed length data segment of a row. So to access a string, you need to read a uint32 where there string column is. You can then obtain the offset of the string by doing the following:

rowOffset + header.dataOffset + columnValue

Where rowOffset is the offset of the first column of a row, the dataOffset from the header and the uint32 that you read out from the column.

Packed Bools

Packed bools are always 8 bits/1 byte long. To each type is basically which bit you need to bitwise and against to read the correct bool out.

A simple way of doing this for all of them is something like the following:

var shift = (int)column.type - (int)ExcelColumnDataType.PackedBool0;
var bit = 1 << shift;

return (data & bit) == bit;

Alternatively, you can handle each one indivdually, but it's better to be lazy and do it the lazy way instead.

Everything Else

Read and use it as is. It just works™ - no magic required. Just make sure the endianness is correct.