mittens_engine/scripting/
component_method_registry.rs1use 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}