Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

GFF (Generic File Format)

The Generic File Format (GFF) is BioWare’s core binary serialization format, functioning like a binary JSON object or XML tree. It holds arbitrarily nested structures, typed fields, and lists, powering UI layouts, character sheets, dialogues, and area descriptions.

At a Glance

PropertyValue
Extension(s).gff, .utc, .uti, .utp, .ute, .utd, .dlg, .are, .ifo, etc.
Magic SignatureTarget type (e.g. UTC ) / V3.2
TypeGeneric Hierarchical Data
Rust ReferenceView rakata_formats::Gff in Rustdocs

File Layout

A 56-byte header holding an offset-and-count pair for each of six sections, and the sections themselves. The write order below is fixed and every section is located by its own header entry, so a reader never has to assume adjacency. (The audit further down counts seven contiguous sections, which is the same file: it counts the header as the first.)

Header (56 bytes)

OffsetFieldOffsetField
0x00file_type (fourcc)0x04version (fourcc, always V3.2)
0x08struct_offset0x0Cstruct_count
0x10field_offset0x14field_count
0x18label_offset0x1Clabel_count
0x20field_data_offset0x24field_data_count
0x28field_indices_offset0x2Cfield_indices_count
0x30list_indices_offset0x34list_indices_count

The last three counts are byte lengths, not element counts, unlike the first three. Field data is a heterogeneous blob and both index arrays are u32 runs whose element boundaries only make sense once you know which struct or field is pointing into them.

Struct record (12 bytes)

OffsetFieldTypeNotes
0x00struct_idi32Not a free tag. Several formats read it as a discriminator. See below.
0x04data_or_offsetu32Overloaded. See below.
0x08field_countu32How many fields this struct has.

Warning

struct_id carries real meaning in several formats, and a writer that invents one breaks the file silently The container itself does nothing with the value. Per-format readers do, and they read it instead of a field, so there is no label to search for and nothing reports a wrong value.

Two attested cases. A UTC’s Equip_ItemList element carries the equipment slot as its struct id, as a single bit: across every creature in chitin.key and the module RIM archives the ids that occur are 1, 2, 8, 16, 32, 128, 256, 512, 1024, 16384, 32768, 65536 and 131072, and nothing else. A GIT object list uses one fixed id per list, which is how an element knows what kind of object it is.

So a writer copying a struct from one place to another must carry its id, and a writer creating one must know which value the format expects. Emitting -1 everywhere produces a file that parses and loses every equipped item.

A GIT’s Creature List is the case traced end to end. The loader compares each element’s id against the one value it expects and skips the element entirely when it differs, silently, moving straight to the next index. The creature does not load, nothing is defaulted, and nothing fails.

One accessor is the only way to reach an element’s id, CResGFF::GetElementType, and the reader class hands no raw struct pointer outside itself: every method takes and returns a wrapper carrying an index. So a list whose loader never calls that accessor has ids nothing can be reading, and the lists below are settled rather than merely unobserved.

A UTC’s ItemList never reads its element ids, in the same function that reads Equip_ItemList’s. Neither does ClassList, nor FeatList, nor any list in a DLG, nor a GIT’s CameraList. The sequential ids an ItemList carries and the constant every FeatList element carries are whatever a writer happened to put there.

-1 genuinely means nothing where a format does not use the field, which is most structs and every one this page describes generically.

Which record is the root

Struct record 0. Nothing in the header says so, which is the gap: a reader holding the struct array and the field array has no stated place to begin.

Measured rather than assumed, and by the property that defines a root rather than by position: across every UTC and GIT in chitin.key and the module RIM archives, no struct field and no list element ever points at index 0. Every other record is reachable from it and it is reachable from nothing.

Field record (12 bytes)

OffsetFieldType
0x00field_typeu32
0x04label_indexu32
0x08data_or_offsetu32

label_index indexes the label section, which is a flat array of 16-byte NUL-padded names. Labels are shared, so two fields with the same name in different structs point at one entry.

Important

Two fields with the same label in one struct: the first wins and the second is unreachable Lookup is a linear scan across the struct’s fields in stored order, comparing the label bytes and returning the first match (CResGFF::GetFieldByLabel). A duplicate is neither an error nor a replacement. The later field is dead weight nothing ever reads.

