Skip to main content

arora_behavior/
graph.rs

1//! The shared behavior **graph model**: one serde-friendly data representation
2//! every [`BehaviorInterpreter`](super::BehaviorInterpreter) reads.
3//!
4//! A behavior — a behavior tree, a node graph — is authored as a [`Graph`]: a
5//! set of [`Node`]s, each bound to a **function** (a statically-known id the
6//! interpreter handles natively, **or** a module call routed by that same id —
7//! exactly how `arora-behavior-tree` already treats its builtins and module
8//! actions homogeneously), with typed slots ([`Io`] — inputs and outputs, told apart by
9//! which [`Node`] list holds them) and **links**
10//! ([`Link`]) wiring an output/port of one node into the input of another.
11//!
12//! This module is *only* the data model. It does not tick anything and it does
13//! not know how any particular interpreter walks the links — the behavior tree
14//! reads them as argument/child edges; a node graph reads them as dataflow. Each
15//! interpreter lowers this shared model into its own runtime form (see
16//! `arora-behavior-tree`'s `graph` module for the tree lowering).
17//!
18//! Edition happens through [`GraphDiff`]: a set of node/link additions and
19//! removals (plus predetermined-key overrides). "Loading" a behavior is just
20//! [`Graph::apply`]ing a diff onto an [`Graph::empty`] graph — which is why
21//! [`BehaviorInterpreter::apply`](super::BehaviorInterpreter::apply) is the one
22//! edition entry point.
23
24use std::collections::HashMap;
25
26use arora_types::module::high::TypeRef;
27use arora_types::value::Value;
28use serde::{Deserialize, Serialize};
29use uuid::Uuid;
30
31/// A typed slot on a [`Node`] — one entry of [`Node::inputs`] or
32/// [`Node::outputs`]. **Which of the two lists holds it is what makes it an
33/// input or an output**; the slot itself carries no direction, so the two can
34/// never disagree.
35///
36/// `id` matches the function's parameter id (for an input) or is the node's
37/// output slot id (a return is conventionally the function id itself). `ty` is
38/// the arora **`Value` type** of the slot, taken from the frozen `Function`
39/// record's signature when known; `None` means "leave it to the interpreter to
40/// derive from the function record at build time".
41#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
42pub struct Io {
43    /// The slot id: a parameter id for an input, an output slot id for an output.
44    pub id: Uuid,
45    /// The slot's arora `Value` type, when known from the function signature.
46    #[serde(default, skip_serializing_if = "Option::is_none")]
47    pub ty: Option<TypeRef>,
48    /// An optional **predetermined key**: the slot's default store binding (an
49    /// animation track's authored key, a sink node's path). The interpreter
50    /// binds the slot to this key unless a [`Link`] overrides it. Carried here
51    /// per proposal §3.6; the full predetermined-I/O semantics land in a later
52    /// pass, but the field travels with the model now.
53    #[serde(default, skip_serializing_if = "Option::is_none")]
54    pub predetermined_key: Option<String>,
55}
56
57impl Io {
58    /// A bare slot with no type or predetermined key.
59    pub fn new(id: Uuid) -> Self {
60        Self {
61            id,
62            ty: None,
63            predetermined_key: None,
64        }
65    }
66}
67
68/// A reference to one slot on one node: `(node, port)`. Generalizes the behavior
69/// tree's `NodeParameterId`.
70#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
71pub struct Port {
72    /// The node the slot belongs to.
73    pub node: Uuid,
74    /// The slot id on that node (an [`Io::id`]).
75    pub port: Uuid,
76}
77
78impl Port {
79    /// A `(node, port)` reference.
80    pub fn new(node: Uuid, port: Uuid) -> Self {
81        Self { node, port }
82    }
83}
84
85/// Where the value feeding a [`Link`]'s target comes from.
86///
87/// Generalizes the behavior tree's `Expression` link kinds (a literal value, a
88/// shared blackboard variable, or another node's slot). Full expression links
89/// (arithmetic over sources) are deferred — proposal Q-D.
90#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
91#[serde(rename_all = "snake_case")]
92pub enum LinkSource {
93    /// A constant value.
94    Literal(Value),
95    /// A shared blackboard variable, by id — the interpreter resolves it to a
96    /// store slot (or a tree-local cell) at build time.
97    Variable(Uuid),
98    /// Another node's slot: the output/port the link reads from. Generalizes
99    /// `Expression::NodeArgument`.
100    Port(Port),
101}
102
103/// A directed wire feeding one node input from a [`LinkSource`].
104#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
105pub struct Link {
106    /// The input slot being fed. At most one link targets a given port.
107    pub target: Port,
108    /// What feeds it.
109    pub source: LinkSource,
110}
111
112impl Link {
113    /// A link feeding `target` from `source`.
114    pub fn new(target: Port, source: LinkSource) -> Self {
115        Self { target, source }
116    }
117}
118
119/// A node bound to a function, with its typed inputs/outputs and (optionally)
120/// ordered children.
121///
122/// One node kind, routed by `function`: an interpreter dispatches natively for
123/// the ids it knows and calls a module for the rest — the same homogeneous split
124/// the behavior tree already makes.
125#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
126pub struct Node {
127    /// This node's id.
128    pub id: Uuid,
129    /// The function bound to this node: a statically-known id **or** a module
130    /// function id. The interpreter routes on it.
131    pub function: Uuid,
132    /// Declared inputs.
133    #[serde(default)]
134    pub inputs: Vec<Io>,
135    /// Declared outputs.
136    #[serde(default)]
137    pub outputs: Vec<Io>,
138    /// Ordered children, for interpreters (like the tree) whose structure is a
139    /// child relation. `None` for a leaf.
140    #[serde(default)]
141    pub children: Option<Vec<Uuid>>,
142}
143
144/// The shared behavior graph: nodes, the links between their slots, the named
145/// blackboard variables, and (for tree-shaped interpreters) the root node.
146#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
147pub struct Graph {
148    /// The entry node, for interpreters that need one (a behavior tree's root).
149    #[serde(default, skip_serializing_if = "Option::is_none")]
150    pub root: Option<Uuid>,
151    /// All nodes, indexed by id.
152    #[serde(default)]
153    pub nodes: HashMap<Uuid, Node>,
154    /// The links wiring node slots together.
155    #[serde(default)]
156    pub links: Vec<Link>,
157    /// Named blackboard variables (`id -> name`); a [`LinkSource::Variable`]
158    /// refers to one of these, and the name is what an interpreter resolves
159    /// against the data store.
160    #[serde(default)]
161    pub variables: HashMap<Uuid, String>,
162}
163
164/// A graph edition failed to apply.
165#[derive(Debug, Clone, PartialEq, Eq)]
166pub struct GraphError {
167    /// Human-readable description.
168    pub message: String,
169}
170
171impl std::fmt::Display for GraphError {
172    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173        f.write_str(&self.message)
174    }
175}
176
177impl std::error::Error for GraphError {}
178
179/// An edit to a [`Graph`]: what to add and remove.
180///
181/// Applied in a fixed order (removals of links, then nodes; additions of nodes,
182/// then links; then predetermined-key and root/variable settings) so a single
183/// diff can both delete and rebuild a region. Loading a fresh behavior is
184/// [`Graph::apply`]ing a diff whose `add_nodes`/`add_links` describe the whole
185/// graph onto an [`Graph::empty`] graph.
186#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
187pub struct GraphDiff {
188    /// Nodes to insert (replacing any node with the same id).
189    #[serde(default)]
190    pub add_nodes: Vec<Node>,
191    /// Node ids to remove. Links touching a removed node are dropped too.
192    #[serde(default)]
193    pub remove_nodes: Vec<Uuid>,
194    /// Links to insert. Adding a link whose `target` already has one replaces it
195    /// (at most one link per input).
196    #[serde(default)]
197    pub add_links: Vec<Link>,
198    /// Link targets to unwire.
199    #[serde(default)]
200    pub remove_links: Vec<Port>,
201    /// Predetermined-key overrides: set (`Some`) or clear (`None`) the
202    /// [`Io::predetermined_key`] of the slot at each [`Port`].
203    #[serde(default)]
204    pub set_predetermined: Vec<(Port, Option<String>)>,
205    /// If set, becomes the graph's [`root`](Graph::root).
206    #[serde(default, skip_serializing_if = "Option::is_none")]
207    pub set_root: Option<Uuid>,
208    /// Named variables to declare/rename (`id -> name`), merged in.
209    #[serde(default)]
210    pub variables: HashMap<Uuid, String>,
211}
212
213impl GraphDiff {
214    /// A diff that loads `graph` wholesale: every node and link as additions,
215    /// carrying its root and variables. `apply`ing this onto [`Graph::empty`]
216    /// reproduces `graph`.
217    pub fn load(graph: Graph) -> Self {
218        let mut add_nodes: Vec<Node> = graph.nodes.into_values().collect();
219        // Root first keeps interpreters that treat node order as significant
220        // (the behavior tree takes the first node as the root) well-defined.
221        if let Some(root) = graph.root {
222            add_nodes.sort_by_key(|n| n.id != root);
223        }
224        Self {
225            add_nodes,
226            add_links: graph.links,
227            set_root: graph.root,
228            variables: graph.variables,
229            ..Self::default()
230        }
231    }
232}
233
234impl Graph {
235    /// An empty graph — the starting point a load diff is applied onto.
236    pub fn empty() -> Self {
237        Self::default()
238    }
239
240    /// The node with id `id`, if present.
241    pub fn node(&self, id: &Uuid) -> Option<&Node> {
242        self.nodes.get(id)
243    }
244
245    /// The link feeding `target`, if any.
246    pub fn link_to(&self, target: &Port) -> Option<&Link> {
247        self.links.iter().find(|l| &l.target == target)
248    }
249
250    /// Apply `diff` in place.
251    pub fn apply(&mut self, diff: GraphDiff) -> Result<(), GraphError> {
252        // 1. Remove links, then nodes (and any links still touching them).
253        for target in &diff.remove_links {
254            self.links.retain(|l| &l.target != target);
255        }
256        for id in &diff.remove_nodes {
257            self.nodes.remove(id);
258            self.links
259                .retain(|l| &l.target.node != id && !links_source_is_node(&l.source, id));
260            if self.root == Some(*id) {
261                self.root = None;
262            }
263        }
264
265        // 2. Add nodes, then links (replacing any link on the same target).
266        for node in diff.add_nodes {
267            self.nodes.insert(node.id, node);
268        }
269        for link in diff.add_links {
270            self.links.retain(|l| l.target != link.target);
271            self.links.push(link);
272        }
273
274        // 3. Predetermined-key overrides.
275        for (port, key) in diff.set_predetermined {
276            let node = self.nodes.get_mut(&port.node).ok_or_else(|| GraphError {
277                message: format!("predetermined key targets unknown node {}", port.node),
278            })?;
279            let io = node
280                .inputs
281                .iter_mut()
282                .chain(node.outputs.iter_mut())
283                .find(|io| io.id == port.port)
284                .ok_or_else(|| GraphError {
285                    message: format!(
286                        "predetermined key targets unknown slot {} on node {}",
287                        port.port, port.node
288                    ),
289                })?;
290            io.predetermined_key = key;
291        }
292
293        // 4. Root and variables.
294        if let Some(root) = diff.set_root {
295            self.root = Some(root);
296        }
297        self.variables.extend(diff.variables);
298        Ok(())
299    }
300}
301
302/// Whether a [`LinkSource`] reads from node `id` (so the link dangles once that
303/// node is removed).
304fn links_source_is_node(source: &LinkSource, id: &Uuid) -> bool {
305    matches!(source, LinkSource::Port(p) if &p.node == id)
306}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311
312    fn node(id: Uuid, function: Uuid) -> Node {
313        Node {
314            id,
315            function,
316            ..Node::default()
317        }
318    }
319
320    #[test]
321    fn load_onto_empty_reproduces_the_graph() {
322        let a = Uuid::from_u128(0xA);
323        let b = Uuid::from_u128(0xB);
324        let mut graph = Graph::empty();
325        graph.nodes.insert(a, node(a, Uuid::from_u128(0xF1)));
326        graph.nodes.insert(b, node(b, Uuid::from_u128(0xF2)));
327        graph.links.push(Link::new(
328            Port::new(b, Uuid::from_u128(0x1)),
329            LinkSource::Port(Port::new(a, Uuid::from_u128(0x2))),
330        ));
331        graph.root = Some(a);
332        graph
333            .variables
334            .insert(Uuid::from_u128(0x9), "battery".into());
335
336        let mut rebuilt = Graph::empty();
337        rebuilt.apply(GraphDiff::load(graph.clone())).unwrap();
338        assert_eq!(rebuilt, graph);
339    }
340
341    #[test]
342    fn load_diff_lists_the_root_node_first() {
343        let a = Uuid::from_u128(0xA);
344        let b = Uuid::from_u128(0xB);
345        let c = Uuid::from_u128(0xC);
346        let mut graph = Graph::empty();
347        for id in [a, b, c] {
348            graph.nodes.insert(id, node(id, Uuid::from_u128(0xF0)));
349        }
350        graph.root = Some(c);
351        let diff = GraphDiff::load(graph);
352        assert_eq!(diff.add_nodes.first().unwrap().id, c);
353    }
354
355    #[test]
356    fn removing_a_node_drops_links_touching_it() {
357        let a = Uuid::from_u128(0xA);
358        let b = Uuid::from_u128(0xB);
359        let mut graph = Graph::empty();
360        graph.nodes.insert(a, node(a, Uuid::from_u128(0xF1)));
361        graph.nodes.insert(b, node(b, Uuid::from_u128(0xF2)));
362        // a's input is fed from b's output; removing b must drop the link.
363        graph.links.push(Link::new(
364            Port::new(a, Uuid::from_u128(0x1)),
365            LinkSource::Port(Port::new(b, Uuid::from_u128(0x2))),
366        ));
367
368        graph
369            .apply(GraphDiff {
370                remove_nodes: vec![b],
371                ..GraphDiff::default()
372            })
373            .unwrap();
374        assert!(!graph.nodes.contains_key(&b));
375        assert!(graph.links.is_empty(), "dangling link dropped");
376    }
377
378    #[test]
379    fn adding_a_link_replaces_the_one_on_the_same_target() {
380        let a = Uuid::from_u128(0xA);
381        let target = Port::new(a, Uuid::from_u128(0x1));
382        let mut graph = Graph::empty();
383        graph.nodes.insert(a, node(a, Uuid::from_u128(0xF1)));
384        graph
385            .apply(GraphDiff {
386                add_links: vec![Link::new(target, LinkSource::Literal(Value::U8(1)))],
387                ..GraphDiff::default()
388            })
389            .unwrap();
390        graph
391            .apply(GraphDiff {
392                add_links: vec![Link::new(target, LinkSource::Literal(Value::U8(2)))],
393                ..GraphDiff::default()
394            })
395            .unwrap();
396        assert_eq!(graph.links.len(), 1);
397        assert_eq!(
398            graph.link_to(&target).unwrap().source,
399            LinkSource::Literal(Value::U8(2))
400        );
401    }
402
403    #[test]
404    fn set_predetermined_key_overrides_the_slot() {
405        let a = Uuid::from_u128(0xA);
406        let slot = Uuid::from_u128(0x1);
407        let mut graph = Graph::empty();
408        graph.nodes.insert(
409            a,
410            Node {
411                id: a,
412                function: Uuid::from_u128(0xF1),
413                inputs: vec![Io::new(slot)],
414                ..Node::default()
415            },
416        );
417        graph
418            .apply(GraphDiff {
419                set_predetermined: vec![(Port::new(a, slot), Some("head/pitch".into()))],
420                ..GraphDiff::default()
421            })
422            .unwrap();
423        assert_eq!(
424            graph.nodes[&a].inputs[0].predetermined_key.as_deref(),
425            Some("head/pitch")
426        );
427    }
428
429    #[test]
430    fn predetermined_key_on_unknown_slot_is_an_error() {
431        let a = Uuid::from_u128(0xA);
432        let mut graph = Graph::empty();
433        graph.nodes.insert(a, node(a, Uuid::from_u128(0xF1)));
434        let err = graph.apply(GraphDiff {
435            set_predetermined: vec![(Port::new(a, Uuid::from_u128(0x2)), Some("k".into()))],
436            ..GraphDiff::default()
437        });
438        assert!(err.is_err());
439    }
440}