diff --git a/Cargo.toml b/Cargo.toml index 4b5f2cfcb6..e95b105da1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ features = ["lua54", "rhai"] default = ["core_functions", "bevy_bindings"] ## lua -lua = ["bevy_mod_scripting_lua"] +lua = ["bevy_mod_scripting_lua", "bevy_mod_scripting_functions/lua_bindings"] # one of these must be selected lua51 = ["bevy_mod_scripting_lua/lua51", "lua"] lua52 = ["bevy_mod_scripting_lua/lua52", "lua"] @@ -45,7 +45,7 @@ mlua_async = ["bevy_mod_scripting_lua?/mlua_async"] ## rhai -rhai = ["bevy_mod_scripting_rhai"] +rhai = ["bevy_mod_scripting_rhai", "bevy_mod_scripting_functions/rhai_bindings"] ## rune # rune = ["bevy_mod_scripting_rune"] diff --git a/assets/tests/add_system/adds_system_in_correct_order.lua b/assets/tests/add_system/adds_system_in_correct_order.lua new file mode 100644 index 0000000000..36e0e1e88f --- /dev/null +++ b/assets/tests/add_system/adds_system_in_correct_order.lua @@ -0,0 +1,48 @@ +-- add two systems, one before and one after the existing `on_test_post_update` callback, then assert all systems have run +-- in the `on_test_last` callback + +local runs = {} + +-- runs on `Update` +function on_test() + local post_update_schedule = world.get_schedule_by_name("PostUpdate") + + local test_system = post_update_schedule:get_system_by_name("on_test_post_update") + + local system_after = world.add_system( + post_update_schedule, + system_builder("custom_system_after", script_id) + :after(test_system) + ) + + local system_before = world.add_system( + post_update_schedule, + system_builder("custom_system_before", script_id) + :before(test_system) + ) +end + + +function custom_system_before() + print("custom_system_before") + runs[#runs + 1] = "custom_system_before" +end + +-- runs on post_update +function on_test_post_update() + print("on_test_post_update") + runs[#runs + 1] = "on_test_post_update" +end + +function custom_system_after() + print("custom_system_after") + runs[#runs + 1] = "custom_system_after" +end + +-- runs in the `Last` bevy schedule +function on_test_last() + assert(#runs == 3, "Expected 3 runs, got: " .. #runs) + assert(runs[1] == "custom_system_before", "Expected custom_system_before to run first, got: " .. runs[1]) + assert(runs[2] == "on_test_post_update", "Expected on_test_post_update to run second, got: " .. runs[2]) + assert(runs[3] == "custom_system_after", "Expected custom_system_after to run third, got: " .. runs[3]) +end \ No newline at end of file diff --git a/assets/tests/add_system/adds_system_in_correct_order.rhai b/assets/tests/add_system/adds_system_in_correct_order.rhai new file mode 100644 index 0000000000..e989b50d7c --- /dev/null +++ b/assets/tests/add_system/adds_system_in_correct_order.rhai @@ -0,0 +1,34 @@ +let runs = []; + +fn on_test() { + let post_update_schedule = world.get_schedule_by_name.call("PostUpdate"); + let test_system = post_update_schedule.get_system_by_name.call("on_test_post_update"); + + let builder_after = system_builder.call("custom_system_after", script_id).after.call(test_system); + let system_after = world.add_system.call(post_update_schedule, builder_after); + + let builder_before = system_builder.call("custom_system_before", script_id).before.call(test_system); + let system_before = world.add_system.call(post_update_schedule, builder_before); +} + +fn custom_system_before() { + print("custom_system_before"); + runs.push("custom_system_before"); +} + +fn on_test_post_update() { + print("on_test_post_update"); + runs.push("on_test_post_update"); +} + +fn custom_system_after() { + print("custom_system_after"); + runs.push("custom_system_after"); +} + +fn on_test_last() { + assert(runs.len() == 3, "Expected 3 runs, got: " + runs.len().to_string()); + assert(runs[0] == "custom_system_before", "Expected custom_system_before to run first, got: " + runs[0]); + assert(runs[1] == "on_test_post_update", "Expected on_test_post_update to run second, got: " + runs[1]); + assert(runs[2] == "custom_system_after", "Expected custom_system_after to run third, got: " + runs[2]); +} \ No newline at end of file diff --git a/assets/tests/get_schedule_by_name/retrieves_existing_schedule.lua b/assets/tests/get_schedule_by_name/retrieves_existing_schedule.lua new file mode 100644 index 0000000000..45e638d2b0 --- /dev/null +++ b/assets/tests/get_schedule_by_name/retrieves_existing_schedule.lua @@ -0,0 +1,2 @@ +assert(world.get_schedule_by_name("Update") ~= nil, "Schedule not found under short identifier") +assert(world.get_schedule_by_name("bevy_app::main_schedule::Update") ~= nil, "Schedule not found under long identifier") \ No newline at end of file diff --git a/assets/tests/get_schedule_by_name/retrieves_existing_schedule.rhai b/assets/tests/get_schedule_by_name/retrieves_existing_schedule.rhai new file mode 100644 index 0000000000..6dad85d275 --- /dev/null +++ b/assets/tests/get_schedule_by_name/retrieves_existing_schedule.rhai @@ -0,0 +1,2 @@ +assert(type_of(world.get_schedule_by_name.call("Update")) != "()", "Schedule not found under short identifier"); +assert(type_of(world.get_schedule_by_name.call("bevy_app::main_schedule::Update")) != "()", "Schedule not found under long identifier"); \ No newline at end of file diff --git a/assets/tests/rhai_tests.rs b/assets/tests/rhai_tests.rs deleted file mode 100644 index 2ab89b04c6..0000000000 --- a/assets/tests/rhai_tests.rs +++ /dev/null @@ -1,144 +0,0 @@ -#![allow( - clippy::unwrap_used, - clippy::todo, - clippy::expect_used, - clippy::panic, - missing_docs -)] -use bevy_mod_scripting_core::{ - bindings::{pretty_print::DisplayWithWorld, ThreadWorldContainer, WorldContainer}, - error::ScriptError, - AddRuntimeInitializer, -}; -use bevy_mod_scripting_rhai::RhaiScriptingPlugin; -use libtest_mimic::{Arguments, Failed, Trial}; -use rhai::{Dynamic, EvalAltResult, FnPtr, NativeCallContext}; -use script_integration_test_harness::execute_integration_test; -use std::{ - fs::{self, DirEntry}, - io, panic, - path::{Path, PathBuf}, -}; - -struct Test { - code: String, - path: PathBuf, -} - -impl Test { - fn execute(self) -> Result<(), Failed> { - execute_integration_test::( - |world, type_registry| { - let _ = world; - let _ = type_registry; - }, - |app| { - app.add_plugins(RhaiScriptingPlugin::default()); - app.add_runtime_initializer::(|runtime| { - runtime.register_fn("assert", |a: Dynamic, b: &str| { - if !a.is::() { - panic!("Expected a boolean value, but got {:?}", a); - } - if !a.as_bool().unwrap() { - panic!("Assertion failed. {}", b); - } - }); - - runtime.register_fn("assert", |a: Dynamic| { - if !a.is::() { - panic!("Expected a boolean value, but got {:?}", a); - } - if !a.as_bool().unwrap() { - panic!("Assertion failed"); - } - }); - runtime.register_fn("assert_throws", |ctxt: NativeCallContext, fn_: FnPtr, regex: String| { - let world = ThreadWorldContainer.try_get_world()?; - let args: [Dynamic;0] = []; - let result = fn_.call_within_context::<()>(&ctxt, args); - match result { - Ok(_) => panic!("Expected function to throw error, but it did not."), - Err(e) => { - let e = ScriptError::from_rhai_error(*e); - let err = e.display_with_world(world); - let regex = regex::Regex::new(®ex).unwrap(); - if regex.is_match(&err) { - Ok::<(), Box>(()) - } else { - panic!( - "Expected error message to match the regex: \n{}\n\nBut got:\n{}", - regex.as_str(), - err - ) - } - }, - } - }); - Ok(()) - }); - }, - self.path.as_os_str().to_str().unwrap(), - self.code.as_bytes(), - ) - .map_err(Failed::from) - } - - fn name(&self) -> String { - format!( - "script_test - lua - {}", - self.path - .to_string_lossy() - .split(&format!("tests{}data", std::path::MAIN_SEPARATOR)) - .last() - .unwrap() - ) - } -} - -fn visit_dirs(dir: &Path, cb: &mut dyn FnMut(&DirEntry)) -> io::Result<()> { - if dir.is_dir() { - for entry in fs::read_dir(dir)? { - let entry = entry?; - let path = entry.path(); - if path.is_dir() { - visit_dirs(&path, cb)?; - } else { - cb(&entry); - } - } - } else { - panic!("Not a directory: {:?}", dir); - } - Ok(()) -} - -fn discover_all_tests() -> Vec { - let workspace_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let test_root = workspace_root.join("tests").join("data"); - let mut test_files = Vec::new(); - visit_dirs(&test_root, &mut |entry| { - let path = entry.path(); - let code = fs::read_to_string(&path).unwrap(); - if path.extension().unwrap() == "rhai" { - test_files.push(Test { code, path }); - } - }) - .unwrap(); - - test_files -} - -// run this with `cargo test --features lua54 --package bevy_mod_scripting_lua --test lua_tests` -// or filter using the prefix "lua test -" -fn main() { - // Parse command line arguments - let args = Arguments::from_args(); - - // Create a list of tests and/or benchmarks (in this case: two dummy tests). - let tests = discover_all_tests() - .into_iter() - .map(|t| Trial::test(t.name(), move || t.execute())); - - // Run all tests and exit the application appropriatly. - libtest_mimic::run(&args, tests.collect()).exit(); -} diff --git a/assets/tests/schedule/gets_all_schedule_systems.lua b/assets/tests/schedule/gets_all_schedule_systems.lua new file mode 100644 index 0000000000..c5d931d986 --- /dev/null +++ b/assets/tests/schedule/gets_all_schedule_systems.lua @@ -0,0 +1,13 @@ +function on_test() + local startup_schedule = world.get_schedule_by_name("Startup") + + + local expected_systems = { + "dummy_startup_system", + } + + for i, system in ipairs(expected_systems) do + local found_system = startup_schedule:get_system_by_name(system) + assert(found_system ~= nil, "Expected system not found: " .. system) + end +end \ No newline at end of file diff --git a/assets/tests/schedule/gets_all_schedule_systems.rhai b/assets/tests/schedule/gets_all_schedule_systems.rhai new file mode 100644 index 0000000000..8ea17906a0 --- /dev/null +++ b/assets/tests/schedule/gets_all_schedule_systems.rhai @@ -0,0 +1,13 @@ +fn on_test() { + let startup_schedule = world.get_schedule_by_name.call("Startup"); + + + let expected_systems = [ + "dummy_startup_system" + ]; + + for system in expected_systems { + let found_system = startup_schedule.get_system_by_name.call(system); + assert(type_of(found_system) != "()", "Expected system not found: " + system); + } +} \ No newline at end of file diff --git a/crates/bevy_mod_scripting_core/Cargo.toml b/crates/bevy_mod_scripting_core/Cargo.toml index d980cd97f1..bf1c39ea7c 100644 --- a/crates/bevy_mod_scripting_core/Cargo.toml +++ b/crates/bevy_mod_scripting_core/Cargo.toml @@ -41,6 +41,7 @@ derivative = "2.2" profiling = { workspace = true } bevy_mod_scripting_derive = { workspace = true } fixedbitset = "0.5" +petgraph = "0.6" [dev-dependencies] test_utils = { workspace = true } diff --git a/crates/bevy_mod_scripting_core/src/bindings/function/script_function.rs b/crates/bevy_mod_scripting_core/src/bindings/function/script_function.rs index 424b2b3ebe..c7d6061f2e 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/function/script_function.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/function/script_function.rs @@ -1,6 +1,7 @@ //! Implementations of the [`ScriptFunction`] and [`ScriptFunctionMut`] traits for functions with up to 13 arguments. use super::{from::FromScript, into::IntoScript, namespace::Namespace}; +use crate::asset::Language; use crate::bindings::function::arg_meta::ArgMeta; use crate::docgen::info::{FunctionInfo, GetFunctionInfo}; use crate::{ @@ -38,24 +39,30 @@ pub trait ScriptFunctionMut<'env, Marker> { /// The caller context when calling a script function. /// Functions can choose to react to caller preferences such as converting 1-indexed numbers to 0-indexed numbers -#[derive(Clone, Copy, Debug, Reflect, Default)] +#[derive(Clone, Debug, Reflect)] #[reflect(opaque)] pub struct FunctionCallContext { - /// Whether the caller uses 1-indexing on all indexes and expects 0-indexing conversions to be performed. - pub convert_to_0_indexed: bool, + language: Language, } impl FunctionCallContext { /// Create a new FunctionCallContext with the given 1-indexing conversion preference - pub fn new(convert_to_0_indexed: bool) -> Self { - Self { - convert_to_0_indexed, - } + pub const fn new(language: Language) -> Self { + Self { language } } /// Tries to access the world, returning an error if the world is not available pub fn world<'l>(&self) -> Result, InteropError> { ThreadWorldContainer.try_get_world() } + /// Whether the caller uses 1-indexing on all indexes and expects 0-indexing conversions to be performed. + pub fn convert_to_0_indexed(&self) -> bool { + matches!(&self.language, Language::Lua) + } + + /// Gets the scripting language of the caller + pub fn language(&self) -> Language { + self.language.clone() + } } #[derive(Clone, Reflect)] @@ -589,7 +596,7 @@ macro_rules! impl_script_function { let received_args_len = args.len(); let expected_arg_count = count!($($param )*); - $( let $context = caller_context; )? + $( let $context = caller_context.clone(); )? let world = caller_context.world()?; // Safety: we're not holding any references to the world, the arguments which might have aliased will always be dropped let ret: Result = unsafe { @@ -672,7 +679,10 @@ mod test { with_local_world(|| { let out = script_function - .call(vec![ScriptValue::from(1)], FunctionCallContext::default()) + .call( + vec![ScriptValue::from(1)], + FunctionCallContext::new(Language::Lua), + ) .unwrap(); assert_eq!(out, ScriptValue::from(1)); @@ -685,8 +695,10 @@ mod test { let script_function = fn_.into_dynamic_script_function().with_name("my_fn"); with_local_world(|| { - let out = - script_function.call(vec![ScriptValue::from(1)], FunctionCallContext::default()); + let out = script_function.call( + vec![ScriptValue::from(1)], + FunctionCallContext::new(Language::Lua), + ); assert!(out.is_err()); assert_eq!( @@ -709,11 +721,13 @@ mod test { let script_function = fn_.into_dynamic_script_function().with_name("my_fn"); with_local_world(|| { - let out = - script_function.call(vec![ScriptValue::from(1)], FunctionCallContext::default()); + let out = script_function.call( + vec![ScriptValue::from(1)], + FunctionCallContext::new(Language::Lua), + ); assert!(out.is_err()); - let world = FunctionCallContext::default().world().unwrap(); + let world = FunctionCallContext::new(Language::Lua).world().unwrap(); // assert no access is held assert!(world.list_accesses().is_empty()); }); diff --git a/crates/bevy_mod_scripting_core/src/bindings/mod.rs b/crates/bevy_mod_scripting_core/src/bindings/mod.rs index e7076abbbc..b41c2da725 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/mod.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/mod.rs @@ -6,6 +6,7 @@ pub mod function; pub mod pretty_print; pub mod query; pub mod reference; +pub mod schedule; pub mod script_value; pub mod world; diff --git a/crates/bevy_mod_scripting_core/src/bindings/schedule.rs b/crates/bevy_mod_scripting_core/src/bindings/schedule.rs new file mode 100644 index 0000000000..4c70179c98 --- /dev/null +++ b/crates/bevy_mod_scripting_core/src/bindings/schedule.rs @@ -0,0 +1,737 @@ +//! Dynamic scheduling from scripts + +use std::{ + any::TypeId, + borrow::Cow, + collections::HashMap, + hash::Hash, + ops::Deref, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, +}; + +use bevy::{ + app::{ + First, FixedFirst, FixedLast, FixedMain, FixedPostUpdate, FixedPreUpdate, FixedUpdate, + Last, PostStartup, PostUpdate, PreStartup, PreUpdate, RunFixedMainLoop, Startup, Update, + }, + ecs::{ + entity::Entity, + intern::Interned, + schedule::{ + InternedScheduleLabel, IntoSystemConfigs, NodeId, Schedule, ScheduleLabel, Schedules, + SystemSet, + }, + system::{IntoSystem, Resource, System, SystemInput, SystemState}, + world::World, + }, + reflect::Reflect, +}; +use parking_lot::RwLock; + +use crate::{ + error::InteropError, + event::CallbackLabel, + extractors::{HandlerContext, WithWorldGuard}, + handler::handle_script_errors, + script::ScriptId, + IntoScriptPluginParams, +}; + +use super::{WorldAccessGuard, WorldGuard}; + +#[derive(Reflect, Debug, Clone)] +/// A reflectable system. +pub struct ReflectSystem { + name: Cow<'static, str>, + type_id: TypeId, + node_id: ReflectNodeId, +} + +#[derive(Reflect, Clone, Debug)] +#[reflect(opaque)] +pub(crate) struct ReflectNodeId(pub(crate) NodeId); + +impl ReflectSystem { + /// Creates a reflect system from a system specification + pub fn from_system( + system: &dyn System, + node_id: NodeId, + ) -> Self { + ReflectSystem { + name: system.name().clone(), + type_id: system.type_id(), + node_id: ReflectNodeId(node_id), + } + } + + /// gets the short identifier of the system, i.e. just the function name + pub fn identifier(&self) -> &str { + // if it contains generics it might contain more than + if self.name.contains("<") { + self.name + .split("<") + .next() + .unwrap_or_default() + .split("::") + .last() + .unwrap_or_default() + } else { + self.name.split("::").last().unwrap_or_default() + } + } + + /// gets the path of the system, i.e. the fully qualified function name + pub fn path(&self) -> &str { + self.name.as_ref() + } +} + +/// A reflectable schedule. +#[derive(Reflect, Clone, Debug)] +pub struct ReflectSchedule { + /// The name of the schedule. + type_path: &'static str, + label: ReflectableScheduleLabel, +} + +#[derive(Reflect, Clone, Debug)] +#[reflect(opaque)] +struct ReflectableScheduleLabel(InternedScheduleLabel); + +impl Deref for ReflectableScheduleLabel { + type Target = InternedScheduleLabel; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From for ReflectableScheduleLabel { + fn from(label: InternedScheduleLabel) -> Self { + Self(label) + } +} + +impl ReflectSchedule { + /// Retrieves the name of the schedule. + pub fn type_path(&self) -> &'static str { + self.type_path + } + + /// Retrieves the short identifier of the schedule + pub fn identifier(&self) -> &'static str { + self.type_path.split("::").last().unwrap_or_default() + } + + /// Retrieves the label of the schedule + pub fn label(&self) -> &InternedScheduleLabel { + &self.label + } + + /// Creates a new reflect schedule from a schedule label + pub fn from_label(label: T) -> Self { + ReflectSchedule { + type_path: std::any::type_name::(), + label: label.intern().into(), + } + } +} + +#[derive(Default, Clone, Resource)] +/// A Send + Sync registry of bevy schedules. +pub struct AppScheduleRegistry(Arc>); + +impl AppScheduleRegistry { + /// Reads the schedule registry. + pub fn read(&self) -> parking_lot::RwLockReadGuard { + self.0.read() + } + + /// Writes to the schedule registry. + pub fn write(&self) -> parking_lot::RwLockWriteGuard { + self.0.write() + } + + /// Creates a new schedule registry pre-populated with default bevy schedules. + pub fn new() -> Self { + Self(Arc::new(RwLock::new(ScheduleRegistry::new()))) + } +} + +#[derive(Default)] +/// A registry of bevy schedules. +pub struct ScheduleRegistry { + schedules: HashMap, +} + +impl ScheduleRegistry { + /// Creates a new schedule registry containing all default bevy schedules. + pub fn new() -> Self { + let mut self_ = Self::default(); + self_ + .register(Update) + .register(First) + .register(PreUpdate) + .register(RunFixedMainLoop) + .register(PostUpdate) + .register(Last) + .register(PreStartup) + .register(Startup) + .register(PostStartup) + .register(FixedMain) + .register(FixedFirst) + .register(FixedPreUpdate) + .register(FixedUpdate) + .register(FixedPostUpdate) + .register(FixedLast); + self_ + } + + /// Retrieves a schedule by name + pub fn get_schedule_by_name(&self, name: &str) -> Option<&ReflectSchedule> { + self.schedules.iter().find_map(|(_, schedule)| { + (schedule.identifier() == name || schedule.type_path() == name).then_some(schedule) + }) + } + + /// Registers a schedule + pub fn register(&mut self, label: T) -> &mut Self { + let schedule = ReflectSchedule::from_label(label); + self.schedules.insert(TypeId::of::(), schedule); + self + } + + /// Retrieves the given schedule + pub fn get(&self, type_id: TypeId) -> Option<&ReflectSchedule> { + self.schedules.get(&type_id) + } + + /// Retrieves the given schedule mutably + pub fn get_mut(&mut self, type_id: TypeId) -> Option<&mut ReflectSchedule> { + self.schedules.get_mut(&type_id) + } + + /// Checks if the given schedule is contained + pub fn contains(&self, type_id: TypeId) -> bool { + self.schedules.contains_key(&type_id) + } + + /// Creates an iterator over all schedules + pub fn iter(&self) -> impl Iterator { + self.schedules.iter() + } + + /// Creates an iterator over all schedules mutably + pub fn iter_mut(&mut self) -> impl Iterator { + self.schedules.iter_mut() + } +} + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +/// a system set for script systems. +pub struct ScriptSystemSet(usize); + +impl ScriptSystemSet { + /// Creates a new script system set with a unique id + pub fn next() -> Self { + static CURRENT_ID: AtomicUsize = AtomicUsize::new(0); + Self(CURRENT_ID.fetch_add(1, Ordering::Relaxed)) + } +} + +impl SystemSet for ScriptSystemSet { + fn dyn_clone(&self) -> bevy::ecs::label::Box { + Box::new(*self) + } + + fn as_dyn_eq(&self) -> &dyn bevy::ecs::label::DynEq { + self + } + + fn dyn_hash(&self, mut state: &mut dyn ::core::hash::Hasher) { + self.hash(&mut state); + } +} + +/// A builder for systems living in scripts +#[derive(Reflect)] +pub struct ScriptSystemBuilder { + name: CallbackLabel, + script_id: ScriptId, + before: Vec, + after: Vec, +} + +impl ScriptSystemBuilder { + /// Creates a new script system builder + pub fn new(name: CallbackLabel, script_id: ScriptId) -> Self { + Self { + before: vec![], + after: vec![], + name, + script_id, + } + } + + /// Adds a system to run before the script system + pub fn before(&mut self, system: ReflectSystem) -> &mut Self { + self.before.push(system); + self + } + + /// Adds a system to run after the script system + pub fn after(&mut self, system: ReflectSystem) -> &mut Self { + self.after.push(system); + self + } + + /// Selects the most granual system set it can for the given system node id or None + fn get_individual_system_system_set( + node_id: NodeId, + schedule: &Schedule, + ) -> Option> { + // if let Some(system) = schedule.graph().get_system_at(node_id) { + // // this only works for normal bevy rust systems + // if let Some(system_set) = system.default_system_sets().first() { + // return Some(system_set.dyn_clone()); + // } + // } + if let Ok(systems) = schedule.systems() { + for (system_id, system) in systems { + if system_id == node_id { + return system.default_system_sets().first().cloned(); + } + } + } + + None + } + + /// Builds the system and inserts it into the given schedule + #[allow(deprecated)] + pub fn build( + self, + world: WorldGuard, + schedule: &ReflectSchedule, + ) -> Result { + world.scope_schedule(schedule, |world, schedule| { + // this is different to a normal event handler + // the system doesn't listen to events + // it immediately calls a singular script with a predefined payload + let system_name = format!("script_system_{}", &self.name); + let _system = move |world: &mut World, + system_state: &mut SystemState< + WithWorldGuard>, + >| { + let mut with_guard = system_state.get_mut(world); + + { + let (guard, handler_ctxt) = with_guard.get_mut(); + let name = self.name.clone(); + bevy::log::debug_once!("First call to script system {}", name); + match handler_ctxt.call_dynamic_label( + &name, + self.script_id.clone(), + Entity::from_raw(0), + vec![], + guard.clone(), + ) { + Ok(_) => {} + Err(err) => { + handle_script_errors( + guard, + vec![err.with_script(self.script_id.clone())].into_iter(), + ); + } + }; + } + + system_state.apply(world); + }; + + let function_system = IntoSystem::into_system(_system.clone()).with_name(system_name); + + // dummy node id for now + let mut reflect_system = + ReflectSystem::from_system(&function_system, NodeId::System(0)); + + // this is quite important, by default systems are placed in a set defined by their TYPE, i.e. in this case + // all script systems would be the same + let set = ScriptSystemSet::next(); + let mut config = IntoSystemConfigs::into_configs(function_system.in_set(set)); + + // apply ordering + for (other, is_before) in self + .before + .iter() + .map(|b| (b, true)) + .chain(self.after.iter().map(|a| (a, false))) + { + match Self::get_individual_system_system_set(other.node_id.0, schedule) { + Some(set) => { + if is_before { + config = config.before(set); + } else { + config = config.after(set); + } + } + None => { + bevy::log::warn!( + "Could not find system set for system {:?}", + other.identifier() + ); + } + } + } + + schedule.configure_sets(set); + schedule.add_systems(config); + + schedule.initialize(world)?; + + let node_id = NodeId::System(schedule.systems_len()); + + reflect_system.node_id = ReflectNodeId(node_id); + + Ok(reflect_system) + })? + } +} + +#[profiling::all_functions] +/// Impls to do with dynamically querying systems and schedules +impl WorldAccessGuard<'_> { + /// Temporarilly removes the given schedule from the world, and calls the given function on it, then re-inserts it. + /// + /// Useful for initializing schedules, or modifying systems + pub fn scope_schedule O>( + &self, + label: &ReflectSchedule, + f: F, + ) -> Result { + self.with_global_access(|world| { + let mut schedules = world.get_resource_mut::().ok_or_else(|| { + InteropError::unsupported_operation( + None, + None, + "accessing schedules in a world with no schedules", + ) + })?; + + let mut removed_schedule = schedules + .remove(*label.label) + .ok_or_else(|| InteropError::missing_schedule(label.identifier()))?; + + let result = f(world, &mut removed_schedule); + + let mut schedules = world.get_resource_mut::().ok_or_else(|| { + InteropError::unsupported_operation( + None, + None, + "removing `Schedules` resource within a schedule scope", + ) + })?; + + assert!( + removed_schedule.label() == *label.label, + "removed schedule label doesn't match the original" + ); + schedules.insert(removed_schedule); + + Ok(result) + })? + } + + /// Retrieves all the systems in a schedule + pub fn systems(&self, schedule: &ReflectSchedule) -> Result, InteropError> { + self.with_resource(|schedules: &Schedules| { + let schedule = schedules + .get(*schedule.label) + .ok_or_else(|| InteropError::missing_schedule(schedule.identifier()))?; + + let systems = schedule.systems()?; + + Ok(systems + .map(|(node_id, system)| ReflectSystem::from_system(system.as_ref(), node_id)) + .collect()) + })? + } + + /// Creates a system from a system builder and inserts it into the given schedule + pub fn add_system( + &self, + schedule: &ReflectSchedule, + builder: ScriptSystemBuilder, + ) -> Result { + bevy::log::debug!( + "Adding script system '{}' for script '{}' to schedule '{}'", + builder.name, + builder.script_id, + schedule.identifier() + ); + + builder.build::

