subtr-actor 0.4.0

Rocket League replay transformer
Documentation
use super::*;

const DEMO_REPEAT_FRAME_WINDOW: usize = 8;

#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, ts_rs::TS)]
#[ts(export)]
pub struct DemoPlayerStats {
    pub demos_inflicted: u32,
    pub demos_taken: u32,
}

#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, ts_rs::TS)]
#[ts(export)]
pub struct DemoTeamStats {
    pub demos_inflicted: u32,
}

#[derive(Debug, Clone, Default, PartialEq)]
pub struct DemoCalculator {
    player_stats: HashMap<PlayerId, DemoPlayerStats>,
    player_teams: HashMap<PlayerId, bool>,
    team_zero_stats: DemoTeamStats,
    team_one_stats: DemoTeamStats,
    timeline: Vec<TimelineEvent>,
    last_seen_frame: HashMap<(PlayerId, PlayerId), usize>,
}

impl DemoCalculator {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn player_stats(&self) -> &HashMap<PlayerId, DemoPlayerStats> {
        &self.player_stats
    }

    pub fn team_zero_stats(&self) -> &DemoTeamStats {
        &self.team_zero_stats
    }

    pub fn team_one_stats(&self) -> &DemoTeamStats {
        &self.team_one_stats
    }

    pub fn timeline(&self) -> &[TimelineEvent] {
        &self.timeline
    }

    fn should_count_demo(
        &mut self,
        attacker: &PlayerId,
        victim: &PlayerId,
        frame_number: usize,
    ) -> bool {
        let key = (attacker.clone(), victim.clone());
        let already_counted = self
            .last_seen_frame
            .get(&key)
            .map(|previous_frame| {
                frame_number.saturating_sub(*previous_frame) <= DEMO_REPEAT_FRAME_WINDOW
            })
            .unwrap_or(false);
        self.last_seen_frame.insert(key, frame_number);
        !already_counted
    }

    pub fn update(
        &mut self,
        frame: &FrameInfo,
        players: &PlayerFrameState,
        events: &FrameEventsState,
    ) -> SubtrActorResult<()> {
        for player in &players.players {
            self.player_teams
                .insert(player.player_id.clone(), player.is_team_0);
        }

        if !events.demo_events.is_empty() {
            for demo in &events.demo_events {
                self.record_demo(&demo.attacker, &demo.victim, demo.time, demo.frame);
            }
            return Ok(());
        }

        for demo in &events.active_demos {
            self.record_demo(&demo.attacker, &demo.victim, frame.time, frame.frame_number);
        }

        Ok(())
    }
}

impl DemoCalculator {
    fn record_demo(
        &mut self,
        attacker: &PlayerId,
        victim: &PlayerId,
        time: f32,
        frame_number: usize,
    ) {
        if !self.should_count_demo(attacker, victim, frame_number) {
            return;
        }

        self.player_stats
            .entry(attacker.clone())
            .or_default()
            .demos_inflicted += 1;
        self.player_stats
            .entry(victim.clone())
            .or_default()
            .demos_taken += 1;

        match self.player_teams.get(attacker).copied() {
            Some(true) => self.team_zero_stats.demos_inflicted += 1,
            Some(false) => self.team_one_stats.demos_inflicted += 1,
            None => {}
        }

        self.timeline.push(TimelineEvent {
            time,
            kind: TimelineEventKind::Kill,
            player_id: Some(attacker.clone()),
            is_team_0: self.player_teams.get(attacker).copied(),
        });
        self.timeline.push(TimelineEvent {
            time,
            kind: TimelineEventKind::Death,
            player_id: Some(victim.clone()),
            is_team_0: self.player_teams.get(victim).copied(),
        });
    }
}