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}