(self.clone(), schedule) + } +} + +#[cfg(test)] +#[allow( + dead_code, + unused_imports, + reason = "tests are there but not working currently" +)] +mod tests { + + use bevy::{ + app::{App, Update}, + ecs::system::IntoSystem, + }; + use test_utils::make_test_plugin; + + use super::*; + + #[test] + fn test_schedule_registry() { + let mut registry = ScheduleRegistry::default(); + registry.register(Update); + + assert!(registry.contains(TypeId::of::())); + + let schedule = registry.get(TypeId::of::()).unwrap(); + assert_eq!(schedule.identifier(), "Update"); + assert_eq!(schedule.type_path(), std::any::type_name::()); + assert_eq!( + registry + .get_schedule_by_name("Update") + .unwrap() + .identifier(), + "Update" + ); + } + + fn test_system_generic() {} + fn test_system() {} + + #[test] + fn test_reflect_system_names() { + let system = IntoSystem::into_system(test_system_generic::); + let system = ReflectSystem::from_system(&system, NodeId::Set(0)); + + assert_eq!(system.identifier(), "test_system_generic"); + assert_eq!(system.path(), "bevy_mod_scripting_core::bindings::schedule::tests::test_system_generic"); + + let system = IntoSystem::into_system(test_system); + let system = ReflectSystem::from_system(&system, NodeId::Set(0)); + + assert_eq!(system.identifier(), "test_system"); + assert_eq!( + system.path(), + "bevy_mod_scripting_core::bindings::schedule::tests::test_system" + ); + } + + make_test_plugin!(crate); + + // #[test] + // fn test_into_system_set_identical_for_real_and_reflect_set() { + // let root_system = || {}; + // let as_system = IntoSystem::into_system(root_system); + // let as_reflect_system = ReflectSystem::from_system(&as_system); + + // let set1 = Box::new(IntoSystemSet::into_system_set(root_system)) as Box; + // let set2 = + // Box::new(IntoSystemSet::into_system_set(as_reflect_system)) as Box; + + // let mut hasher1 = std::collections::hash_map::DefaultHasher::new(); + // set1.dyn_hash(&mut hasher1); + // let mut hasher2 = std::collections::hash_map::DefaultHasher::new(); + // set2.dyn_hash(&mut hasher2); + // pretty_assertions::assert_eq!(hasher1.finish(), hasher2.finish()); + + // pretty_assertions::assert_eq!(set1.system_type(), set2.system_type()); + // assert!(set1.dyn_eq(&set2)); + // } + + #[derive(ScheduleLabel, Hash, PartialEq, Eq, Debug, Clone)] + struct TestSchedule; + + fn test_system_a() {} + fn test_system_b() {} + + /// Verifies that the given schedule graph contains the expected node names and edges. + /// + /// # Arguments + /// + /// * `app` - A mutable reference to the Bevy App. + /// * `schedule_label` - The schedule label to locate the schedule. + /// * `expected_nodes` - A slice of node names expected to be present. + /// * `expected_edges` - A slice of tuples representing expected edges (from, to). + pub fn verify_schedule_graph( + app: &mut bevy::prelude::App, + schedule_label: T, + expected_nodes: &[&str], + expected_edges: &[(&str, &str)], + ) where + T: ScheduleLabel + std::hash::Hash + Eq + Clone + Send + Sync + 'static, + { + // Remove schedules, then remove the schedule to verify. + let mut schedules = app + .world_mut() + .remove_resource::() + .expect("Schedules resource not found"); + let mut schedule = schedules + .remove(schedule_label.clone()) + .expect("Schedule not found"); + + schedule.initialize(app.world_mut()).unwrap(); + let graph = schedule.graph(); + + // Build a mapping from system name to its node id. + + let resolve_name = |node_id: NodeId| { + let out = { + // try systems + if let Some(system) = graph.get_system_at(node_id) { + system.name().clone().to_string() + } else if let Some(system_set) = graph.get_set_at(node_id) { + format!("{:?}", system_set).to_string() + } else { + // try schedule systems + let mut default = format!("{node_id:?}").to_string(); + for (system_node, system) in schedule.systems().unwrap() { + if node_id == system_node { + default = system.name().clone().to_string(); + } + } + default + } + }; + + // trim module path + let trim = "bevy_mod_scripting_core::bindings::schedule::tests::"; + out.replace(trim, "") + }; + + let all_nodes = graph + .dependency() + .graph() + .nodes() + .map(&resolve_name) + .collect::>(); + + // Assert expected nodes exist. + for &node in expected_nodes { + assert!( + all_nodes.contains(&node.to_owned()), + "Graph does not contain expected node '{node}' nodes: {all_nodes:?}" + ); + } + + // Collect all edges as (from, to) name pairs. + let mut found_edges = Vec::new(); + for (from, to, _) in graph.dependency().graph().all_edges() { + let name_from = resolve_name(from); + let name_to = resolve_name(to); + found_edges.push((name_from, name_to)); + } + + // Assert each expected edge exists. + for &(exp_from, exp_to) in expected_edges { + assert!( + found_edges.contains(&(exp_from.to_owned(), exp_to.to_owned())), + "Expected edge ({} -> {}) not found. Found edges: {:?}", + exp_from, + exp_to, + found_edges + ); + } + + // Optionally, reinsert the schedule back into the schedules resource. + schedules.insert(schedule); + app.world_mut().insert_resource(schedules); + } + + // #[test] + // fn test_builder_creates_correct_system_graph_against_rust_systems() { + // let mut app = App::new(); + // app.add_plugins(( + // bevy::asset::AssetPlugin::default(), + // bevy::diagnostic::DiagnosticsPlugin, + // TestPlugin::default(), // assuming TestPlugin is defined appropriately + // )); + + // let system_a = IntoSystem::into_system(test_system_a); + + // let system_b = IntoSystem::into_system(test_system_b); + + // let mut system_builder = ScriptSystemBuilder::new("test".into(), ScriptId::from("test")); + // // Set ordering: script system runs after "root1" and before "root2". + // system_builder + // .after(ReflectSystem::from_system(&system_a, NodeId::System(0))) + // .before(ReflectSystem::from_system(&system_b, NodeId::System(1))); + + // app.init_schedule(TestSchedule); + // app.add_systems(TestSchedule, system_a); + // app.add_systems(TestSchedule, system_b); + // let _ = system_builder.build::( + // WorldGuard::new(app.world_mut()), + // &ReflectSchedule::from_label(TestSchedule), + // ); + + // verify_schedule_graph( + // &mut app, + // TestSchedule, + // // expected nodes + // &["test_system_a", "test_system_b", "script_system_test"], + // // expected edges (from, to), i.e. before, after, relationships + // &[ + // ("SystemTypeSet(fn bevy_ecs::system::function_system::FunctionSystem())", "script_system_test"), + // ("script_system_test", "SystemTypeSet(fn bevy_ecs::system::function_system::FunctionSystem())"), + // ], + // ); + // } + + // #[test] + // fn test_builder_creates_correct_system_graph_against_script_systems() { + // let mut app = App::new(); + // app.add_plugins(( + // bevy::asset::AssetPlugin::default(), + // bevy::diagnostic::DiagnosticsPlugin, + // TestPlugin::default(), // assuming TestPlugin is defined appropriately + // )); + // app.init_schedule(TestSchedule); + // let reflect_schedule = ReflectSchedule::from_label(TestSchedule); + + // let system_root = ScriptSystemBuilder::new("root".into(), "script_root.lua".into()) + // .build::(WorldGuard::new(app.world_mut()), &reflect_schedule) + // .unwrap(); + + // let mut system_a = ScriptSystemBuilder::new("a".into(), "script_a.lua".into()); + // system_a.before(system_root.clone()); + // system_a + // .build::(WorldGuard::new(app.world_mut()), &reflect_schedule) + // .unwrap(); + + // let mut system_b = ScriptSystemBuilder::new("b".into(), "script_b.lua".into()); + // system_b.after(system_root.clone()); + // system_b + // .build::(WorldGuard::new(app.world_mut()), &reflect_schedule) + // .unwrap(); + + // verify_schedule_graph( + // &mut app, + // TestSchedule, + // // expected nodes + // &["script_system_root", "script_system_a", "script_system_b"], + // // expected edges (from, to), i.e. before, after, relationships + // &[ + // // this doesn't work currently TODO: fix this, i.e. we inject the systems but not the ordering constraints + // // ("SystemTypeSet(fn bevy_ecs::system::function_system::FunctionSystem())", "script_system_test"), + // // ("script_system_test", "SystemTypeSet(fn bevy_ecs::system::function_system::FunctionSystem())"), + // ], + // ); + // } +} diff --git a/crates/bevy_mod_scripting_core/src/bindings/world.rs b/crates/bevy_mod_scripting_core/src/bindings/world.rs index 8ef2d44af0..9f9938d9d2 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/world.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/world.rs @@ -12,6 +12,7 @@ use super::{ script_function::{AppScriptFunctionRegistry, DynamicScriptFunction, FunctionCallContext}, }, pretty_print::DisplayWithWorld, + schedule::{AppScheduleRegistry, ReflectSchedule}, script_value::ScriptValue, AppReflectAllocator, ReflectBase, ReflectBaseType, ReflectReference, ScriptComponentRegistration, ScriptResourceRegistration, ScriptTypeRegistration, @@ -70,8 +71,12 @@ pub(crate) struct WorldAccessGuardInner<'w> { pub(crate) accesses: AccessMap, /// Cached for convenience, since we need it for most operations, means we don't need to lock the type registry every time type_registry: TypeRegistryArc, + /// The script allocator for the world allocator: AppReflectAllocator, + /// The function registry for the world function_registry: AppScriptFunctionRegistry, + /// The schedule registry for the world + schedule_registry: AppScheduleRegistry, } impl std::fmt::Debug for WorldAccessGuardInner<'_> { @@ -158,6 +163,8 @@ impl<'w> WorldAccessGuard<'w> { let function_registry = world .get_resource_or_init::() .clone(); + + let schedule_registry = world.get_resource_or_init::().clone(); Self { inner: Rc::new(WorldAccessGuardInner { cell: world.as_unsafe_world_cell(), @@ -165,6 +172,7 @@ impl<'w> WorldAccessGuard<'w> { allocator, type_registry, function_registry, + schedule_registry, }), invalid: Rc::new(false.into()), } @@ -275,6 +283,11 @@ impl<'w> WorldAccessGuard<'w> { self.inner.type_registry.clone() } + /// Returns the schedule registry for the world + pub fn schedule_registry(&self) -> AppScheduleRegistry { + self.inner.schedule_registry.clone() + } + /// Returns the script allocator for the world pub fn allocator(&self) -> AppReflectAllocator { self.inner.allocator.clone() @@ -486,7 +499,7 @@ impl<'w> WorldAccessGuard<'w> { let mut last_error = None; for overload in overload_iter { - match overload.call(args.clone(), context) { + match overload.call(args.clone(), context.clone()) { Ok(out) => return Ok(out), Err(e) => last_error = Some(e), } @@ -624,7 +637,7 @@ impl WorldAccessGuard<'_> { .type_id(), )) }) - .collect::, _>>()?; + .collect::, InteropError>>()?; let mut dynamic = self.construct_dynamic_struct(&mut payload, fields_iter)?; dynamic.set_represented_type(Some(type_info)); Box::new(dynamic) @@ -641,7 +654,7 @@ impl WorldAccessGuard<'_> { })? .type_id()) }) - .collect::, _>>()?; + .collect::, InteropError>>()?; let mut dynamic = self.construct_dynamic_tuple_struct(&mut payload, fields_iter, one_indexed)?; @@ -660,7 +673,7 @@ impl WorldAccessGuard<'_> { })? .type_id()) }) - .collect::, _>>()?; + .collect::, InteropError>>()?; let mut dynamic = self.construct_dynamic_tuple(&mut payload, fields_iter, one_indexed)?; @@ -701,7 +714,7 @@ impl WorldAccessGuard<'_> { .type_id(), )) }) - .collect::, _>>()?; + .collect::, InteropError>>()?; let dynamic = self.construct_dynamic_struct(&mut payload, fields_iter)?; DynamicVariant::Struct(dynamic) @@ -719,7 +732,7 @@ impl WorldAccessGuard<'_> { })? .type_id()) }) - .collect::, _>>()?; + .collect::, InteropError>>()?; let dynamic = self.construct_dynamic_tuple(&mut payload, fields_iter, one_indexed)?; @@ -767,6 +780,16 @@ impl WorldAccessGuard<'_> { .map(|registration| ScriptTypeRegistration::new(Arc::new(registration.clone()))) } + /// get a schedule by name + pub fn get_schedule_by_name(&self, schedule_name: String) -> Option { + let schedule_registry = self.schedule_registry(); + let schedule_registry = schedule_registry.read(); + + schedule_registry + .get_schedule_by_name(&schedule_name) + .cloned() + } + /// get a component type registration for the type pub fn get_component_type( &self, diff --git a/crates/bevy_mod_scripting_core/src/error.rs b/crates/bevy_mod_scripting_core/src/error.rs index ad1d871da6..338b44da5e 100644 --- a/crates/bevy_mod_scripting_core/src/error.rs +++ b/crates/bevy_mod_scripting_core/src/error.rs @@ -12,7 +12,10 @@ use crate::{ script::ScriptId, }; use bevy::{ - ecs::component::ComponentId, + ecs::{ + component::ComponentId, + schedule::{ScheduleBuildError, ScheduleNotInitialized}, + }, prelude::Entity, reflect::{PartialReflect, Reflect}, }; @@ -221,6 +224,18 @@ impl DisplayWithWorld for ScriptError { } } +impl From for InteropError { + fn from(value: ScheduleBuildError) -> Self { + InteropError::external_error(Box::new(value)) + } +} + +impl From for InteropError { + fn from(value: ScheduleNotInitialized) -> Self { + InteropError::external_error(Box::new(value)) + } +} + #[cfg(feature = "mlua_impls")] impl From for mlua::Error { fn from(value: ScriptError) -> Self { @@ -591,6 +606,13 @@ impl InteropError { })) } + /// Thrown when a schedule is missing from the registry. + pub fn missing_schedule(schedule_name: impl Into>) -> Self { + Self(Arc::new(InteropErrorInner::MissingSchedule { + schedule_name: schedule_name.into(), + })) + } + /// Returns the inner error pub fn inner(&self) -> &InteropErrorInner { &self.0 @@ -795,6 +817,11 @@ pub enum InteropErrorInner { /// The script that was attempting to access the context script_id: ScriptId, }, + /// Thrown when a schedule is missing from the registry. + MissingSchedule { + /// The name of the schedule that was missing + schedule_name: Cow<'static, str>, + }, } /// For test purposes @@ -1035,6 +1062,10 @@ impl PartialEq for InteropErrorInner { script_id: d, }, ) => a == c && b == d, + ( + InteropErrorInner::MissingSchedule { schedule_name: a }, + InteropErrorInner::MissingSchedule { schedule_name: b }, + ) => a == b, _ => false, } } @@ -1261,6 +1292,12 @@ macro_rules! missing_context_for_callback { }; } +macro_rules! missing_schedule_error { + ($schedule:expr) => { + format!("Missing schedule: '{}'. This can happen if you try to access a schedule from within itself. Have all schedules been registered?", $schedule) + }; +} + impl DisplayWithWorld for InteropErrorInner { fn display_with_world(&self, world: crate::bindings::WorldGuard) -> String { match self { @@ -1402,6 +1439,9 @@ impl DisplayWithWorld for InteropErrorInner { script_id ) }, + InteropErrorInner::MissingSchedule { schedule_name } => { + missing_schedule_error!(schedule_name) + }, } } @@ -1546,6 +1586,9 @@ impl DisplayWithWorld for InteropErrorInner { script_id ) }, + InteropErrorInner::MissingSchedule { schedule_name } => { + missing_schedule_error!(schedule_name) + }, } } } diff --git a/crates/bevy_mod_scripting_core/src/event.rs b/crates/bevy_mod_scripting_core/src/event.rs index 4e74ed7bcc..ceed3e8a92 100644 --- a/crates/bevy_mod_scripting_core/src/event.rs +++ b/crates/bevy_mod_scripting_core/src/event.rs @@ -1,7 +1,7 @@ //! Event handlers and event types for scripting. use crate::{bindings::script_value::ScriptValue, error::ScriptError, script::ScriptId}; -use bevy::{ecs::entity::Entity, prelude::Event}; +use bevy::{ecs::entity::Entity, prelude::Event, reflect::Reflect}; /// An error coming from a script #[derive(Debug, Event)] @@ -14,7 +14,7 @@ pub struct ScriptErrorEvent { /// particularly at the start of the string /// /// a valid callback label starts with a letter or underscore, and contains only ascii characters, as well as disallows some common keywords -#[derive(Clone, PartialEq, Eq, Hash, Debug)] +#[derive(Reflect, Clone, PartialEq, Eq, Hash, Debug)] pub struct CallbackLabel(String); impl CallbackLabel { diff --git a/crates/bevy_mod_scripting_core/src/extractors.rs b/crates/bevy_mod_scripting_core/src/extractors.rs index fa7aaeb7e7..0daefff90b 100644 --- a/crates/bevy_mod_scripting_core/src/extractors.rs +++ b/crates/bevy_mod_scripting_core/src/extractors.rs @@ -22,7 +22,7 @@ use crate::{ }, context::{ContextLoadingSettings, ScriptContexts}, error::{InteropError, ScriptError}, - event::IntoCallbackLabel, + event::{CallbackLabel, IntoCallbackLabel}, handler::CallbackSettings, runtime::RuntimeContainer, script::{ScriptId, Scripts, StaticScripts}, @@ -220,12 +220,10 @@ impl HandlerContext<'_, P> { .contains_key(&script.context_id) } - /// Invoke a callback in a script immediately. - /// - /// This will return [`crate::error::InteropErrorInner::MissingScript`] or [`crate::error::InteropErrorInner::MissingContext`] errors while the script is loading. - /// Run [`Self::is_script_fully_loaded`] before calling the script to ensure the script and context were loaded ahead of time. - pub fn call( + /// Equivalent to [`Self::call`] but with a dynamically passed in label + pub fn call_dynamic_label( &mut self, + label: &CallbackLabel, script_id: ScriptId, entity: Entity, payload: Vec, @@ -254,13 +252,27 @@ impl HandlerContext<'_, P> { payload, entity, &script_id, - &C::into_callback_label(), + label, context, pre_handling_initializers, runtime, guard, ) } + + /// Invoke a callback in a script immediately. + /// + /// This will return [`crate::error::InteropErrorInner::MissingScript`] or [`crate::error::InteropErrorInner::MissingContext`] errors while the script is loading. + /// Run [`Self::is_script_fully_loaded`] before calling the script to ensure the script and context were loaded ahead of time. + pub fn call( + &mut self, + script_id: ScriptId, + entity: Entity, + payload: Vec, + guard: WorldGuard<'_>, + ) -> Result { + self.call_dynamic_label(&C::into_callback_label(), script_id, entity, payload, guard) + } } /// A wrapper around a world which pre-populates access, to safely co-exist with other system params, diff --git a/crates/bevy_mod_scripting_core/src/handler.rs b/crates/bevy_mod_scripting_core/src/handler.rs index d47ccab9bd..a53c99659f 100644 --- a/crates/bevy_mod_scripting_core/src/handler.rs +++ b/crates/bevy_mod_scripting_core/src/handler.rs @@ -1,5 +1,4 @@ //! Contains the logic for handling script callback events -#[allow(deprecated)] use crate::{ bindings::{ pretty_print::DisplayWithWorld, script_value::ScriptValue, ThreadWorldContainer, @@ -8,7 +7,7 @@ use crate::{ context::ContextPreHandlingInitializer, error::{InteropErrorInner, ScriptError}, event::{CallbackLabel, IntoCallbackLabel, ScriptCallbackEvent, ScriptErrorEvent}, - extractors::{EventReaderScope, HandlerContext, WithWorldGuard}, + extractors::{HandlerContext, WithWorldGuard}, script::{ScriptComponent, ScriptId}, IntoScriptPluginParams, }; @@ -109,26 +108,35 @@ macro_rules! push_err_and_continue { #[allow(deprecated)] pub fn event_handler( world: &mut World, - state: &mut SystemState<( - Local)>>, - EventReaderScope, - WithWorldGuard>, - )>, + state: &mut EventHandlerSystemState

