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