KEY (Global Index)
Think of the KEY file as the absolute master table of contents governing the entire game directory. Because uncompressed BIF archives are completely blind payloads that contain no internal filenames, the KEY file acts as the singular, authoritative index that tells the engine exactly which BIF holds which file, and precisely where to seek inside that BIF to find it.
At a Glance
| Property | Value |
|---|---|
| Extension(s) | .key |
| Magic Signatures | KEY (version V1 ) |
| Type | Archive Global Index |
| Rust Reference | View rakata_formats::Key in Rustdocs |
File Layout
Four blocks, and only three of them are found the usual way. The header sits at 0x00, the BIF file table and the key table are each located by their own header offset, and the BIF filenames are not a located block at all: every file entry carries its own offset and length pointing into the filename region, so the strings are addressed individually rather than as a table.
| Block | Size | Located by |
|---|---|---|
| Header | 64 bytes | Always at 0x00 |
| BIF file table | 12 bytes per BIF | file_table_offset |
| BIF filenames | variable | Each file entry’s own filename_offset and filename_size |
| Key table | 22 bytes per key | key_table_offset |
Header (64 bytes)
| Offset | Field | Type | Notes |
|---|---|---|---|
0x00 | magic | fourcc | KEY , trailing space included. |
0x04 | version | fourcc | V1 , with two trailing spaces. See the version note below. |
0x08 | bif_count | u32 | |
0x0C | key_count | u32 | |
0x10 | file_table_offset | u32 | |
0x14 | key_table_offset | u32 | |
0x18 | build_year | u32 | Years since 1900. |
0x1C | build_day | u32 | Day of the year. |
0x20 | reserved | 32 bytes | Preserved verbatim on round-trip. |
BIF File Entry (12 bytes each)
| Offset | Field | Type | Notes |
|---|---|---|---|
0x00 | file_size | u32 | Size of the BIF this entry names. |
0x04 | filename_offset | u32 | Absolute, from the start of the KEY. |
0x08 | filename_size | u16 | Includes the terminating NUL: the count is the string length plus one, in every entry of a retail chitin.key. |
0x0A | drives | u16 | A bitfield naming which install medium holds the BIF. Nothing in the audits below traces what swkotor.exe does with it, so treat it as unexamined rather than inert. |
Key Entry (22 bytes each)
| Offset | Field | Type |
|---|---|---|
0x00 | resref | char[16], null-padded |
0x10 | type_id | u16 (see the resource type codes) |
0x12 | resource_id | u32 |
The resource_id bitmask
This is the whole point of the format, and it is what the engine’s payload mapping below is tearing apart. A resource_id is not an index into anything on its own. It is two numbers packed into one word:
| Bits | Meaning |
|---|---|
31..20 | Index into the BIF file table |
19..0 | Index of the resource within that BIF |
So resolving a resource is: find the key entry by resref and type, split its resource_id, use the high twelve bits to pick a BIF from the file table, and the low twenty bits to pick an entry inside it.
Warning
The format allocates twenty bits to the index and the loader reads fourteen, so a BIF holds 16,384 resources at most Both halves are confirmed and they do not conflict. The twenty-bit split above is what the field carries: across every key entry in a full install, the low twenty bits equal the resource’s index within its BIF and the high twelve equal that BIF’s position in the file table, without a single exception. The loader is narrower. The BIF audit records
CExoResFile::ReadResourcemasking with0x3fffbefore scaling by the 16-byte entry stride, which is fourteen bits, and nothing on that path reads or tests bits 14 through 31 at all.Bits 14 to 19 are therefore allocated by the format and thrown away by the reader, and the consequence lands on a writer rather than a reader. A BIF carrying more than 16,384 resources yields a KEY the engine resolves to the wrong entry, quietly, because index 16,384 masks down to zero. The bounds check on that path does not help: it compares the masked index against
variable_count, so an aliased index is in range and reads as a legitimate hit. Rakata implements the full twenty bits and is thus more permissive than the engine. Treat 16,384 as the ceiling.Nothing shipped comes near it. The largest vanilla archive,
models.bif, holds well under half that many resources, so no vanilla lookup can reach the divergence and a tool that only ever reads retail data will never see it.
Note
The version field has two trailing spaces It is
V1, notV1.0. AV1.1constant exists in our reader and is accepted only in compatibility mode; the canonical K1 mode takesV1alone, matching the engine. As the audit below records, there is noV1.1branch anywhere in vanilla K1 and it is not established that such a file exists at all.
Engine Audits & Decompilation
Read from CExoKeyTable::AddKeyTableContents at 0x0040fb80 in swkotor.exe, with each subsection naming its own function below. Provenance: derived, not attested. The rows below have not been separately re-derived, so they sit on the reverse-engineering queue.
Key Table Registration (CExoKeyTable::AddKeyTableContents)
Mapped from 0x0040fb80.
| Action | Engine Behavior |
|---|---|
| Signature Check | Validates exactly for the KEY magic and the explicit V1 version signature. |
| Version Branching | There is absolutely zero logic handling any speculative V1.1 version branch in vanilla K1. It is currently unknown if a V1.1 KEY format actually exists in the wild, but the engine certainly wouldn’t load it. |
| Payload Mapping | Extrapolates the file location natively by tearing apart the ResourceId bitmask to locate both the target BIF file index and the internal struct array offset. |
Note
The engine handles
KEYtable loading extremely early in the application lifecycle duringCExoBase::InitObject. If a globalKEYfails to mount due to malformed headers, the engine immediately aborts execution.