subtr-actor 1.0.0

Rocket League replay transformer
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
use super::*;

impl CapturedStatsData<StatsSnapshotFrame> {
    pub(in crate::collector::stats::playback) fn timeline_frame_value(
        &self,
        frame: &StatsSnapshotFrame,
    ) -> SubtrActorResult<Value> {
        let mut timeline = Map::new();
        timeline.insert(
            "frame_number".to_owned(),
            serialize_to_json_value(&frame.frame_number)?,
        );
        timeline.insert("time".to_owned(), serialize_to_json_value(&frame.time)?);
        timeline.insert("dt".to_owned(), serialize_to_json_value(&frame.dt)?);
        timeline.insert(
            "seconds_remaining".to_owned(),
            serialize_to_json_value(&frame.seconds_remaining)?,
        );
        timeline.insert(
            "game_state".to_owned(),
            serialize_to_json_value(&frame.game_state)?,
        );
        timeline.insert(
            "ball_has_been_hit".to_owned(),
            serialize_to_json_value(&frame.ball_has_been_hit)?,
        );
        timeline.insert(
            "kickoff_countdown_time".to_owned(),
            serialize_to_json_value(&frame.kickoff_countdown_time)?,
        );
        timeline.insert(
            "gameplay_phase".to_owned(),
            serialize_to_json_value(&frame.gameplay_phase)?,
        );
        timeline.insert(
            "is_live_play".to_owned(),
            serialize_to_json_value(&frame.is_live_play)?,
        );
        timeline.insert(
            "fifty_fifty".to_owned(),
            self.frame_stats_or_default::<FiftyFiftyStats>(frame, "fifty_fifty"),
        );
        timeline.insert(
            "kickoff".to_owned(),
            self.frame_stats_or_default::<KickoffStats>(frame, "kickoff"),
        );
        timeline.insert(
            "possession".to_owned(),
            self.frame_stats_or_default::<PossessionStats>(frame, "possession"),
        );
        timeline.insert(
            "ball_half".to_owned(),
            self.frame_stats_or_default::<BallHalfStats>(frame, "ball_half"),
        );
        timeline.insert(
            "territorial_pressure".to_owned(),
            self.frame_stats_or_default::<TerritorialPressureStats>(frame, "territorial_pressure"),
        );
        timeline.insert(
            "rush".to_owned(),
            self.frame_stats_or_default::<RushStats>(frame, "rush"),
        );
        timeline.insert(
            "team_zero".to_owned(),
            self.timeline_team_value(frame, "team_zero")?,
        );
        timeline.insert(
            "team_one".to_owned(),
            self.timeline_team_value(frame, "team_one")?,
        );
        timeline.insert(
            "players".to_owned(),
            Value::Array(
                self.replay_meta
                    .player_order()
                    .map(|player| self.timeline_player_value(frame, player))
                    .collect::<SubtrActorResult<Vec<_>>>()?,
            ),
        );
        Ok(Value::Object(timeline))
    }

    pub(crate) fn replay_stats_frame(
        &self,
        frame: &StatsSnapshotFrame,
    ) -> SubtrActorResult<ReplayStatsFrame> {
        Ok(ReplayStatsFrame {
            frame_number: frame.frame_number,
            time: frame.time,
            dt: frame.dt,
            seconds_remaining: frame.seconds_remaining,
            game_state: frame.game_state,
            ball_has_been_hit: frame.ball_has_been_hit,
            kickoff_countdown_time: frame.kickoff_countdown_time,
            gameplay_phase: frame.gameplay_phase,
            is_live_play: frame.is_live_play,
            team_zero: self.replay_team_stats(frame, "team_zero")?,
            team_one: self.replay_team_stats(frame, "team_one")?,
            players: self
                .replay_meta
                .player_order()
                .map(|player| self.replay_player_stats(frame, player))
                .collect::<SubtrActorResult<Vec<_>>>()?,
        })
    }

