diff --git a/Cargo.toml b/Cargo.toml index 61bf0bfe3f..cb26b56ccc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -160,7 +160,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/crates/bevy_mod_scripting_core/Cargo.toml b/crates/bevy_mod_scripting_core/Cargo.toml index b94c1a88a3..df4cb68231 100644 --- a/crates/bevy_mod_scripting_core/Cargo.toml +++ b/crates/bevy_mod_scripting_core/Cargo.toml @@ -42,6 +42,7 @@ fixedbitset = "0.5" petgraph = "0.6" bevy_mod_debugdump = "0.12" bevy_system_reflection = { path = "../bevy_system_reflection", version = "0.1.1" } +serde = { version = "1.0", features = ["derive"] } [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..29b4f2dae6 100644 --- a/crates/bevy_mod_scripting_core/src/asset.rs +++ b/crates/bevy_mod_scripting_core/src/asset.rs @@ -1,27 +1,30 @@ //! Systems and resources for handling script assets and events use crate::{ + StaticScripts, + ScriptComponent, commands::{CreateOrUpdateScript, DeleteScript}, error::ScriptError, - script::ScriptId, + script::{DisplayProxy, ScriptId, Domain, ScriptDomain}, IntoScriptPluginParams, ScriptingSystemSet, }; use bevy::{ app::{App, PreUpdate}, - asset::{Asset, AssetEvent, AssetId, AssetLoader, AssetPath, Assets}, + asset::{Asset, AssetEvent, AssetId, AssetLoader, AssetPath, Assets, LoadState}, ecs::system::Resource, log::{debug, info, trace, warn}, prelude::{ Commands, Event, EventReader, EventWriter, IntoSystemConfigs, IntoSystemSetConfigs, Res, - ResMut, + ResMut, Added, Query, Local, Handle, AssetServer, Entity, }, reflect::TypePath, utils::hashbrown::HashMap, }; -use std::borrow::Cow; +use std::{borrow::Cow, collections::VecDeque}; +use serde::{Deserialize, Serialize}; /// 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,16 +55,25 @@ 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(), + } + } } -#[derive(Event, Debug, Clone)] -pub(crate) enum ScriptAssetEvent { - Added(ScriptMetadata), - Removed(ScriptMetadata), - Modified(ScriptMetadata), +/// 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)] @@ -77,14 +89,14 @@ pub struct ScriptAssetLoader { 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 +107,22 @@ impl AssetLoader for ScriptAssetLoader { if let Some(processor) = &self.preprocessor { processor(&mut content)?; } + let language = settings + .language + .clone() + .unwrap_or_else(|| + match load_context.path().extension().and_then(|e| e.to_str()).unwrap_or_default() { + "lua" => Language::Lua, + "rhai" => Language::Rhai, + "rn" => Language::Rune, + x => { + warn!("Unknown language for {:?}", load_context.path().display()); + Language::Unknown + } + }); let asset = ScriptAsset { content: content.into_boxed_slice(), - asset_path: load_context.asset_path().to_owned(), + language, }; Ok(asset) } @@ -107,230 +132,113 @@ 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"], - } - } -} - -/// 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) - } - - /// Checks if the store contains a metadata entry - pub fn contains(&self, id: AssetId) -> bool { - self.map.contains_key(&id) - } -} - -/// Converts incoming asset events, into internal script asset events, also loads and inserts metadata for newly added scripts +/// Listens to [`AssetEvent`] events and dispatches [`CreateOrUpdateScript`] and [`DeleteScript`] commands accordingly. +/// +/// Allows for hot-reloading of scripts. #[profiling::function] -pub(crate) fn dispatch_script_asset_events( +pub(crate) fn sync_script_data( mut events: EventReader>, - mut script_asset_events: EventWriter, - assets: Res>, - mut metadata_store: ResMut, - settings: Res, + script_assets: Res>, + mut static_scripts: ResMut, + asset_server: Res, + mut commands: Commands, ) { for event in events.read() { + + trace!("{}: Received script asset 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); + // emitted when a new script asset is loaded for the first time + AssetEvent::LoadedWithDependencies { id } | AssetEvent::Added { id } | AssetEvent::Modified{ id } => { + if let Some(asset) = script_assets.get(*id) { + if asset.language != P::LANGUAGE { + match asset_server.get_path(*id) { + Some(path) => { + trace!( + "{}: Script path {} is for a different language than this sync system. Skipping.", + P::LANGUAGE, + path); + } + None => { + trace!( + "{}: Script id {} is for a different language than this sync system. Skipping.", + P::LANGUAGE, + id); + } } - 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); + continue; + } + + if static_scripts.iter().any(|handle| handle.id() == *id) { + info!("{}: Loading static script: {:?}", P::LANGUAGE, id); + commands.queue(CreateOrUpdateScript::

::new( + Handle::Weak(*id), + asset.content.clone(), // Cloning seems bad! + None, // No domain for static scripts. + )); } } } - 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); - } - } - 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); + AssetEvent::Removed{ id } => { + if static_scripts.remove(id) { + info!("{}: Removing static script {:?}", P::LANGUAGE, id); } } - _ => {} - } - } -} - -/// 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, -) { - 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); + AssetEvent::Unused { id } => { } - } + }; } } -/// Listens to [`ScriptAssetEvent`] events and dispatches [`CreateOrUpdateScript`] and [`DeleteScript`] commands accordingly. -/// -/// Allows for hot-reloading of scripts. -#[profiling::function] -pub(crate) fn sync_script_data( - mut events: EventReader, +pub(crate) fn eval_script( + script_comps: Query<(Entity, &ScriptComponent, Option<&ScriptDomain>), Added>, + mut script_queue: Local, Option)>>, script_assets: Res>, + asset_server: Res, mut commands: Commands, -) { - 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; + ) { + for (id, script_comp, domain_maybe) in &script_comps { + for handle in &script_comp.0 { + script_queue.push_back((id, handle.clone_weak(), domain_maybe.map(|x| x.0.clone()))); } - - 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(), + } + while ! script_queue.is_empty() { + let script_ready = script_queue.front().map(|(_, script_id, _)| match asset_server.load_state(&*script_id) { + LoadState::Failed(e) => { + warn!("Failed to load script {}", script_id.display()); + true + } + LoadState::Loaded => true, + _ => false + }).unwrap_or(false); + if ! script_ready { + break; + } + // NOTE: Maybe once pop_front_if is stabalized. + // if let Some(script_id) = script_queue.pop_front_if(|script_id| match asset_server.load_state(script_id) { + // LoadState::Failed(e) => { + // warn!("Failed to load script {}", &script_id); + // true + // } + // LoadState::Loaded => true, + // _ => false + // }) { + if let Some((id, script_id, domain_maybe)) = script_queue.pop_front() { + if let Some(asset) = script_assets.get(&script_id) { + if asset.language == P::LANGUAGE { + commands.entity(id).queue(CreateOrUpdateScript::

::new( + script_id, asset.content.clone(), - Some(script_assets.reserve_handle().clone_weak()), + domain_maybe )); } + } else { + // This is probably a load failure. What to do? We've already + // provided a warning on failure. Doing nothing is fine then we + // process the next one. } - ScriptAssetEvent::Removed(_) => { - info!("{}: Deleting Script: {:?}", P::LANGUAGE, metadata.script_id,); - commands.queue(DeleteScript::

::new(metadata.script_id.clone())); - } - }; + } else { + break; + } } } @@ -339,13 +247,14 @@ pub(crate) fn sync_script_data( pub(crate) fn configure_asset_systems(app: &mut App) -> &mut App { // these should be in the same set as bevy's asset systems // currently this is in the PreUpdate set - app.add_systems( - PreUpdate, - ( - dispatch_script_asset_events.in_set(ScriptingSystemSet::ScriptAssetDispatch), - remove_script_metadata.in_set(ScriptingSystemSet::ScriptMetadataRemoval), - ), - ) + app + // .add_systems( + // PreUpdate, + // ( + // dispatch_script_asset_events.in_set(ScriptingSystemSet::ScriptAssetDispatch), + // remove_script_metadata.in_set(ScriptingSystemSet::ScriptMetadataRemoval), + // ), + // ) .configure_sets( PreUpdate, ( @@ -354,10 +263,7 @@ pub(crate) fn configure_asset_systems(app: &mut App) -> &mut App { .after(ScriptingSystemSet::ScriptAssetDispatch) .before(ScriptingSystemSet::ScriptMetadataRemoval), ), - ) - .init_resource::() - .init_resource::() - .add_event::(); + ); app } @@ -369,7 +275,7 @@ pub(crate) fn configure_asset_systems_for_plugin( ) -> &mut App { app.add_systems( PreUpdate, - sync_script_data::

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

, sync_script_data::

).in_set(ScriptingSystemSet::ScriptCommandDispatch), ); app } 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..153a69de31 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/script_system.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/script_system.rs @@ -10,6 +10,7 @@ use super::{ WorldGuard, }; use crate::{ + ScriptAsset, bindings::pretty_print::DisplayWithWorld, context::ContextLoadingSettings, error::{InteropError, ScriptError}, @@ -17,10 +18,12 @@ use crate::{ extractors::get_all_access_ids, handler::CallbackSettings, runtime::RuntimeContainer, - script::{ScriptId, Scripts}, + script::{ScriptId, ScriptContextProvider, ScriptContext, Domain}, IntoScriptPluginParams, }; use bevy::{ + asset::Handle, + prelude::{Query, AssetServer}, ecs::{ archetype::{ArchetypeComponentId, ArchetypeGeneration}, component::{ComponentId, Tick}, @@ -78,12 +81,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 +99,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 +201,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 +215,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 +227,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 +241,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 +261,15 @@ impl<'w, P: IntoScriptPluginParams> DynamicHandlerContext<'w, P> { pub fn call_dynamic_label( &self, label: &CallbackLabel, - script_id: &ScriptId, + script_id: &Handle, entity: Entity, + domain: &Option, 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) = self.script_context.get(Some(entity), &script_id.id(), domain) else { + return Err(InteropError::missing_context(script_id.clone()).into()); }; // call the script @@ -272,7 +279,7 @@ 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, @@ -339,10 +346,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 +372,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 +429,10 @@ impl System for DynamicScriptSystem

{ }; let mut payload = Vec::with_capacity(state.system_params.len()); + let script_id = { + 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() }; @@ -489,8 +502,10 @@ impl System for DynamicScriptSystem

{ let result = handler_ctxt.call_dynamic_label( &state.callback_label, - &self.target_script, + // &self.target_script, + &script_id, Entity::from_raw(0), + &self.domain, payload, guard.clone(), ); diff --git a/crates/bevy_mod_scripting_core/src/commands.rs b/crates/bevy_mod_scripting_core/src/commands.rs index 95ea651b03..c8f0bb99b5 100644 --- a/crates/bevy_mod_scripting_core/src/commands.rs +++ b/crates/bevy_mod_scripting_core/src/commands.rs @@ -1,6 +1,7 @@ //! Commands for creating, updating and deleting scripts use crate::{ + AssetId, asset::ScriptAsset, bindings::{ScriptValue, WorldGuard}, context::ContextBuilder, @@ -9,28 +10,30 @@ use crate::{ CallbackLabel, IntoCallbackLabel, OnScriptLoaded, OnScriptUnloaded, ScriptCallbackResponseEvent, }, - extractors::{with_handler_system_state, HandlerContext}, + extractors::{with_handler_system_state, HandlerContext, WithWorldGuard}, handler::{handle_script_errors, send_callback_response}, - script::{Script, ScriptId, Scripts, StaticScripts}, + script::{ScriptId, StaticScripts, DisplayProxy, ScriptContextProvider, Domain}, IntoScriptPluginParams, }; -use bevy::{asset::Handle, ecs::entity::Entity, log::debug, prelude::Command}; +use bevy::{asset::{Assets, Handle}, ecs::entity::Entity, log::{warn, debug}, prelude::{EntityCommand, Command}}; use parking_lot::Mutex; use std::{marker::PhantomData, sync::Arc}; /// Deletes a script with the given ID pub struct DeleteScript { /// The ID of the script to delete - pub id: ScriptId, + pub id: AssetId, + domain: Option, /// 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(id: AssetId, domain: Option) -> Self { Self { id, + domain, _ph: PhantomData, } } @@ -40,16 +43,17 @@ impl Command for DeleteScript

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

::new( - self.id.clone(), + Handle::Weak(self.id.clone()), Entity::from_raw(0), + self.domain, OnScriptUnloaded::into_callback_label(), vec![], false, ) .apply(world); - let mut scripts = world.get_resource_or_init::>(); - if scripts.remove(self.id.clone()) { + let mut scripts = world.get_resource_or_init::(); + if scripts.remove(&self.id) { debug!("Deleted script with id: {}", self.id); } else { bevy::log::error!( @@ -64,9 +68,10 @@ impl Command for DeleteScript

{ /// /// 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, + id: Handle, + // It feels like we're using a Box, which requires a clone merely to satisfy the Command trait. content: Box<[u8]>, - asset: Option>, + domain: 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, } @@ -74,57 +79,54 @@ pub struct CreateOrUpdateScript { #[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 { + pub fn new(id: Handle, content: Box<[u8]>, domain: Option) -> Self { Self { id, content, - asset, + domain, _ph: std::marker::PhantomData, } } fn reload_context( - &self, + id: &Handle, + content: &[u8], + context: &mut P::C, guard: WorldGuard, handler_ctxt: &HandlerContext

, ) -> Result<(), ScriptError> { - 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(), - ) - } - }; + // let mut context = script + // .contexts + // .get_mut(&id.id()) + // .ok_or_else(|| InteropError::invariant("Tried to reload script which doesn't have a context"))?; // reload context - let mut context = existing_script.context.lock(); + // let mut context = context.lock(); (ContextBuilder::

::reload)( handler_ctxt.context_loading_settings.loader.reload, - &self.id, - &self.content, - &mut context, + &id, + 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, + id: &Handle, + content: &[u8], guard: WorldGuard, handler_ctxt: &mut HandlerContext

, - ) -> Result<(), ScriptError> { + ) -> Result { let context = (ContextBuilder::

::load)( handler_ctxt.context_loading_settings.loader.load, - &self.id, - &self.content, + &id, + content, &handler_ctxt.context_loading_settings.context_initializers, &handler_ctxt .context_loading_settings @@ -133,125 +135,136 @@ impl CreateOrUpdateScript

{ &handler_ctxt.runtime_container.runtime, )?; - let context = Arc::new(Mutex::new(context)); + // Ok(Arc::new(Mutex::new(context))) + Ok(context) + } - handler_ctxt.scripts.scripts.insert( - self.id.clone(), - Script { - id: self.id.clone(), - asset: self.asset.clone(), - context, - }, - ); - Ok(()) + pub(crate) fn create_or_update_script( + entity: Option, + id: &Handle, + content: &[u8], + domain: &Option, + guard: WorldGuard, + handler_ctxt: &mut HandlerContext

) -> Result<(), ScriptError> { + let assignment_strategy = handler_ctxt.context_loading_settings.assignment_strategy; + + let phrase; + let result = match handler_ctxt.script_context.get(entity, &id.id(), domain) { + Some(context) => { + bevy::log::debug!("{}: reloading script {}", P::LANGUAGE, id.display()); + let mut lcontext = context.lock(); + phrase = "reloading"; + Self::reload_context(id, content, &mut lcontext, guard.clone(), handler_ctxt) + .map(|_| None) + } + None => { + bevy::log::debug!("{}: loading script {}", P::LANGUAGE, id.display()); + phrase = "loading"; + Self::load_context(id, content, guard.clone(), handler_ctxt) + .map(Some) + } + }; + + match result { + Ok(maybe_context) => { + if let Some(context) = maybe_context { + if handler_ctxt.script_context.insert(entity, &id.id(), &None, context).is_err() { + warn!("Unable to insert script context for entity {:?} with script {}.", entity, id.display()); + } + } + + bevy::log::debug!( + "{}: script {} successfully created or updated", + P::LANGUAGE, + id.display() + ); + Ok(())// none until individual context support added. + } + Err(err) => { + handle_script_errors( + guard, + vec![err.clone() + .with_script(id.display()) + .with_context(P::LANGUAGE) + .with_context(phrase)] + .into_iter(), + ); + Err(err) + } + } } + } + #[profiling::all_functions] impl Command for CreateOrUpdateScript

{ fn apply(self, world: &mut bevy::prelude::World) { - let success = with_handler_system_state( + // todo!() + let result = with_handler_system_state( world, |guard, handler_ctxt: &mut HandlerContext

| { - let is_new_script = !handler_ctxt.scripts.scripts.contains_key(&self.id); - - let assigned_shared_context = - match handler_ctxt.context_loading_settings.assignment_strategy { - crate::context::ContextAssignmentStrategy::Individual => None, - crate::context::ContextAssignmentStrategy::Global => { - let is_new_context = handler_ctxt.scripts.scripts.is_empty(); - if !is_new_context { - handler_ctxt - .scripts - .scripts - .values() - .next() - .map(|s| s.context.clone()) - } else { - None - } - } - }; - - debug!( - "{}: CreateOrUpdateScript command applying (script_id: {}, new context?: {}, new script?: {})", - P::LANGUAGE, - self.id, - assigned_shared_context.is_none(), - is_new_script - ); - - let result = match &assigned_shared_context { - Some(assigned_shared_context) => { - if is_new_script { - // this will happen when sharing contexts - // make a new script with the shared context - let script = Script { - id: self.id.clone(), - asset: self.asset.clone(), - context: assigned_shared_context.clone(), - }; - // it can potentially be loaded but without a successful script reload but that - // leaves us in an okay state - handler_ctxt.scripts.scripts.insert(self.id.clone(), script); - } - bevy::log::debug!("{}: reloading script with id: {}", P::LANGUAGE, self.id); - self.reload_context(guard.clone(), handler_ctxt) - } - None => { - bevy::log::debug!("{}: loading script with id: {}", P::LANGUAGE, self.id); - self.load_context(guard.clone(), handler_ctxt) - } - }; - - let phrase = if assigned_shared_context.is_some() { - "reloading" - } else { - "loading" - }; - - if let Err(err) = result { - handle_script_errors( - guard, - vec![err - .with_script(self.id.clone()) - .with_context(P::LANGUAGE) - .with_context(phrase)] - .into_iter(), - ); - return false; - } + Self::create_or_update_script(None, &self.id, &self.content, &self.domain, + guard, handler_ctxt) + }); - bevy::log::debug!( - "{}: script with id: {} successfully created or updated", - P::LANGUAGE, - self.id - ); + // immediately run command for callback, but only if loading went fine + match result { + Ok(_) => { + + RunScriptCallback::

::new( + self.id, + Entity::from_raw(0), + self.domain, + OnScriptLoaded::into_callback_label(), + vec![], + false, + ) + .apply(world) + } + Err(_) => () + } + } +} - true - }, - ); +#[profiling::all_functions] +impl EntityCommand for CreateOrUpdateScript

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

| { + Self::create_or_update_script(Some(entity), &self.id, &self.content, &self.domain, + guard, handler_ctxt) + }); // immediately run command for callback, but only if loading went fine - if success { - RunScriptCallback::

::new( - self.id, - Entity::from_raw(0), - OnScriptLoaded::into_callback_label(), - vec![], - false, - ) - .apply(world) + match result { + Ok(maybe_script) => { + + RunScriptCallback::

::new( + self.id, + entity, + self.domain, + OnScriptLoaded::into_callback_label(), + vec![], + false, + ) + .apply(world) + } + Err(_) => () } } } + /// 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, + pub id: Handle, /// The entity to use for the callback pub entity: Entity, + /// The domain if any + pub domain: Option, /// The callback to run pub callback: CallbackLabel, /// optional context passed down to errors @@ -267,8 +280,9 @@ pub struct RunScriptCallback { impl RunScriptCallback

{ /// Creates a new RunCallbackCommand with the given ID, callback and arguments pub fn new( - id: ScriptId, + id: Handle, entity: Entity, + domain: Option, callback: CallbackLabel, args: Vec, trigger_response: bool, @@ -276,6 +290,7 @@ impl RunScriptCallback

{ Self { id, entity, + domain, context: None, callback, args, @@ -294,19 +309,20 @@ impl RunScriptCallback

{ impl Command for RunScriptCallback

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

| { - 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.id - ); - return; - } + // if !handler_ctxt.is_script_fully_loaded(self.id.id()) { + // bevy::log::error!( + // "{}: Cannot apply callback command, as script {} does not exist. Ignoring.", + // P::LANGUAGE, + // self.id.display() + // ); + // return; + // } let result = handler_ctxt.call_dynamic_label( &self.callback, &self.id, self.entity, + &self.domain, self.args, guard.clone(), ); @@ -316,14 +332,14 @@ impl Command for RunScriptCallback

{ guard.clone(), ScriptCallbackResponseEvent::new( self.callback, - self.id.clone(), + self.id.id(), result.clone(), ), ); } if let Err(err) = result { - let mut error_with_context = err.with_script(self.id).with_context(P::LANGUAGE); + let mut error_with_context = err.with_script(self.id.display()).with_context(P::LANGUAGE); if let Some(ctxt) = self.context { error_with_context = error_with_context.with_context(ctxt); } @@ -337,12 +353,12 @@ impl Command for RunScriptCallback

{ /// 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() } } } @@ -357,12 +373,12 @@ impl Command for AddStaticScript { /// 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 } } } @@ -371,7 +387,7 @@ 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.id()); } } diff --git a/crates/bevy_mod_scripting_core/src/context.rs b/crates/bevy_mod_scripting_core/src/context.rs index dec993f7be..ea300662af 100644 --- a/crates/bevy_mod_scripting_core/src/context.rs +++ b/crates/bevy_mod_scripting_core/src/context.rs @@ -1,12 +1,16 @@ //! Traits and types for managing script contexts. use crate::{ + ScriptAsset, bindings::{ThreadWorldContainer, WorldContainer, WorldGuard}, error::{InteropError, ScriptError}, script::ScriptId, IntoScriptPluginParams, }; -use bevy::ecs::{entity::Entity, system::Resource}; +use bevy::{ + ecs::{entity::Entity, system::Resource}, + asset::Handle, +}; /// A trait that all script contexts must implement. /// @@ -17,11 +21,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(&Handle, &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(&Handle, Entity, &mut

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

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

], pre_handling_initializers: &[ContextPreHandlingInitializer

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

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

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

::C, context_initializers: &[ContextInitializer

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

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

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

], pre_handling_initializers: &[ContextPreHandlingInitializer

], @@ -121,7 +125,7 @@ impl ContextBuilder

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

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

], @@ -161,3 +165,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..c5d3ee4c85 100644 --- a/crates/bevy_mod_scripting_core/src/error.rs +++ b/crates/bevy_mod_scripting_core/src/error.rs @@ -1,6 +1,7 @@ //! Errors that can occur when interacting with the scripting system use crate::{ + ScriptAsset, bindings::{ access_map::{DisplayCodeLocation, ReflectAccessId}, function::namespace::Namespace, @@ -8,9 +9,10 @@ use crate::{ script_value::ScriptValue, ReflectBaseType, ReflectReference, }, - script::ScriptId, + script::{DisplayProxy, ScriptId}, }; use bevy::{ + asset::Handle, ecs::{ component::ComponentId, schedule::{ScheduleBuildError, ScheduleNotInitialized}, @@ -592,14 +594,14 @@ 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(script_id: impl Into>) -> Self { Self(Arc::new(InteropErrorInner::MissingContext { script_id: script_id.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, + script_id: Handle, }, /// Thrown when a schedule is missing from the registry. MissingSchedule { @@ -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() ) }; } @@ -1283,8 +1285,8 @@ macro_rules! argument_count_mismatch_msg { macro_rules! missing_context_for_callback { ($script_id:expr) => { format!( - "Missing context for script with id: {}. Was the script loaded?.", - $script_id + "Missing context for script {}. Was the script loaded?.", + $script_id.display() ) }; } diff --git a/crates/bevy_mod_scripting_core/src/event.rs b/crates/bevy_mod_scripting_core/src/event.rs index de9002ce85..5334b2fcfb 100644 --- a/crates/bevy_mod_scripting_core/src/event.rs +++ b/crates/bevy_mod_scripting_core/src/event.rs @@ -1,6 +1,6 @@ //! 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::{Domain, ScriptId}}; use bevy::{ecs::entity::Entity, prelude::Event, reflect::Reflect}; /// An error coming from a script @@ -122,6 +122,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), } diff --git a/crates/bevy_mod_scripting_core/src/extractors.rs b/crates/bevy_mod_scripting_core/src/extractors.rs index d105cca7b5..1058e5d9a6 100644 --- a/crates/bevy_mod_scripting_core/src/extractors.rs +++ b/crates/bevy_mod_scripting_core/src/extractors.rs @@ -4,18 +4,23 @@ #![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 bevy::{ + asset::{LoadState, Handle}, + ecs::{ + component::ComponentId, + entity::Entity, + event::{Event, EventCursor, EventIterator, Events}, + query::{Access, AccessConflicts}, + storage::SparseSetIndex, + system::{Local, Resource, SystemParam, SystemState}, + world::World, + }, + prelude::{AssetServer, Query, Res}, }; use fixedbitset::FixedBitSet; use crate::{ + ScriptAsset, bindings::{ access_map::ReflectAccessId, pretty_print::DisplayWithWorld, script_value::ScriptValue, WorldAccessGuard, WorldGuard, @@ -25,7 +30,7 @@ use crate::{ event::{CallbackLabel, IntoCallbackLabel}, handler::CallbackSettings, runtime::RuntimeContainer, - script::{ScriptId, Scripts, StaticScripts}, + script::{ScriptId, StaticScripts, ScriptContext, DisplayProxy, ScriptContextProvider, Domain}, IntoScriptPluginParams, }; @@ -47,16 +52,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> { @@ -133,6 +146,7 @@ impl EventReaderScope<'_, T> { } } + /// Context for systems which handle events for scripts #[derive(SystemParam)] pub struct HandlerContext<'s, P: IntoScriptPluginParams> { @@ -140,15 +154,15 @@ 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

>, } -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 +172,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 +193,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 +203,30 @@ 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, + script_id: &Handle, entity: Entity, + domain: &Option, 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) = self.script_context.get(Some(entity), &script_id.id(), domain) else { + return Err(InteropError::missing_context(script_id.clone()).into()); }; // call the script @@ -223,7 +236,7 @@ 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, @@ -244,12 +257,13 @@ 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, + script_id: &Handle, entity: Entity, + domain: &Option, 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(), script_id, entity, domain, payload, guard) } } diff --git a/crates/bevy_mod_scripting_core/src/handler.rs b/crates/bevy_mod_scripting_core/src/handler.rs index 34f259c4c7..40a0810ff0 100644 --- a/crates/bevy_mod_scripting_core/src/handler.rs +++ b/crates/bevy_mod_scripting_core/src/handler.rs @@ -1,5 +1,6 @@ //! Contains the logic for handling script callback events use crate::{ + ScriptAsset, bindings::{ pretty_print::DisplayWithWorld, script_value::ScriptValue, ThreadWorldContainer, WorldContainer, WorldGuard, @@ -11,10 +12,11 @@ use crate::{ ScriptErrorEvent, }, extractors::{HandlerContext, WithWorldGuard}, - script::{ScriptComponent, ScriptId}, + script::{ScriptComponent, ScriptId, ScriptDomain, Domain, ScriptContextProvider}, IntoScriptPluginParams, }; use bevy::{ + asset::Handle, ecs::{ entity::Entity, query::QueryState, @@ -23,13 +25,14 @@ 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, + script_id: &Handle, callback: &CallbackLabel, context: &mut

::C, pre_handling_initializers: &[ContextPreHandlingInitializer

], @@ -71,7 +74,7 @@ impl CallbackSettings

{ handler: HandlerFn

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

], @@ -129,7 +132,7 @@ 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,27 +141,27 @@ 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, Option>)>>, mut script_events: crate::extractors::EventReaderScope, mut handler_ctxt: WithWorldGuard>, ) { + const NO_ENTITY: Entity = Entity::from_raw(0); let (guard, handler_ctxt) = handler_ctxt.get_mut(); 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())) + .map(|(e, s, d)| (e, s.0.clone(), d.map(|x| x.0.clone()))) .chain( handler_ctxt .static_scripts .scripts .iter() - .map(|s| (Entity::from_raw(0), vec![s.clone()])), + .map(|s| (NO_ENTITY, vec![s.clone()], None)), ) .collect::>() }); @@ -166,7 +169,7 @@ pub(crate) fn event_handler_inner( let entity_and_static_scripts = match entity_and_static_scripts { Ok(entity_and_static_scripts) => entity_and_static_scripts, Err(e) => { - bevy::log::error!( + bevy::log::error_once!( "{}: Failed to query entities with scripts: {}", P::LANGUAGE, e.display_with_world(guard.clone()) @@ -175,12 +178,23 @@ pub(crate) fn event_handler_inner( } }; + // 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 event in events.into_iter().filter(|e| e.label == callback_label) { - for (entity, entity_scripts) in entity_and_static_scripts.iter() { + for (entity, entity_scripts, domain) 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 => + if *target_script_id != script_id.id() => { continue } @@ -192,13 +206,30 @@ pub(crate) fn event_handler_inner( { continue } - _ => {} + crate::event::Recipients::Domain(target_domain) + if domain.as_ref().map(|x| *x != *target_domain).unwrap_or(false) => + { + continue + } + crate::event::Recipients::All => (), + _ => () + + } + let context_hash = handler_ctxt.script_context.hash((*entity != NO_ENTITY).then_some(*entity), + &script_id.id(), + domain); + if let Some(hash) = context_hash { + if !called_contexts.insert(hash) { + // Only call a context once, not once per script. + continue; + } } let call_result = handler_ctxt.call_dynamic_label( &callback_label, - script_id, + &script_id, *entity, + domain, event.args.clone(), guard.clone(), ); @@ -208,7 +239,7 @@ pub(crate) fn event_handler_inner( guard.clone(), ScriptCallbackResponseEvent::new( callback_label.clone(), - script_id.clone(), + script_id.id(), call_result.clone(), ), ); @@ -219,11 +250,19 @@ pub(crate) fn event_handler_inner( 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 - ); + if let Some(path) = script_id.path() { + trace_once!( + "{}: Script path `{}` on entity `{:?}` is either still loading, doesn't exist, or is for another language, ignoring until the corresponding script is loaded.", + P::LANGUAGE, + path, entity + ); + } else { + trace_once!( + "{}: Script id `{}` 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.id(), entity + ); + } continue; } Some(InteropErrorInner::MissingContext { .. }) => { @@ -234,9 +273,15 @@ pub(crate) fn event_handler_inner( } _ => {} } - let e = e - .with_script(script_id.clone()) - .with_context(format!("Event handling for: Language: {}", P::LANGUAGE)); + let e = { + + if let Some(path) = script_id.path() { + e.with_script(path) + } else { + e + } + .with_context(format!("Event handling for: Language: {}", P::LANGUAGE)) + }; push_err_and_continue!(errors, Err(e)); } }; diff --git a/crates/bevy_mod_scripting_core/src/lib.rs b/crates/bevy_mod_scripting_core/src/lib.rs index 15faf31ff4..ce4f3eb0e4 100644 --- a/crates/bevy_mod_scripting_core/src/lib.rs +++ b/crates/bevy_mod_scripting_core/src/lib.rs @@ -5,7 +5,7 @@ use crate::event::ScriptErrorEvent; use asset::{ configure_asset_systems, configure_asset_systems_for_plugin, Language, ScriptAsset, - ScriptAssetLoader, ScriptAssetSettings, + ScriptAssetLoader, //ScriptAssetSettings, }; use bevy::prelude::*; use bindings::{ @@ -22,7 +22,7 @@ use error::ScriptError; use event::{ScriptCallbackEvent, ScriptCallbackResponseEvent}; use handler::{CallbackSettings, HandlerFn}; use runtime::{initialize_runtime, Runtime, RuntimeContainer, RuntimeInitializer, RuntimeSettings}; -use script::{ScriptComponent, ScriptId, Scripts, StaticScripts}; +use script::{ScriptComponent, ScriptId, StaticScripts, ScriptContext, EntityContext}; pub mod asset; pub mod bindings; @@ -129,7 +129,12 @@ impl Plugin for ScriptingPlugin

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

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

::per_entity() + }); register_script_plugin_systems::

(app); @@ -277,17 +282,17 @@ impl Plugin for BMSScriptingInfrastructurePlugin { fn finish(&self, app: &mut App) { // read extensions from asset settings - let asset_settings_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 - ); - + // let asset_settings_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 + // ); + let asset_settings_extensions = &["lua", "luau", "rhai", "rn"]; app.register_asset_loader(ScriptAssetLoader { extensions: asset_settings_extensions, preprocessor: None, @@ -353,21 +358,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 } @@ -394,24 +399,25 @@ impl ConfigureScriptAssetSettings for App { extensions: &[&'static str], language: Language, ) -> &mut Self { - let mut asset_settings = self - .world_mut() - .get_resource_or_init::(); + todo!() + // let mut asset_settings = self + // .world_mut() + // .get_resource_or_init::(); - let mut new_arr = Vec::from(asset_settings.supported_extensions); + // let mut new_arr = Vec::from(asset_settings.supported_extensions); - new_arr.extend(extensions); + // new_arr.extend(extensions); - let new_arr_static = Vec::leak(new_arr); + // 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()); - } + // asset_settings.supported_extensions = new_arr_static; + // for extension in extensions { + // asset_settings + // .extension_to_language_map + // .insert(*extension, language.clone()); + // } - self + // self } } 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/domain_context.rs b/crates/bevy_mod_scripting_core/src/script/domain_context.rs new file mode 100644 index 0000000000..69a5c56278 --- /dev/null +++ b/crates/bevy_mod_scripting_core/src/script/domain_context.rs @@ -0,0 +1,34 @@ +use super::*; + +/// Stores the script context by entity. +pub struct DomainContext(HashMap, Arc>>); + +impl Default for DomainContext

{ + fn default() -> Self { + Self(HashMap::new()) + } +} + +impl ScriptContextProvider

for DomainContext

{ + fn hash(&self, id: Option, script_id: &ScriptId, domain: &Option) -> Option { + domain.as_ref().map(|d| { + let mut hasher = DefaultHashBuilder::default().build_hasher(); + d.hash(&mut hasher); + hasher.finish() + }) + } + fn get(&self, id: Option, script_id: &ScriptId, domain: &Option) -> Option<&Arc>> { + domain.as_ref().and_then(|id| self.0.get(id)) + } + fn insert(&mut self, id: Option, script_id: &ScriptId, domain: &Option, context: P::C) -> Result<(), P::C> { + if let Some(id) = domain { + self.0.insert(id.clone(), Arc::new(Mutex::new(context))); + Ok(()) + } else { + Err(context) + } + } + fn contains(&self, id: Option, script_id: &ScriptId, domain: &Option) -> bool { + domain.as_ref().map(|id| self.0.contains_key(id)).unwrap_or(false) + } +} diff --git a/crates/bevy_mod_scripting_core/src/script/entity_context.rs b/crates/bevy_mod_scripting_core/src/script/entity_context.rs new file mode 100644 index 0000000000..0dde95ea1b --- /dev/null +++ b/crates/bevy_mod_scripting_core/src/script/entity_context.rs @@ -0,0 +1,34 @@ +use super::*; + +/// Stores the script context by entity. +pub struct EntityContext(HashMap>>); + +impl Default for EntityContext

{ + fn default() -> Self { + Self(HashMap::new()) + } +} + +impl ScriptContextProvider

for EntityContext

{ + fn hash(&self, id: Option, script_id: &ScriptId, domain: &Option) -> Option { + id.map(|id| { + let mut hasher = DefaultHashBuilder::default().build_hasher(); + id.hash(&mut hasher); + hasher.finish() + }) + } + fn get(&self, id: Option, script_id: &ScriptId, domain: &Option) -> Option<&Arc>> { + id.and_then(|id| self.0.get(&id)) + } + fn insert(&mut self, id: Option, script_id: &ScriptId, domain: &Option, context: P::C) -> Result<(), P::C> { + if let Some(id) = id { + self.0.insert(id, Arc::new(Mutex::new(context))); + Ok(()) + } else { + Err(context) + } + } + fn contains(&self, id: Option, script_id: &ScriptId, domain: &Option) -> bool { + id.map(|id| self.0.contains_key(&id)).unwrap_or(false) + } +} 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..bf1148066e --- /dev/null +++ b/crates/bevy_mod_scripting_core/src/script/mod.rs @@ -0,0 +1,154 @@ +//! Script related types, functions and components + +use crate::{asset::ScriptAsset, IntoScriptPluginParams}; +use bevy::prelude::{Component, ReflectComponent, Deref, DerefMut, Entity}; +use bevy::{asset::{Asset, AssetId, Handle}, ecs::system::Resource, reflect::Reflect, utils::HashSet}; +use parking_lot::Mutex; +use std::{borrow::Cow, collections::HashMap, ops::Deref, sync::Arc, fmt, hash::{Hash, Hasher, BuildHasher}}; +use bevy::utils::hashbrown::hash_map::DefaultHashBuilder; + +mod script_context; +mod shared_context; +mod entity_context; +mod domain_context; +mod scriptid_context; +pub use script_context::*; +pub use shared_context::*; +pub use entity_context::*; +pub use domain_context::*; +pub use scriptid_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: &ScriptId) -> bool { + 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: &ScriptId) -> bool { + self.scripts.iter().any(|handle| handle.id() == *script_id) + } + + /// 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/script_context.rs b/crates/bevy_mod_scripting_core/src/script/script_context.rs new file mode 100644 index 0000000000..c39f7d32a4 --- /dev/null +++ b/crates/bevy_mod_scripting_core/src/script/script_context.rs @@ -0,0 +1,124 @@ +use super::*; +use crate::{asset::ScriptAsset, IntoScriptPluginParams}; +use bevy::prelude::{Component, ReflectComponent, Deref, DerefMut, Entity}; +use bevy::{asset::{Asset, AssetId, Handle}, ecs::system::Resource, reflect::Reflect, utils::HashSet}; +use parking_lot::Mutex; +use std::{borrow::Cow, collections::HashMap, ops::Deref, sync::Arc, fmt}; + +/// A kind of catch all type for script context selection +/// +/// I believe this is what the original ScriptId was intended to be. +pub type Domain = Cow<'static, str>; + +/// A generic script context provider +pub trait ScriptContextProvider { + /// Get the context. + fn get(&self, id: Option, script_id: &ScriptId, domain: &Option) -> Option<&Arc>>; + /// Insert a context. + /// + /// If the context cannot be inserted, it is returned as an `Err`. + fn insert(&mut self, id: Option, script_id: &ScriptId, domain: &Option, context: P::C) -> Result<(), P::C>; + /// Returns true if there is a context. + fn contains(&self, id: Option, script_id: &ScriptId, domain: &Option) -> bool; + /// Hash for context. + /// + /// Useful for tracking what context will be returned by `get()` without + /// requiring that `P::C` impl `Hash` and cheaper too. + fn hash(&self, id: Option, script_id: &ScriptId, domain: &Option) -> Option; +} + +#[derive(Resource)] +/// Keeps track of script contexts +pub enum ScriptContext { + /// One shared script context + Shared(SharedContext

), + /// One script context per entity + /// + /// Stores context by entity with a shared context as a last resort when no + /// entity is provided. + /// + /// TODO: Should check for entity despawns and remove from this + /// EntityContext. + Entity(EntityContext

, SharedContext

), + /// Domain contexts or shared context + /// + /// Stores context by domain with a shared context as a last resort when no + /// domain is provided. + Domain(DomainContext

, SharedContext

), + /// A script context per script + ScriptId(ScriptIdContext

), + // NOTE: We could also have the following which would support domains; + // failing that entities; failing that a shared context. + // DomainEntity(DomainContext

, EntityContext

, SharedContext

), +} + +impl ScriptContext

{ + /// Use a shared script context + pub fn shared() -> Self { + Self::Shared(SharedContext::default()) + } + /// Domain contexts or a shared context + pub fn with_domains() -> Self { + Self::Domain(DomainContext::default(), SharedContext::default()) + } + /// Use one script ontext per entity + pub fn per_entity() -> Self { + Self::Entity(EntityContext::default(), SharedContext::default()) + } + /// Use one script ontext per entity + pub fn per_script() -> Self { + Self::ScriptId(ScriptIdContext::default()) + } +} + +impl Default for ScriptContext

{ + fn default() -> Self { + Self::shared() + } +} + +impl ScriptContextProvider

for ScriptContext

{ + fn get(&self, id: Option, script_id: &ScriptId, domain: &Option) -> Option<&Arc>> { + match self { + ScriptContext::Shared(a) => a.get(id, script_id, domain), + ScriptContext::ScriptId(a) => a.get(id, script_id, domain), + ScriptContext::Entity(a, b) => a.get(id, script_id, domain) + .or_else(|| b.get(id, script_id, domain)), + ScriptContext::Domain(a, b) => a.get(id, script_id, domain) + .or_else(|| b.get(id, script_id, domain)), + } + } + fn insert(&mut self, id: Option, script_id: &ScriptId, domain: &Option, context: P::C) -> Result<(), P::C> { + match self { + ScriptContext::Shared(a) => a.insert(id, script_id, domain, context), + ScriptContext::ScriptId(a) => a.insert(id, script_id, domain, context), + ScriptContext::Entity(a, b) => match a.insert(id, script_id, domain, context) { + Ok(()) => Ok(()), + Err(context) => b.insert(id, script_id, domain, context) + } + ScriptContext::Domain(a, b) => match a.insert(id, script_id, domain, context) { + Ok(()) => Ok(()), + Err(context) => b.insert(id, script_id, domain, context) + } + } + } + fn contains(&self, id: Option, script_id: &ScriptId, domain: &Option) -> bool { + match self { + ScriptContext::Shared(a) => a.contains(id, script_id, domain), + ScriptContext::ScriptId(a) => a.contains(id, script_id, domain), + ScriptContext::Entity(a, b) => a.contains(id, script_id, domain) || b.contains(id, script_id, domain), + ScriptContext::Domain(a, b) => a.contains(id, script_id, domain) || b.contains(id, script_id, domain), + } + } + fn hash(&self, id: Option, script_id: &ScriptId, domain: &Option) -> Option { + match self { + ScriptContext::Shared(a) => a.hash(id, script_id, domain), + ScriptContext::ScriptId(a) => a.hash(id, script_id, domain), + ScriptContext::Entity(a, b) => a.hash(id, script_id, domain) + .or_else(|| b.hash(id, script_id, domain)), + ScriptContext::Domain(a, b) => a.hash(id, script_id, domain) + .or_else(|| b.hash(id, script_id, domain)), + } + } +} + diff --git a/crates/bevy_mod_scripting_core/src/script/scriptid_context.rs b/crates/bevy_mod_scripting_core/src/script/scriptid_context.rs new file mode 100644 index 0000000000..f4d3bc67ed --- /dev/null +++ b/crates/bevy_mod_scripting_core/src/script/scriptid_context.rs @@ -0,0 +1,28 @@ +use super::*; + +/// Stores the script context by entity. +pub struct ScriptIdContext(HashMap>>); + +impl Default for ScriptIdContext

{ + fn default() -> Self { + Self(HashMap::new()) + } +} + +impl ScriptContextProvider

for ScriptIdContext

{ + fn hash(&self, id: Option, script_id: &ScriptId, domain: &Option) -> Option { + let mut hasher = DefaultHashBuilder::default().build_hasher(); + script_id.hash(&mut hasher); + Some(hasher.finish()) + } + fn get(&self, id: Option, script_id: &ScriptId, domain: &Option) -> Option<&Arc>> { + self.0.get(script_id) + } + fn insert(&mut self, id: Option, script_id: &ScriptId, domain: &Option, context: P::C) -> Result<(), P::C> { + self.0.insert(script_id.clone(), Arc::new(Mutex::new(context))); + Ok(()) + } + fn contains(&self, id: Option, script_id: &ScriptId, domain: &Option) -> bool { + self.0.contains_key(&script_id) + } +} diff --git a/crates/bevy_mod_scripting_core/src/script/shared_context.rs b/crates/bevy_mod_scripting_core/src/script/shared_context.rs new file mode 100644 index 0000000000..b47c099f7a --- /dev/null +++ b/crates/bevy_mod_scripting_core/src/script/shared_context.rs @@ -0,0 +1,27 @@ +use super::*; + +/// Contains the shared context. +pub struct SharedContext(pub Option>>); + +impl ScriptContextProvider

for SharedContext

{ + fn hash(&self, id: Option, script_id: &ScriptId, domain: &Option) -> Option { + self.0.is_some().then_some(0) + } + + fn get(&self, id: Option, script_id: &ScriptId, domain: &Option) -> Option<&Arc>> { + self.0.as_ref() + } + fn insert(&mut self, id: Option, script_id: &ScriptId, domain: &Option, context: P::C) -> Result<(), P::C> { + self.0 = Some(Arc::new(Mutex::new(context))); + Ok(()) + } + fn contains(&self, id: Option, script_id: &ScriptId, domain: &Option) -> bool { + self.0.is_some() + } +} + +impl Default for SharedContext

{ + fn default() -> Self { + Self(None) + } +} diff --git a/crates/bevy_mod_scripting_functions/src/core.rs b/crates/bevy_mod_scripting_functions/src/core.rs index 08963a02d3..de80404a2f 100644 --- a/crates/bevy_mod_scripting_functions/src/core.rs +++ b/crates/bevy_mod_scripting_functions/src/core.rs @@ -4,6 +4,7 @@ use std::{collections::HashMap, ops::Deref}; use bevy::prelude::*; use bevy_mod_scripting_core::{ + script::ScriptId, bindings::{ function::{ from::Union, namespace::GlobalNamespace, script_function::DynamicScriptFunctionMut, @@ -1261,8 +1262,9 @@ 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(|x| x.into())).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..f9e625a766 100644 --- a/crates/languages/bevy_mod_scripting_lua/src/lib.rs +++ b/crates/languages/bevy_mod_scripting_lua/src/lib.rs @@ -1,10 +1,11 @@ //! Lua integration for the bevy_mod_scripting system. use bevy::{ app::Plugin, + asset::Handle, ecs::{entity::Entity, world::World}, }; use bevy_mod_scripting_core::{ - asset::Language, + asset::{ScriptAsset, Language}, bindings::{ function::namespace::Namespace, globals::AppScriptGlobalsRegistry, script_value::ScriptValue, ThreadWorldContainer, WorldContainer, @@ -124,9 +125,10 @@ impl Default for LuaScriptingPlugin { LuaReflectReference(::allocate(Box::new(entity), world)), ) .map_err(ScriptError::from_mlua_error)?; + let path = script_id.path().map(|p| p.to_string()).unwrap_or_else(|| script_id.id().to_string()); context .globals() - .set("script_id", script_id) + .set("script_id", path) .map_err(ScriptError::from_mlua_error)?; Ok(()) }], @@ -149,7 +151,7 @@ impl Plugin for LuaScriptingPlugin { fn load_lua_content_into_context( context: &mut Lua, - script_id: &ScriptId, + script_id: &Handle, content: &[u8], initializers: &[ContextInitializer], pre_handling_initializers: &[ContextPreHandlingInitializer], @@ -173,7 +175,7 @@ fn load_lua_content_into_context( #[profiling::function] /// Load a lua context from a script pub fn lua_context_load( - script_id: &ScriptId, + script_id: &Handle, content: &[u8], initializers: &[ContextInitializer], pre_handling_initializers: &[ContextPreHandlingInitializer], @@ -197,7 +199,7 @@ pub fn lua_context_load( #[profiling::function] /// Reload a lua context from a script pub fn lua_context_reload( - script: &ScriptId, + script: &Handle, content: &[u8], old_ctxt: &mut Lua, initializers: &[ContextInitializer], @@ -220,7 +222,7 @@ pub fn lua_context_reload( pub fn lua_handler( args: Vec, entity: bevy::ecs::entity::Entity, - script_id: &ScriptId, + script_id: &Handle, callback_label: &CallbackLabel, context: &mut Lua, pre_handling_initializers: &[ContextPreHandlingInitializer], @@ -234,11 +236,22 @@ pub fn lua_handler( Ok(handler) => handler, // not subscribed to this event type Err(_) => { - bevy::log::trace!( - "Script {} is not subscribed to callback {}", - script_id, - callback_label.as_ref() - ); + match script_id.path() { + Some(path) => { + bevy::log::trace!( + "Script path {} is not subscribed to callback {}", + path, + callback_label.as_ref() + ); + } + None => { + bevy::log::trace!( + "Script id {} is not subscribed to callback {}", + script_id.id(), + callback_label.as_ref() + ); + } + } return Ok(ScriptValue::Unit); } }; @@ -262,13 +275,14 @@ 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); lua_context_load( - &script_id, + &handle, "function hello_world_from_first_load() end" @@ -280,7 +294,7 @@ mod test { .unwrap(); lua_context_reload( - &script_id, + &handle, "function hello_world_from_second_load() end" diff --git a/crates/languages/bevy_mod_scripting_rhai/src/lib.rs b/crates/languages/bevy_mod_scripting_rhai/src/lib.rs index 6f72de7362..539e9b3336 100644 --- a/crates/languages/bevy_mod_scripting_rhai/src/lib.rs +++ b/crates/languages/bevy_mod_scripting_rhai/src/lib.rs @@ -3,11 +3,12 @@ use std::ops::Deref; use bevy::{ + asset::Handle, app::Plugin, ecs::{entity::Entity, world::World}, }; use bevy_mod_scripting_core::{ - asset::Language, + asset::{Language, ScriptAsset}, bindings::{ function::namespace::Namespace, globals::AppScriptGlobalsRegistry, script_value::ScriptValue, ThreadWorldContainer, WorldContainer, @@ -17,7 +18,7 @@ use bevy_mod_scripting_core::{ event::CallbackLabel, reflection_extensions::PartialReflectExt, runtime::RuntimeSettings, - script::ScriptId, + script::{DisplayProxy, ScriptId}, IntoScriptPluginParams, ScriptingPlugin, }; use bindings::{ @@ -180,7 +181,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, + script: &Handle, content: &[u8], initializers: &[ContextInitializer], pre_handling_initializers: &[ContextPreHandlingInitializer], @@ -189,7 +190,7 @@ 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()); + context.ast.set_source(script.display().to_string()); initializers .iter() @@ -205,7 +206,7 @@ fn load_rhai_content_into_context( /// Load a rhai context from a script. pub fn rhai_context_load( - script: &ScriptId, + script: &Handle, content: &[u8], initializers: &[ContextInitializer], pre_handling_initializers: &[ContextPreHandlingInitializer], @@ -229,7 +230,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, + script: &Handle, content: &[u8], context: &mut RhaiScriptContext, initializers: &[ContextInitializer], @@ -251,7 +252,7 @@ pub fn rhai_context_reload( pub fn rhai_callback_handler( args: Vec, entity: Entity, - script_id: &ScriptId, + script_id: &Handle, callback: &CallbackLabel, context: &mut RhaiScriptContext, pre_handling_initializers: &[ContextPreHandlingInitializer], @@ -271,7 +272,7 @@ pub fn rhai_callback_handler( bevy::log::trace!( "Calling callback {} in script {} with args: {:?}", callback, - script_id, + script_id.display(), args ); let runtime = runtime.read(); @@ -288,7 +289,7 @@ pub fn rhai_callback_handler( if let EvalAltResult::ErrorFunctionNotFound(_, _) = e.unwrap_inner() { bevy::log::trace!( "Script {} is not subscribed to callback {} with the provided arguments.", - script_id, + script_id.display(), callback ); Ok(ScriptValue::Unit) 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..77a3963bf2 100644 --- a/crates/testing_crates/script_integration_test_harness/src/lib.rs +++ b/crates/testing_crates/script_integration_test_harness/src/lib.rs @@ -4,11 +4,12 @@ use std::{ marker::PhantomData, path::PathBuf, time::{Duration, Instant}, + sync::{Arc,Mutex}, }; use bevy::{ app::{Last, Plugin, PostUpdate, Startup, Update}, - asset::{AssetServer, Handle}, + asset::{AssetServer, Handle, AssetPath, AssetId, LoadState}, ecs::{ component::Component, event::{Event, Events}, @@ -17,7 +18,7 @@ use bevy::{ world::{Command, FromWorld, Mut}, }, log::Level, - prelude::{Entity, World}, + prelude::{Entity, World, Commands}, 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::{ScriptComponent, ScriptId, DisplayProxy, ScriptContextProvider}, BMSScriptingInfrastructurePlugin, IntoScriptPluginParams, ScriptingPlugin, }; use bevy_mod_scripting_functions::ScriptFunctionsPlugin; @@ -55,13 +56,15 @@ struct TestCallbackBuilder { } impl TestCallbackBuilder { - fn build(script_id: impl Into, expect_response: bool) -> SystemConfigs { - let script_id = script_id.into(); + fn build<'a>(script_path: impl Into>, expect_response: bool) -> SystemConfigs { + let script_path = script_path.into().into_owned(); IntoSystem::into_system( move |world: &mut World, system_state: &mut SystemState>>| { + let script_id = world.resource::().load(script_path.clone()).id(); + let with_guard = system_state.get_mut(world); - let _ = run_test_callback::(&script_id.clone(), with_guard, expect_response); + let _ = run_test_callback::(&script_id, with_guard, expect_response); system_state.apply(world); }, @@ -92,7 +95,7 @@ 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( + LuaScriptingPlugin::default().enable_context_sharing().add_context_initializer( |_, ctxt: &mut bevy_mod_scripting_lua::mlua::Lua| { let globals = ctxt.globals(); globals.set( @@ -199,14 +202,15 @@ pub fn execute_rhai_integration_test(script_id: &str) -> Result<(), String> { execute_integration_test(plugin, |_, _| {}, script_id) } -pub fn execute_integration_test< +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 +236,27 @@ 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 script_path = script_id.clone_owned(); - 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.path().to_str().map(|s| s.contains("__RETURN")).unwrap_or(false); + let load_system = move |server: Res, mut commands: Commands| { + commands.spawn(ScriptComponent::new([server.load(script_path.clone())])); }; - // tests can opt in to this via "__RETURN" - let expect_callback_response = script_id.contains("__RETURN"); app.add_systems(Startup, load_system); app.add_systems( Update, - TestCallbackBuilder::::build(script_id, expect_callback_response), + TestCallbackBuilder::::build(&script_id, expect_callback_response), ); app.add_systems( PostUpdate, - TestCallbackBuilder::::build(script_id, expect_callback_response), + TestCallbackBuilder::::build(&script_id, expect_callback_response), ); app.add_systems( Last, - TestCallbackBuilder::::build(script_id, expect_callback_response), + TestCallbackBuilder::::build(&script_id, expect_callback_response), ); app.add_systems(Update, dummy_update_system); app.add_systems(Startup, dummy_startup_system::); @@ -296,19 +299,20 @@ pub fn execute_integration_test< } fn run_test_callback( - script_id: &str, + script_id: &ScriptId, 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(), + &Handle::Weak(*script_id), Entity::from_raw(0), + &None, vec![], guard.clone(), ); @@ -406,9 +410,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 +429,10 @@ 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 +442,37 @@ 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) { + _ => continue, + LoadState::Loaded => break, + LoadState::Failed(e) => { + return Err(format!("Failed to load script {}: {e}", script_handle.display())); + } + } + } + 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 ctxt_arc = context.script_context().get(Some(entity), &script_id, &None).cloned().unwrap(); + 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) + }); 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 +488,7 @@ pub fn run_plugin_script_load_benchmark< benchmark_id: &str, content: &str, criterion: &mut criterion::BenchmarkGroup, - script_id_generator: impl Fn(u64) -> String, + script_id_generator: impl Fn(u64) -> AssetId, reload_probability: f32, ) { let mut app = setup_integration_test(|_, _| {}); @@ -495,14 +501,15 @@ 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_id = if is_reload { 0 } else { rng.random::() }; - let random_script_id = script_id_generator(random_id); + // let random_script_id = script_id_generator(random_id); + 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 let content = content.to_string().into_boxed_str(); ( CreateOrUpdateScript::

::new( - random_script_id.into(), + Handle::Weak(random_script_id), content.clone().into(), None, ), diff --git a/crates/testing_crates/test_utils/src/test_data.rs b/crates/testing_crates/test_utils/src/test_data.rs index 030f7dc020..1214fa2891 100644 --- a/crates/testing_crates/test_utils/src/test_data.rs +++ b/crates/testing_crates/test_utils/src/test_data.rs @@ -348,10 +348,10 @@ pub fn setup_integration_test(init: F) AssetPlugin::default(), HierarchyPlugin, DiagnosticsPlugin, - LogPlugin { - filter: log_level, - ..Default::default() - }, + // LogPlugin { + // filter: log_level, + // ..Default::default() + // }, )); app } diff --git a/examples/game_of_life.rs b/examples/game_of_life.rs index 325b4a4db9..081862a38b 100644 --- a/examples/game_of_life.rs +++ b/examples/game_of_life.rs @@ -17,6 +17,7 @@ use bevy::{ use bevy_console::{make_layer, AddConsoleCommand, ConsoleCommand, ConsoleOpen, ConsolePlugin}; use bevy_mod_scripting::{BMSPlugin, ScriptFunctionsPlugin}; use bevy_mod_scripting_core::{ + ConfigureScriptPlugin, asset::ScriptAsset, bindings::{ function::namespace::{GlobalNamespace, NamespaceBuilder}, @@ -24,12 +25,13 @@ use bevy_mod_scripting_core::{ AllocatorDiagnosticPlugin, CoreScriptGlobalsPlugin, }, callback_labels, - commands::AddStaticScript, + commands::{DeleteScript, AddStaticScript}, event::ScriptCallbackEvent, handler::event_handler, - script::ScriptComponent, + script::{ScriptId, ScriptComponent}, }; use bevy_mod_scripting_lua::LuaScriptingPlugin; +#[cfg(feature = "rhai")] use bevy_mod_scripting_rhai::RhaiScriptingPlugin; use clap::Parser; @@ -54,7 +56,11 @@ 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, + mut script_handle: Local>>, + script_comps: Query>, + mut static_lua_scripts: Local>, + mut static_rhai_scripts: Local>, ) { if let Some(Ok(command)) = log.take() { match command { @@ -63,32 +69,46 @@ 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"); - // I am not mapping the handles to the script names, so I'll just clear the entire list - loaded_scripts.0.clear(); - // you could also do - // commands.queue(DeleteScript::::new( - // "scripts/game_of_life.lua".into(), - // )); - // as this will retain your script asset and handle + for id in &script_comps { + commands.entity(id).despawn(); + } + + for script_id in static_lua_scripts.drain(..) { + commands.queue(DeleteScript::::new( + script_id, + None, + )); + } + + #[cfg(feature = "rhai")] + for script_id in static_rhai_scripts.drain(..) { + commands.queue(DeleteScript::::new( + script_id, + None, + )); + } } } } @@ -115,12 +135,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 +149,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 +167,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 +189,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/tests/script_tests.rs b/tests/script_tests.rs index cbb451abd6..06d93fb5c9 100644 --- a/tests/script_tests.rs +++ b/tests/script_tests.rs @@ -4,7 +4,7 @@ use std::path::PathBuf; use libtest_mimic::{Arguments, Failed, Trial}; use script_integration_test_harness::{ - execute_lua_integration_test, execute_rhai_integration_test, + execute_lua_integration_test, }; use test_utils::{discover_all_tests, Test, TestKind}; @@ -16,11 +16,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(())