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 ball_half_config = self.config.get("ball_half").and_then(Value::as_object);
200 let territorial_pressure_config = self
201 .config
202 .get("territorial_pressure")
203 .and_then(Value::as_object);
204 let territorial_pressure_defaults = TerritorialPressureCalculatorConfig::default();
205 let rotation_config = self.config.get("rotation").and_then(Value::as_object);
206 let rotation_defaults = RotationCalculatorConfig::default();
207 let rush_config = self.config.get("rush").and_then(Value::as_object);
208 let rush_defaults = RushCalculatorConfig::default();
209 let aerial_goal_config = self.config.get("aerial_goal").and_then(Value::as_object);
210 let high_aerial_goal_config = self
211 .config
212 .get("high_aerial_goal")
213 .and_then(Value::as_object);
214 let long_distance_goal_config = self
215 .config
216 .get("long_distance_goal")
217 .and_then(Value::as_object);
218 let own_half_goal_config = self.config.get("own_half_goal").and_then(Value::as_object);
219 let empty_net_goal_config = self.config.get("empty_net_goal").and_then(Value::as_object);
220 let flick_goal_config = self.config.get("flick_goal").and_then(Value::as_object);
221 let ceiling_shot_goal_config = self
222 .config
223 .get("ceiling_shot_goal")
224 .and_then(Value::as_object);
225 let double_tap_goal_config = self
226 .config
227 .get("double_tap_goal")
228 .and_then(Value::as_object);
229 let one_timer_goal_config = self.config.get("one_timer_goal").and_then(Value::as_object);
230 let air_dribble_goal_config = self
231 .config
232 .get("air_dribble_goal")
233 .and_then(Value::as_object);
234 let flip_reset_goal_config = self
235 .config
236 .get("flip_reset_goal")
237 .and_then(Value::as_object);
238 let flip_into_ball_goal_config = self
239 .config
240 .get("flip_into_ball_goal")
241 .and_then(Value::as_object);
242 let bump_goal_config = self.config.get("bump_goal").and_then(Value::as_object);
243 let demo_goal_config = self.config.get("demo_goal").and_then(Value::as_object);
244 let half_volley_config = self.config.get("half_volley").and_then(Value::as_object);
245 let half_volley_goal_config = self
246 .config
247 .get("half_volley_goal")
248 .and_then(Value::as_object);
249
250 StatsTimelineConfig {
251 most_back_forward_threshold_y: positioning_config
252 .and_then(|config| config.get("most_back_forward_threshold_y"))
253 .and_then(json_f32)
254 .unwrap_or(PositioningCalculatorConfig::default().most_back_forward_threshold_y),
255 level_ball_depth_margin: positioning_config
256 .and_then(|config| config.get("level_ball_depth_margin"))
257 .and_then(json_f32)
258 .unwrap_or(PositioningCalculatorConfig::default().level_ball_depth_margin),
259 closest_to_ball_switch_margin: positioning_config
260 .and_then(|config| config.get("closest_to_ball_switch_margin"))
261 .and_then(json_f32)
262 .unwrap_or(PositioningCalculatorConfig::default().closest_to_ball_switch_margin),
263 closest_to_ball_switch_min_seconds: positioning_config
264 .and_then(|config| config.get("closest_to_ball_switch_min_seconds"))
265 .and_then(json_f32)
266 .unwrap_or(
267 PositioningCalculatorConfig::default().closest_to_ball_switch_min_seconds,
268 ),
269 ball_half_neutral_zone_half_width_y: ball_half_config
270 .and_then(|config| config.get("ball_half_neutral_zone_half_width_y"))
271 .and_then(json_f32)
272 .unwrap_or(BallHalfCalculatorConfig::default().neutral_zone_half_width_y),
273 territorial_pressure_neutral_zone_half_width_y: territorial_pressure_config
274 .and_then(|config| config.get("territorial_pressure_neutral_zone_half_width_y"))
275 .and_then(json_f32)
276 .unwrap_or(territorial_pressure_defaults.neutral_zone_half_width_y),
277 territorial_pressure_min_establish_seconds: territorial_pressure_config
278 .and_then(|config| config.get("territorial_pressure_min_establish_seconds"))
279 .and_then(json_f32)
280 .unwrap_or(territorial_pressure_defaults.min_establish_seconds),
281 territorial_pressure_min_establish_third_seconds: territorial_pressure_config
282 .and_then(|config| config.get("territorial_pressure_min_establish_third_seconds"))
283 .and_then(json_f32)
284 .unwrap_or(territorial_pressure_defaults.min_establish_third_seconds),
285 territorial_pressure_relief_grace_seconds: territorial_pressure_config
286 .and_then(|config| config.get("territorial_pressure_relief_grace_seconds"))
287 .and_then(json_f32)
288 .unwrap_or(territorial_pressure_defaults.relief_grace_seconds),
289 territorial_pressure_confirmed_relief_grace_seconds: territorial_pressure_config
290 .and_then(|config| {
291 config.get("territorial_pressure_confirmed_relief_grace_seconds")
292 })
293 .and_then(json_f32)
294 .unwrap_or(territorial_pressure_defaults.confirmed_relief_grace_seconds),
295 rotation_role_depth_margin: rotation_config
296 .and_then(|config| config.get("role_depth_margin"))
297 .and_then(json_f32)
298 .unwrap_or(rotation_defaults.role_depth_margin),
299 rotation_first_man_ambiguity_margin: rotation_config
300 .and_then(|config| config.get("first_man_ambiguity_margin"))
301 .and_then(json_f32)
302 .unwrap_or(rotation_defaults.first_man_ambiguity_margin),
303 rotation_first_man_debounce_seconds: rotation_config
304 .and_then(|config| config.get("first_man_debounce_seconds"))
305 .and_then(json_f32)
306 .unwrap_or(rotation_defaults.first_man_debounce_seconds),
307 rotation_first_man_stint_end_grace_seconds: rotation_config
308 .and_then(|config| config.get("first_man_stint_end_grace_seconds"))
309 .and_then(json_f32)
310 .unwrap_or(rotation_defaults.first_man_stint_end_grace_seconds),
311 rush_max_start_y: rush_config
312 .and_then(|config| config.get("rush_max_start_y"))
313 .and_then(json_f32)
314 .unwrap_or(rush_defaults.max_start_y),
315 rush_attack_support_distance_y: rush_config
316 .and_then(|config| config.get("rush_attack_support_distance_y"))
317 .and_then(json_f32)
318 .unwrap_or(rush_defaults.attack_support_distance_y),
319 rush_defender_distance_y: rush_config
320 .and_then(|config| config.get("rush_defender_distance_y"))
321 .and_then(json_f32)
322 .unwrap_or(rush_defaults.defender_distance_y),
323 rush_min_possession_retained_seconds: rush_config
324 .and_then(|config| config.get("rush_min_possession_retained_seconds"))
325 .and_then(json_f32)
326 .unwrap_or(rush_defaults.min_possession_retained_seconds),
327 aerial_goal_min_ball_z: aerial_goal_config
328 .and_then(|config| config.get("aerial_goal_min_ball_z"))
329 .and_then(json_f32)
330 .unwrap_or(AerialGoalCalculatorConfig::default().min_ball_z),
331 high_aerial_goal_min_ball_z: high_aerial_goal_config
332 .and_then(|config| config.get("high_aerial_goal_min_ball_z"))
333 .and_then(json_f32)
334 .unwrap_or(HighAerialGoalCalculatorConfig::default().min_ball_z),
335 long_distance_goal_max_attacking_y: long_distance_goal_config
336 .and_then(|config| config.get("long_distance_goal_max_attacking_y"))
337 .and_then(json_f32)
338 .unwrap_or(LongDistanceGoalCalculatorConfig::default().max_attacking_y),
339 own_half_goal_max_attacking_y: own_half_goal_config
340 .and_then(|config| config.get("own_half_goal_max_attacking_y"))
341 .and_then(json_f32)
342 .unwrap_or(OwnHalfGoalCalculatorConfig::default().max_attacking_y),
343 empty_net_min_defender_y_margin: empty_net_goal_config
344 .and_then(|config| config.get("empty_net_min_defender_y_margin"))
345 .and_then(json_f32)
346 .unwrap_or(EmptyNetGoalCalculatorConfig::default().min_defender_y_margin),
347 empty_net_min_defender_distance: empty_net_goal_config
348 .and_then(|config| config.get("empty_net_min_defender_distance"))
349 .and_then(json_f32)
350 .unwrap_or(EmptyNetGoalCalculatorConfig::default().min_defender_distance),
351 empty_net_max_touch_attacking_y: empty_net_goal_config
352 .and_then(|config| config.get("empty_net_max_touch_attacking_y"))
353 .and_then(json_f32)
354 .unwrap_or(EmptyNetGoalCalculatorConfig::default().max_touch_attacking_y),
355 flick_goal_max_event_to_goal_seconds: json_config_f32(
356 flick_goal_config,
357 "flick_goal_max_event_to_goal_seconds",
358 "flick_goal_max_event_to_touch_seconds",
359 )
360 .unwrap_or(FlickGoalCalculatorConfig::default().max_event_to_goal_seconds),
361 ceiling_shot_goal_max_event_to_goal_seconds: json_config_f32(
362 ceiling_shot_goal_config,
363 "ceiling_shot_goal_max_event_to_goal_seconds",
364 "ceiling_shot_goal_max_event_to_touch_seconds",
365 )
366 .unwrap_or(CeilingShotGoalCalculatorConfig::default().max_event_to_goal_seconds),
367 double_tap_goal_max_event_to_goal_seconds: json_config_f32(
368 double_tap_goal_config,
369 "double_tap_goal_max_event_to_goal_seconds",
370 "double_tap_goal_max_event_to_touch_seconds",
371 )
372 .unwrap_or(DoubleTapGoalCalculatorConfig::default().max_event_to_goal_seconds),
373 one_timer_goal_max_event_to_goal_seconds: json_config_f32(
374 one_timer_goal_config,
375 "one_timer_goal_max_event_to_goal_seconds",
376 "one_timer_goal_max_event_to_touch_seconds",
377 )
378 .unwrap_or(OneTimerGoalCalculatorConfig::default().max_event_to_goal_seconds),
379 air_dribble_goal_max_end_to_goal_seconds: json_config_f32(
380 air_dribble_goal_config,
381 "air_dribble_goal_max_end_to_goal_seconds",
382 "air_dribble_goal_max_end_to_touch_seconds",
383 )
384 .unwrap_or(AirDribbleGoalCalculatorConfig::default().max_end_to_goal_seconds),
385 flip_reset_goal_max_event_to_goal_seconds: json_config_f32(
386 flip_reset_goal_config,
387 "flip_reset_goal_max_event_to_goal_seconds",
388 "flip_reset_goal_max_event_to_touch_seconds",
389 )
390 .unwrap_or(FlipResetGoalCalculatorConfig::default().max_event_to_goal_seconds),
391 flip_into_ball_goal_max_touch_to_goal_seconds: flip_into_ball_goal_config
392 .and_then(|config| config.get("flip_into_ball_goal_max_touch_to_goal_seconds"))
393 .and_then(json_f32)
394 .unwrap_or(FlipIntoBallGoalCalculatorConfig::default().max_touch_to_goal_seconds),
395 bump_goal_max_event_to_goal_seconds: json_config_f32(
396 bump_goal_config,
397 "bump_goal_max_event_to_goal_seconds",
398 "bump_goal_max_event_to_touch_seconds",
399 )
400 .unwrap_or(BumpGoalCalculatorConfig::default().max_event_to_goal_seconds),
401 demo_goal_max_event_to_goal_seconds: json_config_f32(
402 demo_goal_config,
403 "demo_goal_max_event_to_goal_seconds",
404 "demo_goal_max_event_to_touch_seconds",
405 )
406 .unwrap_or(DemoGoalCalculatorConfig::default().max_event_to_goal_seconds),
407 half_volley_max_bounce_to_touch_seconds: half_volley_config
408 .and_then(|config| config.get("half_volley_max_bounce_to_touch_seconds"))
409 .and_then(json_f32)
410 .unwrap_or(HalfVolleyCalculatorConfig::default().max_bounce_to_touch_seconds),
411 half_volley_min_ball_speed: half_volley_config
412 .and_then(|config| config.get("half_volley_min_ball_speed"))
413 .and_then(json_f32)
414 .unwrap_or(HalfVolleyCalculatorConfig::default().min_ball_speed),
415 half_volley_goal_max_touch_to_goal_seconds: half_volley_goal_config
416 .and_then(|config| config.get("half_volley_goal_max_touch_to_goal_seconds"))
417 .and_then(json_f32)
418 .unwrap_or(HalfVolleyGoalCalculatorConfig::default().max_touch_to_goal_seconds),
419 half_volley_goal_min_goal_alignment: half_volley_goal_config
420 .and_then(|config| config.get("half_volley_goal_min_goal_alignment"))
421 .and_then(json_f32)
422 .unwrap_or(HalfVolleyGoalCalculatorConfig::default().min_goal_alignment),
423 }
424 }
425
426 fn timeline_config_value(&self) -> SubtrActorResult<Value> {
427 let positioning_config = self.config.get("positioning").and_then(Value::as_object);
428 let ball_half_config = self.config.get("ball_half").and_then(Value::as_object);
429 let territorial_pressure_config = self
430 .config
431 .get("territorial_pressure")
432 .and_then(Value::as_object);
433 let rotation_config = self.config.get("rotation").and_then(Value::as_object);
434 let rush_config = self.config.get("rush").and_then(Value::as_object);
435 let aerial_goal_config = self.config.get("aerial_goal").and_then(Value::as_object);
436 let high_aerial_goal_config = self
437 .config
438 .get("high_aerial_goal")
439 .and_then(Value::as_object);
440 let long_distance_goal_config = self
441 .config
442 .get("long_distance_goal")
443 .and_then(Value::as_object);
444 let own_half_goal_config = self.config.get("own_half_goal").and_then(Value::as_object);
445 let empty_net_goal_config = self.config.get("empty_net_goal").and_then(Value::as_object);
446 let flick_goal_config = self.config.get("flick_goal").and_then(Value::as_object);
447 let double_tap_goal_config = self
448 .config
449 .get("double_tap_goal")
450 .and_then(Value::as_object);
451 let one_timer_goal_config = self.config.get("one_timer_goal").and_then(Value::as_object);
452 let air_dribble_goal_config = self
453 .config
454 .get("air_dribble_goal")
455 .and_then(Value::as_object);
456 let flip_reset_goal_config = self
457 .config
458 .get("flip_reset_goal")
459 .and_then(Value::as_object);
460 let flip_into_ball_goal_config = self
461 .config
462 .get("flip_into_ball_goal")
463 .and_then(Value::as_object);
464 let bump_goal_config = self.config.get("bump_goal").and_then(Value::as_object);
465 let demo_goal_config = self.config.get("demo_goal").and_then(Value::as_object);
466 let half_volley_config = self.config.get("half_volley").and_then(Value::as_object);
467 let half_volley_goal_config = self
468 .config
469 .get("half_volley_goal")
470 .and_then(Value::as_object);
471
472 let mut config = Map::new();
473 config.insert(
474 "most_back_forward_threshold_y".to_owned(),
475 serialize_to_json_value(
476 &positioning_config
477 .and_then(|config| config.get("most_back_forward_threshold_y"))
478 .and_then(Value::as_f64)
479 .unwrap_or(
480 PositioningCalculatorConfig::default().most_back_forward_threshold_y as f64,
481 ),
482 )?,
483 );
484 config.insert(
485 "level_ball_depth_margin".to_owned(),
486 serialize_to_json_value(
487 &positioning_config
488 .and_then(|config| config.get("level_ball_depth_margin"))
489 .and_then(Value::as_f64)
490 .unwrap_or(
491 PositioningCalculatorConfig::default().level_ball_depth_margin as f64,
492 ),
493 )?,
494 );
495 config.insert(
496 "closest_to_ball_switch_margin".to_owned(),
497 serialize_to_json_value(
498 &positioning_config
499 .and_then(|config| config.get("closest_to_ball_switch_margin"))
500 .and_then(Value::as_f64)
501 .unwrap_or(
502 PositioningCalculatorConfig::default().closest_to_ball_switch_margin as f64,
503 ),
504 )?,
505 );
506 config.insert(
507 "closest_to_ball_switch_min_seconds".to_owned(),
508 serialize_to_json_value(
509 &positioning_config
510 .and_then(|config| config.get("closest_to_ball_switch_min_seconds"))
511 .and_then(Value::as_f64)
512 .unwrap_or(
513 PositioningCalculatorConfig::default().closest_to_ball_switch_min_seconds
514 as f64,
515 ),
516 )?,
517 );
518 config.insert(
519 "ball_half_neutral_zone_half_width_y".to_owned(),
520 serialize_to_json_value(
521 &ball_half_config
522 .and_then(|config| config.get("ball_half_neutral_zone_half_width_y"))
523 .and_then(Value::as_f64)
524 .unwrap_or(
525 BallHalfCalculatorConfig::default().neutral_zone_half_width_y as f64,
526 ),
527 )?,
528 );
529 let territorial_pressure_defaults = TerritorialPressureCalculatorConfig::default();
530 for (key, default_value) in [
531 (
532 "territorial_pressure_neutral_zone_half_width_y",
533 territorial_pressure_defaults.neutral_zone_half_width_y,
534 ),
535 (
536 "territorial_pressure_min_establish_seconds",
537 territorial_pressure_defaults.min_establish_seconds,
538 ),
539 (
540 "territorial_pressure_min_establish_third_seconds",
541 territorial_pressure_defaults.min_establish_third_seconds,
542 ),
543 (
544 "territorial_pressure_relief_grace_seconds",
545 territorial_pressure_defaults.relief_grace_seconds,
546 ),
547 (
548 "territorial_pressure_confirmed_relief_grace_seconds",
549 territorial_pressure_defaults.confirmed_relief_grace_seconds,
550 ),
551 ] {
552 config.insert(
553 key.to_owned(),
554 serialize_to_json_value(
555 &territorial_pressure_config
556 .and_then(|config| config.get(key))
557 .and_then(Value::as_f64)
558 .unwrap_or(default_value as f64),
559 )?,
560 );
561 }
562 let rotation_defaults = RotationCalculatorConfig::default();
563 for (key, default_value) in [
564 (
565 "rotation_role_depth_margin",
566 rotation_defaults.role_depth_margin,
567 ),
568 (
569 "rotation_first_man_ambiguity_margin",
570 rotation_defaults.first_man_ambiguity_margin,
571 ),
572 (
573 "rotation_first_man_debounce_seconds",
574 rotation_defaults.first_man_debounce_seconds,
575 ),
576 (
577 "rotation_first_man_stint_end_grace_seconds",
578 rotation_defaults.first_man_stint_end_grace_seconds,
579 ),
580 ] {
581 let source_key = key.strip_prefix("rotation_").unwrap_or(key);
582 config.insert(
583 key.to_owned(),
584 serialize_to_json_value(
585 &rotation_config
586 .and_then(|config| config.get(source_key))
587 .and_then(Value::as_f64)
588 .unwrap_or(default_value as f64),
589 )?,
590 );
591 }
592 let rush_defaults = RushCalculatorConfig::default();
593 config.insert(
594 "rush_max_start_y".to_owned(),
595 serialize_to_json_value(
596 &rush_config
597 .and_then(|config| config.get("rush_max_start_y"))
598 .and_then(Value::as_f64)
599 .unwrap_or(rush_defaults.max_start_y as f64),
600 )?,
601 );
602 config.insert(
603 "rush_attack_support_distance_y".to_owned(),
604 serialize_to_json_value(
605 &rush_config
606 .and_then(|config| config.get("rush_attack_support_distance_y"))
607 .and_then(Value::as_f64)
608 .unwrap_or(rush_defaults.attack_support_distance_y as f64),
609 )?,
610 );
611 config.insert(
612 "rush_defender_distance_y".to_owned(),
613 serialize_to_json_value(
614 &rush_config
615 .and_then(|config| config.get("rush_defender_distance_y"))
616 .and_then(Value::as_f64)
617 .unwrap_or(rush_defaults.defender_distance_y as f64),
618 )?,
619 );
620 config.insert(
621 "rush_min_possession_retained_seconds".to_owned(),
622 serialize_to_json_value(
623 &rush_config
624 .and_then(|config| config.get("rush_min_possession_retained_seconds"))
625 .and_then(Value::as_f64)
626 .unwrap_or(rush_defaults.min_possession_retained_seconds as f64),
627 )?,
628 );
629 for (module_config, key, default_value) in [
630 (
631 aerial_goal_config,
632 "aerial_goal_min_ball_z",
633 AerialGoalCalculatorConfig::default().min_ball_z,
634 ),
635 (
636 high_aerial_goal_config,
637 "high_aerial_goal_min_ball_z",
638 HighAerialGoalCalculatorConfig::default().min_ball_z,
639 ),
640 (
641 long_distance_goal_config,
642 "long_distance_goal_max_attacking_y",
643 LongDistanceGoalCalculatorConfig::default().max_attacking_y,
644 ),
645 (
646 own_half_goal_config,
647 "own_half_goal_max_attacking_y",
648 OwnHalfGoalCalculatorConfig::default().max_attacking_y,
649 ),
650 (
651 empty_net_goal_config,
652 "empty_net_min_defender_y_margin",
653 EmptyNetGoalCalculatorConfig::default().min_defender_y_margin,
654 ),
655 (
656 empty_net_goal_config,
657 "empty_net_min_defender_distance",
658 EmptyNetGoalCalculatorConfig::default().min_defender_distance,
659 ),
660 (
661 empty_net_goal_config,
662 "empty_net_max_touch_attacking_y",
663 EmptyNetGoalCalculatorConfig::default().max_touch_attacking_y,
664 ),
665 (
666 flick_goal_config,
667 "flick_goal_max_event_to_goal_seconds",
668 FlickGoalCalculatorConfig::default().max_event_to_goal_seconds,
669 ),
670 (
671 double_tap_goal_config,
672 "double_tap_goal_max_event_to_goal_seconds",
673 DoubleTapGoalCalculatorConfig::default().max_event_to_goal_seconds,
674 ),
675 (
676 one_timer_goal_config,
677 "one_timer_goal_max_event_to_goal_seconds",
678 OneTimerGoalCalculatorConfig::default().max_event_to_goal_seconds,
679 ),
680 (
681 air_dribble_goal_config,
682 "air_dribble_goal_max_end_to_goal_seconds",
683 AirDribbleGoalCalculatorConfig::default().max_end_to_goal_seconds,
684 ),
685 (
686 flip_reset_goal_config,
687 "flip_reset_goal_max_event_to_goal_seconds",
688 FlipResetGoalCalculatorConfig::default().max_event_to_goal_seconds,
689 ),
690 (
691 flip_into_ball_goal_config,
692 "flip_into_ball_goal_max_touch_to_goal_seconds",
693 FlipIntoBallGoalCalculatorConfig::default().max_touch_to_goal_seconds,
694 ),
695 (
696 bump_goal_config,
697 "bump_goal_max_event_to_goal_seconds",
698 BumpGoalCalculatorConfig::default().max_event_to_goal_seconds,
699 ),
700 (
701 demo_goal_config,
702 "demo_goal_max_event_to_goal_seconds",
703 DemoGoalCalculatorConfig::default().max_event_to_goal_seconds,
704 ),
705 (
706 half_volley_config,
707 "half_volley_max_bounce_to_touch_seconds",
708 HalfVolleyCalculatorConfig::default().max_bounce_to_touch_seconds,
709 ),
710 (
711 half_volley_config,
712 "half_volley_min_ball_speed",
713 HalfVolleyCalculatorConfig::default().min_ball_speed,
714 ),
715 (
716 half_volley_goal_config,
717 "half_volley_goal_max_touch_to_goal_seconds",
718 HalfVolleyGoalCalculatorConfig::default().max_touch_to_goal_seconds,
719 ),
720 (
721 half_volley_goal_config,
722 "half_volley_goal_min_goal_alignment",
723 HalfVolleyGoalCalculatorConfig::default().min_goal_alignment,
724 ),
725 ] {
726 config.insert(
727 key.to_owned(),
728 serialize_to_json_value(
729 &module_config
730 .and_then(|config| config.get(key))
731 .and_then(Value::as_f64)
732 .unwrap_or(default_value as f64),
733 )?,
734 );
735 }
736 Ok(Value::Object(config))
737 }
738}
739
740impl CapturedStatsData<ReplayStatsFrame> {
741 pub fn into_legacy_replay_stats_timeline(self) -> SubtrActorResult<ReplayStatsTimeline> {
742 let CapturedStatsData {
743 replay_meta,
744 config,
745 modules,
746 frames,
747 } = self;
748 CapturedStatsData::<StatsSnapshotFrame> {
749 replay_meta,
750 config,
751 modules,
752 frames: Vec::new(),
753 }
754 .into_replay_stats_timeline_with_frames(frames)
755 }
756
757 #[deprecated(
758 note = "use into_legacy_replay_stats_timeline for full partial-sum snapshots, or StatsTimelineEventCollector for compact event-backed timelines"
759 )]
760 pub fn into_replay_stats_timeline(self) -> SubtrActorResult<ReplayStatsTimeline> {
761 self.into_legacy_replay_stats_timeline()
762 }
763}