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

Format Implementation Reference

Every file format KotOR uses, what the engine does with it, and how much of that Rakata implements.

Start here if you are looking for a format. The tables below link every page and say how far each is implemented. If you are reading a page and want to know how far to trust it, or what a convention means, that is further down.

A format page runs in a fixed order: At a Glance for the extension and magic, File Layout for the on-disk structure, Field Schema where the format is GFF-backed, Engine Audits & Decompilation for what the loader does, and Implemented Linter Rules. Knowing the order is the fastest way to find one fact on an unfamiliar page.

On the GFF-backed pages the full generated field table sits at the very end rather than inline, because it runs to hundreds of rows on the larger formats and everything worth reading would otherwise sit behind it.

Status Legend:

  • Full: Binary reader/writer implemented with roundtrip tests.
  • Generics: Strongly-typed wrappers and linting schemas implemented.
  • Documented: Engine-audited spec page exists; not yet wrapped in rakata-generics or given a standalone reader/writer.
  • Read-only: Reader implemented; the writer cannot yet emit a form the engine reads.
  • Reference: Structurally covered by another format’s page; not separately modelled.
  • Deferred: Currently unimplemented, with no strongly-typed wrapper yet.

Archive Formats

FormatStatusNotes
BIFFullSupports variable/fixed tables. BZF compression feature-gated. Note our writer aligns payloads to 4 bytes where all 26 vanilla archives pack contiguously.
KEYFullFirst-match lookup semantics (native verified). Duplicate key insertions ignored.
ERFFullSupports ERF/MOD/SAV. Optional blank-block emission for MODs is explicit opt-in.
RIMFullSupports V1.0. Offset fallback handled. Note our writer packs tightly where vanilla archives pad; see the page.

GFF & Blueprints

FormatStatusNotes
GFF StructureFullCore binary parity for structs/lists/fields. Localized strings supported. Stable list ordering.
GenericsGenerics13 typed blueprints completed: ARE, DLG, GIT, IFO, UTC, UTD, UTE, UTI, UTM, UTP, UTS, UTT, UTW. Tied into rakata-lint.
FACDocumentedFaction & reputation table. Engine-audited spec page; not yet wrapped in rakata-generics.
GUIDocumentedInterface panel layouts. A file restyles a panel the binary already knows about; it cannot describe a new one.
JRLDocumentedThe quest journal. Only global.jrl is ever opened, so a module’s own journal is dead content.
PTHDocumentedArea path networks. Two-dimensional, with the walkmesh supplying height.
BICReferenceAurora player-character record: a header (Mod_CommntyName, Mod_IsPrimaryPlr, ObjectId) around a UTC creature snapshot, structurally one IFO Mod_PlayerList entry. Used by character generation and transport; on disk only as the gated Player.bic, which normal K1 saves never produce. Reference-only, not separately modelled.

The save-only GFFs (NFO savenfo, PT partytable, GVT globalvars) each have a page under Save Games rather than here; they exist only inside a save folder and are handled by rakata-save. FAC keeps its own page because repute.fac is a real module and save resource. BIC gets only the reference row above: it is structurally an IFO/UTC record with no routine on-disk presence to model, and its gated Player.bic save behaviour is covered in the Save Game Deep Dive.

3D Models & Walkmeshes

FormatStatusNotes
MDL/MDXFullBinary reader/writer with full geometry, node hierarchy, controllers, and MDX vertex data. ASCII reader/writer for modder interop. In-game verified.
BWM / WOKFullV1.0 binary tables (vertices, faces, materials, etc.). Strict bounds validation.

Texture Formats

FormatStatusNotes
TPCFullContainer header/payload/footer. Three pixel types: DXT1, DXT5 and an uncompressed one-byte-per-pixel form. Mip payload sizing matches the engine’s unclamped right-shift, verified by tiling against the shipped textures.
DDSRead-onlyReads both the standard D3D header and the K1 CResDDS 20-byte prefix. Writes only the standard form, which the engine does not read, so Rakata cannot currently emit a .dds the game loads. Deliberate: emitting the prefixed variant means reproducing a container no other tool reads.
TGAFullReader normalizes to RGBA8888. Lossless passthrough when source pixels are unmodified.
TXIFullASCII format. Case-insensitive command tokens (native verified). Coordinate block support.

Text & Data Formats

