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