Skip to main content

behaviortree_core/behaviors/
parallel.rs

1// Copyright © 2025 Stephan Kunz
2//! [`Parallel`] [`Control`] implementation.
3
4use alloc::{boxed::Box, collections::btree_set::BTreeSet};
5
6use behaviortree_derive::Control;
7use dataport::{PortArray, PortCollectionAccessors, create_inbound_entry_parseable};
8use tinyscript::SharedRuntime;
9
10use crate::{
11	self as behaviortree_core, BehaviorResult, port_array_init,
12	tree::BehaviorTreeElementList,
13	{behavior_data::BehaviorData, behavior_state::BehaviorState, error::Error},
14};
15
16/// A [`Parallel`] behaviors executes its children __concurrently__ in one thread.
17///
18/// It differs from [`ParallelAll`](crate::behaviors::parallel_all::ParallelAll) in how it handles the parallelity:
19/// The standard variant is completed either when the `success_threshold` or the `failure_threshold` is reached.
20/// These are configured using the ports `success_count` and `failure_count`.
21/// If any of the thresholds is reached, still running children will be halted.
22/// Both ports default to `-1` which means any number or don't care.
23///
24/// The variant is gated behind the feature `parallel`.
25///
26/// Example:
27///
28/// ```xml
29/// <Parallel success_count="2" failure_count="1">
30///    <Behavior1/>
31///    <Behavior2/>
32///    <Behavior3/>
33/// </Parallel>
34/// ```
35#[derive(Control, Debug)]
36#[behavior(groot2)]
37pub struct Parallel {
38	/// The amount of completed sub behaviors that succeeded.
39	success_count: i32,
40	/// The amount of completed sub behaviors that failed.
41	failure_count: i32,
42	/// The list of completed sub behaviors
43	completed_list: BTreeSet<usize>,
44	/// The ports
45	#[behavior(portlist)]
46	portlist: PortArray<2>,
47}
48
49/// The port names
50const SUCCESS_COUNT: &str = "success_count";
51const FAILURE_COUNT: &str = "failure_count";
52
53#[async_trait::async_trait]
54impl crate::behavior_traits::Behavior for Parallel {
55	fn on_halt(&mut self) -> Result<(), Error> {
56		self.completed_list.clear();
57		self.success_count = 0;
58		self.failure_count = 0;
59		Ok(())
60	}
61
62	#[allow(clippy::cast_possible_truncation)]
63	#[allow(clippy::cast_possible_wrap)]
64	fn on_start(
65		&mut self,
66		behavior: &mut BehaviorData,
67		children: &mut BehaviorTreeElementList,
68		_runtime: &SharedRuntime,
69	) -> Result<(), Error> {
70		// check composition only once at start
71		let children_count = children.len();
72		// The minimum needed Successes to return a Success.
73		// "-1" signals any number.
74		let success_threshold = self.portlist.get(SUCCESS_COUNT).unwrap_or(-1);
75
76		if (children_count as i32) < success_threshold {
77			return Err(Error::Composition {
78				txt: "Number of children is less than success threshold. Can never succeed.".into(),
79			});
80		}
81
82		// The maximum allowed failures.
83		// "-1" signals any number.
84		let failure_threshold = self.portlist.get(FAILURE_COUNT).unwrap_or(-1);
85
86		if (children_count as i32) < failure_threshold {
87			return Err(Error::Composition {
88				txt: "Number of children is less than failure threshold. Can never fail.".into(),
89			});
90		}
91		behavior.set_state(BehaviorState::Running);
92		Ok(())
93	}
94
95	#[allow(clippy::cast_possible_truncation)]
96	#[allow(clippy::cast_possible_wrap)]
97	#[allow(clippy::set_contains_or_insert)]
98	async fn tick(
99		&mut self,
100		_behavior: &mut BehaviorData,
101		children: &mut BehaviorTreeElementList,
102		runtime: &SharedRuntime,
103	) -> BehaviorResult {
104		// The minimum needed Successes to return a Success.
105		// "-1" signals any number.
106		let success_threshold = self.portlist.get(SUCCESS_COUNT).unwrap_or(-1);
107		// The maximum allowed failures.
108		// "-1" signals any number.
109		let failure_threshold = self.portlist.get(FAILURE_COUNT).unwrap_or(-1);
110
111		let children_count = children.len();
112		let mut skipped_count = 0;
113
114		for i in 0..children_count {
115			// Skip completed node
116			if !self.completed_list.contains(&i) {
117				let child = &mut children[i];
118				match child.tick(runtime).await? {
119					BehaviorState::Failure => {
120						self.completed_list.insert(i);
121						self.failure_count += 1;
122					}
123					// Throw error, should never happen
124					BehaviorState::Idle => {
125						return Err(Error::State {
126							behavior: "Parallel".into(),
127							state: BehaviorState::Idle,
128						});
129					}
130					BehaviorState::Running => {}
131					BehaviorState::Skipped => skipped_count += 1,
132					BehaviorState::Success => {
133						self.completed_list.insert(i);
134						self.success_count += 1;
135					}
136				}
137			}
138
139			let sum = self.failure_count + self.success_count + skipped_count;
140			if sum >= children_count as i32 {
141				let state = if skipped_count == children_count as i32 {
142					BehaviorState::Skipped
143				} else if failure_threshold <= 0 && success_threshold <= 0 {
144					BehaviorState::Success
145				} else if failure_threshold <= 0 {
146					if self.success_count >= success_threshold {
147						BehaviorState::Success
148					} else {
149						BehaviorState::Failure
150					}
151				} else if (self.failure_count > failure_threshold) || (self.success_count < success_threshold) {
152					BehaviorState::Failure
153				} else {
154					BehaviorState::Success
155				};
156
157				self.completed_list.clear();
158				self.success_count = 0;
159				self.failure_count = 0;
160				children.halt(runtime)?;
161
162				return Ok(state);
163			}
164		}
165
166		if skipped_count == children_count as i32 {
167			return Ok(BehaviorState::Skipped);
168		}
169
170		let sum = skipped_count + self.completed_list.len() as i32;
171		if sum >= children_count as i32 {
172			let state = if (failure_threshold >= 0) && (self.failure_count > failure_threshold) {
173				BehaviorState::Failure
174			} else {
175				BehaviorState::Success
176			};
177
178			// Done!
179			children.halt(runtime)?;
180			self.completed_list.clear();
181
182			return Ok(state);
183		}
184
185		Ok(BehaviorState::Running)
186	}
187}
188
189impl Parallel {
190	port_array_init! {
191		2,
192		create_inbound_entry_parseable!(SUCCESS_COUNT, i32, -1),
193		create_inbound_entry_parseable!(FAILURE_COUNT, i32, -1),
194	}
195}