    pub(in crate::collector::stats::playback) fn replay_team_stats(
        &self,
        frame: &StatsSnapshotFrame,
        team_key: &str,
    ) -> SubtrActorResult<TeamStatsSnapshot> {
        let is_team_zero = team_key == "team_zero";
        Ok(TeamStatsSnapshot {
            fifty_fifty: self
                .frame_stats_or_default_typed::<FiftyFiftyStats>(frame, "fifty_fifty")?
                .for_team(is_team_zero),
            kickoff: self
                .frame_stats_or_default_typed::<KickoffStats>(frame, "kickoff")?
                .for_team(is_team_zero),
            possession: self
                .frame_stats_or_default_typed::<PossessionStats>(frame, "possession")?
                .for_team(is_team_zero),
            ball_half: self
                .frame_stats_or_default_typed::<BallHalfStats>(frame, "ball_half")?
                .for_team(is_team_zero),
            territorial_pressure: self
                .frame_stats_or_default_typed::<TerritorialPressureStats>(
                    frame,
                    "territorial_pressure",
                )?
                .for_team(is_team_zero),
            rotation: self.frame_team_stat_or_default_typed(frame, "rotation", team_key)?,
            rush: self
                .frame_stats_or_default_typed::<RushStats>(frame, "rush")?
                .for_team(is_team_zero),
            core: self.frame_team_stat_or_default_typed(frame, "core", team_key)?,
            backboard: self.frame_team_stat_or_default_typed(frame, "backboard", team_key)?,
            double_tap: self.frame_team_stat_or_default_typed(frame, "double_tap", team_key)?,
            one_timer: self.frame_team_stat_or_default_typed(frame, "one_timer", team_key)?,
            pass: self.frame_team_stat_or_default_typed(frame, "pass", team_key)?,
            ball_carry: self.frame_team_stat_or_default_typed(frame, "ball_carry", team_key)?,
            controlled_play: self.frame_team_stat_or_default_typed(
                frame,
                "controlled_play",
                team_key,
            )?,
            air_dribble: self.frame_team_stat_or_default_typed(frame, "air_dribble", team_key)?,
            boost: self.frame_team_stat_or_default_typed(frame, "boost", team_key)?,
            bump: self.frame_team_stat_or_default_typed(frame, "bump", team_key)?,
            half_volley: self.frame_team_stat_or_default_typed(frame, "half_volley", team_key)?,
            movement: self.frame_team_stat_or_default_typed(frame, "movement", team_key)?,
            positioning: self.frame_team_stat_or_default_typed(frame, "positioning", team_key)?,
            powerslide: self.frame_team_stat_or_default_typed(frame, "powerslide", team_key)?,
            demo: self.frame_team_stat_or_default_typed(frame, "demo", team_key)?,
        })
    }

