Skip to main content

subtr_actor/stats/calculators/
demo.rs

1use super::*;
2
3const DEMO_REPEAT_FRAME_WINDOW: usize = 8;
4
5#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, ts_rs::TS)]
6#[ts(export)]
7pub struct DemoPlayerStats {
8    pub demos_inflicted: u32,
9    pub demos_taken: u32,
10}
11
12#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, ts_rs::TS)]
13#[ts(export)]
14pub struct DemoTeamStats {
15    pub demos_inflicted: u32,
16}
17
18#[derive(Debug, Clone, Default, PartialEq)]
19pub struct DemoCalculator {
20    player_stats: HashMap<PlayerId, DemoPlayerStats>,
21    player_teams: HashMap<PlayerId, bool>,
22    team_zero_stats: DemoTeamStats,
23    team_one_stats: DemoTeamStats,
24    timeline: Vec<TimelineEvent>,
25    last_seen_frame: HashMap<(PlayerId, PlayerId), usize>,
26    active_pairs: HashSet<(PlayerId, PlayerId)>,
27}
28
29impl DemoCalculator {
30    pub fn new() -> Self {
31        Self::default()
32    }
33
34    pub fn player_stats(&self) -> &HashMap<PlayerId, DemoPlayerStats> {
35        &self.player_stats
36    }
37
38    pub fn team_zero_stats(&self) -> &DemoTeamStats {
39        &self.team_zero_stats
40    }
41
42    pub fn team_one_stats(&self) -> &DemoTeamStats {
43        &self.team_one_stats
44    }
45
46    pub fn timeline(&self) -> &[TimelineEvent] {
47        &self.timeline
48    }
49
50    fn should_count_demo(
51        &mut self,
52        attacker: &PlayerId,
53        victim: &PlayerId,
54        frame_number: usize,
55    ) -> bool {
56        let key = (attacker.clone(), victim.clone());
57        let already_counted = self
58            .last_seen_frame
59            .get(&key)
60            .map(|previous_frame| {
61                frame_number.saturating_sub(*previous_frame) <= DEMO_REPEAT_FRAME_WINDOW
62            })
63            .unwrap_or(false);
64        self.last_seen_frame.insert(key, frame_number);
65        !already_counted
66    }
67
68    pub fn update(
69        &mut self,
70        frame: &FrameInfo,
71        players: &PlayerFrameState,
72        events: &FrameEventsState,
73    ) -> SubtrActorResult<()> {
74        for player in &players.players {
75            self.player_teams
76                .insert(player.player_id.clone(), player.is_team_0);
77        }
78
79        if !events.demo_events.is_empty() {
80            for demo in &events.demo_events {
81                self.record_demo(&demo.attacker, &demo.victim, demo.time, demo.frame);
82            }
83            self.active_pairs = active_demo_pairs(events);
84            return Ok(());
85        }
86
87        let current_active_pairs = active_demo_pairs(events);
88        for demo in &events.active_demos {
89            if self
90                .active_pairs
91                .contains(&(demo.attacker.clone(), demo.victim.clone()))
92            {
93                continue;
94            }
95            self.record_demo(&demo.attacker, &demo.victim, frame.time, frame.frame_number);
96        }
97        self.active_pairs = current_active_pairs;
98
99        Ok(())
100    }
101}
102
103fn active_demo_pairs(events: &FrameEventsState) -> HashSet<(PlayerId, PlayerId)> {
104    events
105        .active_demos
106        .iter()
107        .map(|demo| (demo.attacker.clone(), demo.victim.clone()))
108        .collect()
109}
110
111impl DemoCalculator {
112    fn record_demo(
113        &mut self,
114        attacker: &PlayerId,
115        victim: &PlayerId,
116        time: f32,
117        frame_number: usize,
118    ) {
119        if !self.should_count_demo(attacker, victim, frame_number) {
120            return;
121        }
122
123        self.player_stats
124            .entry(attacker.clone())
125            .or_default()
126            .demos_inflicted += 1;
127        self.player_stats
128            .entry(victim.clone())
129            .or_default()
130            .demos_taken += 1;
131
132        match self.player_teams.get(attacker).copied() {
133            Some(true) => self.team_zero_stats.demos_inflicted += 1,
134            Some(false) => self.team_one_stats.demos_inflicted += 1,
135            None => {}
136        }
137
138        self.timeline.push(TimelineEvent {
139            time,
140            frame: Some(frame_number),
141            kind: TimelineEventKind::Kill,
142            player_id: Some(attacker.clone()),
143            is_team_0: self.player_teams.get(attacker).copied(),
144        });
145        self.timeline.push(TimelineEvent {
146            time,
147            frame: Some(frame_number),
148            kind: TimelineEventKind::Death,
149            player_id: Some(victim.clone()),
150            is_team_0: self.player_teams.get(victim).copied(),
151        });
152    }
153}
154
155#[cfg(test)]
156#[path = "demo_tests.rs"]
157mod tests;