infinite-fusion 0.2.0

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

#[derive(Debug, Clone, Deserialize, Serialize)]
#[cfg_attr(test, serde(deny_unknown_fields))]
pub struct Type {
    #[serde(rename = "@id", deserialize_with = "as_transparent")]
    pub id: String,
    #[serde(rename = "@id_number")]
    pub id_number: u8, // max as of writing is 27
    #[serde(rename = "@real_name", deserialize_with = "as_transparent")]
    pub real_name: String,
    #[serde(rename = "@pseudo_type")]
    pub pseudo_type: bool,
    #[serde(rename = "@special_type")]
    pub special_type: bool,
    #[serde(rename = "@weaknesses")]
    pub weaknesses: Vec<String>,
    #[serde(rename = "@resistances")]
    pub resistances: Vec<String>,
    #[serde(rename = "@immunities")]
    pub immunities: Vec<String>,
}

/// [`Deserialize`] implementation for `types.dat`
#[derive(Debug, Clone, Serialize)]
pub struct TypeDex(pub Vec<Type>);

impl<'de> serde::Deserialize<'de> for TypeDex {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        reikland::DualKeyVec::<(Type, Ignored)>::deserialize(deserializer)
            .map(|reikland::DualKeyVec(mut d)| {
                d.sort_by_key(|(t, _)| t.id_number);
                d.into_iter().map(|(t, _)| t).collect()
            })
            .map(TypeDex)
    }
}

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

impl<'de> serde::Deserialize<'de> for TypeMap {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        TypeDex::deserialize(deserializer)
            .map(|TypeDex(v)| TypeMap(v.into_iter().map(|t| (t.id.clone(), t)).collect()))
    }
}

pub(crate) fn fuse_types<'a>(
    head_1: &'a String,
    head_2: Option<&'a String>,
    body_1: &'a String,
    body_2: Option<&'a String>,
) -> (&'a String, Option<&'a String>) {
    let type_1 = if let Some(secondary) = head_2
        && is_normal_flying(head_1, head_2)
    {
        secondary
    } else {
        head_1
    };

    let type_2 = match body_2 {
        None if body_1 == type_1 => None,
        None => Some(body_1),
        Some(secondary) if secondary == type_1 => Some(body_1),
        Some(secondary) => Some(secondary),
    };
    (type_1, type_2)
}

fn is_normal_flying(head_1: &String, head_2: Option<&String>) -> bool {
    if head_1 == "NORMAL" {
        head_2.is_some_and(|v| v == "FLYING")
    } else {
        false
    }
}

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

    use super::*;

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

    #[test]
    fn deserialize_types_dat_vec() {
        let PathTracked(dex) = reikland::from_bytes::<PathTracked<TypeDex>>(TYPES_DAT)
            .unwrap_or_else(|e| panic!("types.dat failed to deserialize: {e}"));

        assert!(!dex.0.is_empty());
        assert_normal(dex.0.first().unwrap());
    }

    #[test]
    fn deserialize_types_dat_map() {
        let PathTracked(dex) = reikland::from_bytes::<PathTracked<TypeMap>>(TYPES_DAT)
            .unwrap_or_else(|e| panic!("types.dat failed to deserialize: {e}"));

        assert!(!dex.0.is_empty());
        assert_normal(dex.0.first().unwrap().1);
    }

    fn assert_normal(t: &Type) {
        assert_eq!(t.id, "NORMAL");
        assert_eq!(t.id_number, 0);
        assert_eq!(t.real_name, "Normal");
        assert!(!t.pseudo_type);
        assert!(!t.special_type);
        assert_eq!(t.weaknesses, vec!["FIGHTING"]);
        assert!(t.resistances.is_empty());
        assert_eq!(t.immunities, vec!["GHOST"]);
    }
}