behaviortree-core 0.1.0

Core implementaion of behaviortree
Documentation
// Copyright © 2025 Stephan Kunz
//! [`Parallel`] [`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, port_array_init,
	tree::BehaviorTreeElementList,
	{behavior_data::BehaviorData, behavior_state::BehaviorState, error::Error},
};

/// A [`Parallel`] behaviors executes its children __concurrently__ in one thread.
///
/// It differs from [`ParallelAll`](crate::behaviors::parallel_all::ParallelAll) in how it handles the parallelity:
/// The standard variant is completed either when the `success_threshold` or the `failure_threshold` is reached.
/// These are configured using the ports `success_count` and `failure_count`.
/// If any of the thresholds is reached, still running children will be halted.
/// Both ports default to `-1` which means any number or don't care.
///
/// The variant is gated behind the feature `parallel`.
///
/// Example:
///
/// ```xml
/// <Parallel success_count="2" failure_count="1">
///    <Behavior1/>
///    <Behavior2/>
///    <Behavior3/>
/// </Parallel>
/// ```
#[derive(Control, Debug)]
#[behavior(groot2)]
pub struct Parallel {
	/// The amount of completed sub behaviors that succeeded.
	success_count: i32,
	/// 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<2>,
}

/// The port names
const SUCCESS_COUNT: &str = "success_count";
const FAILURE_COUNT: &str = "failure_count";

#[async_trait::async_trait]
impl crate::behavior_traits::Behavior for Parallel {
	fn on_halt(&mut self) -> Result<(), Error> {
		self.completed_list.clear();
		self.success_count = 0;
		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 minimum needed Successes to return a Success.
		// "-1" signals any number.
		let success_threshold = self.portlist.get(SUCCESS_COUNT).unwrap_or(-1);

		if (children_count as i32) < success_threshold {
			return Err(Error::Composition {
				txt: "Number of children is less than success threshold. Can never succeed.".into(),
			});
		}

		// The maximum allowed failures.
		// "-1" signals any number.
		let failure_threshold = self.portlist.get(FAILURE_COUNT).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 minimum needed Successes to return a Success.
		// "-1" signals any number.
		let success_threshold = self.portlist.get(SUCCESS_COUNT).unwrap_or(-1);
		// The maximum allowed failures.
		// "-1" signals any number.
		let failure_threshold = self.portlist.get(FAILURE_COUNT).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: "Parallel".into(),
							state: BehaviorState::Idle,
						});
					}
					BehaviorState::Running => {}
					BehaviorState::Skipped => skipped_count += 1,
					BehaviorState::Success => {
						self.completed_list.insert(i);
						self.success_count += 1;
					}
				}
			}

			let sum = self.failure_count + self.success_count + skipped_count;
			if sum >= children_count as i32 {
				let state = if skipped_count == children_count as i32 {
					BehaviorState::Skipped
				} else if failure_threshold <= 0 && success_threshold <= 0 {
					BehaviorState::Success
				} else if failure_threshold <= 0 {
					if self.success_count >= success_threshold {
						BehaviorState::Success
					} else {
						BehaviorState::Failure
					}
				} else if (self.failure_count > failure_threshold) || (self.success_count < success_threshold) {
					BehaviorState::Failure
				} else {
					BehaviorState::Success
				};

				self.completed_list.clear();
				self.success_count = 0;
				self.failure_count = 0;
				children.halt(runtime)?;

				return Ok(state);
			}
		}

		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 Parallel {
	port_array_init! {
		2,
		create_inbound_entry_parseable!(SUCCESS_COUNT, i32, -1),
		create_inbound_entry_parseable!(FAILURE_COUNT, i32, -1),
	}
}