rakata_formats/tlk/
mod.rs

1//! TLK V3.0 binary reader and writer.
2//!
3//! TLK is the global talk table format used for localized game strings.
4//! Entries store flags, optional voice-over resrefs, and text offsets into a
5//! shared string blob.
6//!
7//! ## Format Layout
8//! ```text
9//! +------------------------------+ 0x0000
10//! | Header (20 bytes)            |
11//! | magic/version/language/count |
12//! +------------------------------+ 0x0014
13//! | Entry table                  |
14//! | 40 bytes * entry_count       |
15//! +------------------------------+ entries_offset
16//! | Text blob                    |
17//! | concatenated string bytes    |
18//! +------------------------------+
19//! ```
20//!
21//! ## Header (20 bytes)
22//! ```text
23//! 0x00..0x04  magic          ("TLK ")
24//! 0x04..0x08  version        ("V3.0")
25//! 0x08..0x0C  language_id    (u32)
26//! 0x0C..0x10  entry_count    (u32)
27//! 0x10..0x14  entries_offset (u32, text blob start)
28//! ```
29//!
30//! ## Entry Record (40 bytes)
31//! ```text
32//! 0x00..0x04  flags        (u32)
33//! 0x04..0x14  sound_resref (16-byte field)
34//! 0x14..0x18  volume var   (u32, reserved)
35//! 0x18..0x1C  pitch var    (u32, reserved)
36//! 0x1C..0x20  text_offset  (u32, relative to text blob start)
37//! 0x20..0x24  text_length  (u32)
38//! 0x24..0x28  sound_length (f32)
39//! ```
40//!
41//! Text encoding is selected from `language_id` and enforced strictly for
42//! lossless behavior.
43
44mod reader;
45mod writer;
46
47pub use reader::{read_tlk, read_tlk_from_bytes};
48pub use writer::{write_tlk, write_tlk_to_vec};
49
50use thiserror::Error;
51
52#[cfg(feature = "serde")]
53use serde::{Deserialize, Serialize};
54
55use rakata_core::{DecodeTextError, EncodeTextError, LanguageId, ResRef, ResRefError};
56
57use crate::binary::{self, DecodeBinary, EncodeBinary};
58
59/// TLK file type marker.
60const TLK_MAGIC: [u8; 4] = *b"TLK ";
61/// TLK format version used by KotOR.
62const TLK_VERSION_V3: [u8; 4] = *b"V3.0";
63/// TLK header size in bytes.
64const FILE_HEADER_SIZE: usize = 20;
65/// Per-entry header size in bytes.
66const ENTRY_SIZE: usize = 40;
67
68/// In-memory representation of a TLK talk table.
69#[derive(Debug, Clone, PartialEq)]
70#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
71pub struct Tlk {
72    /// Language identifier stored in TLK header.
73    pub language_id: LanguageId,
74    /// Ordered string entries (stringref index = vector index).
75    pub entries: Vec<TlkEntry>,
76}
77
78impl Tlk {
79    /// Creates an empty TLK with the provided language identifier.
80    pub fn new(language_id: impl Into<LanguageId>) -> Self {
81        Self {
82            language_id: language_id.into(),
83            entries: Vec::new(),
84        }
85    }
86}
87
88impl DecodeBinary for Tlk {
89    type Error = TlkBinaryError;
90
91    fn decode_binary(bytes: &[u8]) -> Result<Self, Self::Error> {
92        read_tlk_from_bytes(bytes)
93    }
94}
95
96impl EncodeBinary for Tlk {
97    type Error = TlkBinaryError;
98
99    fn encode_binary(&self) -> Result<Vec<u8>, Self::Error> {
100        write_tlk_to_vec(self)
101    }
102}
103
104/// One TLK string entry and its metadata flags.
105#[derive(Debug, Clone, PartialEq)]
106#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
107pub struct TlkEntry {
108    /// Localized text payload.
109    pub text: String,
110    /// Voice-over resource reference.
111    pub voiceover: ResRef,
112    /// TLK flag bit 0: text present.
113    pub text_present: bool,
114    /// TLK flag bit 1: sound present.
115    pub sound_present: bool,
116    /// TLK flag bit 2: sound length present.
117    pub sound_length_present: bool,
118    /// Voice-over length in seconds.
119    pub sound_length: f32,
120    /// Entry offset 0x14: volume variance (u32, reserved -- written by engine, not semantically used).
121    pub volume_var: u32,
122    /// Entry offset 0x18: pitch variance (u32, reserved -- written by engine, not semantically used).
123    pub pitch_var: u32,
124}
125
126impl TlkEntry {
127    /// Creates a TLK entry with sensible default flags from content.
128    pub fn new(text: impl Into<String>, voiceover: ResRef) -> Self {
129        let text = text.into();
130        Self {
131            text_present: !text.is_empty(),
132            sound_present: !voiceover.is_blank(),
133            sound_length_present: false,
134            sound_length: 0.0,
135            volume_var: 0,
136            pitch_var: 0,
137            text,
138            voiceover,
139        }
140    }
141
142    /// Returns a canonicalized entry that enforces internally consistent flags.
143    pub fn normalized(&self) -> Self {
144        let mut normalized = self.clone();
145        normalized.text_present = !normalized.text.is_empty();
146        normalized.sound_present = !normalized.voiceover.is_blank();
147        if !normalized.sound_present {
148            normalized.sound_length_present = false;
149            normalized.sound_length = 0.0;
150        } else {
151            normalized.sound_length_present =
152                normalized.sound_length_present || normalized.sound_length != 0.0;
153        }
154        normalized
155    }
156}
157
158/// Errors produced while parsing or serializing TLK binary data.
159#[derive(Debug, Error)]
160pub enum TlkBinaryError {
161    /// I/O read/write failure.
162    #[error(transparent)]
163    Io(#[from] std::io::Error),
164    /// Header magic does not match `TLK `.
165    #[error("invalid TLK magic: {0:?}")]
166    InvalidMagic([u8; 4]),
167    /// Header version is unsupported.
168    #[error("invalid TLK version: {0:?}")]
169    InvalidVersion([u8; 4]),
170    /// Header/offset table is structurally invalid.
171    #[error("invalid TLK header: {0}")]
172    InvalidHeader(String),
173    /// Value cannot be represented in binary field width.
174    #[error("value overflow while writing field `{0}`")]
175    ValueOverflow(&'static str),
176    /// Language ID maps to a text encoding that is not currently supported.
177    #[error("unsupported TLK language id {0} for text encoding")]
178    UnsupportedLanguageEncoding(u32),
179    /// Entry text contains characters that cannot be represented in the selected encoding.
180    #[error("entry {entry_index} text encoding failed: {source}")]
181    TextEncoding {
182        /// Index of the entry that failed to encode.
183        entry_index: usize,
184        /// Source encoding error with exact character location.
185        #[source]
186        source: EncodeTextError,
187    },
188    /// Entry text bytes could not be decoded without data loss.
189    #[error("entry {entry_index} text decoding failed: {source}")]
190    TextDecoding {
191        /// Index of the entry that failed to decode.
192        entry_index: usize,
193        /// Source decoding error with byte position details.
194        #[source]
195        source: DecodeTextError,
196    },
197    /// Entry sound resref bytes decoded but failed ResRef validation.
198    #[error("entry {entry_index} sound resref `{value}` failed validation: {source}")]
199    InvalidSoundResRef {
200        /// Index of the entry that failed to decode.
201        entry_index: usize,
202        /// Decoded sound resref token.
203        value: String,
204        /// ResRef validation error.
205        #[source]
206        source: ResRefError,
207    },
208}
209
210impl From<binary::BinaryLayoutError> for TlkBinaryError {
211    fn from(error: binary::BinaryLayoutError) -> Self {
212        Self::InvalidHeader(error.to_string())
213    }
214}
215
216//
217// Serde Support (JSON)
218//
219
220#[cfg(feature = "serde")]
221mod serde_impl {
222    use super::*;
223    use serde_json::{from_slice, from_str, to_string_pretty, to_vec};
224
225    /// Serializes a TLK to JSON string.
226    pub fn write_tlk_to_json(tlk: &Tlk) -> Result<String, TlkBinaryError> {
227        to_string_pretty(tlk).map_err(|e| TlkBinaryError::Io(std::io::Error::other(e)))
228    }
229
230    /// Serializes a TLK to JSON bytes.
231    pub fn write_tlk_to_json_vec(tlk: &Tlk) -> Result<Vec<u8>, TlkBinaryError> {
232        to_vec(tlk).map_err(|e| TlkBinaryError::Io(std::io::Error::other(e)))
233    }
234
235    /// Deserializes a TLK from JSON string.
236    pub fn read_tlk_from_json(json: &str) -> Result<Tlk, TlkBinaryError> {
237        from_str(json).map_err(|e| TlkBinaryError::Io(std::io::Error::other(e)))
238    }
239
240    /// Deserializes a TLK from JSON bytes.
241    pub fn read_tlk_from_json_bytes(bytes: &[u8]) -> Result<Tlk, TlkBinaryError> {
242        from_slice(bytes).map_err(|e| TlkBinaryError::Io(std::io::Error::other(e)))
243    }
244}
245
246#[cfg(feature = "serde")]
247pub use serde_impl::{
248    read_tlk_from_json, read_tlk_from_json_bytes, write_tlk_to_json, write_tlk_to_json_vec,
249};