1use std::collections::{HashMap, HashSet};
2
3use super::*;
4use crate::stats::calculators::*;
5use crate::{PlayerId, SubtrActorResult};
6
7#[derive(Debug, Clone, Default)]
9pub struct StatsProjectionState {
10 pub core: CoreStatsAccumulator,
11 pub backboard: BackboardStatsAccumulator,
12 pub ceiling_shot: CeilingShotStatsAccumulator,
13 pub wall_aerial: WallAerialStatsAccumulator,
14 pub wall_aerial_shot: WallAerialShotStatsAccumulator,
15 pub double_tap: DoubleTapStatsAccumulator,
16 pub one_timer: OneTimerStatsAccumulator,
17 pub pass: PassStatsAccumulator,
18 pub fifty_fifty: FiftyFiftyStatsAccumulator,
19 pub kickoff: KickoffStatsAccumulator,
20 pub possession: PossessionStatsAccumulator,
21 pub ball_half: BallHalfStatsAccumulator,
22 pub ball_third: BallThirdStatsAccumulator,
23 pub territorial_pressure: TerritorialPressureStatsAccumulator,
24 pub rotation: RotationStatsAccumulator,
25 pub rush: RushStatsAccumulator,
26 pub touch: TouchStatsAccumulator,
27 pub whiff: WhiffStatsAccumulator,
28 pub wavedash: WavedashStatsAccumulator,
29 pub speed_flip: SpeedFlipStatsAccumulator,
30 pub half_flip: HalfFlipStatsAccumulator,
31 pub flick: FlickStatsAccumulator,
32 pub dodge_reset: DodgeResetStatsAccumulator,
33 pub flip_reset: FlipResetStatsAccumulator,
34 pub ball_carry: BallCarryStatsAccumulator,
35 pub boost: BoostStatsAccumulator,
36 pub bump: BumpStatsAccumulator,
37 pub half_volley: HalfVolleyStatsAccumulator,
38 pub movement: MovementStatsAccumulator,
39 pub positioning: PositioningStatsAccumulator,
40 pub powerslide: PowerslideStatsAccumulator,
41 pub demo: DemoStatsAccumulator,
42 pub center: CenterStatsAccumulator,
43 pub controlled_play: ControlledPlayStatsAccumulator,
44}
45
46#[derive(Debug, Clone, Default)]
47struct PowerslideProjectionState {
48 active_players: HashMap<PlayerId, bool>,
49 player_teams: HashMap<PlayerId, bool>,
50}
51
52impl PowerslideProjectionState {
53 fn apply_frame(
54 &mut self,
55 stats: &mut PowerslideStatsAccumulator,
56 frame: &FrameInfo,
57 events: &[PowerslideEvent],
58 counts_toward_motion: bool,
59 ) {
60 let mut started_this_frame = HashSet::new();
61 for event in events {
62 self.player_teams
63 .insert(event.player.clone(), event.is_team_0);
64 if event.active {
65 stats.apply_sample(
66 &event.player,
67 event.is_team_0,
68 true,
69 false,
70 frame.dt,
71 counts_toward_motion,
72 );
73 self.active_players.insert(event.player.clone(), true);
74 started_this_frame.insert(event.player.clone());
75 } else {
76 self.active_players.insert(event.player.clone(), false);
77 }
78 }
79
80 let active_players = self
81 .active_players
82 .iter()
83 .filter(|(player_id, active)| **active && !started_this_frame.contains(*player_id))
84 .map(|(player_id, _)| player_id.clone())
85 .collect::<Vec<_>>();
86 for player_id in active_players {
87 let Some(is_team_0) = self.player_teams.get(&player_id).copied() else {
88 continue;
89 };
90 stats.apply_sample(
91 &player_id,
92 is_team_0,
93 true,
94 true,
95 frame.dt,
96 counts_toward_motion,
97 );
98 }
99 }
100}
101
102#[derive(Debug, Clone, Default)]
118struct IncrementalMovementProjection {
119 base: MovementStatsAccumulator,
120 committed_cursor: usize,
121 #[cfg(test)]
126 committed_folds: usize,
127}
128
129impl IncrementalMovementProjection {
130 fn project(
133 &mut self,
134 committed: &[MovementEvent],
135 pending: &[MovementEvent],
136 ) -> MovementStatsAccumulator {
137 for event in committed.get(self.committed_cursor..).unwrap_or(&[]) {
138 self.base.apply_event(event);
139 #[cfg(test)]
140 {
141 self.committed_folds += 1;
142 }
143 }
144 self.committed_cursor = committed.len();
145
146 let mut snapshot = self.base.clone();
147 for event in pending {
148 snapshot.apply_event(event);
149 }
150 snapshot
151 }
152}
153
154#[derive(Debug, Clone, Default)]
156pub struct StatsProjectionNode {
157 state: StatsProjectionState,
158 cursors: StatsProjectionCursors,
159 movement_projection: IncrementalMovementProjection,
160 powerslide: PowerslideProjectionState,
161 boost_current_amount_consistency: BoostCurrentAmountConsistencyTracker,
162 last_powerslide_sample_frame: Option<usize>,
163 last_possession_sample_frame: Option<usize>,
164 possession_frame_buffer: Vec<PossessionFrameSample>,
169 territorial_pressure_tracked_time: f32,
170 previous_live_play: Option<bool>,
171}
172
173#[derive(Debug, Clone, Default)]
176struct PossessionFrameSample {
177 frame: usize,
178 dt: f32,
179 field_third: Option<String>,
180 field_half: Option<String>,
181}
182
183#[derive(Debug, Clone, Default)]
184struct StatsProjectionCursors {
185 core_player: usize,
186 core_player_goal_context: usize,
187 backboard: usize,
188 ceiling_shot: usize,
189 wall_aerial: usize,
190 wall_aerial_shot: usize,
191 double_tap: usize,
192 one_timer: usize,
193 pass: usize,
194 fifty_fifty: usize,
195 kickoff: usize,
196 ball_half: usize,
197 ball_third: usize,
198 rush: usize,
199 touch: usize,
200 whiff: usize,
201 wavedash: usize,
202 speed_flip: usize,
203 half_flip: usize,
204 flick: usize,
205 dodge_reset: usize,
206 flip_reset: usize,
207 dodge_reset_flip_reset_outcome: usize,
208 ball_carry: usize,
209 air_dribble: usize,
210 bump: usize,
211 half_volley: usize,
212 powerslide: usize,
213 demo_timeline: usize,
214 center: usize,
215 controlled_play: usize,
216}
217
218impl StatsProjectionNode {
219 pub fn new() -> Self {
220 Self::default()
221 }
222
223 fn drain_possession_buffer_through(&mut self, end_frame: usize, label: &str) {
226 let mut drained = 0;
227 while drained < self.possession_frame_buffer.len() {
228 if self.possession_frame_buffer[drained].frame > end_frame {
229 break;
230 }
231 let (dt, field_third, field_half) = {
232 let sample = &self.possession_frame_buffer[drained];
233 (
234 sample.dt,
235 sample.field_third.clone(),
236 sample.field_half.clone(),
237 )
238 };
239 self.state.possession.apply_frame(
240 label,
241 field_third.as_deref(),
242 field_half.as_deref(),
243 dt,
244 );
245 drained += 1;
246 }
247 self.possession_frame_buffer.drain(0..drained);
248 }
249
250 fn flush_possession_buffer_as_neutral(&mut self) {
253 if self.possession_frame_buffer.is_empty() {
254 return;
255 }
256 self.drain_possession_buffer_through(usize::MAX, "neutral");
257 }
258
259 fn begin_sample(&mut self, frame: &FrameInfo, live_play: bool) {
260 self.state.backboard.begin_sample(frame);
261 self.state.center.begin_sample(frame);
262 self.state.double_tap.begin_sample(frame);
263 self.state.half_volley.begin_sample(frame);
264 self.state.one_timer.begin_sample(frame);
265 self.state.pass.begin_sample(frame);
266 self.state.wall_aerial.begin_sample(frame);
267 self.state.wall_aerial_shot.begin_sample(frame);
268
269 if !live_play {
270 self.state.center.clear_current_last();
271 self.state.one_timer.clear_current_last();
272 self.state.pass.clear_current_last();
273 self.state.half_volley.reset_current_last_event_marker();
274 self.state.touch.set_current_last_touch_player(None);
275 self.state.wall_aerial.reset_current_last_event_marker();
276 self.state
277 .wall_aerial_shot
278 .reset_current_last_event_marker();
279 }
280
281 if live_play && self.previous_live_play == Some(false) {
282 self.state.ceiling_shot.reset_current_last_event_marker();
283 self.state.flick.reset_current_last_event_marker();
284 self.state.half_flip.reset_current_last_event_marker();
285 self.state.wavedash.reset_current_last_event_marker();
286 self.state.whiff.reset_current_last_event_marker();
287 }
288
289 if live_play {
290 self.state.ceiling_shot.begin_sample(frame);
291 self.state.flick.begin_sample(frame);
292 self.state.half_flip.begin_sample(frame);
293 self.state.touch.begin_sample(frame);
294 self.state.wavedash.begin_sample(frame);
295 self.state.whiff.begin_sample(frame);
296 }
297 }
298
299 fn finish_sample(&mut self) {
300 self.state.center.finish_sample();
301 self.state.double_tap.finish_sample();
302 self.state.one_timer.finish_sample();
303 self.state.pass.finish_sample();
304 self.state.half_volley.restore_current_last_event_marker();
305 self.state.touch.restore_current_last_touch_marker();
306 self.state.wall_aerial.restore_current_last_event_marker();
307 self.state
308 .wall_aerial_shot
309 .restore_current_last_event_marker();
310 self.state.whiff.restore_current_last_event_marker();
311 }
312
313 fn events_since<'a, E>(cursor: &mut usize, events: &'a [E]) -> &'a [E] {
314 let start = (*cursor).min(events.len());
315 *cursor = events.len();
316 &events[start..]
317 }
318
319 fn check_boost_current_amount_consistency(
320 &mut self,
321 frame: &FrameInfo,
322 players: &PlayerFrameState,
323 ) {
324 for player in &players.players {
325 if player.boost_active {
326 continue;
327 }
328 let Some(observed_byte) = player
329 .last_boost_amount
330 .map(|amount| amount.round().clamp(0.0, BOOST_MAX_AMOUNT) as u8)
331 else {
332 continue;
333 };
334 let stats = self.state.boost.player_stats_for(&player.player_id);
335 self.boost_current_amount_consistency.observe(
336 frame.frame_number,
337 frame.time,
338 &player.player_id,
339 &stats,
340 observed_byte,
341 );
342 }
343 }
344
345 fn warn_for_unresolved_boost_current_amount_drift(&self) {
346 for warning in self.boost_current_amount_consistency.unresolved_warnings() {
347 log::warn!(
348 "Boost invariant violation for player {:?} at frame {} (t={:.3}): {}",
349 warning.player_id,
350 warning.frame,
351 warning.time,
352 warning.message(),
353 );
354 }
355 }
356
357 fn project_frame(&mut self, ctx: &AnalysisStateContext<'_>) -> SubtrActorResult<()> {
358 let frame = ctx.get::<FrameInfo>()?;
359 let players = ctx.get::<PlayerFrameState>()?;
360 let live_play_state = ctx.get::<LivePlayState>()?;
361 let live_play = live_play_state.is_live_play;
362 let counts_toward_powerslide_motion = matches!(
363 live_play_state.gameplay_phase,
364 GameplayPhase::ActivePlay | GameplayPhase::KickoffWaitingForTouch
365 );
366 let gameplay = ctx.get::<GameplayState>()?;
367 let speed_flip_stats_advance = live_play || gameplay.ball_has_been_hit == Some(false);
368 let should_sample_powerslide =
369 self.last_powerslide_sample_frame != Some(frame.frame_number);
370 self.begin_sample(frame, live_play);
371
372 let match_stats = ctx.get::<MatchStatsCalculator>()?;
373 for event in Self::events_since(
374 &mut self.cursors.core_player,
375 match_stats.core_player_events(),
376 ) {
377 self.state.core.apply_scoreboard_event(event);
378 }
379 for event in Self::events_since(
380 &mut self.cursors.core_player_goal_context,
381 match_stats.core_player_goal_context_events(),
382 ) {
383 self.state.core.apply_goal_context_event(event);
384 }
385
386 let backboard = ctx.get::<BackboardCalculator>()?;
387 self.state.backboard.apply_events(
388 frame,
389 Self::events_since(&mut self.cursors.backboard, backboard.events()),
390 );
391
392 let ceiling_shot = ctx.get::<CeilingShotCalculator>()?;
393 if live_play {
394 for event in Self::events_since(&mut self.cursors.ceiling_shot, ceiling_shot.events()) {
395 self.state.ceiling_shot.apply_event(event, frame);
396 }
397 }
398 let wall_aerial = ctx.get::<WallAerialCalculator>()?;
399 if live_play {
400 for event in Self::events_since(&mut self.cursors.wall_aerial, wall_aerial.events()) {
401 self.state.wall_aerial.apply_event(event, frame);
402 }
403 }
404 let wall_aerial_shot = ctx.get::<WallAerialShotCalculator>()?;
405 if live_play {
406 for event in Self::events_since(
407 &mut self.cursors.wall_aerial_shot,
408 wall_aerial_shot.events(),
409 ) {
410 self.state.wall_aerial_shot.apply_event(event, frame);
411 }
412 }
413 let double_tap = ctx.get::<DoubleTapCalculator>()?;
414 for event in Self::events_since(&mut self.cursors.double_tap, double_tap.events()) {
415 self.state.double_tap.apply_event(frame, event);
416 }
417 let one_timer = ctx.get::<OneTimerCalculator>()?;
418 if live_play {
419 for event in Self::events_since(&mut self.cursors.one_timer, one_timer.events()) {
420 self.state.one_timer.apply_event(frame, event);
421 }
422 }
423 let pass = ctx.get::<PassCalculator>()?;
424 if live_play {
425 for event in Self::events_since(&mut self.cursors.pass, pass.events()) {
426 self.state.pass.apply_event(frame, event);
427 }
428 }
429 let fifty_fifty = ctx.get::<FiftyFiftyCalculator>()?;
430 for event in Self::events_since(&mut self.cursors.fifty_fifty, fifty_fifty.events()) {
431 self.state.fifty_fifty.apply_event(event);
432 }
433 let kickoff = ctx.get::<KickoffCalculator>()?;
434 for event in Self::events_since(&mut self.cursors.kickoff, kickoff.events()) {
435 self.state.kickoff.apply_event(event);
436 }
437 let possession = ctx.get::<PossessionCalculator>()?;
449 let ball_third = ctx.get::<BallThirdCalculator>()?;
450 let ball_half_calculator = ctx.get::<BallHalfCalculator>()?;
451 if live_play && self.last_possession_sample_frame != Some(frame.frame_number) {
452 let field_third = ball_third
453 .current_event()
454 .filter(|event| event.active)
455 .map(|event| event.field_third.clone());
456 let field_half = ball_half_calculator
457 .current_event()
458 .filter(|event| event.active)
459 .map(|event| event.field_half.clone());
460 self.possession_frame_buffer.push(PossessionFrameSample {
461 frame: frame.frame_number,
462 dt: frame.dt,
463 field_third,
464 field_half,
465 });
466 }
467 self.last_possession_sample_frame = Some(frame.frame_number);
468 for segment in possession.new_resolved() {
469 self.drain_possession_buffer_through(segment.end_frame, segment.label.as_label_value());
470 }
471 let ball_half = ctx.get::<BallHalfCalculator>()?;
472 let projected_ball_half_events = ball_half.projected_events();
473 self.state.ball_half = BallHalfStatsAccumulator::default();
474 for event in projected_ball_half_events.iter() {
475 self.state.ball_half.apply_event(event);
476 }
477 self.cursors.ball_half = ball_half.events().len();
478 let ball_third = ctx.get::<BallThirdCalculator>()?;
479 let projected_ball_third_events = ball_third.projected_events();
480 self.state.ball_third = BallThirdStatsAccumulator::default();
481 for event in projected_ball_third_events.iter() {
482 self.state.ball_third.apply_event(event);
483 }
484 self.cursors.ball_third = ball_third.events().len();
485 let territorial_pressure = ctx.get::<TerritorialPressureCalculator>()?;
486 if live_play {
487 self.territorial_pressure_tracked_time += frame.dt;
488 }
489 let projected_territorial_pressure_events = territorial_pressure.projected_events();
490 self.state.territorial_pressure = TerritorialPressureStatsAccumulator::default();
491 self.state
492 .territorial_pressure
493 .set_tracked_time(self.territorial_pressure_tracked_time);
494 for event in projected_territorial_pressure_events.iter() {
495 self.state.territorial_pressure.apply_event(event);
496 }
497 let rotation = ctx.get::<RotationCalculator>()?;
498 self.state.rotation = RotationStatsAccumulator::with_first_man_stint_end_grace_seconds(
499 rotation.config().first_man_stint_end_grace_seconds,
500 );
501 for event in rotation.role_events().iter() {
502 self.state.rotation.apply_role_event(event);
503 }
504 for event in rotation.first_man_change_events() {
505 self.state.rotation.apply_first_man_change_event(event);
506 }
507 let rush = ctx.get::<RushCalculator>()?;
508 for event in Self::events_since(&mut self.cursors.rush, rush.events()) {
509 self.state.rush.apply_event(event);
510 }
511 let touch = ctx.get::<TouchCalculator>()?;
512 if live_play || self.cursors.touch != touch.events().len() {
513 self.state.touch = TouchStatsAccumulator::default();
514 for event in touch.events() {
515 self.state.touch.apply_touch_event(event, frame);
516 }
517 self.cursors.touch = touch.events().len();
518 }
519 let whiff = ctx.get::<WhiffCalculator>()?;
520 if live_play {
521 for event in Self::events_since(&mut self.cursors.whiff, whiff.events()) {
522 self.state.whiff.apply_event(event, frame);
523 }
524 }
525 let wavedash = ctx.get::<WavedashCalculator>()?;
526 if live_play {
527 for event in Self::events_since(&mut self.cursors.wavedash, wavedash.events()) {
528 self.state.wavedash.apply_event(event);
529 }
530 }
531 let speed_flip = ctx.get::<SpeedFlipCalculator>()?;
532 if speed_flip_stats_advance {
533 self.state.speed_flip.begin_sample(frame);
534 for event in Self::events_since(&mut self.cursors.speed_flip, speed_flip.events()) {
535 self.state.speed_flip.apply_event(event);
536 }
537 }
538 let half_flip = ctx.get::<HalfFlipCalculator>()?;
539 if live_play {
540 for event in Self::events_since(&mut self.cursors.half_flip, half_flip.events()) {
541 self.state.half_flip.apply_event(event);
542 }
543 }
544 let flick = ctx.get::<FlickCalculator>()?;
545 if live_play {
546 for event in Self::events_since(&mut self.cursors.flick, flick.events()) {
547 self.state.flick.apply_event(event, frame);
548 }
549 }
550 let dodge_reset = ctx.get::<DodgeResetCalculator>()?;
551 for event in Self::events_since(&mut self.cursors.dodge_reset, dodge_reset.events()) {
552 self.state.dodge_reset.apply_event(event);
553 }
554 for event in Self::events_since(
555 &mut self.cursors.flip_reset,
556 dodge_reset.confirmed_flip_reset_events(),
557 ) {
558 self.state.flip_reset.apply_event(event);
559 }
560 for event in Self::events_since(
561 &mut self.cursors.dodge_reset_flip_reset_outcome,
562 dodge_reset.flip_reset_outcome_events(),
563 ) {
564 self.state.dodge_reset.apply_flip_reset_outcome_event(event);
565 }
566 let ball_carry = ctx.get::<BallCarryCalculator>()?;
567 for event in Self::events_since(&mut self.cursors.ball_carry, ball_carry.carry_events()) {
568 self.state.ball_carry.apply_event(event);
569 }
570 let air_dribble = ctx.get::<AirDribbleCalculator>()?;
571 for event in Self::events_since(&mut self.cursors.air_dribble, air_dribble.events()) {
572 self.state.ball_carry.apply_event(event);
573 }
574 let boost = ctx.get::<BoostCalculator>()?;
575 self.state.boost = boost.boost_stats().clone();
578 if live_play {
579 self.check_boost_current_amount_consistency(frame, players);
580 }
581 let bump = ctx.get::<BumpCalculator>()?;
582 for event in Self::events_since(&mut self.cursors.bump, bump.events()) {
583 self.state.bump.apply_event(event);
584 }
585 let half_volley = ctx.get::<HalfVolleyCalculator>()?;
586 if live_play {
587 for event in Self::events_since(&mut self.cursors.half_volley, half_volley.events()) {
588 self.state.half_volley.apply_event(event, frame);
589 }
590 }
591 let movement = ctx.get::<MovementCalculator>()?;
592 self.state.movement = self
593 .movement_projection
594 .project(movement.events(), &movement.pending_events());
595 let positioning = ctx.get::<PositioningCalculator>()?;
596 self.state.positioning = PositioningStatsAccumulator::default();
597 for event in positioning.activity_events().iter() {
598 self.state.positioning.apply_activity_event(event);
599 }
600 for event in positioning.field_third_events().iter() {
601 self.state.positioning.apply_field_third_event(event);
602 }
603 for event in positioning.field_half_events().iter() {
604 self.state.positioning.apply_field_half_event(event);
605 }
606 for event in positioning.ball_depth_events().iter() {
607 self.state.positioning.apply_ball_depth_event(event);
608 }
609 for event in positioning.depth_role_events().iter() {
610 self.state.positioning.apply_depth_role_event(event);
611 }
612 for event in positioning.ball_proximity_events().iter() {
613 self.state.positioning.apply_ball_proximity_event(event);
614 }
615 for event in positioning.shadow_defense_events().iter() {
616 self.state.positioning.apply_shadow_defense_event(event);
617 }
618 for (player, signal) in positioning.signals() {
619 self.state.positioning.apply_signal(player, signal);
620 }
621 let powerslide = ctx.get::<PowerslideCalculator>()?;
622 let powerslide_events =
623 Self::events_since(&mut self.cursors.powerslide, powerslide.events());
624 if should_sample_powerslide {
625 self.powerslide.apply_frame(
626 &mut self.state.powerslide,
627 frame,
628 powerslide_events,
629 counts_toward_powerslide_motion,
630 );
631 self.last_powerslide_sample_frame = Some(frame.frame_number);
632 }
633 let demo = ctx.get::<DemoCalculator>()?;
634 for event in Self::events_since(&mut self.cursors.demo_timeline, demo.events()) {
635 self.state.demo.apply_demolition_event(event);
636 }
637 let center = ctx.get::<CenterCalculator>()?;
638 for event in Self::events_since(&mut self.cursors.center, center.events()) {
639 self.state.center.apply_event(frame, event);
640 }
641 let controlled_play = ctx.get::<ControlledPlayCalculator>()?;
642 for event in Self::events_since(&mut self.cursors.controlled_play, controlled_play.events())
643 {
644 self.state.controlled_play.apply_event(event);
645 }
646
647 self.finish_sample();
648 self.previous_live_play = Some(live_play);
649 Ok(())
650 }
651}
652
653impl AnalysisNode for StatsProjectionNode {
654 type State = StatsProjectionState;
655
656 fn name(&self) -> &'static str {
657 "stats_projection"
658 }
659
660 fn dependencies(&self) -> NodeDependencies {
661 vec![
662 frame_info_dependency(),
663 gameplay_state_dependency(),
664 live_play_dependency(),
665 player_frame_state_dependency(),
666 match_stats_dependency(),
667 backboard_dependency(),
668 ceiling_shot_dependency(),
669 wall_aerial_dependency(),
670 wall_aerial_shot_dependency(),
671 double_tap_dependency(),
672 one_timer_dependency(),
673 pass_dependency(),
674 fifty_fifty_dependency(),
675 kickoff_dependency(),
676 possession_dependency(),
677 ball_half_dependency(),
678 ball_third_dependency(),
679 territorial_pressure_dependency(),
680 rotation_dependency(),
681 rush_dependency(),
682 touch_dependency(),
683 whiff_dependency(),
684 wavedash_dependency(),
685 speed_flip_dependency(),
686 half_flip_dependency(),
687 flick_dependency(),
688 dodge_reset_dependency(),
689 ball_carry_dependency(),
690 air_dribble_dependency(),
691 boost_dependency(),
692 bump_dependency(),
693 half_volley_dependency(),
694 movement_dependency(),
695 positioning_dependency(),
696 powerslide_dependency(),
697 demo_dependency(),
698 center_dependency(),
699 controlled_play_dependency(),
700 ]
701 }
702
703 fn evaluate(&mut self, ctx: &AnalysisStateContext<'_>) -> SubtrActorResult<()> {
704 self.project_frame(ctx)
705 }
706
707 fn finish(&mut self, ctx: &AnalysisStateContext<'_>) -> SubtrActorResult<()> {
708 self.project_frame(ctx)?;
709 self.flush_possession_buffer_as_neutral();
710 self.warn_for_unresolved_boost_current_amount_drift();
711 Ok(())
712 }
713
714 fn state(&self) -> &Self::State {
715 &self.state
716 }
717}
718
719pub(crate) fn boxed_default() -> Box<dyn AnalysisNodeDyn> {
720 Box::new(StatsProjectionNode::new())
721}
722
723#[cfg(test)]
724#[path = "stats_projection_tests.rs"]
725mod tests;