Which makes on-disk order load-bearing, because “first” means first in the file and nothing else. A tool that changes a field by appending a fresh copy of the label has written a file where its own edit does not take effect, and no part of the format reports it.

Important

A one-field struct stores its field differently, so growing one to two fields is a rewrite field_count selects between two physical shapes. A struct with exactly one field puts that field’s index straight into its own data_or_offset, with no indirection at all. A struct with two or more puts a byte offset there instead, pointing into the shared field indices array.

Reading this is easy enough, since the count tells you which shape you are looking at. Writing into an existing file is where it bites: adding a field to a struct that already has two or more appends to that shared array and nothing else moves, but adding a second field to a struct that has one has to lift the existing index out of data_or_offset, put both indices into a fresh span of the array, and grow the header’s count to match. Same operation from the caller’s side, two different things underneath.

Field type codes

Eighteen codes, and the first ten are the ordinary scalars. The Storage column is the part a reader has to get right: it says whether data_or_offset holds the value itself or points somewhere, and where.

CodeTypeStorage
0BYTEValue, in the low byte
1CHARValue, signed, in the low byte
2WORDValue, in the low two bytes
3SHORTValue, signed, in the low two bytes
4DWORDValue
5INTValue, signed
6DWORD64Offset into field data; 8 bytes
7INT64Offset into field data; 8 bytes, signed
8FLOATValue, reinterpreted as f32
9DOUBLEOffset into field data; 8 bytes
10CExoStringOffset into field data
11CResRefOffset into field data
12CExoLocStringOffset into field data
13VOIDOffset into field data
14StructA struct-array index, not an offset
15ListByte offset into the list indices array
16Vector4Offset into field data; four f32
17Vector3Offset into field data; three f32
18StrRefEncoding not established. Defined by the format and present in none of the four archive families surveyed below, so nothing attests how it stores its value. See the note under the table.

The four-byte scalars sit in data_or_offset directly rather than being pointed at. Anything wider than four bytes is in the field-data blob, including DWORD64, INT64 and DOUBLE, which is easy to miss because their siblings are immediate.

Warning

Code 18 has no storage rule here Whether StrRef holds its value immediately like the other four-byte scalars, or points into the field-data blob, is not established: it occurs in none of the four archive families, so no file demonstrates either reading. INT64 and DOUBLE are also absent from the static archives but do have rules, taken from the codes around them and from saves; 18 has neither.

A reader that meets one should surface it rather than guess. Picking the wrong branch reads a plausible number out of the wrong region, failing as quietly as the Struct case below.

The complex encodings

Each is written at the offset data_or_offset gives, counted from field_data_offset.

TypeLayout at that offset
CExoStringu32 byte length, then exactly that many bytes. Not NUL-terminated.
CResRefu8 byte length, then that many bytes. The length is one byte, not four, and the value is capped at 16.
CExoLocStringu32 total size of everything after this word, then u32 StrRef, then u32 substring count, then that many substrings. Each substring is a u32 language-and-gender id, a u32 length, and that many bytes. See below for what the id holds.
VOIDu32 byte length, then that many raw bytes.
Vector3Three f32: x, y, z.
Vector4Four f32: w, x, y, z. Scalar first.

Note

Only one language-and-gender id is attested The conventional encoding is language * 2 + gender, 0 being masculine, so an English feminine substring is 1. (Provenance: paraphrase. General knowledge about the format, not something this binary was seen doing.)

Every substring in chitin.key and the module RIM archives carries id 0, across the handful of localised strings holding inline text at all. The multiplier and the gender bit are unattested here, so the formula is worth treating as a convention rather than a measurement.

Language codes themselves, and the one the engine special-cases, are on the TLK page with their codepage mapping.

Important

A Struct field holds an index, not an offset Code 14 puts a direct index into the struct array in data_or_offset. Every other pointer-like field in the format holds a byte offset, so this one reads as an inconsistency and is easy to implement as an offset by analogy with its neighbours.

The two readings do not fail loudly against each other. Struct records are 12 bytes, so index n and byte offset 12n are both plausible numbers landing inside the same section, and a reader using the wrong one produces a file that parses into the wrong tree rather than one that errors.

List is the other indirection. data_or_offset is a byte offset into the list indices array, where a u32 count is followed by that many struct-array indices, one per element.