    pub(in crate::collector::stats::playback) fn replay_player_stats(
        &self,
        frame: &StatsSnapshotFrame,
        player: &PlayerInfo,
    ) -> SubtrActorResult<PlayerStatsSnapshot> {
        let player_key = player_info_key(player)?;
        Ok(PlayerStatsSnapshot {
            player_id: player.remote_id.clone(),
            name: player.name.clone(),
            is_team_0: self.is_team_zero_player(player),
            core: self.frame_core_player_stat_or_default_by_key(frame, &player_key)?,
            backboard: self.frame_player_stat_or_default_typed_by_key(
                frame,
                "backboard",
                &player_key,
            )?,
            ceiling_shot: self.frame_player_stat_or_default_typed_by_key(
                frame,
                "ceiling_shot",
                &player_key,
            )?,
            wall_aerial: self.frame_player_stat_or_default_typed_by_key(
                frame,
                "wall_aerial",
                &player_key,
            )?,
            wall_aerial_shot: self.frame_player_stat_or_default_typed_by_key(
                frame,
                "wall_aerial_shot",
                &player_key,
            )?,
            double_tap: self.frame_player_stat_or_default_typed_by_key(
                frame,
                "double_tap",
                &player_key,
            )?,
            one_timer: self.frame_player_stat_or_default_typed_by_key(
                frame,
                "one_timer",
                &player_key,
            )?,
            pass: self.frame_player_stat_or_default_typed_by_key(frame, "pass", &player_key)?,
            fifty_fifty: self.frame_player_stat_or_default_typed_by_key(
                frame,
                "fifty_fifty",
                &player_key,
            )?,
            kickoff: self.frame_player_stat_or_default_typed_by_key(
                frame,
                "kickoff",
                &player_key,
            )?,
            speed_flip: self.frame_player_stat_or_default_typed_by_key(
                frame,
                "speed_flip",
                &player_key,
            )?,
            half_flip: self.frame_player_stat_or_default_typed_by_key(
                frame,
                "half_flip",
                &player_key,
            )?,
            wavedash: self.frame_player_stat_or_default_typed_by_key(
                frame,
                "wavedash",
                &player_key,
            )?,
            touch: if frame.modules.contains_key("touch") {
                self.frame_player_stat_or_default_with_by_key(frame, "touch", &player_key, || {
                    TouchStats::default().with_complete_labeled_touch_counts()
                })?
            } else {
                self.frame_player_stat_or_default_typed_by_key(frame, "touch", &player_key)?
            },
            whiff: self.frame_player_stat_or_default_typed_by_key(frame, "whiff", &player_key)?,
            flick: self.frame_player_stat_or_default_typed_by_key(frame, "flick", &player_key)?,
            musty_flick: self.frame_player_stat_or_default_typed_by_key(
                frame,
                "musty_flick",
                &player_key,
            )?,
            dodge_reset: self.frame_player_stat_or_default_typed_by_key(
                frame,
                "dodge_reset",
                &player_key,
            )?,
            ball_carry: self.frame_player_stat_or_default_typed_by_key(
                frame,
                "ball_carry",
                &player_key,
            )?,
            controlled_play: self.frame_player_stat_or_default_typed_by_key(
                frame,
                "controlled_play",
                &player_key,
            )?,
            air_dribble: self.frame_player_stat_or_default_typed_by_key(
                frame,
                "air_dribble",
                &player_key,
            )?,
            boost: self.frame_player_stat_or_default_typed_by_key(frame, "boost", &player_key)?,
            bump: self.frame_player_stat_or_default_typed_by_key(frame, "bump", &player_key)?,
            half_volley: self.frame_player_stat_or_default_typed_by_key(
                frame,
                "half_volley",
                &player_key,
            )?,
            movement: self.frame_player_stat_or_default_with_by_key(
                frame,
                "movement",
                &player_key,
                || MovementStats::default().with_complete_labeled_tracked_time(),
            )?,
            positioning: self.frame_player_stat_or_default_typed_by_key(
                frame,
                "positioning",
                &player_key,
            )?,
            rotation: self.frame_player_stat_or_default_typed_by_key(
                frame,
                "rotation",
                &player_key,
            )?,
            powerslide: self.frame_player_stat_or_default_typed_by_key(
                frame,
                "powerslide",
                &player_key,
            )?,
            demo: self.frame_player_stat_or_default_typed_by_key(frame, "demo", &player_key)?,
        })
    }

    pub(in crate::collector::stats::playback) fn is_team_zero_player(
        &self,
        player: &PlayerInfo,
    ) -> bool {
        self.replay_meta
            .team_zero
            .iter()
            .any(|team_player| team_player.remote_id == player.remote_id)
    }

