mvdparser 0.18.1

Extract information from QuakeWorld MVD demos.
Documentation
use anyhow::Result;
use bstr::ByteSlice;
use quake_text::unicode;

use crate::client::Client;
use crate::clientinfo;

pub fn clients(data: &[u8]) -> Result<Vec<Client>> {
    let clients: Vec<Client> = clientinfo::clientinfo(data)?
        .iter()
        .enumerate()
        .map(|(number, info)| {
            let mut client = Client::from(info);
            client.number = number as u8;
            client
        })
        .collect();
    Ok(clients)
}

pub fn has_bot_players(data: &[u8]) -> Result<bool> {
    if player_clients(data)?.iter().any(|c| c.is_bot) {
        return Ok(true);
    }

    Ok(data.rfind(br#""bot": {"#).is_some())
}

pub fn has_human_players(data: &[u8]) -> Result<bool> {
    Ok(player_clients(data)?.iter().any(|c| !c.is_bot))
}

pub fn player_clients(data: &[u8]) -> Result<Vec<Client>> {
    let players = clients(data)?
        .iter()
        .filter(|c| !c.is_spectator)
        .cloned()
        .collect();
    Ok(players)
}

pub fn player_names(data: &[u8]) -> Result<Vec<String>> {
    let names = player_clients(data)?
        .iter()
        .map(|c| c.name.clone())
        .collect::<Vec<String>>();
    Ok(unicode::sort(&names))
}

pub fn spectator_clients(data: &[u8]) -> Result<Vec<Client>> {
    let spectators = clients(data)?
        .iter()
        .filter(|c| c.is_spectator)
        .cloned()
        .collect();
    Ok(spectators)
}

pub fn spectator_names(data: &[u8]) -> Result<Vec<String>> {
    let names: Vec<String> = spectator_clients(data)?
        .iter()
        .map(|c| c.name.clone())
        .collect();
    Ok(unicode::sort(&names))
}

pub fn team_names(data: &[u8]) -> Result<Vec<String>> {
    let mut names: Vec<String> = player_clients(data)?
        .iter()
        .map(|c| c.team.clone())
        .collect();

    names = unicode::sort(&names);
    names.dedup();
    Ok(names)
}

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

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

    use super::*;

    #[test]
    fn test_clients() -> Result<()> {
        assert_eq!(
            clients(&read(
                "tests/files/duel_equ_vs_kaboom[povdmm4]20240422-1038.mvd"
            )?)?,
            vec![
                Client {
                    number: 0,
                    name: "eQu".to_string(),
                    team: "red".to_string(),
                    color: [4, 4],
                    is_spectator: false,
                    is_bot: false,
                    auth_username: None,
                    auth_cc: None,
                },
                Client {
                    number: 1,
                    name: "[ServeMe]".to_string(),
                    team: "lqwc".to_string(),
                    color: [12, 11],
                    is_spectator: true,
                    is_bot: false,
                    auth_username: None,
                    auth_cc: None,
                },
                Client {
                    number: 2,
                    name: "KabÏÏm".to_string(),
                    team: "".to_string(),
                    color: [2, 2],
                    is_spectator: false,
                    is_bot: false,
                    auth_username: None,
                    auth_cc: None,
                },
            ]
        );

        Ok(())
    }

    #[test]
    fn test_has_bot_players() -> Result<()> {
        {
            let demo_data = read("tests/files/ffa_5[dm4]20240501-1229.mvd")?;
            assert!(has_bot_players(&demo_data)?);
        }
        {
            let demo_data = read("tests/files/2on2_sf_vs_red[frobodm2]220104-0915.mvd")?;
            assert!(has_bot_players(&demo_data)?);
        }
        {
            let demo_data = read("tests/files/duel_equ_vs_kaboom[povdmm4]20240422-1038.mvd")?;
            assert!(!has_bot_players(&demo_data)?);
        }
        {
            let demo_data = read("tests/files/duel_holy_vs_dago[bravado]20240426-1659.mvd")?;
            assert!(!has_bot_players(&demo_data)?);
        }

        Ok(())
    }

    #[test]
    fn test_has_human_players() -> Result<()> {
        {
            let demo_data = read("tests/files/ffa_5[dm4]20240501-1229.mvd")?;
            assert!(has_human_players(&demo_data)?);
        }
        {
            let demo_data = read("tests/files/2on2_sf_vs_red[frobodm2]220104-0915.mvd")?;
            assert!(has_human_players(&demo_data)?);
        }
        {
            let demo_data = read("tests/files/duel_equ_vs_kaboom[povdmm4]20240422-1038.mvd")?;
            assert!(has_human_players(&demo_data)?);
        }
        {
            let demo_data = read("tests/files/duel_holy_vs_dago[bravado]20240426-1659.mvd")?;
            assert!(has_human_players(&demo_data)?);
        }

        Ok(())
    }

    #[test]
    fn test_player_clients() -> Result<()> {
        assert_eq!(
            player_clients(&read(
                "tests/files/duel_equ_vs_kaboom[povdmm4]20240422-1038.mvd"
            )?)?,
            vec![
                Client {
                    number: 0,
                    name: "eQu".to_string(),
                    team: "red".to_string(),
                    color: [4, 4],
                    is_spectator: false,
                    is_bot: false,
                    auth_username: None,
                    auth_cc: None,
                },
                Client {
                    number: 2,
                    name: "KabÏÏm".to_string(),
                    team: "".to_string(),
                    color: [2, 2],
                    is_spectator: false,
                    is_bot: false,
                    auth_username: None,
                    auth_cc: None,
                },
            ]
        );

        Ok(())
    }

    #[test]
    fn test_spectator_clients() -> Result<()> {
        assert_eq!(
            spectator_clients(&read(
                "tests/files/duel_equ_vs_kaboom[povdmm4]20240422-1038.mvd"
            )?)?,
            vec![Client {
                number: 1,
                name: "[ServeMe]".to_string(),
                team: "lqwc".to_string(),
                color: [12, 11],
                is_spectator: true,
                is_bot: false,
                auth_username: None,
                auth_cc: None,
            },]
        );

        Ok(())
    }

    #[test]
    fn test_spectator_names() -> Result<()> {
        assert_eq!(
            spectator_names(&read(
                "tests/files/duel_equ_vs_kaboom[povdmm4]20240422-1038.mvd"
            )?)?,
            vec!["[ServeMe]".to_string()]
        );

        Ok(())
    }

    #[test]
    fn test_player_names() -> Result<()> {
        assert_eq!(
            player_names(&read(
                "tests/files/duel_equ_vs_kaboom[povdmm4]20240422-1038.mvd"
            )?)?,
            vec!["eQu".to_string(), "KabÏÏm".to_string()]
        );

        assert_eq!(
            player_names(&read(
                "tests/files/2on2_sf_vs_red[frobodm2]220104-0915.mvd"
            )?)?,
            vec![
                ": Sujoy".to_string(),
                ": Timber".to_string(),
                "> MrJustice".to_string(),
                "Final".to_string(),
            ]
        );

        Ok(())
    }

    #[test]
    fn test_team_names() -> Result<()> {
        assert_eq!(
            team_names(&read(
                "tests/files/2on2_sf_vs_red[frobodm2]220104-0915.mvd"
            )?)?,
            vec!["=SF=".to_string(), "red".to_string()]
        );

        assert_eq!(
            team_names(&read("tests/files/ctf_blue_vs_red[ctf5]20240520-1925.mvd")?)?,
            vec!["blue".to_string(), "red".to_string()]
        );

        assert_eq!(
            team_names(&read("tests/files/4on4_oeks_vs_tsq[dm2]20240426-1716.mvd")?)?,
            vec!["oeks".to_string(), "tSÑ".to_string()]
        );

        assert_eq!(
            team_names(&read(
                "tests/files/wipeout_red_vs_blue[q3dm6qw]20240406-2028.mvd"
            )?)?,
            vec!["blue".to_string(), "red".to_string()]
        );

        Ok(())
    }
}