Skip to main content

concinnity_core/behavior/
run.rs

1// Evaluating a compiled body: expressions read the world through a per-tick
2// view, nodes append to an effect buffer the caller drains onto the runtime's
3// request queues.
4//
5// Nothing here mutates the world. An expression that cannot produce a value
6// (an empty query's `first`, an unresolved name, a despawned entity) yields
7// None, and the node holding it is skipped rather than guessing.
8
9use alloc::string::String;
10use alloc::vec::Vec;
11
12use crate::behavior::program::{CExpr, CNode, COp};
13use crate::behavior::value::{Arith, Cmp, Val};
14use crate::components::{PlayCue, StoryPlayback, Transform};
15use crate::ecs::{Entity, asset_id::AssetId};
16use crate::math::sqrt;
17
18/// What a behavior may read this tick.
19pub struct View<'a> {
20    /// Seconds this tick advances.
21    pub dt: f32,
22    /// Seconds of simulated time so far.
23    pub elapsed: f32,
24    /// The world's variables, in slot order.
25    pub vars: &'a [Val],
26    /// This instance's locals, in slot order.
27    pub locals: &'a [Val],
28    /// Exactly as wide as the body's binding high-water mark, so a slot is
29    /// always in range and the frame never grows mid-run.
30    pub bindings: &'a mut [Option<Val>],
31    /// The entities each declared query selected this tick, in slot order.
32    pub queries: &'a [Vec<Entity>],
33    /// Resolves a name to the entity carrying it.
34    pub by_name: &'a dyn Fn(AssetId) -> Option<Entity>,
35    /// Reads an entity's transform.
36    pub transforms: &'a dyn Fn(Entity) -> Option<Transform>,
37    /// Whether an entity still exists.
38    pub alive: &'a dyn Fn(Entity) -> bool,
39    /// The entity this run is scoped to, if any.
40    pub self_entity: Option<Entity>,
41    /// Node ids executed this run, recorded only while tracing is requested
42    /// (`None` costs one branch per node).
43    pub trace: &'a mut Option<Vec<u32>>,
44}
45
46/// One world change a behavior asked for, in body order.
47#[derive(Debug, Clone)]
48pub enum Effect {
49    /// Write a world variable.
50    SetVar {
51        /// The variable's slot.
52        slot: u16,
53        /// The value produced.
54        value: Val,
55        /// Add to the current value rather than replacing it.
56        add: bool,
57    },
58    /// Write one of the instance's locals.
59    SetLocal {
60        /// The local's slot.
61        slot: u16,
62        /// The value produced.
63        value: Val,
64        /// Add to the current value rather than replacing it.
65        add: bool,
66    },
67    /// Replace an entity's transform.
68    SetTransform {
69        /// The entity to move.
70        entity: Entity,
71        /// Its new transform.
72        transform: Transform,
73    },
74    /// Copy a template into the world.
75    Spawn(SpawnEffect),
76    /// Remove an entity.
77    Despawn(Entity),
78    /// Move an entity under another parent, or to the root.
79    Reparent {
80        /// The entity to move.
81        child: Entity,
82        /// Its new parent, or `None` for the root.
83        parent: Option<Entity>,
84    },
85    /// Show or hide an entity.
86    Visible(Entity, bool),
87    /// Play an audio cue.
88    Sound(PlayCue),
89    /// Load a scene.
90    Scene {
91        /// The scene to load.
92        scene: AssetId,
93        /// The transition to play.
94        transition: String,
95    },
96    /// Show a screen.
97    Screen(AssetId),
98    /// Drive story playback.
99    Story(StoryPlayback),
100    /// Persist the world's behavior state.
101    Save,
102}
103
104/// A requested copy of a template.
105#[derive(Debug, Clone)]
106pub struct SpawnEffect {
107    /// The template to copy.
108    pub template: AssetId,
109    /// Where the copy starts.
110    pub transform: Transform,
111    /// Seconds the copy lives, or `None` for no limit.
112    pub lifetime: Option<f32>,
113}
114
115fn eval(expr: &CExpr, view: &View<'_>) -> Option<Val> {
116    match expr {
117        CExpr::Lit(v) => Some(*v),
118        CExpr::Var(slot) => view.vars.get(*slot as usize).copied(),
119        CExpr::Local(slot) => view.locals.get(*slot as usize).copied(),
120        CExpr::Bind(slot) => view.bindings.get(*slot as usize).copied().flatten(),
121        CExpr::Named(id) => (view.by_name)(*id).map(Val::Entity),
122        CExpr::SelfEntity => view.self_entity.map(Val::Entity),
123        CExpr::Dt => Some(Val::Float(view.dt)),
124        CExpr::Elapsed => Some(Val::Float(view.elapsed)),
125        CExpr::Position(e) => {
126            let entity = eval(e, view)?.as_entity()?;
127            Some(Val::Vec3((view.transforms)(entity)?.position))
128        }
129        CExpr::Alive(e) => {
130            // An expression that yields no entity at all is not alive.
131            let alive = eval(e, view)
132                .and_then(Val::as_entity)
133                .is_some_and(view.alive);
134            Some(Val::Bool(alive))
135        }
136        CExpr::Distance(a, b) => {
137            let a = (view.transforms)(eval(a, view)?.as_entity()?)?.position;
138            let b = (view.transforms)(eval(b, view)?.as_entity()?)?.position;
139            let d = [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
140            Some(Val::Float(sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2])))
141        }
142        CExpr::First(slot) => view
143            .queries
144            .get(*slot as usize)?
145            .first()
146            .copied()
147            .map(Val::Entity),
148        CExpr::Count(slot) => Some(Val::Int(view.queries.get(*slot as usize)?.len() as i32)),
149        CExpr::Normalize(e) => {
150            let v = eval(e, view)?.as_vec3()?;
151            let len = sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
152            Some(Val::Vec3(if len > f32::EPSILON {
153                [v[0] / len, v[1] / len, v[2] / len]
154            } else {
155                [0.0; 3]
156            }))
157        }
158        CExpr::Arith(op, a, b) => arith(*op, eval(a, view)?, eval(b, view)?),
159        CExpr::Compare(op, a, b) => compare(*op, eval(a, view)?, eval(b, view)?),
160        CExpr::Not(e) => Some(Val::Bool(!eval(e, view)?.as_bool()?)),
161        CExpr::All(items) => {
162            for item in items {
163                if !eval(item, view)?.as_bool()? {
164                    return Some(Val::Bool(false));
165                }
166            }
167            Some(Val::Bool(true))
168        }
169        CExpr::Any(items) => {
170            for item in items {
171                if eval(item, view)?.as_bool()? {
172                    return Some(Val::Bool(true));
173                }
174            }
175            Some(Val::Bool(false))
176        }
177        CExpr::Never => None,
178    }
179}
180
181fn arith(op: Arith, a: Val, b: Val) -> Option<Val> {
182    let scalar = |x: f32, y: f32| match op {
183        Arith::Add => x + y,
184        Arith::Sub => x - y,
185        Arith::Mul => x * y,
186        // Division by zero yields zero rather than an infinity that would
187        // silently poison every downstream transform.
188        Arith::Div => {
189            if y.abs() > f32::EPSILON {
190                x / y
191            } else {
192                0.0
193            }
194        }
195    };
196    match (a, b) {
197        (Val::Int(x), Val::Int(y)) => Some(Val::Int(scalar(x as f32, y as f32) as i32)),
198        (Val::Vec3(x), Val::Vec3(y)) => Some(Val::Vec3([
199            scalar(x[0], y[0]),
200            scalar(x[1], y[1]),
201            scalar(x[2], y[2]),
202        ])),
203        (Val::Vec3(v), other) => {
204            let s = other.as_f32()?;
205            Some(Val::Vec3([
206                scalar(v[0], s),
207                scalar(v[1], s),
208                scalar(v[2], s),
209            ]))
210        }
211        (other, Val::Vec3(v)) => {
212            let s = other.as_f32()?;
213            Some(Val::Vec3([
214                scalar(s, v[0]),
215                scalar(s, v[1]),
216                scalar(s, v[2]),
217            ]))
218        }
219        (x, y) => Some(Val::Float(scalar(x.as_f32()?, y.as_f32()?))),
220    }
221}
222
223fn compare(op: Cmp, a: Val, b: Val) -> Option<Val> {
224    let equal = match (a, b) {
225        (Val::Entity(x), Val::Entity(y)) => x == y,
226        (Val::Bool(x), Val::Bool(y)) => x == y,
227        (x, y) => x.as_f32()? == y.as_f32()?,
228    };
229    Some(Val::Bool(match op {
230        Cmp::Eq => equal,
231        Cmp::Ne => !equal,
232        Cmp::Lt => a.as_f32()? < b.as_f32()?,
233        Cmp::Le => a.as_f32()? <= b.as_f32()?,
234        Cmp::Gt => a.as_f32()? > b.as_f32()?,
235        Cmp::Ge => a.as_f32()? >= b.as_f32()?,
236    }))
237}
238
239/// Run a compiled body against `view`, appending what it asked for to `out`.
240pub fn exec(nodes: &[CNode], view: &mut View<'_>, out: &mut Vec<Effect>) {
241    for node in nodes {
242        exec_node(node, view, out);
243    }
244}
245
246fn exec_node(node: &CNode, view: &mut View<'_>, out: &mut Vec<Effect>) {
247    if let Some(t) = view.trace.as_mut() {
248        t.push(node.id);
249    }
250    match &node.op {
251        COp::If {
252            cond,
253            then,
254            otherwise,
255        } => {
256            let Some(Val::Bool(pass)) = eval(cond, view) else {
257                return;
258            };
259            exec(if pass { then } else { otherwise }, view, out);
260        }
261        COp::ForEach { query, bind, body } => {
262            let Some(entities) = view.queries.get(*query as usize) else {
263                return;
264            };
265            // Cloned so the body may read other queries (and mutate bindings)
266            // while this one is iterated.
267            for entity in entities.clone() {
268                set_binding(view, *bind, Some(Val::Entity(entity)));
269                exec(body, view, out);
270            }
271        }
272        COp::Let { bind, value } => {
273            let value = eval(value, view);
274            set_binding(view, *bind, value);
275        }
276        COp::SetVar { slot, value, add } => {
277            let Some(value) = eval(value, view) else {
278                return;
279            };
280            out.push(Effect::SetVar {
281                slot: *slot,
282                value,
283                add: *add,
284            });
285        }
286        COp::SetLocal { slot, value, add } => {
287            let Some(value) = eval(value, view) else {
288                return;
289            };
290            out.push(Effect::SetLocal {
291                slot: *slot,
292                value,
293                add: *add,
294            });
295        }
296        COp::SetTransform {
297            entity,
298            position,
299            rotation_deg,
300            scale,
301        } => {
302            let Some(entity) = eval(entity, view).and_then(Val::as_entity) else {
303                return;
304            };
305            let Some(mut transform) = (view.transforms)(entity) else {
306                return;
307            };
308            let field = |expr: &Option<CExpr>, into: &mut [f32; 3]| {
309                if let Some(expr) = expr
310                    && let Some(v) = eval(expr, view).and_then(Val::as_vec3)
311                {
312                    *into = v;
313                }
314            };
315            field(position, &mut transform.position);
316            field(rotation_deg, &mut transform.rotation_deg);
317            field(scale, &mut transform.scale);
318            out.push(Effect::SetTransform { entity, transform });
319        }
320        COp::Spawn {
321            template,
322            position,
323            rotation_deg,
324            scale,
325            lifetime,
326            bind,
327        } => {
328            out.push(Effect::Spawn(SpawnEffect {
329                template: *template,
330                transform: Transform {
331                    position: *position,
332                    rotation_deg: *rotation_deg,
333                    scale: *scale,
334                },
335                lifetime: (*lifetime > 0.0).then_some(*lifetime),
336            }));
337            // The entity does not exist until SpawnSystem applies the request,
338            // so the binding holds nothing this tick and reads skip their node.
339            if let Some(bind) = bind {
340                set_binding(view, *bind, None);
341            }
342        }
343        COp::Despawn(target) => {
344            if let Some(entity) = eval(target, view).and_then(Val::as_entity) {
345                out.push(Effect::Despawn(entity));
346            }
347        }
348        COp::Reparent { child, parent } => {
349            let Some(child) = eval(child, view).and_then(Val::as_entity) else {
350                return;
351            };
352            // A named-but-unresolvable parent skips, so a stale reference never
353            // silently detaches the child to a root.
354            let parent = match parent {
355                Some(expr) => match eval(expr, view).and_then(Val::as_entity) {
356                    Some(entity) => Some(entity),
357                    None => return,
358                },
359                None => None,
360            };
361            out.push(Effect::Reparent { child, parent });
362        }
363        COp::Visible(target, visible) => {
364            if let Some(entity) = eval(target, view).and_then(Val::as_entity) {
365                out.push(Effect::Visible(entity, *visible));
366            }
367        }
368        COp::Sound { clip, kind, volume } => out.push(Effect::Sound(PlayCue {
369            clip: *clip,
370            kind: *kind,
371            volume: *volume,
372            priority: 0,
373        })),
374        COp::Scene { scene, transition } => out.push(Effect::Scene {
375            scene: *scene,
376            transition: transition.clone(),
377        }),
378        COp::Screen(screen) => out.push(Effect::Screen(*screen)),
379        COp::Story(playback) => out.push(Effect::Story(*playback)),
380        COp::Save => out.push(Effect::Save),
381        COp::Never => {}
382    }
383}
384
385fn set_binding(view: &mut View<'_>, slot: u16, value: Option<Val>) {
386    if let Some(slot) = view.bindings.get_mut(slot as usize) {
387        *slot = value;
388    }
389}