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
| Property | Value |
|---|---|
| Extension(s) | .tlk |
| Magic Signature | TLK / V3.0 |
| Type | Localized String Bundle |
| Rust Reference | View 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.
| Block | Size | Located by |
|---|---|---|
| Header | 20 bytes | Always at 0x00 |
| Entry table | 40 bytes per entry | Always at 0x14, immediately after the header |
| Text blob | remainder of the file | entries_offset, with each entry’s text_offset relative to it |
Header (20 bytes)
| Offset | Field | Type | Notes |
|---|---|---|---|
0x00 | magic | fourcc | TLK , trailing space included. |
0x04 | version | fourcc | V3.0. Not validated by the engine, but it does pick the entry size. See below. |
0x08 | language_id | u32 | Selects the text encoding. |
0x0C | entry_count | u32 | |
0x10 | entries_offset | u32 | Start of the text blob, not of the entry table. |
Entry Record (40 bytes each)
| Offset | Field | Type | Notes |
|---|---|---|---|
0x00 | flags | u32 | Three presence bits and a reject sentinel. See below. |
0x04 | sound_resref | char[16] | Voice-over resource, when the entry has one. |
0x14 | volume variance | u32 | Reserved. Write 0; see below. |
0x18 | pitch variance | u32 | Reserved. Write 0; see below. |
0x1C | text_offset | u32 | Relative to entries_offset, not absolute. |
0x20 | text_length | u32 | The text’s extent. It is not NUL-terminated; see below. |
0x24 | sound_length | f32 |
Note
String text is length-delimited, not NUL-terminated Each entry’s text runs for exactly
text_lengthbytes from itstext_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 retaildialog.tlk, none of which carries a trailing NUL.
flags
| Bit | Value | Meaning |
|---|---|---|
| 0 | 0x1 | Entry text is present. Clear means the text is left empty rather than read from the blob. |
| 1 | 0x2 | sound_resref is populated. Clear means the consumer receives an empty resref. |
| 2 | 0x4 | sound_length is populated. Clear means the duration is zeroed. |
| 15 | 0x8000 | Reject 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
0in every entry of thedialog.tlka retail install ships, so the value to write is0rather 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, atV3.0and 40 bytes. Confirmed arithmetically:20 + 49,265 × 40lands exactly onentries_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 == 5sets 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_idto 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 Event | Ghidra Provenance & Engine Behavior |
|---|---|
| Magic Check | Function: 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 Dispatching | Function: 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 Dialects | Function: CTlkFile::AddFileWhen 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. |