uxie 0.5.1

Data fetching library for Pokemon Gen 4 romhacking - map headers, C parsing, and more
Documentation
use super::types::{EVOLUTION_FILE_SIZE, EvolutionData};
#[cfg(test)]
use super::types::{EvolutionEntry, EvolutionMethod};
use binrw::{BinRead, BinWrite};
use std::io::{self, Read, Seek, Write};

impl EvolutionData {
    pub fn from_binary<R: Read + Seek>(reader: &mut R) -> io::Result<Self> {
        Self::read_le(reader).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
    }

    pub fn to_binary<W: Write + Seek>(&self, writer: &mut W) -> io::Result<()> {
        self.write_le(writer)
            .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
    }

    pub fn to_bytes(&self) -> Vec<u8> {
        let mut buf = std::io::Cursor::new(Vec::with_capacity(EVOLUTION_FILE_SIZE));
        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_roundtrip() {
        let mut evo = EvolutionData::default();
        evo.entries[0] = EvolutionEntry {
            method: 4,
            param: 16,
            target_species: 2,
        };
        evo.entries[1] = EvolutionEntry {
            method: 1,
            param: 32,
            target_species: 3,
        };

        let bytes = evo.to_bytes();
        assert_eq!(bytes.len(), EVOLUTION_FILE_SIZE);

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

        assert_eq!(evo, parsed);
    }

    #[test]
    fn test_active_evolutions() {
        let mut evo = EvolutionData::default();
        evo.entries[0] = EvolutionEntry {
            method: 4,
            param: 16,
            target_species: 2,
        };
        evo.entries[2] = EvolutionEntry {
            method: 1,
            param: 32,
            target_species: 3,
        };

        assert_eq!(evo.active_evolutions().count(), 2);
    }

    fn evolution_entry_strategy() -> impl Strategy<Value = EvolutionEntry> {
        (any::<u16>(), any::<u16>(), any::<u16>()).prop_map(|(method, param, target_species)| {
            EvolutionEntry {
                method,
                param,
                target_species,
            }
        })
    }

    fn evolution_data_strategy() -> impl Strategy<Value = EvolutionData> {
        prop::collection::vec(evolution_entry_strategy(), 7).prop_map(|entries| EvolutionData {
            entries: entries.try_into().unwrap(),
        })
    }

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

        #[test]
        fn prop_evolution_data_roundtrip(evo in evolution_data_strategy()) {
            let bytes = evo.to_bytes();
            prop_assert_eq!(bytes.len(), EVOLUTION_FILE_SIZE);
            let mut cursor = Cursor::new(bytes);
            let parsed = EvolutionData::from_binary(&mut cursor).unwrap();
            prop_assert_eq!(evo, parsed);
        }

        #[test]
        fn prop_active_evolutions_count_matches_non_empty(evo in evolution_data_strategy()) {
            let expected = evo.entries.iter().filter(|e| !e.is_empty()).count();
            let actual = evo.active_evolutions().count();
            prop_assert_eq!(actual, expected);
        }

        #[test]
        fn prop_evolution_method_from_u16_maps(v in any::<u16>()) {
            let mapped = EvolutionMethod::from(v);
            let expected = match v {
                0 => EvolutionMethod::None,
                1 => EvolutionMethod::Happiness,
                2 => EvolutionMethod::HappinessDay,
                3 => EvolutionMethod::HappinessNight,
                4 => EvolutionMethod::LevelUp,
                5 => EvolutionMethod::Trade,
                6 => EvolutionMethod::TradeWithItem,
                7 => EvolutionMethod::UseItem,
                8 => EvolutionMethod::LevelUpAtkGtDef,
                9 => EvolutionMethod::LevelUpAtkEqDef,
                10 => EvolutionMethod::LevelUpDefGtAtk,
                11 => EvolutionMethod::LevelUpPersonalityLow,
                12 => EvolutionMethod::LevelUpPersonalityHigh,
                13 => EvolutionMethod::LevelUpSpawnPokemon,
                14 => EvolutionMethod::LevelUpSpawnEmptySlot,
                15 => EvolutionMethod::Beauty,
                16 => EvolutionMethod::UseItemMale,
                17 => EvolutionMethod::UseItemFemale,
                18 => EvolutionMethod::LevelUpWithItem,
                19 => EvolutionMethod::LevelUpWithMoveType,
                20 => EvolutionMethod::LevelUpWithPartyMember,
                21 => EvolutionMethod::LevelUpMale,
                22 => EvolutionMethod::LevelUpFemale,
                23 => EvolutionMethod::LevelUpAtLocation,
                24 => EvolutionMethod::LevelUpAtMtCoronet,
                25 => EvolutionMethod::LevelUpNearMossRock,
                26 => EvolutionMethod::LevelUpNearIceRock,
                n => EvolutionMethod::Unknown(n),
            };
            prop_assert_eq!(mapped, expected);
        }
    }

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

        let mut roundtripped_members = 0usize;
        let members = narc.members_owned().unwrap();
        for (i, original_bytes) in members.iter().enumerate().take(700) {
            if original_bytes.len() != EVOLUTION_FILE_SIZE {
                continue;
            }

            roundtripped_members += 1;
            let mut cursor = Cursor::new(original_bytes.as_slice());
            let evolution = EvolutionData::from_binary(&mut cursor)
                .unwrap_or_else(|_| panic!("Failed to parse evolution member {}", i));
            let serialized = evolution.to_bytes();

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

        assert!(
            roundtripped_members > 0,
            "expected at least one evolution entry with {} bytes in {}",
            EVOLUTION_FILE_SIZE,
            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/evo.narc", "data/a/0/3/4"],
            "evolution data real ROM roundtrip test (platinum)",
        ) else {
            return;
        };

        assert_real_evolution_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/evo.narc", "data/a/0/3/4"],
            "evolution data real ROM roundtrip test (hgss)",
        ) else {
            return;
        };

        assert_real_evolution_narc_roundtrip(&narc_path);
    }
}