use crate::composition::{
ConditionNode, CooldownNode, ForceFailureNode, ForceSuccessNode, InverterNode, ParallelNode, RepeatNode, RetryNode,
SelectOrNode, SequenceNode, SubsumptionNode, TimeoutNode, WatchNode, WeightedSelectNode,
};
use crate::config::{BehaviorConfig, CompositionConfig};
use crate::registry::NodeRegistry;
use crate::BoxedBehavior;
use anyhow::{anyhow, Context as AnyhowContext, Result};
use serde_json::Value;
use std::path::{Path, PathBuf};
use tracing::{debug, info};
#[derive(Clone)]
pub struct BehaviorLoader {
registry: NodeRegistry,
}
impl BehaviorLoader {
pub fn new(registry: NodeRegistry) -> Self {
Self { registry }
}
pub fn load(&self, config: &BehaviorConfig) -> Result<BoxedBehavior> {
self.load_with_base_dir(config, None)
}
pub fn load_from_json(&self, json: &str) -> Result<BoxedBehavior> {
self.load(&BehaviorConfig::from_json(json)?)
}
pub fn load_from_file(&self, path: impl AsRef<Path>) -> Result<BoxedBehavior> {
let path = path.as_ref();
let config = BehaviorConfig::from_file(path)
.with_context(|| format!("Failed to read behavior config from {}", path.display()))?;
let base_dir = path.parent().map(Path::to_path_buf);
self.load_with_base_dir(&config, base_dir.as_deref())
}
fn load_with_base_dir(&self, config: &BehaviorConfig, base_dir: Option<&Path>) -> Result<BoxedBehavior> {
info!("Loading behavior tree: {}", config.name);
if let Some(desc) = &config.description {
debug!("Description: {}", desc);
}
self.load_composition(&config.root, config, base_dir)
.with_context(|| format!("Failed to load behavior tree '{}'", config.name))
}
fn load_composition(
&self,
composition: &CompositionConfig,
config: &BehaviorConfig,
base_dir: Option<&Path>,
) -> Result<BoxedBehavior> {
match composition {
CompositionConfig::Sequence { children, name } => {
debug!(
"Loading sequence node{}",
name.as_ref().map(|n| format!(" '{}'", n)).unwrap_or_default()
);
let child_nodes = children
.iter()
.enumerate()
.map(|(i, child)| {
self.load_composition(child, config, base_dir)
.with_context(|| format!("Failed to load sequence child {}", i))
})
.collect::<Result<Vec<_>>>()?;
debug!("Sequence node loaded with {} children", child_nodes.len());
Ok(Box::new(SequenceNode::new(child_nodes)))
}
CompositionConfig::Selector { children, name } => {
debug!(
"Loading selector node{}",
name.as_ref().map(|n| format!(" '{}'", n)).unwrap_or_default()
);
let child_nodes = children
.iter()
.enumerate()
.map(|(i, child)| {
self.load_composition(child, config, base_dir)
.with_context(|| format!("Failed to load selector child {}", i))
})
.collect::<Result<Vec<_>>>()?;
debug!("Selector node loaded with {} children", child_nodes.len());
Ok(Box::new(SelectOrNode::new(child_nodes)))
}
CompositionConfig::WeightedSelect { children, name } => {
debug!(
"Loading weighted_select node{} with {} children",
name.as_ref().map(|n| format!(" '{}'", n)).unwrap_or_default(),
children.len()
);
let weighted_children = children
.iter()
.enumerate()
.map(|(i, weighted_child)| {
let child_node = self
.load_composition(&weighted_child.node, config, base_dir)
.with_context(|| format!("Failed to load weighted_select child {}", i))?;
Ok((weighted_child.weight, child_node))
})
.collect::<Result<Vec<_>>>()?;
debug!("WeightedSelect node loaded with {} children", weighted_children.len());
Ok(Box::new(WeightedSelectNode::new(weighted_children)))
}
CompositionConfig::Parallel { children, policy, name } => {
debug!(
"Loading parallel node{} with policy {:?}",
name.as_ref().map(|n| format!(" '{}'", n)).unwrap_or_default(),
policy
);
let child_nodes = children
.iter()
.enumerate()
.map(|(i, child)| {
self.load_composition(child, config, base_dir)
.with_context(|| format!("Failed to load parallel child {}", i))
})
.collect::<Result<Vec<_>>>()?;
debug!("Parallel node loaded with {} children", child_nodes.len());
Ok(Box::new(ParallelNode::new(child_nodes, policy.clone().into())))
}
CompositionConfig::Watch { children, policy, name } => {
debug!(
"Loading watch node{} with policy {:?}",
name.as_ref().map(|n| format!(" '{}'", n)).unwrap_or_default(),
policy
);
let child_nodes = children
.iter()
.enumerate()
.map(|(i, child)| {
self.load_composition(child, config, base_dir)
.with_context(|| format!("Failed to load watch child {}", i))
})
.collect::<Result<Vec<_>>>()?;
debug!("Watch node loaded with {} children", child_nodes.len());
Ok(Box::new(WatchNode::new(child_nodes, policy.clone().into())))
}
CompositionConfig::Subsumption { layers, name } => {
debug!(
"Loading subsumption node{} with {} layers",
name.as_ref().map(|n| format!(" '{}'", n)).unwrap_or_default(),
layers.len()
);
let mut node = SubsumptionNode::default();
for (i, layer) in layers.iter().enumerate() {
let behavior = self
.load_composition(&layer.node, config, base_dir)
.with_context(|| format!("Failed to load subsumption layer {}", i))?;
let mut subsumption_layer = crate::composition::SubsumptionLayer::new(layer.priority, behavior);
if let Some(layer_name) = &layer.name {
subsumption_layer = subsumption_layer.with_name(layer_name.clone());
}
node.layers.push(subsumption_layer);
}
node.layers.sort_by_key(|l| std::cmp::Reverse(l.priority));
debug!("Subsumption node loaded with {} layers", node.layers.len());
Ok(Box::new(node))
}
CompositionConfig::Inverter { child, name } => {
debug!(
"Loading inverter node{}",
name.as_ref().map(|n| format!(" '{}'", n)).unwrap_or_default()
);
let child_node = self
.load_composition(child, config, base_dir)
.with_context(|| "Failed to load inverter child")?;
Ok(Box::new(InverterNode::new(child_node)))
}
CompositionConfig::ForceSuccess { child, name } => {
debug!(
"Loading force_success node{}",
name.as_ref().map(|n| format!(" '{}'", n)).unwrap_or_default()
);
let child_node = self
.load_composition(child, config, base_dir)
.with_context(|| "Failed to load force_success child")?;
Ok(Box::new(ForceSuccessNode::new(child_node)))
}
CompositionConfig::ForceFailure { child, name } => {
debug!(
"Loading force_failure node{}",
name.as_ref().map(|n| format!(" '{}'", n)).unwrap_or_default()
);
let child_node = self
.load_composition(child, config, base_dir)
.with_context(|| "Failed to load force_failure child")?;
Ok(Box::new(ForceFailureNode::new(child_node)))
}
CompositionConfig::Repeat { child, count, name } => {
debug!(
"Loading repeat node{} (count: {:?})",
name.as_ref().map(|n| format!(" '{}'", n)).unwrap_or_default(),
count
);
let child_node = self
.load_composition(child, config, base_dir)
.with_context(|| "Failed to load repeat child")?;
Ok(Box::new(RepeatNode::new(child_node, *count)))
}
CompositionConfig::Retry {
child,
max_attempts,
name,
} => {
debug!(
"Loading retry node{} (max_attempts: {})",
name.as_ref().map(|n| format!(" '{}'", n)).unwrap_or_default(),
max_attempts
);
let child_node = self
.load_composition(child, config, base_dir)
.with_context(|| "Failed to load retry child")?;
Ok(Box::new(RetryNode::new(child_node, *max_attempts)))
}
CompositionConfig::Timeout {
child,
timeout_secs,
name,
} => {
debug!(
"Loading timeout node{} (timeout_secs: {})",
name.as_ref().map(|n| format!(" '{}'", n)).unwrap_or_default(),
timeout_secs
);
let child_node = self
.load_composition(child, config, base_dir)
.with_context(|| "Failed to load timeout child")?;
Ok(Box::new(TimeoutNode::new(
child_node,
std::time::Duration::from_secs_f64(timeout_secs.max(0.0)),
)))
}
CompositionConfig::Cooldown {
child,
cooldown_secs,
name,
} => {
debug!(
"Loading cooldown node{} (cooldown_secs: {})",
name.as_ref().map(|n| format!(" '{}'", n)).unwrap_or_default(),
cooldown_secs
);
let child_node = self
.load_composition(child, config, base_dir)
.with_context(|| "Failed to load cooldown child")?;
Ok(Box::new(CooldownNode::new(
child_node,
std::time::Duration::from_secs_f64(cooldown_secs.max(0.0)),
)))
}
CompositionConfig::Condition { condition, child, name } => {
debug!(
"Loading condition node{} ({:?})",
name.as_ref().map(|n| format!(" '{}'", n)).unwrap_or_default(),
condition
);
let child_node = self
.load_composition(child, config, base_dir)
.with_context(|| "Failed to load condition child")?;
Ok(Box::new(ConditionNode::new(condition.clone().into(), child_node)))
}
CompositionConfig::Include { path, name } => {
debug!(
"Loading include node{} (path: {})",
name.as_ref().map(|n| format!(" '{}'", n)).unwrap_or_default(),
path
);
let dir = base_dir.ok_or_else(|| {
anyhow!(
"Cannot resolve 'include' path '{}' - the behavior tree was not loaded from a file \
(use BehaviorLoader::load_from_file, not load/load_from_json, for a tree that uses 'include')",
path
)
})?;
let fragment_path: PathBuf = dir.join(path);
let fragment_content = std::fs::read_to_string(&fragment_path)
.with_context(|| format!("Failed to read include fragment '{}'", fragment_path.display()))?;
let fragment: CompositionConfig = serde_json::from_str(&fragment_content).with_context(|| {
format!(
"Failed to parse include fragment '{}' as a CompositionConfig",
fragment_path.display()
)
})?;
let fragment_base_dir = fragment_path.parent().map(Path::to_path_buf);
self.load_composition(&fragment, config, fragment_base_dir.as_deref())
.with_context(|| format!("Failed to load include fragment '{}'", fragment_path.display()))
}
CompositionConfig::Node {
node,
config_ref,
config: inline_config,
name,
} => {
debug!(
"Loading leaf node '{}' (type: {}){}",
name.as_ref().unwrap_or(node),
node,
config_ref
.as_ref()
.map(|r| format!(" with config_ref '{}'", r))
.unwrap_or_default()
);
let node_config = if let Some(inline) = inline_config {
debug!("Using inline config for node '{}'", node);
inline.clone()
} else if let Some(ref_key) = config_ref {
config
.get_config(ref_key)
.ok_or_else(|| {
anyhow!(
"Config reference '{}' not found in configs map for node '{}'",
ref_key,
node
)
})?
.clone()
} else {
debug!("No config provided for node '{}'", node);
Value::Null
};
self.registry
.create(node, node_config)
.with_context(|| format!("Failed to create node '{}'", node))
}
}
}
pub fn registry(&self) -> &NodeRegistry {
&self.registry
}
}
impl std::fmt::Debug for BehaviorLoader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BehaviorLoader")
.field("registry", &self.registry)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn load_real_seed_behavior(name: &str) -> BoxedBehavior {
let mut registry = NodeRegistry::new();
crate::register_builtin_actions(&mut registry);
let loader = BehaviorLoader::new(registry);
let config_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/seed_behaviors")
.join(name)
.join("config.json");
loader
.load_from_file(&config_path)
.unwrap_or_else(|e| panic!("{} should load: {:?}", config_path.display(), e))
}
#[test]
fn find_target_seed_behavior_loads_with_the_emergency_stop_interrupt_layer() {
load_real_seed_behavior("find_target");
}
#[test]
fn patrol_seed_behavior_loads_with_the_emergency_stop_interrupt_layer() {
load_real_seed_behavior("patrol");
}
}