w3grs 0.1.0

A Rust port of w3gjs for parsing Warcraft III replay files.
Documentation
//! Low-level replay parser facade port.

use crate::{
    Result,
    action::Action,
    game_data::{GameDataBlock, GameDataParser},
    metadata::{MetadataParser, ReplayMetadata},
    raw::{Header, RawParser, SubHeader},
};
use serde::{Deserialize, Serialize};

#[derive(Debug, Default)]
pub struct ReplayParser {
    raw_parser: RawParser,
    metadata_parser: MetadataParser,
    game_data_parser: GameDataParser,
}

impl ReplayParser {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn parse(&self, input: &[u8]) -> Result<ReplayParserOutput> {
        let raw = self.raw_parser.parse(input)?;
        let metadata = self.metadata_parser.parse(&raw.blocks)?;
        let game_data_blocks = self
            .game_data_parser
            .parse(&metadata.game_data, metadata.is_post_202_replay_format)?;

        Ok(ReplayParserOutput {
            header: raw.header,
            subheader: raw.subheader,
            metadata,
            game_data_blocks,
        })
    }
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ReplayParserOutput {
    pub header: Header,
    pub subheader: SubHeader,
    pub metadata: ReplayMetadata,
    pub game_data_blocks: Vec<GameDataBlock>,
}

impl ReplayParserOutput {
    pub fn iter_timed_actions(&self) -> TimedActions<'_> {
        TimedActions::new(&self.game_data_blocks)
    }

    pub fn timed_actions(&self) -> Vec<TimedAction<'_>> {
        self.iter_timed_actions().collect()
    }
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TimedAction<'a> {
    pub action: &'a Action,
    pub time_ms: u32,
    pub frame: u32,
    pub block_id: u8,
    pub player_id: u8,
    pub sequence: usize,
}

#[derive(Debug, Clone)]
pub struct TimedActions<'a> {
    blocks: &'a [GameDataBlock],
    block_index: usize,
    command_index: usize,
    action_index: usize,
    time_ms: u32,
    frame: u32,
    block_id: u8,
    sequence: usize,
    in_timeslot: bool,
}

impl<'a> TimedActions<'a> {
    fn new(blocks: &'a [GameDataBlock]) -> Self {
        Self {
            blocks,
            block_index: 0,
            command_index: 0,
            action_index: 0,
            time_ms: 0,
            frame: 0,
            block_id: 0,
            sequence: 0,
            in_timeslot: false,
        }
    }
}

impl<'a> Iterator for TimedActions<'a> {
    type Item = TimedAction<'a>;

    fn next(&mut self) -> Option<Self::Item> {
        while self.block_index < self.blocks.len() {
            let GameDataBlock::Timeslot(timeslot) = &self.blocks[self.block_index] else {
                self.block_index += 1;
                self.in_timeslot = false;
                continue;
            };

            if !self.in_timeslot {
                self.time_ms += u32::from(timeslot.time_increment);
                self.frame = frame_from_ms(self.time_ms);
                self.block_id = timeslot.id;
                self.command_index = 0;
                self.action_index = 0;
                self.in_timeslot = true;
            }

            while self.command_index < timeslot.command_blocks.len() {
                let command = &timeslot.command_blocks[self.command_index];
                if self.action_index < command.actions.len() {
                    let action = &command.actions[self.action_index];
                    self.action_index += 1;
                    let sequence = self.sequence;
                    self.sequence += 1;
                    return Some(TimedAction {
                        action,
                        time_ms: self.time_ms,
                        frame: self.frame,
                        block_id: self.block_id,
                        player_id: command.player_id,
                        sequence,
                    });
                }

                self.command_index += 1;
                self.action_index = 0;
            }

            self.block_index += 1;
            self.in_timeslot = false;
        }

        None
    }
}

pub fn frame_from_ms(ms: u32) -> u32 {
    ((u64::from(ms) * 64 + 500) / 1000) as u32
}

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

    #[test]
    fn builds_timed_action_timeline_from_replay() {
        let bytes = include_bytes!("../fixtures/replays/132/reforged1.w3g");
        let parsed = ReplayParser::new().parse(bytes).unwrap();
        let actions = parsed.timed_actions();

        assert!(!actions.is_empty());
        assert_eq!(actions[0].time_ms, 1372);
        assert_eq!(actions[0].frame, 88);
        assert_eq!(actions[0].block_id, 31);
        assert_eq!(actions[0].player_id, 2);
        assert_eq!(actions[0].sequence, 0);
    }

    #[test]
    fn lazy_timed_action_iterator_matches_vec_helper() {
        let bytes = include_bytes!("../fixtures/replays/132/reforged1.w3g");
        let parsed = ReplayParser::new().parse(bytes).unwrap();
        let lazy = parsed.iter_timed_actions().collect::<Vec<_>>();

        assert_eq!(lazy, parsed.timed_actions());
    }
}