behaviortree-core 0.1.0

Core implementaion of behaviortree
Documentation
// Copyright © 2025 Stephan Kunz
//! `Fallback` & `AsyncFallback` [`Control`] implementations.

use alloc::boxed::Box;

use behaviortree_derive::Control;
use tinyscript::SharedRuntime;

use crate::{
	self as behaviortree_core, BehaviorCreationFn, BehaviorResult, ConstString, behavior_data::BehaviorData,
	behavior_description::BehaviorDescription, behavior_state::BehaviorState, error::Error, tree::BehaviorTreeElementList,
};

/// Kind of the Fallback
#[derive(Clone, Copy, Debug)]
pub enum FallbackKind {
	/// Asynchronous
	Async,
	/// Reactive
	Reactive,
	/// Standard
	Standard,
}

/// The `Fallback` behavior is used to try different strategies until one succeeds.
///
/// It implements 3 modes, which differ in how they handle a childs return value:
/// - The synchronous mode will try to succeed within one tick from its parent.
///   If any child returns [`BehaviorState::Running`], previous children will NOT be ticked again.
///   - If a child returns [`BehaviorState::Failure`], continue to the next sibling.
///     If all the children fail, than this node returns [`BehaviorState::Failure`].
///   - If a child returns [`BehaviorState::Running`], this node returns [`BehaviorState::Running`]
///     and continues with the same child at the next tick from parent.
///   - If a child returns [`BehaviorState::Success`] this behavior returns [`BehaviorState::Success`].
/// - The asynchronous mode will return the flow contol after a childs failure to its parent.
///   If any child returns [`BehaviorState::Running`], previous children will NOT be ticked again.
///   - If a the children returns [`BehaviorState::Failure`], this node returns [`BehaviorState::Running`].
///     and continues with the next child at the next tick from parent.
///     If all the children fail, than this node returns [`BehaviorState::Failure`].
///   - If a child returns [`BehaviorState::Running`], this node returns [`BehaviorState::Running`]
///     and continues with the same child at the next tick from parent.
///   - If a child returns [`BehaviorState::Success`] this behavior returns [`BehaviorState::Success`].
/// - The reactive mode always ticks all children from first to last:
///   If any child returns [`BehaviorState::Running`], previous children will be ticked again.
///   IMPORTANT: Having asynchronous children (aka children that return [`BehaviorState::Running`]) makes
///   this behavior difficult to predict. Avoid having more than one asynchronous children!
///   - If a child returns [`BehaviorState::Running`], continue to the next sibling.
///   - If a child returns [`BehaviorState::Failure`], continue to the next sibling.
///     If all the children fail, than this node returns [`BehaviorState::Failure`].
///   - If a child returns [`BehaviorState::Success`], stop running child and return [`BehaviorState::Success`].
///
/// The variants are gated behind features `fallback`,`async_fallback` and `reactive_fallback` respectively.
///
/// Examples:
///
/// The synchronous variant is gated behind feature `fallback`.
/// ```xml
/// <Fallback>
///    <Behavior1/>
///    <Behavior2/>
///    <Behavior3/>
/// </Fallback>
/// ```
///
/// The asynchronous variant is gated behind feature `async_fallback`.
/// ```xml
/// <AsyncFallback>
///    <Behavior1/>
///    <Behavior2/>
///    <Behavior3/>
/// </AsyncFallback>
/// ```
#[derive(Control, Debug)]
#[behavior(groot2, no_create, no_register, no_register_with)]
pub struct Fallbacks {
	/// Kind of Fallback
	kind: FallbackKind,
	/// Current child
	child_idx: usize,
	/// Amount of skipped children
	skipped: usize,
	/// Running child
	running_child_idx: i32,
}

#[async_trait::async_trait]
impl crate::behavior_traits::Behavior for Fallbacks {
	fn on_start(
		&mut self,
		behavior: &mut BehaviorData,
		_children: &mut BehaviorTreeElementList,
		_runtime: &SharedRuntime,
	) -> Result<(), Error> {
		self.child_idx = 0;
		self.skipped = 0;
		self.running_child_idx = -1;
		behavior.set_state(BehaviorState::Running);
		Ok(())
	}

