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