Skip to main content

rakata_extract/
talktable.rs

1//! Talktable (`dialog.tlk`) primitives.
2//!
3//! This module provides a lightweight read-only wrapper around parsed TLK data
4//! for string/sound lookup by `StrRef`.
5
6use std::fs;
7use std::path::{Path, PathBuf};
8
9use thiserror::Error;
10
11use rakata_core::{LanguageId, ResRef, StrRef};
12use rakata_formats::{read_tlk_from_bytes, Tlk, TlkBinaryError, TlkEntry};
13
14/// Read-only TLK lookup wrapper.
15#[derive(Debug, Clone, PartialEq)]
16pub struct TalkTable {
17    path: Option<PathBuf>,
18    tlk: Tlk,
19}
20
21impl TalkTable {
22    /// Reads a talktable from in-memory bytes.
23    pub fn read_from_bytes(bytes: &[u8]) -> Result<Self, TalkTableError> {
24        Ok(Self {
25            path: None,
26            tlk: read_tlk_from_bytes(bytes)?,
27        })
28    }
29
30    /// Reads a talktable from disk.
31    pub fn read_from_file(path: impl AsRef<Path>) -> Result<Self, TalkTableError> {
32        let path = path.as_ref();
33        let bytes = fs::read(path).map_err(|source| TalkTableError::Io {
34            path: path.to_path_buf(),
35            source,
36        })?;
37        let mut table = Self::read_from_bytes(&bytes)?;
38        table.path = Some(path.to_path_buf());
39        Ok(table)
40    }
41
42    /// Returns the source path when loaded from disk.
43    pub fn path(&self) -> Option<&Path> {
44        self.path.as_deref()
45    }
46
47    /// Returns the parsed TLK value.
48    pub fn tlk(&self) -> &Tlk {
49        &self.tlk
50    }
51
52    /// Returns the talktable language identifier.
53    pub fn language_id(&self) -> LanguageId {
54        self.tlk.language_id
55    }
56
57    /// Returns the number of entries in the table.
58    pub fn len(&self) -> usize {
59        self.tlk.entries.len()
60    }
61
62    /// Returns `true` when the table has no entries.
63    pub fn is_empty(&self) -> bool {
64        self.tlk.entries.is_empty()
65    }
66
67    /// Returns one TLK entry by `StrRef`.
68    pub fn entry(&self, strref: StrRef) -> Option<&TlkEntry> {
69        let index = usize::try_from(strref.index()?).ok()?;
70        self.tlk.entries.get(index)
71    }
72
73    /// Returns one string by `StrRef`.
74    pub fn string(&self, strref: StrRef) -> Option<&str> {
75        self.entry(strref).map(|entry| entry.text.as_str())
76    }
77
78    /// Returns one voiceover `ResRef` by `StrRef`.
79    pub fn sound(&self, strref: StrRef) -> Option<&ResRef> {
80        self.entry(strref).map(|entry| &entry.voiceover)
81    }
82}
83
84/// Errors produced by talktable primitives.
85#[derive(Debug, Error)]
86pub enum TalkTableError {
87    /// I/O failure while reading an on-disk TLK file.
88    #[error("I/O failure for `{path}`: {source}")]
89    Io {
90        /// Path being read.
91        path: PathBuf,
92        /// Underlying OS error.
93        #[source]
94        source: std::io::Error,
95    },
96    /// TLK parse failure.
97    #[error(transparent)]
98    Tlk(#[from] TlkBinaryError),
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    use rakata_formats::{write_tlk_to_vec, TlkEntry};
106    use tempfile::TempDir;
107
108    #[test]
109    fn reads_from_bytes_and_resolves_entries() {
110        let mut tlk = Tlk::new(LanguageId::from_raw(0));
111        tlk.entries.push(TlkEntry::new(
112            "hello there",
113            ResRef::new("n_gend001").expect("valid resref"),
114        ));
115        tlk.entries.push(TlkEntry::new("", ResRef::blank()));
116        let bytes = write_tlk_to_vec(&tlk).expect("write tlk");
117
118        let table = TalkTable::read_from_bytes(&bytes).expect("read talktable");
119        assert_eq!(table.language_id(), LanguageId::from_raw(0));
120        assert_eq!(table.len(), 2);
121        assert_eq!(table.string(StrRef::from_raw(0)), Some("hello there"));
122        assert_eq!(
123            table
124                .sound(StrRef::from_raw(0))
125                .expect("entry has sound")
126                .as_bytes(),
127            b"n_gend001"
128        );
129        assert_eq!(table.string(StrRef::from_raw(-1)), None);
130        assert_eq!(table.string(StrRef::from_raw(9999)), None);
131    }
132
133    #[test]
134    fn reads_from_file_and_sets_path() {
135        let mut tlk = Tlk::new(LanguageId::from_raw(0));
136        tlk.entries.push(TlkEntry::new(
137            "text",
138            ResRef::new("vo_line").expect("valid resref"),
139        ));
140        let bytes = write_tlk_to_vec(&tlk).expect("write tlk");
141
142        let temp = TempDir::new().expect("create tempdir");
143        let path = temp.path().join("dialog.tlk");
144        fs::write(&path, bytes).expect("write fixture");
145
146        let table = TalkTable::read_from_file(&path).expect("read talktable");
147        assert_eq!(table.path(), Some(path.as_path()));
148    }
149}