FormatStatusNotes
2DAFullBinary V2.b.
TLKFullStrict language-aware decode/encode. Validated against test.tlk.
VISFullASCII format. Case-insensitive room normalization. Deterministic ordering.
LYTFullASCII format. Strict Windows-1252 text handling. Count-driven parsing.
LTRFullV1.0 headers. 28-char probability tables.

Audio Formats

FormatStatusNotes
WAVFullStandard RIFF + KotOR SFX/VO obfuscation wrappers. MP3-in-WAV unwrapping support.
LIPFullV1.0 header + keyframes. Deterministic writer.
SSFFullV1.1 header + 28-slot sound table.

Missing / Deferred Formats

No page and no dedicated reader. The two GFF-backed ones parse as a generic GFF tree today; what is missing is the type schema saying what the labels mean.

FormatStatusNotes
NCS / NSSDeferredNWScript Source and Compiled bytecode. NCS decompilation is slated for future work via an independent pipeline.
ITPDeferredItem Palette definitions (GFF).
BIKDeferredBink Video container (proprietary video format). Unlikely to be implemented natively.

Resource Type Codes

Every archive in the game identifies its contents by a numeric type code rather than by a filename extension. KEY and ERF store it as a u16; RIM and BIF give the field four bytes and use the low two. The table is the same everywhere, and nothing but this table connects a code to a format.

Each code is marked against the four archive families that have been read end to end. means the type was found there, that it was not.

ColumnCovers
KEYThe BIFs indexed by chitin.key
RIMThe module archives under modules/ and rims/
SaveA corpus of save folders, including the per-module ERFs nested inside them
TexThe four ERF archives under TexturePacks/

TexturePacks/ is not indexed by chitin.key. An enumeration that starts from the KEY file walks past the game’s entire texture library with no hint that it exists, so the two codes that live there, 3007 and 2022, read as attested nowhere until those archives are opened directly.

CodeExtKEYRIMSaveTex
0(generic)
3tga
4wav
2002mdl
2009nss
2010ncs
2011mod
2012are
2014ifo
2015bic
2016wok
20172da
2022txi
2023git
2024bti
2025uti
2026btc
2027utc
2029dlg
2030itp
2032utt
2033dds
2035uts
2036ltr
2037gff
2038fac
2040ute
2042utd
2044utp
2047gui
2051utm
2052dwk
2053pwk
2056jrl
2057sav
2058utw
2060ssf
3000lyt
3001vis
3002rim
3003pth
3004lip
3005bwm
3007tpc
3008mdx
9997erf
9998bif
9999key
26000bzf

A dash means these four families were opened and the type was not in them, not that it never occurs. For mod, erf, bif, key, rim and bzf that is expected: those name containers rather than things stored inside one.

Six codes were confirmed by opening a sample rather than by convention. 2026, 2030 and 2047 carry the GFF fourccs BTC , ITP and GUI in their own first four bytes, all at version V3.2; 2009 is NWScript source text; 2052 and 2053 carry walkmesh magic.

are, ifo and git occur exactly once per module in the RIM archives. ncs outnumbers everything else there by a wide margin.

Note

A nine-byte ASCII read of any GFF yields <fourcc>V3.28, and the 8 is not part of the version The header is 56 bytes, so struct_offset holds 0x38, which is ASCII 8. Read eight bytes and you get the fourcc and V3.2; read nine and the low byte of the next field joins the string. There is no V3.28.

Note

Rakata names 34 of these and carries the rest as raw numbers. ResourceTypeCode is a transparent wrapper over the u16, so a code the crate has no name for is read, stored and written back unchanged — an unknown type is never a parse failure. What it loses is the extension: extracting a 2052 resource yields the right bytes with no .dwk on the end. Worth knowing if you are enumerating an archive rather than fetching a known resource.


Conventions that apply everywhere

These hold for every page in this section, so no page repeats them. The first three are facts about the formats themselves. The last two are about how far to trust a claim on any page.

Important

Every multi-byte integer and float in every format here is little-endian, and no page repeats it. KotOR shipped on x86 and the engine reads its structures without byte-swapping anywhere, so the on-disk representation is the platform’s. Take this as read for every offset table in this manual: a u32 at 0x08 is four bytes low-order first. The only exceptions are called out where they occur, and there are currently none in the KotOR-native formats. The notes elsewhere about endianness concern an embedded LZMA sub-header in BZF and the MDL float ABI, neither of which is a departure from this rule.

