Skip to main content

subtr_actor/collector/stats/
collector.rs

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}