Skip to content

feat: ✨ Dynamic Script Components, register_new_component binding, remove_component no longer requires ReflectComponent data #379

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Mar 21, 2025
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
local NewComponent = world.register_new_component("ScriptComponentA")
assert(NewComponent ~= nil, "Failed to register new component")
assert(NewComponent:short_name() == "ScriptComponent", "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")
17 changes: 17 additions & 0 deletions assets/tests/register_new_component/new_component_can_be_set.lua
Original file line number Diff line number Diff line change
@@ -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.ScriptComponent, {
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
11 changes: 6 additions & 5 deletions crates/bevy_mod_scripting_core/src/bindings/function/from_ref.rs
Original file line number Diff line number Diff line change
@@ -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`].
///
Expand Down Expand Up @@ -56,6 +56,7 @@ impl FromScriptRef for Box<dyn PartialReflect> {
tq : String => return <String>::from_script(value, world).map(|a| Box::new(a) as _),
tr : PathBuf => return <PathBuf>::from_script(value, world).map(|a| Box::new(a) as _),
ts : OsString=> return <OsString>::from_script(value, world).map(|a| Box::new(a) as _),
tsv: ScriptValue => return <ScriptValue>::from_script(value, world).map(|a| Box::new(a) as _),
tn : () => return <()>::from_script(value, world).map(|a| Box::new(a) as _)
}
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
);
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_mod_scripting_core/src/bindings/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ crate::private::export_all_in_modules! {
script_system,
script_value,
world,
script_component,
type_data
}
76 changes: 74 additions & 2 deletions crates/bevy_mod_scripting_core/src/bindings/query.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
//! Utilities for querying the world.

use super::{with_global_access, ReflectReference, WorldAccessGuard};
use super::{with_global_access, ReflectReference, ScriptComponent, 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`.
///
Expand All @@ -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)]
Expand Down Expand Up @@ -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::<ScriptComponent>(),
registration,
component_id,
}
Expand All @@ -120,6 +128,70 @@ impl ScriptComponentRegistration {
pub fn into_type_registration(self) -> ScriptTypeRegistration {
self.registration
}

/// 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<dyn Reflect>,
) -> 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::<ScriptComponent>().map_err(|v| {
InteropError::type_mismatch(TypeId::of::<ScriptComponent>(), 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 ScriptComponent).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::<ReflectComponent>()
.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(), &registry);
}
Ok(())
})?
}
}
}

impl std::fmt::Debug for ScriptTypeRegistration {
Expand Down
171 changes: 171 additions & 0 deletions crates/bevy_mod_scripting_core/src/bindings/script_component.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
//! 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, with script set
#[derive(Reflect, Clone, Default)]
#[reflect(Default)]
pub struct ScriptComponent {
data: ScriptValue,
}

/// Some metadata about dynamic script components
pub struct ScriptComponentInfo {
/// The name of the component
pub name: String,
/// The type registration for the component
pub registration: ScriptComponentRegistration,
}

impl Component for ScriptComponent {
const STORAGE_TYPE: StorageType = StorageType::Table;
}

/// A registry of dynamically registered script components
#[derive(Clone, Resource, Default)]
pub struct AppScriptComponentRegistry(pub Arc<RwLock<ScriptComponentRegistry>>);

impl AppScriptComponentRegistry {
/// Reads the underlying registry
pub fn read(&self) -> parking_lot::RwLockReadGuard<ScriptComponentRegistry> {
self.0.read()
}

/// Writes to the underlying registry
pub fn write(&self) -> parking_lot::RwLockWriteGuard<ScriptComponentRegistry> {
self.0.write()
}
}

#[derive(Default)]
/// A registry of dynamically registered script components
pub struct ScriptComponentRegistry {
components: HashMap<String, ScriptComponentInfo>,
}

impl ScriptComponentRegistry {
/// Registers a dynamic script component, possibly overwriting an existing one
pub fn register(&mut self, info: ScriptComponentInfo) {
self.components.insert(info.name.clone(), info);
}

/// Gets a dynamic script component by name
pub fn get(&self, name: &str) -> Option<&ScriptComponentInfo> {
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<ScriptComponentRegistration, InteropError> {
if !component_name.starts_with("Script") {
return Err(InteropError::unsupported_operation(
None,
None,
"script registered component name must start with 'Script'",
));
}
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(),
ScriptComponent::STORAGE_TYPE,
Layout::new::<ScriptComponent>(),
needs_drop::<ScriptComponent>().then_some(|x| x.drop_as::<ScriptComponent>()),
)
};
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(
<ScriptComponent as GetTypeRegistration>::get_type_registration(),
)),
component_id,
);

let component_info = ScriptComponentInfo {
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::<AppScriptComponentRegistry>()
.register_type::<ScriptComponent>();
}
}

#[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::<AppScriptComponentRegistry>().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");
}
}
Loading
Loading