Skip to main content

subtr_actor/stats/calculators/
boost.rs

1use super::*;
2
3#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, ts_rs::TS)]
4#[ts(export)]
5pub struct BoostStats {
6    pub tracked_time: f32,
7    pub boost_integral: f32,
8    pub time_zero_boost: f32,
9    pub time_hundred_boost: f32,
10    pub time_boost_0_25: f32,
11    pub time_boost_25_50: f32,
12    pub time_boost_50_75: f32,
13    pub time_boost_75_100: f32,
14    pub amount_collected: f32,
15    pub amount_stolen: f32,
16    pub big_pads_collected: u32,
17    pub small_pads_collected: u32,
18    pub big_pads_stolen: u32,
19    pub small_pads_stolen: u32,
20    pub amount_collected_big: f32,
21    pub amount_stolen_big: f32,
22    pub amount_collected_small: f32,
23    pub amount_stolen_small: f32,
24    pub amount_respawned: f32,
25    pub overfill_total: f32,
26    pub overfill_from_stolen: f32,
27    pub amount_used: f32,
28    pub amount_used_while_grounded: f32,
29    pub amount_used_while_airborne: f32,
30    pub amount_used_while_supersonic: f32,
31}
32
33impl BoostStats {
34    pub fn average_boost_amount(&self) -> f32 {
35        if self.tracked_time == 0.0 {
36            0.0
37        } else {
38            self.boost_integral / self.tracked_time
39        }
40    }
41
42    pub fn bpm(&self) -> f32 {
43        if self.tracked_time == 0.0 {
44            0.0
45        } else {
46            self.amount_collected * 60.0 / self.tracked_time
47        }
48    }
49
50    fn pct(&self, value: f32) -> f32 {
51        if self.tracked_time == 0.0 {
52            0.0
53        } else {
54            value * 100.0 / self.tracked_time
55        }
56    }
57
58    pub fn zero_boost_pct(&self) -> f32 {
59        self.pct(self.time_zero_boost)
60    }
61
62    pub fn hundred_boost_pct(&self) -> f32 {
63        self.pct(self.time_hundred_boost)
64    }
65
66    pub fn boost_0_25_pct(&self) -> f32 {
67        self.pct(self.time_boost_0_25)
68    }
69
70    pub fn boost_25_50_pct(&self) -> f32 {
71        self.pct(self.time_boost_25_50)
72    }
73
74    pub fn boost_50_75_pct(&self) -> f32 {
75        self.pct(self.time_boost_50_75)
76    }
77
78    pub fn boost_75_100_pct(&self) -> f32 {
79        self.pct(self.time_boost_75_100)
80    }
81
82    pub fn amount_obtained(&self) -> f32 {
83        self.amount_collected_big + self.amount_collected_small + self.amount_respawned
84    }
85
86    pub fn amount_used_by_vertical_band(&self) -> f32 {
87        self.amount_used_while_grounded + self.amount_used_while_airborne
88    }
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
92pub struct BoostCalculatorConfig {
93    pub include_non_live_pickups: bool,
94}
95
96#[derive(Debug, Clone, Default)]
97pub struct BoostCalculator {
98    config: BoostCalculatorConfig,
99    player_stats: HashMap<PlayerId, BoostStats>,
100    team_zero_stats: BoostStats,
101    team_one_stats: BoostStats,
102    previous_boost_amounts: HashMap<PlayerId, f32>,
103    previous_player_speeds: HashMap<PlayerId, f32>,
104    observed_pad_positions: HashMap<String, PadPositionEstimate>,
105    known_pad_sizes: HashMap<String, BoostPadSize>,
106    known_pad_indices: HashMap<String, usize>,
107    unavailable_pads: HashSet<String>,
108    seen_pickup_sequences: HashSet<(String, u8)>,
109    pickup_frames: HashMap<(String, PlayerId), usize>,
110    last_pickup_times: HashMap<String, f32>,
111    kickoff_phase_active_last_frame: bool,
112    kickoff_respawn_awarded: HashSet<PlayerId>,
113    initial_respawn_awarded: HashSet<PlayerId>,
114    pending_demo_respawns: HashSet<PlayerId>,
115    previous_boost_levels_live: Option<bool>,
116    active_invariant_warnings: HashSet<BoostInvariantWarningKey>,
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, Hash)]
120struct BoostInvariantWarningKey {
121    scope: String,
122    kind: BoostInvariantKind,
123}
124
125#[derive(Debug, Clone)]
126struct PendingBoostPickup {
127    player_id: PlayerId,
128    is_team_0: bool,
129    previous_boost_amount: f32,
130    pre_applied_collected_amount: f32,
131    pre_applied_pad_size: Option<BoostPadSize>,
132    player_position: glam::Vec3,
133}
134
135impl BoostCalculator {
136    pub fn new() -> Self {
137        Self::with_config(BoostCalculatorConfig::default())
138    }
139
140    pub fn with_config(config: BoostCalculatorConfig) -> Self {
141        Self {
142            config,
143            ..Self::default()
144        }
145    }
146
147    pub fn player_stats(&self) -> &HashMap<PlayerId, BoostStats> {
148        &self.player_stats
149    }
150
151    pub fn team_zero_stats(&self) -> &BoostStats {
152        &self.team_zero_stats
153    }
154
155    pub fn team_one_stats(&self) -> &BoostStats {
156        &self.team_one_stats
157    }
158
159    fn estimated_pad_position(&self, pad_id: &str) -> Option<glam::Vec3> {
160        self.observed_pad_positions
161            .get(pad_id)
162            .and_then(PadPositionEstimate::mean)
163    }
164
165    fn observed_pad_positions(&self, pad_id: &str) -> &[glam::Vec3] {
166        self.observed_pad_positions
167            .get(pad_id)
168            .map(PadPositionEstimate::observations)
169            .unwrap_or(&[])
170    }
171
172    fn pad_match_radius(pad_size: BoostPadSize) -> f32 {
173        match pad_size {
174            BoostPadSize::Big => STANDARD_PAD_MATCH_RADIUS_BIG,
175            BoostPadSize::Small => STANDARD_PAD_MATCH_RADIUS_SMALL,
176        }
177    }
178
179    pub fn resolved_boost_pads(&self) -> Vec<ResolvedBoostPad> {
180        standard_soccar_boost_pad_layout()
181            .iter()
182            .enumerate()
183            .map(|(index, (position, size))| ResolvedBoostPad {
184                index,
185                pad_id: self
186                    .known_pad_indices
187                    .iter()
188                    .find_map(|(pad_id, pad_index)| (*pad_index == index).then(|| pad_id.clone())),
189                size: *size,
190                position: glam_to_vec(position),
191            })
192            .collect()
193    }
194
195    fn infer_pad_index(
196        &self,
197        pad_id: &str,
198        pad_size: BoostPadSize,
199        observed_position: glam::Vec3,
200    ) -> Option<usize> {
201        if let Some(index) = self.known_pad_indices.get(pad_id).copied() {
202            return Some(index);
203        }
204
205        let observed_position = self
206            .estimated_pad_position(pad_id)
207            .unwrap_or(observed_position);
208        let layout = &*STANDARD_SOCCAR_BOOST_PAD_LAYOUT;
209        let used_indices: HashSet<usize> = self.known_pad_indices.values().copied().collect();
210        let radius = Self::pad_match_radius(pad_size);
211        let observed_positions = self.observed_pad_positions(pad_id);
212        let best_candidate = |allow_used: bool| {
213            layout
214                .iter()
215                .enumerate()
216                .filter(|(index, (_, size))| {
217                    *size == pad_size && (allow_used || !used_indices.contains(index))
218                })
219                .filter_map(|(index, (candidate_position, _))| {
220                    let mut vote_count = 0usize;
221                    let mut total_vote_distance = 0.0f32;
222                    let mut best_vote_distance = f32::INFINITY;
223
224                    for position in observed_positions {
225                        let distance = position.distance(*candidate_position);
226                        if distance <= radius {
227                            vote_count += 1;
228                            total_vote_distance += distance;
229                            best_vote_distance = best_vote_distance.min(distance);
230                        }
231                    }
232
233                    if vote_count == 0 {
234                        return None;
235                    }
236
237                    let representative_distance = observed_position.distance(*candidate_position);
238                    Some((
239                        index,
240                        vote_count,
241                        total_vote_distance / vote_count as f32,
242                        best_vote_distance,
243                        representative_distance,
244                    ))
245                })
246                .max_by(|left, right| {
247                    left.1
248                        .cmp(&right.1)
249                        .then_with(|| right.2.partial_cmp(&left.2).unwrap())
250                        .then_with(|| right.3.partial_cmp(&left.3).unwrap())
251                        .then_with(|| right.4.partial_cmp(&left.4).unwrap())
252                })
253                .map(|(index, _, _, _, _)| index)
254        };
255
256        best_candidate(false)
257            .or_else(|| best_candidate(true))
258            .or_else(|| {
259                layout
260                    .iter()
261                    .enumerate()
262                    .filter(|(index, (_, size))| *size == pad_size && !used_indices.contains(index))
263                    .min_by(|(_, (a, _)), (_, (b, _))| {
264                        observed_position
265                            .distance_squared(*a)
266                            .partial_cmp(&observed_position.distance_squared(*b))
267                            .unwrap()
268                    })
269                    .map(|(index, _)| index)
270            })
271            .or_else(|| {
272                layout
273                    .iter()
274                    .enumerate()
275                    .filter(|(_, (_, size))| *size == pad_size)
276                    .min_by(|(_, (a, _)), (_, (b, _))| {
277                        observed_position
278                            .distance_squared(*a)
279                            .partial_cmp(&observed_position.distance_squared(*b))
280                            .unwrap()
281                    })
282                    .map(|(index, _)| index)
283            })
284            .filter(|index| {
285                observed_position.distance(standard_soccar_boost_pad_position(*index)) <= radius
286            })
287    }
288
289    fn infer_pad_details_from_position(
290        &self,
291        pad_id: &str,
292        observed_position: glam::Vec3,
293    ) -> Option<(usize, BoostPadSize)> {
294        if let Some(index) = self.known_pad_indices.get(pad_id).copied() {
295            let (_, size) = standard_soccar_boost_pad_layout().get(index)?;
296            return Some((index, *size));
297        }
298
299        let observed_position = self
300            .estimated_pad_position(pad_id)
301            .unwrap_or(observed_position);
302        let layout = &*STANDARD_SOCCAR_BOOST_PAD_LAYOUT;
303        let used_indices: HashSet<usize> = self.known_pad_indices.values().copied().collect();
304        let observed_positions = self.observed_pad_positions(pad_id);
305        let best_candidate = |allow_used: bool| {
306            layout
307                .iter()
308                .enumerate()
309                .filter(|(index, _)| allow_used || !used_indices.contains(index))
310                .filter_map(|(index, (candidate_position, size))| {
311                    let radius = Self::pad_match_radius(*size);
312                    let mut vote_count = 0usize;
313                    let mut total_vote_distance = 0.0f32;
314                    let mut best_vote_distance = f32::INFINITY;
315
316                    for position in observed_positions {
317                        let distance = position.distance(*candidate_position);
318                        if distance <= radius {
319                            vote_count += 1;
320                            total_vote_distance += distance;
321                            best_vote_distance = best_vote_distance.min(distance);
322                        }
323                    }
324
325                    if vote_count == 0 {
326                        return None;
327                    }
328
329                    let representative_distance = observed_position.distance(*candidate_position);
330                    Some((
331                        index,
332                        *size,
333                        vote_count,
334                        total_vote_distance / vote_count as f32,
335                        best_vote_distance,
336                        representative_distance,
337                    ))
338                })
339                .max_by(|left, right| {
340                    left.2
341                        .cmp(&right.2)
342                        .then_with(|| right.3.partial_cmp(&left.3).unwrap())
343                        .then_with(|| right.4.partial_cmp(&left.4).unwrap())
344                        .then_with(|| right.5.partial_cmp(&left.5).unwrap())
345                })
346                .map(|(index, size, _, _, _, _)| (index, size))
347        };
348
349        best_candidate(false).or_else(|| best_candidate(true))
350    }
351
352    fn guess_pad_size_from_position(
353        &self,
354        pad_id: &str,
355        observed_position: glam::Vec3,
356    ) -> Option<BoostPadSize> {
357        if let Some(pad_size) = self.known_pad_sizes.get(pad_id).copied() {
358            return Some(pad_size);
359        }
360
361        if let Some((_, pad_size)) = self.infer_pad_details_from_position(pad_id, observed_position)
362        {
363            return Some(pad_size);
364        }
365
366        let observed_position = self
367            .estimated_pad_position(pad_id)
368            .unwrap_or(observed_position);
369        standard_soccar_boost_pad_layout()
370            .iter()
371            .min_by(|(left_position, _), (right_position, _)| {
372                observed_position
373                    .distance_squared(*left_position)
374                    .partial_cmp(&observed_position.distance_squared(*right_position))
375                    .unwrap()
376            })
377            .map(|(_, pad_size)| *pad_size)
378    }
379
380    fn resolve_pickup(
381        &mut self,
382        pad_id: &str,
383        pending_pickup: PendingBoostPickup,
384        pad_size: BoostPadSize,
385    ) {
386        let observed_position = self
387            .estimated_pad_position(pad_id)
388            .unwrap_or(pending_pickup.player_position);
389        let pad_position = self
390            .infer_pad_index(pad_id, pad_size, observed_position)
391            .map(|index| {
392                self.known_pad_indices.insert(pad_id.to_string(), index);
393                standard_soccar_boost_pad_position(index)
394            })
395            .unwrap_or(observed_position);
396        let stolen = is_enemy_side(pending_pickup.is_team_0, pad_position);
397        let stats = self
398            .player_stats
399            .entry(pending_pickup.player_id.clone())
400            .or_default();
401        let team_stats = if pending_pickup.is_team_0 {
402            &mut self.team_zero_stats
403        } else {
404            &mut self.team_one_stats
405        };
406        let nominal_gain = match pad_size {
407            BoostPadSize::Big => BOOST_MAX_AMOUNT,
408            BoostPadSize::Small => SMALL_PAD_AMOUNT_RAW,
409        };
410        let collected_amount = (BOOST_MAX_AMOUNT - pending_pickup.previous_boost_amount)
411            .min(nominal_gain)
412            .max(pending_pickup.pre_applied_collected_amount);
413        let collected_amount_delta = collected_amount - pending_pickup.pre_applied_collected_amount;
414        let overfill = (nominal_gain - collected_amount).max(0.0);
415
416        stats.amount_collected += collected_amount_delta;
417        team_stats.amount_collected += collected_amount_delta;
418
419        match pending_pickup.pre_applied_pad_size {
420            Some(pre_applied_pad_size) if pre_applied_pad_size == pad_size => {
421                Self::apply_collected_bucket_amount(stats, pad_size, collected_amount_delta);
422                Self::apply_collected_bucket_amount(team_stats, pad_size, collected_amount_delta);
423            }
424            Some(pre_applied_pad_size) => {
425                Self::apply_collected_bucket_amount(
426                    stats,
427                    pre_applied_pad_size,
428                    -pending_pickup.pre_applied_collected_amount,
429                );
430                Self::apply_collected_bucket_amount(
431                    team_stats,
432                    pre_applied_pad_size,
433                    -pending_pickup.pre_applied_collected_amount,
434                );
435                Self::apply_collected_bucket_amount(stats, pad_size, collected_amount);
436                Self::apply_collected_bucket_amount(team_stats, pad_size, collected_amount);
437            }
438            None => {
439                Self::apply_collected_bucket_amount(stats, pad_size, collected_amount);
440                Self::apply_collected_bucket_amount(team_stats, pad_size, collected_amount);
441            }
442        }
443
444        if stolen {
445            stats.amount_stolen += collected_amount;
446            team_stats.amount_stolen += collected_amount;
447        }
448
449        match pad_size {
450            BoostPadSize::Big => {
451                stats.big_pads_collected += 1;
452                team_stats.big_pads_collected += 1;
453                if stolen {
454                    stats.big_pads_stolen += 1;
455                    team_stats.big_pads_stolen += 1;
456                    stats.amount_stolen_big += collected_amount;
457                    team_stats.amount_stolen_big += collected_amount;
458                }
459            }
460            BoostPadSize::Small => {
461                stats.small_pads_collected += 1;
462                team_stats.small_pads_collected += 1;
463                if stolen {
464                    stats.small_pads_stolen += 1;
465                    team_stats.small_pads_stolen += 1;
466                    stats.amount_stolen_small += collected_amount;
467                    team_stats.amount_stolen_small += collected_amount;
468                }
469            }
470        }
471
472        stats.overfill_total += overfill;
473        team_stats.overfill_total += overfill;
474        if stolen {
475            stats.overfill_from_stolen += overfill;
476            team_stats.overfill_from_stolen += overfill;
477        }
478    }
479
480    fn apply_collected_bucket_amount(stats: &mut BoostStats, pad_size: BoostPadSize, amount: f32) {
481        if amount == 0.0 {
482            return;
483        }
484
485        match pad_size {
486            BoostPadSize::Big => stats.amount_collected_big += amount,
487            BoostPadSize::Small => stats.amount_collected_small += amount,
488        }
489    }
490
491    fn apply_pickup_collected_amount(
492        &mut self,
493        player_id: &PlayerId,
494        is_team_0: bool,
495        amount: f32,
496        pad_size: Option<BoostPadSize>,
497    ) {
498        if amount <= 0.0 {
499            return;
500        }
501
502        let stats = self.player_stats.entry(player_id.clone()).or_default();
503        let team_stats = if is_team_0 {
504            &mut self.team_zero_stats
505        } else {
506            &mut self.team_one_stats
507        };
508        stats.amount_collected += amount;
509        team_stats.amount_collected += amount;
510        if let Some(pad_size) = pad_size {
511            Self::apply_collected_bucket_amount(stats, pad_size, amount);
512            Self::apply_collected_bucket_amount(team_stats, pad_size, amount);
513        }
514    }
515
516    fn apply_respawn_amount(&mut self, player_id: &PlayerId, is_team_0: bool, amount: f32) {
517        if amount <= 0.0 {
518            return;
519        }
520
521        let stats = self.player_stats.entry(player_id.clone()).or_default();
522        let team_stats = if is_team_0 {
523            &mut self.team_zero_stats
524        } else {
525            &mut self.team_one_stats
526        };
527        stats.amount_respawned += amount;
528        team_stats.amount_respawned += amount;
529    }
530
531    fn warn_for_boost_invariant_violations(
532        &mut self,
533        scope: &str,
534        frame_number: usize,
535        time: f32,
536        stats: &BoostStats,
537        observed_boost_amount: Option<f32>,
538    ) {
539        let violations = boost_invariant_violations(stats, observed_boost_amount);
540        let active_kinds: HashSet<BoostInvariantKind> =
541            violations.iter().map(|violation| violation.kind).collect();
542
543        for violation in violations {
544            let key = BoostInvariantWarningKey {
545                scope: scope.to_string(),
546                kind: violation.kind,
547            };
548            if self.active_invariant_warnings.insert(key) {
549                log::warn!(
550                    "Boost invariant violation for {} at frame {} (t={:.3}): {}",
551                    scope,
552                    frame_number,
553                    time,
554                    violation.message(),
555                );
556            }
557        }
558
559        for kind in BoostInvariantKind::ALL {
560            if active_kinds.contains(&kind) {
561                continue;
562            }
563            self.active_invariant_warnings
564                .remove(&BoostInvariantWarningKey {
565                    scope: scope.to_string(),
566                    kind,
567                });
568        }
569    }
570
571    fn warn_for_sample_boost_invariants(&mut self, frame: &FrameInfo, players: &PlayerFrameState) {
572        let team_zero_stats = self.team_zero_stats.clone();
573        let team_one_stats = self.team_one_stats.clone();
574        let player_scopes: Vec<(PlayerId, Option<f32>, BoostStats)> = players
575            .players
576            .iter()
577            .map(|player| {
578                (
579                    player.player_id.clone(),
580                    player.boost_amount,
581                    self.player_stats
582                        .get(&player.player_id)
583                        .cloned()
584                        .unwrap_or_default(),
585                )
586            })
587            .collect();
588
589        self.warn_for_boost_invariant_violations(
590            "team_zero",
591            frame.frame_number,
592            frame.time,
593            &team_zero_stats,
594            None,
595        );
596        self.warn_for_boost_invariant_violations(
597            "team_one",
598            frame.frame_number,
599            frame.time,
600            &team_one_stats,
601            None,
602        );
603        for (player_id, observed_boost_amount, stats) in player_scopes {
604            self.warn_for_boost_invariant_violations(
605                &format!("player {player_id:?}"),
606                frame.frame_number,
607                frame.time,
608                &stats,
609                observed_boost_amount,
610            );
611        }
612    }
613
614    fn interval_fraction_in_boost_range(
615        start_boost: f32,
616        end_boost: f32,
617        min_boost: f32,
618        max_boost: f32,
619    ) -> f32 {
620        if (end_boost - start_boost).abs() <= f32::EPSILON {
621            return ((start_boost >= min_boost) && (start_boost < max_boost)) as i32 as f32;
622        }
623
624        let t_at_min = (min_boost - start_boost) / (end_boost - start_boost);
625        let t_at_max = (max_boost - start_boost) / (end_boost - start_boost);
626        let interval_start = t_at_min.min(t_at_max).max(0.0);
627        let interval_end = t_at_min.max(t_at_max).min(1.0);
628        (interval_end - interval_start).max(0.0)
629    }
630
631    fn pad_respawn_time_seconds(pad_size: BoostPadSize) -> f32 {
632        match pad_size {
633            BoostPadSize::Big => 10.0,
634            BoostPadSize::Small => 4.0,
635        }
636    }
637
638    fn boost_levels_live(live_play: bool) -> bool {
639        live_play
640    }
641
642    fn tracks_boost_levels(boost_levels_live: bool) -> bool {
643        boost_levels_live
644    }
645
646    fn tracks_boost_pickups(gameplay: &GameplayState, live_play: bool) -> bool {
647        live_play
648            || (gameplay.ball_has_been_hit == Some(false)
649                && gameplay.game_state != Some(GAME_STATE_KICKOFF_COUNTDOWN)
650                && gameplay.kickoff_countdown_time.is_none_or(|t| t <= 0))
651    }
652
653    pub fn update_parts(
654        &mut self,
655        frame: &FrameInfo,
656        gameplay: &GameplayState,
657        players: &PlayerFrameState,
658        events: &FrameEventsState,
659        vertical_state: &PlayerVerticalState,
660        live_play: bool,
661    ) -> SubtrActorResult<()> {
662        let boost_levels_live = Self::boost_levels_live(live_play);
663        let track_boost_levels = Self::tracks_boost_levels(boost_levels_live);
664        let track_boost_pickups = Self::tracks_boost_pickups(gameplay, live_play);
665        let boost_levels_resumed_this_sample =
666            boost_levels_live && !self.previous_boost_levels_live.unwrap_or(false);
667        let kickoff_phase_active = gameplay.game_state == Some(GAME_STATE_KICKOFF_COUNTDOWN)
668            || gameplay.kickoff_countdown_time.is_some_and(|t| t > 0)
669            || gameplay.ball_has_been_hit == Some(false);
670        let kickoff_phase_started = kickoff_phase_active && !self.kickoff_phase_active_last_frame;
671        if kickoff_phase_started {
672            self.kickoff_respawn_awarded.clear();
673        }
674        for demo in &events.demo_events {
675            self.pending_demo_respawns.insert(demo.victim.clone());
676        }
677
678        let mut current_boost_amounts = Vec::new();
679        let mut pickup_counts_by_player = HashMap::<PlayerId, usize>::new();
680        let mut respawn_amounts_by_player = HashMap::<PlayerId, f32>::new();
681
682        for event in &events.boost_pad_events {
683            let BoostPadEventKind::PickedUp { .. } = event.kind else {
684                continue;
685            };
686            let Some(player_id) = &event.player else {
687                continue;
688            };
689            *pickup_counts_by_player
690                .entry(player_id.clone())
691                .or_default() += 1;
692        }
693
694        for player in &players.players {
695            let Some(boost_amount) = player.boost_amount else {
696                continue;
697            };
698            let previous_boost_amount = player.last_boost_amount.unwrap_or_else(|| {
699                self.previous_boost_amounts
700                    .get(&player.player_id)
701                    .copied()
702                    .unwrap_or(boost_amount)
703            });
704            let previous_boost_amount = if boost_levels_resumed_this_sample {
705                boost_amount
706            } else {
707                previous_boost_amount
708            };
709            if track_boost_levels {
710                let average_boost_amount = (previous_boost_amount + boost_amount) * 0.5;
711                let time_zero_boost = frame.dt
712                    * Self::interval_fraction_in_boost_range(
713                        previous_boost_amount,
714                        boost_amount,
715                        0.0,
716                        BOOST_ZERO_BAND_RAW,
717                    );
718                let time_hundred_boost = frame.dt
719                    * Self::interval_fraction_in_boost_range(
720                        previous_boost_amount,
721                        boost_amount,
722                        BOOST_FULL_BAND_MIN_RAW,
723                        BOOST_MAX_AMOUNT + 1.0,
724                    );
725                let time_boost_0_25 = frame.dt
726                    * Self::interval_fraction_in_boost_range(
727                        previous_boost_amount,
728                        boost_amount,
729                        0.0,
730                        boost_percent_to_amount(25.0),
731                    );
732                let time_boost_25_50 = frame.dt
733                    * Self::interval_fraction_in_boost_range(
734                        previous_boost_amount,
735                        boost_amount,
736                        boost_percent_to_amount(25.0),
737                        boost_percent_to_amount(50.0),
738                    );
739                let time_boost_50_75 = frame.dt
740                    * Self::interval_fraction_in_boost_range(
741                        previous_boost_amount,
742                        boost_amount,
743                        boost_percent_to_amount(50.0),
744                        boost_percent_to_amount(75.0),
745                    );
746                let time_boost_75_100 = frame.dt
747                    * Self::interval_fraction_in_boost_range(
748                        previous_boost_amount,
749                        boost_amount,
750                        boost_percent_to_amount(75.0),
751                        BOOST_MAX_AMOUNT + 1.0,
752                    );
753                let stats = self
754                    .player_stats
755                    .entry(player.player_id.clone())
756                    .or_default();
757                let team_stats = if player.is_team_0 {
758                    &mut self.team_zero_stats
759                } else {
760                    &mut self.team_one_stats
761                };
762
763                stats.tracked_time += frame.dt;
764                stats.boost_integral += average_boost_amount * frame.dt;
765                team_stats.tracked_time += frame.dt;
766                team_stats.boost_integral += average_boost_amount * frame.dt;
767                stats.time_zero_boost += time_zero_boost;
768                team_stats.time_zero_boost += time_zero_boost;
769                stats.time_hundred_boost += time_hundred_boost;
770                team_stats.time_hundred_boost += time_hundred_boost;
771                stats.time_boost_0_25 += time_boost_0_25;
772                team_stats.time_boost_0_25 += time_boost_0_25;
773                stats.time_boost_25_50 += time_boost_25_50;
774                team_stats.time_boost_25_50 += time_boost_25_50;
775                stats.time_boost_50_75 += time_boost_50_75;
776                team_stats.time_boost_50_75 += time_boost_50_75;
777                stats.time_boost_75_100 += time_boost_75_100;
778                team_stats.time_boost_75_100 += time_boost_75_100;
779            }
780
781            let mut respawn_amount = 0.0;
782            // Grant initial kickoff respawn the first time we see each player.
783            // This handles replays that start after the kickoff countdown has
784            // already ended (game_state != 55 on the first frame).
785            let first_seen_player = self
786                .initial_respawn_awarded
787                .insert(player.player_id.clone());
788            if first_seen_player
789                || (kickoff_phase_active
790                    && !self.kickoff_respawn_awarded.contains(&player.player_id))
791            {
792                respawn_amount += BOOST_KICKOFF_START_AMOUNT;
793                self.kickoff_respawn_awarded
794                    .insert(player.player_id.clone());
795            }
796            if self.pending_demo_respawns.contains(&player.player_id) && player.rigid_body.is_some()
797            {
798                respawn_amount += BOOST_KICKOFF_START_AMOUNT;
799                self.pending_demo_respawns.remove(&player.player_id);
800            }
801            if respawn_amount > 0.0 {
802                self.apply_respawn_amount(&player.player_id, player.is_team_0, respawn_amount);
803            }
804            respawn_amounts_by_player.insert(player.player_id.clone(), respawn_amount);
805
806            current_boost_amounts.push((player.player_id.clone(), boost_amount));
807        }
808
809        for event in &events.boost_pad_events {
810            match event.kind {
811                BoostPadEventKind::PickedUp { sequence } => {
812                    if !track_boost_pickups && !self.config.include_non_live_pickups {
813                        continue;
814                    }
815                    if self.unavailable_pads.contains(&event.pad_id) {
816                        continue;
817                    }
818                    let Some(player_id) = &event.player else {
819                        continue;
820                    };
821                    let pickup_key = (event.pad_id.clone(), player_id.clone());
822                    if self.pickup_frames.get(&pickup_key).copied() == Some(event.frame) {
823                        continue;
824                    }
825                    self.pickup_frames.insert(pickup_key, event.frame);
826                    if !self
827                        .seen_pickup_sequences
828                        .insert((event.pad_id.clone(), sequence))
829                    {
830                        continue;
831                    }
832                    self.unavailable_pads.insert(event.pad_id.clone());
833                    self.last_pickup_times
834                        .insert(event.pad_id.clone(), event.time);
835                    let Some(player) = players
836                        .players
837                        .iter()
838                        .find(|player| &player.player_id == player_id)
839                    else {
840                        continue;
841                    };
842                    if let Some(position) = player.position() {
843                        self.observed_pad_positions
844                            .entry(event.pad_id.clone())
845                            .or_default()
846                            .observe(position);
847                    }
848                    let previous_boost_amount = player.last_boost_amount.unwrap_or_else(|| {
849                        self.previous_boost_amounts
850                            .get(player_id)
851                            .copied()
852                            .unwrap_or_else(|| player.boost_amount.unwrap_or(0.0))
853                    });
854                    let pre_applied_collected_amount =
855                        if pickup_counts_by_player.get(player_id).copied() == Some(1) {
856                            self.previous_boost_amounts
857                                .get(player_id)
858                                .copied()
859                                .map(|previous_sample_boost_amount| {
860                                    let respawn_amount = respawn_amounts_by_player
861                                        .get(player_id)
862                                        .copied()
863                                        .unwrap_or(0.0);
864                                    (player.boost_amount.unwrap_or(previous_boost_amount)
865                                        - previous_sample_boost_amount
866                                        - respawn_amount)
867                                        .max(0.0)
868                                })
869                                .unwrap_or(0.0)
870                        } else {
871                            0.0
872                        };
873                    let pre_applied_pad_size = (pre_applied_collected_amount > 0.0)
874                        .then(|| {
875                            self.guess_pad_size_from_position(
876                                &event.pad_id,
877                                player.position().unwrap_or(glam::Vec3::ZERO),
878                            )
879                        })
880                        .flatten();
881                    self.apply_pickup_collected_amount(
882                        player_id,
883                        player.is_team_0,
884                        pre_applied_collected_amount,
885                        pre_applied_pad_size,
886                    );
887                    let pending_pickup = PendingBoostPickup {
888                        player_id: player_id.clone(),
889                        is_team_0: player.is_team_0,
890                        previous_boost_amount,
891                        pre_applied_collected_amount,
892                        pre_applied_pad_size,
893                        player_position: player.position().unwrap_or(glam::Vec3::ZERO),
894                    };
895
896                    let pad_size = self
897                        .known_pad_sizes
898                        .get(&event.pad_id)
899                        .copied()
900                        .or_else(|| {
901                            let mut size = self.guess_pad_size_from_position(
902                                &event.pad_id,
903                                player.position().unwrap_or(glam::Vec3::ZERO),
904                            )?;
905                            // Sanity check: if the observed boost gain clearly
906                            // exceeds what a small pad can provide, the pad must
907                            // be big.  Use a margin to avoid float imprecision.
908                            if size == BoostPadSize::Small
909                                && pre_applied_collected_amount > SMALL_PAD_AMOUNT_RAW * 1.5
910                            {
911                                size = BoostPadSize::Big;
912                            }
913                            self.known_pad_sizes.insert(event.pad_id.clone(), size);
914                            Some(size)
915                        });
916                    if let Some(pad_size) = pad_size {
917                        self.resolve_pickup(&event.pad_id, pending_pickup, pad_size);
918                    }
919                }
920                BoostPadEventKind::Available => {
921                    if let Some(pad_size) = self.known_pad_sizes.get(&event.pad_id).copied() {
922                        let Some(last_pickup_time) = self.last_pickup_times.get(&event.pad_id)
923                        else {
924                            continue;
925                        };
926                        if event.time - *last_pickup_time < Self::pad_respawn_time_seconds(pad_size)
927                        {
928                            continue;
929                        }
930                    }
931                    self.unavailable_pads.remove(&event.pad_id);
932                }
933            }
934        }
935
936        let mut team_zero_used = self.team_zero_stats.amount_used;
937        let mut team_one_used = self.team_one_stats.amount_used;
938        for player in &players.players {
939            let Some(boost_amount) = player.boost_amount else {
940                continue;
941            };
942            let stats = self
943                .player_stats
944                .entry(player.player_id.clone())
945                .or_default();
946            let previous_amount_used = stats.amount_used;
947            let amount_used_raw = (stats.amount_obtained() - boost_amount).max(0.0);
948            let amount_used = amount_used_raw.max(stats.amount_used);
949            if track_boost_levels {
950                let split_amount = stats.amount_used_by_vertical_band();
951                let amount_used_delta = (amount_used - split_amount).max(0.0);
952                if amount_used_delta > 0.0 {
953                    let speed = player.speed();
954                    let previous_speed = self
955                        .previous_player_speeds
956                        .get(&player.player_id)
957                        .copied()
958                        .or(speed);
959                    let previous_speed = if boost_levels_resumed_this_sample {
960                        speed
961                    } else {
962                        previous_speed
963                    };
964                    let used_while_supersonic = player.boost_active
965                        && speed.unwrap_or(0.0) >= SUPERSONIC_SPEED_THRESHOLD
966                        && previous_speed.unwrap_or(0.0) >= SUPERSONIC_SPEED_THRESHOLD;
967                    let team_stats = if player.is_team_0 {
968                        &mut self.team_zero_stats
969                    } else {
970                        &mut self.team_one_stats
971                    };
972                    if vertical_state.is_grounded(&player.player_id) {
973                        stats.amount_used_while_grounded += amount_used_delta;
974                        team_stats.amount_used_while_grounded += amount_used_delta;
975                    } else {
976                        stats.amount_used_while_airborne += amount_used_delta;
977                        team_stats.amount_used_while_airborne += amount_used_delta;
978                    }
979                    if used_while_supersonic {
980                        stats.amount_used_while_supersonic += amount_used_delta;
981                        team_stats.amount_used_while_supersonic += amount_used_delta;
982                    }
983                }
984            }
985            stats.amount_used = amount_used;
986            let amount_used_delta = amount_used - previous_amount_used;
987            if amount_used_delta <= 0.0 {
988                continue;
989            }
990            if player.is_team_0 {
991                team_zero_used += amount_used_delta;
992            } else {
993                team_one_used += amount_used_delta;
994            }
995        }
996        self.team_zero_stats.amount_used = team_zero_used;
997        self.team_one_stats.amount_used = team_one_used;
998        for (player_id, boost_amount) in current_boost_amounts {
999            self.previous_boost_amounts.insert(player_id, boost_amount);
1000        }
1001        for player in &players.players {
1002            if let Some(speed) = player.speed() {
1003                self.previous_player_speeds
1004                    .insert(player.player_id.clone(), speed);
1005            }
1006        }
1007        self.warn_for_sample_boost_invariants(frame, players);
1008        self.kickoff_phase_active_last_frame = kickoff_phase_active;
1009        self.previous_boost_levels_live = Some(boost_levels_live);
1010
1011        Ok(())
1012    }
1013}