poe_data_tools 1.0.0

A library for working with Path of Exile game data
Documentation
use anyhow::anyhow;
use winnow::{
    Parser,
    ascii::{dec_uint, float, space1},
    binary::length_repeat,
    combinator::{cond, dispatch, empty, fail, opt, preceded as P, repeat},
    token::literal,
};

use super::types::*;
use crate::file_parsers::{
    VersionedResult, VersionedResultExt,
    shared::{
        lift::{SliceParser, lift},
        winnow::{
            WinnowParser, nullable_uint, quoted_str, repeat_array, separated_array, unquoted_str,
            version_line,
        },
    },
};

fn animation_stage<'a>() -> impl SliceParser<'a, &'a str, AnimationStage> {
    winnow::trace!(
        "animation_stage",
        (
            lift(quoted_str), //
            lift(dec_uint),
            lift(separated_array(space1, float)),
        )
            .map(|(name, time, floats)| AnimationStage { name, time, floats })
    )
}

fn bone_rotation<'a>(num_coords: usize) -> impl WinnowParser<&'a str, BoneRotation> {
    winnow::trace!("bone_rotation", move |input: &mut &str| {
        let bone = quoted_str(input)?;

        let coord_order = P(space1, unquoted_str).parse_next(input)?;

        let coords = repeat(
            num_coords * coord_order.len(), //
            P(space1, float::<_, f32, _>),
        )
        .parse_next(input)?;

        let rotation = BoneRotation {
            bone,
            coord_order,
            coords,
        };

        Ok(rotation)
    })
}

fn bone_rotations<'a>() -> impl SliceParser<'a, &'a str, Vec<BoneRotation>> {
    winnow::trace!("bone_rotations", |input: &mut &[&str]| {
        let (num_rotations, num_coords): (usize, Option<usize>) =
            lift((dec_uint, opt(P(space1, dec_uint)))).parse_next(input)?;

        let rotations = repeat(
            num_rotations, //
            lift(bone_rotation(num_coords.unwrap_or(0))),
        )
        .parse_next(input)?;

        Ok(rotations)
    })
}

fn floats<'a>() -> impl WinnowParser<&'a str, Vec<f32>> {
    winnow::trace!(
        "floats",
        length_repeat(
            dec_uint::<_, u32, _>, //
            P(space1, float::<_, f32, _>),
        )
    )
}

fn group<'a>(version: u32) -> impl SliceParser<'a, &'a str, Group> {
    winnow::trace!(
        "group",
        (
            lift(quoted_str),
            lift(unquoted_str),
            lift(dec_uint),
            length_repeat(
                lift(dec_uint::<_, u32, _>), //
                animation_stage(),
            ),
            dispatch! {
                empty.value(version);
                1 => empty.value(None),
                2 => opt(lift(floats())),
                3.. => lift(floats()).map(Some),
                _ => fail,
            },
            cond(version >= 4, bone_rotations()),
            opt(repeat_array(lift(nullable_uint()))),
        )
            .map(
                |(
                    name,
                    animation_type,
                    animation_time,
                    animation_stages,
                    float_group,
                    bone_rotations,
                    extra_ints,
                )| {
                    Group {
                        name,
                        animation_type,
                        animation_time,
                        animation_stages,
                        float_group,
                        bone_rotations,
                        extra_ints,
                    }
                },
            )
    )
}

fn bone_group<'a>() -> impl WinnowParser<&'a str, BoneGroup> {
    winnow::trace!(
        "bone_group",
        (
            quoted_str, //
            P(
                space1,
                length_repeat(
                    dec_uint::<_, u32, _>, //
                    P(space1, quoted_str),
                ),
            ),
        )
            .map(|(name, bones)| BoneGroup { name, bones })
    )
}

fn bone_groups<'a>() -> impl SliceParser<'a, &'a str, Vec<BoneGroup>> {
    winnow::trace!(
        "bone_groups",
        length_repeat(
            lift(P(
                (literal("BoneGroups"), space1), //
                dec_uint::<_, u32, _>,
            )), //
            lift(bone_group()),
        )
    )
}

pub fn parse_amd_str(contents: &str) -> VersionedResult<AMDFile> {
    // NOTE: Some files have missing newlines, so split on tabs aswell
    let lines = contents
        .split_inclusive(['\n', '\t'])
        .map(|l| l.trim())
        .filter(|l| !l.is_empty() && !l.starts_with("//"))
        .collect::<Vec<_>>();
    let mut lines = lines.as_slice();

    let version = lift(version_line())
        .parse_next(&mut lines)
        .map_err(|e| anyhow!("Failed to parse file: {e:?}"))?;

    let mut parser = (
        length_repeat(
            lift(dec_uint::<_, u32, _>), //
            group(version),
        ),
        cond(version >= 5, bone_groups()),
    )
        .map(|(groups, bone_groups)| AMDFile {
            version,
            groups,
            bone_groups,
        });

    parser
        .parse(lines)
        .map_err(|e| anyhow!("Failed to parse file: {e:?}"))
        .with_version(Some(version))
}