Important

An empty list is not the same thing as an absent field Three states are distinguishable in the file: the label is absent, present with a count of zero, or present with elements. An empty list is an ordinary field record of type 15 whose data_or_offset points at a u32 zero, with no struct indices after it.

Offset 0 is legal and used, so a reader must not read a zero offset as a missing field. Empty lists are ordinary rather than exceptional across every GFF in chitin.key and the module RIM archives.

A writer that drops a list rather than emitting a zero count has changed the file’s meaning. Four format pages turn on this, and UTC records a case where absent and present-but-empty behave identically on a fresh template and diverge on an object that already holds values.

A CExoLocString in chitin.key is almost always a bare StrRef pointing into the TLK, with no inline text at all: only a handful carry any, and each of those carries exactly one substring, with language-and-gender id 0.

That is a fact about chitin.key and nothing else. Module RIMs and saves were not in it, and a save is where player-entered text would have to live, so whether either exercises the substring loop is unchecked rather than settled. Implement the loop and test it against something other than the base archives.

Important

The substring wins and the StrRef is the fallback, not the other way round Reading one of these, the engine looks for the inline substring matching the running language and gender first, unconditionally, and only consults the StrRef when that lookup comes back empty (CExoLocString::GetString). A field carrying a valid StrRef and a matching substring displays the substring.

The obvious reading is the reverse, because 0xFFFFFFFF in the StrRef looks like a “use the inline text instead” marker. It is not one. The engine never inspects the StrRef’s validity to make this choice, so the invalid value is simply the case where the fallback has nothing to offer either.

The rule lives on the type rather than on any field. Dialogue lines, item property descriptions, map notes and the save’s stat block all read through the same accessor.

Language 0 ignores the gender bit. The packed substring id is language * 2 + gender, but the gender term is only honoured when the language is non-zero. For language 0 the lookup always probes the ungendered slot, whatever gender was asked for. So a gendered language-0 substring is text the engine will never display.

The write side collapses it identically: asked to add a gendered substring under language 0, the engine lands it in the ungendered slot. So the engine’s own code cannot produce two language-0 substrings split by gender, and their absence from any corpus is structural rather than a gap in what has been looked at. A tool writing one is writing something the engine never would. Why the default language is special is not established, and it is not visible from the code either way.

How these were established

(Provenance: measured.) The encodings are derived from the bytes rather than inherited from another project’s header, and checked by tiling: decoding every field with the rules above, the field-data blob closes exactly in every GFF in a retail chitin.key and in a save corpus, with no gap, no overlap, and the last span ending on field_data_count. The six sections likewise tile from byte 56 to end of file in every GFF of both.

A wrong length rule for any variable-length type leaves a hole or an overrun, and a wrong reading of Struct or List puts an index outside its array, so a corpus that tiles with no slack constrains every rule at once. Saves are included because they carry type codes chitin.key does not.

Which codes actually appear, and where

Reported per archive, because each family exercises a different part of the code space. Four have been opened: the GFFs in chitin.key, the module RIM archives under modules/ and rims/, the static ERF archives (lips/ plus patch.erf), and a save corpus.

Codechitin.keyModule RIMsStatic ERFsSaves
0 2 3 4 5 8 10 11 12 14 15 17partial
13 VOID
16 Vector4
1 CHAR, 6 DWORD64
7 INT64, 9 DOUBLE

VOID and Vector4 are not save-only. Both are absent from chitin.key and present in module RIMs, so a reader working from the base BIFs alone will not meet them and a reader working from any module will.

Every Vector4 in either population is a GIT Orientation, which corroborates rather than complicates the audit below: GIT’s camera list really is the format’s only Vector4 call site, and the field is written by the toolset into static GITs as well as by the engine into saved ones. VOID in a module RIM is likewise narrow; in saves it carries the packed value arrays in GLOBALVARS.res and a flag block in PARTYTABLE.res.

CHAR and DWORD64 are the codes that really are save-only: absent from chitin.key, from every module RIM and from the static ERFs, and abundant in saves. INT64 and DOUBLE appear in none of the four, which is an absence with a named scope rather than a reason to think the codes are unreal. A reader must still handle them.

Note

