infinite-fusion 0.2.0

A simple crate for reading Pokemon Infinite Fusion's data
Documentation
pub mod abilities;
pub mod encounters;
pub mod items;
pub mod moves;
pub mod species;
pub mod types;

use reikland::Transparent;
use reikland::deserializer::MarshalDeserializeError;
use serde::{Deserialize, Deserializer};

pub use abilities::{AbilityDex, AbilityMap};
pub use encounters::{EncounterDex, EncounterMap};
pub use items::{ItemDex, ItemMap};
pub use moves::{MoveDex, MoveMap};
pub use species::{SpeciesDex, SpeciesMap};
pub use types::{TypeDex, TypeMap};

use crate::{
    species::{Species, base_stats::BaseStats},
    types::fuse_types,
};

/// Deserialize T as maybe being wrapped in some other type (such as an ivar)
pub(crate) fn as_transparent<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
    D: Deserializer<'de>,
    T: Deserialize<'de>,
{
    Transparent::<T>::deserialize(deserializer).map(|t| t.0)
}

#[derive(Debug)]
pub enum FromDatError {
    Io(std::io::Error),
    Marshal(MarshalDeserializeError),
}

impl std::fmt::Display for FromDatError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Io(e) => write!(f, "io error: {e}"),
            Self::Marshal(e) => write!(f, "marshal error: {e}"),
        }
    }
}

impl std::error::Error for FromDatError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Self::Io(e) => Some(e),
            Self::Marshal(e) => Some(e),
        }
    }
}

impl From<std::io::Error> for FromDatError {
    fn from(e: std::io::Error) -> Self {
        Self::Io(e)
    }
}

impl From<MarshalDeserializeError> for FromDatError {
    fn from(e: MarshalDeserializeError) -> Self {
        Self::Marshal(e)
    }
}

/// Generates `from_bytes` / `from_file` / `from_path` inherent methods on the given type.
macro_rules! impl_from_dat {
    ($t:ty) => {
        impl $t {
            pub fn from_bytes(bytes: &[u8]) -> Result<Self, FromDatError> {
                Ok(reikland::from_bytes(bytes)?)
            }

            pub fn from_file(file: &mut std::fs::File) -> Result<Self, FromDatError> {
                let mut bytes = Vec::new();
                std::io::Read::read_to_end(file, &mut bytes)?;
                Self::from_bytes(&bytes)
            }

            pub fn from_path(path: impl AsRef<std::path::Path>) -> Result<Self, FromDatError> {
                let bytes = std::fs::read(path)?;
                Self::from_bytes(&bytes)
            }
        }
    };
}

impl_from_dat!(AbilityDex);
impl_from_dat!(AbilityMap);
impl_from_dat!(EncounterDex);
impl_from_dat!(EncounterMap);
impl_from_dat!(ItemDex);
impl_from_dat!(ItemMap);
impl_from_dat!(MoveDex);
impl_from_dat!(MoveMap);
impl_from_dat!(SpeciesDex);
impl_from_dat!(SpeciesMap);
impl_from_dat!(TypeDex);
impl_from_dat!(TypeMap);

/// The result of fusing two pokemon via [`fuse`]
#[derive(Debug, Clone)]
pub struct FusedPokemon {
    pub head_id: u32,
    pub body_id: u32,
    pub head_name: String,
    pub body_name: String,
    pub type1: String,
    pub type2: Option<String>,
    pub abilities: Vec<String>,
    pub hidden_abilities: Vec<String>,
    pub moves: Vec<(u8, String)>,
    pub egg_moves: Vec<String>,
    pub tutor_moves: Vec<String>,
    pub base_stats: BaseStats,
}

/// Fuse two pokemon. Note the implementation is naive due to the data structures being naive. This may or may not scale well across the full list of pokemon fusions.
/// If performance is a large concern, this crate is likely a poor fit and you may wish to take my implementations from FusionFinder Desktop (if I have released it when you're reading this)
pub fn fuse(head: &Species, body: &Species) -> FusedPokemon {
    use itertools::Itertools;
    let (type1, type2) = fuse_types(
        &head.type1,
        head.type2.as_ref(),
        &body.type1,
        body.type2.as_ref(),
    );

    fn combine_string_vec(head: &[String], body: &[String]) -> Vec<String> {
        head.iter().chain(body.iter()).unique().cloned().collect()
    }

    let abilities = combine_string_vec(&head.abilities, &body.abilities);
    let hidden_abilities = combine_string_vec(&head.hidden_abilities, &body.hidden_abilities);

    let mut moves = head
        .moves
        .iter()
        .chain(body.moves.iter())
        .cloned()
        .collect::<Vec<_>>();

    moves.sort_unstable_by_key(|(level, _)| *level);

    let egg_moves = combine_string_vec(&head.egg_moves, &body.egg_moves);
    let tutor_moves = combine_string_vec(&head.tutor_moves, &body.tutor_moves);

    let base_stats = head.base_stats.fuse(&body.base_stats);

    FusedPokemon {
        head_id: head.id_number,
        body_id: body.id_number,
        head_name: head.real_name.clone(),
        body_name: body.real_name.clone(),
        type1: type1.clone(),
        type2: type2.cloned(),
        abilities,
        hidden_abilities,
        moves,
        egg_moves,
        tutor_moves,
        base_stats,
    }
}

#[cfg(test)]
pub(crate) mod test {
    use serde::Deserializer;

    pub(crate) struct PathTracked<T>(pub(crate) T);

    impl<'de, T> serde::Deserialize<'de> for PathTracked<T>
    where
        T: serde::Deserialize<'de>,
    {
        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
        where
            D: Deserializer<'de>,
        {
            match serde_path_to_error::deserialize::<D, T>(deserializer) {
                Ok(value) => Ok(PathTracked(value)),
                Err(err) => {
                    let path = err.path().to_string();
                    let inner = err.into_inner();
                    Err(serde::de::Error::custom(format!("at `{path}`: {inner}")))
                }
            }
        }
    }
}