diff --git a/Cargo.toml b/Cargo.toml index e0402aa5e6..dd9dbe0a6c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -159,7 +159,7 @@ name = "game_of_life" path = "examples/game_of_life.rs" required-features = [ "lua54", - "rhai", + # "rhai", "bevy/file_watcher", "bevy/multi_threaded", ] diff --git a/assets/reload.lua b/assets/reload.lua new file mode 100644 index 0000000000..0d3cbab694 --- /dev/null +++ b/assets/reload.lua @@ -0,0 +1,22 @@ +-- reload.lua +-- +-- An example of the script reload feature. Exercise with this command: +-- ```sh +-- cargo run --features lua54,bevy/file_watcher,bevy/multi_threaded --example run-script -- reload.lua +-- ``` +function on_script_loaded() + world.info("Hello world") +end + +function on_script_unloaded() + world.info("Goodbye world") + return "house" +end + +function on_script_reloaded(value) + if value then + world.info("I'm back. Thanks for the "..value.." keys!") + else + world.info('I have not saved any state before unloading') + end +end diff --git a/assets/tests/add_system/added_systems_run_in_parallel.lua b/assets/tests/add_system/added_systems_run_in_parallel.lua index 2adf8104d0..a0fc7c33ee 100644 --- a/assets/tests/add_system/added_systems_run_in_parallel.lua +++ b/assets/tests/add_system/added_systems_run_in_parallel.lua @@ -22,22 +22,22 @@ function on_test() local expected_dot_graph = [[ digraph { node_0 [label="bevy_mod_scripting_core::bindings::allocator::garbage_collector"]; - node_1 [label="on_test_post_update"]; - node_2 [label="script_integration_test_harness::dummy_before_post_update_system"]; - node_3 [label="script_integration_test_harness::dummy_post_update_system"]; - node_4 [label="custom_system_a"]; - node_5 [label="custom_system_b"]; - node_6 [label="SystemSet GarbageCollection"]; - node_7 [label="SystemSet ScriptSystem(custom_system_a)"]; - node_8 [label="SystemSet ScriptSystem(custom_system_b)"]; - node_0 -> node_6 [color=red, label="child of", arrowhead=diamond]; - node_4 -> node_7 [color=red, label="child of", arrowhead=diamond]; + node_1 [label="bevy_mod_scripting_core::asset::remove_entity_associated_contexts"]; + node_2 [label="on_test_post_update"]; + node_3 [label="script_integration_test_harness::dummy_before_post_update_system"]; + node_4 [label="script_integration_test_harness::dummy_post_update_system"]; + node_5 [label="custom_system_a"]; + node_6 [label="custom_system_b"]; + node_7 [label="SystemSet GarbageCollection"]; + node_8 [label="SystemSet ScriptSystem(custom_system_a)"]; + node_9 [label="SystemSet ScriptSystem(custom_system_b)"]; + node_0 -> node_7 [color=red, label="child of", arrowhead=diamond]; node_5 -> node_8 [color=red, label="child of", arrowhead=diamond]; - node_1 -> node_4 [color=blue, label="runs before", arrowhead=normal]; - node_1 -> node_5 [color=blue, label="runs before", arrowhead=normal]; - node_2 -> node_3 [color=blue, label="runs before", arrowhead=normal]; + node_6 -> node_9 [color=red, label="child of", arrowhead=diamond]; + node_2 -> node_5 [color=blue, label="runs before", arrowhead=normal]; + node_2 -> node_6 [color=blue, label="runs before", arrowhead=normal]; + node_3 -> node_4 [color=blue, label="runs before", arrowhead=normal]; } ]] - assert_str_eq(dot_graph, expected_dot_graph, "Expected the schedule graph to match the expected graph") end diff --git a/benches/benchmarks.rs b/benches/benchmarks.rs index 82b3745d46..b9a71dba70 100644 --- a/benches/benchmarks.rs +++ b/benches/benchmarks.rs @@ -248,14 +248,7 @@ fn script_load_benchmarks(criterion: &mut Criterion) { // lua let plugin = make_test_lua_plugin(); let content = include_str!("../assets/macro_benchmarks/loading/empty.lua"); - run_plugin_script_load_benchmark( - plugin, - "empty Lua", - content, - &mut group, - |rand| format!("{rand}.lua"), - reload_probability, - ); + run_plugin_script_load_benchmark(plugin, "empty Lua", content, &mut group, reload_probability); // rhai let plugin = make_test_rhai_plugin(); @@ -265,7 +258,6 @@ fn script_load_benchmarks(criterion: &mut Criterion) { "empty Rhai", content, &mut group, - |rand| format!("{rand}.rhai"), reload_probability, ); } diff --git a/crates/bevy_mod_scripting_core/Cargo.toml b/crates/bevy_mod_scripting_core/Cargo.toml index d316343556..66bfc8fecd 100644 --- a/crates/bevy_mod_scripting_core/Cargo.toml +++ b/crates/bevy_mod_scripting_core/Cargo.toml @@ -42,6 +42,8 @@ fixedbitset = "0.5" petgraph = "0.6" bevy_mod_debugdump = "0.12" bevy_system_reflection = { path = "../bevy_system_reflection", version = "0.1.2" } +serde = { version = "1.0", features = ["derive"] } +uuid = "1.11" [dev-dependencies] test_utils = { workspace = true } diff --git a/crates/bevy_mod_scripting_core/src/asset.rs b/crates/bevy_mod_scripting_core/src/asset.rs index f3135f55b4..72ee4d172a 100644 --- a/crates/bevy_mod_scripting_core/src/asset.rs +++ b/crates/bevy_mod_scripting_core/src/asset.rs @@ -1,27 +1,27 @@ //! Systems and resources for handling script assets and events use crate::{ - commands::{CreateOrUpdateScript, DeleteScript}, + commands::CreateOrUpdateScript, error::ScriptError, - script::ScriptId, - IntoScriptPluginParams, ScriptingSystemSet, + event::ScriptEvent, + script::{ContextKey, DisplayProxy, ScriptContext, ScriptDomain}, + IntoScriptPluginParams, LanguageExtensions, ScriptComponent, ScriptingSystemSet, StaticScripts, }; use bevy::{ - app::{App, PreUpdate}, - asset::{Asset, AssetEvent, AssetId, AssetLoader, AssetPath, Assets}, - ecs::system::Resource, - log::{debug, info, trace, warn}, + app::{App, PostUpdate, PreUpdate}, + asset::{Asset, AssetEvent, AssetLoader, Assets, LoadState}, + log::{error, info, trace, warn, warn_once}, prelude::{ - Commands, Event, EventReader, EventWriter, IntoSystemConfigs, IntoSystemSetConfigs, Res, - ResMut, + Added, AssetServer, Commands, Entity, EventReader, EventWriter, Handle, IntoSystemConfigs, + IntoSystemSetConfigs, Local, Query, RemovedComponents, Res, ResMut, }, reflect::TypePath, - utils::hashbrown::HashMap, }; -use std::borrow::Cow; +use serde::{Deserialize, Serialize}; +use std::{borrow::Cow, collections::VecDeque}; /// Represents a scripting language. Languages which compile into another language should use the target language as their language. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Default)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)] pub enum Language { /// The Rhai scripting language Rhai, @@ -52,39 +52,88 @@ impl std::fmt::Display for Language { #[derive(Asset, TypePath, Clone)] pub struct ScriptAsset { /// The body of the script - pub content: Box<[u8]>, - /// The virtual filesystem path of the asset, used to map to the script Id for asset backed scripts - pub asset_path: AssetPath<'static>, + pub content: Box<[u8]>, // Any chance a Cow<'static, ?> could work here? + /// The language of the script + pub language: Language, +} + +impl From for ScriptAsset { + fn from(s: String) -> ScriptAsset { + ScriptAsset { + content: s.into_bytes().into_boxed_slice(), + language: Language::default(), + } + } +} + +impl ScriptAsset { + /// Create a new script asset with an unknown language. + pub fn new(s: impl Into) -> Self { + s.into().into() + } } -#[derive(Event, Debug, Clone)] -pub(crate) enum ScriptAssetEvent { - Added(ScriptMetadata), - Removed(ScriptMetadata), - Modified(ScriptMetadata), +/// The queue that evaluates scripts. +type ScriptQueue = VecDeque; +/// Script settings +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct ScriptSettings { + /// Define the language for a script or use the extension if None. + pub language: Option, } #[derive(Default)] /// A loader for script assets pub struct ScriptAssetLoader { /// The file extensions this loader should handle - pub extensions: &'static [&'static str], + language_extensions: LanguageExtensions, + extensions: &'static [&'static str], /// preprocessor to run on the script before saving the content to an asset pub preprocessor: Option Result<(), ScriptError> + Send + Sync>>, } +impl ScriptAssetLoader { + /// Create a new script asset loader for the given extensions. + pub fn new(language_extensions: LanguageExtensions) -> Self { + let extensions: Vec<&'static str> = language_extensions.keys().copied().collect(); + let new_arr_static = Vec::leak(extensions); + Self { + language_extensions, + extensions: new_arr_static, + preprocessor: None, + } + } + + /// For testing purposes. + #[allow(dead_code)] + pub(crate) fn for_extension(extension: &'static str) -> Self { + let mut language_extensions = LanguageExtensions::default(); + language_extensions.insert(extension, Language::Unknown); + Self::new(language_extensions) + } + + /// Add a preprocessor + pub fn with_preprocessor( + mut self, + preprocessor: Box Result<(), ScriptError> + Send + Sync>, + ) -> Self { + self.preprocessor = Some(preprocessor); + self + } +} + #[profiling::all_functions] impl AssetLoader for ScriptAssetLoader { type Asset = ScriptAsset; - type Settings = (); + type Settings = ScriptSettings; type Error = ScriptError; async fn load( &self, reader: &mut dyn bevy::asset::io::Reader, - _settings: &Self::Settings, + settings: &Self::Settings, load_context: &mut bevy::asset::LoadContext<'_>, ) -> Result { let mut content = Vec::new(); @@ -95,9 +144,31 @@ impl AssetLoader for ScriptAssetLoader { if let Some(processor) = &self.preprocessor { processor(&mut content)?; } + let language = settings.language.clone().unwrap_or_else(|| { + let ext = load_context + .path() + .extension() + .and_then(|e| e.to_str()) + .unwrap_or_default(); + self.language_extensions + .get(ext) + .cloned() + .unwrap_or_else(|| { + warn!("Unknown language for {:?}", load_context.path().display()); + Language::Unknown + }) + }); + if language == Language::Lua && cfg!(not(feature = "mlua")) { + warn_once!("Script {:?} is a Lua script but the {:?} feature is not enabled; the script will not be evaluated.", + load_context.path().display(), "mlua"); + } + if language == Language::Rhai && cfg!(not(feature = "rhai")) { + warn_once!("Script {:?} is a Rhai script but the {:?} feature is not enabled; the script will not be evaluated.", + load_context.path().display(), "rhai"); + } let asset = ScriptAsset { content: content.into_boxed_slice(), - asset_path: load_context.asset_path().to_owned(), + language, }; Ok(asset) } @@ -107,280 +178,275 @@ impl AssetLoader for ScriptAssetLoader { } } -#[derive(Clone, Resource)] -/// Settings to do with script assets and how they are handled -pub struct ScriptAssetSettings { - /// Strategy for mapping asset paths to script ids, by default this is the identity function - pub script_id_mapper: AssetPathToScriptIdMapper, - /// Mapping from extension to script language - pub extension_to_language_map: HashMap<&'static str, Language>, - - /// The currently supported asset extensions - /// Should be updated by each scripting plugin to include the extensions it supports. - /// - /// Will be used to populate the script asset loader with the supported extensions - pub supported_extensions: &'static [&'static str], -} - -#[profiling::all_functions] -impl ScriptAssetSettings { - /// Selects the language for a given asset path - pub fn select_script_language(&self, path: &AssetPath) -> Language { - let extension = path - .path() - .extension() - .and_then(|ext| ext.to_str()) - .unwrap_or_default(); - self.extension_to_language_map - .get(extension) - .cloned() - .unwrap_or_default() - } -} - -impl Default for ScriptAssetSettings { - fn default() -> Self { - Self { - script_id_mapper: AssetPathToScriptIdMapper { - map: (|path: &AssetPath| path.path().to_string_lossy().into_owned().into()), - }, - extension_to_language_map: HashMap::from_iter(vec![ - ("lua", Language::Lua), - ("luau", Language::Lua), - ("rhai", Language::Rhai), - ("rn", Language::Rune), - ]), - supported_extensions: &["lua", "luau", "rhai", "rn"], +fn sync_assets( + mut events: EventReader>, + mut script_events: EventWriter, +) { + for event in events.read() { + match event { + AssetEvent::Modified { id } => { + script_events.send(ScriptEvent::Modified { script: *id }); + } + AssetEvent::Added { id } => { + script_events.send(ScriptEvent::Added { script: *id }); + } + AssetEvent::Removed { id } => { + script_events.send(ScriptEvent::Removed { script: *id }); + } + _ => (), } } } -/// Strategy for mapping asset paths to script ids, by default this is the identity function -#[derive(Clone, Copy)] -pub struct AssetPathToScriptIdMapper { - /// The mapping function - pub map: fn(&AssetPath) -> ScriptId, -} - -/// A cache of asset id's to their script id's. Necessary since when we drop an asset we won't have the ability to get the path from the asset. -#[derive(Default, Debug, Resource)] -pub struct ScriptMetadataStore { - /// The map of asset id's to their metadata - pub map: HashMap, ScriptMetadata>, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -/// Metadata for a script asset -pub struct ScriptMetadata { - /// The asset id of the script - pub asset_id: AssetId, - /// The script id of the script - pub script_id: ScriptId, - /// The language of the script - pub language: Language, -} - -#[profiling::all_functions] -impl ScriptMetadataStore { - /// Inserts a new metadata entry - pub fn insert(&mut self, id: AssetId, meta: ScriptMetadata) { - // TODO: new generations of assets are not going to have the same ID as the old one - self.map.insert(id, meta); - } - - /// Gets a metadata entry - pub fn get(&self, id: AssetId) -> Option<&ScriptMetadata> { - self.map.get(&id) - } - - /// Removes a metadata entry - pub fn remove(&mut self, id: AssetId) -> Option { - self.map.remove(&id) +fn sync_components( + script_comps: Query>, + mut removed: RemovedComponents, + mut script_events: EventWriter, +) { + for id in &script_comps { + script_events.send(ScriptEvent::Attached { entity: id }); } - - /// Checks if the store contains a metadata entry - pub fn contains(&self, id: AssetId) -> bool { - self.map.contains_key(&id) + for id in removed.read() { + script_events.send(ScriptEvent::Detached { entity: id }); } } -/// Converts incoming asset events, into internal script asset events, also loads and inserts metadata for newly added scripts +/// Listens to [`ScriptEvent`] events and dispatches [`CreateOrUpdateScript`] and [`DeleteScript`] commands accordingly. +/// +/// Allows for hot-reloading of scripts. #[profiling::function] -pub(crate) fn dispatch_script_asset_events( - mut events: EventReader>, - mut script_asset_events: EventWriter, - assets: Res>, - mut metadata_store: ResMut, - settings: Res, +fn handle_script_events( + mut events: EventReader, + script_assets: Res>, + static_scripts: Res, + scripts: Query<(Entity, &ScriptComponent, Option<&ScriptDomain>)>, + asset_server: Res, + mut script_queue: Local, + mut commands: Commands, ) { for event in events.read() { + trace!("{}: Received script event: {:?}", P::LANGUAGE, event); match event { - AssetEvent::LoadedWithDependencies { id } | AssetEvent::Added { id } => { - // these can occur multiple times, we only send one added event though - if !metadata_store.contains(*id) { - let asset = assets.get(*id); - if let Some(asset) = asset { - let path = &asset.asset_path; - let converter = settings.script_id_mapper.map; - let script_id = converter(path); - - let language = settings.select_script_language(path); - if language == Language::Unknown { - let extension = path - .path() - .extension() - .and_then(|ext| ext.to_str()) - .unwrap_or_default(); - warn!("A script {:?} was added but its language is unknown. Consider adding the {:?} extension to the `ScriptAssetSettings`.", &script_id, extension); + ScriptEvent::Modified { script: id } => { + if let Some(asset) = script_assets.get(*id) { + if asset.language != P::LANGUAGE { + continue; + } + // We need to reload the script for any context it's + // associated with. That could be static scripts, script + // components. + for (entity, script_component, script_domain_maybe) in &scripts { + if let Some(handle) = + script_component.0.iter().find(|handle| handle.id() == *id) + { + commands.queue(CreateOrUpdateScript::

::new(ContextKey { + entity: Some(entity), + script: Some(handle.clone()), + domain: script_domain_maybe.map(|x| x.0), + })); } - let metadata = ScriptMetadata { - asset_id: *id, - script_id, - language, - }; - debug!("Script loaded, populating metadata: {:?}:", metadata); - script_asset_events.send(ScriptAssetEvent::Added(metadata.clone())); - metadata_store.insert(*id, metadata); - } else { - warn!("A script was added but it's asset was not found, failed to compute metadata. This script will not be loaded. Did you forget to store `Handle` somewhere?. {}", id); + } + + if let Some(handle) = static_scripts.scripts.iter().find(|s| s.id() == *id) { + commands.queue(CreateOrUpdateScript::

::new(handle.clone())); } } } - AssetEvent::Removed { id } => { - if let Some(metadata) = metadata_store.get(*id) { - debug!("Script removed: {:?}", metadata); - script_asset_events.send(ScriptAssetEvent::Removed(metadata.clone())); - } else { - warn!("Script metadata not found for removed script asset: {}. Cannot properly clean up script", id); + ScriptEvent::StaticAttached { script } => { + trace!( + "{}: Add static script {} to script queue.", + P::LANGUAGE, + script + ); + script_queue.push_back(ContextKey { + entity: None, + script: Some(Handle::Weak(*script)), + domain: None, + }); + } + ScriptEvent::Attached { entity } => { + trace!( + "{}: Add entity {} contents to script queue.", + P::LANGUAGE, + entity + ); + match scripts.get(*entity) { + Ok((id, script_comp, domain_maybe)) => { + let domain = domain_maybe.map(|x| x.0); + for handle in &script_comp.0 { + script_queue.push_back(ContextKey { + entity: Some(id), + script: Some(handle.clone_weak()), + domain, + }); + } + } + Err(e) => { + error!( + "{}: Unable to look up attached entity {}: {}", + P::LANGUAGE, + entity, + e + ); + } } } - AssetEvent::Modified { id } => { - if let Some(metadata) = metadata_store.get(*id) { - debug!("Script modified: {:?}", metadata); - script_asset_events.send(ScriptAssetEvent::Modified(metadata.clone())); - } else { - warn!("Script metadata not found for modified script asset: {}. Cannot properly update script", id); + _ => (), + } + } + + // Evalute the scripts in the order they were attached. + // + // If a script is not loaded yet, we stop evaluation and try again on the + // next call. + while !script_queue.is_empty() { + let mut script_failed = false; + // NOTE: Maybe using pop_front_if once stabalized. + let script_ready = script_queue + .front() + .map(|context_key| { + // If there is a script, wait for the script to load. + context_key + .script + .as_ref() + .map(|script| { + script_assets.contains(script.id()) + || match asset_server.load_state(script) { + LoadState::NotLoaded => false, + LoadState::Loading => false, + LoadState::Loaded => true, + LoadState::Failed(e) => { + script_failed = true; + error!( + "Failed to load script {} for eval: {e}.", + script.display() + ); + true + } + } + }) + .unwrap_or(true) + }) + .unwrap_or(false); + if !script_ready { + // We can't evaluate it yet. It's still loading. + break; + } + if let Some(context_key) = script_queue.pop_front() { + if script_failed { + continue; + } + let language = context_key + .script + .as_ref() + .and_then(|script_id| script_assets.get(script_id)) + .map(|asset| asset.language.clone()) + .unwrap_or_default(); + if language == P::LANGUAGE { + match context_key.entity { + Some(id) => { + commands + .entity(id) + .queue(CreateOrUpdateScript::

::new(context_key)); + } + None => { + commands.queue(CreateOrUpdateScript::

::new(context_key)); + } } } - _ => {} + } else { + break; } } } -/// Listens to [`ScriptAssetEvent::Removed`] events and removes the corresponding script metadata. -#[profiling::function] -pub(crate) fn remove_script_metadata( - mut events: EventReader, - mut asset_path_map: ResMut, +/// Remove contexts that are associated with removed entities. +fn remove_entity_associated_contexts( + mut removed: RemovedComponents, + mut script_context: ResMut>, ) { - for event in events.read() { - if let ScriptAssetEvent::Removed(metadata) = event { - let previous = asset_path_map.remove(metadata.asset_id); - if let Some(previous) = previous { - debug!("Removed script metadata: {:?}", previous); - } - } + let mut context_key = ContextKey::default(); + for id in removed.read() { + context_key.entity = Some(id); + script_context.remove(&context_key); } } -/// Listens to [`ScriptAssetEvent`] events and dispatches [`CreateOrUpdateScript`] and [`DeleteScript`] commands accordingly. +/// When a [ScriptAsset] is removed---all of its strong handles have +/// dropped---then this system will remove any [StaticScripts] that exist and it +/// will remove any contexts associated solely with that script. /// -/// Allows for hot-reloading of scripts. +/// In BMS version 0.11 and prior, this was the default behavior. Add this +/// system to restore that behavior. #[profiling::function] -pub(crate) fn sync_script_data( - mut events: EventReader, - script_assets: Res>, - mut commands: Commands, +pub fn remove_context_on_script_removal( + mut events: EventReader>, + mut static_scripts: ResMut, + mut script_contexts: ResMut>, ) { for event in events.read() { - let metadata = match event { - ScriptAssetEvent::Added(script_metadata) - | ScriptAssetEvent::Removed(script_metadata) - | ScriptAssetEvent::Modified(script_metadata) => script_metadata, - }; - - if metadata.language != P::LANGUAGE { - continue; - } - - trace!("{}: Received script asset event: {:?}", P::LANGUAGE, event); - match event { - // emitted when a new script asset is loaded for the first time - ScriptAssetEvent::Added(_) | ScriptAssetEvent::Modified(_) => { - 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()), - )); - } + if let AssetEvent::Removed { id } = event { + info!("{}: Asset removed {:?}", P::LANGUAGE, id); + if static_scripts.remove(*id) { + info!("{}: Removing static script {:?}", P::LANGUAGE, id); } - ScriptAssetEvent::Removed(_) => { - info!("{}: Deleting Script: {:?}", P::LANGUAGE, metadata.script_id,); - commands.queue(DeleteScript::

::new(metadata.script_id.clone())); + // We're removing a context because its handle was removed. This + // makes sense specifically for ScriptIdContext. However, it + // doesn't quite work for the other context providers, and it + // requires we keep a script loaded in memory when technically + // it needn't be. + // + // If we want this kind of behavior, it seems like we'd want to + // have handles to contexts. + // + if script_contexts.remove(&ContextKey::from(*id)).is_some() { + info!("{}: Removed context for script {:?}", P::LANGUAGE, id); } - }; + } } } /// Setup all the asset systems for the scripting plugin and the dependencies #[profiling::function] -pub(crate) fn configure_asset_systems(app: &mut App) -> &mut App { +pub(crate) fn configure_asset_systems(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), - ), + (sync_assets, sync_components.after(sync_assets)) + .in_set(ScriptingSystemSet::ScriptAssetDispatch), ) .configure_sets( PreUpdate, ( ScriptingSystemSet::ScriptAssetDispatch.after(bevy::asset::TrackAssets), ScriptingSystemSet::ScriptCommandDispatch - .after(ScriptingSystemSet::ScriptAssetDispatch) - .before(ScriptingSystemSet::ScriptMetadataRemoval), + .after(ScriptingSystemSet::ScriptAssetDispatch), ), - ) - .init_resource::() - .init_resource::() - .add_event::(); - - app + ); } /// Setup all the asset systems for the scripting plugin and the dependencies #[profiling::function] -pub(crate) fn configure_asset_systems_for_plugin( - app: &mut App, -) -> &mut App { +pub(crate) fn configure_asset_systems_for_plugin(app: &mut App) { app.add_systems( PreUpdate, - sync_script_data::

.in_set(ScriptingSystemSet::ScriptCommandDispatch), + handle_script_events::

