behaviortree-core 0.1.0

Core implementaion of behaviortree
Documentation
// Copyright © 2025 Stephan Kunz
//! [`ParallelAll`] [`Control`] implementation.

use alloc::{boxed::Box, collections::btree_set::BTreeSet};

use behaviortree_derive::Control;
use dataport::{PortArray, PortCollectionAccessors, create_inbound_entry_parseable};
use tinyscript::SharedRuntime;

use crate::{
	self as behaviortree_core, BehaviorResult, behavior_data::BehaviorData, behavior_state::BehaviorState, error::Error,
	port_array_init, tree::BehaviorTreeElementList,
};

/// A [`ParallelAll`] behaviors executes its children __concurrently__ in one thread.
///
/// It differs from [`Parallel`](crate::behaviors::parallel::Parallel) in how it handles the parallelity:
/// The all variant finishes the execution of all its children before deciding
/// whether its a [`BehaviorState::Success`] or a [`BehaviorState::Failure`].
/// The optional port `max_failures` gives the number of children that may fail without
/// the behavior becoming a [`BehaviorState::Failure`]. it defaults to `-1`
/// which means any number or don't care.
///
/// The variant is gated behind feature `parallel_all`.
///
/// Example:
///
/// ```xml
/// <ParallelAll max_failures="0">
///    <Behavior1/>
///    <Behavior2/>
///    <Behavior3/>
/// </ParallelAll>
/// ```
#[derive(Control, Debug)]
#[behavior(groot2)]
pub struct ParallelAll {
	/// The amount of completed sub behaviors that failed.
	failure_count: i32,
	/// The list of completed sub behaviors
	completed_list: BTreeSet<usize>,
	/// The ports
	#[behavior(portlist)]
	portlist: PortArray<1>,
}

/// The port names
const MAX_FAILURES: &str = "max_failures";

#[async_trait::async_trait]
impl crate::behavior_traits::Behavior for ParallelAll {
	fn on_halt(&mut self) -> Result<(), Error> {
		self.completed_list.clear();
		self.failure_count = 0;
		Ok(())
	}

	#[allow(clippy::cast_possible_truncation)]
	#[allow(clippy::cast_possible_wrap)]
	fn on_start(
		&mut self,
		behavior: &mut BehaviorData,
		children: &mut BehaviorTreeElementList,
		_runtime: &SharedRuntime,
	) -> Result<(), Error> {
		// check composition only once at start
		let children_count = children.len();

		// The maximum allowed failures.
		// "-1" signals any number.
		let failure_threshold = self.portlist.get(MAX_FAILURES).unwrap_or(-1);

		if (children_count as i32) < failure_threshold {
			return Err(Error::Composition {
				txt: "Number of children is less than failure threshold. Can never fail.".into(),
			});
		}
		behavior.set_state(BehaviorState::Running);
		Ok(())
	}

	#[allow(clippy::cast_possible_truncation)]
	#[allow(clippy::cast_possible_wrap)]
	#[allow(clippy::set_contains_or_insert)]
	async fn tick(
		&mut self,
		_behavior: &mut BehaviorData,
		children: &mut BehaviorTreeElementList,
		runtime: &SharedRuntime,
	) -> BehaviorResult {
		// The maximum allowed failures.
		// "-1" signals any number.
		let failure_threshold = self.portlist.get(MAX_FAILURES).unwrap_or(-1);

		let children_count = children.len();
		let mut skipped_count = 0;

		for i in 0..children_count {
			// Skip completed node
			if !self.completed_list.contains(&i) {
				let child = &mut children[i];
				match child.tick(runtime).await? {
					BehaviorState::Failure => {
						self.completed_list.insert(i);
						self.failure_count += 1;
					}
					// Throw error, should never happen
					BehaviorState::Idle => {
						return Err(Error::State {
							behavior: "ParallelAll".into(),
							state: BehaviorState::Idle,
						});
					}
					BehaviorState::Running => {}
					BehaviorState::Skipped => skipped_count += 1,
					BehaviorState::Success => {
						self.completed_list.insert(i);
					}
				}
			}
		}

		if skipped_count == children_count as i32 {
			return Ok(BehaviorState::Skipped);
		}

		let sum = skipped_count + self.completed_list.len() as i32;
		if sum >= children_count as i32 {
			let state = if (failure_threshold >= 0) && (self.failure_count > failure_threshold) {
				BehaviorState::Failure
			} else {
				BehaviorState::Success
			};

			// Done!
			children.halt(runtime)?;
			self.completed_list.clear();

			return Ok(state);
		}

		Ok(BehaviorState::Running)
	}
}

impl ParallelAll {
	port_array_init! {
		1,
		create_inbound_entry_parseable!(MAX_FAILURES, i32, -1),
	}
}