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}