BIF (Binary Information File)
BIFs are essentially giant, uncompressed data silos. Because they act as the raw storage tier of the KOTOR engine, they don’t waste bytes on complex metadata or internal filenames – they are simply pure, tightly packed continuous byte arrays for game resources. They are designed to be randomly accessed extremely quickly at runtime strictly via their companion KEY index file.
At a Glance
| Property | Value |
|---|---|
| Extension(s) | .bif, .bzf (compressed; mobile ports only) |
| Magic Signatures | BIFF (version V1 ) for both |
| Type | Archive Blob Payload |
| Rust Reference | View rakata_formats::Bif in Rustdocs |
Data Model Structure
The rakata-formats crate handles raw Bif parsing for you by reading the internal offset tables. However, developers very rarely interact with a raw Bif file on its own.
- Unified Access: Typically, you’ll use the
KeyFileAPI (rakata_extract::keyfile::KeyFile), which automatically ties.keyindex files to their.bifdata payloads so you don’t have to map them yourself. - Seek Performance: To prevent loading 100MB+ binary files completely into memory just to read a tiny script, Rakata parses the archive’s entry table once, caches it, then jumps straight to the coordinate of the single resource you asked for.
models.bifalone is nearly a gigabyte, so this is the difference between a lookup costing a hash probe and it costing the whole file.
The Compressed Variant (.bzf)
Compression did not exist in the original 2003 PC release. Aspyr added it for the modern mobile ports to save storage, and it is the one place the BIF format gets genuinely confusing, because nothing inside the file says it is compressed.
| Property | Uncompressed | Compressed |
|---|---|---|
| Signature | BIFF / V1 | BIFF / V1 (identical) |
| Header and tables | as documented above | byte-for-byte the same shape |
data_size in the table | the resource’s length | the uncompressed length |
| How the KEY names it | data\2da.bif | data\2da.bif (still .bif) |
| On-disk filename | 2da.bif | 2da.bzf |
The extension is the only discriminator that exists in shipping data. Everything else matches. That has a direct consequence for tooling: compression has to be stated, not sniffed. Rakata reads it from the path when opening a file, and requires it as an argument when reading from a window that has no filename attached.
Payload Layout
Each resource is stored as its own LZMA-alone stream:
0x00 properties byte packs (pb * 5 + lp) * 9 + lc
0x01..0x05 dictionary size u32, little endian
0x05.. compressed data terminated by an end-of-stream marker
There is no length field in the stream, because the uncompressed length already lives in the entry table. And there is no packed length recorded anywhere: an entry’s compressed extent runs from its offset to wherever the next entry begins, with the last one running to the end of the file. Trailing alignment inside that span is harmless, since the decoder stops at the end-of-stream marker.
Note
Ground truth and its limits. The facts above are read from a shipping Android bundle (
com.aspyr.swkotor), whose 26 compressed archives every one carry theBIFFsignature. Rakata decodes all 26, each entry to its exact declared length. iOS and Switch are unverified and simply assumed to match.Two things worth flagging as inference rather than fact. Community references describe a
BZFsignature; no file in the bundle uses it, so Rakata does not look for one. And the engine presumably swaps the extension when resolving a KEY entry on mobile, since the KEY says.bifand the disk says.bzf, but that path has not been traced in a mobile binary. The Ghidra project carries the Android builds, so it is auditable whenever someone wants to.
Engine Audits & Decompilation
The following documents the engine’s exact load sequence and field requirements for .bif archive headers mapped from swkotor.exe.
(Decompilation logic for this section was entirely audited and verified via native Ghidra pipeline against swkotor.exe, explicitly pulling from CExoResFile::LoadHeader (0x0040d910) and CExoResFile::ReadResource (0x0040da20).)
Archive Initialization (CExoResFile::LoadHeader)
Mapped from 0x0040d910.
| Pipeline Event | Engine Behavior & Result |
|---|---|
| Signature Check | The engine strictly validates both the BIFF magic and the exact V1 version. It does not actively process any files that deviate from this signature pair. |
| Variable Table Loading | The system extracts the variable_count value from the header and physically reads variable_count * 16 bytes from the variable_table_offset to map the resource keys. |
| Fixed Table Bypass | The fixed_count header scalar is entirely decorative. It is not part of the active runtime read path (files with nonzero values are accepted but never mapped). |
| Direct Asset Extraction | When reading a physical asset out of the .bif, the engine isolates the entry_index using (resource_id & 0x3fff) * 0x10. It then calls a direct C fseek(SEEK_SET) strictly matching the raw data_offset extracted from the 16-byte variable table entry. No alignment or structural normalization is applied—the data is dumped entirely blindly. |
Caution
Because the engine passes the internal
data_offsetinteger directly into a raw Cfseek(SEEK_SET), any custom BIF files must meticulously guarantee byte-perfect offset tables. If the offset is even slightly misaligned, the engine will read garbage data into the stream, inevitably crashing the game.