teehistorian 0.12.0

Teehistorian parser for DDNet
Documentation
use crate::error::Result;
use crate::{Chunk, Th, ThBufRead};
use std::collections::HashSet;

pub trait ThStream {
    /// May only be called before the first `next` method call.
    fn header(&mut self) -> Result<&[u8]>;
    fn next_chunk(&mut self) -> Result<Chunk>;
    /// Undos a single chunk read.
    /// May only be called once after each `next_chunk` call.
    fn reset_chunk(&mut self);
}

// Implement the same interface, so that we can decide during runtime which to use
impl<R: ThBufRead> ThStream for Th<R> {
    fn header(&mut self) -> Result<&[u8]> {
        self.header()
    }
    fn next_chunk(&mut self) -> Result<Chunk> {
        self.next_chunk()
    }

    fn reset_chunk(&mut self) {
        self.reset_chunk();
    }
}

/// Like [`Th`], with some added compatibility to bridge version differences.
pub struct ThCompat<R: ThBufRead> {
    inner: ThCompatEnum<R>,
}

enum ThCompatEnum<R: ThBufRead> {
    V2(ThMinor2Compat<R>),
    V4(ThMinor4Compat<R>),
    Newest(Th<R>),
}

#[derive(Debug, PartialEq)]
enum ThCompatVersion {
    V2,
    V4,
    Newest,
}

impl<R: ThBufRead> ThCompat<R> {
    pub fn parse(reader: R) -> Result<Self> {
        let mut raw = Th::parse(reader)?;
        let header = raw.header()?;
        let compat_version = header_version(header);
        let inner = match compat_version {
            None | Some(ThCompatVersion::V2) => {
                ThCompatEnum::V2(ThMinor2Compat::new(ThMinor4Compat::new(raw)))
            }
            Some(ThCompatVersion::V4) => ThCompatEnum::V4(ThMinor4Compat::new(raw)),
            Some(ThCompatVersion::Newest) => ThCompatEnum::Newest(raw),
        };
        Ok(Self { inner })
    }
}

impl<R: ThBufRead> ThStream for ThCompat<R> {
    fn header(&mut self) -> Result<&[u8]> {
        match &mut self.inner {
            ThCompatEnum::V2(inner) => inner.header(),
            ThCompatEnum::V4(inner) => inner.header(),
            ThCompatEnum::Newest(inner) => inner.header(),
        }
    }

    fn next_chunk(&mut self) -> Result<Chunk> {
        match &mut self.inner {
            ThCompatEnum::V2(inner) => inner.next_chunk(),
            ThCompatEnum::V4(inner) => inner.next_chunk(),
            ThCompatEnum::Newest(inner) => inner.next_chunk(),
        }
    }

    fn reset_chunk(&mut self) {
        match &mut self.inner {
            ThCompatEnum::V2(inner) => inner.reset_chunk(),
            ThCompatEnum::V4(inner) => inner.reset_chunk(),
            ThCompatEnum::Newest(inner) => inner.reset_chunk(),
        }
    }
}

/// Here we perform some hand-written json parsing.
/// We extract the value of the key `minor_version`.
fn header_version(slice: &[u8]) -> Option<ThCompatVersion> {
    let key_pattern = b"\"version_minor\"";
    let key_pos = slice
        .windows(key_pattern.len())
        .position(|w| w == key_pattern)?;
    let after_key_pos = key_pos + key_pattern.len();
    let mut remaining = slice[after_key_pos..].iter();
    let after_key_len = remaining.len();
    // Skip potential whitespaces to `:`
    loop {
        let c = *remaining.next()?;
        match c {
            b' ' | b'\t' | b'\n' | b'\r' => continue,
            b':' => break,
            _ => return None,
        }
    }
    // Skip potential whitespaces to `"`
    loop {
        let c = *remaining.next()?;
        match c {
            b' ' | b'\t' | b'\n' | b'\r' => continue,
            b'"' => break,
            _ => return None,
        }
    }
    let version_start = after_key_pos + after_key_len - remaining.len();
    // Skip string to `"` (ignore the possibility of `\"`)
    loop {
        let c = *remaining.next()?;
        if c == b'"' {
            break;
        }
    }
    let version_end = after_key_pos + after_key_len - remaining.len() - 1;
    let version_slice = &slice[version_start..version_end];
    let version_str = std::str::from_utf8(version_slice).ok()?;
    let version: u32 = version_str.parse().ok()?;
    Some(match version {
        0..=2 => ThCompatVersion::V2,
        3..=4 => ThCompatVersion::V4,
        _ => ThCompatVersion::Newest,
    })
}

// allow reset of last action
enum LastAction2 {
    Add(i32),
    Remove(i32),
    Chunk,
}

/// The PLAYER_READY was introduced in version 3
/// https://github.com/ddnet/ddnet/commit/3ea55dcc0ebc1c791e11cab0c268febe7e783504
struct ThMinor2Compat<R: ThBufRead> {
    inner: ThMinor4Compat<R>,
    had_player_ready: HashSet<i32>,
    last_action: LastAction2,
}

impl<R: ThBufRead> ThMinor2Compat<R> {
    fn new(reader: ThMinor4Compat<R>) -> Self {
        Self {
            inner: reader,
            had_player_ready: HashSet::new(),
            last_action: LastAction2::Chunk,
        }
    }
}

