Skip to main content

mittens_engine/engine/ecs/component/
action.rs

1use super::{Component, ComponentRef};
2use crate::engine::ecs::{ComponentId, IntentValue};
3
4#[derive(Debug, Clone)]
5pub struct ActionComponent {
6    /// Runtime intent. ComponentId slots inside are `ComponentId::null()`
7    /// placeholders until resolution; after resolution, they're real ids
8    /// filled in from `target_sources` in the variant's declaration order.
9    pub signal: IntentValue,
10    /// Authoring metadata, one entry per ComponentId slot in `signal`,
11    /// ordered by the slot's declaration order in the variant. Used by
12    /// dump (lossless round-trip) and by resolution (look up ids).
13    pub target_sources: Vec<ComponentRef>,
14    /// Whether `signal`'s ComponentId slots hold real ids (true) or null
15    /// placeholders (false). Set by the resolution pass invoked by
16    /// `AnimationSystem` per the owning `AnimationComponent`'s configured
17    /// resolve timing.
18    pub resolved: bool,
19}
20
21impl ActionComponent {
22    /// Construct from an already-resolved IntentValue (no ComponentId
23    /// targets, or all targets pre-resolved). Use this for built-in /
24    /// engine-emitted actions; MMS authoring goes through the registry
25    /// which builds with `new_authored` instead.
26    pub fn new(signal: IntentValue) -> Self {
27        Self {
28            signal,
29            target_sources: Vec::new(),
30            resolved: true,
31        }
32    }
33
34    /// Construct from a signal whose ComponentId slots are placeholders
35    /// plus the authoring sources for each slot (in declaration order).
36    /// `resolved` starts false; resolution happens when the owning
37    /// `AnimationSystem` first processes this action.
38    pub fn new_authored(signal: IntentValue, target_sources: Vec<ComponentRef>) -> Self {
39        Self {
40            signal,
41            target_sources,
42            resolved: false,
43        }
44    }
45
46    pub fn print(message: impl Into<String>) -> Self {
47        Self::new(IntentValue::Print {
48            message: message.into(),
49        })
50    }
51}
52
53impl Default for ActionComponent {
54    fn default() -> Self {
55        Self {
56            signal: IntentValue::Noop,
57            target_sources: Vec::new(),
58            resolved: true,
59        }
60    }
61}
62
63// ---------------------------------------------------------------------------
64// IntentValue slot enumeration
65//
66// Used by ActionComponent for: (a) sanity-checking that `target_sources.len()`
67// matches the number of ComponentId slots in the variant; (b) reading current
68// slot values for dump; (c) writing resolved ids into slots after lookup.
69//
70// Only covers the variants ActionComponent.signal actually carries. Variants
71// the engine emits internally (Register*, intra-system bookkeeping) are not
72// authorable from MMS and never appear here — they return zero slots.
73// ---------------------------------------------------------------------------
74
75/// Number of ComponentId slots in `signal`'s variant (counts each element
76/// of any `Vec<ComponentId>` field).
77pub fn signal_target_slot_count(signal: &IntentValue) -> usize {
78    use IntentValue::*;
79    match signal {
80        Noop | Print { .. } => 0,
81
82        SetColor { component_ids, .. }
83        | SetText { component_ids, .. }
84        | SetEmissiveIntensity { component_ids, .. }
85        | SetPosition { component_ids, .. }
86        | LookAt { component_ids, .. }
87        | GLTFArmatureVisible { component_ids, .. }
88        | Detach { component_ids }
89        | RemoveSubtree { component_ids }
90        | AudioGraphRebuild { component_ids }
91        | RequestRaycast { component_ids }
92        | AudioLowPassSetCutoffHz { component_ids, .. }
93        | AudioBandPassSetCenterHz { component_ids, .. }
94        | OscillatorSetEnabled { component_ids, .. }
95        | OscillatorSetPitch { component_ids, .. }
96        | OscillatorScheduleSetPitch { component_ids, .. }
97        | AudioSchedulePlay { component_ids, .. }
98        | UpdateTransform { component_ids, .. } => component_ids.len(),
99
100        Attach { parents, .. } | AttachClone { parents, .. } => parents.len() + 1,
101        RemoveChild { parents, .. } | RemoveChildren { parents } => parents.len(),
102
103        // Variants ActionComponent never carries — no authored targets.
104        _ => 0,
105    }
106}
107
108/// Apply resolved ids back into `signal`'s ComponentId slots in declaration
109/// order. Caller must guarantee `ids.len() == signal_target_slot_count(signal)`.
110pub fn apply_resolved_targets(signal: &mut IntentValue, ids: &[ComponentId]) {
111    use IntentValue::*;
112    let mut cursor = 0usize;
113    let mut take = |n: usize| -> &[ComponentId] {
114        let slice = &ids[cursor..cursor + n];
115        cursor += n;
116        slice
117    };
118    match signal {
119        Noop | Print { .. } => {}
120
121        SetColor { component_ids, .. }
122        | SetText { component_ids, .. }
123        | SetEmissiveIntensity { component_ids, .. }
124        | SetPosition { component_ids, .. }
125        | LookAt { component_ids, .. }
126        | GLTFArmatureVisible { component_ids, .. }
127        | Detach { component_ids }
128        | RemoveSubtree { component_ids }
129        | AudioGraphRebuild { component_ids }
130        | RequestRaycast { component_ids }
131        | AudioLowPassSetCutoffHz { component_ids, .. }
132        | AudioBandPassSetCenterHz { component_ids, .. }
133        | OscillatorSetEnabled { component_ids, .. }
134        | OscillatorSetPitch { component_ids, .. }
135        | OscillatorScheduleSetPitch { component_ids, .. }
136        | AudioSchedulePlay { component_ids, .. }
137        | UpdateTransform { component_ids, .. } => {
138            let n = component_ids.len();
139            component_ids.copy_from_slice(take(n));
140        }
141
142        Attach { parents, child }
143        | AttachClone {
144            parents,
145            prefab_root: child,
146        } => {
147            let n = parents.len();
148            parents.copy_from_slice(take(n));
149            *child = take(1)[0];
150        }
151        RemoveChild { parents, .. } | RemoveChildren { parents } => {
152            let n = parents.len();
153            parents.copy_from_slice(take(n));
154        }
155
156        _ => {}
157    }
158    debug_assert_eq!(
159        cursor,
160        ids.len(),
161        "slot count mismatch in apply_resolved_targets"
162    );
163}
164
165impl Component for ActionComponent {
166    fn name(&self) -> &'static str {
167        "action"
168    }
169
170    fn as_any(&self) -> &dyn std::any::Any {
171        self
172    }
173
174    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
175        self
176    }
177
178    fn init(&mut self, emit: &mut dyn crate::engine::ecs::SignalEmitter, component: ComponentId) {
179        emit.push_intent_now(
180            component,
181            crate::engine::ecs::IntentValue::RegisterAction {
182                component_ids: vec![component],
183            },
184        );
185    }
186
187    fn to_mms_ast(
188        &self,
189        _world: &crate::engine::ecs::World,
190    ) -> crate::scripting::ast::ComponentExpression {
191        use crate::engine::ecs::component::ce_helpers::*;
192        use crate::scripting::ast::Expression;
193
194        // Render an ComponentRef back to the surface form the author wrote.
195        // Guid → "@uuid:<hex>". Query → the original selector string. Both
196        // are Expression::String so the registry's arg_target_source can
197        // re-parse them.
198        fn target_expr(t: &ComponentRef) -> Expression {
199            match t {
200                ComponentRef::Guid(u) => Expression::String(format!("@uuid:{u}")),
201                ComponentRef::Query(s) => Expression::String(s.clone()),
202            }
203        }
204        // For "vec-of-targets" arg slots: always emit an Array so the
205        // dump form matches the registry's expectation
206        // (`arg_target_source_vec` accepts arrays or single values, but
207        // emitting an array is unambiguous and round-trip-stable).
208        let targets_expr = |slice: &[ComponentRef]| -> Expression {
209            Expression::Array(slice.iter().map(target_expr).collect())
210        };
211
212        match &self.signal {
213            IntentValue::Noop => ce_call("Action", "noop", vec![]),
214            IntentValue::Print { message } => ce_call("Action", "print", vec![s(message)]),
215            IntentValue::SetColor { rgba, .. } => ce_call(
216                "Action",
217                "set_color",
218                vec![
219                    targets_expr(&self.target_sources),
220                    array(nums(rgba.iter().map(|&v| v as f64))),
221                ],
222            ),
223            IntentValue::SetText { text, .. } => ce_call(
224                "Action",
225                "set_text",
226                vec![targets_expr(&self.target_sources), s(text)],
227            ),
228            IntentValue::SetEmissiveIntensity { intensity, .. } => ce_call(
229                "Action",
230                "set_emissive_intensity",
231                vec![targets_expr(&self.target_sources), num(*intensity as f64)],
232            ),
233            IntentValue::SetPosition { position, .. } => ce_call(
234                "Action",
235                "set_position",
236                vec![
237                    targets_expr(&self.target_sources),
238                    array(nums(position.iter().map(|&v| v as f64))),
239                ],
240            ),
241            IntentValue::Attach { .. } => {
242                // target_sources convention: [parents..., child].
243                let (parents, child) = self
244                    .target_sources
245                    .split_at(self.target_sources.len().saturating_sub(1));
246                let child_expr = child.first().map(target_expr).unwrap_or_else(|| s(""));
247                ce_call("Action", "attach", vec![targets_expr(parents), child_expr])
248            }
249            IntentValue::AttachClone { .. } => {
250                let (parents, prefab) = self
251                    .target_sources
252                    .split_at(self.target_sources.len().saturating_sub(1));
253                let prefab_expr = prefab.first().map(target_expr).unwrap_or_else(|| s(""));
254                ce_call(
255                    "Action",
256                    "attach_clone",
257                    vec![targets_expr(parents), prefab_expr],
258                )
259            }
260            IntentValue::Detach { .. } => {
261                ce_call("Action", "detach", vec![targets_expr(&self.target_sources)])
262            }
263            IntentValue::RemoveSubtree { .. } => ce_call(
264                "Action",
265                "remove_subtree",
266                vec![targets_expr(&self.target_sources)],
267            ),
268            IntentValue::RequestRaycast { .. } => ce_call(
269                "Action",
270                "request_raycast",
271                vec![targets_expr(&self.target_sources)],
272            ),
273            IntentValue::UpdateTransform {
274                translation,
275                rotation_quat_xyzw,
276                scale,
277                ..
278            } => {
279                // Dump uses the quat form (`update_transform_quat`) for
280                // lossless round-trip. The runtime form is always a
281                // quaternion; the euler authoring path is one-way.
282                let target_expr_ = self
283                    .target_sources
284                    .first()
285                    .map(target_expr)
286                    .unwrap_or_else(|| s(""));
287                ce_call(
288                    "Action",
289                    "update_transform_quat",
290                    vec![
291                        target_expr_,
292                        array(nums(translation.iter().map(|&v| v as f64))),
293                        array(nums(rotation_quat_xyzw.iter().map(|&v| v as f64))),
294                        array(nums(scale.iter().map(|&v| v as f64))),
295                    ],
296                )
297            }
298            // Variants ActionComponent shouldn't carry (engine-internal,
299            // or not yet wired into the MMS surface). Fall back to a
300            // noop so dump still produces parseable output.
301            _ => ce_call("Action", "noop", vec![]),
302        }
303    }
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309    use slotmap::{Key, KeyData};
310
311    fn cid(n: u64) -> ComponentId {
312        ComponentId::from(KeyData::from_ffi(n))
313    }
314
315    #[test]
316    fn slot_count_matches_apply_for_attach_variant() {
317        let mut iv = IntentValue::Attach {
318            parents: vec![ComponentId::null(), ComponentId::null()],
319            child: ComponentId::null(),
320        };
321        assert_eq!(signal_target_slot_count(&iv), 3);
322        apply_resolved_targets(&mut iv, &[cid(10), cid(11), cid(12)]);
323        let IntentValue::Attach { parents, child } = iv else {
324            unreachable!()
325        };
326        assert_eq!(parents, vec![cid(10), cid(11)]);
327        assert_eq!(child, cid(12));
328    }
329
330    #[test]
331    fn slot_count_matches_apply_for_vec_only_variant() {
332        let mut iv = IntentValue::SetColor {
333            component_ids: vec![ComponentId::null(), ComponentId::null()],
334            rgba: [1.0, 0.0, 0.0, 1.0],
335        };
336        assert_eq!(signal_target_slot_count(&iv), 2);
337        apply_resolved_targets(&mut iv, &[cid(7), cid(8)]);
338        let IntentValue::SetColor { component_ids, .. } = iv else {
339            unreachable!()
340        };
341        assert_eq!(component_ids, vec![cid(7), cid(8)]);
342    }
343
344    #[test]
345    fn no_slots_for_print() {
346        let iv = IntentValue::Print {
347            message: "hi".into(),
348        };
349        assert_eq!(signal_target_slot_count(&iv), 0);
350    }
351}