Skip to main content

rakata_formats/tlk/
writer.rs

1//! TLK binary writer.
2
3use std::io::{Cursor, Write};
4
5use rakata_core::{encode_text, text_encoding_for_language, ResRef};
6
7use crate::binary::{write_f32, write_u32};
8
9use super::{Tlk, TlkBinaryError, ENTRY_SIZE, FILE_HEADER_SIZE, TLK_MAGIC, TLK_VERSION_V3};
10
11/// Writes a TLK to a writer in KotOR TLK V3.0 binary format.
12///
13/// # Errors
14///
15/// [`TlkBinaryError::UnsupportedLanguageEncoding`] when the table's language
16/// id maps to no codepage, and [`TlkBinaryError::TextEncoding`] when an
17/// entry's text has no form in it.
18///
19/// [`TlkBinaryError::ValueOverflow`] when the entry count or a string offset
20/// or length will not fit its `u32`, and [`TlkBinaryError::Io`] when the
21/// writer fails, which can leave a partial file.
22#[cfg_attr(
23    feature = "tracing",
24    tracing::instrument(level = "debug", skip(writer, tlk))
25)]
26pub fn write_tlk<W: Write>(writer: &mut W, tlk: &Tlk) -> Result<(), TlkBinaryError> {
27    let text_encoding = text_encoding_for_language(tlk.language_id)
28        .map_err(|err| TlkBinaryError::UnsupportedLanguageEncoding(err.language_id.raw()))?;
29    let entry_count = u32::try_from(tlk.entries.len())
30        .map_err(|_| TlkBinaryError::ValueOverflow("entry_count"))?;
31    let entries_offset = u32::try_from(
32        FILE_HEADER_SIZE
33            .checked_add(
34                tlk.entries
35                    .len()
36                    .checked_mul(ENTRY_SIZE)
37                    .ok_or(TlkBinaryError::ValueOverflow("entries_offset"))?,
38            )
39            .ok_or(TlkBinaryError::ValueOverflow("entries_offset"))?,
40    )
41    .map_err(|_| TlkBinaryError::ValueOverflow("entries_offset"))?;
42
43    writer.write_all(&TLK_MAGIC)?;
44    writer.write_all(&TLK_VERSION_V3)?;
45    write_u32(writer, tlk.language_id.raw())?;
46    write_u32(writer, entry_count)?;
47    write_u32(writer, entries_offset)?;
48
49    let mut text_blob = Vec::new();
50    for (entry_index, entry) in tlk.entries.iter().enumerate() {
51        let normalized = entry.normalized();
52        let text_bytes = encode_text(&normalized.text, text_encoding).map_err(|source| {
53            TlkBinaryError::TextEncoding {
54                entry_index,
55                source,
56            }
57        })?;
58        let text_offset = u32::try_from(text_blob.len())
59            .map_err(|_| TlkBinaryError::ValueOverflow("text_offset"))?;
60        let text_length = u32::try_from(text_bytes.len())
61            .map_err(|_| TlkBinaryError::ValueOverflow("text_length"))?;
62
63        let mut flags = 0_u32;
64        if normalized.text_present {
65            flags |= 0x0001;
66        }
67        if normalized.sound_present {
68            flags |= 0x0002;
69        }
70        if normalized.sound_length_present {
71            flags |= 0x0004;
72        }
73
74        write_u32(writer, flags)?;
75        write_resref_field(writer, &normalized.voiceover)?;
76        write_u32(writer, normalized.volume_var)?;
77        write_u32(writer, normalized.pitch_var)?;
78        write_u32(writer, text_offset)?;
79        write_u32(writer, text_length)?;
80        write_f32(writer, normalized.sound_length)?;
81
82        text_blob.extend_from_slice(&text_bytes);
83    }
84
85    writer.write_all(&text_blob)?;
86    Ok(())
87}
88
89/// Serializes a TLK to a byte vector in KotOR TLK V3.0 format.
90///
91/// # Errors
92///
93/// Every non-I/O failure [`write_tlk`] describes. The `Vec` target has no I/O
94/// to fail at.
95#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(tlk)))]
96pub fn write_tlk_to_vec(tlk: &Tlk) -> Result<Vec<u8>, TlkBinaryError> {
97    let mut cursor = Cursor::new(Vec::new());
98    write_tlk(&mut cursor, tlk)?;
99    Ok(cursor.into_inner())
100}
101
102/// Writes a fixed 16-byte TLK sound resref field.
103fn write_resref_field<W: Write>(writer: &mut W, resref: &ResRef) -> Result<(), TlkBinaryError> {
104    // No overflow arm: a `ResRef` refuses anything past sixteen bytes at
105    // construction, so one that would not fit this field cannot reach here.
106    writer.write_all(&resref.to_padded())?;
107    Ok(())
108}