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::{Arora, AroraBuilder, BehaviorTreeInterpreter, ModuleFunction, StepOutcome};
70use arora_behavior::BehaviorInterpreter;
71use arora_bridge::{Bridge, FakeBridge};
72use arora_hal::{FakeHal, Hal};
73use arora_simple_data_store::SimpleDataStore;
74use arora_types::data::{DataStore, Key, StateChange, Subscription};
75use std::time::Duration;
76
77pub struct BrowserRuntime {
86 arora: Arora,
87 changes: Subscription,
88}
89
90impl BrowserRuntime {
91 pub async fn start(
105 hal: Box<dyn Hal>,
106 bridge: Box<dyn Bridge>,
107 store: Box<dyn DataStore>,
108 behavior_interpreter: Box<dyn BehaviorInterpreter>,
109 ) -> Result<BrowserRuntime, JsValue> {
110 Self::builder()
111 .with_hal(hal)
112 .with_bridge(bridge)
113 .with_data_store(store)
114 .with_behavior_interpreter(behavior_interpreter)
115 .build()
116 }
117
118 pub fn builder() -> BrowserRuntimeBuilder {
125 BrowserRuntimeBuilder::default()
126 }
127
128 pub fn store(&self) -> &dyn DataStore {
130 self.arora.store()
131 }
132
133 pub fn step(&mut self, dt: Duration) -> Result<bool, JsValue> {
140 match self.arora.step(dt) {
141 Ok(StepOutcome::Live) => Ok(true),
142 Ok(StepOutcome::Unregistered) => Ok(false),
143 Err(e) => Err(JsValue::from_str(&format!("step failed: {e}"))),
144 }
145 }
146
147 pub fn set_value(&self, path: &str, value_json: &str) -> Result<(), JsValue> {
150 let value: Value = serde_json::from_str(value_json)
151 .map_err(|e| JsValue::from_str(&format!("invalid value json for {path}: {e}")))?;
152 self.store()
153 .write(StateChange::set(path, value))
154 .map_err(|e| JsValue::from_str(&format!("write {path} failed: {e}")))
155 }
156
157 pub fn write_values(&self, values_json: &str) -> Result<(), JsValue> {
161 let map: serde_json::Map<String, serde_json::Value> = serde_json::from_str(values_json)
162 .map_err(|e| JsValue::from_str(&format!("invalid values json: {e}")))?;
163 let mut change = StateChange::new();
164 for (path, raw) in map {
165 let value: Value = serde_json::from_value(raw)
166 .map_err(|e| JsValue::from_str(&format!("invalid value for {path}: {e}")))?;
167 change.set.insert(Key::new(path), Some(value));
168 }
169 self.store()
170 .write(change)
171 .map_err(|e| JsValue::from_str(&format!("write failed: {e}")))
172 }
173
174 pub fn read_values(&self, paths: JsValue) -> Result<JsValue, JsValue> {
177 let paths: Vec<String> = serde_wasm_bindgen::from_value(paths)
178 .map_err(|e| JsValue::from_str(&format!("paths must be a string[]: {e}")))?;
179 let keys: Vec<Key> = paths.iter().map(Key::new).collect();
180 let values = self.store().read(&keys);
181 let mut out = serde_json::Map::with_capacity(paths.len());
182 for (path, value) in paths.into_iter().zip(values) {
183 out.insert(path, value_to_json(value)?);
184 }
185 to_js_object(out)
186 }
187
188 pub fn snapshot(&self) -> Result<JsValue, JsValue> {
191 let state = self.store().snapshot();
192 let mut out = serde_json::Map::with_capacity(state.storage.len());
193 for (key, value) in state.storage {
194 out.insert(key.path, value_to_json(value)?);
195 }
196 to_js_object(out)
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 to_js_object(out)
215 }
216}
217
218#[derive(Default)]
225pub struct BrowserRuntimeBuilder {
226 inner: AroraBuilder,
227}
228
229impl BrowserRuntimeBuilder {
230 pub fn with_hal(mut self, hal: Box<dyn Hal>) -> Self {
232 self.inner = self.inner.with_hal(hal);
233 self
234 }
235
236 pub fn with_bridge(mut self, bridge: Box<dyn Bridge>) -> Self {
239 self.inner = self.inner.with_bridge(bridge);
240 self
241 }
242
243 pub fn with_data_store(mut self, store: Box<dyn DataStore>) -> Self {
245 self.inner = self.inner.with_data_store(store);
246 self
247 }
248
249 pub fn with_behavior_interpreter(mut self, interpreter: Box<dyn BehaviorInterpreter>) -> Self {
251 self.inner = self.inner.with_behavior_interpreter(interpreter);
252 self
253 }
254
255 pub fn with_module(mut self, header: Header, executable: impl Into<Box<[u8]>>) -> Self {
258 self.inner = self.inner.with_module(header, executable);
259 self
260 }
261
262 pub fn with_host_module(mut self, functions: impl IntoIterator<Item = ModuleFunction>) -> Self {
264 self.inner = self.inner.with_host_module(functions);
265 self
266 }
267
268 pub fn build(self) -> Result<BrowserRuntime, JsValue> {
272 install_panic_hook();
273 let arora = self
274 .inner
275 .build()
276 .map_err(|e| JsValue::from_str(&format!("arora build failed: {e:?}")))?;
277 let changes = arora.store().subscribe();
278 Ok(BrowserRuntime { arora, changes })
279 }
280}
281
282fn to_js_object(out: serde_json::Map<String, serde_json::Value>) -> Result<JsValue, JsValue> {
286 use serde::Serialize;
287 serde_json::Value::Object(out)
288 .serialize(&serde_wasm_bindgen::Serializer::json_compatible())
289 .map_err(|e| JsValue::from_str(&format!("serialize failed: {e}")))
290}
291
292fn value_to_json(value: Option<Value>) -> Result<serde_json::Value, JsValue> {
294 match value {
295 Some(v) => serde_json::to_value(v)
296 .map_err(|e| JsValue::from_str(&format!("serialize value failed: {e}"))),
297 None => Ok(serde_json::Value::Null),
298 }
299}
300
301#[wasm_bindgen]
312pub struct AroraRuntime {
313 inner: BrowserRuntime,
314}
315
316#[wasm_bindgen]
317impl AroraRuntime {
318 pub async fn start() -> Result<AroraRuntime, JsValue> {
322 let interpreter = BehaviorTreeInterpreter::new(Rc::new(HashMap::new()));
326 let inner = BrowserRuntime::start(
327 Box::new(FakeHal::new()),
328 Box::new(FakeBridge::new()),
329 Box::new(SimpleDataStore::new()),
330 Box::new(interpreter),
331 )
332 .await?;
333 Ok(AroraRuntime { inner })
334 }
335
336 pub fn step(&mut self, dt_ms: f64) -> Result<bool, JsValue> {
342 self.inner.step(Duration::from_secs_f64(dt_ms / 1_000.0))
343 }
344
345 #[wasm_bindgen(js_name = setValue)]
347 pub fn set_value(&self, path: &str, value_json: &str) -> Result<(), JsValue> {
348 self.inner.set_value(path, value_json)
349 }
350
351 #[wasm_bindgen(js_name = readValues)]
353 pub fn read_values(&self, paths: JsValue) -> Result<JsValue, JsValue> {
354 self.inner.read_values(paths)
355 }
356
357 pub fn snapshot(&self) -> Result<JsValue, JsValue> {
359 self.inner.snapshot()
360 }
361}
362
363#[wasm_bindgen]
365pub struct Engine {
366 inner: std::pin::Pin<Box<arora_engine::engine::Engine>>,
367 loader: SharedLoaderRc,
368 function_module: HashMap<Uuid, Uuid>,
369 module_headers: HashMap<Uuid, String>,
370}
371
372#[wasm_bindgen]
373impl Engine {
374 #[wasm_bindgen(constructor)]
375 pub fn new() -> Engine {
376 install_panic_hook();
377 let executor = BrowserExecutor::new();
378 let loader = executor.shared();
379 let inner = EngineBuilder::new().add_executor(executor).build();
380 Engine {
381 inner,
382 loader,
383 function_module: HashMap::new(),
384 module_headers: HashMap::new(),
385 }
386 }
387
388 #[wasm_bindgen(js_name = loadModule)]
395 pub fn load_module(&mut self, header_json: &str, executable: &[u8]) -> Result<String, JsValue> {
396 self.load_module_inner(header_json, executable.to_vec().into_boxed_slice())
397 }
398
399 #[wasm_bindgen(js_name = prepareModule)]
403 pub fn prepare_module(&self, header_json: &str, executable: Vec<u8>) -> js_sys::Promise {
404 prepare_module_impl(self.loader.clone(), header_json, executable)
405 }
406
407 #[wasm_bindgen(js_name = loadPreparedModule)]
410 pub fn load_prepared_module(&mut self, header_json: &str) -> Result<String, JsValue> {
411 self.load_module_inner(header_json, Box::new([]))
412 }
413
414 fn load_module_inner(
415 &mut self,
416 header_json: &str,
417 executable: Box<[u8]>,
418 ) -> Result<String, JsValue> {
419 let header: Header = serde_json::from_str(header_json)
420 .map_err(|e| JsValue::from_str(&format!("invalid header json: {e}")))?;
421 let header_json_str = header_json.to_string();
422 let loaded = load_module_from_parts(&mut *self.inner, header, executable)
423 .map_err(|e| JsValue::from_str(&format!("load_module failed: {e}")))?;
424 for fn_id in &loaded.function_ids {
425 self.function_module.insert(*fn_id, loaded.id);
426 }
427 self.module_headers.insert(loaded.id, header_json_str);
428 Ok(loaded.id.to_string())
429 }
430
431 #[wasm_bindgen(js_name = listModules)]
433 pub fn list_modules(&self) -> String {
434 let headers: Vec<serde_json::Value> = self
435 .module_headers
436 .values()
437 .filter_map(|s| serde_json::from_str(s).ok())
438 .collect();
439 serde_json::to_string(&headers).unwrap_or_else(|_| "[]".to_string())
440 }
441
442 #[wasm_bindgen]
445 pub fn call(&mut self, call_json: &str) -> Result<String, JsValue> {
446 let call: Call = serde_json::from_str(call_json)
447 .map_err(|e| JsValue::from_str(&format!("invalid call json: {e}")))?;
448 let module_id = if let Some(m) = call.module_id {
449 m
450 } else {
451 *self
452 .function_module
453 .get(&call.id)
454 .ok_or_else(|| JsValue::from_str("no module known for function"))?
455 };
456 let result = self
457 .inner
458 .arora_call(&module_id, call)
459 .map_err(|e| JsValue::from_str(&format!("call failed: {e}")))?;
460 serde_json::to_string(&result)
461 .map_err(|e| JsValue::from_str(&format!("serialize failed: {e}")))
462 }
463}
464
465impl Default for Engine {
466 fn default() -> Self {
467 Self::new()
468 }
469}
470
471fn prepare_module_impl(
475 loader: SharedLoaderRc,
476 header_json: &str,
477 executable: Vec<u8>,
478) -> js_sys::Promise {
479 let header: Result<Header, _> = serde_json::from_str(header_json);
480 wasm_bindgen_futures::future_to_promise(async move {
481 let header = header.map_err(|e| JsValue::from_str(&format!("invalid header json: {e}")))?;
482 loader.prepare(header.id, executable).await?;
483 Ok(JsValue::UNDEFINED)
484 })
485}
486
487const TICK_ID_STRUCT_BYTES: [u8; 16] = [
497 0x6f, 0x49, 0xe6, 0x50, 0x84, 0xca, 0x48, 0x99, 0xa9, 0xbd, 0x1f, 0x3b, 0xf1, 0x7f, 0xab, 0x51,
498];
499const TICK_ID_CALLABLE_FIELD_BYTES: [u8; 16] = [
501 0x23, 0x79, 0x92, 0xd2, 0x17, 0xd1, 0x45, 0x9f, 0xbc, 0xa1, 0x71, 0x85, 0xfa, 0x6a, 0x69, 0xd7,
502];
503const STATUS_SUCCESS_BYTES: [u8; 16] = [
505 0x76, 0x6e, 0x9e, 0x9a, 0x44, 0x6d, 0x4e, 0x46, 0x83, 0xe6, 0x14, 0xb7, 0xca, 0x10, 0x11, 0x69,
506];
507const STATUS_FAILURE_BYTES: [u8; 16] = [
509 0x24, 0x68, 0xf4, 0x6c, 0xbb, 0x60, 0x42, 0x5c, 0x9a, 0x4d, 0x9a, 0xd3, 0x26, 0xcc, 0xc7, 0xe2,
510];
511const STATUS_ENUM_BYTES: [u8; 16] = [
513 0x32, 0x5a, 0x57, 0x67, 0xe3, 0x44, 0x45, 0x32, 0x86, 0x0e, 0x07, 0x49, 0xbc, 0xf2, 0xe4, 0x28,
514];
515const RET_PARAM_BYTES: [u8; 16] = [
517 0x5f, 0x72, 0x65, 0x74, 0x00, 0x00, 0x40, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
518];
519
520fn value_to_status(v: &Value) -> &'static str {
521 if let Value::Enumeration(e) = v {
522 if *e.variant_id.as_bytes() == STATUS_SUCCESS_BYTES {
523 return "success";
524 }
525 if *e.variant_id.as_bytes() == STATUS_FAILURE_BYTES {
526 return "failure";
527 }
528 }
529 "running"
530}
531
532#[derive(serde::Deserialize, Clone, Debug)]
535#[serde(rename_all = "snake_case")]
536enum BtExpression {
537 VariableId(Uuid),
538 Value(Value),
539}
540
541#[derive(serde::Deserialize, Debug)]
543struct BtNode {
544 id: Uuid,
545 function: Uuid,
546 #[serde(default)]
547 children: Option<Vec<Uuid>>,
548 #[serde(default)]
552 arguments: HashMap<Uuid, BtExpression>,
553}
554
555struct FnMeta {
557 module_id: Uuid,
558 children_param_id: Option<Uuid>,
561}
562
563struct NodeCallable {
565 node_id: Uuid,
566 fn_id: Uuid,
567 module_id: Uuid,
568 children_param_id: Option<Uuid>,
569 children_callable_ids: Vec<u64>,
570 arguments: HashMap<Uuid, BtExpression>,
571 variables: Rc<RefCell<HashMap<Uuid, Value>>>,
572 trace: Rc<RefCell<Vec<(Uuid, &'static str)>>>,
573}
574
575impl Callable for NodeCallable {
576 fn call(&self, caller: &mut dyn CallBridge) -> Result<Value, arora_engine::call::CallError> {
577 let tick_id_type = Uuid::from_bytes(TICK_ID_STRUCT_BYTES);
578 let callable_field = Uuid::from_bytes(TICK_ID_CALLABLE_FIELD_BYTES);
579 let ret_param_id = Uuid::from_bytes(RET_PARAM_BYTES);
580
581 let mut args = Vec::new();
582
583 if let Some(children_param_id) = self.children_param_id {
584 let elements: Vec<StructureWithoutId> = self
585 .children_callable_ids
586 .iter()
587 .map(|&id| StructureWithoutId {
588 fields: vec![StructureField {
589 id: callable_field,
590 value: Box::new(Value::U64(id)),
591 }],
592 })
593 .collect();
594 args.push(StructureField {
595 id: children_param_id,
596 value: Box::new(Value::ArrayStructure {
597 id: tick_id_type,
598 elements,
599 }),
600 });
601 }
602
603 for (¶m_id, expr) in &self.arguments {
604 if param_id == ret_param_id {
605 continue;
606 }
607 let value = match expr {
608 BtExpression::Value(v) => v.clone(),
609 BtExpression::VariableId(var_id) => self
610 .variables
611 .borrow()
612 .get(var_id)
613 .cloned()
614 .unwrap_or(Value::Unit),
615 };
616 args.push(StructureField {
617 id: param_id,
618 value: Box::new(value),
619 });
620 }
621
622 let mutable_param_vars: HashMap<Uuid, Uuid> = self
625 .arguments
626 .iter()
627 .filter_map(|(¶m_id, expr)| {
628 if param_id == ret_param_id {
629 return None;
630 }
631 if let BtExpression::VariableId(var_id) = expr {
632 Some((param_id, *var_id))
633 } else {
634 None
635 }
636 })
637 .collect();
638
639 let result = caller.arora_call(
640 &self.module_id,
641 Call {
642 module_id: None,
643 id: self.fn_id,
644 args,
645 },
646 )?;
647
648 for mutated in &result.mutated {
650 if let Some(&var_id) = mutable_param_vars.get(&mutated.id) {
651 self.variables
652 .borrow_mut()
653 .insert(var_id, *mutated.value.clone());
654 }
655 }
656
657 let has_ret = self.arguments.contains_key(&ret_param_id);
658 if has_ret {
659 if let Some(BtExpression::VariableId(var_id)) = self.arguments.get(&ret_param_id) {
660 self.variables
661 .borrow_mut()
662 .insert(*var_id, result.ret.clone());
663 }
664 }
665
666 let s = if has_ret {
667 "success"
668 } else {
669 value_to_status(&result.ret)
670 };
671 self.trace.borrow_mut().push((self.node_id, s));
672
673 if has_ret {
674 Ok(Value::Enumeration(Enumeration {
675 id: Uuid::from_bytes(STATUS_ENUM_BYTES),
676 variant_id: Uuid::from_bytes(STATUS_SUCCESS_BYTES),
677 value: Box::new(Value::Unit),
678 }))
679 } else {
680 Ok(result.ret)
681 }
682 }
683}
684
685fn register_node(
688 engine: &mut dyn CallBridge,
689 node_id: Uuid,
690 node_index: &HashMap<Uuid, BtNode>,
691 fn_meta: &HashMap<Uuid, FnMeta>,
692 trace: &Rc<RefCell<Vec<(Uuid, &'static str)>>>,
693 variables: &Rc<RefCell<HashMap<Uuid, Value>>>,
694) -> Result<u64, String> {
695 let node = node_index
696 .get(&node_id)
697 .ok_or_else(|| format!("node {node_id} not found in tree"))?;
698 let meta = fn_meta
699 .get(&node.function)
700 .ok_or_else(|| format!("function {} not registered in fn_meta", node.function))?;
701
702 let children_callable_ids = match &node.children {
703 None => vec![],
704 Some(ids) => ids
705 .iter()
706 .map(|&child_id| register_node(engine, child_id, node_index, fn_meta, trace, variables))
707 .collect::<Result<Vec<_>, _>>()?,
708 };
709
710 let callable: Rc<dyn Callable> = Rc::new(NodeCallable {
711 node_id,
712 fn_id: node.function,
713 module_id: meta.module_id,
714 children_param_id: meta.children_param_id,
715 children_callable_ids,
716 arguments: node.arguments.clone(),
717 variables: variables.clone(),
718 trace: trace.clone(),
719 });
720 let id = engine.arora_register_callable(callable);
721 Ok(id.id)
722}
723
724#[wasm_bindgen]
733pub struct BehaviorTreeRunner {
734 inner: Pin<Box<arora_engine::engine::Engine>>,
735 loader: SharedLoaderRc,
736 fn_meta: HashMap<Uuid, FnMeta>,
737 variables: Rc<RefCell<HashMap<Uuid, Value>>>,
738 module_headers: HashMap<Uuid, String>,
739}
740
741#[wasm_bindgen]
742impl BehaviorTreeRunner {
743 #[wasm_bindgen(constructor)]
744 pub fn new() -> BehaviorTreeRunner {
745 install_panic_hook();
746 let executor = BrowserExecutor::new();
747 let loader = executor.shared();
748 let inner = EngineBuilder::new().add_executor(executor).build();
749 BehaviorTreeRunner {
750 inner,
751 loader,
752 fn_meta: HashMap::new(),
753 variables: Rc::new(RefCell::new(HashMap::new())),
754 module_headers: HashMap::new(),
755 }
756 }
757
758 #[wasm_bindgen(js_name = loadModule)]
766 pub fn load_module(&mut self, header_json: &str, executable: &[u8]) -> Result<String, JsValue> {
767 self.load_module_inner(header_json, executable.to_vec().into_boxed_slice())
768 }
769
770 #[wasm_bindgen(js_name = prepareModule)]
774 pub fn prepare_module(&self, header_json: &str, executable: Vec<u8>) -> js_sys::Promise {
775 prepare_module_impl(self.loader.clone(), header_json, executable)
776 }
777
778 #[wasm_bindgen(js_name = loadPreparedModule)]
781 pub fn load_prepared_module(&mut self, header_json: &str) -> Result<String, JsValue> {
782 self.load_module_inner(header_json, Box::new([]))
783 }
784
785 fn load_module_inner(
786 &mut self,
787 header_json: &str,
788 executable: Box<[u8]>,
789 ) -> Result<String, JsValue> {
790 let header: Header = serde_json::from_str(header_json)
791 .map_err(|e| JsValue::from_str(&format!("invalid header: {e}")))?;
792 let module_id = header.id;
793 let header_json_str = header_json.to_string();
794
795 for export in &header.exports {
796 let arora_types::module::low::ExportSymbol::Function(f) = export;
797 let children_param_id = f.parameters.first().and_then(|p| {
798 if let arora_types::module::low::TypeRef::Array { id } = &p.ty {
799 if id == &Uuid::from_bytes(TICK_ID_STRUCT_BYTES) {
800 Some(p.id)
801 } else {
802 None
803 }
804 } else {
805 None
806 }
807 });
808
809 self.fn_meta.insert(
810 f.id,
811 FnMeta {
812 module_id,
813 children_param_id,
814 },
815 );
816 }
817
818 let result = load_module_from_parts(&mut *self.inner, header, executable)
819 .map_err(|e| JsValue::from_str(&format!("load failed: {e}")))?;
820 self.module_headers.insert(result.id, header_json_str);
821 Ok(result.id.to_string())
822 }
823
824 #[wasm_bindgen(js_name = listModules)]
826 pub fn list_modules(&self) -> String {
827 let headers: Vec<serde_json::Value> = self
828 .module_headers
829 .values()
830 .filter_map(|s| serde_json::from_str(s).ok())
831 .collect();
832 serde_json::to_string(&headers).unwrap_or_else(|_| "[]".to_string())
833 }
834
835 #[wasm_bindgen(js_name = setVariable)]
838 pub fn set_variable(&mut self, var_id: &str, value_json: &str) -> Result<(), JsValue> {
839 let var_id: Uuid = var_id
840 .parse()
841 .map_err(|_| JsValue::from_str("bad var_id: not a valid UUID"))?;
842 let value: Value = serde_json::from_str(value_json)
843 .map_err(|e| JsValue::from_str(&format!("bad value JSON: {e}")))?;
844 self.variables.borrow_mut().insert(var_id, value);
845 Ok(())
846 }
847
848 pub fn tick(&mut self, nodes_json: &str) -> Result<String, JsValue> {
855 let nodes: Vec<BtNode> = serde_json::from_str(nodes_json)
856 .map_err(|e| JsValue::from_str(&format!("bad nodes JSON: {e}")))?;
857 if nodes.is_empty() {
858 return Err(JsValue::from_str("tree has no nodes"));
859 }
860
861 let root_id = nodes[0].id;
862 let node_index: HashMap<Uuid, BtNode> = nodes.into_iter().map(|n| (n.id, n)).collect();
863 let trace: Rc<RefCell<Vec<(Uuid, &'static str)>>> = Rc::new(RefCell::new(Vec::new()));
864
865 let fn_meta = &self.fn_meta;
866 let root_callable_id = register_node(
867 &mut *self.inner,
868 root_id,
869 &node_index,
870 fn_meta,
871 &trace,
872 &self.variables,
873 )
874 .map_err(|e| JsValue::from_str(&format!("setup error: {e}")))?;
875
876 let callable_id = CallableId {
877 id: root_callable_id,
878 };
879 let result = Callable::call(&callable_id, &mut *self.inner)
880 .map_err(|e| JsValue::from_str(&format!("tick error: {e}")))?;
881 let status = value_to_status(&result);
882
883 let trace_json: Vec<serde_json::Value> = trace
884 .borrow()
885 .iter()
886 .map(|(id, s)| serde_json::json!({ "nodeId": id.to_string(), "status": s }))
887 .collect();
888
889 let vars_json: serde_json::Map<String, serde_json::Value> = self
890 .variables
891 .borrow()
892 .iter()
893 .filter_map(|(id, v)| serde_json::to_value(v).ok().map(|jv| (id.to_string(), jv)))
894 .collect();
895
896 Ok(serde_json::json!({
897 "status": status,
898 "trace": trace_json,
899 "variables": vars_json,
900 })
901 .to_string())
902 }
903
904 pub fn run(&mut self, nodes_json: &str) -> Result<String, JsValue> {
914 let nodes: Vec<BtNode> = serde_json::from_str(nodes_json)
915 .map_err(|e| JsValue::from_str(&format!("bad nodes JSON: {e}")))?;
916 if nodes.is_empty() {
917 return Err(JsValue::from_str("tree has no nodes"));
918 }
919
920 let root_id = nodes[0].id;
921 let node_index: HashMap<Uuid, BtNode> = nodes.into_iter().map(|n| (n.id, n)).collect();
922 let trace: Rc<RefCell<Vec<(Uuid, &'static str)>>> = Rc::new(RefCell::new(Vec::new()));
923
924 let fn_meta = &self.fn_meta;
925 let root_callable_id = register_node(
926 &mut *self.inner,
927 root_id,
928 &node_index,
929 fn_meta,
930 &trace,
931 &self.variables,
932 )
933 .map_err(|e| JsValue::from_str(&format!("setup error: {e}")))?;
934
935 let callable_id = CallableId {
936 id: root_callable_id,
937 };
938
939 let mut last_status = "running";
940 for _ in 0..10_000 {
941 let result = Callable::call(&callable_id, &mut *self.inner)
942 .map_err(|e| JsValue::from_str(&format!("tick error: {e}")))?;
943 last_status = value_to_status(&result);
944 if last_status != "running" {
945 break;
946 }
947 }
948
949 let trace_json: Vec<serde_json::Value> = trace
950 .borrow()
951 .iter()
952 .map(|(id, s)| serde_json::json!({ "nodeId": id.to_string(), "status": s }))
953 .collect();
954
955 let out = serde_json::json!({
956 "status": last_status,
957 "trace": trace_json,
958 });
959 Ok(out.to_string())
960 }
961}
962
963impl Default for BehaviorTreeRunner {
964 fn default() -> Self {
965 Self::new()
966 }
967}
968
969#[wasm_bindgen]
978pub struct Registry {
979 entries: HashMap<Uuid, (String, Option<Uuid>)>,
980}
981
982#[derive(serde::Deserialize)]
983struct RecordEntry {
984 id: Uuid,
985 name: String,
986 parent: Option<Uuid>,
987}
988
989#[wasm_bindgen]
990impl Registry {
991 #[wasm_bindgen(constructor)]
992 pub fn new() -> Registry {
993 Registry {
994 entries: HashMap::new(),
995 }
996 }
997
998 #[wasm_bindgen(js_name = loadRecordsJson)]
1000 pub fn load_records_json(&mut self, json: &str) -> Result<(), JsValue> {
1001 let records: Vec<RecordEntry> = serde_json::from_str(json)
1002 .map_err(|e| JsValue::from_str(&format!("invalid records JSON: {e}")))?;
1003 for r in records {
1004 self.entries.insert(r.id, (r.name, r.parent));
1005 }
1006 Ok(())
1007 }
1008
1009 #[wasm_bindgen(js_name = resolveId)]
1012 pub fn resolve_id(&self, id: &str) -> Option<String> {
1013 let uuid: Uuid = id.parse().ok()?;
1014 self.compute_path(&uuid)
1015 }
1016
1017 fn compute_path(&self, id: &Uuid) -> Option<String> {
1018 let (name, parent) = self.entries.get(id)?;
1019 match parent {
1020 None => Some(name.clone()),
1021 Some(parent_id) if !self.entries.contains_key(parent_id) => Some(name.clone()),
1022 Some(parent_id) => Some(format!("{}.{}", self.compute_path(parent_id)?, name)),
1023 }
1024 }
1025}
1026
1027impl Default for Registry {
1028 fn default() -> Self {
1029 Self::new()
1030 }
1031}