subtr_actor/stats/calculators/
demo.rs1use super::*;
2
3const DEMO_REPEAT_FRAME_WINDOW: usize = 8;
4
5#[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#[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;