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//! ## Format Layout
7//! ```text
8//! +------------------------------+ 0x0000
9//! | Header (9 bytes)             |
10//! +------------------------------+ 0x0009
11//! | Singles block                |
12//! | 3 * 28 * f32                 |
13//! +------------------------------+
14//! | Doubles blocks               |
15//! | 28 * (3 * 28 * f32)          |
16//! +------------------------------+
17//! | Triples blocks               |
18//! | 28 * 28 * (3 * 28 * f32)     |
19//! +------------------------------+
20//! ```
21//!
22//! ## Header (9 bytes)
23//! ```text
24//! 0x00..0x04  magic         "LTR "
25//! 0x04..0x08  version       "V1.0"
26//! 0x08..0x09  letter_count  (u8, KotOR expects 28)
27//! ```
28
29mod reader;
30mod writer;
31
32pub use reader::{read_ltr, read_ltr_from_bytes};
33pub use writer::{write_ltr, write_ltr_to_vec};
34
35use std::array::from_fn;
36use thiserror::Error;
37
38use crate::binary::{DecodeBinary, EncodeBinary};
39
40/// KotOR LTR header size in bytes.
41const FILE_HEADER_SIZE: usize = 9;
42/// LTR file signature.
43const LTR_MAGIC: [u8; 4] = *b"LTR ";
44/// LTR version used by KotOR.
45const LTR_VERSION_V10: [u8; 4] = *b"V1.0";
46/// Number of letters in KotOR LTR tables (`a-z`, `'`, `-`).
47pub const LTR_CHARACTER_COUNT: usize = 28;
48const LTR_CHARACTER_COUNT_U8: u8 = 28;
49const PROBABILITY_SET_COUNT_PER_BLOCK: usize = 3;
50const FLOAT_SIZE_BYTES: usize = std::mem::size_of::<f32>();
51const BLOCK_FLOAT_COUNT: usize = LTR_CHARACTER_COUNT * PROBABILITY_SET_COUNT_PER_BLOCK;
52const DOUBLE_BLOCK_COUNT: usize = LTR_CHARACTER_COUNT;
53const TRIPLE_BLOCK_COUNT: usize = LTR_CHARACTER_COUNT * LTR_CHARACTER_COUNT;
54const TOTAL_BLOCK_COUNT: usize = 1 + DOUBLE_BLOCK_COUNT + TRIPLE_BLOCK_COUNT;
55const TOTAL_FLOAT_COUNT: usize = TOTAL_BLOCK_COUNT * BLOCK_FLOAT_COUNT;
56const EXPECTED_PAYLOAD_SIZE: usize = TOTAL_FLOAT_COUNT * FLOAT_SIZE_BYTES;
57const EXPECTED_FILE_SIZE: usize = FILE_HEADER_SIZE + EXPECTED_PAYLOAD_SIZE;
58
59/// One LTR probability block.
60///
61/// Each block stores probabilities for one context level:
62/// - `start`: probability of each character at name start,
63/// - `middle`: probability of each character in middle positions,
64/// - `end`: probability of each character as terminating character.
65#[derive(Debug, Clone, PartialEq)]
66pub struct LtrProbabilityBlock {
67    /// Start-position probabilities indexed by character ID.
68    pub start: [f32; LTR_CHARACTER_COUNT],
69    /// Middle-position probabilities indexed by character ID.
70    pub middle: [f32; LTR_CHARACTER_COUNT],
71    /// End-position probabilities indexed by character ID.
72    pub end: [f32; LTR_CHARACTER_COUNT],
73}
74
75impl Default for LtrProbabilityBlock {
76    fn default() -> Self {
77        Self {
78            start: [0.0; LTR_CHARACTER_COUNT],
79            middle: [0.0; LTR_CHARACTER_COUNT],
80            end: [0.0; LTR_CHARACTER_COUNT],
81        }
82    }
83}
84
85impl LtrProbabilityBlock {
86    /// Creates a zero-initialized probability block.
87    pub fn new() -> Self {
88        Self::default()
89    }
90}
91
92/// In-memory LTR container.
93#[derive(Debug, Clone, PartialEq)]
94pub struct Ltr {
95    /// Singles table (no previous-character context).
96    pub singles: LtrProbabilityBlock,
97    /// Doubles table (indexed by one previous character).
98    pub doubles: Box<[LtrProbabilityBlock; LTR_CHARACTER_COUNT]>,
99    /// Triples table (indexed by two previous characters).
100    pub triples: Box<[[LtrProbabilityBlock; LTR_CHARACTER_COUNT]; LTR_CHARACTER_COUNT]>,
101}
102
103impl Default for Ltr {
104    fn default() -> Self {
105        Self {
106            singles: LtrProbabilityBlock::new(),
107            doubles: Box::new(from_fn(|_| LtrProbabilityBlock::new())),
108            triples: Box::new(from_fn(|_| from_fn(|_| LtrProbabilityBlock::new()))),
109        }
110    }
111}
112
113impl Ltr {
114    /// Creates an empty LTR table set.
115    pub fn new() -> Self {
116        Self::default()
117    }
118}
119
120impl DecodeBinary for Ltr {
121    type Error = LtrBinaryError;
122
123    fn decode_binary(bytes: &[u8]) -> Result<Self, Self::Error> {
124        read_ltr_from_bytes(bytes)
125    }
126}
127
128impl EncodeBinary for Ltr {
129    type Error = LtrBinaryError;
130
131    fn encode_binary(&self) -> Result<Vec<u8>, Self::Error> {
132        write_ltr_to_vec(self)
133    }
134}
135
136/// Errors produced while parsing or serializing LTR binary data.
137#[derive(Debug, Error)]
138pub enum LtrBinaryError {
139    /// I/O read/write failure.
140    #[error(transparent)]
141    Io(#[from] std::io::Error),
142    /// Header signature is not `LTR `.
143    #[error("invalid LTR magic: {0:?}")]
144    InvalidMagic([u8; 4]),
145    /// Header version is unsupported.
146    #[error("invalid LTR version: {0:?}")]
147    InvalidVersion([u8; 4]),
148    /// Header/body layout is invalid or truncated.
149    #[error("invalid LTR header: {0}")]
150    InvalidHeader(String),
151    /// LTR content is structurally invalid.
152    #[error("invalid LTR data: {0}")]
153    InvalidData(String),
154    /// Letter count is not supported for current KotOR scope.
155    #[error("unsupported LTR letter count `{0}` (expected 28)")]
156    UnsupportedLetterCount(u8),
157}