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};
69use arora_behavior::BehaviorInterpreter;
70use arora_bridge::{Bridge, FakeBridge};
71use arora_hal::{FakeHal, Hal};
72use arora_simple_data_store::SimpleDataStore;
73use arora_types::data::{DataStore, Key, StateChange, Subscription};
74use std::sync::Arc;
75
76pub struct BrowserRuntime {
85 runtime: Runtime,
86 store: Arc<dyn DataStore>,
87 changes: Subscription,
88}
89
90impl BrowserRuntime {
91 pub async fn start(
97 hal: Arc<dyn Hal>,
98 bridge: Arc<dyn Bridge>,
99 store: Arc<dyn DataStore>,
100 ) -> Result<BrowserRuntime, JsValue> {
101 install_panic_hook();
102 let arora = arora::Arora::start()
103 .await
104 .map_err(|e| JsValue::from_str(&format!("arora start failed: {e:?}")))?;
105 let changes = store.subscribe();
106 let runtime = Runtime::with_io_in(arora, hal, bridge, store.clone());
107 Ok(BrowserRuntime {
108 runtime,
109 store,
110 changes,
111 })
112 }
113
114 pub fn store(&self) -> &Arc<dyn DataStore> {
116 &self.store
117 }
118
119 pub fn queue_behavior(&mut self, behavior: Box<dyn BehaviorInterpreter>) {
121 self.runtime.queue_behavior(behavior);
122 }
123
124 pub fn queue_groot_xml(&mut self, xml: &str) -> Result<(), JsValue> {
126 self.runtime
127 .queue_groot_xml(xml)
128 .map_err(|e| JsValue::from_str(&format!("queue failed: {e}")))
129 }
130
131 pub fn step(&mut self, dt_ns: u64) -> Result<bool, JsValue> {
138 match self.runtime.step(dt_ns) {
139 Ok(StepOutcome::Live) => Ok(true),
140 Ok(StepOutcome::Unregistered) => Ok(false),
141 Err(e) => Err(JsValue::from_str(&format!("step failed: {e}"))),
142 }
143 }
144
145 pub fn set_value(&self, path: &str, value_json: &str) -> Result<(), JsValue> {
148 let value: Value = serde_json::from_str(value_json)
149 .map_err(|e| JsValue::from_str(&format!("invalid value json for {path}: {e}")))?;
150 self.store
151 .write(StateChange::set(path, value))
152 .map_err(|e| JsValue::from_str(&format!("write {path} failed: {e}")))
153 }
154
155 pub fn write_values(&self, values_json: &str) -> Result<(), JsValue> {
159 let map: serde_json::Map<String, serde_json::Value> = serde_json::from_str(values_json)
160 .map_err(|e| JsValue::from_str(&format!("invalid values json: {e}")))?;
161 let mut change = StateChange::new();
162 for (path, raw) in map {
163 let value: Value = serde_json::from_value(raw)
164 .map_err(|e| JsValue::from_str(&format!("invalid value for {path}: {e}")))?;
165 change.set.insert(Key::new(path), Some(value));
166 }
167 self.store
168 .write(change)
169 .map_err(|e| JsValue::from_str(&format!("write failed: {e}")))
170 }
171
172 pub fn read_values(&self, paths: JsValue) -> Result<JsValue, JsValue> {
175 let paths: Vec<String> = serde_wasm_bindgen::from_value(paths)
176 .map_err(|e| JsValue::from_str(&format!("paths must be a string[]: {e}")))?;
177 let keys: Vec<Key> = paths.iter().map(Key::new).collect();
178 let values = self.store.read(&keys);
179 let mut out = serde_json::Map::with_capacity(paths.len());
180 for (path, value) in paths.into_iter().zip(values) {
181 out.insert(path, value_to_json(value)?);
182 }
183 serde_wasm_bindgen::to_value(&serde_json::Value::Object(out))
184 .map_err(|e| JsValue::from_str(&format!("serialize failed: {e}")))
185 }
186
187 pub fn snapshot(&self) -> Result<JsValue, JsValue> {
190 let state = self.store.snapshot();
191 let mut out = serde_json::Map::with_capacity(state.storage.len());
192 for (key, value) in state.storage {
193 out.insert(key.path, value_to_json(value)?);
194 }
195 serde_wasm_bindgen::to_value(&serde_json::Value::Object(out))
196 .map_err(|e| JsValue::from_str(&format!("serialize failed: {e}")))
197 }
198
199 pub fn drain_changes(&self) -> Result<JsValue, JsValue> {
205 let mut out = serde_json::Map::new();
206 while let Some(change) = self.changes.try_recv() {
207 for (key, value) in change.set {
208 out.insert(key.path, value_to_json(value)?);
209 }
210 for key in change.unset {
211 out.insert(key.path, serde_json::Value::Null);
212 }
213 }
214 serde_wasm_bindgen::to_value(&serde_json::Value::Object(out))
215 .map_err(|e| JsValue::from_str(&format!("serialize failed: {e}")))
216 }
217}
218
219fn value_to_json(value: Option<Value>) -> Result<serde_json::Value, JsValue> {
221 match value {
222 Some(v) => serde_json::to_value(v)
223 .map_err(|e| JsValue::from_str(&format!("serialize value failed: {e}"))),
224 None => Ok(serde_json::Value::Null),
225 }
226}
227
228#[wasm_bindgen]
239pub struct AroraRuntime {
240 inner: BrowserRuntime,
241}
242
243#[wasm_bindgen]
244impl AroraRuntime {
245 pub async fn start() -> Result<AroraRuntime, JsValue> {
249 let inner = BrowserRuntime::start(
250 Arc::new(FakeHal::new()),
251 Arc::new(FakeBridge::new()),
252 Arc::new(SimpleDataStore::new()),
253 )
254 .await?;
255 Ok(AroraRuntime { inner })
256 }
257
258 pub fn step(&mut self, dt_ms: f64) -> Result<bool, JsValue> {
264 self.inner.step((dt_ms * 1_000_000.0) as u64)
265 }
266
267 #[wasm_bindgen(js_name = queueGrootXml)]
269 pub fn queue_groot_xml(&mut self, xml: &str) -> Result<(), JsValue> {
270 self.inner.queue_groot_xml(xml)
271 }
272
273 #[wasm_bindgen(js_name = setValue)]
275 pub fn set_value(&self, path: &str, value_json: &str) -> Result<(), JsValue> {
276 self.inner.set_value(path, value_json)
277 }
278
279 #[wasm_bindgen(js_name = readValues)]
281 pub fn read_values(&self, paths: JsValue) -> Result<JsValue, JsValue> {
282 self.inner.read_values(paths)
283 }
284
285 pub fn snapshot(&self) -> Result<JsValue, JsValue> {
287 self.inner.snapshot()
288 }
289}
290
291#[wasm_bindgen]
293pub struct Engine {
294 inner: std::pin::Pin<Box<arora_engine::engine::Engine>>,
295 loader: SharedLoaderRc,
296 function_module: HashMap<Uuid, Uuid>,
297 module_headers: HashMap<Uuid, String>,
298}
299
300#[wasm_bindgen]
301impl Engine {
302 #[wasm_bindgen(constructor)]
303 pub fn new() -> Engine {
304 install_panic_hook();
305 let executor = BrowserExecutor::new();
306 let loader = executor.shared();
307 let inner = EngineBuilder::new().add_executor(executor).build();
308 Engine {
309 inner,
310 loader,
311 function_module: HashMap::new(),
312 module_headers: HashMap::new(),
313 }
314 }
315
316 #[wasm_bindgen(js_name = loadModule)]
323 pub fn load_module(&mut self, header_json: &str, executable: &[u8]) -> Result<String, JsValue> {
324 self.load_module_inner(header_json, executable.to_vec().into_boxed_slice())
325 }
326
327 #[wasm_bindgen(js_name = prepareModule)]
331 pub fn prepare_module(&self, header_json: &str, executable: Vec<u8>) -> js_sys::Promise {
332 prepare_module_impl(self.loader.clone(), header_json, executable)
333 }
334
335 #[wasm_bindgen(js_name = loadPreparedModule)]
338 pub fn load_prepared_module(&mut self, header_json: &str) -> Result<String, JsValue> {
339 self.load_module_inner(header_json, Box::new([]))
340 }
341
342 fn load_module_inner(
343 &mut self,
344 header_json: &str,
345 executable: Box<[u8]>,
346 ) -> Result<String, JsValue> {
347 let header: Header = serde_json::from_str(header_json)
348 .map_err(|e| JsValue::from_str(&format!("invalid header json: {e}")))?;
349 let header_json_str = header_json.to_string();
350 let loaded = load_module_from_parts(&mut *self.inner, header, executable)
351 .map_err(|e| JsValue::from_str(&format!("load_module failed: {e}")))?;
352 for fn_id in &loaded.function_ids {
353 self.function_module.insert(*fn_id, loaded.id);
354 }
355 self.module_headers.insert(loaded.id, header_json_str);
356 Ok(loaded.id.to_string())
357 }
358
359 #[wasm_bindgen(js_name = listModules)]
361 pub fn list_modules(&self) -> String {
362 let headers: Vec<serde_json::Value> = self
363 .module_headers
364 .values()
365 .filter_map(|s| serde_json::from_str(s).ok())
366 .collect();
367 serde_json::to_string(&headers).unwrap_or_else(|_| "[]".to_string())
368 }
369
370 #[wasm_bindgen]
373 pub fn call(&mut self, call_json: &str) -> Result<String, JsValue> {
374 let call: Call = serde_json::from_str(call_json)
375 .map_err(|e| JsValue::from_str(&format!("invalid call json: {e}")))?;
376 let module_id = if let Some(m) = call.module_id {
377 m
378 } else {
379 *self
380 .function_module
381 .get(&call.id)
382 .ok_or_else(|| JsValue::from_str("no module known for function"))?
383 };
384 let result = self
385 .inner
386 .arora_call(&module_id, call)
387 .map_err(|e| JsValue::from_str(&format!("call failed: {e}")))?;
388 serde_json::to_string(&result)
389 .map_err(|e| JsValue::from_str(&format!("serialize failed: {e}")))
390 }
391}
392
393impl Default for Engine {
394 fn default() -> Self {
395 Self::new()
396 }
397}
398
399fn prepare_module_impl(
403 loader: SharedLoaderRc,
404 header_json: &str,
405 executable: Vec<u8>,
406) -> js_sys::Promise {
407 let header: Result<Header, _> = serde_json::from_str(header_json);
408 wasm_bindgen_futures::future_to_promise(async move {
409 let header = header.map_err(|e| JsValue::from_str(&format!("invalid header json: {e}")))?;
410 loader.prepare(header.id, executable).await?;
411 Ok(JsValue::UNDEFINED)
412 })
413}
414
415const TICK_ID_STRUCT_BYTES: [u8; 16] = [
425 0x6f, 0x49, 0xe6, 0x50, 0x84, 0xca, 0x48, 0x99, 0xa9, 0xbd, 0x1f, 0x3b, 0xf1, 0x7f, 0xab, 0x51,
426];
427const TICK_ID_CALLABLE_FIELD_BYTES: [u8; 16] = [
429 0x23, 0x79, 0x92, 0xd2, 0x17, 0xd1, 0x45, 0x9f, 0xbc, 0xa1, 0x71, 0x85, 0xfa, 0x6a, 0x69, 0xd7,
430];
431const STATUS_SUCCESS_BYTES: [u8; 16] = [
433 0x76, 0x6e, 0x9e, 0x9a, 0x44, 0x6d, 0x4e, 0x46, 0x83, 0xe6, 0x14, 0xb7, 0xca, 0x10, 0x11, 0x69,
434];
435const STATUS_FAILURE_BYTES: [u8; 16] = [
437 0x24, 0x68, 0xf4, 0x6c, 0xbb, 0x60, 0x42, 0x5c, 0x9a, 0x4d, 0x9a, 0xd3, 0x26, 0xcc, 0xc7, 0xe2,
438];
439const STATUS_ENUM_BYTES: [u8; 16] = [
441 0x32, 0x5a, 0x57, 0x67, 0xe3, 0x44, 0x45, 0x32, 0x86, 0x0e, 0x07, 0x49, 0xbc, 0xf2, 0xe4, 0x28,
442];
443const RET_PARAM_BYTES: [u8; 16] = [
445 0x5f, 0x72, 0x65, 0x74, 0x00, 0x00, 0x40, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
446];
447
448fn value_to_status(v: &Value) -> &'static str {
449 if let Value::Enumeration(e) = v {
450 if *e.variant_id.as_bytes() == STATUS_SUCCESS_BYTES {
451 return "success";
452 }
453 if *e.variant_id.as_bytes() == STATUS_FAILURE_BYTES {
454 return "failure";
455 }
456 }
457 "running"
458}
459
460#[derive(serde::Deserialize, Clone, Debug)]
463#[serde(rename_all = "snake_case")]
464enum BtExpression {
465 VariableId(Uuid),
466 Value(Value),
467}
468
469#[derive(serde::Deserialize, Debug)]
471struct BtNode {
472 id: Uuid,
473 function: Uuid,
474 #[serde(default)]
475 children: Option<Vec<Uuid>>,
476 #[serde(default)]
480 arguments: HashMap<Uuid, BtExpression>,
481}
482
483struct FnMeta {
485 module_id: Uuid,
486 children_param_id: Option<Uuid>,
489}
490
491struct NodeCallable {
493 node_id: Uuid,
494 fn_id: Uuid,
495 module_id: Uuid,
496 children_param_id: Option<Uuid>,
497 children_callable_ids: Vec<u64>,
498 arguments: HashMap<Uuid, BtExpression>,
499 variables: Rc<RefCell<HashMap<Uuid, Value>>>,
500 trace: Rc<RefCell<Vec<(Uuid, &'static str)>>>,
501}
502
503impl Callable for NodeCallable {
504 fn call(&self, caller: &mut dyn CallBridge) -> Result<Value, arora_engine::call::CallError> {
505 let tick_id_type = Uuid::from_bytes(TICK_ID_STRUCT_BYTES);
506 let callable_field = Uuid::from_bytes(TICK_ID_CALLABLE_FIELD_BYTES);
507 let ret_param_id = Uuid::from_bytes(RET_PARAM_BYTES);
508
509 let mut args = Vec::new();
510
511 if let Some(children_param_id) = self.children_param_id {
512 let elements: Vec<StructureWithoutId> = self
513 .children_callable_ids
514 .iter()
515 .map(|&id| StructureWithoutId {
516 fields: vec![StructureField {
517 id: callable_field,
518 value: Box::new(Value::U64(id)),
519 }],
520 })
521 .collect();
522 args.push(StructureField {
523 id: children_param_id,
524 value: Box::new(Value::ArrayStructure {
525 id: tick_id_type,
526 elements,
527 }),
528 });
529 }
530
531 for (¶m_id, expr) in &self.arguments {
532 if param_id == ret_param_id {
533 continue;
534 }
535 let value = match expr {
536 BtExpression::Value(v) => v.clone(),
537 BtExpression::VariableId(var_id) => self
538 .variables
539 .borrow()
540 .get(var_id)
541 .cloned()
542 .unwrap_or(Value::Unit),
543 };
544 args.push(StructureField {
545 id: param_id,
546 value: Box::new(value),
547 });
548 }
549
550 let mutable_param_vars: HashMap<Uuid, Uuid> = self
553 .arguments
554 .iter()
555 .filter_map(|(¶m_id, expr)| {
556 if param_id == ret_param_id {
557 return None;
558 }
559 if let BtExpression::VariableId(var_id) = expr {
560 Some((param_id, *var_id))
561 } else {
562 None
563 }
564 })
565 .collect();
566
567 let result = caller.arora_call(
568 &self.module_id,
569 Call {
570 module_id: None,
571 id: self.fn_id,
572 args,
573 },
574 )?;
575
576 for mutated in &result.mutated {
578 if let Some(&var_id) = mutable_param_vars.get(&mutated.id) {
579 self.variables
580 .borrow_mut()
581 .insert(var_id, *mutated.value.clone());
582 }
583 }
584
585 let has_ret = self.arguments.contains_key(&ret_param_id);
586 if has_ret {
587 if let Some(BtExpression::VariableId(var_id)) = self.arguments.get(&ret_param_id) {
588 self.variables
589 .borrow_mut()
590 .insert(*var_id, result.ret.clone());
591 }
592 }
593
594 let s = if has_ret {
595 "success"
596 } else {
597 value_to_status(&result.ret)
598 };
599 self.trace.borrow_mut().push((self.node_id, s));
600
601 if has_ret {
602 Ok(Value::Enumeration(Enumeration {
603 id: Uuid::from_bytes(STATUS_ENUM_BYTES),
604 variant_id: Uuid::from_bytes(STATUS_SUCCESS_BYTES),
605 value: Box::new(Value::Unit),
606 }))
607 } else {
608 Ok(result.ret)
609 }
610 }
611}
612
613fn register_node(
616 engine: &mut dyn CallBridge,
617 node_id: Uuid,
618 node_index: &HashMap<Uuid, BtNode>,
619 fn_meta: &HashMap<Uuid, FnMeta>,
620 trace: &Rc<RefCell<Vec<(Uuid, &'static str)>>>,
621 variables: &Rc<RefCell<HashMap<Uuid, Value>>>,
622) -> Result<u64, String> {
623 let node = node_index
624 .get(&node_id)
625 .ok_or_else(|| format!("node {node_id} not found in tree"))?;
626 let meta = fn_meta
627 .get(&node.function)
628 .ok_or_else(|| format!("function {} not registered in fn_meta", node.function))?;
629
630 let children_callable_ids = match &node.children {
631 None => vec![],
632 Some(ids) => ids
633 .iter()
634 .map(|&child_id| register_node(engine, child_id, node_index, fn_meta, trace, variables))
635 .collect::<Result<Vec<_>, _>>()?,
636 };
637
638 let callable: Rc<dyn Callable> = Rc::new(NodeCallable {
639 node_id,
640 fn_id: node.function,
641 module_id: meta.module_id,
642 children_param_id: meta.children_param_id,
643 children_callable_ids,
644 arguments: node.arguments.clone(),
645 variables: variables.clone(),
646 trace: trace.clone(),
647 });
648 let id = engine.arora_register_callable(callable);
649 Ok(id.id)
650}
651
652#[wasm_bindgen]
661pub struct BehaviorTreeRunner {
662 inner: Pin<Box<arora_engine::engine::Engine>>,
663 loader: SharedLoaderRc,
664 fn_meta: HashMap<Uuid, FnMeta>,
665 variables: Rc<RefCell<HashMap<Uuid, Value>>>,
666 module_headers: HashMap<Uuid, String>,
667}
668
669#[wasm_bindgen]
670impl BehaviorTreeRunner {
671 #[wasm_bindgen(constructor)]
672 pub fn new() -> BehaviorTreeRunner {
673 install_panic_hook();
674 let executor = BrowserExecutor::new();
675 let loader = executor.shared();
676 let inner = EngineBuilder::new().add_executor(executor).build();
677 BehaviorTreeRunner {
678 inner,
679 loader,
680 fn_meta: HashMap::new(),
681 variables: Rc::new(RefCell::new(HashMap::new())),
682 module_headers: HashMap::new(),
683 }
684 }
685
686 #[wasm_bindgen(js_name = loadModule)]
694 pub fn load_module(&mut self, header_json: &str, executable: &[u8]) -> Result<String, JsValue> {
695 self.load_module_inner(header_json, executable.to_vec().into_boxed_slice())
696 }
697
698 #[wasm_bindgen(js_name = prepareModule)]
702 pub fn prepare_module(&self, header_json: &str, executable: Vec<u8>) -> js_sys::Promise {
703 prepare_module_impl(self.loader.clone(), header_json, executable)
704 }
705
706 #[wasm_bindgen(js_name = loadPreparedModule)]
709 pub fn load_prepared_module(&mut self, header_json: &str) -> Result<String, JsValue> {
710 self.load_module_inner(header_json, Box::new([]))
711 }
712
713 fn load_module_inner(
714 &mut self,
715 header_json: &str,
716 executable: Box<[u8]>,
717 ) -> Result<String, JsValue> {
718 let header: Header = serde_json::from_str(header_json)
719 .map_err(|e| JsValue::from_str(&format!("invalid header: {e}")))?;
720 let module_id = header.id;
721 let header_json_str = header_json.to_string();
722
723 for export in &header.exports {
724 let arora_types::module::low::ExportSymbol::Function(f) = export;
725 let children_param_id = f.parameters.first().and_then(|p| {
726 if let arora_types::module::low::TypeRef::Array { id } = &p.ty {
727 if id == &Uuid::from_bytes(TICK_ID_STRUCT_BYTES) {
728 Some(p.id)
729 } else {
730 None
731 }
732 } else {
733 None
734 }
735 });
736
737 self.fn_meta.insert(
738 f.id,
739 FnMeta {
740 module_id,
741 children_param_id,
742 },
743 );
744 }
745
746 let result = load_module_from_parts(&mut *self.inner, header, executable)
747 .map_err(|e| JsValue::from_str(&format!("load failed: {e}")))?;
748 self.module_headers.insert(result.id, header_json_str);
749 Ok(result.id.to_string())
750 }
751
752 #[wasm_bindgen(js_name = listModules)]
754 pub fn list_modules(&self) -> String {
755 let headers: Vec<serde_json::Value> = self
756 .module_headers
757 .values()
758 .filter_map(|s| serde_json::from_str(s).ok())
759 .collect();
760 serde_json::to_string(&headers).unwrap_or_else(|_| "[]".to_string())
761 }
762
763 #[wasm_bindgen(js_name = setVariable)]
766 pub fn set_variable(&mut self, var_id: &str, value_json: &str) -> Result<(), JsValue> {
767 let var_id: Uuid = var_id
768 .parse()
769 .map_err(|_| JsValue::from_str("bad var_id: not a valid UUID"))?;
770 let value: Value = serde_json::from_str(value_json)
771 .map_err(|e| JsValue::from_str(&format!("bad value JSON: {e}")))?;
772 self.variables.borrow_mut().insert(var_id, value);
773 Ok(())
774 }
775
776 pub fn tick(&mut self, nodes_json: &str) -> Result<String, JsValue> {
783 let nodes: Vec<BtNode> = serde_json::from_str(nodes_json)
784 .map_err(|e| JsValue::from_str(&format!("bad nodes JSON: {e}")))?;
785 if nodes.is_empty() {
786 return Err(JsValue::from_str("tree has no nodes"));
787 }
788
789 let root_id = nodes[0].id;
790 let node_index: HashMap<Uuid, BtNode> = nodes.into_iter().map(|n| (n.id, n)).collect();
791 let trace: Rc<RefCell<Vec<(Uuid, &'static str)>>> = Rc::new(RefCell::new(Vec::new()));
792
793 let fn_meta = &self.fn_meta;
794 let root_callable_id = register_node(
795 &mut *self.inner,
796 root_id,
797 &node_index,
798 fn_meta,
799 &trace,
800 &self.variables,
801 )
802 .map_err(|e| JsValue::from_str(&format!("setup error: {e}")))?;
803
804 let callable_id = CallableId {
805 id: root_callable_id,
806 };
807 let result = Callable::call(&callable_id, &mut *self.inner)
808 .map_err(|e| JsValue::from_str(&format!("tick error: {e}")))?;
809 let status = value_to_status(&result);
810
811 let trace_json: Vec<serde_json::Value> = trace
812 .borrow()
813 .iter()
814 .map(|(id, s)| serde_json::json!({ "nodeId": id.to_string(), "status": s }))
815 .collect();
816
817 let vars_json: serde_json::Map<String, serde_json::Value> = self
818 .variables
819 .borrow()
820 .iter()
821 .filter_map(|(id, v)| serde_json::to_value(v).ok().map(|jv| (id.to_string(), jv)))
822 .collect();
823
824 Ok(serde_json::json!({
825 "status": status,
826 "trace": trace_json,
827 "variables": vars_json,
828 })
829 .to_string())
830 }
831
832 pub fn run(&mut self, nodes_json: &str) -> Result<String, JsValue> {
842 let nodes: Vec<BtNode> = serde_json::from_str(nodes_json)
843 .map_err(|e| JsValue::from_str(&format!("bad nodes JSON: {e}")))?;
844 if nodes.is_empty() {
845 return Err(JsValue::from_str("tree has no nodes"));
846 }
847
848 let root_id = nodes[0].id;
849 let node_index: HashMap<Uuid, BtNode> = nodes.into_iter().map(|n| (n.id, n)).collect();
850 let trace: Rc<RefCell<Vec<(Uuid, &'static str)>>> = Rc::new(RefCell::new(Vec::new()));
851
852 let fn_meta = &self.fn_meta;
853 let root_callable_id = register_node(
854 &mut *self.inner,
855 root_id,
856 &node_index,
857 fn_meta,
858 &trace,
859 &self.variables,
860 )
861 .map_err(|e| JsValue::from_str(&format!("setup error: {e}")))?;
862
863 let callable_id = CallableId {
864 id: root_callable_id,
865 };
866
867 let mut last_status = "running";
868 for _ in 0..10_000 {
869 let result = Callable::call(&callable_id, &mut *self.inner)
870 .map_err(|e| JsValue::from_str(&format!("tick error: {e}")))?;
871 last_status = value_to_status(&result);
872 if last_status != "running" {
873 break;
874 }
875 }
876
877 let trace_json: Vec<serde_json::Value> = trace
878 .borrow()
879 .iter()
880 .map(|(id, s)| serde_json::json!({ "nodeId": id.to_string(), "status": s }))
881 .collect();
882
883 let out = serde_json::json!({
884 "status": last_status,
885 "trace": trace_json,
886 });
887 Ok(out.to_string())
888 }
889}
890
891impl Default for BehaviorTreeRunner {
892 fn default() -> Self {
893 Self::new()
894 }
895}
896
897#[wasm_bindgen]
906pub struct Registry {
907 entries: HashMap<Uuid, (String, Option<Uuid>)>,
908}
909
910#[derive(serde::Deserialize)]
911struct RecordEntry {
912 id: Uuid,
913 name: String,
914 parent: Option<Uuid>,
915}
916
917#[wasm_bindgen]
918impl Registry {
919 #[wasm_bindgen(constructor)]
920 pub fn new() -> Registry {
921 Registry {
922 entries: HashMap::new(),
923 }
924 }
925
926 #[wasm_bindgen(js_name = loadRecordsJson)]
928 pub fn load_records_json(&mut self, json: &str) -> Result<(), JsValue> {
929 let records: Vec<RecordEntry> = serde_json::from_str(json)
930 .map_err(|e| JsValue::from_str(&format!("invalid records JSON: {e}")))?;
931 for r in records {
932 self.entries.insert(r.id, (r.name, r.parent));
933 }
934 Ok(())
935 }
936
937 #[wasm_bindgen(js_name = resolveId)]
940 pub fn resolve_id(&self, id: &str) -> Option<String> {
941 let uuid: Uuid = id.parse().ok()?;
942 self.compute_path(&uuid)
943 }
944
945 fn compute_path(&self, id: &Uuid) -> Option<String> {
946 let (name, parent) = self.entries.get(id)?;
947 match parent {
948 None => Some(name.clone()),
949 Some(parent_id) if !self.entries.contains_key(parent_id) => Some(name.clone()),
950 Some(parent_id) => Some(format!("{}.{}", self.compute_path(parent_id)?, name)),
951 }
952 }
953}
954
955impl Default for Registry {
956 fn default() -> Self {
957 Self::new()
958 }
959}