diff --git a/crates/bevy_mod_scripting_core/Cargo.toml b/crates/bevy_mod_scripting_core/Cargo.toml index c36561d628..75887c2db7 100644 --- a/crates/bevy_mod_scripting_core/Cargo.toml +++ b/crates/bevy_mod_scripting_core/Cargo.toml @@ -48,6 +48,7 @@ bevy_mod_scripting_derive = { workspace = true } [dev-dependencies] test_utils = { workspace = true } tokio = { version = "1", features = ["rt", "macros"] } +pretty_assertions = "1.4" [lints] workspace = true diff --git a/crates/bevy_mod_scripting_core/src/bindings/function/arg_meta.rs b/crates/bevy_mod_scripting_core/src/bindings/function/arg_meta.rs index 5fdec4ade0..ade2e33e20 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/function/arg_meta.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/function/arg_meta.rs @@ -8,7 +8,7 @@ use crate::{ }; use super::{ - from::{FromScript, Mut, Ref, Val}, + from::{FromScript, Mut, Ref, Union, Val}, into::IntoScript, script_function::{DynamicScriptFunction, DynamicScriptFunctionMut, FunctionCallContext}, type_dependencies::GetTypeDependencies, @@ -72,6 +72,7 @@ impl_arg_info!( &'static str ); +impl ArgMeta for Union {} impl ArgMeta for Val {} impl ArgMeta for Ref<'_, T> {} impl ArgMeta for Mut<'_, T> {} diff --git a/crates/bevy_mod_scripting_core/src/bindings/function/from.rs b/crates/bevy_mod_scripting_core/src/bindings/function/from.rs index ebb9ec5237..e3b9fed070 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/function/from.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/function/from.rs @@ -456,3 +456,46 @@ where } } } + +/// A union of two or more (by nesting unions) types. +pub struct Union(Result); + +impl Union { + /// Try interpret the union as the left type + pub fn into_left(self) -> Result { + match self.0 { + Ok(r) => Ok(r), + Err(l) => Err(l), + } + } + + /// Try interpret the union as the right type + pub fn into_right(self) -> Result { + match self.0 { + Err(r) => Ok(r), + Ok(l) => Err(l), + } + } +} + +impl FromScript for Union +where + for<'a> T1::This<'a>: Into, + for<'a> T2::This<'a>: Into, +{ + type This<'w> = Self; + fn from_script( + value: ScriptValue, + world: WorldGuard<'_>, + ) -> Result, InteropError> { + let _ = match T1::from_script(value.clone(), world.clone()) { + Ok(v) => return Ok(Union(Ok(v.into()))), + Err(e) => e, + }; + + match T2::from_script(value, world) { + Ok(v) => Ok(Union(Err(v.into()))), + Err(e) => Err(e), + } + } +} diff --git a/crates/bevy_mod_scripting_core/src/bindings/function/mod.rs b/crates/bevy_mod_scripting_core/src/bindings/function/mod.rs index d659932ea2..274e3c4cb0 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/function/mod.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/function/mod.rs @@ -18,7 +18,7 @@ mod test { use crate::{ bindings::{ function::{ - from::{Ref, Val}, + from::{Ref, Union, Val}, namespace::IntoNamespace, script_function::AppScriptFunctionRegistry, }, @@ -109,6 +109,8 @@ mod test { #[test] fn composites_are_valid_args() { + test_is_valid_arg::>(); + fn test_val() where T: ScriptArgument + ScriptReturn, diff --git a/crates/bevy_mod_scripting_core/src/bindings/function/type_dependencies.rs b/crates/bevy_mod_scripting_core/src/bindings/function/type_dependencies.rs index 866702643b..a22c29f80c 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/function/type_dependencies.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/function/type_dependencies.rs @@ -1,7 +1,7 @@ //! This module contains the [`GetTypeDependencies`] trait and its implementations for various types. use super::{ - from::{Mut, Ref, Val}, + from::{Mut, Ref, Union, Val}, script_function::FunctionCallContext, }; use crate::{ @@ -88,6 +88,13 @@ recursive_type_dependencies!( (HashMap where K: GetTypeRegistration;FromReflect;Typed;Hash;Eq, V: GetTypeRegistration;FromReflect;Typed => with Self) ); +impl GetTypeDependencies for Union { + fn register_type_dependencies(registry: &mut TypeRegistry) { + T1::register_type_dependencies(registry); + T2::register_type_dependencies(registry); + } +} + bevy::utils::all_tuples!(register_tuple_dependencies, 1, 14, T); /// A trait collecting type dependency information for a whole function. Used to register everything used by a function with the type registry diff --git a/crates/bevy_mod_scripting_core/src/bindings/query.rs b/crates/bevy_mod_scripting_core/src/bindings/query.rs index 6a877f83a2..68b8d72f8d 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/query.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/query.rs @@ -80,6 +80,11 @@ impl ScriptResourceRegistration { pub fn type_registration(&self) -> &ScriptTypeRegistration { &self.registration } + + /// Convert to a generic [`ScriptTypeRegistration`] ditching the resource information. + pub fn into_type_registration(self) -> ScriptTypeRegistration { + self.registration + } } impl ScriptComponentRegistration { @@ -101,6 +106,11 @@ impl ScriptComponentRegistration { pub fn type_registration(&self) -> &ScriptTypeRegistration { &self.registration } + + /// Convert to a generic [`ScriptTypeRegistration`] ditching the component information. + pub fn into_type_registration(self) -> ScriptTypeRegistration { + self.registration + } } impl std::fmt::Debug for ScriptTypeRegistration { diff --git a/crates/bevy_mod_scripting_core/src/bindings/reference.rs b/crates/bevy_mod_scripting_core/src/bindings/reference.rs index 7cc9fc3803..ab83201df0 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/reference.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/reference.rs @@ -61,6 +61,16 @@ pub enum TypeIdSource { } #[profiling::all_functions] impl ReflectReference { + /// If this points to a variant of an enum, returns the name of the variant. + pub fn variant_name(&self, world: WorldGuard) -> Result, InteropError> { + self.with_reflect(world, |s| { + s.reflect_ref() + .as_enum() + .ok() + .map(|enum_ref| enum_ref.variant_name().to_owned()) + }) + } + /// Creates a new infinite iterator. This iterator will keep returning the next element reference forever. pub fn into_iter_infinite(self) -> ReflectRefIter { ReflectRefIter::new_indexed(self) diff --git a/crates/bevy_mod_scripting_core/src/bindings/world.rs b/crates/bevy_mod_scripting_core/src/bindings/world.rs index 62120c407e..9e4c11aba8 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/world.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/world.rs @@ -16,7 +16,12 @@ use super::{ AppReflectAllocator, ReflectBase, ReflectBaseType, ReflectReference, ScriptComponentRegistration, ScriptResourceRegistration, ScriptTypeRegistration, }; -use crate::{error::InteropError, with_access_read, with_access_write, with_global_access}; +use crate::{ + bindings::function::{from::FromScript, from_ref::FromScriptRef}, + error::InteropError, + reflection_extensions::PartialReflectExt, + with_access_read, with_access_write, with_global_access, +}; use bevy::{ app::AppExit, ecs::{ @@ -27,12 +32,16 @@ use bevy::{ world::{unsafe_world_cell::UnsafeWorldCell, CommandQueue, Mut, World}, }, hierarchy::{BuildChildren, Children, DespawnRecursiveExt, Parent}, - reflect::{std_traits::ReflectDefault, ParsedPath, TypeRegistryArc}, + reflect::{ + std_traits::ReflectDefault, DynamicEnum, DynamicStruct, DynamicTuple, DynamicTupleStruct, + DynamicVariant, ParsedPath, PartialReflect, TypeRegistryArc, + }, }; use std::{ any::TypeId, borrow::Cow, cell::{Cell, RefCell}, + collections::HashMap, fmt::Debug, rc::Rc, sync::Arc, @@ -431,6 +440,256 @@ impl<'w> WorldAccessGuard<'w> { /// Impl block for higher level world methods #[profiling::all_functions] impl WorldAccessGuard<'_> { + fn construct_from_script_value( + &self, + descriptor: impl Into>, + type_id: TypeId, + value: Option, + ) -> Result, InteropError> { + // if the value is missing, try to construct a default and return it + let value = match value { + Some(value) => value, + None => { + let type_registry = self.type_registry(); + let type_registry = type_registry.read(); + let default_data = type_registry + .get_type_data::(type_id) + .ok_or_else(|| { + InteropError::missing_data_in_constructor(type_id, descriptor) + })?; + return Ok(default_data.default().into_partial_reflect()); + } + }; + + // otherwise we need to use from_script_ref + >::from_script_ref(type_id, value, self.clone()) + } + + fn construct_dynamic_struct( + &self, + payload: &mut HashMap, + fields: Vec<(&'static str, TypeId)>, + ) -> Result { + let mut dynamic = DynamicStruct::default(); + for (field_name, field_type_id) in fields { + let constructed = self.construct_from_script_value( + field_name, + field_type_id, + payload.remove(field_name), + )?; + + dynamic.insert_boxed(field_name, constructed); + } + Ok(dynamic) + } + + fn construct_dynamic_tuple_struct( + &self, + payload: &mut HashMap, + fields: Vec, + one_indexed: bool, + ) -> Result { + let mut dynamic = DynamicTupleStruct::default(); + for (field_idx, field_type_id) in fields.into_iter().enumerate() { + // correct for indexing + let script_idx = if one_indexed { + field_idx + 1 + } else { + field_idx + }; + let field_string = format!("_{script_idx}"); + dynamic.insert_boxed(self.construct_from_script_value( + field_string.clone(), + field_type_id, + payload.remove(&field_string), + )?); + } + Ok(dynamic) + } + + fn construct_dynamic_tuple( + &self, + payload: &mut HashMap, + fields: Vec, + one_indexed: bool, + ) -> Result { + let mut dynamic = DynamicTuple::default(); + for (field_idx, field_type_id) in fields.into_iter().enumerate() { + // correct for indexing + let script_idx = if one_indexed { + field_idx + 1 + } else { + field_idx + }; + + let field_string = format!("_{script_idx}"); + + dynamic.insert_boxed(self.construct_from_script_value( + field_string.clone(), + field_type_id, + payload.remove(&field_string), + )?); + } + Ok(dynamic) + } + + /// An arbitrary type constructor utility. + /// + /// Allows the construction of arbitrary types (within limits dictated by the API) from the script directly + pub fn construct( + &self, + type_: ScriptTypeRegistration, + mut payload: HashMap, + one_indexed: bool, + ) -> Result, InteropError> { + // figure out the kind of type we're building + let type_info = type_.registration.type_info(); + // we just need to a) extract fields, if enum we need a "variant" field specifying the variant + // then build the corresponding dynamic structure, whatever it may be + + let dynamic: Box = match type_info { + bevy::reflect::TypeInfo::Struct(struct_info) => { + let fields_iter = struct_info + .field_names() + .iter() + .map(|f| { + Ok(( + *f, + struct_info + .field(f) + .ok_or_else(|| { + InteropError::invariant( + "field in field_names should have reflection information", + ) + })? + .type_id(), + )) + }) + .collect::, _>>()?; + let mut dynamic = self.construct_dynamic_struct(&mut payload, fields_iter)?; + dynamic.set_represented_type(Some(type_info)); + Box::new(dynamic) + } + bevy::reflect::TypeInfo::TupleStruct(tuple_struct_info) => { + let fields_iter = (0..tuple_struct_info.field_len()) + .map(|f| { + Ok(tuple_struct_info + .field_at(f) + .ok_or_else(|| { + InteropError::invariant( + "field in field_names should have reflection information", + ) + })? + .type_id()) + }) + .collect::, _>>()?; + + let mut dynamic = + self.construct_dynamic_tuple_struct(&mut payload, fields_iter, one_indexed)?; + dynamic.set_represented_type(Some(type_info)); + Box::new(dynamic) + } + bevy::reflect::TypeInfo::Tuple(tuple_info) => { + let fields_iter = (0..tuple_info.field_len()) + .map(|f| { + Ok(tuple_info + .field_at(f) + .ok_or_else(|| { + InteropError::invariant( + "field in field_names should have reflection information", + ) + })? + .type_id()) + }) + .collect::, _>>()?; + + let mut dynamic = + self.construct_dynamic_tuple(&mut payload, fields_iter, one_indexed)?; + dynamic.set_represented_type(Some(type_info)); + Box::new(dynamic) + } + bevy::reflect::TypeInfo::Enum(enum_info) => { + // extract variant from "variant" + let variant = payload.remove("variant").ok_or_else(|| { + InteropError::missing_data_in_constructor( + enum_info.type_id(), + "\"variant\" field for enum", + ) + })?; + + let variant_name = String::from_script(variant, self.clone())?; + + let variant = enum_info.variant(&variant_name).ok_or_else(|| { + InteropError::invalid_enum_variant(enum_info.type_id(), variant_name.clone()) + })?; + + let variant = match variant { + bevy::reflect::VariantInfo::Struct(struct_variant_info) => { + // same as above struct variant + let fields_iter = struct_variant_info + .field_names() + .iter() + .map(|f| { + Ok(( + *f, + struct_variant_info + .field(f) + .ok_or_else(|| { + InteropError::invariant( + "field in field_names should have reflection information", + ) + })? + .type_id(), + )) + }) + .collect::, _>>()?; + + let dynamic = self.construct_dynamic_struct(&mut payload, fields_iter)?; + DynamicVariant::Struct(dynamic) + } + bevy::reflect::VariantInfo::Tuple(tuple_variant_info) => { + // same as tuple variant + let fields_iter = (0..tuple_variant_info.field_len()) + .map(|f| { + Ok(tuple_variant_info + .field_at(f) + .ok_or_else(|| { + InteropError::invariant( + "field in field_names should have reflection information", + ) + })? + .type_id()) + }) + .collect::, _>>()?; + + let dynamic = + self.construct_dynamic_tuple(&mut payload, fields_iter, one_indexed)?; + DynamicVariant::Tuple(dynamic) + } + bevy::reflect::VariantInfo::Unit(_) => DynamicVariant::Unit, + }; + let mut dynamic = DynamicEnum::new(variant_name, variant); + dynamic.set_represented_type(Some(type_info)); + Box::new(dynamic) + } + _ => { + return Err(InteropError::unsupported_operation( + Some(type_info.type_id()), + Some(Box::new(payload)), + "Type constructor not supported", + )) + } + }; + + // try to construct type from reflect + // TODO: it would be nice to have a ::from_reflect_with_fallback equivalent, that does exactly that + // only using this as it's already there and convenient, the clone variant hitting will be confusing to end users + Ok(::from_reflect_or_clone( + dynamic.as_ref(), + self.clone(), + )) + } + /// Spawns a new entity in the world pub fn spawn(&self) -> Result { self.with_global_access(|world| { @@ -867,3 +1126,156 @@ impl WorldContainer for ThreadWorldContainer { .map(|v| v.shorten_lifetime()) } } + +#[cfg(test)] +mod test { + use super::*; + use bevy::reflect::{GetTypeRegistration, Reflect, ReflectFromReflect}; + use test_utils::test_data::{setup_world, SimpleEnum, SimpleStruct, SimpleTupleStruct}; + + #[test] + fn test_construct_struct() { + let mut world = setup_world(|_, _| {}); + let world = WorldAccessGuard::new(&mut world); + + let registry = world.type_registry(); + let registry = registry.read(); + + let registration = registry.get(TypeId::of::()).unwrap().clone(); + let type_registration = ScriptTypeRegistration::new(Arc::new(registration)); + + let payload = HashMap::from_iter(vec![("foo".to_owned(), ScriptValue::Integer(1))]); + + let result = world.construct(type_registration, payload, false); + let expected = + Ok::<_, InteropError>(Box::new(SimpleStruct { foo: 1 }) as Box); + pretty_assertions::assert_str_eq!(format!("{result:#?}"), format!("{expected:#?}")); + } + + #[test] + fn test_construct_tuple_struct() { + let mut world = setup_world(|_, _| {}); + let world = WorldAccessGuard::new(&mut world); + + let registry = world.type_registry(); + let registry = registry.read(); + + let registration = registry + .get(TypeId::of::()) + .unwrap() + .clone(); + let type_registration = ScriptTypeRegistration::new(Arc::new(registration)); + + // zero indexed + let payload = HashMap::from_iter(vec![("_0".to_owned(), ScriptValue::Integer(1))]); + + let result = world.construct(type_registration.clone(), payload, false); + let expected = + Ok::<_, InteropError>(Box::new(SimpleTupleStruct(1)) as Box); + pretty_assertions::assert_str_eq!(format!("{result:#?}"), format!("{expected:#?}")); + + // one indexed + let payload = HashMap::from_iter(vec![("_1".to_owned(), ScriptValue::Integer(1))]); + + let result = world.construct(type_registration, payload, true); + let expected = + Ok::<_, InteropError>(Box::new(SimpleTupleStruct(1)) as Box); + + pretty_assertions::assert_str_eq!(format!("{result:#?}"), format!("{expected:#?}")); + } + + #[derive(Reflect)] + struct Test { + pub hello: (usize, usize), + } + + #[test] + fn test_construct_tuple() { + let mut world = setup_world(|_, registry| { + registry.register::<(usize, usize)>(); + // TODO: does this ever get registered on normal types? I don't think so: https://github.com/bevyengine/bevy/issues/17981 + registry.register_type_data::<(usize, usize), ReflectFromReflect>(); + }); + + ::get_type_registration(); + let world = WorldAccessGuard::new(&mut world); + + let registry = world.type_registry(); + let registry = registry.read(); + + let registration = registry + .get(TypeId::of::<(usize, usize)>()) + .unwrap() + .clone(); + let type_registration = ScriptTypeRegistration::new(Arc::new(registration)); + + // zero indexed + let payload = HashMap::from_iter(vec![ + ("_0".to_owned(), ScriptValue::Integer(1)), + ("_1".to_owned(), ScriptValue::Integer(2)), + ]); + + let result = world.construct(type_registration.clone(), payload, false); + let expected = Ok::<_, InteropError>(Box::new((1, 2)) as Box); + pretty_assertions::assert_str_eq!(format!("{result:#?}"), format!("{expected:#?}")); + + // one indexed + let payload = HashMap::from_iter(vec![ + ("_1".to_owned(), ScriptValue::Integer(1)), + ("_2".to_owned(), ScriptValue::Integer(2)), + ]); + + let result = world.construct(type_registration.clone(), payload, true); + let expected = Ok::<_, InteropError>(Box::new((1, 2)) as Box); + pretty_assertions::assert_str_eq!(format!("{result:#?}"), format!("{expected:#?}")); + } + + #[test] + fn test_construct_enum() { + let mut world = setup_world(|_, _| {}); + let world = WorldAccessGuard::new(&mut world); + + let registry = world.type_registry(); + let registry = registry.read(); + + let registration = registry.get(TypeId::of::()).unwrap().clone(); + let type_registration = ScriptTypeRegistration::new(Arc::new(registration)); + + // struct version + let payload = HashMap::from_iter(vec![ + ("foo".to_owned(), ScriptValue::Integer(1)), + ("variant".to_owned(), ScriptValue::String("Struct".into())), + ]); + + let result = world.construct(type_registration.clone(), payload, false); + let expected = Ok::<_, InteropError>( + Box::new(SimpleEnum::Struct { foo: 1 }) as Box + ); + pretty_assertions::assert_str_eq!(format!("{result:#?}"), format!("{expected:#?}")); + + // tuple struct version + let payload = HashMap::from_iter(vec![ + ("_0".to_owned(), ScriptValue::Integer(1)), + ( + "variant".to_owned(), + ScriptValue::String("TupleStruct".into()), + ), + ]); + + let result = world.construct(type_registration.clone(), payload, false); + let expected = + Ok::<_, InteropError>(Box::new(SimpleEnum::TupleStruct(1)) as Box); + + pretty_assertions::assert_str_eq!(format!("{result:#?}"), format!("{expected:#?}")); + + // unit version + let payload = HashMap::from_iter(vec![( + "variant".to_owned(), + ScriptValue::String("Unit".into()), + )]); + + let result = world.construct(type_registration, payload, false); + let expected = Ok::<_, InteropError>(Box::new(SimpleEnum::Unit) as Box); + pretty_assertions::assert_str_eq!(format!("{result:#?}"), format!("{expected:#?}")); + } +} diff --git a/crates/bevy_mod_scripting_core/src/docgen/typed_through.rs b/crates/bevy_mod_scripting_core/src/docgen/typed_through.rs index e6fefdd88f..2ce558f7f3 100644 --- a/crates/bevy_mod_scripting_core/src/docgen/typed_through.rs +++ b/crates/bevy_mod_scripting_core/src/docgen/typed_through.rs @@ -8,7 +8,7 @@ use bevy::reflect::{TypeInfo, Typed}; use crate::{ bindings::{ function::{ - from::{Mut, Ref, Val}, + from::{Mut, Ref, Union, Val}, script_function::{ DynamicScriptFunction, DynamicScriptFunctionMut, FunctionCallContext, }, @@ -59,6 +59,8 @@ pub enum UntypedWrapperKind { /// The kind of typed wrapper. #[derive(Debug, Clone)] pub enum TypedWrapperKind { + /// A union of many possible types + Union(Vec), /// Wraps a `Vec` of a through typed type. Vec(Box), /// Wraps a `HashMap` of a through typed type. @@ -79,6 +81,15 @@ pub trait TypedThrough { fn through_type_info() -> ThroughTypeInfo; } +impl TypedThrough for Union { + fn through_type_info() -> ThroughTypeInfo { + ThroughTypeInfo::TypedWrapper(TypedWrapperKind::Union(vec![ + T1::through_type_info(), + T2::through_type_info(), + ])) + } +} + impl TypedThrough for Ref<'_, T> { fn through_type_info() -> ThroughTypeInfo { ThroughTypeInfo::UntypedWrapper { diff --git a/crates/bevy_mod_scripting_core/src/error.rs b/crates/bevy_mod_scripting_core/src/error.rs index f077dc94d5..057a5cb6ca 100644 --- a/crates/bevy_mod_scripting_core/src/error.rs +++ b/crates/bevy_mod_scripting_core/src/error.rs @@ -536,6 +536,25 @@ impl InteropError { }, )) } + + /// Thrown when constructing types and we find missing data needed to construct the type + pub fn missing_data_in_constructor( + type_id: TypeId, + missing_data_name: impl Into>, + ) -> Self { + Self(Arc::new(InteropErrorInner::MissingDataInConstructor { + type_id, + missing_data_name: missing_data_name.into(), + })) + } + + /// Thrown if an enum variant is invalid. + pub fn invalid_enum_variant(type_id: TypeId, variant_name: impl ToString) -> Self { + Self(Arc::new(InteropErrorInner::InvalidEnumVariant { + type_id, + variant_name: variant_name.to_string(), + })) + } } /// For errors to do with reflection, type conversions or other interop issues @@ -703,11 +722,23 @@ pub(crate) enum InteropErrorInner { /// The type that was not registered type_name: Cow<'static, str>, }, + /// Thrown when constructing types and we find missing data + MissingDataInConstructor { + /// The type id of the type we're constructing + type_id: TypeId, + /// the name of the missing data + missing_data_name: Cow<'static, str>, + }, /// Thrown when an invariant is violated Invariant { /// The message that describes the invariant violation message: String, }, + /// New variant for invalid enum variant errors. + InvalidEnumVariant { + type_id: TypeId, + variant_name: String, + }, } /// For test purposes @@ -896,6 +927,26 @@ impl PartialEq for InteropErrorInner { InteropErrorInner::Invariant { message: a }, InteropErrorInner::Invariant { message: b }, ) => a == b, + ( + InteropErrorInner::MissingDataInConstructor { + type_id: a, + missing_data_name: b, + }, + InteropErrorInner::MissingDataInConstructor { + type_id: c, + missing_data_name: d, + }, + ) => a == c && b == d, + ( + InteropErrorInner::InvalidEnumVariant { + type_id: a, + variant_name: b, + }, + InteropErrorInner::InvalidEnumVariant { + type_id: c, + variant_name: d, + }, + ) => a == c && b == d, _ => false, } } @@ -1063,6 +1114,15 @@ macro_rules! invalid_access_count { }; } +macro_rules! missing_data_in_constructor { + ($type_id:expr, $missing_data_name:expr) => { + format!( + "Missing data in constructor for type: {}. Missing data: {}", + $type_id, $missing_data_name + ) + }; +} + macro_rules! invariant { ($message:expr) => { format!( @@ -1207,6 +1267,16 @@ impl DisplayWithWorld for InteropErrorInner { InteropErrorInner::UnregisteredComponentOrResourceType { type_name } => { unregistered_component_or_resource_type!(type_name) }, + InteropErrorInner::MissingDataInConstructor { type_id, missing_data_name } => { + missing_data_in_constructor!(type_id.display_with_world(world), missing_data_name) + }, + InteropErrorInner::InvalidEnumVariant { type_id, variant_name } => { + format!( + "Invalid enum variant: {} for enum: {}", + variant_name, + type_id.display_with_world(world) + ) + }, } } @@ -1336,6 +1406,16 @@ impl DisplayWithWorld for InteropErrorInner { InteropErrorInner::UnregisteredComponentOrResourceType { type_name } => { unregistered_component_or_resource_type!(type_name) }, + InteropErrorInner::MissingDataInConstructor { type_id, missing_data_name } => { + missing_data_in_constructor!(type_id.display_without_world(), missing_data_name) + }, + InteropErrorInner::InvalidEnumVariant { type_id, variant_name } => { + format!( + "Invalid enum variant: {} for enum: {}", + variant_name, + type_id.display_without_world() + ) + }, } } } diff --git a/crates/bevy_mod_scripting_functions/src/core.rs b/crates/bevy_mod_scripting_functions/src/core.rs index e3d97032de..2142c5b3d5 100644 --- a/crates/bevy_mod_scripting_functions/src/core.rs +++ b/crates/bevy_mod_scripting_functions/src/core.rs @@ -1,8 +1,14 @@ //! Contains functions defined by the [`bevy_mod_scripting_core`] crate +use std::collections::HashMap; + use bevy::{prelude::*, reflect::ParsedPath}; use bevy_mod_scripting_core::{ - bindings::function::script_function::DynamicScriptFunctionMut, docgen::info::FunctionInfo, *, + bindings::function::{ + from::Union, namespace::GlobalNamespace, script_function::DynamicScriptFunctionMut, + }, + docgen::info::FunctionInfo, + *, }; use bevy_mod_scripting_derive::script_bindings; use bindings::{ @@ -253,6 +259,15 @@ impl World { name = "reflect_reference_functions" )] impl ReflectReference { + fn variant_name( + ctxt: FunctionCallContext, + s: ReflectReference, + ) -> Result, InteropError> { + profiling::function_scope!("variant_name"); + let world = ctxt.world()?; + s.variant_name(world) + } + fn display_ref(ctxt: FunctionCallContext, s: ReflectReference) -> Result { profiling::function_scope!("display_ref"); let world = ctxt.world()?; @@ -593,6 +608,56 @@ impl ScriptQueryResult { } } +#[script_bindings( + remote, + bms_core_path = "bevy_mod_scripting_core", + name = "global_namespace_functions", + unregistered +)] +impl GlobalNamespace { + /// Attempts to construct the given type, given an arbitrary map of values. + /// + /// Arguments: + /// * `registration`: The type to construct. + /// * `payload`: The values to use to construct the type. + /// Returns: + /// * `reference`: The constructed type. + fn construct( + ctxt: FunctionCallContext, + registration: Union< + Val, + Union, Val>, + >, + payload: HashMap, + ) -> Result { + let registration = match registration.into_left() { + Ok(l) => l.into_inner(), + Err(r) => match r.into_left() { + Ok(l) => (l.into_inner()).into_type_registration(), + Err(r) => (r.into_inner()).into_type_registration(), + }, + }; + + let world = ctxt.world()?; + let one_indexed = ctxt.convert_to_0_indexed; + + let val = world.construct(registration.clone(), payload, one_indexed)?; + let allocator = world.allocator(); + let mut allocator = allocator.write(); + let reflect_val = val.try_into_reflect().map_err(|_| { + InteropError::failed_from_reflect( + Some(registration.type_id()), + "Could not construct the type".into(), + ) + })?; + + Ok(ReflectReference::new_allocated_boxed( + reflect_val, + &mut allocator, + )) + } +} + pub fn register_core_functions(app: &mut App) { let world = app.world_mut(); // we don't exclude from compilation here, @@ -611,5 +676,7 @@ pub fn register_core_functions(app: &mut App) { register_script_query_builder_functions(world); register_script_query_result_functions(world); + + register_global_namespace_functions(world); } } diff --git a/crates/ladfile/src/lib.rs b/crates/ladfile/src/lib.rs index 6c8964a322..66b86f44c3 100644 --- a/crates/ladfile/src/lib.rs +++ b/crates/ladfile/src/lib.rs @@ -176,6 +176,8 @@ pub enum LadArgumentKind { Array(Box, usize), /// A primitive type, implementing `IntoScript` and `FromScript` natively in BMS. Primitive(LadBMSPrimitiveKind), + /// A union of two or more types + Union(Vec), /// An arbitrary type which is either unsupported, doesn't contain type information, or is generally unknown. /// /// This will be the variant used for external primitives as well. @@ -248,6 +250,13 @@ pub trait ArgumentVisitor { self.visit_lad_bms_primitive_kind(primitive_kind); } + /// traverse a union of arguments, by default calls `visit` on each argument + fn walk_union(&mut self, inner: &[LadArgumentKind]) { + for arg in inner { + self.visit(arg); + } + } + /// traverse an unknown argument, by default calls `visit` on the type id fn walk_unknown(&mut self, type_id: &LadTypeId) { self.visit_lad_type_id(type_id); @@ -270,6 +279,7 @@ pub trait ArgumentVisitor { LadArgumentKind::Tuple(inner) => self.walk_tuple(inner), LadArgumentKind::Array(inner, size) => self.walk_array(inner, *size), LadArgumentKind::Primitive(primitive_kind) => self.walk_primitive(primitive_kind), + LadArgumentKind::Union(inner) => self.walk_union(inner), LadArgumentKind::Unknown(type_id) => self.walk_unknown(type_id), } } diff --git a/crates/ladfile_builder/src/lib.rs b/crates/ladfile_builder/src/lib.rs index 39c4f3f18b..329b6247d1 100644 --- a/crates/ladfile_builder/src/lib.rs +++ b/crates/ladfile_builder/src/lib.rs @@ -581,6 +581,14 @@ impl<'t> LadFileBuilder<'t> { }) .collect(), ), + TypedWrapperKind::Union(through_type_infos) => LadArgumentKind::Union( + through_type_infos + .iter() + .map(|through_type_info| { + self.lad_argument_type_from_through_type(through_type_info) + }) + .collect(), + ), }, ThroughTypeInfo::TypeInfo(type_info) => { match primitive_from_type_id(type_info.type_id()) { diff --git a/crates/languages/bevy_mod_scripting_lua/tests/data/construct/construct_enum.lua b/crates/languages/bevy_mod_scripting_lua/tests/data/construct/construct_enum.lua new file mode 100644 index 0000000000..74b8189239 --- /dev/null +++ b/crates/languages/bevy_mod_scripting_lua/tests/data/construct/construct_enum.lua @@ -0,0 +1,27 @@ +local type = world.get_type_by_name("SimpleEnum") + +-- Struct Variant +local constructed = construct(type, { + variant = "Struct", + foo = 123 +}) + +assert(constructed:variant_name() == "Struct", "Value was constructed incorrectly, expected constructed.variant to be Struct but got " .. constructed:variant_name()) +assert(constructed.foo == 123, "Value was constructed incorrectly, expected constructed.foo to be 123 but got " .. constructed.foo) + + +-- TupleStruct Variant +local constructed = construct(type, { + variant = "TupleStruct", + _1 = 123 +}) + +assert(constructed:variant_name() == "TupleStruct", "Value was constructed incorrectly, expected constructed.variant to be TupleStruct but got " .. constructed:variant_name()) +assert(constructed._1 == 123, "Value was constructed incorrectly, expected constructed._1 to be 123 but got " .. constructed._1) + +-- Unit Variant +local constructed = construct(type, { + variant = "Unit" +}) + +assert(constructed:variant_name() == "Unit", "Value was constructed incorrectly, expected constructed.variant to be Unit but got " .. constructed:variant_name()) \ No newline at end of file diff --git a/crates/languages/bevy_mod_scripting_lua/tests/data/construct/construct_struct.lua b/crates/languages/bevy_mod_scripting_lua/tests/data/construct/construct_struct.lua new file mode 100644 index 0000000000..46ea2ded88 --- /dev/null +++ b/crates/languages/bevy_mod_scripting_lua/tests/data/construct/construct_struct.lua @@ -0,0 +1,6 @@ +local type = world.get_type_by_name("SimpleStruct") +local constructed = construct(type, { + foo = 123 +}) + +assert(constructed.foo == 123, "Value was constructed incorrectly, expected constructed.foo to be 123 but got " .. constructed.foo) \ No newline at end of file diff --git a/crates/languages/bevy_mod_scripting_lua/tests/data/construct/construct_tuple_struct.lua b/crates/languages/bevy_mod_scripting_lua/tests/data/construct/construct_tuple_struct.lua new file mode 100644 index 0000000000..06cc2f2550 --- /dev/null +++ b/crates/languages/bevy_mod_scripting_lua/tests/data/construct/construct_tuple_struct.lua @@ -0,0 +1,8 @@ +local type = world.get_type_by_name("SimpleTupleStruct") +local constructed = construct(type, { + _1 = 123 +}) + +print(constructed:display_value()) + +assert(constructed._1 == 123, "Value was constructed incorrectly, expected constructed.foo to be 123 but got " .. constructed._1) \ No newline at end of file diff --git a/crates/languages/bevy_mod_scripting_rhai/tests/data/construct/simple_enum.rhai b/crates/languages/bevy_mod_scripting_rhai/tests/data/construct/simple_enum.rhai new file mode 100644 index 0000000000..463de1f9a7 --- /dev/null +++ b/crates/languages/bevy_mod_scripting_rhai/tests/data/construct/simple_enum.rhai @@ -0,0 +1,18 @@ +let type = world.get_type_by_name.call("SimpleEnum"); + +// Struct Variant +let constructed = construct.call(type, #{ variant: "Struct", foo: 123 }); + +assert(constructed.variant_name.call() == "Struct", "Value was constructed incorrectly, expected constructed.variant to be Struct but got " + constructed.variant_name.call()); +assert(constructed.foo == 123, "Value was constructed incorrectly, expected constructed.foo to be 123 but got " + constructed.foo); + +// TupleStruct Variant +constructed = construct.call(type, #{ variant: "TupleStruct", "_0": 123 }); + +assert(constructed.variant_name.call() == "TupleStruct", "Value was constructed incorrectly, expected constructed.variant to be TupleStruct but got " + constructed.variant_name.call()); +assert(constructed["_0"] == 123, "Value was constructed incorrectly, expected constructed._0 to be 123 but got " + constructed["_0"]); + +// Unit Variant +constructed = construct.call(type, #{ variant: "Unit" }); + +assert(constructed.variant_name.call() == "Unit", "Value was constructed incorrectly, expected constructed.variant to be Unit but got " + constructed.variant_name.call()); \ No newline at end of file diff --git a/crates/languages/bevy_mod_scripting_rhai/tests/data/construct/simple_struct.rhai b/crates/languages/bevy_mod_scripting_rhai/tests/data/construct/simple_struct.rhai new file mode 100644 index 0000000000..e35edbddd2 --- /dev/null +++ b/crates/languages/bevy_mod_scripting_rhai/tests/data/construct/simple_struct.rhai @@ -0,0 +1,4 @@ +let type = world.get_type_by_name.call("SimpleStruct"); +let constructed = construct.call(type, #{ foo: 123 }); + +assert(constructed.foo == 123, "Value was constructed incorrectly, expected constructed.foo to be 123 but got " + constructed.foo); \ No newline at end of file diff --git a/crates/languages/bevy_mod_scripting_rhai/tests/data/construct/simple_tuple_struct.rhai b/crates/languages/bevy_mod_scripting_rhai/tests/data/construct/simple_tuple_struct.rhai new file mode 100644 index 0000000000..411b3e806f --- /dev/null +++ b/crates/languages/bevy_mod_scripting_rhai/tests/data/construct/simple_tuple_struct.rhai @@ -0,0 +1,4 @@ +let type = world.get_type_by_name.call("SimpleTupleStruct"); +let constructed = construct.call(type, #{ "_0": 123 }); + +assert(constructed["_0"] == 123, "Value was constructed incorrectly, expected constructed.foo to be 123 but got " + constructed["_0"]); \ No newline at end of file diff --git a/crates/testing_crates/test_utils/src/test_data.rs b/crates/testing_crates/test_utils/src/test_data.rs index d12305d1ed..b1bf0341e4 100644 --- a/crates/testing_crates/test_utils/src/test_data.rs +++ b/crates/testing_crates/test_utils/src/test_data.rs @@ -139,6 +139,45 @@ impl TestResourceWithVariousFields { } } +#[derive(Component, Reflect, PartialEq, Eq, Debug, Default)] +#[reflect(Component, Default)] +pub struct SimpleStruct { + pub foo: usize, +} + +impl SimpleStruct { + pub fn init() -> Self { + Self { foo: 42 } + } +} + +#[derive(Component, Reflect, PartialEq, Eq, Debug, Default)] +#[reflect(Component, Default)] +pub struct SimpleTupleStruct(pub usize); + +impl SimpleTupleStruct { + pub fn init() -> Self { + Self(42) + } +} + +#[derive(Component, Reflect, PartialEq, Eq, Debug, Default)] +#[reflect(Component, Default)] +pub enum SimpleEnum { + Struct { + foo: usize, + }, + TupleStruct(usize), + #[default] + Unit, +} + +impl SimpleEnum { + pub fn init() -> Self { + Self::Struct { foo: 42 } + } +} + pub(crate) const TEST_COMPONENT_ID_START: usize = 20; pub(crate) const TEST_ENTITY_ID_START: u32 = 0; @@ -182,16 +221,16 @@ macro_rules! impl_test_component_ids { world.register_component::<$comp_type>(); registry.register::<$comp_type>(); let registered_id = world.component_id::<$comp_type>().unwrap().index(); - assert_eq!(registered_id, TEST_COMPONENT_ID_START + $comp_id, "Test setup failed. Did you register components before running setup_world?"); + assert_eq!(registered_id, TEST_COMPONENT_ID_START + $comp_id, "Test setup failed. Did you register components before running setup_world?: {}", stringify!($comp_type)); let entity = world.spawn(<$comp_type>::init()).id(); - assert_eq!(entity.index(), TEST_ENTITY_ID_START + $comp_id, "Test setup failed. Did you spawn entities before running setup_world?"); - assert_eq!(entity.generation(), 1, "Test setup failed. Did you spawn entities before running setup_world?"); + assert_eq!(entity.index(), TEST_ENTITY_ID_START + $comp_id, "Test setup failed. Did you spawn entities before running setup_world?: {}", stringify!($comp_type)); + assert_eq!(entity.generation(), 1, "Test setup failed. Did you spawn entities before running setup_world?: {}", stringify!($comp_type)); )* $( world.insert_resource::<$res_type>(<$res_type>::init()); registry.register::<$res_type>(); let registered_id = world.resource_id::<$res_type>().unwrap().index(); - assert_eq!(registered_id, TEST_COMPONENT_ID_START + $res_id, "Test setup failed. Did you register components before running setup_world?"); + assert_eq!(registered_id, TEST_COMPONENT_ID_START + $res_id, "Test setup failed. Did you register components before running setup_world?: {}", stringify!($res_type)); )* } @@ -216,12 +255,15 @@ impl_test_component_ids!( CompWithFromWorld => 1, CompWithDefault => 2, CompWithDefaultAndComponentData => 3, - CompWithFromWorldAndComponentData => 4 + CompWithFromWorldAndComponentData => 4, + SimpleStruct => 5, + SimpleTupleStruct => 6, + SimpleEnum => 7, ], [ - TestResource => 5, - ResourceWithDefault => 6, - TestResourceWithVariousFields => 7, + TestResource => 8, + ResourceWithDefault => 9, + TestResourceWithVariousFields => 10, ] );