mecha10_behavior_runtime/loader.rs
1//! Behavior tree loader for creating trees from JSON configuration
2//!
3//! This module provides the `BehaviorLoader` for loading behavior trees from
4//! `BehaviorConfig` definitions using a `NodeRegistry`.
5
6use crate::composition::{ParallelNode, SelectOrNode, SequenceNode};
7use crate::config::{BehaviorConfig, CompositionConfig};
8use crate::registry::NodeRegistry;
9use crate::BoxedBehavior;
10use anyhow::{anyhow, Context as AnyhowContext, Result};
11use serde_json::Value;
12use tracing::{debug, info};
13
14/// Loader for creating behavior trees from JSON configuration.
15///
16/// The `BehaviorLoader` takes a `BehaviorConfig` (which can be loaded from JSON)
17/// and uses a `NodeRegistry` to instantiate the complete behavior tree.
18///
19/// # Features
20///
21/// - Recursive composition parsing (sequence/selector/parallel nodes)
22/// - Config reference resolution from the config map
23/// - Inline config support
24/// - Clear error messages for missing node types
25///
26/// # Example
27///
28/// ```rust
29/// use mecha10_behavior_runtime::prelude::*;
30/// use mecha10_behavior_runtime::{NodeRegistry, BehaviorLoader};
31/// use serde_json::json;
32///
33/// # #[derive(Debug)]
34/// # struct MyBehavior;
35/// # #[async_trait::async_trait]
36/// # impl BehaviorNode for MyBehavior {
37/// # async fn tick(&mut self, _ctx: &Context) -> anyhow::Result<NodeStatus> {
38/// # Ok(NodeStatus::Success)
39/// # }
40/// # }
41///
42/// // Create and populate registry
43/// let mut registry = NodeRegistry::new();
44/// registry.register("my_behavior", |_| Ok(Box::new(MyBehavior)));
45///
46/// // Load behavior tree from JSON
47/// let json = r#"{
48/// "name": "test_behavior",
49/// "root": {
50/// "type": "sequence",
51/// "children": [
52/// {
53/// "type": "node",
54/// "node": "my_behavior"
55/// }
56/// ]
57/// }
58/// }"#;
59///
60/// let config = BehaviorConfig::from_json(json).unwrap();
61/// let loader = BehaviorLoader::new(registry);
62/// let behavior = loader.load(&config).unwrap();
63/// ```
64#[derive(Clone)]
65pub struct BehaviorLoader {
66 registry: NodeRegistry,
67}
68
69impl BehaviorLoader {
70 /// Create a new behavior loader with the given node registry.
71 ///
72 /// # Arguments
73 ///
74 /// * `registry` - The node registry containing registered node types
75 ///
76 /// # Example
77 ///
78 /// ```rust
79 /// use mecha10_behavior_runtime::{NodeRegistry, BehaviorLoader};
80 ///
81 /// let registry = NodeRegistry::new();
82 /// let loader = BehaviorLoader::new(registry);
83 /// ```
84 pub fn new(registry: NodeRegistry) -> Self {
85 Self { registry }
86 }
87
88 /// Load a behavior tree from a configuration.
89 ///
90 /// This is the main entry point for loading behavior trees. It takes a
91 /// `BehaviorConfig` (typically loaded from JSON) and recursively instantiates
92 /// all nodes in the tree.
93 ///
94 /// # Arguments
95 ///
96 /// * `config` - The behavior configuration to load
97 ///
98 /// # Returns
99 ///
100 /// A boxed behavior node representing the root of the tree, or an error if:
101 /// - A referenced node type is not registered
102 /// - A config reference is invalid
103 /// - A factory function fails
104 ///
105 /// # Example
106 ///
107 /// ```rust
108 /// use mecha10_behavior_runtime::{BehaviorConfig, NodeRegistry, BehaviorLoader};
109 ///
110 /// # let mut registry = NodeRegistry::new();
111 /// # registry.register("test", |_| Ok(Box::new(mecha10_behavior_runtime::SequenceNode::new(vec![]))));
112 /// let loader = BehaviorLoader::new(registry);
113 ///
114 /// let json = r#"{
115 /// "name": "test",
116 /// "root": {
117 /// "type": "node",
118 /// "node": "test"
119 /// }
120 /// }"#;
121 ///
122 /// let config = BehaviorConfig::from_json(json).unwrap();
123 /// let behavior = loader.load(&config).unwrap();
124 /// ```
125 pub fn load(&self, config: &BehaviorConfig) -> Result<BoxedBehavior> {
126 info!("Loading behavior tree: {}", config.name);
127 if let Some(desc) = &config.description {
128 debug!("Description: {}", desc);
129 }
130
131 self.load_composition(&config.root, config)
132 .with_context(|| format!("Failed to load behavior tree '{}'", config.name))
133 }
134
135 /// Load a behavior tree from a JSON string.
136 ///
137 /// This is a convenience method that combines parsing and loading.
138 ///
139 /// # Arguments
140 ///
141 /// * `json` - JSON string containing a BehaviorConfig
142 ///
143 /// # Example
144 ///
145 /// ```rust
146 /// use mecha10_behavior_runtime::{NodeRegistry, BehaviorLoader};
147 ///
148 /// # let mut registry = NodeRegistry::new();
149 /// # registry.register("test", |_| Ok(Box::new(mecha10_behavior_runtime::SequenceNode::new(vec![]))));
150 /// let loader = BehaviorLoader::new(registry);
151 ///
152 /// let json = r#"{
153 /// "name": "test",
154 /// "root": {
155 /// "type": "node",
156 /// "node": "test"
157 /// }
158 /// }"#;
159 ///
160 /// let behavior = loader.load_from_json(json).unwrap();
161 /// ```
162 pub fn load_from_json(&self, json: &str) -> Result<BoxedBehavior> {
163 let config = BehaviorConfig::from_json(json)?;
164 self.load(&config)
165 }
166
167 /// Load a behavior tree from a JSON file.
168 ///
169 /// This is a convenience method that reads a file and loads the behavior tree.
170 ///
171 /// # Arguments
172 ///
173 /// * `path` - Path to the JSON file
174 ///
175 /// # Example
176 ///
177 /// ```rust,no_run
178 /// use mecha10_behavior_runtime::{NodeRegistry, BehaviorLoader};
179 ///
180 /// # let registry = NodeRegistry::new();
181 /// let loader = BehaviorLoader::new(registry);
182 /// let behavior = loader.load_from_file("behaviors/patrol.json").unwrap();
183 /// ```
184 pub fn load_from_file(&self, path: impl AsRef<std::path::Path>) -> Result<BoxedBehavior> {
185 let config = BehaviorConfig::from_file(path)?;
186 self.load(&config)
187 }
188
189 /// Recursively load a composition node and its children.
190 fn load_composition(&self, composition: &CompositionConfig, config: &BehaviorConfig) -> Result<BoxedBehavior> {
191 match composition {
192 CompositionConfig::Sequence { children, name } => {
193 debug!(
194 "Loading sequence node{}",
195 name.as_ref().map(|n| format!(" '{}'", n)).unwrap_or_default()
196 );
197
198 let child_nodes = children
199 .iter()
200 .enumerate()
201 .map(|(i, child)| {
202 self.load_composition(child, config)
203 .with_context(|| format!("Failed to load sequence child {}", i))
204 })
205 .collect::<Result<Vec<_>>>()?;
206
207 debug!("Sequence node loaded with {} children", child_nodes.len());
208 Ok(Box::new(SequenceNode::new(child_nodes)))
209 }
210
211 CompositionConfig::Selector { children, name } => {
212 debug!(
213 "Loading selector node{}",
214 name.as_ref().map(|n| format!(" '{}'", n)).unwrap_or_default()
215 );
216
217 let child_nodes = children
218 .iter()
219 .enumerate()
220 .map(|(i, child)| {
221 self.load_composition(child, config)
222 .with_context(|| format!("Failed to load selector child {}", i))
223 })
224 .collect::<Result<Vec<_>>>()?;
225
226 debug!("Selector node loaded with {} children", child_nodes.len());
227 Ok(Box::new(SelectOrNode::new(child_nodes)))
228 }
229
230 CompositionConfig::Parallel { children, policy, name } => {
231 debug!(
232 "Loading parallel node{} with policy {:?}",
233 name.as_ref().map(|n| format!(" '{}'", n)).unwrap_or_default(),
234 policy
235 );
236
237 let child_nodes = children
238 .iter()
239 .enumerate()
240 .map(|(i, child)| {
241 self.load_composition(child, config)
242 .with_context(|| format!("Failed to load parallel child {}", i))
243 })
244 .collect::<Result<Vec<_>>>()?;
245
246 debug!("Parallel node loaded with {} children", child_nodes.len());
247 Ok(Box::new(ParallelNode::new(child_nodes, policy.clone().into())))
248 }
249
250 CompositionConfig::Node {
251 node,
252 config_ref,
253 config: inline_config,
254 name,
255 } => {
256 debug!(
257 "Loading leaf node '{}' (type: {}){}",
258 name.as_ref().unwrap_or(node),
259 node,
260 config_ref
261 .as_ref()
262 .map(|r| format!(" with config_ref '{}'", r))
263 .unwrap_or_default()
264 );
265
266 // Resolve configuration (prefer inline over config_ref)
267 let node_config = if let Some(inline) = inline_config {
268 debug!("Using inline config for node '{}'", node);
269 inline.clone()
270 } else if let Some(ref_key) = config_ref {
271 config
272 .get_config(ref_key)
273 .ok_or_else(|| {
274 anyhow!(
275 "Config reference '{}' not found in configs map for node '{}'",
276 ref_key,
277 node
278 )
279 })?
280 .clone()
281 } else {
282 debug!("No config provided for node '{}'", node);
283 Value::Null
284 };
285
286 // Create the node using the registry
287 self.registry
288 .create(node, node_config)
289 .with_context(|| format!("Failed to create node '{}'", node))
290 }
291 }
292 }
293
294 /// Get a reference to the underlying node registry.
295 ///
296 /// This can be useful for inspecting registered types or adding new types
297 /// after the loader has been created.
298 pub fn registry(&self) -> &NodeRegistry {
299 &self.registry
300 }
301}
302
303impl std::fmt::Debug for BehaviorLoader {
304 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
305 f.debug_struct("BehaviorLoader")
306 .field("registry", &self.registry)
307 .finish()
308 }
309}