Skip to main content

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    // duplicate: false positive — `load_from_json`/`load_from_file` are already the thinnest
163    // possible named wrappers around the shared `self.load(...)` logic; they differ in the
164    // `BehaviorConfig` constructor they delegate to (parse a JSON string vs. read a file path),
165    // which is the actual behavior each name promises, not incidental drift to squeeze out.
166    pub fn load_from_json(&self, json: &str) -> Result<BoxedBehavior> {
167        self.load(&BehaviorConfig::from_json(json)?)
168    }
169
170    /// Load a behavior tree from a JSON file.
171    ///
172    /// This is a convenience method that reads a file and loads the behavior tree.
173    ///
174    /// # Arguments
175    ///
176    /// * `path` - Path to the JSON file
177    ///
178    /// # Example
179    ///
180    /// ```rust,no_run
181    /// use mecha10_behavior_runtime::{NodeRegistry, BehaviorLoader};
182    ///
183    /// # let registry = NodeRegistry::new();
184    /// let loader = BehaviorLoader::new(registry);
185    /// let behavior = loader.load_from_file("configs/behaviors/@mecha10/patrol/config.json").unwrap();
186    /// ```
187    pub fn load_from_file(&self, path: impl AsRef<std::path::Path>) -> Result<BoxedBehavior> {
188        self.load(&BehaviorConfig::from_file(path)?)
189    }
190
191    /// Recursively load a composition node and its children.
192    fn load_composition(&self, composition: &CompositionConfig, config: &BehaviorConfig) -> Result<BoxedBehavior> {
193        match composition {
194            CompositionConfig::Sequence { children, name } => {
195                debug!(
196                    "Loading sequence node{}",
197                    name.as_ref().map(|n| format!(" '{}'", n)).unwrap_or_default()
198                );
199
200                let child_nodes = children
201                    .iter()
202                    .enumerate()
203                    .map(|(i, child)| {
204                        self.load_composition(child, config)
205                            .with_context(|| format!("Failed to load sequence child {}", i))
206                    })
207                    .collect::<Result<Vec<_>>>()?;
208
209                debug!("Sequence node loaded with {} children", child_nodes.len());
210                Ok(Box::new(SequenceNode::new(child_nodes)))
211            }
212
213            CompositionConfig::Selector { children, name } => {
214                debug!(
215                    "Loading selector node{}",
216                    name.as_ref().map(|n| format!(" '{}'", n)).unwrap_or_default()
217                );
218
219                let child_nodes = children
220                    .iter()
221                    .enumerate()
222                    .map(|(i, child)| {
223                        self.load_composition(child, config)
224                            .with_context(|| format!("Failed to load selector child {}", i))
225                    })
226                    .collect::<Result<Vec<_>>>()?;
227
228                debug!("Selector node loaded with {} children", child_nodes.len());
229                Ok(Box::new(SelectOrNode::new(child_nodes)))
230            }
231
232            CompositionConfig::Parallel { children, policy, name } => {
233                debug!(
234                    "Loading parallel node{} with policy {:?}",
235                    name.as_ref().map(|n| format!(" '{}'", n)).unwrap_or_default(),
236                    policy
237                );
238
239                let child_nodes = children
240                    .iter()
241                    .enumerate()
242                    .map(|(i, child)| {
243                        self.load_composition(child, config)
244                            .with_context(|| format!("Failed to load parallel child {}", i))
245                    })
246                    .collect::<Result<Vec<_>>>()?;
247
248                debug!("Parallel node loaded with {} children", child_nodes.len());
249                Ok(Box::new(ParallelNode::new(child_nodes, policy.clone().into())))
250            }
251
252            CompositionConfig::Node {
253                node,
254                config_ref,
255                config: inline_config,
256                name,
257            } => {
258                debug!(
259                    "Loading leaf node '{}' (type: {}){}",
260                    name.as_ref().unwrap_or(node),
261                    node,
262                    config_ref
263                        .as_ref()
264                        .map(|r| format!(" with config_ref '{}'", r))
265                        .unwrap_or_default()
266                );
267
268                let node_config = if let Some(inline) = inline_config {
269                    debug!("Using inline config for node '{}'", node);
270                    inline.clone()
271                } else if let Some(ref_key) = config_ref {
272                    config
273                        .get_config(ref_key)
274                        .ok_or_else(|| {
275                            anyhow!(
276                                "Config reference '{}' not found in configs map for node '{}'",
277                                ref_key,
278                                node
279                            )
280                        })?
281                        .clone()
282                } else {
283                    debug!("No config provided for node '{}'", node);
284                    Value::Null
285                };
286
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
303// duplicate: false positive — see the note on `CircuitBreaker`'s manual `Debug` impl in
304// packages/core/src/circuit_breaker.rs.
305impl std::fmt::Debug for BehaviorLoader {
306    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
307        f.debug_struct("BehaviorLoader")
308            .field("registry", &self.registry)
309            .finish()
310    }
311}