1#![cfg(target_arch = "wasm32")]
21
22use std::cell::RefCell;
23use std::collections::HashMap;
24use std::pin::Pin;
25use std::rc::Rc;
26
27use arora_engine::{
28 call::{CallBridge, Callable, CallableId},
29 engine::EngineBuilder,
30 executor::browser::{BrowserExecutor, SharedLoaderRc},
31 load::load_module_from_parts,
32};
33use arora_types::module::low::Header;
34use arora_types::{
35 call::Call,
36 value::{Enumeration, StructureField, StructureWithoutId, Value},
37};
38use uuid::Uuid;
39use wasm_bindgen::prelude::*;
40
41fn install_panic_hook() {
47 console_error_panic_hook::set_once();
48}
49
50use arora::runtime::{Runtime, StepOutcome};
68use arora_behavior::Behavior;
69use arora_bridge::{Bridge, FakeBridge};
70use arora_hal::{FakeHal, Hal};
71use arora_simple_data_store::SimpleDataStore;
72use arora_types::data::{DataStore, Key, StateChange, Subscription};
73use std::sync::Arc;
74
75pub struct BrowserRuntime {
84 runtime: Runtime,
85 store: Arc<dyn DataStore>,
86 changes: Subscription,
87}
88
89impl BrowserRuntime {
90 pub async fn start(
95 hal: Arc<dyn Hal>,
96 bridge: Arc<dyn Bridge>,
97 store: Arc<dyn DataStore>,
98 ) -> Result<BrowserRuntime, JsValue> {
99 install_panic_hook();
100 let arora = arora::Arora::start()
101 .await
102 .map_err(|e| JsValue::from_str(&format!("arora start failed: {e:?}")))?;
103 let changes = store.subscribe();
104 let (runtime, io) = Runtime::with_io_in(arora, hal, bridge, store.clone());
105 wasm_bindgen_futures::spawn_local(io);
106 Ok(BrowserRuntime {
107 runtime,
108 store,
109 changes,
110 })
111 }
112
113 pub fn store(&self) -> &Arc<dyn DataStore> {
115 &self.store
116 }
117
118 pub fn queue_behavior(&mut self, behavior: Box<dyn Behavior>) {
120 self.runtime.queue_behavior(behavior);
121 }
122
123 pub fn queue_groot_xml(&mut self, xml: &str) -> Result<(), JsValue> {
125 self.runtime
126 .queue_groot_xml(xml)
127 .map_err(|e| JsValue::from_str(&format!("queue failed: {e}")))
128 }
129
130 pub fn step(&mut self) -> Result<bool, JsValue> {
133 match self.runtime.step() {
134 Ok(StepOutcome::Live) => Ok(true),
135 Ok(StepOutcome::Unregistered) => Ok(false),
136 Err(e) => Err(JsValue::from_str(&format!("step failed: {e}"))),
137 }
138 }
139
140 pub fn set_value(&self, path: &str, value_json: &str) -> Result<(), JsValue> {
143 let value: Value = serde_json::from_str(value_json)
144 .map_err(|e| JsValue::from_str(&format!("invalid value json for {path}: {e}")))?;
145 self.store
146 .write(StateChange::set(path, value))
147 .map_err(|e| JsValue::from_str(&format!("write {path} failed: {e}")))
148 }
149
150 pub fn write_values(&self, values_json: &str) -> Result<(), JsValue> {
154 let map: serde_json::Map<String, serde_json::Value> = serde_json::from_str(values_json)
155 .map_err(|e| JsValue::from_str(&format!("invalid values json: {e}")))?;
156 let mut change = StateChange::new();
157 for (path, raw) in map {
158 let value: Value = serde_json::from_value(raw)
159 .map_err(|e| JsValue::from_str(&format!("invalid value for {path}: {e}")))?;
160 change.set.insert(Key::new(path), Some(value));
161 }
162 self.store
163 .write(change)
164 .map_err(|e| JsValue::from_str(&format!("write failed: {e}")))
165 }
166
167 pub fn read_values(&self, paths: JsValue) -> Result<JsValue, JsValue> {
170 let paths: Vec<String> = serde_wasm_bindgen::from_value(paths)
171 .map_err(|e| JsValue::from_str(&format!("paths must be a string[]: {e}")))?;
172 let keys: Vec<Key> = paths.iter().map(Key::new).collect();
173 let values = self.store.read(&keys);
174 let mut out = serde_json::Map::with_capacity(paths.len());
175 for (path, value) in paths.into_iter().zip(values) {
176 out.insert(path, value_to_json(value)?);
177 }
178 serde_wasm_bindgen::to_value(&serde_json::Value::Object(out))
179 .map_err(|e| JsValue::from_str(&format!("serialize failed: {e}")))
180 }
181
182 pub fn snapshot(&self) -> Result<JsValue, JsValue> {
185 let state = self.store.snapshot();
186 let mut out = serde_json::Map::with_capacity(state.storage.len());
187 for (key, value) in state.storage {
188 out.insert(key.path, value_to_json(value)?);
189 }
190 serde_wasm_bindgen::to_value(&serde_json::Value::Object(out))
191 .map_err(|e| JsValue::from_str(&format!("serialize failed: {e}")))
192 }
193
194 pub fn drain_changes(&self) -> Result<JsValue, JsValue> {
200 let mut out = serde_json::Map::new();
201 while let Some(change) = self.changes.try_recv() {
202 for (key, value) in change.set {
203 out.insert(key.path, value_to_json(value)?);
204 }
205 for key in change.unset {
206 out.insert(key.path, serde_json::Value::Null);
207 }
208 }
209 serde_wasm_bindgen::to_value(&serde_json::Value::Object(out))
210 .map_err(|e| JsValue::from_str(&format!("serialize failed: {e}")))
211 }
212}
213
214fn value_to_json(value: Option<Value>) -> Result<serde_json::Value, JsValue> {
216 match value {
217 Some(v) => serde_json::to_value(v)
218 .map_err(|e| JsValue::from_str(&format!("serialize value failed: {e}"))),
219 None => Ok(serde_json::Value::Null),
220 }
221}
222
223#[wasm_bindgen]
234pub struct AroraRuntime {
235 inner: BrowserRuntime,
236}
237
238#[wasm_bindgen]
239impl AroraRuntime {
240 pub async fn start() -> Result<AroraRuntime, JsValue> {
244 let inner = BrowserRuntime::start(
245 Arc::new(FakeHal::new()),
246 Arc::new(FakeBridge::new()),
247 Arc::new(SimpleDataStore::new()),
248 )
249 .await?;
250 Ok(AroraRuntime { inner })
251 }
252
253 pub fn step(&mut self) -> Result<bool, JsValue> {
256 self.inner.step()
257 }
258
259 #[wasm_bindgen(js_name = queueGrootXml)]
261 pub fn queue_groot_xml(&mut self, xml: &str) -> Result<(), JsValue> {
262 self.inner.queue_groot_xml(xml)
263 }
264
265 #[wasm_bindgen(js_name = setValue)]
267 pub fn set_value(&self, path: &str, value_json: &str) -> Result<(), JsValue> {
268 self.inner.set_value(path, value_json)
269 }
270
271 #[wasm_bindgen(js_name = readValues)]
273 pub fn read_values(&self, paths: JsValue) -> Result<JsValue, JsValue> {
274 self.inner.read_values(paths)
275 }
276
277 pub fn snapshot(&self) -> Result<JsValue, JsValue> {
279 self.inner.snapshot()
280 }
281}
282
283#[wasm_bindgen]
285pub struct Engine {
286 inner: std::pin::Pin<Box<arora_engine::engine::Engine>>,
287 loader: SharedLoaderRc,
288 function_module: HashMap<Uuid, Uuid>,
289 module_headers: HashMap<Uuid, String>,
290}
291
292#[wasm_bindgen]
293impl Engine {
294 #[wasm_bindgen(constructor)]
295 pub fn new() -> Engine {
296 install_panic_hook();
297 let executor = BrowserExecutor::new();
298 let loader = executor.shared();
299 let inner = EngineBuilder::new().add_executor(executor).build();
300 Engine {
301 inner,
302 loader,
303 function_module: HashMap::new(),
304 module_headers: HashMap::new(),
305 }
306 }
307
308 #[wasm_bindgen(js_name = loadModule)]
315 pub fn load_module(&mut self, header_json: &str, executable: &[u8]) -> Result<String, JsValue> {
316 self.load_module_inner(header_json, executable.to_vec().into_boxed_slice())
317 }
318
319 #[wasm_bindgen(js_name = prepareModule)]
323 pub fn prepare_module(&self, header_json: &str, executable: Vec<u8>) -> js_sys::Promise {
324 prepare_module_impl(self.loader.clone(), header_json, executable)
325 }
326
327 #[wasm_bindgen(js_name = loadPreparedModule)]
330 pub fn load_prepared_module(&mut self, header_json: &str) -> Result<String, JsValue> {
331 self.load_module_inner(header_json, Box::new([]))
332 }
333
334 fn load_module_inner(
335 &mut self,
336 header_json: &str,
337 executable: Box<[u8]>,
338 ) -> Result<String, JsValue> {
339 let header: Header = serde_json::from_str(header_json)
340 .map_err(|e| JsValue::from_str(&format!("invalid header json: {e}")))?;
341 let header_json_str = header_json.to_string();
342 let loaded = load_module_from_parts(&mut *self.inner, header, executable)
343 .map_err(|e| JsValue::from_str(&format!("load_module failed: {e}")))?;
344 for fn_id in &loaded.function_ids {
345 self.function_module.insert(*fn_id, loaded.id);
346 }
347 self.module_headers.insert(loaded.id, header_json_str);
348 Ok(loaded.id.to_string())
349 }
350
351 #[wasm_bindgen(js_name = listModules)]
353 pub fn list_modules(&self) -> String {
354 let headers: Vec<serde_json::Value> = self
355 .module_headers
356 .values()
357 .filter_map(|s| serde_json::from_str(s).ok())
358 .collect();
359 serde_json::to_string(&headers).unwrap_or_else(|_| "[]".to_string())
360 }
361
362 #[wasm_bindgen]
365 pub fn call(&mut self, call_json: &str) -> Result<String, JsValue> {
366 let call: Call = serde_json::from_str(call_json)
367 .map_err(|e| JsValue::from_str(&format!("invalid call json: {e}")))?;
368 let module_id = if let Some(m) = call.module_id {
369 m
370 } else {
371 *self
372 .function_module
373 .get(&call.id)
374 .ok_or_else(|| JsValue::from_str("no module known for function"))?
375 };
376 let result = self
377 .inner
378 .arora_call(&module_id, call)
379 .map_err(|e| JsValue::from_str(&format!("call failed: {e}")))?;
380 serde_json::to_string(&result)
381 .map_err(|e| JsValue::from_str(&format!("serialize failed: {e}")))
382 }
383}
384
385impl Default for Engine {
386 fn default() -> Self {
387 Self::new()
388 }
389}
390
391fn prepare_module_impl(
395 loader: SharedLoaderRc,
396 header_json: &str,
397 executable: Vec<u8>,
398) -> js_sys::Promise {
399 let header: Result<Header, _> = serde_json::from_str(header_json);
400 wasm_bindgen_futures::future_to_promise(async move {
401 let header = header.map_err(|e| JsValue::from_str(&format!("invalid header json: {e}")))?;
402 loader.prepare(header.id, executable).await?;
403 Ok(JsValue::UNDEFINED)
404 })
405}
406
407const TICK_ID_STRUCT_BYTES: [u8; 16] = [
417 0x6f, 0x49, 0xe6, 0x50, 0x84, 0xca, 0x48, 0x99, 0xa9, 0xbd, 0x1f, 0x3b, 0xf1, 0x7f, 0xab, 0x51,
418];
419const TICK_ID_CALLABLE_FIELD_BYTES: [u8; 16] = [
421 0x23, 0x79, 0x92, 0xd2, 0x17, 0xd1, 0x45, 0x9f, 0xbc, 0xa1, 0x71, 0x85, 0xfa, 0x6a, 0x69, 0xd7,
422];
423const STATUS_SUCCESS_BYTES: [u8; 16] = [
425 0x76, 0x6e, 0x9e, 0x9a, 0x44, 0x6d, 0x4e, 0x46, 0x83, 0xe6, 0x14, 0xb7, 0xca, 0x10, 0x11, 0x69,
426];
427const STATUS_FAILURE_BYTES: [u8; 16] = [
429 0x24, 0x68, 0xf4, 0x6c, 0xbb, 0x60, 0x42, 0x5c, 0x9a, 0x4d, 0x9a, 0xd3, 0x26, 0xcc, 0xc7, 0xe2,
430];
431const STATUS_ENUM_BYTES: [u8; 16] = [
433 0x32, 0x5a, 0x57, 0x67, 0xe3, 0x44, 0x45, 0x32, 0x86, 0x0e, 0x07, 0x49, 0xbc, 0xf2, 0xe4, 0x28,
434];
435const RET_PARAM_BYTES: [u8; 16] = [
437 0x5f, 0x72, 0x65, 0x74, 0x00, 0x00, 0x40, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
438];
439
440fn value_to_status(v: &Value) -> &'static str {
441 if let Value::Enumeration(e) = v {
442 if *e.variant_id.as_bytes() == STATUS_SUCCESS_BYTES {
443 return "success";
444 }
445 if *e.variant_id.as_bytes() == STATUS_FAILURE_BYTES {
446 return "failure";
447 }
448 }
449 "running"
450}
451
452#[derive(serde::Deserialize, Clone, Debug)]
455#[serde(rename_all = "snake_case")]
456enum BtExpression {
457 VariableId(Uuid),
458 Value(Value),
459}
460
461#[derive(serde::Deserialize, Debug)]
463struct BtNode {
464 id: Uuid,
465 function: Uuid,
466 #[serde(default)]
467 children: Option<Vec<Uuid>>,
468 #[serde(default)]
472 arguments: HashMap<Uuid, BtExpression>,
473}
474
475struct FnMeta {
477 module_id: Uuid,
478 children_param_id: Option<Uuid>,
481}
482
483struct NodeCallable {
485 node_id: Uuid,
486 fn_id: Uuid,
487 module_id: Uuid,
488 children_param_id: Option<Uuid>,
489 children_callable_ids: Vec<u64>,
490 arguments: HashMap<Uuid, BtExpression>,
491 variables: Rc<RefCell<HashMap<Uuid, Value>>>,
492 trace: Rc<RefCell<Vec<(Uuid, &'static str)>>>,
493}
494
495impl Callable for NodeCallable {
496 fn call(&self, caller: &mut dyn CallBridge) -> Result<Value, arora_engine::call::CallError> {
497 let tick_id_type = Uuid::from_bytes(TICK_ID_STRUCT_BYTES);
498 let callable_field = Uuid::from_bytes(TICK_ID_CALLABLE_FIELD_BYTES);
499 let ret_param_id = Uuid::from_bytes(RET_PARAM_BYTES);
500
501 let mut args = Vec::new();
502
503 if let Some(children_param_id) = self.children_param_id {
504 let elements: Vec<StructureWithoutId> = self
505 .children_callable_ids
506 .iter()
507 .map(|&id| StructureWithoutId {
508 fields: vec![StructureField {
509 id: callable_field,
510 value: Box::new(Value::U64(id)),
511 }],
512 })
513 .collect();
514 args.push(StructureField {
515 id: children_param_id,
516 value: Box::new(Value::ArrayStructure {
517 id: tick_id_type,
518 elements,
519 }),
520 });
521 }
522
523 for (¶m_id, expr) in &self.arguments {
524 if param_id == ret_param_id {
525 continue;
526 }
527 let value = match expr {
528 BtExpression::Value(v) => v.clone(),
529 BtExpression::VariableId(var_id) => self
530 .variables
531 .borrow()
532 .get(var_id)
533 .cloned()
534 .unwrap_or(Value::Unit),
535 };
536 args.push(StructureField {
537 id: param_id,
538 value: Box::new(value),
539 });
540 }
541
542 let mutable_param_vars: HashMap<Uuid, Uuid> = self
545 .arguments
546 .iter()
547 .filter_map(|(¶m_id, expr)| {
548 if param_id == ret_param_id {
549 return None;
550 }
551 if let BtExpression::VariableId(var_id) = expr {
552 Some((param_id, *var_id))
553 } else {
554 None
555 }
556 })
557 .collect();
558
559 let result = caller.arora_call(
560 &self.module_id,
561 Call {
562 module_id: None,
563 id: self.fn_id,
564 args,
565 },
566 )?;
567
568 for mutated in &result.mutated {
570 if let Some(&var_id) = mutable_param_vars.get(&mutated.id) {
571 self.variables
572 .borrow_mut()
573 .insert(var_id, *mutated.value.clone());
574 }
575 }
576
577 let has_ret = self.arguments.contains_key(&ret_param_id);
578 if has_ret {
579 if let Some(BtExpression::VariableId(var_id)) = self.arguments.get(&ret_param_id) {
580 self.variables
581 .borrow_mut()
582 .insert(*var_id, result.ret.clone());
583 }
584 }
585
586 let s = if has_ret {
587 "success"
588 } else {
589 value_to_status(&result.ret)
590 };
591 self.trace.borrow_mut().push((self.node_id, s));
592
593 if has_ret {
594 Ok(Value::Enumeration(Enumeration {
595 id: Uuid::from_bytes(STATUS_ENUM_BYTES),
596 variant_id: Uuid::from_bytes(STATUS_SUCCESS_BYTES),
597 value: Box::new(Value::Unit),
598 }))
599 } else {
600 Ok(result.ret)
601 }
602 }
603}
604
605fn register_node(
608 engine: &mut dyn CallBridge,
609 node_id: Uuid,
610 node_index: &HashMap<Uuid, BtNode>,
611 fn_meta: &HashMap<Uuid, FnMeta>,
612 trace: &Rc<RefCell<Vec<(Uuid, &'static str)>>>,
613 variables: &Rc<RefCell<HashMap<Uuid, Value>>>,
614) -> Result<u64, String> {
615 let node = node_index
616 .get(&node_id)
617 .ok_or_else(|| format!("node {node_id} not found in tree"))?;
618 let meta = fn_meta
619 .get(&node.function)
620 .ok_or_else(|| format!("function {} not registered in fn_meta", node.function))?;
621
622 let children_callable_ids = match &node.children {
623 None => vec![],
624 Some(ids) => ids
625 .iter()
626 .map(|&child_id| register_node(engine, child_id, node_index, fn_meta, trace, variables))
627 .collect::<Result<Vec<_>, _>>()?,
628 };
629
630 let callable: Rc<dyn Callable> = Rc::new(NodeCallable {
631 node_id,
632 fn_id: node.function,
633 module_id: meta.module_id,
634 children_param_id: meta.children_param_id,
635 children_callable_ids,
636 arguments: node.arguments.clone(),
637 variables: variables.clone(),
638 trace: trace.clone(),
639 });
640 let id = engine.arora_register_callable(callable);
641 Ok(id.id)
642}
643
644#[wasm_bindgen]
653pub struct BehaviorTreeRunner {
654 inner: Pin<Box<arora_engine::engine::Engine>>,
655 loader: SharedLoaderRc,
656 fn_meta: HashMap<Uuid, FnMeta>,
657 variables: Rc<RefCell<HashMap<Uuid, Value>>>,
658 module_headers: HashMap<Uuid, String>,
659}
660
661#[wasm_bindgen]
662impl BehaviorTreeRunner {
663 #[wasm_bindgen(constructor)]
664 pub fn new() -> BehaviorTreeRunner {
665 install_panic_hook();
666 let executor = BrowserExecutor::new();
667 let loader = executor.shared();
668 let inner = EngineBuilder::new().add_executor(executor).build();
669 BehaviorTreeRunner {
670 inner,
671 loader,
672 fn_meta: HashMap::new(),
673 variables: Rc::new(RefCell::new(HashMap::new())),
674 module_headers: HashMap::new(),
675 }
676 }
677
678 #[wasm_bindgen(js_name = loadModule)]
686 pub fn load_module(&mut self, header_json: &str, executable: &[u8]) -> Result<String, JsValue> {
687 self.load_module_inner(header_json, executable.to_vec().into_boxed_slice())
688 }
689
690 #[wasm_bindgen(js_name = prepareModule)]
694 pub fn prepare_module(&self, header_json: &str, executable: Vec<u8>) -> js_sys::Promise {
695 prepare_module_impl(self.loader.clone(), header_json, executable)
696 }
697
698 #[wasm_bindgen(js_name = loadPreparedModule)]
701 pub fn load_prepared_module(&mut self, header_json: &str) -> Result<String, JsValue> {
702 self.load_module_inner(header_json, Box::new([]))
703 }
704
705 fn load_module_inner(
706 &mut self,
707 header_json: &str,
708 executable: Box<[u8]>,
709 ) -> Result<String, JsValue> {
710 let header: Header = serde_json::from_str(header_json)
711 .map_err(|e| JsValue::from_str(&format!("invalid header: {e}")))?;
712 let module_id = header.id;
713 let header_json_str = header_json.to_string();
714
715 for export in &header.exports {
716 let arora_types::module::low::ExportSymbol::Function(f) = export;
717 let children_param_id = f.parameters.first().and_then(|p| {
718 if let arora_types::module::low::TypeRef::Array { id } = &p.ty {
719 if id == &Uuid::from_bytes(TICK_ID_STRUCT_BYTES) {
720 Some(p.id)
721 } else {
722 None
723 }
724 } else {
725 None
726 }
727 });
728
729 self.fn_meta.insert(
730 f.id,
731 FnMeta {
732 module_id,
733 children_param_id,
734 },
735 );
736 }
737
738 let result = load_module_from_parts(&mut *self.inner, header, executable)
739 .map_err(|e| JsValue::from_str(&format!("load failed: {e}")))?;
740 self.module_headers.insert(result.id, header_json_str);
741 Ok(result.id.to_string())
742 }
743
744 #[wasm_bindgen(js_name = listModules)]
746 pub fn list_modules(&self) -> String {
747 let headers: Vec<serde_json::Value> = self
748 .module_headers
749 .values()
750 .filter_map(|s| serde_json::from_str(s).ok())
751 .collect();
752 serde_json::to_string(&headers).unwrap_or_else(|_| "[]".to_string())
753 }
754
755 #[wasm_bindgen(js_name = setVariable)]
758 pub fn set_variable(&mut self, var_id: &str, value_json: &str) -> Result<(), JsValue> {
759 let var_id: Uuid = var_id
760 .parse()
761 .map_err(|_| JsValue::from_str("bad var_id: not a valid UUID"))?;
762 let value: Value = serde_json::from_str(value_json)
763 .map_err(|e| JsValue::from_str(&format!("bad value JSON: {e}")))?;
764 self.variables.borrow_mut().insert(var_id, value);
765 Ok(())
766 }
767
768 pub fn tick(&mut self, nodes_json: &str) -> Result<String, JsValue> {
775 let nodes: Vec<BtNode> = serde_json::from_str(nodes_json)
776 .map_err(|e| JsValue::from_str(&format!("bad nodes JSON: {e}")))?;
777 if nodes.is_empty() {
778 return Err(JsValue::from_str("tree has no nodes"));
779 }
780
781 let root_id = nodes[0].id;
782 let node_index: HashMap<Uuid, BtNode> = nodes.into_iter().map(|n| (n.id, n)).collect();
783 let trace: Rc<RefCell<Vec<(Uuid, &'static str)>>> = Rc::new(RefCell::new(Vec::new()));
784
785 let fn_meta = &self.fn_meta;
786 let root_callable_id = register_node(
787 &mut *self.inner,
788 root_id,
789 &node_index,
790 fn_meta,
791 &trace,
792 &self.variables,
793 )
794 .map_err(|e| JsValue::from_str(&format!("setup error: {e}")))?;
795
796 let callable_id = CallableId {
797 id: root_callable_id,
798 };
799 let result = Callable::call(&callable_id, &mut *self.inner)
800 .map_err(|e| JsValue::from_str(&format!("tick error: {e}")))?;
801 let status = value_to_status(&result);
802
803 let trace_json: Vec<serde_json::Value> = trace
804 .borrow()
805 .iter()
806 .map(|(id, s)| serde_json::json!({ "nodeId": id.to_string(), "status": s }))
807 .collect();
808
809 let vars_json: serde_json::Map<String, serde_json::Value> = self
810 .variables
811 .borrow()
812 .iter()
813 .filter_map(|(id, v)| serde_json::to_value(v).ok().map(|jv| (id.to_string(), jv)))
814 .collect();
815
816 Ok(serde_json::json!({
817 "status": status,
818 "trace": trace_json,
819 "variables": vars_json,
820 })
821 .to_string())
822 }
823
824 pub fn run(&mut self, nodes_json: &str) -> Result<String, JsValue> {
834 let nodes: Vec<BtNode> = serde_json::from_str(nodes_json)
835 .map_err(|e| JsValue::from_str(&format!("bad nodes JSON: {e}")))?;
836 if nodes.is_empty() {
837 return Err(JsValue::from_str("tree has no nodes"));
838 }
839
840 let root_id = nodes[0].id;
841 let node_index: HashMap<Uuid, BtNode> = nodes.into_iter().map(|n| (n.id, n)).collect();
842 let trace: Rc<RefCell<Vec<(Uuid, &'static str)>>> = Rc::new(RefCell::new(Vec::new()));
843
844 let fn_meta = &self.fn_meta;
845 let root_callable_id = register_node(
846 &mut *self.inner,
847 root_id,
848 &node_index,
849 fn_meta,
850 &trace,
851 &self.variables,
852 )
853 .map_err(|e| JsValue::from_str(&format!("setup error: {e}")))?;
854
855 let callable_id = CallableId {
856 id: root_callable_id,
857 };
858
859 let mut last_status = "running";
860 for _ in 0..10_000 {
861 let result = Callable::call(&callable_id, &mut *self.inner)
862 .map_err(|e| JsValue::from_str(&format!("tick error: {e}")))?;
863 last_status = value_to_status(&result);
864 if last_status != "running" {
865 break;
866 }
867 }
868
869 let trace_json: Vec<serde_json::Value> = trace
870 .borrow()
871 .iter()
872 .map(|(id, s)| serde_json::json!({ "nodeId": id.to_string(), "status": s }))
873 .collect();
874
875 let out = serde_json::json!({
876 "status": last_status,
877 "trace": trace_json,
878 });
879 Ok(out.to_string())
880 }
881}
882
883impl Default for BehaviorTreeRunner {
884 fn default() -> Self {
885 Self::new()
886 }
887}
888
889#[wasm_bindgen]
898pub struct Registry {
899 entries: HashMap<Uuid, (String, Option<Uuid>)>,
900}
901
902#[derive(serde::Deserialize)]
903struct RecordEntry {
904 id: Uuid,
905 name: String,
906 parent: Option<Uuid>,
907}
908
909#[wasm_bindgen]
910impl Registry {
911 #[wasm_bindgen(constructor)]
912 pub fn new() -> Registry {
913 Registry {
914 entries: HashMap::new(),
915 }
916 }
917
918 #[wasm_bindgen(js_name = loadRecordsJson)]
920 pub fn load_records_json(&mut self, json: &str) -> Result<(), JsValue> {
921 let records: Vec<RecordEntry> = serde_json::from_str(json)
922 .map_err(|e| JsValue::from_str(&format!("invalid records JSON: {e}")))?;
923 for r in records {
924 self.entries.insert(r.id, (r.name, r.parent));
925 }
926 Ok(())
927 }
928
929 #[wasm_bindgen(js_name = resolveId)]
932 pub fn resolve_id(&self, id: &str) -> Option<String> {
933 let uuid: Uuid = id.parse().ok()?;
934 self.compute_path(&uuid)
935 }
936
937 fn compute_path(&self, id: &Uuid) -> Option<String> {
938 let (name, parent) = self.entries.get(id)?;
939 match parent {
940 None => Some(name.clone()),
941 Some(parent_id) if !self.entries.contains_key(parent_id) => Some(name.clone()),
942 Some(parent_id) => Some(format!("{}.{}", self.compute_path(parent_id)?, name)),
943 }
944 }
945}
946
947impl Default for Registry {
948 fn default() -> Self {
949 Self::new()
950 }
951}