uxie 0.5.1

Data fetching library for Pokemon Gen 4 romhacking - map headers, C parsing, and more
Documentation
use rustc_hash::FxHashMap;
use std::path::Path;

#[derive(Debug, Clone, Default)]
pub struct TextBankTable {
    pub(crate) names: Vec<String>,
    pub(crate) name_to_id: FxHashMap<String, usize>,
}

impl TextBankTable {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn load_list_file(&mut self, path: impl AsRef<Path>) -> std::io::Result<()> {
        let content = std::fs::read_to_string(path)?;
        self.load_list_str(&content)
    }

    pub fn load_list_str(&mut self, content: &str) -> std::io::Result<()> {
        for line in content.lines() {
            let line = line.trim();
            if line.is_empty() || line.starts_with('#') || line.starts_with("//") {
                continue;
            }
            let name = line
                .find('=')
                .map_or_else(|| line.to_string(), |pos| line[..pos].trim().to_string());

            if !self.name_to_id.contains_key(&name) {
                self.name_to_id.insert(name.clone(), self.names.len());
                self.names.push(name);
            }
        }
        Ok(())
    }

    pub fn get_name(&self, id: usize) -> Option<&str> {
        self.names.get(id).map(|s| s.as_str())
    }

    pub fn get_id(&self, name: &str) -> Option<usize> {
        self.name_to_id.get(name).copied()
    }

    pub fn get_all_names(&self) -> &[String] {
        &self.names
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use proptest::prelude::*;
    use std::fmt::Write;

    fn unique_names_strategy() -> impl Strategy<Value = Vec<String>> {
        prop::collection::vec(any::<u16>(), 0..64).prop_map(|ids| {
            ids.into_iter()
                .enumerate()
                .map(|(idx, value)| format!("TEXT_BANK_{}_{}", idx, value))
                .collect()
        })
    }

    fn names_with_duplicates_strategy() -> impl Strategy<Value = Vec<String>> {
        prop::collection::vec(any::<u8>(), 0..64).prop_map(|ids| {
            ids.into_iter()
                .map(|value| format!("TEXT_BANK_DUP_{}", value % 16))
                .collect()
        })
    }

    #[test]
    fn test_load_list_skips_comments_and_blank_lines() {
        let mut table = TextBankTable::new();
        table
            .load_list_str(
                r"
                # hash comment
                // slash comment

                TEXT_BANK_COMMON = 0x0D5
                TEXT_BANK_CITY
                ",
            )
            .unwrap();

        assert_eq!(
            table.get_all_names(),
            vec!["TEXT_BANK_COMMON".to_string(), "TEXT_BANK_CITY".to_string()].as_slice()
        );
        assert_eq!(table.get_id("TEXT_BANK_COMMON"), Some(0));
        assert_eq!(table.get_id("TEXT_BANK_CITY"), Some(1));
    }

    proptest! {
        #![proptest_config(ProptestConfig {
            cases: 64,
            .. ProptestConfig::default()
        })]

        #[test]
        fn prop_unique_names_roundtrip(names in unique_names_strategy()) {
            let mut content = String::new();
            for name in &names {
                content.push_str(name);
                content.push('\n');
            }

            let mut table = TextBankTable::new();
            table.load_list_str(&content).unwrap();

            prop_assert_eq!(table.get_all_names().len(), names.len());
            for (idx, name) in names.iter().enumerate() {
                prop_assert_eq!(table.get_name(idx), Some(name.as_str()));
                prop_assert_eq!(table.get_id(name), Some(idx));
            }
        }

        #[test]
        fn prop_assignment_syntax_extracts_lhs(
            names in unique_names_strategy(),
            values in prop::collection::vec(any::<u16>(), 0..64)
        ) {
            let mut content = String::new();
            for (idx, name) in names.iter().enumerate() {
                let value = values.get(idx).copied().unwrap_or(0);
                content.push_str("  ");
                content.push_str(name);
                content.push_str("   =   ");
                write!(&mut content, "0x{:X}", value).unwrap();
                content.push('\n');
            }

            let mut table = TextBankTable::new();
            table.load_list_str(&content).unwrap();

            prop_assert_eq!(table.get_all_names(), names.as_slice());
            for (idx, name) in names.iter().enumerate() {
                prop_assert_eq!(table.get_id(name), Some(idx));
            }
        }

        #[test]
        fn prop_duplicate_names_first_wins(names in names_with_duplicates_strategy()) {
            let mut content = String::new();
            for name in &names {
                content.push_str(name);
                content.push_str(" = 1\n");
            }

            let mut table = TextBankTable::new();
            table.load_list_str(&content).unwrap();

            let mut unique_names = Vec::<String>::new();
            for name in &names {
                if !unique_names.contains(name) {
                    unique_names.push(name.clone());
                }
            }

            prop_assert_eq!(table.get_all_names(), unique_names.as_slice());
            for (unique_idx, name) in unique_names.iter().enumerate() {
                prop_assert_eq!(table.get_id(name), Some(unique_idx));
            }
        }

        #[test]
        fn prop_unknown_or_oob_queries_return_none(names in unique_names_strategy()) {
            let mut content = String::new();
            for name in &names {
                content.push_str(name);
                content.push('\n');
            }

            let mut table = TextBankTable::new();
            table.load_list_str(&content).unwrap();

            let unknown = "TEXT_BANK_DOES_NOT_EXIST";
            prop_assert_eq!(table.get_id(unknown), None);
            prop_assert_eq!(table.get_name(names.len()), None);
            prop_assert_eq!(table.get_name(names.len().saturating_add(100)), None);
        }
    }
}