	async fn tick(
		&mut self,
		_behavior: &mut BehaviorData,
		children: &mut BehaviorTreeElementList,
		runtime: &SharedRuntime,
	) -> BehaviorResult {
		let children_count = children.len();
		// In the reactive mode start loop from the beginning with every tick
		if matches!(self.kind, FallbackKind::Reactive) {
			self.child_idx = 0;
		}
		while self.child_idx < children_count {
			let child = &mut children[self.child_idx];
			let prev_state = child.state();
			let child_state = child.tick(runtime).await?;

			match child_state {
				BehaviorState::Failure => {
					self.child_idx += 1;
					match self.kind {
						FallbackKind::Async => {
							if (prev_state == BehaviorState::Idle) && (self.child_idx < children_count) {
								return Ok(BehaviorState::Running);
							}
						}
						FallbackKind::Reactive => self.running_child_idx = -1,
						FallbackKind::Standard => {}
					}
				}
				BehaviorState::Idle => {
					let behavior: ConstString = match self.kind {
						FallbackKind::Async => "AsnycFallback".into(),
						FallbackKind::Reactive => "ReactiveFallback".into(),
						FallbackKind::Standard => "Fallback".into(),
					};
					return Err(Error::State {
						behavior,
						state: child_state,
					});
				}
				BehaviorState::Running => {
					match self.kind {
						FallbackKind::Async | FallbackKind::Standard => {}
						#[allow(clippy::cast_possible_truncation)]
						#[allow(clippy::cast_sign_loss)]
						#[allow(clippy::cast_possible_wrap)]
						FallbackKind::Reactive => {
							// halt previously running child
							if self.running_child_idx != (self.child_idx as i32) && self.running_child_idx != -1 {
								children[self.running_child_idx as usize].halt_children(runtime)?;
							}
							self.running_child_idx = self.child_idx as i32;
							if self.running_child_idx == -1 {
								self.running_child_idx = self.child_idx as i32;
							} else if self.running_child_idx != (self.child_idx as i32) {
								// Multiple children running at the same time
								return Err(Error::Composition {
									txt: "[ReactiveFallback]: Only a single child can return Running.".into(),
								});
							}
						}
					}
					return Ok(child_state);
				}
				BehaviorState::Success => {
					children.reset(runtime)?;
					children.halt(runtime)?;
					self.child_idx = 0;
					self.skipped = 0;
					self.running_child_idx = -1;
					return Ok(child_state);
				}
				BehaviorState::Skipped => {
					self.child_idx += 1;
					self.skipped += 1;
				}
			}
		}

		// loop ended without a success,
		// so either all children failed or were skipped
		let all_skipped = self.skipped == children_count;
		if self.child_idx >= children_count {
			children.reset(runtime)?;
			self.child_idx = 0;
			self.skipped = 0;
			self.running_child_idx = -1;
		}
		if all_skipped {
			Ok(BehaviorState::Skipped)
		} else {
			Ok(BehaviorState::Failure)
		}
	}
}

impl Fallbacks {
	/// Returns a Fallback behavior with the given kind.
	#[must_use]
	pub const fn new(kind: FallbackKind) -> Self {
		Self {
			kind,
			child_idx: 0,
			skipped: 0,
			running_child_idx: -1,
		}
	}

	/// Creates a `creation_fn()` for `Fallback` with the given kind.
	#[must_use]
	pub fn create_fn(kind: FallbackKind) -> Box<BehaviorCreationFn> {
		Box::new(move |_blackboard| Box::new(Self::new(kind)))
	}

	/// Registers the `Fallback` behavior in the factory.
	/// # Errors
	/// - if registration fails
	pub fn register_with(
		registry: &mut impl behaviortree_core::behavior_traits::BehaviorRegistry,
		name: &str,
		kind: FallbackKind,
	) -> Result<(), Error> {
		let bhvr_desc = BehaviorDescription::new(name, name, true);
		let bhvr_creation_fn = Self::create_fn(kind);
		registry.add_behavior(bhvr_desc, bhvr_creation_fn)
	}
}