twgame-core 0.9.0

Game trait, helper types and helper functions for twgame
Documentation
use crate::database::Finishes;
use crate::Game;
use crate::Snapper;
use crate::ThHeader;
use teehistorian::Chunk;
use twsnap::{
    compat::ddnet::{DemoWriter, WriteError},
    time::Instant,
    Snap,
};
use vek::Vec2;

pub type DemoChatPtr<'a> = Option<&'a mut dyn DemoChatWrite>;
pub type DemoPtr<'a, T> = Option<&'a mut dyn DemoWrite<T>>;

#[derive(Debug, Clone, PartialEq)]
pub struct ReplayerTeeInfo {
    pub team: i32,
    pub practice: bool,
    pub pos: Vec2<i32>,
}

/// separate out the `ReplayerConfigure` from `ReplayerChecker`, because
/// the physics implementations only implement this function, but all
/// other replayer implementations (including the wrapper around the physics)
/// implement all functions
pub trait ReplayerConfigure {
    fn on_teehistorian_header(&mut self, raw: &[u8], header: &ThHeader);
}

pub trait ReplayerChecker: ReplayerConfigure {
    /// called on each Teehistorian chunk
    fn on_teehistorian_chunk(&mut self, now: Instant, chunk: &Chunk);

    fn on_finish(&mut self, now: Instant, finish: &Finishes);

    fn check_tees(
        &mut self,
        cur_time: Instant,
        tees: &[Option<ReplayerTeeInfo>],
        demo: DemoChatPtr,
    );

    /// called after last chunk was read from Teehistorian file
    fn finalize(&mut self);
}

pub trait ReplayerCheckerHelper {
    fn check_tees(
        &mut self,
        cur_time: Instant,
        tees: &[Option<ReplayerTeeInfo>],
        demo: DemoChatPtr,
    ) -> bool;
}

/// trait for easier GameValidator implementation
pub trait GameValidator {
    fn tee_pos(&self, id: u32) -> Option<Vec2<i32>>;
    // at least peak number of player, where player_count-1 is still in game
    fn max_tee_id(&self) -> u32;

    // adjust functions
    fn set_tee_pos(&mut self, id: u32, pos: Option<Vec2<i32>>);

    // to check from replayer if players are in correct team. Return 0 if player doesn't exist
    fn player_team(&self, id: u32) -> i32;
    // sets player to specific team if incorrect team was return in `player_team`
    fn set_player_team(&mut self, id: u32, team: i32);
}

#[macro_export]
macro_rules! info_demo {
    ($write_chat:expr, $cur_time:expr, $($format_arg:tt)*) => {
        if ::log::log_enabled!(log::Level::Info) {
            let write_chat = $write_chat;
            let cur_time: Instant = $cur_time;
            let msg = format!($($format_arg)*);
            ::log::info!("{cur_time}: {msg}");
            if let Some(demo) = write_chat {
                demo.write_chat(&msg).unwrap()
            }
        }
    };
}

impl<T> ReplayerCheckerHelper for T
where
    T: GameValidator,
{
    fn check_tees(
        &mut self,
        cur_time: Instant,
        tees: &[Option<ReplayerTeeInfo>],
        demo: DemoChatPtr,
    ) -> bool {
        let mut demo = demo;
        let mut correct = true;
        for (player_id, player) in tees.iter().enumerate() {
            let player_id = player_id as u32;
            let world_pos = self.tee_pos(player_id);

            let in_practice = player.as_ref().is_some_and(|p| p.practice);
            let teehistorian_pos = player.as_ref().map(|p| p.pos);

            if teehistorian_pos != world_pos {
                if !in_practice {
                    correct = false;
                    info_demo!(
                        demo.as_deref_mut(),
                        cur_time,
                        "incorrect tee_pos player_id={}, world={:?}, teehistorian={:?} ({:?}), diff={:?}",
                        player_id,
                        world_pos,
                        teehistorian_pos,
                        teehistorian_pos.map(|p| p/32),
                        teehistorian_pos.zip(world_pos).map(|(a,b)| a - b)
                    );
                }
                self.set_tee_pos(player_id, teehistorian_pos);
            }

            // only check team if tee exists
            if teehistorian_pos.is_some() {
                let world_team = self.player_team(player_id);
                let teehistorian_team = player.as_ref().map(|p| p.team).unwrap();
                if teehistorian_team != world_team {
                    correct = false;
                    info_demo!(
                        demo.as_deref_mut(),
                        cur_time,
                        "incorrect team player_id={player_id}, world={world_team}, teehistorian={teehistorian_team}"
                    );
                    self.set_player_team(player_id, teehistorian_team);
                }
            }
        }
        for player_id in tees.len() as u32..self.max_tee_id() {
            if let Some(world_pos) = self.tee_pos(player_id) {
                correct = false;
                info_demo!(
                    demo.as_deref_mut(),
                    cur_time,
                    "incorrect tee_pos player_id={}, world={:?}, teehistorian=None",
                    player_id,
                    world_pos,
                );
                self.set_tee_pos(player_id, None);
            }
        }
        correct
    }
}

/// All structs that implement
/// * Game and
/// * GameReplayer (either directly or automatically through GameValidator)
///
/// implement this marker trait and can be passed to the Teehistorian Replayer
pub trait GameReplayerAll: Game + ReplayerChecker {}
impl<T> GameReplayerAll for T where T: Game + ReplayerChecker {}

pub trait DemoWrite<World>: DemoChatWrite {
    fn snap_and_write(
        &mut self,
        tick: Instant,
        world: &World,
        snap_buf: &mut Snap,
    ) -> Result<(), WriteError>;

    // workaround until https://github.com/rust-lang/rust/issues/65991 stablilzed
    fn chat(&mut self) -> &mut dyn DemoChatWrite;
}
pub trait DemoChatWrite {
    fn write_chat(&mut self, msg: &str) -> Result<(), WriteError>;
    fn write_player_chat(&mut self, player_id: i32, msg: &str) -> Result<(), WriteError>;
}

impl<T: Snapper> DemoWrite<T> for DemoWriter<'_> {
    fn snap_and_write(
        &mut self,
        tick: Instant,
        world: &T,
        snap_buf: &mut Snap,
    ) -> Result<(), WriteError> {
        world.snap(snap_buf);
        let res = self.write_snapshot(tick, snap_buf);
        snap_buf.clear();
        res
    }

    fn chat(&mut self) -> &mut dyn DemoChatWrite {
        self
    }
}

impl DemoChatWrite for DemoWriter<'_> {
    fn write_chat(&mut self, msg: &str) -> Result<(), WriteError> {
        self.write_chat(msg)
    }
    fn write_player_chat(&mut self, player_id: i32, msg: &str) -> Result<(), WriteError> {
        self.write_player_chat(player_id, msg)
    }
}