Why Vector4’s component order can be read off the files The w, x, y, z order is documented from the engine below, and saved GITs confirm it independently. Across every Vector4 in the save fixtures the second and third slots are zero and the first and fourth vary, and all of them are unit-length. A camera that only turns on the spot has a non-zero scalar and one non-zero axis term, which under w, x, y, z is exactly the first and fourth slots. Under an x, y, z, w reading the non-zero pair would have to be the third and fourth instead.

The identity value settles it from the other side. One camera carries (1, 0, 0, 0), the identity under w, x, y, z; under the alternative that same value is a half-turn about x, which is not an orientation a resting camera holds.

Text encoding

GFF stores text as bytes and declares no encoding anywhere in the file, so a reader cannot learn it from the container. There is no lookup in the engine either: it hands the bytes to the process’s ANSI codepage, which is Windows-1252 on a western install. The TLK page documents the one place the engine special-cases a language and what follows from that.

In practice the text in chitin.key is almost entirely ASCII. The only byte outside that range anywhere in its strings and resrefs is the plus-or-minus sign, in the Pazaak card names. Treating GFF text as single-byte and decoding it as Windows-1252 reproduces that archive exactly, and nothing in it requires a multi-byte sequence. Module RIMs and saves were not scanned for this, and player-entered text lives in the latter.

The overloaded word

data_or_offset is the mechanic the whole format turns on, and it means different things depending on context rather than carrying a discriminator of its own:

  • On a field whose type fits in four bytes, it is the value. For anything larger, so strings, resrefs, doubles, structs and lists, it is an offset into the field data blob or into the index arrays.
  • On a struct with exactly one field, it is that field’s index directly. With any other count it is a byte offset into the field indices array, where field_count consecutive indices live.

A struct with field_count == 0 has no defined meaning for the word, and no vanilla file provides one: not a single zero-field struct occurs in any GFF in chitin.key or the module RIM archives. The rule above still applies literally, so the word is an offset into the field indices array naming zero consecutive indices, and nothing dereferences it. A reader must not follow it, and a writer creating one has no vanilla precedent to match. UTI describes what the engine does with an empty entry in a PropertiesList, which is a code path a hand-authored file can reach even though no shipped file does.

So the same u32 is variously an immediate value, an offset into one of three different regions, or an index. What it is at any point follows from the field’s type and the struct’s field count, never from anything stored beside it. A reader that guesses wrong here does not fail loudly; it reads a plausible number out of the wrong region.

Engine Audits & Decompilation

Binary: swkotor.exe

Serialization Architecture (WriteGFFFile)

Derived from 0x00413030 / 0x004113d0.

The writer builds the whole file in memory and emits seven sections back to back in a fixed order, with no padding or alignment bytes between them. It records each section’s byte offset into the 56-byte header as it goes. This is the path save games and area extraction both write through.

OrderSectionSize
1Header56 bytes (0x38)
2Struct array12B × struct_count
3Field array12B × field_count
4Label array16B × label_count
5Field data blobVariable
6Field indicesVariable
7List indicesVariable

Warning

The label array holds fixed 16-byte elements, so a label longer than 16 characters is truncated on write.

Note

The GFF version is always V3.2. The header’s version field is written by CResGFF::CreateGFFFile (0x00411260) from a single global value, and the version string a caller passes in is ignored. So the on-disk version never varies, even where the calling code asks for something else (several save-game writers request V2.0, but it never reaches disk). Read the V3.2 you observe; the version is not a per-resource signal.

Note

Field-label lookup is case-sensitive. Every CResGFF::ReadField* wrapper resolves its label through CResGFF::GetFieldByLabel (0x00411630), which copies the requested label into a fixed 16-byte buffer with no case-folding and compares it against each field’s stored label with an inlined byte-for-byte comparison, not a case-insensitive string function. A label that differs from the one the engine’s own code constructs only in capitalization never matches: FortBonus against the engine’s fortbonus is a different string, not a fallback path. This holds for the whole ReadField* family, every scalar and string type, so exact-string field matching in a reader is the behaviourally correct model of this engine, not a shortcut that happens to work on vanilla data.

Important

A Vector4 field’s on-disk component order is w, x, y, z. Slot 0 is w, slot 1 x, slot 2 y, slot 3 z, so the identity quaternion is [1.0, 0.0, 0.0, 0.0] and not [0.0, 0.0, 0.0, 1.0].

