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