subtr_actor/stats/calculators/
player_state_span.rs1use super::*;
2
3#[derive(Debug, Clone, PartialEq, Serialize, ts_rs::TS)]
14#[ts(export)]
15pub struct PlayerStateSpan<S> {
16 pub time: f32,
17 pub frame: usize,
18 pub end_time: f32,
19 pub end_frame: usize,
20 pub duration: f32,
21 #[ts(as = "crate::interop::ts_bindings::RemoteIdTs")]
22 pub player: PlayerId,
23 #[serde(default, skip_serializing_if = "Option::is_none")]
24 pub player_position: Option<[f32; 3]>,
25 pub is_team_0: bool,
26 pub state: S,
27}
28
29fn player_sort_key(player: &PlayerId) -> String {
30 format!("{player:?}")
31}
32
33#[derive(Debug, Clone)]
38pub struct PlayerSpanTracker<S> {
39 open: HashMap<PlayerId, PlayerStateSpan<S>>,
40 closed: EventStream<PlayerStateSpan<S>>,
41}
42
43impl<S> Default for PlayerSpanTracker<S> {
44 fn default() -> Self {
45 Self {
46 open: HashMap::new(),
47 closed: EventStream::default(),
48 }
49 }
50}
51
52impl<S: Clone + PartialEq> PlayerSpanTracker<S> {
53 pub fn begin_update(&mut self) {
54 self.closed.begin_update();
55 }
56
57 #[allow(clippy::too_many_arguments)]
60 pub fn record(
61 &mut self,
62 frame_number: usize,
63 start_time: f32,
64 end_time: f32,
65 duration: f32,
66 player: &PlayerId,
67 player_position: Option<[f32; 3]>,
68 is_team_0: bool,
69 state: S,
70 ) {
71 if let Some(open) = self.open.get_mut(player) {
72 if open.state == state {
73 open.end_time = end_time;
74 open.end_frame = frame_number;
75 open.duration += duration;
76 open.player_position = player_position;
77 return;
78 }
79 }
80 let span = PlayerStateSpan {
81 time: start_time,
82 frame: frame_number,
83 end_time,
84 end_frame: frame_number,
85 duration,
86 player: player.clone(),
87 player_position,
88 is_team_0,
89 state,
90 };
91 if let Some(previous) = self.open.insert(player.clone(), span) {
92 self.closed.push(previous);
93 }
94 }
95
96 pub fn close(&mut self, player: &PlayerId) {
97 if let Some(span) = self.open.remove(player) {
98 self.closed.push(span);
99 }
100 }
101
102 pub fn close_all(&mut self) {
103 let mut spans: Vec<_> = self.open.drain().map(|(_, span)| span).collect();
104 spans.sort_by_key(|span| player_sort_key(&span.player));
105 self.closed.extend(spans);
106 }
107
108 pub fn events(&self) -> &[PlayerStateSpan<S>] {
110 self.closed.all()
111 }
112
113 pub fn new_events(&self) -> &[PlayerStateSpan<S>] {
115 self.closed.new_events()
116 }
117
118 pub fn projected_events(&self) -> Vec<PlayerStateSpan<S>> {
120 let mut events = self.closed.all().to_vec();
121 let mut open: Vec<_> = self.open.values().cloned().collect();
122 open.sort_by_key(|span| player_sort_key(&span.player));
123 events.extend(open);
124 events
125 }
126}
127
128pub(crate) fn scalar_state_segments<S: Copy>(
133 start: f32,
134 end: f32,
135 thresholds: &[f32],
136 states: &[S],
137) -> Vec<(S, f32)> {
138 debug_assert_eq!(states.len(), thresholds.len() + 1);
139 let region = |value: f32| -> usize { thresholds.iter().take_while(|&&t| value >= t).count() };
140 let start_region = region(start);
141 let end_region = region(end);
142 if (end - start).abs() <= f32::EPSILON || start_region == end_region {
143 return vec![(states[start_region], 1.0)];
144 }
145 let direction: isize = if end > start { 1 } else { -1 };
146 let mut segments = Vec::new();
147 let mut current_region = start_region;
148 let mut previous_t = 0.0f32;
149 while current_region != end_region {
150 let crossing = if direction > 0 {
151 thresholds[current_region]
152 } else {
153 thresholds[current_region - 1]
154 };
155 let t = ((crossing - start) / (end - start)).clamp(0.0, 1.0);
156 segments.push((states[current_region], (t - previous_t).max(0.0)));
157 previous_t = t;
158 current_region = (current_region as isize + direction) as usize;
159 }
160 segments.push((states[end_region], (1.0 - previous_t).max(0.0)));
161 segments
162}
163
164#[cfg(test)]
165#[path = "player_state_span_tests.rs"]
166mod tests;