subtr-actor 0.5.0

Rocket League replay transformer
Documentation
const FRAME_RESOLUTION_EPSILON_SECONDS: f32 = 1e-4;

#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub enum StatsFrameResolution {
    #[default]
    EveryFrame,
    TimeStep {
        seconds: f32,
    },
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) enum FinalStatsFrameAction {
    Append { dt: f32 },
    ReplaceLast { dt: f32 },
}

#[derive(Debug, Clone, Copy)]
pub(crate) struct StatsFramePersistenceController {
    resolution: StatsFrameResolution,
    next_emit_time: Option<f32>,
    last_emitted_frame_number: Option<usize>,
    last_emitted_time: Option<f32>,
    last_emitted_dt: f32,
}

impl StatsFramePersistenceController {
    pub(crate) fn new(resolution: StatsFrameResolution) -> Self {
        Self {
            resolution,
            next_emit_time: None,
            last_emitted_frame_number: None,
            last_emitted_time: None,
            last_emitted_dt: 0.0,
        }
    }

    pub(crate) fn on_frame(&mut self, frame_number: usize, current_time: f32) -> Option<f32> {
        if self.last_emitted_time.is_none() {
            return Some(self.record_emit(frame_number, current_time));
        }

        match self.resolution {
            StatsFrameResolution::EveryFrame => Some(self.record_emit(frame_number, current_time)),
            StatsFrameResolution::TimeStep { seconds } => {
                if !seconds.is_finite() || seconds <= 0.0 {
                    return Some(self.record_emit(frame_number, current_time));
                }

                let next_emit_time = self.next_emit_time.unwrap_or(current_time + seconds);
                if current_time + FRAME_RESOLUTION_EPSILON_SECONDS < next_emit_time {
                    self.next_emit_time = Some(next_emit_time);
                    return None;
                }

                let dt = self.record_emit(frame_number, current_time);
                let mut advanced_next_emit_time = next_emit_time;
                while advanced_next_emit_time <= current_time + FRAME_RESOLUTION_EPSILON_SECONDS {
                    advanced_next_emit_time += seconds;
                }
                self.next_emit_time = Some(advanced_next_emit_time);
                Some(dt)
            }
        }
    }

    pub(crate) fn final_frame_action(
        &self,
        frame_number: usize,
        current_time: f32,
    ) -> Option<FinalStatsFrameAction> {
        let Some(last_emitted_time) = self.last_emitted_time else {
            return Some(FinalStatsFrameAction::Append { dt: 0.0 });
        };

        if self.last_emitted_frame_number == Some(frame_number) {
            return Some(FinalStatsFrameAction::ReplaceLast {
                dt: self.last_emitted_dt,
            });
        }

        Some(FinalStatsFrameAction::Append {
            dt: (current_time - last_emitted_time).max(0.0),
        })
    }

    fn record_emit(&mut self, frame_number: usize, current_time: f32) -> f32 {
        let dt = self
            .last_emitted_time
            .map(|last_time| (current_time - last_time).max(0.0))
            .unwrap_or(0.0);
        self.last_emitted_frame_number = Some(frame_number);
        self.last_emitted_time = Some(current_time);
        self.last_emitted_dt = dt;
        self.next_emit_time = match self.resolution {
            StatsFrameResolution::EveryFrame => None,
            StatsFrameResolution::TimeStep { seconds } if seconds.is_finite() && seconds > 0.0 => {
                Some(current_time + seconds)
            }
            StatsFrameResolution::TimeStep { .. } => None,
        };
        dt
    }
}

#[cfg(test)]
mod tests {
    use super::{FinalStatsFrameAction, StatsFramePersistenceController, StatsFrameResolution};

    #[test]
    fn every_frame_resolution_emits_every_frame() {
        let mut controller = StatsFramePersistenceController::new(StatsFrameResolution::EveryFrame);

        assert_eq!(controller.on_frame(10, 0.0), Some(0.0));
        assert_eq!(controller.on_frame(11, 0.1), Some(0.1));
        assert_eq!(controller.on_frame(12, 0.25), Some(0.15));
        assert_eq!(
            controller.final_frame_action(12, 0.25),
            Some(FinalStatsFrameAction::ReplaceLast { dt: 0.15 })
        );
    }

    #[test]
    fn timestep_resolution_emits_crossings_and_appends_final_frame() {
        let mut controller =
            StatsFramePersistenceController::new(StatsFrameResolution::TimeStep { seconds: 0.5 });

        assert_eq!(controller.on_frame(0, 0.0), Some(0.0));
        assert_eq!(controller.on_frame(1, 0.2), None);
        assert_eq!(controller.on_frame(2, 0.49), None);
        assert_eq!(controller.on_frame(3, 0.5), Some(0.5));
        assert_eq!(controller.on_frame(4, 0.74), None);
        match controller.final_frame_action(4, 0.74) {
            Some(FinalStatsFrameAction::Append { dt }) => {
                assert!(
                    (dt - 0.24).abs() < 1e-6,
                    "expected dt close to 0.24, got {dt}"
                );
            }
            action => panic!("expected append action, got {action:?}"),
        }
    }
}