uxie 0.5.6

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

pub const MOVE_DATA_SIZE: usize = 14;

impl MoveData {
    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(MOVE_DATA_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 move_data = MoveData {
            battle_effect: 1,
            split: MoveSplit::Physical,
            power: 40,
            move_type: 0,
            accuracy: 100,
            pp: 35,
            side_effect_chance: 0,
            target: 0,
            priority: 0,
            flags: MoveFlags::MAKES_CONTACT,
            contest_appeal: 4,
            contest_condition: 0,
        };

        let bytes = move_data.to_bytes();
        assert_eq!(bytes.len(), MOVE_DATA_SIZE);

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

        assert_eq!(move_data, parsed);
    }

    #[test]
    fn test_negative_priority() {
        let move_data = MoveData {
            battle_effect: 100,
            split: MoveSplit::Status,
            power: 0,
            move_type: 14,
            accuracy: 0,
            pp: 10,
            side_effect_chance: 0,
            target: 0,
            priority: -6,
            flags: MoveFlags::empty(),
            contest_appeal: 0,
            contest_condition: 0,
        };

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

        assert_eq!(parsed.priority, -6);
    }

    #[test]
    fn test_from_binary_invalid_move_flags_returns_error() {
        let mut bytes = MoveData {
            battle_effect: 0,
            split: MoveSplit::Physical,
            power: 0,
            move_type: 0,
            accuracy: 0,
            pp: 0,
            side_effect_chance: 0,
            target: 0,
            priority: 0,
            flags: MoveFlags::empty(),
            contest_appeal: 0,
            contest_condition: 0,
        }
        .to_bytes();
        bytes[11] = 0b1000_0000;
        let mut cursor = Cursor::new(bytes);

        let err = MoveData::from_binary(&mut cursor).unwrap_err();
        assert_eq!(err.kind(), io::ErrorKind::InvalidData);
        assert!(err.to_string().contains("Invalid move flags bits"));
    }

    fn move_split_strategy() -> impl Strategy<Value = MoveSplit> {
        prop_oneof![
            Just(MoveSplit::Physical),
            Just(MoveSplit::Special),
            Just(MoveSplit::Status),
        ]
    }

    fn move_flags_strategy() -> impl Strategy<Value = MoveFlags> {
        (0u8..=0x3F).prop_map(|bits| {
            MoveFlags::from_bits(bits).expect("0..=0x3F should always be valid move-flag bits")
        })
    }

    fn move_data_strategy() -> impl Strategy<Value = MoveData> {
        (
            any::<u16>(),
            move_split_strategy(),
            any::<u8>(),
            any::<u8>(),
            any::<u8>(),
            any::<u8>(),
            any::<u8>(),
            any::<u16>(),
            any::<i8>(),
            move_flags_strategy(),
            any::<u8>(),
            any::<u8>(),
        )
            .prop_map(
                |(
                    battle_effect,
                    split,
                    power,
                    move_type,
                    accuracy,
                    pp,
                    side_effect_chance,
                    target,
                    priority,
                    flags,
                    contest_appeal,
                    contest_condition,
                )| MoveData {
                    battle_effect,
                    split,
                    power,
                    move_type,
                    accuracy,
                    pp,
                    side_effect_chance,
                    target,
                    priority,
                    flags,
                    contest_appeal,
                    contest_condition,
                },
            )
    }

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

        #[test]
        fn prop_move_data_roundtrip(move_data in move_data_strategy()) {
            let bytes = move_data.to_bytes();
            prop_assert_eq!(bytes.len(), MOVE_DATA_SIZE);
            let mut cursor = Cursor::new(bytes);
            let parsed = MoveData::from_binary(&mut cursor).unwrap();
            prop_assert_eq!(move_data, parsed);
        }

        #[test]
        fn prop_move_split_from_u8_maps(v in any::<u8>()) {
            let mapped = MoveSplit::from(v);
            let expected = match v {
                0 => MoveSplit::Physical,
                1 => MoveSplit::Special,
                _ => MoveSplit::Status,
            };
            prop_assert_eq!(mapped, expected);
        }
    }

    fn assert_real_move_narc_roundtrip(narc_path: &Path) {
        let file = File::open(narc_path).expect("Failed to open move-data NARC");
        let mut reader = BufReader::new(file);
        let narc = crate::Narc::from_binary(&mut reader).expect("Failed to load move-data 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() != MOVE_DATA_SIZE {
                continue;
            }

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

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

        assert!(
            roundtripped_members > 0,
            "expected at least one move-data entry with {} bytes in {}",
            MOVE_DATA_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/waza/pl_waza_tbl.narc",
                "data/poketool/waza/waza_tbl.narc",
                "data/pbr/waza_tbl.narc",
            ],
            "move data real ROM roundtrip test (platinum)",
        ) else {
            return;
        };

        assert_real_move_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/data/kowaza.narc",
                "data/poketool/waza/waza_tbl.narc",
                "data/poketool/waza/pl_waza_tbl.narc",
                "data/pbr/waza_tbl.narc",
            ],
            "move data real ROM roundtrip test (hgss)",
        ) else {
            return;
        };

        assert_real_move_narc_roundtrip(&narc_path);
    }
}