diff --git a/.github/workflows/mdbook.yml b/.github/workflows/mdbook.yml index ab68716504..2d6f1489c5 100644 --- a/.github/workflows/mdbook.yml +++ b/.github/workflows/mdbook.yml @@ -9,6 +9,7 @@ on: - 'docs/**' - 'crates/xtask/**' - '.github/workflows/mdbook.yml' + - 'crates/bevy_mod_scripting_functions/**' pull_request: branches: - "**" @@ -16,6 +17,7 @@ on: - 'docs/**' - 'crates/xtask/**' - '.github/workflows/mdbook.yml' + - 'crates/bevy_mod_scripting_functions/**' jobs: diff --git a/assets/tests/has_component/dynamic_component.lua b/assets/tests/has_component/dynamic_component.lua new file mode 100644 index 0000000000..fc197bb2db --- /dev/null +++ b/assets/tests/has_component/dynamic_component.lua @@ -0,0 +1,6 @@ +local NewComponent = world.register_new_component("ScriptComponentA") +local entity = world.spawn() + +assert(world.has_component(entity, NewComponent) == false, "Entity should not have component") +world.add_default_component(entity, NewComponent) +assert(world.has_component(entity, NewComponent) == true, "Entity should have component") \ No newline at end of file diff --git a/assets/tests/has_component/dynamic_component.rhai b/assets/tests/has_component/dynamic_component.rhai new file mode 100644 index 0000000000..0c628ccaa4 --- /dev/null +++ b/assets/tests/has_component/dynamic_component.rhai @@ -0,0 +1,6 @@ +let NewComponent = world.register_new_component.call("ScriptComponentA"); +let entity = world.spawn_.call(); + +assert(world.has_component.call(entity, NewComponent) == false, "Entity should not have component"); +world.add_default_component.call(entity, NewComponent); +assert(world.has_component.call(entity, NewComponent) == true, "Entity should have component"); \ No newline at end of file diff --git a/assets/tests/register_new_component/new_component_can_be_retrieved.lua b/assets/tests/register_new_component/new_component_can_be_retrieved.lua new file mode 100644 index 0000000000..30e66b30ce --- /dev/null +++ b/assets/tests/register_new_component/new_component_can_be_retrieved.lua @@ -0,0 +1,12 @@ +local NewComponent = world.register_new_component("ScriptComponentA") +assert(NewComponent ~= nil, "Failed to register new component") +assert(NewComponent:short_name() == "DynamicComponent", "Unexpected component type") + + +local new_entity = world.spawn() + +world.add_default_component(new_entity, NewComponent) + +local component_intance = world.get_component(new_entity, NewComponent) + +assert(component_intance ~= nil, "Failed to get component instance") \ No newline at end of file diff --git a/assets/tests/register_new_component/new_component_can_be_set.lua b/assets/tests/register_new_component/new_component_can_be_set.lua new file mode 100644 index 0000000000..fe4c4458ba --- /dev/null +++ b/assets/tests/register_new_component/new_component_can_be_set.lua @@ -0,0 +1,17 @@ +function on_test() + local NewComponent = world.register_new_component("ScriptComponentA") + + local new_entity = world.spawn() + world.insert_component(new_entity, NewComponent, construct(types.DynamicComponent, { + data = "Hello World" + })) + + local component_instance = world.get_component(new_entity, NewComponent) + assert(component_instance.data == "Hello World", "unexpected value: " .. component_instance.data) + + component_instance.data = { + foo = "bar" + } + + assert(component_instance.data.foo == "bar", "unexpected value: " .. component_instance.data.foo) +end \ No newline at end of file diff --git a/assets/tests/remove_component/can_remove_dynamic_component.lua b/assets/tests/remove_component/can_remove_dynamic_component.lua new file mode 100644 index 0000000000..d7eb6204af --- /dev/null +++ b/assets/tests/remove_component/can_remove_dynamic_component.lua @@ -0,0 +1,11 @@ +local NewComponent = world.register_new_component("ScriptComponentA") +local new_entity = world.spawn() +world.add_default_component(new_entity, NewComponent) + +local component_instance = world.get_component(new_entity, NewComponent) +assert(component_instance ~= nil, "unexpected value: " .. tostring(component_instance.data)) + +world.remove_component(new_entity, NewComponent) +local component_instance = world.get_component(new_entity, NewComponent) + +assert(component_instance == nil, "unexpected value: " .. tostring(component_instance)) diff --git a/assets/tests/remove_component/can_remove_dynamic_component.rhai b/assets/tests/remove_component/can_remove_dynamic_component.rhai new file mode 100644 index 0000000000..a7ad664d65 --- /dev/null +++ b/assets/tests/remove_component/can_remove_dynamic_component.rhai @@ -0,0 +1,11 @@ +let NewComponent = world.register_new_component.call("ScriptComponentA"); +let new_entity = world.spawn_.call(); +world.add_default_component.call(new_entity, NewComponent); + +let component_instance = world.get_component.call(new_entity, NewComponent); +assert(type_of(component_instance) != "()", "unexpected value: " + component_instance.data); + +world.remove_component.call(new_entity, NewComponent); +let component_instance_after = world.get_component.call(new_entity, NewComponent); + +assert(type_of(component_instance_after) == "()", "unexpected value: " + component_instance_after); \ No newline at end of file diff --git a/assets/tests/remove_component/no_component_data.lua b/assets/tests/remove_component/no_component_data.lua new file mode 100644 index 0000000000..c473ca055c --- /dev/null +++ b/assets/tests/remove_component/no_component_data.lua @@ -0,0 +1,5 @@ + +local entity = world._get_entity_with_test_component("CompWithDefault") +local component = world.get_type_by_name("CompWithDefault") +world.remove_component(entity, component) +assert(world.has_component(entity, component) == false, "Component was not removed") diff --git a/assets/tests/remove_component/no_component_data_errors.rhai b/assets/tests/remove_component/no_component_data.rhai similarity index 50% rename from assets/tests/remove_component/no_component_data_errors.rhai rename to assets/tests/remove_component/no_component_data.rhai index 07f8f714a8..bf70543716 100644 --- a/assets/tests/remove_component/no_component_data_errors.rhai +++ b/assets/tests/remove_component/no_component_data.rhai @@ -2,6 +2,5 @@ let entity = world._get_entity_with_test_component.call("CompWithDefault"); let component = world.get_type_by_name.call("CompWithDefault"); -assert_throws(||{ - world.remove_component.call(entity, component); -}, "Missing type data ReflectComponent for type: .*CompWithDefault.*") +world.remove_component.call(entity, component); +assert(world.has_component.call(entity, component) == false, "Component was not removed"); \ No newline at end of file diff --git a/assets/tests/remove_component/no_component_data_errors.lua b/assets/tests/remove_component/no_component_data_errors.lua deleted file mode 100644 index 0dc5c2d780..0000000000 --- a/assets/tests/remove_component/no_component_data_errors.lua +++ /dev/null @@ -1,7 +0,0 @@ - -local entity = world._get_entity_with_test_component("CompWithDefault") -local component = world.get_type_by_name("CompWithDefault") - -assert_throws(function () - world.remove_component(entity, component) -end, "Missing type data ReflectComponent for type: .*CompWithDefault.*") diff --git a/crates/bevy_mod_scripting_core/src/bindings/function/from_ref.rs b/crates/bevy_mod_scripting_core/src/bindings/function/from_ref.rs index bf43ccf5a6..3308392750 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/function/from_ref.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/function/from_ref.rs @@ -1,15 +1,15 @@ //! Contains the [`FromScriptRef`] trait and its implementations. -use std::{any::TypeId, ffi::OsString, path::PathBuf}; -use bevy::reflect::{ - DynamicEnum, DynamicList, DynamicMap, DynamicTuple, DynamicVariant, Map, PartialReflect, -}; use crate::{ - bindings::{match_by_type, WorldGuard, FromScript}, + bindings::{match_by_type, FromScript, WorldGuard}, error::InteropError, reflection_extensions::TypeInfoExtensions, ScriptValue, }; +use bevy::reflect::{ + DynamicEnum, DynamicList, DynamicMap, DynamicTuple, DynamicVariant, Map, PartialReflect, +}; +use std::{any::TypeId, ffi::OsString, path::PathBuf}; /// Converts from a [`ScriptValue`] to a value equivalent to the given [`TypeId`]. /// @@ -56,6 +56,7 @@ impl FromScriptRef for Box { tq : String => return ::from_script(value, world).map(|a| Box::new(a) as _), tr : PathBuf => return ::from_script(value, world).map(|a| Box::new(a) as _), ts : OsString=> return ::from_script(value, world).map(|a| Box::new(a) as _), + tsv: ScriptValue => return ::from_script(value, world).map(|a| Box::new(a) as _), tn : () => return <()>::from_script(value, world).map(|a| Box::new(a) as _) } ); diff --git a/crates/bevy_mod_scripting_core/src/bindings/function/into_ref.rs b/crates/bevy_mod_scripting_core/src/bindings/function/into_ref.rs index 3679c784a2..bdce9f9630 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/function/into_ref.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/function/into_ref.rs @@ -102,6 +102,7 @@ fn into_script_ref( }, tr : PathBuf => return downcast_into_value!(r, PathBuf).clone().into_script(world), ts : OsString=> return downcast_into_value!(r, OsString).clone().into_script(world), + tsv: ScriptValue=> return Ok(downcast_into_value!(r, ScriptValue).clone()), tn : () => return Ok(ScriptValue::Unit) } ); diff --git a/crates/bevy_mod_scripting_core/src/bindings/globals/core.rs b/crates/bevy_mod_scripting_core/src/bindings/globals/core.rs index 5dee9ee92e..202ae6ea70 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/globals/core.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/globals/core.rs @@ -69,6 +69,8 @@ impl CoreGlobals { /// A cache of types normally available through the `world.get_type_by_name` function. /// /// You can use this to avoid having to store type references. + /// + /// Note that this cache will NOT contain types manually registered by scripts via `register_new_component`. fn types( guard: WorldGuard, ) -> Result< diff --git a/crates/bevy_mod_scripting_core/src/bindings/mod.rs b/crates/bevy_mod_scripting_core/src/bindings/mod.rs index 6cfb01fc35..d7402ec681 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/mod.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/mod.rs @@ -12,5 +12,6 @@ crate::private::export_all_in_modules! { script_system, script_value, world, + script_component, type_data } diff --git a/crates/bevy_mod_scripting_core/src/bindings/query.rs b/crates/bevy_mod_scripting_core/src/bindings/query.rs index 648b71bd38..33e4ce3c02 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/query.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/query.rs @@ -1,18 +1,20 @@ //! Utilities for querying the world. -use super::{with_global_access, ReflectReference, WorldAccessGuard}; +use super::{with_global_access, DynamicComponent, ReflectReference, WorldAccessGuard, WorldGuard}; use crate::error::InteropError; use bevy::{ ecs::{ component::ComponentId, entity::Entity, query::{QueryData, QueryState}, + reflect::ReflectComponent, world::World, }, prelude::{EntityRef, QueryBuilder}, + ptr::OwningPtr, reflect::{ParsedPath, Reflect, TypeRegistration}, }; -use std::{any::TypeId, collections::VecDeque, sync::Arc}; +use std::{any::TypeId, collections::VecDeque, ptr::NonNull, sync::Arc}; /// A reference to a type which is not a `Resource` or `Component`. /// @@ -27,9 +29,13 @@ pub struct ScriptTypeRegistration { /// A reference to a component type's reflection registration. /// /// In general think of this as a handle to a type. +/// +/// Not to be confused with script registered dynamic components, although this can point to a script registered component. pub struct ScriptComponentRegistration { pub(crate) registration: ScriptTypeRegistration, pub(crate) component_id: ComponentId, + /// whether this is a component registered BY a script + pub(crate) is_dynamic_script_component: bool, } #[derive(Clone, Reflect, Debug)] @@ -100,6 +106,8 @@ impl ScriptComponentRegistration { /// Creates a new [`ScriptComponentRegistration`] from a [`ScriptTypeRegistration`] and a [`ComponentId`]. pub fn new(registration: ScriptTypeRegistration, component_id: ComponentId) -> Self { Self { + is_dynamic_script_component: registration.type_id() + == std::any::TypeId::of::(), registration, component_id, } @@ -120,6 +128,85 @@ impl ScriptComponentRegistration { pub fn into_type_registration(self) -> ScriptTypeRegistration { self.registration } + + /// Removes an instance of this component from the given entity + pub fn remove_from_entity( + &self, + world: WorldGuard, + entity: Entity, + ) -> Result<(), InteropError> { + world.with_global_access(|world| { + let mut entity = world + .get_entity_mut(entity) + .map_err(|_| InteropError::missing_entity(entity))?; + entity.remove_by_id(self.component_id); + Ok(()) + })? + } + + /// Inserts an instance of this component into the given entity + /// + /// Requires whole world access + pub fn insert_into_entity( + &self, + world: WorldGuard, + entity: Entity, + instance: Box, + ) -> Result<(), InteropError> { + if self.is_dynamic_script_component { + // if dynamic we already know the type i.e. `ScriptComponent` + // so we can just insert it + + world.with_global_access(|world| { + let mut entity = world + .get_entity_mut(entity) + .map_err(|_| InteropError::missing_entity(entity))?; + let cast = instance.downcast::().map_err(|v| { + InteropError::type_mismatch(TypeId::of::(), Some(v.type_id())) + })?; + // the reason we leak the box, is because we don't want to double drop the owning ptr + + let ptr = (Box::leak(cast) as *mut DynamicComponent).cast(); + // Safety: cannot be null as we just created it from a valid reference + let non_null_ptr = unsafe { NonNull::new_unchecked(ptr) }; + // Safety: + // - we know the type is ScriptComponent, as we just created the pointer + // - the box will stay valid for the life of this function, and we do not return the ptr + // - pointer is alligned correctly + // - nothing else will call drop on this + let owning_ptr = unsafe { OwningPtr::new(non_null_ptr) }; + // Safety: + // - Owning Ptr is valid as we just created it + // - TODO: do we need to check if ComponentId is from this world? How? + unsafe { entity.insert_by_id(self.component_id, owning_ptr) }; + Ok(()) + })? + } else { + let component_data = self + .type_registration() + .type_registration() + .data::() + .ok_or_else(|| { + InteropError::missing_type_data( + self.registration.type_id(), + "ReflectComponent".to_owned(), + ) + })?; + + // TODO: this shouldn't need entire world access it feels + let type_registry = world.type_registry(); + world.with_global_access(|world| { + let mut entity = world + .get_entity_mut(entity) + .map_err(|_| InteropError::missing_entity(entity))?; + { + let registry = type_registry.read(); + component_data.insert(&mut entity, instance.as_partial_reflect(), ®istry); + } + Ok(()) + })? + } + } } impl std::fmt::Debug for ScriptTypeRegistration { diff --git a/crates/bevy_mod_scripting_core/src/bindings/script_component.rs b/crates/bevy_mod_scripting_core/src/bindings/script_component.rs new file mode 100644 index 0000000000..b1e12f3dc3 --- /dev/null +++ b/crates/bevy_mod_scripting_core/src/bindings/script_component.rs @@ -0,0 +1,164 @@ +//! Everything necessary to support scripts registering their own components + +use super::{ScriptComponentRegistration, ScriptTypeRegistration, ScriptValue, WorldAccessGuard}; +use crate::error::InteropError; +use bevy::{ + app::{App, Plugin}, + ecs::{ + component::{Component, ComponentDescriptor, StorageType}, + system::Resource, + }, + reflect::{prelude::ReflectDefault, GetTypeRegistration, Reflect}, + utils::HashMap, +}; +use parking_lot::RwLock; +use std::{alloc::Layout, mem::needs_drop, sync::Arc}; + +/// A dynamic script component +#[derive(Reflect, Clone, Default)] +#[reflect(Default)] +pub struct DynamicComponent { + data: ScriptValue, +} + +/// Some metadata about dynamic script components +pub struct DynamicComponentInfo { + /// The name of the component + pub name: String, + /// The type registration for the component + pub registration: ScriptComponentRegistration, +} + +impl Component for DynamicComponent { + const STORAGE_TYPE: StorageType = StorageType::Table; +} + +/// A registry of dynamically registered script components +#[derive(Clone, Resource, Default)] +pub struct AppScriptComponentRegistry(pub Arc>); + +impl AppScriptComponentRegistry { + /// Reads the underlying registry + pub fn read(&self) -> parking_lot::RwLockReadGuard { + self.0.read() + } + + /// Writes to the underlying registry + pub fn write(&self) -> parking_lot::RwLockWriteGuard { + self.0.write() + } +} + +#[derive(Default)] +/// A registry of dynamically registered script components +pub struct ScriptComponentRegistry { + components: HashMap, +} + +impl ScriptComponentRegistry { + /// Registers a dynamic script component, possibly overwriting an existing one + pub fn register(&mut self, info: DynamicComponentInfo) { + self.components.insert(info.name.clone(), info); + } + + /// Gets a dynamic script component by name + pub fn get(&self, name: &str) -> Option<&DynamicComponentInfo> { + self.components.get(name) + } +} + +impl WorldAccessGuard<'_> { + /// Registers a dynamic script component, and returns a reference to its registration + pub fn register_script_component( + &self, + component_name: String, + ) -> Result { + let component_registry = self.component_registry(); + let component_registry_read = component_registry.read(); + if component_registry_read.get(&component_name).is_some() { + return Err(InteropError::unsupported_operation( + None, + None, + "script registered component already exists", + )); + } + + let component_id = self.with_global_access(|w| { + let descriptor = unsafe { + // Safety: same safety guarantees as ComponentDescriptor::new + // we know the type in advance + // we only use this method to name the component + ComponentDescriptor::new_with_layout( + component_name.clone(), + DynamicComponent::STORAGE_TYPE, + Layout::new::(), + needs_drop::().then_some(|x| x.drop_as::()), + ) + }; + w.register_component_with_descriptor(descriptor) + })?; + drop(component_registry_read); + let mut component_registry = component_registry.write(); + + let registration = ScriptComponentRegistration::new( + ScriptTypeRegistration::new(Arc::new( + ::get_type_registration(), + )), + component_id, + ); + + let component_info = DynamicComponentInfo { + name: component_name.clone(), + registration: registration.clone(), + }; + + component_registry.register(component_info); + + // TODO: we should probably retrieve this from the registry, but I don't see what people would want to register on this type + // in addition to the existing registrations. + Ok(registration) + } +} + +/// A plugin to support dynamic script components +pub(crate) struct DynamicScriptComponentPlugin; + +impl Plugin for DynamicScriptComponentPlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .register_type::(); + } +} + +#[cfg(test)] +mod test { + use super::*; + use bevy::ecs::world::World; + + #[test] + fn test_script_component() { + let mut world = World::new(); + let registration = { + let guard = WorldAccessGuard::new_exclusive(&mut world); + + guard + .register_script_component("ScriptTest".to_string()) + .unwrap() + }; + + let registry = world.get_resource::().unwrap(); + + let registry = registry.read(); + let info = registry.get("ScriptTest").unwrap(); + assert_eq!(info.registration.component_id, registration.component_id); + assert_eq!(info.name, "ScriptTest"); + + // can get the component through the world + let component = world + .components() + .get_info(info.registration.component_id) + .unwrap(); + + assert_eq!(component.name(), "ScriptTest"); + } +} diff --git a/crates/bevy_mod_scripting_core/src/bindings/script_system.rs b/crates/bevy_mod_scripting_core/src/bindings/script_system.rs index feaf42be54..f9be183de8 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/script_system.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/script_system.rs @@ -5,8 +5,9 @@ use super::{ function::{from::Val, into::IntoScript, script_function::AppScriptFunctionRegistry}, schedule::AppScheduleRegistry, script_value::ScriptValue, - AppReflectAllocator, ReflectBaseType, ReflectReference, ScriptQueryBuilder, ScriptQueryResult, - ScriptResourceRegistration, WorldAccessGuard, WorldGuard, + AppReflectAllocator, AppScriptComponentRegistry, ReflectBaseType, ReflectReference, + ScriptQueryBuilder, ScriptQueryResult, ScriptResourceRegistration, WorldAccessGuard, + WorldGuard, }; use crate::{ bindings::pretty_print::DisplayWithWorld, @@ -288,6 +289,7 @@ struct ScriptSystemState { type_registry: AppTypeRegistry, function_registry: AppScriptFunctionRegistry, schedule_registry: AppScheduleRegistry, + component_registry: AppScriptComponentRegistry, allocator: AppReflectAllocator, subset: HashSet, callback_label: CallbackLabel, @@ -424,6 +426,7 @@ impl System for DynamicScriptSystem

