use std::collections::{HashMap, HashSet};
use arora_behavior::graph::{Graph, LinkSource, Port};
use arora_types::gen_uuid_from_str;
use uuid::Uuid;
use crate::error::BehaviorTreeError;
use crate::schema::{Expression, Node as SchemaNode, NodeParameterId};
use crate::variable::VariableResolver;
use crate::{load_behavior_tree_nodes_with, BehaviorTree};
pub fn graph_to_bt_nodes(graph: &Graph) -> Result<Vec<SchemaNode>, BehaviorTreeError> {
Ok(lower_graph(graph)?.0)
}
fn lower_graph(
graph: &Graph,
) -> Result<(Vec<SchemaNode>, HashMap<Uuid, String>), BehaviorTreeError> {
let mut args_by_node: HashMap<Uuid, HashMap<Uuid, Expression>> = HashMap::new();
for link in &graph.links {
let expr = link_source_to_expression(&link.source);
args_by_node
.entry(link.target.node)
.or_default()
.insert(link.target.port, expr);
}
let linked_sources: HashSet<Port> = graph
.links
.iter()
.filter_map(|link| match &link.source {
LinkSource::Port(port) => Some(*port),
_ => None,
})
.collect();
let mut variables = graph.variables.clone();
for (id, node) in &graph.nodes {
let mut bind = |io: &arora_behavior::graph::Io, overridden: bool| {
let Some(key) = &io.predetermined_key else {
return;
};
if overridden {
return;
}
let args = args_by_node.entry(*id).or_default();
if args.contains_key(&io.id) {
return;
}
let variable = gen_uuid_from_str(key);
variables.entry(variable).or_insert_with(|| key.clone());
args.insert(io.id, Expression::VariableId(variable));
};
for io in &node.inputs {
bind(io, false);
}
for io in &node.outputs {
bind(
io,
linked_sources.contains(&Port {
node: *id,
port: io.id,
}),
);
}
}
let mut ordered: Vec<&Uuid> = graph.nodes.keys().collect();
if let Some(root) = &graph.root {
if !graph.nodes.contains_key(root) {
return Err(BehaviorTreeError::InconsistentTreeError {
message: format!("graph root {root} is not a node"),
});
}
ordered.sort_by_key(|id| *id != root);
}
let mut nodes = Vec::with_capacity(graph.nodes.len());
for id in ordered {
let node = &graph.nodes[id];
nodes.push(SchemaNode {
id: node.id,
function: node.function,
arguments: args_by_node.remove(&node.id).unwrap_or_default(),
children: node.children.clone(),
});
}
Ok((nodes, variables))
}
pub fn build_behavior_tree(
graph: &Graph,
resolver: &VariableResolver,
) -> Result<BehaviorTree, BehaviorTreeError> {
let (nodes, variables) = lower_graph(graph)?;
load_behavior_tree_nodes_with(nodes, resolver, &variables)
}
fn link_source_to_expression(source: &LinkSource) -> Expression {
match source {
LinkSource::Literal(value) => Expression::Value(value.clone()),
LinkSource::Variable(id) => Expression::VariableId(*id),
LinkSource::Port(port) => Expression::NodeArgument(NodeParameterId {
node: port.node,
parameter: port.port,
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nodes::{FAIL_FUNCTION_ID, SEQ_FUNCTION_ID, SUCCEED_FUNCTION_ID};
use crate::run_behavior_tree;
use arora_behavior::graph::{GraphDiff, Node as GraphNode};
use arora_types::call::{Call, CallBridge, CallError, CallResult, Callable, CallableId};
use arora_types::value::Value;
use std::rc::Rc;
#[derive(Default)]
struct NativeBridge {
registered: HashMap<u64, Rc<dyn Callable>>,
next_id: u64,
}
impl CallBridge for NativeBridge {
fn arora_call(&mut self, _module: &Uuid, call: Call) -> Result<CallResult, CallError> {
Err(CallError::FunctionNotFound { id: call.id })
}
fn arora_register_callable(&mut self, callable: Rc<dyn Callable>) -> CallableId {
let id = self.next_id;
self.next_id += 1;
self.registered.insert(id, callable);
CallableId { id }
}
fn arora_unregister_callable(&mut self, callable_id: &CallableId) {
self.registered.remove(&callable_id.id);
}
fn arora_call_indirect(&mut self, callable_id: &CallableId) -> Result<Value, CallError> {
let callable =
self.registered
.get(&callable_id.id)
.cloned()
.ok_or(CallError::Generic {
message: format!("unknown callable {}", callable_id.id),
})?;
callable.call(self)
}
}
fn control(id: Uuid, function: Uuid, children: Vec<Uuid>) -> GraphNode {
GraphNode {
id,
function,
children: Some(children),
..GraphNode::default()
}
}
fn leaf(id: Uuid, function: Uuid) -> GraphNode {
GraphNode {
id,
function,
..GraphNode::default()
}
}
fn run(graph: &Graph) -> crate::arora_generated::behavior_tree::status::Status {
let tree = build_behavior_tree(graph, &|_| None).expect("tree builds");
let mut bridge = NativeBridge::default();
run_behavior_tree(&tree, Rc::new(HashMap::new()), &mut bridge, false).expect("run")
}
#[test]
fn seq_of_builtins_lowers_and_runs() {
use crate::arora_generated::behavior_tree::status::Status;
let root = Uuid::from_u128(0x100);
let a = Uuid::from_u128(0x1);
let b = Uuid::from_u128(0x2);
let mut graph = Graph::empty();
graph.root = Some(root);
graph
.nodes
.insert(root, control(root, SEQ_FUNCTION_ID, vec![a, b]));
graph.nodes.insert(a, leaf(a, SUCCEED_FUNCTION_ID));
graph.nodes.insert(b, leaf(b, SUCCEED_FUNCTION_ID));
assert_eq!(run(&graph), Status::Success);
graph
.apply(GraphDiff {
add_nodes: vec![leaf(b, FAIL_FUNCTION_ID)],
..GraphDiff::default()
})
.unwrap();
assert_eq!(run(&graph), Status::Failure);
}
#[test]
fn root_node_is_lowered_first() {
let root = Uuid::from_u128(0xEEE);
let child = Uuid::from_u128(0x1);
let mut graph = Graph::empty();
graph.root = Some(root);
graph.nodes.insert(child, leaf(child, SUCCEED_FUNCTION_ID));
graph
.nodes
.insert(root, control(root, SEQ_FUNCTION_ID, vec![child]));
let nodes = graph_to_bt_nodes(&graph).unwrap();
assert_eq!(nodes.first().unwrap().id, root);
}
#[test]
fn missing_root_node_is_an_error() {
let mut graph = Graph::empty();
graph.root = Some(Uuid::from_u128(0xDEAD));
assert!(graph_to_bt_nodes(&graph).is_err());
}
#[test]
fn groot_lowers_to_graph_and_runs() {
use crate::arora_generated::behavior_tree::status::Status;
use crate::schema_groot::BehaviorTree as GrootTree;
let xml = r#"<root main_tree_to_execute="MainTree">
<BehaviorTree ID="MainTree">
<Sequence>
<Succeed/>
<Succeed/>
</Sequence>
</BehaviorTree>
</root>"#;
let groot = GrootTree::try_from_groot_xml(xml).expect("parse");
let graph = groot.into_graph(&HashMap::new()).expect("lower to graph");
assert!(graph.root.is_some());
assert_eq!(graph.nodes.len(), 3, "sequence + two leaves");
assert_eq!(run(&graph), Status::Success);
}
#[test]
fn predetermined_slots_bind_unless_linked() {
use arora_behavior::graph::{Io, Link, Port};
let node_id = Uuid::from_u128(0x1);
let bound_port = Uuid::from_u128(0x10);
let linked_port = Uuid::from_u128(0x11);
let mut node = leaf(node_id, SUCCEED_FUNCTION_ID);
node.inputs = vec![
Io {
predetermined_key: Some("face/x".to_string()),
..Io::new(bound_port)
},
Io {
predetermined_key: Some("face/y".to_string()),
..Io::new(linked_port)
},
];
let mut graph = Graph::empty();
graph.root = Some(node_id);
graph.nodes.insert(node_id, node);
graph.links.push(Link::new(
Port {
node: node_id,
port: linked_port,
},
LinkSource::Literal(Value::Boolean(true)),
));
let nodes = graph_to_bt_nodes(&graph).unwrap();
let lowered = &nodes[0];
assert_eq!(
lowered.arguments[&bound_port],
Expression::VariableId(gen_uuid_from_str("face/x")),
"the unlinked slot binds to its predetermined key's variable"
);
assert_eq!(
lowered.arguments[&linked_port],
Expression::Value(Value::Boolean(true)),
"the link wins over the predetermined key"
);
}
#[test]
fn predetermined_outputs_bind_unless_read_by_a_link() {
use arora_behavior::graph::{Io, Link, Port};
let producer = Uuid::from_u128(0x1);
let consumer = Uuid::from_u128(0x2);
let out_port = Uuid::from_u128(0x20);
let in_port = Uuid::from_u128(0x21);
let make = |linked: bool| {
let mut p = leaf(producer, SUCCEED_FUNCTION_ID);
p.outputs = vec![Io {
predetermined_key: Some("motor/left".to_string()),
..Io::new(out_port)
}];
let mut graph = Graph::empty();
graph.root = Some(producer);
graph.nodes.insert(producer, p);
if linked {
graph
.nodes
.insert(consumer, leaf(consumer, SUCCEED_FUNCTION_ID));
graph.links.push(Link::new(
Port {
node: consumer,
port: in_port,
},
LinkSource::Port(Port {
node: producer,
port: out_port,
}),
));
}
graph
};
let unlinked = graph_to_bt_nodes(&make(false)).unwrap();
assert_eq!(
unlinked[0].arguments[&out_port],
Expression::VariableId(gen_uuid_from_str("motor/left")),
"the unread output binds to its predetermined key's variable"
);
let linked = graph_to_bt_nodes(&make(true)).unwrap();
assert!(
!linked
.iter()
.find(|n| n.id == producer)
.unwrap()
.arguments
.contains_key(&out_port),
"a link reading the output overrides its predetermination"
);
}
#[test]
fn predetermined_keys_resolve_by_name() {
use arora_behavior::graph::Io;
use std::cell::RefCell;
let node_id = Uuid::from_u128(0x1);
let port = Uuid::from_u128(0x10);
let mut node = leaf(node_id, SUCCEED_FUNCTION_ID);
node.inputs = vec![Io {
predetermined_key: Some("face/x".to_string()),
..Io::new(port)
}];
let mut graph = Graph::empty();
graph.root = Some(node_id);
graph.nodes.insert(node_id, node);
let asked = RefCell::new(Vec::new());
build_behavior_tree(&graph, &|name: &str| {
asked.borrow_mut().push(name.to_string());
None
})
.expect("tree builds");
assert!(
asked.borrow().contains(&"face/x".to_string()),
"the loader asked the resolver for the predetermined key, got {:?}",
asked.borrow()
);
}
}