1use std::collections::HashSet;
2use std::marker::PhantomData;
3
4use serde_json::{Map, Value};
5
6use crate::collector::frame_resolution::{
7 FinalStatsFrameAction, StatsFramePersistenceController, StatsFrameResolution,
8};
9use crate::stats::analysis_graph::{graph_with_builtin_analysis_nodes, AnalysisGraph};
10use crate::*;
11
12use super::builtins::{
13 builtin_module_json, builtin_snapshot_config_json, builtin_snapshot_frame_json,
14 builtin_stats_module_names,
15};
16use super::playback::{
17 CapturedStatsData, CapturedStatsFrame, StatsSnapshotData, StatsSnapshotFrame,
18};
19use super::types::{serialize_to_json_value, CollectedStats, CollectedStatsModule};
20
21#[derive(Default)]
22enum SampleMode {
23 #[default]
24 Aggregate,
25 Timeline,
26}
27
28struct BuiltinModuleSelection {
29 module_names: Vec<&'static str>,
30}
31
32impl BuiltinModuleSelection {
33 fn all() -> Self {
34 Self {
35 module_names: builtin_stats_module_names().to_vec(),
36 }
37 }
38
39 fn from_names<I, S>(module_names: I) -> SubtrActorResult<Self>
40 where
41 I: IntoIterator<Item = S>,
42 S: AsRef<str>,
43 {
44 let mut selected = Vec::new();
45 let mut seen = HashSet::new();
46 for module_name in module_names {
47 let module_name = module_name.as_ref();
48 let resolved_name = builtin_stats_module_names()
49 .iter()
50 .copied()
51 .find(|candidate| *candidate == module_name)
52 .ok_or_else(|| {
53 SubtrActorError::new(SubtrActorErrorVariant::UnknownStatsModuleName(
54 module_name.to_owned(),
55 ))
56 })?;
57 if seen.insert(resolved_name) {
58 selected.push(resolved_name);
59 }
60 }
61 Ok(Self {
62 module_names: selected,
63 })
64 }
65
66 fn graph(&self) -> SubtrActorResult<AnalysisGraph> {
67 graph_with_builtin_analysis_nodes(self.module_names.iter().copied())
68 }
69
70 fn collected_modules(
71 &self,
72 graph: &AnalysisGraph,
73 ) -> SubtrActorResult<Vec<CollectedStatsModule>> {
74 self.module_names
75 .iter()
76 .copied()
77 .map(|module_name| {
78 Ok(CollectedStatsModule {
79 name: module_name,
80 value: builtin_module_json(module_name, graph)?,
81 })
82 })
83 .collect()
84 }
85
86 fn modules_json(&self, graph: &AnalysisGraph) -> SubtrActorResult<Map<String, Value>> {
87 let mut modules = Map::new();
88 for module_name in self.module_names.iter().copied() {
89 modules.insert(
90 module_name.to_owned(),
91 builtin_module_json(module_name, graph)?,
92 );
93 }
94 Ok(modules)
95 }
96
97 fn frame_modules_json(
98 &self,
99 graph: &AnalysisGraph,
100 replay_meta: &ReplayMeta,
101 ) -> SubtrActorResult<Map<String, Value>> {
102 let mut modules = Map::new();
103 for module_name in self.module_names.iter().copied() {
104 if let Some(snapshot) = builtin_snapshot_frame_json(module_name, graph, replay_meta)? {
105 modules.insert(module_name.to_owned(), snapshot);
106 }
107 }
108 Ok(modules)
109 }
110
111 fn snapshot_config_json(&self, graph: &AnalysisGraph) -> SubtrActorResult<Map<String, Value>> {
112 let mut config = Map::new();
113 for module_name in self.module_names.iter().copied() {
114 if let Some(module_config) = builtin_snapshot_config_json(module_name, graph)? {
115 config.insert(module_name.to_owned(), module_config);
116 }
117 }
118 Ok(config)
119 }
120
121 fn snapshot_frame(
122 &self,
123 graph: &AnalysisGraph,
124 replay_meta: &ReplayMeta,
125 ) -> SubtrActorResult<StatsSnapshotFrame> {
126 let frame = graph.state::<FrameInfo>().ok_or_else(|| {
127 SubtrActorError::new(SubtrActorErrorVariant::CallbackError(
128 "missing FrameInfo state while snapshotting stats frame".to_owned(),
129 ))
130 })?;
131 let gameplay = graph.state::<GameplayState>().ok_or_else(|| {
132 SubtrActorError::new(SubtrActorErrorVariant::CallbackError(
133 "missing GameplayState state while snapshotting stats frame".to_owned(),
134 ))
135 })?;
136 let live_play_state = graph.state::<LivePlayState>().cloned().unwrap_or_default();
137 Ok(StatsSnapshotFrame {
138 frame_number: frame.frame_number,
139 time: frame.time,
140 dt: frame.dt,
141 seconds_remaining: frame.seconds_remaining,
142 game_state: gameplay.game_state,
143 gameplay_phase: live_play_state.gameplay_phase,
144 is_live_play: live_play_state.is_live_play,
145 modules: self.frame_modules_json(graph, replay_meta)?,
146 })
147 }
148}
149
150pub trait FrameTransform {
151 type Output;
152
153 fn transform(
154 &mut self,
155 replay_meta: &ReplayMeta,
156 frame: StatsSnapshotFrame,
157 ) -> SubtrActorResult<Self::Output>;
158}
159
160impl<F, T> FrameTransform for F
161where
162 F: FnMut(&ReplayMeta, StatsSnapshotFrame) -> SubtrActorResult<T>,
163{
164 type Output = T;
165
166 fn transform(
167 &mut self,
168 replay_meta: &ReplayMeta,
169 frame: StatsSnapshotFrame,
170 ) -> SubtrActorResult<Self::Output> {
171 self(replay_meta, frame)
172 }
173}
174
175#[derive(Default, Clone, Copy)]
176pub struct IdentityFrameTransform;
177
178impl FrameTransform for IdentityFrameTransform {
179 type Output = StatsSnapshotFrame;
180
181 fn transform(
182 &mut self,
183 _replay_meta: &ReplayMeta,
184 frame: StatsSnapshotFrame,
185 ) -> SubtrActorResult<Self::Output> {
186 Ok(frame)
187 }
188}
189
190pub struct ModuleFrameTransform<F> {
191 transform: F,
192}
193
194impl<F> ModuleFrameTransform<F> {
195 fn new(transform: F) -> Self {
196 Self { transform }
197 }
198}
199
200impl<F, Modules> FrameTransform for ModuleFrameTransform<F>
201where
202 F: FnMut(Map<String, Value>) -> SubtrActorResult<Modules>,
203{
204 type Output = CapturedStatsFrame<Modules>;
205
206 fn transform(
207 &mut self,
208 _replay_meta: &ReplayMeta,
209 frame: StatsSnapshotFrame,
210 ) -> SubtrActorResult<Self::Output> {
211 frame.map_modules(&mut self.transform)
212 }
213}
214
215struct ReplayStatsFrameTransform;
216
217impl FrameTransform for ReplayStatsFrameTransform {
218 type Output = ReplayStatsFrame;
219
220 fn transform(
221 &mut self,
222 replay_meta: &ReplayMeta,
223 frame: StatsSnapshotFrame,
224 ) -> SubtrActorResult<Self::Output> {
225 CapturedStatsData::<StatsSnapshotFrame> {
226 replay_meta: replay_meta.clone(),
227 config: Map::new(),
228 modules: Map::new(),
229 frames: Vec::new(),
230 }
231 .replay_stats_frame(&frame)
232 }
233}
234
235pub struct StatsCollector<T = StatsSnapshotFrame, F = IdentityFrameTransform> {
236 modules: BuiltinModuleSelection,
237 graph: AnalysisGraph,
238 replay_meta: Option<ReplayMeta>,
239 last_replay_meta_player_count: Option<usize>,
240 frame_transform: F,
241 captured_frames: Option<Vec<T>>,
242 sample_mode: SampleMode,
243 last_sample_time: Option<f32>,
244 frame_persistence: StatsFramePersistenceController,
245 last_demolish_count: usize,
246 last_boost_pad_event_count: usize,
247 last_touch_event_count: usize,
248 last_player_stat_event_count: usize,
249 last_goal_event_count: usize,
250 _marker: PhantomData<T>,
251}
252
253impl Default for StatsCollector<StatsSnapshotFrame, IdentityFrameTransform> {
254 fn default() -> Self {
255 Self::new()
256 }
257}
258
259impl StatsCollector<StatsSnapshotFrame, IdentityFrameTransform> {
260 pub fn new() -> Self {
261 Self::with_selection_and_frame_transform(
262 BuiltinModuleSelection::all(),
263 IdentityFrameTransform,
264 )
265 .expect("builtin stats modules should resolve without conflicts")
266 }
267
268 pub fn only_modules<I>(modules: I) -> Self
269 where
270 I: IntoIterator,
271 I::Item: AsRef<str>,
272 {
273 Self::try_only_modules(modules).expect("builtin stats module names should be valid")
274 }
275
276 pub fn try_only_modules<I>(modules: I) -> SubtrActorResult<Self>
277 where
278 I: IntoIterator,
279 I::Item: AsRef<str>,
280 {
281 Self::with_builtin_module_names(modules)
282 }
283
284 pub fn with_builtin_module_names<I, S>(module_names: I) -> SubtrActorResult<Self>
285 where
286 I: IntoIterator<Item = S>,
287 S: AsRef<str>,
288 {
289 Self::with_selection_and_frame_transform(
290 BuiltinModuleSelection::from_names(module_names)?,
291 IdentityFrameTransform,
292 )
293 }
294
295 pub fn get_snapshot_data(self, replay: &boxcars::Replay) -> SubtrActorResult<StatsSnapshotData>
296 where
297 IdentityFrameTransform: FrameTransform<Output = StatsSnapshotFrame>,
298 {
299 self.capture_frames().get_captured_data(replay)
300 }
301
302 pub fn get_stats_timeline_value(self, replay: &boxcars::Replay) -> SubtrActorResult<Value> {
303 serialize_to_json_value(&self.get_replay_stats_timeline(replay)?)
304 }
305
306 pub fn get_replay_stats_timeline(
307 self,
308 replay: &boxcars::Replay,
309 ) -> SubtrActorResult<ReplayStatsTimeline> {
310 self.with_frame_transform(ReplayStatsFrameTransform)
311 .capture_frames()
312 .get_captured_data(replay)?
313 .into_replay_stats_timeline()
314 }
315
316 pub fn into_snapshot_data(self) -> SubtrActorResult<StatsSnapshotData> {
317 self.into_captured_data()
318 }
319
320 pub fn into_stats_timeline_value(self) -> SubtrActorResult<Value> {
321 self.into_snapshot_data()?.to_stats_timeline_value()
322 }
323
324 pub fn into_replay_stats_timeline(self) -> SubtrActorResult<ReplayStatsTimeline> {
325 self.into_snapshot_data()?.into_stats_timeline()
326 }
327}
328
329impl<T, F> StatsCollector<T, F> {
330 fn with_selection_and_frame_transform(
331 modules: BuiltinModuleSelection,
332 frame_transform: F,
333 ) -> SubtrActorResult<Self> {
334 Ok(Self {
335 graph: modules.graph()?,
336 modules,
337 replay_meta: None,
338 last_replay_meta_player_count: None,
339 frame_transform,
340 captured_frames: None,
341 sample_mode: SampleMode::Aggregate,
342 last_sample_time: None,
343 frame_persistence: StatsFramePersistenceController::new(StatsFrameResolution::default()),
344 last_demolish_count: 0,
345 last_boost_pad_event_count: 0,
346 last_touch_event_count: 0,
347 last_player_stat_event_count: 0,
348 last_goal_event_count: 0,
349 _marker: PhantomData,
350 })
351 }
352
353 pub fn capture_frames(mut self) -> Self {
354 self.captured_frames = Some(Vec::new());
355 self.sample_mode = SampleMode::Timeline;
356 self
357 }
358
359 pub fn with_frame_transform<U, G>(self, frame_transform: G) -> StatsCollector<U, G> {
360 let StatsCollector {
361 modules,
362 graph,
363 replay_meta,
364 last_replay_meta_player_count,
365 captured_frames,
366 sample_mode,
367 last_sample_time,
368 frame_persistence,
369 last_demolish_count,
370 last_boost_pad_event_count,
371 last_touch_event_count,
372 last_player_stat_event_count,
373 last_goal_event_count,
374 ..
375 } = self;
376 StatsCollector {
377 modules,
378 graph,
379 replay_meta,
380 last_replay_meta_player_count,
381 frame_transform,
382 captured_frames: captured_frames.map(|_| Vec::new()),
383 sample_mode,
384 last_sample_time,
385 frame_persistence,
386 last_demolish_count,
387 last_boost_pad_event_count,
388 last_touch_event_count,
389 last_player_stat_event_count,
390 last_goal_event_count,
391 _marker: PhantomData,
392 }
393 }
394
395 pub fn with_module_transform<Modules, G>(
396 self,
397 transform: G,
398 ) -> StatsCollector<CapturedStatsFrame<Modules>, ModuleFrameTransform<G>>
399 where
400 G: FnMut(Map<String, Value>) -> SubtrActorResult<Modules>,
401 {
402 self.with_frame_transform(ModuleFrameTransform::new(transform))
403 }
404
405 pub fn with_frame_resolution(mut self, resolution: StatsFrameResolution) -> Self {
406 self.frame_persistence = StatsFramePersistenceController::new(resolution);
407 self
408 }
409
410 pub fn get_stats(mut self, replay: &boxcars::Replay) -> SubtrActorResult<CollectedStats>
411 where
412 F: FrameTransform<Output = T>,
413 {
414 self.sample_mode = SampleMode::Aggregate;
415 let mut processor = ReplayProcessor::new(replay)?;
416 processor.process(&mut self)?;
417 if self.replay_meta.is_none() {
418 self.replay_meta = Some(processor.get_replay_meta()?);
419 }
420 self.into_stats()
421 }
422
423 pub fn get_captured_data(
424 mut self,
425 replay: &boxcars::Replay,
426 ) -> SubtrActorResult<CapturedStatsData<T>>
427 where
428 F: FrameTransform<Output = T>,
429 {
430 let mut processor = ReplayProcessor::new(replay)?;
431 processor.process(&mut self)?;
432 if self.replay_meta.is_none() {
433 self.replay_meta = Some(processor.get_replay_meta()?);
434 }
435 self.into_captured_data()
436 }
437
438 pub fn into_stats(self) -> SubtrActorResult<CollectedStats> {
439 let replay_meta = self
440 .replay_meta
441 .ok_or_else(|| SubtrActorError::new(SubtrActorErrorVariant::CouldNotBuildReplayMeta))?;
442 Ok(CollectedStats {
443 replay_meta,
444 modules: self.modules.collected_modules(&self.graph)?,
445 })
446 }
447
448 pub fn into_captured_data(self) -> SubtrActorResult<CapturedStatsData<T>> {
449 let replay_meta = self
450 .replay_meta
451 .ok_or_else(|| SubtrActorError::new(SubtrActorErrorVariant::CouldNotBuildReplayMeta))?;
452 Ok(CapturedStatsData {
453 replay_meta: replay_meta.clone(),
454 config: self.modules.snapshot_config_json(&self.graph)?,
455 modules: self.modules.modules_json(&self.graph)?,
456 frames: self.captured_frames.unwrap_or_default(),
457 })
458 }
459
460 fn capture_frame_snapshot(
461 &mut self,
462 replay_meta: &ReplayMeta,
463 frame: StatsSnapshotFrame,
464 ) -> SubtrActorResult<()>
465 where
466 F: FrameTransform<Output = T>,
467 {
468 if let Some(frames) = &mut self.captured_frames {
469 frames.push(self.frame_transform.transform(replay_meta, frame)?);
470 }
471 Ok(())
472 }
473
474 fn replace_last_frame_snapshot(
475 &mut self,
476 replay_meta: &ReplayMeta,
477 frame: StatsSnapshotFrame,
478 ) -> SubtrActorResult<()>
479 where
480 F: FrameTransform<Output = T>,
481 {
482 if let Some(frames) = &mut self.captured_frames {
483 if let Some(last_frame) = frames.last_mut() {
484 *last_frame = self.frame_transform.transform(replay_meta, frame)?;
485 }
486 }
487 Ok(())
488 }
489
490 fn refresh_replay_meta(&mut self, processor: &ReplayProcessor) -> SubtrActorResult<()> {
491 let player_count = processor.player_count();
492 if self.last_replay_meta_player_count == Some(player_count) {
493 return Ok(());
494 }
495
496 let replay_meta = processor.get_replay_meta()?;
497 self.graph.on_replay_meta(&replay_meta)?;
498 self.replay_meta = Some(replay_meta);
499 self.last_replay_meta_player_count = Some(player_count);
500 Ok(())
501 }
502}
503
504impl<T, F> Collector for StatsCollector<T, F>
505where
506 F: FrameTransform<Output = T>,
507{
508 fn process_frame(
509 &mut self,
510 processor: &ReplayProcessor,
511 _frame: &boxcars::Frame,
512 frame_number: usize,
513 current_time: f32,
514 ) -> SubtrActorResult<TimeAdvance> {
515 self.refresh_replay_meta(processor)?;
516
517 let dt = self
518 .last_sample_time
519 .map(|last_time| (current_time - last_time).max(0.0))
520 .unwrap_or(0.0);
521 let frame_input = match self.sample_mode {
522 SampleMode::Aggregate => FrameInput::aggregate(
523 processor,
524 frame_number,
525 current_time,
526 dt,
527 self.last_demolish_count,
528 self.last_boost_pad_event_count,
529 self.last_touch_event_count,
530 self.last_player_stat_event_count,
531 self.last_goal_event_count,
532 ),
533 SampleMode::Timeline => FrameInput::timeline(processor, frame_number, current_time, dt),
534 };
535 self.graph.evaluate_with_state(&frame_input)?;
536
537 if self.captured_frames.is_some() {
538 let replay_meta = self
539 .replay_meta
540 .as_ref()
541 .expect("replay metadata should be initialized before snapshotting")
542 .clone();
543 if let Some(emitted_dt) = self.frame_persistence.on_frame(frame_number, current_time) {
544 let mut frame = self.modules.snapshot_frame(&self.graph, &replay_meta)?;
545 frame.dt = emitted_dt;
546 self.capture_frame_snapshot(&replay_meta, frame)?;
547 }
548 }
549
550 self.last_sample_time = Some(current_time);
551 if matches!(self.sample_mode, SampleMode::Aggregate) {
552 self.last_demolish_count = processor.demolishes.len();
553 self.last_boost_pad_event_count = processor.boost_pad_events.len();
554 self.last_touch_event_count = processor.touch_events.len();
555 self.last_player_stat_event_count = processor.player_stat_events.len();
556 self.last_goal_event_count = processor.goal_events.len();
557 }
558
559 Ok(TimeAdvance::NextFrame)
560 }
561
562 fn finish_replay(&mut self, _processor: &ReplayProcessor) -> SubtrActorResult<()> {
563 self.graph.finish()?;
564 let Some(replay_meta) = self.replay_meta.as_ref().cloned() else {
565 return Ok(());
566 };
567 let Some(_) = self.graph.state::<FrameInfo>() else {
568 return Ok(());
569 };
570 let mut final_snapshot = self.modules.snapshot_frame(&self.graph, &replay_meta)?;
571 if self.captured_frames.is_some() {
572 match self
573 .frame_persistence
574 .final_frame_action(final_snapshot.frame_number, final_snapshot.time)
575 {
576 Some(FinalStatsFrameAction::Append { dt }) => {
577 final_snapshot.dt = dt;
578 self.capture_frame_snapshot(&replay_meta, final_snapshot)?;
579 }
580 Some(FinalStatsFrameAction::ReplaceLast { dt }) => {
581 final_snapshot.dt = dt;
582 self.replace_last_frame_snapshot(&replay_meta, final_snapshot)?;
583 }
584 None => {}
585 }
586 }
587 Ok(())
588 }
589}