1use std::collections::{HashMap, HashSet};
2use std::sync::LazyLock;
3
4use boxcars;
5use boxcars::HeaderProp;
6use serde::Serialize;
7
8use super::boost_invariants::{boost_invariant_violations, BoostInvariantKind};
9use crate::*;
10
11#[derive(Debug, Clone)]
12pub struct BallSample {
13 pub rigid_body: boxcars::RigidBody,
14}
15
16impl BallSample {
17 pub fn position(&self) -> glam::Vec3 {
18 vec_to_glam(&self.rigid_body.location)
19 }
20
21 pub fn velocity(&self) -> glam::Vec3 {
22 self.rigid_body
23 .linear_velocity
24 .as_ref()
25 .map(vec_to_glam)
26 .unwrap_or(glam::Vec3::ZERO)
27 }
28}
29
30fn interval_fraction_in_scalar_range(start: f32, end: f32, min_value: f32, max_value: f32) -> f32 {
31 if (end - start).abs() <= f32::EPSILON {
32 return ((start >= min_value) && (start < max_value)) as i32 as f32;
33 }
34
35 let t_at_min = (min_value - start) / (end - start);
36 let t_at_max = (max_value - start) / (end - start);
37 let interval_start = t_at_min.min(t_at_max).max(0.0);
38 let interval_end = t_at_min.max(t_at_max).min(1.0);
39 (interval_end - interval_start).max(0.0)
40}
41
42fn interval_fraction_below_threshold(start: f32, end: f32, threshold: f32) -> f32 {
43 if (end - start).abs() <= f32::EPSILON {
44 return (start < threshold) as i32 as f32;
45 }
46
47 let threshold_time = ((threshold - start) / (end - start)).clamp(0.0, 1.0);
48 if start < threshold {
49 if end < threshold {
50 1.0
51 } else {
52 threshold_time
53 }
54 } else if end < threshold {
55 1.0 - threshold_time
56 } else {
57 0.0
58 }
59}
60
61fn interval_fraction_above_threshold(start: f32, end: f32, threshold: f32) -> f32 {
62 if (end - start).abs() <= f32::EPSILON {
63 return (start > threshold) as i32 as f32;
64 }
65
66 let threshold_time = ((threshold - start) / (end - start)).clamp(0.0, 1.0);
67 if start > threshold {
68 if end > threshold {
69 1.0
70 } else {
71 threshold_time
72 }
73 } else if end > threshold {
74 1.0 - threshold_time
75 } else {
76 0.0
77 }
78}
79
80#[derive(Debug, Clone)]
81pub struct PlayerSample {
82 pub player_id: PlayerId,
83 pub is_team_0: bool,
84 pub rigid_body: Option<boxcars::RigidBody>,
85 pub boost_amount: Option<f32>,
86 pub last_boost_amount: Option<f32>,
87 pub boost_active: bool,
88 pub powerslide_active: bool,
89 pub match_goals: Option<i32>,
90 pub match_assists: Option<i32>,
91 pub match_saves: Option<i32>,
92 pub match_shots: Option<i32>,
93 pub match_score: Option<i32>,
94}
95
96impl PlayerSample {
97 pub fn position(&self) -> Option<glam::Vec3> {
98 self.rigid_body.as_ref().map(|rb| vec_to_glam(&rb.location))
99 }
100
101 pub fn velocity(&self) -> Option<glam::Vec3> {
102 self.rigid_body
103 .as_ref()
104 .and_then(|rb| rb.linear_velocity.as_ref().map(vec_to_glam))
105 }
106
107 pub fn speed(&self) -> Option<f32> {
108 self.velocity().map(|velocity| velocity.length())
109 }
110}
111
112#[derive(Debug, Clone, PartialEq, Eq)]
113pub struct DemoEventSample {
114 pub attacker: PlayerId,
115 pub victim: PlayerId,
116}
117
118#[derive(Debug, Clone)]
119pub struct StatsSample {
120 pub frame_number: usize,
121 pub time: f32,
122 pub dt: f32,
123 pub seconds_remaining: Option<i32>,
124 pub game_state: Option<i32>,
125 pub ball_has_been_hit: Option<bool>,
126 pub kickoff_countdown_time: Option<i32>,
127 pub team_zero_score: Option<i32>,
128 pub team_one_score: Option<i32>,
129 pub possession_team_is_team_0: Option<bool>,
130 pub scored_on_team_is_team_0: Option<bool>,
131 pub current_in_game_team_player_counts: Option<[usize; 2]>,
132 pub ball: Option<BallSample>,
133 pub players: Vec<PlayerSample>,
134 pub active_demos: Vec<DemoEventSample>,
135 pub demo_events: Vec<DemolishInfo>,
136 pub boost_pad_events: Vec<BoostPadEvent>,
137 pub touch_events: Vec<TouchEvent>,
138 pub dodge_refreshed_events: Vec<DodgeRefreshedEvent>,
139 pub player_stat_events: Vec<PlayerStatEvent>,
140 pub goal_events: Vec<GoalEvent>,
141}
142
143const GAME_STATE_KICKOFF_COUNTDOWN: i32 = 55;
144const GAME_STATE_GOAL_SCORED_REPLAY: i32 = 86;
145
146#[derive(Debug, Clone, Default, PartialEq)]
147pub struct LivePlayTracker {
148 post_goal_phase_active: bool,
149 last_score: Option<(i32, i32)>,
150}
151
152impl LivePlayTracker {
153 fn current_score(sample: &StatsSample) -> Option<(i32, i32)> {
154 Some((sample.team_zero_score?, sample.team_one_score?))
155 }
156
157 fn kickoff_phase_active(sample: &StatsSample) -> bool {
158 sample.game_state == Some(GAME_STATE_KICKOFF_COUNTDOWN)
159 || sample.kickoff_countdown_time.is_some_and(|time| time > 0)
160 }
161
162 fn live_play_internal(&mut self, sample: &StatsSample) -> bool {
163 let kickoff_phase_active = Self::kickoff_phase_active(sample);
164 let score_changed = Self::current_score(sample)
165 .zip(self.last_score)
166 .is_some_and(
167 |((team_zero_score, team_one_score), (last_team_zero, last_team_one))| {
168 team_zero_score > last_team_zero || team_one_score > last_team_one
169 },
170 );
171
172 if !sample.goal_events.is_empty() || score_changed {
173 self.post_goal_phase_active = true;
174 }
175
176 let live_play = sample.is_live_play() && !self.post_goal_phase_active;
177
178 if kickoff_phase_active {
179 self.post_goal_phase_active = false;
180 }
181
182 if let Some(score) = Self::current_score(sample) {
183 self.last_score = Some(score);
184 }
185
186 live_play
187 }
188
189 pub fn is_live_play(&mut self, sample: &StatsSample) -> bool {
190 self.live_play_internal(sample)
191 }
192}
193
194impl StatsSample {
195 pub(crate) fn from_processor(
196 processor: &ReplayProcessor,
197 frame_number: usize,
198 current_time: f32,
199 dt: f32,
200 ) -> SubtrActorResult<Self> {
201 let ball = processor
202 .get_interpolated_ball_rigid_body(current_time, 0.0)
203 .ok()
204 .filter(|rigid_body| !rigid_body.sleeping)
205 .map(|rigid_body| BallSample { rigid_body });
206
207 let mut players = Vec::new();
208 for player_id in processor.iter_player_ids_in_order() {
209 let Ok(is_team_0) = processor.get_player_is_team_0(player_id) else {
213 continue;
214 };
215 players.push(PlayerSample {
216 player_id: player_id.clone(),
217 is_team_0,
218 rigid_body: processor
219 .get_interpolated_player_rigid_body(player_id, current_time, 0.0)
220 .ok()
221 .filter(|rigid_body| !rigid_body.sleeping),
222 boost_amount: processor.get_player_boost_level(player_id).ok(),
223 last_boost_amount: processor.get_player_last_boost_level(player_id).ok(),
224 boost_active: processor.get_boost_active(player_id).unwrap_or(0) % 2 == 1,
225 powerslide_active: processor.get_powerslide_active(player_id).unwrap_or(false),
226 match_goals: processor.get_player_match_goals(player_id).ok(),
227 match_assists: processor.get_player_match_assists(player_id).ok(),
228 match_saves: processor.get_player_match_saves(player_id).ok(),
229 match_shots: processor.get_player_match_shots(player_id).ok(),
230 match_score: processor.get_player_match_score(player_id).ok(),
231 });
232 }
233
234 let team_scores = processor.get_team_scores().ok();
235 let possession_team_is_team_0 =
236 processor
237 .get_ball_hit_team_num()
238 .ok()
239 .and_then(|team_num| match team_num {
240 0 => Some(true),
241 1 => Some(false),
242 _ => None,
243 });
244 let scored_on_team_is_team_0 =
245 processor
246 .get_scored_on_team_num()
247 .ok()
248 .and_then(|team_num| match team_num {
249 0 => Some(true),
250 1 => Some(false),
251 _ => None,
252 });
253 let active_demos = if let Ok(demos) = processor.get_active_demos() {
254 demos
255 .filter_map(|demo| {
256 let attacker = processor
257 .get_player_id_from_car_id(&demo.attacker_actor_id())
258 .ok()?;
259 let victim = processor
260 .get_player_id_from_car_id(&demo.victim_actor_id())
261 .ok()?;
262 Some(DemoEventSample { attacker, victim })
263 })
264 .collect()
265 } else {
266 Vec::new()
267 };
268
269 Ok(Self {
270 frame_number,
271 time: current_time,
272 dt,
273 seconds_remaining: processor.get_seconds_remaining().ok(),
274 game_state: processor.get_replicated_state_name().ok(),
275 ball_has_been_hit: processor.get_ball_has_been_hit().ok(),
276 kickoff_countdown_time: processor.get_replicated_game_state_time_remaining().ok(),
277 team_zero_score: team_scores.map(|scores| scores.0),
278 team_one_score: team_scores.map(|scores| scores.1),
279 possession_team_is_team_0,
280 scored_on_team_is_team_0,
281 current_in_game_team_player_counts: Some(
282 processor.current_in_game_team_player_counts(),
283 ),
284 ball,
285 players,
286 active_demos,
287 demo_events: Vec::new(),
288 boost_pad_events: processor.current_frame_boost_pad_events().to_vec(),
289 touch_events: processor.current_frame_touch_events().to_vec(),
290 dodge_refreshed_events: processor.current_frame_dodge_refreshed_events().to_vec(),
291 player_stat_events: processor.current_frame_player_stat_events().to_vec(),
292 goal_events: processor.current_frame_goal_events().to_vec(),
293 })
294 }
295
296 pub fn is_live_play(&self) -> bool {
304 if matches!(
305 self.game_state,
306 Some(GAME_STATE_KICKOFF_COUNTDOWN | GAME_STATE_GOAL_SCORED_REPLAY)
307 ) {
308 return false;
309 }
310
311 true
312 }
313
314 pub fn current_in_game_team_player_count(&self, is_team_0: bool) -> usize {
315 self.current_in_game_team_player_counts
316 .map(|counts| counts[usize::from(!is_team_0)])
317 .unwrap_or_else(|| {
318 self.players
319 .iter()
320 .filter(|player| player.is_team_0 == is_team_0)
321 .count()
322 })
323 }
324}
325
326pub trait StatsReducer {
327 fn on_replay_meta(&mut self, _meta: &ReplayMeta) -> SubtrActorResult<()> {
328 Ok(())
329 }
330
331 fn on_sample(&mut self, _sample: &StatsSample) -> SubtrActorResult<()> {
332 Ok(())
333 }
334
335 fn on_sample_with_context(
336 &mut self,
337 sample: &StatsSample,
338 _ctx: &AnalysisContext,
339 ) -> SubtrActorResult<()> {
340 self.on_sample(sample)
341 }
342
343 fn finish(&mut self) -> SubtrActorResult<()> {
344 Ok(())
345 }
346}
347
348#[derive(Default)]
349pub struct CompositeStatsReducer {
350 children: Vec<Box<dyn StatsReducer>>,
351}
352
353impl CompositeStatsReducer {
354 pub fn new() -> Self {
355 Self::default()
356 }
357
358 pub fn push<R: StatsReducer + 'static>(&mut self, reducer: R) {
359 self.children.push(Box::new(reducer));
360 }
361
362 pub fn with_child<R: StatsReducer + 'static>(mut self, reducer: R) -> Self {
363 self.push(reducer);
364 self
365 }
366
367 pub fn children(&self) -> &[Box<dyn StatsReducer>] {
368 &self.children
369 }
370
371 pub fn children_mut(&mut self) -> &mut [Box<dyn StatsReducer>] {
372 &mut self.children
373 }
374}
375
376impl StatsReducer for CompositeStatsReducer {
377 fn on_replay_meta(&mut self, meta: &ReplayMeta) -> SubtrActorResult<()> {
378 for child in &mut self.children {
379 child.on_replay_meta(meta)?;
380 }
381 Ok(())
382 }
383
384 fn on_sample(&mut self, sample: &StatsSample) -> SubtrActorResult<()> {
385 for child in &mut self.children {
386 child.on_sample(sample)?;
387 }
388 Ok(())
389 }
390
391 fn on_sample_with_context(
392 &mut self,
393 sample: &StatsSample,
394 ctx: &AnalysisContext,
395 ) -> SubtrActorResult<()> {
396 for child in &mut self.children {
397 child.on_sample_with_context(sample, ctx)?;
398 }
399 Ok(())
400 }
401
402 fn finish(&mut self) -> SubtrActorResult<()> {
403 for child in &mut self.children {
404 child.finish()?;
405 }
406 Ok(())
407 }
408}
409
410pub struct ReducerCollector<R> {
411 reducer: R,
412 derived_signals: DerivedSignalGraph,
413 last_sample_time: Option<f32>,
414 replay_meta_initialized: bool,
415 last_demolish_count: usize,
416 last_boost_pad_event_count: usize,
417 last_touch_event_count: usize,
418 last_player_stat_event_count: usize,
419 last_goal_event_count: usize,
420}
421
422impl<R> ReducerCollector<R> {
423 pub fn new(reducer: R) -> Self {
424 Self {
425 reducer,
426 derived_signals: default_derived_signal_graph(),
427 last_sample_time: None,
428 replay_meta_initialized: false,
429 last_demolish_count: 0,
430 last_boost_pad_event_count: 0,
431 last_touch_event_count: 0,
432 last_player_stat_event_count: 0,
433 last_goal_event_count: 0,
434 }
435 }
436
437 pub fn into_inner(self) -> R {
438 self.reducer
439 }
440
441 pub fn reducer(&self) -> &R {
442 &self.reducer
443 }
444
445 pub fn reducer_mut(&mut self) -> &mut R {
446 &mut self.reducer
447 }
448}
449
450impl<R> From<R> for ReducerCollector<R> {
451 fn from(reducer: R) -> Self {
452 Self::new(reducer)
453 }
454}
455
456impl<R: StatsReducer> Collector for ReducerCollector<R> {
457 fn process_frame(
458 &mut self,
459 processor: &ReplayProcessor,
460 _frame: &boxcars::Frame,
461 frame_number: usize,
462 current_time: f32,
463 ) -> SubtrActorResult<TimeAdvance> {
464 if !self.replay_meta_initialized {
465 let replay_meta = processor.get_replay_meta()?;
466 self.derived_signals.on_replay_meta(&replay_meta)?;
467 self.reducer.on_replay_meta(&replay_meta)?;
468 self.replay_meta_initialized = true;
469 }
470
471 let dt = self
472 .last_sample_time
473 .map(|last_time| (current_time - last_time).max(0.0))
474 .unwrap_or(0.0);
475 let mut sample = StatsSample::from_processor(processor, frame_number, current_time, dt)?;
476 sample.active_demos.clear();
477 sample.demo_events = processor.demolishes[self.last_demolish_count..].to_vec();
478 sample.boost_pad_events =
479 processor.boost_pad_events[self.last_boost_pad_event_count..].to_vec();
480 sample.touch_events = processor.touch_events[self.last_touch_event_count..].to_vec();
481 sample.player_stat_events =
482 processor.player_stat_events[self.last_player_stat_event_count..].to_vec();
483 sample.goal_events = processor.goal_events[self.last_goal_event_count..].to_vec();
484 let analysis_context = self.derived_signals.evaluate(&sample)?;
485 self.reducer
486 .on_sample_with_context(&sample, analysis_context)?;
487 self.last_sample_time = Some(current_time);
488 self.last_demolish_count = processor.demolishes.len();
489 self.last_boost_pad_event_count = processor.boost_pad_events.len();
490 self.last_touch_event_count = processor.touch_events.len();
491 self.last_player_stat_event_count = processor.player_stat_events.len();
492 self.last_goal_event_count = processor.goal_events.len();
493
494 Ok(TimeAdvance::NextFrame)
495 }
496
497 fn finish_replay(&mut self, _processor: &ReplayProcessor) -> SubtrActorResult<()> {
498 self.derived_signals.finish()?;
499 self.reducer.finish()
500 }
501}
502
503const CAR_MAX_SPEED: f32 = 2300.0;
504const SUPERSONIC_SPEED_THRESHOLD: f32 = 2200.0;
505const BOOST_SPEED_THRESHOLD: f32 = 1410.0;
506const GROUND_Z_THRESHOLD: f32 = 20.0;
507const POWERSLIDE_MAX_Z_THRESHOLD: f32 = 40.0;
508const BALL_RADIUS_Z: f32 = 92.75;
509const BALL_CARRY_MIN_BALL_Z: f32 = BALL_RADIUS_Z + 5.0;
510const BALL_CARRY_MAX_BALL_Z: f32 = 600.0;
511const BALL_CARRY_MAX_HORIZONTAL_GAP: f32 = BALL_RADIUS_Z * 1.4;
512const BALL_CARRY_MAX_VERTICAL_GAP: f32 = 220.0;
513const BALL_CARRY_MIN_DURATION: f32 = 1.0;
514const HIGH_AIR_Z_THRESHOLD: f32 = 642.775 + BALL_RADIUS_Z;
517const FIELD_ZONE_BOUNDARY_Y: f32 = BOOST_PAD_SIDE_LANE_Y;
521const DEFAULT_MOST_BACK_FORWARD_THRESHOLD_Y: f32 = 236.0;
523const SMALL_PAD_AMOUNT_RAW: f32 = BOOST_MAX_AMOUNT * 12.0 / 100.0;
524const BOOST_ZERO_BAND_RAW: f32 = 1.0;
525const BOOST_FULL_BAND_MIN_RAW: f32 = BOOST_MAX_AMOUNT - 1.0;
526const STANDARD_PAD_MATCH_RADIUS_SMALL: f32 = 450.0;
527const STANDARD_PAD_MATCH_RADIUS_BIG: f32 = 1000.0;
528const BOOST_PAD_MIDFIELD_TOLERANCE_Y: f32 = 128.0;
529const BOOST_PAD_SMALL_Z: f32 = 70.0;
530const BOOST_PAD_BIG_Z: f32 = 73.0;
531const BOOST_PAD_BACK_CORNER_X: f32 = 3072.0;
532const BOOST_PAD_BACK_CORNER_Y: f32 = 4096.0;
533const BOOST_PAD_BACK_LANE_X: f32 = 1792.0;
534const BOOST_PAD_BACK_LANE_Y: f32 = 4184.0;
535const BOOST_PAD_BACK_MID_X: f32 = 940.0;
536const BOOST_PAD_BACK_MID_Y: f32 = 3308.0;
537const BOOST_PAD_CENTER_BACK_Y: f32 = 2816.0;
538const BOOST_PAD_SIDE_WALL_X: f32 = 3584.0;
539const BOOST_PAD_SIDE_WALL_Y: f32 = 2484.0;
540const BOOST_PAD_SIDE_LANE_X: f32 = 1788.0;
541const BOOST_PAD_SIDE_LANE_Y: f32 = 2300.0;
542const BOOST_PAD_FRONT_LANE_X: f32 = 2048.0;
543const BOOST_PAD_FRONT_LANE_Y: f32 = 1036.0;
544const BOOST_PAD_CENTER_X: f32 = 1024.0;
545const BOOST_PAD_CENTER_MID_Y: f32 = 1024.0;
546const BOOST_PAD_GOAL_LINE_Y: f32 = 4240.0;
547
548fn push_pad(
549 pads: &mut Vec<(glam::Vec3, BoostPadSize)>,
550 x: f32,
551 y: f32,
552 z: f32,
553 size: BoostPadSize,
554) {
555 pads.push((glam::Vec3::new(x, y, z), size));
556}
557
558fn push_mirror_x(
559 pads: &mut Vec<(glam::Vec3, BoostPadSize)>,
560 x: f32,
561 y: f32,
562 z: f32,
563 size: BoostPadSize,
564) {
565 push_pad(pads, -x, y, z, size);
566 push_pad(pads, x, y, z, size);
567}
568
569fn push_mirror_y(
570 pads: &mut Vec<(glam::Vec3, BoostPadSize)>,
571 x: f32,
572 y: f32,
573 z: f32,
574 size: BoostPadSize,
575) {
576 push_pad(pads, x, -y, z, size);
577 push_pad(pads, x, y, z, size);
578}
579
580fn push_mirror_xy(
581 pads: &mut Vec<(glam::Vec3, BoostPadSize)>,
582 x: f32,
583 y: f32,
584 z: f32,
585 size: BoostPadSize,
586) {
587 push_mirror_x(pads, x, -y, z, size);
588 push_mirror_x(pads, x, y, z, size);
589}
590
591fn build_standard_soccar_boost_pad_layout() -> Vec<(glam::Vec3, BoostPadSize)> {
592 let mut pads = Vec::with_capacity(34);
593
594 push_mirror_y(
595 &mut pads,
596 0.0,
597 BOOST_PAD_GOAL_LINE_Y,
598 BOOST_PAD_SMALL_Z,
599 BoostPadSize::Small,
600 );
601 push_mirror_xy(
602 &mut pads,
603 BOOST_PAD_BACK_LANE_X,
604 BOOST_PAD_BACK_LANE_Y,
605 BOOST_PAD_SMALL_Z,
606 BoostPadSize::Small,
607 );
608 push_mirror_xy(
609 &mut pads,
610 BOOST_PAD_BACK_CORNER_X,
611 BOOST_PAD_BACK_CORNER_Y,
612 BOOST_PAD_BIG_Z,
613 BoostPadSize::Big,
614 );
615 push_mirror_xy(
616 &mut pads,
617 BOOST_PAD_BACK_MID_X,
618 BOOST_PAD_BACK_MID_Y,
619 BOOST_PAD_SMALL_Z,
620 BoostPadSize::Small,
621 );
622 push_mirror_y(
623 &mut pads,
624 0.0,
625 BOOST_PAD_CENTER_BACK_Y,
626 BOOST_PAD_SMALL_Z,
627 BoostPadSize::Small,
628 );
629 push_mirror_xy(
630 &mut pads,
631 BOOST_PAD_SIDE_WALL_X,
632 BOOST_PAD_SIDE_WALL_Y,
633 BOOST_PAD_SMALL_Z,
634 BoostPadSize::Small,
635 );
636 push_mirror_xy(
637 &mut pads,
638 BOOST_PAD_SIDE_LANE_X,
639 BOOST_PAD_SIDE_LANE_Y,
640 BOOST_PAD_SMALL_Z,
641 BoostPadSize::Small,
642 );
643 push_mirror_xy(
644 &mut pads,
645 BOOST_PAD_FRONT_LANE_X,
646 BOOST_PAD_FRONT_LANE_Y,
647 BOOST_PAD_SMALL_Z,
648 BoostPadSize::Small,
649 );
650 push_mirror_y(
651 &mut pads,
652 0.0,
653 BOOST_PAD_CENTER_MID_Y,
654 BOOST_PAD_SMALL_Z,
655 BoostPadSize::Small,
656 );
657 push_mirror_x(
658 &mut pads,
659 BOOST_PAD_SIDE_WALL_X,
660 0.0,
661 BOOST_PAD_BIG_Z,
662 BoostPadSize::Big,
663 );
664 push_mirror_x(
665 &mut pads,
666 BOOST_PAD_CENTER_X,
667 0.0,
668 BOOST_PAD_SMALL_Z,
669 BoostPadSize::Small,
670 );
671
672 pads
673}
674
675static STANDARD_SOCCAR_BOOST_PAD_LAYOUT: LazyLock<Vec<(glam::Vec3, BoostPadSize)>> =
676 LazyLock::new(build_standard_soccar_boost_pad_layout);
677
678pub fn standard_soccar_boost_pad_layout() -> &'static [(glam::Vec3, BoostPadSize)] {
679 STANDARD_SOCCAR_BOOST_PAD_LAYOUT.as_slice()
680}
681
682fn normalized_y(is_team_0: bool, position: glam::Vec3) -> f32 {
683 if is_team_0 {
684 position.y
685 } else {
686 -position.y
687 }
688}
689
690fn is_enemy_side(is_team_0: bool, position: glam::Vec3) -> bool {
691 normalized_y(is_team_0, position) > BOOST_PAD_MIDFIELD_TOLERANCE_Y
692}
693
694fn standard_soccar_boost_pad_position(index: usize) -> glam::Vec3 {
695 STANDARD_SOCCAR_BOOST_PAD_LAYOUT[index].0
696}
697
698#[derive(Debug, Clone, Default)]
699struct PadPositionEstimate {
700 observations: Vec<glam::Vec3>,
701}
702
703impl PadPositionEstimate {
704 fn observe(&mut self, position: glam::Vec3) {
705 self.observations.push(position);
706 }
707
708 fn observations(&self) -> &[glam::Vec3] {
709 self.observations.as_slice()
710 }
711
712 fn mean(&self) -> Option<glam::Vec3> {
713 if self.observations.is_empty() {
714 return None;
715 }
716
717 let sum = self
718 .observations
719 .iter()
720 .copied()
721 .fold(glam::Vec3::ZERO, |acc, position| acc + position);
722 Some(sum / self.observations.len() as f32)
723 }
724}
725
726fn header_prop_to_f32(prop: &HeaderProp) -> Option<f32> {
727 match prop {
728 HeaderProp::Float(value) => Some(*value),
729 HeaderProp::Int(value) => Some(*value as f32),
730 HeaderProp::QWord(value) => Some(*value as f32),
731 _ => None,
732 }
733}
734
735fn get_header_f32(stats: &HashMap<String, HeaderProp>, keys: &[&str]) -> Option<f32> {
736 keys.iter()
737 .find_map(|key| stats.get(*key).and_then(header_prop_to_f32))
738}
739
740pub mod powerslide;
741#[allow(unused_imports)]
742pub use powerslide::*;
743pub mod analysis;
744pub use analysis::*;
745pub mod pressure;
746#[allow(unused_imports)]
747pub use pressure::*;
748pub mod rush;
749#[allow(unused_imports)]
750pub use rush::*;
751pub mod possession;
752#[allow(unused_imports)]
753pub use possession::*;
754pub mod settings;
755pub use settings::*;
756pub mod match_stats;
757pub use match_stats::*;
758pub mod demo;
759pub use demo::*;
760pub mod dodge_reset;
761pub use dodge_reset::*;
762pub mod touch;
763pub use touch::*;
764pub mod fifty_fifty;
765pub use fifty_fifty::*;
766pub mod movement;
767pub use movement::*;
768pub mod positioning;
769pub use positioning::*;
770pub mod ball_carry;
771pub use ball_carry::*;
772pub mod boost;
773pub use boost::*;