Skip to main content

subtr_actor/stats/export/
mod.rs

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