mecha10-behavior-runtime 0.8.0

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 JSON configuration.

use crate::composition::{
    ForceFailureNode, ForceSuccessNode, InverterNode, ParallelNode, RepeatNode, RetryNode, SelectOrNode, SequenceNode,
    SubsumptionNode, TimeoutNode, WatchNode,
};
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};

#[derive(Clone)]
pub struct BehaviorLoader {
    registry: NodeRegistry,
}

impl BehaviorLoader {
    /// Create a new behavior loader with the given node registry.
    pub fn new(registry: NodeRegistry) -> Self {
        Self { registry }
    }

    /// Load a behavior tree from a `BehaviorConfig`, recursively instantiating all nodes.
    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 (parses then loads).
    pub fn load_from_json(&self, json: &str) -> Result<BoxedBehavior> {
        self.load(&BehaviorConfig::from_json(json)?)
    }

    /// Load a behavior tree from a JSON file (reads then loads).
    pub fn load_from_file(&self, path: impl AsRef<std::path::Path>) -> Result<BoxedBehavior> {
        self.load(&BehaviorConfig::from_file(path)?)
    }

    /// 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::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)
                            .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)
                        .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)
                    .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)
                    .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)
                    .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)
                    .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)
                    .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)
                    .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::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))
            }
        }
    }

    /// Get a reference to the underlying node registry.
    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()
    }
}