Skip to main content

arora_behavior/
lib.rs

1//! Behavior interpreters: the executors the Arora runtime ticks each step.
2//!
3//! Two things share the word "behavior", and this crate is about only one of
4//! them:
5//!
6//! - A **behavior** (the noun) is an *authored, editable representation* of what
7//!   a device should do — a behavior tree, a node graph — produced in a visual
8//!   editor (Studio, the Vizij Workspace) and shipped as data.
9//! - A [`BehaviorInterpreter`] is the *runtime-level executor* that runs one of
10//!   those: a behavior-tree interpreter, a node-graph interpreter. It is the
11//!   thing the runtime actually ticks.
12//!
13//! The runtime holds one `Box<dyn BehaviorInterpreter>` and ticks it — swapping
14//! the interpreter replaces the behavior. The behavior tree is one interpreter
15//! (`arora-behavior-tree`'s [`BehaviorTreeInterpreter`]); a Vizij node graph is
16//! another. Adding a new *kind of authored behavior* means adding a new
17//! interpreter here. Hand-implementing the trait to hard-code a single behavior
18//! in Rust is possible, but it is a corner case — the promoted path is to author
19//! a behavior in an editor and let an interpreter run it.
20//!
21//! An authored behavior is a [`graph::Graph`] — nodes bound to functions, with
22//! typed I/Os and links — and interpreters are edited by
23//! [`apply`](BehaviorInterpreter::apply)ing a [`graph::GraphDiff`]. Loading a
24//! behavior is applying a diff onto an empty graph. See [`graph`] for the model.
25//!
26//! Each tick an interpreter gets a [`BehaviorContext`]: the shared
27//! [`DataStore`](arora_types::data::DataStore) (read inputs, write intent /
28//! outputs) and a [`CallBridge`](arora_types::call::CallBridge) (so a
29//! module-calling interpreter like the behavior tree can reach the engine). An
30//! interpreter uses whichever it needs — a graph reads/writes the store; the
31//! tree drives the caller.
32//!
33//! Timing is not a tick argument. The runtime publishes the frame's clock into
34//! the store under the [`golden`] keys before it ticks, so an interpreter that
35//! needs `dt` or elapsed time reads it from the store like any other slot.
36
37use arora_types::call::CallBridge;
38use arora_types::data::DataStore;
39
40pub mod golden;
41pub mod graph;
42pub mod interpreter_module;
43
44pub use graph::{Graph, GraphDiff};
45
46/// Whether an interpreter wants to be ticked again.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum BehaviorStatus {
49    /// Tick me again next step (a node graph; a tree still running).
50    Running,
51    /// I reached a terminal state; the runtime may drop me.
52    Done,
53}
54
55/// What a [`BehaviorInterpreter`] receives each
56/// [`tick`](BehaviorInterpreter::tick).
57pub struct BehaviorContext<'a> {
58    /// The shared blackboard: read inputs, write intent / outputs.
59    pub store: &'a dyn DataStore,
60    /// The module-call bridge (the engine), for interpreters that call modules.
61    pub call_bridge: &'a mut dyn CallBridge,
62}
63
64/// An interpreter failed to tick.
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct BehaviorError {
67    /// Human-readable description.
68    pub message: String,
69}
70
71impl std::fmt::Display for BehaviorError {
72    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73        f.write_str(&self.message)
74    }
75}
76
77impl std::error::Error for BehaviorError {}
78
79/// A behavior *executor*: something the Arora runtime ticks once per step to run
80/// an authored behavior.
81///
82/// Implemented by the behavior tree (`arora-behavior-tree`'s
83/// [`BehaviorTreeInterpreter`]) and by other interpreters such as a Vizij
84/// node-graph interpreter. The runtime holds one `Box<dyn BehaviorInterpreter>`
85/// and ticks it without knowing which is which.
86///
87/// Implement this to add a new *kind of executor* (a new authored-behavior
88/// representation the runtime can run), not to hand-code one particular
89/// behavior — authored behaviors come from the visual editors, run by an
90/// existing interpreter.
91///
92/// Not `Send`: the runtime is a single-owner, single-thread step loop.
93pub trait BehaviorInterpreter {
94    /// Advance one step. Return [`BehaviorStatus::Running`] to be ticked again,
95    /// or [`BehaviorStatus::Done`] to be dropped.
96    fn tick(&mut self, ctx: &mut BehaviorContext) -> Result<BehaviorStatus, BehaviorError>;
97
98    /// Edit the running behavior by applying a [`GraphDiff`](graph::GraphDiff):
99    /// add/remove nodes and links, set/override predetermined keys. **Loading** a
100    /// behavior is applying a diff onto an empty graph.
101    ///
102    /// The default rejects edition — override it in interpreters that carry an
103    /// editable [`graph::Graph`] (the behavior tree does). A hand-coded,
104    /// non-graph interpreter (the corner case above) can leave this as-is.
105    ///
106    /// `apply` validates and stores the edit; an implementation may defer
107    /// re-lowering its runtime form to the next [`tick`](Self::tick) (which
108    /// carries the store in its context), so a lowering problem can surface
109    /// there rather than here. This keeps `apply` callable from anywhere a
110    /// [`GraphDiff`](graph::GraphDiff) arrives — notably the interpreter's
111    /// engine-registered edit function, which holds no store.
112    fn apply(&mut self, diff: graph::GraphDiff) -> Result<(), BehaviorError> {
113        let _ = diff;
114        Err(BehaviorError {
115            message: "this interpreter does not support graph edition".to_string(),
116        })
117    }
118
119    /// Replace the running behavior with `graph`, whole. Like
120    /// [`apply`](Self::apply) it is store-less — an implementation may defer
121    /// lowering to the next [`tick`](Self::tick) — so it is callable from the
122    /// interpreter's engine-registered load function. The default rejects
123    /// loading, for hand-coded interpreters that carry no graph.
124    fn load(&mut self, graph: graph::Graph) -> Result<(), BehaviorError> {
125        let _ = graph;
126        Err(BehaviorError {
127            message: "this interpreter does not support loading a graph".to_string(),
128        })
129    }
130}