Skip to main content

subtr_actor/stats/export/
mod.rs

1use serde::{Deserialize, Deserializer, Serialize};
2
3mod air_dribble;
4mod backboard;
5mod ball_carry;
6mod boost;
7mod ceiling_shot;
8mod core;
9mod demo;
10mod dodge_reset;
11mod double_tap;
12mod fifty_fifty;
13mod flick;
14mod half_flip;
15mod movement;
16mod musty_flick;
17mod one_timer;
18mod pass;
19mod positioning;
20mod possession;
21mod powerslide;
22mod pressure;
23mod rush;
24mod speed_flip;
25mod touch;
26mod wavedash;
27mod whiff;
28
29pub const LEGACY_STAT_VARIANT: &str = "legacy";
30pub const LABELED_STAT_VARIANT: &str = "labeled";
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
33#[serde(rename_all = "snake_case")]
34pub enum StatUnit {
35    Seconds,
36    Percent,
37    UnrealUnits,
38    UnrealUnitsPerSecond,
39    Boost,
40    BoostPerMinute,
41    Count,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, ts_rs::TS)]
45#[ts(export)]
46pub struct StatLabel {
47    pub key: &'static str,
48    pub value: &'static str,
49}
50
51impl StatLabel {
52    pub const fn new(key: &'static str, value: &'static str) -> Self {
53        Self { key, value }
54    }
55}
56
57impl<'de> Deserialize<'de> for StatLabel {
58    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
59    where
60        D: Deserializer<'de>,
61    {
62        #[derive(Deserialize)]
63        struct OwnedStatLabel {
64            key: String,
65            value: String,
66        }
67
68        let owned = OwnedStatLabel::deserialize(deserializer)?;
69        Ok(Self {
70            key: leak_string(owned.key),
71            value: leak_string(owned.value),
72        })
73    }
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
77pub struct StatDescriptor {
78    pub domain: &'static str,
79    pub name: &'static str,
80    pub variant: &'static str,
81    pub unit: StatUnit,
82    #[serde(default, skip_serializing_if = "Vec::is_empty")]
83    pub labels: Vec<StatLabel>,
84}
85
86impl<'de> Deserialize<'de> for StatDescriptor {
87    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
88    where
89        D: Deserializer<'de>,
90    {
91        #[derive(Deserialize)]
92        struct OwnedStatDescriptor {
93            domain: String,
94            name: String,
95            variant: String,
96            unit: StatUnit,
97            #[serde(default)]
98            labels: Vec<StatLabel>,
99        }
100
101        let owned = OwnedStatDescriptor::deserialize(deserializer)?;
102        Ok(Self {
103            domain: leak_string(owned.domain),
104            name: leak_string(owned.name),
105            variant: leak_string(owned.variant),
106            unit: owned.unit,
107            labels: owned.labels,
108        })
109    }
110}
111
112#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
113#[serde(tag = "value_type", content = "value", rename_all = "snake_case")]
114pub enum StatValue {
115    Float(f32),
116    Unsigned(u32),
117    Signed(i32),
118}
119
120#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)]
121#[ts(export)]
122pub struct LabeledCountEntry {
123    pub labels: Vec<StatLabel>,
124    pub count: u32,
125}
126
127#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)]
128#[ts(export)]
129pub struct LabeledCounts {
130    pub entries: Vec<LabeledCountEntry>,
131}
132
133impl LabeledCounts {
134    pub fn increment<I>(&mut self, labels: I)
135    where
136        I: IntoIterator<Item = StatLabel>,
137    {
138        let mut labels: Vec<_> = labels.into_iter().collect();
139        labels.sort();
140
141        if let Some(entry) = self.entries.iter_mut().find(|entry| entry.labels == labels) {
142            entry.count += 1;
143            return;
144        }
145
146        self.entries.push(LabeledCountEntry { labels, count: 1 });
147        self.entries
148            .sort_by(|left, right| left.labels.cmp(&right.labels));
149    }
150
151    pub fn count_matching(&self, required_labels: &[StatLabel]) -> u32 {
152        self.entries
153            .iter()
154            .filter(|entry| {
155                required_labels
156                    .iter()
157                    .all(|required_label| entry.labels.contains(required_label))
158            })
159            .map(|entry| entry.count)
160            .sum()
161    }
162
163    pub fn count_exact(&self, labels: &[StatLabel]) -> u32 {
164        let mut normalized_labels = labels.to_vec();
165        normalized_labels.sort();
166
167        self.entries
168            .iter()
169            .find(|entry| entry.labels == normalized_labels)
170            .map(|entry| entry.count)
171            .unwrap_or(0)
172    }
173
174    pub fn is_empty(&self) -> bool {
175        self.entries.is_empty()
176    }
177}
178
179#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ts_rs::TS)]
180#[ts(export)]
181pub struct LabeledFloatSumEntry {
182    pub labels: Vec<StatLabel>,
183    pub value: f32,
184}
185
186#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, ts_rs::TS)]
187#[ts(export)]
188pub struct LabeledFloatSums {
189    pub entries: Vec<LabeledFloatSumEntry>,
190}
191
192impl LabeledFloatSums {
193    pub fn add<I>(&mut self, labels: I, value: f32)
194    where
195        I: IntoIterator<Item = StatLabel>,
196    {
197        let mut labels: Vec<_> = labels.into_iter().collect();
198        labels.sort();
199
200        if let Some(entry) = self.entries.iter_mut().find(|entry| entry.labels == labels) {
201            entry.value += value;
202            return;
203        }
204
205        self.entries.push(LabeledFloatSumEntry { labels, value });
206        self.entries
207            .sort_by(|left, right| left.labels.cmp(&right.labels));
208    }
209
210    pub fn sum_matching(&self, required_labels: &[StatLabel]) -> f32 {
211        self.entries
212            .iter()
213            .filter(|entry| {
214                required_labels
215                    .iter()
216                    .all(|required_label| entry.labels.contains(required_label))
217            })
218            .map(|entry| entry.value)
219            .sum()
220    }
221
222    pub fn sum_exact(&self, labels: &[StatLabel]) -> f32 {
223        let mut normalized_labels = labels.to_vec();
224        normalized_labels.sort();
225
226        self.entries
227            .iter()
228            .find(|entry| entry.labels == normalized_labels)
229            .map(|entry| entry.value)
230            .unwrap_or(0.0)
231    }
232
233    pub fn is_empty(&self) -> bool {
234        self.entries.is_empty()
235    }
236}
237
238#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
239pub struct ExportedStat {
240    #[serde(flatten)]
241    pub descriptor: StatDescriptor,
242    pub value: StatValue,
243}
244
245impl ExportedStat {
246    pub fn float(domain: &'static str, name: &'static str, unit: StatUnit, value: f32) -> Self {
247        Self {
248            descriptor: StatDescriptor {
249                domain,
250                name,
251                variant: LEGACY_STAT_VARIANT,
252                unit,
253                labels: Vec::new(),
254            },
255            value: StatValue::Float(value),
256        }
257    }
258
259    pub fn unsigned(domain: &'static str, name: &'static str, unit: StatUnit, value: u32) -> Self {
260        Self {
261            descriptor: StatDescriptor {
262                domain,
263                name,
264                variant: LEGACY_STAT_VARIANT,
265                unit,
266                labels: Vec::new(),
267            },
268            value: StatValue::Unsigned(value),
269        }
270    }
271
272    pub fn signed(domain: &'static str, name: &'static str, unit: StatUnit, value: i32) -> Self {
273        Self {
274            descriptor: StatDescriptor {
275                domain,
276                name,
277                variant: LEGACY_STAT_VARIANT,
278                unit,
279                labels: Vec::new(),
280            },
281            value: StatValue::Signed(value),
282        }
283    }
284
285    pub fn unsigned_labeled(
286        domain: &'static str,
287        name: &'static str,
288        unit: StatUnit,
289        labels: Vec<StatLabel>,
290        value: u32,
291    ) -> Self {
292        Self {
293            descriptor: StatDescriptor {
294                domain,
295                name,
296                variant: LABELED_STAT_VARIANT,
297                unit,
298                labels,
299            },
300            value: StatValue::Unsigned(value),
301        }
302    }
303
304    pub fn float_labeled(
305        domain: &'static str,
306        name: &'static str,
307        unit: StatUnit,
308        labels: Vec<StatLabel>,
309        value: f32,
310    ) -> Self {
311        Self {
312            descriptor: StatDescriptor {
313                domain,
314                name,
315                variant: LABELED_STAT_VARIANT,
316                unit,
317                labels,
318            },
319            value: StatValue::Float(value),
320        }
321    }
322}
323
324pub trait StatFieldProvider {
325    fn visit_stat_fields(&self, visitor: &mut dyn FnMut(ExportedStat));
326
327    fn stat_fields(&self) -> Vec<ExportedStat> {
328        let mut fields = Vec::new();
329        self.visit_stat_fields(&mut |field| fields.push(field));
330        fields
331    }
332}
333
334fn leak_string(value: String) -> &'static str {
335    Box::leak(value.into_boxed_str())
336}