Skip to content

Commit a6a989c

Browse files
authored
feat: ✨ Dynamic Script Components, register_new_component binding, remove_component no longer requires ReflectComponent data (#379)
# Summary In order to support dynamic script components we need: - to relax some requirements on having a `ReflectComponent` registration to allow component operations - add a script component registry, which stores metadata about each dynamically registered component - Add `ScriptValue` to `into_script_ref` and `from_script_ref` implementations to allow any value to be inserted into the components payload The nice thing about this implementation is it *just* works with the reference system. The current problems are that the global type cache will not reflect this script component, to fix that we'd need a mechanism for "re-computing" certain globals for scripts. This also does not allow setting a schema on these, which is handy but also footgunny As a side effect I've also made `remove_component` no longer require the `ReflectComponent` registration
1 parent e1d17f6 commit a6a989c

File tree

23 files changed

+464
-129
lines changed

23 files changed

+464
-129
lines changed

.github/workflows/mdbook.yml

+2
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ on:
99
- 'docs/**'
1010
- 'crates/xtask/**'
1111
- '.github/workflows/mdbook.yml'
12+
- 'crates/bevy_mod_scripting_functions/**'
1213
pull_request:
1314
branches:
1415
- "**"
1516
paths:
1617
- 'docs/**'
1718
- 'crates/xtask/**'
1819
- '.github/workflows/mdbook.yml'
20+
- 'crates/bevy_mod_scripting_functions/**'
1921

2022
jobs:
2123

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
local NewComponent = world.register_new_component("ScriptComponentA")
2+
local entity = world.spawn()
3+
4+
assert(world.has_component(entity, NewComponent) == false, "Entity should not have component")
5+
world.add_default_component(entity, NewComponent)
6+
assert(world.has_component(entity, NewComponent) == true, "Entity should have component")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
let NewComponent = world.register_new_component.call("ScriptComponentA");
2+
let entity = world.spawn_.call();
3+
4+
assert(world.has_component.call(entity, NewComponent) == false, "Entity should not have component");
5+
world.add_default_component.call(entity, NewComponent);
6+
assert(world.has_component.call(entity, NewComponent) == true, "Entity should have component");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
local NewComponent = world.register_new_component("ScriptComponentA")
2+
assert(NewComponent ~= nil, "Failed to register new component")
3+
assert(NewComponent:short_name() == "DynamicComponent", "Unexpected component type")
4+
5+
6+
local new_entity = world.spawn()
7+
8+
world.add_default_component(new_entity, NewComponent)
9+
10+
local component_intance = world.get_component(new_entity, NewComponent)
11+
12+
assert(component_intance ~= nil, "Failed to get component instance")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
function on_test()
2+
local NewComponent = world.register_new_component("ScriptComponentA")
3+
4+
local new_entity = world.spawn()
5+
world.insert_component(new_entity, NewComponent, construct(types.DynamicComponent, {
6+
data = "Hello World"
7+
}))
8+
9+
local component_instance = world.get_component(new_entity, NewComponent)
10+
assert(component_instance.data == "Hello World", "unexpected value: " .. component_instance.data)
11+
12+
component_instance.data = {
13+
foo = "bar"
14+
}
15+
16+
assert(component_instance.data.foo == "bar", "unexpected value: " .. component_instance.data.foo)
17+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
local NewComponent = world.register_new_component("ScriptComponentA")
2+
local new_entity = world.spawn()
3+
world.add_default_component(new_entity, NewComponent)
4+
5+
local component_instance = world.get_component(new_entity, NewComponent)
6+
assert(component_instance ~= nil, "unexpected value: " .. tostring(component_instance.data))
7+
8+
world.remove_component(new_entity, NewComponent)
9+
local component_instance = world.get_component(new_entity, NewComponent)
10+
11+
assert(component_instance == nil, "unexpected value: " .. tostring(component_instance))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
let NewComponent = world.register_new_component.call("ScriptComponentA");
2+
let new_entity = world.spawn_.call();
3+
world.add_default_component.call(new_entity, NewComponent);
4+
5+
let component_instance = world.get_component.call(new_entity, NewComponent);
6+
assert(type_of(component_instance) != "()", "unexpected value: " + component_instance.data);
7+
8+
world.remove_component.call(new_entity, NewComponent);
9+
let component_instance_after = world.get_component.call(new_entity, NewComponent);
10+
11+
assert(type_of(component_instance_after) == "()", "unexpected value: " + component_instance_after);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
2+
local entity = world._get_entity_with_test_component("CompWithDefault")
3+
local component = world.get_type_by_name("CompWithDefault")
4+
world.remove_component(entity, component)
5+
assert(world.has_component(entity, component) == false, "Component was not removed")

assets/tests/remove_component/no_component_data_errors.rhai renamed to assets/tests/remove_component/no_component_data.rhai

+2-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,5 @@
22
let entity = world._get_entity_with_test_component.call("CompWithDefault");
33
let component = world.get_type_by_name.call("CompWithDefault");
44

5-
assert_throws(||{
6-
world.remove_component.call(entity, component);
7-
}, "Missing type data ReflectComponent for type: .*CompWithDefault.*")
5+
world.remove_component.call(entity, component);
6+
assert(world.has_component.call(entity, component) == false, "Component was not removed");

assets/tests/remove_component/no_component_data_errors.lua

-7
This file was deleted.

crates/bevy_mod_scripting_core/src/bindings/function/from_ref.rs

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
//! Contains the [`FromScriptRef`] trait and its implementations.
22
3-
use std::{any::TypeId, ffi::OsString, path::PathBuf};
4-
use bevy::reflect::{
5-
DynamicEnum, DynamicList, DynamicMap, DynamicTuple, DynamicVariant, Map, PartialReflect,
6-
};
73
use crate::{
8-
bindings::{match_by_type, WorldGuard, FromScript},
4+
bindings::{match_by_type, FromScript, WorldGuard},
95
error::InteropError,
106
reflection_extensions::TypeInfoExtensions,
117
ScriptValue,
128
};
9+
use bevy::reflect::{
10+
DynamicEnum, DynamicList, DynamicMap, DynamicTuple, DynamicVariant, Map, PartialReflect,
11+
};
12+
use std::{any::TypeId, ffi::OsString, path::PathBuf};
1313

1414
/// Converts from a [`ScriptValue`] to a value equivalent to the given [`TypeId`].
1515
///
@@ -56,6 +56,7 @@ impl FromScriptRef for Box<dyn PartialReflect> {
5656
tq : String => return <String>::from_script(value, world).map(|a| Box::new(a) as _),
5757
tr : PathBuf => return <PathBuf>::from_script(value, world).map(|a| Box::new(a) as _),
5858
ts : OsString=> return <OsString>::from_script(value, world).map(|a| Box::new(a) as _),
59+
tsv: ScriptValue => return <ScriptValue>::from_script(value, world).map(|a| Box::new(a) as _),
5960
tn : () => return <()>::from_script(value, world).map(|a| Box::new(a) as _)
6061
}
6162
);

