diff --git a/src/lib.rs b/src/lib.rs index 8f2c554..e9c73a9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,7 @@ mod transpile; use asset::JsScriptLoader; use bevy::{asset::AssetStage, prelude::*, utils::HashSet}; +pub use bevy_reflect_fns; pub use asset::JsScript; pub use bevy_ecs_dynamic; pub use runtime::{ diff --git a/src/runtime/native.rs b/src/runtime/native.rs index 4a82ebf..b804af3 100644 --- a/src/runtime/native.rs +++ b/src/runtime/native.rs @@ -285,6 +285,7 @@ fn op_bevy_mod_js_scripting( args: serde_json::Value, ) -> Result { with_state(state, |state, custom_op_state| { + let args = convert_safe_ints(args); let script_info = state.borrow::(); let ops = state.borrow::(); let op_names = state.borrow::(); @@ -323,3 +324,46 @@ fn with_state R>(state: &mut O r } + +/// Takes a [`serde_json::Value`] and converts all floating point number types that are safe +/// integers, to integers. +/// +/// This is important for deserializing numbers to integers, because of the way `serde_json` handles +/// them. +/// +/// For example, `serde_json` will not deserialize `1.0` to a `u32` without an error, but it will +/// deserialize `1`. `serde_v8` seems to retun numbers with a decimal point, even when they are +/// valid integers, so this function makes the conversion of safe integers back to integers without +/// a decimal point. +fn convert_safe_ints(value: serde_json::Value) -> serde_json::Value { + match value { + serde_json::Value::Number(n) => { + let max_safe_int = (2u64.pow(53) - 1) as f64; + + serde_json::Value::Number(if let Some(f) = n.as_f64() { + if f.abs() <= max_safe_int && f.fract() == 0.0 { + if f == 0.0 { + serde_json::Number::from(0u64) + } else if f.is_sign_negative() { + serde_json::Number::from(f as i64) + } else { + serde_json::Number::from(f as u64) + } + } else { + n + } + } else { + n + }) + } + serde_json::Value::Array(arr) => { + serde_json::Value::Array(arr.into_iter().map(|x| convert_safe_ints(x)).collect()) + } + serde_json::Value::Object(obj) => serde_json::Value::Object( + obj.into_iter() + .map(|(k, v)| (k, convert_safe_ints(v))) + .collect(), + ), + other => other, + } +} diff --git a/src/runtime/ops/ecs.rs b/src/runtime/ops/ecs.rs index f4c8933..741c4e5 100644 --- a/src/runtime/ops/ecs.rs +++ b/src/runtime/ops/ecs.rs @@ -4,12 +4,12 @@ use crate::runtime::{JsRuntimeOp, OpMap}; use self::types::{JsReflectFunctions, JsValueRefs}; -mod component; mod info; mod query; mod resource; pub mod types; mod value; +mod world; pub fn insert_ecs_ops(ops: &mut OpMap) { ops.insert("ecs_js", Box::new(EcsJs)); @@ -41,9 +41,11 @@ pub fn insert_ecs_ops(ops: &mut OpMap) { ); ops.insert("ecs_value_ref_patch", Box::new(value::ecs_value_ref_patch)); ops.insert("ecs_value_ref_cleanup", Box::new(value::EcsValueRefCleanup)); + ops.insert("ecs_entity_spawn", Box::new(world::ecs_entity_spawn)); + ops.insert("ecs_value_ref_cleanup", Box::new(value::EcsValueRefCleanup)); ops.insert( "ecs_component_insert", - Box::new(component::ecs_component_insert), + Box::new(world::ecs_component_insert), ); } diff --git a/src/runtime/ops/ecs/ecs.js b/src/runtime/ops/ecs/ecs.js index ea37834..c0290fe 100644 --- a/src/runtime/ops/ecs/ecs.js +++ b/src/runtime/ops/ecs/ecs.js @@ -79,7 +79,7 @@ default: const collected = collectedQuery(target); const prop = collected[propName]; - return prop.bind ? prop.bind(collected) : prop; + return prop && prop.bind ? prop.bind(collected) : prop; } } }) @@ -97,6 +97,10 @@ Value.unwrapValueRef(component) ); } + + spawn() { + return Value.wrapValueRef(bevyModJsScriptingOpSync("ecs_entity_spawn")); + } } const VALUE_REF_GET_INNER = Symbol("value_ref_get_inner"); @@ -106,7 +110,16 @@ unwrapValueRef(valueRefProxy) { if (valueRefProxy === null || valueRefProxy === undefined) return valueRefProxy; const inner = valueRefProxy[VALUE_REF_GET_INNER] - return inner ? inner : valueRefProxy; + if (inner) { + return inner; + } else { + if (typeof valueRefProxy == 'object') { + for (const key of Reflect.ownKeys(valueRefProxy)) { + valueRefProxy[key] = Value.unwrapValueRef(valueRefProxy[key]); + } + } + return valueRefProxy; + } }, // keep primitives, null and undefined as is, otherwise wraps the object @@ -160,7 +173,7 @@ "ecs_value_ref_set", target.valueRef, p, - value + Value.unwrapValueRef(value) ); }, apply: (target, thisArg, args) => { @@ -179,7 +192,7 @@ // Instantiates the default value of a given bevy type create(type, patch) { - return Value.wrapValueRef(bevyModJsScriptingOpSync("ecs_value_ref_default", type.typeName, patch)); + return Value.wrapValueRef(bevyModJsScriptingOpSync("ecs_value_ref_default", type.typeName, Value.unwrapValueRef(patch))); }, patch(value, patch) { diff --git a/src/runtime/ops/ecs/value.rs b/src/runtime/ops/ecs/value.rs index b60625f..ab2882b 100644 --- a/src/runtime/ops/ecs/value.rs +++ b/src/runtime/ops/ecs/value.rs @@ -1,12 +1,15 @@ use std::any::TypeId; use anyhow::{bail, format_err, Context}; -use bevy::prelude::{default, ReflectDefault, World}; +use bevy::{ + prelude::{default, ReflectDefault, World}, + utils::HashMap, +}; use bevy_ecs_dynamic::reflect_value_ref::ReflectValueRef; use bevy_reflect::{Reflect, ReflectRef, TypeRegistryArc}; use bevy_reflect_fns::{PassMode, ReflectArg, ReflectMethods}; -use crate::runtime::OpContext; +use crate::{runtime::OpContext, JsReflectFunctions, JsRuntimeOp}; use super::{ types::{ @@ -36,31 +39,99 @@ macro_rules! try_downcast_leaf_set { return Ok(()); })* - Ok::<(), anyhow::Error>(()) + bail!("Couldn't assign to primitive"); })() }; } +#[derive(Debug)] +pub enum JsonValueOrReflect { + Null, + Bool(bool), + Number(serde_json::Number), + String(String), + Array(Vec), + Object(HashMap), + Reflect(Box), +} + +impl JsonValueOrReflect { + fn into_primitive_value(self) -> Option { + use serde_json::Value as V; + Some(match self { + JsonValueOrReflect::Null => V::Null, + JsonValueOrReflect::Bool(b) => V::Bool(b), + JsonValueOrReflect::Number(n) => V::Number(n), + JsonValueOrReflect::String(s) => V::String(s), + _ => return None, + }) + } + fn from_value( + value: serde_json::Value, + value_refs: &JsValueRefs, + world: &World, + ) -> anyhow::Result { + if let Ok(value_ref) = serde_json::from_value::(value.clone()) { + let value_ref = value_refs + .get(value_ref.key) + .ok_or_else(|| format_err!("Value ref doesn't exist"))?; + let reflect = value_ref.get(world)?.clone_value(); + + Ok(Self::Reflect(reflect)) + } else { + match value { + serde_json::Value::Null => Ok(Self::Null), + serde_json::Value::Bool(b) => Ok(Self::Bool(b)), + serde_json::Value::Number(n) => Ok(Self::Number(n)), + serde_json::Value::String(s) => Ok(Self::String(s)), + serde_json::Value::Array(arr) => Ok(Self::Array( + arr.into_iter() + .map(|value| Self::from_value(value, value_refs, world)) + .collect::>()?, + )), + serde_json::Value::Object(map) => { + let mut object = HashMap::default(); + for (key, value) in map { + object.insert(key, Self::from_value(value, value_refs, world)?); + } + Ok(Self::Object(object)) + } + } + } + } +} + /// Converts a JSON value to a dynamic reflect struct or list pub fn patch_reflect_with_json( value: &mut dyn Reflect, - patch: serde_json::Value, + patch: JsonValueOrReflect, ) -> anyhow::Result<()> { match patch { - serde_json::Value::Null => { + JsonValueOrReflect::Reflect(patch) => { + if !reflect_is_compatible(value, patch.as_reflect()) { + bail!( + "Cannot assign type {} to {}", + value.type_name(), + patch.type_name() + ); + } + value.apply(patch.as_reflect()); + } + JsonValueOrReflect::Null => { bail!("Can't patch values with null"); } - patch @ (serde_json::Value::Bool(_) - | serde_json::Value::Number(_) - | serde_json::Value::String(_)) => { + patch @ (JsonValueOrReflect::Bool(_) + | JsonValueOrReflect::Number(_) + | JsonValueOrReflect::String(_)) => { + let patch = patch.into_primitive_value().unwrap(); try_downcast_leaf_set!(value <- patch for u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, String, char, bool, f32, f64 )?; } - serde_json::Value::Array(array) => match value.reflect_mut() { - bevy_reflect::ReflectMut::Struct(_) => todo!(), + JsonValueOrReflect::Array(array) => match value.reflect_mut() { + bevy_reflect::ReflectMut::Struct(_) => bail!("Cannot patch struct with Array"), bevy_reflect::ReflectMut::List(target) => { let target_len = target.len(); let patch_len = array.len(); @@ -112,7 +183,7 @@ pub fn patch_reflect_with_json( bevy_reflect::ReflectMut::Map(_) => bail!("Cannot patch map with array"), bevy_reflect::ReflectMut::Value(_) => bail!("Cannot patch primitive value with array"), }, - serde_json::Value::Object(map) => match value.reflect_mut() { + JsonValueOrReflect::Object(map) => match value.reflect_mut() { bevy_reflect::ReflectMut::Struct(target) => { for (key, value) in map { let field = target.field_mut(&key).ok_or_else(|| { @@ -152,6 +223,53 @@ pub fn patch_reflect_with_json( Ok(()) } +/// Check whether or not it's safe to `Reflect.apply` one reflect to another +fn reflect_is_compatible(reflect1: &dyn Reflect, reflect2: &dyn Reflect) -> bool { + match (reflect1.reflect_ref(), reflect2.reflect_ref()) { + (ReflectRef::Value(value1), ReflectRef::Value(value2)) => { + value1.type_id() == value2.type_id() + } + (ReflectRef::Array(arr1), ReflectRef::Array(arr2)) => { + arr1.iter() + .zip(arr2.iter()) + .fold(true, |compatible, (reflect1, reflect2)| { + compatible && reflect_is_compatible(reflect1, reflect2) + }) + } + (ReflectRef::List(list1), ReflectRef::List(list2)) => { + list1 + .iter() + .zip(list2.iter()) + .fold(true, |compatible, (reflect1, reflect2)| { + compatible && reflect_is_compatible(reflect1, reflect2) + }) + } + (ReflectRef::Tuple(tuple1), ReflectRef::Tuple(tuple2)) => tuple1 + .iter_fields() + .zip(tuple2.iter_fields()) + .fold(true, |compatible, (reflect1, reflect2)| { + compatible && reflect_is_compatible(reflect1, reflect2) + }), + (ReflectRef::TupleStruct(tuple1), ReflectRef::TupleStruct(tuple2)) => tuple1 + .iter_fields() + .zip(tuple2.iter_fields()) + .fold(true, |compatible, (reflect1, reflect2)| { + compatible && reflect_is_compatible(reflect1, reflect2) + }), + (ReflectRef::Struct(struct1), ReflectRef::Struct(struct2)) => struct1 + .iter_fields() + .enumerate() + .fold(true, |compatible, (i, field1)| { + if let Some(field2) = struct2.field(struct1.name_at(i).unwrap()) { + compatible && reflect_is_compatible(field1, field2) + } else { + compatible + } + }), + _ => false, + } +} + pub fn ecs_value_ref_get( context: OpContext, world: &mut bevy::prelude::World, @@ -244,22 +362,53 @@ pub fn ecs_value_ref_set( // Access the provided path on the value ref let mut value_ref = append_path(value_ref, path, world)?; - // Get the reflect value - let mut reflect = value_ref.get_mut(world).unwrap(); - - // Try to store a primitive in the value - try_downcast_leaf_set!(reflect <- new_value for - u8, u16, u32, u64, u128, usize, - i8, i16, i32, i64, i128, isize, - String, char, bool, f32, f64 - ) - .map(|_| serde_json::Value::Null) - .map_err(|e| { - format_err!( - "could not set value reference: type `{typename}` is not a primitive type: {e}", - typename = reflect.type_name(), + // Try to asign as a primitive + let primitive_assignment_result = { + let new_value = new_value.clone(); + let mut reflect = value_ref.get_mut(world)?; + + // Try to store a primitive in the value + try_downcast_leaf_set!(reflect <- new_value for + u8, u16, u32, u64, u128, usize, + i8, i16, i32, i64, i128, isize, + String, char, bool, f32, f64 ) - }) + .map_err(|e| { + format_err!( + "could not set value reference: type `{type_name}` is not a primitive \ + type or value ref: {e}", + type_name = reflect.type_name(), + ) + }) + }; + + // If we could not assign a primitive + if let Err(e) = primitive_assignment_result { + // Try to assign as a reflect value + if let Ok(new_js_value_ref) = serde_json::from_value::(new_value) { + let new_value_ref = value_refs + .get(new_js_value_ref.key) + .ok_or_else(|| format_err!("Value ref doesn't exist"))?; + let new_reflect = new_value_ref.get(world)?.clone_value(); + let mut reflect = value_ref.get_mut(world)?; + + if !reflect_is_compatible(new_reflect.as_reflect(), reflect.as_reflect()) { + bail!( + "Cannot assign value ref. {} cannot be assigned to {}", + new_reflect.type_name(), + reflect.type_name() + ); + } + + reflect.apply(new_reflect.as_reflect()); + + Ok(serde_json::Value::Null) + } else { + Err(e) + } + } else { + Ok(serde_json::Value::Null) + } } pub fn ecs_value_ref_keys( @@ -339,6 +488,10 @@ pub fn ecs_value_ref_default( .entry::() .or_insert_with(default); + let patch = patch + .map(|patch| JsonValueOrReflect::from_value(patch, value_refs, world)) + .transpose()?; + // Load the type registry let type_registry = world.resource::(); let type_registry = type_registry.read(); @@ -378,6 +531,8 @@ pub fn ecs_value_ref_patch( .entry::() .or_insert_with(default); + let patch = JsonValueOrReflect::from_value(patch, value_refs, world)?; + let value_ref = value_refs .get_mut(value_ref.key) .ok_or_else(|| format_err!("Value ref does not exist"))?; @@ -483,14 +638,25 @@ pub fn ecs_value_ref_call( // Finally call the method let ret = method.call(args.as_mut_slice()).unwrap(); - // Drop our intermediates and args so that we can use `value_refs` again, below. - drop(args); - drop(arg_intermediates); - drop(receiver_intermediate); + // Try to downcast return value to a primitive + let primitive = try_downcast_leaf_get!(ret for + u8, u16, u32, u64, u128, usize, + i8, i16, i32, i64, i128, isize, + String, char, bool, f32, f64 + )?; - let ret = JsValueRef::new_free(ret, value_refs); + if let Some(primitive) = primitive { + Ok(primitive) + } else { + // Drop our intermediates and args so that we can use `value_refs` again, below. + drop(args); + drop(arg_intermediates); + drop(receiver_intermediate); - Ok(serde_json::to_value(ret)?) + let ret = JsValueRef::new_free(ret, value_refs); + + Ok(serde_json::to_value(ret)?) + } }) } @@ -559,3 +725,62 @@ fn append_path( }; Ok(value_ref.append_path(&path, world)?) } + +#[cfg(test)] +mod test { + use super::*; + + #[derive(Reflect, Default)] + struct S1 { + a: String, + b: f32, + } + + #[derive(Reflect, Default)] + struct S2 { + a: String, + b: f32, + c: u32, + } + + #[derive(Reflect, Default)] + struct S3 { + a: String, + b: u32, + } + + #[test] + fn test_reflect_is_compatible_check() { + let string = Box::new(String::default()) as Box; + let uint = Box::new(0u32) as Box; + let mut s1 = Box::new(S1::default()) as Box; + let s2 = Box::new(S2::default()) as Box; + let s3 = Box::new(S3::default()) as Box; + + assert!(!reflect_is_compatible( + uint.as_reflect(), + string.as_reflect() + )); + assert!(!reflect_is_compatible(s1.as_reflect(), string.as_reflect())); + + assert!(reflect_is_compatible(s1.as_reflect(), s2.as_reflect())); + s1.apply(s2.as_reflect()); + + assert!(!reflect_is_compatible(s1.as_reflect(), s3.as_reflect())); + + let mut l1 = Box::new(vec![1, 2, 3]) as Box; + let l2 = Box::new(vec![5, 4, 3, 2, 1]) as Box; + + assert!(reflect_is_compatible(l1.as_reflect(), l2.as_reflect())); + l1.apply(l2.as_reflect()); + assert!(!reflect_is_compatible(l1.as_reflect(), s1.as_reflect())); + + let mut t1 = Box::new((0u32, String::from("hi"))) as Box; + let t2 = Box::new((1u32, String::from("bye"))) as Box; + let t3 = Box::new((0f32, String::from("bye"))) as Box; + + assert!(reflect_is_compatible(t1.as_reflect(), t2.as_reflect())); + t1.apply(t2.as_reflect()); + assert!(!reflect_is_compatible(t1.as_reflect(), t3.as_reflect())); + } +} diff --git a/src/runtime/ops/ecs/component.rs b/src/runtime/ops/ecs/world.rs similarity index 81% rename from src/runtime/ops/ecs/component.rs rename to src/runtime/ops/ecs/world.rs index 0579253..3998454 100644 --- a/src/runtime/ops/ecs/component.rs +++ b/src/runtime/ops/ecs/world.rs @@ -4,6 +4,22 @@ use bevy_reflect::TypeRegistryArc; use crate::{JsValueRef, JsValueRefs, OpContext}; +pub fn ecs_entity_spawn( + context: OpContext, + world: &mut bevy::prelude::World, + _args: serde_json::Value, +) -> anyhow::Result { + let value_refs = context + .op_state + .entry::() + .or_insert_with(default); + + let entity = world.spawn().id(); + let value_ref = JsValueRef::new_free(Box::new(entity), value_refs); + + Ok(serde_json::to_value(value_ref)?) +} + pub fn ecs_component_insert( context: OpContext, world: &mut bevy::prelude::World, diff --git a/src/runtime/wasm.rs b/src/runtime/wasm.rs index 8e14ad9..73ef0d3 100644 --- a/src/runtime/wasm.rs +++ b/src/runtime/wasm.rs @@ -131,6 +131,15 @@ impl FromWorld for JsRuntime { impl JsRuntimeApi for JsRuntime { fn load_script(&self, handle: &Handle, script: &JsScript, _reload: bool) { + // Set script info + { + let mut state = self.state.try_lock().expect(LOCK_SHOULD_NOT_FAIL); + state.script_info = ScriptInfo { + path: script.path.clone(), + handle: handle.clone_weak(), + }; + } + let function = js_sys::Function::new_no_args(&format!( r#"return ((window) => {{ {code} @@ -146,6 +155,15 @@ impl JsRuntimeApi for JsRuntime { } }; + // Clear script info + { + let mut state = self.state.try_lock().expect(LOCK_SHOULD_NOT_FAIL); + state.script_info = ScriptInfo { + path: default(), + handle: default(), + }; + } + self.scripts.try_lock().expect(LOCK_SHOULD_NOT_FAIL).insert( handle.clone_weak(), ScriptData { diff --git a/types/lib.bevy.d.ts b/types/lib.bevy.d.ts index 246bd84..dc5c2f3 100644 --- a/types/lib.bevy.d.ts +++ b/types/lib.bevy.d.ts @@ -22,9 +22,13 @@ declare interface BevyScript { declare type RawValueRef = unknown; +type RecursivePartial = { + [P in keyof T]?: RecursivePartial; +}; + declare interface ValueGlobal { - create(t: BevyType, patch?: any): T; - patch(value: any, patch: any): T; + create(t: BevyType, patch?: RecursivePartial): T; + patch(value: T, patch: RecursivePartial): T; } declare let Value: ValueGlobal; @@ -80,6 +84,7 @@ declare class World { query(...query: Q): QueryItems; get(entity: Entity, component: BevyType): T | undefined; insert(entity: Entity, component: any): void; + spawn(): Entity; } declare let world: World;