    pub(in crate::collector::stats::playback) fn timeline_team_value(
        &self,
        frame: &StatsSnapshotFrame,
        team_key: &str,
    ) -> SubtrActorResult<Value> {
        let is_team_zero = team_key == "team_zero";
        let mut team = Map::new();
        team.insert(
            "fifty_fifty".to_owned(),
            serialize_to_json_value(
                &self
                    .frame_stats_or_default_typed::<FiftyFiftyStats>(frame, "fifty_fifty")?
                    .for_team(is_team_zero),
            )?,
        );
        team.insert(
            "kickoff".to_owned(),
            serialize_to_json_value(
                &self
                    .frame_stats_or_default_typed::<KickoffStats>(frame, "kickoff")?
                    .for_team(is_team_zero),
            )?,
        );
        team.insert(
            "possession".to_owned(),
            serialize_to_json_value(
                &self
                    .frame_stats_or_default_typed::<PossessionStats>(frame, "possession")?
                    .for_team(is_team_zero),
            )?,
        );
        team.insert(
            "ball_half".to_owned(),
            serialize_to_json_value(
                &self
                    .frame_stats_or_default_typed::<BallHalfStats>(frame, "ball_half")?
                    .for_team(is_team_zero),
            )?,
        );
        team.insert(
            "territorial_pressure".to_owned(),
            serialize_to_json_value(
                &self
                    .frame_stats_or_default_typed::<TerritorialPressureStats>(
                        frame,
                        "territorial_pressure",
                    )?
                    .for_team(is_team_zero),
            )?,
        );
        team.insert(
            "rotation".to_owned(),
            self.frame_team_stat_or_default::<RotationTeamStats>(frame, "rotation", team_key),
        );
        team.insert(
            "rush".to_owned(),
            serialize_to_json_value(
                &self
                    .frame_stats_or_default_typed::<RushStats>(frame, "rush")?
                    .for_team(is_team_zero),
            )?,
        );
        team.insert(
            "core".to_owned(),
            self.frame_team_stat_or_default::<CoreTeamStats>(frame, "core", team_key),
        );
        team.insert(
            "backboard".to_owned(),
            self.frame_team_stat_or_default::<BackboardTeamStats>(frame, "backboard", team_key),
        );
        team.insert(
            "double_tap".to_owned(),
            self.frame_team_stat_or_default::<DoubleTapTeamStats>(frame, "double_tap", team_key),
        );
        team.insert(
            "one_timer".to_owned(),
            self.frame_team_stat_or_default::<OneTimerTeamStats>(frame, "one_timer", team_key),
        );
        team.insert(
            "pass".to_owned(),
            self.frame_team_stat_or_default::<PassTeamStats>(frame, "pass", team_key),
        );
        team.insert(
            "ball_carry".to_owned(),
            self.frame_team_stat_or_default::<BallCarryStats>(frame, "ball_carry", team_key),
        );
        team.insert(
            "controlled_play".to_owned(),
            self.frame_team_stat_or_default::<ControlledPlayStats>(
                frame,
                "controlled_play",
                team_key,
            ),
        );
        team.insert(
            "air_dribble".to_owned(),
            self.frame_team_stat_or_default::<AirDribbleStats>(frame, "air_dribble", team_key),
        );
        team.insert(
            "boost".to_owned(),
            self.frame_team_stat_or_default::<BoostStats>(frame, "boost", team_key),
        );
        team.insert(
            "bump".to_owned(),
            self.frame_team_stat_or_default::<BumpTeamStats>(frame, "bump", team_key),
        );
        team.insert(
            "half_volley".to_owned(),
            self.frame_team_stat_or_default::<HalfVolleyTeamStats>(frame, "half_volley", team_key),
        );
        team.insert(
            "movement".to_owned(),
            self.frame_team_stat_or_default::<MovementStats>(frame, "movement", team_key),
        );
        team.insert(
            "positioning".to_owned(),
            self.frame_team_stat_or_default::<PositioningTeamStats>(frame, "positioning", team_key),
        );
        team.insert(
            "powerslide".to_owned(),
            self.frame_team_stat_or_default::<PowerslideStats>(frame, "powerslide", team_key),
        );
        team.insert(
            "demo".to_owned(),
            self.frame_team_stat_or_default::<DemoTeamStats>(frame, "demo", team_key),
        );
        Ok(Value::Object(team))
    }

