Skip to content
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