1use crate::composition::{
6 ConditionNode, CooldownNode, ForceFailureNode, ForceSuccessNode, InverterNode, ParallelNode, RepeatNode, RetryNode,
7 SelectOrNode, SequenceNode, SubsumptionNode, TimeoutNode, WatchNode, WeightedSelectNode,
8};
9use crate::config::{BehaviorConfig, CompositionConfig};
10use crate::registry::NodeRegistry;
11use crate::BoxedBehavior;
12use anyhow::{anyhow, Context as AnyhowContext, Result};
13use serde_json::Value;
14use std::path::{Path, PathBuf};
15use tracing::{debug, info};
16
17#[derive(Clone)]
18pub struct BehaviorLoader {
19 registry: NodeRegistry,
20}
21
22impl BehaviorLoader {
23 pub fn new(registry: NodeRegistry) -> Self {
25 Self { registry }
26 }
27
28 pub fn load(&self, config: &BehaviorConfig) -> Result<BoxedBehavior> {
34 self.load_with_base_dir(config, None)
35 }
36
37 pub fn load_from_json(&self, json: &str) -> Result<BoxedBehavior> {
40 self.load(&BehaviorConfig::from_json(json)?)
41 }
42
43 pub fn load_from_file(&self, path: impl AsRef<Path>) -> Result<BoxedBehavior> {
47 let path = path.as_ref();
48 let config = BehaviorConfig::from_file(path)
49 .with_context(|| format!("Failed to read behavior config from {}", path.display()))?;
50 let base_dir = path.parent().map(Path::to_path_buf);
51 self.load_with_base_dir(&config, base_dir.as_deref())
52 }
53
54 fn load_with_base_dir(&self, config: &BehaviorConfig, base_dir: Option<&Path>) -> Result<BoxedBehavior> {
55 info!("Loading behavior tree: {}", config.name);
56 if let Some(desc) = &config.description {
57 debug!("Description: {}", desc);
58 }
59
60 self.load_composition(&config.root, config, base_dir)
61 .with_context(|| format!("Failed to load behavior tree '{}'", config.name))
62 }
63
64 fn load_composition(
69 &self,
70 composition: &CompositionConfig,
71 config: &BehaviorConfig,
72 base_dir: Option<&Path>,
73 ) -> Result<BoxedBehavior> {
74 match composition {
75 CompositionConfig::Sequence { children, name } => {
76 debug!(
77 "Loading sequence node{}",
78 name.as_ref().map(|n| format!(" '{}'", n)).unwrap_or_default()
79 );
80
81 let child_nodes = children
82 .iter()
83 .enumerate()
84 .map(|(i, child)| {
85 self.load_composition(child, config, base_dir)
86 .with_context(|| format!("Failed to load sequence child {}", i))
87 })
88 .collect::<Result<Vec<_>>>()?;
89
90 debug!("Sequence node loaded with {} children", child_nodes.len());
91 Ok(Box::new(SequenceNode::new(child_nodes)))
92 }
93
94 CompositionConfig::Selector { children, name } => {
95 debug!(
96 "Loading selector node{}",
97 name.as_ref().map(|n| format!(" '{}'", n)).unwrap_or_default()
98 );
99
100 let child_nodes = children
101 .iter()
102 .enumerate()
103 .map(|(i, child)| {
104 self.load_composition(child, config, base_dir)
105 .with_context(|| format!("Failed to load selector child {}", i))
106 })
107 .collect::<Result<Vec<_>>>()?;
108
109 debug!("Selector node loaded with {} children", child_nodes.len());
110 Ok(Box::new(SelectOrNode::new(child_nodes)))
111 }
112
113 CompositionConfig::WeightedSelect { children, name } => {
114 debug!(
115 "Loading weighted_select node{} with {} children",
116 name.as_ref().map(|n| format!(" '{}'", n)).unwrap_or_default(),
117 children.len()
118 );
119
120 let weighted_children = children
121 .iter()
122 .enumerate()
123 .map(|(i, weighted_child)| {
124 let child_node = self
125 .load_composition(&weighted_child.node, config, base_dir)
126 .with_context(|| format!("Failed to load weighted_select child {}", i))?;
127 Ok((weighted_child.weight, child_node))
128 })
129 .collect::<Result<Vec<_>>>()?;
130
131 debug!("WeightedSelect node loaded with {} children", weighted_children.len());
132 Ok(Box::new(WeightedSelectNode::new(weighted_children)))
133 }
134
135 CompositionConfig::Parallel { children, policy, name } => {
136 debug!(
137 "Loading parallel node{} with policy {:?}",
138 name.as_ref().map(|n| format!(" '{}'", n)).unwrap_or_default(),
139 policy
140 );
141
142 let child_nodes = children
143 .iter()
144 .enumerate()
145 .map(|(i, child)| {
146 self.load_composition(child, config, base_dir)
147 .with_context(|| format!("Failed to load parallel child {}", i))
148 })
149 .collect::<Result<Vec<_>>>()?;
150
151 debug!("Parallel node loaded with {} children", child_nodes.len());
152 Ok(Box::new(ParallelNode::new(child_nodes, policy.clone().into())))
153 }
154
155 CompositionConfig::Watch { children, policy, name } => {
156 debug!(
157 "Loading watch node{} with policy {:?}",
158 name.as_ref().map(|n| format!(" '{}'", n)).unwrap_or_default(),
159 policy
160 );
161
162 let child_nodes = children
163 .iter()
164 .enumerate()
165 .map(|(i, child)| {
166 self.load_composition(child, config, base_dir)
167 .with_context(|| format!("Failed to load watch child {}", i))
168 })
169 .collect::<Result<Vec<_>>>()?;
170
171 debug!("Watch node loaded with {} children", child_nodes.len());
172 Ok(Box::new(WatchNode::new(child_nodes, policy.clone().into())))
173 }
174
175 CompositionConfig::Subsumption { layers, name } => {
176 debug!(
177 "Loading subsumption node{} with {} layers",
178 name.as_ref().map(|n| format!(" '{}'", n)).unwrap_or_default(),
179 layers.len()
180 );
181
182 let mut node = SubsumptionNode::default();
183 for (i, layer) in layers.iter().enumerate() {
184 let behavior = self
185 .load_composition(&layer.node, config, base_dir)
186 .with_context(|| format!("Failed to load subsumption layer {}", i))?;
187 let mut subsumption_layer = crate::composition::SubsumptionLayer::new(layer.priority, behavior);
188 if let Some(layer_name) = &layer.name {
189 subsumption_layer = subsumption_layer.with_name(layer_name.clone());
190 }
191 node.layers.push(subsumption_layer);
192 }
193 node.layers.sort_by_key(|l| std::cmp::Reverse(l.priority));
194
195 debug!("Subsumption node loaded with {} layers", node.layers.len());
196 Ok(Box::new(node))
197 }
198
199 CompositionConfig::Inverter { child, name } => {
200 debug!(
201 "Loading inverter node{}",
202 name.as_ref().map(|n| format!(" '{}'", n)).unwrap_or_default()
203 );
204
205 let child_node = self
206 .load_composition(child, config, base_dir)
207 .with_context(|| "Failed to load inverter child")?;
208 Ok(Box::new(InverterNode::new(child_node)))
209 }
210
211 CompositionConfig::ForceSuccess { child, name } => {
212 debug!(
213 "Loading force_success node{}",
214 name.as_ref().map(|n| format!(" '{}'", n)).unwrap_or_default()
215 );
216
217 let child_node = self
218 .load_composition(child, config, base_dir)
219 .with_context(|| "Failed to load force_success child")?;
220 Ok(Box::new(ForceSuccessNode::new(child_node)))
221 }
222
223 CompositionConfig::ForceFailure { child, name } => {
224 debug!(
225 "Loading force_failure node{}",
226 name.as_ref().map(|n| format!(" '{}'", n)).unwrap_or_default()
227 );
228
229 let child_node = self
230 .load_composition(child, config, base_dir)
231 .with_context(|| "Failed to load force_failure child")?;
232 Ok(Box::new(ForceFailureNode::new(child_node)))
233 }
234
235 CompositionConfig::Repeat { child, count, name } => {
236 debug!(
237 "Loading repeat node{} (count: {:?})",
238 name.as_ref().map(|n| format!(" '{}'", n)).unwrap_or_default(),
239 count
240 );
241
242 let child_node = self
243 .load_composition(child, config, base_dir)
244 .with_context(|| "Failed to load repeat child")?;
245 Ok(Box::new(RepeatNode::new(child_node, *count)))
246 }
247
248 CompositionConfig::Retry {
249 child,
250 max_attempts,
251 name,
252 } => {
253 debug!(
254 "Loading retry node{} (max_attempts: {})",
255 name.as_ref().map(|n| format!(" '{}'", n)).unwrap_or_default(),
256 max_attempts
257 );
258
259 let child_node = self
260 .load_composition(child, config, base_dir)
261 .with_context(|| "Failed to load retry child")?;
262 Ok(Box::new(RetryNode::new(child_node, *max_attempts)))
263 }
264
265 CompositionConfig::Timeout {
266 child,
267 timeout_secs,
268 name,
269 } => {
270 debug!(
271 "Loading timeout node{} (timeout_secs: {})",
272 name.as_ref().map(|n| format!(" '{}'", n)).unwrap_or_default(),
273 timeout_secs
274 );
275
276 let child_node = self
277 .load_composition(child, config, base_dir)
278 .with_context(|| "Failed to load timeout child")?;
279 Ok(Box::new(TimeoutNode::new(
280 child_node,
281 std::time::Duration::from_secs_f64(timeout_secs.max(0.0)),
282 )))
283 }
284
285 CompositionConfig::Cooldown {
286 child,
287 cooldown_secs,
288 name,
289 } => {
290 debug!(
291 "Loading cooldown node{} (cooldown_secs: {})",
292 name.as_ref().map(|n| format!(" '{}'", n)).unwrap_or_default(),
293 cooldown_secs
294 );
295
296 let child_node = self
297 .load_composition(child, config, base_dir)
298 .with_context(|| "Failed to load cooldown child")?;
299 Ok(Box::new(CooldownNode::new(
300 child_node,
301 std::time::Duration::from_secs_f64(cooldown_secs.max(0.0)),
302 )))
303 }
304
305 CompositionConfig::Condition { condition, child, name } => {
306 debug!(
307 "Loading condition node{} ({:?})",
308 name.as_ref().map(|n| format!(" '{}'", n)).unwrap_or_default(),
309 condition
310 );
311
312 let child_node = self
313 .load_composition(child, config, base_dir)
314 .with_context(|| "Failed to load condition child")?;
315 Ok(Box::new(ConditionNode::new(condition.clone().into(), child_node)))
316 }
317
318 CompositionConfig::Include { path, name } => {
319 debug!(
320 "Loading include node{} (path: {})",
321 name.as_ref().map(|n| format!(" '{}'", n)).unwrap_or_default(),
322 path
323 );
324
325 let dir = base_dir.ok_or_else(|| {
326 anyhow!(
327 "Cannot resolve 'include' path '{}' - the behavior tree was not loaded from a file \
328 (use BehaviorLoader::load_from_file, not load/load_from_json, for a tree that uses 'include')",
329 path
330 )
331 })?;
332
333 let fragment_path: PathBuf = dir.join(path);
334 let fragment_content = std::fs::read_to_string(&fragment_path)
335 .with_context(|| format!("Failed to read include fragment '{}'", fragment_path.display()))?;
336 let fragment: CompositionConfig = serde_json::from_str(&fragment_content).with_context(|| {
337 format!(
338 "Failed to parse include fragment '{}' as a CompositionConfig",
339 fragment_path.display()
340 )
341 })?;
342
343 let fragment_base_dir = fragment_path.parent().map(Path::to_path_buf);
344 self.load_composition(&fragment, config, fragment_base_dir.as_deref())
345 .with_context(|| format!("Failed to load include fragment '{}'", fragment_path.display()))
346 }
347
348 CompositionConfig::Node {
349 node,
350 config_ref,
351 config: inline_config,
352 name,
353 } => {
354 debug!(
355 "Loading leaf node '{}' (type: {}){}",
356 name.as_ref().unwrap_or(node),
357 node,
358 config_ref
359 .as_ref()
360 .map(|r| format!(" with config_ref '{}'", r))
361 .unwrap_or_default()
362 );
363
364 let node_config = if let Some(inline) = inline_config {
365 debug!("Using inline config for node '{}'", node);
366 inline.clone()
367 } else if let Some(ref_key) = config_ref {
368 config
369 .get_config(ref_key)
370 .ok_or_else(|| {
371 anyhow!(
372 "Config reference '{}' not found in configs map for node '{}'",
373 ref_key,
374 node
375 )
376 })?
377 .clone()
378 } else {
379 debug!("No config provided for node '{}'", node);
380 Value::Null
381 };
382
383 self.registry
384 .create(node, node_config)
385 .with_context(|| format!("Failed to create node '{}'", node))
386 }
387 }
388 }
389
390 pub fn registry(&self) -> &NodeRegistry {
392 &self.registry
393 }
394}
395
396impl std::fmt::Debug for BehaviorLoader {
397 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
398 f.debug_struct("BehaviorLoader")
399 .field("registry", &self.registry)
400 .finish()
401 }
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407
408 fn load_real_seed_behavior(name: &str) -> BoxedBehavior {
418 let mut registry = NodeRegistry::new();
419 crate::register_builtin_actions(&mut registry);
420 let loader = BehaviorLoader::new(registry);
421
422 let config_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
423 .join("tests/fixtures/seed_behaviors")
424 .join(name)
425 .join("config.json");
426
427 loader
428 .load_from_file(&config_path)
429 .unwrap_or_else(|e| panic!("{} should load: {:?}", config_path.display(), e))
430 }
431
432 #[test]
433 fn find_target_seed_behavior_loads_with_the_emergency_stop_interrupt_layer() {
434 load_real_seed_behavior("find_target");
435 }
436
437 #[test]
438 fn patrol_seed_behavior_loads_with_the_emergency_stop_interrupt_layer() {
439 load_real_seed_behavior("patrol");
440 }
441}