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