Skip to main content

subtr_actor/collector/stats/
playback.rs

1use serde::Serialize;
2use serde::de::DeserializeOwned;
3use serde_json::{Map, Value};
4
5use crate::*;
6
7use super::types::serialize_to_json_value;
8
9#[path = "playback_event_parsers.rs"]
10mod playback_event_parsers;
11#[path = "playback_events.rs"]
12mod playback_events;
13#[path = "playback_frames.rs"]
14mod playback_frames;
15#[path = "playback_json.rs"]
16mod playback_json;
17use playback_event_parsers::*;
18use playback_json::*;
19
20#[derive(Debug, Clone, PartialEq, Serialize)]
21pub struct CapturedStatsFrame<Modules> {
22    pub frame_number: usize,
23    pub time: f32,
24    pub dt: f32,
25    pub seconds_remaining: Option<i32>,
26    pub game_state: Option<i32>,
27    pub ball_has_been_hit: Option<bool>,
28    pub kickoff_countdown_time: Option<i32>,
29    pub gameplay_phase: GameplayPhase,
30    pub is_live_play: bool,
31    pub modules: Modules,
32}
33
34pub type StatsSnapshotFrame = CapturedStatsFrame<Map<String, Value>>;
35
36#[derive(Debug, Clone, PartialEq, Serialize)]
37pub struct CapturedStatsData<Frame> {
38    pub replay_meta: ReplayMeta,
39    pub config: Map<String, Value>,
40    pub modules: Map<String, Value>,
41    pub frames: Vec<Frame>,
42}
43
44pub type StatsSnapshotData = CapturedStatsData<StatsSnapshotFrame>;
45
46impl<Modules> CapturedStatsFrame<Modules> {
47    pub fn map_modules<Mapped, F>(
48        self,
49        transform: F,
50    ) -> SubtrActorResult<CapturedStatsFrame<Mapped>>
51    where
52        F: FnOnce(Modules) -> SubtrActorResult<Mapped>,
53    {
54        Ok(CapturedStatsFrame {
55            frame_number: self.frame_number,
56            time: self.time,
57            dt: self.dt,
58            seconds_remaining: self.seconds_remaining,
59            game_state: self.game_state,
60            ball_has_been_hit: self.ball_has_been_hit,
61            kickoff_countdown_time: self.kickoff_countdown_time,
62            gameplay_phase: self.gameplay_phase,
63            is_live_play: self.is_live_play,
64            modules: transform(self.modules)?,
65        })
66    }
67}
68
69impl CapturedStatsData<StatsSnapshotFrame> {
70    pub fn into_legacy_replay_stats_timeline(self) -> SubtrActorResult<ReplayStatsTimeline> {
71        self.to_legacy_replay_stats_timeline()
72    }
73
74    #[deprecated(
75        note = "use into_legacy_replay_stats_timeline for full partial-sum snapshots, or StatsTimelineEventCollector for compact event-backed timelines"
76    )]
77    pub fn into_stats_timeline(self) -> SubtrActorResult<ReplayStatsTimeline> {
78        self.into_legacy_replay_stats_timeline()
79    }
80
81    pub fn into_legacy_replay_stats_timeline_with_progress<F>(
82        self,
83        frame_interval: usize,
84        mut on_progress: F,
85    ) -> SubtrActorResult<ReplayStatsTimeline>
86    where
87        F: FnMut(usize, usize) -> SubtrActorResult<()>,
88    {
89        let frame_interval = frame_interval.max(1);
90        let total_frames = self.frames.len();
91        on_progress(0, total_frames)?;
92        let frames = self
93            .frames
94            .iter()
95            .enumerate()
96            .map(|(frame_index, frame)| {
97                let replay_frame = self.replay_stats_frame(frame)?;
98                let processed_frames = frame_index + 1;
99                if processed_frames == total_frames
100                    || processed_frames.is_multiple_of(frame_interval)
101                {
102                    on_progress(processed_frames, total_frames)?;
103                }
104                Ok(replay_frame)
105            })
106            .collect::<SubtrActorResult<Vec<_>>>()?;
107        self.to_replay_stats_timeline_with_frames(frames)
108    }
109
110    #[deprecated(
111        note = "use into_legacy_replay_stats_timeline_with_progress for full partial-sum snapshots, or StatsTimelineEventCollector for compact event-backed timelines"
112    )]
113    pub fn into_stats_timeline_with_progress<F>(
114        self,
115        frame_interval: usize,
116        on_progress: F,
117    ) -> SubtrActorResult<ReplayStatsTimeline>
118    where
119        F: FnMut(usize, usize) -> SubtrActorResult<()>,
120    {
121        self.into_legacy_replay_stats_timeline_with_progress(frame_interval, on_progress)
122    }
123
124    pub fn to_legacy_replay_stats_timeline(&self) -> SubtrActorResult<ReplayStatsTimeline> {
125        self.to_replay_stats_timeline_with_frames(
126            self.frames
127                .iter()
128                .map(|frame| self.replay_stats_frame(frame))
129                .collect::<SubtrActorResult<Vec<_>>>()?,
130        )
131    }
132
133    #[deprecated(
134        note = "use to_legacy_replay_stats_timeline for full partial-sum snapshots, or StatsTimelineEventCollector for compact event-backed timelines"
135    )]
136    pub fn to_stats_timeline(&self) -> SubtrActorResult<ReplayStatsTimeline> {
137        self.to_legacy_replay_stats_timeline()
138    }
139
140    pub(crate) fn into_replay_stats_timeline_with_frames(
141        self,
142        frames: Vec<ReplayStatsFrame>,
143    ) -> SubtrActorResult<ReplayStatsTimeline> {
144        self.to_replay_stats_timeline_with_frames(frames)
145    }
146
147    fn to_replay_stats_timeline_with_frames(
148        &self,
149        frames: Vec<ReplayStatsFrame>,
150    ) -> SubtrActorResult<ReplayStatsTimeline> {
151        Ok(ReplayStatsTimeline {
152            config: self.timeline_config(),
153            replay_meta: self.replay_meta.clone(),
154            events: self.timeline_event_sets_typed()?,
155            frames,
156        })
157    }
158
159    pub fn into_legacy_stats_timeline_value(self) -> SubtrActorResult<Value> {
160        self.to_legacy_stats_timeline_value()
161    }
162
163    #[deprecated(
164        note = "use into_legacy_stats_timeline_value for full partial-sum snapshots, or StatsTimelineEventCollector for compact event-backed timelines"
165    )]
166    pub fn into_stats_timeline_value(self) -> SubtrActorResult<Value> {
167        self.into_legacy_stats_timeline_value()
168    }
169
170    pub fn to_legacy_stats_timeline_value(&self) -> SubtrActorResult<Value> {
171        let mut timeline = Map::new();
172        timeline.insert("config".to_owned(), self.timeline_config_value()?);
173        timeline.insert(
174            "replay_meta".to_owned(),
175            serialize_to_json_value(&self.replay_meta)?,
176        );
177        timeline.insert("events".to_owned(), self.timeline_event_sets_value()?);
178        timeline.insert(
179            "frames".to_owned(),
180            Value::Array(
181                self.frames
182                    .iter()
183                    .map(|frame| self.timeline_frame_value(frame))
184                    .collect::<SubtrActorResult<Vec<_>>>()?,
185            ),
186        );
187        Ok(Value::Object(timeline))
188    }
189
190    #[deprecated(
191        note = "use to_legacy_stats_timeline_value for full partial-sum snapshots, or StatsTimelineEventCollector for compact event-backed timelines"
192    )]
193    pub fn to_stats_timeline_value(&self) -> SubtrActorResult<Value> {
194        self.to_legacy_stats_timeline_value()
195    }
196
197    fn timeline_config(&self) -> StatsTimelineConfig {
198        let positioning_config = self.config.get("positioning").and_then(Value::as_object);
199        let positioning_defaults = PositioningCalculatorConfig::default();
200        let ball_half_config = self.config.get("ball_half").and_then(Value::as_object);
201        let ball_third_config = self.config.get("ball_third").and_then(Value::as_object);
202        let territorial_pressure_config = self
203            .config
204            .get("territorial_pressure")
205            .and_then(Value::as_object);
206        let territorial_pressure_defaults = TerritorialPressureCalculatorConfig::default();
207        let rotation_config = self.config.get("rotation").and_then(Value::as_object);
208        let rotation_defaults = RotationCalculatorConfig::default();
209        let rush_config = self.config.get("rush").and_then(Value::as_object);
210        let rush_defaults = RushCalculatorConfig::default();
211        let aerial_goal_config = self.config.get("aerial_goal").and_then(Value::as_object);
212        let high_aerial_goal_config = self
213            .config
214            .get("high_aerial_goal")
215            .and_then(Value::as_object);
216        let long_distance_goal_config = self
217            .config
218            .get("long_distance_goal")
219            .and_then(Value::as_object);
220        let own_half_goal_config = self.config.get("own_half_goal").and_then(Value::as_object);
221        let empty_net_goal_config = self.config.get("empty_net_goal").and_then(Value::as_object);
222        let flick_goal_config = self.config.get("flick_goal").and_then(Value::as_object);
223        let ceiling_shot_goal_config = self
224            .config
225            .get("ceiling_shot_goal")
226            .and_then(Value::as_object);
227        let double_tap_goal_config = self
228            .config
229            .get("double_tap_goal")
230            .and_then(Value::as_object);
231        let one_timer_goal_config = self.config.get("one_timer_goal").and_then(Value::as_object);
232        let air_dribble_goal_config = self
233            .config
234            .get("air_dribble_goal")
235            .and_then(Value::as_object);
236        let flip_reset_goal_config = self
237            .config
238            .get("flip_reset_goal")
239            .and_then(Value::as_object);
240        let flip_into_ball_goal_config = self
241            .config
242            .get("flip_into_ball_goal")
243            .and_then(Value::as_object);
244        let bump_goal_config = self.config.get("bump_goal").and_then(Value::as_object);
245        let demo_goal_config = self.config.get("demo_goal").and_then(Value::as_object);
246        let half_volley_config = self.config.get("half_volley").and_then(Value::as_object);
247        let half_volley_goal_config = self
248            .config
249            .get("half_volley_goal")
250            .and_then(Value::as_object);
251
252        StatsTimelineConfig {
253            most_back_forward_threshold_y: positioning_config
254                .and_then(|config| config.get("most_back_forward_threshold_y"))
255                .and_then(json_f32)
256                .unwrap_or(positioning_defaults.most_back_forward_threshold_y),
257            level_ball_depth_margin: positioning_config
258                .and_then(|config| config.get("level_ball_depth_margin"))
259                .and_then(json_f32)
260                .unwrap_or(positioning_defaults.level_ball_depth_margin),
261            closest_to_ball_switch_margin: positioning_config
262                .and_then(|config| config.get("closest_to_ball_switch_margin"))
263                .and_then(json_f32)
264                .unwrap_or(positioning_defaults.closest_to_ball_switch_margin),
265            closest_to_ball_switch_min_seconds: positioning_config
266                .and_then(|config| config.get("closest_to_ball_switch_min_seconds"))
267                .and_then(json_f32)
268                .unwrap_or(positioning_defaults.closest_to_ball_switch_min_seconds),
269            shadow_defense_max_ball_y: positioning_config
270                .and_then(|config| config.get("shadow_defense_max_ball_y"))
271                .and_then(json_f32)
272                .unwrap_or(positioning_defaults.shadow_defense_max_ball_y),
273            shadow_defense_min_goal_side_y: positioning_config
274                .and_then(|config| config.get("shadow_defense_min_goal_side_y"))
275                .and_then(json_f32)
276                .unwrap_or(positioning_defaults.shadow_defense_min_goal_side_y),
277            shadow_defense_min_gap: positioning_config
278                .and_then(|config| config.get("shadow_defense_min_gap"))
279                .and_then(json_f32)
280                .unwrap_or(positioning_defaults.shadow_defense_min_gap),
281            shadow_defense_max_gap: positioning_config
282                .and_then(|config| config.get("shadow_defense_max_gap"))
283                .and_then(json_f32)
284                .unwrap_or(positioning_defaults.shadow_defense_max_gap),
285            shadow_defense_max_lateral_gap: positioning_config
286                .and_then(|config| config.get("shadow_defense_max_lateral_gap"))
287                .and_then(json_f32)
288                .unwrap_or(positioning_defaults.shadow_defense_max_lateral_gap),
289            shadow_defense_min_retreat_speed: positioning_config
290                .and_then(|config| config.get("shadow_defense_min_retreat_speed"))
291                .and_then(json_f32)
292                .unwrap_or(positioning_defaults.shadow_defense_min_retreat_speed),
293            shadow_defense_max_speed_delta: positioning_config
294                .and_then(|config| config.get("shadow_defense_max_speed_delta"))
295                .and_then(json_f32)
296                .unwrap_or(positioning_defaults.shadow_defense_max_speed_delta),
297            ball_half_neutral_zone_half_width_y: ball_half_config
298                .and_then(|config| config.get("ball_half_neutral_zone_half_width_y"))
299                .and_then(json_f32)
300                .unwrap_or(BallHalfCalculatorConfig::default().neutral_zone_half_width_y),
301            ball_third_boundary_y: ball_third_config
302                .and_then(|config| config.get("ball_third_boundary_y"))
303                .and_then(json_f32)
304                .unwrap_or(BallThirdCalculatorConfig::default().boundary_y),
305            territorial_pressure_neutral_zone_half_width_y: territorial_pressure_config
306                .and_then(|config| config.get("territorial_pressure_neutral_zone_half_width_y"))
307                .and_then(json_f32)
308                .unwrap_or(territorial_pressure_defaults.neutral_zone_half_width_y),
309            territorial_pressure_min_establish_seconds: territorial_pressure_config
310                .and_then(|config| config.get("territorial_pressure_min_establish_seconds"))
311                .and_then(json_f32)
312                .unwrap_or(territorial_pressure_defaults.min_establish_seconds),
313            territorial_pressure_min_establish_third_seconds: territorial_pressure_config
314                .and_then(|config| config.get("territorial_pressure_min_establish_third_seconds"))
315                .and_then(json_f32)
316                .unwrap_or(territorial_pressure_defaults.min_establish_third_seconds),
317            territorial_pressure_relief_grace_seconds: territorial_pressure_config
318                .and_then(|config| config.get("territorial_pressure_relief_grace_seconds"))
319                .and_then(json_f32)
320                .unwrap_or(territorial_pressure_defaults.relief_grace_seconds),
321            territorial_pressure_confirmed_relief_grace_seconds: territorial_pressure_config
322                .and_then(|config| {
323                    config.get("territorial_pressure_confirmed_relief_grace_seconds")
324                })
325                .and_then(json_f32)
326                .unwrap_or(territorial_pressure_defaults.confirmed_relief_grace_seconds),
327            rotation_role_depth_margin: rotation_config
328                .and_then(|config| config.get("role_depth_margin"))
329                .and_then(json_f32)
330                .unwrap_or(rotation_defaults.role_depth_margin),
331            rotation_first_man_ambiguity_margin: rotation_config
332                .and_then(|config| config.get("first_man_ambiguity_margin"))
333                .and_then(json_f32)
334                .unwrap_or(rotation_defaults.first_man_ambiguity_margin),
335            rotation_first_man_debounce_seconds: rotation_config
336                .and_then(|config| config.get("first_man_debounce_seconds"))
337                .and_then(json_f32)
338                .unwrap_or(rotation_defaults.first_man_debounce_seconds),
339            rotation_first_man_stint_end_grace_seconds: rotation_config
340                .and_then(|config| config.get("first_man_stint_end_grace_seconds"))
341                .and_then(json_f32)
342                .unwrap_or(rotation_defaults.first_man_stint_end_grace_seconds),
343            rush_max_start_y: rush_config
344                .and_then(|config| config.get("rush_max_start_y"))
345                .and_then(json_f32)
346                .unwrap_or(rush_defaults.max_start_y),
347            rush_attack_support_distance_y: rush_config
348                .and_then(|config| config.get("rush_attack_support_distance_y"))
349                .and_then(json_f32)
350                .unwrap_or(rush_defaults.attack_support_distance_y),
351            rush_defender_distance_y: rush_config
352                .and_then(|config| config.get("rush_defender_distance_y"))
353                .and_then(json_f32)
354                .unwrap_or(rush_defaults.defender_distance_y),
355            rush_min_possession_retained_seconds: rush_config
356                .and_then(|config| config.get("rush_min_possession_retained_seconds"))
357                .and_then(json_f32)
358                .unwrap_or(rush_defaults.min_possession_retained_seconds),
359            aerial_goal_min_ball_z: aerial_goal_config
360                .and_then(|config| config.get("aerial_goal_min_ball_z"))
361                .and_then(json_f32)
362                .unwrap_or(AerialGoalCalculatorConfig::default().min_ball_z),
363            high_aerial_goal_min_ball_z: high_aerial_goal_config
364                .and_then(|config| config.get("high_aerial_goal_min_ball_z"))
365                .and_then(json_f32)
366                .unwrap_or(HighAerialGoalCalculatorConfig::default().min_ball_z),
367            long_distance_goal_max_attacking_y: long_distance_goal_config
368                .and_then(|config| config.get("long_distance_goal_max_attacking_y"))
369                .and_then(json_f32)
370                .unwrap_or(LongDistanceGoalCalculatorConfig::default().max_attacking_y),
371            own_half_goal_max_attacking_y: own_half_goal_config
372                .and_then(|config| config.get("own_half_goal_max_attacking_y"))
373                .and_then(json_f32)
374                .unwrap_or(OwnHalfGoalCalculatorConfig::default().max_attacking_y),
375            empty_net_min_defender_y_margin: empty_net_goal_config
376                .and_then(|config| config.get("empty_net_min_defender_y_margin"))
377                .and_then(json_f32)
378                .unwrap_or(EmptyNetGoalCalculatorConfig::default().min_defender_y_margin),
379            empty_net_min_defender_distance: empty_net_goal_config
380                .and_then(|config| config.get("empty_net_min_defender_distance"))
381                .and_then(json_f32)
382                .unwrap_or(EmptyNetGoalCalculatorConfig::default().min_defender_distance),
383            empty_net_max_touch_attacking_y: empty_net_goal_config
384                .and_then(|config| config.get("empty_net_max_touch_attacking_y"))
385                .and_then(json_f32)
386                .unwrap_or(EmptyNetGoalCalculatorConfig::default().max_touch_attacking_y),
387            flick_goal_max_event_to_goal_seconds: json_config_f32(
388                flick_goal_config,
389                "flick_goal_max_event_to_goal_seconds",
390                "flick_goal_max_event_to_touch_seconds",
391            )
392            .unwrap_or(FlickGoalCalculatorConfig::default().max_event_to_goal_seconds),
393            ceiling_shot_goal_max_event_to_goal_seconds: json_config_f32(
394                ceiling_shot_goal_config,
395                "ceiling_shot_goal_max_event_to_goal_seconds",
396                "ceiling_shot_goal_max_event_to_touch_seconds",
397            )
398            .unwrap_or(CeilingShotGoalCalculatorConfig::default().max_event_to_goal_seconds),
399            double_tap_goal_max_event_to_goal_seconds: json_config_f32(
400                double_tap_goal_config,
401                "double_tap_goal_max_event_to_goal_seconds",
402                "double_tap_goal_max_event_to_touch_seconds",
403            )
404            .unwrap_or(DoubleTapGoalCalculatorConfig::default().max_event_to_goal_seconds),
405            one_timer_goal_max_event_to_goal_seconds: json_config_f32(
406                one_timer_goal_config,
407                "one_timer_goal_max_event_to_goal_seconds",
408                "one_timer_goal_max_event_to_touch_seconds",
409            )
410            .unwrap_or(OneTimerGoalCalculatorConfig::default().max_event_to_goal_seconds),
411            air_dribble_goal_max_end_to_goal_seconds: json_config_f32(
412                air_dribble_goal_config,
413                "air_dribble_goal_max_end_to_goal_seconds",
414                "air_dribble_goal_max_end_to_touch_seconds",
415            )
416            .unwrap_or(AirDribbleGoalCalculatorConfig::default().max_end_to_goal_seconds),
417            flip_reset_goal_max_event_to_goal_seconds: json_config_f32(
418                flip_reset_goal_config,
419                "flip_reset_goal_max_event_to_goal_seconds",
420                "flip_reset_goal_max_event_to_touch_seconds",
421            )
422            .unwrap_or(FlipResetGoalCalculatorConfig::default().max_event_to_goal_seconds),
423            flip_into_ball_goal_max_touch_to_goal_seconds: flip_into_ball_goal_config
424                .and_then(|config| config.get("flip_into_ball_goal_max_touch_to_goal_seconds"))
425                .and_then(json_f32)
426                .unwrap_or(FlipIntoBallGoalCalculatorConfig::default().max_touch_to_goal_seconds),
427            bump_goal_max_event_to_goal_seconds: json_config_f32(
428                bump_goal_config,
429                "bump_goal_max_event_to_goal_seconds",
430                "bump_goal_max_event_to_touch_seconds",
431            )
432            .unwrap_or(BumpGoalCalculatorConfig::default().max_event_to_goal_seconds),
433            demo_goal_max_event_to_goal_seconds: json_config_f32(
434                demo_goal_config,
435                "demo_goal_max_event_to_goal_seconds",
436                "demo_goal_max_event_to_touch_seconds",
437            )
438            .unwrap_or(DemoGoalCalculatorConfig::default().max_event_to_goal_seconds),
439            half_volley_max_bounce_to_touch_seconds: half_volley_config
440                .and_then(|config| config.get("half_volley_max_bounce_to_touch_seconds"))
441                .and_then(json_f32)
442                .unwrap_or(HalfVolleyCalculatorConfig::default().max_bounce_to_touch_seconds),
443            half_volley_min_ball_speed: half_volley_config
444                .and_then(|config| config.get("half_volley_min_ball_speed"))
445                .and_then(json_f32)
446                .unwrap_or(HalfVolleyCalculatorConfig::default().min_ball_speed),
447            half_volley_goal_max_touch_to_goal_seconds: half_volley_goal_config
448                .and_then(|config| config.get("half_volley_goal_max_touch_to_goal_seconds"))
449                .and_then(json_f32)
450                .unwrap_or(HalfVolleyGoalCalculatorConfig::default().max_touch_to_goal_seconds),
451            half_volley_goal_min_goal_alignment: half_volley_goal_config
452                .and_then(|config| config.get("half_volley_goal_min_goal_alignment"))
453                .and_then(json_f32)
454                .unwrap_or(HalfVolleyGoalCalculatorConfig::default().min_goal_alignment),
455        }
456    }
457
458    fn timeline_config_value(&self) -> SubtrActorResult<Value> {
459        let positioning_config = self.config.get("positioning").and_then(Value::as_object);
460        let positioning_defaults = PositioningCalculatorConfig::default();
461        let ball_half_config = self.config.get("ball_half").and_then(Value::as_object);
462        let ball_third_config = self.config.get("ball_third").and_then(Value::as_object);
463        let territorial_pressure_config = self
464            .config
465            .get("territorial_pressure")
466            .and_then(Value::as_object);
467        let rotation_config = self.config.get("rotation").and_then(Value::as_object);
468        let rush_config = self.config.get("rush").and_then(Value::as_object);
469        let aerial_goal_config = self.config.get("aerial_goal").and_then(Value::as_object);
470        let high_aerial_goal_config = self
471            .config
472            .get("high_aerial_goal")
473            .and_then(Value::as_object);
474        let long_distance_goal_config = self
475            .config
476            .get("long_distance_goal")
477            .and_then(Value::as_object);
478        let own_half_goal_config = self.config.get("own_half_goal").and_then(Value::as_object);
479        let empty_net_goal_config = self.config.get("empty_net_goal").and_then(Value::as_object);
480        let flick_goal_config = self.config.get("flick_goal").and_then(Value::as_object);
481        let double_tap_goal_config = self
482            .config
483            .get("double_tap_goal")
484            .and_then(Value::as_object);
485        let one_timer_goal_config = self.config.get("one_timer_goal").and_then(Value::as_object);
486        let air_dribble_goal_config = self
487            .config
488            .get("air_dribble_goal")
489            .and_then(Value::as_object);
490        let flip_reset_goal_config = self
491            .config
492            .get("flip_reset_goal")
493            .and_then(Value::as_object);
494        let flip_into_ball_goal_config = self
495            .config
496            .get("flip_into_ball_goal")
497            .and_then(Value::as_object);
498        let bump_goal_config = self.config.get("bump_goal").and_then(Value::as_object);
499        let demo_goal_config = self.config.get("demo_goal").and_then(Value::as_object);
500        let half_volley_config = self.config.get("half_volley").and_then(Value::as_object);
501        let half_volley_goal_config = self
502            .config
503            .get("half_volley_goal")
504            .and_then(Value::as_object);
505
506        let mut config = Map::new();
507        config.insert(
508            "most_back_forward_threshold_y".to_owned(),
509            serialize_to_json_value(
510                &positioning_config
511                    .and_then(|config| config.get("most_back_forward_threshold_y"))
512                    .and_then(Value::as_f64)
513                    .unwrap_or(positioning_defaults.most_back_forward_threshold_y as f64),
514            )?,
515        );
516        config.insert(
517            "level_ball_depth_margin".to_owned(),
518            serialize_to_json_value(
519                &positioning_config
520                    .and_then(|config| config.get("level_ball_depth_margin"))
521                    .and_then(Value::as_f64)
522                    .unwrap_or(positioning_defaults.level_ball_depth_margin as f64),
523            )?,
524        );
525        config.insert(
526            "closest_to_ball_switch_margin".to_owned(),
527            serialize_to_json_value(
528                &positioning_config
529                    .and_then(|config| config.get("closest_to_ball_switch_margin"))
530                    .and_then(Value::as_f64)
531                    .unwrap_or(positioning_defaults.closest_to_ball_switch_margin as f64),
532            )?,
533        );
534        config.insert(
535            "closest_to_ball_switch_min_seconds".to_owned(),
536            serialize_to_json_value(
537                &positioning_config
538                    .and_then(|config| config.get("closest_to_ball_switch_min_seconds"))
539                    .and_then(Value::as_f64)
540                    .unwrap_or(positioning_defaults.closest_to_ball_switch_min_seconds as f64),
541            )?,
542        );
543        config.insert(
544            "shadow_defense_max_ball_y".to_owned(),
545            serialize_to_json_value(
546                &positioning_config
547                    .and_then(|config| config.get("shadow_defense_max_ball_y"))
548                    .and_then(Value::as_f64)
549                    .unwrap_or(positioning_defaults.shadow_defense_max_ball_y as f64),
550            )?,
551        );
552        config.insert(
553            "shadow_defense_min_goal_side_y".to_owned(),
554            serialize_to_json_value(
555                &positioning_config
556                    .and_then(|config| config.get("shadow_defense_min_goal_side_y"))
557                    .and_then(Value::as_f64)
558                    .unwrap_or(positioning_defaults.shadow_defense_min_goal_side_y as f64),
559            )?,
560        );
561        config.insert(
562            "shadow_defense_min_gap".to_owned(),
563            serialize_to_json_value(
564                &positioning_config
565                    .and_then(|config| config.get("shadow_defense_min_gap"))
566                    .and_then(Value::as_f64)
567                    .unwrap_or(positioning_defaults.shadow_defense_min_gap as f64),
568            )?,
569        );
570        config.insert(
571            "shadow_defense_max_gap".to_owned(),
572            serialize_to_json_value(
573                &positioning_config
574                    .and_then(|config| config.get("shadow_defense_max_gap"))
575                    .and_then(Value::as_f64)
576                    .unwrap_or(positioning_defaults.shadow_defense_max_gap as f64),
577            )?,
578        );
579        config.insert(
580            "shadow_defense_max_lateral_gap".to_owned(),
581            serialize_to_json_value(
582                &positioning_config
583                    .and_then(|config| config.get("shadow_defense_max_lateral_gap"))
584                    .and_then(Value::as_f64)
585                    .unwrap_or(positioning_defaults.shadow_defense_max_lateral_gap as f64),
586            )?,
587        );
588        config.insert(
589            "shadow_defense_min_retreat_speed".to_owned(),
590            serialize_to_json_value(
591                &positioning_config
592                    .and_then(|config| config.get("shadow_defense_min_retreat_speed"))
593                    .and_then(Value::as_f64)
594                    .unwrap_or(positioning_defaults.shadow_defense_min_retreat_speed as f64),
595            )?,
596        );
597        config.insert(
598            "shadow_defense_max_speed_delta".to_owned(),
599            serialize_to_json_value(
600                &positioning_config
601                    .and_then(|config| config.get("shadow_defense_max_speed_delta"))
602                    .and_then(Value::as_f64)
603                    .unwrap_or(positioning_defaults.shadow_defense_max_speed_delta as f64),
604            )?,
605        );
606        config.insert(
607            "ball_half_neutral_zone_half_width_y".to_owned(),
608            serialize_to_json_value(
609                &ball_half_config
610                    .and_then(|config| config.get("ball_half_neutral_zone_half_width_y"))
611                    .and_then(Value::as_f64)
612                    .unwrap_or(
613                        BallHalfCalculatorConfig::default().neutral_zone_half_width_y as f64,
614                    ),
615            )?,
616        );
617        config.insert(
618            "ball_third_boundary_y".to_owned(),
619            serialize_to_json_value(
620                &ball_third_config
621                    .and_then(|config| config.get("ball_third_boundary_y"))
622                    .and_then(Value::as_f64)
623                    .unwrap_or(BallThirdCalculatorConfig::default().boundary_y as f64),
624            )?,
625        );
626        let territorial_pressure_defaults = TerritorialPressureCalculatorConfig::default();
627        for (key, default_value) in [
628            (
629                "territorial_pressure_neutral_zone_half_width_y",
630                territorial_pressure_defaults.neutral_zone_half_width_y,
631            ),
632            (
633                "territorial_pressure_min_establish_seconds",
634                territorial_pressure_defaults.min_establish_seconds,
635            ),
636            (
637                "territorial_pressure_min_establish_third_seconds",
638                territorial_pressure_defaults.min_establish_third_seconds,
639            ),
640            (
641                "territorial_pressure_relief_grace_seconds",
642                territorial_pressure_defaults.relief_grace_seconds,
643            ),
644            (
645                "territorial_pressure_confirmed_relief_grace_seconds",
646                territorial_pressure_defaults.confirmed_relief_grace_seconds,
647            ),
648        ] {
649            config.insert(
650                key.to_owned(),
651                serialize_to_json_value(
652                    &territorial_pressure_config
653                        .and_then(|config| config.get(key))
654                        .and_then(Value::as_f64)
655                        .unwrap_or(default_value as f64),
656                )?,
657            );
658        }
659        let rotation_defaults = RotationCalculatorConfig::default();
660        for (key, default_value) in [
661            (
662                "rotation_role_depth_margin",
663                rotation_defaults.role_depth_margin,
664            ),
665            (
666                "rotation_first_man_ambiguity_margin",
667                rotation_defaults.first_man_ambiguity_margin,
668            ),
669            (
670                "rotation_first_man_debounce_seconds",
671                rotation_defaults.first_man_debounce_seconds,
672            ),
673            (
674                "rotation_first_man_stint_end_grace_seconds",
675                rotation_defaults.first_man_stint_end_grace_seconds,
676            ),
677        ] {
678            let source_key = key.strip_prefix("rotation_").unwrap_or(key);
679            config.insert(
680                key.to_owned(),
681                serialize_to_json_value(
682                    &rotation_config
683                        .and_then(|config| config.get(source_key))
684                        .and_then(Value::as_f64)
685                        .unwrap_or(default_value as f64),
686                )?,
687            );
688        }
689        let rush_defaults = RushCalculatorConfig::default();
690        config.insert(
691            "rush_max_start_y".to_owned(),
692            serialize_to_json_value(
693                &rush_config
694                    .and_then(|config| config.get("rush_max_start_y"))
695                    .and_then(Value::as_f64)
696                    .unwrap_or(rush_defaults.max_start_y as f64),
697            )?,
698        );
699        config.insert(
700            "rush_attack_support_distance_y".to_owned(),
701            serialize_to_json_value(
702                &rush_config
703                    .and_then(|config| config.get("rush_attack_support_distance_y"))
704                    .and_then(Value::as_f64)
705                    .unwrap_or(rush_defaults.attack_support_distance_y as f64),
706            )?,
707        );
708        config.insert(
709            "rush_defender_distance_y".to_owned(),
710            serialize_to_json_value(
711                &rush_config
712                    .and_then(|config| config.get("rush_defender_distance_y"))
713                    .and_then(Value::as_f64)
714                    .unwrap_or(rush_defaults.defender_distance_y as f64),
715            )?,
716        );
717        config.insert(
718            "rush_min_possession_retained_seconds".to_owned(),
719            serialize_to_json_value(
720                &rush_config
721                    .and_then(|config| config.get("rush_min_possession_retained_seconds"))
722                    .and_then(Value::as_f64)
723                    .unwrap_or(rush_defaults.min_possession_retained_seconds as f64),
724            )?,
725        );
726        for (module_config, key, default_value) in [
727            (
728                aerial_goal_config,
729                "aerial_goal_min_ball_z",
730                AerialGoalCalculatorConfig::default().min_ball_z,
731            ),
732            (
733                high_aerial_goal_config,
734                "high_aerial_goal_min_ball_z",
735                HighAerialGoalCalculatorConfig::default().min_ball_z,
736            ),
737            (
738                long_distance_goal_config,
739                "long_distance_goal_max_attacking_y",
740                LongDistanceGoalCalculatorConfig::default().max_attacking_y,
741            ),
742            (
743                own_half_goal_config,
744                "own_half_goal_max_attacking_y",
745                OwnHalfGoalCalculatorConfig::default().max_attacking_y,
746            ),
747            (
748                empty_net_goal_config,
749                "empty_net_min_defender_y_margin",
750                EmptyNetGoalCalculatorConfig::default().min_defender_y_margin,
751            ),
752            (
753                empty_net_goal_config,
754                "empty_net_min_defender_distance",
755                EmptyNetGoalCalculatorConfig::default().min_defender_distance,
756            ),
757            (
758                empty_net_goal_config,
759                "empty_net_max_touch_attacking_y",
760                EmptyNetGoalCalculatorConfig::default().max_touch_attacking_y,
761            ),
762            (
763                flick_goal_config,
764                "flick_goal_max_event_to_goal_seconds",
765                FlickGoalCalculatorConfig::default().max_event_to_goal_seconds,
766            ),
767            (
768                double_tap_goal_config,
769                "double_tap_goal_max_event_to_goal_seconds",
770                DoubleTapGoalCalculatorConfig::default().max_event_to_goal_seconds,
771            ),
772            (
773                one_timer_goal_config,
774                "one_timer_goal_max_event_to_goal_seconds",
775                OneTimerGoalCalculatorConfig::default().max_event_to_goal_seconds,
776            ),
777            (
778                air_dribble_goal_config,
779                "air_dribble_goal_max_end_to_goal_seconds",
780                AirDribbleGoalCalculatorConfig::default().max_end_to_goal_seconds,
781            ),
782            (
783                flip_reset_goal_config,
784                "flip_reset_goal_max_event_to_goal_seconds",
785                FlipResetGoalCalculatorConfig::default().max_event_to_goal_seconds,
786            ),
787            (
788                flip_into_ball_goal_config,
789                "flip_into_ball_goal_max_touch_to_goal_seconds",
790                FlipIntoBallGoalCalculatorConfig::default().max_touch_to_goal_seconds,
791            ),
792            (
793                bump_goal_config,
794                "bump_goal_max_event_to_goal_seconds",
795                BumpGoalCalculatorConfig::default().max_event_to_goal_seconds,
796            ),
797            (
798                demo_goal_config,
799                "demo_goal_max_event_to_goal_seconds",
800                DemoGoalCalculatorConfig::default().max_event_to_goal_seconds,
801            ),
802            (
803                half_volley_config,
804                "half_volley_max_bounce_to_touch_seconds",
805                HalfVolleyCalculatorConfig::default().max_bounce_to_touch_seconds,
806            ),
807            (
808                half_volley_config,
809                "half_volley_min_ball_speed",
810                HalfVolleyCalculatorConfig::default().min_ball_speed,
811            ),
812            (
813                half_volley_goal_config,
814                "half_volley_goal_max_touch_to_goal_seconds",
815                HalfVolleyGoalCalculatorConfig::default().max_touch_to_goal_seconds,
816            ),
817            (
818                half_volley_goal_config,
819                "half_volley_goal_min_goal_alignment",
820                HalfVolleyGoalCalculatorConfig::default().min_goal_alignment,
821            ),
822        ] {
823            config.insert(
824                key.to_owned(),
825                serialize_to_json_value(
826                    &module_config
827                        .and_then(|config| config.get(key))
828                        .and_then(Value::as_f64)
829                        .unwrap_or(default_value as f64),
830                )?,
831            );
832        }
833        Ok(Value::Object(config))
834    }
835}
836
837impl CapturedStatsData<ReplayStatsFrame> {
838    pub fn into_legacy_replay_stats_timeline(self) -> SubtrActorResult<ReplayStatsTimeline> {
839        let CapturedStatsData {
840            replay_meta,
841            config,
842            modules,
843            frames,
844        } = self;
845        CapturedStatsData::<StatsSnapshotFrame> {
846            replay_meta,
847            config,
848            modules,
849            frames: Vec::new(),
850        }
851        .into_replay_stats_timeline_with_frames(frames)
852    }
853
854    #[deprecated(
855        note = "use into_legacy_replay_stats_timeline for full partial-sum snapshots, or StatsTimelineEventCollector for compact event-backed timelines"
856    )]
857    pub fn into_replay_stats_timeline(self) -> SubtrActorResult<ReplayStatsTimeline> {
858        self.into_legacy_replay_stats_timeline()
859    }
860}