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