mvdparser 0.18.1

Extract information from QuakeWorld MVD demos.
Documentation
use std::collections::HashMap;
use std::io::Cursor;

use anyhow::{anyhow as e, Result};

use crate::clients;
use crate::qw::fragevent::FragEvent;
use crate::qw::frame;
use crate::qw::message::message_type::ReadMessageType;
use crate::qw::message::print::ReadPrint;
use crate::qw::message::update_frags::ReadUpdateFrags;
use crate::qw::message::Print;
use crate::qw::prot::{MessageType, PrintId};

pub fn frags_per_player_name(data: &[u8]) -> HashMap<String, i32> {
    let mut index = 0;
    let mut print_frames: Vec<(Print, frame::Info)> = vec![];

    while let Ok(frame_info) = frame::Info::from_data_and_index(data, index) {
        if frame_info.body_size == 0 {
            index += frame_info.size;
            continue;
        }

        let mut body = Cursor::new(&data[frame_info.clone().body_range]);

        if body
            .read_message_type()
            .is_ok_and(|t| t == MessageType::Print)
        {
            if let Ok(p) = body.read_print() {
                if !p.content.is_empty() && PrintId::Medium == p.id {
                    print_frames.push((p, frame_info.clone()))
                }
            }
        }

        index += frame_info.size;
    }

    let mut frags: HashMap<String, i32> = HashMap::new();

    for (print, frame_info) in print_frames {
        let content_u = quake_text::bytestr::to_unicode(&print.content);

        match FragEvent::try_from(content_u.trim_end()) {
            Ok(event) => match event {
                FragEvent::Frag { killer, .. } => {
                    let killer = frags.entry(killer).or_insert(0);
                    *killer += 1;
                }
                FragEvent::Death { player } => {
                    let player = frags.entry(player).or_insert(0);
                    *player -= 1;
                }
                FragEvent::Suicide { player } => {
                    let player = frags.entry(player).or_insert(0);
                    *player -= 2;
                }
                FragEvent::SuicideByWeapon { player } => {
                    let player = frags.entry(player).or_insert(0);
                    *player -= 1;
                }
                FragEvent::Teamkill { killer } => {
                    let killer = frags.entry(killer).or_insert(0);
                    *killer -= 1;
                }
                FragEvent::TeamkillByUnknown { victim } => {
                    if let Ok(name) = find_team_killer(data, frame_info.index, &victim) {
                        let killer = frags.entry(name).or_insert(0);
                        *killer -= 1;
                    }
                }
            },
            Err(_e) => {
                // println!("UNKNOWN {:?}", e);
            }
        }
    }

    frags
}

fn find_team_killer(data: &[u8], index: usize, victim_name: &str) -> Result<String> {
    let mut index = index;
    let mut frame_count: usize = 1;
    let mut frag_update_player_numbers: Vec<u8> = vec![];
    const MAX_FRAME_COUNT: usize = 4;

    while let Ok(info) = frame::Info::from_data_and_index(data, index) {
        if frame_count >= MAX_FRAME_COUNT {
            break;
        } else if info.body_size == 0 {
            index += info.size;
            continue;
        }

        let mut body = Cursor::new(&data[info.body_range.clone()]);

        if frame_count == 1 {
            body.read_print().ok();
        }

        if body
            .read_message_type()
            .is_ok_and(|t| t == MessageType::UpdateFrags)
        {
            if let Ok(u) = body.read_update_frags() {
                frag_update_player_numbers.push(u.player_number);
            }
        }

        index += info.size;
        frame_count += 1;
    }

    if frag_update_player_numbers.is_empty() {
        return Err(e!("Unable to find nearby frag updates"));
    }

    let clients = clients::clients(data)?;
    let Some(victim_client) = clients.iter().find(|c| c.name == *victim_name) else {
        return Err(e!("Unable to find victim"));
    };

    let killer = frag_update_player_numbers
        .iter()
        .filter_map(|player_number| clients.iter().find(|c| c.number == *player_number))
        .find(|c| c.team == victim_client.team && c.name != victim_client.name)
        .map(|c| c.name.clone());

    match killer {
        Some(killer) => Ok(killer),
        None => Err(e!("Unable to find team killer")),
    }
}

#[cfg(test)]
mod tests {
    use std::fs::read;

    use anyhow::Result;
    use pretty_assertions::assert_eq;

    use super::*;

    #[test]
    fn test_frags_per_player_name() -> Result<()> {
        {
            let demo_data = read("tests/files/duel_equ_vs_kaboom[povdmm4]20240422-1038.mvd")?;
            let frags_map = frags_per_player_name(&demo_data);
            assert_eq!(frags_map.len(), 2);
            assert_eq!(frags_map.get("eQu"), Some(&19));
            assert_eq!(frags_map.get("KabÏÏm"), Some(&20));
        }

        {
            let demo_data = read("tests/files/duel_holy_vs_dago[bravado]20240426-1659.mvd")?;
            let frags_map = frags_per_player_name(&demo_data);
            assert_eq!(frags_map.len(), 2);
            assert_eq!(frags_map.get("HoLy"), Some(&25));
            assert_eq!(frags_map.get("äáçï"), Some(&31));
        }

        {
            let demo_data = read("tests/files/4on4_oeks_vs_tsq[dm2]20240426-1716.mvd")?;
            let frags_map = frags_per_player_name(&demo_data);
            assert_eq!(frags_map.len(), 8);
            assert_eq!(frags_map.get("conan"), Some(&71));
            assert_eq!(frags_map.get("djevulsk"), Some(&74));
            assert_eq!(frags_map.get("elguapo"), Some(&60));
            assert_eq!(frags_map.get("muttan"), Some(&89));
            assert_eq!(frags_map.get("tco.........áøå"), Some(&32));
            assert_eq!(frags_map.get("trl.........áøå"), Some(&26));
            assert_eq!(frags_map.get("tim.........áøå"), Some(&33));
            assert_eq!(frags_map.get("bar.........áøå"), Some(&27));
        }

        {
            let demo_data = read("tests/files/ffa_5[dm4]20240501-1229.mvd")?;
            let frags_map = frags_per_player_name(&demo_data);
            assert_eq!(frags_map.len(), 5);
            assert_eq!(frags_map.get("test"), Some(&4));
            assert_eq!(frags_map.get("/ bro"), Some(&6));
            assert_eq!(frags_map.get("/ goldenboy"), Some(&5));
            assert_eq!(frags_map.get("/ tincan"), Some(&8));
            assert_eq!(frags_map.get("/ grue"), Some(&6));
        }

        Ok(())
    }
}