>,
+ mut commands: Commands,
+) {
+ for event in events.read() {
+ trace!("{}: Received script asset event: {:?}", P::LANGUAGE, event);
+ match event {
+ // emitted when a new script asset is loaded for the first time
+ ScriptAssetEvent::Added(metadata) | ScriptAssetEvent::Modified(metadata) => {
+ if metadata.language != P::LANGUAGE {
+ trace!(
+ "{}: Script asset with id: {} is for a different langauge than this sync system. Skipping.",
+ P::LANGUAGE,
+ metadata.script_id
+ );
+ continue;
+ }
+
+ info!("{}: Loading Script: {:?}", P::LANGUAGE, metadata.script_id,);
+
+ if let Some(asset) = script_assets.get(metadata.asset_id) {
+ commands.queue(CreateOrUpdateScript::::new(
+ metadata.script_id.clone(),
+ asset.content.clone(),
+ Some(script_assets.reserve_handle().clone_weak()),
+ ));
+ }
+ }
+ ScriptAssetEvent::Removed(metadata) => {
+ info!("{}: Deleting Script: {:?}", P::LANGUAGE, metadata.script_id,);
+ commands.queue(DeleteScript::
::new(metadata.script_id.clone()));
+ }
+ };
+ }
+}
+
+/// Setup all the asset systems for the scripting plugin and the dependencies
+pub(crate) fn configure_asset_systems(app: &mut App) -> &mut App {
+ // these should be in the same set as bevy's asset systems
+ // currently this is in the PreUpdate set
+ app.add_systems(
+ PreUpdate,
+ (
+ dispatch_script_asset_events.in_set(ScriptingSystemSet::ScriptAssetDispatch),
+ remove_script_metadata.in_set(ScriptingSystemSet::ScriptMetadataRemoval),
+ ),
+ )
+ .configure_sets(
+ PreUpdate,
+ (
+ ScriptingSystemSet::ScriptAssetDispatch.after(bevy::asset::TrackAssets),
+ ScriptingSystemSet::ScriptCommandDispatch
+ .after(ScriptingSystemSet::ScriptAssetDispatch)
+ .before(ScriptingSystemSet::ScriptMetadataRemoval),
+ ),
+ )
+ .init_resource::()
+ .init_resource::()
+ .add_event::();
+
+ app
+}
+
+/// Setup all the asset systems for the scripting plugin and the dependencies
+pub(crate) fn configure_asset_systems_for_plugin(
+ app: &mut App,
+) -> &mut App {
+ app.add_systems(
+ PreUpdate,
+ sync_script_data::.in_set(ScriptingSystemSet::ScriptCommandDispatch),
+ );
+ app
+}
+
+#[cfg(test)]
+mod tests {
+ use bevy::{
+ app::{App, Update},
+ asset::{AssetApp, AssetPlugin, AssetServer, Assets, Handle, LoadState},
+ MinimalPlugins,
+ };
+
+ use super::*;
+
+ fn init_loader_test(loader: ScriptAssetLoader) -> App {
+ let mut app = App::new();
+ app.add_plugins((MinimalPlugins, AssetPlugin::default()));
+ app.init_asset::();
+ app.register_asset_loader(loader);
+ app
+ }
+
+ fn make_test_settings() -> ScriptAssetSettings {
+ ScriptAssetSettings {
+ script_id_mapper: AssetPathToScriptIdMapper {
+ map: |path| path.to_string_lossy().into_owned().into(),
+ },
+ script_language_mappers: vec![
+ AssetPathToLanguageMapper {
+ map: |path| {
+ if path.extension().unwrap() == "lua" {
+ Language::Lua
+ } else {
+ Language::Unknown
+ }
+ },
+ },
+ AssetPathToLanguageMapper {
+ map: |path| {
+ if path.extension().unwrap() == "rhai" {
+ Language::Rhai
+ } else {
+ Language::Unknown
+ }
+ },
+ },
+ ],
+ }
+ }
+
+ fn load_asset(app: &mut App, path: &str) -> Handle {
+ let handle = app.world_mut().resource::().load(path);
+
+ loop {
+ let state = app
+ .world()
+ .resource::()
+ .get_load_state(&handle)
+ .unwrap();
+ if !matches!(state, LoadState::Loading) {
+ break;
+ }
+ app.update();
+ }
+
+ match app
+ .world()
+ .resource::()
+ .get_load_state(&handle)
+ .unwrap()
+ {
+ LoadState::NotLoaded => panic!("Asset not loaded"),
+ LoadState::Loaded => {}
+ LoadState::Failed(asset_load_error) => {
+ panic!("Asset load failed: {:?}", asset_load_error)
+ }
+ _ => panic!("Unexpected load state"),
+ }
+
+ handle
+ }
+
+ #[test]
+ fn test_asset_loader_loads() {
+ let loader = ScriptAssetLoader {
+ extensions: &["script"],
+ preprocessor: None,
+ };
+ let mut app = init_loader_test(loader);
+
+ let handle = load_asset(&mut app, "test_assets/test_script.script");
+ let asset = app
+ .world()
+ .get_resource::>()
+ .unwrap()
+ .get(&handle)
+ .unwrap();
+
+ assert_eq!(
+ asset.asset_path,
+ PathBuf::from("test_assets/test_script.script")
+ );
+
+ assert_eq!(
+ String::from_utf8(asset.content.clone().to_vec()).unwrap(),
+ "test script".to_string()
+ );
+ }
+
+ #[test]
+ fn test_asset_loader_applies_preprocessor() {
+ let loader = ScriptAssetLoader {
+ extensions: &["script"],
+ preprocessor: Some(Box::new(|content| {
+ content[0] = b'p';
+ Ok(())
+ })),
+ };
+ let mut app = init_loader_test(loader);
+
+ let handle = load_asset(&mut app, "test_assets/test_script.script");
+ let asset = app
+ .world()
+ .get_resource::>()
+ .unwrap()
+ .get(&handle)
+ .unwrap();
+
+ assert_eq!(
+ asset.asset_path,
+ PathBuf::from("test_assets/test_script.script")
+ );
+ assert_eq!(
+ String::from_utf8(asset.content.clone().to_vec()).unwrap(),
+ "pest script".to_string()
+ );
+ }
+
+ #[test]
+ fn test_metadata_store() {
+ let mut store = ScriptMetadataStore::default();
+ let id = AssetId::invalid();
+ let meta = ScriptMetadata {
+ asset_id: AssetId::invalid(),
+ script_id: "test".into(),
+ language: Language::Lua,
+ };
+
+ store.insert(id, meta.clone());
+ assert_eq!(store.get(id), Some(&meta));
+
+ assert_eq!(store.remove(id), Some(meta));
+ }
+
+ #[test]
+ fn test_script_asset_settings_select_language() {
+ let settings = make_test_settings();
+
+ let path = Path::new("test.lua");
+ assert_eq!(settings.select_script_language(path), Language::Lua);
+ assert_eq!(
+ settings.select_script_language(Path::new("test.rhai")),
+ Language::Rhai
+ );
+ assert_eq!(
+ settings.select_script_language(Path::new("test.blob")),
+ Language::Unknown
+ );
+ }
+
+ fn run_app_untill_asset_event(app: &mut App, event_kind: AssetEvent) {
+ let checker_system = |mut reader: EventReader>,
+ mut event_target: ResMut| {
+ println!("Reading asset events this frame");
+ for event in reader.read() {
+ println!("{:?}", event);
+ if matches!(
+ (event_target.event, event),
+ (AssetEvent::Added { .. }, AssetEvent::Added { .. })
+ | (AssetEvent::Modified { .. }, AssetEvent::Modified { .. })
+ | (AssetEvent::Removed { .. }, AssetEvent::Removed { .. })
+ | (AssetEvent::Unused { .. }, AssetEvent::Unused { .. })
+ | (
+ AssetEvent::LoadedWithDependencies { .. },
+ AssetEvent::LoadedWithDependencies { .. },
+ )
+ ) {
+ println!("Event matched");
+ event_target.happened = true;
+ }
+ }
+ };
+
+ if !app.world().contains_resource::() {
+ // for when we run this multiple times in a test
+ app.add_systems(Update, checker_system);
+ }
+
+ #[derive(Resource)]
+ struct EventTarget {
+ event: AssetEvent,
+ happened: bool,
+ }
+ app.world_mut().insert_resource(EventTarget {
+ event: event_kind,
+ happened: false,
+ });
+
+ loop {
+ println!("Checking if asset event was dispatched");
+ if app.world().get_resource::().unwrap().happened {
+ println!("Stopping loop");
+ break;
+ }
+ println!("Running app");
+
+ app.update();
+ }
+ }
+
+ struct DummyPlugin;
+
+ impl IntoScriptPluginParams for DummyPlugin {
+ type R = ();
+ type C = ();
+ const LANGUAGE: Language = Language::Lua;
+
+ fn build_runtime() -> Self::R {
+ todo!()
+ }
+ }
+
+ #[test]
+ fn test_asset_metadata_systems() {
+ // test metadata flow
+ let mut app = init_loader_test(ScriptAssetLoader {
+ extensions: &[],
+ preprocessor: None,
+ });
+ app.world_mut().insert_resource(make_test_settings());
+ configure_asset_systems(&mut app);
+
+ // update untill the asset event gets dispatched
+ let asset_server: &AssetServer = app.world().resource::();
+ let handle = asset_server.load("test_assets/test_script.lua");
+ run_app_untill_asset_event(
+ &mut app,
+ AssetEvent::LoadedWithDependencies {
+ id: AssetId::invalid(),
+ },
+ );
+ let asset_id = handle.id();
+
+ // we expect the metadata to be inserted now, in the same frame as the asset is loaded
+ let metadata = app
+ .world()
+ .get_resource::()
+ .unwrap()
+ .get(asset_id)
+ .expect("Metadata not found");
+
+ assert_eq!(metadata.script_id, "test_assets/test_script.lua");
+ assert_eq!(metadata.language, Language::Lua);
+
+ // ----------------- REMOVING -----------------
+
+ // we drop the handle and wait untill the first asset event is dispatched
+ drop(handle);
+
+ run_app_untill_asset_event(
+ &mut app,
+ AssetEvent::Removed {
+ id: AssetId::invalid(),
+ },
+ );
+
+ // we expect the metadata to be removed now, in the same frame as the asset is removed
+ let metadata_len = app
+ .world()
+ .get_resource::()
+ .unwrap()
+ .map
+ .len();
+
+ assert_eq!(metadata_len, 0);
+ }
+
+ // #[test]
+ // fn test_syncing_assets() {
+ // todo!()
+ // }
}
diff --git a/crates/bevy_mod_scripting_core/src/bindings/allocator.rs b/crates/bevy_mod_scripting_core/src/bindings/allocator.rs
index 52b56edda1..d31fcc9090 100644
--- a/crates/bevy_mod_scripting_core/src/bindings/allocator.rs
+++ b/crates/bevy_mod_scripting_core/src/bindings/allocator.rs
@@ -1,4 +1,4 @@
-use bevy::{ecs::system::Resource, reflect::PartialReflect};
+use bevy::{ecs::system::Resource, prelude::ResMut, reflect::PartialReflect};
use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use std::{
any::TypeId,
@@ -205,6 +205,12 @@ impl ReflectAllocator {
}
}
+/// Cleans up dangling script allocations
+pub fn garbage_collector(allocator: ResMut) {
+ let mut allocator = allocator.write();
+ allocator.clean_garbage_allocations()
+}
+
#[cfg(test)]
mod test {
use super::*;
diff --git a/crates/bevy_mod_scripting_core/src/bindings/pretty_print.rs b/crates/bevy_mod_scripting_core/src/bindings/pretty_print.rs
index c7faba4d64..d32c741e3f 100644
--- a/crates/bevy_mod_scripting_core/src/bindings/pretty_print.rs
+++ b/crates/bevy_mod_scripting_core/src/bindings/pretty_print.rs
@@ -366,7 +366,7 @@ impl DisplayWithWorld for ReflectBaseType {
impl DisplayWithWorld for TypeId {
fn display_with_world(&self, world: WorldGuard) -> String {
if *self == TypeId::of::() {
- return "Dynamic Type".to_owned();
+ return "Unknown Type".to_owned();
} else if *self == TypeId::of::() {
// does not implement Reflect, so we do this manually
return "World".to_owned();
@@ -485,3 +485,118 @@ impl DisplayWithWorld for Vec {
string
}
}
+
+#[cfg(test)]
+mod test {
+ use bevy::prelude::AppTypeRegistry;
+
+ use crate::bindings::{
+ function::script_function::AppScriptFunctionRegistry, AppReflectAllocator,
+ ReflectAllocationId, WorldAccessGuard,
+ };
+
+ use super::*;
+
+ fn setup_world() -> World {
+ let mut world = World::default();
+
+ let type_registry = AppTypeRegistry::default();
+ world.insert_resource(type_registry);
+
+ let allocator = AppReflectAllocator::default();
+ world.insert_resource(allocator);
+
+ let script_function_registry = AppScriptFunctionRegistry::default();
+ world.insert_resource(script_function_registry);
+
+ world
+ }
+
+ #[test]
+ fn test_type_id() {
+ let mut world = setup_world();
+ let world = WorldGuard::new(WorldAccessGuard::new(&mut world));
+
+ let type_id = TypeId::of::();
+ assert_eq!(type_id.display_with_world(world.clone()), "usize");
+ assert_eq!(type_id.display_value_with_world(world.clone()), "usize");
+ assert_eq!(type_id.display_without_world(), format!("{:?}", type_id));
+
+ let type_id = TypeId::of::();
+ assert_eq!(type_id.display_with_world(world.clone()), "Unknown Type");
+ assert_eq!(
+ type_id.display_value_with_world(world.clone()),
+ "Unknown Type"
+ );
+ assert_eq!(type_id.display_without_world(), format!("{:?}", type_id));
+ }
+
+ #[test]
+ fn test_reflect_base_type() {
+ let mut world = setup_world();
+ let world = WorldGuard::new(WorldAccessGuard::new(&mut world));
+
+ let type_id = TypeId::of::();
+
+ assert_eq!(
+ ReflectBaseType {
+ base_id: ReflectBase::Owned(ReflectAllocationId::new(0)),
+ type_id,
+ }
+ .display_with_world(world.clone()),
+ "Allocation(0)(usize)"
+ );
+
+ assert_eq!(
+ ReflectBaseType {
+ base_id: ReflectBase::Owned(ReflectAllocationId::new(0)),
+ type_id,
+ }
+ .display_value_with_world(world.clone()),
+ "Allocation(0)(usize)"
+ );
+
+ assert_eq!(
+ ReflectBaseType {
+ base_id: ReflectBase::Owned(ReflectAllocationId::new(0)),
+ type_id,
+ }
+ .display_without_world(),
+ format!("Allocation(0)({:?})", type_id)
+ );
+ }
+
+ #[test]
+ fn test_reflect_reference() {
+ let mut world = setup_world();
+
+ let world = WorldGuard::new(WorldAccessGuard::new(&mut world));
+
+ let type_id = TypeId::of::();
+
+ let allocator = world.allocator();
+ let mut allocator_write = allocator.write();
+ let reflect_reference = ReflectReference::new_allocated(2usize, &mut allocator_write);
+ let id = match reflect_reference.base.base_id {
+ ReflectBase::Owned(ref id) => id.to_string(),
+ _ => panic!("Expected owned allocation"),
+ };
+
+ drop(allocator_write);
+
+ assert_eq!(
+ reflect_reference.display_with_world(world.clone()),
+ format!(" usize>")
+ );
+
+ assert_eq!(
+ reflect_reference.display_value_with_world(world.clone()),
+ "Reflect(usize(2))"
+ );
+
+ assert_eq!(
+ reflect_reference.display_without_world(),
+ format!("", type_id)
+ );
+ }
+}
diff --git a/crates/bevy_mod_scripting_core/src/bindings/reference.rs b/crates/bevy_mod_scripting_core/src/bindings/reference.rs
index 92948b5ae5..d1709cf8e6 100644
--- a/crates/bevy_mod_scripting_core/src/bindings/reference.rs
+++ b/crates/bevy_mod_scripting_core/src/bindings/reference.rs
@@ -80,6 +80,11 @@ impl ReflectReference {
})
}
+ /// Create a new reference to a value by allocating it.
+ ///
+ /// You can retrieve the allocator from the world using [`WorldGuard::allocator`].
+ /// Make sure to drop the allocator write guard before doing anything with the reference to prevent deadlocks.
+ ///
pub fn new_allocated(
value: T,
allocator: &mut ReflectAllocator,
@@ -547,3 +552,282 @@ impl Iterator for ReflectRefIter {
Some(result)
}
}
+
+#[cfg(test)]
+mod test {
+ use bevy::prelude::{AppTypeRegistry, World};
+
+ use crate::bindings::{
+ function::script_function::AppScriptFunctionRegistry, AppReflectAllocator, WorldAccessGuard,
+ };
+
+ use super::*;
+
+ #[derive(Reflect, Component, Debug, Clone, PartialEq)]
+ struct Component(Vec);
+
+ #[derive(Reflect, Resource, Debug, Clone, PartialEq)]
+ struct Resource(Vec);
+
+ fn setup_world() -> World {
+ let mut world = World::default();
+
+ let type_registry = AppTypeRegistry::default();
+ {
+ let mut guard_type_registry = type_registry.write();
+ guard_type_registry.register::();
+ guard_type_registry.register::();
+ }
+
+ world.insert_resource(type_registry);
+
+ let allocator = AppReflectAllocator::default();
+ world.insert_resource(allocator);
+
+ let script_function_registry = AppScriptFunctionRegistry::default();
+ world.insert_resource(script_function_registry);
+
+ world
+ }
+
+ #[test]
+ fn test_component_ref() {
+ let mut world = setup_world();
+
+ let entity = world
+ .spawn(Component(vec!["hello".to_owned(), "world".to_owned()]))
+ .id();
+
+ let world_guard = WorldGuard::new(WorldAccessGuard::new(&mut world));
+
+ let mut component_ref =
+ ReflectReference::new_component_ref::(entity, world_guard.clone())
+ .expect("could not create component reference");
+
+ // index into component
+ assert_eq!(
+ component_ref
+ .tail_type_id(world_guard.clone())
+ .unwrap()
+ .unwrap(),
+ TypeId::of::()
+ );
+
+ component_ref
+ .with_reflect(world_guard.clone(), |s| {
+ let s = s.try_downcast_ref::().unwrap();
+ assert_eq!(s, &Component(vec!["hello".to_owned(), "world".to_owned()]));
+ })
+ .unwrap();
+
+ // index into vec field
+ component_ref.index_path(ParsedPath::parse_static(".0").unwrap());
+ assert_eq!(
+ component_ref
+ .tail_type_id(world_guard.clone())
+ .unwrap()
+ .unwrap(),
+ TypeId::of::>()
+ );
+
+ assert_eq!(
+ component_ref
+ .element_type_id(world_guard.clone())
+ .unwrap()
+ .unwrap(),
+ TypeId::of::()
+ );
+
+ assert_eq!(
+ component_ref
+ .key_type_id(world_guard.clone())
+ .unwrap()
+ .unwrap(),
+ TypeId::of::()
+ );
+
+ component_ref
+ .with_reflect(world_guard.clone(), |s| {
+ let s = s.try_downcast_ref::>().unwrap();
+ assert_eq!(s, &vec!["hello".to_owned(), "world".to_owned()]);
+ })
+ .unwrap();
+
+ // index into vec
+ component_ref.index_path(ParsedPath::parse_static("[0]").unwrap());
+
+ component_ref
+ .with_reflect(world_guard.clone(), |s| {
+ let s = s.try_downcast_ref::().unwrap();
+ assert_eq!(s, "hello");
+ })
+ .unwrap();
+
+ assert_eq!(
+ component_ref
+ .tail_type_id(world_guard.clone())
+ .unwrap()
+ .unwrap(),
+ TypeId::of::()
+ );
+ }
+
+ #[test]
+ fn test_resource_ref() {
+ let mut world = setup_world();
+
+ world.insert_resource(Resource(vec!["hello".to_owned(), "world".to_owned()]));
+
+ let world_guard = WorldGuard::new(WorldAccessGuard::new(&mut world));
+
+ let mut resource_ref = ReflectReference::new_resource_ref::(world_guard.clone())
+ .expect("could not create resource reference");
+
+ // index into resource
+ assert_eq!(
+ resource_ref
+ .tail_type_id(world_guard.clone())
+ .unwrap()
+ .unwrap(),
+ TypeId::of::()
+ );
+
+ resource_ref
+ .with_reflect(world_guard.clone(), |s| {
+ let s = s.try_downcast_ref::().unwrap();
+ assert_eq!(s, &Resource(vec!["hello".to_owned(), "world".to_owned()]));
+ })
+ .unwrap();
+
+ // index into vec field
+ resource_ref.index_path(ParsedPath::parse_static(".0").unwrap());
+ assert_eq!(
+ resource_ref
+ .tail_type_id(world_guard.clone())
+ .unwrap()
+ .unwrap(),
+ TypeId::of::>()
+ );
+
+ assert_eq!(
+ resource_ref
+ .element_type_id(world_guard.clone())
+ .unwrap()
+ .unwrap(),
+ TypeId::of::()
+ );
+
+ assert_eq!(
+ resource_ref
+ .key_type_id(world_guard.clone())
+ .unwrap()
+ .unwrap(),
+ TypeId::of::()
+ );
+
+ resource_ref
+ .with_reflect(world_guard.clone(), |s| {
+ let s = s.try_downcast_ref::>().unwrap();
+ assert_eq!(s, &vec!["hello".to_owned(), "world".to_owned()]);
+ })
+ .unwrap();
+
+ // index into vec
+ resource_ref.index_path(ParsedPath::parse_static("[0]").unwrap());
+
+ resource_ref
+ .with_reflect(world_guard.clone(), |s| {
+ let s = s.try_downcast_ref::().unwrap();
+ assert_eq!(s, "hello");
+ })
+ .unwrap();
+
+ assert_eq!(
+ resource_ref
+ .tail_type_id(world_guard.clone())
+ .unwrap()
+ .unwrap(),
+ TypeId::of::()
+ );
+ }
+
+ #[test]
+ fn test_allocation_ref() {
+ let mut world = setup_world();
+
+ let value = Component(vec!["hello".to_owned(), "world".to_owned()]);
+
+ let world_guard = WorldGuard::new(WorldAccessGuard::new(&mut world));
+ let allocator = world_guard.allocator();
+ let mut allocator_write = allocator.write();
+ let mut allocation_ref = ReflectReference::new_allocated(value, &mut allocator_write);
+ drop(allocator_write);
+
+ // index into component
+ assert_eq!(
+ allocation_ref
+ .tail_type_id(world_guard.clone())
+ .unwrap()
+ .unwrap(),
+ TypeId::of::()
+ );
+
+ allocation_ref
+ .with_reflect(world_guard.clone(), |s| {
+ let s = s.try_downcast_ref::().unwrap();
+ assert_eq!(s, &Component(vec!["hello".to_owned(), "world".to_owned()]));
+ })
+ .unwrap();
+
+ // index into vec field
+ allocation_ref.index_path(ParsedPath::parse_static(".0").unwrap());
+ assert_eq!(
+ allocation_ref
+ .tail_type_id(world_guard.clone())
+ .unwrap()
+ .unwrap(),
+ TypeId::of::>()
+ );
+
+ assert_eq!(
+ allocation_ref
+ .element_type_id(world_guard.clone())
+ .unwrap()
+ .unwrap(),
+ TypeId::of::()
+ );
+
+ assert_eq!(
+ allocation_ref
+ .key_type_id(world_guard.clone())
+ .unwrap()
+ .unwrap(),
+ TypeId::of::()
+ );
+
+ allocation_ref
+ .with_reflect(world_guard.clone(), |s| {
+ let s = s.try_downcast_ref::>().unwrap();
+ assert_eq!(s, &vec!["hello".to_owned(), "world".to_owned()]);
+ })
+ .unwrap();
+
+ // index into vec
+ allocation_ref.index_path(ParsedPath::parse_static("[0]").unwrap());
+
+ allocation_ref
+ .with_reflect(world_guard.clone(), |s| {
+ let s = s.try_downcast_ref::().unwrap();
+ assert_eq!(s, "hello");
+ })
+ .unwrap();
+
+ assert_eq!(
+ allocation_ref
+ .tail_type_id(world_guard.clone())
+ .unwrap()
+ .unwrap(),
+ TypeId::of::()
+ );
+ }
+}
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 9cbf5b7167..993980141b 100644
--- a/crates/bevy_mod_scripting_core/src/bindings/script_value.rs
+++ b/crates/bevy_mod_scripting_core/src/bindings/script_value.rs
@@ -182,3 +182,35 @@ impl TryFrom for ParsedPath {
})
}
}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn test_script_value_to_parsed_path() {
+ let value = ScriptValue::String("test".into());
+ let parsed_path = ParsedPath::from(vec![OffsetAccess {
+ access: bevy::reflect::Access::Field("test".to_owned().into()),
+ offset: Some(4),
+ }]);
+ assert_eq!(parsed_path, ParsedPath::try_from(value).unwrap());
+
+ let value = ScriptValue::String("_0".into());
+ let parsed_path = ParsedPath::from(vec![OffsetAccess {
+ access: bevy::reflect::Access::TupleIndex(0),
+ offset: Some(1),
+ }]);
+ assert_eq!(parsed_path, ParsedPath::try_from(value).unwrap());
+
+ let value = ScriptValue::Integer(0);
+ let parsed_path = ParsedPath::from(vec![OffsetAccess {
+ access: bevy::reflect::Access::ListIndex(0),
+ offset: Some(1),
+ }]);
+ assert_eq!(parsed_path, ParsedPath::try_from(value).unwrap());
+
+ let value = ScriptValue::Float(0.0);
+ assert!(ParsedPath::try_from(value).is_err());
+ }
+}
diff --git a/crates/bevy_mod_scripting_core/src/commands.rs b/crates/bevy_mod_scripting_core/src/commands.rs
index ac548f78ab..d9357353ca 100644
--- a/crates/bevy_mod_scripting_core/src/commands.rs
+++ b/crates/bevy_mod_scripting_core/src/commands.rs
@@ -2,10 +2,9 @@ use crate::{
asset::ScriptAsset,
context::{ContextLoadingSettings, ScriptContexts},
event::{IntoCallbackLabel, OnScriptLoaded, OnScriptUnloaded},
- handler::CallbackSettings,
+ handler::{handle_script_errors, CallbackSettings, HandlerFn},
runtime::RuntimeContainer,
script::{Script, ScriptId, Scripts},
- systems::handle_script_errors,
IntoScriptPluginParams,
};
use bevy::{asset::Handle, ecs::world::Mut, log::debug, prelude::Command};
@@ -36,8 +35,7 @@ impl Command for DeleteScript {
let runner = world
.get_resource::>()
.expect("No CallbackSettings resource found")
- .callback_handler
- .expect("No callback handler set");
+ .callback_handler;
let mut ctxts = world
.remove_non_send_resource::>()
@@ -76,12 +74,8 @@ impl Command for DeleteScript {
}
}
- let assigner = settings
- .assigner
- .as_ref()
- .expect("Could not find context assigner in settings");
debug!("Removing script with id: {}", self.id);
- (assigner.remove)(script.context_id, &script, &mut ctxts)
+ (settings.assigner.remove)(script.context_id, &script, &mut ctxts)
} else {
bevy::log::error!(
"Attempted to delete script with id: {} but it does not exist, doing nothing!",
@@ -116,107 +110,466 @@ impl CreateOrUpdateScript {
_ph: std::marker::PhantomData,
}
}
-}
-
-impl Command for CreateOrUpdateScript {
- fn apply(self, world: &mut bevy::prelude::World) {
- let settings = world
- .get_resource::>()
- .unwrap()
- .clone();
- let mut contexts = world
- .remove_non_send_resource::>()
- .unwrap();
- let mut runtime = world
- .remove_non_send_resource::>()
- .unwrap();
-
- let runner = world.get_resource::>().unwrap();
- // assign context
- let assigner = settings.assigner.clone().expect("No context assigner set");
- let builder = settings.loader.clone().expect("No context loader set");
- let runner = runner.callback_handler.expect("No callback handler set");
-
- world.resource_scope(|world, mut scripts: Mut| {
-
- // check if script already exists
-
- let mut script = scripts.scripts.get_mut(&self.id);
- let previous_context_id = script.as_ref().map(|s| s.context_id);
- debug!(
- "{}: CreateOrUpdateScript command applying (script_id: {}, previous_context_id: {:?})",
- P::LANGUAGE,
- self.id, previous_context_id
- );
- // If None assign new context ID, otherwise assign the old one
- // If re-loading and different from the previous one, the old one will be removed
- let current_context_id = (assigner.assign)(script.as_deref(), &self.id, &self.content, &mut contexts);
+ fn run_on_load_callback(
+ &self,
+ settings: &ContextLoadingSettings,
+ runtime: &mut RuntimeContainer
,
+ runner: HandlerFn
,
+ world: &mut bevy::prelude::World,
+ ctxt: &mut
::C,
+ ) {
+ match (runner)(
+ vec![],
+ bevy::ecs::entity::Entity::from_raw(0),
+ &self.id,
+ &OnScriptLoaded::into_callback_label(),
+ ctxt,
+ &settings.context_pre_handling_initializers,
+ &mut runtime.runtime,
+ world,
+ ) {
+ Ok(_) => {}
+ Err(e) => {
+ handle_script_errors(
+ world,
+ [e.with_context(format!(
+ "{}: Running initialization hook for script with id: {}",
+ P::LANGUAGE,
+ self.id
+ ))]
+ .into_iter(),
+ );
+ }
+ }
+ }
- debug!("{}: New context assigned?: {:?}", P::LANGUAGE, current_context_id.is_none() || current_context_id != previous_context_id);
+ #[inline(always)]
+ fn reload_context(
+ &self,
+ world: &mut bevy::prelude::World,
+ settings: &ContextLoadingSettings
,
+ runtime: &mut RuntimeContainer
,
+ builder: &crate::context::ContextBuilder
,
+ log_context: String,
+ previous_context: &mut
::C,
+ ) -> bool {
+ match (builder.reload)(
+ &self.id,
+ &self.content,
+ previous_context,
+ &settings.context_initializers,
+ &settings.context_pre_handling_initializers,
+ world,
+ &mut runtime.runtime,
+ ) {
+ Ok(_) => {}
+ Err(e) => {
+ handle_script_errors(world, [e.with_context(log_context)].into_iter());
+ return false;
+ }
+ };
+ true
+ }
- let current_context_id = if let Some(id) = current_context_id {
- // reload existing context
- id
- } else {
- let log_context = format!("{}: Loading script: {}", P::LANGUAGE, self.id);
- bevy::log::info!("{}", log_context);
- let ctxt = (builder.load)(&self.id, &self.content, &settings.context_initializers, &settings.context_pre_handling_initializers, world, &mut runtime.runtime);
- match ctxt {
- Ok(ctxt) => contexts.insert(ctxt),
- Err(e) => {
- handle_script_errors(world, [e.with_context(log_context)].into_iter());
+ #[inline(always)]
+ fn execute(
+ self,
+ world: &mut bevy::prelude::World,
+ settings: &ContextLoadingSettings
,
+ contexts: &mut ScriptContexts
,
+ runtime: &mut RuntimeContainer
,
+ scripts: &mut Scripts,
+ assigner: crate::context::ContextAssigner
,
+ builder: crate::context::ContextBuilder
,
+ runner: HandlerFn
,
+ previous_context_id: Option,
+ ) {
+ match previous_context_id {
+ Some(previous_context_id) => {
+ if let Some(previous_context) = contexts.get_mut(previous_context_id) {
+ let log_context = format!("{}: Reloading script: {}.", P::LANGUAGE, self.id);
+ bevy::log::debug!("{}", log_context);
+ if !self.reload_context(
+ world,
+ settings,
+ runtime,
+ &builder,
+ log_context,
+ previous_context,
+ ) {
return;
}
+ self.run_on_load_callback(settings, runtime, runner, world, previous_context);
+ } else {
+ bevy::log::error!("{}: Could not find previous context with id: {}. Could not reload script: {}. Someone deleted the context.", P::LANGUAGE, previous_context_id, self.id);
}
- };
+ }
+ None => {
+ let log_context = format!("{}: Loading script: {}", P::LANGUAGE, self.id);
+ let new_context_id = (assigner.assign)(&self.id, &self.content, contexts)
+ .unwrap_or_else(|| contexts.allocate_id());
+ if let Some(existing_context) = contexts.get_mut(new_context_id) {
+ // this can happen if we're sharing contexts between scripts
+ if !self.reload_context(
+ world,
+ settings,
+ runtime,
+ &builder,
+ log_context,
+ existing_context,
+ ) {
+ return;
+ }
- if let Some(previous) = previous_context_id {
- if let Some(previous_context_id) = contexts.get_mut(previous) {
- let log_context = format!("{}: Reloading script: {}.", P::LANGUAGE, self.id);
- bevy::log::info!("{}", log_context);
- match (builder.reload)(&self.id, &self.content, previous_context_id, &settings.context_initializers, &settings.context_pre_handling_initializers, world, &mut runtime.runtime) {
- Ok(_) => {},
+ self.run_on_load_callback(settings, runtime, runner, world, existing_context);
+ } else {
+ // load new context
+ bevy::log::debug!("{}", log_context);
+ let ctxt = (builder.load)(
+ &self.id,
+ &self.content,
+ &settings.context_initializers,
+ &settings.context_pre_handling_initializers,
+ world,
+ &mut runtime.runtime,
+ );
+ let mut ctxt = match ctxt {
+ Ok(ctxt) => ctxt,
Err(e) => {
handle_script_errors(world, [e.with_context(log_context)].into_iter());
return;
}
};
- } else {
- bevy::log::error!("{}: Could not find previous context with id: {}. Could not reload script: {}", P::LANGUAGE, previous, self.id);
- }
- if previous != current_context_id {
- bevy::log::info!("{}: Unloading script with id: {}. As it was assigned to a new context", P::LANGUAGE, self.id);
- script.as_deref_mut().unwrap().context_id = current_context_id;
- (assigner.remove)(previous, script.unwrap(), &mut contexts);
- }
- }
+ self.run_on_load_callback(settings, runtime, runner, world, &mut ctxt);
- if let Some(context) = contexts.get_mut(current_context_id) {
- match (runner)(vec![], bevy::ecs::entity::Entity::from_raw(0), &self.id, &OnScriptLoaded::into_callback_label(), context, &settings.context_pre_handling_initializers, &mut runtime.runtime, world) {
- Ok(_) => {},
- Err(e) => {
- handle_script_errors(world, [e.with_context(format!("{}: Running initialization hook for script with id: {}", P::LANGUAGE, self.id))].into_iter());
- },
+ if contexts.insert_with_id(new_context_id, ctxt).is_some() {
+ bevy::log::warn!("{}: Context with id {} was not expected to exist. Overwriting it with a new context. This might happen if a script is not completely removed.", P::LANGUAGE, new_context_id);
+ }
}
- // we only want to insert the script if a context is present, otherwise something went wrong
scripts.scripts.insert(
self.id.clone(),
Script {
id: self.id,
asset: self.asset,
- context_id: current_context_id,
+ context_id: new_context_id,
},
);
- } else {
- bevy::log::error!("{}: Context loading failed for script: {}. Did not run on_script_loaded hook",P::LANGUAGE ,self.id);
}
- });
+ }
+ }
+}
+
+impl Command for CreateOrUpdateScript {
+ fn apply(self, world: &mut bevy::prelude::World) {
+ let settings = world
+ .get_resource::>()
+ .expect(
+ "Missing ContextLoadingSettings resource. Was the plugin initialized correctly?",
+ )
+ .clone();
+ let mut contexts = world
+ .remove_non_send_resource::>()
+ .expect("No ScriptContexts resource found. Was the plugin initialized correctly?");
+ let mut runtime = world
+ .remove_non_send_resource::>()
+ .expect("No RuntimeContainer resource found. Was the plugin initialized correctly?");
+ let mut scripts = world
+ .remove_resource::()
+ .expect("No Scripts resource found. Was the plugin initialized correctly?");
+
+ let runner = world.get_resource::>().unwrap();
+ // assign context
+ let assigner = settings.assigner.clone();
+ let builder = settings.loader.clone();
+ let runner = runner.callback_handler;
+
+ let script = scripts.scripts.get(&self.id);
+ let previous_context_id = script.as_ref().map(|s| s.context_id);
+ debug!(
+ "{}: CreateOrUpdateScript command applying (script_id: {}, previous_context_id: {:?})",
+ P::LANGUAGE,
+ self.id,
+ previous_context_id
+ );
+
+ // closure to prevent returns from re-inserting resources
+ self.execute(
+ world,
+ &settings,
+ &mut contexts,
+ &mut runtime,
+ &mut scripts,
+ assigner,
+ builder,
+ runner,
+ previous_context_id,
+ );
+
+ world.insert_resource(scripts);
world.insert_resource(settings);
world.insert_non_send_resource(runtime);
world.insert_non_send_resource(contexts);
}
}
+
+#[cfg(test)]
+mod test {
+ use bevy::{
+ app::App,
+ prelude::{Entity, World},
+ };
+
+ use crate::{
+ asset::Language,
+ bindings::script_value::ScriptValue,
+ context::{ContextAssigner, ContextBuilder},
+ };
+
+ use super::*;
+
+ fn setup_app() -> App {
+ // setup all the resources necessary
+ let mut app = App::new();
+
+ app.insert_resource(ContextLoadingSettings:: {
+ loader: ContextBuilder {
+ load: |name, c, init, pre_run_init, _, _| {
+ let mut context = String::from_utf8_lossy(c).into();
+ for init in init {
+ init(name, &mut context)?;
+ }
+ for init in pre_run_init {
+ init(name, Entity::from_raw(0), &mut context)?;
+ }
+ Ok(context)
+ },
+ reload: |name, new, existing, init, pre_run_init, _, _| {
+ *existing = String::from_utf8_lossy(new).into();
+ for init in init {
+ init(name, existing)?;
+ }
+ for init in pre_run_init {
+ init(name, Entity::from_raw(0), existing)?;
+ }
+ Ok(())
+ },
+ },
+ assigner: Default::default(),
+ context_initializers: vec![|_, c| {
+ c.push_str(" initialized");
+ Ok(())
+ }],
+ context_pre_handling_initializers: vec![|_, _, c| {
+ c.push_str(" pre-handling-initialized");
+ Ok(())
+ }],
+ })
+ .insert_non_send_resource(ScriptContexts:: {
+ contexts: Default::default(),
+ })
+ .insert_non_send_resource(RuntimeContainer:: {
+ runtime: "Runtime".to_string(),
+ })
+ .insert_resource(CallbackSettings:: {
+ callback_handler: |_, _, _, callback, c, _, _, _| {
+ c.push_str(format!(" callback-ran-{}", callback).as_str());
+ Ok(ScriptValue::Unit)
+ },
+ })
+ .insert_resource(Scripts {
+ scripts: Default::default(),
+ });
+
+ app
+ }
+
+ struct DummyPlugin;
+
+ impl IntoScriptPluginParams for DummyPlugin {
+ type R = String;
+ type C = String;
+ const LANGUAGE: Language = Language::Unknown;
+
+ fn build_runtime() -> Self::R {
+ "Runtime".to_string()
+ }
+ }
+
+ fn assert_context_and_script(world: &World, id: &str, context: &str) {
+ let contexts = world
+ .get_non_send_resource::>()
+ .unwrap();
+ let scripts = world.get_resource::().unwrap();
+
+ let script = scripts.scripts.get(id).expect("Script not found");
+
+ assert_eq!(id, script.id);
+ let found_context = contexts
+ .contexts
+ .get(&script.context_id)
+ .expect("Context not found");
+
+ assert_eq!(found_context, context);
+ }
+
+ #[test]
+ fn test_commands_with_default_assigner() {
+ let mut app = setup_app();
+
+ let world = app.world_mut();
+ let content = "content".as_bytes().to_vec().into_boxed_slice();
+ let command = CreateOrUpdateScript::::new("script".into(), content, None);
+ command.apply(world);
+
+ // check script
+ assert_context_and_script(
+ world,
+ "script",
+ "content initialized pre-handling-initialized callback-ran-on_script_loaded",
+ );
+
+ // update the script
+ let content = "new content".as_bytes().to_vec().into_boxed_slice();
+ let command = CreateOrUpdateScript::::new("script".into(), content, None);
+ command.apply(world);
+
+ // check script
+ assert_context_and_script(
+ world,
+ "script",
+ "new content initialized pre-handling-initialized callback-ran-on_script_loaded",
+ );
+
+ // create second script
+ let content = "content2".as_bytes().to_vec().into_boxed_slice();
+ let command = CreateOrUpdateScript::::new("script2".into(), content, None);
+
+ command.apply(world);
+
+ // check second script
+
+ assert_context_and_script(
+ world,
+ "script2",
+ "content2 initialized pre-handling-initialized callback-ran-on_script_loaded",
+ );
+
+ // delete both scripts
+ let command = DeleteScript::::new("script".into());
+ command.apply(world);
+ let command = DeleteScript::::new("script2".into());
+ command.apply(world);
+
+ // check that the scripts are gone
+ let scripts = world.get_resource::().unwrap();
+ assert!(scripts.scripts.is_empty());
+
+ let contexts = world
+ .get_non_send_resource::>()
+ .unwrap();
+ assert!(contexts.contexts.is_empty());
+ }
+
+ #[test]
+ fn test_commands_with_global_assigner() {
+ // setup all the resources necessary
+ let mut app = setup_app();
+
+ let mut settings = app
+ .world_mut()
+ .get_resource_mut::>()
+ .unwrap();
+
+ settings.assigner = ContextAssigner::new_global_context_assigner();
+
+ // create a script
+ let content = "content".as_bytes().to_vec().into_boxed_slice();
+ let command = CreateOrUpdateScript::::new("script".into(), content, None);
+
+ command.apply(app.world_mut());
+
+ // check script
+ assert_context_and_script(
+ app.world(),
+ "script",
+ "content initialized pre-handling-initialized callback-ran-on_script_loaded",
+ );
+
+ // update the script
+
+ let content = "new content".as_bytes().to_vec().into_boxed_slice();
+ let command = CreateOrUpdateScript::::new("script".into(), content, None);
+
+ command.apply(app.world_mut());
+
+ // check script
+
+ assert_context_and_script(
+ app.world(),
+ "script",
+ "new content initialized pre-handling-initialized callback-ran-on_script_loaded",
+ );
+
+ // create second script
+
+ let content = "content2".as_bytes().to_vec().into_boxed_slice();
+ let command = CreateOrUpdateScript::::new("script2".into(), content, None);
+
+ command.apply(app.world_mut());
+
+ // check both scripts have the new context
+
+ assert_context_and_script(
+ app.world(),
+ "script",
+ "content2 initialized pre-handling-initialized callback-ran-on_script_loaded",
+ );
+
+ assert_context_and_script(
+ app.world(),
+ "script2",
+ "content2 initialized pre-handling-initialized callback-ran-on_script_loaded",
+ );
+
+ // check one context exists only
+ let context = app
+ .world()
+ .get_non_send_resource::>()
+ .unwrap();
+ assert!(context.contexts.len() == 1);
+
+ // delete first script
+ let command = DeleteScript::::new("script".into());
+
+ command.apply(app.world_mut());
+
+ // check second script still has the context, and on unload was called
+ assert_context_and_script(
+ app.world(),
+ "script2",
+ "content2 initialized pre-handling-initialized callback-ran-on_script_loaded callback-ran-on_script_unloaded",
+ );
+
+ // delete second script
+
+ let command = DeleteScript::::new("script2".into());
+
+ command.apply(app.world_mut());
+
+ // check that the scripts are gone, but context is still there
+
+ let scripts = app.world().get_resource::().unwrap();
+ assert!(scripts.scripts.is_empty());
+
+ let contexts = app
+ .world()
+ .get_non_send_resource::>()
+ .unwrap();
+
+ assert!(contexts.contexts.len() == 1);
+ }
+}
diff --git a/crates/bevy_mod_scripting_core/src/context.rs b/crates/bevy_mod_scripting_core/src/context.rs
index 04318645e9..8c338859fa 100644
--- a/crates/bevy_mod_scripting_core/src/context.rs
+++ b/crates/bevy_mod_scripting_core/src/context.rs
@@ -11,6 +11,7 @@ impl Context for T {}
pub type ContextId = u32;
+/// Stores script state for a scripting plugin. Scripts are identified by their `ScriptId`, while contexts are identified by their `ContextId`.
#[derive(Resource)]
pub struct ScriptContexts {
pub contexts: HashMap,
@@ -26,12 +27,6 @@ impl Default for ScriptContexts {
static CONTEXT_ID_COUNTER: AtomicU32 = AtomicU32::new(0);
impl ScriptContexts {
- pub fn new() -> Self {
- Self {
- contexts: HashMap::new(),
- }
- }
-
/// Allocates a new ContextId and inserts the context into the map
pub fn insert(&mut self, ctxt: P::C) -> ContextId {
let id = CONTEXT_ID_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
@@ -39,6 +34,10 @@ impl ScriptContexts {
id
}
+ pub fn insert_with_id(&mut self, id: ContextId, ctxt: P::C) -> Option {
+ self.contexts.insert(id, ctxt)
+ }
+
/// Allocate new context id without inserting a context
pub fn allocate_id(&self) -> ContextId {
CONTEXT_ID_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
@@ -55,39 +54,34 @@ impl ScriptContexts {
pub fn get_mut(&mut self, id: ContextId) -> Option<&mut P::C> {
self.contexts.get_mut(&id)
}
+
+ pub fn contains(&self, id: ContextId) -> bool {
+ self.contexts.contains_key(&id)
+ }
}
-/// Initializer run once after creating a context but before executing it for the first time
+/// Initializer run once after creating a context but before executing it for the first time as well as after re-loading the script
pub type ContextInitializer
=
fn(&str, &mut
::C) -> Result<(), ScriptError>;
-/// Initializer run every time before executing or loading a script
+
+/// Initializer run every time before executing or loading/re-loading a script
pub type ContextPreHandlingInitializer
=
fn(&str, Entity, &mut
::C) -> Result<(), ScriptError>;
+/// Settings concerning the creation and assignment of script contexts as well as their initialization.
#[derive(Resource)]
pub struct ContextLoadingSettings {
/// Defines the strategy used to load and reload contexts
- pub loader: Option>,
+ pub loader: ContextBuilder,
/// Defines the strategy used to assign contexts to scripts
- pub assigner: Option>,
+ pub assigner: ContextAssigner,
/// Initializers run once after creating a context but before executing it for the first time
pub context_initializers: Vec>,
/// Initializers run every time before executing or loading a script
pub context_pre_handling_initializers: Vec>,
}
-impl Default for ContextLoadingSettings {
- fn default() -> Self {
- Self {
- loader: None,
- assigner: None,
- context_initializers: Default::default(),
- context_pre_handling_initializers: Default::default(),
- }
- }
-}
-
-impl Clone for ContextLoadingSettings {
+impl Clone for ContextLoadingSettings {
fn clone(&self) -> Self {
Self {
loader: self.loader.clone(),
@@ -130,30 +124,52 @@ impl Clone for ContextBuilder {
/// A strategy for assigning contexts to new and existing but re-loaded scripts as well as for managing old contexts
pub struct ContextAssigner {
- /// Assign a context to the script, if script is `None`, this is a new script, otherwise it is an existing script with a context inside `contexts`.
- /// Returning None means the script should be assigned a new context
+ /// Assign a context to the script.
+ /// The assigner can either return `Some(id)` or `None`.
+ /// Returning None will request the processor to assign a new context id to assign to this script.
+ ///
+ /// Regardless, whether a script gets a new context id or not, the processor will check if the given context exists.
+ /// If it does not exist, it will create a new context and assign it to the script.
+ /// If it does exist, it will NOT create a new context, but assign the existing one to the script, and re-load the context.
+ ///
+ /// This function is only called once for each script, when it is loaded for the first time.
pub assign: fn(
- old_script: Option<&Script>,
script_id: &ScriptId,
new_content: &[u8],
contexts: &ScriptContexts,
) -> Option,
/// Handle the removal of the script, if any clean up in contexts is necessary perform it here.
- /// This will also be called, when a script is assigned a contextId on reload different from the previous one
- /// the context_id in that case will be the old context_id and the one stored in the script will be the old one
+ ///
+ /// If you do not clean up the context here, it will stay in the context map!
pub remove: fn(context_id: ContextId, script: &Script, contexts: &mut ScriptContexts),
}
-impl Default for ContextAssigner {
- fn default() -> Self {
+impl ContextAssigner {
+ /// Create an assigner which re-uses a single global context for all scripts, only use if you know what you're doing.
+ /// Will not perform any clean up on removal.
+ pub fn new_global_context_assigner() -> Self {
+ Self {
+ assign: |_, _, _| Some(0), // always use the same id in rotation
+ remove: |_, _, _| {}, // do nothing
+ }
+ }
+
+ /// Create an assigner which assigns a new context to each script. This is the default strategy.
+ pub fn new_individual_context_assigner() -> Self {
Self {
- assign: |old, _, _, _| old.map(|s| s.context_id),
+ assign: |_, _, _| None,
remove: |id, _, c| _ = c.remove(id),
}
}
}
+impl Default for ContextAssigner {
+ fn default() -> Self {
+ Self::new_individual_context_assigner()
+ }
+}
+
impl Clone for ContextAssigner {
fn clone(&self) -> Self {
Self {
@@ -162,3 +178,54 @@ impl Clone for ContextAssigner {
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use crate::asset::Language;
+
+ use super::*;
+
+ struct DummyParams;
+ impl IntoScriptPluginParams for DummyParams {
+ type C = String;
+ type R = ();
+
+ const LANGUAGE: Language = Language::Lua;
+
+ fn build_runtime() -> Self::R {
+ todo!()
+ }
+ }
+
+ #[test]
+ fn test_script_contexts_insert_get() {
+ let mut contexts: ScriptContexts = ScriptContexts::default();
+ let id = contexts.insert("context1".to_string());
+ assert_eq!(contexts.contexts.get(&id), Some(&"context1".to_string()));
+ assert_eq!(
+ contexts.contexts.get_mut(&id),
+ Some(&mut "context1".to_string())
+ );
+ }
+
+ #[test]
+ fn test_script_contexts_allocate_id() {
+ let contexts: ScriptContexts = ScriptContexts::default();
+ let id = contexts.allocate_id();
+ let next_id = contexts.allocate_id();
+ assert_eq!(next_id, id + 1);
+ }
+
+ #[test]
+ fn test_script_contexts_remove() {
+ let mut contexts: ScriptContexts = ScriptContexts::default();
+ let id = contexts.insert("context1".to_string());
+ let removed = contexts.remove(id);
+ assert_eq!(removed, Some("context1".to_string()));
+ assert!(!contexts.contexts.contains_key(&id));
+
+ // assert next id is still incremented
+ let next_id = contexts.allocate_id();
+ assert_eq!(next_id, id + 1);
+ }
+}
diff --git a/crates/bevy_mod_scripting_core/src/docs.rs b/crates/bevy_mod_scripting_core/src/docs.rs
deleted file mode 100644
index d186bcfe96..0000000000
--- a/crates/bevy_mod_scripting_core/src/docs.rs
+++ /dev/null
@@ -1,24 +0,0 @@
-use bevy::ecs::system::Resource;
-
-/// A documentation piece which can be used to make a piece of documentation, most often a module.
-pub trait DocumentationFragment: 'static + Sized {
- /// Merges two documentation fragments into one, retaining the title of the first fragment.
- fn merge(self, o: Self) -> Self;
- fn gen_docs(self) -> Result<(), Box>;
-
- /// Retrieves the name of the documentation fragment, most likely the name of your game!
- fn name(&self) -> &'static str;
-}
-
-#[derive(Resource)]
-pub struct Documentation {
- pub fragments: Vec,
-}
-
-impl Default for Documentation {
- fn default() -> Self {
- Self {
- fragments: Default::default(),
- }
- }
-}
diff --git a/crates/bevy_mod_scripting_core/src/error.rs b/crates/bevy_mod_scripting_core/src/error.rs
index 36776aa393..bb14ac623d 100644
--- a/crates/bevy_mod_scripting_core/src/error.rs
+++ b/crates/bevy_mod_scripting_core/src/error.rs
@@ -584,78 +584,220 @@ impl PartialEq for InteropErrorInner {
}
}
+macro_rules! missing_function_error {
+ ($function_name:expr, $on:expr) => {
+ format!(
+ "Could not find function: {} for type: {}",
+ $function_name, $on
+ )
+ };
+}
+
+macro_rules! unregistered_base {
+ ($base:expr) => {
+ format!("Unregistered base type: {}", $base)
+ };
+}
+
+macro_rules! cannot_claim_access {
+ ($base:expr, $location:expr) => {
+ format!(
+ "Cannot claim access to base type: {}. The base is already claimed by something else in a way which prevents safe access. Location: {}",
+ $base, $location
+ )
+ };
+}
+
+macro_rules! impossible_conversion {
+ ($into:expr) => {
+ format!("Cannot convert to type: {}", $into)
+ };
+}
+
+macro_rules! type_mismatch {
+ ($expected:expr, $got:expr) => {
+ format!("Type mismatch, expected: {}, got: {}", $expected, $got)
+ };
+}
+
+macro_rules! string_type_mismatch {
+ ($expected:expr, $got:expr) => {
+ format!("Type mismatch, expected: {}, got: {}", $expected, $got)
+ };
+}
+
+macro_rules! could_not_downcast {
+ ($from:expr, $to:expr) => {
+ format!("Could not downcast from: {} to: {}", $from, $to)
+ };
+}
+
+macro_rules! garbage_collected_allocation {
+ ($reference:expr) => {
+ format!(
+ "Allocation was garbage collected. Could not access reference: {} as a result.",
+ $reference
+ )
+ };
+}
+
+macro_rules! reflection_path_error {
+ ($error:expr, $reference:expr) => {
+ format!(
+ "Error while reflecting path: {} on reference: {}",
+ $error, $reference
+ )
+ };
+}
+
+macro_rules! missing_type_data {
+ ($type_data:expr, $type_id:expr) => {
+ format!(
+ "Missing type data {} for type: {}. Did you register the type correctly?",
+ $type_data, $type_id
+ )
+ };
+}
+
+macro_rules! failed_from_reflect {
+ ($type_id:expr, $reason:expr) => {
+ format!(
+ "Failed to convert from reflect for type: {} with reason: {}",
+ $type_id, $reason
+ )
+ };
+}
+
+macro_rules! value_mismatch {
+ ($expected:expr, $got:expr) => {
+ format!("Value mismatch, expected: {}, got: {}", $expected, $got)
+ };
+}
+
+macro_rules! unsupported_operation {
+ ($operation:expr, $base:expr, $value:expr) => {
+ format!(
+ "Unsupported operation: {} on base: {} with value: {:?}",
+ $operation, $base, $value
+ )
+ };
+}
+
+macro_rules! invalid_index {
+ ($value:expr, $reason:expr) => {
+ format!("Invalid index for value: {}: {}", $value, $reason)
+ };
+}
+
+macro_rules! missing_entity {
+ ($entity:expr) => {
+ format!("Missing or invalid entity: {}", $entity)
+ };
+}
+
+macro_rules! invalid_component {
+ ($component_id:expr) => {
+ format!("Invalid component: {:?}", $component_id)
+ };
+}
+
+macro_rules! function_interop_error {
+ ($display_name:expr, $opt_on:expr, $error:expr) => {
+ format!(
+ "Error in function {} {}: {}",
+ $display_name, $opt_on, $error
+ )
+ };
+}
+
+macro_rules! function_arg_conversion_error {
+ ($argument:expr, $error:expr) => {
+ format!("Error converting argument {}: {}", $argument, $error)
+ };
+}
+
+macro_rules! function_call_error {
+ ($inner:expr) => {
+ format!("Error in function call: {}", $inner)
+ };
+}
+
+macro_rules! better_conversion_exists {
+ ($context:expr) => {
+ format!("Unfinished conversion in context of: {}. A better conversion exists but caller didn't handle the case.", $context)
+ };
+}
+
+macro_rules! length_mismatch {
+ ($expected:expr, $got:expr) => {
+ format!(
+ "Array/List Length mismatch, expected: {}, got: {}",
+ $expected, $got
+ )
+ };
+}
+
+macro_rules! invalid_access_count {
+ ($expected:expr, $count:expr, $context:expr) => {
+ format!(
+ "Invalid access count, expected: {}, got: {}. {}",
+ $expected, $count, $context
+ )
+ };
+}
+
impl DisplayWithWorld for InteropErrorInner {
fn display_with_world(&self, world: crate::bindings::WorldGuard) -> String {
match self {
InteropErrorInner::MissingFunctionError { on, function_name } => {
- format!(
- "Could not find function: {} for type: {}",
- function_name,
- on.display_with_world(world)
- )
+ missing_function_error!(function_name, on.display_with_world(world))
},
InteropErrorInner::UnregisteredBase { base } => {
- format!("Unregistered base type: {}", base.display_with_world(world))
+ unregistered_base!(base.display_with_world(world))
}
InteropErrorInner::CannotClaimAccess { base, location } => {
- format!(
- "Cannot claim access to base type: {}. The base is already claimed by something else in a way which prevents safe access. Location: {}",
- base.display_with_world(world),
- location.display_location()
- )
+ cannot_claim_access!(base.display_with_world(world), location.display_location())
}
InteropErrorInner::ImpossibleConversion { into } => {
- format!("Cannot convert to type: {}", into.display_with_world(world))
+ impossible_conversion!(into.display_with_world(world))
}
InteropErrorInner::TypeMismatch { expected, got } => {
- format!(
- "Type mismatch, expected: {}, got: {}",
+ type_mismatch!(
expected.display_with_world(world.clone()),
got.map(|t| t.display_with_world(world))
.unwrap_or("None".to_owned())
)
}
InteropErrorInner::StringTypeMismatch { expected, got } => {
- format!(
- "Type mismatch, expected: {}, got: {}",
+ string_type_mismatch!(
expected,
got.map(|t| t.display_with_world(world))
.unwrap_or("None".to_owned())
)
}
InteropErrorInner::CouldNotDowncast { from, to } => {
- format!(
- "Could not downcast from: {} to: {}",
+ could_not_downcast!(
from.display_with_world(world.clone()),
to.display_with_world(world)
)
}
InteropErrorInner::GarbageCollectedAllocation { reference } => {
- format!(
- "Allocation was garbage collected. Could not access reference: {} as a result.",
- reference.display_with_world(world),
- )
+ garbage_collected_allocation!(reference.display_with_world(world))
}
InteropErrorInner::ReflectionPathError { error, reference } => {
- format!(
- "Error while reflecting path: {} on reference: {}",
+ reflection_path_error!(
error,
reference
.as_ref()
.map(|r| r.display_with_world(world))
- .unwrap_or("None".to_owned()),
+ .unwrap_or("None".to_owned())
)
}
InteropErrorInner::MissingTypeData { type_id, type_data } => {
- format!(
- "Missing type data {} for type: {}. Did you register the type correctly?",
- type_data,
- type_id.display_with_world(world),
- )
+ missing_type_data!(type_data, type_id.display_with_world(world))
}
InteropErrorInner::FailedFromReflect { type_id, reason } => {
- format!(
- "Failed to convert from reflect for type: {} with reason: {}",
+ failed_from_reflect!(
type_id
.map(|t| t.display_with_world(world))
.unwrap_or("None".to_owned()),
@@ -663,8 +805,7 @@ impl DisplayWithWorld for InteropErrorInner {
)
}
InteropErrorInner::ValueMismatch { expected, got } => {
- format!(
- "Value mismatch, expected: {}, got: {}",
+ value_mismatch!(
expected.display_with_world(world.clone()),
got.display_with_world(world)
)
@@ -674,8 +815,7 @@ impl DisplayWithWorld for InteropErrorInner {
value,
operation,
} => {
- format!(
- "Unsupported operation: {} on base: {} with value: {:?}",
+ unsupported_operation!(
operation,
base.map(|t| t.display_with_world(world))
.unwrap_or("None".to_owned()),
@@ -683,17 +823,13 @@ impl DisplayWithWorld for InteropErrorInner {
)
}
InteropErrorInner::InvalidIndex { value, reason } => {
- format!(
- "Invalid index for value: {}: {}",
- value.display_with_world(world),
- reason
- )
+ invalid_index!(value.display_with_world(world), reason)
}
InteropErrorInner::MissingEntity { entity } => {
- format!("Missing or invalid entity: {}", entity)
+ missing_entity!(entity)
}
InteropErrorInner::InvalidComponent { component_id } => {
- format!("Invalid component: {:?}", component_id)
+ invalid_component!(component_id)
}
InteropErrorInner::StaleWorldAccess => {
"Stale world access. The world has been dropped and a script tried to access it. Do not try to store or copy the world."
@@ -712,32 +848,23 @@ impl DisplayWithWorld for InteropErrorInner {
} else {
function_name.as_str()
};
- format!(
- "Error in function {} {}: {}",
- display_name,
- opt_on,
- error.display_with_world(world),
- )
+ function_interop_error!(display_name, opt_on, error.display_with_world(world))
},
InteropErrorInner::FunctionArgConversionError { argument, error } => {
- format!(
- "Error converting argument {}: {}",
- argument,
- error.display_with_world(world)
- )
+ function_arg_conversion_error!(argument, error.display_with_world(world))
},
InteropErrorInner::FunctionCallError { inner } => {
- format!("Error in function call: {}", inner)
+ function_call_error!(inner)
},
InteropErrorInner::BetterConversionExists{ context } => {
- format!("Unfinished conversion in context of: {}. A better conversion exists but caller didn't handle the case.", context)
+ better_conversion_exists!(context)
},
InteropErrorInner::OtherError { error } => error.to_string(),
InteropErrorInner::LengthMismatch { expected, got } => {
- format!("Array/List Length mismatch, expected: {}, got: {}", expected, got)
+ length_mismatch!(expected, got)
},
InteropErrorInner::InvalidAccessCount { count, expected, context } => {
- format!("Invalid access count, expected: {}, got: {}. {}", expected, count, context)
+ invalid_access_count!(expected, count, context)
},
}
}
@@ -746,74 +873,54 @@ impl DisplayWithWorld for InteropErrorInner {
fn display_without_world(&self) -> String {
match self {
InteropErrorInner::MissingFunctionError { on, function_name } => {
- format!(
- "Could not find function: {} for type: {}",
- function_name,
- on.display_without_world()
- )
+ missing_function_error!(function_name, on.display_without_world())
},
InteropErrorInner::UnregisteredBase { base } => {
- format!("Unregistered base type: {}", base.display_without_world())
+ unregistered_base!(base.display_without_world())
}
InteropErrorInner::CannotClaimAccess { base, location } => {
- format!(
- "Cannot claim access to base type: {}. The base is already claimed by something else in a way which prevents safe access. Location: {}",
- base.display_without_world(),
- location.display_location()
- )
+ cannot_claim_access!(base.display_without_world(), location.display_location())
}
InteropErrorInner::ImpossibleConversion { into } => {
- format!("Cannot convert to type: {}", into.display_without_world())
+ impossible_conversion!(into.display_without_world())
}
InteropErrorInner::TypeMismatch { expected, got } => {
- format!(
- "Type mismatch, expected: {}, got: {}",
+ type_mismatch!(
expected.display_without_world(),
got.map(|t| t.display_without_world())
.unwrap_or("None".to_owned())
)
}
InteropErrorInner::StringTypeMismatch { expected, got } => {
- format!(
- "Type mismatch, expected: {}, got: {}",
+ string_type_mismatch!(
expected,
got.map(|t| t.display_without_world())
.unwrap_or("None".to_owned())
)
}
InteropErrorInner::CouldNotDowncast { from, to } => {
- format!(
- "Could not downcast from: {} to: {}",
+ could_not_downcast!(
from.display_without_world(),
to.display_without_world()
)
}
InteropErrorInner::GarbageCollectedAllocation { reference } => {
- format!(
- "Allocation was garbage collected. Could not access reference: {} as a result.",
- reference.display_without_world(),
- )
+ garbage_collected_allocation!(reference.display_without_world())
}
InteropErrorInner::ReflectionPathError { error, reference } => {
- format!(
- "Error while reflecting path: {} on reference: {}",
+ reflection_path_error!(
error,
reference
.as_ref()
.map(|r| r.display_without_world())
- .unwrap_or("None".to_owned()),
+ .unwrap_or("None".to_owned())
)
}
InteropErrorInner::MissingTypeData { type_id, type_data } => {
- format!(
- "Missing type data {} for type: {}. Did you register the type correctly?",
- type_data,
- type_id.display_without_world(),
- )
+ missing_type_data!(type_data, type_id.display_without_world())
}
InteropErrorInner::FailedFromReflect { type_id, reason } => {
- format!(
- "Failed to convert from reflect for type: {} with reason: {}",
+ failed_from_reflect!(
type_id
.map(|t| t.display_without_world())
.unwrap_or("None".to_owned()),
@@ -821,8 +928,7 @@ impl DisplayWithWorld for InteropErrorInner {
)
}
InteropErrorInner::ValueMismatch { expected, got } => {
- format!(
- "Value mismatch, expected: {}, got: {}",
+ value_mismatch!(
expected.display_without_world(),
got.display_without_world()
)
@@ -832,8 +938,7 @@ impl DisplayWithWorld for InteropErrorInner {
value,
operation,
} => {
- format!(
- "Unsupported operation: {} on base: {} with value: {:?}",
+ unsupported_operation!(
operation,
base.map(|t| t.display_without_world())
.unwrap_or("None".to_owned()),
@@ -841,17 +946,13 @@ impl DisplayWithWorld for InteropErrorInner {
)
}
InteropErrorInner::InvalidIndex { value, reason } => {
- format!(
- "Invalid index for value: {}: {}",
- value.display_without_world(),
- reason
- )
+ invalid_index!(value.display_without_world(), reason)
}
InteropErrorInner::MissingEntity { entity } => {
- format!("Missing or invalid entity: {}", entity)
+ missing_entity!(entity)
}
InteropErrorInner::InvalidComponent { component_id } => {
- format!("Invalid component: {:?}", component_id)
+ invalid_component!(component_id)
}
InteropErrorInner::StaleWorldAccess => {
"Stale world access. The world has been dropped and a script tried to access it. Do not try to store or copy the world."
@@ -864,37 +965,29 @@ impl DisplayWithWorld for InteropErrorInner {
let opt_on = match on {
Namespace::Global => "".to_owned(),
Namespace::OnType(type_id) => format!("on type: {}", type_id.display_without_world()),
- }; let display_name = if function_name.starts_with("TypeId") {
+ };
+ let display_name = if function_name.starts_with("TypeId") {
function_name.split("::").last().unwrap()
} else {
function_name.as_str()
};
- format!(
- "Error in function {} {}: {}",
- display_name,
- opt_on,
- error.display_without_world(),
- )
+ function_interop_error!(display_name, opt_on, error.display_without_world())
},
InteropErrorInner::FunctionArgConversionError { argument, error } => {
- format!(
- "Error converting argument {}: {}",
- argument,
- error.display_without_world()
- )
+ function_arg_conversion_error!(argument, error.display_without_world())
},
InteropErrorInner::FunctionCallError { inner } => {
- format!("Error in function call: {}", inner)
+ function_call_error!(inner)
},
InteropErrorInner::BetterConversionExists{ context } => {
- format!("Unfinished conversion in context of: {}. A better conversion exists but caller didn't handle the case.", context)
+ better_conversion_exists!(context)
},
InteropErrorInner::OtherError { error } => error.to_string(),
InteropErrorInner::LengthMismatch { expected, got } => {
- format!("Array/List Length mismatch, expected: {}, got: {}", expected, got)
+ length_mismatch!(expected, got)
},
InteropErrorInner::InvalidAccessCount { count, expected, context } => {
- format!("Invalid access count, expected: {}, got: {}. {}", expected, count, context)
+ invalid_access_count!(expected, count, context)
},
}
}
@@ -906,3 +999,47 @@ impl Default for InteropErrorInner {
InteropErrorInner::StaleWorldAccess
}
}
+
+#[cfg(test)]
+mod test {
+ use bevy::prelude::{AppTypeRegistry, World};
+
+ use crate::bindings::{
+ function::script_function::AppScriptFunctionRegistry, AppReflectAllocator,
+ WorldAccessGuard, WorldGuard,
+ };
+
+ use super::*;
+
+ #[test]
+ fn test_error_display() {
+ let error =
+ InteropError::failed_from_reflect(Some(TypeId::of::()), "reason".to_owned());
+ let mut world = World::default();
+ let type_registry = AppTypeRegistry::default();
+ world.insert_resource(type_registry);
+
+ let script_allocator = AppReflectAllocator::default();
+ world.insert_resource(script_allocator);
+
+ let script_function_registry = AppScriptFunctionRegistry::default();
+ world.insert_resource(script_function_registry);
+
+ let world_guard = WorldGuard::new(WorldAccessGuard::new(&mut world));
+ assert_eq!(
+ error.display_with_world(world_guard),
+ format!(
+ "Failed to convert from reflect for type: {} with reason: reason",
+ std::any::type_name::()
+ )
+ );
+
+ assert_eq!(
+ error.display_without_world(),
+ format!(
+ "Failed to convert from reflect for type: {:?} with reason: reason",
+ TypeId::of::()
+ )
+ );
+ }
+}
diff --git a/crates/bevy_mod_scripting_core/src/handler.rs b/crates/bevy_mod_scripting_core/src/handler.rs
index dff84c585a..6145d4345b 100644
--- a/crates/bevy_mod_scripting_core/src/handler.rs
+++ b/crates/bevy_mod_scripting_core/src/handler.rs
@@ -1,8 +1,25 @@
+use std::any::type_name;
+
use crate::{
- bindings::script_value::ScriptValue, context::ContextPreHandlingInitializer,
- error::ScriptError, event::CallbackLabel, script::ScriptId, IntoScriptPluginParams,
+ bindings::{
+ pretty_print::DisplayWithWorld, script_value::ScriptValue, WorldAccessGuard, WorldGuard,
+ },
+ context::{ContextLoadingSettings, ContextPreHandlingInitializer, ScriptContexts},
+ error::ScriptError,
+ event::{CallbackLabel, IntoCallbackLabel, ScriptCallbackEvent, ScriptErrorEvent},
+ runtime::RuntimeContainer,
+ script::{ScriptComponent, ScriptId, Scripts},
+ IntoScriptPluginParams,
+};
+use bevy::{
+ ecs::{
+ entity::Entity,
+ system::{Resource, SystemState},
+ world::World,
+ },
+ log::{debug, trace},
+ prelude::{EventReader, Events, Query, Ref, Res},
};
-use bevy::ecs::{entity::Entity, system::Resource, world::World};
pub trait Args: Clone + Send + Sync + 'static {}
impl Args for T {}
@@ -21,13 +38,394 @@ pub type HandlerFn = fn(
/// A resource that holds the settings for the callback handler for a specific combination of type parameters
#[derive(Resource)]
pub struct CallbackSettings {
- pub callback_handler: Option>,
+ pub callback_handler: HandlerFn,
}
-impl Default for CallbackSettings {
- fn default() -> Self {
- Self {
- callback_handler: None,
+macro_rules! push_err_and_continue {
+ ($errors:ident, $expr:expr) => {
+ match $expr {
+ Ok(v) => v,
+ Err(e) => {
+ $errors.push(e);
+ continue;
+ }
}
+ };
+}
+
+/// Passes events with the specified label to the script callback with the same name and runs the callback
+pub fn event_handler(
+ world: &mut World,
+ params: &mut SystemState<(
+ EventReader,
+ Res>,
+ Res>,
+ Res,
+ Query<(Entity, Ref)>,
+ )>,
+) {
+ let mut runtime_container = world
+ .remove_non_send_resource::>()
+ .unwrap_or_else(|| {
+ panic!(
+ "No runtime container for runtime {} found. Was the scripting plugin initialized correctly?",
+ type_name::()
+ )
+ });
+ let runtime = &mut runtime_container.runtime;
+ let mut script_contexts = world
+ .remove_non_send_resource::>()
+ .unwrap_or_else(|| panic!("No script contexts found for context {}", type_name::()));
+
+ let (mut script_events, callback_settings, context_settings, scripts, entities) =
+ params.get_mut(world);
+
+ let handler = callback_settings.callback_handler;
+ let pre_handling_initializers = context_settings.context_pre_handling_initializers.clone();
+ let scripts = scripts.clone();
+ let mut errors = Vec::default();
+
+ let events = script_events.read().cloned().collect::>();
+ let entity_scripts = entities
+ .iter()
+ .map(|(e, s)| (e, s.0.clone()))
+ .collect::>();
+
+ for event in events
+ .into_iter()
+ .filter(|e| e.label == L::into_callback_label())
+ {
+ for (entity, entity_scripts) in entity_scripts.iter() {
+ for script_id in entity_scripts.iter() {
+ match &event.recipients {
+ crate::event::Recipients::Script(target_script_id)
+ if target_script_id != script_id =>
+ {
+ continue
+ }
+ crate::event::Recipients::Entity(target_entity) if target_entity != entity => {
+ continue
+ }
+ _ => (),
+ }
+ debug!(
+ "Handling event for script {} on entity {:?}",
+ script_id, entity
+ );
+ let script = match scripts.scripts.get(script_id) {
+ Some(s) => s,
+ None => {
+ trace!(
+ "Script `{}` on entity `{:?}` is either still loading or doesn't exist, ignoring.",
+ script_id, entity
+ );
+ continue;
+ }
+ };
+
+ let ctxt = match script_contexts.contexts.get_mut(&script.context_id) {
+ Some(ctxt) => ctxt,
+ None => {
+ // if we don't have a context for the script, it's either:
+ // 1. a script for a different language, in which case we ignore it
+ // 2. something went wrong. This should not happen though and it's best we ignore this
+ continue;
+ }
+ };
+
+ let handler_result = (handler)(
+ event.args.clone(),
+ *entity,
+ &script.id,
+ &L::into_callback_label(),
+ ctxt,
+ &pre_handling_initializers,
+ runtime,
+ world,
+ )
+ .map_err(|e| {
+ e.with_script(script.id.clone())
+ .with_context(format!("Event handling for: Language: {}", P::LANGUAGE))
+ });
+
+ let _ = push_err_and_continue!(errors, handler_result);
+ }
+ }
+ }
+
+ world.insert_non_send_resource(runtime_container);
+ world.insert_non_send_resource(script_contexts);
+
+ handle_script_errors(world, errors.into_iter());
+}
+
+/// Handles errors caused by script execution and sends them to the error event channel
+pub(crate) fn handle_script_errors + Clone>(
+ world: &mut World,
+ errors: I,
+) {
+ let mut error_events = world
+ .get_resource_mut::>()
+ .expect("Missing events resource");
+
+ for error in errors.clone() {
+ error_events.send(ScriptErrorEvent { error });
+ }
+
+ for error in errors {
+ let arc_world = WorldGuard::new(WorldAccessGuard::new(world));
+ bevy::log::error!("{}", error.display_with_world(arc_world));
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use std::{borrow::Cow, collections::HashMap};
+
+ use bevy::app::{App, Update};
+
+ use crate::{
+ bindings::script_value::ScriptValue,
+ context::{ContextAssigner, ContextBuilder, ContextLoadingSettings, ScriptContexts},
+ event::{CallbackLabel, IntoCallbackLabel, ScriptCallbackEvent, ScriptErrorEvent},
+ handler::HandlerFn,
+ runtime::RuntimeContainer,
+ script::{Script, ScriptComponent, ScriptId, Scripts},
+ };
+
+ use super::*;
+ struct OnTestCallback;
+
+ impl IntoCallbackLabel for OnTestCallback {
+ fn into_callback_label() -> CallbackLabel {
+ "OnTest".into()
+ }
+ }
+
+ struct TestPlugin;
+
+ impl IntoScriptPluginParams for TestPlugin {
+ type C = TestContext;
+ type R = TestRuntime;
+
+ const LANGUAGE: crate::asset::Language = crate::asset::Language::Unknown;
+
+ fn build_runtime() -> Self::R {
+ TestRuntime {
+ invocations: vec![],
+ }
+ }
+ }
+
+ struct TestRuntime {
+ pub invocations: Vec<(Entity, ScriptId)>,
+ }
+
+ struct TestContext {
+ pub invocations: Vec,
+ }
+
+ fn setup_app(
+ handler_fn: HandlerFn,
+ runtime: P::R,
+ contexts: HashMap,
+ scripts: HashMap,
+ ) -> App {
+ let mut app = App::new();
+
+ app.add_event::();
+ app.add_event::();
+ app.insert_resource::>(CallbackSettings {
+ callback_handler: handler_fn,
+ });
+ app.add_systems(Update, event_handler::);
+ app.insert_resource::(Scripts { scripts });
+ app.insert_non_send_resource(RuntimeContainer:: { runtime });
+ app.insert_non_send_resource(ScriptContexts::
{ contexts });
+ app.insert_resource(ContextLoadingSettings::
{
+ loader: ContextBuilder {
+ load: |_, _, _, _, _, _| todo!(),
+ reload: |_, _, _, _, _, _, _| todo!(),
+ },
+ assigner: ContextAssigner {
+ assign: |_, _, _| todo!(),
+ remove: |_, _, _| todo!(),
+ },
+ context_initializers: vec![],
+ context_pre_handling_initializers: vec![],
+ });
+ app.finish();
+ app.cleanup();
+ app
+ }
+
+ #[test]
+ fn test_handler_called_with_right_args() {
+ let test_script_id = Cow::Borrowed("test_script");
+ let test_ctxt_id = 0;
+ let test_script = Script {
+ id: test_script_id.clone(),
+ asset: None,
+ context_id: test_ctxt_id,
+ };
+ let scripts = HashMap::from_iter(vec![(test_script_id.clone(), test_script.clone())]);
+ let contexts = HashMap::from_iter(vec![(
+ test_ctxt_id,
+ TestContext {
+ invocations: vec![],
+ },
+ )]);
+ let runtime = TestRuntime {
+ invocations: vec![],
+ };
+ let mut app = setup_app::(
+ |args, entity, script, _, ctxt, _, runtime, _| {
+ ctxt.invocations.extend(args);
+ runtime.invocations.push((entity, script.clone()));
+ Ok(ScriptValue::Unit)
+ },
+ runtime,
+ contexts,
+ scripts,
+ );
+ let test_entity_id = app
+ .world_mut()
+ .spawn(ScriptComponent(vec![test_script_id.clone()]))
+ .id();
+
+ app.world_mut().send_event(ScriptCallbackEvent::new_for_all(
+ OnTestCallback::into_callback_label(),
+ vec![ScriptValue::String("test_args".into())],
+ ));
+ app.update();
+
+ let test_context = app
+ .world()
+ .get_non_send_resource::>()
+ .unwrap();
+ let test_runtime = app
+ .world()
+ .get_non_send_resource::>()
+ .unwrap();
+
+ assert_eq!(
+ test_context
+ .contexts
+ .get(&test_ctxt_id)
+ .unwrap()
+ .invocations,
+ vec![ScriptValue::String("test_args".into())]
+ );
+
+ assert_eq!(
+ test_runtime
+ .runtime
+ .invocations
+ .iter()
+ .map(|(e, s)| (*e, s.clone()))
+ .collect::>(),
+ vec![(test_entity_id, test_script_id.clone())]
+ );
+ }
+
+ #[test]
+ fn test_handler_called_on_right_recipients() {
+ let test_script_id = Cow::Borrowed("test_script");
+ let test_ctxt_id = 0;
+ let test_script = Script {
+ id: test_script_id.clone(),
+ asset: None,
+ context_id: test_ctxt_id,
+ };
+ let scripts = HashMap::from_iter(vec![
+ (test_script_id.clone(), test_script.clone()),
+ (
+ "wrong".into(),
+ Script {
+ id: "wrong".into(),
+ asset: None,
+ context_id: 1,
+ },
+ ),
+ ]);
+ let contexts = HashMap::from_iter(vec![
+ (
+ test_ctxt_id,
+ TestContext {
+ invocations: vec![],
+ },
+ ),
+ (
+ 1,
+ TestContext {
+ invocations: vec![],
+ },
+ ),
+ ]);
+ let runtime = TestRuntime {
+ invocations: vec![],
+ };
+ let mut app = setup_app::(
+ |args, entity, script, _, ctxt, _, runtime, _| {
+ ctxt.invocations.extend(args);
+ runtime.invocations.push((entity, script.clone()));
+ Ok(ScriptValue::Unit)
+ },
+ runtime,
+ contexts,
+ scripts,
+ );
+ let test_entity_id = app
+ .world_mut()
+ .spawn(ScriptComponent(vec![test_script_id.clone()]))
+ .id();
+
+ app.world_mut().send_event(ScriptCallbackEvent::new(
+ OnTestCallback::into_callback_label(),
+ vec![ScriptValue::String("test_args_script".into())],
+ crate::event::Recipients::Script(test_script_id.clone()),
+ ));
+
+ app.world_mut().send_event(ScriptCallbackEvent::new(
+ OnTestCallback::into_callback_label(),
+ vec![ScriptValue::String("test_args_entity".into())],
+ crate::event::Recipients::Entity(test_entity_id),
+ ));
+
+ app.update();
+
+ let test_context = app
+ .world()
+ .get_non_send_resource::>()
+ .unwrap();
+ let test_runtime = app
+ .world()
+ .get_non_send_resource::>()
+ .unwrap();
+
+ assert_eq!(
+ test_context
+ .contexts
+ .get(&test_ctxt_id)
+ .unwrap()
+ .invocations,
+ vec![
+ ScriptValue::String("test_args_script".into()),
+ ScriptValue::String("test_args_entity".into())
+ ]
+ );
+
+ assert_eq!(
+ test_runtime
+ .runtime
+ .invocations
+ .iter()
+ .map(|(e, s)| (*e, s.clone()))
+ .collect::>(),
+ vec![
+ (test_entity_id, test_script_id.clone()),
+ (test_entity_id, test_script_id.clone())
+ ]
+ );
}
}
diff --git a/crates/bevy_mod_scripting_core/src/lib.rs b/crates/bevy_mod_scripting_core/src/lib.rs
index a25e3b6e0a..64ec4ce5ad 100644
--- a/crates/bevy_mod_scripting_core/src/lib.rs
+++ b/crates/bevy_mod_scripting_core/src/lib.rs
@@ -1,41 +1,50 @@
use crate::event::ScriptErrorEvent;
use asset::{
- AssetPathToLanguageMapper, Language, ScriptAsset, ScriptAssetLoader, ScriptAssetSettings,
- ScriptMetadataStore,
+ configure_asset_systems, configure_asset_systems_for_plugin, AssetPathToLanguageMapper,
+ Language, ScriptAsset, ScriptAssetLoader, ScriptAssetSettings,
};
use bevy::prelude::*;
use bindings::{
- function::script_function::AppScriptFunctionRegistry, script_value::ScriptValue,
- AppReflectAllocator, ReflectAllocator, ReflectReference, ScriptTypeRegistration,
- WorldCallbackAccess,
+ function::script_function::AppScriptFunctionRegistry, garbage_collector,
+ script_value::ScriptValue, AppReflectAllocator, ReflectAllocator, ReflectReference,
+ ScriptTypeRegistration, WorldCallbackAccess,
};
use context::{
Context, ContextAssigner, ContextBuilder, ContextInitializer, ContextLoadingSettings,
ContextPreHandlingInitializer, ScriptContexts,
};
-use docs::{Documentation, DocumentationFragment};
use event::ScriptCallbackEvent;
use handler::{CallbackSettings, HandlerFn};
-
-use runtime::{Runtime, RuntimeContainer, RuntimeInitializer, RuntimeSettings};
+use runtime::{initialize_runtime, Runtime, RuntimeContainer, RuntimeInitializer, RuntimeSettings};
use script::Scripts;
-use systems::{
- garbage_collector, initialize_runtime, insert_script_metadata, remove_script_metadata,
- sync_script_data, ScriptingSystemSet,
-};
pub mod asset;
pub mod bindings;
pub mod commands;
pub mod context;
-pub mod docs;
pub mod error;
pub mod event;
pub mod handler;
pub mod reflection_extensions;
pub mod runtime;
pub mod script;
-pub mod systems;
+
+#[derive(SystemSet, Hash, Debug, Eq, PartialEq, Clone)]
+/// Labels for various BMS systems
+pub enum ScriptingSystemSet {
+ /// Systems which handle the processing of asset events for script assets, and dispatching internal script asset events
+ ScriptAssetDispatch,
+ /// Systems which read incoming internal script asset events and produce script lifecycle commands
+ ScriptCommandDispatch,
+ /// Systems which read incoming script asset events and remove metadata for removed assets
+ ScriptMetadataRemoval,
+
+ /// One time runtime initialization systems
+ RuntimeInitialization,
+
+ /// Systems which handle the garbage collection of allocated values
+ GarbageCollection,
+}
/// Types which act like scripting plugins, by selecting a context and runtime
/// Each individual combination of context and runtime has specific infrastructure built for it and does not interact with other scripting plugins
@@ -45,21 +54,20 @@ pub trait IntoScriptPluginParams: 'static {
type R: Runtime;
fn build_runtime() -> Self::R;
-
- // fn supported_language() -> Language;
}
/// Bevy plugin enabling scripting within the bevy mod scripting framework
pub struct ScriptingPlugin {
/// Settings for the runtime
- pub runtime_settings: Option>,
+ pub runtime_settings: RuntimeSettings,
/// The handler used for executing callbacks in scripts
- pub callback_handler: Option>,
+ pub callback_handler: HandlerFn,
/// The context builder for loading contexts
- pub context_builder: Option>,
- /// The context assigner for assigning contexts to scripts, if not provided default strategy of keeping each script in its own context is used
- pub context_assigner: Option>,
- pub language_mapper: Option,
+ pub context_builder: ContextBuilder,
+ /// The context assigner for assigning contexts to scripts.
+ pub context_assigner: ContextAssigner
,
+
+ pub language_mapper: AssetPathToLanguageMapper,
/// initializers for the contexts, run when loading the script
pub context_initializers: Vec>,
@@ -67,26 +75,9 @@ pub struct ScriptingPlugin {
pub context_pre_handling_initializers: Vec>,
}
-impl Default for ScriptingPlugin
-where
- P::R: Default,
-{
- fn default() -> Self {
- Self {
- runtime_settings: Default::default(),
- callback_handler: Default::default(),
- context_builder: Default::default(),
- context_assigner: Default::default(),
- language_mapper: Default::default(),
- context_initializers: Default::default(),
- context_pre_handling_initializers: Default::default(),
- }
- }
-}
-
impl Plugin for ScriptingPlugin {
fn build(&self, app: &mut bevy::prelude::App) {
- app.insert_resource(self.runtime_settings.as_ref().cloned().unwrap_or_default())
+ app.insert_resource(self.runtime_settings.clone())
.insert_non_send_resource::>(RuntimeContainer {
runtime: P::build_runtime(),
})
@@ -96,38 +87,28 @@ impl Plugin for ScriptingPlugin {
})
.insert_resource::>(ContextLoadingSettings {
loader: self.context_builder.clone(),
- assigner: Some(self.context_assigner.clone().unwrap_or_default()),
- context_initializers: vec![],
- context_pre_handling_initializers: vec![],
+ assigner: self.context_assigner.clone(),
+ context_initializers: self.context_initializers.clone(),
+ context_pre_handling_initializers: self.context_pre_handling_initializers.clone(),
});
register_script_plugin_systems::(app);
once_per_app_init(app);
- if let Some(language_mapper) = &self.language_mapper {
- app.world_mut()
- .resource_mut::()
- .as_mut()
- .script_language_mappers
- .push(*language_mapper);
- }
+ app.world_mut()
+ .resource_mut::()
+ .as_mut()
+ .script_language_mappers
+ .push(self.language_mapper);
register_types(app);
-
- for initializer in self.context_initializers.iter() {
- app.add_context_initializer::(*initializer);
- }
-
- for initializer in self.context_pre_handling_initializers.iter() {
- app.add_context_pre_handling_initializer::
(*initializer);
- }
}
}
impl ScriptingPlugin {
/// Adds a context initializer to the plugin
///
- /// Initializers will be run every time a context is loaded or re-loaded
+ /// Initializers will be run every time a context is loaded or re-loaded and before any events are handled
pub fn add_context_initializer(&mut self, initializer: ContextInitializer
) -> &mut Self {
self.context_initializers.push(initializer);
self
@@ -135,7 +116,7 @@ impl ScriptingPlugin {
/// Adds a context pre-handling initializer to the plugin.
///
- /// Initializers will be run every time before handling events.
+ /// Initializers will be run every time before handling events and after the context is loaded or re-loaded.
pub fn add_context_pre_handling_initializer(
&mut self,
initializer: ContextPreHandlingInitializer
,
@@ -148,10 +129,41 @@ impl ScriptingPlugin {
///
/// Initializers will be run after the runtime is created, but before any contexts are loaded.
pub fn add_runtime_initializer(&mut self, initializer: RuntimeInitializer
) -> &mut Self {
- self.runtime_settings
- .get_or_insert_with(Default::default)
- .initializers
- .push(initializer);
+ self.runtime_settings.initializers.push(initializer);
+ self
+ }
+}
+
+/// Utility trait for configuring all scripting plugins.
+pub trait ConfigureScriptPlugin {
+ type P: IntoScriptPluginParams;
+ fn add_context_initializer(self, initializer: ContextInitializer) -> Self;
+ fn add_context_pre_handling_initializer(
+ self,
+ initializer: ContextPreHandlingInitializer,
+ ) -> Self;
+ fn add_runtime_initializer(self, initializer: RuntimeInitializer) -> Self;
+}
+
+impl>> ConfigureScriptPlugin for P {
+ type P = P;
+
+ fn add_context_initializer(mut self, initializer: ContextInitializer) -> Self {
+ self.as_mut().add_context_initializer(initializer);
+ self
+ }
+
+ fn add_context_pre_handling_initializer(
+ mut self,
+ initializer: ContextPreHandlingInitializer,
+ ) -> Self {
+ self.as_mut()
+ .add_context_pre_handling_initializer(initializer);
+ self
+ }
+
+ fn add_runtime_initializer(mut self, initializer: RuntimeInitializer