infinite-fusion 0.2.0

A simple crate for reading Pokemon Infinite Fusion's data
Documentation
use std::ops::Deref;

use indexmap::IndexMap;
use reikland::Ignored;
use serde::{Deserialize, Deserializer, Serialize};

use crate::species::{
    base_stats::BaseStats, color::Color, egg_group::EggGroup, evolution::Evolution,
    gender_ratio::GenderRatio, growth_rate::GrowthRate, habitat::Habitat, shape::Shape,
};

pub mod base_stats;
pub mod color;
pub mod egg_group;
pub mod evolution;
pub mod gender_ratio;
pub mod growth_rate;
pub mod habitat;
pub mod shape;

pub type EffortValues = BaseStats;
pub type Level = u8;

#[derive(Debug, Clone, Deserialize, Serialize)]
#[cfg_attr(test, serde(deny_unknown_fields))]
pub struct Species {
    #[serde(rename = "@id")]
    pub id: String,
    #[serde(rename = "@id_number")]
    pub id_number: u32, // max as of writing is 1,000,029
    #[serde(rename = "@species")]
    pub species: String, // should be identical to id
    #[serde(rename = "@form")]
    pub form: u8, // current values are only 0 and 1
    #[serde(rename = "@real_name")]
    pub real_name: String,
    #[serde(rename = "@real_form_name")]
    pub real_form_name: Option<String>,
    #[serde(rename = "@real_category")]
    pub real_category: Option<String>,
    #[serde(rename = "@real_pokedex_entry")]
    pub real_pokedex_entry: Option<String>,
    #[serde(rename = "@pokedex_form")]
    pub pokedex_form: u8,
    #[serde(rename = "@type1")]
    pub type1: String, // tri-fused types such as WaterGroundFlying are in a single type hence string over a strict enum. as a general rule: dynamic stuff defined in other files shouldnt be an enum imo
    #[serde(rename = "@type2")]
    pub type2: Option<String>,
    #[serde(rename = "@base_stats")]
    pub base_stats: BaseStats,
    #[serde(rename = "@evs")]
    pub evs: EffortValues,
    #[serde(rename = "@base_exp")]
    pub base_exp: u16, // caps out at 608 as of me writing this so u8 too small
    #[serde(rename = "@growth_rate")]
    pub growth_rate: GrowthRate,
    #[serde(rename = "@gender_ratio")]
    pub gender_ratio: GenderRatio,
    #[serde(rename = "@catch_rate")]
    pub catch_rate: u8,
    #[serde(rename = "@happiness")]
    pub happiness: u8,
    #[serde(rename = "@moves")]
    pub moves: Vec<(Level, String)>,
    #[serde(rename = "@tutor_moves")]
    pub tutor_moves: Vec<String>,
    #[serde(rename = "@egg_moves")]
    pub egg_moves: Vec<String>,
    #[serde(rename = "@abilities")]
    pub abilities: Vec<String>,
    #[serde(rename = "@hidden_abilities")]
    pub hidden_abilities: Vec<String>,
    #[serde(rename = "@wild_item_common")]
    pub wild_item_common: Option<String>,
    #[serde(rename = "@wild_item_uncommon")]
    pub wild_item_uncommon: Option<String>,
    #[serde(rename = "@wild_item_rare")]
    pub wild_item_rare: Option<String>,
    #[serde(rename = "@egg_groups")]
    pub egg_groups: Vec<EggGroup>,
    #[serde(rename = "@hatch_steps")]
    pub hatch_steps: u16, // max 30855
    #[serde(rename = "@incense")]
    pub incense: Option<String>,
    #[serde(rename = "@evolutions")]
    pub evolutions: Vec<Evolution>,
    #[serde(rename = "@height")]
    pub height: u8,
    #[serde(rename = "@weight")]
    pub weight: u16,
    #[serde(rename = "@color")]
    pub color: Color,
    #[serde(rename = "@shape")]
    pub shape: Shape,
    #[serde(rename = "@habitat")]
    pub habitat: Habitat,
    #[serde(rename = "@generation")]
    pub generation: u8,
    #[serde(rename = "@mega_stone")]
    pub mega_stone: Option<String>,
    #[serde(rename = "@mega_move")]
    pub mega_move: Option<String>,
    #[serde(rename = "@unmega_form")]
    pub unmega_form: u8,
    #[serde(rename = "@mega_message")]
    pub mega_message: u8,
    #[serde(rename = "@back_sprite_x")]
    pub back_sprite_x: i8,
    #[serde(rename = "@back_sprite_y")]
    pub back_sprite_y: i8,
    #[serde(rename = "@front_sprite_x")]
    pub front_sprite_x: i8,
    #[serde(rename = "@front_sprite_y")]
    pub front_sprite_y: i8,
    #[serde(rename = "@front_sprite_altitude")]
    pub front_sprite_altitude: u8,
    #[serde(rename = "@shadow_x")]
    pub shadow_x: i8,
    #[serde(rename = "@shadow_size")]
    pub shadow_size: u8,
    #[serde(rename = "@alwaysUseGeneratedSprite")]
    pub always_use_generated_sprite: bool,
}