, ) { // we wrap the inner event handler, so that we can immediately re-insert all the resources back. // otherwise this would happen in the next schedule { let (entity_query_state, script_events, handler_ctxt) = state.get_mut(world); - event_handler_inner::(entity_query_state, script_events, handler_ctxt); + event_handler_inner::

( + L::into_callback_label(), + entity_query_state, + script_events, + handler_ctxt, + ); } state.apply(world); } +#[allow(deprecated)] +pub(crate) type EventHandlerSystemState<'w, 's, P> = SystemState<( + Local<'s, QueryState<(Entity, Ref<'w, ScriptComponent>)>>, + crate::extractors::EventReaderScope<'s, ScriptCallbackEvent>, + WithWorldGuard<'w, 's, HandlerContext<'s, P>>, +)>; + #[profiling::function] #[allow(deprecated)] -fn event_handler_inner( +pub(crate) fn event_handler_inner( + callback_label: CallbackLabel, mut entity_query_state: Local)>>, - mut script_events: EventReaderScope, + mut script_events: crate::extractors::EventReaderScope, mut handler_ctxt: WithWorldGuard>, ) { let (guard, handler_ctxt) = handler_ctxt.get_mut(); @@ -164,10 +172,7 @@ fn event_handler_inner( } }; - for event in events - .into_iter() - .filter(|e| e.label == L::into_callback_label()) - { + for event in events.into_iter().filter(|e| e.label == callback_label) { for (entity, entity_scripts) in entity_and_static_scripts.iter() { for script_id in entity_scripts.iter() { match &event.recipients { @@ -187,7 +192,8 @@ fn event_handler_inner( _ => {} } - let call_result = handler_ctxt.call::( + let call_result = handler_ctxt.call_dynamic_label( + &callback_label, script_id.clone(), *entity, event.args.clone(), diff --git a/crates/bevy_mod_scripting_core/src/lib.rs b/crates/bevy_mod_scripting_core/src/lib.rs index afe9402e99..43bddb9ec3 100644 --- a/crates/bevy_mod_scripting_core/src/lib.rs +++ b/crates/bevy_mod_scripting_core/src/lib.rs @@ -10,8 +10,8 @@ use asset::{ use bevy::prelude::*; use bindings::{ function::script_function::AppScriptFunctionRegistry, garbage_collector, - script_value::ScriptValue, AppReflectAllocator, ReflectAllocator, ReflectReference, - ScriptTypeRegistration, + schedule::AppScheduleRegistry, script_value::ScriptValue, AppReflectAllocator, + ReflectAllocator, ReflectReference, ScriptTypeRegistration, }; use commands::{AddStaticScript, RemoveStaticScript}; use context::{ @@ -289,7 +289,8 @@ fn once_per_app_init(app: &mut App) { .init_resource::() .init_resource::() .init_asset::() - .init_resource::(); + .init_resource::() + .insert_resource(AppScheduleRegistry::new()); app.add_systems( PostUpdate, diff --git a/crates/bevy_mod_scripting_functions/Cargo.toml b/crates/bevy_mod_scripting_functions/Cargo.toml index a155a38531..be9b006e9a 100644 --- a/crates/bevy_mod_scripting_functions/Cargo.toml +++ b/crates/bevy_mod_scripting_functions/Cargo.toml @@ -14,6 +14,8 @@ readme = "readme.md" [features] core_functions = [] bevy_bindings = [] +lua_bindings = ["bevy_mod_scripting_lua"] +rhai_bindings = ["bevy_mod_scripting_rhai"] [dependencies] @@ -33,6 +35,8 @@ uuid = "1.11" smol_str = "0.2.2" bevy_mod_scripting_core = { workspace = true } bevy_mod_scripting_derive = { workspace = true } +bevy_mod_scripting_lua = { path = "../languages/bevy_mod_scripting_lua", optional = true } +bevy_mod_scripting_rhai = { path = "../languages/bevy_mod_scripting_rhai", optional = true } [lints] workspace = true diff --git a/crates/bevy_mod_scripting_functions/src/core.rs b/crates/bevy_mod_scripting_functions/src/core.rs index 2142c5b3d5..95e29711c6 100644 --- a/crates/bevy_mod_scripting_functions/src/core.rs +++ b/crates/bevy_mod_scripting_functions/src/core.rs @@ -4,8 +4,11 @@ use std::collections::HashMap; use bevy::{prelude::*, reflect::ParsedPath}; use bevy_mod_scripting_core::{ - bindings::function::{ - from::Union, namespace::GlobalNamespace, script_function::DynamicScriptFunctionMut, + bindings::{ + function::{ + from::Union, namespace::GlobalNamespace, script_function::DynamicScriptFunctionMut, + }, + schedule::{ReflectSchedule, ReflectSystem, ScriptSystemBuilder}, }, docgen::info::FunctionInfo, *, @@ -77,6 +80,24 @@ impl World { }) } + /// Retrieves the schedule with the given name, Also ensures the schedule is initialized before returning it. + fn get_schedule_by_name( + ctxt: FunctionCallContext, + name: String, + ) -> Result>, InteropError> { + profiling::function_scope!("get_schedule_by_name"); + let world = ctxt.world()?; + let schedule = match world.get_schedule_by_name(name) { + Some(schedule) => schedule, + None => return Ok(None), + }; + + // do two things, check it actually exists + world.scope_schedule(&schedule, |world, schedule| schedule.initialize(world))??; + + Ok(Some(schedule.into())) + } + fn get_component( ctxt: FunctionCallContext, entity: Val, @@ -171,7 +192,7 @@ impl World { ) -> Result<(), InteropError> { profiling::function_scope!("insert_children"); let world = ctxt.world()?; - let index = if ctxt.convert_to_0_indexed { + let index = if ctxt.convert_to_0_indexed() { index - 1 } else { index @@ -246,6 +267,47 @@ impl World { Ok(Val(query_builder)) } + /// Adds the given system to the world. + /// + /// Arguments: + /// * `schedule`: The schedule to add the system to. + /// * `builder`: The system builder specifying the system and its dependencies. + /// Returns: + /// * `system`: The system that was added. + fn add_system( + ctxt: FunctionCallContext, + schedule: Val, + builder: Val, + ) -> Result, InteropError> { + profiling::function_scope!("add_system"); + let world = ctxt.world()?; + let system = match ctxt.language() { + #[cfg(feature = "lua_bindings")] + asset::Language::Lua => world + .add_system::( + &schedule, + builder.into_inner(), + )?, + #[cfg(feature = "rhai_bindings")] + asset::Language::Rhai => world + .add_system::( + &schedule, + builder.into_inner(), + )?, + _ => { + return Err(InteropError::unsupported_operation( + None, + None, + format!( + "creating a system in {} scripting language", + ctxt.language() + ), + )) + } + }; + Ok(Val(system)) + } + fn exit(ctxt: FunctionCallContext) -> Result<(), InteropError> { profiling::function_scope!("exit"); let world = ctxt.world()?; @@ -290,7 +352,7 @@ impl ReflectReference { ) -> Result { profiling::function_scope!("get"); let mut path: ParsedPath = key.try_into()?; - if ctxt.convert_to_0_indexed { + if ctxt.convert_to_0_indexed() { path.convert_to_0_indexed(); } self_.index_path(path); @@ -308,7 +370,7 @@ impl ReflectReference { if let ScriptValue::Reference(mut self_) = self_ { let world = ctxt.world()?; let mut path: ParsedPath = key.try_into()?; - if ctxt.convert_to_0_indexed { + if ctxt.convert_to_0_indexed() { path.convert_to_0_indexed(); } self_.index_path(path); @@ -382,7 +444,7 @@ impl ReflectReference { let mut key = >::from_script_ref(key_type_id, k, world.clone())?; - if ctxt.convert_to_0_indexed { + if ctxt.convert_to_0_indexed() { key.convert_to_0_indexed_key(); } @@ -428,7 +490,7 @@ impl ReflectReference { let mut key = >::from_script_ref(key_type_id, k, world.clone())?; - if ctxt.convert_to_0_indexed { + if ctxt.convert_to_0_indexed() { key.convert_to_0_indexed_key(); } @@ -608,6 +670,121 @@ impl ScriptQueryResult { } } +#[script_bindings( + remote, + bms_core_path = "bevy_mod_scripting_core", + name = "reflect_schedule_functions" +)] +impl ReflectSchedule { + /// Retrieves all the systems in the schedule. + /// + /// Arguments: + /// * `self_`: The schedule to retrieve the systems from. + /// Returns: + /// * `systems`: The systems in the schedule. + fn systems( + ctxt: FunctionCallContext, + self_: Ref, + ) -> Result>, InteropError> { + profiling::function_scope!("systems"); + let world = ctxt.world()?; + let systems = world.systems(&self_); + Ok(systems?.into_iter().map(Into::into).collect()) + } + + /// Retrieves the system with the given name in the schedule + /// + /// Arguments: + /// * `self_`: The schedule to retrieve the system from. + /// * `name`: The identifier or full path of the system to retrieve. + /// Returns: + /// * `system`: The system with the given name, if it exists. + fn get_system_by_name( + ctxt: FunctionCallContext, + self_: Ref, + name: String, + ) -> Result>, InteropError> { + profiling::function_scope!("system_by_name"); + let world = ctxt.world()?; + let system = world.systems(&self_)?; + Ok(system + .into_iter() + .find_map(|s| (s.identifier() == name || s.path() == name).then_some(s.into()))) + } +} + +#[script_bindings( + remote, + bms_core_path = "bevy_mod_scripting_core", + name = "reflect_system_functions" +)] +impl ReflectSystem { + /// Retrieves the identifier of the system + /// Arguments: + /// * `self_`: The system to retrieve the identifier from. + /// Returns: + /// * `identifier`: The identifier of the system, e.g. `my_system` + fn identifier(self_: Ref) -> String { + profiling::function_scope!("identifier"); + self_.identifier().to_string() + } + + /// Retrieves the full path of the system + /// Arguments: + /// * `self_`: The system to retrieve the path from. + /// Returns: + /// * `path`: The full path of the system, e.g. `my_crate::systems::my_system` + fn path(self_: Ref) -> String { + profiling::function_scope!("path"); + self_.path().to_string() + } +} + +#[script_bindings( + remote, + bms_core_path = "bevy_mod_scripting_core", + name = "script_system_builder_functions" +)] +impl ScriptSystemBuilder { + /// Specifies the system is to run *after* the given system + /// + /// Note: this is an experimental feature, and the ordering might not work correctly for script initialized systems + /// + /// Arguments: + /// * `self_`: The system builder to add the dependency to. + /// * `system`: The system to run after. + /// Returns: + /// * `builder`: The system builder with the dependency added. + fn after( + self_: Val, + system: Val, + ) -> Val { + profiling::function_scope!("after"); + let mut builder = self_.into_inner(); + builder.after(system.into_inner()); + Val(builder) + } + + /// Specifies the system is to run *before* the given system. + /// + /// Note: this is an experimental feature, and the ordering might not work correctly for script initialized systems + /// + /// Arguments: + /// * `self_`: The system builder to add the dependency to. + /// * `system`: The system to run before. + /// Returns: + /// * `builder`: The system builder with the dependency added. + fn before( + self_: Val, + system: Val, + ) -> Val { + profiling::function_scope!("before"); + let mut builder = self_.into_inner(); + builder.before(system.into_inner()); + Val(builder) + } +} + #[script_bindings( remote, bms_core_path = "bevy_mod_scripting_core", @@ -639,7 +816,7 @@ impl GlobalNamespace { }; let world = ctxt.world()?; - let one_indexed = ctxt.convert_to_0_indexed; + let one_indexed = ctxt.convert_to_0_indexed(); let val = world.construct(registration.clone(), payload, one_indexed)?; let allocator = world.allocator(); @@ -656,6 +833,19 @@ impl GlobalNamespace { &mut allocator, )) } + + /// Creates a new script function builder + /// Arguments: + /// * `callback`: The functio name in the script, this system will call + /// * `script_id`: The id of the script + /// Returns: + /// * `builder`: The new system builder + fn system_builder( + callback: String, + script_id: String, + ) -> Result, InteropError> { + Ok(ScriptSystemBuilder::new(callback.into(), script_id.into()).into()) + } } pub fn register_core_functions(app: &mut App) { @@ -677,6 +867,10 @@ pub fn register_core_functions(app: &mut App) { register_script_query_builder_functions(world); register_script_query_result_functions(world); + register_reflect_schedule_functions(world); + register_reflect_system_functions(world); + register_script_system_builder_functions(world); + register_global_namespace_functions(world); } } diff --git a/crates/languages/bevy_mod_scripting_lua/Cargo.toml b/crates/languages/bevy_mod_scripting_lua/Cargo.toml index 74245453ac..6d266dce01 100644 --- a/crates/languages/bevy_mod_scripting_lua/Cargo.toml +++ b/crates/languages/bevy_mod_scripting_lua/Cargo.toml @@ -39,7 +39,6 @@ path = "src/lib.rs" [dependencies] bevy = { workspace = true, default-features = false } bevy_mod_scripting_core = { workspace = true, features = ["mlua_impls"] } -bevy_mod_scripting_functions = { workspace = true, features = [] } mlua = { version = "0.10", features = ["vendored", "send", "macros"] } parking_lot = "0.12.1" uuid = "1.1" diff --git a/crates/languages/bevy_mod_scripting_lua/src/bindings/script_value.rs b/crates/languages/bevy_mod_scripting_lua/src/bindings/script_value.rs index 7d39eb8f70..888f4e821a 100644 --- a/crates/languages/bevy_mod_scripting_lua/src/bindings/script_value.rs +++ b/crates/languages/bevy_mod_scripting_lua/src/bindings/script_value.rs @@ -1,6 +1,7 @@ use super::reference::LuaReflectReference; -use bevy_mod_scripting_core::bindings::{ - function::script_function::FunctionCallContext, script_value::ScriptValue, +use bevy_mod_scripting_core::{ + asset::Language, + bindings::{function::script_function::FunctionCallContext, script_value::ScriptValue}, }; use mlua::{FromLua, IntoLua, Value, Variadic}; use std::{ @@ -104,9 +105,7 @@ impl FromLua for LuaScriptValue { } /// The context for calling a function from Lua -pub const LUA_CALLER_CONTEXT: FunctionCallContext = FunctionCallContext { - convert_to_0_indexed: true, -}; +pub const LUA_CALLER_CONTEXT: FunctionCallContext = FunctionCallContext::new(Language::Lua); #[profiling::all_functions] impl IntoLua for LuaScriptValue { fn into_lua(self, lua: &mlua::Lua) -> mlua::Result { diff --git a/crates/languages/bevy_mod_scripting_lua/src/lib.rs b/crates/languages/bevy_mod_scripting_lua/src/lib.rs index bcec36a081..6b6a58de0a 100644 --- a/crates/languages/bevy_mod_scripting_lua/src/lib.rs +++ b/crates/languages/bevy_mod_scripting_lua/src/lib.rs @@ -73,6 +73,7 @@ impl Default for LuaScriptingPlugin { LuaStaticReflectReference(std::any::TypeId::of::()), ) .map_err(ScriptError::from_mlua_error)?; + Ok(()) }, |_script_id, context: &mut Lua| { diff --git a/crates/languages/bevy_mod_scripting_rhai/Cargo.toml b/crates/languages/bevy_mod_scripting_rhai/Cargo.toml index 61a0340e6b..82ef93168c 100644 --- a/crates/languages/bevy_mod_scripting_rhai/Cargo.toml +++ b/crates/languages/bevy_mod_scripting_rhai/Cargo.toml @@ -19,8 +19,6 @@ path = "src/lib.rs" bevy = { workspace = true, default-features = false } rhai = { version = "1.21" } bevy_mod_scripting_core = { workspace = true, features = ["rhai_impls"] } -bevy_mod_scripting_functions = { workspace = true, features = [ -], default-features = false } strum = { version = "0.26", features = ["derive"] } [dev-dependencies] diff --git a/crates/languages/bevy_mod_scripting_rhai/src/bindings/script_value.rs b/crates/languages/bevy_mod_scripting_rhai/src/bindings/script_value.rs index bb917b6700..d2e793ea0c 100644 --- a/crates/languages/bevy_mod_scripting_rhai/src/bindings/script_value.rs +++ b/crates/languages/bevy_mod_scripting_rhai/src/bindings/script_value.rs @@ -1,4 +1,5 @@ use bevy_mod_scripting_core::{ + asset::Language, bindings::{ function::script_function::{DynamicScriptFunction, FunctionCallContext}, script_value::ScriptValue, @@ -11,9 +12,7 @@ use std::str::FromStr; use super::reference::RhaiReflectReference; /// The default function call context for rhai -pub const RHAI_CALLER_CONTEXT: FunctionCallContext = FunctionCallContext { - convert_to_0_indexed: false, -}; +pub const RHAI_CALLER_CONTEXT: FunctionCallContext = FunctionCallContext::new(Language::Rhai); /// A function curried with one argument, i.e. the receiver pub struct FunctionWithReceiver { diff --git a/crates/testing_crates/script_integration_test_harness/Cargo.toml b/crates/testing_crates/script_integration_test_harness/Cargo.toml index 2f30451def..d335d8ee69 100644 --- a/crates/testing_crates/script_integration_test_harness/Cargo.toml +++ b/crates/testing_crates/script_integration_test_harness/Cargo.toml @@ -11,5 +11,7 @@ bevy_mod_scripting_core = { workspace = true } bevy_mod_scripting_functions = { workspace = true, features = [ "bevy_bindings", "core_functions", + "rhai_bindings", + "lua_bindings", ] } regex = { version = "1.11" } diff --git a/crates/testing_crates/script_integration_test_harness/src/lib.rs b/crates/testing_crates/script_integration_test_harness/src/lib.rs index afa128bb84..c4d2c40b36 100644 --- a/crates/testing_crates/script_integration_test_harness/src/lib.rs +++ b/crates/testing_crates/script_integration_test_harness/src/lib.rs @@ -1,16 +1,18 @@ pub mod test_functions; use std::{ + marker::PhantomData, path::PathBuf, time::{Duration, Instant}, }; use bevy::{ - app::{App, Update}, + app::{App, Last, PostUpdate, Startup, Update}, asset::{AssetServer, Handle}, ecs::{ event::{Event, Events}, - system::{Local, Res}, + schedule::{IntoSystemConfigs, SystemConfigs}, + system::{IntoSystem, Local, Res, SystemState}, world::Mut, }, prelude::{Entity, World}, @@ -20,15 +22,42 @@ use bevy_mod_scripting_core::{ asset::ScriptAsset, bindings::{pretty_print::DisplayWithWorld, script_value::ScriptValue, WorldGuard}, callback_labels, - event::ScriptErrorEvent, + event::{IntoCallbackLabel, ScriptErrorEvent}, extractors::{HandlerContext, WithWorldGuard}, handler::handle_script_errors, + script::ScriptId, IntoScriptPluginParams, }; use bevy_mod_scripting_functions::ScriptFunctionsPlugin; use test_functions::register_test_functions; use test_utils::test_data::setup_integration_test; +fn dummy_update_system() {} +fn dummy_startup_system() {} + +#[derive(Event)] +struct TestEventFinished; + +struct TestCallbackBuilder { + _ph: PhantomData<(P, L)>, +} + +impl TestCallbackBuilder { + fn build(script_id: impl Into) -> SystemConfigs { + let script_id = script_id.into(); + IntoSystem::into_system( + move |world: &mut World, + system_state: &mut SystemState>>| { + let with_guard = system_state.get_mut(world); + run_test_callback::(&script_id.clone(), with_guard); + system_state.apply(world); + }, + ) + .with_name(L::into_callback_label().to_string()) + .into_configs() + } +} + pub fn execute_integration_test< P: IntoScriptPluginParams, F: FnOnce(&mut World, &mut TypeRegistry), @@ -59,12 +88,12 @@ pub fn execute_integration_test< init_app(&mut app); - #[derive(Event)] - struct TestEventFinished; app.add_event::(); callback_labels!( - OnTest => "on_test" + OnTest => "on_test", + OnTestPostUpdate => "on_test_post_update", + OnTestLast => "on_test_last", ); let script_id = script_id.to_owned(); @@ -73,35 +102,16 @@ pub fn execute_integration_test< let load_system = |server: Res, mut handle: Local>| { *handle = server.load(script_id.to_owned()); }; - let run_on_test_callback = |mut with_guard: WithWorldGuard>| { - let (guard, handler_ctxt) = with_guard.get_mut(); - - if !handler_ctxt.is_script_fully_loaded(script_id.into()) { - return; - } - let res = handler_ctxt.call::( - script_id.into(), - Entity::from_raw(0), - vec![], - guard.clone(), - ); - let e = match res { - Ok(ScriptValue::Error(e)) => e.into(), - Err(e) => e, - _ => { - match guard.with_resource_mut(|mut events: Mut>| { - events.send(TestEventFinished) - }) { - Ok(_) => return, - Err(e) => e.into(), - } - } - }; - handle_script_errors(guard, vec![e].into_iter()) - }; - - app.add_systems(Update, (load_system, run_on_test_callback)); + app.add_systems(Startup, load_system); + app.add_systems(Update, TestCallbackBuilder::::build(script_id)); + app.add_systems( + PostUpdate, + TestCallbackBuilder::::build(script_id), + ); + app.add_systems(Last, TestCallbackBuilder::::build(script_id)); + app.add_systems(Update, dummy_update_system); + app.add_systems(Startup, dummy_startup_system::); app.cleanup(); app.finish(); @@ -115,11 +125,6 @@ pub fn execute_integration_test< return Err("Timeout after 10 seconds".into()); } - let events_completed = app.world_mut().resource_ref::>(); - if events_completed.len() > 0 { - return Ok(()); - } - let error_events = app .world_mut() .resource_mut::>() @@ -131,5 +136,41 @@ pub fn execute_integration_test< .error .display_with_world(WorldGuard::new(app.world_mut()))); } + + let events_completed = app.world_mut().resource_ref::>(); + if events_completed.len() > 0 { + return Ok(()); + } + } +} + +fn run_test_callback( + script_id: &str, + mut with_guard: WithWorldGuard<'_, '_, HandlerContext<'_, P>>, +) { + let (guard, handler_ctxt) = with_guard.get_mut(); + + if !handler_ctxt.is_script_fully_loaded(script_id.to_string().into()) { + return; } + + let res = handler_ctxt.call::( + script_id.to_string().into(), + Entity::from_raw(0), + vec![], + guard.clone(), + ); + let e = match res { + Ok(ScriptValue::Error(e)) => e.into(), + Err(e) => e, + _ => { + match guard.with_resource_mut(|mut events: Mut>| { + events.send(TestEventFinished) + }) { + Ok(_) => return, + Err(e) => e.into(), + } + } + }; + handle_script_errors(guard, vec![e].into_iter()) } diff --git a/crates/testing_crates/script_integration_test_harness/src/test_functions.rs b/crates/testing_crates/script_integration_test_harness/src/test_functions.rs index 4c52fee834..d57ccd810f 100644 --- a/crates/testing_crates/script_integration_test_harness/src/test_functions.rs +++ b/crates/testing_crates/script_integration_test_harness/src/test_functions.rs @@ -7,6 +7,7 @@ use bevy::{ reflect::{Reflect, TypeRegistration}, }; use bevy_mod_scripting_core::{ + asset::Language, bindings::{ function::{ namespace::{GlobalNamespace, NamespaceBuilder}, @@ -75,7 +76,7 @@ pub fn register_test_functions(world: &mut App) { |s: FunctionCallContext, f: DynamicScriptFunctionMut, reg: String| { let world = s.world().unwrap(); - let result = f.call(vec![], FunctionCallContext::default()); + let result = f.call(vec![], FunctionCallContext::new(Language::Unknown)); let err = match result { Ok(_) => { return Err(InteropError::external_error( diff --git a/crates/testing_crates/test_utils/src/test_plugin.rs b/crates/testing_crates/test_utils/src/test_plugin.rs index f56fbcd2f2..6acd16e248 100644 --- a/crates/testing_crates/test_utils/src/test_plugin.rs +++ b/crates/testing_crates/test_utils/src/test_plugin.rs @@ -37,7 +37,7 @@ macro_rules! make_test_plugin { } struct TestContext { - pub invocations: Vec, + pub invocations: Vec<$ident::bindings::script_value::ScriptValue>, } }; }