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