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