1use std::collections::HashMap;
3use std::sync::{Mutex, RwLock};
4
5use anyhow::Context as _;
6use pite_core::{NodeId, NodeTree, SignalRegistry};
7use pyo3::prelude::*;
8use pyo3::types::PyTuple;
9
10static CURRENT_HOST: RwLock<Option<std::sync::Arc<ScriptHost>>> = RwLock::new(None);
11
12#[cfg(test)]
13pub(crate) static HOST_SERIAL: Mutex<()> = Mutex::new(());
14
15pub fn set_current_host(host: std::sync::Arc<ScriptHost>) {
16 *CURRENT_HOST.write().unwrap() = Some(host);
17}
18
19pub(crate) fn current_host() -> Option<std::sync::Arc<ScriptHost>> {
20 CURRENT_HOST.read().unwrap().clone()
21}
22
23pub(crate) fn bound_target(callable: &Bound<'_, PyAny>) -> anyhow::Result<(String, String)> {
24 let owner = callable
25 .getattr("__self__")
26 .map_err(|_| anyhow::anyhow!("connect needs a bound pite method"))?;
27 let node_path: String = owner
28 .getattr("node_path")
29 .map_err(|_| anyhow::anyhow!("connect target is not a pite node method"))?
30 .extract()?;
31 let name: String = callable
32 .getattr("__name__")
33 .map_err(|_| anyhow::anyhow!("connect target has no method name"))?
34 .extract()?;
35 Ok((node_path, name))
36}
37
38pub struct ScriptHost {
39 tree: Mutex<NodeTree>,
40 instances: Mutex<HashMap<String, Py<PyAny>>>,
41 signals: Mutex<HashMap<String, SignalRegistry>>,
42 connections: Mutex<HashMap<(String, String), Vec<(String, String)>>>,
43 callable_connections: Mutex<HashMap<(String, String), Vec<Py<PyAny>>>>,
44 proxy_cache: Mutex<HashMap<(String, String), Py<NodeProxy>>>,
45}
46
47pub const SIGNAL_TYPES: &[&str] = &["int", "float", "str", "bool"];
48
49#[derive(Debug, Clone, PartialEq)]
50pub enum PayloadValue {
51 Int(i64),
52 Float(f64),
53 Str(String),
54 Bool(bool),
55}
56
57impl ScriptHost {
58 pub fn new(tree: NodeTree) -> Self {
59 Self {
60 tree: Mutex::new(tree),
61 instances: Mutex::new(HashMap::new()),
62 signals: Mutex::new(HashMap::new()),
63 connections: Mutex::new(HashMap::new()),
64 callable_connections: Mutex::new(HashMap::new()),
65 proxy_cache: Mutex::new(HashMap::new()),
66 }
67 }
68
69 pub fn get_node_position(&self, id: &str) -> Option<(f64, f64)> {
70 use pite_core::PropValue;
71 let tree = self.tree.lock().unwrap();
72 let node = tree.get(&NodeId::from(id.to_string()))?;
73 match node.props.get("position") {
74 Some(PropValue::Vec2(x, y)) => Some((*x, *y)),
75 _ => Some((0.0, 0.0)),
76 }
77 }
78
79 pub fn set_node_position(&self, id: &str, x: f64, y: f64) {
80 use pite_core::PropValue;
81 let mut tree = self.tree.lock().unwrap();
82 if let Some(node) = tree.get_mut(&NodeId::from(id.to_string())) {
83 node.props
84 .insert("position".to_string(), PropValue::Vec2(x, y));
85 }
86 }
87
88 pub fn set_node_text(&self, id: &str, text: &str) {
89 use pite_core::PropValue;
90 let mut tree = self.tree.lock().unwrap();
91 if let Some(node) = tree.get_mut(&NodeId::from(id.to_string())) {
92 node.props
93 .insert("text".to_string(), PropValue::Str(text.to_string()));
94 }
95 }
96
97 pub fn set_node_size(&self, id: &str, w: f64, h: f64) {
98 use pite_core::PropValue;
99 let mut tree = self.tree.lock().unwrap();
100 if let Some(node) = tree.get_mut(&NodeId::from(id.to_string())) {
101 node.props.insert("size".to_string(), PropValue::Vec2(w, h));
102 }
103 }
104
105 pub fn resolve_proxy(
106 self: &std::sync::Arc<Self>,
107 py: Python<'_>,
108 caller: &str,
109 path: &str,
110 ) -> anyhow::Result<Py<NodeProxy>> {
111 let key = (caller.to_string(), path.to_string());
112 {
113 let cache = self.proxy_cache.lock().unwrap();
114 if let Some(proxy) = cache.get(&key) {
115 return Ok(proxy.clone_ref(py));
116 }
117 }
118 let target = self
119 .resolve(caller, path)
120 .ok_or_else(|| anyhow::anyhow!("{caller}: get_node({path:?}) found nothing"))?;
121 let proxy = Py::new(py, proxy_for(self.clone(), target))?;
122 self.proxy_cache
123 .lock()
124 .unwrap()
125 .insert(key, proxy.clone_ref(py));
126 Ok(proxy)
127 }
128
129 pub fn register(&self, node: impl Into<String>, instance: Py<PyAny>) {
130 self.instances.lock().unwrap().insert(node.into(), instance);
131 }
132
133 pub fn unregister(&self, node: &str) {
134 self.instances.lock().unwrap().remove(node);
135 }
136
137 pub fn with_tree<R>(&self, f: impl FnOnce(&NodeTree) -> R) -> R {
138 f(&self.tree.lock().unwrap())
139 }
140
141 pub fn with_tree_mut<R>(&self, f: impl FnOnce(&mut NodeTree) -> R) -> R {
142 f(&mut self.tree.lock().unwrap())
143 }
144
145 pub fn resolve(&self, caller: &str, path: &str) -> Option<String> {
146 let tree = self.tree.lock().unwrap();
147 if let Some(abs) = path.strip_prefix('/') {
148 return tree
149 .get(&NodeId::from(abs.to_string()))
150 .map(|n| n.id.to_string());
151 }
152 let mut cursor = tree.get(&NodeId::from(caller.to_string()))?.clone();
153 for part in path.split('/') {
154 match part {
155 "" | "." => {}
156 ".." => {
157 let parent = cursor.parent.clone()?;
158 cursor = tree.get(&parent)?.clone();
159 }
160 name => {
161 let child = cursor
162 .children
163 .iter()
164 .map(|id| tree.get(id))
165 .find(|n| n.map(|n| n.name == name).unwrap_or(false))?;
166 cursor = child?.clone();
167 }
168 }
169 }
170 Some(cursor.id.to_string())
171 }
172
173 pub fn lookup_instance(&self, py: Python<'_>, node: &str) -> Option<Py<PyAny>> {
174 self.instances
175 .lock()
176 .unwrap()
177 .get(node)
178 .map(|inst| inst.clone_ref(py))
179 }
180
181 pub fn declare_signals(
182 &self,
183 node: &str,
184 defs: Vec<(String, Vec<String>)>,
185 ) -> anyhow::Result<()> {
186 for (name, payload) in &defs {
187 for ty in payload {
188 if !SIGNAL_TYPES.contains(&ty.as_str()) {
189 anyhow::bail!(
190 "{node}: signal {name:?} has unknown payload type {ty:?} (want one of {SIGNAL_TYPES:?})"
191 );
192 }
193 }
194 }
195 let mut signals = self.signals.lock().unwrap();
196 let registry = signals.entry(node.to_string()).or_default();
197 for (name, payload) in defs {
198 registry.register(name, payload);
199 }
200 Ok(())
201 }
202
203 pub fn connect(
204 &self,
205 source: &str,
206 signal: &str,
207 target: &str,
208 handler: &str,
209 ) -> anyhow::Result<()> {
210 let signals = self.signals.lock().unwrap();
211 if signals
212 .get(source)
213 .and_then(|reg| reg.get(signal))
214 .is_none()
215 {
216 anyhow::bail!("{source}: connect to unknown signal {signal:?}");
217 }
218 drop(signals);
219 self.with_tree(|tree| {
220 if tree.get(&NodeId::from(target.to_string())).is_none() {
221 anyhow::bail!("connect target node {target:?} does not exist");
222 }
223 Ok(())
224 })?;
225 self.connections
226 .lock()
227 .unwrap()
228 .entry((source.to_string(), signal.to_string()))
229 .or_default()
230 .push((target.to_string(), handler.to_string()));
231 Ok(())
232 }
233
234 pub fn connect_callable(
235 &self,
236 _py: Python<'_>,
237 source: &str,
238 signal: &str,
239 callable: Py<PyAny>,
240 ) -> anyhow::Result<()> {
241 let signals = self.signals.lock().unwrap();
242 if signals
243 .get(source)
244 .and_then(|reg| reg.get(signal))
245 .is_none()
246 {
247 anyhow::bail!("{source}: connect to unknown signal {signal:?}");
248 }
249 drop(signals);
250 self.callable_connections
251 .lock()
252 .unwrap()
253 .entry((source.to_string(), signal.to_string()))
254 .or_default()
255 .push(callable);
256 Ok(())
257 }
258
259 pub fn emit(
260 &self,
261 py: Python<'_>,
262 source: &str,
263 signal: &str,
264 args: &Bound<'_, PyTuple>,
265 ) -> anyhow::Result<()> {
266 let expected = self
267 .signals
268 .lock()
269 .unwrap()
270 .get(source)
271 .and_then(|reg| reg.get(signal).cloned())
272 .ok_or_else(|| anyhow::anyhow!("{source}: emit of unknown signal {signal:?}"))?;
273 if args.len() != expected.payload.len() {
274 anyhow::bail!(
275 "{source}: signal {signal:?} wants {} values, got {}",
276 expected.payload.len(),
277 args.len()
278 );
279 }
280 let mut values = Vec::with_capacity(args.len());
281 for (i, (want, arg)) in expected.payload.iter().zip(args.iter()).enumerate() {
282 let value = coerce_payload(want, &arg)
283 .with_context(|| format!("{source}: signal {signal:?} arg {i} wants {want}"))?;
284 values.push(value);
285 }
286 let callables: Vec<Py<PyAny>> = self
287 .callable_connections
288 .lock()
289 .unwrap()
290 .get(&(source.to_string(), signal.to_string()))
291 .map(|list| list.iter().map(|c| c.clone_ref(py)).collect())
292 .unwrap_or_default();
293 if !callables.is_empty() {
294 let py_args: Vec<Py<PyAny>> = values.iter().map(|v| v.to_object(py)).collect();
295 let tuple = PyTuple::new(py, py_args).map_err(|e| anyhow::anyhow!("{e}"))?;
296 for c in callables {
297 c.bind(py)
298 .call(tuple.clone(), None)
299 .map_err(|e| anyhow::anyhow!("callable raised: {e}"))?;
300 }
301 }
302 let targets: Vec<(String, String)> = self
303 .connections
304 .lock()
305 .unwrap()
306 .get(&(source.to_string(), signal.to_string()))
307 .cloned()
308 .unwrap_or_default();
309 for (target, handler) in targets {
310 let Some(inst) = self.lookup_instance(py, &target) else {
311 continue;
312 };
313 let callable = inst.bind(py).getattr(handler.as_str()).map_err(|_| {
314 anyhow::anyhow!("{target}: handler {handler:?} missing for signal {signal:?}")
315 })?;
316 let py_args: Vec<Py<PyAny>> = values.iter().map(|v| v.to_object(py)).collect();
317 callable
318 .call(
319 PyTuple::new(py, py_args).map_err(|e| anyhow::anyhow!("{e}"))?,
320 None,
321 )
322 .map_err(|e| anyhow::anyhow!("{target}.{handler} raised: {e}"))?;
323 }
324 Ok(())
325 }
326
327 pub fn has_signal(&self, node: &str, signal: &str) -> bool {
328 self.signals
329 .lock()
330 .unwrap()
331 .get(node)
332 .and_then(|reg| reg.get(signal))
333 .is_some()
334 }
335
336 pub fn disconnect_node(&self, node: &str) {
337 self.signals.lock().unwrap().remove(node);
338 self.connections
339 .lock()
340 .unwrap()
341 .retain(|(src, _), targets| {
342 if src == node {
343 return false;
344 }
345 targets.retain(|(t, _)| t != node);
346 !targets.is_empty()
347 });
348 self.callable_connections
349 .lock()
350 .unwrap()
351 .retain(|(src, _), _| src != node);
352 self.proxy_cache
353 .lock()
354 .unwrap()
355 .retain(|(src, _), _| src != node);
356 }
357
358 pub fn invalidate_proxy_cache(&self, node: &str) {
359 self.proxy_cache
360 .lock()
361 .unwrap()
362 .retain(|(src, _), _| src != node);
363 }
364}
365
366fn coerce_payload(want: &str, arg: &Bound<'_, PyAny>) -> anyhow::Result<PayloadValue> {
367 use pyo3::types::{PyBool, PyInt, PyString};
368 match want {
369 "bool" => {
370 if arg.is_instance_of::<PyBool>() {
371 Ok(PayloadValue::Bool(arg.extract()?))
372 } else {
373 anyhow::bail!("not a bool")
374 }
375 }
376 "int" => {
377 if arg.is_instance_of::<PyBool>() || !arg.is_instance_of::<PyInt>() {
378 anyhow::bail!("not an int");
379 }
380 Ok(PayloadValue::Int(arg.extract()?))
381 }
382 "float" => {
383 if arg.is_instance_of::<PyBool>() {
384 anyhow::bail!("not a float");
385 }
386 if let Ok(f) = arg.extract::<f64>() {
387 Ok(PayloadValue::Float(f))
388 } else {
389 anyhow::bail!("not a float");
390 }
391 }
392 "str" => {
393 if arg.is_instance_of::<PyString>() {
394 Ok(PayloadValue::Str(arg.extract()?))
395 } else {
396 anyhow::bail!("not a str");
397 }
398 }
399 other => anyhow::bail!("unknown payload type {other:?}"),
400 }
401}
402
403impl PayloadValue {
404 fn to_object(&self, py: Python<'_>) -> Py<PyAny> {
405 match self {
406 PayloadValue::Int(i) => i.into_pyobject(py).unwrap().into_any().unbind(),
407 PayloadValue::Float(f) => f.into_pyobject(py).unwrap().into_any().unbind(),
408 PayloadValue::Str(s) => s.into_pyobject(py).unwrap().into_any().unbind(),
409 PayloadValue::Bool(b) => pyo3::types::PyBool::new(py, *b)
410 .to_owned()
411 .unbind()
412 .into_any(),
413 }
414 }
415}
416
417#[pyclass(name = "NodeProxy")]
418pub struct NodeProxy {
419 host: std::sync::Arc<ScriptHost>,
420 target: String,
421}
422
423#[pymethods]
424impl NodeProxy {
425 #[getter]
426 fn position(&self) -> PyResult<crate::vec2::Vec2> {
427 use pite_core::PropValue;
428 self.host.with_tree(|tree| {
429 let node = tree
430 .get(&NodeId::from(self.target.clone()))
431 .ok_or_else(|| {
432 pyo3::exceptions::PyKeyError::new_err(format!(
433 "node {:?} no longer exists",
434 self.target
435 ))
436 })?;
437 match node.props.get("position") {
438 Some(PropValue::Vec2(x, y)) => Ok(crate::vec2::Vec2::new(*x, *y)),
439 _ => Ok(crate::vec2::Vec2::new(0.0, 0.0)),
440 }
441 })
442 }
443
444 #[setter]
445 fn set_position(&self, val: &Bound<'_, PyAny>) -> PyResult<()> {
446 let (x, y) = if let Ok(v) = val.extract::<crate::vec2::Vec2>() {
447 (v.x, v.y)
448 } else if let Ok(t) = val.extract::<(f64, f64)>() {
449 t
450 } else {
451 return Err(pyo3::exceptions::PyTypeError::new_err(
452 "position must be Vec2 or (x, y) tuple",
453 ));
454 };
455 use pite_core::PropValue;
456 self.host.with_tree_mut(|tree| {
457 if let Some(node) = tree.get_mut(&NodeId::from(self.target.clone())) {
458 node.props
459 .insert("position".to_string(), PropValue::Vec2(x, y));
460 }
461 });
462 Ok(())
463 }
464
465 #[getter]
466 fn text(&self) -> PyResult<String> {
467 use pite_core::PropValue;
468 self.host.with_tree(|tree| {
469 let node = tree
470 .get(&NodeId::from(self.target.clone()))
471 .ok_or_else(|| {
472 pyo3::exceptions::PyKeyError::new_err(format!(
473 "node {:?} no longer exists",
474 self.target
475 ))
476 })?;
477 match node.props.get("text") {
478 Some(PropValue::Str(s)) => Ok(s.clone()),
479 _ => Ok(String::new()),
480 }
481 })
482 }
483
484 #[setter]
485 fn set_text(&self, text: String) {
486 use pite_core::PropValue;
487 self.host.with_tree_mut(|tree| {
488 if let Some(node) = tree.get_mut(&NodeId::from(self.target.clone())) {
489 node.props.insert("text".to_string(), PropValue::Str(text));
490 }
491 });
492 }
493
494 #[getter]
495 fn size(&self) -> PyResult<crate::vec2::Vec2> {
496 use pite_core::PropValue;
497 self.host.with_tree(|tree| {
498 let node = tree
499 .get(&NodeId::from(self.target.clone()))
500 .ok_or_else(|| {
501 pyo3::exceptions::PyKeyError::new_err(format!(
502 "node {:?} no longer exists",
503 self.target
504 ))
505 })?;
506 match node.props.get("size") {
507 Some(PropValue::Vec2(w, h)) => Ok(crate::vec2::Vec2::new(*w, *h)),
508 _ => Ok(crate::vec2::Vec2::new(0.0, 0.0)),
509 }
510 })
511 }
512
513 #[setter]
514 fn set_size(&self, val: &Bound<'_, PyAny>) -> PyResult<()> {
515 let (w, h) = if let Ok(v) = val.extract::<crate::vec2::Vec2>() {
516 (v.x, v.y)
517 } else if let Ok(t) = val.extract::<(f64, f64)>() {
518 t
519 } else {
520 return Err(pyo3::exceptions::PyTypeError::new_err(
521 "size must be Vec2 or (w, h) tuple",
522 ));
523 };
524 use pite_core::PropValue;
525 self.host.with_tree_mut(|tree| {
526 if let Some(node) = tree.get_mut(&NodeId::from(self.target.clone())) {
527 node.props.insert("size".to_string(), PropValue::Vec2(w, h));
528 }
529 });
530 Ok(())
531 }
532
533 fn __getattr__(&self, py: Python<'_>, name: String) -> PyResult<Py<PyAny>> {
534 if name == "position" || name == "text" || name == "size" {
535 return Err(pyo3::exceptions::PyAttributeError::new_err(name));
536 }
537 if self.host.has_signal(&self.target, &name) {
538 let bound = BoundSignal {
539 host: self.host.clone(),
540 node: self.target.clone(),
541 signal: name,
542 };
543 return Ok(Bound::new(py, bound)?.into_any().unbind());
544 }
545 let inst = self.host.lookup_instance(py, &self.target).ok_or_else(|| {
546 pyo3::exceptions::PyAttributeError::new_err(format!(
547 "node {:?} has no script with {name:?}",
548 self.target
549 ))
550 })?;
551 inst.bind(py).getattr(name.as_str()).map(|b| b.into())
552 }
553}
554
555#[pyclass(name = "BoundSignal")]
556pub struct BoundSignal {
557 host: std::sync::Arc<ScriptHost>,
558 node: String,
559 signal: String,
560}
561
562impl BoundSignal {
563 pub fn with_host(host: std::sync::Arc<ScriptHost>, node: String, signal: String) -> Self {
564 Self { host, node, signal }
565 }
566}
567
568#[pymethods]
569impl BoundSignal {
570 fn connect(&self, py: Python<'_>, callable: Bound<'_, PyAny>) -> PyResult<()> {
571 if let Ok((target, handler)) = bound_target(&callable) {
572 self.host
573 .connect(&self.node, &self.signal, &target, &handler)
574 .map_err(|e| pyo3::exceptions::PyKeyError::new_err(e.to_string()))
575 } else {
576 self.host
577 .connect_callable(py, &self.node, &self.signal, callable.into_any().unbind())
578 .map_err(|e| pyo3::exceptions::PyKeyError::new_err(e.to_string()))
579 }
580 }
581
582 #[pyo3(signature = (*args))]
583 fn emit(&self, py: Python<'_>, args: &Bound<'_, PyTuple>) -> PyResult<()> {
584 self.host
585 .emit(py, &self.node, &self.signal, args)
586 .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))
587 }
588}
589
590pub fn proxy_for(host: std::sync::Arc<ScriptHost>, target: String) -> NodeProxy {
591 NodeProxy { host, target }
592}
593
594pub fn resolve_caller(caller: &str, path: &str) -> anyhow::Result<NodeProxy> {
595 let host = current_host().ok_or_else(|| anyhow::anyhow!("get_node has no script host"))?;
596 let target = host
597 .resolve(caller, path)
598 .ok_or_else(|| anyhow::anyhow!("{caller}: get_node({path:?}) found nothing"))?;
599 Ok(proxy_for(host, target))
600}
601
602pub fn resolve_caller_py(
603 py: Python<'_>,
604 caller: &str,
605 path: &str,
606) -> anyhow::Result<Py<NodeProxy>> {
607 let host = current_host().ok_or_else(|| anyhow::anyhow!("get_node has no script host"))?;
608 host.resolve_proxy(py, caller, path)
609}
610
611pub fn fire_pressed(host: &ScriptHost, id: &str) -> anyhow::Result<()> {
612 Python::attach(|py| host.emit(py, id, "pressed", &PyTuple::empty(py)))
613}
614
615pub fn fire_timeout(host: &ScriptHost, id: &str) -> anyhow::Result<()> {
616 Python::attach(|py| host.emit(py, id, "timeout", &PyTuple::empty(py)))
617}
618
619pub fn fire_body_entered(host: &ScriptHost, area_id: &str, body_id: &str) -> anyhow::Result<()> {
620 Python::attach(|py| {
621 let arg = PyTuple::new(py, [body_id])?;
622 host.emit(py, area_id, "body_entered", &arg)
623 })
624}
625
626pub fn fire_body_exited(host: &ScriptHost, area_id: &str, body_id: &str) -> anyhow::Result<()> {
627 Python::attach(|py| {
628 let arg = PyTuple::new(py, [body_id])?;
629 host.emit(py, area_id, "body_exited", &arg)
630 })
631}
632
633pub fn fire_area_entered(
634 host: &ScriptHost,
635 area_id: &str,
636 other_area_id: &str,
637) -> anyhow::Result<()> {
638 Python::attach(|py| {
639 let arg = PyTuple::new(py, [other_area_id])?;
640 host.emit(py, area_id, "area_entered", &arg)
641 })
642}
643
644pub fn fire_area_exited(
645 host: &ScriptHost,
646 area_id: &str,
647 other_area_id: &str,
648) -> anyhow::Result<()> {
649 Python::attach(|py| {
650 let arg = PyTuple::new(py, [other_area_id])?;
651 host.emit(py, area_id, "area_exited", &arg)
652 })
653}