mecha10-behavior-runtime 0.1.23

Behavior tree runtime for Mecha10 - unified AI and logic composition system
Documentation
//! Behavior tree loader for creating trees from JSON configuration
//!
//! This module provides the `BehaviorLoader` for loading behavior trees from
//! `BehaviorConfig` definitions using a `NodeRegistry`.

use crate::composition::{ParallelNode, SelectOrNode, SequenceNode};
use crate::config::{BehaviorConfig, CompositionConfig};
use crate::registry::NodeRegistry;
use crate::BoxedBehavior;
use anyhow::{anyhow, Context as AnyhowContext, Result};
use serde_json::Value;
use tracing::{debug, info};

/// Loader for creating behavior trees from JSON configuration.
///
/// The `BehaviorLoader` takes a `BehaviorConfig` (which can be loaded from JSON)
/// and uses a `NodeRegistry` to instantiate the complete behavior tree.
///
/// # Features
///
/// - Recursive composition parsing (sequence/selector/parallel nodes)
/// - Config reference resolution from the config map
/// - Inline config support
/// - Clear error messages for missing node types
///
/// # Example
///
/// ```rust
/// use mecha10_behavior_runtime::prelude::*;
/// use mecha10_behavior_runtime::{NodeRegistry, BehaviorLoader};
/// use serde_json::json;
///
/// # #[derive(Debug)]
/// # struct MyBehavior;
/// # #[async_trait::async_trait]
/// # impl BehaviorNode for MyBehavior {
/// #     async fn tick(&mut self, _ctx: &Context) -> anyhow::Result<NodeStatus> {
/// #         Ok(NodeStatus::Success)
/// #     }
/// # }
///
/// // Create and populate registry
/// let mut registry = NodeRegistry::new();
/// registry.register("my_behavior", |_| Ok(Box::new(MyBehavior)));
///
/// // Load behavior tree from JSON
/// let json = r#"{
///     "name": "test_behavior",
///     "root": {
///         "type": "sequence",
///         "children": [
///             {
///                 "type": "node",
///                 "node": "my_behavior"
///             }
///         ]
///     }
/// }"#;
///
/// let config = BehaviorConfig::from_json(json).unwrap();
/// let loader = BehaviorLoader::new(registry);
/// let behavior = loader.load(&config).unwrap();
/// ```
#[derive(Clone)]
pub struct BehaviorLoader {
    registry: NodeRegistry,
}

impl BehaviorLoader {
    /// Create a new behavior loader with the given node registry.
    ///
    /// # Arguments
    ///
    /// * `registry` - The node registry containing registered node types
    ///
    /// # Example
    ///
    /// ```rust
    /// use mecha10_behavior_runtime::{NodeRegistry, BehaviorLoader};
    ///
    /// let registry = NodeRegistry::new();
    /// let loader = BehaviorLoader::new(registry);
    /// ```
    pub fn new(registry: NodeRegistry) -> Self {
        Self { registry }
    }

    /// Load a behavior tree from a configuration.
    ///
    /// This is the main entry point for loading behavior trees. It takes a
    /// `BehaviorConfig` (typically loaded from JSON) and recursively instantiates
    /// all nodes in the tree.
    ///
    /// # Arguments
    ///
    /// * `config` - The behavior configuration to load
    ///
    /// # Returns
    ///
    /// A boxed behavior node representing the root of the tree, or an error if:
    /// - A referenced node type is not registered
    /// - A config reference is invalid
    /// - A factory function fails
    ///
    /// # Example
    ///
    /// ```rust
    /// use mecha10_behavior_runtime::{BehaviorConfig, NodeRegistry, BehaviorLoader};
    ///
    /// # let mut registry = NodeRegistry::new();
    /// # registry.register("test", |_| Ok(Box::new(mecha10_behavior_runtime::SequenceNode::new(vec![]))));
    /// let loader = BehaviorLoader::new(registry);
    ///
    /// let json = r#"{
    ///     "name": "test",
    ///     "root": {
    ///         "type": "node",
    ///         "node": "test"
    ///     }
    /// }"#;
    ///
    /// let config = BehaviorConfig::from_json(json).unwrap();
    /// let behavior = loader.load(&config).unwrap();
    /// ```
    pub fn load(&self, config: &BehaviorConfig) -> Result<BoxedBehavior> {
        info!("Loading behavior tree: {}", config.name);
        if let Some(desc) = &config.description {
            debug!("Description: {}", desc);
        }

        self.load_composition(&config.root, config)
            .with_context(|| format!("Failed to load behavior tree '{}'", config.name))
    }

    /// Load a behavior tree from a JSON string.
    ///
    /// This is a convenience method that combines parsing and loading.
    ///
    /// # Arguments
    ///
    /// * `json` - JSON string containing a BehaviorConfig
    ///
    /// # Example
    ///
    /// ```rust
    /// use mecha10_behavior_runtime::{NodeRegistry, BehaviorLoader};
    ///
    /// # let mut registry = NodeRegistry::new();
    /// # registry.register("test", |_| Ok(Box::new(mecha10_behavior_runtime::SequenceNode::new(vec![]))));
    /// let loader = BehaviorLoader::new(registry);
    ///
    /// let json = r#"{
    ///     "name": "test",
    ///     "root": {
    ///         "type": "node",
    ///         "node": "test"
    ///     }
    /// }"#;
    ///
    /// let behavior = loader.load_from_json(json).unwrap();
    /// ```
    pub fn load_from_json(&self, json: &str) -> Result<BoxedBehavior> {
        let config = BehaviorConfig::from_json(json)?;
        self.load(&config)
    }

    /// Load a behavior tree from a JSON file.
    ///
    /// This is a convenience method that reads a file and loads the behavior tree.
    ///
    /// # Arguments
    ///
    /// * `path` - Path to the JSON file
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use mecha10_behavior_runtime::{NodeRegistry, BehaviorLoader};
    ///
    /// # let registry = NodeRegistry::new();
    /// let loader = BehaviorLoader::new(registry);
    /// let behavior = loader.load_from_file("behaviors/patrol.json").unwrap();
    /// ```
    pub fn load_from_file(&self, path: impl AsRef<std::path::Path>) -> Result<BoxedBehavior> {
        let config = BehaviorConfig::from_file(path)?;
        self.load(&config)
    }

    /// Recursively load a composition node and its children.
    fn load_composition(&self, composition: &CompositionConfig, config: &BehaviorConfig) -> 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)
                            .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)
                            .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::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)
                            .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::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()
                );

                // Resolve configuration (prefer inline over config_ref)
                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
                };

                // Create the node using the registry
                self.registry
                    .create(node, node_config)
                    .with_context(|| format!("Failed to create node '{}'", node))
            }
        }
    }

    /// Get a reference to the underlying node registry.
    ///
    /// This can be useful for inspecting registered types or adding new types
    /// after the loader has been created.
    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()
    }
}