Skip to main content

subtr_actor/stats/calculators/
demo.rs

1use super::*;
2
3const DEMO_REPEAT_FRAME_WINDOW: usize = 8;
4
5/// A single demolition, linking the demoer (`attacker`) to the demoee
6/// (`victim`). Each demolition emits exactly one of these events; the attacker
7/// side contributes a demo inflicted and the victim side a demo taken.
8#[derive(Debug, Clone, PartialEq, Serialize, ts_rs::TS)]
9#[ts(export)]
10pub struct DemolitionEvent {
11    pub time: f32,
12    pub frame: usize,
13    #[ts(as = "crate::interop::ts_bindings::RemoteIdTs")]
14    pub attacker: PlayerId,
15    #[ts(as = "crate::interop::ts_bindings::RemoteIdTs")]
16    pub victim: PlayerId,
17    #[serde(default, skip_serializing_if = "Option::is_none")]
18    pub attacker_is_team_0: Option<bool>,
19    #[serde(default, skip_serializing_if = "Option::is_none")]
20    pub victim_is_team_0: Option<bool>,
21    #[serde(default, skip_serializing_if = "Option::is_none")]
22    pub attacker_position: Option<[f32; 3]>,
23    #[serde(default, skip_serializing_if = "Option::is_none")]
24    pub victim_position: Option<[f32; 3]>,
25}
26
27/// Detects demolitions from player frame state and frame events.
28#[derive(Debug, Clone, Default, PartialEq)]
29pub struct DemoCalculator {
30    player_teams: HashMap<PlayerId, bool>,
31    timeline: EventStream<DemolitionEvent>,
32    last_seen_frame: HashMap<(PlayerId, PlayerId), usize>,
33    active_pairs: HashSet<(PlayerId, PlayerId)>,
34}
35
36impl DemoCalculator {
37    pub fn new() -> Self {
38        Self::default()
39    }
40
41    pub fn events(&self) -> &[DemolitionEvent] {
42        self.timeline.all()
43    }
44
45    pub fn new_events(&self) -> &[DemolitionEvent] {
46        self.timeline.new_events()
47    }
48
49    fn should_count_demo(
50        &mut self,
51        attacker: &PlayerId,
52        victim: &PlayerId,
53        frame_number: usize,
54    ) -> bool {
55        let key = (attacker.clone(), victim.clone());
56        let already_counted = self
57            .last_seen_frame
58            .get(&key)
59            .map(|previous_frame| {
60                frame_number.saturating_sub(*previous_frame) <= DEMO_REPEAT_FRAME_WINDOW
61            })
62            .unwrap_or(false);
63        self.last_seen_frame.insert(key, frame_number);
64        !already_counted
65    }
66
67    pub fn update(
68        &mut self,
69        frame: &FrameInfo,
70        players: &PlayerFrameState,
71        events: &FrameEventsState,
72    ) -> SubtrActorResult<()> {
73        self.timeline.begin_update();
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(
82                    &demo.attacker,
83                    demo.attacker_location
84                        .map(|position| vec_to_glam(&position).to_array()),
85                    &demo.victim,
86                    Some(vec_to_glam(&demo.victim_location).to_array()),
87                    demo.time,
88                    demo.frame,
89                );
90            }
91            self.active_pairs = active_demo_pairs(events);
92            return Ok(());
93        }
94
95        let current_active_pairs = active_demo_pairs(events);
96        for demo in &events.active_demos {
97            if self
98                .active_pairs
99                .contains(&(demo.attacker.clone(), demo.victim.clone()))
100            {
101                continue;
102            }
103            self.record_demo(
104                &demo.attacker,
105                None,
106                &demo.victim,
107                None,
108                frame.time,
109                frame.frame_number,
110            );
111        }
112        self.active_pairs = current_active_pairs;
113
114        Ok(())
115    }
116}
117
118fn active_demo_pairs(events: &FrameEventsState) -> HashSet<(PlayerId, PlayerId)> {
119    events
120        .active_demos
121        .iter()
122        .map(|demo| (demo.attacker.clone(), demo.victim.clone()))
123        .collect()
124}
125
126impl DemoCalculator {
127    fn record_demo(
128        &mut self,
129        attacker: &PlayerId,
130        attacker_position: Option<[f32; 3]>,
131        victim: &PlayerId,
132        victim_position: Option<[f32; 3]>,
133        time: f32,
134        frame_number: usize,
135    ) {
136        if !self.should_count_demo(attacker, victim, frame_number) {
137            return;
138        }
139
140        self.timeline.push(DemolitionEvent {
141            time,
142            frame: frame_number,
143            attacker: attacker.clone(),
144            victim: victim.clone(),
145            attacker_is_team_0: self.player_teams.get(attacker).copied(),
146            victim_is_team_0: self.player_teams.get(victim).copied(),
147            attacker_position,
148            victim_position,
149        });
150    }
151}
152
153#[cfg(test)]
154#[path = "demo_tests.rs"]
155mod tests;