nwn_lib_rs/
twoda.rs

1//! 2DA table file format (.2da)
2//!
3//! Example
4//! ```rust
5//! #![feature(generic_const_exprs)]
6//! #![allow(incomplete_features)]
7//! use nwn_lib_rs::twoda::TwoDA;
8//!
9//! // Load a 2DA file
10//! let twoda_data = std::fs::read_to_string("unittest/restsys_wm_tables.2da").unwrap();
11//! let (_, twoda) = TwoDA::from_str(&twoda_data, true).unwrap();
12//!
13//! // Get 2DA value
14//! assert_eq!(twoda.get_row_label(5, "TableName").as_deref(), Some("UpperBarrow"));
15//! assert_eq!(twoda.get_row_label(5, "Enc1_ResRefs").as_deref(), Some("a_telthorwolf,a_telthorwolf"));
16//!
17//! // Serialize beautiful 2DA data
18//! let twoda_data = twoda.to_string(false);
19//! ```
20//!
21
22use std::cmp::max;
23use std::collections::{BTreeMap, HashMap};
24
25use crate::parsing::*;
26
27use nom::character::complete as ncc;
28use nom::error::VerboseError;
29use nom::IResult;
30
31use serde::{Deserialize, Serialize};
32
33#[derive(Debug, Deserialize, Serialize)]
34struct TwoDARepr {
35    file_type: FixedSizeString<4>,
36    file_version: FixedSizeString<4>,
37    default_value: Option<String>,
38    columns: Vec<String>,
39    rows: Vec<Vec<Option<String>>>,
40}
41impl From<TwoDARepr> for TwoDA {
42    fn from(repr: TwoDARepr) -> Self {
43        let column_lookup = repr
44            .columns
45            .iter()
46            .enumerate()
47            .map(|(i, col)| (col.to_lowercase(), i))
48            .collect::<HashMap<_, _>>();
49        let data = repr.rows.into_iter().flatten().collect(); // TODO: this does not handle well inconsistent row/col numbers
50        Self {
51            file_type: repr.file_type,
52            file_version: repr.file_version,
53            default_value: repr.default_value,
54            columns: repr.columns.into_iter().collect(),
55            column_lookup,
56            data,
57            merge_meta: None,
58        }
59    }
60}
61impl From<TwoDA> for TwoDARepr {
62    fn from(twoda: TwoDA) -> Self {
63        let rows: Vec<Vec<Option<String>>> = twoda.iter().map(|row| row.to_vec()).collect();
64        Self {
65            file_type: twoda.file_type,
66            file_version: twoda.file_version,
67            default_value: twoda.default_value,
68            columns: twoda.columns,
69            rows,
70        }
71    }
72}
73
74/// 2DA table file
75#[derive(Debug, Clone, Deserialize, Serialize)]
76#[serde(from = "TwoDARepr", into = "TwoDARepr")]
77pub struct TwoDA {
78    /// File type (2DA)
79    pub file_type: FixedSizeString<4>,
80    /// File version
81    pub file_version: FixedSizeString<4>,
82    pub default_value: Option<String>,
83    columns: Vec<String>,
84    column_lookup: HashMap<String, usize>,
85    data: Vec<Option<String>>,
86    merge_meta: Option<MergeMetadata>,
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
90pub enum MergeAction {
91    Ignore,
92    Expect,
93    Set,
94}
95
96#[derive(Debug, Clone)]
97struct MergeMetadata {
98    /// If a row number is referenced here, only columns with a true mask must be merged
99    merge_actions: BTreeMap<usize, Vec<MergeAction>>,
100}
101
102impl TwoDA {
103    /// Parse 2DA data an input str
104    ///
105    /// # Args:
106    /// * input: data to parse
107    /// * repair: true to repair inconsistent 2DA data (bad column count, row indices, ...)
108    pub fn from_str(input: &str, repair: bool) -> IResult<&str, Self, VerboseError<&str>> {
109        // 2DA V2.0
110        //
111        //     Terrain               R    G    B    Material STR_REF
112        // 0   TT_GD_Dirt_01         124  109  27   Dirt     ****
113        // 1   TT_GD_Dirt_02         84   78   17   Dirt     ****
114
115        let (input, twoda_header) = ncc::not_line_ending(input)?;
116        let (input, _) = ncc::line_ending(input)?;
117        let file_type = FixedSizeString::<4>::from_str(
118            twoda_header
119                .get(0..4)
120                .ok_or(nom_context_error("Missing file type", input))?,
121        )
122        .unwrap(); // string cannot be too long
123        let file_version = FixedSizeString::<4>::from_str(
124            twoda_header
125                .get(4..8)
126                .ok_or(nom_context_error("Missing file version", input))?,
127        )
128        .unwrap(); // string cannot be too long
129
130        let mut merge_meta = if file_type == "2DAM" {
131            Some(MergeMetadata {
132                merge_actions: BTreeMap::new(),
133            })
134        } else {
135            None
136        };
137
138        enum ParserState {
139            Defaults,
140            Headers,
141            Rows,
142        }
143
144        // Data
145        let mut twoda_data = vec![];
146        let mut state = ParserState::Defaults;
147        let mut default_value = None;
148        let mut columns = vec![];
149        let mut input = input;
150        while input.len() > 0 {
151            // Consume entire line
152            let line;
153            (input, line) = ncc::not_line_ending(input)?;
154            if input.len() > 0 {
155                (input, _) = ncc::line_ending(input)?;
156            }
157
158            // Skip empty lines (do not increment row index)
159            if line.is_empty() || line.chars().all(|c| c.is_whitespace()) {
160                continue;
161            }
162
163            // Handle default value
164            if let ParserState::Defaults = state {
165                if line.starts_with("DEFAULT:") {
166                    (_, default_value) = Self::field_parser(line)?;
167                    state = ParserState::Headers;
168                    continue;
169                } else {
170                    state = ParserState::Headers;
171                    // continue parsing this line
172                }
173            }
174
175            match state {
176                ParserState::Defaults => panic!("handled previously"),
177                ParserState::Headers => {
178                    columns = Self::parse_row(line)?
179                        .1
180                        .into_iter()
181                        .map(|c| c.unwrap_or("".to_string()))
182                        .collect::<Vec<_>>();
183                    if columns.len() == 0 {
184                        return Err(nom_context_error("no columns detected", input));
185                    }
186                    state = ParserState::Rows;
187                }
188                ParserState::Rows => {
189                    let (mut row_data, row_merge_actions): (_, Option<Vec<_>>) =
190                        if merge_meta.is_none() {
191                            (Self::parse_row(line)?.1, None)
192                        } else {
193                            Self::parse_merge_row(line)?.1
194                        };
195
196                    if row_data.is_empty() {
197                        return Err(nom_context_error("missing row number", line));
198                    }
199
200                    let row_index_res = row_data.remove(0).map(|s| s.parse::<usize>());
201
202                    let row_index = if let Some(Ok(idx)) = row_index_res {
203                        idx
204                    } else if repair {
205                        0
206                    } else {
207                        return Err(nom_context_error("failed to parse row number", line));
208                    };
209
210                    if row_data.len() != columns.len() {
211                        if repair {
212                            row_data.resize(columns.len(), None);
213                        } else {
214                            return Err(nom_context_error("Invalid number of columns", line));
215                        }
216                    }
217
218                    if let Some(ref mut merge_meta) = merge_meta {
219                        // Trust row indices
220                        let start = row_index * columns.len();
221                        let end = start + columns.len();
222                        if twoda_data.len() < end {
223                            twoda_data.resize(end, None);
224                        }
225
226                        twoda_data[start..end].clone_from_slice(&row_data);
227
228                        // Save merge mask if any
229                        if let Some(mut row_merge_actions) = row_merge_actions {
230                            // Remove ? "merge action" for index number
231                            row_merge_actions.remove(0);
232
233                            if row_merge_actions.len() != row_data.len() {
234                                row_merge_actions.resize(row_data.len(), MergeAction::Ignore);
235                            }
236                            merge_meta
237                                .merge_actions
238                                .insert(row_index, row_merge_actions);
239                        }
240                    } else {
241                        if row_index != twoda_data.len() / columns.len() && !repair {
242                            return Err(nom_context_error("Inconsistent row index", line));
243                        }
244
245                        // Copy all data except row number field
246                        twoda_data.extend_from_slice(&row_data);
247                    }
248                }
249            }
250        }
251
252        let column_lookup = columns
253            .iter()
254            .enumerate()
255            .map(|(i, col)| (col.to_lowercase(), i))
256            .collect::<HashMap<_, _>>();
257
258        Ok((
259            input,
260            Self {
261                file_type,
262                file_version,
263                default_value,
264                columns,
265                column_lookup,
266                data: twoda_data,
267                merge_meta,
268            },
269        ))
270    }
271    /// Serializes the 2DA table
272    ///
273    /// # Args:
274    /// * compact: true will generate minified 2DA data. NWN2 will be able to
275    ///   parse this table, but this parser will require repair=true to be set
276    ///   when parsing
277    pub fn to_string(&self, compact: bool) -> String {
278        let mut ret = String::new();
279        ret.push_str("2DA V2.0\n\n");
280        ret.push_str(&Self::encode_rows(
281            &self.iter().enumerate().collect::<Vec<_>>(),
282            Some(&self.columns),
283            compact,
284        ));
285
286        ret
287    }
288
289    /// Returns the columns list
290    pub fn get_columns(&self) -> &Vec<String> {
291        &self.columns
292    }
293
294    /// Returns the number of columns
295    pub fn len_cols(&self) -> usize {
296        self.columns.len()
297    }
298
299    /// Returns the number of rows
300    pub fn len_rows(&self) -> usize {
301        self.data.len() / self.columns.len()
302    }
303
304    /// Resize the 2DA table to be able to contain `len` rows
305    pub fn resize_rows(&mut self, len: usize) {
306        self.data.resize(len * self.len_cols(), None)
307    }
308
309    /// Returns a row as a list of 2DA fields
310    pub fn get_row(&self, row: usize) -> Option<&[Option<String>]> {
311        let start = row * self.columns.len();
312        let end = start + self.columns.len();
313        self.data.get(start..end)
314    }
315
316    /// Returns a row as a list of 2DA fields
317    pub fn get_row_mut(&mut self, row: usize) -> Option<&mut [Option<String>]> {
318        let start = row * self.columns.len();
319        let end = start + self.columns.len();
320        self.data.get_mut(start..end)
321    }
322
323    /// Returns a field using its row index and column index
324    pub fn get_row_col(&self, row: usize, column: usize) -> &Option<String> {
325        let index = row * self.columns.len() + column;
326        if let Some(field) = self.data.get(index) {
327            field
328        } else {
329            &None
330        }
331    }
332
333    /// Returns a field using its row index and column name. Column names are case-insensitive
334    pub fn get_row_label(&self, row: usize, label: &str) -> &Option<String> {
335        // TODO: only call to_lowercase if not lowercase
336        if let Some(col) = self.column_lookup.get(&label.to_string().to_lowercase()) {
337            self.get_row_col(row, *col)
338        } else {
339            &None
340        }
341    }
342
343    fn str_encoded_len<S: AsRef<str>>(s: S) -> usize {
344        let s = s.as_ref();
345        if s == "****" || s.chars().any(char::is_whitespace) {
346            // Quoted field
347            let mut len = 2usize; // Two double quotes
348            for c in s.chars() {
349                match c {
350                    '\\' => len += 1,
351                    '\n' => len += 1,
352                    '\t' => len += 1,
353                    '"' => len += 1,
354                    _ => {}
355                }
356                len += c.len_utf8();
357            }
358            len
359        } else {
360            // Raw field
361            s.len()
362        }
363    }
364    /// Returns the length of a 2DA field when encoded (using quotes and escape sequences when needed)
365    pub fn field_encoded_len<S: AsRef<str>>(field: &Option<S>) -> usize {
366        match field.as_ref() {
367            None => 4,
368            Some(s) => Self::str_encoded_len(s),
369        }
370    }
371
372    /// Encode a 2DA field, adding quotes when needed, and returns the resulting string
373    pub fn encode_field<S: AsRef<str>>(field: &Option<S>) -> String {
374        // TODO: Return either &str or String to avoid unnecessary allocations?
375        match field {
376            None => "****".to_string(),
377            Some(s) => {
378                let s = s.as_ref();
379                if s == "****" || s.chars().any(char::is_whitespace) {
380                    // Quoted field
381                    let mut res = "\"".to_string();
382                    for c in s.chars() {
383                        match c {
384                            '\\' => res.push_str(r"\\"),
385                            '\n' => res.push_str(r"\n"),
386                            '\t' => res.push_str(r"\t"),
387                            '"' => res.push_str("\\\""),
388                            _ => res.push(c),
389                        }
390                    }
391                    res.push('\"');
392                    res
393                } else {
394                    // Raw field
395                    s.to_string()
396                }
397            }
398        }
399    }
400
401    fn field_parser(input: &str) -> IResult<&str, Option<String>, VerboseError<&str>> {
402        if input.is_empty() || input.chars().all(char::is_whitespace) {
403            return Err(nom_context_error("End of input", input));
404        }
405
406        if let Some(input) = input.strip_prefix('"') {
407            let mut res = String::new();
408            let mut escaped = false;
409            let mut input_end = 0usize;
410            for c in input.chars() {
411                input_end += c.len_utf8();
412                if escaped {
413                    escaped = false;
414                    match c {
415                        '\\' => res.push('\\'),
416                        'n' => res.push('\n'),
417                        't' => res.push('\t'),
418                        '"' => res.push('"'),
419                        c => res.extend(&['\\', c]),
420                    }
421                } else {
422                    match c {
423                        '\\' => escaped = true,
424                        '"' => break,
425                        c => res.push(c),
426                    }
427                }
428            }
429            let input = &input[input_end..];
430            // eprintln!("   => field_parser end:   input={:?}", input);
431            Ok((input, Some(res)))
432        } else {
433            let end = input.find(char::is_whitespace);
434            let (input, res) = if let Some(end) = end {
435                (&input[end..], &input[..end])
436            } else {
437                (&input[input.len()..], input)
438            };
439
440            // eprintln!("   => field_parser end:   input={:?}", input);
441            if res == "****" {
442                Ok((input, None))
443            } else {
444                Ok((input, Some(res.to_string())))
445            }
446        }
447    }
448
449    /// Parses a 2DA row returning a list of fields (including the row index number)
450    pub fn parse_row(input: &str) -> IResult<&str, Vec<Option<String>>, VerboseError<&str>> {
451        let (input, _) = ncc::space0(input)?;
452        nom::multi::separated_list0(ncc::space1, Self::field_parser)(input)
453    }
454
455    /// Parses a 2DAM row returning a list of fields and merge mask
456    fn parse_merge_row(
457        input: &str,
458    ) -> IResult<&str, (Vec<Option<String>>, Option<Vec<MergeAction>>), VerboseError<&str>> {
459        let (input, _) = ncc::space0(input)?;
460
461        if input.starts_with('?') {
462            let (input, fields_meta) = nom::multi::separated_list0(
463                ncc::space1,
464                nom::sequence::tuple((ncc::one_of("?=-"), Self::field_parser)),
465            )(input)?;
466
467            let (mda, fields) = fields_meta
468                .into_iter()
469                .map(|(c, field)| {
470                    let action = match c {
471                        '?' => MergeAction::Expect,
472                        '-' => MergeAction::Ignore,
473                        '=' => MergeAction::Set,
474                        _ => panic!(),
475                    };
476                    (action, field)
477                })
478                .unzip();
479
480            Ok((input, (fields, Some(mda))))
481
482            // unimplemented!()
483        } else {
484            let (input, fields) = Self::parse_row(input)?;
485            Ok((input, (fields, None)))
486        }
487    }
488
489    /// Serializes a row to string
490    ///
491    /// # Args:
492    /// * row_index: Row index number
493    /// * row_data: List of row fields (without the row index)
494    /// * column_sizes: If Some, the sizes will be used for padding the different fields, to align multiple columns together
495    /// * compact: true to minify the row by removing empty trailing fields. Note: column_sizes is used even if compact is true
496    pub fn encode_row<S: AsRef<str>>(
497        row_index: usize,
498        row_data: &[Option<S>],
499        column_sizes: Option<&[usize]>,
500        compact: bool,
501    ) -> String {
502        let get_col_size = |i: usize| {
503            if let Some(column_sizes) = column_sizes {
504                assert_eq!(row_data.len() + 1, column_sizes.len());
505                column_sizes[i]
506            } else {
507                0
508            }
509        };
510
511        let mut ret = String::new();
512        ret.push_str(&format!("{:<w$}", row_index, w = get_col_size(0)));
513
514        let end = if compact {
515            let last_column = row_data
516                .iter()
517                .enumerate()
518                .rfind(|&(_, field)| field.is_some());
519            if let Some((i, _)) = last_column {
520                i + 1
521            } else {
522                1
523            }
524        } else {
525            row_data.len()
526        };
527
528        ret.push_str(
529            &row_data[..end]
530                .iter()
531                .enumerate()
532                .map(|(i, field)| {
533                    format!("{:<w$}", Self::encode_field(field), w = get_col_size(i + 1))
534                })
535                .fold(String::new(), |mut res, s| {
536                    res.push(' ');
537                    res.push_str(&s);
538                    res
539                }),
540        );
541        ret
542    }
543    /// Serializes a list of row to string
544    ///
545    /// # Args:
546    /// * rows: List of tuples containing the row index + fields list
547    /// * columns: If Some, adds the columns list atop if the rows
548    /// * compact: true to minify the output by not aligning columns and removing trailing empty fields
549    pub fn encode_rows<S1: AsRef<str>, S2: AsRef<str>>(
550        rows: &[(usize, &[Option<S1>])],
551        columns: Option<&[S2]>,
552        compact: bool,
553    ) -> String {
554        let mut column_sizes = vec![
555            0usize;
556            rows.iter()
557                .map(|(_, cols)| cols.len())
558                .max()
559                .expect("no rows to encode")
560                + 1
561        ];
562
563        if !compact {
564            column_sizes[0] = rows
565                .iter()
566                .map(|(i, _)| i)
567                .max()
568                .expect("no rows to encode")
569                .ilog10() as usize
570                + 1;
571            if let Some(columns) = columns {
572                for (i, column) in columns.iter().enumerate() {
573                    if i + 1 == columns.len() {
574                        continue; // Last column must not get padded
575                    }
576                    column_sizes[i + 1] = max(column_sizes[i + 1], Self::str_encoded_len(column));
577                }
578            }
579
580            for (_irow, row) in rows {
581                for (ifield, field) in row.iter().enumerate() {
582                    if ifield + 2 == column_sizes.len() {
583                        continue; // Last column must not get padded
584                    }
585                    column_sizes[ifield + 1] =
586                        max(column_sizes[ifield + 1], Self::field_encoded_len(field));
587                }
588            }
589        }
590
591        let mut ret = String::new();
592
593        if let Some(columns) = columns {
594            ret.push_str(&" ".repeat(column_sizes[0]));
595            ret.push_str(
596                &columns
597                    .iter()
598                    .enumerate()
599                    .map(|(i, col)| format!("{:<w$}", col.as_ref(), w = column_sizes[i + 1]))
600                    .fold(String::new(), |mut res, s| {
601                        res.push(' ');
602                        res.push_str(&s);
603                        res
604                    }),
605            );
606            ret.push('\n');
607        }
608
609        for (i, (irow, row)) in rows.iter().enumerate() {
610            ret.push_str(&Self::encode_row(*irow, row, Some(&column_sizes), compact));
611            if i + 1 < rows.len() {
612                ret.push('\n');
613            }
614        }
615        ret
616    }
617
618    /// Returns an iterator over each 2DA row
619    pub fn iter(&self) -> std::slice::Chunks<'_, Option<String>> {
620        self.data.chunks(self.columns.len())
621    }
622
623    /// Returns true if the 2DA is meant to be merged (2DAM file type)
624    pub fn is_merge(&self) -> bool {
625        self.merge_meta.is_some()
626    }
627
628    /// Returns the merge mask for the given row, if set. Only useful if it was parsed as a 2DAM file
629    pub fn get_row_merge_actions(&self, row_index: usize) -> Option<&[MergeAction]> {
630        if let Some(meta) = self.merge_meta.as_ref() {
631            meta.merge_actions.get(&row_index).map(Vec::as_slice)
632        } else {
633            None
634        }
635    }
636}
637
638#[cfg(test)]
639mod tests {
640    use super::*;
641
642    #[test]
643    fn test_twoda() {
644        let twoda_bytes = include_str!("../unittest/restsys_wm_tables.2da");
645
646        let twoda = TwoDA::from_str(twoda_bytes, true).unwrap().1;
647        assert_eq!(
648            twoda.get_row_label(0, "TableName"),
649            &Some("INVALID_TABLE".to_string())
650        );
651        assert_eq!(
652            twoda.get_row_label(0, "Comment"),
653            &Some(",start w/ comma (makes excel add quotes!) Dont_change this row!".to_string())
654        );
655        assert_eq!(
656            twoda.get_row_label(0, "FeedBackStrRefSuccess"),
657            &Some("83306".to_string())
658        );
659        assert_eq!(twoda.get_row_label(0, "Enc3_ResRefs"), &None);
660        assert_eq!(
661            twoda.get_row_label(21, "TableName"),
662            &Some("b_wells_of_lurue".to_string())
663        );
664        assert_eq!(
665            twoda.get_row_label(21, "Enc3_ResRefs"),
666            &Some("b10_earth_elemental,b10_orglash".to_string())
667        );
668
669        // Bad number of columns
670        // Row 10 has only 8 columns instead of 9
671        assert!(TwoDA::from_str(twoda_bytes, false).is_err());
672        assert_eq!(
673            twoda.get_row_label(10, "TableName"),
674            &Some("c_cave".to_string())
675        );
676        assert_eq!(
677            twoda.get_row_label(10, "Comment"),
678            &Some(",beasts".to_string())
679        );
680        assert_eq!(
681            twoda.get_row_label(10, "Enc2_Prob"),
682            &Some("50".to_string())
683        );
684        assert_eq!(twoda.get_row_label(10, "Enc3_ResRefs"), &None);
685    }
686    #[test]
687    fn test_bloodmagus() {
688        // Contains CRLF line endings
689        let twoda_bytes = include_str!("../unittest/cls_feat_bloodmagus.2DA");
690        let twoda = TwoDA::from_str(twoda_bytes, false).unwrap().1;
691
692        assert_eq!(twoda.columns.len(), 5);
693        assert_eq!(twoda.len_rows(), 22);
694
695        assert_eq!(twoda.columns[0], "FeatLabel");
696        assert_eq!(
697            twoda.get_row_label(0, "FeatLabel"),
698            &Some("FEAT_EPIC_SPELL_DRAGON_KNIGHT".to_string())
699        );
700        assert_eq!(twoda.columns[1], "FeatIndex");
701        assert_eq!(
702            twoda.get_row_label(8, "FeatIndex"),
703            &Some("2784".to_string())
704        );
705    }
706    #[test]
707    fn test_savthr_nw9a() {
708        // Contains empty lines between column names and data
709        let twoda_bytes = include_str!("../unittest/cls_savthr_nw9a.2DA");
710        let twoda = TwoDA::from_str(twoda_bytes, false).unwrap().1;
711
712        assert_eq!(twoda.columns.len(), 4);
713        assert_eq!(twoda.len_rows(), 5);
714
715        assert_eq!(twoda.columns[0], "Level");
716
717        assert_eq!(
718            twoda.get_row(0).unwrap(),
719            &[
720                Some("1".to_string()),
721                Some("2".to_string()),
722                Some("0".to_string()),
723                Some("2".to_string())
724            ]
725        );
726        assert_eq!(
727            twoda.get_row(1).unwrap(),
728            &[
729                Some("2".to_string()),
730                Some("3".to_string()),
731                Some("0".to_string()),
732                Some("3".to_string())
733            ]
734        );
735        assert_eq!(
736            twoda.get_row(3).unwrap(),
737            &[
738                Some("4".to_string()),
739                Some("4".to_string()),
740                Some("1".to_string()),
741                Some("4".to_string())
742            ]
743        );
744    }
745    #[test]
746    fn test_nwn_2_colors() {
747        // No newline on data end
748        let twoda_bytes = include_str!("../unittest/NWN2_Colors.2DA");
749        let twoda = TwoDA::from_str(twoda_bytes, false).unwrap().1;
750
751        assert_eq!(twoda.columns.as_slice(), &["ColorName", "ColorHEX"]);
752        assert_eq!(twoda.len_rows(), 153);
753
754        assert_eq!(
755            twoda.get_row(0).unwrap(),
756            &[Some("AliceBlue".to_string()), Some("F0F8FF".to_string()),]
757        );
758        assert_eq!(
759            twoda.get_row(152).unwrap(),
760            &[
761                Some("HotbarItmNoUse".to_string()),
762                Some("FF0000".to_string()),
763            ]
764        );
765    }
766    #[test]
767    fn test_cls_bfeat_dwdef() {
768        let twoda_bytes = include_str!("../unittest/cls_bfeat_dwdef.2da");
769        let twoda = TwoDA::from_str(twoda_bytes, true).unwrap().1;
770
771        assert_eq!(twoda.columns.as_slice(), &["Bonus"]);
772        assert_eq!(twoda.len_rows(), 60);
773    }
774    #[test]
775    fn test_combatmodes() {
776        // Has random text notes near the end
777        let twoda_bytes = include_str!("../unittest/combatmodes.2da");
778        let twoda = TwoDA::from_str(twoda_bytes, true).unwrap().1;
779
780        assert_eq!(
781            twoda.columns.as_slice(),
782            &[
783                "CombatMode",
784                "ActivatedName",
785                "DeactivatedName",
786                "NeedsTarget"
787            ]
788        );
789        assert_eq!(twoda.len_rows(), 19);
790        assert_eq!(
791            twoda.get_row(0).unwrap(),
792            &[
793                Some("Detect".to_string()),
794                Some("58275".to_string()),
795                Some("58276".to_string()),
796                Some("0".to_string())
797            ]
798        );
799        assert_eq!(
800            twoda.get_row(13).unwrap(),
801            &[
802                Some("Taunt".to_string()),
803                Some("11468".to_string()),
804                Some("11468".to_string()),
805                Some("0".to_string())
806            ]
807        );
808        assert_eq!(
809            twoda.get_row(14).unwrap(),
810            &[Some("Attack".to_string()), None, None, None]
811        );
812        assert_eq!(
813            twoda.get_row(15).unwrap(),
814            &[Some("Power_Attack".to_string()), None, None, None]
815        );
816        assert_eq!(
817            twoda.get_row(18).unwrap(),
818            &[
819                Some("stealth,".to_string()),
820                Some("counterspell,".to_string()),
821                Some("defensive_Cast,".to_string()),
822                Some("taunt".to_string())
823            ]
824        );
825    }
826    #[test]
827    fn test_creaturespeed() {
828        // Has missing default line
829        let twoda_bytes = include_str!("../unittest/creaturespeed.2da");
830        let twoda = TwoDA::from_str(twoda_bytes, true).unwrap().1;
831
832        assert_eq!(
833            twoda.columns.as_slice(),
834            &["Label", "Name", "2DAName", "WALKRATE", "RUNRATE"]
835        );
836        assert_eq!(twoda.len_rows(), 9);
837
838        assert_eq!(
839            twoda.get_row(0).unwrap(),
840            &[
841                Some("PC_Movement".to_string()),
842                None,
843                Some("PLAYER".to_string()),
844                Some("2.00".to_string()),
845                Some("4.00".to_string())
846            ]
847        );
848    }
849    #[test]
850    fn test_des_treas_items() {
851        // Has missing / non-numeric row numbers
852        let twoda_bytes = include_str!("../unittest/des_treas_items.2da");
853        let twoda = TwoDA::from_str(twoda_bytes, true).unwrap().1;
854
855        assert_eq!(twoda.len_cols(), 59);
856        assert_eq!(twoda.len_rows(), 25);
857
858        assert_eq!(twoda.get_row_col(0, 0), &Some("2".to_string()));
859        assert_eq!(twoda.get_row_col(1, 0), &Some("NW_WSWMGS003".to_string()));
860    }
861    // #[test]
862    // fn test_nwn2_tips() {
863    //     // Has invalid UTF8 sequences
864    //     let twoda_bytes = include_bytes!("../unittest/nwn2_tips.2da");
865    //     let twoda = TwoDA::from_bytes(, true).unwrap().1;
866    // }
867
868    #[test]
869    fn test_twodam() {
870        let twoda_bytes = include_str!("../unittest/restsys_wm_tables_merge.2da");
871        let twoda = TwoDA::from_str(twoda_bytes, true).unwrap().1;
872        assert_eq!(
873            twoda.get_row_label(11, "TableName"),
874            &Some("conflicting".to_string())
875        );
876        assert_eq!(twoda.get_row_merge_actions(11), None);
877
878        assert_eq!(
879            twoda.get_row_label(12, "TableName"),
880            &Some("force".to_string())
881        );
882        assert_eq!(
883            twoda.get_row_merge_actions(12),
884            Some(
885                [
886                    MergeAction::Expect,
887                    MergeAction::Set,
888                    MergeAction::Set,
889                    MergeAction::Ignore,
890                    MergeAction::Ignore,
891                    MergeAction::Ignore,
892                    MergeAction::Ignore,
893                    MergeAction::Ignore,
894                    MergeAction::Ignore
895                ]
896                .as_slice()
897            )
898        );
899
900        assert_eq!(
901            twoda.get_row_label(19, "Comment"),
902            &Some(",Death Knights, Mummified Priests, Dread Wraiths".to_string())
903        );
904        assert_eq!(
905            twoda.get_row_label(19, "FeedBackStrRefSuccess"),
906            &Some("hello world".to_string())
907        );
908        assert_eq!(
909            twoda.get_row_merge_actions(19),
910            Some(
911                [
912                    MergeAction::Ignore,
913                    MergeAction::Expect,
914                    MergeAction::Set,
915                    MergeAction::Ignore,
916                    MergeAction::Ignore,
917                    MergeAction::Ignore,
918                    MergeAction::Ignore,
919                    MergeAction::Ignore,
920                    MergeAction::Ignore
921                ]
922                .as_slice()
923            )
924        );
925
926        assert_eq!(
927            twoda.get_row_label(20, "TableName"),
928            &Some("b_lower_vault".to_string())
929        );
930        assert_eq!(
931            twoda.get_row_label(20, "FeedBackStrRefFail"),
932            &Some("e1rref".to_string())
933        );
934        assert_eq!(twoda.get_row_label(20, "Enc1_ResRefs"), &None);
935        assert_eq!(
936            twoda.get_row_merge_actions(20),
937            Some(
938                [
939                    MergeAction::Expect,
940                    MergeAction::Ignore,
941                    MergeAction::Ignore,
942                    MergeAction::Set,
943                    MergeAction::Set,
944                    MergeAction::Set,
945                    MergeAction::Set,
946                    MergeAction::Set,
947                    MergeAction::Set
948                ]
949                .as_slice()
950            )
951        );
952
953        assert_eq!(
954            twoda.get_row_label(22, "TableName"),
955            &Some("non".to_string())
956        );
957        assert_eq!(twoda.get_row_label(22, "Enc1_ResRefs"), &None);
958        assert_eq!(twoda.get_row_merge_actions(22), None);
959    }
960}