LTR (Letter Frequency)
LTR files contain matrices defining the probabilistic sequence groupings of letters used by the engine’s random name generator.
At a Glance
| Property | Value |
|---|---|
| Extension(s) | .ltr |
| Magic Signature | LTR / V1.0 |
| Type | Naming State Matrix |
| Rust Reference | View rakata_formats::Ltr in Rustdocs |
File Layout
A 9-byte header followed by three probability blocks, one per Markov order. Nothing points anywhere: every block’s size falls out of letter_count, so the whole file is positional.
| Offset | Field | Type | Notes |
|---|---|---|---|
0x00 | magic | fourcc | LTR , trailing space included. |
0x04 | version | fourcc | V1.0, strictly validated. |
0x08 | letter_count | u8 | 28 in every KotOR file. Sizes everything below. |
Writing L for letter_count, the three blocks follow immediately at 0x09:
| Block | Element | Index order | Size at L = 28 |
|---|---|---|---|
| Singles | f32 | [position][next] | 336 bytes |
| Doubles | f32 | [prev][position][next] | 9,408 bytes |
| Triples | f32 | [prev2][prev][position][next] | 263,424 bytes |
Every block is a flat f32 array in C order, with the next letter varying fastest and any preceding-letter context varying slowest. So a run of L consecutive floats is one distribution over the alphabet, and the three blocks differ only in how much context selects which run you read.
What a run of L floats contains
Each run is a cumulative distribution, not per-letter probabilities: values ascend across the run and the last non-zero entry is exactly 1.0 in every populated run in the shipped files. A generator draws a uniform value and takes the first entry at or above it.
0.0 is a sentinel meaning the letter cannot occur in that context, rather than a probability of zero folded into the running total. That distinction is what makes the array look non-monotonic on a naive read: a run like 0.066, 0.115, 0.197, 0.23, 0.0, 0.246 is ascending once the sentinel is skipped, and a reader that treats the 0.0 as a cumulative value will conclude the data is corrupt.
Most runs are entirely zero, which is simply a context that never occurs in the source names, unsurprising at the triples order, where the great majority of three-letter contexts are unattested. A handful of shipped runs are not perfectly ordered even after skipping sentinels; a reader should tolerate that rather than reject the file, since the engine does.
The recurring 3 is the position within a name: start, middle and end each get their own distribution, which is how the generator knows that some letters are plausible openers and others only ever appear inside a word. The three blocks are the first, second and third Markov orders: the singles say what letter comes next given nothing, the doubles given one preceding letter, the triples given two.
The alphabet
Every index in every block refers to a position in this sequence, and nothing in the file states it: a table indexed by letter with the letters left implicit:
| Index | Letter | Index | Letter |
|---|---|---|---|
0–25 | a through z, in order | 26 | ' (apostrophe) |
27 | - (hyphen) |
The two non-alphabetic entries are what let the generator produce names like Bel'aya or Jar-Kai rather than treating them as impossible. A reader that assumes 26 letters reads the tables correctly for a-z and silently misplaces everything after.
Note
Every KotOR
.ltris exactly 273,177 bytes Nothing in the format is variable-length onceletter_countis fixed, so at the 28 letters KotOR uses, the total is9 + 336 + 9,408 + 263,424. The engine closes its read by asserting that the final parse offset equals the buffer length, as the audit below records, which means a file even one byte off that figure is rejected outright rather than partially accepted. A generator producing these has no slack whatsoever.
Engine Audits & Decompilation
Read from CResLTR::OnResourceServiced at 0x00712410 in swkotor.exe. Provenance: derived, not attested. The rows below have not been separately re-derived, so they sit on the reverse-engineering queue.
| Pipeline Event | Ghidra Provenance & Engine Behavior |
|---|---|
| Magic Validation | The native parser enforces a mandatory "LTR " signature and strictly validates the "V1.0" format tag. These parameters collectively structure a rigid 9-byte header block. The sequence natively defines the letter_count variable as a single byte resting exactly at offset +0x08. |
| Contiguous Ingestion | Memory buffer extraction initiates immediately at offset +0x09. The parser algorithm sequentially extracts natively chained string arrays grouping start, middle, and end blocks to map against procedural probability matrices. |
| Payload Bounds Check | Upon closing the read operations, the memory allocator immediately verifies a structural bounding condition asserting that the terminal parsing offset explicitly matches the buffer array’s total byte allocation length. |