{ state.allocator.clone(), state.function_registry.clone(), state.schedule_registry.clone(), + state.component_registry.clone(), ) }; @@ -577,6 +580,9 @@ impl System for DynamicScriptSystem

{ .clone(), schedule_registry: world.get_resource_or_init::().clone(), allocator: world.get_resource_or_init::().clone(), + component_registry: world + .get_resource_or_init::() + .clone(), subset, callback_label: self.name.to_string().into(), system_params, diff --git a/crates/bevy_mod_scripting_core/src/bindings/script_value.rs b/crates/bevy_mod_scripting_core/src/bindings/script_value.rs index 306d318a32..c8cbb21e48 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/script_value.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/script_value.rs @@ -13,10 +13,11 @@ use super::{ /// An abstraction of values that can be passed to and from scripts. /// This allows us to re-use logic between scripting languages. -#[derive(Debug, Clone, PartialEq, Reflect)] +#[derive(Debug, Clone, PartialEq, Reflect, Default)] #[reflect(opaque)] pub enum ScriptValue { /// Represents the absence of a value. + #[default] Unit, /// Represents a boolean value. Bool(bool), diff --git a/crates/bevy_mod_scripting_core/src/bindings/world.rs b/crates/bevy_mod_scripting_core/src/bindings/world.rs index b6ef742261..79d082a6c1 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/world.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/world.rs @@ -9,13 +9,23 @@ use super::{ access_map::{ AccessCount, AccessMapKey, AnyAccessMap, DynamicSystemMeta, ReflectAccessId, ReflectAccessKind, SubsetAccessMap, - }, function::{ + }, + function::{ namespace::Namespace, script_function::{AppScriptFunctionRegistry, DynamicScriptFunction, FunctionCallContext}, - }, pretty_print::DisplayWithWorld, schedule::AppScheduleRegistry, script_value::ScriptValue, with_global_access, AppReflectAllocator, ReflectBase, ReflectBaseType, ReflectReference, ScriptComponentRegistration, ScriptResourceRegistration, ScriptTypeRegistration, Union + }, + pretty_print::DisplayWithWorld, + schedule::AppScheduleRegistry, + script_value::ScriptValue, + with_global_access, AppReflectAllocator, AppScriptComponentRegistry, ReflectBase, + ReflectBaseType, ReflectReference, ScriptComponentRegistration, ScriptResourceRegistration, + ScriptTypeRegistration, Union, }; use crate::{ - bindings::{function::{from::FromScript, from_ref::FromScriptRef}, with_access_read, with_access_write}, + bindings::{ + function::{from::FromScript, from_ref::FromScriptRef}, + with_access_read, with_access_write, + }, error::InteropError, reflection_extensions::PartialReflectExt, }; @@ -24,7 +34,7 @@ use bevy::{ ecs::{ component::{Component, ComponentId}, entity::Entity, - reflect::{AppTypeRegistry, ReflectComponent, ReflectFromWorld, ReflectResource}, + reflect::{AppTypeRegistry, ReflectFromWorld, ReflectResource}, system::{Commands, Resource}, world::{unsafe_world_cell::UnsafeWorldCell, CommandQueue, Mut, World}, }, @@ -73,6 +83,8 @@ pub(crate) struct WorldAccessGuardInner<'w> { function_registry: AppScriptFunctionRegistry, /// The schedule registry for the world schedule_registry: AppScheduleRegistry, + /// The registry of script registered components + script_component_registry: AppScriptComponentRegistry, } impl std::fmt::Debug for WorldAccessGuardInner<'_> { @@ -159,6 +171,7 @@ impl<'w> WorldAccessGuard<'w> { allocator: AppReflectAllocator, function_registry: AppScriptFunctionRegistry, schedule_registry: AppScheduleRegistry, + script_component_registry: AppScriptComponentRegistry, ) -> Self { Self { inner: Rc::new(WorldAccessGuardInner { @@ -172,6 +185,7 @@ impl<'w> WorldAccessGuard<'w> { allocator, function_registry, schedule_registry, + script_component_registry, }), invalid: Rc::new(false.into()), } @@ -194,6 +208,10 @@ impl<'w> WorldAccessGuard<'w> { .get_resource_or_init::() .clone(); + let script_component_registry = world + .get_resource_or_init::() + .clone(); + let schedule_registry = world.get_resource_or_init::().clone(); Self { inner: Rc::new(WorldAccessGuardInner { @@ -203,6 +221,7 @@ impl<'w> WorldAccessGuard<'w> { type_registry, function_registry, schedule_registry, + script_component_registry, }), invalid: Rc::new(false.into()), } @@ -318,6 +337,11 @@ impl<'w> WorldAccessGuard<'w> { self.inner.schedule_registry.clone() } + /// Returns the component registry for the world + pub fn component_registry(&self) -> AppScriptComponentRegistry { + self.inner.script_component_registry.clone() + } + /// Returns the script allocator for the world pub fn allocator(&self) -> AppReflectAllocator { self.inner.allocator.clone() @@ -542,7 +566,6 @@ 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>, @@ -802,18 +825,26 @@ impl WorldAccessGuard<'_> { } /// get a type registration for the type, without checking if it's a component or resource - pub fn get_type_by_name(&self, type_name: String) -> Option { + pub fn get_type_by_name(&self, type_name: &str) -> Option { let type_registry = self.type_registry(); let type_registry = type_registry.read(); type_registry - .get_with_short_type_path(&type_name) - .or_else(|| type_registry.get_with_type_path(&type_name)) + .get_with_short_type_path(type_name) + .or_else(|| type_registry.get_with_type_path(type_name)) .map(|registration| ScriptTypeRegistration::new(Arc::new(registration.clone()))) } /// get a type erased type registration for the type including information about whether it's a component or resource - pub(crate) fn get_type_registration(&self, registration: ScriptTypeRegistration) -> Result>, InteropError> { - + pub(crate) fn get_type_registration( + &self, + registration: ScriptTypeRegistration, + ) -> Result< + Union< + ScriptTypeRegistration, + Union, + >, + InteropError, + > { let registration = match self.get_resource_type(registration)? { Ok(res) => { return Ok(Union::new_right(Union::new_right(res))); @@ -831,15 +862,31 @@ impl WorldAccessGuard<'_> { Ok(Union::new_left(registration)) } - /// Similar to [`Self::get_type_by_name`] but returns a type erased [`ScriptTypeRegistration`], [`ScriptComponentRegistration`] or [`ScriptResourceRegistration`] + /// Similar to [`Self::get_type_by_name`] but returns a type erased [`ScriptTypeRegistration`], [`ScriptComponentRegistration`] or [`ScriptResourceRegistration`] /// depending on the underlying type and state of the world. - pub fn get_type_registration_by_name(&self, type_name: String) -> Result>>, InteropError> { - let val = self.get_type_by_name(type_name); + pub fn get_type_registration_by_name( + &self, + type_name: String, + ) -> Result< + Option< + Union< + ScriptTypeRegistration, + Union, + >, + >, + InteropError, + > { + let val = self.get_type_by_name(&type_name); Ok(match val { - Some(registration) => { - Some(self.get_type_registration(registration)?) + Some(registration) => Some(self.get_type_registration(registration)?), + None => { + // try the component registry + let components = self.component_registry(); + let components = components.read(); + components + .get(&type_name) + .map(|c| Union::new_right(Union::new_left(c.registration.clone()))) } - None => None, }) } @@ -881,18 +928,6 @@ impl WorldAccessGuard<'_> { entity: Entity, registration: ScriptComponentRegistration, ) -> Result<(), InteropError> { - // let cell = self.as_unsafe_world_cell()?; - let component_data = registration - .type_registration() - .type_registration() - .data::() - .ok_or_else(|| { - InteropError::missing_type_data( - registration.registration.type_id(), - "ReflectComponent".to_owned(), - ) - })?; - // we look for ReflectDefault or ReflectFromWorld data then a ReflectComponent data let instance = if let Some(default_td) = registration .type_registration() @@ -913,19 +948,7 @@ impl WorldAccessGuard<'_> { )); }; - // TODO: this shouldn't need entire world access it feels - self.with_global_access(|world| { - let type_registry = self.type_registry(); - - let mut entity = world - .get_entity_mut(entity) - .map_err(|_| InteropError::missing_entity(entity))?; - { - let registry = type_registry.read(); - component_data.insert(&mut entity, instance.as_partial_reflect(), ®istry); - } - Ok(()) - })? + registration.insert_into_entity(self.clone(), entity, instance) } /// insert the component into the entity @@ -935,63 +958,41 @@ impl WorldAccessGuard<'_> { registration: ScriptComponentRegistration, value: ReflectReference, ) -> Result<(), InteropError> { - let component_data = registration - .type_registration() - .type_registration() - .data::() - .ok_or_else(|| { - InteropError::missing_type_data( - registration.registration.type_id(), - "ReflectComponent".to_owned(), - ) - })?; - - with_global_access!(&self.inner.accesses, "Could not insert element", { - let cell = self.as_unsafe_world_cell()?; - let type_registry = self.type_registry(); - let type_registry = type_registry.read(); - let world_mut = unsafe { cell.world_mut() }; - let mut entity = world_mut - .get_entity_mut(entity) - .map_err(|_| InteropError::missing_entity(entity))?; + let instance = >::from_script_ref( + registration.type_registration().type_id(), + ScriptValue::Reference(value), + self.clone(), + )?; - let ref_ = unsafe { value.reflect_unsafe(self.clone())? }; - component_data.apply_or_insert(&mut entity, ref_, &type_registry); + let reflect = instance.try_into_reflect().map_err(|v| { + InteropError::failed_from_reflect( + Some(registration.type_registration().type_id()), + format!("instance produced by conversion to target type when inserting component is not a full reflect type: {v:?}"), + ) + })?; - Ok(()) - })? + registration.insert_into_entity(self.clone(), entity, reflect) } /// get the component from the entity pub fn get_component( &self, entity: Entity, - component_id: ComponentId, + component_registration: ScriptComponentRegistration, ) -> Result, InteropError> { let cell = self.as_unsafe_world_cell()?; let entity = cell .get_entity(entity) .ok_or_else(|| InteropError::missing_entity(entity))?; - let component_info = cell - .components() - .get_info(component_id) - .ok_or_else(|| InteropError::invalid_component(component_id))?; - - if entity.contains_id(component_id) { + if entity.contains_id(component_registration.component_id) { Ok(Some(ReflectReference { base: ReflectBaseType { - type_id: component_info.type_id().ok_or_else(|| { - InteropError::unsupported_operation( - None, - None, - format!( - "Component {} does not have a type id. Such components are not supported by BMS.", - component_id.display_without_world() - ), - ) - })?, - base_id: ReflectBase::Component(entity.id(), component_id), + type_id: component_registration.type_registration().type_id(), + base_id: ReflectBase::Component( + entity.id(), + component_registration.component_id, + ), }, reflect_path: ParsedPath(vec![]), })) @@ -1020,25 +1021,7 @@ impl WorldAccessGuard<'_> { entity: Entity, registration: ScriptComponentRegistration, ) -> Result<(), InteropError> { - let component_data = registration - .type_registration() - .type_registration() - .data::() - .ok_or_else(|| { - InteropError::missing_type_data( - registration.registration.type_id(), - "ReflectComponent".to_owned(), - ) - })?; - - // TODO: this shouldn't need entire world access it feels - self.with_global_access(|world| { - let mut entity = world - .get_entity_mut(entity) - .map_err(|_| InteropError::missing_entity(entity))?; - component_data.remove(&mut entity); - Ok(()) - })? + registration.remove_from_entity(self.clone(), entity) } /// get the given resource diff --git a/crates/bevy_mod_scripting_core/src/error.rs b/crates/bevy_mod_scripting_core/src/error.rs index 1c63c64bd0..3e2a104c57 100644 --- a/crates/bevy_mod_scripting_core/src/error.rs +++ b/crates/bevy_mod_scripting_core/src/error.rs @@ -401,10 +401,10 @@ impl InteropError { /// Thrown if a type cannot be converted from reflect, this can happen if the type was unable to /// re-construct itself from a dynamic value. - pub fn failed_from_reflect(type_id: Option, reason: String) -> Self { + pub fn failed_from_reflect(type_id: Option, reason: impl Into) -> Self { Self(Arc::new(InteropErrorInner::FailedFromReflect { type_id, - reason, + reason: reason.into(), })) } diff --git a/crates/bevy_mod_scripting_core/src/lib.rs b/crates/bevy_mod_scripting_core/src/lib.rs index de21ffed62..a244130b61 100644 --- a/crates/bevy_mod_scripting_core/src/lib.rs +++ b/crates/bevy_mod_scripting_core/src/lib.rs @@ -14,7 +14,8 @@ use bindings::{ globals::{core::CoreScriptGlobalsPlugin, AppScriptGlobalsRegistry}, schedule::AppScheduleRegistry, script_value::ScriptValue, - AppReflectAllocator, ReflectAllocator, ReflectReference, ScriptTypeRegistration, + AppReflectAllocator, DynamicScriptComponentPlugin, ReflectAllocator, ReflectReference, + ScriptTypeRegistration, }; use commands::{AddStaticScript, RemoveStaticScript}; use context::{ @@ -312,7 +313,7 @@ fn once_per_app_init(app: &mut App) { ((garbage_collector).in_set(ScriptingSystemSet::GarbageCollection),), ); - app.add_plugins(CoreScriptGlobalsPlugin); + app.add_plugins((CoreScriptGlobalsPlugin, DynamicScriptComponentPlugin)); configure_asset_systems(app); } diff --git a/crates/bevy_mod_scripting_functions/src/core.rs b/crates/bevy_mod_scripting_functions/src/core.rs index bcac621f01..a3689932ca 100644 --- a/crates/bevy_mod_scripting_functions/src/core.rs +++ b/crates/bevy_mod_scripting_functions/src/core.rs @@ -116,7 +116,7 @@ impl World { ) -> Result, InteropError> { profiling::function_scope!("get_component"); let world = ctxt.world()?; - let val = world.get_component(*entity, registration.component_id())?; + let val = world.get_component(*entity, registration.into_inner())?; Ok(val) } @@ -445,6 +445,28 @@ impl World { let world = ctxt.world()?; world.exit() } + + /// Registers a new component type with the world. + /// + /// The component will behave like any other native component for all intents and purposes. + /// The type that will be instantiated to back this component will be `DynamicComponent` which contains just one field: + /// - `data` + /// + /// This field can be set to any value and modified freely. + /// + /// Arguments: + /// * `ctxt`: The function call context. + /// * `name`: The name of the component type + /// Returns: + /// * `registration`: The registration of the new component type if successful. + fn register_new_component( + ctxt: FunctionCallContext, + name: String, + ) -> Result, InteropError> { + profiling::function_scope!("register_new_component"); + let world = ctxt.world()?; + world.register_script_component(name).map(Val) + } } #[script_bindings( @@ -1272,7 +1294,7 @@ impl GlobalNamespace { let reflect_val = val.try_into_reflect().map_err(|_| { InteropError::failed_from_reflect( Some(registration.type_id()), - "Could not construct the type".into(), + "Could not construct the type", ) })?; diff --git a/crates/languages/bevy_mod_scripting_lua/tests/lua_tests.rs b/crates/languages/bevy_mod_scripting_lua/tests/lua_tests.rs index 3fa8b4b75d..cacfd11bb6 100644 --- a/crates/languages/bevy_mod_scripting_lua/tests/lua_tests.rs +++ b/crates/languages/bevy_mod_scripting_lua/tests/lua_tests.rs @@ -14,12 +14,15 @@ use std::{ path::{Path, PathBuf}, }; +#[derive(Debug)] struct Test { path: PathBuf, } impl Test { fn execute(self) -> Result<(), Failed> { + println!("Running test: {:?}", self.path); + execute_integration_test::( |world, type_registry| { let _ = world; @@ -126,10 +129,11 @@ fn main() { let args = Arguments::from_args(); // Create a list of tests and/or benchmarks (in this case: two dummy tests). - let tests = discover_all_tests() + let all_tests = discover_all_tests(); + println!("discovered {} tests. {:?}", all_tests.len(), all_tests); + let tests = all_tests .into_iter() .map(|t| Trial::test(t.name(), move || t.execute())); - // Run all tests and exit the application appropriatly. libtest_mimic::run(&args, tests.collect()).exit(); }