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

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

PropertyValue
Extension(s).ltr
Magic SignatureLTR / V1.0
TypeNaming State Matrix
Rust ReferenceView 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.

OffsetFieldTypeNotes
0x00magicfourccLTR , trailing space included.
0x04versionfourccV1.0, strictly validated.
0x08letter_countu828 in every KotOR file. Sizes everything below.

Writing L for letter_count, the three blocks follow immediately at 0x09:

BlockElementIndex orderSize at L = 28
Singlesf32[position][next]336 bytes
Doublesf32[prev][position][next]9,408 bytes
Triplesf32[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:

IndexLetterIndexLetter
025a through z, in order26' (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 .ltr is exactly 273,177 bytes Nothing in the format is variable-length once letter_count is fixed, so at the 28 letters KotOR uses, the total is 9 + 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 EventGhidra Provenance & Engine Behavior
Magic ValidationThe 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 IngestionMemory 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 CheckUpon 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.