impl Species {
    pub fn abilities_iter(&self) -> impl Iterator<Item = &str> {
        self.abilities
            .iter()
            .map(Deref::deref)
            .chain(self.hidden_abilities.iter().map(String::as_str))
    }
}

/// [`Deserialize`] implentation for `species.dat`
#[derive(Debug, Clone, Serialize)]
pub struct SpeciesDex(pub Vec<Species>);

impl<'de> serde::Deserialize<'de> for SpeciesDex {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        // Species is an RbClass which is basically a tuple of (ActualData, ClassName)
        // i could validate the class name but that feels wrong
        reikland::DualKeyVec::<(Species, Ignored)>::deserialize(deserializer)
            .map(|reikland::DualKeyVec(mut d)| {
                // species is usually sorted so this should basically just be a quick scan that makes sure its in order
                d.sort_by_key(|(s, _)| s.id_number); // making sure anyone relying on the invariant of being in id order doesn't get frustrated

                // hopefully the compiler realises this is basically an iter_mut followed by a Vec::from_raw_parts / transmute
                // I tried unsafe but miri seemingly got stuck and never completed and i refuse to write unsafe code without miri
                d.into_iter()
                    .map(|(mut s, _)| {
                        s.type2.take_if(|type2| type2 == &s.type1); // type2 is confusingly set to the same as type1 for monotypes
                        s
                    })
                    .collect()
            })
            .map(SpeciesDex)
    }
}

/// [`Deserialize`] implementation for `species.dat`, indexable by key or value
#[derive(Debug, Clone, Serialize)]
pub struct SpeciesMap(pub IndexMap<String, Species>);

impl<'de> serde::Deserialize<'de> for SpeciesMap {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        // todo: drive deserialize visitor manually since that would let me reuse the exact ID from the map and probably be faster
        // however i am lazy
        SpeciesDex::deserialize(deserializer)
            .map(|SpeciesDex(v)| SpeciesMap(v.into_iter().map(|s| (s.id.clone(), s)).collect()))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    use crate::test::PathTracked;

    const SPECIES_DAT: &[u8] = include_bytes!("../InfiniteFusion/Data/species.dat");

    #[test]
    fn deserialize_species_dat_vec() {
        let PathTracked(dex) = reikland::from_bytes::<PathTracked<SpeciesDex>>(SPECIES_DAT)
            .unwrap_or_else(|e| panic!("species.dat failed to deserialize: {e}"));

        assert!(!dex.0.is_empty(), "SpeciesDex should contain entries");
        assert_bulbasaur(dex.0.first().unwrap());
    }

    #[test]
    fn deserialize_species_dat_map() {
        let PathTracked(dex) = reikland::from_bytes::<PathTracked<SpeciesMap>>(SPECIES_DAT)
            .unwrap_or_else(|e| panic!("species.dat failed to deserialize: {e}"));

        assert!(!dex.0.is_empty(), "SpeciesDex should contain entries");
        assert_bulbasaur(dex.0.first().unwrap().1);
    }

    // this is my favourite function name of all time btw
    fn assert_bulbasaur(species: &Species) {
        assert_eq!(species.id, "BULBASAUR");
        assert_eq!(species.type1, "GRASS");
        assert_eq!(species.type2.as_deref(), Some("POISON"));
        assert_eq!(species.id_number, 1);

        assert_eq!(
            species.base_stats,
            BaseStats {
                hp: 45,
                atk: 49,
                def: 49,
                spa: 65,
                spd: 65,
                spe: 45,
            }
        );
        assert_eq!(
            species.evs,
            BaseStats {
                hp: 0,
                atk: 0,
                def: 0,
                spa: 1,
                spd: 0,
                spe: 0,
            }
        );

        assert_eq!(species.evolutions.len(), 1);
        let evo = &species.evolutions[0];
        assert_eq!(
            evo.target,
            evolution::EvolutionTarget::Into {
                species: String::from("IVYSAUR")
            },
        );
        assert_eq!(evo.kind, evolution::EvolutionKind::Level { level: 16 });
    }
}