subtr_actor/stats/calculators/
demo.rs1use 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;