.in_set(ScriptingSystemSet::ScriptCommandDispatch), + ) + .add_systems( + PostUpdate, + remove_entity_associated_contexts::

, //.in_set(ScriptingSystemSet::EntityRemoval) ); - app } #[cfg(test)] mod tests { - use std::path::{Path, PathBuf}; + use std::path::PathBuf; use bevy::{ app::{App, Update}, - asset::{AssetApp, AssetPlugin, AssetServer, Assets, Handle, LoadState}, + asset::{AssetApp, AssetPath, AssetPlugin, AssetServer, Assets, Handle, LoadState}, + prelude::Resource, MinimalPlugins, }; @@ -394,19 +460,6 @@ mod tests { app } - fn make_test_settings() -> ScriptAssetSettings { - ScriptAssetSettings { - supported_extensions: &[], - script_id_mapper: AssetPathToScriptIdMapper { - map: |path| path.path().to_string_lossy().into_owned().into(), - }, - extension_to_language_map: HashMap::from_iter(vec![ - ("lua", Language::Lua), - ("rhai", Language::Rhai), - ]), - } - } - fn load_asset(app: &mut App, path: &str) -> Handle { let handle = app.world_mut().resource::().load(path); @@ -441,10 +494,7 @@ mod tests { #[test] fn test_asset_loader_loads() { - let loader = ScriptAssetLoader { - extensions: &["script"], - preprocessor: None, - }; + let loader = ScriptAssetLoader::for_extension("script"); let mut app = init_loader_test(loader); let handle = load_asset(&mut app, "test_assets/test_script.script"); @@ -455,11 +505,6 @@ mod tests { .get(&handle) .unwrap(); - assert_eq!( - asset.asset_path, - AssetPath::from_path(&PathBuf::from("test_assets/test_script.script")) - ); - assert_eq!( String::from_utf8(asset.content.clone().to_vec()).unwrap(), "test script".to_string() @@ -468,13 +513,11 @@ mod tests { #[test] fn test_asset_loader_applies_preprocessor() { - let loader = ScriptAssetLoader { - extensions: &["script"], - preprocessor: Some(Box::new(|content| { + let loader = + ScriptAssetLoader::for_extension("script").with_preprocessor(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"); @@ -486,8 +529,8 @@ mod tests { .unwrap(); assert_eq!( - asset.asset_path, - AssetPath::from(PathBuf::from("test_assets/test_script.script")) + handle.path().unwrap(), + &AssetPath::from(PathBuf::from("test_assets/test_script.script")) ); assert_eq!( String::from_utf8(asset.content.clone().to_vec()).unwrap(), @@ -495,39 +538,8 @@ mod tests { ); } - #[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 = AssetPath::from(Path::new("test.lua")); - assert_eq!(settings.select_script_language(&path), Language::Lua); - assert_eq!( - settings.select_script_language(&AssetPath::from(Path::new("test.rhai"))), - Language::Rhai - ); - assert_eq!( - settings.select_script_language(&AssetPath::from(Path::new("test.blob"))), - Language::Unknown - ); - } - - fn run_app_untill_asset_event(app: &mut App, event_kind: AssetEvent) { + #[allow(dead_code)] + fn run_app_until_asset_event(app: &mut App, event_kind: AssetEvent) { let checker_system = |mut reader: EventReader>, mut event_target: ResMut| { println!("Reading asset events this frame"); @@ -587,61 +599,6 @@ mod tests { fn build_runtime() -> Self::R {} } - #[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/globals/core.rs b/crates/bevy_mod_scripting_core/src/bindings/globals/core.rs index 44e85bd38d..8e31b4bc26 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/globals/core.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/globals/core.rs @@ -53,7 +53,7 @@ impl Plugin for CoreScriptGlobalsPlugin { app.init_resource::(); } fn finish(&self, app: &mut bevy::app::App) { - profiling::function_scope!("app finish"); + // profiling::function_scope!("app finish"); if self.register_static_references { register_static_core_globals(app.world_mut(), self.filter); @@ -121,7 +121,7 @@ impl CoreGlobals { >, InteropError, > { - profiling::function_scope!("registering core globals"); + // profiling::function_scope!("registering core globals"); let type_registry = guard.type_registry(); let type_registry = type_registry.read(); let mut type_cache = HashMap::::default(); diff --git a/crates/bevy_mod_scripting_core/src/bindings/script_system.rs b/crates/bevy_mod_scripting_core/src/bindings/script_system.rs index c523b0d231..edf427e8cd 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/script_system.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/script_system.rs @@ -17,10 +17,11 @@ use crate::{ extractors::get_all_access_ids, handler::CallbackSettings, runtime::RuntimeContainer, - script::{ScriptId, Scripts}, + script::{ScriptContext, Domain, ContextKey}, IntoScriptPluginParams, }; use bevy::{ + prelude::AssetServer, ecs::{ archetype::{ArchetypeComponentId, ArchetypeGeneration}, component::{ComponentId, Tick}, @@ -35,7 +36,8 @@ use bevy::{ utils::hashbrown::HashSet, }; use bevy_system_reflection::{ReflectSchedule, ReflectSystem}; -use std::{any::TypeId, borrow::Cow, hash::Hash, marker::PhantomData, ops::Deref}; +use std::{any::TypeId, borrow::Cow, hash::Hash, marker::PhantomData, ops::Deref, sync::Arc}; +use parking_lot::Mutex; #[derive(Clone, Hash, PartialEq, Eq)] /// a system set for script systems. pub struct ScriptSystemSet(Cow<'static, str>); @@ -78,12 +80,15 @@ enum ScriptSystemParamDescriptor { EntityQuery(ScriptQueryBuilder), } +type ScriptPath = Cow<'static, str>; + /// A builder for systems living in scripts #[derive(Reflect, Clone)] #[reflect(opaque)] pub struct ScriptSystemBuilder { pub(crate) name: CallbackLabel, - pub(crate) script_id: ScriptId, + pub(crate) script_id: ScriptPath, + // domain: Option, before: Vec, after: Vec, system_params: Vec, @@ -93,12 +98,13 @@ pub struct ScriptSystemBuilder { #[profiling::all_functions] impl ScriptSystemBuilder { /// Creates a new script system builder - pub fn new(name: CallbackLabel, script_id: ScriptId) -> Self { + pub fn new(name: CallbackLabel, script_id: ScriptPath, _domain: Option) -> Self { Self { before: vec![], after: vec![], name, script_id, + // domain, system_params: vec![], is_exclusive: false, } @@ -194,7 +200,7 @@ impl ScriptSystemBuilder { } struct DynamicHandlerContext<'w, P: IntoScriptPluginParams> { - scripts: &'w Scripts

, + script_context: &'w ScriptContext

, callback_settings: &'w CallbackSettings

, context_loading_settings: &'w ContextLoadingSettings

, runtime_container: &'w RuntimeContainer

, @@ -208,9 +214,8 @@ impl<'w, P: IntoScriptPluginParams> DynamicHandlerContext<'w, P> { )] pub fn init_param(world: &mut World, system: &mut FilteredAccessSet) { let mut access = FilteredAccess::::matches_nothing(); - let scripts_res_id = world - .resource_id::>() - .expect("Scripts resource not found"); + // let scripts_res_id = world + // .query::<&Script

>(); let callback_settings_res_id = world .resource_id::>() .expect("CallbackSettings resource not found"); @@ -221,7 +226,6 @@ impl<'w, P: IntoScriptPluginParams> DynamicHandlerContext<'w, P> { .resource_id::>() .expect("RuntimeContainer resource not found"); - access.add_resource_read(scripts_res_id); access.add_resource_read(callback_settings_res_id); access.add_resource_read(context_loading_settings_res_id); access.add_resource_read(runtime_container_res_id); @@ -236,7 +240,9 @@ impl<'w, P: IntoScriptPluginParams> DynamicHandlerContext<'w, P> { pub fn get_param(system: &UnsafeWorldCell<'w>) -> Self { unsafe { Self { - scripts: system.get_resource().expect("Scripts resource not found"), + script_context: system + .get_resource() + .expect("Scripts resource not found"), callback_settings: system .get_resource() .expect("CallbackSettings resource not found"), @@ -254,15 +260,14 @@ impl<'w, P: IntoScriptPluginParams> DynamicHandlerContext<'w, P> { pub fn call_dynamic_label( &self, label: &CallbackLabel, - script_id: &ScriptId, - entity: Entity, + context_key: &ContextKey, + context: Option<&Arc>>, payload: Vec, guard: WorldGuard<'_>, ) -> Result { // find script - let script = match self.scripts.scripts.get(script_id) { - Some(script) => script, - None => return Err(InteropError::missing_script(script_id.clone()).into()), + let Some(context) = context.or_else(|| self.script_context.get(context_key)) else { + return Err(InteropError::missing_context(context_key.clone()).into()); }; // call the script @@ -272,13 +277,12 @@ impl<'w, P: IntoScriptPluginParams> DynamicHandlerContext<'w, P> { .context_pre_handling_initializers; let runtime = &self.runtime_container.runtime; - let mut context = script.context.lock(); + let mut context = context.lock(); CallbackSettings::

::call( handler, payload, - entity, - script_id, + context_key, label, &mut context, pre_handling_initializers, @@ -339,10 +343,11 @@ pub struct DynamicScriptSystem { /// cause a conflict pub(crate) archetype_component_access: Access, pub(crate) last_run: Tick, - target_script: ScriptId, + target_script: ScriptPath, archetype_generation: ArchetypeGeneration, system_param_descriptors: Vec, state: Option, + domain: Option, _marker: std::marker::PhantomData P>, } @@ -364,6 +369,7 @@ impl IntoSystem<(), (), IsDynamicScriptSystem

> last_run: Default::default(), target_script: builder.script_id, state: None, + domain: None, component_access_set: Default::default(), archetype_component_access: Default::default(), _marker: Default::default(), @@ -420,6 +426,10 @@ impl System for DynamicScriptSystem

{ }; let mut payload = Vec::with_capacity(state.system_params.len()); + let script = { + let asset_server = world.world().resource::(); + asset_server.load(&*self.target_script) + }; let guard = if self.exclusive { // safety: we are an exclusive system, therefore the cell allows us to do this let world = unsafe { world.world_mut() }; @@ -482,20 +492,27 @@ impl System for DynamicScriptSystem

{ } } - // now that we have everything ready, we need to run the callback on the targetted scripts - // let's start with just calling the one targetted script + // Now that we have everything ready, we need to run the callback on the + // targetted scripts. Let's start with just calling the one targetted + // script. let handler_ctxt = DynamicHandlerContext::

::get_param(&world); + let context_key = ContextKey { + script: Some(script), + entity: None, + domain: self.domain, + }; let result = handler_ctxt.call_dynamic_label( &state.callback_label, - &self.target_script, - Entity::from_raw(0), + &context_key, + None,// context payload, guard.clone(), ); - // TODO: emit error events via commands, maybe accumulate in state instead and use apply + // TODO: Emit error events via commands, maybe accumulate in state + // instead and use apply. match result { Ok(_) => {} Err(err) => { @@ -722,7 +739,7 @@ mod test { }); // now dynamically add script system via builder - let mut builder = ScriptSystemBuilder::new("test".into(), "empty_script".into()); + let mut builder = ScriptSystemBuilder::new("test".into(), "empty_script".into(), None); builder.before_system(test_system); let _ = builder diff --git a/crates/bevy_mod_scripting_core/src/commands.rs b/crates/bevy_mod_scripting_core/src/commands.rs index b3c7cd069d..04d50d7144 100644 --- a/crates/bevy_mod_scripting_core/src/commands.rs +++ b/crates/bevy_mod_scripting_core/src/commands.rs @@ -7,30 +7,39 @@ use crate::{ error::{InteropError, ScriptError}, event::{ CallbackLabel, IntoCallbackLabel, OnScriptLoaded, OnScriptReloaded, OnScriptUnloaded, - ScriptCallbackResponseEvent, + ScriptCallbackResponseEvent, ScriptEvent, }, extractors::{with_handler_system_state, HandlerContext}, handler::{handle_script_errors, send_callback_response}, - script::{Script, ScriptId, Scripts, StaticScripts}, - IntoScriptPluginParams, + script::{ContextKey, ContextRule, DisplayProxy, StaticScripts}, + IntoScriptPluginParams, ScriptContext, }; -use bevy::{asset::Handle, ecs::entity::Entity, log::debug, prelude::Command}; -use parking_lot::Mutex; -use std::{marker::PhantomData, sync::Arc}; +use bevy::{ + asset::Handle, + ecs::entity::Entity, + log::{debug, error, warn}, + prelude::{Command, EntityCommand}, +}; +use std::marker::PhantomData; /// Deletes a script with the given ID +/// +/// And deletes its associated context. pub struct DeleteScript { - /// The ID of the script to delete - pub id: ScriptId, + /// The context key + pub context_key: ContextKey, + /// Force deletes the context even if the context is shared. + pub force: bool, /// hack to make this Send, C does not need to be Send since it is not stored in the command pub _ph: PhantomData, } impl DeleteScript

{ /// Creates a new DeleteScript command with the given ID - pub fn new(id: ScriptId) -> Self { + pub fn new(context_key: impl Into) -> Self { Self { - id, + context_key: context_key.into(), + force: false, _ph: PhantomData, } } @@ -39,95 +48,127 @@ impl DeleteScript

{ impl Command for DeleteScript

{ fn apply(self, world: &mut bevy::prelude::World) { // first apply unload callback - RunScriptCallback::

::new( - self.id.clone(), - Entity::from_raw(0), - OnScriptUnloaded::into_callback_label(), - vec![], - false, - ) - .apply(world); + Command::apply( + RunScriptCallback::

::new( + self.context_key.clone(), + OnScriptUnloaded::into_callback_label(), + vec![], + false, + ), + world, + ); + + let mut deleted = false; + if let Some(script_id) = self.context_key.script.as_ref() { + { + let mut scripts = world.get_resource_or_init::(); + if scripts.remove(script_id) { + debug!("Deleted static script {}", script_id.display()); + deleted = true; + } + } + { + let mut script_contexts = world.get_resource_or_init::>(); + let delete_context = match script_contexts.policy.which_rule(&self.context_key) { + Some(ContextRule::Domain | ContextRule::Shared) => { + // Don't delete these shared contexts. + // + // Perhaps this should be defined by the rule itself. + false + } + _ => true, + }; - let mut scripts = world.get_resource_or_init::>(); - if scripts.remove(self.id.clone()) { - debug!("Deleted script with id: {}", self.id); - } else { + if (self.force || delete_context) + && script_contexts.remove(&self.context_key).is_some() + { + bevy::log::info!( + "{}: Deleted context for script {:?}", + P::LANGUAGE, + script_id.display() + ); + deleted = true; + } + } + } + if !deleted { bevy::log::error!( - "Attempted to delete script with id: {} but it does not exist, doing nothing!", - self.id + "Attempted to delete script context {} but it does not exist; doing nothing!", + self.context_key ); } } } +impl EntityCommand for DeleteScript

{ + fn apply(mut self, entity: Entity, world: &mut bevy::prelude::World) { + self.context_key.entity = Some(entity); + Command::apply(self, world) + } +} + /// Creates new script with the given ID, if a script with the given ID already exists, this is treated as an update /// /// If script comes from an asset, expects it to be loaded, otherwise this command will fail to process the script. pub struct CreateOrUpdateScript { - id: ScriptId, - content: Box<[u8]>, - asset: Option>, + context_key: ContextKey, + // It feels like we're using a Box, which requires a clone merely to satisfy the Command trait. + content: Option>, // Hack to make this Send, C does not need to be Send since it is not stored in the command _ph: std::marker::PhantomData, } #[profiling::all_functions] impl CreateOrUpdateScript

{ - /// Creates a new CreateOrUpdateScript command with the given ID, content and asset - pub fn new(id: ScriptId, content: Box<[u8]>, asset: Option>) -> Self { + /// Creates a new CreateOrUpdateScript command with the given ID, content + pub fn new(context_key: impl Into) -> Self { Self { - id, - content, - asset, + context_key: context_key.into(), + content: None, _ph: std::marker::PhantomData, } } + /// Add content to be evaluated. + pub fn with_content(mut self, content: impl Into) -> Self { + let content = content.into(); + self.content = Some(content.into_bytes().into_boxed_slice()); + self + } fn reload_context( - &self, + context_key: &ContextKey, + content: &[u8], + context: &mut P::C, guard: WorldGuard, handler_ctxt: &HandlerContext

, ) -> Result<(), ScriptError> { - bevy::log::debug!("{}: reloading script with id: {}", P::LANGUAGE, self.id); - let existing_script = match handler_ctxt.scripts.scripts.get(&self.id) { - Some(script) => script, - None => { - return Err( - InteropError::invariant("Tried to reload script which doesn't exist").into(), - ) - } - }; - + bevy::log::debug!("{}: reloading context {}", P::LANGUAGE, context_key); // reload context - let mut context = existing_script.context.lock(); - (ContextBuilder::

::reload)( handler_ctxt.context_loading_settings.loader.reload, - &self.id, - &self.content, - &mut context, + context_key, + content, + context, &handler_ctxt.context_loading_settings.context_initializers, &handler_ctxt .context_loading_settings .context_pre_handling_initializers, guard.clone(), &handler_ctxt.runtime_container.runtime, - )?; - - Ok(()) + ) } fn load_context( - &self, + context_key: &ContextKey, + content: &[u8], guard: WorldGuard, - handler_ctxt: &mut HandlerContext

, - ) -> Result<(), ScriptError> { - bevy::log::debug!("{}: loading script with id: {}", P::LANGUAGE, self.id); - + handler_ctxt: &HandlerContext

, + ) -> Result { + bevy::log::debug!("{}: loading context {}", P::LANGUAGE, context_key); let context = (ContextBuilder::

::load)( handler_ctxt.context_loading_settings.loader.load, - &self.id, - &self.content, + context_key, + content, &handler_ctxt.context_loading_settings.context_initializers, &handler_ctxt .context_loading_settings @@ -135,196 +176,204 @@ impl CreateOrUpdateScript

{ guard.clone(), &handler_ctxt.runtime_container.runtime, )?; - - let context = Arc::new(Mutex::new(context)); - - handler_ctxt.scripts.scripts.insert( - self.id.clone(), - Script { - id: self.id.clone(), - asset: self.asset.clone(), - context, - }, - ); - Ok(()) + Ok(context) } fn before_load( - &self, + context_key: ContextKey, world: WorldGuard, - handler_ctxt: &mut HandlerContext

, - is_reload: bool, + handler_ctxt: &HandlerContext

, ) -> Option { - if is_reload { - // if something goes wrong, the error will be handled in the command - // but we will not pass the script state to the after_load - return RunScriptCallback::

::new( - self.id.clone(), - Entity::from_raw(0), - OnScriptUnloaded::into_callback_label(), - vec![], - false, - ) - .with_context(P::LANGUAGE) - .with_context("saving reload state") - .run_with_handler(world, handler_ctxt) - .ok(); - } - - None + // if something goes wrong, the error will be handled in the command + // but we will not pass the script state to the after_load + RunScriptCallback::

::new( + context_key.clone(), + OnScriptUnloaded::into_callback_label(), + vec![], + false, + ) + .with_context(P::LANGUAGE) + .with_context("saving reload state") + .run_with_handler(world, handler_ctxt) + .inspect_err(|e| { + error!( + "{}: on_script_unloaded problem for {}: {e}", + P::LANGUAGE, + &context_key + ); + }) + .ok() } fn after_load( - &self, + context_key: ContextKey, world: WorldGuard, - handler_ctxt: &mut HandlerContext

, + handler_ctxt: &HandlerContext

, script_state: Option, - is_reload: bool, ) { let _ = RunScriptCallback::

::new( - self.id.clone(), - Entity::from_raw(0), + context_key.clone(), OnScriptLoaded::into_callback_label(), vec![], false, ) .with_context(P::LANGUAGE) .with_context("on loaded callback") - .run_with_handler(world.clone(), handler_ctxt); + .run_with_handler(world.clone(), handler_ctxt) + .inspect_err(|e| { + error!( + "{}: on_script_loaded problem for {}: {e}", + P::LANGUAGE, + &context_key + ); + }); - if is_reload { - let state = script_state.unwrap_or(ScriptValue::Unit); + if let Some(state) = script_state { let _ = RunScriptCallback::

::new( - self.id.clone(), - Entity::from_raw(0), + context_key.clone(), OnScriptReloaded::into_callback_label(), vec![state], false, ) .with_context(P::LANGUAGE) .with_context("on reloaded callback") - .run_with_handler(world, handler_ctxt); + .run_with_handler(world, handler_ctxt) + .inspect_err(|e| { + error!( + "{}: on_script_reloaded problem for {}: {e}", + P::LANGUAGE, + &context_key + ); + }); } } - fn handle_global_context( - &self, + pub(crate) fn create_or_update_script( + context_key: &ContextKey, + content: Option<&[u8]>, guard: WorldGuard, handler_ctxt: &mut HandlerContext

, - ) -> (Result<(), ScriptError>, Option, bool) { - let existing_context = handler_ctxt - .scripts - .scripts - .values() - .next() - .map(|s| s.context.clone()); - - debug!( - "{}: CreateOrUpdateScript command applying to global context (script_id: {}, new context?: {}, new script?: {})", - P::LANGUAGE, - self.id, - existing_context.is_none(), - !handler_ctxt.scripts.scripts.contains_key(&self.id) - ); - - let is_reload = existing_context.is_some(); - - if let Some(context) = existing_context { - // point all new scripts to the shared context - handler_ctxt.scripts.scripts.insert( - self.id.clone(), - Script { - id: self.id.clone(), - asset: self.asset.clone(), - context, - }, - ); - } - - let script_state = self.before_load(guard.clone(), handler_ctxt, is_reload); - - let result = if is_reload { - self.reload_context(guard, handler_ctxt) - } else { - self.load_context(guard, handler_ctxt) + ) -> Result, ScriptError> { + let mut script_id = &Handle::default(); + let Some(content) = content.or_else(|| { + context_key.script.as_ref().and_then(|id| { + script_id = id; + handler_ctxt + .scripts + .get(script_id) + .map(|script| &*script.content) + // .ok_or_else(|| ScriptError::new(InteropError::missing_script(id))) + }) + }) else { + warn!("No content for context {} to create or update", context_key); + match &context_key.script { + Some(script_id) => { + return Err(ScriptError::new(InteropError::missing_script( + script_id.clone(), + ))); + } + None => { + return Err(ScriptError::new(String::from( + "No content and no script given.", + ))); + } + } }; - - (result, script_state, is_reload) - } - - fn handle_individual_context( - &self, - guard: WorldGuard, - handler_ctxt: &mut HandlerContext

, - ) -> (Result<(), ScriptError>, Option, bool) { - let is_new_script = !handler_ctxt.scripts.scripts.contains_key(&self.id); - let is_reload = !is_new_script; - - debug!( - "{}: CreateOrUpdateScript command applying (script_id: {}, new context?: {}, new script?: {})", - P::LANGUAGE, - self.id, - is_new_script, - !handler_ctxt.scripts.scripts.contains_key(&self.id) - ); - - let script_state = self.before_load(guard.clone(), handler_ctxt, is_reload); - let result = if is_new_script { - self.load_context(guard, handler_ctxt) - } else { - self.reload_context(guard, handler_ctxt) + let phrase; + let success; + let mut script_state = None; + let result = match handler_ctxt.script_context.get(context_key) { + Some(context) => { + bevy::log::debug!("{}: reloading context {}", P::LANGUAGE, context_key); + script_state = Self::before_load(context_key.clone(), guard.clone(), handler_ctxt); + + let mut lcontext = context.lock(); + phrase = "reloading"; + success = "updated"; + Self::reload_context( + context_key, + content, + &mut lcontext, + guard.clone(), + handler_ctxt, + ) + .map(|_| None) + } + None => { + bevy::log::debug!("{}: loading context {}", P::LANGUAGE, context_key); + phrase = "loading"; + success = "created"; + Self::load_context(context_key, content, guard.clone(), handler_ctxt).map(Some) + } }; - (result, script_state, is_reload) - } -} -#[profiling::all_functions] -impl Command for CreateOrUpdateScript

{ - fn apply(self, world: &mut bevy::prelude::World) { - with_handler_system_state(world, |guard, handler_ctxt: &mut HandlerContext

| { - let (result, script_state, is_reload) = - match handler_ctxt.context_loading_settings.assignment_strategy { - crate::context::ContextAssignmentStrategy::Global => { - self.handle_global_context(guard.clone(), handler_ctxt) - } - crate::context::ContextAssignmentStrategy::Individual => { - self.handle_individual_context(guard.clone(), handler_ctxt) + match result { + Ok(maybe_context) => { + if let Some(context) = maybe_context { + if handler_ctxt + .script_context + .insert(context_key, context) + .is_err() + { + warn!("Unable to insert script context for {}.", context_key); } - }; + } - if let Err(err) = result { + bevy::log::debug!( + "{}: script {} successfully {}", + P::LANGUAGE, + context_key, + success, + ); + Ok(script_state) + } + Err(err) => { handle_script_errors( guard, vec![err - .with_script(self.id.clone()) + .clone() + .with_script(script_id.display()) .with_context(P::LANGUAGE) - .with_context(if is_reload { - "reloading an existing script or context" - } else { - "loading a new script or context" - })] + .with_context(phrase)] .into_iter(), ); - return; // don't run after_load if there was an error + Err(err) } + } + } +} - bevy::log::debug!( - "{}: script with id: {} successfully created or updated", - P::LANGUAGE, - self.id +#[profiling::all_functions] +impl Command for CreateOrUpdateScript

{ + fn apply(self, world: &mut bevy::prelude::World) { + with_handler_system_state(world, |guard, handler_ctxt: &mut HandlerContext

| { + let result = Self::create_or_update_script( + &self.context_key, + self.content.as_deref(), + guard.clone(), + handler_ctxt, ); - - self.after_load(guard, handler_ctxt, script_state, is_reload); + if let Ok(script_state) = result { + Self::after_load(self.context_key, guard, handler_ctxt, script_state); + } else { + // XXX: The error is logged by create_or_update_script? + } }); } } +#[profiling::all_functions] +impl EntityCommand for CreateOrUpdateScript

{ + fn apply(mut self, entity: Entity, world: &mut bevy::prelude::World) { + self.context_key.entity = Some(entity); + Command::apply(self, world); + } +} + /// Runs a callback on the script with the given ID if it exists pub struct RunScriptCallback { - /// The ID of the script to run the callback on - pub id: ScriptId, - /// The entity to use for the callback - pub entity: Entity, + /// The context key + pub context_key: ContextKey, /// The callback to run pub callback: CallbackLabel, /// optional context passed down to errors @@ -340,16 +389,14 @@ pub struct RunScriptCallback { impl RunScriptCallback

{ /// Creates a new RunCallbackCommand with the given ID, callback and arguments pub fn new( - id: ScriptId, - entity: Entity, + context_key: impl Into, callback: CallbackLabel, args: Vec, trigger_response: bool, ) -> Self { Self { - id, - entity, - context: Default::default(), + context_key: context_key.into(), + context: vec![], callback, args, trigger_response, @@ -367,24 +414,12 @@ impl RunScriptCallback

{ pub fn run_with_handler( self, guard: WorldGuard, - handler_ctxt: &mut HandlerContext

, + handler_ctxt: &HandlerContext

, ) -> Result { - if !handler_ctxt.is_script_fully_loaded(self.id.clone()) { - bevy::log::error!( - "{}: Cannot apply callback {} command, as script does not exist: {}. Ignoring.", - P::LANGUAGE, - self.callback, - self.id - ); - return Err(ScriptError::new(InteropError::missing_script( - self.id.clone(), - ))); - } - let result = handler_ctxt.call_dynamic_label( &self.callback, - &self.id, - self.entity, + &self.context_key, + None, self.args, guard.clone(), ); @@ -393,23 +428,27 @@ impl RunScriptCallback

{ send_callback_response( guard.clone(), ScriptCallbackResponseEvent::new( - self.entity, self.callback, - self.id.clone(), + self.context_key.clone(), result.clone(), ), ); } - if let Err(err) = &result { - let mut error_with_context = err.clone().with_script(self.id).with_context(P::LANGUAGE); - for ctxt in &self.context { + if let Err(ref err) = result { + let mut error_with_context = + if let Some(script_id) = self.context_key.script.as_ref() { + err.clone().with_script(script_id.display()) + } else { + err.clone() + } + .with_context(P::LANGUAGE); + for ctxt in self.context { error_with_context = error_with_context.with_context(ctxt); } handle_script_errors(guard, vec![error_with_context].into_iter()); } - result } @@ -425,40 +464,49 @@ impl RunScriptCallback

{ impl Command for RunScriptCallback

{ fn apply(self, world: &mut bevy::prelude::World) { - // internals handle this + // Internals handle this. let _ = self.run(world); } } +impl EntityCommand for RunScriptCallback

{ + fn apply(mut self, id: Entity, world: &mut bevy::prelude::World) { + self.context_key.entity = Some(id); + Command::apply(self, world); + } +} + /// Adds a static script to the collection of static scripts pub struct AddStaticScript { /// The ID of the script to add - id: ScriptId, + id: Handle, } impl AddStaticScript { /// Creates a new AddStaticScript command with the given ID - pub fn new(id: impl Into) -> Self { + pub fn new(id: impl Into>) -> Self { Self { id: id.into() } } } impl Command for AddStaticScript { fn apply(self, world: &mut bevy::prelude::World) { + let script_id = self.id.id(); let mut static_scripts = world.get_resource_or_init::(); static_scripts.insert(self.id); + world.send_event(ScriptEvent::StaticAttached { script: script_id }); } } /// Removes a static script from the collection of static scripts pub struct RemoveStaticScript { /// The ID of the script to remove - id: ScriptId, + id: Handle, } impl RemoveStaticScript { /// Creates a new RemoveStaticScript command with the given ID - pub fn new(id: ScriptId) -> Self { + pub fn new(id: Handle) -> Self { Self { id } } } @@ -467,17 +515,15 @@ impl RemoveStaticScript { impl Command for RemoveStaticScript { fn apply(self, world: &mut bevy::prelude::World) { let mut static_scripts = world.get_resource_or_init::(); - static_scripts.remove(self.id); + static_scripts.remove(&self.id); } } #[cfg(test)] mod test { use bevy::{ - app::App, - ecs::event::Events, log::{Level, LogPlugin}, - prelude::{Entity, World}, + prelude::*, }; use crate::{ @@ -486,7 +532,8 @@ mod test { context::{ContextBuilder, ContextLoadingSettings}, handler::CallbackSettings, runtime::RuntimeContainer, - script::Scripts, + script::{ContextPolicy, ScriptContext}, + ManageStaticScripts, }; use super::*; @@ -496,12 +543,16 @@ mod test { let mut app = App::new(); app.add_event::(); + app.add_event::(); + app.add_plugins(bevy::asset::AssetPlugin::default()); + app.init_asset::(); app.add_plugins(LogPlugin { filter: "bevy_mod_scripting_core=debug,info".to_owned(), level: Level::TRACE, ..Default::default() }); - + app.add_plugins(crate::configure_asset_systems); + app.add_plugins(crate::configure_asset_systems_for_plugin::); app.insert_resource(ContextLoadingSettings:: { loader: ContextBuilder { load: |name, c, init, pre_run_init, _| { @@ -510,7 +561,7 @@ mod test { init(name, &mut context)?; } for init in pre_run_init { - init(name, Entity::from_raw(0), &mut context)?; + init(name, &mut context)?; } Ok(context) }, @@ -520,7 +571,7 @@ mod test { init(name, &mut new)?; } for init in pre_run_init { - init(name, Entity::from_raw(0), &mut new)?; + init(name, &mut new)?; } existing.push_str(" | "); existing.push_str(&new); @@ -532,7 +583,7 @@ mod test { c.push_str(" initialized"); Ok(()) }], - context_pre_handling_initializers: vec![|_, _, c| { + context_pre_handling_initializers: vec![|_, c| { c.push_str(" pre-handling-initialized"); Ok(()) }], @@ -542,13 +593,10 @@ mod test { }) .init_resource::() .insert_resource(CallbackSettings:: { - callback_handler: |_, _, _, callback, c, _, _| { + callback_handler: |_, _, callback, c, _, _| { c.push_str(format!(" callback-ran-{callback}").as_str()); Ok(ScriptValue::Unit) }, - }) - .insert_resource(Scripts:: { - scripts: Default::default(), }); app @@ -566,21 +614,23 @@ mod test { } } - fn assert_context_and_script(world: &World, id: &str, context: &str, message: &str) { - let scripts = world.get_resource::>().unwrap(); + fn assert_context_and_script( + world: &World, + context_key: impl Into, + context: &str, + message: &str, + ) { + let scripts = world.get_resource::>().unwrap(); - let script = scripts - .scripts - .get(id) - .unwrap_or_else(|| panic!("Script not found {message}")); + let context_key: ContextKey = context_key.into(); + let context_arc = scripts + .get(&context_key) + .unwrap_or_else(|| panic!("Context not found {message}")); - assert_eq!(id, script.id); - let found_context = script.context.lock(); - pretty_assertions::assert_eq!( - *context, - *found_context, - "expected context != actual context. {message}" - ); + // assert_eq!(id, script.id); + let found_context = context_arc.lock(); + + assert_eq!(context, found_context.as_str(), "{message}"); } fn assert_response_events( @@ -600,7 +650,7 @@ mod test { ); for (a, b) in responses.iter().zip(expected.iter()) { assert_eq!(a.label, b.label, "{context}"); - assert_eq!(a.script, b.script, "{context}"); + assert_eq!(a.context_key, b.context_key, "{context}"); assert_eq!(a.response, b.response, "{context}"); } } @@ -608,25 +658,27 @@ mod test { #[test] fn test_commands_with_default_assigner() { let mut app = setup_app(); - - let content = "content".as_bytes().to_vec().into_boxed_slice(); - let command = CreateOrUpdateScript::::new("script".into(), content, None); - command.apply(app.world_mut()); + // app.insert_resource(ScriptContext::::per_script()); + let script = add_script(&mut app, "content"); + let command = CreateOrUpdateScript::::new(script.clone()); + Command::apply(command, app.world_mut()); // check script let loaded_script_expected_content = "content initialized pre-handling-initialized callback-ran-on_script_loaded"; assert_context_and_script( app.world_mut(), - "script", + script.id(), loaded_script_expected_content, "Initial script creation failed", ); // 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()); + let content = "new content"; + // let command = CreateOrUpdateScript::::new(script.clone(), Some(content), None); + let command = + CreateOrUpdateScript::::new(script.clone()).with_content(content); + Command::apply(command, app.world_mut()); // check script let reloaded_script_expected_content = format!("{loaded_script_expected_content} callback-ran-on_script_unloaded \ @@ -634,39 +686,40 @@ mod test { assert_context_and_script( app.world_mut(), - "script", + script.id(), &reloaded_script_expected_content, "Script update failed", ); // create second script - let content = "content2".as_bytes().to_vec().into_boxed_slice(); - let command = CreateOrUpdateScript::::new("script2".into(), content, None); + let script2 = add_script(&mut app, "content2"); + let command = CreateOrUpdateScript::::new(script2.clone()); - command.apply(app.world_mut()); + Command::apply(command, app.world_mut()); // check second script assert_context_and_script( app.world_mut(), - "script2", + script2.id(), "content2 initialized pre-handling-initialized callback-ran-on_script_loaded", "Second script creation failed", ); // run a specific callback on the first script - RunScriptCallback::::new( - "script".into(), - Entity::from_raw(0), - OnScriptLoaded::into_callback_label(), - vec![], - true, - ) - .apply(app.world_mut()); + Command::apply( + RunScriptCallback::::new( + script.clone(), + OnScriptLoaded::into_callback_label(), + vec![], + true, + ), + app.world_mut(), + ); // check this has applied assert_context_and_script( app.world_mut(), - "script", + script.id(), &format!("{reloaded_script_expected_content} callback-ran-on_script_loaded"), "Script callback failed", ); @@ -674,26 +727,22 @@ mod test { assert_response_events( app.world_mut(), vec![ScriptCallbackResponseEvent::new( - Entity::from_raw(0), OnScriptLoaded::into_callback_label(), - "script".into(), + script.id(), Ok(ScriptValue::Unit), )] .into_iter(), - "script callback failed", + "script callback response failed", ); // delete both scripts - let command = DeleteScript::::new("script".into()); - command.apply(app.world_mut()); - let command = DeleteScript::::new("script2".into()); - command.apply(app.world_mut()); + let command = DeleteScript::::new(script.id()); + Command::apply(command, app.world_mut()); + let command = DeleteScript::::new(script2.id()); + Command::apply(command, app.world_mut()); // check that the scripts are gone - let scripts = app - .world_mut() - .get_resource::>() - .unwrap(); + let scripts = app.world_mut().get_resource::().unwrap(); assert!(scripts.scripts.is_empty()); assert_response_events( @@ -703,40 +752,47 @@ mod test { ); } + fn add_script(app: &mut App, content: impl Into) -> Handle { + app.world_mut() + .resource_mut::>() + .add(ScriptAsset::from(content.into())) + } + + fn update_script(app: &mut App, handle: AssetId, content: impl Into) { + let mut scripts = app.world_mut().resource_mut::>(); + let script_asset = scripts.get_mut(handle).unwrap(); + script_asset.content = content.into().into_bytes().into_boxed_slice(); + } + #[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.assignment_strategy = crate::context::ContextAssignmentStrategy::Global; + app.insert_resource(ScriptContext::::new(ContextPolicy::shared())); // create a script - let content = "content".as_bytes().to_vec().into_boxed_slice(); - let command = CreateOrUpdateScript::::new("script".into(), content, None); + let script = add_script(&mut app, "content"); + let command = CreateOrUpdateScript::::new(script.clone()); - command.apply(app.world_mut()); + app.add_static_script(script.clone()); + Command::apply(command, app.world_mut()); // check script let loaded_script_expected_content = "content initialized pre-handling-initialized callback-ran-on_script_loaded"; assert_context_and_script( app.world(), - "script", + script.id(), loaded_script_expected_content, "Initial script creation failed", ); // update the script - let content = "new content".as_bytes().to_vec().into_boxed_slice(); - let command = CreateOrUpdateScript::::new("script".into(), content, None); + update_script(&mut app, script.id(), "new content"); + let command = CreateOrUpdateScript::::new(script.clone()); - command.apply(app.world_mut()); + Command::apply(command, app.world_mut()); // check script @@ -745,17 +801,18 @@ mod test { | new content initialized pre-handling-initialized callback-ran-on_script_loaded callback-ran-on_script_reloaded"); assert_context_and_script( app.world(), - "script", + script.id(), &second_loaded_script_expected_content, "Script update failed", ); // create second script - let content = "content2".as_bytes().to_vec().into_boxed_slice(); - let command = CreateOrUpdateScript::::new("script2".into(), content, None); + let script2 = add_script(&mut app, "content2"); + app.add_static_script(script2.clone()); + let command = CreateOrUpdateScript::::new(script2.clone()); - command.apply(app.world_mut()); + Command::apply(command, app.world_mut()); // check both scripts have the new context let third_loaded_script_expected_content = format!( @@ -764,46 +821,48 @@ mod test { ); assert_context_and_script( app.world(), - "script2", + script2.id(), &third_loaded_script_expected_content, "second script context was not created correctly", ); assert_context_and_script( app.world(), - "script", + script2.id(), + // "script", &third_loaded_script_expected_content, "First script context was not updated on second script insert", ); - let scripts = app.world().get_resource::>().unwrap(); - assert!(scripts.scripts.len() == 2); + let scripts = app.world().get_resource::().unwrap(); + assert_eq!(scripts.scripts.len(), 2); // delete first script - let command = DeleteScript::::new("script".into()); + let command = DeleteScript::::new(script); - command.apply(app.world_mut()); + Command::apply(command, app.world_mut()); + // Have to run the systems to evaluate the events. + app.update(); // check second script still has the context, and on unload was called assert_context_and_script( app.world(), - "script2", + script2.id(), &format!("{third_loaded_script_expected_content} callback-ran-on_script_unloaded"), "first script unload didn't have the desired effect", ); // delete second script - let command = DeleteScript::::new("script2".into()); + let command = DeleteScript::::new(script2); - command.apply(app.world_mut()); + Command::apply(command, app.world_mut()); // check that the scripts are gone, and so is the context - let scripts = app.world().get_resource::>().unwrap(); + let scripts = app.world().get_resource::().unwrap(); assert!(scripts.scripts.is_empty()); - let scripts = app.world().get_resource::>().unwrap(); - + let scripts = app.world().get_resource::().unwrap(); assert_eq!(scripts.scripts.len(), 0, "scripts weren't removed"); assert_response_events( app.world_mut(), @@ -816,18 +875,19 @@ mod test { fn test_static_scripts() { let mut app = setup_app(); + let script = add_script(&mut app, ""); let world = app.world_mut(); - let command = AddStaticScript::new("script"); - command.apply(world); + let command = AddStaticScript::new(script.clone()); + Command::apply(command, world); let static_scripts = world.get_resource::().unwrap(); - assert!(static_scripts.contains("script")); + assert!(static_scripts.contains(&script)); - let command = RemoveStaticScript::new("script".into()); - command.apply(world); + let command = RemoveStaticScript::new(script.clone()); + Command::apply(command, world); let static_scripts = world.get_resource::().unwrap(); - assert!(!static_scripts.contains("script")); + assert!(!static_scripts.contains(&script)); } } diff --git a/crates/bevy_mod_scripting_core/src/context.rs b/crates/bevy_mod_scripting_core/src/context.rs index dec993f7be..c57d2ade67 100644 --- a/crates/bevy_mod_scripting_core/src/context.rs +++ b/crates/bevy_mod_scripting_core/src/context.rs @@ -3,10 +3,10 @@ use crate::{ bindings::{ThreadWorldContainer, WorldContainer, WorldGuard}, error::{InteropError, ScriptError}, - script::ScriptId, + script::ContextKey, IntoScriptPluginParams, }; -use bevy::ecs::{entity::Entity, system::Resource}; +use bevy::ecs::system::Resource; /// A trait that all script contexts must implement. /// @@ -17,11 +17,11 @@ impl Context for T {} /// 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>; + fn(&ContextKey, &mut

::C) -> Result<(), ScriptError>; /// Initializer run every time before executing or loading/re-loading a script pub type ContextPreHandlingInitializer

= - fn(&str, Entity, &mut

::C) -> Result<(), ScriptError>; + fn(&ContextKey, &mut

::C) -> Result<(), ScriptError>; /// Settings concerning the creation and assignment of script contexts as well as their initialization. #[derive(Resource)] @@ -59,7 +59,7 @@ impl Clone for ContextLoadingSettings { } /// A strategy for loading contexts pub type ContextLoadFn

= fn( - script_id: &ScriptId, + context_key: &ContextKey, content: &[u8], context_initializers: &[ContextInitializer

], pre_handling_initializers: &[ContextPreHandlingInitializer

], @@ -68,7 +68,7 @@ pub type ContextLoadFn

= fn( /// A strategy for reloading contexts pub type ContextReloadFn

= fn( - script_id: &ScriptId, + context_key: &ContextKey, content: &[u8], previous_context: &mut

::C, context_initializers: &[ContextInitializer

], @@ -99,7 +99,7 @@ impl ContextBuilder

{ /// load a context pub fn load( loader: ContextLoadFn

, - script: &ScriptId, + context_key: &ContextKey, content: &[u8], context_initializers: &[ContextInitializer

], pre_handling_initializers: &[ContextPreHandlingInitializer

], @@ -109,7 +109,7 @@ impl ContextBuilder

{ WorldGuard::with_existing_static_guard(world.clone(), |world| { ThreadWorldContainer.set_world(world)?; (loader)( - script, + context_key, content, context_initializers, pre_handling_initializers, @@ -121,7 +121,7 @@ impl ContextBuilder

{ /// reload a context pub fn reload( reloader: ContextReloadFn

, - script: &ScriptId, + context_key: &ContextKey, content: &[u8], previous_context: &mut P::C, context_initializers: &[ContextInitializer

], @@ -132,7 +132,7 @@ impl ContextBuilder

{ WorldGuard::with_existing_static_guard(world, |world| { ThreadWorldContainer.set_world(world)?; (reloader)( - script, + context_key, content, previous_context, context_initializers, @@ -161,3 +161,10 @@ pub enum ContextAssignmentStrategy { /// Share contexts with all other scripts Global, } + +impl ContextAssignmentStrategy { + /// Returns true if there is one global context. + pub fn is_global(&self) -> bool { + matches!(self, ContextAssignmentStrategy::Global) + } +} diff --git a/crates/bevy_mod_scripting_core/src/error.rs b/crates/bevy_mod_scripting_core/src/error.rs index 8b2bd2a967..be12021992 100644 --- a/crates/bevy_mod_scripting_core/src/error.rs +++ b/crates/bevy_mod_scripting_core/src/error.rs @@ -8,9 +8,11 @@ use crate::{ script_value::ScriptValue, ReflectBaseType, ReflectReference, }, - script::ScriptId, + script::{ContextKey, DisplayProxy}, + ScriptAsset, }; use bevy::{ + asset::Handle, ecs::{ component::ComponentId, schedule::{ScheduleBuildError, ScheduleNotInitialized}, @@ -592,16 +594,16 @@ impl InteropError { } /// Thrown if a script could not be found when trying to call a synchronous callback or otherwise - pub fn missing_script(script_id: impl Into) -> Self { + pub fn missing_script(script_id: impl Into>) -> Self { Self(Arc::new(InteropErrorInner::MissingScript { script_id: script_id.into(), })) } /// Thrown if the required context for an operation is missing. - pub fn missing_context(script_id: impl Into) -> Self { + pub fn missing_context(context_key: impl Into) -> Self { Self(Arc::new(InteropErrorInner::MissingContext { - script_id: script_id.into(), + context_key: context_key.into(), })) } @@ -628,7 +630,7 @@ pub enum InteropErrorInner { /// Thrown if a script could not be found when trying to call a synchronous callback. MissingScript { /// The script id that was not found. - script_id: ScriptId, + script_id: Handle, }, /// Thrown if a base type is not registered with the reflection system UnregisteredBase { @@ -812,7 +814,7 @@ pub enum InteropErrorInner { /// Thrown if the required context for an operation is missing. MissingContext { /// The script that was attempting to access the context - script_id: ScriptId, + context_key: ContextKey, }, /// Thrown when a schedule is missing from the registry. MissingSchedule { @@ -1050,8 +1052,8 @@ impl PartialEq for InteropErrorInner { }, ) => a == c && b == d, ( - InteropErrorInner::MissingContext { script_id: b }, - InteropErrorInner::MissingContext { script_id: d }, + InteropErrorInner::MissingContext { context_key: b }, + InteropErrorInner::MissingContext { context_key: d }, ) => b == d, ( InteropErrorInner::MissingSchedule { schedule_name: a }, @@ -1254,8 +1256,8 @@ macro_rules! unregistered_component_or_resource_type { macro_rules! missing_script_for_callback { ($script_id:expr) => { format!( - "Could not find script with id: {}. Is the script loaded?", - $script_id + "Could not find script {}. Is the script loaded?", + $script_id.display() ) }; } @@ -1281,10 +1283,10 @@ macro_rules! argument_count_mismatch_msg { } macro_rules! missing_context_for_callback { - ($script_id:expr) => { + ($context_key:expr) => { format!( - "Missing context for script with id: {}. Was the script loaded?.", - $script_id + "Missing context for {}. Was the script loaded?.", + $context_key ) }; } @@ -1430,9 +1432,9 @@ impl DisplayWithWorld for InteropErrorInner { InteropErrorInner::MissingScript { script_id } => { missing_script_for_callback!(script_id) }, - InteropErrorInner::MissingContext { script_id } => { + InteropErrorInner::MissingContext { context_key } => { missing_context_for_callback!( - script_id + context_key ) }, InteropErrorInner::MissingSchedule { schedule_name } => { @@ -1576,9 +1578,9 @@ impl DisplayWithWorld for InteropErrorInner { InteropErrorInner::MissingScript { script_id } => { missing_script_for_callback!(script_id) }, - InteropErrorInner::MissingContext { script_id } => { + InteropErrorInner::MissingContext { context_key } => { missing_context_for_callback!( - script_id + context_key ) }, InteropErrorInner::MissingSchedule { schedule_name } => { diff --git a/crates/bevy_mod_scripting_core/src/event.rs b/crates/bevy_mod_scripting_core/src/event.rs index b280ccb503..7476065706 100644 --- a/crates/bevy_mod_scripting_core/src/event.rs +++ b/crates/bevy_mod_scripting_core/src/event.rs @@ -1,8 +1,78 @@ //! Event handlers and event types for scripting. -use crate::{bindings::script_value::ScriptValue, error::ScriptError, script::ScriptId}; +use crate::{ + bindings::script_value::ScriptValue, + error::ScriptError, + script::{ContextKey, Domain, ScriptId}, +}; use bevy::{ecs::entity::Entity, prelude::Event, reflect::Reflect}; +/// A script event +#[derive(Event, Debug, Clone)] +pub enum ScriptEvent { + /// A script asset was added. + Added { + /// The script + script: ScriptId, + }, + /// A script asset was removed. + Removed { + /// The script + script: ScriptId, + }, + /// A script asset was modified. + Modified { + /// The script + script: ScriptId, + }, + /// A script component was attached to an entity. + Attached { + /// The entity + entity: Entity, + }, + /// A script was added to [StaticScripts]. + StaticAttached { + /// The script + script: ScriptId, + }, + // We could deliver this through `RemovedComponents` but + // it won't have any further information since the component is gone. + /// An entity with a [ScriptComponent] was removed. + Detached { + /// The entity + entity: Entity, + }, + // These were some other events I was considering. I thought Unloaded might + // be interesting, but if I implemented it the way things work currently it + // could only be a notification. The user wouldn't be able to do anything + // between an Unloaded and Loaded event that could affect the Unloaded + // value. Maybe that's fine. I'm leaving it here purely to communicate the + // idea. It can be removed. + + // /// A script was loaded/evaluated. + // Loaded { + // /// The script + // script: ScriptId, + // /// The entity + // entity: Option, + // /// The domain + // domain: Option, + // }, + // /// A script was unloaded, perhaps producing a value. + // Unloaded { + // /// The context key + // context_key: ContextKey, + // // /// The script + // // script: ScriptId, + // // /// The entity + // // entity: Option, + // // /// The domain + // // domain: Option, + // /// The unloaded value + // value: Option + // }, +} + /// An error coming from a script #[derive(Debug, Event)] pub struct ScriptErrorEvent { @@ -128,6 +198,8 @@ pub enum Recipients { Script(ScriptId), /// The event is to be handled by all the scripts on the specified entity Entity(Entity), + /// The event is to be handled by a specific domain + Domain(Domain), /// The event is to be handled by all the scripts of one language Language(crate::asset::Language), } @@ -175,35 +247,36 @@ impl ScriptCallbackEvent { } } -/// Event published when a script completes a callback, and a response is requested +/// Event published when a script completes a callback and a response is requested. #[derive(Clone, Event, Debug)] #[non_exhaustive] pub struct ScriptCallbackResponseEvent { - /// the entity that the script was invoked on, - pub entity: Entity, /// the label of the callback pub label: CallbackLabel, - /// the script that replied - pub script: ScriptId, + /// the key to the context that replied + pub context_key: ContextKey, /// the response received pub response: Result, } impl ScriptCallbackResponseEvent { - /// Creates a new callback response event with the given label, script and response + /// Creates a new callback response event with the given label, script, and response. pub fn new>( - entity: Entity, label: L, - script: ScriptId, + context_key: impl Into, response: Result, ) -> Self { Self { - entity, label: label.into(), - script, + context_key: context_key.into(), response, } } + + /// Return the source entity for the callback if there was any. + pub fn source_entity(&self) -> Option { + self.context_key.entity + } } static FORBIDDEN_KEYWORDS: [&str; 82] = [ diff --git a/crates/bevy_mod_scripting_core/src/extractors.rs b/crates/bevy_mod_scripting_core/src/extractors.rs index d105cca7b5..d00efaebb4 100644 --- a/crates/bevy_mod_scripting_core/src/extractors.rs +++ b/crates/bevy_mod_scripting_core/src/extractors.rs @@ -2,16 +2,22 @@ //! //! These are designed to be used to pipe inputs into other systems which require them, while handling any configuration erorrs nicely. #![allow(deprecated)] -use std::ops::{Deref, DerefMut}; - -use bevy::ecs::{ - component::ComponentId, - entity::Entity, - event::{Event, EventCursor, EventIterator, Events}, - query::{Access, AccessConflicts}, - storage::SparseSetIndex, - system::{Local, Resource, SystemParam, SystemState}, - world::World, +use parking_lot::Mutex; +use std::{ + ops::{Deref, DerefMut}, + sync::Arc, +}; + +use bevy::{ + asset::Assets, + ecs::{ + component::ComponentId, + event::{Event, EventCursor, EventIterator, Events}, + query::{Access, AccessConflicts}, + storage::SparseSetIndex, + system::{Local, Resource, SystemParam, SystemState}, + world::World, + }, }; use fixedbitset::FixedBitSet; @@ -25,8 +31,8 @@ use crate::{ event::{CallbackLabel, IntoCallbackLabel}, handler::CallbackSettings, runtime::RuntimeContainer, - script::{ScriptId, Scripts, StaticScripts}, - IntoScriptPluginParams, + script::{ContextKey, ScriptContext, StaticScripts}, + IntoScriptPluginParams, ScriptAsset, }; /// Executes `system_state.get_mut` followed by `system_state.apply` after running the given closure, makes sure state is correctly handled in the context of an exclusive system. @@ -47,16 +53,24 @@ pub fn with_handler_system_state< o } -/// Semantics of [`bevy::ecs::change_detection::Res`] but doesn't claim read or write on the world by removing the resource from it ahead of time. +/// Semantics of [`bevy::ecs::change_detection::Res`] but doesn't claim read or +/// write on the world by removing the resource from it ahead of time. /// /// Similar to using [`World::resource_scope`]. /// -/// This is useful for interacting with scripts, since [`WithWorldGuard`] will ensure scripts cannot gain exclusive access to the world if *any* reads or writes -/// are claimed on the world. Removing the resource from the world lets you access it in the context of running scripts without blocking exclusive world access. +/// This is useful for interacting with scripts, since [`WithWorldGuard`] will +/// ensure scripts cannot gain exclusive access to the world if *any* reads or +/// writes are claimed on the world. Removing the resource from the world lets +/// you access it in the context of running scripts without blocking exclusive +/// world access. /// /// # Safety -/// - Because the resource is removed during the `get_param` call, if there is a conflicting resource access, this will be unsafe -/// - You must ensure you're only using this in combination with system parameters which will not read or write to this resource in `get_param` +/// +/// - Because the resource is removed during the `get_param` call, if there is a +/// conflicting resource access, this will be unsafe +/// +/// - You must ensure you're only using this in combination with system +/// parameters which will not read or write to this resource in `get_param` pub(crate) struct ResScope<'state, T: Resource + Default>(pub &'state mut T); impl Deref for ResScope<'_, T> { @@ -140,15 +154,17 @@ pub struct HandlerContext<'s, P: IntoScriptPluginParams> { pub(crate) callback_settings: ResScope<'s, CallbackSettings

>, /// Settings for loading contexts pub(crate) context_loading_settings: ResScope<'s, ContextLoadingSettings

>, - /// Scripts - pub(crate) scripts: ResScope<'s, Scripts

>, /// The runtime container pub(crate) runtime_container: ResScope<'s, RuntimeContainer

>, /// List of static scripts pub(crate) static_scripts: ResScope<'s, StaticScripts>, + /// Script context + pub(crate) script_context: ResScope<'s, ScriptContext

>, + /// Scripts + pub(crate) scripts: ResScope<'s, Assets>, } -impl HandlerContext<'_, P> { +impl<'s, P: IntoScriptPluginParams> HandlerContext<'s, P> { /// Splits the handler context into its individual components. /// /// Useful if you are needing multiple resources from the handler context. @@ -158,14 +174,12 @@ impl HandlerContext<'_, P> { ) -> ( &mut CallbackSettings

, &mut ContextLoadingSettings

, - &mut Scripts

, &mut RuntimeContainer

, &mut StaticScripts, ) { ( &mut self.callback_settings, &mut self.context_loading_settings, - &mut self.scripts, &mut self.runtime_container, &mut self.static_scripts, ) @@ -181,11 +195,6 @@ impl HandlerContext<'_, P> { &mut self.context_loading_settings } - /// Get the scripts - pub fn scripts(&mut self) -> &mut Scripts

{ - &mut self.scripts - } - /// Get the runtime container pub fn runtime_container(&mut self) -> &mut RuntimeContainer

{ &mut self.runtime_container @@ -196,24 +205,29 @@ impl HandlerContext<'_, P> { &mut self.static_scripts } - /// checks if the script is loaded such that it can be executed. - pub fn is_script_fully_loaded(&self, script_id: ScriptId) -> bool { - self.scripts.scripts.contains_key(&script_id) + /// Get the static scripts + pub fn script_context(&mut self) -> &mut ScriptContext

{ + &mut self.script_context } + // /// checks if the script is loaded such that it can be executed. + // pub fn is_script_fully_loaded(&self, script_id: ScriptId) -> bool { + // todo!() + // // matches!(self.asset_server.load_state(script_id), LoadState::Loaded) + // } + /// Equivalent to [`Self::call`] but with a dynamically passed in label pub fn call_dynamic_label( &self, label: &CallbackLabel, - script_id: &ScriptId, - entity: Entity, + context_key: &ContextKey, + context: Option<&Arc>>, payload: Vec, guard: WorldGuard<'_>, ) -> Result { // find script - let script = match self.scripts.scripts.get(script_id) { - Some(script) => script, - None => return Err(InteropError::missing_script(script_id.clone()).into()), + let Some(context) = context.or_else(|| self.script_context.get(context_key)) else { + return Err(InteropError::missing_context(context_key.clone()).into()); }; // call the script @@ -223,13 +237,12 @@ impl HandlerContext<'_, P> { .context_pre_handling_initializers; let runtime = &self.runtime_container.runtime; - let mut context = script.context.lock(); + let mut context = context.lock(); CallbackSettings::

::call( handler, payload, - entity, - script_id, + context_key, label, &mut context, pre_handling_initializers, @@ -244,12 +257,11 @@ impl HandlerContext<'_, P> { /// Run [`Self::is_script_fully_loaded`] before calling the script to ensure the script and context were loaded ahead of time. pub fn call( &self, - script_id: &ScriptId, - entity: Entity, + context_key: &ContextKey, payload: Vec, guard: WorldGuard<'_>, ) -> Result { - self.call_dynamic_label(&C::into_callback_label(), script_id, entity, payload, guard) + self.call_dynamic_label(&C::into_callback_label(), context_key, None, payload, guard) } } diff --git a/crates/bevy_mod_scripting_core/src/handler.rs b/crates/bevy_mod_scripting_core/src/handler.rs index 322ea6adfb..ddc3e8a50a 100644 --- a/crates/bevy_mod_scripting_core/src/handler.rs +++ b/crates/bevy_mod_scripting_core/src/handler.rs @@ -7,12 +7,12 @@ use crate::{ context::ContextPreHandlingInitializer, error::{InteropErrorInner, ScriptError}, event::{ - CallbackLabel, IntoCallbackLabel, ScriptCallbackEvent, ScriptCallbackResponseEvent, - ScriptErrorEvent, + CallbackLabel, IntoCallbackLabel, Recipients, ScriptCallbackEvent, + ScriptCallbackResponseEvent, ScriptErrorEvent, }, extractors::{HandlerContext, WithWorldGuard}, - script::{ScriptComponent, ScriptId}, - IntoScriptPluginParams, + script::{ContextKey, DisplayProxy, ScriptComponent, ScriptDomain}, + IntoScriptPluginParams, Language, }; use bevy::{ ecs::{ @@ -23,13 +23,13 @@ use bevy::{ }, log::trace_once, prelude::{Events, Ref}, + utils::HashSet, }; /// A function that handles a callback event pub type HandlerFn

= fn( args: Vec, - entity: Entity, - script_id: &ScriptId, + context_key: &ContextKey, callback: &CallbackLabel, context: &mut

::C, pre_handling_initializers: &[ContextPreHandlingInitializer

], @@ -46,7 +46,7 @@ pub struct CallbackSettings { impl Default for CallbackSettings

{ fn default() -> Self { Self { - callback_handler: |_, _, _, _, _, _, _| Ok(ScriptValue::Unit), + callback_handler: |_, _, _, _, _, _| Ok(ScriptValue::Unit), } } } @@ -70,8 +70,7 @@ impl CallbackSettings

{ pub fn call( handler: HandlerFn

, args: Vec, - entity: Entity, - script_id: &ScriptId, + context_key: &ContextKey, callback: &CallbackLabel, script_ctxt: &mut P::C, pre_handling_initializers: &[ContextPreHandlingInitializer

], @@ -82,8 +81,7 @@ impl CallbackSettings

{ ThreadWorldContainer.set_world(world)?; (handler)( args, - entity, - script_id, + context_key, callback, script_ctxt, pre_handling_initializers, @@ -93,18 +91,6 @@ impl CallbackSettings

{ } } -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. /// /// If any of the resources required for the handler are missing, the system will log this issue and do nothing. @@ -129,7 +115,14 @@ pub fn event_handler( #[allow(deprecated)] pub(crate) type EventHandlerSystemState<'w, 's, P> = SystemState<( - Local<'s, QueryState<(Entity, Ref<'w, ScriptComponent>)>>, + Local< + 's, + QueryState<( + Entity, + Ref<'w, ScriptComponent>, + Option>, + )>, + >, crate::extractors::EventReaderScope<'s, ScriptCallbackEvent>, WithWorldGuard<'w, 's, HandlerContext<'s, P>>, )>; @@ -138,67 +131,184 @@ pub(crate) type EventHandlerSystemState<'w, 's, P> = SystemState<( #[allow(deprecated)] pub(crate) fn event_handler_inner( callback_label: CallbackLabel, - mut entity_query_state: Local)>>, + mut entity_query_state: Local< + QueryState<(Entity, Ref, Option>)>, + >, mut script_events: crate::extractors::EventReaderScope, mut handler_ctxt: WithWorldGuard>, ) { - let (guard, handler_ctxt) = handler_ctxt.get_mut(); - + let events = script_events.read(); + if events.len() == 0 { + return; + } let mut errors = Vec::default(); - let events = script_events.read().cloned().collect::>(); - - // query entities + chain static scripts - let entity_and_static_scripts = guard.with_global_access(|world| { - entity_query_state - .iter(world) - .map(|(e, s)| (e, s.0.clone())) - .chain( - handler_ctxt - .static_scripts - .scripts - .iter() - .map(|s| (Entity::from_raw(0), vec![s.clone()])), - ) - .collect::>() - }); - - let entity_and_static_scripts = match entity_and_static_scripts { - Ok(entity_and_static_scripts) => entity_and_static_scripts, - Err(e) => { - bevy::log::error!( - "{}: Failed to query entities with scripts: {}", - P::LANGUAGE, - e.display_with_world(guard.clone()) - ); - return; - } - }; - - 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 { - crate::event::Recipients::Script(target_script_id) - if target_script_id != script_id => + let (guard, handler_ctxt) = handler_ctxt.get_mut(); + for event in events.filter(|e| e.label == callback_label) { + match &event.recipients { + Recipients::Script(target_script_id) => { + match guard.with_global_access(|world| { + let mut keys = vec![]; + for (id, script_component, script_domain_maybe) in + entity_query_state.iter(world) { - continue - } - crate::event::Recipients::Entity(target_entity) if target_entity != entity => { - continue + if let Some(handle) = script_component + .0 + .iter() + .find(|handle| handle.id() == *target_script_id) + { + keys.push(ContextKey { + entity: Some(id), + script: Some(handle.clone()), + domain: script_domain_maybe.map(|x| x.0), + }); + } } - crate::event::Recipients::Language(target_language) - if *target_language != P::LANGUAGE => + if let Some(handle) = handler_ctxt + .static_scripts + .scripts + .iter() + .find(|handle| handle.id() == *target_script_id) { - continue + keys.push(ContextKey { + entity: None, + script: Some(handle.clone()), + domain: None, + }); + } + keys + }) { + Ok(keys) => { + // Keep track of the contexts that have been called. Don't duplicate the + // calls on account of multiple matches. + let mut called_contexts: HashSet = HashSet::new(); + for context_key in keys { + if let Some(hash) = handler_ctxt.script_context.hash(&context_key) { + if called_contexts.insert(hash) { + let call_result = handler_ctxt.call_dynamic_label( + &callback_label, + &context_key, + None, + event.args.clone(), + guard.clone(), + ); + + if event.trigger_response { + send_callback_response( + guard.clone(), + ScriptCallbackResponseEvent::new( + callback_label.clone(), + *target_script_id, + call_result.clone(), + ), + ); + } + collect_errors( + call_result, + context_key.entity, + P::LANGUAGE, + &mut errors, + ); + } + } + } + } + Err(e) => { + bevy::log::error_once!( + "{}: Failed to query for script {}: {}", + P::LANGUAGE, + target_script_id, + e.display_with_world(guard.clone()) + ); + } + } + continue; + } + Recipients::Entity(target_entity) => { + match guard.with_global_access(|world| { + match entity_query_state.get(world, *target_entity) { + Ok((_, script_component, script_domain_maybe)) => { + let domain = script_domain_maybe.map(|x| x.0); + (script_component.0.clone(), domain) + } + Err(e) => { + bevy::log::error_once!( + "{}: Failed to get entity {} with scripts: {}", + P::LANGUAGE, + target_entity, + e, + ); + (vec![], None) + } + } + }) { + Ok((handles, domain)) => { + // Keep track of the contexts that have been called. Don't duplicate the + // calls on account of multiple matches. + // + // If I have five scripts all in one shared context, and I fire a call to + // `Recipients::All`, then I want that call to go to the shared context + // once. + // + // If I have four scripts in three different contexts, and I fire a call to + // `Recipients::All`, then I want that call to be evaluated three times, + // once in each context. + let mut called_contexts: HashSet = HashSet::new(); + for script_handle in handles { + let script_id = script_handle.id(); + let context_key = ContextKey { + entity: Some(*target_entity), + script: Some(script_handle), + domain, + }; + if let Some(hash) = handler_ctxt.script_context.hash(&context_key) { + if called_contexts.insert(hash) { + let call_result = handler_ctxt.call_dynamic_label( + &callback_label, + &context_key, + None, + event.args.clone(), + guard.clone(), + ); + + if event.trigger_response { + send_callback_response( + guard.clone(), + ScriptCallbackResponseEvent::new( + callback_label.clone(), + script_id, + call_result.clone(), + ), + ); + } + collect_errors( + call_result, + Some(*target_entity), + P::LANGUAGE, + &mut errors, + ); + } + } + } + } + Err(e) => { + bevy::log::error_once!( + "{}: Failed to get entity {} with scripts: {}", + P::LANGUAGE, + target_entity, + e, + ); } - _ => {} } + continue; + } + Recipients::Domain(target_domain) => { + let context_key = ContextKey::from(*target_domain); let call_result = handler_ctxt.call_dynamic_label( &callback_label, - script_id, - *entity, + &context_key, + None, event.args.clone(), guard.clone(), ); @@ -207,47 +317,99 @@ pub(crate) fn event_handler_inner( send_callback_response( guard.clone(), ScriptCallbackResponseEvent::new( - *entity, callback_label.clone(), - script_id.clone(), + context_key, call_result.clone(), ), ); } + collect_errors(call_result, None, P::LANGUAGE, &mut errors); + } + Recipients::Language(target_language) if *target_language != P::LANGUAGE => { + continue; + } + Recipients::Language(_) | Recipients::All => { + // All and language are effectively the same modulo the other + // languages, which are handled by the other P handlers. + for (key, context) in handler_ctxt.script_context.iter() { + let call_result = handler_ctxt.call_dynamic_label( + &callback_label, + key, + Some(context), + event.args.clone(), + guard.clone(), + ); - match call_result { - Ok(_) => {} - Err(e) => { - match e.downcast_interop_inner() { - Some(InteropErrorInner::MissingScript { script_id }) => { - trace_once!( - "{}: Script `{}` on entity `{:?}` is either still loading, doesn't exist, or is for another language, ignoring until the corresponding script is loaded.", - P::LANGUAGE, - script_id, entity - ); - continue; - } - Some(InteropErrorInner::MissingContext { .. }) => { - // 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 e = e - .with_script(script_id.clone()) - .with_context(format!("Event handling for: Language: {}", P::LANGUAGE)); - push_err_and_continue!(errors, Err(e)); + if event.trigger_response { + send_callback_response( + guard.clone(), + ScriptCallbackResponseEvent::new( + callback_label.clone(), + key.clone(), + call_result.clone(), + ), + ); } - }; + // The tricky thing here is a context _could_ be associated + // with an entity, but we don't quite have means to express + // that relationship yet. Maybe + // `ScriptContextProvider.iter()` ought to provide that + // association using a struct like this: + // + // ContextKeys { + // script: Option>, + // entity: Option, + // domain: Option, + // } + collect_errors(call_result, None, P::LANGUAGE, &mut errors); + } + continue; } } } - handle_script_errors(guard, errors.into_iter()); } +fn collect_errors( + call_result: Result, + entity: Option, + language: Language, + errors: &mut Vec, +) { + match call_result { + Ok(_) => {} + Err(e) => { + match e.downcast_interop_inner() { + Some(InteropErrorInner::MissingScript { script_id }) => { + trace_once!( + "{}: Script `{}` on entity `{:?}` is either still loading, doesn't exist, or is for another language; ignoring until the corresponding script is loaded.", + language, + script_id.display(), entity + ); + return; + } + Some(InteropErrorInner::MissingContext { .. }) => { + // 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. + return; + } + _ => {} + } + // let e = { + // // if let Some(path) = + // // script_id.path().map(|path| e.with_script(path)).unwrap_or_else(|| e)// { + // // e.with_script(path) + // // } else { + // // e + // // } + // }; + // push_err_and_continue!(errors, Err(e)); + errors.push(e.with_context(format!("Event handling for language {language}"))); + } + } +} + /// Sends a callback response event to the world pub fn send_callback_response(world: WorldGuard, response: ScriptCallbackResponseEvent) { let err = world.with_resource_mut(|mut events: Mut>| { @@ -285,24 +447,26 @@ pub fn handle_script_errors + Clone>(world: Worl #[cfg(test)] #[allow(clippy::todo)] mod test { - use std::{borrow::Cow, collections::HashMap, sync::Arc}; + // use std::{borrow::Cow, collections::HashMap, sync::Arc}; use bevy::{ app::{App, Update}, - asset::AssetPlugin, + asset::{AssetApp, AssetId, AssetPlugin, Assets, Handle}, diagnostic::DiagnosticsPlugin, ecs::world::FromWorld, }; - use parking_lot::Mutex; use test_utils::make_test_plugin; use crate::{ + asset::ScriptAsset, bindings::script_value::ScriptValue, context::{ContextBuilder, ContextLoadingSettings}, - event::{CallbackLabel, IntoCallbackLabel, ScriptCallbackEvent, ScriptErrorEvent}, + event::{ + CallbackLabel, IntoCallbackLabel, ScriptCallbackEvent, ScriptErrorEvent, ScriptEvent, + }, runtime::RuntimeContainer, - script::{Script, ScriptComponent, ScriptId, Scripts, StaticScripts}, - BMSScriptingInfrastructurePlugin, + script::{ScriptComponent, StaticScripts}, + BMSScriptingInfrastructurePlugin, ManageStaticScripts, ScriptContext, }; use super::*; @@ -332,36 +496,40 @@ mod test { ); for (a, b) in responses.iter().zip(expected.iter()) { assert_eq!(a.label, b.label); - assert_eq!(a.script, b.script); + assert_eq!(a.context_key, b.context_key); assert_eq!(a.response, b.response); } } - fn setup_app( - runtime: TestRuntime, - scripts: HashMap>, - ) -> App { + fn setup_app(runtime: TestRuntime) -> App { let mut app = App::new(); - + // app.add_plugins(bevy::log::LogPlugin::default()); + app.add_plugins(bevy::asset::AssetPlugin::default()); + app.init_asset::(); app.add_event::(); app.add_event::(); app.add_event::(); + app.add_event::(); app.insert_resource::>(CallbackSettings { - callback_handler: |args, entity, script, _, ctxt, _, runtime| { + callback_handler: |args, context_key, _, ctxt, _, runtime| { ctxt.invocations.extend(args); let mut runtime = runtime.invocations.lock(); - runtime.push((entity, script.clone())); + runtime.push(( + context_key.entity, + context_key.script.as_ref().map(|x| x.id()), + )); Ok(ScriptValue::Unit) }, }); + app.add_plugins(crate::configure_asset_systems); + app.add_plugins(crate::configure_asset_systems_for_plugin::); app.add_systems(Update, event_handler::); - app.insert_resource::>(Scripts { scripts }); app.insert_resource(RuntimeContainer:: { runtime }); app.init_resource::(); app.insert_resource(ContextLoadingSettings:: { loader: ContextBuilder { - load: |_, _, _, _, _| todo!(), - reload: |_, _, _, _, _, _| todo!(), + load: |_, _, _, _, _| Ok(TestContext::default()), + reload: |_, _, _, _, _, _| Ok(()), }, assignment_strategy: Default::default(), context_initializers: vec![], @@ -372,29 +540,30 @@ mod test { app } + fn add_script(app: &mut App, content: impl Into) -> Handle { + app.world_mut() + .resource_mut::>() + .add(ScriptAsset::from(content.into())) + } + #[test] fn test_handler_emits_response_events() { - let test_script_id = Cow::Borrowed("test_script"); - let test_script = Script { - id: test_script_id.clone(), - asset: None, - context: Arc::new(Mutex::new(TestContext::default())), - }; - let scripts = HashMap::from_iter(vec![(test_script_id.clone(), test_script.clone())]); let runtime = TestRuntime { invocations: vec![].into(), }; - let mut app = setup_app::(runtime, scripts); - let entity = app + let mut app = setup_app::(runtime); + let test_script = add_script(&mut app, ""); + let test_entity_id = app .world_mut() - .spawn(ScriptComponent(vec![test_script_id.clone()])) + .spawn(ScriptComponent(vec![test_script.clone()])) .id(); + app.update(); app.world_mut().send_event( ScriptCallbackEvent::new( OnTestCallback::into_callback_label(), vec![ScriptValue::String("test_args".into())], - crate::event::Recipients::All, + Recipients::All, ) .with_response(), ); @@ -403,9 +572,8 @@ mod test { assert_response_events( app.world_mut(), vec![ScriptCallbackResponseEvent::new( - entity, OnTestCallback::into_callback_label(), - test_script_id.clone(), + ContextKey::from(test_script.id()).or(test_entity_id.into()), Ok(ScriptValue::Unit), )] .into_iter(), @@ -413,21 +581,94 @@ mod test { } #[test] - fn test_handler_called_with_right_args() { - let test_script_id = Cow::Borrowed("test_script"); - let test_script = Script { - id: test_script_id.clone(), - asset: None, - context: Arc::new(Mutex::new(TestContext::default())), + fn test_handler_emits_response_events_for_static_script() { + let runtime = TestRuntime { + invocations: vec![].into(), }; - let scripts = HashMap::from_iter(vec![(test_script_id.clone(), test_script.clone())]); + let mut app = setup_app::(runtime); + let test_script = add_script(&mut app, ""); + app.add_static_script(test_script.clone()); + app.update(); + + app.world_mut().send_event( + ScriptCallbackEvent::new( + OnTestCallback::into_callback_label(), + vec![ScriptValue::String("test_args".into())], + Recipients::All, + ) + .with_response(), + ); + app.update(); + + assert_response_events( + app.world_mut(), + vec![ScriptCallbackResponseEvent::new( + OnTestCallback::into_callback_label(), + test_script.id(), + Ok(ScriptValue::Unit), + )] + .into_iter(), + ); + } + + #[test] + fn test_handler_init() { + let runtime = TestRuntime { + invocations: vec![].into(), + }; + let mut app = setup_app::(runtime); + let test_script = add_script(&mut app, ""); + // app.add_static_script(test_script.clone()); + let test_entity_id = app + .world_mut() + .spawn(ScriptComponent(vec![test_script.clone()])) + .id(); + + app.update(); + { + let script_context = app + .world() + .get_resource::>() + .unwrap(); + assert_eq!(script_context.iter().count(), 1); + // assert_eq!(Some(test_entity_id), key.entity); + // // assert_eq!(Some(test_script), key.script_id); + // assert_eq!(1, script_context.iter_box().count()); + let key = ContextKey::from(test_entity_id).or(test_script.id().into()); + let context_arc = script_context.get(&key).cloned().expect("script context"); + + let test_context = context_arc.lock(); + + let test_runtime = app + .world() + .get_resource::>() + .unwrap(); + + assert_eq!(test_context.invocations, vec![]); + + let runtime_invocations = test_runtime.runtime.invocations.lock(); + assert_eq!( + runtime_invocations + .iter() + .map(|(e, s)| (*e, *s)) + .collect::>(), + vec![(Some(test_entity_id), Some(test_script.id()))] + ); + } + assert_response_events(app.world_mut(), vec![].into_iter()); + } + + #[test] + fn test_handler_called_with_right_args() { let runtime = TestRuntime { invocations: vec![].into(), }; - let mut app = setup_app::(runtime, scripts); + let mut app = setup_app::(runtime); + let test_script = add_script(&mut app, ""); + // app.add_static_script(test_script.clone()); let test_entity_id = app .world_mut() - .spawn(ScriptComponent(vec![test_script_id.clone()])) + .spawn(ScriptComponent(vec![test_script.clone()])) .id(); app.world_mut().send_event(ScriptCallbackEvent::new_for_all( @@ -436,15 +677,17 @@ mod test { )); app.update(); { - let test_script = app + let script_context = app .world() - .get_resource::>() - .unwrap() - .scripts - .get(&test_script_id) + .get_resource::>() .unwrap(); + let key = script_context.iter().next().map(|x| x.0).unwrap(); + assert_eq!(Some(test_entity_id), key.entity); + // assert_eq!(Some(test_script), key.script_id); + assert_eq!(1, script_context.iter().count()); + let context_arc = script_context.get(key).cloned().expect("script context"); - let test_context = test_script.context.lock(); + let test_context = context_arc.lock(); let test_runtime = app .world() @@ -460,9 +703,14 @@ mod test { assert_eq!( runtime_invocations .iter() - .map(|(e, s)| (*e, s.clone())) + .map(|(e, s)| (*e, *s)) .collect::>(), - vec![(test_entity_id, test_script_id.clone())] + vec![ + // Once for evaluating the script. + (Some(test_entity_id), Some(test_script.id())), + // Once for the callback. + (Some(test_entity_id), Some(test_script.id())) + ] ); } assert_response_events(app.world_mut(), vec![].into_iter()); @@ -470,55 +718,53 @@ mod test { #[test] fn test_handler_called_on_right_recipients() { - let test_script_id = Cow::Borrowed("test_script"); - let test_script = Script { - id: test_script_id.clone(), - asset: None, - context: Arc::new(Mutex::new(TestContext::default())), - }; - let scripts = HashMap::from_iter(vec![ - (test_script_id.clone(), test_script.clone()), - ( - "wrong".into(), - Script { - id: "wrong".into(), - asset: None, - context: Arc::new(Mutex::new(TestContext::default())), - }, - ), - ]); - let runtime = TestRuntime { invocations: vec![].into(), }; - let mut app = setup_app::(runtime, scripts); + let mut app = setup_app::(runtime); + // app.insert_resource(ScriptContext::::per_entity_and_scriptid()); + let test_script = add_script(&mut app, ""); + let test_script2_id = add_script(&mut app, "wrong"); + // app.add_static_script(test_script.clone()); let test_entity_id = app .world_mut() - .spawn(ScriptComponent(vec![test_script_id.clone()])) + .spawn(ScriptComponent(vec![test_script.clone()])) .id(); + let test_entity2_id = app + .world_mut() + .spawn(ScriptComponent(vec![test_script2_id.clone()])) + .id(); + app.update(); + 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()), + Recipients::Script(test_script.id()), )); 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), + Recipients::Entity(test_entity_id), )); app.update(); { - let test_scripts = app.world().get_resource::>().unwrap(); let test_runtime = app .world() .get_resource::>() .unwrap(); let test_runtime = test_runtime.runtime.invocations.lock(); - let script_after = test_scripts.scripts.get(&test_script_id).unwrap(); - let context_after = script_after.context.lock(); + + let script_context = app + .world() + .get_resource::>() + .unwrap(); + + let key = ContextKey::from(test_entity_id).or(test_script.id().into()); + let context_arc = script_context.get(&key).cloned().expect("script context"); + let context_after = context_arc.lock(); assert_eq!( context_after.invocations, vec![ @@ -530,11 +776,17 @@ mod test { assert_eq!( test_runtime .iter() - .map(|(e, s)| (*e, s.clone())) + .map(|(e, s)| (*e, *s)) .collect::>(), vec![ - (test_entity_id, test_script_id.clone()), - (test_entity_id, test_script_id.clone()) + // Load 1 + (Some(test_entity_id), Some(test_script.id())), + // Load 2 + (Some(test_entity2_id), Some(test_script2_id.id())), + // Call 1, + (Some(test_entity_id), Some(test_script.id())), + // Call 2, + (Some(test_entity_id), Some(test_script.id())), ] ); } @@ -543,52 +795,42 @@ mod test { #[test] fn test_handler_called_for_static_scripts() { - let test_script_id = Cow::Borrowed("test_script"); - - let scripts = HashMap::from_iter(vec![( - test_script_id.clone(), - Script { - id: test_script_id.clone(), - asset: None, - context: Arc::new(Mutex::new(TestContext::default())), - }, - )]); let runtime = TestRuntime { invocations: vec![].into(), }; - let mut app = setup_app::(runtime, scripts); - app.world_mut().insert_resource(StaticScripts { - scripts: vec![test_script_id.clone()].into_iter().collect(), - }); + let mut app = setup_app::(runtime); + let test_script = add_script(&mut app, ""); + app.add_static_script(test_script.clone()); + app.update(); app.world_mut().send_event(ScriptCallbackEvent::new( OnTestCallback::into_callback_label(), vec![ScriptValue::String("test_args_script".into())], - crate::event::Recipients::All, + Recipients::All, )); app.world_mut().send_event(ScriptCallbackEvent::new( OnTestCallback::into_callback_label(), - vec![ScriptValue::String("test_script_id".into())], - crate::event::Recipients::Script(test_script_id.clone()), + vec![ScriptValue::String("test_script".into())], + Recipients::Script(test_script.id()), )); app.update(); { - let test_scripts = app.world().get_resource::>().unwrap(); - let test_context = test_scripts - .scripts - .get(&test_script_id) - .unwrap() - .context - .lock(); + let script_context = app + .world() + .get_resource::>() + .unwrap(); + let key = ContextKey::from(test_script); + let context_arc = script_context.get(&key).cloned().expect("script context"); + let test_context = context_arc.lock(); assert_eq!( test_context.invocations, vec![ ScriptValue::String("test_args_script".into()), - ScriptValue::String("test_script_id".into()) + ScriptValue::String("test_script".into()) ] ); } @@ -622,4 +864,13 @@ mod test { .get_resource::>() .is_some()); } + + #[test] + fn default_script_asset() { + let default_handle: Handle = Handle::default(); + let handle: Handle = Handle::Weak(AssetId::Uuid { + uuid: uuid::uuid!("97128bb1-2588-480b-bdc6-87b4adbec477"), + }); + assert_eq!(default_handle.id(), handle.id()); + } } diff --git a/crates/bevy_mod_scripting_core/src/lib.rs b/crates/bevy_mod_scripting_core/src/lib.rs index 15faf31ff4..f0599fdeac 100644 --- a/crates/bevy_mod_scripting_core/src/lib.rs +++ b/crates/bevy_mod_scripting_core/src/lib.rs @@ -5,9 +5,9 @@ use crate::event::ScriptErrorEvent; use asset::{ configure_asset_systems, configure_asset_systems_for_plugin, Language, ScriptAsset, - ScriptAssetLoader, ScriptAssetSettings, + ScriptAssetLoader, }; -use bevy::prelude::*; +use bevy::{prelude::*, utils::HashMap}; use bindings::{ function::script_function::AppScriptFunctionRegistry, garbage_collector, schedule::AppScheduleRegistry, script_value::ScriptValue, AppReflectAllocator, @@ -19,10 +19,10 @@ use context::{ ContextPreHandlingInitializer, }; use error::ScriptError; -use event::{ScriptCallbackEvent, ScriptCallbackResponseEvent}; +use event::{ScriptCallbackEvent, ScriptCallbackResponseEvent, ScriptEvent}; use handler::{CallbackSettings, HandlerFn}; use runtime::{initialize_runtime, Runtime, RuntimeContainer, RuntimeInitializer, RuntimeSettings}; -use script::{ScriptComponent, ScriptId, Scripts, StaticScripts}; +use script::{ContextPolicy, ScriptComponent, ScriptContext, StaticScripts}; pub mod asset; pub mod bindings; @@ -46,12 +46,10 @@ pub enum ScriptingSystemSet { 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, - + // /// Systems which read entity removal events and remove contexts associated with them + // EntityRemoval, /// One time runtime initialization systems RuntimeInitialization, - /// Systems which handle the garbage collection of allocated values GarbageCollection, } @@ -128,8 +126,14 @@ impl Plugin for ScriptingPlugin

{ assignment_strategy: self.context_assignment_strategy, context_initializers: self.context_initializers.clone(), context_pre_handling_initializers: self.context_pre_handling_initializers.clone(), - }) - .init_resource::>(); + }); + if !app.world().contains_resource::>() { + app.insert_resource(if self.context_assignment_strategy.is_global() { + ScriptContext::

::new(ContextPolicy::shared()) + } else { + ScriptContext::

::default() + }); + } register_script_plugin_systems::

(app); @@ -224,6 +228,8 @@ impl>> ConfigureScriptPlugi self } + // TODO: Enable deprecation once the proper version is set. + // #[deprecated(since="0.13", note="please use app.insert_resource(ScriptContext::shared()) instead")] fn enable_context_sharing(mut self) -> Self { self.as_mut().context_assignment_strategy = ContextAssignmentStrategy::Global; self @@ -257,6 +263,7 @@ pub struct BMSScriptingInfrastructurePlugin; impl Plugin for BMSScriptingInfrastructurePlugin { fn build(&self, app: &mut App) { app.add_event::() + .add_event::() .add_event::() .add_event::() .init_resource::() @@ -270,30 +277,19 @@ impl Plugin for BMSScriptingInfrastructurePlugin { ((garbage_collector).in_set(ScriptingSystemSet::GarbageCollection),), ); - configure_asset_systems(app); + app.add_plugins(configure_asset_systems); DynamicScriptComponentPlugin.build(app); } fn finish(&self, app: &mut App) { - // read extensions from asset settings - let asset_settings_extensions = app + // Read extensions. + let language_extensions = app .world_mut() - .get_resource_or_init::() - .supported_extensions; - - // convert extensions to static array - bevy::log::info!( - "Initializing BMS with Supported extensions: {:?}", - asset_settings_extensions - ); - - app.register_asset_loader(ScriptAssetLoader { - extensions: asset_settings_extensions, - preprocessor: None, - }); - - // pre-register component id's + .remove_resource::() + .unwrap_or_default(); + app.register_asset_loader(ScriptAssetLoader::new(language_extensions)); + // Pre-register component IDs. pre_register_components(app); DynamicScriptComponentPlugin.finish(app); } @@ -311,7 +307,7 @@ fn register_script_plugin_systems(app: &mut App) { .in_set(ScriptingSystemSet::RuntimeInitialization), ); - configure_asset_systems_for_plugin::

(app); + app.add_plugins(configure_asset_systems_for_plugin::

); } /// Register all types that need to be accessed via reflection @@ -353,21 +349,21 @@ pub trait ManageStaticScripts { /// Registers a script id as a static script. /// /// Event handlers will run these scripts on top of the entity scripts. - fn add_static_script(&mut self, script_id: impl Into) -> &mut Self; + fn add_static_script(&mut self, script_id: impl Into>) -> &mut Self; /// Removes a script id from the list of static scripts. /// /// Does nothing if the script id is not in the list. - fn remove_static_script(&mut self, script_id: impl Into) -> &mut Self; + fn remove_static_script(&mut self, script_id: impl Into>) -> &mut Self; } impl ManageStaticScripts for App { - fn add_static_script(&mut self, script_id: impl Into) -> &mut Self { + fn add_static_script(&mut self, script_id: impl Into>) -> &mut Self { AddStaticScript::new(script_id.into()).apply(self.world_mut()); self } - fn remove_static_script(&mut self, script_id: impl Into) -> &mut Self { + fn remove_static_script(&mut self, script_id: impl Into>) -> &mut Self { RemoveStaticScript::new(script_id.into()).apply(self.world_mut()); self } @@ -388,29 +384,39 @@ pub trait ConfigureScriptAssetSettings { ) -> &mut Self; } +/// Collect the language extensions supported during initialization. +/// +/// NOTE: This resource is removed after plugin setup. +#[derive(Debug, Resource, Deref, DerefMut)] +pub struct LanguageExtensions(HashMap<&'static str, Language>); + +impl Default for LanguageExtensions { + fn default() -> Self { + LanguageExtensions( + [ + ("lua", Language::Lua), + ("rhai", Language::Rhai), + ("rn", Language::Rune), + ] + .into_iter() + .collect(), + ) + } +} + impl ConfigureScriptAssetSettings for App { fn add_supported_script_extensions( &mut self, extensions: &[&'static str], language: Language, ) -> &mut Self { - let mut asset_settings = self + let mut language_extensions = self .world_mut() - .get_resource_or_init::(); - - let mut new_arr = Vec::from(asset_settings.supported_extensions); + .get_resource_or_init::(); - new_arr.extend(extensions); - - let new_arr_static = Vec::leak(new_arr); - - asset_settings.supported_extensions = new_arr_static; for extension in extensions { - asset_settings - .extension_to_language_map - .insert(*extension, language.clone()); + language_extensions.insert(extension, language.clone()); } - self } } @@ -422,13 +428,8 @@ mod test { #[tokio::test] async fn test_asset_extensions_correctly_accumulate() { let mut app = App::new(); - app.init_resource::(); app.add_plugins(AssetPlugin::default()); - app.world_mut() - .resource_mut::() - .supported_extensions = &["lua", "rhai"]; - BMSScriptingInfrastructurePlugin.finish(&mut app); let asset_loader = app diff --git a/crates/bevy_mod_scripting_core/src/script.rs b/crates/bevy_mod_scripting_core/src/script.rs deleted file mode 100644 index d5b0fe46ae..0000000000 --- a/crates/bevy_mod_scripting_core/src/script.rs +++ /dev/null @@ -1,171 +0,0 @@ -//! Script related types, functions and components - -use crate::{asset::ScriptAsset, IntoScriptPluginParams}; -use bevy::prelude::ReflectComponent; -use bevy::{asset::Handle, ecs::system::Resource, reflect::Reflect, utils::HashSet}; -use parking_lot::Mutex; -use std::{borrow::Cow, collections::HashMap, ops::Deref, sync::Arc}; - -/// A unique identifier for a script, by default corresponds to the path of the asset excluding the asset source. -/// -/// I.e. an asset with the path `path/to/asset.ext` will have the script id `path/to/asset.ext` -pub type ScriptId = Cow<'static, str>; - -#[derive(bevy::ecs::component::Component, Reflect, Clone)] -#[reflect(Component)] -/// A component which identifies the scripts existing on an entity. -/// -/// Event handlers search for components with this component to figure out which scripts to run and on which entities. -pub struct ScriptComponent(pub Vec); - -impl Deref for ScriptComponent { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl ScriptComponent { - /// Creates a new [`ScriptComponent`] with the given ScriptID's - pub fn new, I: IntoIterator>(components: I) -> Self { - Self(components.into_iter().map(Into::into).collect()) - } -} - -/// All the scripts which are currently loaded or loading and their mapping to contexts -#[derive(Resource)] -pub struct Scripts { - pub(crate) scripts: HashMap>, -} - -#[profiling::all_functions] -impl Scripts

{ - /// Inserts a script into the collection - pub fn insert(&mut self, script: Script

) { - self.scripts.insert(script.id.clone(), script); - } - - /// Removes a script from the collection, returning `true` if the script was in the collection, `false` otherwise - pub fn remove>(&mut self, script: S) -> bool { - self.scripts.remove(&script.into()).is_some() - } - - /// Checks if a script is in the collection - /// Returns `true` if the script is in the collection, `false` otherwise - pub fn contains>(&self, script: S) -> bool { - self.scripts.contains_key(&script.into()) - } - - /// Returns a reference to the script with the given id - pub fn get>(&self, script: S) -> Option<&Script

> { - self.scripts.get(&script.into()) - } - - /// Returns a mutable reference to the script with the given id - pub fn get_mut>(&mut self, script: S) -> Option<&mut Script

> { - self.scripts.get_mut(&script.into()) - } - - /// Returns an iterator over the scripts - pub fn iter(&self) -> impl Iterator> { - self.scripts.values() - } - - /// Returns a mutable iterator over the scripts - pub fn iter_mut(&mut self) -> impl Iterator> { - self.scripts.values_mut() - } -} - -impl Default for Scripts

{ - fn default() -> Self { - Self { - scripts: Default::default(), - } - } -} - -/// A script -pub struct Script { - /// The id of the script - pub id: ScriptId, - /// the asset holding the content of the script if it comes from an asset - pub asset: Option>, - /// The context of the script, possibly shared with other scripts - pub context: Arc>, -} - -impl Clone for Script

{ - fn clone(&self) -> Self { - Self { - id: self.id.clone(), - asset: self.asset.clone(), - context: self.context.clone(), - } - } -} - -/// A collection of scripts, not associated with any entity. -/// -/// Useful for `global` or `static` scripts which operate over a larger scope than a single entity. -#[derive(Default, Resource)] -pub struct StaticScripts { - pub(crate) scripts: HashSet, -} - -#[profiling::all_functions] -impl StaticScripts { - /// Inserts a static script into the collection - pub fn insert>(&mut self, script: S) { - self.scripts.insert(script.into()); - } - - /// Removes a static script from the collection, returning `true` if the script was in the collection, `false` otherwise - pub fn remove>(&mut self, script: S) -> bool { - self.scripts.remove(&script.into()) - } - - /// Checks if a static script is in the collection - /// Returns `true` if the script is in the collection, `false` otherwise - pub fn contains>(&self, script: S) -> bool { - self.scripts.contains(&script.into()) - } - - /// Returns an iterator over the static scripts - pub fn iter(&self) -> impl Iterator { - self.scripts.iter() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn static_scripts_insert() { - let mut static_scripts = StaticScripts::default(); - static_scripts.insert("script1"); - assert_eq!(static_scripts.scripts.len(), 1); - assert!(static_scripts.scripts.contains("script1")); - } - - #[test] - fn static_scripts_remove() { - let mut static_scripts = StaticScripts::default(); - static_scripts.insert("script1"); - assert_eq!(static_scripts.scripts.len(), 1); - assert!(static_scripts.scripts.contains("script1")); - assert!(static_scripts.remove("script1")); - assert_eq!(static_scripts.scripts.len(), 0); - assert!(!static_scripts.scripts.contains("script1")); - } - - #[test] - fn static_scripts_contains() { - let mut static_scripts = StaticScripts::default(); - static_scripts.insert("script1"); - assert!(static_scripts.contains("script1")); - assert!(!static_scripts.contains("script2")); - } -} diff --git a/crates/bevy_mod_scripting_core/src/script/context_key.rs b/crates/bevy_mod_scripting_core/src/script/context_key.rs new file mode 100644 index 0000000000..0e660c9596 --- /dev/null +++ b/crates/bevy_mod_scripting_core/src/script/context_key.rs @@ -0,0 +1,148 @@ +use super::*; +use crate::ScriptAsset; +use bevy::prelude::{default, Entity}; +use std::fmt; + +/// The key for a context. +#[derive(Debug, Hash, Clone, Default, PartialEq, Eq)] +pub struct ContextKey { + /// Entity if there is one. + pub entity: Option, + /// Script ID if there is one. + pub script: Option>, + /// Domain if there is one. + pub domain: Option, +} + +impl fmt::Display for ContextKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // write!(f, "context ")?; + let mut empty = true; + if let Some(script_id) = &self.script { + write!(f, "script {}", script_id.display())?; + empty = false; + } + if let Some(id) = self.entity { + write!(f, "entity {id}")?; + empty = false; + } + if let Some(domain) = self.domain { + write!(f, "domain {domain:?}")?; + empty = false; + } + if empty { + write!(f, "empty")?; + } + Ok(()) + } +} + +// /// A context key reference. +// #[derive(Debug, PartialEq, Eq, Hash)] +// pub struct ContextKeyRef<'a> { +// pub entity: Option, +// pub script_id: Option<&'a Handle>, +// pub domain: Option<&'a Domain>, +// } + +impl ContextKey { + /// Is the key empty? + pub fn is_empty(&self) -> bool { + self.entity.is_none() && self.script.is_none() && self.domain.is_none() + } + + /// or with other context + pub fn or(self, other: ContextKey) -> Self { + Self { + entity: self.entity.or(other.entity), + script: self.script.or(other.script), + domain: self.domain.or(other.domain), + } + } + + /// Returns true if self is a subset of other. + /// + /// Subset meaning if `self.entity` is `Some`, then other must be `Some` and + /// equal. + /// + /// If `self.entity` is `None`, then other.entity can be anything. + /// + /// An empty [ContextKey] is a subset of any context key. + pub fn is_subset(&self, other: &ContextKey) -> bool { + self.entity + .map(|a| other.entity.map(|b| a == b).unwrap_or(false)) + .unwrap_or(true) + || self + .script + .as_ref() + .map(|a| other.script.as_ref().map(|b| a == b).unwrap_or(false)) + .unwrap_or(true) + || self + .domain + .as_ref() + .map(|a| other.domain.as_ref().map(|b| a == b).unwrap_or(false)) + .unwrap_or(true) + } + + /// If a script handle is present and is strong, convert it to a weak + /// handle. + pub fn into_weak(mut self) -> Self { + if let Some(script) = self.script.as_mut() { + if script.is_strong() { + *script = Handle::Weak(script.id()); + } + } + self + } + + // pub fn as_ref(&self) -> ContextKeyRef<'_> { + // ContextKeyRef { + // entity: self.entity, + // script: self.script.as_ref().map(Handle::Weak), + // domain: self.domain.as_ref(), + // } + // } +} + +// impl<'a> Borrow> for ContextKey { +// #[inline] +// fn borrow(&self) -> &ContextKeyRef<'a> { +// self.as_ref() +// } +// } + +impl From for ContextKey { + fn from(entity: Entity) -> Self { + Self { + entity: Some(entity), + ..default() + } + } +} + +impl From for ContextKey { + fn from(script_id: ScriptId) -> Self { + Self { + script: Some(Handle::Weak(script_id)), + ..default() + } + } +} + +impl From> for ContextKey { + fn from(handle: Handle) -> Self { + Self { + script: Some(handle), + ..default() + } + } +} + +impl From for ContextKey { + fn from(domain: Domain) -> Self { + Self { + domain: Some(domain), + ..default() + } + } +} diff --git a/crates/bevy_mod_scripting_core/src/script/mod.rs b/crates/bevy_mod_scripting_core/src/script/mod.rs new file mode 100644 index 0000000000..0938e8fbb5 --- /dev/null +++ b/crates/bevy_mod_scripting_core/src/script/mod.rs @@ -0,0 +1,171 @@ +//! Script related types, functions and components + +use crate::asset::ScriptAsset; +use bevy::prelude::{Component, ReflectComponent}; +use bevy::utils::hashbrown::hash_map::DefaultHashBuilder; +use bevy::{ + asset::{Asset, AssetId, Handle}, + ecs::system::Resource, + reflect::Reflect, + utils::HashSet, +}; +use std::{collections::HashMap, fmt, hash::BuildHasher, ops::Deref}; + +mod context_key; +mod script_context; +pub use context_key::*; +pub use script_context::*; + +/// A unique identifier for a script, by default corresponds to the path of the asset excluding the asset source. +/// +/// I.e. an asset with the path `path/to/asset.ext` will have the script id `path/to/asset.ext` +pub type ScriptId = AssetId; + +/// Display the path of a script or its asset ID. +#[doc(hidden)] +pub struct HandleDisplay<'a, T: Asset>(&'a Handle); + +impl<'a, A: Asset> fmt::Display for HandleDisplay<'a, A> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(path) = self.0.path() { + write!(f, "path {path}") + } else { + write!(f, "id {}", self.0.id()) + } + } +} + +impl<'a, A: Asset> fmt::Debug for HandleDisplay<'a, A> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(path) = self.0.path() { + write!(f, "path {path:?}") + } else { + write!(f, "id {:?}", self.0.id()) + } + } +} + +/// Make a type display-able. +pub trait DisplayProxy { + /// The type that does the displaying. + type D<'a>: fmt::Display + fmt::Debug + where + Self: 'a; + /// Return a display-able reference. + fn display<'a>(&'a self) -> Self::D<'a>; +} + +impl DisplayProxy for Handle { + type D<'a> = HandleDisplay<'a, A>; + + fn display<'a>(&'a self) -> Self::D<'a> { + HandleDisplay(self) + } +} + +/// Defines the domain of a script +#[derive(Component)] +pub struct ScriptDomain(pub Domain); + +#[derive(bevy::ecs::component::Component, Reflect, Clone)] +#[reflect(Component)] +/// A component which identifies the scripts existing on an entity. +/// +/// Event handlers search for components with this component to figure out which scripts to run and on which entities. +pub struct ScriptComponent(pub Vec>); + +impl Deref for ScriptComponent { + type Target = Vec>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl ScriptComponent { + /// Creates a new [`ScriptComponent`] with the given ScriptID's + pub fn new>, I: IntoIterator>(components: I) -> Self { + Self(components.into_iter().map(Into::into).collect()) + } +} + +/// A collection of scripts, not associated with any entity. +/// +/// Useful for `global` or `static` scripts which operate over a larger scope than a single entity. +#[derive(Default, Resource)] +pub struct StaticScripts { + pub(crate) scripts: HashSet>, +} + +#[profiling::all_functions] +impl StaticScripts { + /// Inserts a static script into the collection + pub fn insert>>(&mut self, script: S) { + self.scripts.insert(script.into()); + } + + /// Removes a static script from the collection, returning `true` if the script was in the collection, `false` otherwise + pub fn remove(&mut self, script_id: impl Into) -> bool { + let script_id = script_id.into(); + self.scripts + .extract_if(|handle| handle.id() == script_id) + .next() + .is_some() + } + + /// Checks if a static script is in the collection + /// Returns `true` if the script is in the collection, `false` otherwise + pub fn contains(&self, script_id: impl Into) -> bool { + let script_id = script_id.into(); + self.scripts.iter().any(|handle| handle.id() == script_id) + } + + /// Returns an iterator over the static scripts + pub fn values(&self) -> impl Iterator> { + self.scripts.iter() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn static_scripts_insert() { + let mut static_scripts = StaticScripts::default(); + let script1 = Handle::default(); + static_scripts.insert(script1.clone()); + assert_eq!(static_scripts.scripts.len(), 1); + assert!(static_scripts.scripts.contains(&script1)); + } + + #[test] + fn static_scripts_remove() { + let mut static_scripts = StaticScripts::default(); + let script1 = Handle::default(); + static_scripts.insert(script1.clone()); + assert_eq!(static_scripts.scripts.len(), 1); + assert!(static_scripts.scripts.contains(&script1)); + assert!(static_scripts.remove(&script1)); + assert_eq!(static_scripts.scripts.len(), 0); + assert!(!static_scripts.scripts.contains(&script1)); + } + + fn scriptid_from_u128(uuid: u128) -> ScriptId { + ScriptId::from(uuid::Builder::from_random_bytes(uuid.to_le_bytes()).into_uuid()) + } + + fn handle_from_u128(uuid: u128) -> Handle { + Handle::Weak(scriptid_from_u128(uuid)) + } + + #[test] + fn static_scripts_contains() { + let mut static_scripts = StaticScripts::default(); + let script1 = handle_from_u128(0); + let script2 = handle_from_u128(1); + static_scripts.insert(script1.clone()); + assert!(static_scripts.contains(&script1)); + assert!(!static_scripts.contains(&script2)); + } +} diff --git a/crates/bevy_mod_scripting_core/src/script/script_context.rs b/crates/bevy_mod_scripting_core/src/script/script_context.rs new file mode 100644 index 0000000000..c3afe0b56e --- /dev/null +++ b/crates/bevy_mod_scripting_core/src/script/script_context.rs @@ -0,0 +1,243 @@ +use super::*; +use crate::IntoScriptPluginParams; +use bevy::ecs::system::Resource; +use parking_lot::Mutex; +use std::{hash::Hash, sync::Arc}; + +/// A kind of catch all type for script context selection +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +pub struct Domain(u64); + +impl Domain { + /// Create a domain handle. + pub fn new(hashable: impl Hash) -> Self { + Domain(DefaultHashBuilder::default().hash_one(hashable)) + } +} + +/// Determines how contexts are grouped by manipulating the context key. +pub trait ContextKeySelector { + /// The given context key represents a possible script, entity, domain that + /// is requesting a context. + /// + /// This selector returns + /// - `None` when the given `context_key` is not relevant to its policy, or + /// - `Some(selected_key)` when the appropriate key has been determined. + fn select(&self, context_key: &ContextKey) -> Option; +} + +impl Option> ContextKeySelector for F { + fn select(&self, context_key: &ContextKey) -> Option { + (self)(context_key) + } +} + +/// A rule for context selection +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum ContextRule { + /// If domain exists, return only that. + Domain, + /// If entity-script pair exists, return only that. + EntityScript, + /// If entity exists, return only that. + Entity, + /// If script exists, return only that. + Script, + /// Check nothing; return empty context key. + Shared, + /// A custom rule + Custom(fn(&ContextKey) -> Option), // XXX: Custom rule with this opaque type makes it harder to have the + // derives above that we might want. So maybe we drop this variant. + // Custom(Box) +} + +impl ContextKeySelector for ContextRule { + /// Depending on the enum variant, executes that rule. + /// + /// For example a rule of `Domain` will check for a domain in the + /// `context_key`. If it is present, a ContextKey that only + /// has that domain will be returned. + fn select(&self, context_key: &ContextKey) -> Option { + match self { + ContextRule::Domain => context_key.domain.map(ContextKey::from), + ContextRule::Entity => context_key.entity.map(ContextKey::from), + ContextRule::Script => context_key.script.clone().map(ContextKey::from), + ContextRule::EntityScript => { + context_key + .entity + .zip(context_key.script.clone()) + .map(|(entity, script)| ContextKey { + entity: Some(entity), + script: Some(script), + domain: None, + }) + } + ContextRule::Shared => Some(ContextKey::default()), + ContextRule::Custom(rule) => rule.select(context_key), + } + } +} + +/// This is a configurable context policy based on priority. +#[derive(Resource, Debug, Clone, PartialEq, Eq, Hash)] +pub struct ContextPolicy { + /// The rules in order of priority. + pub priorities: Vec, +} + +/// Returns a `[Domain, EntityScript, Script, Shared]` policy. +impl Default for ContextPolicy { + fn default() -> Self { + ContextPolicy { + priorities: vec![ + ContextRule::Domain, + ContextRule::EntityScript, + ContextRule::Script, + ContextRule::Shared, + ], + } + } +} + +impl ContextPolicy { + /// Return which rule is used for context_key. + pub fn which_rule(&self, context_key: &ContextKey) -> Option<&ContextRule> { + self.priorities + .iter() + .find(|rule| rule.select(context_key).is_some()) + } + /// Use a shared script context. + pub fn shared() -> Self { + ContextPolicy { + priorities: vec![ContextRule::Shared], + } + } + /// If a domain is given, use that first. + pub fn with_domains(mut self) -> Self { + if !self.priorities.contains(&ContextRule::Domain) { + self.priorities.insert(0, ContextRule::Domain); + } + self + } + /// Domain contexts or a shared context. + pub fn domains() -> Self { + ContextPolicy { + priorities: vec![ContextRule::Domain, ContextRule::Shared], + } + } + /// Use one script context per entity or a shared context. + pub fn per_entity() -> Self { + ContextPolicy { + priorities: vec![ContextRule::Entity, ContextRule::Shared], + } + } + /// Use one script context per entity or a shared context. + pub fn per_script() -> Self { + ContextPolicy { + priorities: vec![ContextRule::Script, ContextRule::Shared], + } + } + /// Use one script context per entity-script, or a script context, or a shared context. + pub fn per_entity_and_script() -> Self { + ContextPolicy { + priorities: vec![ + ContextRule::EntityScript, + ContextRule::Script, + ContextRule::Shared, + ], + } + } +} + +impl ContextKeySelector for ContextPolicy { + fn select(&self, context_key: &ContextKey) -> Option { + self.priorities + .iter() + .find_map(|priority| priority.select(context_key)) + } +} + +#[derive(Resource)] +/// Keeps track of script contexts and enforces the context selection policy. +pub struct ScriptContext { + map: HashMap>>, + /// The policy used to determine the context key. + pub policy: ContextPolicy, +} + +impl ScriptContext

{ + /// Construct a new ScriptContext with the given policy. + pub fn new(policy: ContextPolicy) -> Self { + Self { + map: HashMap::default(), + policy, + } + } + + /// Get the context. + pub fn get(&self, context_key: &ContextKey) -> Option<&Arc>> { + self.policy + .select(context_key) + .and_then(|key| self.map.get(&key)) + } + /// Insert a context. + /// + /// If the context cannot be inserted, it is returned as an `Err`. + pub fn insert(&mut self, context_key: &ContextKey, context: P::C) -> Result<(), P::C> { + match self.policy.select(context_key) { + Some(key) => { + self.map + .insert(key.into_weak(), Arc::new(Mutex::new(context))); + Ok(()) + } + None => Err(context), + } + } + /// Returns true if there is a context. + pub fn contains(&self, context_key: &ContextKey) -> bool { + self.policy + .select(context_key) + .map(|key| self.map.contains_key(&key)) + .unwrap_or(false) + } + /// Hash for context. + /// + /// Useful for tracking what context will be returned by `get()` without + /// requiring that `P::C` impl `Hash` and cheaper too. + /// + /// Note: The existence of the hash does not imply the context exists. It + /// only declares what its hash will be. + pub fn hash(&self, context_key: &ContextKey) -> Option { + self.policy + .select(context_key) + .map(|key| DefaultHashBuilder::default().hash_one(&key)) + } + /// Iterate through contexts. + pub fn values(&self) -> impl Iterator>> { + self.map.values() + } + /// Remove a context. + /// + /// Returns context if removed. + pub fn remove(&mut self, context_key: &ContextKey) -> Option>> { + self.policy + .select(context_key) + .and_then(|key| self.map.remove(&key)) + } + + /// Iterate through keys and contexts. + pub fn iter(&self) -> impl Iterator>)> { + self.map.iter() + } +} + +/// Use one script context per entity and script with domains by default; see +/// [ScriptContext::per_entity_and_script]. +impl Default for ScriptContext

{ + fn default() -> Self { + Self { + map: HashMap::default(), + policy: ContextPolicy::default(), + } + } +} diff --git a/crates/bevy_mod_scripting_functions/src/core.rs b/crates/bevy_mod_scripting_functions/src/core.rs index 08963a02d3..29fde7ebf8 100644 --- a/crates/bevy_mod_scripting_functions/src/core.rs +++ b/crates/bevy_mod_scripting_functions/src/core.rs @@ -11,6 +11,7 @@ use bevy_mod_scripting_core::{ script_system::ScriptSystemBuilder, }, docgen::info::FunctionInfo, + script::Domain, *, }; use bevy_mod_scripting_derive::script_bindings; @@ -417,20 +418,20 @@ impl World { /// * `system`: The system that was added. fn add_system( ctxt: FunctionCallContext, - schedule: Val, - builder: Val, + #[allow(unused_variables)] schedule: Val, + #[allow(unused_variables)] builder: Val, ) -> Result, InteropError> { profiling::function_scope!("add_system"); - let world = ctxt.world()?; - let system = match ctxt.language() { + let _world = ctxt.world()?; + let _system = match ctxt.language() { #[cfg(feature = "lua_bindings")] - asset::Language::Lua => world + asset::Language::Lua => _world .add_system::( &schedule, builder.into_inner(), )?, #[cfg(feature = "rhai_bindings")] - asset::Language::Rhai => world + asset::Language::Rhai => _world .add_system::( &schedule, builder.into_inner(), @@ -446,7 +447,8 @@ impl World { )) } }; - Ok(Val(system)) + #[allow(unreachable_code)] + Ok(Val(_system)) } /// Quits the program. @@ -1261,8 +1263,12 @@ impl GlobalNamespace { fn system_builder( callback: String, script_id: String, + domain: Option, ) -> Result, InteropError> { - Ok(ScriptSystemBuilder::new(callback.into(), script_id.into()).into()) + Ok( + ScriptSystemBuilder::new(callback.into(), script_id.into(), domain.map(Domain::new)) + .into(), + ) } } diff --git a/crates/languages/bevy_mod_scripting_lua/src/lib.rs b/crates/languages/bevy_mod_scripting_lua/src/lib.rs index 473c98d24c..06d630333c 100644 --- a/crates/languages/bevy_mod_scripting_lua/src/lib.rs +++ b/crates/languages/bevy_mod_scripting_lua/src/lib.rs @@ -14,7 +14,7 @@ use bevy_mod_scripting_core::{ event::CallbackLabel, reflection_extensions::PartialReflectExt, runtime::RuntimeSettings, - script::ScriptId, + script::ContextKey, IntoScriptPluginParams, ScriptingPlugin, }; use bindings::{ @@ -115,19 +115,27 @@ impl Default for LuaScriptingPlugin { Ok(()) }, ], - context_pre_handling_initializers: vec![|script_id, entity, context| { + context_pre_handling_initializers: vec![|context_key, context| { let world = ThreadWorldContainer.try_get_world()?; - context - .globals() - .set( - "entity", - LuaReflectReference(::allocate(Box::new(entity), world)), - ) - .map_err(ScriptError::from_mlua_error)?; - context - .globals() - .set("script_id", script_id) - .map_err(ScriptError::from_mlua_error)?; + if let Some(entity) = context_key.entity { + context + .globals() + .set( + "entity", + LuaReflectReference(::allocate(Box::new(entity), world)), + ) + .map_err(ScriptError::from_mlua_error)?; + } + if let Some(script) = context_key.script.as_ref() { + let path = script + .path() + .map(|p| p.to_string()) + .unwrap_or_else(|| script.id().to_string()); + context + .globals() + .set("script_id", path) + .map_err(ScriptError::from_mlua_error)?; + } Ok(()) }], additional_supported_extensions: &[], @@ -149,18 +157,18 @@ impl Plugin for LuaScriptingPlugin { fn load_lua_content_into_context( context: &mut Lua, - script_id: &ScriptId, + context_key: &ContextKey, content: &[u8], initializers: &[ContextInitializer], pre_handling_initializers: &[ContextPreHandlingInitializer], ) -> Result<(), ScriptError> { initializers .iter() - .try_for_each(|init| init(script_id, context))?; + .try_for_each(|init| init(context_key, context))?; pre_handling_initializers .iter() - .try_for_each(|init| init(script_id, Entity::from_raw(0), context))?; + .try_for_each(|init| init(context_key, context))?; context .load(content) @@ -173,7 +181,7 @@ fn load_lua_content_into_context( #[profiling::function] /// Load a lua context from a script pub fn lua_context_load( - script_id: &ScriptId, + context_key: &ContextKey, content: &[u8], initializers: &[ContextInitializer], pre_handling_initializers: &[ContextPreHandlingInitializer], @@ -186,7 +194,7 @@ pub fn lua_context_load( load_lua_content_into_context( &mut context, - script_id, + context_key, content, initializers, pre_handling_initializers, @@ -197,7 +205,7 @@ pub fn lua_context_load( #[profiling::function] /// Reload a lua context from a script pub fn lua_context_reload( - script: &ScriptId, + context_key: &ContextKey, content: &[u8], old_ctxt: &mut Lua, initializers: &[ContextInitializer], @@ -206,7 +214,7 @@ pub fn lua_context_reload( ) -> Result<(), ScriptError> { load_lua_content_into_context( old_ctxt, - script, + context_key, content, initializers, pre_handling_initializers, @@ -219,8 +227,7 @@ pub fn lua_context_reload( /// The lua handler for events pub fn lua_handler( args: Vec, - entity: bevy::ecs::entity::Entity, - script_id: &ScriptId, + context_key: &ContextKey, callback_label: &CallbackLabel, context: &mut Lua, pre_handling_initializers: &[ContextPreHandlingInitializer], @@ -228,15 +235,15 @@ pub fn lua_handler( ) -> Result { pre_handling_initializers .iter() - .try_for_each(|init| init(script_id, entity, context))?; + .try_for_each(|init| init(context_key, context))?; let handler: Function = match context.globals().raw_get(callback_label.as_ref()) { Ok(handler) => handler, // not subscribed to this event type Err(_) => { bevy::log::trace!( - "Script {} is not subscribed to callback {}", - script_id, + "Context {} is not subscribed to callback {}", + context_key, callback_label.as_ref() ); return Ok(ScriptValue::Unit); @@ -255,6 +262,8 @@ pub fn lua_handler( #[cfg(test)] mod test { + use bevy::prelude::Handle; + use bevy_mod_scripting_core::script::ScriptId; use mlua::Value; use super::*; @@ -262,15 +271,17 @@ mod test { #[test] fn test_reload_doesnt_overwrite_old_context() { let lua = Lua::new(); - let script_id = ScriptId::from("asd.lua"); + let script_id: ScriptId = ScriptId::from(uuid::Uuid::new_v4()); let initializers = vec![]; let pre_handling_initializers = vec![]; let mut old_ctxt = lua.clone(); + let handle = Handle::Weak(script_id); + let context_key = handle.into(); lua_context_load( - &script_id, + &context_key, "function hello_world_from_first_load() - + end" .as_bytes(), &initializers, @@ -280,9 +291,9 @@ mod test { .unwrap(); lua_context_reload( - &script_id, + &context_key, "function hello_world_from_second_load() - + end" .as_bytes(), &mut old_ctxt, diff --git a/crates/languages/bevy_mod_scripting_rhai/src/lib.rs b/crates/languages/bevy_mod_scripting_rhai/src/lib.rs index 6f72de7362..5a75dfae99 100644 --- a/crates/languages/bevy_mod_scripting_rhai/src/lib.rs +++ b/crates/languages/bevy_mod_scripting_rhai/src/lib.rs @@ -17,7 +17,7 @@ use bevy_mod_scripting_core::{ event::CallbackLabel, reflection_extensions::PartialReflectExt, runtime::RuntimeSettings, - script::ScriptId, + script::{ContextKey, DisplayProxy}, IntoScriptPluginParams, ScriptingPlugin, }; use bindings::{ @@ -150,13 +150,18 @@ impl Default for RhaiScriptingPlugin { Ok(()) }, ], - context_pre_handling_initializers: vec![|script, entity, context| { + context_pre_handling_initializers: vec![|context_key, context| { let world = ThreadWorldContainer.try_get_world()?; - context.scope.set_or_push( - "entity", - RhaiReflectReference(::allocate(Box::new(entity), world)), - ); - context.scope.set_or_push("script_id", script.to_owned()); + + if let Some(entity) = context_key.entity { + context.scope.set_or_push( + "entity", + RhaiReflectReference(::allocate(Box::new(entity), world)), + ); + } + if let Some(script_id) = context_key.script.as_ref() { + context.scope.set_or_push("script_id", script_id.to_owned()); + } Ok(()) }], // already supported by BMS core @@ -180,7 +185,7 @@ impl Plugin for RhaiScriptingPlugin { // NEW helper function to load content into an existing context without clearing previous definitions. fn load_rhai_content_into_context( context: &mut RhaiScriptContext, - script: &ScriptId, + context_key: &ContextKey, content: &[u8], initializers: &[ContextInitializer], pre_handling_initializers: &[ContextPreHandlingInitializer], @@ -189,14 +194,16 @@ fn load_rhai_content_into_context( let runtime = runtime.read(); context.ast = runtime.compile(std::str::from_utf8(content)?)?; - context.ast.set_source(script.to_string()); + if let Some(script) = context_key.script.as_ref() { + context.ast.set_source(script.display().to_string()); + } initializers .iter() - .try_for_each(|init| init(script, context))?; + .try_for_each(|init| init(context_key, context))?; pre_handling_initializers .iter() - .try_for_each(|init| init(script, Entity::from_raw(0), context))?; + .try_for_each(|init| init(context_key, context))?; runtime.eval_ast_with_scope(&mut context.scope, &context.ast)?; context.ast.clear_statements(); @@ -205,7 +212,7 @@ fn load_rhai_content_into_context( /// Load a rhai context from a script. pub fn rhai_context_load( - script: &ScriptId, + context_key: &ContextKey, content: &[u8], initializers: &[ContextInitializer], pre_handling_initializers: &[ContextPreHandlingInitializer], @@ -218,7 +225,7 @@ pub fn rhai_context_load( }; load_rhai_content_into_context( &mut context, - script, + context_key, content, initializers, pre_handling_initializers, @@ -229,7 +236,7 @@ pub fn rhai_context_load( /// Reload a rhai context from a script. New content is appended to the existing context. pub fn rhai_context_reload( - script: &ScriptId, + context_key: &ContextKey, content: &[u8], context: &mut RhaiScriptContext, initializers: &[ContextInitializer], @@ -238,7 +245,7 @@ pub fn rhai_context_reload( ) -> Result<(), ScriptError> { load_rhai_content_into_context( context, - script, + context_key, content, initializers, pre_handling_initializers, @@ -250,8 +257,7 @@ pub fn rhai_context_reload( /// The rhai callback handler. pub fn rhai_callback_handler( args: Vec, - entity: Entity, - script_id: &ScriptId, + context_key: &ContextKey, callback: &CallbackLabel, context: &mut RhaiScriptContext, pre_handling_initializers: &[ContextPreHandlingInitializer], @@ -259,7 +265,7 @@ pub fn rhai_callback_handler( ) -> Result { pre_handling_initializers .iter() - .try_for_each(|init| init(script_id, entity, context))?; + .try_for_each(|init| init(context_key, context))?; // we want the call to be able to impact the scope let options = CallFnOptions::new().rewind_scope(false); @@ -269,9 +275,9 @@ pub fn rhai_callback_handler( .collect::, _>>()?; bevy::log::trace!( - "Calling callback {} in script {} with args: {:?}", + "Calling callback {} in context {} with args: {:?}", callback, - script_id, + context_key, args ); let runtime = runtime.read(); @@ -287,8 +293,8 @@ pub fn rhai_callback_handler( Err(e) => { if let EvalAltResult::ErrorFunctionNotFound(_, _) = e.unwrap_inner() { bevy::log::trace!( - "Script {} is not subscribed to callback {} with the provided arguments.", - script_id, + "Context {} is not subscribed to callback {} with the provided arguments.", + context_key, callback ); Ok(ScriptValue::Unit) @@ -306,14 +312,14 @@ mod test { #[test] fn test_reload_doesnt_overwrite_old_context() { let runtime = RhaiRuntime::new(Engine::new()); - let script_id = ScriptId::from("asd.rhai"); + let context_key = ContextKey::default(); let initializers: Vec> = vec![]; let pre_handling_initializers: Vec> = vec![]; // Load first content defining a function that returns 42. let mut context = rhai_context_load( - &script_id, + &context_key, b"let hello = 2;", &initializers, &pre_handling_initializers, @@ -323,7 +329,7 @@ mod test { // Reload with additional content defining a second function that returns 24. rhai_context_reload( - &script_id, + &context_key, b"let hello2 = 3", &mut context, &initializers, diff --git a/crates/testing_crates/script_integration_test_harness/Cargo.toml b/crates/testing_crates/script_integration_test_harness/Cargo.toml index 723e8a8cae..49b849043e 100644 --- a/crates/testing_crates/script_integration_test_harness/Cargo.toml +++ b/crates/testing_crates/script_integration_test_harness/Cargo.toml @@ -6,7 +6,7 @@ publish = false [features] default = ["lua", "rhai"] -lua = ["bevy_mod_scripting_lua", "bevy_mod_scripting_functions/lua_bindings"] +lua = ["bevy_mod_scripting_lua", "bevy_mod_scripting_functions/lua_bindings", "bevy_mod_scripting_lua/lua54"] rhai = ["bevy_mod_scripting_rhai", "bevy_mod_scripting_functions/rhai_bindings"] [dependencies] @@ -23,3 +23,4 @@ bevy_mod_scripting_rhai = { path = "../../languages/bevy_mod_scripting_rhai", op criterion = "0.5" rand = "0.9" rand_chacha = "0.9" +uuid = "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 17956e9c62..2bbec1da1c 100644 --- a/crates/testing_crates/script_integration_test_harness/src/lib.rs +++ b/crates/testing_crates/script_integration_test_harness/src/lib.rs @@ -1,6 +1,7 @@ pub mod test_functions; use std::{ + fs, marker::PhantomData, path::PathBuf, time::{Duration, Instant}, @@ -8,16 +9,16 @@ use std::{ use bevy::{ app::{Last, Plugin, PostUpdate, Startup, Update}, - asset::{AssetServer, Handle}, + asset::{AssetPath, AssetServer, Assets, Handle, LoadState}, ecs::{ component::Component, event::{Event, Events}, schedule::{IntoSystemConfigs, SystemConfigs}, - system::{IntoSystem, Local, Res, Resource, SystemState}, + system::{IntoSystem, Resource, SystemState}, world::{Command, FromWorld, Mut}, }, log::Level, - prelude::{Entity, World}, + prelude::World, reflect::{Reflect, TypeRegistry}, utils::tracing, }; @@ -33,7 +34,7 @@ use bevy_mod_scripting_core::{ event::{IntoCallbackLabel, ScriptErrorEvent}, extractors::{HandlerContext, WithWorldGuard}, handler::handle_script_errors, - script::ScriptId, + script::{ContextKey, DisplayProxy, ScriptComponent, ScriptId}, BMSScriptingInfrastructurePlugin, IntoScriptPluginParams, ScriptingPlugin, }; use bevy_mod_scripting_functions::ScriptFunctionsPlugin; @@ -55,13 +56,13 @@ struct TestCallbackBuilder { } impl TestCallbackBuilder { - fn build(script_id: impl Into, expect_response: bool) -> SystemConfigs { - let script_id = script_id.into(); + fn build(context_key: impl Into, expect_response: bool) -> SystemConfigs { + let context_key = context_key.into(); IntoSystem::into_system( move |world: &mut World, system_state: &mut SystemState>>| { let with_guard = system_state.get_mut(world); - let _ = run_test_callback::(&script_id.clone(), with_guard, expect_response); + let _ = run_test_callback::(&context_key, with_guard, expect_response); system_state.apply(world); }, @@ -92,8 +93,9 @@ pub fn make_test_lua_plugin() -> bevy_mod_scripting_lua::LuaScriptingPlugin { use bevy_mod_scripting_core::{bindings::WorldContainer, ConfigureScriptPlugin}; use bevy_mod_scripting_lua::{mlua, LuaScriptingPlugin}; - LuaScriptingPlugin::default().add_context_initializer( - |_, ctxt: &mut bevy_mod_scripting_lua::mlua::Lua| { + LuaScriptingPlugin::default() + .enable_context_sharing() + .add_context_initializer(|_, ctxt: &mut bevy_mod_scripting_lua::mlua::Lua| { let globals = ctxt.globals(); globals.set( "assert_throws", @@ -123,8 +125,7 @@ pub fn make_test_lua_plugin() -> bevy_mod_scripting_lua::LuaScriptingPlugin { })?, )?; Ok(()) - }, - ) + }) } #[cfg(feature = "rhai")] @@ -200,13 +201,15 @@ pub fn execute_rhai_integration_test(script_id: &str) -> Result<(), String> { } pub fn execute_integration_test< + 'a, P: IntoScriptPluginParams + Plugin + AsMut>, F: FnOnce(&mut World, &mut TypeRegistry), >( plugin: P, init: F, - script_id: &str, + script_id: impl Into>, ) -> Result<(), String> { + let script_id = script_id.into(); // set "BEVY_ASSET_ROOT" to the global assets folder, i.e. CARGO_MANIFEST_DIR/../../../assets let mut manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); @@ -232,28 +235,45 @@ pub fn execute_integration_test< OnTestLast => "on_test_last", ); - let script_id = script_id.to_owned(); - let script_id: &'static str = Box::leak(script_id.into_boxed_str()); - - let load_system = |server: Res, mut handle: Local>| { - *handle = server.load(script_id.to_owned()); - }; - // tests can opt in to this via "__RETURN" - let expect_callback_response = script_id.contains("__RETURN"); - - app.add_systems(Startup, load_system); + let expect_callback_response = script_id + .path() + .to_str() + .map(|s| s.contains("__RETURN")) + .unwrap_or(false); + // The following code did not work, possibly because of the asynchronous + // nature of AssetServer. + // + // ``` + // let handle = app.world_mut().resource_mut::().load(&script_path); + // app.world_mut().spawn(ScriptComponent::new([handle.clone()])); + // ``` + let handle = { + let mut script_dir = manifest_dir.clone(); + script_dir.push("assets"); + script_dir.push(script_id.path()); + // Read the contents and don't do anything async. + let content = fs::read_to_string(&script_dir) + .map_err(|io| format!("io error {io} for path {script_dir:?}"))?; + let mut script = ScriptAsset::from(content); + script.language = P::LANGUAGE; + app.world_mut() + .resource_mut::>() + .add(script) + }; + app.world_mut() + .spawn(ScriptComponent::new([handle.clone()])); app.add_systems( Update, - TestCallbackBuilder::::build(script_id, expect_callback_response), + TestCallbackBuilder::::build(handle.clone(), expect_callback_response), ); app.add_systems( PostUpdate, - TestCallbackBuilder::::build(script_id, expect_callback_response), + TestCallbackBuilder::::build(handle.clone(), expect_callback_response), ); app.add_systems( Last, - TestCallbackBuilder::::build(script_id, expect_callback_response), + TestCallbackBuilder::::build(handle.clone(), expect_callback_response), ); app.add_systems(Update, dummy_update_system); app.add_systems(Startup, dummy_startup_system::); @@ -283,6 +303,10 @@ pub fn execute_integration_test< .collect::>(); if let Some(event) = error_events.into_iter().next() { + // eprintln!("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXx"); + // if ! app.world().resource::().load_state(&handle).is_loaded() { + // continue; + // } return Err(event .error .display_with_world(WorldGuard::new_exclusive(app.world_mut()))); @@ -296,22 +320,17 @@ pub fn execute_integration_test< } fn run_test_callback( - script_id: &str, + context_key: &ContextKey, mut with_guard: WithWorldGuard<'_, '_, HandlerContext<'_, P>>, expect_response: bool, ) -> Result { let (guard, handler_ctxt) = with_guard.get_mut(); - if !handler_ctxt.is_script_fully_loaded(script_id.to_string().into()) { - return Ok(ScriptValue::Unit); - } + // if !handler_ctxt.is_script_fully_loaded(*script_id) { + // return Ok(ScriptValue::Unit); + // } - let res = handler_ctxt.call::( - &script_id.to_string().into(), - Entity::from_raw(0), - vec![], - guard.clone(), - ); + let res = handler_ctxt.call::(context_key, vec![], guard.clone()); let e = match res { Ok(ScriptValue::Error(e)) => e.into(), @@ -406,9 +425,9 @@ pub fn run_rhai_benchmark( ) } -pub fn run_plugin_benchmark( +pub fn run_plugin_benchmark<'a, P, F, M: criterion::measurement::Measurement>( plugin: P, - script_id: &str, + script_path: impl Into>, label: &str, criterion: &mut criterion::BenchmarkGroup, bench_fn: F, @@ -425,14 +444,13 @@ where install_test_plugin(&mut app, plugin, true); - let script_id = script_id.to_owned(); - let script_id_clone = script_id.clone(); - app.add_systems( - Startup, - move |server: Res, mut handle: Local>| { - *handle = server.load(script_id_clone.to_owned()); - }, - ); + let script_path = script_path.into(); + let script_handle = app.world().resource::().load(script_path); + let script_id = script_handle.id(); + let entity = app + .world_mut() + .spawn(ScriptComponent(vec![script_handle.clone()])) + .id(); // finalize app.cleanup(); @@ -442,31 +460,45 @@ where let mut state = SystemState::>>::from_world(app.world_mut()); + // Wait until script is loaded. + loop { + app.update(); + match app.world().resource::().load_state(script_id) { + LoadState::Loaded => break, + LoadState::Failed(e) => { + return Err(format!( + "Failed to load script {}: {e}", + script_handle.display() + )); + } + _ => continue, + } + } + loop { app.update(); let mut handler_ctxt = state.get_mut(app.world_mut()); let (guard, context) = handler_ctxt.get_mut(); - - if context.is_script_fully_loaded(script_id.clone().into()) { - let script = context - .scripts() - .get_mut(script_id.to_owned()) - .ok_or_else(|| String::from("Could not find scripts resource"))?; - let ctxt_arc = script.context.clone(); - let mut ctxt_locked = ctxt_arc.lock(); - - let runtime = &context.runtime_container().runtime; - - return WorldAccessGuard::with_existing_static_guard(guard, |guard| { - // Ensure the world is available via ThreadWorldContainer - ThreadWorldContainer - .set_world(guard.clone()) - .map_err(|e| e.display_with_world(guard))?; - // Pass the locked context to the closure for benchmarking its Lua (or generic) part - bench_fn(&mut ctxt_locked, runtime, label, criterion) - }); - } + let context_key = ContextKey { + entity: Some(entity), + script: Some(Handle::Weak(script_id)), + domain: None, + }; + + let ctxt_arc = context.script_context().get(&context_key).cloned().unwrap(); + let mut ctxt_locked = ctxt_arc.lock(); + + let runtime = &context.runtime_container().runtime; + + let _ = WorldAccessGuard::with_existing_static_guard(guard, |guard| { + // Ensure the world is available via ThreadWorldContainer + ThreadWorldContainer + .set_world(guard.clone()) + .map_err(|e| e.display_with_world(guard))?; + // Pass the locked context to the closure for benchmarking its Lua (or generic) part + bench_fn(&mut ctxt_locked, runtime, label, criterion) + }); state.apply(app.world_mut()); if timer.elapsed() > Duration::from_secs(30) { return Err("Timeout after 30 seconds, could not load script".into()); @@ -482,7 +514,6 @@ pub fn run_plugin_script_load_benchmark< benchmark_id: &str, content: &str, criterion: &mut criterion::BenchmarkGroup, - script_id_generator: impl Fn(u64) -> String, reload_probability: f32, ) { let mut app = setup_integration_test(|_, _| {}); @@ -495,17 +526,13 @@ pub fn run_plugin_script_load_benchmark< || { let mut rng = RNG.lock().unwrap(); let is_reload = rng.random_range(0f32..=1f32) < reload_probability; - let random_id = if is_reload { 0 } else { rng.random::() }; - - let random_script_id = script_id_generator(random_id); - // we manually load the script inside a command - let content = content.to_string().into_boxed_str(); + let random_id = if is_reload { 0 } else { rng.random::() }; + let random_script_id: ScriptId = ScriptId::from( + uuid::Builder::from_random_bytes(random_id.to_le_bytes()).into_uuid(), + ); + // We manually load the script inside a command. ( - CreateOrUpdateScript::

::new( - random_script_id.into(), - content.clone().into(), - None, - ), + CreateOrUpdateScript::

::new(random_script_id).with_content(content), is_reload, ) }, diff --git a/crates/testing_crates/test_utils/src/test_data.rs b/crates/testing_crates/test_utils/src/test_data.rs index 030f7dc020..c99cc466df 100644 --- a/crates/testing_crates/test_utils/src/test_data.rs +++ b/crates/testing_crates/test_utils/src/test_data.rs @@ -4,7 +4,6 @@ use std::collections::HashMap; use bevy::asset::AssetPlugin; use bevy::diagnostic::DiagnosticsPlugin; use bevy::ecs::{component::*, world::World}; -use bevy::log::LogPlugin; use bevy::prelude::*; use bevy::reflect::*; @@ -340,7 +339,7 @@ pub fn setup_integration_test(init: F) // first setup all normal test components and resources let mut app = setup_app(init); - let log_level = + let _log_level = std::env::var("RUST_LOG").unwrap_or_else(|_| "bevy_mod_scripting_core=debug".to_string()); app.add_plugins(( @@ -348,10 +347,10 @@ pub fn setup_integration_test(init: F) AssetPlugin::default(), HierarchyPlugin, DiagnosticsPlugin, - LogPlugin { - filter: log_level, - ..Default::default() - }, + // bevy::log::LogPlugin { + // filter: log_level, + // ..Default::default() + // }, )); app } diff --git a/crates/testing_crates/test_utils/src/test_plugin.rs b/crates/testing_crates/test_utils/src/test_plugin.rs index 7a091f6c8d..5cf48a9245 100644 --- a/crates/testing_crates/test_utils/src/test_plugin.rs +++ b/crates/testing_crates/test_utils/src/test_plugin.rs @@ -33,8 +33,12 @@ macro_rules! make_test_plugin { #[derive(Default)] struct TestRuntime { - pub invocations: - parking_lot::Mutex>, + pub invocations: parking_lot::Mutex< + Vec<( + Option, + Option<$ident::script::ScriptId>, + )>, + >, } #[derive(Default)] diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000000..3f78fdc5a1 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,27 @@ +# BMS Docs + +## How To + +### Run Tests + +To run the tests within the book do this: +``` sh +cd docs; +mdbook test -L ../target/debug/deps +``` + +To run tests on a particular chapter, say the "Managing Scripts" chapter, do this: +``` sh +cd docs; +mdbook test -L ../target/debug/deps -c "Managing Scripts" +``` + +## Troubleshooting + +If there are errors that complain about different rustc versions, consider reinstalling "mdbook" using whatever toolchain BMS is built with. + +``` sh +cargo uninstall mdbook +cargo +stable install mdbook +``` + diff --git a/docs/src/Migration/0.12-to-0.13.md b/docs/src/Migration/0.12-to-0.13.md new file mode 100644 index 0000000000..90b71aadfd --- /dev/null +++ b/docs/src/Migration/0.12-to-0.13.md @@ -0,0 +1 @@ +# 0.12-to-0.13 diff --git a/docs/src/Migration/0.14-to-0.15.md b/docs/src/Migration/0.14-to-0.15.md new file mode 100644 index 0000000000..7341258da8 --- /dev/null +++ b/docs/src/Migration/0.14-to-0.15.md @@ -0,0 +1,168 @@ +# Migration Guide: 0.14 to 0.15 + +The most important changes to be aware of in this release are: + +- BMS now uses `Handle` as its principle means of referring to + scripts as opposed to the `ScriptId` that was used previously. + +- BMS exposes many more choices for deciding how scripts are associated with the + contexts in which they run. + +- Domains represent a new means of controlling how scripts may be grouped into + contexts. + +## Handles + +The use of handles for scripts is perhaps the biggest user-facing change. It makes BMS asset usage follow Bevy's conventions. It requires less configuration. It is more idiomatic. + +### `ScriptComponent` Change + +In prior versions, `ScriptComponent` accepted a vector of `ScriptId`s, which was a type alias for `Cow<'static, str>`. +```diff +-pub struct ScriptComponent(pub Vec>); ++pub struct ScriptComponent(pub Vec>); +``` +Because `ScriptComponent` accepts handles, it is no longer necessary to store the handles somewhere to prevent the script from unloading. Nor is it necessary to configure an asset path to script id mapper. +```rust,ignore +ScriptComponent(vec![asset_server.load("foo.lua")]) +``` + +### `ScriptId` Change + +The `ScriptId` has changed from being a string to being an asset ID. + +```diff +-type ScriptId = Cow<'static, str> ++type ScriptId = AssetId +``` + +### No Evaluation on Load + +In prior versions, BMS would immediately evaluate a `ScriptAsset` when it was loaded, and if multiple script assets were loaded, they would be evaluated in non-deterministic order. (See [issue #426](https://github.com/makspll/bevy_mod_scripting/issues/426).) Script assets no longer evaluate immediately. Script assets are only evaluated when their handles are added to a `ScriptComponent` or they are added to `StaticScripts`. + +In addition when a `ScriptComponent` loads its scripts, it loads them sequentially. + +### `ScriptAssetSettings` Removed + +The `ScriptAssetSettings` has been removed. Let us address each of its fields in turn. + +#### `script_id_mapper` + +See `AssetPathToScriptIdMapper` section. + +#### `extension_to_language_map` and `supported_extensions` +This is now represented by the `LanguageExtensions` resource, which can be configured directly during initialization +```rust,ignore +pub struct LanguageExtensions(HashMap<&'static str, Language>); +``` +or by the `ConfigureScriptAssetSettings` trait: +```rust,ignore +app.add_supported_script_extensions(&["p8lua"], Language::Lua); +``` +In addition one can configure the language of an asset when it is loaded: +```rust,ignore +asset_server.load_with_settings("hello.p8lua", |settings: &mut ScriptSettings| { + settings.language = Some(Language::Lua); +}); +``` +or when it is created: +```rust,ignore +let content = String::from("x = 0"); +let mut script = ScriptAsset::from(content); +script.language = Language::Lua; +``` +### `ScriptMetadata` and `ScriptMetadataStore` Removed + +These were present to associate the previous `ScriptId` with the asset ID and language. +That is no longer necessary as the `ScriptAsset` knows its own language. + +```rust,ignore +pub struct ScriptAsset { + /// The body of the script + pub content: Box<[u8]>, + /// The language of the script + pub language: Language, +} +``` +### `AssetPathToScriptIdMapper` Removed + +No mapper is necessary between a script and a script ID. If you have a script handle, you have its script ID + +```rust,ignore +let handle: Handle = ...; +let script_id: ScriptId = handle.id(); // ScriptId = AssetId +``` +and vice versa. + +```rust,ignore +let script_id: ScriptId = ...; +let handle: Handle = Handle::Weak(script_id); +``` +## Contexts +Choosing how scripts run is a big policy decision. Previously BMS had two options: +- Each script ran in its own context. +- All scripts ran in one context. + +This was controlled by the `enable_context_sharing()` method on +`ConfigureScriptPlugin`. That function is now deprecated. Instead BMS encourages +users to do the following to use a shared context: + +```rust,ignore +app.insert_resource(ScriptContext::::new(ContextPolicy::shared())); +``` +The reason for this change is there are many more choices than before, namely: + +- `ContextPolicy::shared()` +- `ContextPolicy::per_script()` +- `ContextPolicy::per_entity()` +- `ContextPolicy::per_entity_and_script()` + +See [Contexts](../Summary/Contexts.md) for more information. + +In addition each of the above can have domains enabled with a `.with_domains()` +call. Domains are a new concept within BMS. They represent a kind of ad-hoc +context grouping mechanism. See the [domains +section](../Summary/Contexts.md#domains) for more information. + +### No Context Removal on Unload by Default + +Prior versions of BMS would immediately remove a context once all the strong +handles for a script were dropped. The script handles were a proxy for +controlling the contexts. This made sense when the script handles to contexts +were a 1-to-1 relationship, but now BMS allows for many different kinds of +relations between contexts and scripts. + +To restore that previous behavior, one can add the following system: +```rust,ignore +app.add_systems(PostUpdate, remove_context_on_script_removal); +``` +To manually initiate context removal, do the following: +```rust,ignore +commands.queue(DeleteScript::::new(script_handle)); +``` + +### `ContextKey` Added +Previously BMS used a `ScriptId` and sometimes an `Entity` to refer to a +context. If there was no entity then `Entity::from_raw(0)` was used. Instead BMS +now uses this `ContextKey` to look up contexts. + +```rust,ignore +pub struct ContextKey { + /// Entity if there is one. + pub entity: Option, + /// Script ID if there is one. + pub script_id: Option>, + /// Domain if there is one. + pub domain: Option, +} +``` +This change affects the parameters for the `context_pre_handling_instructions` +```diff +- context_pre_handling_initializers: vec![|script_id, entity, context| { ++ context_pre_handling_initializers: vec![|context_key, context| { +``` +and `context_initializers`: +```diff +- context_initializers: vec![|script_id, context| { ++ context_initializers: vec![|context_key, context| { +``` diff --git a/docs/src/Migration/guides.md b/docs/src/Migration/guides.md new file mode 100644 index 0000000000..f90e28727b --- /dev/null +++ b/docs/src/Migration/guides.md @@ -0,0 +1,6 @@ +# Migration Guides + +BMS like Bevy itself is still experimental and may have breaking changes even with minor point releases. To aide migration from one version to another, these guides have been written. They are not comprehensive for all versions but with the breaking changes present in 0.15 it seemed necessary to start there. + +- [0.14-to-0.15](./0.14-to-0.15.md) + diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index cb80e83102..37a79a23cc 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -8,10 +8,11 @@ - [Running Scripts](./Summary/running-scripts.md) - [Controlling Script Bindings](./Summary/controlling-script-bindings.md) - [Modifying Script Contexts](./Summary/customizing-script-contexts.md) -- [Shared Contexts](./Summary/sharing-contexts-between-scripts.md) -- [Script ID Mapping](./Summary/script-id-mapping.md) +- [Contexts](./Summary/contexts.md) - [Script Systems](./ScriptSystems/introduction.md) - [Examples](./Examples/introduction.md) +- [Migration Guides](./Migration/guides.md) + - [0.14-to-0.15](./Migration/0.14-to-0.15.md) # Scripting Reference diff --git a/docs/src/Summary/contexts.md b/docs/src/Summary/contexts.md new file mode 100644 index 0000000000..c05dba2e76 --- /dev/null +++ b/docs/src/Summary/contexts.md @@ -0,0 +1,120 @@ +# Contexts + +Each script runs in a context. By default BMS will create an individual context, or sandbox, for each script-entity pair that is run. This means that each script-entity pair will have its own set of global variables and functions that are isolated from other scripts. However, sometimes this might not be desirable. If you are not worried about scripts interfering with each other, or if you want to easily share data between scripts, you can consider using a different context policy. + +## Context Policies + +### Shared Context +A shared context means that all scripts run in the same context; there is in fact only one context to run in. If the scripts interact heavily with each other, this may be what you want. + +To enable a shared context, insert the `ContextPolicy::shared()` resource. +```rust,ignore +app.insert_resource(ScriptContext::::new(ContextPolicy::shared())); +``` + +### Per Script Context +A per script context provides each script with their own context. However, scripts may be attached to multiple entities, in which case a single script context is shared by multiple entities. + +To enable per script contexts, insert the `ContextPolicy::per_script()` resource. +```rust,ignore +app.insert_resource(ScriptContext::::new(ContextPolicy::per_script())); +``` + +### Per Entity Context +A per entity context provides each entity with their own context. The scripts attached to an entity via `ScriptComponent` all run in the same context. + +To enable per entity contexts, insert the `ContextPolicy::per_entity()` resource. +```rust,ignore +app.insert_resource(ScriptContext::::new(ContextPolicy::per_entity())); +``` + +### Per Entity and Script Context +A per entity-and-script context provides each entity-script pair with their own context. This is a maximally isolated way to run scripts. + +To enable per entity-and-script contexts, insert the `ContextPolicy::per_entity_and_script()` resource. +```rust,ignore +app.insert_resource(ScriptContext::::new(ContextPolicy::per_entity_and_script())); +``` +## Custom Policies + +Here is another way to write the `per_script()` policy. +```rust,ignore +let policy_a = ContextPolicy::per_script(); +let policy_b = ContextPolicy { priorities: vec![ContextRule::Script, ContextRule::Shared] }; +assert_eq!(policy_a, policy_b); +``` +Reminding ourselves how `ContextKey` is defined, +```rust,ignore +pub struct ContextKey { + pub entity: Option, + pub script: Option>, + pub domain: Option, +} +``` +we read `policy_b` like this: if `ContextKey` has a script, return a `ContextKey` with only a script. Failing that `ContextRule::Shared` always returns an empty `ContextKey`. + +One may also provide a custom context rule function with this variant: +```rust,ignore +ContextRule::Custom(fn(&ContextKey) -> Option) +``` +For example a custom rule might put scripts---that come from a directory path that has "special" in it---into a "special" domain. What is a domain, you ask? See the next section. +```rust,ignore +fn domain_for_special(context_key: &ContextKey) -> Option) { + context_key.script.as_ref() + .and_then(|handle| handle.path()) + .and_then(|asset_path| asset_path.path()) + .map(|path| path.components.any(|dir| dir == "special").then_some(ContextKey::from(Domain::new("special")))) +} +// ... +let policy_c = ContextPolicy { priorities: vec![ContextRule::Custom(domain_for_special), ContextRule::Script, ContextRule::Shared] }; +``` + + +# Domains +The above context policies cut along regular boundaries inherent in Bevy's entity and asset architecture. However, sometimes one needs to group the execution of scripts in a more ad-hoc manner. Domains provide an escape hatch that allow one to group scripts into contexts by whatever criterion they choose. + +Suppose one had a set of scripts they wish to run within an "player" context. +```rust,ignore +commands.spawn(( + ScriptComponent(vec![asset_server.load("player.lua")]), + ScriptDomain(Domain::new("player")), +)).with_children(|parent| { + parent.spawn(( + ScriptComponent(vec![asset_server.load("sword.lua")]), + ScriptDomain(Domain::new("player")), + )); +} +``` +And another set of scripts they wish to run within an "environment" context. +```rust,ignore +commands.spawn(( + ScriptComponent(vec![asset_server.load("monster.lua")]), + ScriptDomain(Domain::new("environment")), + )); +``` +## Enable Domains + +To enable domains-based contexts, insert the `ContextPolicy::domains()` resource. +```rust,ignore +app.insert_resource(ScriptContext::::new(ContextPolicy::domains()); +``` + +But in addition, domains can be enabled on any of the above policies. This means that when no `ScriptDomain` is given the other strategy will be used. +```rust,ignore +ContextPolicy::shared().with_domains() +ContextPolicy::per_script().with_domains() +ContextPolicy::per_entity().with_domains() +ContextPolicy::per_entity_and_script().with_domains() +``` +The last context, the per entity-script pair with domains is the same as `ScriptContext::default()` and is the default context policy for BMS. + +## Context Loading Settings + +All context loading settings are stored in a separate resource per scripting plugin namely: `ContextLoadingSettings`. + +The settings are as follows: +- `loader` - the load and unload strategy for contexts. Each scripting plugin will have a load and unload function which is hooked up through here +- `context_initializers` - stores all context initializers for the plugin +- `context_pre_handling_initializers` - stores all context pre-handling initializers for the plugin + +More advanced applications might want to customize these settings to suit their needs. diff --git a/docs/src/Summary/customizing-script-contexts.md b/docs/src/Summary/customizing-script-contexts.md index 071a7a72bc..93ebf368b2 100644 --- a/docs/src/Summary/customizing-script-contexts.md +++ b/docs/src/Summary/customizing-script-contexts.md @@ -12,7 +12,7 @@ For example, let's say you want to set a dynamic amount of globals in your scrip You could do this by customizing the scripting plugin: ```rust,ignore -let plugin = LuaScriptingPlugin::default().add_context_initializer(|script_id: &str, context: &mut Lua| { +let plugin = LuaScriptingPlugin::default().add_context_initializer(|_context_key: &ContextKey, context: &mut Lua| { let globals = context.globals(); for i in 0..10 { globals.set(i, i); @@ -28,21 +28,33 @@ The above will run every time the script is loaded or re-loaded and before it ha ## Context Pre Handling Initializers If you want to customize your context before every time it's about to handle events (and when it's loaded + reloaded), you can use `Context Pre Handling Initializers`: -```rust,ignore -let plugin = LuaScriptingPlugin::default().add_context_pre_handling_initializer(|script_id: &str, entity: Entity, context: &mut Lua| { - let globals = context.globals(); - globals.set("script_name", script_id.to_owned()); - Ok(()) -}); +```rust +use bevy::prelude::*; +use bevy_mod_scripting::prelude::*; +fn scripting_plugin(app: &mut App) { + app.add_plugins(LuaScriptingPlugin::default() + .add_context_pre_handling_initializer(|context_key: &ContextKey, entity: Entity, context: &mut Lua| { + let globals = context.globals(); + if let Some(script_id) = context_key.script_id.as_ref() { + globals.set("script_name", script_id.to_owned()); + } + Ok(()) + })); +} ``` ## Runtime Initializers Some scripting languages, have the concept of a `runtime`. This is a global object which is shared between all contexts. You can customize this object using `Runtime Initializers`: -```rust,ignore -let plugin = SomeScriptingPlugin::default().add_runtime_initializer(|runtime: &mut Runtime| { - runtime.set_max_stack_size(1000); - Ok(()) -}); +```rust +use bevy::prelude::*; +use bevy_mod_scripting::prelude::*; +fn scripting_plugin(app: &mut App) { + app.add_plugin(SomeScriptingPlugin::default().add_runtime_initializer(|runtime: &mut Runtime| { + runtime.set_max_stack_size(1000); + Ok(()) + })); + +} ``` In the case of Lua, the runtime type is `()` i.e. This is because `mlua` does not have a separate runtime concept. @@ -50,12 +62,16 @@ In the case of Lua, the runtime type is `()` i.e. This is because `mlua` does no ## Accessing the World in Initializers You can access the world in these initializers by using the thread local: `ThreadWorldContainer`: -```rust,ignore - -let plugin = LuaScriptingPlugin::default(); -plugin.add_context_initializer(|script_id: &str, context: &mut Lua| { - let world = ThreadWorldContainer.try_get_world().unwrap(); - world.with_resource::(|res| println!("My resource: {:?}", res)); - Ok(()) -}); -``` \ No newline at end of file +```rust +use bevy::prelude::*; +use bevy_mod_scripting::prelude::*; +fn scripting_plugin(app: &mut App) { + let plugin = LuaScriptingPlugin::default(); + plugin.add_context_initializer(|_context_key: &ContextKey, context: &mut Lua| { + let world = ThreadWorldContainer.try_get_world().unwrap(); + world.with_resource::(|res| println!("My resource: {:?}", res)); + Ok(()) + }); + app.add_plugins(plugin); +} +``` diff --git a/docs/src/Summary/managing-scripts.md b/docs/src/Summary/managing-scripts.md index e2f93aecbc..40dcd6f4be 100644 --- a/docs/src/Summary/managing-scripts.md +++ b/docs/src/Summary/managing-scripts.md @@ -1,32 +1,73 @@ # Managing Scripts -Scripts live in the standard bevy `assets` directory. Loading a script means: -- Parsing the script body -- Creating or updating the resources which store script state -- Assigning a name/id to the script so it can be referred to by the rest of the application. +Scripts live in the standard Bevy `assets` directory. Loading a script means obtaining its byte representation and associated language. -## Loading -BMS listens to `ScriptAsset` events and reacts accordingly. In order to load a script, all you need to do is request a handle to it via the asset server and store it somewhere. - -Below is an example system which loads a script called `assets/my_script.lua` and stores the handle in a local system parameter: +Evaluating a script means: +- parsing the script body, +- and creating or updating the resources which store script state. +## Loading +Scripts can be loaded into memory via the `AssetServer`. +```rust,ignore +let handle = asset_server.load::("my_script.lua"); +``` +Or scripts can be created in memory. ```rust,ignore -fn load_script(server: Res, mut handle: Local>) { - let handle_ = server.load::("my_script.lua"); - *handle = handle_; +let mut script = ScriptAsset::from("x = 0".into()); +script.language = Language::Lua; +let handle = script_assets.add(script); +``` +This will not evaluate any code yet. + +## Evaluating +To evaluate a script, add it to a `ScriptComponent` or to `StaticScripts`. +### Load File via `AssetServer` +```rust +# extern crate bevy; +# extern crate bevy_mod_scripting; +# use bevy::prelude::*; +# use bevy_mod_scripting::prelude::*; + +fn load_script(asset_server: Res, mut commands: Commands) { + let handle = asset_server.load::("my_script.lua"); + commands.spawn(ScriptComponent(vec![handle])); +} +``` +### Create `ScriptAsset` and Add It +```rust +# extern crate bevy; +# extern crate bevy_mod_scripting; +# use bevy::{asset::Assets, prelude::*}; +# use bevy_mod_scripting::prelude::*; + +fn add_script(mut script_assets: ResMut>, mut commands: Commands) { + let content: String = "x = 0".into(); + let mut script = ScriptAsset::from(content); + script.language = Language::Lua; + let handle = script_assets.add(script); + commands.spawn(ScriptComponent(vec![handle])); } ``` - -In practice you will likely store this handle in a resource or component, when your load all the scripts necessary for your application. ## Unloading -Scripts are automatically unloaded when the asset is dropped. This means that if you have a handle to a script and it goes out of scope, the script will be unloaded. - - -This will delete references to the script and remove any internal handles to the asset. You will also need to clean up any handles to the asset you hold in your application in order for the asset to be unloaded. - +Scripts are loaded into a context. When the `ScriptAsset` is unloaded, the context may remain. To delete the context do the following: + +```rust +# use bevy::prelude::*; +# use bevy_mod_scripting::prelude::*; +/// To delete all Lua scripts before the app exits, one would: +/// `app.add_systems(Update, delete_all_scripts::.run_if(on_event::))` +fn delete_all_scripts(script_components: Query<(Entity, &ScriptComponent)>) { + for (id, script_component) in &script_components { + for handle in &script_component.0 { + commands.entity(id).queue(DeleteScript::

::new(handle.id())); + } + commands.entity(id).despawn(); + } +} +``` ## Hot-loading scripts -To enable hot-loading of assets, you need to enable the necessary bevy features as normal [see the bevy cheatbook for instructions](https://bevy-cheatbook.github.io/assets/hot-reload.html). +To enable hot-loading of assets, you need to enable the necessary Bevy features as normal [see the bevy cheatbook for instructions](https://bevy-cheatbook.github.io/assets/hot-reload.html). Assuming that hot-reloading is enabled for your app, any changes to script assets will automatically be picked up and the scripts re-loaded. @@ -35,18 +76,25 @@ Normally the set of supported extensions is pre-decided by each language plugin. I.e. Lua supports ".lua" extensions and Rhai supports ".rhai" extensions. -Scripts are mapped to the corresponding language plugin based on these and so it's important to use them correctly. +Scripts are mapped to the corresponding language plugin based on these and so it is important to use them correctly. -If you would like to add more extensions you need to populate them via `app.add_supported_script_extensions`. +If you would like to add more extensions, you need to populate them via `app.add_supported_script_extensions`. +```rust,ignore + app.add_supported_script_extensions(&[".pua"], Language::Lua); +``` ## Advanced -Normally not necessary, but knowing these exist could be useful for more advanced use cases. +Normally not necessary but knowing these exist could be useful for more advanced use cases. ### Manually (re)loading scripts In order to manually re-load or load a script you can issue the `CreateOrUpdateScript` command: ```rust,ignore -CreateOrUpdateScript::::new("my_script.lua".into(), "print(\"hello world from new script body\")".into(), asset_handle) +# use bevy::prelude::*; +# use bevy_mod_scripting::prelude::*; +let create_or_update = CreateOrUpdateScript::::new(script_handle) + .with_content("print(\"hello world from new script body\")"); +commands.queue(create_or_update); ``` replace `LuaScriptingPlugin` with the scripting plugin you are using. @@ -55,10 +103,10 @@ replace `LuaScriptingPlugin` with the scripting plugin you are using. In order to delete a previously loaded script, you will need to issue a `DeleteScript` command like so: ```rust,ignore -DeleteScript::::new("my_script.lua".into()) +commands.queue(DeleteScript::::new(script_handle)); ``` -replace `LuaScriptingPlugin` with the scripting plugin you are using. +Replace `LuaScriptingPlugin` with the scripting plugin you are using. ### Loading/Unloading timeframe -Scripts asset events are processed within the same frame they are issued. This means the moment an asset is loaded, it should be loaded and ready to use in the `Update` schedule. Similarly, the moment an asset is deleted, it should be unloaded and no longer usable in the `Update` schedule. \ No newline at end of file +Script evaluation---initial script evalution and script reloading---happens in the `PreUpdate` schedule, in the `ScriptingSystemSet::ScriptCommandDispatch` system set. diff --git a/docs/src/Summary/script-id-mapping.md b/docs/src/Summary/script-id-mapping.md deleted file mode 100644 index fe8027b0f7..0000000000 --- a/docs/src/Summary/script-id-mapping.md +++ /dev/null @@ -1,11 +0,0 @@ -# Script ID mapping - -Every script is currently identified by a unique ID. - -ID's are derived from the script asset path for scripts loaded via the asset system. - -By default this is an identity mapping, but you can override this by modifying the `AssetPathToScriptIdMapper` inside the `ScriptAssetSettings` resource before loading the script. - -

\ No newline at end of file diff --git a/docs/src/Summary/sharing-contexts-between-scripts.md b/docs/src/Summary/sharing-contexts-between-scripts.md deleted file mode 100644 index e0fb9951ba..0000000000 --- a/docs/src/Summary/sharing-contexts-between-scripts.md +++ /dev/null @@ -1,24 +0,0 @@ -# Shared Contexts - -By default BMS will create an individual script context, or sandbox, for each script that is run. This means that each script will have its own set of global variables and functions that are isolated from other scripts. However, sometimes this might not be desirable, if you aren't worried about scripts interfering with each other, or if you want to easilly share data between scripts. In these cases, you can use shared contexts. - -## Enabling Shared Contexts - -You can enable shared contexts by configuring the relevant scripting plugin like so: -```rust,ignore -let mut plugin = LuaScriptingPlugin::default().enable_context_sharing(); - -app.add_plugins(plugin); -``` - -## Context Loading Settings - -All context loading settings are stored in a separate resource per scripting plugin namely: `ContextLoadingSettings`. - -The settings are as follows: -- `loader` - the load and unload strategy for contexts. Each scripting plugin will have a load and unload function which is hooked up through here -- `assigner` - the strategy for assigning/unassigning contexts to scripts. This is used to determine how to assign a context to a script when it is run, and what to do with the context when the script is finished. -- `context_initializers` - stores all context initializers for the plugin -- `context_pre_handling_initializers` - stores all context pre-handling initializers for the plugin - -More advanced applications might want to customize these settings to suit their needs. \ No newline at end of file diff --git a/examples/game_of_life.rs b/examples/game_of_life.rs index 325b4a4db9..fafe3114e0 100644 --- a/examples/game_of_life.rs +++ b/examples/game_of_life.rs @@ -15,22 +15,7 @@ use bevy::{ window::{PrimaryWindow, WindowResized}, }; use bevy_console::{make_layer, AddConsoleCommand, ConsoleCommand, ConsoleOpen, ConsolePlugin}; -use bevy_mod_scripting::{BMSPlugin, ScriptFunctionsPlugin}; -use bevy_mod_scripting_core::{ - asset::ScriptAsset, - bindings::{ - function::namespace::{GlobalNamespace, NamespaceBuilder}, - script_value::ScriptValue, - AllocatorDiagnosticPlugin, CoreScriptGlobalsPlugin, - }, - callback_labels, - commands::AddStaticScript, - event::ScriptCallbackEvent, - handler::event_handler, - script::ScriptComponent, -}; -use bevy_mod_scripting_lua::LuaScriptingPlugin; -use bevy_mod_scripting_rhai::RhaiScriptingPlugin; +use bevy_mod_scripting::{core::bindings::AllocatorDiagnosticPlugin, prelude::*}; use clap::Parser; // CONSOLE SETUP @@ -54,7 +39,10 @@ fn console_app(app: &mut App) -> &mut App { fn run_script_cmd( mut log: ConsoleCommand, mut commands: Commands, - mut loaded_scripts: ResMut, + asset_server: Res, + script_comps: Query<(Entity, &ScriptComponent)>, + mut static_lua_scripts: Local>, + mut static_rhai_scripts: Local>, ) { if let Some(Ok(command)) = log.take() { match command { @@ -63,32 +51,61 @@ fn run_script_cmd( use_static_script, } => { // create an entity with the script component - bevy::log::info!( - "Starting game of life spawning entity with the game_of_life.{} script", - language - ); + bevy::log::info!("Using game of life script game_of_life.{}", language); let script_path = format!("scripts/game_of_life.{}", language); if !use_static_script { bevy::log::info!("Spawning an entity with ScriptComponent"); - commands.spawn(ScriptComponent::new(vec![script_path])); + commands.spawn(ScriptComponent::new(vec![asset_server.load(script_path)])); } else { bevy::log::info!("Using static script instead of spawning an entity"); - commands.queue(AddStaticScript::new(script_path)) + let handle = asset_server.load(script_path); + if language == "lua" { + static_lua_scripts.push(handle.id()); + } else { + static_rhai_scripts.push(handle.id()); + } + commands.queue(AddStaticScript::new(handle)) } } GameOfLifeCommand::Stop => { // we can simply drop the handle, or manually delete, I'll just drop the handle bevy::log::info!("Stopping game of life by dropping the handles to all scripts"); + for (id, script_component) in &script_comps { + for script in &script_component.0 { + match script + .path() + .and_then(|p| p.get_full_extension()) + .unwrap_or_default() + .as_str() + { + "lua" => { + commands + .entity(id) + .queue(DeleteScript::::new(script.id())); + } + "rhai" => { + #[cfg(feature = "rhai")] + commands + .entity(id) + .queue(DeleteScript::::new(script.id())); + } + ext => { + warn!("Can't delete script with extension {ext:?}."); + } + } + } + commands.entity(id).despawn(); + } - // I am not mapping the handles to the script names, so I'll just clear the entire list - loaded_scripts.0.clear(); + for script in static_lua_scripts.drain(..) { + commands.queue(DeleteScript::::new(script)); + } - // you could also do - // commands.queue(DeleteScript::::new( - // "scripts/game_of_life.lua".into(), - // )); - // as this will retain your script asset and handle + #[cfg(feature = "rhai")] + for script in static_rhai_scripts.drain(..) { + commands.queue(DeleteScript::::new(script)); + } } } } @@ -115,12 +132,12 @@ pub enum GameOfLifeCommand { // ------------- GAME OF LIFE fn game_of_life_app(app: &mut App) -> &mut App { app.insert_resource(Time::::from_seconds(UPDATE_FREQUENCY.into())) + // .add_plugins(BMSPlugin.set(LuaScriptingPlugin::default().enable_context_sharing())) .add_plugins(BMSPlugin) .register_type::() .register_type::() .init_resource::() - .init_resource::() - .add_systems(Startup, (init_game_of_life_state, load_script_assets)) + .add_systems(Startup, init_game_of_life_state) .add_systems(Update, (sync_window_size, send_on_click)) .add_systems( FixedUpdate, @@ -129,8 +146,10 @@ fn game_of_life_app(app: &mut App) -> &mut App { send_on_update.after(update_rendered_state), ( event_handler::, + #[cfg(feature = "rhai")] event_handler::, event_handler::, + #[cfg(feature = "rhai")] event_handler::, ) .after(send_on_update), @@ -145,9 +164,6 @@ pub struct LifeState { pub cells: Vec, } -#[derive(Debug, Resource, Default)] -pub struct LoadedScripts(pub Vec>); - #[derive(Reflect, Resource)] #[reflect(Resource)] pub struct Settings { @@ -170,17 +186,6 @@ impl Default for Settings { } } -/// Prepares any scripts by loading them and storing the handles. -pub fn load_script_assets( - asset_server: Res, - mut loaded_scripts: ResMut, -) { - loaded_scripts.0.extend(vec![ - asset_server.load("scripts/game_of_life.lua"), - asset_server.load("scripts/game_of_life.rhai"), - ]); -} - pub fn register_script_functions(app: &mut App) -> &mut App { let world = app.world_mut(); NamespaceBuilder::::new_unregistered(world) diff --git a/examples/run-script.rs b/examples/run-script.rs new file mode 100644 index 0000000000..63098a69f6 --- /dev/null +++ b/examples/run-script.rs @@ -0,0 +1,66 @@ +use bevy::prelude::*; +use bevy_mod_scripting::prelude::*; + +fn main() { + // Collect command-line arguments, skipping the program name. + let mut args: Vec = std::env::args().skip(1).collect(); + + if args.is_empty() { + eprintln!("Usage: run-script ..."); + std::process::exit(1); + } + + App::new() + .add_plugins(DefaultPlugins) + .add_plugins(BMSPlugin) + .add_plugins(add_logging) + .add_systems( + Startup, + move |asset_server: Res, mut commands: Commands| { + let mut handles = vec![]; + for file_name in args.drain(..) { + handles.push(asset_server.load::(file_name)); + } + commands.spawn(ScriptComponent(handles)); + }, + ) + .add_systems(Update, info_on_asset_event::()) + .run(); +} + +fn add_logging(app: &mut App) { + let world = app.world_mut(); + NamespaceBuilder::::new_unregistered(world) + .register("info", |s: String| { + bevy::log::info!("{}", s); + }) + .register("warn", |s: String| { + bevy::log::warn!("{}", s); + }) + .register("error", |s: String| { + bevy::log::error!("{}", s); + }) + .register("debug", |s: String| { + bevy::log::debug!("{}", s); + }) + .register("trace", |s: String| { + bevy::log::trace!("{}", s); + }); +} + +pub fn info_on_asset_event() -> impl FnMut(EventReader>) { + // The events need to be consumed, so that there are no false positives on subsequent + // calls of the run condition. Simply checking `is_empty` would not be enough. + // PERF: note that `count` is efficient (not actually looping/iterating), + // due to Bevy having a specialized implementation for events. + move |mut reader: EventReader>| { + for event in reader.read() { + match event { + AssetEvent::Modified { .. } => (), + _ => { + info!("ASSET EVENT {:?}", &event); + } + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs index a880540a76..421a3bdb81 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,8 @@ pub mod core { pub use bevy_mod_scripting_core::*; } +pub mod prelude; + #[cfg(feature = "lua")] pub mod lua { pub use bevy_mod_scripting_lua::*; diff --git a/src/prelude.rs b/src/prelude.rs new file mode 100644 index 0000000000..a9eb6990c3 --- /dev/null +++ b/src/prelude.rs @@ -0,0 +1,20 @@ +pub use crate::{BMSPlugin, ScriptFunctionsPlugin}; +pub use bevy_mod_scripting_core::{ + asset::{Language, ScriptAsset}, + bindings::{ + function::namespace::{GlobalNamespace, NamespaceBuilder}, + script_value::ScriptValue, + CoreScriptGlobalsPlugin, + }, + callback_labels, + commands::{AddStaticScript, DeleteScript}, + event::ScriptCallbackEvent, + handler::event_handler, + script::{ScriptComponent, ScriptId}, + ConfigureScriptAssetSettings, ConfigureScriptPlugin, IntoScriptPluginParams, +}; + +#[cfg(feature = "lua")] +pub use bevy_mod_scripting_lua::LuaScriptingPlugin; +#[cfg(feature = "rhai")] +pub use bevy_mod_scripting_rhai::RhaiScriptingPlugin; diff --git a/tests/script_tests.rs b/tests/script_tests.rs index cbb451abd6..b917b6a938 100644 --- a/tests/script_tests.rs +++ b/tests/script_tests.rs @@ -3,9 +3,7 @@ use std::path::PathBuf; use libtest_mimic::{Arguments, Failed, Trial}; -use script_integration_test_harness::{ - execute_lua_integration_test, execute_rhai_integration_test, -}; +use script_integration_test_harness::execute_lua_integration_test; use test_utils::{discover_all_tests, Test, TestKind}; @@ -16,11 +14,22 @@ trait TestExecutor { impl TestExecutor for Test { fn execute(self) -> Result<(), Failed> { - println!("Running test: {:?}", self.path); - match self.kind { - TestKind::Lua => execute_lua_integration_test(&self.path.to_string_lossy())?, - TestKind::Rhai => execute_rhai_integration_test(&self.path.to_string_lossy())?, + TestKind::Lua => { + println!("Running test: {:?}", self.path); + execute_lua_integration_test(&self.path.to_string_lossy())? + } + TestKind::Rhai => { + if cfg!(feature = "rhai") { + println!("Running test: {:?}", self.path); + #[cfg(feature = "rhai")] + script_integration_test_harness::execute_rhai_integration_test( + &self.path.to_string_lossy(), + )? + } else { + println!("Skipping test: {:?}", self.path); + } + } } Ok(())