crates/bevy_mod_scripting_core/src/bindings/function/into_ref.rs

+1
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ fn into_script_ref(
102102
},
103103
tr : PathBuf => return downcast_into_value!(r, PathBuf).clone().into_script(world),
104104
ts : OsString=> return downcast_into_value!(r, OsString).clone().into_script(world),
105+
tsv: ScriptValue=> return Ok(downcast_into_value!(r, ScriptValue).clone()),
105106
tn : () => return Ok(ScriptValue::Unit)
106107
}
107108
);

crates/bevy_mod_scripting_core/src/bindings/globals/core.rs

+2
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ impl CoreGlobals {
6969
/// A cache of types normally available through the `world.get_type_by_name` function.
7070
///
7171
/// You can use this to avoid having to store type references.
72+
///
73+
/// Note that this cache will NOT contain types manually registered by scripts via `register_new_component`.
7274
fn types(
7375
guard: WorldGuard,
7476
) -> Result<

crates/bevy_mod_scripting_core/src/bindings/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@ crate::private::export_all_in_modules! {
1212
script_system,
1313
script_value,
1414
world,
15+
script_component,
1516
type_data
1617
}

crates/bevy_mod_scripting_core/src/bindings/query.rs

+89-2
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
//! Utilities for querying the world.
22
3-
use super::{with_global_access, ReflectReference, WorldAccessGuard};
3+
use super::{with_global_access, DynamicComponent, ReflectReference, WorldAccessGuard, WorldGuard};
44
use crate::error::InteropError;
55
use bevy::{
66
ecs::{
77
component::ComponentId,
88
entity::Entity,
99
query::{QueryData, QueryState},
10+
reflect::ReflectComponent,
1011
world::World,
1112
},
1213
prelude::{EntityRef, QueryBuilder},
14+
ptr::OwningPtr,
1315
reflect::{ParsedPath, Reflect, TypeRegistration},
1416
};
15-
use std::{any::TypeId, collections::VecDeque, sync::Arc};
17+
use std::{any::TypeId, collections::VecDeque, ptr::NonNull, sync::Arc};
1618

1719
/// A reference to a type which is not a `Resource` or `Component`.
1820
///
@@ -27,9 +29,13 @@ pub struct ScriptTypeRegistration {
2729
/// A reference to a component type's reflection registration.
2830
///
2931
/// In general think of this as a handle to a type.
32+
///
33+
/// Not to be confused with script registered dynamic components, although this can point to a script registered component.
3034
pub struct ScriptComponentRegistration {
3135
pub(crate) registration: ScriptTypeRegistration,
3236
pub(crate) component_id: ComponentId,
37+
/// whether this is a component registered BY a script
38+
pub(crate) is_dynamic_script_component: bool,
3339
}
3440

3541
#[derive(Clone, Reflect, Debug)]
@@ -100,6 +106,8 @@ impl ScriptComponentRegistration {
100106
/// Creates a new [`ScriptComponentRegistration`] from a [`ScriptTypeRegistration`] and a [`ComponentId`].
101107
pub fn new(registration: ScriptTypeRegistration, component_id: ComponentId) -> Self {
102108
Self {
109+
is_dynamic_script_component: registration.type_id()
110+
== std::any::TypeId::of::<DynamicComponent>(),
103111
registration,
104112
component_id,
105113
}
@@ -120,6 +128,85 @@ impl ScriptComponentRegistration {
120128
pub fn into_type_registration(self) -> ScriptTypeRegistration {
121129
self.registration
122130
}
131+
132+
/// Removes an instance of this component from the given entity
133+
pub fn remove_from_entity(
134+
&self,
135+
world: WorldGuard,
136+
entity: Entity,
137+
) -> Result<(), InteropError> {
138+
world.with_global_access(|world| {
139+
let mut entity = world
140+
.get_entity_mut(entity)
141+
.map_err(|_| InteropError::missing_entity(entity))?;
142+
entity.remove_by_id(self.component_id);
143+
Ok(())
144+
})?
145+
}
146+
147+
/// Inserts an instance of this component into the given entity
148+
///
149+
/// Requires whole world access
150+
pub fn insert_into_entity(
151+
&self,
152+
world: WorldGuard,
153+
entity: Entity,
154+
instance: Box<dyn Reflect>,
155+
) -> Result<(), InteropError> {
156+
if self.is_dynamic_script_component {
157+
// if dynamic we already know the type i.e. `ScriptComponent`
158+
// so we can just insert it
159+
160+
world.with_global_access(|world| {
161+
let mut entity = world
162+
.get_entity_mut(entity)
163+
.map_err(|_| InteropError::missing_entity(entity))?;
164+
let cast = instance.downcast::<DynamicComponent>().map_err(|v| {
165+
InteropError::type_mismatch(TypeId::of::<DynamicComponent>(), Some(v.type_id()))
166+
})?;
167+
// the reason we leak the box, is because we don't want to double drop the owning ptr
168+
169+
let ptr = (Box::leak(cast) as *mut DynamicComponent).cast();
170+
// Safety: cannot be null as we just created it from a valid reference
171+
let non_null_ptr = unsafe { NonNull::new_unchecked(ptr) };
172+
// Safety:
173+
// - we know the type is ScriptComponent, as we just created the pointer
174+
// - the box will stay valid for the life of this function, and we do not return the ptr
175+
// - pointer is alligned correctly
176+
// - nothing else will call drop on this
177+
let owning_ptr = unsafe { OwningPtr::new(non_null_ptr) };
178+
// Safety:
179+
// - Owning Ptr is valid as we just created it
180+
// - TODO: do we need to check if ComponentId is from this world? How?
181+
unsafe { entity.insert_by_id(self.component_id, owning_ptr) };
182+
Ok(())
183+
})?
184+
} else {
185+
let component_data = self
186+
.type_registration()
187+
.type_registration()
188+
.data::<ReflectComponent>()
189+
.ok_or_else(|| {
190+
InteropError::missing_type_data(
191+
self.registration.type_id(),
192+
"ReflectComponent".to_owned(),
193+
)
194+
})?;
195+
196+
// TODO: this shouldn't need entire world access it feels
197+
let type_registry = world.type_registry();
198+
world.with_global_access(|world| {
199+
let mut entity = world
200+
.get_entity_mut(entity)
201+
.map_err(|_| InteropError::missing_entity(entity))?;
202+
{
203+
let registry = type_registry.read();
204+
component_data.insert(&mut entity, instance.as_partial_reflect(), &registry);
205+
}
206+
Ok(())
207+
})?
208+
}
209+
}
123210
}
124211

125212
impl std::fmt::Debug for ScriptTypeRegistration {

0 commit comments

Comments
 (0)