infinite-fusion 0.2.0

A simple crate for reading Pokemon Infinite Fusion's data
Documentation
use indexmap::IndexMap;
use reikland::RbObject;
use serde::{Deserialize, Deserializer, Serialize};

#[derive(Debug, Deserialize, Clone, Copy, Serialize, Hash, PartialEq, Eq)]
pub enum EncounterMethod {
    Cave,
    GoodRod,
    Land,
    Land1,
    Land2,
    Land3,
    LandDay,
    LandMorning,
    LandNight,
    OldRod,
    RockSmash,
    SuperRod,
    Water,
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct EncounterSlot {
    pub probability: u8,
    pub species: String,
    pub min_level: u8,
    pub max_level: u8,
}

impl<'de> Deserialize<'de> for EncounterSlot {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let (probability, species, min_level, max_level) =
            <(u8, String, u8, u8)>::deserialize(deserializer)?;
        Ok(Self {
            probability,
            species,
            min_level,
            max_level,
        })
    }
}

#[derive(Debug, Clone, Deserialize, Serialize)]
#[cfg_attr(test, serde(deny_unknown_fields))]
pub struct Encounter {
    #[serde(rename = "@id")]
    pub id: String,
    #[serde(rename = "@map")]
    pub map: u16,
    #[serde(rename = "@version")]
    pub version: u8,
    #[serde(rename = "@step_chances")]
    pub step_chances: IndexMap<EncounterMethod, u8>,
    #[serde(rename = "@types")]
    pub types: IndexMap<EncounterMethod, Vec<EncounterSlot>>,
}

/// [`Deserialize`] implementation for `encounters*.dat`, indexable by id
#[derive(Debug, Clone, Serialize)]
pub struct EncounterMap(pub IndexMap<String, Encounter>);

impl<'de> serde::Deserialize<'de> for EncounterMap {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        IndexMap::<String, RbObject<Encounter>>::deserialize(deserializer)
            .map(|m| EncounterMap(m.into_iter().map(|(k, v)| (k, v.fields)).collect()))
    }
}

/// [`Deserialize`] implementation for `encounters*.dat`
#[derive(Debug, Clone, Serialize)]
pub struct EncounterDex(pub Vec<Encounter>);

impl<'de> serde::Deserialize<'de> for EncounterDex {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        EncounterMap::deserialize(deserializer)
            .map(|EncounterMap(m)| EncounterDex(m.into_values().collect()))
    }
}

#[cfg(test)]
mod tests {
    use crate::test::PathTracked;

    use super::*;

    const ENCOUNTERS_DAT: &[u8] = include_bytes!("../InfiniteFusion/Data/encounters.dat");
    const ENCOUNTERS_RANDOMIZED_DAT: &[u8] =
        include_bytes!("../InfiniteFusion/Data/encounters_randomized.dat");
    const ENCOUNTERS_REMIX_DAT: &[u8] =
        include_bytes!("../InfiniteFusion/Data/encounters_remix.dat");

    fn parse_dex(bytes: &[u8]) -> EncounterDex {
        let PathTracked(dex) = reikland::from_bytes::<PathTracked<EncounterDex>>(bytes)
            .unwrap_or_else(|e| panic!("encounters.dat failed to deserialize: {e}"));
        dex
    }

    fn parse_map(bytes: &[u8]) -> EncounterMap {
        let PathTracked(dex) = reikland::from_bytes::<PathTracked<EncounterMap>>(bytes)
            .unwrap_or_else(|e| panic!("encounters.dat failed to deserialize: {e}"));
        dex
    }

    #[test]
    fn deserialize_encounters_dat_vec() {
        let dex = parse_dex(ENCOUNTERS_DAT);
        assert!(!dex.0.is_empty());
        assert_first_standard(dex.0.first().unwrap());
    }

    #[test]
    fn deserialize_encounters_dat_map() {
        let dex = parse_map(ENCOUNTERS_DAT);
        assert_first_standard(&dex.0["8_0"]);
    }

    #[test]
    fn deserialize_encounters_randomized_dat_vec() {
        let dex = parse_dex(ENCOUNTERS_RANDOMIZED_DAT);
        assert_first_randomized(dex.0.first().unwrap());
    }

    #[test]
    fn deserialize_encounters_randomized_dat_map() {
        let dex = parse_map(ENCOUNTERS_RANDOMIZED_DAT);
        assert_first_randomized(&dex.0["1_0"]);
    }

    #[test]
    fn deserialize_encounters_remix_dat_vec() {
        let dex = parse_dex(ENCOUNTERS_REMIX_DAT);
        assert_first_remix(dex.0.first().unwrap());
    }

    #[test]
    fn deserialize_encounters_remix_dat_map() {
        let dex = parse_map(ENCOUNTERS_REMIX_DAT);
        assert_first_remix(&dex.0["1_0"]);
    }

    fn assert_first_standard(e: &Encounter) {
        assert_eq!(e.id, "8_0");
        assert_eq!(e.map, 8);
        assert_eq!(e.version, 0);
        assert_eq!(e.step_chances[&EncounterMethod::Land], 10);
        assert_eq!(e.step_chances[&EncounterMethod::Water], 2);

        let land = &e.types[&EncounterMethod::Land];
        assert_eq!(land.len(), 8);
        assert_eq!(
            land[0],
            EncounterSlot {
                probability: 20,
                species: "ABRA".to_string(),
                min_level: 8,
                max_level: 12,
            }
        );
    }

    fn assert_first_randomized(e: &Encounter) {
        assert_eq!(e.id, "1_0");
        assert_eq!(e.map, 1);
        let water = &e.types[&EncounterMethod::Water];
        assert_eq!(
            water[0],
            EncounterSlot {
                probability: 60,
                species: "SMOOCHUM".to_string(),
                min_level: 5,
                max_level: 40,
            }
        );
    }

    fn assert_first_remix(e: &Encounter) {
        assert_eq!(e.id, "1_0");
        assert_eq!(e.map, 1);
        let water = &e.types[&EncounterMethod::Water];
        assert_eq!(
            water[0],
            EncounterSlot {
                probability: 30,
                species: "BRUXISH".to_string(),
                min_level: 5,
                max_level: 40,
            }
        );
    }
}