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

TLK (Talk Table)

The Talk Table is a massive localized string repository. Every item description, line of dialogue, and UI text in KOTOR references an index (a StrRef) pointing into this master dictionary file.

At a Glance

PropertyValue
Extension(s).tlk
Magic SignatureTLK / V3.0
TypeLocalized String Bundle
Rust ReferenceView rakata_formats::Tlk in Rustdocs

File Layout

Three blocks, and the middle one is the only table in the file. The entry table starts immediately after the header at a fixed 0x14; the text blob is wherever entries_offset says, and every entry’s text_offset is measured from there rather than from the start of the file.

BlockSizeLocated by
Header20 bytesAlways at 0x00
Entry table40 bytes per entryAlways at 0x14, immediately after the header
Text blobremainder of the fileentries_offset, with each entry’s text_offset relative to it

Header (20 bytes)

OffsetFieldTypeNotes
0x00magicfourccTLK , trailing space included.
0x04versionfourccV3.0. Not validated by the engine, but it does pick the entry size. See below.
0x08language_idu32Selects the text encoding.
0x0Centry_countu32
0x10entries_offsetu32Start of the text blob, not of the entry table.

Entry Record (40 bytes each)

OffsetFieldTypeNotes
0x00flagsu32Three presence bits and a reject sentinel. See below.
0x04sound_resrefchar[16]Voice-over resource, when the entry has one.
0x14volume varianceu32Reserved. Write 0; see below.
0x18pitch varianceu32Reserved. Write 0; see below.
0x1Ctext_offsetu32Relative to entries_offset, not absolute.
0x20text_lengthu32The text’s extent. It is not NUL-terminated; see below.
0x24sound_lengthf32

Note

String text is length-delimited, not NUL-terminated Each entry’s text runs for exactly text_length bytes from its text_offset, and no terminator follows it. A reader that scans for a NUL runs past the end of one string into the next, which is the failure counted, terminated, or neither describes as the silent direction. Measured on a sample of three thousand entries from a retail dialog.tlk, none of which carries a trailing NUL.

flags

BitValueMeaning
00x1Entry text is present. Clear means the text is left empty rather than read from the blob.
10x2sound_resref is populated. Clear means the consumer receives an empty resref.
20x4sound_length is populated. Clear means the duration is zeroed.
150x8000Reject sentinel, not a presence flag.

Only three values occur across every entry of a full dialog.tlk: 7 on nearly all of them, with 6 and 0x8000 sharing a small remainder. So 7 is a normal line with text, voice-over and duration, and 6 is an audio-only line carrying no text.

0x8000 is different in kind. The engine tests the high byte early and, if set, drops straight into the path it uses for a missing or unreadable file, so the caller gets the same empty “invalid StrRef” result it would get for a broken table. Nothing else in the binary tests that bit. This is why the entries carrying it have neither text nor sound: the reader never reaches the point of populating either.

Note

The two variance fields have a canonical value, and it is zero “Reserved” says the engine does not use them and leaves a writer with nothing to put there, which is the fixed-position-and-unread case: the bytes have to be emitted whatever they mean, because everything after them is positioned by their width. Both are 0 in every entry of the dialog.tlk a retail install ships, so the value to write is 0 rather than anything a reader has to infer.

This is a claim over one file, which is the only table the install carries. Nothing establishes what a non-zero value would do.

Warning

The version field is unvalidated but load-bearing The engine accepts any version tag without complaint and then uses it to pick the entry stride: 40 bytes for V3.0, 36 for anything else. A mistyped version does not fail. It parses at the wrong stride, and every entry after the first is garbage.

The install ships exactly one table, dialog.tlk, at V3.0 and 40 bytes. Confirmed arithmetically: 20 + 49,265 × 40 lands exactly on entries_offset, where 36 falls 197,060 bytes short.

So the 36-byte record is a path no KotOR file takes, on a population of one file rather than a corpus. No TLK-magic resource appears inside any archive either, and the feminine-dialect companion the loader probes for is absent from the install, so nothing here exercises that probe.

Important

The engine has no encoding table, and ours is a deliberate divergence There is no encoding declaration in the file, and there is no lookup in the executable either. The engine special-cases exactly one value: language_id == 5 sets a Polish locale explicitly, selecting Windows-1250. Ids at 1000 and above toggle IME support and set no codepage. Everything else, including all five official releases, inherits the process’s default ANSI codepage: Windows-1252 on a western install, something else elsewhere. So the engine’s own decoding is not a property of the file at all; it depends on the machine.

Rakata maps language_id to a codepage explicitly instead. For the ids KotOR shipped this agrees with the engine on a western install, and past them it imposes determinism the engine does not have. That is a deliberate choice rather than an implementation of the format’s rule: a table that decodes differently depending on the reader’s OS is not something a specification can round-trip.

Engine Audits & Decompilation

Read from CTlkFile::ReadHeader at 0x0041d890 and CTlkFile::AddFile in swkotor.exe. Provenance: derived, not attested unless a claim says otherwise: the rows have not been separately re-derived, so they sit on the reverse-engineering queue. Individual claims below may carry a level of their own, and where one does it overrides this line for that claim.

Pipeline EventGhidra Provenance & Engine Behavior
Magic CheckFunction: CTlkFile::ReadHeader (0x0041d890)
The parser requires a "TLK " signature. However, strict version validation is entirely absent. The engine accepts essentially any version tag without raising a failure.
Size DispatchingFunction: CTlkFile::ReadHeader (0x0041d890)
While the version isn’t used for rejection, it dynamically determines memory block sizing. A "V3.0" tag dictates 40 bytes (0x28) per entry, whereas any other version tag automatically falls back to 36 bytes (0x24).
Feminine DialectsFunction: CTlkFile::AddFile
When mounting the primary archive, the engine systematically queries the directory for a secondary <basename>F.tlk (e.g., dialogF.tlk) specifically to supply overriding feminine vocabulary strings for character-gendered text queries.