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 total(&self) -> u32 {
181 self.entries.iter().map(|entry| entry.count).sum()
182 }
183
184 pub fn complete_from_label_sets(label_sets: &[&[StatLabel]], counts: &Self) -> Self {
185 fn append_entries(
186 label_sets: &[&[StatLabel]],
187 index: usize,
188 labels: &mut Vec<StatLabel>,
189 counts: &LabeledCounts,
190 entries: &mut Vec<LabeledCountEntry>,
191 ) {
192 if index == label_sets.len() {
193 let mut normalized_labels = labels.clone();
194 normalized_labels.sort();
195 entries.push(LabeledCountEntry {
196 count: counts.count_exact(&normalized_labels),
197 labels: normalized_labels,
198 });
199 return;
200 }
201
202 for label in label_sets[index] {
203 labels.push(label.clone());
204 append_entries(label_sets, index + 1, labels, counts, entries);
205 labels.pop();
206 }
207 }
208
209 let mut entries = Vec::new();
210 append_entries(label_sets, 0, &mut Vec::new(), counts, &mut entries);
211 entries.sort_by(|left, right| left.labels.cmp(&right.labels));
212 Self { entries }
213 }
214
215 pub fn is_empty(&self) -> bool {
216 self.entries.is_empty()
217 }
218}
219
220#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ts_rs::TS)]
221#[ts(export)]
222pub struct LabeledFloatSumEntry {
223 pub labels: Vec<StatLabel>,
224 pub value: f32,
225}
226
227#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, ts_rs::TS)]
228#[ts(export)]
229pub struct LabeledFloatSums {
230 pub entries: Vec<LabeledFloatSumEntry>,
231}
232
233impl LabeledFloatSums {
234 pub fn add<I>(&mut self, labels: I, value: f32)
235 where
236 I: IntoIterator<Item = StatLabel>,
237 {
238 let mut labels: Vec<_> = labels.into_iter().collect();
239 labels.sort();
240
241 if let Some(entry) = self.entries.iter_mut().find(|entry| entry.labels == labels) {
242 entry.value += value;
243 return;
244 }
245
246 self.entries.push(LabeledFloatSumEntry { labels, value });
247 self.entries
248 .sort_by(|left, right| left.labels.cmp(&right.labels));
249 }
250
251 pub fn sum_matching(&self, required_labels: &[StatLabel]) -> f32 {
252 self.entries
253 .iter()
254 .filter(|entry| {
255 required_labels
256 .iter()
257 .all(|required_label| entry.labels.contains(required_label))
258 })
259 .map(|entry| entry.value)
260 .sum()
261 }
262
263 pub fn sum_exact(&self, labels: &[StatLabel]) -> f32 {
264 let mut normalized_labels = labels.to_vec();
265 normalized_labels.sort();
266
267 self.entries
268 .iter()
269 .find(|entry| entry.labels == normalized_labels)
270 .map(|entry| entry.value)
271 .unwrap_or(0.0)
272 }
273
274 pub fn is_empty(&self) -> bool {
275 self.entries.is_empty()
276 }
277}
278
279#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
280pub struct ExportedStat {
281 #[serde(flatten)]
282 pub descriptor: StatDescriptor,
283 pub value: StatValue,
284}
285
286impl ExportedStat {
287 pub fn float(domain: &'static str, name: &'static str, unit: StatUnit, value: f32) -> Self {
288 Self {
289 descriptor: StatDescriptor {
290 domain,
291 name,
292 variant: LEGACY_STAT_VARIANT,
293 unit,
294 labels: Vec::new(),
295 },
296 value: StatValue::Float(value),
297 }
298 }
299
300 pub fn unsigned(domain: &'static str, name: &'static str, unit: StatUnit, value: u32) -> Self {
301 Self {
302 descriptor: StatDescriptor {
303 domain,
304 name,
305 variant: LEGACY_STAT_VARIANT,
306 unit,
307 labels: Vec::new(),
308 },
309 value: StatValue::Unsigned(value),
310 }
311 }
312
313 pub fn signed(domain: &'static str, name: &'static str, unit: StatUnit, value: i32) -> Self {
314 Self {
315 descriptor: StatDescriptor {
316 domain,
317 name,
318 variant: LEGACY_STAT_VARIANT,
319 unit,
320 labels: Vec::new(),
321 },
322 value: StatValue::Signed(value),
323 }
324 }
325
326 pub fn unsigned_labeled(
327 domain: &'static str,
328 name: &'static str,
329 unit: StatUnit,
330 labels: Vec<StatLabel>,
331 value: u32,
332 ) -> Self {
333 Self {
334 descriptor: StatDescriptor {
335 domain,
336 name,
337 variant: LABELED_STAT_VARIANT,
338 unit,
339 labels,
340 },
341 value: StatValue::Unsigned(value),
342 }
343 }
344
345 pub fn float_labeled(
346 domain: &'static str,
347 name: &'static str,
348 unit: StatUnit,
349 labels: Vec<StatLabel>,
350 value: f32,
351 ) -> Self {
352 Self {
353 descriptor: StatDescriptor {
354 domain,
355 name,
356 variant: LABELED_STAT_VARIANT,
357 unit,
358 labels,
359 },
360 value: StatValue::Float(value),
361 }
362 }
363}
364
365pub trait StatFieldProvider {
366 fn visit_stat_fields(&self, visitor: &mut dyn FnMut(ExportedStat));
367
368 fn stat_fields(&self) -> Vec<ExportedStat> {
369 let mut fields = Vec::new();
370 self.visit_stat_fields(&mut |field| fields.push(field));
371 fields
372 }
373}
374
375fn leak_string(value: String) -> &'static str {
376 Box::leak(value.into_boxed_str())
377}