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