1use super::*;
2
3const PENDING_TURNOVER_CONFIRMATION_WINDOW_SECONDS: f32 = 1.25;
4const LOOSE_BALL_TIMEOUT_SECONDS: f32 = 3.0;
5
6const POSSESSION_RESOLUTION_WINDOW_SECONDS: f32 = LOOSE_BALL_TIMEOUT_SECONDS;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub(crate) enum PossessionLabel {
25 TeamZero,
26 TeamOne,
27 Neutral,
28}
29
30impl PossessionLabel {
31 pub(crate) fn team(team_is_team_0: bool) -> Self {
32 if team_is_team_0 {
33 Self::TeamZero
34 } else {
35 Self::TeamOne
36 }
37 }
38
39 pub(crate) fn as_label_value(self) -> &'static str {
40 match self {
41 Self::TeamZero => "team_zero",
42 Self::TeamOne => "team_one",
43 Self::Neutral => "neutral",
44 }
45 }
46}
47
48#[derive(Debug, Clone, PartialEq)]
53pub(crate) struct ResolvedPossession {
54 pub start_time: f32,
55 pub start_frame: usize,
56 pub end_time: f32,
57 pub end_frame: usize,
58 pub label: PossessionLabel,
59 pub player: Option<PlayerId>,
60}
61
62#[derive(Debug, Clone, PartialEq)]
66pub(crate) struct OpenPossession {
67 pub start_time: f32,
68 pub start_frame: usize,
69 pub label: PossessionLabel,
70 pub player: Option<PlayerId>,
71}
72
73#[derive(Debug, Clone)]
76enum TouchInput {
77 None,
78 Single {
80 team_is_team_0: bool,
81 player: Option<PlayerId>,
82 },
83 Contested {
85 team_zero_player: Option<PlayerId>,
86 team_one_player: Option<PlayerId>,
87 },
88}
89
90impl TouchInput {
91 fn opponent_player(&self, owner_team_is_team_0: bool) -> Option<PlayerId> {
92 match self {
93 TouchInput::Contested {
94 team_zero_player,
95 team_one_player,
96 } => {
97 if owner_team_is_team_0 {
98 team_one_player.clone()
99 } else {
100 team_zero_player.clone()
101 }
102 }
103 _ => None,
104 }
105 }
106
107 fn owner_player(&self, owner_team_is_team_0: bool) -> Option<PlayerId> {
108 match self {
109 TouchInput::Contested {
110 team_zero_player,
111 team_one_player,
112 } => {
113 if owner_team_is_team_0 {
114 team_zero_player.clone()
115 } else {
116 team_one_player.clone()
117 }
118 }
119 _ => None,
120 }
121 }
122}
123
124#[derive(Debug, Clone, PartialEq, Default)]
127enum ResolverPhase {
128 #[default]
130 Neutral,
131 Acquiring {
134 team_is_team_0: bool,
135 first_touch_time: f32,
136 first_touch_frame: usize,
137 player: Option<PlayerId>,
138 },
139 Held {
142 team_is_team_0: bool,
143 player: Option<PlayerId>,
144 last_touch_time: f32,
145 last_touch_frame: usize,
146 },
147 HeldChallenged {
150 team_is_team_0: bool,
151 player: Option<PlayerId>,
152 held_last_touch_time: f32,
153 held_last_touch_frame: usize,
154 challenger_first_time: f32,
155 challenger_first_frame: usize,
156 challenger_player: Option<PlayerId>,
157 },
158}
159
160#[derive(Debug, Clone, PartialEq, Default)]
166pub(crate) struct PossessionResolver {
167 phase: ResolverPhase,
168 open_start_time: f32,
169 open_start_frame: usize,
170 newly_resolved: Vec<ResolvedPossession>,
171}
172
173impl PossessionResolver {
174 fn reset(&mut self) {
175 self.phase = ResolverPhase::Neutral;
176 self.open_start_time = 0.0;
177 self.open_start_frame = 0;
178 self.newly_resolved.clear();
179 }
180
181 fn finalize(
184 &mut self,
185 end_time: f32,
186 end_frame: usize,
187 label: PossessionLabel,
188 player: Option<PlayerId>,
189 ) {
190 if end_time > self.open_start_time {
191 self.newly_resolved.push(ResolvedPossession {
192 start_time: self.open_start_time,
193 start_frame: self.open_start_frame,
194 end_time,
195 end_frame,
196 label,
197 player,
198 });
199 }
200 self.open_start_time = end_time;
201 self.open_start_frame = end_frame;
202 }
203
204 fn within_window(now: f32, since: f32) -> bool {
205 now - since <= POSSESSION_RESOLUTION_WINDOW_SECONDS
206 }
207
208 fn touch_input(
209 touched_team_zero_player: &Option<PlayerId>,
210 touched_team_one_player: &Option<PlayerId>,
211 touched_team_zero: bool,
212 touched_team_one: bool,
213 ) -> TouchInput {
214 match (touched_team_zero, touched_team_one) {
215 (false, false) => TouchInput::None,
216 (true, false) => TouchInput::Single {
217 team_is_team_0: true,
218 player: touched_team_zero_player.clone(),
219 },
220 (false, true) => TouchInput::Single {
221 team_is_team_0: false,
222 player: touched_team_one_player.clone(),
223 },
224 (true, true) => TouchInput::Contested {
225 team_zero_player: touched_team_zero_player.clone(),
226 team_one_player: touched_team_one_player.clone(),
227 },
228 }
229 }
230
231 fn update(
234 &mut self,
235 frame: &FrameInfo,
236 touched_team_zero_player: &Option<PlayerId>,
237 touched_team_one_player: &Option<PlayerId>,
238 touched_team_zero: bool,
239 touched_team_one: bool,
240 ) {
241 self.newly_resolved.clear();
242 let time = frame.time;
243 let fnum = frame.frame_number;
244 let input = Self::touch_input(
245 touched_team_zero_player,
246 touched_team_one_player,
247 touched_team_zero,
248 touched_team_one,
249 );
250
251 let phase = std::mem::take(&mut self.phase);
252 self.phase = match phase {
253 ResolverPhase::Neutral => self.step_neutral(time, fnum, input),
254 ResolverPhase::Acquiring {
255 team_is_team_0,
256 first_touch_time,
257 first_touch_frame,
258 player,
259 } => self.step_acquiring(
260 time,
261 fnum,
262 input,
263 team_is_team_0,
264 first_touch_time,
265 first_touch_frame,
266 player,
267 ),
268 ResolverPhase::Held {
269 team_is_team_0,
270 player,
271 last_touch_time,
272 last_touch_frame,
273 } => self.step_held(
274 time,
275 fnum,
276 input,
277 team_is_team_0,
278 player,
279 last_touch_time,
280 last_touch_frame,
281 ),
282 ResolverPhase::HeldChallenged {
283 team_is_team_0,
284 player,
285 held_last_touch_time,
286 held_last_touch_frame,
287 challenger_first_time,
288 challenger_first_frame,
289 challenger_player,
290 } => self.step_held_challenged(
291 time,
292 fnum,
293 input,
294 team_is_team_0,
295 player,
296 held_last_touch_time,
297 held_last_touch_frame,
298 challenger_first_time,
299 challenger_first_frame,
300 challenger_player,
301 ),
302 };
303 }
304
305 fn step_neutral(&mut self, time: f32, fnum: usize, input: TouchInput) -> ResolverPhase {
306 match input {
307 TouchInput::Single {
310 team_is_team_0,
311 player,
312 } => ResolverPhase::Acquiring {
313 team_is_team_0,
314 first_touch_time: time,
315 first_touch_frame: fnum,
316 player,
317 },
318 TouchInput::None | TouchInput::Contested { .. } => ResolverPhase::Neutral,
320 }
321 }
322
323 #[allow(clippy::too_many_arguments)]
324 fn step_acquiring(
325 &mut self,
326 time: f32,
327 fnum: usize,
328 input: TouchInput,
329 team_is_team_0: bool,
330 first_touch_time: f32,
331 first_touch_frame: usize,
332 player: Option<PlayerId>,
333 ) -> ResolverPhase {
334 let still_acquiring = ResolverPhase::Acquiring {
335 team_is_team_0,
336 first_touch_time,
337 first_touch_frame,
338 player: player.clone(),
339 };
340 match input {
341 TouchInput::None => {
342 if Self::within_window(time, first_touch_time) {
343 still_acquiring
344 } else {
345 ResolverPhase::Neutral
347 }
348 }
349 TouchInput::Single {
350 team_is_team_0: touch_team,
351 player: touch_player,
352 } => {
353 if touch_team == team_is_team_0 {
354 self.finalize(
357 first_touch_time,
358 first_touch_frame,
359 PossessionLabel::Neutral,
360 None,
361 );
362 ResolverPhase::Held {
363 team_is_team_0,
364 player: touch_player.or(player),
365 last_touch_time: time,
366 last_touch_frame: fnum,
367 }
368 } else {
369 ResolverPhase::Acquiring {
371 team_is_team_0: touch_team,
372 first_touch_time: time,
373 first_touch_frame: fnum,
374 player: touch_player,
375 }
376 }
377 }
378 TouchInput::Contested { .. } => still_acquiring,
380 }
381 }
382
383 #[allow(clippy::too_many_arguments)]
384 fn step_held(
385 &mut self,
386 time: f32,
387 fnum: usize,
388 input: TouchInput,
389 team_is_team_0: bool,
390 player: Option<PlayerId>,
391 last_touch_time: f32,
392 last_touch_frame: usize,
393 ) -> ResolverPhase {
394 match input {
395 TouchInput::None => {
396 if Self::within_window(time, last_touch_time) {
397 ResolverPhase::Held {
398 team_is_team_0,
399 player,
400 last_touch_time,
401 last_touch_frame,
402 }
403 } else {
404 self.finalize(
407 last_touch_time,
408 last_touch_frame,
409 PossessionLabel::team(team_is_team_0),
410 player,
411 );
412 ResolverPhase::Neutral
413 }
414 }
415 TouchInput::Single {
416 team_is_team_0: touch_team,
417 player: touch_player,
418 } => {
419 if touch_team == team_is_team_0 {
420 ResolverPhase::Held {
423 team_is_team_0,
424 player: touch_player.or(player),
425 last_touch_time: time,
426 last_touch_frame: fnum,
427 }
428 } else {
429 ResolverPhase::HeldChallenged {
430 team_is_team_0,
431 player,
432 held_last_touch_time: last_touch_time,
433 held_last_touch_frame: last_touch_frame,
434 challenger_first_time: time,
435 challenger_first_frame: fnum,
436 challenger_player: touch_player,
437 }
438 }
439 }
440 TouchInput::Contested { .. } => {
441 ResolverPhase::HeldChallenged {
444 team_is_team_0,
445 player: input.owner_player(team_is_team_0).or(player),
446 held_last_touch_time: time,
447 held_last_touch_frame: fnum,
448 challenger_first_time: time,
449 challenger_first_frame: fnum,
450 challenger_player: input.opponent_player(team_is_team_0),
451 }
452 }
453 }
454 }
455
456 #[allow(clippy::too_many_arguments)]
457 fn step_held_challenged(
458 &mut self,
459 time: f32,
460 fnum: usize,
461 input: TouchInput,
462 team_is_team_0: bool,
463 player: Option<PlayerId>,
464 held_last_touch_time: f32,
465 held_last_touch_frame: usize,
466 challenger_first_time: f32,
467 challenger_first_frame: usize,
468 challenger_player: Option<PlayerId>,
469 ) -> ResolverPhase {
470 let confirm_turnover = |resolver: &mut Self, new_player: Option<PlayerId>| {
471 resolver.finalize(
475 held_last_touch_time,
476 held_last_touch_frame,
477 PossessionLabel::team(team_is_team_0),
478 player.clone(),
479 );
480 resolver.finalize(
481 challenger_first_time,
482 challenger_first_frame,
483 PossessionLabel::Neutral,
484 None,
485 );
486 ResolverPhase::Held {
487 team_is_team_0: !team_is_team_0,
488 player: new_player.or_else(|| challenger_player.clone()),
489 last_touch_time: time,
490 last_touch_frame: fnum,
491 }
492 };
493
494 match input {
495 TouchInput::None => {
496 if Self::within_window(time, challenger_first_time) {
497 ResolverPhase::HeldChallenged {
498 team_is_team_0,
499 player,
500 held_last_touch_time,
501 held_last_touch_frame,
502 challenger_first_time,
503 challenger_first_frame,
504 challenger_player,
505 }
506 } else {
507 self.finalize(
510 held_last_touch_time,
511 held_last_touch_frame,
512 PossessionLabel::team(team_is_team_0),
513 player,
514 );
515 ResolverPhase::Neutral
516 }
517 }
518 TouchInput::Single {
519 team_is_team_0: touch_team,
520 player: touch_player,
521 } => {
522 if touch_team == team_is_team_0 {
523 ResolverPhase::Held {
530 team_is_team_0,
531 player: touch_player.or(player),
532 last_touch_time: time,
533 last_touch_frame: fnum,
534 }
535 } else {
536 confirm_turnover(self, touch_player)
537 }
538 }
539 TouchInput::Contested { .. } => {
540 confirm_turnover(self, input.opponent_player(team_is_team_0))
541 }
542 }
543 }
544
545 fn open(&self) -> OpenPossession {
548 let (label, player) = match &self.phase {
549 ResolverPhase::Neutral | ResolverPhase::Acquiring { .. } => {
550 (PossessionLabel::Neutral, None)
551 }
552 ResolverPhase::Held {
553 team_is_team_0,
554 player,
555 ..
556 }
557 | ResolverPhase::HeldChallenged {
558 team_is_team_0,
559 player,
560 ..
561 } => (PossessionLabel::team(*team_is_team_0), player.clone()),
562 };
563 OpenPossession {
564 start_time: self.open_start_time,
565 start_frame: self.open_start_frame,
566 label,
567 player,
568 }
569 }
570
571 fn flush(&mut self, end_time: f32, end_frame: usize) {
574 let phase = std::mem::take(&mut self.phase);
575 match phase {
576 ResolverPhase::Neutral | ResolverPhase::Acquiring { .. } => {
577 self.finalize(end_time, end_frame, PossessionLabel::Neutral, None);
578 }
579 ResolverPhase::Held {
580 team_is_team_0,
581 player,
582 last_touch_time,
583 last_touch_frame,
584 } => {
585 self.finalize(
586 last_touch_time,
587 last_touch_frame,
588 PossessionLabel::team(team_is_team_0),
589 player,
590 );
591 self.finalize(end_time, end_frame, PossessionLabel::Neutral, None);
592 }
593 ResolverPhase::HeldChallenged {
594 team_is_team_0,
595 player,
596 held_last_touch_time,
597 held_last_touch_frame,
598 ..
599 } => {
600 self.finalize(
601 held_last_touch_time,
602 held_last_touch_frame,
603 PossessionLabel::team(team_is_team_0),
604 player,
605 );
606 self.finalize(end_time, end_frame, PossessionLabel::Neutral, None);
607 }
608 }
609 self.phase = ResolverPhase::Neutral;
610 }
611}
612
613#[derive(Debug, Clone, PartialEq, Serialize, ts_rs::TS)]
615#[ts(export)]
616pub struct PossessionEvent {
617 pub time: f32,
618 pub frame: usize,
619 pub end_time: f32,
620 pub end_frame: usize,
621 pub active: bool,
622 pub duration: f32,
623 pub possession_state: String,
624 #[ts(as = "Option<crate::interop::ts_bindings::RemoteIdTs>")]
625 pub player_id: Option<PlayerId>,
626}
627
628#[derive(Debug, Clone, Default, PartialEq)]
629pub(crate) struct PossessionTracker {
630 current_team_is_team_0: Option<bool>,
631 current_player: Option<PlayerId>,
632 last_possession_touch_time: Option<f32>,
633 pending_turnover_team_is_team_0: Option<bool>,
634 pending_turnover_touch_time: Option<f32>,
635 resolver: PossessionResolver,
638}
639
640impl PossessionTracker {
641 fn clear_pending_turnover(&mut self) {
642 self.pending_turnover_team_is_team_0 = None;
643 self.pending_turnover_touch_time = None;
644 }
645
646 pub(crate) fn reset(&mut self) {
647 self.current_team_is_team_0 = None;
648 self.current_player = None;
649 self.last_possession_touch_time = None;
650 self.clear_pending_turnover();
651 }
652
653 pub(crate) fn begin_resolver(&mut self, frame: &FrameInfo) {
655 self.resolver.reset();
656 self.resolver.open_start_time = frame.time;
657 self.resolver.open_start_frame = frame.frame_number;
658 }
659
660 pub(crate) fn flush_resolver(&mut self, frame: &FrameInfo) -> Vec<ResolvedPossession> {
663 self.resolver.newly_resolved.clear();
664 self.resolver.flush(frame.time, frame.frame_number);
665 std::mem::take(&mut self.resolver.newly_resolved)
666 }
667
668 fn expire_pending_turnover(&mut self, time: f32) {
669 let Some(pending_time) = self.pending_turnover_touch_time else {
670 return;
671 };
672 if time - pending_time < PENDING_TURNOVER_CONFIRMATION_WINDOW_SECONDS {
673 return;
674 }
675
676 self.current_team_is_team_0 = None;
677 self.current_player = None;
678 self.last_possession_touch_time = None;
679 self.clear_pending_turnover();
680 }
681
682 fn expire_loose_ball(&mut self, time: f32) {
683 if self.pending_turnover_team_is_team_0.is_some() {
684 return;
685 }
686 let Some(last_touch_time) = self.last_possession_touch_time else {
687 return;
688 };
689 if time - last_touch_time < LOOSE_BALL_TIMEOUT_SECONDS {
690 return;
691 }
692
693 self.current_team_is_team_0 = None;
694 self.current_player = None;
695 self.last_possession_touch_time = None;
696 }
697
698 fn register_single_team_touch(&mut self, team_is_team_0: bool, time: f32) {
699 if self.current_team_is_team_0 == Some(team_is_team_0) {
700 self.last_possession_touch_time = Some(time);
701 self.clear_pending_turnover();
702 return;
703 }
704
705 if self.current_team_is_team_0.is_none() {
706 self.current_team_is_team_0 = Some(team_is_team_0);
707 self.last_possession_touch_time = Some(time);
708 self.clear_pending_turnover();
709 return;
710 }
711
712 if self.pending_turnover_team_is_team_0 == Some(team_is_team_0) {
713 self.current_team_is_team_0 = Some(team_is_team_0);
714 self.last_possession_touch_time = Some(time);
715 self.clear_pending_turnover();
716 return;
717 }
718
719 self.pending_turnover_team_is_team_0 = Some(team_is_team_0);
720 self.pending_turnover_touch_time = Some(time);
721 }
722
723 fn register_contested_touch(&mut self, time: f32) {
724 let Some(current_team_is_team_0) = self.current_team_is_team_0 else {
725 self.clear_pending_turnover();
726 return;
727 };
728
729 self.last_possession_touch_time = Some(time);
730 self.pending_turnover_team_is_team_0 = Some(!current_team_is_team_0);
731 self.pending_turnover_touch_time = Some(time);
732 }
733
734 fn update_player_control(
735 &mut self,
736 active_team_before_sample: Option<bool>,
737 touched_team_zero_player: Option<&PlayerId>,
738 touched_team_one_player: Option<&PlayerId>,
739 ) {
740 let Some(current_team_is_team_0) = self.current_team_is_team_0 else {
741 self.current_player = None;
742 return;
743 };
744
745 if self.pending_turnover_team_is_team_0.is_some() {
746 self.current_player = None;
747 return;
748 }
749
750 let controlling_touch_player = if current_team_is_team_0 {
751 touched_team_zero_player
752 } else {
753 touched_team_one_player
754 };
755 if let Some(player) = controlling_touch_player {
756 self.current_player = Some(player.clone());
757 return;
758 }
759
760 if active_team_before_sample != self.current_team_is_team_0 {
761 self.current_player = None;
762 }
763 }
764
765 fn latest_touch_player_for_team(
766 touch_events: &[TouchEvent],
767 team_is_team_0: bool,
768 ) -> Option<PlayerId> {
769 touch_events
770 .iter()
771 .filter(|touch| touch.team_is_team_0 == team_is_team_0)
772 .max_by(|left, right| TouchEvent::timestamp_ordering(left, right))
773 .and_then(|touch| touch.player.clone())
774 }
775
776 pub(crate) fn update(
777 &mut self,
778 frame: &FrameInfo,
779 touch_events: &[TouchEvent],
780 ) -> PossessionState {
781 let time = frame.time;
782 self.expire_pending_turnover(time);
783 self.expire_loose_ball(time);
784
785 let active_team_before_sample = self.current_team_is_team_0;
786 let active_player_before_sample = self.current_player.clone();
787 let touched_team_zero = touch_events.iter().any(|touch| touch.team_is_team_0);
788 let touched_team_one = touch_events.iter().any(|touch| !touch.team_is_team_0);
789 let touched_team_zero_player = Self::latest_touch_player_for_team(touch_events, true);
790 let touched_team_one_player = Self::latest_touch_player_for_team(touch_events, false);
791
792 match (touched_team_zero, touched_team_one) {
793 (true, true) => self.register_contested_touch(time),
794 (true, false) => self.register_single_team_touch(true, time),
795 (false, true) => self.register_single_team_touch(false, time),
796 (false, false) => {}
797 }
798 self.update_player_control(
799 active_team_before_sample,
800 touched_team_zero_player.as_ref(),
801 touched_team_one_player.as_ref(),
802 );
803
804 self.resolver.update(
805 frame,
806 &touched_team_zero_player,
807 &touched_team_one_player,
808 touched_team_zero,
809 touched_team_one,
810 );
811
812 PossessionState {
813 active_team_before_sample,
814 current_team_is_team_0: self.current_team_is_team_0,
815 active_player_before_sample,
816 current_player: self.current_player.clone(),
817 newly_resolved: std::mem::take(&mut self.resolver.newly_resolved),
818 open_possession: Some(self.resolver.open()),
819 }
820 }
821}
822
823#[cfg(test)]
824#[path = "possession_tests.rs"]
825mod tests;
826
827#[derive(Debug, Clone, Default, PartialEq)]
836pub struct PossessionCalculator {
837 events: EventStream<PossessionEvent>,
838 new_resolved: Vec<ResolvedPossession>,
841 open_event: Option<PossessionEvent>,
843 inactive_pending: Option<PossessionEvent>,
845}
846
847impl PossessionCalculator {
848 pub fn new() -> Self {
849 Self::default()
850 }
851
852 pub fn events(&self) -> &[PossessionEvent] {
853 self.events.all()
854 }
855
856 pub fn new_events(&self) -> &[PossessionEvent] {
857 self.events.new_events()
858 }
859
860 pub fn projected_events(&self) -> Vec<PossessionEvent> {
863 let mut events = self.events.all().to_vec();
864 if let Some(open) = &self.open_event {
865 events.push(open.clone());
866 }
867 if let Some(inactive) = &self.inactive_pending {
868 events.push(inactive.clone());
869 }
870 events
871 }
872
873 pub(crate) fn new_resolved(&self) -> &[ResolvedPossession] {
875 &self.new_resolved
876 }
877
878 pub fn flush_pending_event(&mut self) {
879 if let Some(inactive) = self.inactive_pending.take() {
880 self.events.push(inactive);
881 }
882 if let Some(open) = self.open_event.take() {
883 self.events.push(open);
884 }
885 }
886
887 pub fn current_event(&self) -> Option<&PossessionEvent> {
890 self.open_event
891 .as_ref()
892 .or(self.inactive_pending.as_ref())
893 .or_else(|| self.events.all().last())
894 }
895
896 fn segment_event(segment: &ResolvedPossession) -> PossessionEvent {
897 PossessionEvent {
898 time: segment.start_time,
899 frame: segment.start_frame,
900 end_time: segment.end_time,
901 end_frame: segment.end_frame,
902 active: true,
903 duration: (segment.end_time - segment.start_time).max(0.0),
904 possession_state: segment.label.as_label_value().to_owned(),
905 player_id: segment.player.clone(),
906 }
907 }
908
909 fn flush_inactive(&mut self) {
910 if let Some(inactive) = self.inactive_pending.take() {
911 self.events.push(inactive);
912 }
913 }
914
915 pub fn update(
916 &mut self,
917 frame: &FrameInfo,
918 possession_state: &PossessionState,
919 live_play_state: &LivePlayState,
920 ) -> SubtrActorResult<()> {
921 self.events.begin_update();
922 self.new_resolved.clear();
923
924 if !possession_state.newly_resolved.is_empty() {
928 self.flush_inactive();
929 for segment in &possession_state.newly_resolved {
930 self.events.push(Self::segment_event(segment));
931 self.new_resolved.push(segment.clone());
932 }
933 }
934
935 if !live_play_state.is_live_play {
936 self.open_event = None;
937 match self.inactive_pending.as_mut() {
938 Some(inactive) => {
939 inactive.end_time = frame.time;
940 inactive.end_frame = frame.frame_number;
941 inactive.duration = (inactive.end_time - inactive.time).max(0.0);
942 }
943 None => {
944 self.inactive_pending = Some(PossessionEvent {
945 time: frame.time,
946 frame: frame.frame_number,
947 end_time: frame.time,
948 end_frame: frame.frame_number,
949 active: false,
950 duration: 0.0,
951 possession_state: PossessionLabel::Neutral.as_label_value().to_owned(),
952 player_id: None,
953 });
954 }
955 }
956 return Ok(());
957 }
958
959 self.flush_inactive();
960 self.open_event = possession_state.open_possession.as_ref().map(|open| {
961 let label = open.label.as_label_value().to_owned();
962 PossessionEvent {
963 time: open.start_time,
964 frame: open.start_frame,
965 end_time: frame.time,
966 end_frame: frame.frame_number,
967 active: true,
968 duration: (frame.time - open.start_time).max(0.0),
969 possession_state: label,
970 player_id: open.player.clone(),
971 }
972 });
973 Ok(())
974 }
975}