CResGFF::ReadFieldQuaternion (0x004121b0) and CResGFF::WriteFieldQuaternion (0x00412ca0) both assign field by field rather than blitting memory, so the order is legible rather than inferred.

Confirmed at the format’s only Vector4-typed call site, GIT’s CameraList[].Orientation, and cross-checked against the engine’s Quaternion struct layout, which is the same struct the MDL format already documents.

Note

Carry-over versus fresh-literal is a property of the read call, not of the field, and it recurs on every format page in this section. Every GFF scalar read takes a “default if absent” argument, and what sits in that argument is the whole distinction:

MechanismThe default argument holds
Carry-overThe field’s already-constructed value on the object, so an absent field leaves it where it was.
Fresh literalAn unrelated literal baked into the call site, stamped over whatever the object held.

Either way the assignment always executes, with no visible branch on presence, which is why the two are indistinguishable from the resulting value alone.

They can diverge sharply. UTC’s LoadCreature fields include five where the read literal disagrees with the constructor outright. Naming which mechanism a field uses is the most repeated question these pages answer, so it is defined once here.


Engine Blueprints: Specialized GFF Containers

The sections above describe raw GFF nodes. The engine also uses GFF as a wrapper for a family of fixed layouts called blueprints: creatures, dialogue trees, placeables, area parameters, and the rest.

Each blueprint page documents how the K1 GOG executable (swkotor.exe) maps that layout into memory through its Load*FromGFF functions, which is what rakata-lint validates against.

How far to trust any given claim is a per-page question, and each page answers it. Every Engine Audits section names the function it was read from and its provenance level, and those levels differ: some rows are traced instruction by instruction, some are measured against real files, and most are derived from one decompilation pass that nothing has separately re-checked.

Reading the field tables

Every blueprint page ends with a field table generated from the schema itself, so no label can be quietly left out. It sits at the end because it runs long, and the page’s prose comes first. Each table splits in two.

What the engine does with each field holds the labels where the engine’s behaviour is established, with what it does and what it holds when a file omits the label.

Fields nobody has examined holds the rest. That is a gap, not a finding: nobody has established whether the engine reads them, which is not the same as establishing that it does not. Many still have an answer under When absent, because the two questions were settled separately.

A bracketed number sends you to the finding under the table. The same number on many rows means one passage settles all of them, which is worth seeing before you rely on any single row.

Note

The typed blueprint structs documented below (Utc, Uti, Are, Git, Dlg, Ifo, Utd, Ute, Utm, Utp, Uts, Utt, Utw) are projections over raw GFF, not replacements. Each from_gff extracts only the documented fields and silently drops anything else; to_gff writes only those documented fields. The raw Gff tree stays alongside the typed view for callers that need byte-exact fidelity. See Typed Views and Raw GFF in the architecture guide for the full rationale and the choose-which-layer guidance.

The Blueprint Engine Audits

Each audit in this section’s navigation bar records the fields a format declares, how the engine loads them, and the rules it enforces beyond what the container’s own structure guarantees.

A field can be structurally valid GFF and still break the engine. rakata-lint flags those cases, and these audits are what it validates against.

ExtTypeCore Function
.areArea Static BlueprintAn area’s static properties: weather, day/night limits, and physics constraints.
.dlgDialogueOne conversation: the branching graph of lines and replies, with the cinematic actions attached to them.
.gitGame Instance TemplateWhere everything in an area is: placement, orientation, and which template each object spawns from.
.ifoModule InfoModule-level metadata: the entry point, the module’s scripts, and its spawn state.
.utcCreatureA creature: every NPC, enemy, droid and companion, with its stats and appearance.
.utdDoorA door: the transition it opens onto, plus its lock and trap state.
.uteEncounterAn encounter: the boundary and spawn points deciding what appears, and when.
.utiItemAn item: every weapon, suit of armour, medpac and upgrade, with its properties.
.utmStoreA merchant’s store: the inventory it offers and its buy and sell markup.
.utpPlaceableA placeable: containers, scenery, and traps the player can interact with.
.utsSoundA positional sound emitter: its clips, and how volume falls off with distance.
.uttTriggerA trigger: an invisible polygon that fires scripts when something enters it.
.utwWaypointA waypoint: a named position used for navigation and area transitions.