mecha10-behavior-runtime 0.6.5

Behavior tree runtime for Mecha10 - unified AI and logic composition system
Documentation
//! Shared helper for leaf action nodes that gate on wall-clock time elapsed since their
//! first tick after construction/reset.
//!
//! `TimerNode` and `PauseNode` (`timing.rs`) and `MoveNode` (`movement.rs`) all start a clock
//! on first tick, compare elapsed time against a configured duration, and clear the clock on
//! reset - cargo-dupes flagged their near-identical `tick`/`reset` bodies as duplicates of each
//! other. Centralizing the clock logic here, plus the `reset_timer_field!` macro below for the
//! one-line `reset_self` override every one of them needs, removes the duplication instead of
//! just relocating it.

use std::time::{Duration, Instant};

/// Tracks how much time has elapsed since the first call to [`elapsed_since_start`] after
/// construction or [`reset`].
///
/// [`elapsed_since_start`]: ElapsedTimer::elapsed_since_start
/// [`reset`]: ElapsedTimer::reset
#[derive(Debug, Default)]
pub(crate) struct ElapsedTimer {
    start_time: Option<Instant>,
}

impl ElapsedTimer {
    /// Returns `true` if the clock has already been started (i.e. `elapsed_since_start` has
    /// been called at least once since construction/reset).
    pub(crate) fn is_running(&self) -> bool {
        self.start_time.is_some()
    }

    /// Returns `true` once `duration` has elapsed since the first call after
    /// construction/reset; starts the clock on the first call.
    pub(crate) fn elapsed_since_start(&mut self, duration: Duration) -> bool {
        let now = Instant::now();
        let start = *self.start_time.get_or_insert(now);
        now.duration_since(start) >= duration
    }

    /// Clear the tracked start time, so the next `elapsed_since_start` call restarts the clock.
    pub(crate) fn reset(&mut self) {
        self.start_time = None;
    }
}

/// Generate the `reset_self` override for a leaf node that clears an [`ElapsedTimer`] field.
///
/// See the module docs above for why this needs to be a macro rather than a shared method:
/// `BehaviorNode::reset_self` must be implemented individually per concrete type, and every
/// caller's body is otherwise byte-for-byte identical.
macro_rules! reset_timer_field {
    ($field:ident) => {
        fn reset_self(&mut self) {
            self.$field.reset();
        }
    };
}

pub(crate) use reset_timer_field;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn new_timer_is_not_running() {
        let timer = ElapsedTimer::default();
        assert!(!timer.is_running());
    }

    #[test]
    fn elapsed_since_start_starts_the_clock_on_first_call() {
        let mut timer = ElapsedTimer::default();
        assert!(!timer.elapsed_since_start(Duration::from_secs(60)));
        assert!(timer.is_running());
    }

    #[test]
    fn elapsed_since_start_reports_elapsed_once_duration_is_zero() {
        let mut timer = ElapsedTimer::default();
        assert!(timer.elapsed_since_start(Duration::ZERO));
    }

    #[test]
    fn elapsed_since_start_keeps_the_original_start_time_across_calls() {
        let mut timer = ElapsedTimer::default();
        assert!(!timer.elapsed_since_start(Duration::from_millis(50)));
        std::thread::sleep(Duration::from_millis(75));
        assert!(timer.elapsed_since_start(Duration::from_millis(50)));
    }

    #[test]
    fn reset_clears_the_clock() {
        let mut timer = ElapsedTimer::default();
        timer.elapsed_since_start(Duration::from_secs(60));
        assert!(timer.is_running());

        timer.reset();
        assert!(!timer.is_running());
    }
}