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