Skip to main content

mittens_engine/scripting/
component_method_registry.rs

1use crate::engine::ecs::component::{EmissiveComponent, TransformComponent, TransitionComponent};
2use crate::engine::ecs::{ComponentId, IntentValue, World};
3use crate::scripting::object::Value;
4
5pub(crate) fn supports_component_method(component_type: &str, method: &str) -> bool {
6    (matches!(
7        component_type,
8        "T" | "Transform" | "TransformComponent" | "transform"
9    ) && matches!(method, "update_transform" | "look_at"))
10        || (matches!(
11            component_type,
12            "EM" | "Emissive" | "EmissiveComponent" | "emissive"
13        ) && matches!(method, "set_intensity" | "on" | "off"))
14        || (matches!(
15            component_type,
16            "HttpClient" | "HttpClientComponent" | "http_client"
17        ) && matches!(method, "get" | "post" | "put" | "delete"))
18        || (matches!(
19            component_type,
20            "HttpServer" | "HttpServerComponent" | "http_server"
21        ) && matches!(method, "reply_text"))
22}
23
24pub(crate) fn invoke_component_method(
25    world: &mut World,
26    id: ComponentId,
27    component_type: &str,
28    method: &str,
29    args: &[Value],
30    mut emit_intent: impl FnMut(IntentValue),
31) -> Result<Value, String> {
32    match (component_type, method) {
33        ("T" | "Transform" | "TransformComponent" | "transform", "update_transform") => {
34            let [translation, rotation_euler, scale] = match args {
35                [translation, rotation, scale] => [
36                    value_as_f32_array::<3>(translation)?,
37                    value_as_f32_array::<3>(rotation)?,
38                    value_as_f32_array::<3>(scale)?,
39                ],
40                other => {
41                    return Err(format!(
42                        "update_transform: expected three vec3 array arguments, got {:?}",
43                        other
44                    ));
45                }
46            };
47
48            world
49                .get_component_by_id_as::<TransformComponent>(id)
50                .ok_or_else(|| "update_transform(): not a TransformComponent".to_string())?;
51
52            emit_intent(IntentValue::UpdateTransform {
53                component_ids: vec![id],
54                translation,
55                rotation_quat_xyzw: TransformComponent::new()
56                    .with_rotation_euler(rotation_euler[0], rotation_euler[1], rotation_euler[2])
57                    .transform
58                    .rotation,
59                scale,
60            });
61            Ok(Value::Null)
62        }
63        ("T" | "Transform" | "TransformComponent" | "transform", "look_at") => {
64            let [target_world] = match args {
65                [target_world] => [value_as_f32_array::<3>(target_world)?],
66                other => {
67                    return Err(format!(
68                        "look_at: expected one vec3 array argument, got {:?}",
69                        other
70                    ));
71                }
72            };
73
74            world
75                .get_component_by_id_as::<TransformComponent>(id)
76                .ok_or_else(|| "look_at(): not a TransformComponent".to_string())?;
77
78            emit_intent(IntentValue::LookAt {
79                component_ids: vec![id],
80                target_world,
81            });
82            Ok(Value::Null)
83        }
84        ("EM" | "Emissive" | "EmissiveComponent" | "emissive", "set_intensity" | "on" | "off") => {
85            let intensity = match method {
86                "on" => 1.0,
87                "off" => 0.0,
88                "set_intensity" => match args.first() {
89                    Some(Value::Number(n)) => (*n as f32).max(0.0),
90                    Some(other) => {
91                        return Err(format!(
92                            "set_intensity: expected number argument, got {:?}",
93                            other
94                        ));
95                    }
96                    None => return Err("set_intensity: missing number argument".into()),
97                },
98                _ => unreachable!(),
99            };
100
101            world
102                .get_component_by_id_as::<EmissiveComponent>(id)
103                .ok_or_else(|| format!("{method}(): not an EmissiveComponent"))?;
104
105            let has_transition_child = world.children_of(id).iter().any(|&child| {
106                world
107                    .get_component_by_id_as::<TransitionComponent>(child)
108                    .is_some()
109            });
110            let is_attached = world.parent_of(id).is_some();
111            if !(is_attached && has_transition_child) {
112                let emissive = world
113                    .get_component_by_id_as_mut::<EmissiveComponent>(id)
114                    .ok_or_else(|| format!("{method}(): not an EmissiveComponent"))?;
115                emissive.intensity = intensity;
116            }
117
118            emit_intent(IntentValue::SetEmissiveIntensity {
119                component_ids: vec![id],
120                intensity,
121            });
122            Ok(Value::Null)
123        }
124        ("HttpClient" | "HttpClientComponent" | "http_client", "get" | "delete") => {
125            let [url] = match args {
126                [url] => [value_as_string(url, method)?],
127                other => {
128                    return Err(format!(
129                        "{method}: expected one string url argument, got {:?}",
130                        other
131                    ));
132                }
133            };
134            emit_intent(IntentValue::HttpClientRequest {
135                component_id: id,
136                method: method.to_ascii_uppercase(),
137                url,
138                headers: vec![],
139                body_text: None,
140            });
141            Ok(Value::Null)
142        }
143        ("HttpClient" | "HttpClientComponent" | "http_client", "post" | "put") => {
144            let (url, body_text) = match args {
145                [url, body_text] => (
146                    value_as_string(url, method)?,
147                    value_as_string(body_text, method)?,
148                ),
149                other => {
150                    return Err(format!(
151                        "{method}: expected url and body_text string arguments, got {:?}",
152                        other
153                    ));
154                }
155            };
156            emit_intent(IntentValue::HttpClientRequest {
157                component_id: id,
158                method: method.to_ascii_uppercase(),
159                url,
160                headers: vec![],
161                body_text: Some(body_text),
162            });
163            Ok(Value::Null)
164        }
165        ("HttpServer" | "HttpServerComponent" | "http_server", "reply_text") => {
166            let (request_id, status, body_text) = match args {
167                [request, status, body_text] => (
168                    request_id_from_value(request)?,
169                    value_as_u16(status, method)?,
170                    value_as_string(body_text, method)?,
171                ),
172                other => {
173                    return Err(format!(
174                        "reply_text: expected request, status, body_text arguments, got {:?}",
175                        other
176                    ));
177                }
178            };
179            emit_intent(IntentValue::HttpServerReply {
180                component_id: id,
181                request_id,
182                status,
183                headers: vec![],
184                body_text,
185            });
186            Ok(Value::Null)
187        }
188        _ => Err(format!(
189            "unsupported live component method '{}.{}'",
190            component_type, method
191        )),
192    }
193}
194
195fn value_as_f32_array<const N: usize>(value: &Value) -> Result<[f32; N], String> {
196    let Value::Array(values) = value else {
197        return Err(format!("expected array, got {:?}", value));
198    };
199    if values.len() != N {
200        return Err(format!("expected array of len {}, got {}", N, values.len()));
201    }
202    let mut out = [0.0_f32; N];
203    for (i, value) in values.iter().enumerate() {
204        match value {
205            Value::Number(n) => out[i] = *n as f32,
206            other => return Err(format!("expected numeric array element, got {:?}", other)),
207        }
208    }
209    Ok(out)
210}
211
212fn value_as_string(value: &Value, method: &str) -> Result<String, String> {
213    match value {
214        Value::String(s) => Ok(s.clone()),
215        other => Err(format!("{method}: expected string, got {:?}", other)),
216    }
217}
218
219fn value_as_u16(value: &Value, method: &str) -> Result<u16, String> {
220    match value {
221        Value::Number(n) if *n >= 0.0 && *n <= u16::MAX as f64 => Ok(*n as u16),
222        other => Err(format!("{method}: expected status number, got {:?}", other)),
223    }
224}
225
226fn request_id_from_value(value: &Value) -> Result<u64, String> {
227    let Value::Map(map) = value else {
228        return Err(format!(
229            "reply_text: expected request object, got {:?}",
230            value
231        ));
232    };
233    let Some(Value::Number(request_id)) = map.get("request_id") else {
234        return Err("reply_text: request missing numeric request_id".to_string());
235    };
236    if *request_id < 0.0 {
237        return Err("reply_text: request_id must be non-negative".to_string());
238    }
239    Ok(*request_id as u64)
240}