subtr-actor 1.0.0

Rocket League replay transformer
Documentation
use super::*;

const DEMO_REPEAT_FRAME_WINDOW: usize = 8;

#[derive(Debug, Clone, Default, PartialEq)]
pub struct DemoCalculator {
    player_teams: HashMap<PlayerId, bool>,
    timeline: EventStream<TimelineEvent>,
    last_seen_frame: HashMap<(PlayerId, PlayerId), usize>,
    active_pairs: HashSet<(PlayerId, PlayerId)>,
}

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

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

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

    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<()> {
        self.timeline.begin_update();
        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.attacker_location
                        .map(|position| vec_to_glam(&position).to_array()),
                    &demo.victim,
                    Some(vec_to_glam(&demo.victim_location).to_array()),
                    demo.time,
                    demo.frame,
                );
            }
            self.active_pairs = active_demo_pairs(events);
            return Ok(());
        }

        let current_active_pairs = active_demo_pairs(events);
        for demo in &events.active_demos {
            if self
                .active_pairs
                .contains(&(demo.attacker.clone(), demo.victim.clone()))
            {
                continue;
            }
            self.record_demo(
                &demo.attacker,
                None,
                &demo.victim,
                None,
                frame.time,
                frame.frame_number,
            );
        }
        self.active_pairs = current_active_pairs;

        Ok(())
    }
}

fn active_demo_pairs(events: &FrameEventsState) -> HashSet<(PlayerId, PlayerId)> {
    events
        .active_demos
        .iter()
        .map(|demo| (demo.attacker.clone(), demo.victim.clone()))
        .collect()
}

impl DemoCalculator {
    fn record_demo(
        &mut self,
        attacker: &PlayerId,
        attacker_position: Option<[f32; 3]>,
        victim: &PlayerId,
        victim_position: Option<[f32; 3]>,
        time: f32,
        frame_number: usize,
    ) {
        if !self.should_count_demo(attacker, victim, frame_number) {
            return;
        }

        let kill_event = TimelineEvent {
            time,
            frame: Some(frame_number),
            kind: TimelineEventKind::Kill,
            player_id: Some(attacker.clone()),
            player_position: attacker_position,
            is_team_0: self.player_teams.get(attacker).copied(),
        };
        self.timeline.push(kill_event);

        let death_event = TimelineEvent {
            time,
            frame: Some(frame_number),
            kind: TimelineEventKind::Death,
            player_id: Some(victim.clone()),
            player_position: victim_position,
            is_team_0: self.player_teams.get(victim).copied(),
        };
        self.timeline.push(death_event);
    }
}

#[cfg(test)]
#[path = "demo_tests.rs"]
mod tests;