Skip to main content

rakata_formats/ltr/
mod.rs

1//! LTR binary reader and writer.
2//!
3//! LTR resources store third-order Markov probability tables used by KotOR
4//! name generation.
5//!
6//! ## Shape of the container
7//!
8//! A 9-byte header, then three probability blocks of first, second and third
9//! Markov order. Nothing points anywhere: every size falls out of the header's
10//! `letter_count`, so the whole file is positional.
11//!
12//! The recurring factor of three is position within a name -- start, middle and
13//! end each carry their own distribution. At KotOR's 28 letters the blocks are
14//! `3 x 28`, `28 x (3 x 28)` and `28 x 28 x (3 x 28)` floats, which makes every
15//! file exactly 273,177 bytes. The engine closes its read by asserting the final
16//! offset equals the buffer length, so there is no slack: a file a byte off is
17//! rejected rather than partially accepted.
18//!
19//! Byte-level field maps live in `docs/src/formats/text/ltr.md`, with the
20//! engine's own load sequence.
21
22mod reader;
23mod writer;
24
25pub use reader::{read_ltr, read_ltr_from_bytes};
26pub use writer::{write_ltr, write_ltr_to_vec};
27
28use std::array::from_fn;
29use thiserror::Error;
30
31use crate::binary::{DecodeBinary, EncodeBinary};
32
33/// KotOR LTR header size in bytes.
34const FILE_HEADER_SIZE: usize = 9;
35/// LTR file signature.
36const LTR_MAGIC: [u8; 4] = *b"LTR ";
37/// LTR version used by KotOR.
38const LTR_VERSION_V10: [u8; 4] = *b"V1.0";
39/// Number of letters in KotOR LTR tables (`a-z`, `'`, `-`).
40pub const LTR_CHARACTER_COUNT: usize = 28;
41const LTR_CHARACTER_COUNT_U8: u8 = 28;
42const PROBABILITY_SET_COUNT_PER_BLOCK: usize = 3;
43const FLOAT_SIZE_BYTES: usize = std::mem::size_of::<f32>();
44const BLOCK_FLOAT_COUNT: usize = LTR_CHARACTER_COUNT * PROBABILITY_SET_COUNT_PER_BLOCK;
45const DOUBLE_BLOCK_COUNT: usize = LTR_CHARACTER_COUNT;
46const TRIPLE_BLOCK_COUNT: usize = LTR_CHARACTER_COUNT * LTR_CHARACTER_COUNT;
47const TOTAL_BLOCK_COUNT: usize = 1 + DOUBLE_BLOCK_COUNT + TRIPLE_BLOCK_COUNT;
48const TOTAL_FLOAT_COUNT: usize = TOTAL_BLOCK_COUNT * BLOCK_FLOAT_COUNT;
49const EXPECTED_PAYLOAD_SIZE: usize = TOTAL_FLOAT_COUNT * FLOAT_SIZE_BYTES;
50const EXPECTED_FILE_SIZE: usize = FILE_HEADER_SIZE + EXPECTED_PAYLOAD_SIZE;
51
52/// One LTR probability block.
53///
54/// Each block stores probabilities for one context level:
55/// - `start`: probability of each character at name start,
56/// - `middle`: probability of each character in middle positions,
57/// - `end`: probability of each character as terminating character.
58#[derive(Debug, Clone, PartialEq)]
59pub struct LtrProbabilityBlock {
60    /// Start-position probabilities indexed by character ID.
61    pub start: [f32; LTR_CHARACTER_COUNT],
62    /// Middle-position probabilities indexed by character ID.
63    pub middle: [f32; LTR_CHARACTER_COUNT],
64    /// End-position probabilities indexed by character ID.
65    pub end: [f32; LTR_CHARACTER_COUNT],
66}
67
68impl Default for LtrProbabilityBlock {
69    fn default() -> Self {
70        Self {
71            start: [0.0; LTR_CHARACTER_COUNT],
72            middle: [0.0; LTR_CHARACTER_COUNT],
73            end: [0.0; LTR_CHARACTER_COUNT],
74        }
75    }
76}
77
78impl LtrProbabilityBlock {
79    /// Creates a zero-initialized probability block.
80    pub fn new() -> Self {
81        Self::default()
82    }
83}
84
85/// In-memory LTR container.
86#[derive(Debug, Clone, PartialEq)]
87pub struct Ltr {
88    /// Singles table (no previous-character context).
89    pub singles: LtrProbabilityBlock,
90    /// Doubles table (indexed by one previous character).
91    pub doubles: Box<[LtrProbabilityBlock; LTR_CHARACTER_COUNT]>,
92    /// Triples table (indexed by two previous characters).
93    pub triples: Box<[[LtrProbabilityBlock; LTR_CHARACTER_COUNT]; LTR_CHARACTER_COUNT]>,
94}
95
96impl Default for Ltr {
97    fn default() -> Self {
98        Self {
99            singles: LtrProbabilityBlock::new(),
100            doubles: Box::new(from_fn(|_| LtrProbabilityBlock::new())),
101            triples: Box::new(from_fn(|_| from_fn(|_| LtrProbabilityBlock::new()))),
102        }
103    }
104}
105
106impl Ltr {
107    /// Creates an empty LTR table set.
108    pub fn new() -> Self {
109        Self::default()
110    }
111}
112
113impl DecodeBinary for Ltr {
114    type Error = LtrBinaryError;
115
116    fn decode_binary(bytes: &[u8]) -> Result<Self, Self::Error> {
117        read_ltr_from_bytes(bytes)
118    }
119}
120
121impl EncodeBinary for Ltr {
122    type Error = LtrBinaryError;
123
124    fn encode_binary(&self) -> Result<Vec<u8>, Self::Error> {
125        write_ltr_to_vec(self)
126    }
127}
128
129/// Errors produced while parsing or serializing LTR binary data.
130#[derive(Debug, Error)]
131pub enum LtrBinaryError {
132    /// I/O read/write failure.
133    #[error(transparent)]
134    Io(#[from] std::io::Error),
135    /// Header signature is not `LTR `.
136    #[error("invalid LTR magic: {0:?}")]
137    InvalidMagic([u8; 4]),
138    /// Header version is unsupported.
139    #[error("invalid LTR version: {0:?}")]
140    InvalidVersion([u8; 4]),
141    /// Header/body layout is invalid or truncated.
142    #[error("invalid LTR header: {0}")]
143    InvalidHeader(String),
144    /// LTR content is structurally invalid.
145    #[error("invalid LTR data: {0}")]
146    InvalidData(String),
147    /// Letter count is not supported for current KotOR scope.
148    #[error("unsupported LTR letter count `{0}` (expected 28)")]
149    UnsupportedLetterCount(u8),
150}