mvdparser 0.18.1

Extract information from QuakeWorld MVD demos.
Documentation
use anyhow::{anyhow as e, Result};
use bstr::ByteSlice;
pub use quake_clientinfo::Clientinfo;

pub fn clientinfo(data: &[u8]) -> Result<Vec<Clientinfo>> {
    let info: Vec<Clientinfo> = clientinfo_strings(data)?
        .iter()
        .map(|s| Clientinfo::from(s.as_str()))
        .collect();
    Ok(info)
}

pub fn clientinfo_strings(data: &[u8]) -> Result<Vec<String>> {
    const CMD_SPAWN: [u8; 0x0A] = [0x09, 0x63, 0x6D, 0x64, 0x20, 0x73, 0x70, 0x61, 0x77, 0x6E];

    let Some(mut offset) = data.find(CMD_SPAWN) else {
        return Err(e!("Unable to find clientinfo strings"));
    };
    const MAX_PLAYERS: usize = 24;
    const MAX_LOOKAHEAD: usize = 512;
    let max_offset: usize = offset + MAX_PLAYERS * MAX_LOOKAHEAD;
    const MIN_LEN: usize = r#"\name\ "#.len();
    const MAX_LEN: usize = 256;

    let mut result: Vec<String> = vec![];

    while let Some(name_offset) = data[offset..offset + MAX_LOOKAHEAD]
        .find(br#"\name\"#)
        .map(|o| offset + o)
    {
        let Some(from) = data[..name_offset].rfind_byte(0).map(|o| o + 1) else {
            break;
        };

        let Some(to) = data[from..].find_byte(0).map(|o| from + o) else {
            break;
        };

        if !(MIN_LEN..=MAX_LEN).contains(&(to - from)) {
            break;
        }

        result.push(quake_text::bytestr::to_unicode(&data[from..to]));
        offset = to;

        if offset >= max_offset {
            break;
        }
    }

    Ok(result)
}

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

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

    use super::*;

    #[test]
    fn test_clientinfo() -> Result<()> {
        assert_eq!(
            clientinfo(&read(
                "tests/files/duel_equ_vs_kaboom[povdmm4]20240422-1038.mvd"
            )?)?,
            vec![
                Clientinfo {
                    name: Some("eQu".to_string()),
                    team: Some("red".to_string()),
                    topcolor: Some(4),
                    bottomcolor: Some(4),
                    spectator: None,
                    auth: None,
                    flag: None,
                    client: Some("ezQuake 1".to_string()),
                    bot: None,
                    chat: None,
                },
                Clientinfo {
                    name: Some("[ServeMe]".to_string()),
                    team: Some("lqwc".to_string()),
                    topcolor: Some(12),
                    bottomcolor: Some(11),
                    spectator: Some(1),
                    auth: None,
                    flag: None,
                    client: Some("libqwclient 0.1".to_string()),
                    bot: None,
                    chat: None,
                },
                Clientinfo {
                    name: Some("KabÏÏm".to_string()),
                    team: None,
                    topcolor: Some(2),
                    bottomcolor: Some(2),
                    spectator: None,
                    auth: None,
                    flag: None,
                    client: Some("ezQuake 1".to_string()),
                    bot: None,
                    chat: Some(1),
                },
            ]
        );

        Ok(())
    }

    #[test]
    fn test_clientinfo_strings() -> Result<()> {
        assert_eq!(
            clientinfo_strings(&read(
                "tests/files/2on2_sf_vs_red[frobodm2]220104-0915.mvd"
            )?)?,
            vec![
                r#"\chat\1\*client\ezQuake 7034\bottomcolor\4\topcolor\0\team\=SF=\name\Final"#,
                r#"\team\red\*bot\1\bottomcolor\4\topcolor\4\skin\base\name\: Timber"#,
                r#"\*client\libqwclient 0.1\*spectator\1\bottomcolor\11\topcolor\12\team\lqwc\name\[ServeMe]"#,
                r#"\team\=SF=\*bot\1\bottomcolor\4\topcolor\0\skin\base\name\> MrJustice"#,
                r#"\team\red\*bot\1\bottomcolor\4\topcolor\4\skin\base\name\: Sujoy"#,
            ]
        );

        assert_eq!(
            clientinfo_strings(&read("tests/files/ffa_5[dm4]20240501-1229.mvd")?)?,
            vec![
                r#"\*client\libqwclient 0.1\*spectator\1\bottomcolor\11\topcolor\12\team\lqwc\name\[ServeMe]"#.to_string(),
                r#"\*client\ezQuake 1\bottomcolor\0\topcolor\0\team\sdf\name\test"#.to_string(),
                r#"\chat\1\*client\ezQuake 1\*spectator\1\bottomcolor\1\topcolor\0\skin\oeks_nig\team\oeks\name\nig.........áøå"#.to_string(),
                r#"\*bot\1\bottomcolor\6\topcolor\0\skin\base\name\/ bro"#.to_string(),
                r#"\chat\2\*spectator\1\*client\ezQuake 1\gender\m\bottomcolor\1\topcolor\2\team\oeks\name\Z"#.to_string(),
                r#"\*bot\1\bottomcolor\13\topcolor\3\skin\base\name\/ goldenboy"#.to_string(),
                r#"\*bot\1\bottomcolor\11\topcolor\10\skin\base\name\/ tincan"#.to_string(),
                r#"\*bot\1\bottomcolor\4\topcolor\3\skin\base\name\/ grue"#.to_string(),
            ]
        );

        assert_eq!(
            clientinfo_strings(&read("tests/files/duel_equ_vs_kaboom[povdmm4]20240422-1038.mvd")?)?,
            vec![
                r#"\*client\ezQuake 1\gender\m\bottomcolor\4\topcolor\4\team\red\name\eQu"#.to_string(),
                r#"\*client\libqwclient 0.1\*spectator\1\bottomcolor\11\topcolor\12\team\lqwc\name\[ServeMe]"#.to_string(),
                r#"\chat\1\*client\ezQuake 1\gender\m\bottomcolor\2\topcolor\2\name\KabÏÏm"#.to_string(),
            ]
        );

        assert_eq!(
            clientinfo_strings(&read("tests/files/duel_holy_vs_dago[bravado]20240426-1659.mvd")?)?,
            vec![
                r#"\*client\ezQuake 1\bottomcolor\4\topcolor\4\team\x\name\HoLy"#.to_string(),
                r#"\*client\ezQuake 1\*qwfwd\1.2\bottomcolor\4\topcolor\4\team\red\name\äáçï"#.to_string(),
                r#"\chat\2\*client\ezQuake 1\*spectator\1\gender\m\bottomcolor\4\topcolor\4\team\red\name\Quake"#.to_string(),
                r#"\chat\2\*client\ezQuake 1\*spectator\1\bottomcolor\3\topcolor\1\team\mix\name\âáóó"#.to_string(),
                r#"\*client\libqwclient 0.1\*spectator\1\bottomcolor\11\topcolor\12\team\lqwc\name\[ServeMe]"#.to_string(),
            ]
        );

        assert_eq!(
            clientinfo_strings(&read("tests/files/4on4_oeks_vs_tsq[dm2]20240426-1716.mvd")?)?,
            vec![
                r#"\*client\ezQuake 7139\bottomcolor\1\topcolor\0\skin\oeks_tco\team\oeks\name\tco.........áøå"#.to_string(),
                r#"\chat\1\*client\ezQuake 1\*qwfwd\1.2\bottomcolor\1\topcolor\0\skin\oeks_bar\team\oeks\name\bar.........áøå"#.to_string(),
                r#"\*client\libqwclient 0.1\*spectator\1\bottomcolor\11\topcolor\12\team\lqwc\name\[ServeMe]"#.to_string(),
                r#"\*client\ezQuake 1\bottomcolor\10\topcolor\11\team\tSÑ\name\elguapo"#.to_string(),
                r#"\*client\ezQuake 7190\bottomcolor\1\topcolor\0\skin\oeks_trl\team\oeks\name\trl.........áøå"#.to_string(),
                r#"\*client\ezQuake 1\bottomcolor\10\topcolor\11\team\tSÑ\name\conan"#.to_string(),
                r#"\*client\ezQuake 1\bottomcolor\10\topcolor\11\skin\base\team\tSÑ\name\muttan"#.to_string(),
                r#"\*client\ezQuake 1\*spectator\1\bottomcolor\10\topcolor\11\team\tSÑ\name\nas"#.to_string(),
                r#"\chat\1\team\tSÑ\gender\m\topcolor\11\bottomcolor\10\*client\ezQuake 1\name\djevulsk"#.to_string(),
                r#"\chat\2\*client\ezQuake 1\bottomcolor\1\topcolor\0\skin\oeks_tim\team\oeks\name\tim.........áøå"#.to_string(),
                r#"\chat\1\*client\ezQuake 1\*spectator\1\bottomcolor\4\topcolor\4\team\red\name\lakso"#.to_string(),
            ]
        );

        assert_eq!(
            clientinfo_strings(&read("tests/files/wipeout_red_vs_blue[q3dm6qw]20240406-2028.mvd")?)?,
            vec![
                r#"\*client\ezQuake 1\bottomcolor\4\topcolor\4\team\red\name\z0mbie90"#.to_string(),
                r#"\*client\ezQuake 0\gender\m\bottomcolor\13\topcolor\13\team\blue\name\Kalle Dangerous"#.to_string(),
                r#"\chat\1\team\blue\*client\ezQuake 1\gender\m\bottomcolor\13\topcolor\13\name\j0rmund"#.to_string(),
                r#"\*client\ezQuake 7683\bottomcolor\0\topcolor\0\team\red\name\luòñ"#.to_string(),
                r#"\*client\libqwclient 0.1\*spectator\1\bottomcolor\11\topcolor\12\team\lqwc\name\[ServeMe]"#.to_string(),
                r#"\*client\ezQuake 1\bottomcolor\4\topcolor\4\team\blue\name\grotzky"#.to_string(),
            ]
        );

        Ok(())
    }
}