nwn_lib_rs/
tlk.rs

1//! Talktable file format (.tlk)
2//!
3//! Example
4//! ```rust
5//! #![feature(generic_const_exprs)]
6//! #![allow(incomplete_features)]
7//! use nwn_lib_rs::tlk::{Tlk, Resolver, Gender};
8//!
9//! // Load a TLK file
10//! let tlk_data = std::fs::read("unittest/dialog.tlk").unwrap();
11//! let (_, tlk) = Tlk::from_bytes(&tlk_data, false).unwrap();
12//!
13//! // Resolve a strref
14//! let text = tlk.get_str(4).unwrap();
15//!
16//! // Get complete entry information (for sounds)
17//! let entry = tlk.get_entry(4).unwrap();
18//!
19//! // Setup a resolver that aggregates multiple TLK files
20//! let main_tlk = tlk;
21//! let (_, user_tlk) = Tlk::from_bytes(
22//!         &std::fs::read("unittest/user.tlk").unwrap(),
23//!         false
24//!     ).unwrap();
25//! let resolver = Resolver::new(&main_tlk, None, Some(&user_tlk));
26//!
27//! // Resolve strrefs
28//! let text = resolver.get_str(1, Gender::Male).unwrap();
29//! let text = resolver.get_str(16_777_217, Gender::Male).unwrap();
30//!
31//! ```
32//!
33
34#![allow(dead_code)]
35
36use crate::parsing::*;
37use nom::multi::count;
38use nom::number::streaming::{le_f32, le_u32};
39use serde::{Deserialize, Serialize};
40
41pub use crate::parsing::FixedSizeString;
42
43pub const TLK_STRREF_USER_OFFSET: u32 = 0x0100_0000;
44
45#[derive(Debug, Deserialize, Clone)]
46#[serde(untagged)]
47enum TlkString {
48    #[serde(skip)]
49    Ptr {
50        offset: u32,
51        size: u32,
52    },
53    Owned(String),
54}
55
56/// TLK entry data
57///
58/// Note: when the TLK is parsed using Tlk::from_bytes, the Entry's text is stored as an offset in `Tlk::strings_data: Vec<u8>`
59#[derive(Debug, Default, Deserialize, Clone)]
60pub struct Entry {
61    /// Sound file name, if any
62    pub sound_resref: Option<FixedSizeString<16>>,
63    /// Unused
64    pub _volume_variance: u32,
65    /// Unused
66    pub _pitch_variance: u32,
67    /// Sound length, if any
68    pub sound_length: Option<f32>,
69    text: Option<TlkString>,
70}
71impl Entry {
72    fn from_bytes(input: &[u8]) -> NWNParseResult<Self> {
73        let (input, flags) = le_u32(input)?;
74        let (input, sound_resref) = FixedSizeString::<16>::from_bytes(input)?;
75        let (input, _volume_variance) = le_u32(input)?;
76        let (input, _pitch_variance) = le_u32(input)?;
77        let (input, offset_to_string) = le_u32(input)?;
78        let (input, string_size) = le_u32(input)?;
79        let (input, sound_length) = le_f32(input)?;
80        Ok((
81            input,
82            Self {
83                sound_resref: if (flags & 2) > 0 {
84                    Some(sound_resref)
85                } else {
86                    None
87                },
88                _volume_variance,
89                _pitch_variance,
90                text: if (flags & 1) > 0 {
91                    Some(TlkString::Ptr {
92                        offset: offset_to_string,
93                        size: string_size,
94                    })
95                } else {
96                    None
97                },
98                sound_length: if (flags & 4) > 0 {
99                    Some(sound_length)
100                } else {
101                    None
102                },
103            },
104        ))
105    }
106
107    /// Returns true if the entry does not contain any text, sound or sound length
108    pub fn is_empty(&self) -> bool {
109        self.sound_resref.is_none() && self.text.is_none() && self.sound_length.is_none()
110    }
111
112    fn get_flags(&self) -> u32 {
113        self.text.is_some() as u32
114            + self.sound_resref.is_some() as u32 * 2
115            + self.sound_length.is_some() as u32 * 4
116    }
117
118    /// Returns a TLK entry text. May fail if the provided entry comes from another TLK
119    ///
120    /// # Panics
121    /// if the entry references data that is out of `strings_data` bounds or is invalid utf8 data
122    pub fn get_str<'a>(&'a self, strings_data: &'a [u8]) -> Option<&'a str> {
123        if let Some(text) = &self.text {
124            match text {
125                TlkString::Owned(s) => Some(s),
126                TlkString::Ptr { offset, size } => {
127                    let start = *offset as usize;
128                    let end = start + *size as usize;
129
130                    Some(
131                        std::str::from_utf8(
132                            strings_data
133                                .get(start..end)
134                                .expect("entry references out of bounds data"),
135                        )
136                        .expect("entry references invalid utf8 data"),
137                    )
138                }
139            }
140        } else {
141            None
142        }
143    }
144
145    /// Returns a TLK entry text length in bytes
146    pub fn get_str_len(&self) -> usize {
147        if let Some(text) = &self.text {
148            match text {
149                TlkString::Owned(s) => s.len(),
150                TlkString::Ptr { offset: _, size } => *size as usize,
151            }
152        } else {
153            0
154        }
155    }
156
157    /// Remove any reference to external data
158    //
159    /// # Panics
160    /// if the entry references data that is out of `strings_data` bounds or is invalid utf8 data
161    pub fn into_owned(mut self, strings_data: &[u8]) -> Self {
162        if let Some(TlkString::Ptr { offset: _, size: _ }) = &self.text {
163            self.text = Some(TlkString::Owned(
164                self.get_str(strings_data)
165                    .expect("should not fail")
166                    .to_string(),
167            ));
168        }
169        self
170    }
171
172    /// Returns a copy of this entry, that does not contain any reference to external data
173    //
174    /// # Panics
175    /// if the entry references data that is out of `strings_data` bounds or is invalid utf8 data
176    pub fn to_owned(&self, strings_data: &[u8]) -> Self {
177        self.clone().into_owned(strings_data)
178    }
179
180    /// Formats a single TLK entry to text (used by `Tlk::to_string_pretty`)
181    ///
182    /// * `index`: Entry strref
183    /// * `index_padding`: Character width of the left side containing the strref
184    /// * `user_indices`: true to display `index` as a user strref
185    /// * `strings_data`: string data vector for non-owned TLK entries
186    ///
187    /// # Panics
188    /// if the entry references data that is out of `strings_data` bounds or is invalid utf8 data
189    pub fn to_string_pretty(
190        &self,
191        index: u32,
192        index_padding: usize,
193        user_indices: bool,
194        strings_data: &[u8],
195    ) -> String {
196        let mut ret = String::new();
197
198        let index = if user_indices {
199            index + TLK_STRREF_USER_OFFSET
200        } else {
201            index
202        };
203
204        ret += &format!(
205            "{0:1$}> sound_resref={2:?} _volume_variance={3} _pitch_variance={4} \
206             sound_length={5:?}",
207            index,
208            index_padding,
209            self.sound_resref,
210            self._volume_variance,
211            self._pitch_variance,
212            self.sound_length,
213        );
214        if let Some(text) = self.get_str(strings_data) {
215            ret += "\n";
216            ret += &text
217                .lines()
218                .map(|line| {
219                    format!(
220                        "{}|{}{}",
221                        " ".repeat(index_padding),
222                        if line.is_empty() { "" } else { " " },
223                        line
224                    )
225                })
226                .collect::<Vec<_>>()
227                .join("\n");
228        }
229        ret
230    }
231}
232/// Structure for serializing TLK entries that contains TlkString::Ptr
233struct EntrySerde<'a> {
234    entry: &'a Entry,
235    strings_data: &'a [u8],
236    /// The strref used to reference this entry
237    strref: u32,
238}
239
240impl<'a> Serialize for EntrySerde<'a> {
241    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
242    where
243        S: serde::Serializer,
244    {
245        use serde::ser::SerializeStruct;
246        let mut ser_struct = serializer.serialize_struct("Entry", 6)?;
247        ser_struct.serialize_field("__hint_strref", &self.strref)?;
248        ser_struct.serialize_field("sound_resref", &self.entry.sound_resref)?;
249        ser_struct.serialize_field("_volume_variance", &self.entry._volume_variance)?;
250        ser_struct.serialize_field("_pitch_variance", &self.entry._pitch_variance)?;
251        ser_struct.serialize_field("sound_length", &self.entry.sound_length)?;
252        ser_struct.serialize_field("text", &self.entry.get_str(self.strings_data))?;
253        ser_struct.end()
254    }
255}
256
257fn default_false() -> bool {
258    false
259}
260
261/// Talktable file format (.tlk)
262///
263/// TLK entries can be stored at two locations:
264/// - In each [`Entry`], as a String.
265/// - Or in [`strings_data`], where everything is packed together. This is the
266///   default storage location when using [`Tlk::from_bytes`], as it reduces
267///   memory allocations. Entry text needs to be reallocated using
268///   [`Entry::into_owned`] in order to be modified.
269#[derive(Debug, Deserialize)]
270pub struct Tlk {
271    /// File type header
272    pub file_type: FixedSizeString<4>,
273    /// File version header
274    pub file_version: FixedSizeString<4>,
275    /// TLK language ID. IDs are different depending on NWN1/2 versions
276    pub language_id: u32,
277    /// TLK entries list
278    pub entries: Vec<Entry>,
279    /// When parsed using `Tlk::from_bytes`, contains all strings data referenced by entries
280    #[serde(skip)]
281    pub strings_data: Vec<u8>,
282    ///
283    #[serde(default = "default_false")]
284    pub hint_is_user: bool,
285}
286
287impl Tlk {
288    /// Parse TLK binary file format
289    ///
290    /// * `repair`: attempt to repair the TLK data (invalid UTF8 strings and out of bound pointers)
291    pub fn from_bytes(input: &[u8], repair: bool) -> NWNParseResult<Self> {
292        // Header
293        let (input, file_type) = FixedSizeString::<4>::from_bytes(input)?;
294        let (input, file_version) = FixedSizeString::<4>::from_bytes(input)?;
295        let (input, language_id) = le_u32(input)?;
296        let (input, string_count) = le_u32(input)?;
297        let (input, string_entries_offset) = le_u32(input)?;
298
299        let (input, mut entries) = nom_parse_context(
300            "while parsing tlk entries",
301            count(Entry::from_bytes, string_count as usize)(input),
302        )?;
303        let strings_data = input.to_vec();
304
305        // Check utf8 strings, convert to TlkString::Owned if invalid
306        // Also do strref mapping in preparation for possible JSON/YAML output
307        for (strref, entry) in entries.iter_mut().enumerate() {
308            if let Some(ref mut text) = &mut entry.text {
309                if let TlkString::Ptr { offset, size } = text {
310                    let start = *offset as usize;
311                    let mut end = start + *size as usize;
312
313                    if repair && end > strings_data.len() {
314                        if start < strings_data.len() {
315                            end = strings_data.len();
316                            *size = (end - start) as u32;
317                        } else {
318                            entry.text = None;
319                            continue;
320                        }
321                    }
322
323                    if let Some(data) = strings_data.get(start..end) {
324                        if let Err(e) = std::str::from_utf8(data) {
325                            if repair {
326                                entry.text = Some(TlkString::Owned(
327                                    String::from_utf8_lossy(data).to_string(),
328                                ));
329                            } else {
330                                return Err(nom::Err::Error(NWNParseError::from(e)));
331                            }
332                        }
333                    } else {
334                        return Err(nom::Err::Error(
335                            format!(
336                                "TLK strref {} is pointing outside of strings_data: text \
337                                 address={}, size={}",
338                                strref,
339                                string_entries_offset + *offset,
340                                size
341                            )
342                            .into(),
343                        ));
344                    }
345                } else {
346                    panic!("unexpected owned string");
347                }
348            }
349        }
350
351        Ok((
352            input,
353            Tlk {
354                file_type,
355                file_version,
356                language_id,
357                entries,
358                strings_data,
359                hint_is_user: false,
360            },
361        ))
362    }
363
364    pub fn write_stream<T>(&self, output: &mut T) -> Result<(), Box<dyn std::error::Error>>
365    where
366        T: std::io::Write + std::io::Seek,
367    {
368        output.write_all(self.file_type.as_bytes())?;
369        output.write_all(self.file_version.as_bytes())?;
370        output.write_all(&self.language_id.to_le_bytes())?;
371        output.write_all(&self.len().to_le_bytes())?; // string_count
372        output.write_all(&(20u32 + self.len() * 40u32).to_le_bytes())?; // string_entries_offset
373
374        let mut strings_end_offset = 0u32;
375        for entry in &self.entries {
376            output.write_all(&entry.get_flags().to_le_bytes())?;
377            output.write_all(
378                entry
379                    .sound_resref
380                    .as_ref()
381                    .unwrap_or(&Default::default())
382                    .as_bytes(),
383            )?;
384            output.write_all(&entry._volume_variance.to_le_bytes())?;
385            output.write_all(&entry._pitch_variance.to_le_bytes())?;
386
387            let offset: u32 = strings_end_offset;
388            let size: u32 = entry.get_str_len() as u32;
389            output.write_all(&offset.to_le_bytes())?;
390            output.write_all(&size.to_le_bytes())?;
391
392            output.write_all(&entry.sound_length.unwrap_or(0f32).to_le_bytes())?;
393
394            strings_end_offset += size;
395        }
396
397        // Preallocate
398        output.seek(std::io::SeekFrom::Current(strings_end_offset as i64))?;
399        output.write_all(&[])?;
400        output.seek(std::io::SeekFrom::Current(-(strings_end_offset as i64)))?;
401
402        for entry in &self.entries {
403            output.write_all(entry.get_str(&self.strings_data).unwrap_or("").as_bytes())?;
404        }
405
406        Ok(())
407    }
408
409    /// Serialize to TLK binary file format
410    pub fn to_bytes(&self) -> Vec<u8> {
411        let mut buf = std::io::Cursor::new(vec![]);
412        self.write_stream(&mut buf)
413            .expect("failed to allocate memory?");
414        buf.into_inner()
415    }
416    /// returns the number of entries in the TLK
417    pub fn len(&self) -> u32 {
418        self.entries.len() as u32
419    }
420    pub fn is_empty(&self) -> bool {
421        self.entries.is_empty()
422    }
423
424    /// returns a a TLK entry matching the strref. None if the strref does not exist.
425    pub fn get_entry(&self, strref: u32) -> Option<&Entry> {
426        self.entries.get(strref as usize)
427    }
428    /// returns a mutable reference to a TLK entry. The entries list is resized if needed
429    pub fn get_entry_mut(&mut self, strref: u32) -> &mut Entry {
430        if strref as usize >= self.entries.len() {
431            self.entries
432                .resize_with(strref as usize + 1, Default::default);
433        }
434        &mut self.entries[strref as usize]
435    }
436    /// Returns a TLK strref text
437    pub fn get_str(&self, strref: u32) -> Option<&str> {
438        self.get_entry(strref)?.get_str(&self.strings_data)
439    }
440    /// Set a TLK strref text
441    pub fn set_string(&mut self, strref: u32, text: Option<String>) {
442        let entry = &mut self.get_entry_mut(strref);
443        entry.text = text.map(TlkString::Owned);
444    }
445
446    /// Formats the TLK to text.
447    pub fn to_string_pretty(&self) -> String {
448        let mut ret = String::new();
449
450        ret += "============================ Header ============================\n";
451        ret += &format!(
452            "file_type={:?} file_version={:?}\n",
453            self.file_type, self.file_version
454        );
455        ret += &format!("language_id={}\n", self.language_id);
456        ret += "============================ Entries ============================\n";
457
458        let max_index = if self.hint_is_user {
459            self.entries.len() + TLK_STRREF_USER_OFFSET as usize
460        } else {
461            self.entries.len()
462        };
463        let index_padding = (max_index.ilog10() + 1) as usize;
464
465        ret += &self
466            .entries
467            .iter()
468            .enumerate()
469            .map(|(index, entry)| {
470                entry.to_string_pretty(
471                    index as u32,
472                    index_padding,
473                    self.hint_is_user,
474                    &self.strings_data,
475                )
476            })
477            .collect::<Vec<_>>()
478            .join("\n");
479        ret
480    }
481
482    /// Sets if the TLK is a base or user TLK (mostly affects serialization)
483    pub fn with_user_hint(mut self, hint: bool) -> Self {
484        self.hint_is_user = hint;
485        self
486    }
487
488    /// Change each entry to remove any reference to `strings_data`, and empty `strings_data`
489    //
490    /// # Panics
491    /// if any entry references data that is out of `strings_data` bounds or is invalid utf8 data
492    pub fn into_owned(mut self) -> Self {
493        self.entries = self
494            .entries
495            .into_iter()
496            .map(|e| e.into_owned(&self.strings_data))
497            .collect();
498        self.strings_data.clear();
499        self.strings_data.shrink_to_fit();
500        self
501    }
502
503    /// Removes trailing empty entries
504    pub fn shrink_to_fit(&mut self) {
505        let mut new_size = self.entries.len();
506
507        for (index, entry) in self.entries.iter().enumerate().rev() {
508            if entry.is_empty() {
509                new_size = index;
510            } else {
511                break;
512            }
513        }
514
515        self.entries.truncate(new_size);
516    }
517
518    /// Resizes entries list by adding empty entries if needed
519    pub fn resize(&mut self, new_len: u32) {
520        self.entries.resize_with(new_len as usize, Default::default);
521    }
522}
523
524impl Serialize for Tlk {
525    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
526    where
527        S: serde::Serializer,
528    {
529        use serde::ser::SerializeStruct;
530        let mut ser_struct = serializer.serialize_struct("Tlk", 5)?;
531        ser_struct.serialize_field("file_type", &self.file_type)?;
532        ser_struct.serialize_field("file_version", &self.file_version)?;
533        ser_struct.serialize_field("language_id", &self.language_id)?;
534        ser_struct.serialize_field("__hint_is_user", &self.hint_is_user)?;
535        ser_struct.serialize_field(
536            "entries",
537            &self
538                .entries
539                .iter()
540                .enumerate()
541                .map(|(i, e)| EntrySerde {
542                    entry: e,
543                    strings_data: &self.strings_data,
544                    strref: if !self.hint_is_user {
545                        i as u32
546                    } else {
547                        i as u32 + TLK_STRREF_USER_OFFSET
548                    },
549                })
550                .collect::<Vec<_>>(),
551        )?;
552        ser_struct.end()
553    }
554}
555
556/// TLK gender
557#[derive(Debug, Default)]
558pub enum Gender {
559    #[default]
560    Male = 0,
561    Female = 1,
562}
563
564/// Helper struct for resolving strrefs, using a base & user TLKs
565#[derive(Debug)]
566pub struct Resolver<'tlk> {
567    pub base: &'tlk Tlk,
568    pub base_f: Option<&'tlk Tlk>,
569    pub user: Option<&'tlk Tlk>,
570}
571impl<'tlk> Resolver<'tlk> {
572    /// Create a new resolver using multiple TLK files
573    pub fn new(base: &'tlk Tlk, base_f: Option<&'tlk Tlk>, user: Option<&'tlk Tlk>) -> Self {
574        Resolver { base, base_f, user }
575    }
576    /// Returns the TLK used to resolve the given strref
577    pub fn get_tlk_for_strref(&self, strref: u32, gender: Gender) -> (Option<&Tlk>, u32) {
578        if strref < TLK_STRREF_USER_OFFSET {
579            match gender {
580                Gender::Male => (Some(self.base), strref),
581                Gender::Female => (Some(self.base_f.as_ref().unwrap_or(&self.base)), strref),
582            }
583        } else {
584            let strref = strref - TLK_STRREF_USER_OFFSET;
585            if let Some(tlk) = &self.user {
586                (Some(tlk), strref)
587            } else {
588                (None, strref)
589            }
590        }
591    }
592    /// Returns the TLK entry corresponding to the given strref
593    pub fn get_entry(&self, strref: u32, gender: Gender) -> Option<&Entry> {
594        let (tlk, strref) = self.get_tlk_for_strref(strref, gender);
595        if let Some(tlk) = tlk {
596            if strref < tlk.len() {
597                tlk.get_entry(strref)
598            } else {
599                None
600            }
601        } else {
602            None
603        }
604    }
605    /// Returns the text corresponding to the given strref
606    pub fn get_str(&self, strref: u32, gender: Gender) -> Option<&str> {
607        let (tlk, strref) = self.get_tlk_for_strref(strref, gender);
608        if let Some(tlk) = tlk {
609            tlk.get_str(strref)
610        } else {
611            None
612        }
613    }
614}
615
616#[cfg(test)]
617mod tests {
618    use super::*;
619
620    #[test]
621    fn test_user_tlk() {
622        let tlk_bytes = include_bytes!("../unittest/user.tlk");
623
624        let tlk = Tlk::from_bytes(tlk_bytes, false).unwrap().1;
625        assert_eq!(tlk.get_str(0), Some("Hello world"));
626        assert_eq!(tlk.get_str(1), Some("Café liégeois"));
627        assert_eq!(tlk.get_str(2), None);
628        assert_eq!(tlk.get_str(3), Some("Custom sound"));
629        assert_eq!(
630            tlk.get_entry(3)
631                .unwrap()
632                .sound_resref
633                .as_ref()
634                .unwrap()
635                .as_str(),
636            "snd_custom"
637        );
638        assert_eq!(tlk.get_str(10), None);
639        assert_eq!(tlk.len(), 4);
640
641        let tlk_bytes_ser = tlk.to_bytes();
642        assert_eq!(tlk_bytes_ser, tlk_bytes);
643
644        // Extend tlk table
645        let mut tlk = Tlk::from_bytes(&tlk_bytes_ser, false).unwrap().1;
646        tlk.set_string(0, Some("Yolo".to_string()));
647        tlk.set_string(1, Some("Sôm€ ütf8 🐧".to_string()));
648        tlk.set_string(9, Some("Extend this tlk !".to_string()));
649
650        let tlk_bytes_ser = tlk.to_bytes();
651        let tlk = Tlk::from_bytes(&tlk_bytes_ser, false).unwrap().1;
652        assert_eq!(tlk.len(), 10);
653        assert_eq!(tlk.get_str(0), Some("Yolo"));
654        assert_eq!(tlk.get_str(1), Some("Sôm€ ütf8 🐧"));
655        assert_eq!(tlk.get_str(2), None);
656        assert_eq!(tlk.get_str(9), Some("Extend this tlk !"));
657    }
658
659    #[test]
660    fn test_dialog_tlk() {
661        let tlk_bytes = include_bytes!("../unittest/dialog.tlk");
662
663        let tlk = Tlk::from_bytes(tlk_bytes, false).unwrap().1;
664        assert_eq!(tlk.get_str(0), Some("Bad Strref"));
665        assert_eq!(tlk.get_str(1), Some("Barbares"));
666        assert_eq!(tlk.get_str(10), Some("Moine"));
667        assert_eq!(tlk.get_str(30), Some("Demi-elfe"));
668        assert_eq!(
669            tlk.get_str(36),
670            Some(concat!(
671                "La dernière tentative pour détecter les paramètres de votre ",
672                "système a échoué. Cela indique généralement une incompatibilité ",
673                "entre Neverwinter Nights 2 et vos pilotes vidéo actuels. Veuillez ",
674                "consultez le site internet du fabriquant de votre carte vidéo pour ",
675                "télécharger les pilotes les plus récents.\n\nVous pouvez :\n\n1. ",
676                "Cliquer sur \"Continuer\" si vous pensez que Neverwinter Nights 2 ",
677                "fonctionnera sur votre système. Si le jeu ne démarre pas, relancez ",
678                "le programme de configuration et essayez une autre option.\n\n2. ",
679                "Cliquez sur \"Assistance\" pour lancer votre navigateur Internet ",
680                "afin d'accéder au site web de l'assistance technique de ",
681                "Neverwinter Nights 2.\n\n3. Cliquez sur \"LisezMoi\" et consultez ",
682                "la section compatibilité pour savoir s'il n'y a pas de problèmes ",
683                "de compatibilité entre Neverwinter Nights 2 et votre ",
684                "système.\n\n4. Cliquez sur \"Quitter\" pour fermer le ",
685                "programme."
686            ))
687        );
688        assert_eq!(tlk.get_str(3000), None);
689
690        let tlk_bytes_ser = tlk.to_bytes();
691        assert_eq!(tlk_bytes_ser, tlk_bytes);
692    }
693
694    #[test]
695    fn test_repair_tlk() {
696        let tlk_bytes = include_bytes!("../unittest/user.broken.tlk");
697
698        let tlk = Tlk::from_bytes(tlk_bytes, true).unwrap().1;
699        assert_eq!(tlk.get_str(0), Some("Some in�(id UTF8 magic"));
700        assert_eq!(
701            tlk.get_str(1),
702            Some("Some text with bad sizeAnother text but with bad offset")
703        );
704        assert_eq!(tlk.get_str(2), None);
705    }
706
707    #[test]
708    fn test_tlk_resolver() {
709        let base = Tlk::from_bytes(include_bytes!("../unittest/dialog.tlk"), false)
710            .unwrap()
711            .1;
712        let user = Tlk::from_bytes(include_bytes!("../unittest/user.tlk"), false)
713            .unwrap()
714            .1;
715        let resolver = Resolver::new(&base, None, Some(&user));
716
717        assert_eq!(resolver.get_str(0, Gender::Male), Some("Bad Strref"));
718        assert_eq!(resolver.get_str(1, Gender::Male), Some("Barbares"));
719        assert_eq!(resolver.get_str(1_000_000, Gender::Male), None);
720        assert_eq!(
721            resolver.get_str(16_777_216, Gender::Male),
722            Some("Hello world")
723        );
724        assert_eq!(
725            resolver.get_str(16_777_217, Gender::Male),
726            Some("Café liégeois")
727        );
728        assert_eq!(resolver.get_str(17_777_216, Gender::Male), None);
729    }
730
731    // extern crate test;
732    // #[bench]
733    // fn bench_dialog_tlk(b: &mut test::Bencher) {
734    //     b.iter(test_dialog_tlk);
735    // }
736}