Skip to main content

bonsai_bt/
bt.rs

1use crate::{state::State, ActionArgs, Behavior, Float, Status, UpdateEvent};
2
3#[cfg(feature = "serde")]
4use serde::{Deserialize, Serialize};
5
6/// Result of [`BT::try_route_recording`]: whether the recording helper consumed
7/// the tick. `Handled` carries the value `tick` should return; `NotHandled`
8/// tells the caller to continue with the no-op path. Under
9/// `not(feature = "visualize")` the helper unconditionally returns
10/// `NotHandled`, so the `Handled` variant is unconstructed.
11#[allow(dead_code)]
12enum TickRoute {
13    NotHandled,
14    Handled(Option<(Status, Float)>),
15}
16
17/// The execution state of a behavior tree, along with a "blackboard" (state
18/// shared between all nodes in the tree).
19#[derive(Clone, Debug)]
20#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
21pub struct BT<A, B> {
22    /// constructed behavior tree
23    pub(crate) state: State<A>,
24    /// keep the initial state
25    pub(crate) initial_behavior: Behavior<A>,
26    /// The data storage shared by all nodes in the tree. This is generally
27    /// referred to as a "blackboard". State is written to and read from a
28    /// blackboard, allowing nodes to share state and communicate each other.
29    pub(crate) bb: B,
30    /// Whether the tree has been finished before.
31    pub(crate) finished: bool,
32    /// Monotonically increasing per-tick counter. Starts at 0; first completed
33    /// `tick`/`tick_recording` call increments to 1. Survives `reset_bt`
34    /// (the counter is global to the BT instance, not the current run).
35    pub(crate) tick_count: u64,
36    /// Bundle of visualize-only state: preorder node metadata, telemetry
37    /// channel sender, dropped-trace counter, and the per-tick recording
38    /// buffer. See [`crate::telemetry_state::TelemetryState`].
39    #[cfg(feature = "visualize")]
40    #[cfg_attr(feature = "serde", serde(skip))]
41    pub(crate) telemetry: crate::telemetry_state::TelemetryState,
42}
43
44impl<A: Clone, B> BT<A, B> {
45    pub fn new(behavior: Behavior<A>, blackboard: B) -> Self {
46        let backup_behavior = behavior.clone();
47        let bt = State::new(behavior);
48
49        #[cfg(feature = "visualize")]
50        let telemetry =
51            crate::telemetry_state::TelemetryState::new(crate::telemetry::build_node_metas(&backup_behavior));
52
53        Self {
54            state: bt,
55            initial_behavior: backup_behavior,
56            bb: blackboard,
57            finished: false,
58            tick_count: 0,
59            #[cfg(feature = "visualize")]
60            telemetry,
61        }
62    }
63
64    /// Updates the cursor that tracks an event. Returns [`None`] if attempting
65    /// to tick after this tree has already returned [`Status::Success`] or
66    /// [`Status::Failure`].
67    ///
68    /// The action need to return status and remaining delta time.
69    /// Returns status and the remaining delta time.
70    ///
71    /// Passes event, delta time in seconds, action and state to closure.
72    /// The closure should return a status and remaining delta time.
73    ///
74    /// return: (Status, Float)
75    /// function returns the result of the tree traversal, and how long
76    /// it actually took to complete the traversal and propagate the
77    /// results back up to the root node
78    #[inline]
79    pub fn tick<E, F>(&mut self, e: &E, f: &mut F) -> Option<(Status, Float)>
80    where
81        E: UpdateEvent,
82        F: FnMut(ActionArgs<E, A>, &mut B) -> (Status, Float),
83    {
84        if self.finished {
85            return None;
86        }
87        if let TickRoute::Handled(out) = self.try_route_recording(e, f) {
88            return out;
89        }
90        self.tick_count += 1;
91        let result = self.dispatch_noop_tick(e, f);
92        if matches!(result, (Status::Success | Status::Failure, _)) {
93            self.finished = true;
94        }
95        Some(result)
96    }
97
98    /// Run `State::tick` with a [`NoopTracer`](crate::tracer::NoopTracer) (the
99    /// non-recording path). The cfg-gated `metas` binding lives inside this
100    /// helper so `tick`'s body can stay free of `#[cfg]` directives.
101    ///
102    /// Disjoint-field borrows: `&self.telemetry.node_metas` (immutable) and
103    /// `&mut self.state` / `&mut self.bb` (mutable) target distinct fields,
104    /// so the borrow checker accepts the simultaneous borrows.
105    ///
106    /// `#[inline(always)]` ensures the cfg branches constant-fold at the
107    /// monomorphization site, leaving identical generated code to the prior
108    /// inlined-in-`tick` version.
109    #[inline(always)]
110    fn dispatch_noop_tick<E, F>(&mut self, e: &E, f: &mut F) -> (Status, Float)
111    where
112        E: UpdateEvent,
113        F: FnMut(ActionArgs<E, A>, &mut B) -> (Status, Float),
114    {
115        let mut tracer = crate::tracer::NoopTracer;
116        #[cfg(feature = "visualize")]
117        let metas: &[crate::tracer::NodeMeta] = &self.telemetry.node_metas;
118        #[cfg(not(feature = "visualize"))]
119        let metas: &[crate::tracer::NodeMeta] = &[];
120        self.state.tick(0, metas, e, &mut self.bb, f, &mut tracer)
121    }
122
123    /// If telemetry is attached, dispatch to `tick_recording` and return its
124    /// result as [`TickRoute::Handled`]. Returns [`TickRoute::NotHandled`]
125    /// otherwise — the caller (`tick`) should proceed with the no-op path.
126    ///
127    /// `#[inline(always)]` lets the optimizer constant-fold the no-op path:
128    /// under `not(feature = "visualize")` the body unconditionally returns
129    /// `TickRoute::NotHandled`, so the `if let TickRoute::Handled(_) = ...`
130    /// branch in `tick` becomes unreachable and disappears.
131    #[inline(always)]
132    fn try_route_recording<E, F>(&mut self, e: &E, f: &mut F) -> TickRoute
133    where
134        E: UpdateEvent,
135        F: FnMut(ActionArgs<E, A>, &mut B) -> (Status, Float),
136    {
137        #[cfg(feature = "visualize")]
138        if self.telemetry.sender.is_some() {
139            return TickRoute::Handled(self.tick_recording(e, f).map(|(result, _)| result));
140        }
141        // Suppress unused-variable warnings on the no-op path (visualize off,
142        // or visualize on but no sender attached).
143        let _ = (e, f);
144        TickRoute::NotHandled
145    }
146
147    /// Retrieve an immutable reference to the blackboard for
148    /// this Behavior Tree
149    pub fn blackboard(&self) -> &B {
150        &self.bb
151    }
152
153    /// Retrieve a mutable reference to the blackboard for
154    /// this Behavior Tree
155    pub fn blackboard_mut(&mut self) -> &mut B {
156        &mut self.bb
157    }
158
159    /// The behavior tree is a stateful data structure in which the immediate
160    /// state of the BT is allocated and updated in heap memory through the lifetime
161    /// of the BT. The state of the BT is said to be `transient` meaning upon entering
162    /// a this state, the process may never return this state again. If a behavior concludes,
163    /// only the latest results will be stored in heap memory.
164    ///
165    /// If your BT has surpassed a desired state or that your BT has reached a steady state - meaning
166    /// that the behavior has concluded and ticking the BT won't progress any further - then it could
167    /// be desirable to return the BT to it's initial state at t=0.0 before it was ever ticked.
168    ///
169    /// <div class="warning">Invoking <code>reset_bt()</code> does not reset the Blackboard.</div>
170    pub fn reset_bt(&mut self) {
171        let initial_behavior = self.initial_behavior.to_owned();
172        self.state = State::new(initial_behavior);
173        self.finished = false;
174        // tick_count is intentionally NOT reset — it identifies tick events
175        // across the BT's lifetime, including across reset_bt boundaries.
176        // dropped_traces resets per-run: it's a diagnostic for the current session.
177        #[cfg(feature = "visualize")]
178        {
179            self.telemetry.dropped_traces = 0;
180        }
181    }
182
183    /// Returns the total number of ticks this BT has completed (across resets).
184    pub fn tick_count(&self) -> u64 {
185        self.tick_count
186    }
187
188    /// Whether this behavior tree is in a completed state (the last tick returned
189    /// [`Status::Success`] or [`Status::Failure`]).
190    pub fn is_finished(&self) -> bool {
191        self.finished
192    }
193}