    pub(in crate::collector::stats::playback) fn timeline_player_value(
        &self,
        frame: &StatsSnapshotFrame,
        player: &PlayerInfo,
    ) -> SubtrActorResult<Value> {
        let player_key = player_info_key(player)?;
        let mut player_value = Map::new();
        player_value.insert(
            "player_id".to_owned(),
            serialize_to_json_value(&player.remote_id)?,
        );
        player_value.insert("name".to_owned(), serialize_to_json_value(&player.name)?);
        player_value.insert(
            "is_team_0".to_owned(),
            serialize_to_json_value(
                &self
                    .replay_meta
                    .team_zero
                    .iter()
                    .any(|team_player| team_player.remote_id == player.remote_id),
            )?,
        );
        player_value.insert(
            "core".to_owned(),
            self.frame_player_stat_or_default_by_key::<CorePlayerStats>(
                frame,
                "core",
                &player_key,
            )?,
        );
        player_value.insert(
            "backboard".to_owned(),
            self.frame_player_stat_or_default_by_key::<BackboardPlayerStats>(
                frame,
                "backboard",
                &player_key,
            )?,
        );
        player_value.insert(
            "ceiling_shot".to_owned(),
            self.frame_player_stat_or_default_by_key::<CeilingShotStats>(
                frame,
                "ceiling_shot",
                &player_key,
            )?,
        );
        player_value.insert(
            "wall_aerial".to_owned(),
            self.frame_player_stat_or_default_by_key::<WallAerialStats>(
                frame,
                "wall_aerial",
                &player_key,
            )?,
        );
        player_value.insert(
            "wall_aerial_shot".to_owned(),
            self.frame_player_stat_or_default_by_key::<WallAerialShotStats>(
                frame,
                "wall_aerial_shot",
                &player_key,
            )?,
        );
        player_value.insert(
            "double_tap".to_owned(),
            self.frame_player_stat_or_default_by_key::<DoubleTapPlayerStats>(
                frame,
                "double_tap",
                &player_key,
            )?,
        );
        player_value.insert(
            "one_timer".to_owned(),
            self.frame_player_stat_or_default_by_key::<OneTimerPlayerStats>(
                frame,
                "one_timer",
                &player_key,
            )?,
        );
        player_value.insert(
            "pass".to_owned(),
            self.frame_player_stat_or_default_by_key::<PassPlayerStats>(
                frame,
                "pass",
                &player_key,
            )?,
        );
        player_value.insert(
            "fifty_fifty".to_owned(),
            self.frame_player_stat_or_default_by_key::<FiftyFiftyPlayerStats>(
                frame,
                "fifty_fifty",
                &player_key,
            )?,
        );
        player_value.insert(
            "kickoff".to_owned(),
            self.frame_player_stat_or_default_by_key::<KickoffPlayerStats>(
                frame,
                "kickoff",
                &player_key,
            )?,
        );
        player_value.insert(
            "speed_flip".to_owned(),
            self.frame_player_stat_or_default_by_key::<SpeedFlipStats>(
                frame,
                "speed_flip",
                &player_key,
            )?,
        );
        player_value.insert(
            "half_flip".to_owned(),
            self.frame_player_stat_or_default_by_key::<HalfFlipStats>(
                frame,
                "half_flip",
                &player_key,
            )?,
        );
        player_value.insert(
            "half_volley".to_owned(),
            self.frame_player_stat_or_default_by_key::<HalfVolleyPlayerStats>(
                frame,
                "half_volley",
                &player_key,
            )?,
        );
        player_value.insert(
            "wavedash".to_owned(),
            self.frame_player_stat_or_default_by_key::<WavedashStats>(
                frame,
                "wavedash",
                &player_key,
            )?,
        );
        player_value.insert(
            "touch".to_owned(),
            self.frame_player_stat_or_value_by_key(
                frame,
                "touch",
                &player_key,
                if frame.modules.contains_key("touch") {
                    serialize_to_json_value(
                        &TouchStats::default().with_complete_labeled_touch_counts(),
                    )?
                } else {
                    default_json_value::<TouchStats>()
                },
            )?,
        );
        player_value.insert(
            "whiff".to_owned(),
            self.frame_player_stat_or_default_by_key::<WhiffStats>(frame, "whiff", &player_key)?,
        );
        player_value.insert(
            "flick".to_owned(),
            self.frame_player_stat_or_default_by_key::<FlickStats>(frame, "flick", &player_key)?,
        );
        player_value.insert(
            "musty_flick".to_owned(),
            self.frame_player_stat_or_default_by_key::<MustyFlickStats>(
                frame,
                "musty_flick",
                &player_key,
            )?,
        );
        player_value.insert(
            "dodge_reset".to_owned(),
            self.frame_player_stat_or_default_by_key::<DodgeResetStats>(
                frame,
                "dodge_reset",
                &player_key,
            )?,
        );
        player_value.insert(
            "ball_carry".to_owned(),
            self.frame_player_stat_or_default_by_key::<BallCarryStats>(
                frame,
                "ball_carry",
                &player_key,
            )?,
        );
        player_value.insert(
            "controlled_play".to_owned(),
            self.frame_player_stat_or_default_by_key::<ControlledPlayStats>(
                frame,
                "controlled_play",
                &player_key,
            )?,
        );
        player_value.insert(
            "air_dribble".to_owned(),
            self.frame_player_stat_or_default_by_key::<AirDribbleStats>(
                frame,
                "air_dribble",
                &player_key,
            )?,
        );
        player_value.insert(
            "boost".to_owned(),
            self.frame_player_stat_or_default_by_key::<BoostStats>(frame, "boost", &player_key)?,
        );
        player_value.insert(
            "bump".to_owned(),
            self.frame_player_stat_or_default_by_key::<BumpPlayerStats>(
                frame,
                "bump",
                &player_key,
            )?,
        );
        player_value.insert(
            "movement".to_owned(),
            self.frame_player_stat_or_value_by_key(
                frame,
                "movement",
                &player_key,
                if frame.modules.contains_key("movement") {
                    serialize_to_json_value(
                        &MovementStats::default().with_complete_labeled_tracked_time(),
                    )?
                } else {
                    default_json_value::<MovementStats>()
                },
            )?,
        );
        player_value.insert(
            "positioning".to_owned(),
            self.frame_player_stat_or_default_by_key::<PositioningStats>(
                frame,
                "positioning",
                &player_key,
            )?,
        );
        player_value.insert(
            "rotation".to_owned(),
            self.frame_player_stat_or_default_by_key::<RotationPlayerStats>(
                frame,
                "rotation",
                &player_key,
            )?,
        );
        player_value.insert(
            "powerslide".to_owned(),
            self.frame_player_stat_or_default_by_key::<PowerslideStats>(
                frame,
                "powerslide",
                &player_key,
            )?,
        );
        player_value.insert(
            "demo".to_owned(),
            self.frame_player_stat_or_default_by_key::<DemoPlayerStats>(
                frame,
                "demo",
                &player_key,
            )?,
        );
        Ok(Value::Object(player_value))
    }

