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