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