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