rakata_extract/
talktable.rs1use 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#[derive(Debug, Clone, PartialEq)]
16pub struct TalkTable {
17 path: Option<PathBuf>,
18 tlk: Tlk,
19}
20
21impl TalkTable {
22 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 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 pub fn path(&self) -> Option<&Path> {
44 self.path.as_deref()
45 }
46
47 pub fn tlk(&self) -> &Tlk {
49 &self.tlk
50 }
51
52 pub fn language_id(&self) -> LanguageId {
54 self.tlk.language_id
55 }
56
57 pub fn len(&self) -> usize {
59 self.tlk.entries.len()
60 }
61
62 pub fn is_empty(&self) -> bool {
64 self.tlk.entries.is_empty()
65 }
66
67 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 pub fn string(&self, strref: StrRef) -> Option<&str> {
75 self.entry(strref).map(|entry| entry.text.as_str())
76 }
77
78 pub fn sound(&self, strref: StrRef) -> Option<&ResRef> {
80 self.entry(strref).map(|entry| &entry.voiceover)
81 }
82}
83
84#[derive(Debug, Error)]
86pub enum TalkTableError {
87 #[error("I/O failure for `{path}`: {source}")]
89 Io {
90 path: PathBuf,
92 #[source]
94 source: std::io::Error,
95 },
96 #[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}