uxie 0.5.6

Data fetching library for Pokemon Gen 4 romhacking - map headers, C parsing, and more
Documentation
use super::types::{LEARNSET_TERMINATOR, LearnsetData, LearnsetEntry};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use std::io::{self, Read, Write};

impl LearnsetData {
    pub fn from_binary<R: Read>(reader: &mut R) -> io::Result<Self> {
        let mut entries = Vec::new();
        loop {
            let packed = reader.read_u16::<LittleEndian>()?;
            if packed == LEARNSET_TERMINATOR {
                break;
            }
            entries.push(LearnsetEntry::from_packed(packed));
        }
        Ok(Self { entries })
    }

    pub fn to_binary<W: Write>(&self, writer: &mut W) -> io::Result<()> {
        for entry in &self.entries {
            writer.write_u16::<LittleEndian>(entry.to_packed())?;
        }
        writer.write_u16::<LittleEndian>(LEARNSET_TERMINATOR)?;
        Ok(())
    }

    pub fn to_bytes(&self) -> Vec<u8> {
        let mut buf = std::io::Cursor::new(Vec::with_capacity((self.entries.len() + 1) * 2));
        self.to_binary(&mut buf).unwrap();
        buf.into_inner()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use proptest::prelude::*;
    use std::fs::File;
    use std::io::BufReader;
    use std::io::Cursor;
    use std::path::Path;

    #[test]
    fn test_entry_packing() {
        let entry = LearnsetEntry::new(33, 15);
        let packed = entry.to_packed();
        let unpacked = LearnsetEntry::from_packed(packed);
        assert_eq!(entry, unpacked);
    }

    #[test]
    fn test_roundtrip() {
        let learnset = LearnsetData {
            entries: vec![
                LearnsetEntry::new(33, 1),
                LearnsetEntry::new(45, 4),
                LearnsetEntry::new(36, 7),
                LearnsetEntry::new(98, 13),
            ],
        };

        let bytes = learnset.to_bytes();
        let mut cursor = Cursor::new(bytes);
        let parsed = LearnsetData::from_binary(&mut cursor).unwrap();

        assert_eq!(learnset, parsed);
    }

    #[test]
    fn test_empty_learnset() {
        let learnset = LearnsetData::default();
        let bytes = learnset.to_bytes();
        assert_eq!(bytes.len(), 2);
        assert_eq!(&bytes, &[0xFF, 0xFF]);

        let mut cursor = Cursor::new(bytes);
        let parsed = LearnsetData::from_binary(&mut cursor).unwrap();
        assert!(parsed.entries.is_empty());
    }

    #[test]
    fn test_moves_at_level() {
        let learnset = LearnsetData {
            entries: vec![
                LearnsetEntry::new(1, 1),
                LearnsetEntry::new(2, 5),
                LearnsetEntry::new(3, 10),
                LearnsetEntry::new(4, 15),
            ],
        };

        assert_eq!(learnset.moves_at_level(10).count(), 3);

        let learned_at_5: Vec<_> = learnset.moves_learned_at(5).collect();
        assert_eq!(learned_at_5.len(), 1);
        assert_eq!(learned_at_5[0].move_id, 2);
    }

    fn valid_entry_strategy() -> impl Strategy<Value = LearnsetEntry> {
        (0u16..=0x1FF, 0u8..=0x7F)
            .prop_filter("avoid reserved terminator encoding", |(move_id, level)| {
                !(*move_id == 0x1FF && *level == 0x7F)
            })
            .prop_map(|(move_id, level)| LearnsetEntry { move_id, level })
    }

    fn learnset_strategy() -> impl Strategy<Value = LearnsetData> {
        prop::collection::vec(valid_entry_strategy(), 0..64)
            .prop_map(|entries| LearnsetData { entries })
    }

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

        #[test]
        fn prop_entry_pack_roundtrip(entry in valid_entry_strategy()) {
            let packed = entry.to_packed();
            prop_assert_ne!(packed, LEARNSET_TERMINATOR);
            let unpacked = LearnsetEntry::from_packed(packed);
            prop_assert_eq!(entry, unpacked);
        }

        #[test]
        fn prop_learnset_roundtrip(learnset in learnset_strategy()) {
            let bytes = learnset.to_bytes();
            prop_assert_eq!(bytes.len(), (learnset.entries.len() + 1) * 2);
            prop_assert_eq!(&bytes[bytes.len() - 2..], &[0xFF, 0xFF]);

            let mut cursor = Cursor::new(bytes);
            let parsed = LearnsetData::from_binary(&mut cursor).unwrap();
            prop_assert_eq!(learnset, parsed);
        }

        #[test]
        fn prop_level_query_helpers_match_filters(learnset in learnset_strategy(), level in any::<u8>()) {
            let expected_at_level: Vec<_> = learnset.entries.iter().filter(|e| e.level <= level).collect();
            let expected_learned_at: Vec<_> = learnset.entries.iter().filter(|e| e.level == level).collect();

            let actual_at_level: Vec<_> = learnset.moves_at_level(level).collect();
            let actual_learned_at: Vec<_> = learnset.moves_learned_at(level).collect();

            prop_assert_eq!(actual_at_level, expected_at_level);
            prop_assert_eq!(actual_learned_at, expected_learned_at);
        }
    }

    fn assert_real_learnset_narc_roundtrip(narc_path: &Path) {
        let file = File::open(narc_path).expect("Failed to open learnset NARC");
        let mut reader = BufReader::new(file);
        let narc = crate::Narc::from_binary(&mut reader).expect("Failed to load learnset NARC");

        let mut roundtripped_members = 0usize;
        let members = narc.members_owned().unwrap();
        for (i, original_bytes) in members.iter().enumerate().take(700) {
            let mut cursor = Cursor::new(original_bytes.as_slice());
            let learnset = LearnsetData::from_binary(&mut cursor)
                .unwrap_or_else(|_| panic!("Failed to parse learnset member {}", i));
            let serialized = learnset.to_bytes();
            roundtripped_members += 1;

            assert_eq!(
                original_bytes.as_slice(),
                serialized.as_slice(),
                "Roundtrip failed for learnset member {}",
                i
            );
        }

        assert!(
            roundtripped_members > 0,
            "expected at least one learnset entry in {}",
            narc_path.display()
        );
    }

    #[test]
    #[ignore = "requires a real Platinum DSPRE project via UXIE_TEST_PLATINUM_DSPRE_PATH"]
    fn integration_real_rom_roundtrip_platinum() {
        let Some(narc_path) = crate::test_env::existing_file_under_project_env(
            "UXIE_TEST_PLATINUM_DSPRE_PATH",
            &["data/poketool/personal/pms.narc", "data/pbr/pms.narc"],
            "learnset data real ROM roundtrip test (platinum)",
        ) else {
            return;
        };

        assert_real_learnset_narc_roundtrip(&narc_path);
    }

    #[test]
    #[ignore = "requires a real HGSS DSPRE project via UXIE_TEST_HGSS_DSPRE_PATH"]
    fn integration_real_rom_roundtrip_hgss() {
        let Some(narc_path) = crate::test_env::existing_file_under_project_env(
            "UXIE_TEST_HGSS_DSPRE_PATH",
            &["data/poketool/personal/pms.narc", "data/pbr/pms.narc"],
            "learnset data real ROM roundtrip test (hgss)",
        ) else {
            return;
        };

        assert_real_learnset_narc_roundtrip(&narc_path);
    }
}