Skip to main content

mecha10_behavior_runtime/
execution.rs

1//! Execution engine for behavior trees
2//!
3//! This module provides the executor that runs behavior trees with tick-based execution.
4
5use crate::{BoxedBehavior, NodeStatus};
6use mecha10_core::Context;
7use std::time::{Duration, Instant};
8use tracing::{debug, info, warn};
9
10#[derive(Clone)]
11pub struct ExecutionContext {
12    ctx: Context,
13
14    tick_rate_hz: f32,
15}
16
17impl ExecutionContext {
18    /// Create a new execution context.
19    pub fn new(ctx: Context, tick_rate_hz: f32) -> Self {
20        Self { ctx, tick_rate_hz }
21    }
22
23    /// Get the underlying context.
24    pub fn context(&self) -> &Context {
25        &self.ctx
26    }
27
28    /// Get the tick rate in Hz.
29    pub fn tick_rate_hz(&self) -> f32 {
30        self.tick_rate_hz
31    }
32}
33
34#[derive(Debug, Clone)]
35pub struct ExecutionStats {
36    pub tick_count: usize,
37
38    pub total_duration: Duration,
39
40    pub avg_tick_duration: Duration,
41
42    pub min_tick_duration: Duration,
43
44    pub max_tick_duration: Duration,
45
46    pub final_status: Option<NodeStatus>,
47}
48
49impl ExecutionStats {
50    fn new() -> Self {
51        Self {
52            tick_count: 0,
53            total_duration: Duration::ZERO,
54            avg_tick_duration: Duration::ZERO,
55            min_tick_duration: Duration::MAX,
56            max_tick_duration: Duration::ZERO,
57            final_status: None,
58        }
59    }
60
61    fn update(&mut self, tick_duration: Duration) {
62        self.tick_count += 1;
63        self.total_duration += tick_duration;
64
65        if tick_duration < self.min_tick_duration {
66            self.min_tick_duration = tick_duration;
67        }
68        if tick_duration > self.max_tick_duration {
69            self.max_tick_duration = tick_duration;
70        }
71
72        self.avg_tick_duration = self.total_duration / self.tick_count as u32;
73    }
74
75    fn finalize(&mut self, status: NodeStatus) {
76        self.final_status = Some(status);
77    }
78}
79
80pub struct BehaviorExecutor {
81    behavior: BoxedBehavior,
82    tick_rate_hz: f32,
83    max_ticks: Option<usize>,
84}
85
86macro_rules! lifecycle_step {
87    ($(#[$meta:meta])* $fn_name:ident, $verb:literal, $behavior_method:ident) => {
88        $(#[$meta])*
89        pub async fn $fn_name(&mut self, ctx: &Context) -> anyhow::Result<()> {
90            info!(concat!($verb, " behavior: {}"), self.behavior.name());
91            self.behavior.$behavior_method(ctx).await?;
92            Ok(())
93        }
94    };
95}
96
97impl BehaviorExecutor {
98    mecha10_core::new_with_defaults!(
99        /// Create a new behavior executor for `behavior`, ticking at `tick_rate_hz` (e.g. 30.0).
100        pub fn new(behavior: BoxedBehavior, tick_rate_hz: f32) -> Self {
101            max_ticks: None
102        }
103    );
104
105    mecha10_core::builder_setter!(into_some, with_max_ticks, max_ticks, usize);
106
107    lifecycle_step!(init, "Initializing", on_init);
108
109    /// Run the behavior until it completes (Success or Failure), ticking at the configured rate.
110    ///
111    /// Returns the final status and execution statistics.
112    pub async fn run_until_complete(&mut self, ctx: &Context) -> anyhow::Result<(NodeStatus, ExecutionStats)> {
113        let mut stats = ExecutionStats::new();
114        let mut interval = tokio::time::interval(Duration::from_secs_f32(1.0 / self.tick_rate_hz));
115
116        info!(
117            "Starting behavior execution: {} at {} Hz",
118            self.behavior.name(),
119            self.tick_rate_hz
120        );
121
122        loop {
123            interval.tick().await;
124
125            if let Some(max_ticks) = self.max_ticks {
126                if stats.tick_count >= max_ticks {
127                    warn!("Behavior exceeded maximum ticks ({})", max_ticks);
128                    return Err(anyhow::anyhow!("Exceeded maximum ticks"));
129                }
130            }
131
132            let tick_start = Instant::now();
133            let status = self.behavior.tick(ctx).await?;
134            let tick_duration = tick_start.elapsed();
135
136            stats.update(tick_duration);
137
138            debug!(
139                "Tick #{}: status={}, duration={:?}",
140                stats.tick_count, status, tick_duration
141            );
142
143            if status.is_complete() {
144                info!(
145                    "Behavior completed with status: {} after {} ticks ({:?})",
146                    status, stats.tick_count, stats.total_duration
147                );
148                stats.finalize(status);
149                return Ok((status, stats));
150            }
151        }
152    }
153
154    /// Run the behavior for a fixed duration, ticking at the configured rate regardless of
155    /// whether it completes.
156    pub async fn run_for_duration(
157        &mut self,
158        ctx: &Context,
159        duration: Duration,
160    ) -> anyhow::Result<(NodeStatus, ExecutionStats)> {
161        let mut stats = ExecutionStats::new();
162        let mut interval = tokio::time::interval(Duration::from_secs_f32(1.0 / self.tick_rate_hz));
163        let start_time = Instant::now();
164
165        info!(
166            "Starting behavior execution: {} for {:?} at {} Hz",
167            self.behavior.name(),
168            duration,
169            self.tick_rate_hz
170        );
171
172        let mut last_status = NodeStatus::Running;
173
174        while start_time.elapsed() < duration {
175            interval.tick().await;
176
177            let tick_start = Instant::now();
178            let status = self.behavior.tick(ctx).await?;
179            let tick_duration = tick_start.elapsed();
180
181            stats.update(tick_duration);
182            last_status = status;
183
184            debug!(
185                "Tick #{}: status={}, duration={:?}",
186                stats.tick_count, status, tick_duration
187            );
188
189            if status.is_complete() {
190                info!("Behavior completed early with status: {}", status);
191                break;
192            }
193        }
194
195        info!(
196            "Behavior execution ended after {} ticks ({:?})",
197            stats.tick_count, stats.total_duration
198        );
199        stats.finalize(last_status);
200        Ok((last_status, stats))
201    }
202
203    lifecycle_step!(terminate, "Terminating", on_terminate);
204
205    /// Reset the behavior to its initial state.
206    pub async fn reset(&mut self) -> anyhow::Result<()> {
207        info!("Resetting behavior: {}", self.behavior.name());
208        self.behavior.reset().await?;
209        Ok(())
210    }
211}