impl<R: ThBufRead> ThStream for ThMinor2Compat<R> {
    fn header(&mut self) -> Result<&[u8]> {
        self.inner.header()
    }

    fn next_chunk(&mut self) -> Result<Chunk> {
        let reader = &mut self.inner as *mut dyn ThStream;
        let cid;
        match self.inner.next_chunk()? {
            Chunk::PlayerNew(p) if !self.had_player_ready.contains(&p.cid) => {
                // reset chunk (after block due to lifetime) and insert PlayerReady event
                self.had_player_ready.insert(p.cid);
                self.last_action = LastAction2::Add(p.cid);
                cid = p.cid;
            }
            next_chunk => {
                self.last_action = LastAction2::Chunk;
                if let Chunk::Drop(d) = &next_chunk {
                    self.had_player_ready.remove(&d.cid);
                    self.last_action = LastAction2::Remove(d.cid);
                } else {
                    self.last_action = LastAction2::Chunk;
                }
                return Ok(next_chunk);
            }
        }
        // # Safety
        //
        // This is safe, because we don't do anything with self anymore.
        // It would compile with polonius. Currently only possible to enable in nightly:
        //
        //     RUSTFLAGS="-Z polonius" cargo +nightly c
        //
        //  When polonius stabilizes this unsafe can be removed.
        unsafe {
            (*reader).reset_chunk();
        }
        Ok(Chunk::PlayerReady { cid })
    }

    fn reset_chunk(&mut self) {
        match self.last_action {
            LastAction2::Add(cid) => self.had_player_ready.remove(&cid),
            LastAction2::Remove(cid) => self.had_player_ready.insert(cid),
            LastAction2::Chunk => false,
        };
        self.inner.reset_chunk();
    }
}

enum LastAction4 {
    Add(i32),
    Chunk,
}

/// A wrapper around [`ThBufRead`] to fix backwards compat issues
///  - After a map vote, the `PlayerJoin` events are missing. Add them for Tees when the first one
///    was missing until the first PlayerJoin is observed
struct ThMinor4Compat<R: ThBufRead> {
    inner: Th<R>,
    had_player_join: HashSet<i32>,
    last_action: LastAction4,
}

impl<R: ThBufRead> ThMinor4Compat<R> {
    fn new(reader: Th<R>) -> Self {
        Self {
            inner: reader,
            had_player_join: Default::default(),
            last_action: LastAction4::Chunk,
        }
    }
}

impl<R: ThBufRead> ThStream for ThMinor4Compat<R> {
    fn header(&mut self) -> Result<&[u8]> {
        self.inner.header()
    }

    fn next_chunk(&mut self) -> Result<Chunk> {
        let reader = &mut self.inner as *mut dyn ThStream;
        let cid;

        // insert join for currently not joined players
        let next_chunk = self.inner.next_chunk()?;
        if let Some(p_cid) = next_chunk.cid() {
            cid = p_cid;
            if cid < 0 || self.had_player_join.contains(&cid) {
                self.last_action = LastAction4::Chunk;
                return Ok(next_chunk);
            }
            if matches!(
                next_chunk,
                Chunk::Join { cid: _ } | Chunk::JoinVer6 { cid: _ } | Chunk::JoinVer7 { cid: _ }
            ) {
                // No need to generate compat Join event for this player
                self.had_player_join.insert(cid);
                return Ok(next_chunk);
            }
            self.last_action = LastAction4::Add(cid);
            self.had_player_join.insert(cid);
        } else {
            self.last_action = LastAction4::Chunk;
            return Ok(next_chunk);
        }

        // # Safety
        //
        // This is safe, because we don't do anything with self anymore.
        // It would compile with polonius. Currently only possible to enable in nightly:
        //
        //     RUSTFLAGS="-Z polonius" cargo +nightly c
        //
        //  When polonius stabilizes this unsafe can be removed.
        unsafe {
            (*reader).reset_chunk();
        }
        Ok(Chunk::Join { cid })
    }

    fn reset_chunk(&mut self) {
        match self.last_action {
            LastAction4::Add(cid) => {
                self.had_player_join.remove(&cid);
            }
            LastAction4::Chunk => {}
        }
        self.inner.reset_chunk();
    }
}

#[cfg(test)]
mod test {
    use super::header_version;
    use super::ThCompatVersion;

    #[test]
    fn header_empty() {
        assert_eq!(header_version(b""), None)
    }
    #[test]
    fn header_empty_json() {
        assert_eq!(header_version(b"{}"), None)
    }
    #[test]
    fn header_simple() {
        assert_eq!(
            header_version(b"{\"version_minor\":\"2\"}"),
            Some(ThCompatVersion::V2)
        )
    }
    #[test]
    fn header_simple2() {
        assert_eq!(header_version(b"{\"version_minor\":\"2a\"}"), None)
    }
    #[test]
    fn header_simple3() {
        assert_eq!(
            header_version(b"{\"version_minor\":\"5\"}"),
            Some(ThCompatVersion::Newest)
        )
    }
    #[test]
    fn header_whitespace() {
        assert_eq!(
            header_version(b"{ \"version_minor\"\t\r: \n \"3\" }\n"),
            Some(ThCompatVersion::V4)
        )
    }
}