    pub(in crate::collector::stats::playback) fn frame_stats_or_default<T>(
        &self,
        frame: &StatsSnapshotFrame,
        module_name: &str,
    ) -> Value
    where
        T: Default + Serialize,
    {
        frame
            .modules
            .get(module_name)
            .and_then(Value::as_object)
            .and_then(|module| module.get("stats"))
            .cloned()
            .unwrap_or_else(|| default_json_value::<T>())
    }

    pub(in crate::collector::stats::playback) fn frame_team_stat_or_default<T>(
        &self,
        frame: &StatsSnapshotFrame,
        module_name: &str,
        team_key: &str,
    ) -> Value
    where
        T: Default + Serialize,
    {
        frame
            .modules
            .get(module_name)
            .and_then(Value::as_object)
            .and_then(|module| module.get(team_key))
            .cloned()
            .unwrap_or_else(|| default_json_value::<T>())
    }

    pub(in crate::collector::stats::playback) fn frame_player_stat_or_default_by_key<T>(
        &self,
        frame: &StatsSnapshotFrame,
        module_name: &str,
        player_key: &str,
    ) -> SubtrActorResult<Value>
    where
        T: Default + Serialize,
    {
        self.frame_player_stat_or_value_by_key(
            frame,
            module_name,
            player_key,
            default_json_value::<T>(),
        )
    }

