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