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