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 is_empty(&self) -> bool {
181 self.entries.is_empty()
182 }
183}
184
185#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ts_rs::TS)]
186#[ts(export)]
187pub struct LabeledFloatSumEntry {
188 pub labels: Vec<StatLabel>,
189 pub value: f32,
190}
191
192#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, ts_rs::TS)]
193#[ts(export)]
194pub struct LabeledFloatSums {
195 pub entries: Vec<LabeledFloatSumEntry>,
196}
197
198impl LabeledFloatSums {
199 pub fn add<I>(&mut self, labels: I, value: f32)
200 where
201 I: IntoIterator<Item = StatLabel>,
202 {
203 let mut labels: Vec<_> = labels.into_iter().collect();
204 labels.sort();
205
206 if let Some(entry) = self.entries.iter_mut().find(|entry| entry.labels == labels) {
207 entry.value += value;
208 return;
209 }
210
211 self.entries.push(LabeledFloatSumEntry { labels, value });
212 self.entries
213 .sort_by(|left, right| left.labels.cmp(&right.labels));
214 }
215
216 pub fn sum_matching(&self, required_labels: &[StatLabel]) -> f32 {
217 self.entries
218 .iter()
219 .filter(|entry| {
220 required_labels
221 .iter()
222 .all(|required_label| entry.labels.contains(required_label))
223 })
224 .map(|entry| entry.value)
225 .sum()
226 }
227
228 pub fn sum_exact(&self, labels: &[StatLabel]) -> f32 {
229 let mut normalized_labels = labels.to_vec();
230 normalized_labels.sort();
231
232 self.entries
233 .iter()
234 .find(|entry| entry.labels == normalized_labels)
235 .map(|entry| entry.value)
236 .unwrap_or(0.0)
237 }
238
239 pub fn is_empty(&self) -> bool {
240 self.entries.is_empty()
241 }
242}
243
244#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
245pub struct ExportedStat {
246 #[serde(flatten)]
247 pub descriptor: StatDescriptor,
248 pub value: StatValue,
249}
250
251impl ExportedStat {
252 pub fn float(domain: &'static str, name: &'static str, unit: StatUnit, value: f32) -> 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::Float(value),
262 }
263 }
264
265 pub fn unsigned(domain: &'static str, name: &'static str, unit: StatUnit, value: u32) -> 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::Unsigned(value),
275 }
276 }
277
278 pub fn signed(domain: &'static str, name: &'static str, unit: StatUnit, value: i32) -> Self {
279 Self {
280 descriptor: StatDescriptor {
281 domain,
282 name,
283 variant: LEGACY_STAT_VARIANT,
284 unit,
285 labels: Vec::new(),
286 },
287 value: StatValue::Signed(value),
288 }
289 }
290
291 pub fn unsigned_labeled(
292 domain: &'static str,
293 name: &'static str,
294 unit: StatUnit,
295 labels: Vec<StatLabel>,
296 value: u32,
297 ) -> Self {
298 Self {
299 descriptor: StatDescriptor {
300 domain,
301 name,
302 variant: LABELED_STAT_VARIANT,
303 unit,
304 labels,
305 },
306 value: StatValue::Unsigned(value),
307 }
308 }
309
310 pub fn float_labeled(
311 domain: &'static str,
312 name: &'static str,
313 unit: StatUnit,
314 labels: Vec<StatLabel>,
315 value: f32,
316 ) -> Self {
317 Self {
318 descriptor: StatDescriptor {
319 domain,
320 name,
321 variant: LABELED_STAT_VARIANT,
322 unit,
323 labels,
324 },
325 value: StatValue::Float(value),
326 }
327 }
328}
329
330pub trait StatFieldProvider {
331 fn visit_stat_fields(&self, visitor: &mut dyn FnMut(ExportedStat));
332
333 fn stat_fields(&self) -> Vec<ExportedStat> {
334 let mut fields = Vec::new();
335 self.visit_stat_fields(&mut |field| fields.push(field));
336 fields
337 }
338}
339
340fn leak_string(value: String) -> &'static str {
341 Box::leak(value.into_boxed_str())
342}