1use std::collections::{HashMap, HashSet};
2
3use super::*;
4use crate::stats::calculators::*;
5use crate::{PlayerId, SubtrActorResult};
6
7#[derive(Debug, Clone, Default)]
8pub struct StatsProjectionState {
9 pub core: CoreStatsAccumulator,
10 pub backboard: BackboardStatsAccumulator,
11 pub ceiling_shot: CeilingShotStatsAccumulator,
12 pub wall_aerial: WallAerialStatsAccumulator,
13 pub wall_aerial_shot: WallAerialShotStatsAccumulator,
14 pub double_tap: DoubleTapStatsAccumulator,
15 pub one_timer: OneTimerStatsAccumulator,
16 pub pass: PassStatsAccumulator,
17 pub fifty_fifty: FiftyFiftyStatsAccumulator,
18 pub kickoff: KickoffStatsAccumulator,
19 pub possession: PossessionStatsAccumulator,
20 pub ball_half: BallHalfStatsAccumulator,
21 pub territorial_pressure: TerritorialPressureStatsAccumulator,
22 pub rotation: RotationStatsAccumulator,
23 pub rush: RushStatsAccumulator,
24 pub touch: TouchStatsAccumulator,
25 pub whiff: WhiffStatsAccumulator,
26 pub wavedash: WavedashStatsAccumulator,
27 pub speed_flip: SpeedFlipStatsAccumulator,
28 pub half_flip: HalfFlipStatsAccumulator,
29 pub flick: FlickStatsAccumulator,
30 pub musty_flick: MustyFlickStatsAccumulator,
31 pub dodge_reset: DodgeResetStatsAccumulator,
32 pub ball_carry: BallCarryStatsAccumulator,
33 pub boost: BoostStatsAccumulator,
34 pub bump: BumpStatsAccumulator,
35 pub half_volley: HalfVolleyStatsAccumulator,
36 pub movement: MovementStatsAccumulator,
37 pub positioning: PositioningStatsAccumulator,
38 pub powerslide: PowerslideStatsAccumulator,
39 pub demo: DemoStatsAccumulator,
40 pub center: CenterStatsAccumulator,
41 pub controlled_play: ControlledPlayStatsAccumulator,
42}
43
44#[derive(Debug, Clone, Default)]
45struct PowerslideProjectionState {
46 active_players: HashMap<PlayerId, bool>,
47 player_teams: HashMap<PlayerId, bool>,
48}
49
50impl PowerslideProjectionState {
51 fn apply_frame(
52 &mut self,
53 stats: &mut PowerslideStatsAccumulator,
54 frame: &FrameInfo,
55 events: &[PowerslideEvent],
56 counts_toward_motion: bool,
57 ) {
58 let mut started_this_frame = HashSet::new();
59 for event in events {
60 self.player_teams
61 .insert(event.player.clone(), event.is_team_0);
62 if event.active {
63 stats.apply_sample(
64 &event.player,
65 event.is_team_0,
66 true,
67 false,
68 frame.dt,
69 counts_toward_motion,
70 );
71 self.active_players.insert(event.player.clone(), true);
72 started_this_frame.insert(event.player.clone());
73 } else {
74 self.active_players.insert(event.player.clone(), false);
75 }
76 }
77
78 let active_players = self
79 .active_players
80 .iter()
81 .filter(|(player_id, active)| **active && !started_this_frame.contains(*player_id))
82 .map(|(player_id, _)| player_id.clone())
83 .collect::<Vec<_>>();
84 for player_id in active_players {
85 let Some(is_team_0) = self.player_teams.get(&player_id).copied() else {
86 continue;
87 };
88 stats.apply_sample(
89 &player_id,
90 is_team_0,
91 true,
92 true,
93 frame.dt,
94 counts_toward_motion,
95 );
96 }
97 }
98}
99
100#[derive(Debug, Clone, Default)]
101pub struct StatsProjectionNode {
102 state: StatsProjectionState,
103 cursors: StatsProjectionCursors,
104 powerslide: PowerslideProjectionState,
105 boost_current_amount_consistency: BoostCurrentAmountConsistencyTracker,
106 last_powerslide_sample_frame: Option<usize>,
107 territorial_pressure_tracked_time: f32,
108 previous_live_play: Option<bool>,
109}
110
111#[derive(Debug, Clone, Default)]
112struct StatsProjectionCursors {
113 core_player: usize,
114 core_player_goal_context: usize,
115 backboard: usize,
116 ceiling_shot: usize,
117 wall_aerial: usize,
118 wall_aerial_shot: usize,
119 double_tap: usize,
120 one_timer: usize,
121 pass: usize,
122 fifty_fifty: usize,
123 kickoff: usize,
124 possession: usize,
125 ball_half: usize,
126 rush: usize,
127 touch: usize,
128 whiff: usize,
129 wavedash: usize,
130 speed_flip: usize,
131 half_flip: usize,
132 flick: usize,
133 musty_flick: usize,
134 dodge_reset: usize,
135 dodge_reset_flip_reset_outcome: usize,
136 ball_carry: usize,
137 bump: usize,
138 half_volley: usize,
139 movement: usize,
140 powerslide: usize,
141 demo_timeline: usize,
142 center: usize,
143 controlled_play: usize,
144}
145
146impl StatsProjectionNode {
147 pub fn new() -> Self {
148 Self::default()
149 }
150
151 fn begin_sample(&mut self, frame: &FrameInfo, live_play: bool) {
152 self.state.backboard.begin_sample(frame);
153 self.state.center.begin_sample(frame);
154 self.state.double_tap.begin_sample(frame);
155 self.state.half_volley.begin_sample(frame);
156 self.state.one_timer.begin_sample(frame);
157 self.state.pass.begin_sample(frame);
158 self.state.wall_aerial.begin_sample(frame);
159 self.state.wall_aerial_shot.begin_sample(frame);
160
161 if !live_play {
162 self.state.center.clear_current_last();
163 self.state.one_timer.clear_current_last();
164 self.state.pass.clear_current_last();
165 self.state.half_volley.reset_current_last_event_marker();
166 self.state.touch.set_current_last_touch_player(None);
167 self.state.wall_aerial.reset_current_last_event_marker();
168 self.state
169 .wall_aerial_shot
170 .reset_current_last_event_marker();
171 }
172
173 if live_play && self.previous_live_play == Some(false) {
174 self.state.ceiling_shot.reset_current_last_event_marker();
175 self.state.flick.reset_current_last_event_marker();
176 self.state.half_flip.reset_current_last_event_marker();
177 self.state.musty_flick.reset_current_last_event_marker();
178 self.state.wavedash.reset_current_last_event_marker();
179 self.state.whiff.reset_current_last_event_marker();
180 }
181
182 if live_play {
183 self.state.ceiling_shot.begin_sample(frame);
184 self.state.flick.begin_sample(frame);
185 self.state.half_flip.begin_sample(frame);
186 self.state.musty_flick.begin_sample(frame);
187 self.state.touch.begin_sample(frame);
188 self.state.wavedash.begin_sample(frame);
189 self.state.whiff.begin_sample(frame);
190 }
191 }
192
193 fn finish_sample(&mut self) {
194 self.state.center.finish_sample();
195 self.state.double_tap.finish_sample();
196 self.state.one_timer.finish_sample();
197 self.state.pass.finish_sample();
198 self.state.half_volley.restore_current_last_event_marker();
199 self.state.touch.restore_current_last_touch_marker();
200 self.state.wall_aerial.restore_current_last_event_marker();
201 self.state
202 .wall_aerial_shot
203 .restore_current_last_event_marker();
204 self.state.whiff.restore_current_last_event_marker();
205 }
206
207 fn events_since<'a, E>(cursor: &mut usize, events: &'a [E]) -> &'a [E] {
208 let start = (*cursor).min(events.len());
209 *cursor = events.len();
210 &events[start..]
211 }
212
213 fn check_boost_current_amount_consistency(
214 &mut self,
215 frame: &FrameInfo,
216 players: &PlayerFrameState,
217 ) {
218 for player in &players.players {
219 if player.boost_active {
220 continue;
221 }
222 let Some(observed_byte) = player
223 .last_boost_amount
224 .map(|amount| amount.round().clamp(0.0, BOOST_MAX_AMOUNT) as u8)
225 else {
226 continue;
227 };
228 let stats = self.state.boost.player_stats_for(&player.player_id);
229 self.boost_current_amount_consistency.observe(
230 frame.frame_number,
231 frame.time,
232 &player.player_id,
233 &stats,
234 observed_byte,
235 );
236 }
237 }
238
239 fn warn_for_unresolved_boost_current_amount_drift(&self) {
240 for warning in self.boost_current_amount_consistency.unresolved_warnings() {
241 log::warn!(
242 "Boost invariant violation for player {:?} at frame {} (t={:.3}): {}",
243 warning.player_id,
244 warning.frame,
245 warning.time,
246 warning.message(),
247 );
248 }
249 }
250
251 fn project_frame(&mut self, ctx: &AnalysisStateContext<'_>) -> SubtrActorResult<()> {
252 let frame = ctx.get::<FrameInfo>()?;
253 let players = ctx.get::<PlayerFrameState>()?;
254 let live_play_state = ctx.get::<LivePlayState>()?;
255 let live_play = live_play_state.is_live_play;
256 let counts_toward_powerslide_motion = matches!(
257 live_play_state.gameplay_phase,
258 GameplayPhase::ActivePlay | GameplayPhase::KickoffWaitingForTouch
259 );
260 let gameplay = ctx.get::<GameplayState>()?;
261 let speed_flip_stats_advance = live_play || gameplay.ball_has_been_hit == Some(false);
262 let should_sample_powerslide =
263 self.last_powerslide_sample_frame != Some(frame.frame_number);
264 self.begin_sample(frame, live_play);
265
266 let match_stats = ctx.get::<MatchStatsCalculator>()?;
267 for event in Self::events_since(
268 &mut self.cursors.core_player,
269 match_stats.core_player_events(),
270 ) {
271 self.state.core.apply_scoreboard_event(event);
272 }
273 for event in Self::events_since(
274 &mut self.cursors.core_player_goal_context,
275 match_stats.core_player_goal_context_events(),
276 ) {
277 self.state.core.apply_goal_context_event(event);
278 }
279
280 let backboard = ctx.get::<BackboardCalculator>()?;
281 self.state.backboard.apply_events(
282 frame,
283 Self::events_since(&mut self.cursors.backboard, backboard.events()),
284 );
285
286 let ceiling_shot = ctx.get::<CeilingShotCalculator>()?;
287 if live_play {
288 for event in Self::events_since(&mut self.cursors.ceiling_shot, ceiling_shot.events()) {
289 self.state.ceiling_shot.apply_event(event, frame);
290 }
291 }
292 let wall_aerial = ctx.get::<WallAerialCalculator>()?;
293 if live_play {
294 for event in Self::events_since(&mut self.cursors.wall_aerial, wall_aerial.events()) {
295 self.state.wall_aerial.apply_event(event, frame);
296 }
297 }
298 let wall_aerial_shot = ctx.get::<WallAerialShotCalculator>()?;
299 if live_play {
300 for event in Self::events_since(
301 &mut self.cursors.wall_aerial_shot,
302 wall_aerial_shot.events(),
303 ) {
304 self.state.wall_aerial_shot.apply_event(event, frame);
305 }
306 }
307 let double_tap = ctx.get::<DoubleTapCalculator>()?;
308 for event in Self::events_since(&mut self.cursors.double_tap, double_tap.events()) {
309 self.state.double_tap.apply_event(frame, event);
310 }
311 let one_timer = ctx.get::<OneTimerCalculator>()?;
312 if live_play {
313 for event in Self::events_since(&mut self.cursors.one_timer, one_timer.events()) {
314 self.state.one_timer.apply_event(frame, event);
315 }
316 }
317 let pass = ctx.get::<PassCalculator>()?;
318 if live_play {
319 for event in Self::events_since(&mut self.cursors.pass, pass.events()) {
320 self.state.pass.apply_event(frame, event);
321 }
322 }
323 let fifty_fifty = ctx.get::<FiftyFiftyCalculator>()?;
324 for event in Self::events_since(&mut self.cursors.fifty_fifty, fifty_fifty.events()) {
325 self.state.fifty_fifty.apply_event(event);
326 }
327 let kickoff = ctx.get::<KickoffCalculator>()?;
328 for event in Self::events_since(&mut self.cursors.kickoff, kickoff.events()) {
329 self.state.kickoff.apply_event(event);
330 }
331 let possession = ctx.get::<PossessionCalculator>()?;
332 let projected_possession_events = possession.projected_events();
333 self.state.possession = PossessionStatsAccumulator::default();
334 for event in projected_possession_events.iter() {
335 self.state.possession.apply_event(event);
336 }
337 self.cursors.possession = possession.events().len();
338 let ball_half = ctx.get::<BallHalfCalculator>()?;
339 let projected_ball_half_events = ball_half.projected_events();
340 self.state.ball_half = BallHalfStatsAccumulator::default();
341 for event in projected_ball_half_events.iter() {
342 self.state.ball_half.apply_event(event);
343 }
344 self.cursors.ball_half = ball_half.events().len();
345 let territorial_pressure = ctx.get::<TerritorialPressureCalculator>()?;
346 if live_play {
347 self.territorial_pressure_tracked_time += frame.dt;
348 }
349 let projected_territorial_pressure_events = territorial_pressure.projected_events();
350 self.state.territorial_pressure = TerritorialPressureStatsAccumulator::default();
351 self.state
352 .territorial_pressure
353 .set_tracked_time(self.territorial_pressure_tracked_time);
354 for event in projected_territorial_pressure_events.iter() {
355 self.state.territorial_pressure.apply_event(event);
356 }
357 let rotation = ctx.get::<RotationCalculator>()?;
358 self.state.rotation = RotationStatsAccumulator::with_first_man_stint_end_grace_seconds(
359 rotation.config().first_man_stint_end_grace_seconds,
360 );
361 for event in rotation.role_events().iter() {
362 self.state.rotation.apply_role_event(event);
363 }
364 for event in rotation.first_man_change_events() {
365 self.state.rotation.apply_first_man_change_event(event);
366 }
367 let rush = ctx.get::<RushCalculator>()?;
368 for event in Self::events_since(&mut self.cursors.rush, rush.events()) {
369 self.state.rush.apply_event(event);
370 }
371 let touch = ctx.get::<TouchCalculator>()?;
372 if live_play || self.cursors.touch != touch.events().len() {
373 self.state.touch = TouchStatsAccumulator::default();
374 for event in touch.events() {
375 self.state.touch.apply_touch_event(event, frame);
376 }
377 self.cursors.touch = touch.events().len();
378 }
379 let whiff = ctx.get::<WhiffCalculator>()?;
380 if live_play {
381 for event in Self::events_since(&mut self.cursors.whiff, whiff.events()) {
382 self.state.whiff.apply_event(event, frame);
383 }
384 }
385 let wavedash = ctx.get::<WavedashCalculator>()?;
386 if live_play {
387 for event in Self::events_since(&mut self.cursors.wavedash, wavedash.events()) {
388 self.state.wavedash.apply_event(event);
389 }
390 }
391 let speed_flip = ctx.get::<SpeedFlipCalculator>()?;
392 if speed_flip_stats_advance {
393 self.state.speed_flip.begin_sample(frame);
394 for event in Self::events_since(&mut self.cursors.speed_flip, speed_flip.events()) {
395 self.state.speed_flip.apply_event(event);
396 }
397 }
398 let half_flip = ctx.get::<HalfFlipCalculator>()?;
399 if live_play {
400 for event in Self::events_since(&mut self.cursors.half_flip, half_flip.events()) {
401 self.state.half_flip.apply_event(event);
402 }
403 }
404 let flick = ctx.get::<FlickCalculator>()?;
405 if live_play {
406 for event in Self::events_since(&mut self.cursors.flick, flick.events()) {
407 self.state.flick.apply_event(event, frame);
408 }
409 }
410 let musty_flick = ctx.get::<MustyFlickCalculator>()?;
411 if live_play {
412 for event in Self::events_since(&mut self.cursors.musty_flick, musty_flick.events()) {
413 self.state.musty_flick.apply_event(event, frame);
414 }
415 }
416 let dodge_reset = ctx.get::<DodgeResetCalculator>()?;
417 for event in Self::events_since(&mut self.cursors.dodge_reset, dodge_reset.events()) {
418 self.state.dodge_reset.apply_event(event);
419 }
420 for event in Self::events_since(
421 &mut self.cursors.dodge_reset_flip_reset_outcome,
422 dodge_reset.flip_reset_outcome_events(),
423 ) {
424 self.state.dodge_reset.apply_flip_reset_outcome_event(event);
425 }
426 let ball_carry = ctx.get::<BallCarryCalculator>()?;
427 for event in Self::events_since(&mut self.cursors.ball_carry, ball_carry.carry_events()) {
428 self.state.ball_carry.apply_event(event);
429 }
430 let boost = ctx.get::<BoostCalculator>()?;
431 self.state.boost = boost.boost_stats().clone();
434 if live_play {
435 self.check_boost_current_amount_consistency(frame, players);
436 }
437 let bump = ctx.get::<BumpCalculator>()?;
438 for event in Self::events_since(&mut self.cursors.bump, bump.events()) {
439 self.state.bump.apply_event(event);
440 }
441 let half_volley = ctx.get::<HalfVolleyCalculator>()?;
442 if live_play {
443 for event in Self::events_since(&mut self.cursors.half_volley, half_volley.events()) {
444 self.state.half_volley.apply_event(event, frame);
445 }
446 }
447 let movement = ctx.get::<MovementCalculator>()?;
448 let projected_movement_events = movement.projected_events();
449 self.state.movement = MovementStatsAccumulator::default();
450 for event in projected_movement_events.iter() {
451 self.state.movement.apply_event(event);
452 }
453 self.cursors.movement = movement.events().len();
454 let positioning = ctx.get::<PositioningCalculator>()?;
455 self.state.positioning = PositioningStatsAccumulator::default();
456 for event in positioning.activity_events().iter() {
457 self.state.positioning.apply_activity_event(event);
458 }
459 for event in positioning.field_third_events().iter() {
460 self.state.positioning.apply_field_third_event(event);
461 }
462 for event in positioning.field_half_events().iter() {
463 self.state.positioning.apply_field_half_event(event);
464 }
465 for event in positioning.ball_depth_events().iter() {
466 self.state.positioning.apply_ball_depth_event(event);
467 }
468 for event in positioning.depth_role_events().iter() {
469 self.state.positioning.apply_depth_role_event(event);
470 }
471 for event in positioning.ball_proximity_events().iter() {
472 self.state.positioning.apply_ball_proximity_event(event);
473 }
474 for (player, signal) in positioning.signals() {
475 self.state.positioning.apply_signal(player, signal);
476 }
477 let powerslide = ctx.get::<PowerslideCalculator>()?;
478 let powerslide_events =
479 Self::events_since(&mut self.cursors.powerslide, powerslide.events());
480 if should_sample_powerslide {
481 self.powerslide.apply_frame(
482 &mut self.state.powerslide,
483 frame,
484 powerslide_events,
485 counts_toward_powerslide_motion,
486 );
487 self.last_powerslide_sample_frame = Some(frame.frame_number);
488 }
489 let demo = ctx.get::<DemoCalculator>()?;
490 for event in Self::events_since(&mut self.cursors.demo_timeline, demo.timeline()) {
491 self.state.demo.apply_timeline_event(event);
492 }
493 let center = ctx.get::<CenterCalculator>()?;
494 for event in Self::events_since(&mut self.cursors.center, center.events()) {
495 self.state.center.apply_event(frame, event);
496 }
497 let controlled_play = ctx.get::<ControlledPlayCalculator>()?;
498 for event in Self::events_since(&mut self.cursors.controlled_play, controlled_play.events())
499 {
500 self.state.controlled_play.apply_event(event);
501 }
502
503 self.finish_sample();
504 self.previous_live_play = Some(live_play);
505 Ok(())
506 }
507}
508
509impl AnalysisNode for StatsProjectionNode {
510 type State = StatsProjectionState;
511
512 fn name(&self) -> &'static str {
513 "stats_projection"
514 }
515
516 fn dependencies(&self) -> NodeDependencies {
517 vec![
518 frame_info_dependency(),
519 gameplay_state_dependency(),
520 live_play_dependency(),
521 player_frame_state_dependency(),
522 match_stats_dependency(),
523 backboard_dependency(),
524 ceiling_shot_dependency(),
525 wall_aerial_dependency(),
526 wall_aerial_shot_dependency(),
527 double_tap_dependency(),
528 one_timer_dependency(),
529 pass_dependency(),
530 fifty_fifty_dependency(),
531 kickoff_dependency(),
532 possession_dependency(),
533 ball_half_dependency(),
534 territorial_pressure_dependency(),
535 rotation_dependency(),
536 rush_dependency(),
537 touch_dependency(),
538 whiff_dependency(),
539 wavedash_dependency(),
540 speed_flip_dependency(),
541 half_flip_dependency(),
542 flick_dependency(),
543 musty_flick_dependency(),
544 dodge_reset_dependency(),
545 ball_carry_dependency(),
546 boost_dependency(),
547 bump_dependency(),
548 half_volley_dependency(),
549 movement_dependency(),
550 positioning_dependency(),
551 powerslide_dependency(),
552 demo_dependency(),
553 center_dependency(),
554 controlled_play_dependency(),
555 ]
556 }
557
558 fn evaluate(&mut self, ctx: &AnalysisStateContext<'_>) -> SubtrActorResult<()> {
559 self.project_frame(ctx)
560 }
561
562 fn finish(&mut self, ctx: &AnalysisStateContext<'_>) -> SubtrActorResult<()> {
563 self.project_frame(ctx)?;
564 self.warn_for_unresolved_boost_current_amount_drift();
565 Ok(())
566 }
567
568 fn state(&self) -> &Self::State {
569 &self.state
570 }
571}
572
573pub(crate) fn boxed_default() -> Box<dyn AnalysisNodeDyn> {
574 Box::new(StatsProjectionNode::new())
575}