    pub(in crate::collector::stats::playback) fn frame_player_stat_or_value_by_key(
        &self,
        frame: &StatsSnapshotFrame,
        module_name: &str,
        player_key: &str,
        default_value: Value,
    ) -> SubtrActorResult<Value> {
        Ok(
            player_stats_value_for_key(frame.modules.get(module_name), player_key)?
                .cloned()
                .unwrap_or(default_value),
        )
    }

    pub(in crate::collector::stats::playback) fn frame_stats_or_default_typed<T>(
        &self,
        frame: &StatsSnapshotFrame,
        module_name: &str,
    ) -> SubtrActorResult<T>
    where
        T: Default + DeserializeOwned + Serialize,
    {
        decode_json_value(self.frame_stats_or_default::<T>(frame, module_name))
    }

    pub(in crate::collector::stats::playback) fn frame_team_stat_or_default_typed<T>(
        &self,
        frame: &StatsSnapshotFrame,
        module_name: &str,
        team_key: &str,
    ) -> SubtrActorResult<T>
    where
        T: Default + DeserializeOwned + Serialize,
    {
        decode_json_value(self.frame_team_stat_or_default::<T>(frame, module_name, team_key))
    }

    pub(in crate::collector::stats::playback) fn frame_player_stat_or_default_typed_by_key<T>(
        &self,
        frame: &StatsSnapshotFrame,
        module_name: &str,
        player_key: &str,
    ) -> SubtrActorResult<T>
    where
        T: Default + DeserializeOwned + Serialize,
    {
        self.frame_player_stat_or_default_with_by_key(frame, module_name, player_key, T::default)
    }

    pub(in crate::collector::stats::playback) fn frame_core_player_stat_or_default_by_key(
        &self,
        frame: &StatsSnapshotFrame,
        player_key: &str,
    ) -> SubtrActorResult<CorePlayerStats> {
        decode_core_player_stats_value(self.frame_player_stat_or_value_by_key(
            frame,
            "core",
            player_key,
            default_json_value::<CorePlayerStats>(),
        )?)
    }

    pub(in crate::collector::stats::playback) fn frame_player_stat_or_default_with_by_key<T, F>(
        &self,
        frame: &StatsSnapshotFrame,
        module_name: &str,
        player_key: &str,
        default: F,
    ) -> SubtrActorResult<T>
    where
        T: DeserializeOwned + Serialize,
        F: FnOnce() -> T,
    {
        decode_json_value(self.frame_player_stat_or_value_by_key(
            frame,
            module_name,
            player_key,
            serialize_to_json_value(&default())?,
        )?)
    }

    pub(in crate::collector::stats::playback) fn module_typed_array<T>(
        &self,
        module_name: &str,
        field: &str,
    ) -> SubtrActorResult<Vec<T>>
    where
        T: DeserializeOwned,
    {
        decode_json_value(Value::Array(self.module_array(module_name, field)))
    }

    pub(in crate::collector::stats::playback) fn module_player_events<T, F>(
        &self,
        module_name: &str,
        field: &str,
        parse: F,
    ) -> SubtrActorResult<Vec<T>>
    where
        F: Fn(&Value) -> SubtrActorResult<T>,
    {
        self.module_array(module_name, field)
            .iter()
            .map(parse)
            .collect()
    }

    pub(in crate::collector::stats::playback) fn module_array(
        &self,
        module_name: &str,
        field: &str,
    ) -> Vec<Value> {
        self.modules
            .get(module_name)
            .and_then(Value::as_object)
            .and_then(|module| module.get(field))
            .and_then(Value::as_array)
            .cloned()
            .unwrap_or_default()
    }
}