“The engine ignores this” is not “you may leave it out”

These are different questions, and knowing the answer to one tells you nothing about the other. A field the engine never reads still has a right answer for a writer, and it is not always “anything”.

Four cases occur, and which one applies is a per-field fact:

CaseWhat a writer should doExample
Fixed-position and unreadWrite the canonical value. Omission is not available at all, since the byte exists whatever you put in it.TGA’s id_len, image_type and image_descriptor
Reserved region, unreadZero it.RIM’s 96-byte dead zone
Unread by the traced loader, still requiredWrite it correctly. Something outside the traced path consumes it.ERF’s keys_offset
Recomputed at loadWrite it correctly anyway. The engine will not care; other tools will.MDL’s derived mesh fields

A fifth case is the genuinely free one: a field present in vanilla, read by nothing, and carrying no positional obligation, so a writer may drop it and produce a file the engine treats identically. Fields the engine never reads is where those live, and it says what dropping them costs.

The row that catches people is the first. “The engine does not read this byte” and “any value passes” are the same statement, and “you may write anything” and “vanilla writes one specific value” are both true at once. Which one matters depends on whether you are aiming for a file the engine loads or a file that matches what shipped.

Counted, terminated, or neither

Every repeated run in these formats is delimited one of three ways. Which one a format uses is on its own page, because guessing wrong is not a graceful failure.

  • Counted. A field earlier in the file says how many follow. Most of the binary formats, and LYT among the text ones.
  • Terminated. A marker ends the run. TXI’s coordinate lists use endlist.
  • Both, per instance. TXI again: a list takes either form, and a reader implementing only the counted one will consume the rest of the file as coordinates the first time it meets a terminated list.
  • Length-delimited. A byte count rather than an element count, which is not the same thing. TLK’s text blob, and every variable-length GFF value.
  • Neither. Extent comes from the file or section boundary. VIS.

The failure mode is asymmetric. Assuming counted where the file is terminated over-reads, usually to end of file. Assuming terminated where the file is counted stops early and silently drops data.

Naming a population

When a page says a field is always something, or never anything, it names the archives that were opened: “every GFF in chitin.key”, “the module RIM archives”, “the ERFs inside a save”.

Category words like vanilla, static, shipped and the corpus look like scope and are not. A category includes archives nobody opened, so a claim worded that way is wider than its evidence.

An absence therefore carries its scope: “not present in the archives we read” and “does not occur” are different claims, and only the first is established by reading files. Populations are given in words rather than as a count, because how many files matched depends on how they were sieved.

A number that is itself the finding stays. The rule is about denominators, not digits. That .pth files outnumber the modules holding them is how a reader learns the format is per-area rather than per-module, and a per-label prevalence column is most of what tells a reader which GUI fields are optional. Neither survives being reworded into “many”.

Provenance

Evidence here is not all of one strength, so every Engine Audits section names its source and its provenance level:

LevelMeans
tracedRead instruction by instruction, in the named function. The strongest thing this manual offers.
measuredChecked against real files, with the archives named.
inferredConcluded by comparing decompilations or reasoning across functions, rather than read off one.
paraphraseGeneral knowledge about the format or the compression, not something this binary was observed doing.
derived, not attestedTaken from a decompilation of the named function, with the individual rows never separately re-derived.

“Derived, not attested” describes most audit tables here, and those rows are the reverse-engineering queue. The level is not a formality: both tables that have since been checked against something other than the decompilation behind them lost rows. The walkmesh table put vertex_count at +0x08 and gave +0x48 to +0x6C to AABB trees; files refute both. Re-auditing DDS found a citation naming the wrong function, an encoding claim spanning two different domains, and a formula that appears nowhere in the binary.

Provenance Policy

Because this project seeks to achieve strict interoperability with a two-decade-old engine, mere “correctness” is insufficient. We guarantee canonical behavior.

  • Target: Canonical vanilla Star Wars: Knights of the Old Republic 1 (2003).
  • Engine Audits: We do not guess how the engine behaves. Code is written exclusively from observed engine evidence notes derived from clean-room reverse engineering (via Ghidra/ret-sync). Every implementation choice is documented directly inside that format’s specific page on this site.
  • Verification: Behaviors are locked via deep integration tests against synthetic fixtures. If a parser perfectly round-trips an invalid file but the game engine rejects it, it is treated as a critical bug.