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