From 3430b92fbaafa46919ad203d46f1f0fd79531ad0 Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Thu, 26 Jun 2025 22:44:53 -0400 Subject: [PATCH 01/41] hack: It compiles. --- crates/bevy_mod_scripting_core/src/asset.rs | 164 +++++++++--------- .../src/bindings/globals/core.rs | 4 +- .../src/bindings/script_system.rs | 17 +- .../bevy_mod_scripting_core/src/commands.rs | 9 +- crates/bevy_mod_scripting_core/src/lib.rs | 51 +++--- crates/bevy_mod_scripting_core/src/script.rs | 4 +- .../bevy_mod_scripting_functions/src/core.rs | 1 + 7 files changed, 132 insertions(+), 118 deletions(-) diff --git a/crates/bevy_mod_scripting_core/src/asset.rs b/crates/bevy_mod_scripting_core/src/asset.rs index c077ce82e6..2a35c2c8a4 100644 --- a/crates/bevy_mod_scripting_core/src/asset.rs +++ b/crates/bevy_mod_scripting_core/src/asset.rs @@ -53,8 +53,8 @@ impl std::fmt::Display for Language { 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>, + /// The language of the script + pub language: Language, } #[derive(Event, Debug, Clone)] @@ -97,7 +97,7 @@ impl AssetLoader for ScriptAssetLoader { } let asset = ScriptAsset { content: content.into_boxed_slice(), - asset_path: load_context.asset_path().to_owned(), + language: Language::Lua, }; Ok(asset) } @@ -107,60 +107,61 @@ 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, -} +// #[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()), +// map: (|path: &AssetPath| path.id()), +// }, +// 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)] @@ -174,8 +175,8 @@ pub struct ScriptMetadataStore { pub struct ScriptMetadata { /// The asset id of the script pub asset_id: AssetId, - /// The script id of the script - pub script_id: ScriptId, + // The script id of the script + // pub script_id: ScriptId, /// The language of the script pub language: Language, } @@ -211,7 +212,7 @@ pub(crate) fn dispatch_script_asset_events( mut script_asset_events: EventWriter, assets: Res>, mut metadata_store: ResMut, - settings: Res, + // settings: Res, ) { for event in events.read() { match event { @@ -220,23 +221,24 @@ pub(crate) fn dispatch_script_asset_events( 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); + // let path = &asset.asset_path; + // let converter = settings.script_id_mapper.map; + // let script_id = converter(path); + + // let language = settings.select_script_language(path); + let language = &asset.language; + 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); } let metadata = ScriptMetadata { asset_id: *id, - script_id, - language, + // script_id, + language: language.clone(), }; debug!("Script loaded, populating metadata: {:?}:", metadata); script_asset_events.send(ScriptAssetEvent::Added(metadata.clone())); @@ -309,26 +311,26 @@ pub(crate) fn sync_script_data( ScriptAssetEvent::Added(_) | ScriptAssetEvent::Modified(_) => { if metadata.language != P::LANGUAGE { trace!( - "{}: Script asset with id: {} is for a different langauge than this sync system. Skipping.", + "{}: Script asset {} is for a different langauge than this sync system. Skipping.", P::LANGUAGE, - metadata.script_id + metadata.asset_id ); continue; } - info!("{}: Loading Script: {:?}", P::LANGUAGE, metadata.script_id,); + info!("{}: Loading Script: {:?}", P::LANGUAGE, metadata.asset_id,); if let Some(asset) = script_assets.get(metadata.asset_id) { commands.queue(CreateOrUpdateScript::

::new( - metadata.script_id.clone(), + metadata.asset_id.clone(), asset.content.clone(), Some(script_assets.reserve_handle().clone_weak()), )); } } ScriptAssetEvent::Removed(_) => { - info!("{}: Deleting Script: {:?}", P::LANGUAGE, metadata.script_id,); - commands.queue(DeleteScript::

::new(metadata.script_id.clone())); + info!("{}: Deleting Script: {:?}", P::LANGUAGE, metadata.asset_id,); + commands.queue(DeleteScript::

::new(metadata.asset_id.clone())); } }; } @@ -356,7 +358,7 @@ pub(crate) fn configure_asset_systems(app: &mut App) -> &mut App { ), ) .init_resource::() - .init_resource::() + // .init_resource::() .add_event::(); 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 ab840e9593..a600653064 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/script_system.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/script_system.rs @@ -21,6 +21,7 @@ use crate::{ IntoScriptPluginParams, }; use bevy::{ + prelude::AssetServer, ecs::{ archetype::{ArchetypeComponentId, ArchetypeGeneration}, component::{ComponentId, Tick}, @@ -78,12 +79,14 @@ 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, before: Vec, after: Vec, system_params: Vec, @@ -93,7 +96,7 @@ 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) -> Self { Self { before: vec![], after: vec![], @@ -339,7 +342,7 @@ 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, @@ -420,6 +423,11 @@ impl System for DynamicScriptSystem

{ }; let mut payload = Vec::with_capacity(state.system_params.len()); + let script_id = { + let asset_server = world.world().resource::(); + let handle = asset_server.load(&*self.target_script); + handle.id() + }; 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,7 +497,8 @@ 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), payload, guard.clone(), diff --git a/crates/bevy_mod_scripting_core/src/commands.rs b/crates/bevy_mod_scripting_core/src/commands.rs index 57b37195a9..b61f793392 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, @@ -21,14 +22,14 @@ 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, /// 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) -> Self { Self { id, _ph: PhantomData, @@ -249,7 +250,7 @@ impl Command for CreateOrUpdateScript

{ /// 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: AssetId, /// The entity to use for the callback pub entity: Entity, /// The callback to run @@ -267,7 +268,7 @@ pub struct RunScriptCallback { impl RunScriptCallback

{ /// Creates a new RunCallbackCommand with the given ID, callback and arguments pub fn new( - id: ScriptId, + id: AssetId, entity: Entity, callback: CallbackLabel, args: Vec, diff --git a/crates/bevy_mod_scripting_core/src/lib.rs b/crates/bevy_mod_scripting_core/src/lib.rs index 15faf31ff4..351258da80 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::{ @@ -277,17 +277,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, @@ -394,24 +394,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 index d5b0fe46ae..6b85983f3e 100644 --- a/crates/bevy_mod_scripting_core/src/script.rs +++ b/crates/bevy_mod_scripting_core/src/script.rs @@ -2,14 +2,14 @@ use crate::{asset::ScriptAsset, IntoScriptPluginParams}; use bevy::prelude::ReflectComponent; -use bevy::{asset::Handle, ecs::system::Resource, reflect::Reflect, utils::HashSet}; +use bevy::{asset::{AssetId, 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>; +pub type ScriptId = AssetId; #[derive(bevy::ecs::component::Component, Reflect, Clone)] #[reflect(Component)] diff --git a/crates/bevy_mod_scripting_functions/src/core.rs b/crates/bevy_mod_scripting_functions/src/core.rs index 08963a02d3..e39fb68964 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, From cf4dbf4c574308b2ce1cf204c70556e66d329025 Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Fri, 27 Jun 2025 00:04:02 -0400 Subject: [PATCH 02/41] hack: Compile with lua54 feature. --- crates/bevy_mod_scripting_core/src/asset.rs | 10 +++- crates/bevy_mod_scripting_core/src/context.rs | 4 +- .../bevy_mod_scripting_lua/src/lib.rs | 2 +- .../src/lib.rs | 48 +++++++++++-------- examples/game_of_life.rs | 8 +++- 5 files changed, 47 insertions(+), 25 deletions(-) diff --git a/crates/bevy_mod_scripting_core/src/asset.rs b/crates/bevy_mod_scripting_core/src/asset.rs index 2a35c2c8a4..6c029b9585 100644 --- a/crates/bevy_mod_scripting_core/src/asset.rs +++ b/crates/bevy_mod_scripting_core/src/asset.rs @@ -95,9 +95,17 @@ impl AssetLoader for ScriptAssetLoader { if let Some(processor) = &self.preprocessor { processor(&mut content)?; } + let language = match load_context.path().extension().and_then(|e| e.to_str()).unwrap_or_default() { + "lua" => Language::Lua, + "rhai" => Language::Rhai, + x => { + warn!("Unknown language for {:?}", load_context.path().display()); + Language::Unknown + } + }; let asset = ScriptAsset { content: content.into_boxed_slice(), - language: Language::Lua, + language, }; Ok(asset) } diff --git a/crates/bevy_mod_scripting_core/src/context.rs b/crates/bevy_mod_scripting_core/src/context.rs index dec993f7be..baf8e41fdc 100644 --- a/crates/bevy_mod_scripting_core/src/context.rs +++ b/crates/bevy_mod_scripting_core/src/context.rs @@ -17,11 +17,11 @@ impl Context for T {} /// Initializer run once after creating a context but before executing it for the first time as well as after re-loading the script pub type ContextInitializer

= - fn(&str, &mut

::C) -> Result<(), ScriptError>; + fn(&ScriptId, &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(&ScriptId, Entity, &mut

::C) -> Result<(), ScriptError>; /// Settings concerning the creation and assignment of script contexts as well as their initialization. #[derive(Resource)] diff --git a/crates/languages/bevy_mod_scripting_lua/src/lib.rs b/crates/languages/bevy_mod_scripting_lua/src/lib.rs index 473c98d24c..dd3d02795c 100644 --- a/crates/languages/bevy_mod_scripting_lua/src/lib.rs +++ b/crates/languages/bevy_mod_scripting_lua/src/lib.rs @@ -126,7 +126,7 @@ impl Default for LuaScriptingPlugin { .map_err(ScriptError::from_mlua_error)?; context .globals() - .set("script_id", script_id) + .set("script_id", script_id.to_string()) .map_err(ScriptError::from_mlua_error)?; Ok(()) }], 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 fa039361e7..fd66be0693 100644 --- a/crates/testing_crates/script_integration_test_harness/src/lib.rs +++ b/crates/testing_crates/script_integration_test_harness/src/lib.rs @@ -8,7 +8,7 @@ use std::{ use bevy::{ app::{Last, Plugin, PostUpdate, Startup, Update}, - asset::{AssetServer, Handle}, + asset::{AssetServer, Handle, AssetPath, AssetId}, ecs::{ component::Component, event::{Event, Events}, @@ -55,13 +55,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); }, @@ -296,18 +298,18 @@ 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()) { + if !handler_ctxt.is_script_fully_loaded(*script_id) { return Ok(ScriptValue::Unit); } let res = handler_ctxt.call::( - &script_id.to_string().into(), + &script_id, Entity::from_raw(0), vec![], guard.clone(), @@ -406,9 +408,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 +427,22 @@ 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_id = script_id.to_owned(); + // let script_id_clone = script_id.clone(); + + + let script_path = script_path.into(); + let script_handle = app.world().resource::().load(script_path); + let script_id = script_handle.id(); + + + // app.add_systems( + // Startup, + // move |server: Res, mut handle: Local>| { + // *handle = server.load(script_id_clone.to_owned()); + // handle.id() + // }, + // ); // finalize app.cleanup(); @@ -482,7 +492,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(|_, _| {}); @@ -502,7 +512,7 @@ pub fn run_plugin_script_load_benchmark< let content = content.to_string().into_boxed_str(); ( CreateOrUpdateScript::

::new( - random_script_id.into(), + random_script_id, content.clone().into(), None, ), diff --git a/examples/game_of_life.rs b/examples/game_of_life.rs index 325b4a4db9..9471a934fd 100644 --- a/examples/game_of_life.rs +++ b/examples/game_of_life.rs @@ -55,6 +55,8 @@ fn run_script_cmd( mut log: ConsoleCommand, mut commands: Commands, mut loaded_scripts: ResMut, + asset_server: Res, + mut script_handle: Local>>, ) { if let Some(Ok(command)) = log.take() { match command { @@ -69,12 +71,14 @@ fn run_script_cmd( ); let script_path = format!("scripts/game_of_life.{}", language); + *script_handle = Some(asset_server.load(script_path)); + let script_id = script_handle.as_ref().unwrap().id(); if !use_static_script { bevy::log::info!("Spawning an entity with ScriptComponent"); - commands.spawn(ScriptComponent::new(vec![script_path])); + commands.spawn(ScriptComponent::new(vec![script_id])); } else { bevy::log::info!("Using static script instead of spawning an entity"); - commands.queue(AddStaticScript::new(script_path)) + commands.queue(AddStaticScript::new(script_id)) } } GameOfLifeCommand::Stop => { From c3a7d965e4feb451bbfda510bba9223ac1d9508b Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Fri, 27 Jun 2025 03:20:02 -0400 Subject: [PATCH 03/41] refactor: Accept Handle in ScriptComponents. --- .../bevy_mod_scripting_core/src/commands.rs | 8 +++--- crates/bevy_mod_scripting_core/src/handler.rs | 18 ++++++++----- crates/bevy_mod_scripting_core/src/lib.rs | 8 +++--- crates/bevy_mod_scripting_core/src/script.rs | 16 ++++++------ examples/game_of_life.rs | 26 +++---------------- 5 files changed, 31 insertions(+), 45 deletions(-) diff --git a/crates/bevy_mod_scripting_core/src/commands.rs b/crates/bevy_mod_scripting_core/src/commands.rs index b61f793392..4550daa66b 100644 --- a/crates/bevy_mod_scripting_core/src/commands.rs +++ b/crates/bevy_mod_scripting_core/src/commands.rs @@ -338,12 +338,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() } } } @@ -358,12 +358,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 } } } diff --git a/crates/bevy_mod_scripting_core/src/handler.rs b/crates/bevy_mod_scripting_core/src/handler.rs index 34f259c4c7..f2794ae6e1 100644 --- a/crates/bevy_mod_scripting_core/src/handler.rs +++ b/crates/bevy_mod_scripting_core/src/handler.rs @@ -180,7 +180,7 @@ pub(crate) fn event_handler_inner( 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 } @@ -197,7 +197,7 @@ pub(crate) fn event_handler_inner( let call_result = handler_ctxt.call_dynamic_label( &callback_label, - script_id, + &script_id.id(), *entity, event.args.clone(), guard.clone(), @@ -208,7 +208,7 @@ pub(crate) fn event_handler_inner( guard.clone(), ScriptCallbackResponseEvent::new( callback_label.clone(), - script_id.clone(), + script_id.id(), call_result.clone(), ), ); @@ -234,9 +234,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 351258da80..353d2ccafe 100644 --- a/crates/bevy_mod_scripting_core/src/lib.rs +++ b/crates/bevy_mod_scripting_core/src/lib.rs @@ -353,21 +353,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 } diff --git a/crates/bevy_mod_scripting_core/src/script.rs b/crates/bevy_mod_scripting_core/src/script.rs index 6b85983f3e..ae8052b34c 100644 --- a/crates/bevy_mod_scripting_core/src/script.rs +++ b/crates/bevy_mod_scripting_core/src/script.rs @@ -16,10 +16,10 @@ pub type ScriptId = AssetId; /// 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); +pub struct ScriptComponent(pub Vec>); impl Deref for ScriptComponent { - type Target = Vec; + type Target = Vec>; fn deref(&self) -> &Self::Target { &self.0 @@ -28,7 +28,7 @@ impl Deref for ScriptComponent { impl ScriptComponent { /// Creates a new [`ScriptComponent`] with the given ScriptID's - pub fn new, I: IntoIterator>(components: I) -> Self { + pub fn new>, I: IntoIterator>(components: I) -> Self { Self(components.into_iter().map(Into::into).collect()) } } @@ -111,29 +111,29 @@ impl Clone for Script

{ /// 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, + pub(crate) scripts: HashSet>, } #[profiling::all_functions] impl StaticScripts { /// Inserts a static script into the collection - pub fn insert>(&mut self, script: S) { + 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 { + 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 { + 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 { + pub fn iter(&self) -> impl Iterator> { self.scripts.iter() } } diff --git a/examples/game_of_life.rs b/examples/game_of_life.rs index 9471a934fd..a1ada8975a 100644 --- a/examples/game_of_life.rs +++ b/examples/game_of_life.rs @@ -54,7 +54,6 @@ 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>>, ) { @@ -71,22 +70,18 @@ fn run_script_cmd( ); let script_path = format!("scripts/game_of_life.{}", language); - *script_handle = Some(asset_server.load(script_path)); - let script_id = script_handle.as_ref().unwrap().id(); if !use_static_script { bevy::log::info!("Spawning an entity with ScriptComponent"); - commands.spawn(ScriptComponent::new(vec![script_id])); + 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_id)) + commands.queue(AddStaticScript::new(asset_server.load(script_path))) } } 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( @@ -123,8 +118,7 @@ fn game_of_life_app(app: &mut App) -> &mut App { .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, @@ -149,9 +143,6 @@ pub struct LifeState { pub cells: Vec, } -#[derive(Debug, Resource, Default)] -pub struct LoadedScripts(pub Vec>); - #[derive(Reflect, Resource)] #[reflect(Resource)] pub struct Settings { @@ -174,17 +165,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) From 324f9f304ffc6406e39e0722c533c609ea9eef83 Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Fri, 27 Jun 2025 04:19:39 -0400 Subject: [PATCH 04/41] feature: Handle static scripts. --- crates/bevy_mod_scripting_core/src/asset.rs | 78 ++++++++++++++++++--- examples/game_of_life.rs | 27 ++++--- 2 files changed, 84 insertions(+), 21 deletions(-) diff --git a/crates/bevy_mod_scripting_core/src/asset.rs b/crates/bevy_mod_scripting_core/src/asset.rs index 6c029b9585..4cf8ab307e 100644 --- a/crates/bevy_mod_scripting_core/src/asset.rs +++ b/crates/bevy_mod_scripting_core/src/asset.rs @@ -1,6 +1,8 @@ //! Systems and resources for handling script assets and events use crate::{ + StaticScripts, + ScriptComponent, commands::{CreateOrUpdateScript, DeleteScript}, error::ScriptError, script::ScriptId, @@ -8,17 +10,17 @@ use crate::{ }; 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, }, reflect::TypePath, utils::hashbrown::HashMap, }; -use std::borrow::Cow; +use std::{borrow::Cow, collections::VecDeque}; /// Represents a scripting language. Languages which compile into another language should use the target language as their language. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Default)] @@ -300,6 +302,7 @@ pub(crate) fn remove_script_metadata( pub(crate) fn sync_script_data( mut events: EventReader, script_assets: Res>, + static_scripts: Res, mut commands: Commands, ) { for event in events.read() { @@ -326,14 +329,16 @@ pub(crate) fn sync_script_data( continue; } - info!("{}: Loading Script: {:?}", P::LANGUAGE, metadata.asset_id,); - if let Some(asset) = script_assets.get(metadata.asset_id) { - commands.queue(CreateOrUpdateScript::

::new( - metadata.asset_id.clone(), - asset.content.clone(), - Some(script_assets.reserve_handle().clone_weak()), - )); + if static_scripts.iter().any(|handle| handle.id() == metadata.asset_id) { + info!("{}: Loading static script: {:?}", P::LANGUAGE, metadata.asset_id,); + if let Some(asset) = script_assets.get(metadata.asset_id) { + commands.queue(CreateOrUpdateScript::

::new( + metadata.asset_id.clone(), + asset.content.clone(), + Some(script_assets.reserve_handle().clone_weak()), + )); + } } } ScriptAssetEvent::Removed(_) => { @@ -344,6 +349,57 @@ pub(crate) fn sync_script_data( } } +pub(crate) fn eval_script( + script_comps: Query<&ScriptComponent, Added>, + mut script_queue: Local>, + script_assets: Res>, + asset_server: Res, + mut commands: Commands, + ) { + for script_comp in &script_comps { + for handle in &script_comp.0 { + script_queue.push_back(handle.id()); + } + } + 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); + 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(script_id) = script_queue.pop_front() { + if let Some(asset) = script_assets.get(script_id) { + commands.queue(CreateOrUpdateScript::

::new( + script_id, + asset.content.clone(), + Some(Handle::Weak(script_id)), + )); + } 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. + } + } else { + break; + } + } +} + /// Setup all the asset systems for the scripting plugin and the dependencies #[profiling::function] pub(crate) fn configure_asset_systems(app: &mut App) -> &mut App { @@ -379,7 +435,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/examples/game_of_life.rs b/examples/game_of_life.rs index a1ada8975a..86db0a9539 100644 --- a/examples/game_of_life.rs +++ b/examples/game_of_life.rs @@ -24,10 +24,10 @@ 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; use bevy_mod_scripting_rhai::RhaiScriptingPlugin; @@ -56,6 +56,8 @@ fn run_script_cmd( mut commands: Commands, asset_server: Res, mut script_handle: Local>>, + script_comps: Query>, + mut static_scripts_created: Local>, ) { if let Some(Ok(command)) = log.take() { match command { @@ -64,10 +66,7 @@ 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 { @@ -75,7 +74,9 @@ fn run_script_cmd( 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(asset_server.load(script_path))) + let handle = asset_server.load(script_path); + static_scripts_created.push(handle.id()); + commands.queue(AddStaticScript::new(handle)) } } GameOfLifeCommand::Stop => { @@ -83,10 +84,16 @@ fn run_script_cmd( bevy::log::info!("Stopping game of life by dropping the handles to all scripts"); + for id in &script_comps { + commands.entity(id).despawn(); + } + // you could also do - // commands.queue(DeleteScript::::new( - // "scripts/game_of_life.lua".into(), - // )); + for script_id in static_scripts_created.drain(..) { + commands.queue(DeleteScript::::new( + script_id + )); + } // as this will retain your script asset and handle } } From 263629537272476c6d997bc2b7401fb973b3b663 Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Fri, 27 Jun 2025 04:35:57 -0400 Subject: [PATCH 05/41] bug: Fix rhea and static script unloading. --- crates/bevy_mod_scripting_core/src/asset.rs | 12 +++++++----- .../bevy_mod_scripting_core/src/commands.rs | 6 +++++- examples/game_of_life.rs | 19 ++++++++++++++----- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/crates/bevy_mod_scripting_core/src/asset.rs b/crates/bevy_mod_scripting_core/src/asset.rs index 4cf8ab307e..f65a27711c 100644 --- a/crates/bevy_mod_scripting_core/src/asset.rs +++ b/crates/bevy_mod_scripting_core/src/asset.rs @@ -384,11 +384,13 @@ pub(crate) fn eval_script( // }) { if let Some(script_id) = script_queue.pop_front() { if let Some(asset) = script_assets.get(script_id) { - commands.queue(CreateOrUpdateScript::

::new( - script_id, - asset.content.clone(), - Some(Handle::Weak(script_id)), - )); + if asset.language == P::LANGUAGE { + commands.queue(CreateOrUpdateScript::

::new( + script_id, + asset.content.clone(), + Some(Handle::Weak(script_id)), + )); + } } else { // This is probably a load failure. What to do? We've already // provided a warning on failure. Doing nothing is fine then we diff --git a/crates/bevy_mod_scripting_core/src/commands.rs b/crates/bevy_mod_scripting_core/src/commands.rs index 4550daa66b..c9b44c7d30 100644 --- a/crates/bevy_mod_scripting_core/src/commands.rs +++ b/crates/bevy_mod_scripting_core/src/commands.rs @@ -84,6 +84,10 @@ impl CreateOrUpdateScript

{ } } + fn script_name(&self) -> String { + self.asset.as_ref().and_then(|handle| handle.path().map(|p| p.to_string())).unwrap_or_else(|| self.id.to_string()) + } + fn reload_context( &self, guard: WorldGuard, @@ -215,7 +219,7 @@ impl Command for CreateOrUpdateScript

{ handle_script_errors( guard, vec![err - .with_script(self.id.clone()) + .with_script(self.script_name()) .with_context(P::LANGUAGE) .with_context(phrase)] .into_iter(), diff --git a/examples/game_of_life.rs b/examples/game_of_life.rs index 86db0a9539..586a56f919 100644 --- a/examples/game_of_life.rs +++ b/examples/game_of_life.rs @@ -57,7 +57,8 @@ fn run_script_cmd( asset_server: Res, mut script_handle: Local>>, script_comps: Query>, - mut static_scripts_created: Local>, + mut static_lua_scripts: Local>, + mut static_rhai_scripts: Local>, ) { if let Some(Ok(command)) = log.take() { match command { @@ -75,7 +76,11 @@ fn run_script_cmd( } else { bevy::log::info!("Using static script instead of spawning an entity"); let handle = asset_server.load(script_path); - static_scripts_created.push(handle.id()); + if language == "lua" { + static_lua_scripts.push(handle.id()); + } else { + static_rhai_scripts.push(handle.id()); + } commands.queue(AddStaticScript::new(handle)) } } @@ -88,13 +93,17 @@ fn run_script_cmd( commands.entity(id).despawn(); } - // you could also do - for script_id in static_scripts_created.drain(..) { + for script_id in static_lua_scripts.drain(..) { commands.queue(DeleteScript::::new( script_id )); } - // as this will retain your script asset and handle + + for script_id in static_rhai_scripts.drain(..) { + commands.queue(DeleteScript::::new( + script_id + )); + } } } } From d6ea7a7118586c8b82540b53b2491bf6990debe4 Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Fri, 27 Jun 2025 04:55:33 -0400 Subject: [PATCH 06/41] feature: Add ScriptSetttings to loader. This introduces a serde dependency though. :( --- crates/bevy_mod_scripting_core/Cargo.toml | 1 + crates/bevy_mod_scripting_core/src/asset.rs | 89 ++++++--------------- 2 files changed, 24 insertions(+), 66 deletions(-) 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 f65a27711c..54ab6428d1 100644 --- a/crates/bevy_mod_scripting_core/src/asset.rs +++ b/crates/bevy_mod_scripting_core/src/asset.rs @@ -21,9 +21,10 @@ use bevy::{ utils::hashbrown::HashMap, }; 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, @@ -66,6 +67,13 @@ pub(crate) enum ScriptAssetEvent { 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)] /// A loader for script assets pub struct ScriptAssetLoader { @@ -79,14 +87,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(); @@ -97,14 +105,18 @@ impl AssetLoader for ScriptAssetLoader { if let Some(processor) = &self.preprocessor { processor(&mut content)?; } - let language = match load_context.path().extension().and_then(|e| e.to_str()).unwrap_or_default() { - "lua" => Language::Lua, - "rhai" => Language::Rhai, - x => { - warn!("Unknown language for {:?}", load_context.path().display()); - Language::Unknown - } - }; + 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, + x => { + warn!("Unknown language for {:?}", load_context.path().display()); + Language::Unknown + } + }); let asset = ScriptAsset { content: content.into_boxed_slice(), language, @@ -117,61 +129,6 @@ 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()), -// map: (|path: &AssetPath| path.id()), -// }, -// 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)] From a2c427a1d94e9f42a1a3d24d0b6b20e273f4a29b Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Fri, 27 Jun 2025 17:04:07 -0400 Subject: [PATCH 07/41] test: Prep tests to run. --- .../src/bindings/script_system.rs | 11 +++--- .../bevy_mod_scripting_core/src/commands.rs | 6 ++-- crates/bevy_mod_scripting_core/src/context.rs | 18 ++++++---- crates/bevy_mod_scripting_core/src/error.rs | 21 +++++++---- .../bevy_mod_scripting_core/src/extractors.rs | 22 +++++++----- crates/bevy_mod_scripting_core/src/handler.rs | 20 ++++++++--- .../bevy_mod_scripting_lua/src/lib.rs | 35 +++++++++++++------ .../test_utils/src/test_data.rs | 8 ++--- 8 files changed, 92 insertions(+), 49 deletions(-) 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 a600653064..941c754f18 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}, @@ -21,6 +22,7 @@ use crate::{ IntoScriptPluginParams, }; use bevy::{ + asset::Handle, prelude::AssetServer, ecs::{ archetype::{ArchetypeComponentId, ArchetypeGeneration}, @@ -257,13 +259,13 @@ impl<'w, P: IntoScriptPluginParams> DynamicHandlerContext<'w, P> { pub fn call_dynamic_label( &self, label: &CallbackLabel, - script_id: &ScriptId, + script_id: &Handle, entity: Entity, payload: Vec, guard: WorldGuard<'_>, ) -> Result { // find script - let script = match self.scripts.scripts.get(script_id) { + let script = match self.scripts.scripts.get(&script_id.id()) { Some(script) => script, None => return Err(InteropError::missing_script(script_id.clone()).into()), }; @@ -281,7 +283,7 @@ impl<'w, P: IntoScriptPluginParams> DynamicHandlerContext<'w, P> { handler, payload, entity, - script_id, + &script_id.id(), label, &mut context, pre_handling_initializers, @@ -425,8 +427,7 @@ impl System for DynamicScriptSystem

{ let mut payload = Vec::with_capacity(state.system_params.len()); let script_id = { let asset_server = world.world().resource::(); - let handle = asset_server.load(&*self.target_script); - handle.id() + 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 diff --git a/crates/bevy_mod_scripting_core/src/commands.rs b/crates/bevy_mod_scripting_core/src/commands.rs index c9b44c7d30..15f8aa7e74 100644 --- a/crates/bevy_mod_scripting_core/src/commands.rs +++ b/crates/bevy_mod_scripting_core/src/commands.rs @@ -107,7 +107,8 @@ impl CreateOrUpdateScript

{ (ContextBuilder::

::reload)( handler_ctxt.context_loading_settings.loader.reload, - &self.id, + // &self.id, + &Handle::Weak(self.id), &self.content, &mut context, &handler_ctxt.context_loading_settings.context_initializers, @@ -128,7 +129,8 @@ impl CreateOrUpdateScript

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

::load)( handler_ctxt.context_loading_settings.loader.load, - &self.id, + // &self.id, + &Handle::Weak(self.id), &self.content, &handler_ctxt.context_loading_settings.context_initializers, &handler_ctxt diff --git a/crates/bevy_mod_scripting_core/src/context.rs b/crates/bevy_mod_scripting_core/src/context.rs index baf8e41fdc..b841551c71 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(&ScriptId, &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(&ScriptId, 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

], diff --git a/crates/bevy_mod_scripting_core/src/error.rs b/crates/bevy_mod_scripting_core/src/error.rs index 8b2bd2a967..cf2d37e5c3 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, @@ -11,6 +12,7 @@ use crate::{ script::ScriptId, }; use bevy::{ + asset::Handle, ecs::{ component::ComponentId, schedule::{ScheduleBuildError, ScheduleNotInitialized}, @@ -592,7 +594,7 @@ 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(), })) @@ -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 { @@ -1253,10 +1255,17 @@ 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 - ) + if let Some(path) = $script_id.path() { + format!( + "Could not find script with path: {}. Is the script loaded?", + path + ) + } else { + format!( + "Could not find script with id: {}. Is the script loaded?", + $script_id.id() + ) + } }; } diff --git a/crates/bevy_mod_scripting_core/src/extractors.rs b/crates/bevy_mod_scripting_core/src/extractors.rs index d105cca7b5..2ea79676c7 100644 --- a/crates/bevy_mod_scripting_core/src/extractors.rs +++ b/crates/bevy_mod_scripting_core/src/extractors.rs @@ -4,14 +4,17 @@ #![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::Handle, + ecs::{ + component::ComponentId, + entity::Entity, + event::{Event, EventCursor, EventIterator, Events}, + query::{Access, AccessConflicts}, + storage::SparseSetIndex, + system::{Local, Resource, SystemParam, SystemState}, + world::World, + } }; use fixedbitset::FixedBitSet; @@ -213,7 +216,8 @@ impl HandlerContext<'_, P> { // find script let script = match self.scripts.scripts.get(script_id) { Some(script) => script, - None => return Err(InteropError::missing_script(script_id.clone()).into()), + // NOTE: It'd be nice to use a handle here because then we have the path. + None => return Err(InteropError::missing_script(Handle::Weak(script_id.clone())).into()), }; // call the script diff --git a/crates/bevy_mod_scripting_core/src/handler.rs b/crates/bevy_mod_scripting_core/src/handler.rs index f2794ae6e1..15714fd56a 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, @@ -15,6 +16,7 @@ use crate::{ IntoScriptPluginParams, }; use bevy::{ + asset::Handle, ecs::{ entity::Entity, query::QueryState, @@ -219,11 +221,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 { .. }) => { diff --git a/crates/languages/bevy_mod_scripting_lua/src/lib.rs b/crates/languages/bevy_mod_scripting_lua/src/lib.rs index dd3d02795c..197026eba6 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.to_string()) + .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); } }; 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 } From 13d351498f6f0b111cbf7dc4dafdfef3609b1ccb Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Fri, 27 Jun 2025 17:51:46 -0400 Subject: [PATCH 08/41] feature: Add DisplayProxy. Refactor continues. --- crates/bevy_mod_scripting_core/src/asset.rs | 24 +++---- .../src/bindings/script_system.rs | 2 +- .../bevy_mod_scripting_core/src/commands.rs | 63 +++++++++---------- crates/bevy_mod_scripting_core/src/error.rs | 17 ++--- .../bevy_mod_scripting_core/src/extractors.rs | 9 +-- crates/bevy_mod_scripting_core/src/handler.rs | 6 +- crates/bevy_mod_scripting_core/src/script.rs | 48 ++++++++++++-- 7 files changed, 101 insertions(+), 68 deletions(-) diff --git a/crates/bevy_mod_scripting_core/src/asset.rs b/crates/bevy_mod_scripting_core/src/asset.rs index 54ab6428d1..d7b20a1cd8 100644 --- a/crates/bevy_mod_scripting_core/src/asset.rs +++ b/crates/bevy_mod_scripting_core/src/asset.rs @@ -5,7 +5,7 @@ use crate::{ ScriptComponent, commands::{CreateOrUpdateScript, DeleteScript}, error::ScriptError, - script::ScriptId, + script::{DisplayProxy, ScriptId}, IntoScriptPluginParams, ScriptingSystemSet, }; use bevy::{ @@ -151,23 +151,23 @@ pub struct ScriptMetadata { #[profiling::all_functions] impl ScriptMetadataStore { /// Inserts a new metadata entry - pub fn insert(&mut self, id: AssetId, meta: ScriptMetadata) { + pub fn insert(&mut self, id: ScriptId, 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> { + pub fn get(&self, id: ScriptId) -> Option<&ScriptMetadata> { self.map.get(&id) } /// Removes a metadata entry - pub fn remove(&mut self, id: AssetId) -> Option { + pub fn remove(&mut self, id: ScriptId) -> Option { self.map.remove(&id) } /// Checks if the store contains a metadata entry - pub fn contains(&self, id: AssetId) -> bool { + pub fn contains(&self, id: ScriptId) -> bool { self.map.contains_key(&id) } } @@ -291,7 +291,7 @@ pub(crate) fn sync_script_data( info!("{}: Loading static script: {:?}", P::LANGUAGE, metadata.asset_id,); if let Some(asset) = script_assets.get(metadata.asset_id) { commands.queue(CreateOrUpdateScript::

::new( - metadata.asset_id.clone(), + Handle::Weak(metadata.asset_id.clone()), asset.content.clone(), Some(script_assets.reserve_handle().clone_weak()), )); @@ -308,20 +308,20 @@ pub(crate) fn sync_script_data( pub(crate) fn eval_script( script_comps: Query<&ScriptComponent, Added>, - mut script_queue: Local>, + mut script_queue: Local>>, script_assets: Res>, asset_server: Res, mut commands: Commands, ) { for script_comp in &script_comps { for handle in &script_comp.0 { - script_queue.push_back(handle.id()); + script_queue.push_back(handle.clone_weak()); } } while ! script_queue.is_empty() { - let script_ready = script_queue.front().map(|script_id| match asset_server.load_state(*script_id) { + 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); + warn!("Failed to load script {}", script_id.display()); true } LoadState::Loaded => true, @@ -340,12 +340,12 @@ pub(crate) fn eval_script( // _ => false // }) { if let Some(script_id) = script_queue.pop_front() { - if let Some(asset) = script_assets.get(script_id) { + if let Some(asset) = script_assets.get(&script_id) { if asset.language == P::LANGUAGE { commands.queue(CreateOrUpdateScript::

::new( script_id, asset.content.clone(), - Some(Handle::Weak(script_id)), + None, )); } } else { 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 941c754f18..53d4e66fc7 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/script_system.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/script_system.rs @@ -283,7 +283,7 @@ impl<'w, P: IntoScriptPluginParams> DynamicHandlerContext<'w, P> { handler, payload, entity, - &script_id.id(), + script_id, label, &mut context, pre_handling_initializers, diff --git a/crates/bevy_mod_scripting_core/src/commands.rs b/crates/bevy_mod_scripting_core/src/commands.rs index 15f8aa7e74..fb532daf58 100644 --- a/crates/bevy_mod_scripting_core/src/commands.rs +++ b/crates/bevy_mod_scripting_core/src/commands.rs @@ -12,7 +12,7 @@ use crate::{ }, extractors::{with_handler_system_state, HandlerContext}, handler::{handle_script_errors, send_callback_response}, - script::{Script, ScriptId, Scripts, StaticScripts}, + script::{Script, ScriptId, Scripts, StaticScripts, DisplayProxy}, IntoScriptPluginParams, }; use bevy::{asset::Handle, ecs::entity::Entity, log::debug, prelude::Command}; @@ -41,7 +41,7 @@ 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), OnScriptUnloaded::into_callback_label(), vec![], @@ -65,7 +65,7 @@ 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, content: Box<[u8]>, asset: Option>, // Hack to make this Send, C does not need to be Send since it is not stored in the command @@ -75,7 +75,7 @@ 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]>, asset: Option>) -> Self { Self { id, content, @@ -84,16 +84,16 @@ impl CreateOrUpdateScript

{ } } - fn script_name(&self) -> String { - self.asset.as_ref().and_then(|handle| handle.path().map(|p| p.to_string())).unwrap_or_else(|| self.id.to_string()) - } + // fn script_name(&self) -> String { + // self.asset.as_ref().and_then(|handle| handle.path().map(|p| p.to_string())).unwrap_or_else(|| self.id.to_string()) + // } fn reload_context( &self, guard: WorldGuard, handler_ctxt: &HandlerContext

, ) -> Result<(), ScriptError> { - let existing_script = match handler_ctxt.scripts.scripts.get(&self.id) { + let existing_script = match handler_ctxt.scripts.scripts.get(&self.id.id()) { Some(script) => script, None => { return Err( @@ -108,7 +108,7 @@ impl CreateOrUpdateScript

{ (ContextBuilder::

::reload)( handler_ctxt.context_loading_settings.loader.reload, // &self.id, - &Handle::Weak(self.id), + &self.id, &self.content, &mut context, &handler_ctxt.context_loading_settings.context_initializers, @@ -130,7 +130,7 @@ impl CreateOrUpdateScript

{ let context = (ContextBuilder::

::load)( handler_ctxt.context_loading_settings.loader.load, // &self.id, - &Handle::Weak(self.id), + &self.id, &self.content, &handler_ctxt.context_loading_settings.context_initializers, &handler_ctxt @@ -142,8 +142,7 @@ impl CreateOrUpdateScript

{ let context = Arc::new(Mutex::new(context)); - handler_ctxt.scripts.scripts.insert( - self.id.clone(), + handler_ctxt.scripts.insert( Script { id: self.id.clone(), asset: self.asset.clone(), @@ -160,7 +159,7 @@ impl Command for CreateOrUpdateScript

{ let success = with_handler_system_state( world, |guard, handler_ctxt: &mut HandlerContext

| { - let is_new_script = !handler_ctxt.scripts.scripts.contains_key(&self.id); + let is_new_script = !handler_ctxt.scripts.scripts.contains_key(&self.id.id()); let assigned_shared_context = match handler_ctxt.context_loading_settings.assignment_strategy { @@ -181,12 +180,12 @@ impl Command for CreateOrUpdateScript

{ }; debug!( - "{}: CreateOrUpdateScript command applying (script_id: {}, new context?: {}, new script?: {})", - P::LANGUAGE, - self.id, - assigned_shared_context.is_none(), - is_new_script - ); + "{}: CreateOrUpdateScript command applying (script {}, new context?: {}, new script?: {})", + P::LANGUAGE, + self.id.display(), + assigned_shared_context.is_none(), + is_new_script + ); let result = match &assigned_shared_context { Some(assigned_shared_context) => { @@ -200,13 +199,13 @@ impl Command for CreateOrUpdateScript

{ }; // 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); + handler_ctxt.scripts.insert(script); } - bevy::log::debug!("{}: reloading script with id: {}", P::LANGUAGE, self.id); + bevy::log::debug!("{}: reloading script {}", P::LANGUAGE, self.id.display()); self.reload_context(guard.clone(), handler_ctxt) } None => { - bevy::log::debug!("{}: loading script with id: {}", P::LANGUAGE, self.id); + bevy::log::debug!("{}: loading script {}", P::LANGUAGE, self.id.display()); self.load_context(guard.clone(), handler_ctxt) } }; @@ -221,7 +220,7 @@ impl Command for CreateOrUpdateScript

{ handle_script_errors( guard, vec![err - .with_script(self.script_name()) + .with_script(self.id.display()) .with_context(P::LANGUAGE) .with_context(phrase)] .into_iter(), @@ -230,9 +229,9 @@ impl Command for CreateOrUpdateScript

{ } bevy::log::debug!( - "{}: script with id: {} successfully created or updated", + "{}: script {} successfully created or updated", P::LANGUAGE, - self.id + self.id.display() ); true @@ -256,7 +255,7 @@ impl Command for CreateOrUpdateScript

{ /// 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: AssetId, + pub id: Handle, /// The entity to use for the callback pub entity: Entity, /// The callback to run @@ -274,7 +273,7 @@ pub struct RunScriptCallback { impl RunScriptCallback

{ /// Creates a new RunCallbackCommand with the given ID, callback and arguments pub fn new( - id: AssetId, + id: Handle, entity: Entity, callback: CallbackLabel, args: Vec, @@ -301,11 +300,11 @@ 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()) { + if !handler_ctxt.is_script_fully_loaded(self.id.id()) { bevy::log::error!( - "{}: Cannot apply callback command, as script does not exist: {}. Ignoring.", + "{}: Cannot apply callback command, as script {} does not exist. Ignoring.", P::LANGUAGE, - self.id + self.id.display() ); return; } @@ -323,14 +322,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); } diff --git a/crates/bevy_mod_scripting_core/src/error.rs b/crates/bevy_mod_scripting_core/src/error.rs index cf2d37e5c3..9ccb66a719 100644 --- a/crates/bevy_mod_scripting_core/src/error.rs +++ b/crates/bevy_mod_scripting_core/src/error.rs @@ -9,7 +9,7 @@ use crate::{ script_value::ScriptValue, ReflectBaseType, ReflectReference, }, - script::ScriptId, + script::{DisplayProxy, ScriptId}, }; use bevy::{ asset::Handle, @@ -1255,17 +1255,10 @@ macro_rules! unregistered_component_or_resource_type { macro_rules! missing_script_for_callback { ($script_id:expr) => { - if let Some(path) = $script_id.path() { - format!( - "Could not find script with path: {}. Is the script loaded?", - path - ) - } else { - format!( - "Could not find script with id: {}. Is the script loaded?", - $script_id.id() - ) - } + format!( + "Could not find script {}. Is the script loaded?", + $script_id.display() + ) }; } diff --git a/crates/bevy_mod_scripting_core/src/extractors.rs b/crates/bevy_mod_scripting_core/src/extractors.rs index 2ea79676c7..1b8f3fa17d 100644 --- a/crates/bevy_mod_scripting_core/src/extractors.rs +++ b/crates/bevy_mod_scripting_core/src/extractors.rs @@ -19,6 +19,7 @@ use bevy::{ use fixedbitset::FixedBitSet; use crate::{ + ScriptAsset, bindings::{ access_map::ReflectAccessId, pretty_print::DisplayWithWorld, script_value::ScriptValue, WorldAccessGuard, WorldGuard, @@ -208,16 +209,16 @@ impl HandlerContext<'_, P> { pub fn call_dynamic_label( &self, label: &CallbackLabel, - script_id: &ScriptId, + script_id: &Handle, entity: Entity, payload: Vec, guard: WorldGuard<'_>, ) -> Result { // find script - let script = match self.scripts.scripts.get(script_id) { + let script = match self.scripts.scripts.get(&script_id.id()) { Some(script) => script, // NOTE: It'd be nice to use a handle here because then we have the path. - None => return Err(InteropError::missing_script(Handle::Weak(script_id.clone())).into()), + None => return Err(InteropError::missing_script(script_id.clone()).into()), }; // call the script @@ -248,7 +249,7 @@ 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, payload: Vec, guard: WorldGuard<'_>, diff --git a/crates/bevy_mod_scripting_core/src/handler.rs b/crates/bevy_mod_scripting_core/src/handler.rs index 15714fd56a..09a0d1eae2 100644 --- a/crates/bevy_mod_scripting_core/src/handler.rs +++ b/crates/bevy_mod_scripting_core/src/handler.rs @@ -31,7 +31,7 @@ use bevy::{ pub type HandlerFn

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

::C, pre_handling_initializers: &[ContextPreHandlingInitializer

], @@ -73,7 +73,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

], @@ -199,7 +199,7 @@ pub(crate) fn event_handler_inner( let call_result = handler_ctxt.call_dynamic_label( &callback_label, - &script_id.id(), + &script_id, *entity, event.args.clone(), guard.clone(), diff --git a/crates/bevy_mod_scripting_core/src/script.rs b/crates/bevy_mod_scripting_core/src/script.rs index ae8052b34c..55a565ed8f 100644 --- a/crates/bevy_mod_scripting_core/src/script.rs +++ b/crates/bevy_mod_scripting_core/src/script.rs @@ -2,15 +2,54 @@ use crate::{asset::ScriptAsset, IntoScriptPluginParams}; use bevy::prelude::ReflectComponent; -use bevy::{asset::{AssetId, Handle}, ecs::system::Resource, reflect::Reflect, utils::HashSet}; +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}; +use std::{borrow::Cow, collections::HashMap, ops::Deref, sync::Arc, fmt}; /// 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. +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) + } +} + #[derive(bevy::ecs::component::Component, Reflect, Clone)] #[reflect(Component)] /// A component which identifies the scripts existing on an entity. @@ -43,7 +82,7 @@ pub struct Scripts { impl Scripts

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

) { - self.scripts.insert(script.id.clone(), script); + self.scripts.insert(script.id.id(), script); } /// Removes a script from the collection, returning `true` if the script was in the collection, `false` otherwise @@ -89,7 +128,8 @@ impl Default for Scripts

{ /// A script pub struct Script { /// The id of the script - pub id: ScriptId, + pub id: Handle, + /// TODO: Let's remove asset if possible. /// 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 From 3a612564c24bcfebbffc7615b80b8db8cec2c189 Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Sat, 28 Jun 2025 01:01:07 -0400 Subject: [PATCH 09/41] refactor: Bring test harness in line. --- crates/languages/bevy_mod_scripting_lua/src/lib.rs | 7 ++++--- .../script_integration_test_harness/Cargo.toml | 3 ++- .../script_integration_test_harness/src/lib.rs | 9 +++++---- tests/script_tests.rs | 12 ++++++++++-- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/crates/languages/bevy_mod_scripting_lua/src/lib.rs b/crates/languages/bevy_mod_scripting_lua/src/lib.rs index 197026eba6..f9e625a766 100644 --- a/crates/languages/bevy_mod_scripting_lua/src/lib.rs +++ b/crates/languages/bevy_mod_scripting_lua/src/lib.rs @@ -275,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" @@ -293,7 +294,7 @@ mod test { .unwrap(); lua_context_reload( - &script_id, + &handle, "function hello_world_from_second_load() end" 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 fd66be0693..ee513681fa 100644 --- a/crates/testing_crates/script_integration_test_harness/src/lib.rs +++ b/crates/testing_crates/script_integration_test_harness/src/lib.rs @@ -309,7 +309,7 @@ fn run_test_callback( } let res = handler_ctxt.call::( - &script_id, + &Handle::Weak(*script_id), Entity::from_raw(0), vec![], guard.clone(), @@ -505,14 +505,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, + Handle::Weak(random_script_id), content.clone().into(), None, ), diff --git a/tests/script_tests.rs b/tests/script_tests.rs index cbb451abd6..f07a32df9f 100644 --- a/tests/script_tests.rs +++ b/tests/script_tests.rs @@ -4,7 +4,9 @@ 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, + #[cfg(feature = "rhai")] + execute_rhai_integration_test, }; use test_utils::{discover_all_tests, Test, TestKind}; @@ -20,7 +22,13 @@ impl TestExecutor for Test { 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::Rhai => { + #[cfg(feature = "rhai")] + execute_rhai_integration_test(&self.path.to_string_lossy())? + #[cfg(not(feature = "rhai"))] + panic!("no 'rhai' feature") + }, + } Ok(()) From 4b3ade0ea5841963a046495ef5afd81b80bf86f2 Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Sat, 28 Jun 2025 01:26:34 -0400 Subject: [PATCH 10/41] test: Tests are running! --- .../bevy_mod_scripting_rhai/src/lib.rs | 19 +++++++------- .../src/lib.rs | 26 +++++++++---------- tests/script_tests.rs | 12 ++++----- 3 files changed, 29 insertions(+), 28 deletions(-) diff --git a/crates/languages/bevy_mod_scripting_rhai/src/lib.rs b/crates/languages/bevy_mod_scripting_rhai/src/lib.rs index a1a477cf5e..a02d410a79 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/src/lib.rs b/crates/testing_crates/script_integration_test_harness/src/lib.rs index ee513681fa..8663b18fcf 100644 --- a/crates/testing_crates/script_integration_test_harness/src/lib.rs +++ b/crates/testing_crates/script_integration_test_harness/src/lib.rs @@ -17,7 +17,7 @@ use bevy::{ world::{Command, FromWorld, Mut}, }, log::Level, - prelude::{Entity, World}, + prelude::{Entity, World, Commands}, reflect::{Reflect, TypeRegistry}, utils::tracing, }; @@ -33,7 +33,7 @@ use bevy_mod_scripting_core::{ event::{IntoCallbackLabel, ScriptErrorEvent}, extractors::{HandlerContext, WithWorldGuard}, handler::handle_script_errors, - script::ScriptId, + script::{ScriptComponent, ScriptId}, BMSScriptingInfrastructurePlugin, IntoScriptPluginParams, ScriptingPlugin, }; use bevy_mod_scripting_functions::ScriptFunctionsPlugin; @@ -201,14 +201,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()); @@ -234,28 +235,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::); diff --git a/tests/script_tests.rs b/tests/script_tests.rs index f07a32df9f..2106508ec5 100644 --- a/tests/script_tests.rs +++ b/tests/script_tests.rs @@ -5,8 +5,6 @@ use std::path::PathBuf; use libtest_mimic::{Arguments, Failed, Trial}; use script_integration_test_harness::{ execute_lua_integration_test, - #[cfg(feature = "rhai")] - execute_rhai_integration_test, }; use test_utils::{discover_all_tests, Test, TestKind}; @@ -23,10 +21,12 @@ impl TestExecutor for Test { match self.kind { TestKind::Lua => execute_lua_integration_test(&self.path.to_string_lossy())?, TestKind::Rhai => { - #[cfg(feature = "rhai")] - execute_rhai_integration_test(&self.path.to_string_lossy())? - #[cfg(not(feature = "rhai"))] - panic!("no 'rhai' feature") + if cfg!(feature = "rhai") { + #[cfg(feature = "rhai")] + script_integration_test_harness::execute_rhai_integration_test(&self.path.to_string_lossy())? + } else { + panic!("no 'rhai' feature") + } }, } From 77ce7a902e47cb1f4f47ac0d523cdd038f732c62 Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Sat, 28 Jun 2025 01:30:44 -0400 Subject: [PATCH 11/41] test: Make test runnable without rhai. --- tests/script_tests.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/script_tests.rs b/tests/script_tests.rs index 2106508ec5..06d93fb5c9 100644 --- a/tests/script_tests.rs +++ b/tests/script_tests.rs @@ -16,16 +16,19 @@ 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::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 { - panic!("no 'rhai' feature") + println!("Skipping test: {:?}", self.path); } }, From 84f5095591e8c193426e3d147354c386232e4fae Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Sat, 28 Jun 2025 02:25:04 -0400 Subject: [PATCH 12/41] excise: Remove ScriptMetadata and ScriptEvent. We don't need them since asset's know their language. --- crates/bevy_mod_scripting_core/src/asset.rs | 215 +++++--------------- 1 file changed, 48 insertions(+), 167 deletions(-) diff --git a/crates/bevy_mod_scripting_core/src/asset.rs b/crates/bevy_mod_scripting_core/src/asset.rs index d7b20a1cd8..929cdd1a6f 100644 --- a/crates/bevy_mod_scripting_core/src/asset.rs +++ b/crates/bevy_mod_scripting_core/src/asset.rs @@ -55,16 +55,18 @@ impl std::fmt::Display for Language { #[derive(Asset, TypePath, Clone)] pub struct ScriptAsset { /// The body of the script - pub content: Box<[u8]>, + pub content: Box<[u8]>, // Any chance a Cow<'static, ?> could work here? /// The language of the script pub language: Language, } -#[derive(Event, Debug, Clone)] -pub(crate) enum ScriptAssetEvent { - Added(ScriptMetadata), - Removed(ScriptMetadata), - Modified(ScriptMetadata), +impl From for ScriptAsset { + fn from(s: String) -> ScriptAsset { + ScriptAsset { + content: s.into_bytes().into_boxed_slice(), + language: Language::default(), + } + } } /// Script settings @@ -112,6 +114,7 @@ impl AssetLoader for ScriptAssetLoader { 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 @@ -129,178 +132,58 @@ impl AssetLoader for ScriptAssetLoader { } } - -/// 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: ScriptId, 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: ScriptId) -> Option<&ScriptMetadata> { - self.map.get(&id) - } - - /// Removes a metadata entry - pub fn remove(&mut self, id: ScriptId) -> Option { - self.map.remove(&id) - } - - /// Checks if the store contains a metadata entry - pub fn contains(&self, id: ScriptId) -> bool { - self.map.contains_key(&id) - } -} - -/// Converts incoming asset events, into internal script asset events, also loads and inserts metadata for newly added scripts -#[profiling::function] -pub(crate) fn dispatch_script_asset_events( - mut events: EventReader>, - mut script_asset_events: EventWriter, - assets: Res>, - mut metadata_store: ResMut, - // settings: Res, -) { - for event in events.read() { - 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); - let language = &asset.language; - 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); - } - let metadata = ScriptMetadata { - asset_id: *id, - // script_id, - language: language.clone(), - }; - 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); - } - } - } - 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); - } - } - _ => {} - } - } -} - -/// 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); - } - } - } -} - -/// Listens to [`ScriptAssetEvent`] events and dispatches [`CreateOrUpdateScript`] and [`DeleteScript`] commands accordingly. +/// Listens to [`AssetEvent`] 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, + mut events: EventReader>, script_assets: Res>, static_scripts: 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; - } 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 {} is for a different langauge than this sync system. Skipping.", - P::LANGUAGE, - metadata.asset_id - ); - continue; - } + 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 langauge than this sync system. Skipping.", + P::LANGUAGE, + path); + } + None => { + trace!( + "{}: Script id {} is for a different langauge than this sync system. Skipping.", + P::LANGUAGE, + id); + } + } + continue; + } - if static_scripts.iter().any(|handle| handle.id() == metadata.asset_id) { - info!("{}: Loading static script: {:?}", P::LANGUAGE, metadata.asset_id,); - if let Some(asset) = script_assets.get(metadata.asset_id) { + if static_scripts.iter().any(|handle| handle.id() == *id) { + info!("{}: Loading static script: {:?}", P::LANGUAGE, id); commands.queue(CreateOrUpdateScript::

::new( - Handle::Weak(metadata.asset_id.clone()), + Handle::Weak(*id), asset.content.clone(), Some(script_assets.reserve_handle().clone_weak()), )); } } } - ScriptAssetEvent::Removed(_) => { - info!("{}: Deleting Script: {:?}", P::LANGUAGE, metadata.asset_id,); - commands.queue(DeleteScript::

::new(metadata.asset_id.clone())); + AssetEvent::Removed{ id } => { + info!("{}: Deleting Script: {:?}", P::LANGUAGE, id); + commands.queue(DeleteScript::

::new(id.clone())); + } + AssetEvent::Unused { id } => { } }; } @@ -364,13 +247,14 @@ pub(crate) fn eval_script( 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, ( @@ -379,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 } From 2450f153fd0c34b76adb239311aa2b262facbc4e Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Sat, 28 Jun 2025 02:32:29 -0400 Subject: [PATCH 13/41] excise: Drop Script::asset field. It's a duplicate with Script::id now. --- crates/bevy_mod_scripting_core/src/asset.rs | 4 +--- crates/bevy_mod_scripting_core/src/commands.rs | 6 +----- crates/bevy_mod_scripting_core/src/script.rs | 4 ---- .../script_integration_test_harness/src/lib.rs | 1 - 4 files changed, 2 insertions(+), 13 deletions(-) diff --git a/crates/bevy_mod_scripting_core/src/asset.rs b/crates/bevy_mod_scripting_core/src/asset.rs index 929cdd1a6f..c7ec898be3 100644 --- a/crates/bevy_mod_scripting_core/src/asset.rs +++ b/crates/bevy_mod_scripting_core/src/asset.rs @@ -173,8 +173,7 @@ pub(crate) fn sync_script_data( info!("{}: Loading static script: {:?}", P::LANGUAGE, id); commands.queue(CreateOrUpdateScript::

::new( Handle::Weak(*id), - asset.content.clone(), - Some(script_assets.reserve_handle().clone_weak()), + asset.content.clone(), // Cloning seems bad! )); } } @@ -228,7 +227,6 @@ pub(crate) fn eval_script( commands.queue(CreateOrUpdateScript::

::new( script_id, asset.content.clone(), - None, )); } } else { diff --git a/crates/bevy_mod_scripting_core/src/commands.rs b/crates/bevy_mod_scripting_core/src/commands.rs index fb532daf58..54beb5c893 100644 --- a/crates/bevy_mod_scripting_core/src/commands.rs +++ b/crates/bevy_mod_scripting_core/src/commands.rs @@ -67,7 +67,6 @@ impl Command for DeleteScript

{ pub struct CreateOrUpdateScript { id: Handle, content: Box<[u8]>, - asset: 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, } @@ -75,11 +74,10 @@ pub struct CreateOrUpdateScript { #[profiling::all_functions] impl CreateOrUpdateScript

{ /// Creates a new CreateOrUpdateScript command with the given ID, content and asset - pub fn new(id: Handle, content: Box<[u8]>, asset: Option>) -> Self { + pub fn new(id: Handle, content: Box<[u8]>) -> Self { Self { id, content, - asset, _ph: std::marker::PhantomData, } } @@ -145,7 +143,6 @@ impl CreateOrUpdateScript

{ handler_ctxt.scripts.insert( Script { id: self.id.clone(), - asset: self.asset.clone(), context, }, ); @@ -194,7 +191,6 @@ impl Command for CreateOrUpdateScript

{ // 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 diff --git a/crates/bevy_mod_scripting_core/src/script.rs b/crates/bevy_mod_scripting_core/src/script.rs index 55a565ed8f..e61e7b177c 100644 --- a/crates/bevy_mod_scripting_core/src/script.rs +++ b/crates/bevy_mod_scripting_core/src/script.rs @@ -129,9 +129,6 @@ impl Default for Scripts

{ pub struct Script { /// The id of the script pub id: Handle, - /// TODO: Let's remove asset if possible. - /// 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>, } @@ -140,7 +137,6 @@ impl Clone for Script

{ fn clone(&self) -> Self { Self { id: self.id.clone(), - asset: self.asset.clone(), context: self.context.clone(), } } 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 8663b18fcf..a9efaa1fc9 100644 --- a/crates/testing_crates/script_integration_test_harness/src/lib.rs +++ b/crates/testing_crates/script_integration_test_harness/src/lib.rs @@ -515,7 +515,6 @@ pub fn run_plugin_script_load_benchmark< CreateOrUpdateScript::

::new( Handle::Weak(random_script_id), content.clone().into(), - None, ), is_reload, ) From e0fa4231761f938fc131cb63650a071189bf00e8 Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Sat, 28 Jun 2025 03:03:24 -0400 Subject: [PATCH 14/41] perf: Can create_or_update_script w/o clone. --- .../bevy_mod_scripting_core/src/commands.rs | 192 ++++++++++-------- 1 file changed, 102 insertions(+), 90 deletions(-) diff --git a/crates/bevy_mod_scripting_core/src/commands.rs b/crates/bevy_mod_scripting_core/src/commands.rs index 54beb5c893..bb61d44cf9 100644 --- a/crates/bevy_mod_scripting_core/src/commands.rs +++ b/crates/bevy_mod_scripting_core/src/commands.rs @@ -10,7 +10,7 @@ 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, DisplayProxy}, IntoScriptPluginParams, @@ -66,6 +66,7 @@ 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: Handle, + // It feels like we're using a Box, which requires a clone merely to satisfy the Command trait. content: Box<[u8]>, // Hack to make this Send, C does not need to be Send since it is not stored in the command _ph: std::marker::PhantomData, @@ -85,13 +86,14 @@ impl CreateOrUpdateScript

{ // fn script_name(&self) -> String { // self.asset.as_ref().and_then(|handle| handle.path().map(|p| p.to_string())).unwrap_or_else(|| self.id.to_string()) // } - + // fn reload_context( - &self, + id: &Handle, + content: &[u8], guard: WorldGuard, handler_ctxt: &HandlerContext

, ) -> Result<(), ScriptError> { - let existing_script = match handler_ctxt.scripts.scripts.get(&self.id.id()) { + let existing_script = match handler_ctxt.scripts.scripts.get(&id.id()) { Some(script) => script, None => { return Err( @@ -105,9 +107,8 @@ impl CreateOrUpdateScript

{ (ContextBuilder::

::reload)( handler_ctxt.context_loading_settings.loader.reload, - // &self.id, - &self.id, - &self.content, + &id, + content, &mut context, &handler_ctxt.context_loading_settings.context_initializers, &handler_ctxt @@ -121,15 +122,15 @@ impl CreateOrUpdateScript

{ } fn load_context( - &self, + id: &Handle, + content: &[u8], guard: WorldGuard, handler_ctxt: &mut HandlerContext

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

::load)( handler_ctxt.context_loading_settings.loader.load, - // &self.id, - &self.id, - &self.content, + &id, + content, &handler_ctxt.context_loading_settings.context_initializers, &handler_ctxt .context_loading_settings @@ -142,98 +143,108 @@ impl CreateOrUpdateScript

{ handler_ctxt.scripts.insert( Script { - id: self.id.clone(), + id: id.clone(), context, }, ); Ok(()) } + + pub(crate) fn create_or_update_script( + id: &Handle, + content: &[u8], + guard: WorldGuard, handler_ctxt: &mut HandlerContext

) -> bool { + + let is_new_script = !handler_ctxt.scripts.contains_key(&id.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 {}, new context?: {}, new script?: {})", + P::LANGUAGE, + id.display(), + 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 { + // NOTE: We don't want script handles to be strong, right? + id: id.clone_weak(), + 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.insert(script); + } + bevy::log::debug!("{}: reloading script {}", P::LANGUAGE, id.display()); + Self::reload_context(id, content, guard.clone(), handler_ctxt) + } + None => { + bevy::log::debug!("{}: loading script {}", P::LANGUAGE, id.display()); + Self::load_context(id, content, 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(id.display()) + .with_context(P::LANGUAGE) + .with_context(phrase)] + .into_iter(), + ); + return false; + } + + bevy::log::debug!( + "{}: script {} successfully created or updated", + P::LANGUAGE, + id.display() + ); + + true + } + } + #[profiling::all_functions] impl Command for CreateOrUpdateScript

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

| { - let is_new_script = !handler_ctxt.scripts.scripts.contains_key(&self.id.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 {}, new context?: {}, new script?: {})", - P::LANGUAGE, - self.id.display(), - 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(), - 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.insert(script); - } - bevy::log::debug!("{}: reloading script {}", P::LANGUAGE, self.id.display()); - self.reload_context(guard.clone(), handler_ctxt) - } - None => { - bevy::log::debug!("{}: loading script {}", P::LANGUAGE, self.id.display()); - 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.display()) - .with_context(P::LANGUAGE) - .with_context(phrase)] - .into_iter(), - ); - return false; - } - - bevy::log::debug!( - "{}: script {} successfully created or updated", - P::LANGUAGE, - self.id.display() - ); - - true - }, - ); - + Self::create_or_update_script(&self.id, &self.content, + guard, handler_ctxt) + }); // immediately run command for callback, but only if loading went fine if success { RunScriptCallback::

::new( @@ -248,6 +259,7 @@ impl Command for CreateOrUpdateScript

{ } } + /// 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 From db83951f49f95d43a53be54e1369f81d3437c7de Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Sat, 28 Jun 2025 06:25:25 -0400 Subject: [PATCH 15/41] feature: Try to use the entities. --- .../src/bindings/script_system.rs | 32 ++- .../bevy_mod_scripting_core/src/commands.rs | 262 +++++++++++------- .../bevy_mod_scripting_core/src/extractors.rs | 52 ++-- crates/bevy_mod_scripting_core/src/lib.rs | 5 +- crates/bevy_mod_scripting_core/src/script.rs | 76 +---- 5 files changed, 226 insertions(+), 201 deletions(-) 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 53d4e66fc7..64ec0a06ec 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/script_system.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/script_system.rs @@ -18,12 +18,12 @@ use crate::{ extractors::get_all_access_ids, handler::CallbackSettings, runtime::RuntimeContainer, - script::{ScriptId, Scripts}, + script::{ScriptId, Script}, IntoScriptPluginParams, }; use bevy::{ asset::Handle, - prelude::AssetServer, + prelude::{Query, AssetServer}, ecs::{ archetype::{ArchetypeComponentId, ArchetypeGeneration}, component::{ComponentId, Tick}, @@ -199,7 +199,7 @@ impl ScriptSystemBuilder { } struct DynamicHandlerContext<'w, P: IntoScriptPluginParams> { - scripts: &'w Scripts

, + // scripts: &'w QueryState<&'static Script

>, callback_settings: &'w CallbackSettings

, context_loading_settings: &'w ContextLoadingSettings

, runtime_container: &'w RuntimeContainer

, @@ -213,9 +213,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"); @@ -226,7 +225,7 @@ impl<'w, P: IntoScriptPluginParams> DynamicHandlerContext<'w, P> { .resource_id::>() .expect("RuntimeContainer resource not found"); - access.add_resource_read(scripts_res_id); + access.add_component_read(world.component_id::>().unwrap()); 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); @@ -241,7 +240,6 @@ 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"), callback_settings: system .get_resource() .expect("CallbackSettings resource not found"), @@ -263,11 +261,20 @@ impl<'w, P: IntoScriptPluginParams> DynamicHandlerContext<'w, P> { entity: Entity, payload: Vec, guard: WorldGuard<'_>, + script_component_id: ComponentId, ) -> Result { // find script - let script = match self.scripts.scripts.get(&script_id.id()) { - Some(script) => script, - None => return Err(InteropError::missing_script(script_id.clone()).into()), + + let access = ReflectAccessId::for_component_id(script_component_id); + // It'd be nice to have the component id for Script

somewhere. + let context = if guard.claim_read_access(access) { + let world = guard.as_unsafe_world_cell_readonly()?; + let world = unsafe { world.world() }; + let maybe_context = world.get::>(entity).and_then(|script| script.contexts.get(&script_id.id())).map(|context| context.clone()); + unsafe { guard.release_access(access) }; + maybe_context.ok_or_else(||InteropError::missing_script(script_id.clone()))? + } else { + return Err(InteropError::missing_script(script_id.clone()).into()); }; // call the script @@ -277,7 +284,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, @@ -503,6 +510,7 @@ impl System for DynamicScriptSystem

{ Entity::from_raw(0), payload, guard.clone(), + world.components().component_id::>().expect("Script

component id"), ); // TODO: emit error events via commands, maybe accumulate in state instead and use apply diff --git a/crates/bevy_mod_scripting_core/src/commands.rs b/crates/bevy_mod_scripting_core/src/commands.rs index bb61d44cf9..a3207b770c 100644 --- a/crates/bevy_mod_scripting_core/src/commands.rs +++ b/crates/bevy_mod_scripting_core/src/commands.rs @@ -12,10 +12,10 @@ use crate::{ }, extractors::{with_handler_system_state, HandlerContext, WithWorldGuard}, handler::{handle_script_errors, send_callback_response}, - script::{Script, ScriptId, Scripts, StaticScripts, DisplayProxy}, + script::{Script, ScriptId, StaticScripts, DisplayProxy}, IntoScriptPluginParams, }; -use bevy::{asset::Handle, ecs::entity::Entity, log::debug, prelude::Command}; +use bevy::{asset::Handle, ecs::entity::Entity, log::debug, prelude::{EntityCommand, Command}}; use parking_lot::Mutex; use std::{marker::PhantomData, sync::Arc}; @@ -49,15 +49,16 @@ impl Command for DeleteScript

{ ) .apply(world); - let mut scripts = world.get_resource_or_init::>(); - if scripts.remove(self.id.clone()) { - debug!("Deleted script with id: {}", self.id); - } else { - bevy::log::error!( - "Attempted to delete script with id: {} but it does not exist, doing nothing!", - self.id - ); - } + todo!() + // let mut scripts = world.get_resource_or_init::>(); + // if scripts.remove(self.id.clone()) { + // debug!("Deleted script with id: {}", self.id); + // } else { + // bevy::log::error!( + // "Attempted to delete script with id: {} but it does not exist, doing nothing!", + // self.id + // ); + // } } } @@ -83,42 +84,33 @@ impl CreateOrUpdateScript

{ } } - // fn script_name(&self) -> String { - // self.asset.as_ref().and_then(|handle| handle.path().map(|p| p.to_string())).unwrap_or_else(|| self.id.to_string()) - // } - // fn reload_context( id: &Handle, content: &[u8], + context: &mut P::C, guard: WorldGuard, handler_ctxt: &HandlerContext

, ) -> Result<(), ScriptError> { - let existing_script = match handler_ctxt.scripts.scripts.get(&id.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, &id, content, - &mut context, + 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( @@ -126,7 +118,7 @@ impl CreateOrUpdateScript

{ content: &[u8], guard: WorldGuard, handler_ctxt: &mut HandlerContext

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

::load)( handler_ctxt.context_loading_settings.loader.load, &id, @@ -139,39 +131,41 @@ impl CreateOrUpdateScript

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

) -> bool { - - let is_new_script = !handler_ctxt.scripts.contains_key(&id.id()); + guard: WorldGuard, + handler_ctxt: &mut HandlerContext

) -> Result>, ScriptError> { + let (is_new_script, is_new_context)= { + let maybe_entity = handler_ctxt.scripts.get(entity); + let has_context = maybe_entity.map(|script| script.contexts.contains_key(&id.id())).unwrap_or(false); + (maybe_entity.is_err(), !has_context) + }; 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 - } + handler_ctxt.scripts.iter() + .next() + .and_then(|s| s.contexts.values().next()) + .map(|c| c.clone()) + // 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 + // } } }; @@ -183,54 +177,87 @@ impl CreateOrUpdateScript

{ is_new_script ); - let result = match &assigned_shared_context { + let phrase = if assigned_shared_context.is_some() { + "reloading" + } else { + "loading" + }; + + + let result = match assigned_shared_context { Some(assigned_shared_context) => { + bevy::log::debug!("{}: reloading script {}", P::LANGUAGE, id.display()); if is_new_script { - // this will happen when sharing contexts - // make a new script with the shared context - let script = Script { - // NOTE: We don't want script handles to be strong, right? - id: id.clone_weak(), - context: assigned_shared_context.clone(), - }; + // This will happen when sharing contexts. + // Make a new script with the shared context. + // let mut script = Script { + // // NOTE: We don't want script handles to be strong, right? + // // id: id.clone_weak(), + // contexts: [(id.id(), assigned_shared_context)] + // .into_iter() + // .collect(), + // }; + let context_arc = assigned_shared_context.clone(); + let mut context = context_arc.lock(); // it can potentially be loaded but without a successful script reload but that // leaves us in an okay state - handler_ctxt.scripts.insert(script); + // handler_ctxt.scripts.insert(script); + Self::reload_context(id, content, &mut context, guard.clone(), handler_ctxt) + .map(|_| Some(Script::

{ + contexts: [(id.id(), assigned_shared_context)].into_iter().collect() + })) + + } else { + let mut context = if is_new_context { + assigned_shared_context.clone() + } else { + let script = handler_ctxt.scripts.get(entity).unwrap(); + script.contexts.get(&id.id()).unwrap().clone() + }; + let mut lcontext = context.lock(); + Self::reload_context(id, content, &mut lcontext, guard.clone(), handler_ctxt) + .map(|_| { + if is_new_context { + let mut script = handler_ctxt.scripts.get_mut(entity).unwrap(); + script.contexts.insert(id.id(), assigned_shared_context); + } + None + }) } - bevy::log::debug!("{}: reloading script {}", P::LANGUAGE, id.display()); - Self::reload_context(id, content, guard.clone(), handler_ctxt) + // Self::reload_context(id, content, guard.clone(), handler_ctxt).map(|_| None) } None => { bevy::log::debug!("{}: loading script {}", P::LANGUAGE, id.display()); Self::load_context(id, content, guard.clone(), handler_ctxt) + .map(|context| + Some(Script::

{ + contexts: [(id.id(), Arc::new(Mutex::new(context)))].into_iter().collect() + }) + ) } }; - let phrase = if assigned_shared_context.is_some() { - "reloading" - } else { - "loading" - }; - - if let Err(err) = result { - handle_script_errors( - guard, - vec![err - .with_script(id.display()) - .with_context(P::LANGUAGE) - .with_context(phrase)] - .into_iter(), - ); - return false; + match result { + ok @ Ok(_) => { + bevy::log::debug!( + "{}: script {} successfully created or updated", + P::LANGUAGE, + id.display() + ); + ok + } + Err(err) => { + handle_script_errors( + guard, + vec![err.clone() + .with_script(id.display()) + .with_context(P::LANGUAGE) + .with_context(phrase)] + .into_iter(), + ); + Err(err) + } } - - bevy::log::debug!( - "{}: script {} successfully created or updated", - P::LANGUAGE, - id.display() - ); - - true } } @@ -239,22 +266,61 @@ impl CreateOrUpdateScript

{ #[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

| { + // Self::create_or_update_script(&self.id, &self.content, + // guard, handler_ctxt) + // }); + + // // immediately run command for callback, but only if loading went fine + // match result { + // Ok(maybe_context) => { + // let maybe_entity = maybe_context.map(|context| + // world.spawn(Script { + // contexts: [(self.id.id(), context)].collect() + // }).id()); + + // RunScriptCallback::

::new( + // self.id, + // maybe_entity.unwrap_or(Entity::from_raw(0)), + // OnScriptLoaded::into_callback_label(), + // vec![], + // false, + // ) + // .apply(world) + // } + // Err(_) => () + // } + } +} + +#[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(&self.id, &self.content, + Self::create_or_update_script(entity, &self.id, &self.content, 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) => { + let maybe_entity = maybe_script.map(|script| world.spawn(script).id()); + + RunScriptCallback::

::new( + self.id, + maybe_entity.unwrap_or(Entity::from_raw(0)), + OnScriptLoaded::into_callback_label(), + vec![], + false, + ) + .apply(world) + } + Err(_) => () } } } diff --git a/crates/bevy_mod_scripting_core/src/extractors.rs b/crates/bevy_mod_scripting_core/src/extractors.rs index 1b8f3fa17d..e1eeb5eccc 100644 --- a/crates/bevy_mod_scripting_core/src/extractors.rs +++ b/crates/bevy_mod_scripting_core/src/extractors.rs @@ -5,7 +5,7 @@ use std::ops::{Deref, DerefMut}; use bevy::{ - asset::Handle, + asset::{LoadState, Handle}, ecs::{ component::ComponentId, entity::Entity, @@ -14,7 +14,8 @@ use bevy::{ storage::SparseSetIndex, system::{Local, Resource, SystemParam, SystemState}, world::World, - } + }, + prelude::{AssetServer, Query, Res}, }; use fixedbitset::FixedBitSet; @@ -29,7 +30,7 @@ use crate::{ event::{CallbackLabel, IntoCallbackLabel}, handler::CallbackSettings, runtime::RuntimeContainer, - script::{ScriptId, Scripts, StaticScripts}, + script::{ScriptId, StaticScripts, Script}, IntoScriptPluginParams, }; @@ -51,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> { @@ -140,16 +149,17 @@ impl EventReaderScope<'_, T> { /// Context for systems which handle events for scripts #[derive(SystemParam)] pub struct HandlerContext<'s, P: IntoScriptPluginParams> { + // pub(crate) scripts: Query<'w, 's, &'static mut Script

>, /// Settings for callbacks 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>, + /// The asset server + pub(crate) asset_server: Res<'s, AssetServer>, // Not default. } impl HandlerContext<'_, P> { @@ -162,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, ) @@ -185,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 @@ -202,7 +205,7 @@ impl HandlerContext<'_, P> { /// 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) + matches!(self.asset_server.load_state(script_id), LoadState::Loaded) } /// Equivalent to [`Self::call`] but with a dynamically passed in label @@ -215,11 +218,12 @@ impl HandlerContext<'_, P> { guard: WorldGuard<'_>, ) -> Result { // find script - let script = match self.scripts.scripts.get(&script_id.id()) { - Some(script) => script, - // NOTE: It'd be nice to use a handle here because then we have the path. - None => return Err(InteropError::missing_script(script_id.clone()).into()), - }; + let mut context = self.scripts + .get(entity) + .ok() + .and_then(|script| + script.contexts.get(&script_id.id())) + .ok_or_else(|| InteropError::missing_script(script_id.clone()))?; // call the script let handler = self.callback_settings.callback_handler; @@ -228,7 +232,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, diff --git a/crates/bevy_mod_scripting_core/src/lib.rs b/crates/bevy_mod_scripting_core/src/lib.rs index 353d2ccafe..f73d92da5d 100644 --- a/crates/bevy_mod_scripting_core/src/lib.rs +++ b/crates/bevy_mod_scripting_core/src/lib.rs @@ -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}; pub mod asset; pub mod bindings; @@ -128,8 +128,7 @@ impl Plugin for ScriptingPlugin

{ assignment_strategy: self.context_assignment_strategy, context_initializers: self.context_initializers.clone(), context_pre_handling_initializers: self.context_pre_handling_initializers.clone(), - }) - .init_resource::>(); + }); register_script_plugin_systems::

(app); diff --git a/crates/bevy_mod_scripting_core/src/script.rs b/crates/bevy_mod_scripting_core/src/script.rs index e61e7b177c..28a62af7ac 100644 --- a/crates/bevy_mod_scripting_core/src/script.rs +++ b/crates/bevy_mod_scripting_core/src/script.rs @@ -1,7 +1,7 @@ //! Script related types, functions and components use crate::{asset::ScriptAsset, IntoScriptPluginParams}; -use bevy::prelude::ReflectComponent; +use bevy::prelude::{Component, ReflectComponent}; 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}; @@ -72,75 +72,23 @@ impl ScriptComponent { } } -/// 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.id(), 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 +#[derive(Component)] pub struct Script { /// The id of the script - pub id: Handle, + // pub id: Handle, /// The context of the script, possibly shared with other scripts - pub context: Arc>, + pub contexts: HashMap>>, } -impl Clone for Script

{ - fn clone(&self) -> Self { - Self { - id: self.id.clone(), - context: self.context.clone(), - } - } -} +// impl Clone for Script

{ +// fn clone(&self) -> Self { +// Self { +// id: self.id.clone(), +// context: self.context.clone(), +// } +// } +// } /// A collection of scripts, not associated with any entity. /// From 42d54b0124182d8165e6e15749443ec596b0892a Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Sat, 28 Jun 2025 07:34:53 -0400 Subject: [PATCH 16/41] hack: It compiles but idk why. I fought with HandlerContext trying to add a Query to it for the longest time. --- crates/bevy_mod_scripting_core/src/extractors.rs | 9 +++++---- crates/bevy_mod_scripting_core/src/handler.rs | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/bevy_mod_scripting_core/src/extractors.rs b/crates/bevy_mod_scripting_core/src/extractors.rs index e1eeb5eccc..a87408a120 100644 --- a/crates/bevy_mod_scripting_core/src/extractors.rs +++ b/crates/bevy_mod_scripting_core/src/extractors.rs @@ -146,10 +146,11 @@ impl EventReaderScope<'_, T> { } } + /// Context for systems which handle events for scripts #[derive(SystemParam)] -pub struct HandlerContext<'s, P: IntoScriptPluginParams> { - // pub(crate) scripts: Query<'w, 's, &'static mut Script

>, +pub struct HandlerContext<'w, 's, P: IntoScriptPluginParams> { + pub(crate) scripts: Query<'w, 's, &'static mut Script

>, /// Settings for callbacks pub(crate) callback_settings: ResScope<'s, CallbackSettings

>, /// Settings for loading contexts @@ -159,10 +160,10 @@ pub struct HandlerContext<'s, P: IntoScriptPluginParams> { /// List of static scripts pub(crate) static_scripts: ResScope<'s, StaticScripts>, /// The asset server - pub(crate) asset_server: Res<'s, AssetServer>, // Not default. + pub(crate) asset_server: Res<'w, AssetServer>, // No Default. } -impl HandlerContext<'_, P> { +impl<'w: 's, 's, P: IntoScriptPluginParams> HandlerContext<'w, 's, P> { /// Splits the handler context into its individual components. /// /// Useful if you are needing multiple resources from the handler context. diff --git a/crates/bevy_mod_scripting_core/src/handler.rs b/crates/bevy_mod_scripting_core/src/handler.rs index 09a0d1eae2..4fce0d71be 100644 --- a/crates/bevy_mod_scripting_core/src/handler.rs +++ b/crates/bevy_mod_scripting_core/src/handler.rs @@ -133,7 +133,7 @@ pub fn event_handler( pub(crate) type EventHandlerSystemState<'w, 's, P> = SystemState<( Local<'s, QueryState<(Entity, Ref<'w, ScriptComponent>)>>, crate::extractors::EventReaderScope<'s, ScriptCallbackEvent>, - WithWorldGuard<'w, 's, HandlerContext<'s, P>>, + WithWorldGuard<'w, 's, HandlerContext<'w, 's, P>>, )>; #[profiling::function] From 82cd159c25f73e2a3510ce9d856dbe1e38180182 Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Sat, 28 Jun 2025 07:53:01 -0400 Subject: [PATCH 17/41] hack: Compiles but doesn't run well. Log is flooded with these two lines: ```log 2025-06-28T11:46:54.321715Z ERROR bevy_mod_scripting_core::handler: Rhai: Failed to query entities with scripts: Cannot claim access to base type: Global. The base is already claimed by something else in a way which prevents safe access. Location: "/Users/shane/Projects/bevy_mod_scripting/crates/bevy_mod_scripting_core/src/extractors.rs:337". Context: Could not claim exclusive world access 2025-06-28T11:46:54.322046Z ERROR bevy_mod_scripting_core::handler: Lua: Failed to query entities with scripts: Cannot claim access to base type: Global. The base is already claimed by something else in a way which prevents safe access. Location: "/Users/shane/Projects/bevy_mod_scripting/crates/bevy_mod_scripting_core/src/extractors.rs:337". Context: Could not claim exclusive world access ``` --- crates/bevy_mod_scripting_core/src/extractors.rs | 3 ++- .../script_integration_test_harness/src/lib.rs | 13 +++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/crates/bevy_mod_scripting_core/src/extractors.rs b/crates/bevy_mod_scripting_core/src/extractors.rs index a87408a120..df61cbf49a 100644 --- a/crates/bevy_mod_scripting_core/src/extractors.rs +++ b/crates/bevy_mod_scripting_core/src/extractors.rs @@ -150,7 +150,8 @@ impl EventReaderScope<'_, T> { /// Context for systems which handle events for scripts #[derive(SystemParam)] pub struct HandlerContext<'w, 's, P: IntoScriptPluginParams> { - pub(crate) scripts: Query<'w, 's, &'static mut Script

>, + /// Query for `Script

`s + pub scripts: Query<'w, 's, &'static mut Script

>, /// Settings for callbacks pub(crate) callback_settings: ResScope<'s, CallbackSettings

>, /// Settings for loading contexts 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 a9efaa1fc9..4dad459e83 100644 --- a/crates/testing_crates/script_integration_test_harness/src/lib.rs +++ b/crates/testing_crates/script_integration_test_harness/src/lib.rs @@ -299,7 +299,7 @@ pub fn execute_integration_test<'a, fn run_test_callback( script_id: &ScriptId, - mut with_guard: WithWorldGuard<'_, '_, HandlerContext<'_, P>>, + mut with_guard: WithWorldGuard<'_, '_, HandlerContext<'_, '_, P>>, expect_response: bool, ) -> Result { let (guard, handler_ctxt) = with_guard.get_mut(); @@ -434,6 +434,7 @@ where 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])).id(); // app.add_systems( @@ -459,11 +460,11 @@ where 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 script_context = context + .scripts.get(entity).ok() + .and_then(|script| script.contexts.get(&script_id)) + .ok_or_else(|| String::from("Could not find script"))?; + let ctxt_arc = script_context.clone(); let mut ctxt_locked = ctxt_arc.lock(); let runtime = &context.runtime_container().runtime; From bb1372ff44c562d3493fd8998d4cac412ffcf1f4 Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Sun, 29 Jun 2025 03:19:22 -0400 Subject: [PATCH 18/41] feature: Use SharedContext(P::C) for global context. --- .../bevy_mod_scripting_core/src/commands.rs | 204 ++++++++++-------- crates/bevy_mod_scripting_core/src/context.rs | 7 + crates/bevy_mod_scripting_core/src/error.rs | 8 +- .../bevy_mod_scripting_core/src/extractors.rs | 48 +++-- crates/bevy_mod_scripting_core/src/handler.rs | 4 +- crates/bevy_mod_scripting_core/src/lib.rs | 5 +- crates/bevy_mod_scripting_core/src/script.rs | 12 +- .../src/lib.rs | 43 ++-- examples/game_of_life.rs | 4 + 9 files changed, 200 insertions(+), 135 deletions(-) diff --git a/crates/bevy_mod_scripting_core/src/commands.rs b/crates/bevy_mod_scripting_core/src/commands.rs index a3207b770c..db9c96a81c 100644 --- a/crates/bevy_mod_scripting_core/src/commands.rs +++ b/crates/bevy_mod_scripting_core/src/commands.rs @@ -141,110 +141,124 @@ impl CreateOrUpdateScript

{ content: &[u8], guard: WorldGuard, handler_ctxt: &mut HandlerContext

) -> Result>, ScriptError> { - let (is_new_script, is_new_context)= { - let maybe_entity = handler_ctxt.scripts.get(entity); - let has_context = maybe_entity.map(|script| script.contexts.contains_key(&id.id())).unwrap_or(false); - (maybe_entity.is_err(), !has_context) - }; - - let assigned_shared_context = - match handler_ctxt.context_loading_settings.assignment_strategy { - crate::context::ContextAssignmentStrategy::Individual => None, - crate::context::ContextAssignmentStrategy::Global => { - handler_ctxt.scripts.iter() - .next() - .and_then(|s| s.contexts.values().next()) - .map(|c| c.clone()) - // 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 {}, new context?: {}, new script?: {})", - P::LANGUAGE, - id.display(), - assigned_shared_context.is_none(), - is_new_script - ); - - let phrase = if assigned_shared_context.is_some() { - "reloading" - } else { - "loading" - }; - + // let (is_new_script, is_new_context)= { + // let maybe_entity = handler_ctxt.scripts.get(entity); + // let has_context = maybe_entity.map(|script| script.contexts.contains_key(&id.id())).unwrap_or(false); + // (maybe_entity.is_err(), !has_context) + // }; + let assignment_strategy = handler_ctxt.context_loading_settings.assignment_strategy; + // let assigned_shared_context: Option>> = + // match assignment_strategy { + // crate::context::ContextAssignmentStrategy::Individual => todo!(),//None, + // crate::context::ContextAssignmentStrategy::Global => { + + // if handler_ctxt.shared_context.is_none() { + // let handle: Handle = Handle::default(); + // handler_ctxt.shared_context = Self::load_context(&handle, b"", guard.clone(), handler_ctxt); + // } + // handler_ctxt.shared_context.clone(); + // // handler_ctxt.scripts.iter() + // // .next() + // // .and_then(|s| s.contexts.values().next()) + // // .map(|c| c.clone()) + // // 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 {}, new context?: {}, new script?: {})", + // P::LANGUAGE, + // id.display(), + // assigned_shared_context.is_none(), + // is_new_script + // ); + + // let phrase = if assigned_shared_context.is_some() { + // "reloading" + // } else { + // "loading" + // }; - let result = match assigned_shared_context { - Some(assigned_shared_context) => { + let phrase; + let result = match handler_ctxt.shared_context.clone() { + Some(context) => { bevy::log::debug!("{}: reloading script {}", P::LANGUAGE, id.display()); - if is_new_script { - // This will happen when sharing contexts. - // Make a new script with the shared context. - // let mut script = Script { - // // NOTE: We don't want script handles to be strong, right? - // // id: id.clone_weak(), - // contexts: [(id.id(), assigned_shared_context)] - // .into_iter() - // .collect(), + // if is_new_script { + // // This will happen when sharing contexts. + // // Make a new script with the shared context. + // // let mut script = Script { + // // // NOTE: We don't want script handles to be strong, right? + // // // id: id.clone_weak(), + // // contexts: [(id.id(), assigned_shared_context)] + // // .into_iter() + // // .collect(), + // // }; + // let context_arc = assigned_shared_context.clone(); + // let mut context = context_arc.lock(); + // // it can potentially be loaded but without a successful script reload but that + // // leaves us in an okay state + // // handler_ctxt.scripts.insert(script); + // Self::reload_context(id, content, &mut context, guard.clone(), handler_ctxt) + // .map(|_| Some(Script::

{ + // contexts: [(id.id(), assigned_shared_context)].into_iter().collect() + // })) + + // } else { + // let mut context = if is_new_context { + // assigned_shared_context.clone() + // } else { + // let script = handler_ctxt.scripts.get(entity).unwrap(); + // script.contexts.get(&id.id()).unwrap().clone() // }; - let context_arc = assigned_shared_context.clone(); - let mut context = context_arc.lock(); - // it can potentially be loaded but without a successful script reload but that - // leaves us in an okay state - // handler_ctxt.scripts.insert(script); - Self::reload_context(id, content, &mut context, guard.clone(), handler_ctxt) - .map(|_| Some(Script::

{ - contexts: [(id.id(), assigned_shared_context)].into_iter().collect() - })) - - } else { - let mut context = if is_new_context { - assigned_shared_context.clone() - } else { - let script = handler_ctxt.scripts.get(entity).unwrap(); - script.contexts.get(&id.id()).unwrap().clone() - }; let mut lcontext = context.lock(); - Self::reload_context(id, content, &mut lcontext, guard.clone(), handler_ctxt) - .map(|_| { - if is_new_context { - let mut script = handler_ctxt.scripts.get_mut(entity).unwrap(); - script.contexts.insert(id.id(), assigned_shared_context); - } - None - }) - } + phrase = "reloading"; + Self::reload_context(id, content, &mut lcontext, guard.clone(), handler_ctxt).map(|_| None) + // .map(|_| { + // if is_new_context { + // let mut script = handler_ctxt.scripts.get_mut(entity).unwrap(); + // script.contexts.insert(id.id(), assigned_shared_context); + // } + // None + // }) + // } // Self::reload_context(id, content, guard.clone(), handler_ctxt).map(|_| None) } None => { bevy::log::debug!("{}: loading script {}", P::LANGUAGE, id.display()); - Self::load_context(id, content, guard.clone(), handler_ctxt) - .map(|context| - Some(Script::

{ - contexts: [(id.id(), Arc::new(Mutex::new(context)))].into_iter().collect() - }) - ) + phrase = "loading"; + Self::load_context(id, content, guard.clone(), handler_ctxt).map(Some) + // .map(|context| + // Some(Script::

{ + // contexts: [(id.id(), Arc::new(Mutex::new(context)))].into_iter().collect() + // }) + // ) } }; match result { - ok @ Ok(_) => { + Ok(maybe_context) => { + if let Some(context) = maybe_context { + if assignment_strategy.is_global() && handler_ctxt.shared_context.is_none() { + **handler_ctxt.shared_context = Some(Arc::new(Mutex::new(context))); + } + } + bevy::log::debug!( "{}: script {} successfully created or updated", P::LANGUAGE, id.display() ); - ok + Ok(None)// none until individual context support added. } Err(err) => { handle_script_errors( @@ -374,14 +388,14 @@ 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.id()) { - bevy::log::error!( - "{}: Cannot apply callback command, as script {} does not exist. Ignoring.", - P::LANGUAGE, - self.id.display() - ); - 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, diff --git a/crates/bevy_mod_scripting_core/src/context.rs b/crates/bevy_mod_scripting_core/src/context.rs index b841551c71..ea300662af 100644 --- a/crates/bevy_mod_scripting_core/src/context.rs +++ b/crates/bevy_mod_scripting_core/src/context.rs @@ -165,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 9ccb66a719..c5d3ee4c85 100644 --- a/crates/bevy_mod_scripting_core/src/error.rs +++ b/crates/bevy_mod_scripting_core/src/error.rs @@ -601,7 +601,7 @@ impl InteropError { } /// 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(), })) @@ -814,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 { @@ -1285,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/extractors.rs b/crates/bevy_mod_scripting_core/src/extractors.rs index df61cbf49a..acf7408730 100644 --- a/crates/bevy_mod_scripting_core/src/extractors.rs +++ b/crates/bevy_mod_scripting_core/src/extractors.rs @@ -30,7 +30,7 @@ use crate::{ event::{CallbackLabel, IntoCallbackLabel}, handler::CallbackSettings, runtime::RuntimeContainer, - script::{ScriptId, StaticScripts, Script}, + script::{ScriptId, StaticScripts, Script, SharedContext, DisplayProxy}, IntoScriptPluginParams, }; @@ -149,9 +149,9 @@ impl EventReaderScope<'_, T> { /// Context for systems which handle events for scripts #[derive(SystemParam)] -pub struct HandlerContext<'w, 's, P: IntoScriptPluginParams> { - /// Query for `Script

`s - pub scripts: Query<'w, 's, &'static mut Script

>, +pub struct HandlerContext<'s, P: IntoScriptPluginParams> { + // Query for `Script

`s + // pub scripts: Query<'w, 's, &'static mut Script

>, /// Settings for callbacks pub(crate) callback_settings: ResScope<'s, CallbackSettings

>, /// Settings for loading contexts @@ -160,11 +160,13 @@ pub struct HandlerContext<'w, 's, P: IntoScriptPluginParams> { pub(crate) runtime_container: ResScope<'s, RuntimeContainer

>, /// List of static scripts pub(crate) static_scripts: ResScope<'s, StaticScripts>, - /// The asset server - pub(crate) asset_server: Res<'w, AssetServer>, // No Default. + /// Shared context if it exists + pub(crate) shared_context: ResScope<'s, SharedContext

>, + // The asset server + // pub(crate) asset_server: ResScope<'w, AssetServer>, // No Default. } -impl<'w: 's, 's, P: IntoScriptPluginParams> HandlerContext<'w, 's, 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. @@ -205,11 +207,17 @@ impl<'w: 's, 's, P: IntoScriptPluginParams> HandlerContext<'w, 's, 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 { - matches!(self.asset_server.load_state(script_id), LoadState::Loaded) + /// Get the static scripts + pub fn shared_context(&mut self) -> &mut SharedContext

{ + &mut self.shared_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, @@ -220,12 +228,20 @@ impl<'w: 's, 's, P: IntoScriptPluginParams> HandlerContext<'w, 's, P> { guard: WorldGuard<'_>, ) -> Result { // find script - let mut context = self.scripts - .get(entity) - .ok() - .and_then(|script| - script.contexts.get(&script_id.id())) - .ok_or_else(|| InteropError::missing_script(script_id.clone()))?; + let context = if self.context_loading_settings.assignment_strategy.is_global() { + let Some(context) = self.shared_context.as_ref() else { + return Err(InteropError::missing_context(script_id.clone()).into()); + }; + context + } else { + todo!() + }; + // let mut context = self.scripts + // .get(entity) + // .ok() + // .and_then(|script| + // script.contexts.get(&script_id.id())) + // .ok_or_else(|| InteropError::missing_script(script_id.clone()))?; // call the script let handler = self.callback_settings.callback_handler; diff --git a/crates/bevy_mod_scripting_core/src/handler.rs b/crates/bevy_mod_scripting_core/src/handler.rs index 4fce0d71be..e441bd3ab3 100644 --- a/crates/bevy_mod_scripting_core/src/handler.rs +++ b/crates/bevy_mod_scripting_core/src/handler.rs @@ -133,7 +133,7 @@ pub fn event_handler( pub(crate) type EventHandlerSystemState<'w, 's, P> = SystemState<( Local<'s, QueryState<(Entity, Ref<'w, ScriptComponent>)>>, crate::extractors::EventReaderScope<'s, ScriptCallbackEvent>, - WithWorldGuard<'w, 's, HandlerContext<'w, 's, P>>, + WithWorldGuard<'w, 's, HandlerContext<'s, P>>, )>; #[profiling::function] @@ -168,7 +168,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()) diff --git a/crates/bevy_mod_scripting_core/src/lib.rs b/crates/bevy_mod_scripting_core/src/lib.rs index f73d92da5d..9ed0aecc58 100644 --- a/crates/bevy_mod_scripting_core/src/lib.rs +++ b/crates/bevy_mod_scripting_core/src/lib.rs @@ -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, StaticScripts}; +use script::{ScriptComponent, ScriptId, StaticScripts, SharedContext}; pub mod asset; pub mod bindings; @@ -128,7 +128,8 @@ impl Plugin for ScriptingPlugin

{ assignment_strategy: self.context_assignment_strategy, context_initializers: self.context_initializers.clone(), context_pre_handling_initializers: self.context_pre_handling_initializers.clone(), - }); + }) + .init_resource::>(); register_script_plugin_systems::

(app); diff --git a/crates/bevy_mod_scripting_core/src/script.rs b/crates/bevy_mod_scripting_core/src/script.rs index 28a62af7ac..691b7dc299 100644 --- a/crates/bevy_mod_scripting_core/src/script.rs +++ b/crates/bevy_mod_scripting_core/src/script.rs @@ -1,7 +1,7 @@ //! Script related types, functions and components use crate::{asset::ScriptAsset, IntoScriptPluginParams}; -use bevy::prelude::{Component, ReflectComponent}; +use bevy::prelude::{Component, ReflectComponent, Deref, DerefMut}; 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}; @@ -122,6 +122,16 @@ impl StaticScripts { } } +/// Contains the shared context. +#[derive(Resource, Deref, DerefMut)] +pub struct SharedContext(pub Option>>); + +impl Default for SharedContext

{ + fn default() -> Self { + Self(None) + } +} + #[cfg(test)] mod tests { use super::*; 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 4dad459e83..7eca38bd28 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, AssetPath, AssetId}, + asset::{AssetServer, Handle, AssetPath, AssetId, LoadState}, ecs::{ component::Component, event::{Event, Events}, @@ -33,7 +34,7 @@ use bevy_mod_scripting_core::{ event::{IntoCallbackLabel, ScriptErrorEvent}, extractors::{HandlerContext, WithWorldGuard}, handler::handle_script_errors, - script::{ScriptComponent, ScriptId}, + script::{ScriptComponent, ScriptId, DisplayProxy}, BMSScriptingInfrastructurePlugin, IntoScriptPluginParams, ScriptingPlugin, }; use bevy_mod_scripting_functions::ScriptFunctionsPlugin; @@ -94,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( @@ -299,14 +300,14 @@ pub fn execute_integration_test<'a, fn run_test_callback( script_id: &ScriptId, - mut with_guard: WithWorldGuard<'_, '_, HandlerContext<'_, '_, P>>, + 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) { - return Ok(ScriptValue::Unit); - } + // if !handler_ctxt.is_script_fully_loaded(*script_id) { + // return Ok(ScriptValue::Unit); + // } let res = handler_ctxt.call::( &Handle::Weak(*script_id), @@ -434,7 +435,7 @@ where 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])).id(); + let entity = app.world_mut().spawn(ScriptComponent(vec![script_handle.clone()])).id(); // app.add_systems( @@ -453,18 +454,30 @@ 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 = context - .scripts.get(entity).ok() - .and_then(|script| script.contexts.get(&script_id)) - .ok_or_else(|| String::from("Could not find script"))?; - let ctxt_arc = script_context.clone(); + // if context.is_script_fully_loaded(script_id.clone().into()) { + // let script_context = context + // .scripts.get(entity).ok() + // .and_then(|script| script.contexts.get(&script_id)) + // .ok_or_else(|| String::from("Could not find script"))?; + let ctxt_arc = context.shared_context().clone().unwrap(); let mut ctxt_locked = ctxt_arc.lock(); let runtime = &context.runtime_container().runtime; @@ -477,7 +490,7 @@ where // 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()); diff --git a/examples/game_of_life.rs b/examples/game_of_life.rs index 586a56f919..e2b1bc7536 100644 --- a/examples/game_of_life.rs +++ b/examples/game_of_life.rs @@ -30,6 +30,7 @@ use bevy_mod_scripting_core::{ script::{ScriptId, ScriptComponent}, }; use bevy_mod_scripting_lua::LuaScriptingPlugin; +#[cfg(feature = "rhai")] use bevy_mod_scripting_rhai::RhaiScriptingPlugin; use clap::Parser; @@ -99,6 +100,7 @@ fn run_script_cmd( )); } + #[cfg(feature = "rhai")] for script_id in static_rhai_scripts.drain(..) { commands.queue(DeleteScript::::new( script_id @@ -143,8 +145,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), From a56f5825877bd61fb06f94c6609f492e9e9e28cf Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Sun, 29 Jun 2025 04:02:22 -0400 Subject: [PATCH 19/41] feature: GOL works with SharedContext. --- crates/bevy_mod_scripting_core/src/asset.rs | 23 ++--- .../bevy_mod_scripting_core/src/commands.rs | 88 ++++++++++--------- crates/bevy_mod_scripting_core/src/script.rs | 8 +- examples/game_of_life.rs | 2 + 4 files changed, 64 insertions(+), 57 deletions(-) diff --git a/crates/bevy_mod_scripting_core/src/asset.rs b/crates/bevy_mod_scripting_core/src/asset.rs index c7ec898be3..b051a3ccb5 100644 --- a/crates/bevy_mod_scripting_core/src/asset.rs +++ b/crates/bevy_mod_scripting_core/src/asset.rs @@ -15,7 +15,7 @@ use bevy::{ log::{debug, info, trace, warn}, prelude::{ Commands, Event, EventReader, EventWriter, IntoSystemConfigs, IntoSystemSetConfigs, Res, - ResMut, Added, Query, Local, Handle, AssetServer, + ResMut, Added, Query, Local, Handle, AssetServer, Entity, }, reflect::TypePath, utils::hashbrown::HashMap, @@ -139,7 +139,7 @@ impl AssetLoader for ScriptAssetLoader { pub(crate) fn sync_script_data( mut events: EventReader>, script_assets: Res>, - static_scripts: Res, + mut static_scripts: ResMut, asset_server: Res, mut commands: Commands, ) { @@ -179,8 +179,9 @@ pub(crate) fn sync_script_data( } } AssetEvent::Removed{ id } => { - info!("{}: Deleting Script: {:?}", P::LANGUAGE, id); - commands.queue(DeleteScript::

::new(id.clone())); + if static_scripts.remove(id) { + info!("{}: Removing static script {:?}", P::LANGUAGE, id); + } } AssetEvent::Unused { id } => { } @@ -189,19 +190,19 @@ pub(crate) fn sync_script_data( } pub(crate) fn eval_script( - script_comps: Query<&ScriptComponent, Added>, - mut script_queue: Local>>, + script_comps: Query<(Entity, &ScriptComponent), Added>, + mut script_queue: Local)>>, script_assets: Res>, asset_server: Res, mut commands: Commands, ) { - for script_comp in &script_comps { + for (id, script_comp) in &script_comps { for handle in &script_comp.0 { - script_queue.push_back(handle.clone_weak()); + script_queue.push_back((id, handle.clone_weak())); } } while ! script_queue.is_empty() { - let script_ready = script_queue.front().map(|script_id| match asset_server.load_state(&*script_id) { + 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 @@ -221,10 +222,10 @@ pub(crate) fn eval_script( // LoadState::Loaded => true, // _ => false // }) { - if let Some(script_id) = script_queue.pop_front() { + if let Some((id, script_id)) = script_queue.pop_front() { if let Some(asset) = script_assets.get(&script_id) { if asset.language == P::LANGUAGE { - commands.queue(CreateOrUpdateScript::

::new( + commands.entity(id).queue(CreateOrUpdateScript::

::new( script_id, asset.content.clone(), )); diff --git a/crates/bevy_mod_scripting_core/src/commands.rs b/crates/bevy_mod_scripting_core/src/commands.rs index db9c96a81c..1dfc6b7f2f 100644 --- a/crates/bevy_mod_scripting_core/src/commands.rs +++ b/crates/bevy_mod_scripting_core/src/commands.rs @@ -49,16 +49,15 @@ impl Command for DeleteScript

{ ) .apply(world); - todo!() - // let mut scripts = world.get_resource_or_init::>(); - // if scripts.remove(self.id.clone()) { - // debug!("Deleted script with id: {}", self.id); - // } else { - // bevy::log::error!( - // "Attempted to delete script with id: {} but it does not exist, doing nothing!", - // self.id - // ); - // } + let mut scripts = world.get_resource_or_init::(); + if scripts.remove(&self.id) { + debug!("Deleted script with id: {}", self.id); + } else { + bevy::log::error!( + "Attempted to delete script with id: {} but it does not exist, doing nothing!", + self.id + ); + } } } @@ -136,7 +135,7 @@ impl CreateOrUpdateScript

{ } pub(crate) fn create_or_update_script( - entity: Entity, + entity: Option, id: &Handle, content: &[u8], guard: WorldGuard, @@ -280,33 +279,34 @@ impl CreateOrUpdateScript

{ #[profiling::all_functions] impl Command for CreateOrUpdateScript

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

| { - // Self::create_or_update_script(&self.id, &self.content, - // guard, handler_ctxt) - // }); - - // // immediately run command for callback, but only if loading went fine - // match result { - // Ok(maybe_context) => { - // let maybe_entity = maybe_context.map(|context| - // world.spawn(Script { - // contexts: [(self.id.id(), context)].collect() - // }).id()); - - // RunScriptCallback::

::new( - // self.id, - // maybe_entity.unwrap_or(Entity::from_raw(0)), - // OnScriptLoaded::into_callback_label(), - // vec![], - // false, - // ) - // .apply(world) - // } - // Err(_) => () - // } + // todo!() + let result = with_handler_system_state( + world, + |guard, handler_ctxt: &mut HandlerContext

| { + Self::create_or_update_script(None, &self.id, &self.content, + guard, handler_ctxt) + }); + + // immediately run command for callback, but only if loading went fine + match result { + Ok(_maybe_script) => { + assert!(world.resource::>().assignment_strategy.is_global()); + // let maybe_entity = maybe_context.map(|context| + // world.spawn(Script { + // contexts: [(self.id.id(), context)].collect() + // }).id()); + + RunScriptCallback::

::new( + self.id, + Entity::from_raw(0), + OnScriptLoaded::into_callback_label(), + vec![], + false, + ) + .apply(world) + } + Err(_) => () + } } } @@ -316,18 +316,22 @@ impl EntityCommand for CreateOrUpdateScript

{ let result = with_handler_system_state( world, |guard, handler_ctxt: &mut HandlerContext

| { - Self::create_or_update_script(entity, &self.id, &self.content, + Self::create_or_update_script(Some(entity), &self.id, &self.content, guard, handler_ctxt) }); // immediately run command for callback, but only if loading went fine match result { Ok(maybe_script) => { - let maybe_entity = maybe_script.map(|script| world.spawn(script).id()); + // if ! world.resource::>().assignment_strategy.is_global() { + if let Some(script) = maybe_script { + world.entity_mut(entity).insert(script); + } + // } RunScriptCallback::

::new( self.id, - maybe_entity.unwrap_or(Entity::from_raw(0)), + entity, OnScriptLoaded::into_callback_label(), vec![], false, @@ -465,7 +469,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/script.rs b/crates/bevy_mod_scripting_core/src/script.rs index 691b7dc299..f8d95a391f 100644 --- a/crates/bevy_mod_scripting_core/src/script.rs +++ b/crates/bevy_mod_scripting_core/src/script.rs @@ -106,14 +106,14 @@ impl StaticScripts { } /// 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()) + 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: S) -> bool { - self.scripts.contains(&script.into()) + pub fn contains(&self, script_id: &ScriptId) -> bool { + self.scripts.iter().any(|handle| handle.id() == *script_id) } /// Returns an iterator over the static scripts diff --git a/examples/game_of_life.rs b/examples/game_of_life.rs index e2b1bc7536..f685d0f2d0 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}, @@ -132,6 +133,7 @@ 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::() From ba60f2ef496faff845f3aa72f70d6da4776c0ef5 Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Sun, 29 Jun 2025 06:30:08 -0400 Subject: [PATCH 20/41] feature: Works with ScriptContextProvider trait. --- Cargo.toml | 2 +- .../src/bindings/script_system.rs | 32 ++--- .../bevy_mod_scripting_core/src/commands.rs | 26 ++-- .../bevy_mod_scripting_core/src/extractors.rs | 23 +--- crates/bevy_mod_scripting_core/src/lib.rs | 3 +- crates/bevy_mod_scripting_core/src/script.rs | 118 ++++++++++++++---- .../src/lib.rs | 4 +- 7 files changed, 128 insertions(+), 80 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b5a0fe4c7d..0eaa1d93c0 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/src/bindings/script_system.rs b/crates/bevy_mod_scripting_core/src/bindings/script_system.rs index 64ec0a06ec..46ce42429f 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/script_system.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/script_system.rs @@ -18,7 +18,7 @@ use crate::{ extractors::get_all_access_ids, handler::CallbackSettings, runtime::RuntimeContainer, - script::{ScriptId, Script}, + script::{ScriptId, Scripts, ScriptContextProvider}, IntoScriptPluginParams, }; use bevy::{ @@ -199,7 +199,7 @@ impl ScriptSystemBuilder { } struct DynamicHandlerContext<'w, P: IntoScriptPluginParams> { - // scripts: &'w QueryState<&'static Script

>, + scripts: &'w Scripts

, callback_settings: &'w CallbackSettings

, context_loading_settings: &'w ContextLoadingSettings

, runtime_container: &'w RuntimeContainer

, @@ -225,7 +225,6 @@ impl<'w, P: IntoScriptPluginParams> DynamicHandlerContext<'w, P> { .resource_id::>() .expect("RuntimeContainer resource not found"); - access.add_component_read(world.component_id::>().unwrap()); 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); @@ -240,6 +239,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"), callback_settings: system .get_resource() .expect("CallbackSettings resource not found"), @@ -261,20 +263,23 @@ impl<'w, P: IntoScriptPluginParams> DynamicHandlerContext<'w, P> { entity: Entity, payload: Vec, guard: WorldGuard<'_>, - script_component_id: ComponentId, ) -> Result { // find script - let access = ReflectAccessId::for_component_id(script_component_id); + // let access = ReflectAccessId::for_component_id(script_component_id); // It'd be nice to have the component id for Script

somewhere. - let context = if guard.claim_read_access(access) { - let world = guard.as_unsafe_world_cell_readonly()?; - let world = unsafe { world.world() }; - let maybe_context = world.get::>(entity).and_then(|script| script.contexts.get(&script_id.id())).map(|context| context.clone()); - unsafe { guard.release_access(access) }; - maybe_context.ok_or_else(||InteropError::missing_script(script_id.clone()))? - } else { - return Err(InteropError::missing_script(script_id.clone()).into()); + // let context = if guard.claim_read_access(access) { + todo!(); + // let world = guard.as_unsafe_world_cell_readonly()?; + // let world = unsafe { world.world() }; + // let maybe_context = world.get::>(entity).and_then(|script| script.contexts.get(&script_id.id())).map(|context| context.clone()); + // unsafe { guard.release_access(access) }; + // maybe_context.ok_or_else(||InteropError::missing_script(script_id.clone()))? + // } else { + // return Err(InteropError::missing_script(script_id.clone()).into()); + // }; + let Some(context) = self.scripts.get(Some(entity), &script_id.id(), None) else { + return Err(InteropError::missing_context(script_id.clone()).into()); }; // call the script @@ -510,7 +515,6 @@ impl System for DynamicScriptSystem

{ Entity::from_raw(0), payload, guard.clone(), - world.components().component_id::>().expect("Script

component id"), ); // TODO: emit error events via commands, maybe accumulate in state instead and use apply diff --git a/crates/bevy_mod_scripting_core/src/commands.rs b/crates/bevy_mod_scripting_core/src/commands.rs index 1dfc6b7f2f..43942775ec 100644 --- a/crates/bevy_mod_scripting_core/src/commands.rs +++ b/crates/bevy_mod_scripting_core/src/commands.rs @@ -12,10 +12,10 @@ use crate::{ }, extractors::{with_handler_system_state, HandlerContext, WithWorldGuard}, handler::{handle_script_errors, send_callback_response}, - script::{Script, ScriptId, StaticScripts, DisplayProxy}, + script::{ScriptId, StaticScripts, DisplayProxy, ScriptContextProvider}, IntoScriptPluginParams, }; -use bevy::{asset::Handle, ecs::entity::Entity, log::debug, prelude::{EntityCommand, Command}}; +use bevy::{asset::Handle, ecs::entity::Entity, log::{warn, debug}, prelude::{EntityCommand, Command}}; use parking_lot::Mutex; use std::{marker::PhantomData, sync::Arc}; @@ -139,7 +139,7 @@ impl CreateOrUpdateScript

{ id: &Handle, content: &[u8], guard: WorldGuard, - handler_ctxt: &mut HandlerContext

) -> Result>, ScriptError> { + handler_ctxt: &mut HandlerContext

) -> Result<(), ScriptError> { // let (is_new_script, is_new_context)= { // let maybe_entity = handler_ctxt.scripts.get(entity); // let has_context = maybe_entity.map(|script| script.contexts.contains_key(&id.id())).unwrap_or(false); @@ -189,7 +189,7 @@ impl CreateOrUpdateScript

{ // }; let phrase; - let result = match handler_ctxt.shared_context.clone() { + let result = match handler_ctxt.shared_context.get(entity, &id.id(), None) { Some(context) => { bevy::log::debug!("{}: reloading script {}", P::LANGUAGE, id.display()); // if is_new_script { @@ -247,8 +247,8 @@ impl CreateOrUpdateScript

{ match result { Ok(maybe_context) => { if let Some(context) = maybe_context { - if assignment_strategy.is_global() && handler_ctxt.shared_context.is_none() { - **handler_ctxt.shared_context = Some(Arc::new(Mutex::new(context))); + if !handler_ctxt.shared_context.insert(entity, &id.id(), None, context) { + warn!("Unable to insert script context for entity {:?} script {}.", entity, id.display()); } } @@ -257,7 +257,7 @@ impl CreateOrUpdateScript

{ P::LANGUAGE, id.display() ); - Ok(None)// none until individual context support added. + Ok(())// none until individual context support added. } Err(err) => { handle_script_errors( @@ -289,12 +289,7 @@ impl Command for CreateOrUpdateScript

{ // immediately run command for callback, but only if loading went fine match result { - Ok(_maybe_script) => { - assert!(world.resource::>().assignment_strategy.is_global()); - // let maybe_entity = maybe_context.map(|context| - // world.spawn(Script { - // contexts: [(self.id.id(), context)].collect() - // }).id()); + Ok(_) => { RunScriptCallback::

::new( self.id, @@ -323,11 +318,6 @@ impl EntityCommand for CreateOrUpdateScript

{ // immediately run command for callback, but only if loading went fine match result { Ok(maybe_script) => { - // if ! world.resource::>().assignment_strategy.is_global() { - if let Some(script) = maybe_script { - world.entity_mut(entity).insert(script); - } - // } RunScriptCallback::

::new( self.id, diff --git a/crates/bevy_mod_scripting_core/src/extractors.rs b/crates/bevy_mod_scripting_core/src/extractors.rs index acf7408730..d1f18266c0 100644 --- a/crates/bevy_mod_scripting_core/src/extractors.rs +++ b/crates/bevy_mod_scripting_core/src/extractors.rs @@ -30,7 +30,7 @@ use crate::{ event::{CallbackLabel, IntoCallbackLabel}, handler::CallbackSettings, runtime::RuntimeContainer, - script::{ScriptId, StaticScripts, Script, SharedContext, DisplayProxy}, + script::{ScriptId, StaticScripts, SharedContext, DisplayProxy, Scripts, ScriptContextProvider}, IntoScriptPluginParams, }; @@ -150,8 +150,6 @@ impl EventReaderScope<'_, T> { /// Context for systems which handle events for scripts #[derive(SystemParam)] pub struct HandlerContext<'s, P: IntoScriptPluginParams> { - // Query for `Script

`s - // pub scripts: Query<'w, 's, &'static mut Script

>, /// Settings for callbacks pub(crate) callback_settings: ResScope<'s, CallbackSettings

>, /// Settings for loading contexts @@ -160,10 +158,10 @@ pub struct HandlerContext<'s, P: IntoScriptPluginParams> { pub(crate) runtime_container: ResScope<'s, RuntimeContainer

>, /// List of static scripts pub(crate) static_scripts: ResScope<'s, StaticScripts>, + /// List of static scripts + pub(crate) scripts: ResScope<'s, Scripts

>, /// Shared context if it exists pub(crate) shared_context: ResScope<'s, SharedContext

>, - // The asset server - // pub(crate) asset_server: ResScope<'w, AssetServer>, // No Default. } impl<'s, P: IntoScriptPluginParams> HandlerContext<'s, P> { @@ -228,20 +226,9 @@ impl<'s, P: IntoScriptPluginParams> HandlerContext<'s, P> { guard: WorldGuard<'_>, ) -> Result { // find script - let context = if self.context_loading_settings.assignment_strategy.is_global() { - let Some(context) = self.shared_context.as_ref() else { - return Err(InteropError::missing_context(script_id.clone()).into()); - }; - context - } else { - todo!() + let Some(context) = self.shared_context.get(Some(entity), &script_id.id(), None) else { + return Err(InteropError::missing_context(script_id.clone()).into()); }; - // let mut context = self.scripts - // .get(entity) - // .ok() - // .and_then(|script| - // script.contexts.get(&script_id.id())) - // .ok_or_else(|| InteropError::missing_script(script_id.clone()))?; // call the script let handler = self.callback_settings.callback_handler; diff --git a/crates/bevy_mod_scripting_core/src/lib.rs b/crates/bevy_mod_scripting_core/src/lib.rs index 9ed0aecc58..7d65dddea6 100644 --- a/crates/bevy_mod_scripting_core/src/lib.rs +++ b/crates/bevy_mod_scripting_core/src/lib.rs @@ -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, StaticScripts, SharedContext}; +use script::{ScriptComponent, ScriptId, StaticScripts, SharedContext, Scripts}; pub mod asset; pub mod bindings; @@ -129,6 +129,7 @@ impl Plugin for ScriptingPlugin

{ context_initializers: self.context_initializers.clone(), context_pre_handling_initializers: self.context_pre_handling_initializers.clone(), }) + .init_resource::>() .init_resource::>(); register_script_plugin_systems::

(app); diff --git a/crates/bevy_mod_scripting_core/src/script.rs b/crates/bevy_mod_scripting_core/src/script.rs index f8d95a391f..b57ca6b445 100644 --- a/crates/bevy_mod_scripting_core/src/script.rs +++ b/crates/bevy_mod_scripting_core/src/script.rs @@ -1,7 +1,7 @@ //! Script related types, functions and components use crate::{asset::ScriptAsset, IntoScriptPluginParams}; -use bevy::prelude::{Component, ReflectComponent, Deref, DerefMut}; +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}; @@ -72,23 +72,99 @@ impl ScriptComponent { } } -/// A script -#[derive(Component)] -pub struct Script { - /// The id of the script - // pub id: Handle, - /// The context of the script, possibly shared with other scripts - pub contexts: HashMap>>, +/// 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 = Option>; + +/// A generic script context provider +pub trait ScriptContextProvider { + /// Get the context. + fn get(&self, id: Option, script_id: &ScriptId, domain: Domain) -> Option<&Arc>>; + /// Insert a context. + fn insert(&mut self, id: Option, script_id: &ScriptId, domain: Domain, context: P::C) -> bool; + /// Returns true if there is a context. + fn contains(&self, id: Option, script_id: &ScriptId, domain: Domain) -> bool; +} + +#[derive(Resource)] +/// Keeps track of contexts +pub enum ScriptContext { + /// One shared script context + Shared(SharedContext

), + /// One script context per entity + PerEntity(Scripts

) +} + +impl ScriptContextProvider

for ScriptContext

{ + fn get(&self, id: Option, script_id: &ScriptId, domain: Domain) -> Option<&Arc>> { + match self { + ScriptContext::Shared(a) => a.get(id, script_id, domain), + ScriptContext::PerEntity(a) => a.get(id, script_id, domain), + } + } + fn insert(&mut self, id: Option, script_id: &ScriptId, domain: Domain, context: P::C) -> bool { + match self { + ScriptContext::Shared(a) => a.insert(id, script_id, domain, context), + ScriptContext::PerEntity(a) => a.insert(id, script_id, domain, context), + } + } + fn contains(&self, id: Option, script_id: &ScriptId, domain: Domain) -> bool { + match self { + ScriptContext::Shared(a) => a.contains(id, script_id, domain), + ScriptContext::PerEntity(a) => a.contains(id, script_id, domain), + } + } } -// impl Clone for Script

{ -// fn clone(&self) -> Self { -// Self { -// id: self.id.clone(), -// context: self.context.clone(), -// } -// } -// } +/// Stores the script context. +#[derive(Resource)] +pub struct Scripts(HashMap>>); + +impl Default for Scripts

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

for Scripts

{ + fn get(&self, id: Option, script_id: &ScriptId, domain: Domain) -> Option<&Arc>> { + id.and_then(|id| self.0.get(&id)) + } + fn insert(&mut self, id: Option, script_id: &ScriptId, domain: Domain, context: P::C) -> bool { + id.map(|id| { + self.0.insert(id, Arc::new(Mutex::new(context))); + true + }) + .unwrap_or(false) + } + fn contains(&self, id: Option, script_id: &ScriptId, domain: Domain) -> bool { + id.map(|id| self.0.contains_key(&id)).unwrap_or(false) + } +} + +/// Contains the shared context. +#[derive(Resource)] +pub struct SharedContext(pub Option>>); + +impl ScriptContextProvider

for SharedContext

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

{ + fn default() -> Self { + Self(None) + } +} /// A collection of scripts, not associated with any entity. /// @@ -122,16 +198,6 @@ impl StaticScripts { } } -/// Contains the shared context. -#[derive(Resource, Deref, DerefMut)] -pub struct SharedContext(pub Option>>); - -impl Default for SharedContext

{ - fn default() -> Self { - Self(None) - } -} - #[cfg(test)] mod tests { use super::*; 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 7eca38bd28..fd28ca7742 100644 --- a/crates/testing_crates/script_integration_test_harness/src/lib.rs +++ b/crates/testing_crates/script_integration_test_harness/src/lib.rs @@ -34,7 +34,7 @@ use bevy_mod_scripting_core::{ event::{IntoCallbackLabel, ScriptErrorEvent}, extractors::{HandlerContext, WithWorldGuard}, handler::handle_script_errors, - script::{ScriptComponent, ScriptId, DisplayProxy}, + script::{ScriptComponent, ScriptId, DisplayProxy, ScriptContextProvider}, BMSScriptingInfrastructurePlugin, IntoScriptPluginParams, ScriptingPlugin, }; use bevy_mod_scripting_functions::ScriptFunctionsPlugin; @@ -477,7 +477,7 @@ where // .scripts.get(entity).ok() // .and_then(|script| script.contexts.get(&script_id)) // .ok_or_else(|| String::from("Could not find script"))?; - let ctxt_arc = context.shared_context().clone().unwrap(); + let ctxt_arc = context.shared_context().get(Some(entity), &script_id, None).cloned().unwrap(); let mut ctxt_locked = ctxt_arc.lock(); let runtime = &context.runtime_container().runtime; From 6b819b3d7945ebc770de4d001a138b41c74d9b98 Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Sun, 29 Jun 2025 06:45:33 -0400 Subject: [PATCH 21/41] refactor: It's all coming together now. --- .../src/bindings/script_system.rs | 8 ++-- .../bevy_mod_scripting_core/src/commands.rs | 4 +- .../bevy_mod_scripting_core/src/extractors.rs | 14 +++--- crates/bevy_mod_scripting_core/src/lib.rs | 11 +++-- crates/bevy_mod_scripting_core/src/script.rs | 22 +++++---- .../src/lib.rs | 45 ++++++------------- 6 files changed, 45 insertions(+), 59 deletions(-) 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 46ce42429f..e97c409db9 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/script_system.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/script_system.rs @@ -18,7 +18,7 @@ use crate::{ extractors::get_all_access_ids, handler::CallbackSettings, runtime::RuntimeContainer, - script::{ScriptId, Scripts, ScriptContextProvider}, + script::{ScriptId, ScriptContextProvider, ScriptContext}, IntoScriptPluginParams, }; use bevy::{ @@ -199,7 +199,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

, @@ -239,7 +239,7 @@ impl<'w, P: IntoScriptPluginParams> DynamicHandlerContext<'w, P> { pub fn get_param(system: &UnsafeWorldCell<'w>) -> Self { unsafe { Self { - scripts: system + script_context: system .get_resource() .expect("Scripts resource not found"), callback_settings: system @@ -278,7 +278,7 @@ impl<'w, P: IntoScriptPluginParams> DynamicHandlerContext<'w, P> { // } else { // return Err(InteropError::missing_script(script_id.clone()).into()); // }; - let Some(context) = self.scripts.get(Some(entity), &script_id.id(), None) else { + let Some(context) = self.script_context.get(Some(entity), &script_id.id(), None) else { return Err(InteropError::missing_context(script_id.clone()).into()); }; diff --git a/crates/bevy_mod_scripting_core/src/commands.rs b/crates/bevy_mod_scripting_core/src/commands.rs index 43942775ec..f51f967031 100644 --- a/crates/bevy_mod_scripting_core/src/commands.rs +++ b/crates/bevy_mod_scripting_core/src/commands.rs @@ -189,7 +189,7 @@ impl CreateOrUpdateScript

{ // }; let phrase; - let result = match handler_ctxt.shared_context.get(entity, &id.id(), None) { + let result = match handler_ctxt.script_context.get(entity, &id.id(), None) { Some(context) => { bevy::log::debug!("{}: reloading script {}", P::LANGUAGE, id.display()); // if is_new_script { @@ -247,7 +247,7 @@ impl CreateOrUpdateScript

{ match result { Ok(maybe_context) => { if let Some(context) = maybe_context { - if !handler_ctxt.shared_context.insert(entity, &id.id(), None, context) { + if !handler_ctxt.script_context.insert(entity, &id.id(), None, context) { warn!("Unable to insert script context for entity {:?} script {}.", entity, id.display()); } } diff --git a/crates/bevy_mod_scripting_core/src/extractors.rs b/crates/bevy_mod_scripting_core/src/extractors.rs index d1f18266c0..9f18f8af69 100644 --- a/crates/bevy_mod_scripting_core/src/extractors.rs +++ b/crates/bevy_mod_scripting_core/src/extractors.rs @@ -30,7 +30,7 @@ use crate::{ event::{CallbackLabel, IntoCallbackLabel}, handler::CallbackSettings, runtime::RuntimeContainer, - script::{ScriptId, StaticScripts, SharedContext, DisplayProxy, Scripts, ScriptContextProvider}, + script::{ScriptId, StaticScripts, ScriptContext, DisplayProxy, ScriptContextProvider}, IntoScriptPluginParams, }; @@ -158,10 +158,8 @@ pub struct HandlerContext<'s, P: IntoScriptPluginParams> { pub(crate) runtime_container: ResScope<'s, RuntimeContainer

>, /// List of static scripts pub(crate) static_scripts: ResScope<'s, StaticScripts>, - /// List of static scripts - pub(crate) scripts: ResScope<'s, Scripts

>, - /// Shared context if it exists - pub(crate) shared_context: ResScope<'s, SharedContext

>, + /// Script context + pub(crate) script_context: ResScope<'s, ScriptContext

>, } impl<'s, P: IntoScriptPluginParams> HandlerContext<'s, P> { @@ -206,8 +204,8 @@ impl<'s, P: IntoScriptPluginParams> HandlerContext<'s, P> { } /// Get the static scripts - pub fn shared_context(&mut self) -> &mut SharedContext

{ - &mut self.shared_context + pub fn script_context(&mut self) -> &mut ScriptContext

{ + &mut self.script_context } // /// checks if the script is loaded such that it can be executed. @@ -226,7 +224,7 @@ impl<'s, P: IntoScriptPluginParams> HandlerContext<'s, P> { guard: WorldGuard<'_>, ) -> Result { // find script - let Some(context) = self.shared_context.get(Some(entity), &script_id.id(), None) else { + let Some(context) = self.script_context.get(Some(entity), &script_id.id(), None) else { return Err(InteropError::missing_context(script_id.clone()).into()); }; diff --git a/crates/bevy_mod_scripting_core/src/lib.rs b/crates/bevy_mod_scripting_core/src/lib.rs index 7d65dddea6..d361dd6b5e 100644 --- a/crates/bevy_mod_scripting_core/src/lib.rs +++ b/crates/bevy_mod_scripting_core/src/lib.rs @@ -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, StaticScripts, SharedContext, Scripts}; +use script::{ScriptComponent, ScriptId, StaticScripts, ScriptContext, EntityContext}; pub mod asset; pub mod bindings; @@ -128,9 +128,12 @@ impl Plugin for ScriptingPlugin

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

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

(app); diff --git a/crates/bevy_mod_scripting_core/src/script.rs b/crates/bevy_mod_scripting_core/src/script.rs index b57ca6b445..3fdad0d70c 100644 --- a/crates/bevy_mod_scripting_core/src/script.rs +++ b/crates/bevy_mod_scripting_core/src/script.rs @@ -93,41 +93,46 @@ pub enum ScriptContext { /// One shared script context Shared(SharedContext

), /// One script context per entity - PerEntity(Scripts

) + Entity(EntityContext

) +} + +impl Default for ScriptContext

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

for ScriptContext

{ fn get(&self, id: Option, script_id: &ScriptId, domain: Domain) -> Option<&Arc>> { match self { ScriptContext::Shared(a) => a.get(id, script_id, domain), - ScriptContext::PerEntity(a) => a.get(id, script_id, domain), + ScriptContext::Entity(a) => a.get(id, script_id, domain), } } fn insert(&mut self, id: Option, script_id: &ScriptId, domain: Domain, context: P::C) -> bool { match self { ScriptContext::Shared(a) => a.insert(id, script_id, domain, context), - ScriptContext::PerEntity(a) => a.insert(id, script_id, domain, context), + ScriptContext::Entity(a) => a.insert(id, script_id, domain, context), } } fn contains(&self, id: Option, script_id: &ScriptId, domain: Domain) -> bool { match self { ScriptContext::Shared(a) => a.contains(id, script_id, domain), - ScriptContext::PerEntity(a) => a.contains(id, script_id, domain), + ScriptContext::Entity(a) => a.contains(id, script_id, domain), } } } /// Stores the script context. -#[derive(Resource)] -pub struct Scripts(HashMap>>); +pub struct EntityContext(HashMap>>); -impl Default for Scripts

{ +impl Default for EntityContext

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

for Scripts

{ +impl ScriptContextProvider

for EntityContext

{ fn get(&self, id: Option, script_id: &ScriptId, domain: Domain) -> Option<&Arc>> { id.and_then(|id| self.0.get(&id)) } @@ -144,7 +149,6 @@ impl ScriptContextProvider

for Scripts

{ } /// Contains the shared context. -#[derive(Resource)] pub struct SharedContext(pub Option>>); impl ScriptContextProvider

for SharedContext

{ 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 fd28ca7742..8893b536cb 100644 --- a/crates/testing_crates/script_integration_test_harness/src/lib.rs +++ b/crates/testing_crates/script_integration_test_harness/src/lib.rs @@ -428,24 +428,11 @@ where install_test_plugin(&mut app, plugin, true); - // let script_id = script_id.to_owned(); - // let script_id_clone = script_id.clone(); - - 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(); - - // app.add_systems( - // Startup, - // move |server: Res, mut handle: Local>| { - // *handle = server.load(script_id_clone.to_owned()); - // handle.id() - // }, - // ); - // finalize app.cleanup(); app.finish(); @@ -472,25 +459,19 @@ where 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 = context - // .scripts.get(entity).ok() - // .and_then(|script| script.contexts.get(&script_id)) - // .ok_or_else(|| String::from("Could not find script"))?; - let ctxt_arc = context.shared_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) - }); - // } + 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()); From e9c4e1708fe20fa7a7c6255a44b1a9901d28c15f Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Sun, 29 Jun 2025 07:08:38 -0400 Subject: [PATCH 22/41] bug: Make per-entity work with static scripts. --- .../src/bindings/script_system.rs | 2 +- .../bevy_mod_scripting_core/src/commands.rs | 4 +- .../bevy_mod_scripting_core/src/extractors.rs | 2 +- crates/bevy_mod_scripting_core/src/lib.rs | 13 ++-- crates/bevy_mod_scripting_core/src/script.rs | 64 ++++++++++++------- .../src/lib.rs | 2 +- 6 files changed, 53 insertions(+), 34 deletions(-) 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 e97c409db9..bba4cd47d1 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/script_system.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/script_system.rs @@ -278,7 +278,7 @@ impl<'w, P: IntoScriptPluginParams> DynamicHandlerContext<'w, P> { // } else { // return Err(InteropError::missing_script(script_id.clone()).into()); // }; - let Some(context) = self.script_context.get(Some(entity), &script_id.id(), None) else { + let Some(context) = self.script_context.get(Some(entity), &script_id.id(), &None) else { return Err(InteropError::missing_context(script_id.clone()).into()); }; diff --git a/crates/bevy_mod_scripting_core/src/commands.rs b/crates/bevy_mod_scripting_core/src/commands.rs index f51f967031..8f49f6cc96 100644 --- a/crates/bevy_mod_scripting_core/src/commands.rs +++ b/crates/bevy_mod_scripting_core/src/commands.rs @@ -189,7 +189,7 @@ impl CreateOrUpdateScript

{ // }; let phrase; - let result = match handler_ctxt.script_context.get(entity, &id.id(), None) { + let result = match handler_ctxt.script_context.get(entity, &id.id(), &None) { Some(context) => { bevy::log::debug!("{}: reloading script {}", P::LANGUAGE, id.display()); // if is_new_script { @@ -247,7 +247,7 @@ impl CreateOrUpdateScript

{ match result { Ok(maybe_context) => { if let Some(context) = maybe_context { - if !handler_ctxt.script_context.insert(entity, &id.id(), None, context) { + if handler_ctxt.script_context.insert(entity, &id.id(), &None, context).is_err() { warn!("Unable to insert script context for entity {:?} script {}.", entity, id.display()); } } diff --git a/crates/bevy_mod_scripting_core/src/extractors.rs b/crates/bevy_mod_scripting_core/src/extractors.rs index 9f18f8af69..bfddb38bba 100644 --- a/crates/bevy_mod_scripting_core/src/extractors.rs +++ b/crates/bevy_mod_scripting_core/src/extractors.rs @@ -224,7 +224,7 @@ impl<'s, P: IntoScriptPluginParams> HandlerContext<'s, P> { guard: WorldGuard<'_>, ) -> Result { // find script - let Some(context) = self.script_context.get(Some(entity), &script_id.id(), None) else { + let Some(context) = self.script_context.get(Some(entity), &script_id.id(), &None) else { return Err(InteropError::missing_context(script_id.clone()).into()); }; diff --git a/crates/bevy_mod_scripting_core/src/lib.rs b/crates/bevy_mod_scripting_core/src/lib.rs index d361dd6b5e..ce4f3eb0e4 100644 --- a/crates/bevy_mod_scripting_core/src/lib.rs +++ b/crates/bevy_mod_scripting_core/src/lib.rs @@ -128,12 +128,13 @@ impl Plugin for ScriptingPlugin

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

::Entity(EntityContext::default())); - } + }) + .insert_resource( + if self.context_assignment_strategy.is_global() { + ScriptContext::

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

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

(app); diff --git a/crates/bevy_mod_scripting_core/src/script.rs b/crates/bevy_mod_scripting_core/src/script.rs index 3fdad0d70c..920e8bd1ff 100644 --- a/crates/bevy_mod_scripting_core/src/script.rs +++ b/crates/bevy_mod_scripting_core/src/script.rs @@ -80,11 +80,11 @@ pub type Domain = Option>; /// A generic script context provider pub trait ScriptContextProvider { /// Get the context. - fn get(&self, id: Option, script_id: &ScriptId, domain: Domain) -> Option<&Arc>>; + fn get(&self, id: Option, script_id: &ScriptId, domain: &Domain) -> Option<&Arc>>; /// Insert a context. - fn insert(&mut self, id: Option, script_id: &ScriptId, domain: Domain, context: P::C) -> bool; + fn insert(&mut self, id: Option, script_id: &ScriptId, domain: &Domain, context: P::C) -> Result<(), P::C>; /// Returns true if there is a context. - fn contains(&self, id: Option, script_id: &ScriptId, domain: Domain) -> bool; + fn contains(&self, id: Option, script_id: &ScriptId, domain: &Domain) -> bool; } #[derive(Resource)] @@ -93,37 +93,54 @@ pub enum ScriptContext { /// One shared script context Shared(SharedContext

), /// One script context per entity - Entity(EntityContext

) + /// + /// Stores by entity or as a shared context + Entity(EntityContext

, SharedContext

) +} + +impl ScriptContext

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

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

for ScriptContext

{ - fn get(&self, id: Option, script_id: &ScriptId, domain: Domain) -> Option<&Arc>> { + fn get(&self, id: Option, script_id: &ScriptId, domain: &Domain) -> Option<&Arc>> { match self { ScriptContext::Shared(a) => a.get(id, script_id, domain), - ScriptContext::Entity(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)), } } - fn insert(&mut self, id: Option, script_id: &ScriptId, domain: Domain, context: P::C) -> bool { + fn insert(&mut self, id: Option, script_id: &ScriptId, domain: &Domain, context: P::C) -> Result<(), P::C> { match self { ScriptContext::Shared(a) => a.insert(id, script_id, domain, context), - ScriptContext::Entity(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) + } } } - fn contains(&self, id: Option, script_id: &ScriptId, domain: Domain) -> bool { + fn contains(&self, id: Option, script_id: &ScriptId, domain: &Domain) -> bool { match self { ScriptContext::Shared(a) => a.contains(id, script_id, domain), - ScriptContext::Entity(a) => a.contains(id, script_id, domain), + ScriptContext::Entity(a, b) => a.contains(id, script_id, domain) || b.contains(id, script_id, domain), } } } -/// Stores the script context. +/// Stores the script context by entity. pub struct EntityContext(HashMap>>); impl Default for EntityContext

{ @@ -133,17 +150,18 @@ impl Default for EntityContext

{ } impl ScriptContextProvider

for EntityContext

{ - fn get(&self, id: Option, script_id: &ScriptId, domain: Domain) -> Option<&Arc>> { + fn get(&self, id: Option, script_id: &ScriptId, domain: &Domain) -> Option<&Arc>> { id.and_then(|id| self.0.get(&id)) } - fn insert(&mut self, id: Option, script_id: &ScriptId, domain: Domain, context: P::C) -> bool { - id.map(|id| { + fn insert(&mut self, id: Option, script_id: &ScriptId, domain: &Domain, context: P::C) -> Result<(), P::C> { + if let Some(id) = id { self.0.insert(id, Arc::new(Mutex::new(context))); - true - }) - .unwrap_or(false) + Ok(()) + } else { + Err(context) + } } - fn contains(&self, id: Option, script_id: &ScriptId, domain: Domain) -> bool { + fn contains(&self, id: Option, script_id: &ScriptId, domain: &Domain) -> bool { id.map(|id| self.0.contains_key(&id)).unwrap_or(false) } } @@ -152,14 +170,14 @@ impl ScriptContextProvider

for EntityContext

{ pub struct SharedContext(pub Option>>); impl ScriptContextProvider

for SharedContext

{ - fn get(&self, id: Option, script_id: &ScriptId, domain: Domain) -> Option<&Arc>> { + fn get(&self, id: Option, script_id: &ScriptId, domain: &Domain) -> Option<&Arc>> { self.0.as_ref() } - fn insert(&mut self, id: Option, script_id: &ScriptId, domain: Domain, context: P::C) -> bool { + fn insert(&mut self, id: Option, script_id: &ScriptId, domain: &Domain, context: P::C) -> Result<(), P::C> { self.0 = Some(Arc::new(Mutex::new(context))); - true + Ok(()) } - fn contains(&self, id: Option, script_id: &ScriptId, domain: Domain) -> bool { + fn contains(&self, id: Option, script_id: &ScriptId, domain: &Domain) -> bool { self.0.is_some() } } 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 8893b536cb..95a67ed3c4 100644 --- a/crates/testing_crates/script_integration_test_harness/src/lib.rs +++ b/crates/testing_crates/script_integration_test_harness/src/lib.rs @@ -459,7 +459,7 @@ where let mut handler_ctxt = state.get_mut(app.world_mut()); let (guard, context) = handler_ctxt.get_mut(); - let ctxt_arc = context.script_context().get(Some(entity), &script_id, None).cloned().unwrap(); + 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; From 8b55f82e28e0a36fb98f606b36fd21754b50c18e Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Sun, 29 Jun 2025 07:48:55 -0400 Subject: [PATCH 23/41] doc: Clarify EntityContext. Hide internal struct. --- crates/bevy_mod_scripting_core/src/script.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/bevy_mod_scripting_core/src/script.rs b/crates/bevy_mod_scripting_core/src/script.rs index 920e8bd1ff..d267052f15 100644 --- a/crates/bevy_mod_scripting_core/src/script.rs +++ b/crates/bevy_mod_scripting_core/src/script.rs @@ -12,6 +12,7 @@ use std::{borrow::Cow, collections::HashMap, ops::Deref, sync::Arc, fmt}; 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> { @@ -94,7 +95,8 @@ pub enum ScriptContext { Shared(SharedContext

), /// One script context per entity /// - /// Stores by entity or as a shared context + /// Stores context by entity with a shared context as a last resort when no + /// entity is provided. Entity(EntityContext

, SharedContext

) } From f1288db8343e0584d7174603af2b3d637737f7b7 Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Tue, 1 Jul 2025 04:52:18 -0400 Subject: [PATCH 24/41] partial: Add domain handling. --- crates/bevy_mod_scripting_core/src/asset.rs | 15 +- .../src/bindings/script_system.rs | 12 +- .../bevy_mod_scripting_core/src/commands.rs | 121 ++++------------- crates/bevy_mod_scripting_core/src/event.rs | 4 +- .../bevy_mod_scripting_core/src/extractors.rs | 8 +- crates/bevy_mod_scripting_core/src/handler.rs | 7 +- .../src/script/domain_context.rs | 27 ++++ .../src/script/entity_context.rs | 27 ++++ .../src/{script.rs => script/mod.rs} | 128 ++---------------- .../src/script/script_context.rs | 103 ++++++++++++++ .../src/script/scriptid_context.rs | 23 ++++ .../src/script/shared_context.rs | 23 ++++ .../bevy_mod_scripting_functions/src/core.rs | 3 +- 13 files changed, 274 insertions(+), 227 deletions(-) create mode 100644 crates/bevy_mod_scripting_core/src/script/domain_context.rs create mode 100644 crates/bevy_mod_scripting_core/src/script/entity_context.rs rename crates/bevy_mod_scripting_core/src/{script.rs => script/mod.rs} (51%) create mode 100644 crates/bevy_mod_scripting_core/src/script/script_context.rs create mode 100644 crates/bevy_mod_scripting_core/src/script/scriptid_context.rs create mode 100644 crates/bevy_mod_scripting_core/src/script/shared_context.rs diff --git a/crates/bevy_mod_scripting_core/src/asset.rs b/crates/bevy_mod_scripting_core/src/asset.rs index f216c14e4c..de734ae37d 100644 --- a/crates/bevy_mod_scripting_core/src/asset.rs +++ b/crates/bevy_mod_scripting_core/src/asset.rs @@ -5,7 +5,7 @@ use crate::{ ScriptComponent, commands::{CreateOrUpdateScript, DeleteScript}, error::ScriptError, - script::{DisplayProxy, ScriptId}, + script::{DisplayProxy, ScriptId, Domain}, IntoScriptPluginParams, ScriptingSystemSet, }; use bevy::{ @@ -58,6 +58,8 @@ pub struct ScriptAsset { pub content: Box<[u8]>, // Any chance a Cow<'static, ?> could work here? /// The language of the script pub language: Language, + /// The domain if any the script will run in + pub domain: Option, } impl From for ScriptAsset { @@ -65,6 +67,7 @@ impl From for ScriptAsset { ScriptAsset { content: s.into_bytes().into_boxed_slice(), language: Language::default(), + domain: None, } } } @@ -74,6 +77,8 @@ impl From for ScriptAsset { pub struct ScriptSettings { /// Define the language for a script or use the extension if None. pub language: Option, + /// Specify the domain for the script. + pub domain: Option, } #[derive(Default)] @@ -123,6 +128,7 @@ impl AssetLoader for ScriptAssetLoader { let asset = ScriptAsset { content: content.into_boxed_slice(), language, + domain: settings.domain.clone(), }; Ok(asset) } @@ -154,13 +160,13 @@ pub(crate) fn sync_script_data( match asset_server.get_path(*id) { Some(path) => { trace!( - "{}: Script path {} is for a different langauge than this sync system. Skipping.", + "{}: Script path {} is for a different language than this sync system. Skipping.", P::LANGUAGE, path); } None => { trace!( - "{}: Script id {} is for a different langauge than this sync system. Skipping.", + "{}: Script id {} is for a different language than this sync system. Skipping.", P::LANGUAGE, id); } @@ -168,12 +174,12 @@ pub(crate) fn sync_script_data( 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! + asset.domain.clone(), )); } } @@ -228,6 +234,7 @@ pub(crate) fn eval_script( commands.entity(id).queue(CreateOrUpdateScript::

::new( script_id, asset.content.clone(), + asset.domain.clone(), )); } } else { 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 b32ef18cad..c50d8450a7 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/script_system.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/script_system.rs @@ -18,7 +18,7 @@ use crate::{ extractors::get_all_access_ids, handler::CallbackSettings, runtime::RuntimeContainer, - script::{ScriptId, ScriptContextProvider, ScriptContext}, + script::{ScriptId, ScriptContextProvider, ScriptContext, Domain}, IntoScriptPluginParams, }; use bevy::{ @@ -89,6 +89,7 @@ type ScriptPath = Cow<'static, str>; pub struct ScriptSystemBuilder { pub(crate) name: CallbackLabel, pub(crate) script_id: ScriptPath, + domain: Option, before: Vec, after: Vec, system_params: Vec, @@ -98,12 +99,13 @@ pub struct ScriptSystemBuilder { #[profiling::all_functions] impl ScriptSystemBuilder { /// Creates a new script system builder - pub fn new(name: CallbackLabel, script_id: ScriptPath) -> 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, } @@ -261,6 +263,7 @@ impl<'w, P: IntoScriptPluginParams> DynamicHandlerContext<'w, P> { label: &CallbackLabel, script_id: &Handle, entity: Entity, + domain: &Option, payload: Vec, guard: WorldGuard<'_>, ) -> Result { @@ -278,7 +281,7 @@ impl<'w, P: IntoScriptPluginParams> DynamicHandlerContext<'w, P> { // } else { // return Err(InteropError::missing_script(script_id.clone()).into()); // }; - let Some(context) = self.script_context.get(Some(entity), &script_id.id(), &None) else { + let Some(context) = self.script_context.get(Some(entity), &script_id.id(), domain) else { return Err(InteropError::missing_context(script_id.clone()).into()); }; @@ -360,6 +363,7 @@ pub struct DynamicScriptSystem { archetype_generation: ArchetypeGeneration, system_param_descriptors: Vec, state: Option, + domain: Option, _marker: std::marker::PhantomData P>, } @@ -381,6 +385,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(), @@ -513,6 +518,7 @@ impl System for DynamicScriptSystem

{ // &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 d7b571a6e5..7f66783f42 100644 --- a/crates/bevy_mod_scripting_core/src/commands.rs +++ b/crates/bevy_mod_scripting_core/src/commands.rs @@ -12,10 +12,10 @@ use crate::{ }, extractors::{with_handler_system_state, HandlerContext, WithWorldGuard}, handler::{handle_script_errors, send_callback_response}, - script::{ScriptId, StaticScripts, DisplayProxy, ScriptContextProvider}, + script::{ScriptId, StaticScripts, DisplayProxy, ScriptContextProvider, Domain}, IntoScriptPluginParams, }; -use bevy::{asset::Handle, ecs::entity::Entity, log::{warn, debug}, prelude::{EntityCommand, 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}; @@ -39,10 +39,12 @@ impl DeleteScript

{ impl Command for DeleteScript

{ fn apply(self, world: &mut bevy::prelude::World) { + let domain = world.resource::>().get(self.id).and_then(|x| x.domain); // first apply unload callback RunScriptCallback::

::new( Handle::Weak(self.id.clone()), Entity::from_raw(0), + domain, OnScriptUnloaded::into_callback_label(), vec![], false, @@ -68,6 +70,7 @@ pub struct CreateOrUpdateScript { id: Handle, // It feels like we're using a Box, which requires a clone merely to satisfy the Command trait. content: Box<[u8]>, + 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, } @@ -75,10 +78,11 @@ pub struct CreateOrUpdateScript { #[profiling::all_functions] impl CreateOrUpdateScript

{ /// Creates a new CreateOrUpdateScript command with the given ID, content and asset - pub fn new(id: Handle, content: Box<[u8]>) -> Self { + pub fn new(id: Handle, content: Box<[u8]>, domain: Option) -> Self { Self { id, content, + domain, _ph: std::marker::PhantomData, } } @@ -138,109 +142,25 @@ impl CreateOrUpdateScript

{ entity: Option, id: &Handle, content: &[u8], + domain: &Option, guard: WorldGuard, handler_ctxt: &mut HandlerContext

) -> Result<(), ScriptError> { - // let (is_new_script, is_new_context)= { - // let maybe_entity = handler_ctxt.scripts.get(entity); - // let has_context = maybe_entity.map(|script| script.contexts.contains_key(&id.id())).unwrap_or(false); - // (maybe_entity.is_err(), !has_context) - // }; let assignment_strategy = handler_ctxt.context_loading_settings.assignment_strategy; - // let assigned_shared_context: Option>> = - // match assignment_strategy { - // crate::context::ContextAssignmentStrategy::Individual => todo!(),//None, - // crate::context::ContextAssignmentStrategy::Global => { - - // if handler_ctxt.shared_context.is_none() { - // let handle: Handle = Handle::default(); - // handler_ctxt.shared_context = Self::load_context(&handle, b"", guard.clone(), handler_ctxt); - // } - // handler_ctxt.shared_context.clone(); - // // handler_ctxt.scripts.iter() - // // .next() - // // .and_then(|s| s.contexts.values().next()) - // // .map(|c| c.clone()) - // // 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 {}, new context?: {}, new script?: {})", - // P::LANGUAGE, - // id.display(), - // assigned_shared_context.is_none(), - // is_new_script - // ); - - // let phrase = if assigned_shared_context.is_some() { - // "reloading" - // } else { - // "loading" - // }; let phrase; - let result = match handler_ctxt.script_context.get(entity, &id.id(), &None) { + let result = match handler_ctxt.script_context.get(entity, &id.id(), domain) { Some(context) => { bevy::log::debug!("{}: reloading script {}", P::LANGUAGE, id.display()); - // if is_new_script { - // // This will happen when sharing contexts. - // // Make a new script with the shared context. - // // let mut script = Script { - // // // NOTE: We don't want script handles to be strong, right? - // // // id: id.clone_weak(), - // // contexts: [(id.id(), assigned_shared_context)] - // // .into_iter() - // // .collect(), - // // }; - // let context_arc = assigned_shared_context.clone(); - // let mut context = context_arc.lock(); - // // it can potentially be loaded but without a successful script reload but that - // // leaves us in an okay state - // // handler_ctxt.scripts.insert(script); - // Self::reload_context(id, content, &mut context, guard.clone(), handler_ctxt) - // .map(|_| Some(Script::

{ - // contexts: [(id.id(), assigned_shared_context)].into_iter().collect() - // })) - - // } else { - // let mut context = if is_new_context { - // assigned_shared_context.clone() - // } else { - // let script = handler_ctxt.scripts.get(entity).unwrap(); - // script.contexts.get(&id.id()).unwrap().clone() - // }; - let mut lcontext = context.lock(); + let mut lcontext = context.lock(); phrase = "reloading"; - Self::reload_context(id, content, &mut lcontext, guard.clone(), handler_ctxt).map(|_| None) - // .map(|_| { - // if is_new_context { - // let mut script = handler_ctxt.scripts.get_mut(entity).unwrap(); - // script.contexts.insert(id.id(), assigned_shared_context); - // } - // None - // }) - // } - // Self::reload_context(id, content, guard.clone(), handler_ctxt).map(|_| None) + 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) - // .map(|context| - // Some(Script::

{ - // contexts: [(id.id(), Arc::new(Mutex::new(context)))].into_iter().collect() - // }) - // ) + Self::load_context(id, content, guard.clone(), handler_ctxt) + .map(Some) } }; @@ -248,7 +168,7 @@ impl CreateOrUpdateScript

{ 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 {:?} script {}.", entity, id.display()); + warn!("Unable to insert script context for entity {:?} with script {}.", entity, id.display()); } } @@ -283,7 +203,7 @@ impl Command for CreateOrUpdateScript

{ let result = with_handler_system_state( world, |guard, handler_ctxt: &mut HandlerContext

| { - Self::create_or_update_script(None, &self.id, &self.content, + Self::create_or_update_script(None, &self.id, &self.content, &self.domain, guard, handler_ctxt) }); @@ -294,6 +214,7 @@ impl Command for CreateOrUpdateScript

{ RunScriptCallback::

::new( self.id, Entity::from_raw(0), + self.domain, OnScriptLoaded::into_callback_label(), vec![], false, @@ -311,7 +232,7 @@ impl EntityCommand for CreateOrUpdateScript

{ let result = with_handler_system_state( world, |guard, handler_ctxt: &mut HandlerContext

| { - Self::create_or_update_script(Some(entity), &self.id, &self.content, + Self::create_or_update_script(Some(entity), &self.id, &self.content, &self.domain, guard, handler_ctxt) }); @@ -322,6 +243,7 @@ impl EntityCommand for CreateOrUpdateScript

{ RunScriptCallback::

::new( self.id, entity, + self.domain, OnScriptLoaded::into_callback_label(), vec![], false, @@ -340,6 +262,8 @@ pub struct RunScriptCallback { 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 @@ -357,6 +281,7 @@ impl RunScriptCallback

{ pub fn new( id: Handle, entity: Entity, + domain: Option, callback: CallbackLabel, args: Vec, trigger_response: bool, @@ -364,6 +289,7 @@ impl RunScriptCallback

{ Self { id, entity, + domain, context: None, callback, args, @@ -395,6 +321,7 @@ impl Command for RunScriptCallback

{ &self.callback, &self.id, self.entity, + &self.domain, self.args, guard.clone(), ); 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 bfddb38bba..1058e5d9a6 100644 --- a/crates/bevy_mod_scripting_core/src/extractors.rs +++ b/crates/bevy_mod_scripting_core/src/extractors.rs @@ -30,7 +30,7 @@ use crate::{ event::{CallbackLabel, IntoCallbackLabel}, handler::CallbackSettings, runtime::RuntimeContainer, - script::{ScriptId, StaticScripts, ScriptContext, DisplayProxy, ScriptContextProvider}, + script::{ScriptId, StaticScripts, ScriptContext, DisplayProxy, ScriptContextProvider, Domain}, IntoScriptPluginParams, }; @@ -220,11 +220,12 @@ impl<'s, P: IntoScriptPluginParams> HandlerContext<'s, P> { label: &CallbackLabel, script_id: &Handle, entity: Entity, + domain: &Option, payload: Vec, guard: WorldGuard<'_>, ) -> Result { // find script - let Some(context) = self.script_context.get(Some(entity), &script_id.id(), &None) else { + let Some(context) = self.script_context.get(Some(entity), &script_id.id(), domain) else { return Err(InteropError::missing_context(script_id.clone()).into()); }; @@ -258,10 +259,11 @@ impl<'s, P: IntoScriptPluginParams> HandlerContext<'s, P> { &self, 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 e441bd3ab3..eb38a48dda 100644 --- a/crates/bevy_mod_scripting_core/src/handler.rs +++ b/crates/bevy_mod_scripting_core/src/handler.rs @@ -194,13 +194,18 @@ pub(crate) fn event_handler_inner( { continue } - _ => {} + crate::event::Recipients::Domain(_domain) => + { + todo!() + } + crate::event::Recipients::All => () } let call_result = handler_ctxt.call_dynamic_label( &callback_label, &script_id, *entity, + event.args.clone(), guard.clone(), ); 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..819cbc879b --- /dev/null +++ b/crates/bevy_mod_scripting_core/src/script/domain_context.rs @@ -0,0 +1,27 @@ +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 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..50a987f568 --- /dev/null +++ b/crates/bevy_mod_scripting_core/src/script/entity_context.rs @@ -0,0 +1,27 @@ +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 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.rs b/crates/bevy_mod_scripting_core/src/script/mod.rs similarity index 51% rename from crates/bevy_mod_scripting_core/src/script.rs rename to crates/bevy_mod_scripting_core/src/script/mod.rs index d267052f15..4526382ab4 100644 --- a/crates/bevy_mod_scripting_core/src/script.rs +++ b/crates/bevy_mod_scripting_core/src/script/mod.rs @@ -6,6 +6,17 @@ use bevy::{asset::{Asset, AssetId, Handle}, ecs::system::Resource, reflect::Refl use parking_lot::Mutex; use std::{borrow::Cow, collections::HashMap, ops::Deref, sync::Arc, fmt}; +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` @@ -73,123 +84,6 @@ impl ScriptComponent { } } -/// 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 = Option>; - -/// A generic script context provider -pub trait ScriptContextProvider { - /// Get the context. - fn get(&self, id: Option, script_id: &ScriptId, domain: &Domain) -> Option<&Arc>>; - /// Insert a context. - fn insert(&mut self, id: Option, script_id: &ScriptId, domain: &Domain, context: P::C) -> Result<(), P::C>; - /// Returns true if there is a context. - fn contains(&self, id: Option, script_id: &ScriptId, domain: &Domain) -> bool; -} - -#[derive(Resource)] -/// Keeps track of 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. - Entity(EntityContext

, SharedContext

) -} - -impl ScriptContext

{ - /// Use a shared script context - pub fn shared() -> Self { - Self::Shared(SharedContext::default()) - } - /// Use one script ontext per entity - pub fn per_entity() -> Self { - Self::Entity(EntityContext::default(), SharedContext::default()) - } -} - -impl Default for ScriptContext

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

for ScriptContext

{ - fn get(&self, id: Option, script_id: &ScriptId, domain: &Domain) -> Option<&Arc>> { - match self { - ScriptContext::Shared(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)), - } - } - fn insert(&mut self, id: Option, script_id: &ScriptId, domain: &Domain, context: P::C) -> Result<(), P::C> { - match self { - ScriptContext::Shared(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) - } - } - } - fn contains(&self, id: Option, script_id: &ScriptId, domain: &Domain) -> bool { - match self { - ScriptContext::Shared(a) => a.contains(id, script_id, domain), - ScriptContext::Entity(a, b) => a.contains(id, script_id, domain) || b.contains(id, script_id, domain), - } - } -} - -/// 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 get(&self, id: Option, script_id: &ScriptId, domain: &Domain) -> Option<&Arc>> { - id.and_then(|id| self.0.get(&id)) - } - fn insert(&mut self, id: Option, script_id: &ScriptId, domain: &Domain, 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: &Domain) -> bool { - id.map(|id| self.0.contains_key(&id)).unwrap_or(false) - } -} - -/// Contains the shared context. -pub struct SharedContext(pub Option>>); - -impl ScriptContextProvider

for SharedContext

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

{ - fn default() -> Self { - Self(None) - } -} - /// 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. 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..760d2e583e --- /dev/null +++ b/crates/bevy_mod_scripting_core/src/script/script_context.rs @@ -0,0 +1,103 @@ +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. + 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; +} + +#[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. + 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: + // 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), + } + } +} + 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..2196d399b5 --- /dev/null +++ b/crates/bevy_mod_scripting_core/src/script/scriptid_context.rs @@ -0,0 +1,23 @@ +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 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..ff97c26273 --- /dev/null +++ b/crates/bevy_mod_scripting_core/src/script/shared_context.rs @@ -0,0 +1,23 @@ +use super::*; + +/// Contains the shared context. +pub struct SharedContext(pub Option>>); + +impl ScriptContextProvider

for SharedContext

{ + 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 e39fb68964..de80404a2f 100644 --- a/crates/bevy_mod_scripting_functions/src/core.rs +++ b/crates/bevy_mod_scripting_functions/src/core.rs @@ -1262,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()) } } From 050a5b54a15cd22e5ab197066fec5d529361dec7 Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Tue, 1 Jul 2025 05:45:17 -0400 Subject: [PATCH 25/41] feature: Add ScriptContextProvider::hash member. --- crates/bevy_mod_scripting_core/src/asset.rs | 24 +++++-------- .../bevy_mod_scripting_core/src/commands.rs | 7 ++-- crates/bevy_mod_scripting_core/src/handler.rs | 35 +++++++++++++------ .../src/script/domain_context.rs | 7 ++++ .../src/script/entity_context.rs | 7 ++++ .../bevy_mod_scripting_core/src/script/mod.rs | 7 +++- .../src/script/script_context.rs | 13 +++++++ .../src/script/scriptid_context.rs | 5 +++ .../src/script/shared_context.rs | 4 +++ .../src/lib.rs | 2 ++ examples/game_of_life.rs | 6 ++-- 11 files changed, 86 insertions(+), 31 deletions(-) diff --git a/crates/bevy_mod_scripting_core/src/asset.rs b/crates/bevy_mod_scripting_core/src/asset.rs index de734ae37d..29b4f2dae6 100644 --- a/crates/bevy_mod_scripting_core/src/asset.rs +++ b/crates/bevy_mod_scripting_core/src/asset.rs @@ -5,7 +5,7 @@ use crate::{ ScriptComponent, commands::{CreateOrUpdateScript, DeleteScript}, error::ScriptError, - script::{DisplayProxy, ScriptId, Domain}, + script::{DisplayProxy, ScriptId, Domain, ScriptDomain}, IntoScriptPluginParams, ScriptingSystemSet, }; use bevy::{ @@ -58,8 +58,6 @@ pub struct ScriptAsset { pub content: Box<[u8]>, // Any chance a Cow<'static, ?> could work here? /// The language of the script pub language: Language, - /// The domain if any the script will run in - pub domain: Option, } impl From for ScriptAsset { @@ -67,7 +65,6 @@ impl From for ScriptAsset { ScriptAsset { content: s.into_bytes().into_boxed_slice(), language: Language::default(), - domain: None, } } } @@ -77,8 +74,6 @@ impl From for ScriptAsset { pub struct ScriptSettings { /// Define the language for a script or use the extension if None. pub language: Option, - /// Specify the domain for the script. - pub domain: Option, } #[derive(Default)] @@ -128,7 +123,6 @@ impl AssetLoader for ScriptAssetLoader { let asset = ScriptAsset { content: content.into_boxed_slice(), language, - domain: settings.domain.clone(), }; Ok(asset) } @@ -179,7 +173,7 @@ pub(crate) fn sync_script_data( commands.queue(CreateOrUpdateScript::

::new( Handle::Weak(*id), asset.content.clone(), // Cloning seems bad! - asset.domain.clone(), + None, // No domain for static scripts. )); } } @@ -196,19 +190,19 @@ pub(crate) fn sync_script_data( } pub(crate) fn eval_script( - script_comps: Query<(Entity, &ScriptComponent), Added>, - mut script_queue: Local)>>, + script_comps: Query<(Entity, &ScriptComponent, Option<&ScriptDomain>), Added>, + mut script_queue: Local, Option)>>, script_assets: Res>, asset_server: Res, mut commands: Commands, ) { - for (id, script_comp) in &script_comps { + for (id, script_comp, domain_maybe) in &script_comps { for handle in &script_comp.0 { - script_queue.push_back((id, handle.clone_weak())); + script_queue.push_back((id, handle.clone_weak(), domain_maybe.map(|x| x.0.clone()))); } } while ! script_queue.is_empty() { - let script_ready = script_queue.front().map(|(_, script_id)| match asset_server.load_state(&*script_id) { + 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 @@ -228,13 +222,13 @@ pub(crate) fn eval_script( // LoadState::Loaded => true, // _ => false // }) { - if let Some((id, script_id)) = script_queue.pop_front() { + 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(), - asset.domain.clone(), + domain_maybe )); } } else { diff --git a/crates/bevy_mod_scripting_core/src/commands.rs b/crates/bevy_mod_scripting_core/src/commands.rs index 7f66783f42..c8f0bb99b5 100644 --- a/crates/bevy_mod_scripting_core/src/commands.rs +++ b/crates/bevy_mod_scripting_core/src/commands.rs @@ -23,15 +23,17 @@ use std::{marker::PhantomData, sync::Arc}; pub struct DeleteScript { /// The ID of the script to delete 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: AssetId) -> Self { + pub fn new(id: AssetId, domain: Option) -> Self { Self { id, + domain, _ph: PhantomData, } } @@ -39,12 +41,11 @@ impl DeleteScript

{ impl Command for DeleteScript

{ fn apply(self, world: &mut bevy::prelude::World) { - let domain = world.resource::>().get(self.id).and_then(|x| x.domain); // first apply unload callback RunScriptCallback::

::new( Handle::Weak(self.id.clone()), Entity::from_raw(0), - domain, + self.domain, OnScriptUnloaded::into_callback_label(), vec![], false, diff --git a/crates/bevy_mod_scripting_core/src/handler.rs b/crates/bevy_mod_scripting_core/src/handler.rs index eb38a48dda..b072c5dafc 100644 --- a/crates/bevy_mod_scripting_core/src/handler.rs +++ b/crates/bevy_mod_scripting_core/src/handler.rs @@ -12,7 +12,7 @@ use crate::{ ScriptErrorEvent, }, extractors::{HandlerContext, WithWorldGuard}, - script::{ScriptComponent, ScriptId}, + script::{ScriptComponent, ScriptId, ScriptDomain, Domain}, IntoScriptPluginParams, }; use bevy::{ @@ -25,6 +25,7 @@ use bevy::{ }, log::trace_once, prelude::{Events, Ref}, + utils::HashSet, }; /// A function that handles a callback event @@ -131,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>>, )>; @@ -140,7 +141,7 @@ 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>, ) { @@ -154,13 +155,13 @@ pub(crate) fn event_handler_inner( 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| (Entity::from_raw(0), vec![s.clone()], None)), ) .collect::>() }); @@ -177,8 +178,11 @@ pub(crate) fn event_handler_inner( } }; + // TODO: Instead of `Domain` we should limit calls by `Context` but I don't + // think we've got a hash for that. + let mut called_domain: 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) @@ -194,18 +198,29 @@ pub(crate) fn event_handler_inner( { continue } - crate::event::Recipients::Domain(_domain) => + crate::event::Recipients::Domain(target_domain) + if domain.as_ref().map(|x| *x != *target_domain).unwrap_or(false) => { - todo!() + continue + } + crate::event::Recipients::All => (), + _ => () + + } + if let Some(domain) = domain { + if called_domain.contains(domain) { + // Only call a domain once. Not once per script. + continue; } - crate::event::Recipients::All => () + called_domain.insert(domain.clone()); } + let call_result = handler_ctxt.call_dynamic_label( &callback_label, &script_id, *entity, - + domain, event.args.clone(), guard.clone(), ); diff --git a/crates/bevy_mod_scripting_core/src/script/domain_context.rs b/crates/bevy_mod_scripting_core/src/script/domain_context.rs index 819cbc879b..69a5c56278 100644 --- a/crates/bevy_mod_scripting_core/src/script/domain_context.rs +++ b/crates/bevy_mod_scripting_core/src/script/domain_context.rs @@ -10,6 +10,13 @@ impl Default for DomainContext

{ } 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)) } diff --git a/crates/bevy_mod_scripting_core/src/script/entity_context.rs b/crates/bevy_mod_scripting_core/src/script/entity_context.rs index 50a987f568..0dde95ea1b 100644 --- a/crates/bevy_mod_scripting_core/src/script/entity_context.rs +++ b/crates/bevy_mod_scripting_core/src/script/entity_context.rs @@ -10,6 +10,13 @@ impl Default for EntityContext

{ } 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)) } diff --git a/crates/bevy_mod_scripting_core/src/script/mod.rs b/crates/bevy_mod_scripting_core/src/script/mod.rs index 4526382ab4..bf1148066e 100644 --- a/crates/bevy_mod_scripting_core/src/script/mod.rs +++ b/crates/bevy_mod_scripting_core/src/script/mod.rs @@ -4,7 +4,8 @@ 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}; +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; @@ -62,6 +63,10 @@ impl DisplayProxy for Handle { } } +/// 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. diff --git a/crates/bevy_mod_scripting_core/src/script/script_context.rs b/crates/bevy_mod_scripting_core/src/script/script_context.rs index 760d2e583e..6336dced01 100644 --- a/crates/bevy_mod_scripting_core/src/script/script_context.rs +++ b/crates/bevy_mod_scripting_core/src/script/script_context.rs @@ -12,6 +12,8 @@ pub type Domain = Cow<'static, str>; /// A generic script context provider pub trait ScriptContextProvider { + /// Hash for context. + fn hash(&self, id: Option, script_id: &ScriptId, domain: &Option) -> Option; /// Get the context. fn get(&self, id: Option, script_id: &ScriptId, domain: &Option) -> Option<&Arc>>; /// Insert a context. @@ -67,6 +69,17 @@ impl Default for ScriptContext

{ } impl ScriptContextProvider

for ScriptContext

{ + + 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)), + } + } fn get(&self, id: Option, script_id: &ScriptId, domain: &Option) -> Option<&Arc>> { match self { ScriptContext::Shared(a) => a.get(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 index 2196d399b5..f4d3bc67ed 100644 --- a/crates/bevy_mod_scripting_core/src/script/scriptid_context.rs +++ b/crates/bevy_mod_scripting_core/src/script/scriptid_context.rs @@ -10,6 +10,11 @@ impl Default for ScriptIdContext

{ } 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) } diff --git a/crates/bevy_mod_scripting_core/src/script/shared_context.rs b/crates/bevy_mod_scripting_core/src/script/shared_context.rs index ff97c26273..b47c099f7a 100644 --- a/crates/bevy_mod_scripting_core/src/script/shared_context.rs +++ b/crates/bevy_mod_scripting_core/src/script/shared_context.rs @@ -4,6 +4,10 @@ use super::*; 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() } 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 3957785b96..77a3963bf2 100644 --- a/crates/testing_crates/script_integration_test_harness/src/lib.rs +++ b/crates/testing_crates/script_integration_test_harness/src/lib.rs @@ -312,6 +312,7 @@ fn run_test_callback( let res = handler_ctxt.call::( &Handle::Weak(*script_id), Entity::from_raw(0), + &None, vec![], guard.clone(), ); @@ -510,6 +511,7 @@ pub fn run_plugin_script_load_benchmark< CreateOrUpdateScript::

::new( Handle::Weak(random_script_id), content.clone().into(), + None, ), is_reload, ) diff --git a/examples/game_of_life.rs b/examples/game_of_life.rs index f685d0f2d0..081862a38b 100644 --- a/examples/game_of_life.rs +++ b/examples/game_of_life.rs @@ -97,14 +97,16 @@ fn run_script_cmd( for script_id in static_lua_scripts.drain(..) { commands.queue(DeleteScript::::new( - script_id + script_id, + None, )); } #[cfg(feature = "rhai")] for script_id in static_rhai_scripts.drain(..) { commands.queue(DeleteScript::::new( - script_id + script_id, + None, )); } } From 25c4fb3441d1037a1dce466b4060532a3c635ff2 Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Tue, 1 Jul 2025 06:03:32 -0400 Subject: [PATCH 26/41] feature: Call labels on a per-context basis. --- .../src/bindings/script_system.rs | 13 ------- crates/bevy_mod_scripting_core/src/handler.rs | 19 +++++----- .../src/script/script_context.rs | 36 +++++++++++-------- 3 files changed, 33 insertions(+), 35 deletions(-) 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 c50d8450a7..153a69de31 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/script_system.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/script_system.rs @@ -268,19 +268,6 @@ impl<'w, P: IntoScriptPluginParams> DynamicHandlerContext<'w, P> { guard: WorldGuard<'_>, ) -> Result { // find script - - // let access = ReflectAccessId::for_component_id(script_component_id); - // It'd be nice to have the component id for Script

somewhere. - // let context = if guard.claim_read_access(access) { - todo!(); - // let world = guard.as_unsafe_world_cell_readonly()?; - // let world = unsafe { world.world() }; - // let maybe_context = world.get::>(entity).and_then(|script| script.contexts.get(&script_id.id())).map(|context| context.clone()); - // unsafe { guard.release_access(access) }; - // maybe_context.ok_or_else(||InteropError::missing_script(script_id.clone()))? - // } else { - // 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()); }; diff --git a/crates/bevy_mod_scripting_core/src/handler.rs b/crates/bevy_mod_scripting_core/src/handler.rs index b072c5dafc..765277e0ed 100644 --- a/crates/bevy_mod_scripting_core/src/handler.rs +++ b/crates/bevy_mod_scripting_core/src/handler.rs @@ -12,7 +12,7 @@ use crate::{ ScriptErrorEvent, }, extractors::{HandlerContext, WithWorldGuard}, - script::{ScriptComponent, ScriptId, ScriptDomain, Domain}, + script::{ScriptComponent, ScriptId, ScriptDomain, Domain, ScriptContextProvider}, IntoScriptPluginParams, }; use bevy::{ @@ -143,12 +143,13 @@ pub(crate) fn event_handler_inner( callback_label: CallbackLabel, mut entity_query_state: Local, Option>)>>, mut script_events: crate::extractors::EventReaderScope, + // script_context_provider: Res>, 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 @@ -161,7 +162,7 @@ pub(crate) fn event_handler_inner( .static_scripts .scripts .iter() - .map(|s| (Entity::from_raw(0), vec![s.clone()], None)), + .map(|s| (NO_ENTITY, vec![s.clone()], None)), ) .collect::>() }); @@ -180,7 +181,7 @@ pub(crate) fn event_handler_inner( // TODO: Instead of `Domain` we should limit calls by `Context` but I don't // think we've got a hash for that. - let mut called_domain: HashSet = HashSet::new(); + let mut called_contexts: HashSet = HashSet::new(); for event in events.into_iter().filter(|e| e.label == callback_label) { for (entity, entity_scripts, domain) in entity_and_static_scripts.iter() { for script_id in entity_scripts.iter() { @@ -207,12 +208,14 @@ pub(crate) fn event_handler_inner( _ => () } - if let Some(domain) = domain { - if called_domain.contains(domain) { - // Only call a domain once. Not once per script. + 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; } - called_domain.insert(domain.clone()); } diff --git a/crates/bevy_mod_scripting_core/src/script/script_context.rs b/crates/bevy_mod_scripting_core/src/script/script_context.rs index 6336dced01..c39f7d32a4 100644 --- a/crates/bevy_mod_scripting_core/src/script/script_context.rs +++ b/crates/bevy_mod_scripting_core/src/script/script_context.rs @@ -12,14 +12,19 @@ pub type Domain = Cow<'static, str>; /// A generic script context provider pub trait ScriptContextProvider { - /// Hash for context. - fn hash(&self, id: Option, script_id: &ScriptId, domain: &Option) -> Option; /// 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)] @@ -31,6 +36,9 @@ pub enum ScriptContext { /// /// 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 /// @@ -39,7 +47,8 @@ pub enum ScriptContext { Domain(DomainContext

, SharedContext

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

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

, EntityContext

, SharedContext

), } @@ -69,17 +78,6 @@ impl Default for ScriptContext

{ } impl ScriptContextProvider

for ScriptContext

{ - - 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)), - } - } fn get(&self, id: Option, script_id: &ScriptId, domain: &Option) -> Option<&Arc>> { match self { ScriptContext::Shared(a) => a.get(id, script_id, domain), @@ -112,5 +110,15 @@ impl ScriptContextProvider

for ScriptContext

{ 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)), + } + } } From 474d71cd1a538a711925d859d395dccee1be533d Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Tue, 1 Jul 2025 06:11:01 -0400 Subject: [PATCH 27/41] doc: Explain call per-context rationale. --- crates/bevy_mod_scripting_core/src/handler.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/crates/bevy_mod_scripting_core/src/handler.rs b/crates/bevy_mod_scripting_core/src/handler.rs index 765277e0ed..40a0810ff0 100644 --- a/crates/bevy_mod_scripting_core/src/handler.rs +++ b/crates/bevy_mod_scripting_core/src/handler.rs @@ -143,7 +143,6 @@ pub(crate) fn event_handler_inner( callback_label: CallbackLabel, mut entity_query_state: Local, Option>)>>, mut script_events: crate::extractors::EventReaderScope, - // script_context_provider: Res>, mut handler_ctxt: WithWorldGuard>, ) { const NO_ENTITY: Entity = Entity::from_raw(0); @@ -179,8 +178,16 @@ pub(crate) fn event_handler_inner( } }; - // TODO: Instead of `Domain` we should limit calls by `Context` but I don't - // think we've got a hash for that. + // 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, domain) in entity_and_static_scripts.iter() { @@ -213,12 +220,11 @@ pub(crate) fn event_handler_inner( domain); if let Some(hash) = context_hash { if !called_contexts.insert(hash) { - // Only call a context once. Not once per script. + // Only call a context once, not once per script. continue; } } - let call_result = handler_ctxt.call_dynamic_label( &callback_label, &script_id, From 58f9d01d9b01817d12a9bb12266616b3742693b1 Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Fri, 4 Jul 2025 06:03:44 -0400 Subject: [PATCH 28/41] feature: More ScriptComponentProvider

variants. --- .../src/script/script_context.rs | 119 +++++++++++------- 1 file changed, 71 insertions(+), 48 deletions(-) diff --git a/crates/bevy_mod_scripting_core/src/script/script_context.rs b/crates/bevy_mod_scripting_core/src/script/script_context.rs index c39f7d32a4..89150b491f 100644 --- a/crates/bevy_mod_scripting_core/src/script/script_context.rs +++ b/crates/bevy_mod_scripting_core/src/script/script_context.rs @@ -3,7 +3,7 @@ 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}; +use std::{borrow::Cow, collections::HashMap, ops::Deref, sync::Arc, fmt, marker::PhantomData}; /// A kind of catch all type for script context selection /// @@ -32,6 +32,8 @@ pub trait ScriptContextProvider { pub enum ScriptContext { /// One shared script context Shared(SharedContext

), + /// One shared script context with domains + DomainShared(Or, SharedContext

>), /// One script context per entity /// /// Stores context by entity with a shared context as a last resort when no @@ -39,17 +41,18 @@ pub enum ScriptContext { /// /// TODO: Should check for entity despawns and remove from this /// EntityContext. - Entity(EntityContext

, SharedContext

), + Entity(Or, SharedContext

>), + /// One script context per entity with domains + DomainEntity(Or, Or, 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

), + Domain(Or, 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

), + ScriptId(Or, SharedContext

>), + /// A script context per script with domains + DomainScriptId(Or, Or, SharedContext

>>), } impl ScriptContext

{ @@ -57,68 +60,88 @@ impl ScriptContext

{ pub fn shared() -> Self { Self::Shared(SharedContext::default()) } + /// If a domain is given, use that first. + pub fn with_domains(self) -> Self { + match self { + ScriptContext::Shared(a) => ScriptContext::DomainShared(Or(DomainContext::default(), a)), + ScriptContext::Entity(a) => ScriptContext::DomainEntity(Or(DomainContext::default(), a)), + ScriptContext::ScriptId(a) => ScriptContext::DomainScriptId(Or(DomainContext::default(), a)), + _ => panic!("Expected `Shared`, `Entity`, or `ScriptId` for with_domains"), + } + } /// Domain contexts or a shared context - pub fn with_domains() -> Self { - Self::Domain(DomainContext::default(), SharedContext::default()) + pub fn domains() -> Self { + Self::Domain(Or(DomainContext::default(), SharedContext::default())) } - /// Use one script ontext per entity + /// Use one script context per entity pub fn per_entity() -> Self { - Self::Entity(EntityContext::default(), SharedContext::default()) + Self::Entity(Or(EntityContext::default(), SharedContext::default())) } - /// Use one script ontext per entity + /// Use one script context per entity pub fn per_script() -> Self { - Self::ScriptId(ScriptIdContext::default()) + Self::ScriptId(Or(ScriptIdContext::default(), SharedContext::default())) } } +/// Use one script context per entity by default; see [ScriptContext::per_script]. impl Default for ScriptContext

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

for ScriptContext

{ +struct Or(T, U); + +impl, U: ScriptContextProvider

, P: IntoScriptPluginParams> ScriptContextProvider

for Or { + #[inline] 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)), - } + self.0.get(id, script_id, domain).or_else(|| self.1.get(id, script_id, domain)) } + #[inline] 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) - } + match self.0.insert(id, script_id, domain, context) { + Ok(()) => Ok(()), + Err(context) => self.1.insert(id, script_id, domain, context) } } + #[inline] 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), - } + self.0.contains(id, script_id, domain) || self.1.contains(id, script_id, domain) } + #[inline] 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)), - } + self.0.hash(id, script_id, domain) + .or_else(|| self.1.hash(id, script_id, domain)) } } +macro_rules! delegate_to_variants { + ( + $( + fn $fn_name:ident ($self:ty, $( $arg:ident : $arg_ty:ty ),* ) -> $ret:ty + ),* $(,)? + ) => { + $( + fn $fn_name(self: $self, $( $arg: $arg_ty ),*) -> $ret { + match self { + ScriptContext::Shared(a) => a.$fn_name($( $arg ),*), + ScriptContext::ScriptId(a) => a.$fn_name($( $arg ),*), + ScriptContext::Entity(a) => a.$fn_name($( $arg ),*), + ScriptContext::Domain(a) => a.$fn_name($( $arg ),*), + ScriptContext::DomainShared(a) => a.$fn_name($( $arg ),*), + ScriptContext::DomainScriptId(a) => a.$fn_name($( $arg ),*), + ScriptContext::DomainEntity(a) => a.$fn_name($( $arg ),*), + } + } + )* + }; +} + +impl ScriptContextProvider

for ScriptContext

{ + delegate_to_variants! { + fn get(&Self, id: Option, script_id: &ScriptId, domain: &Option) -> Option<&Arc>>, + fn contains(&Self, id: Option, script_id: &ScriptId, domain: &Option) -> bool, + fn hash(&Self, id: Option, script_id: &ScriptId, domain: &Option) -> Option, + fn insert(&mut Self, id: Option, script_id: &ScriptId, domain: &Option, context: P::C) -> Result<(), P::C>, + } +} From 5571b9f75cdf52659bd802aec8349b238dc1850a Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Fri, 4 Jul 2025 06:04:31 -0400 Subject: [PATCH 29/41] chore: Restore commented log plugin. --- crates/testing_crates/test_utils/src/test_data.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/testing_crates/test_utils/src/test_data.rs b/crates/testing_crates/test_utils/src/test_data.rs index 1214fa2891..030f7dc020 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 } From 444c266d089204463e268be712657949265bea7b Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Fri, 4 Jul 2025 06:05:00 -0400 Subject: [PATCH 30/41] test: Remove script_id_generator parameter. --- benches/benchmarks.rs | 2 -- .../script_integration_test_harness/src/lib.rs | 5 +---- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/benches/benchmarks.rs b/benches/benchmarks.rs index 82b3745d46..f1218c1c97 100644 --- a/benches/benchmarks.rs +++ b/benches/benchmarks.rs @@ -253,7 +253,6 @@ fn script_load_benchmarks(criterion: &mut Criterion) { "empty Lua", content, &mut group, - |rand| format!("{rand}.lua"), reload_probability, ); @@ -265,7 +264,6 @@ fn script_load_benchmarks(criterion: &mut Criterion) { "empty Rhai", content, &mut group, - |rand| format!("{rand}.rhai"), reload_probability, ); } 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 77a3963bf2..b119f111ec 100644 --- a/crates/testing_crates/script_integration_test_harness/src/lib.rs +++ b/crates/testing_crates/script_integration_test_harness/src/lib.rs @@ -488,7 +488,6 @@ pub fn run_plugin_script_load_benchmark< benchmark_id: &str, content: &str, criterion: &mut criterion::BenchmarkGroup, - script_id_generator: impl Fn(u64) -> AssetId, reload_probability: f32, ) { let mut app = setup_integration_test(|_, _| {}); @@ -502,10 +501,8 @@ pub fn run_plugin_script_load_benchmark< let mut rng = RNG.lock().unwrap(); let is_reload = rng.random_range(0f32..=1f32) < reload_probability; let random_id = if is_reload { 0 } else { rng.random::() }; - - // let random_script_id = script_id_generator(random_id); 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 + // We manually load the script inside a command. let content = content.to_string().into_boxed_str(); ( CreateOrUpdateScript::

::new( From 60d19e7c1c643837c8823cf4f8973d58b13422d6 Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Sat, 5 Jul 2025 02:25:54 -0400 Subject: [PATCH 31/41] test: Remove metadata tests. --- crates/bevy_mod_scripting_core/src/asset.rs | 83 --------------------- 1 file changed, 83 deletions(-) diff --git a/crates/bevy_mod_scripting_core/src/asset.rs b/crates/bevy_mod_scripting_core/src/asset.rs index 29b4f2dae6..8f402c54fe 100644 --- a/crates/bevy_mod_scripting_core/src/asset.rs +++ b/crates/bevy_mod_scripting_core/src/asset.rs @@ -300,19 +300,6 @@ mod tests { app } - fn make_test_settings() -> ScriptAssetSettings { - ScriptAssetSettings { - supported_extensions: &[], - script_id_mapper: AssetPathToScriptIdMapper { - map: |path| path.path().to_string_lossy().into_owned().into(), - }, - extension_to_language_map: HashMap::from_iter(vec![ - ("lua", Language::Lua), - ("rhai", Language::Rhai), - ]), - } - } - fn load_asset(app: &mut App, path: &str) -> Handle { let handle = app.world_mut().resource::().load(path); @@ -401,22 +388,6 @@ mod tests { ); } - #[test] - fn test_metadata_store() { - let mut store = ScriptMetadataStore::default(); - let id = AssetId::invalid(); - let meta = ScriptMetadata { - asset_id: AssetId::invalid(), - script_id: "test".into(), - language: Language::Lua, - }; - - store.insert(id, meta.clone()); - assert_eq!(store.get(id), Some(&meta)); - - assert_eq!(store.remove(id), Some(meta)); - } - #[test] fn test_script_asset_settings_select_language() { let settings = make_test_settings(); @@ -493,60 +464,6 @@ mod tests { fn build_runtime() -> Self::R {} } - #[test] - fn test_asset_metadata_systems() { - // test metadata flow - let mut app = init_loader_test(ScriptAssetLoader { - extensions: &[], - preprocessor: None, - }); - app.world_mut().insert_resource(make_test_settings()); - configure_asset_systems(&mut app); - - // update untill the asset event gets dispatched - let asset_server: &AssetServer = app.world().resource::(); - let handle = asset_server.load("test_assets/test_script.lua"); - run_app_untill_asset_event( - &mut app, - AssetEvent::LoadedWithDependencies { - id: AssetId::invalid(), - }, - ); - let asset_id = handle.id(); - - // we expect the metadata to be inserted now, in the same frame as the asset is loaded - let metadata = app - .world() - .get_resource::() - .unwrap() - .get(asset_id) - .expect("Metadata not found"); - - assert_eq!(metadata.script_id, "test_assets/test_script.lua"); - assert_eq!(metadata.language, Language::Lua); - - // ----------------- REMOVING ----------------- - - // we drop the handle and wait untill the first asset event is dispatched - drop(handle); - - run_app_untill_asset_event( - &mut app, - AssetEvent::Removed { - id: AssetId::invalid(), - }, - ); - - // we expect the metadata to be removed now, in the same frame as the asset is removed - let metadata_len = app - .world() - .get_resource::() - .unwrap() - .map - .len(); - - assert_eq!(metadata_len, 0); - } // #[test] // fn test_syncing_assets() { From e46433f4b2165b12b33714e5f71efe1600929ed1 Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Sat, 5 Jul 2025 05:12:00 -0400 Subject: [PATCH 32/41] refactor: Prefer Option vs Entity::from_raw(0). --- .../src/bindings/script_system.rs | 8 ++--- .../bevy_mod_scripting_core/src/commands.rs | 29 ++++++++----------- crates/bevy_mod_scripting_core/src/context.rs | 2 +- .../bevy_mod_scripting_core/src/extractors.rs | 6 ++-- crates/bevy_mod_scripting_core/src/handler.rs | 14 ++++----- .../bevy_mod_scripting_lua/src/lib.rs | 4 +-- .../bevy_mod_scripting_rhai/src/lib.rs | 4 +-- 7 files changed, 31 insertions(+), 36 deletions(-) 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 153a69de31..9994a88cb6 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/script_system.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/script_system.rs @@ -262,13 +262,13 @@ impl<'w, P: IntoScriptPluginParams> DynamicHandlerContext<'w, P> { &self, label: &CallbackLabel, script_id: &Handle, - entity: Entity, + entity: Option, domain: &Option, payload: Vec, guard: WorldGuard<'_>, ) -> Result { // find script - let Some(context) = self.script_context.get(Some(entity), &script_id.id(), domain) else { + let Some(context) = self.script_context.get(entity, &script_id.id(), domain) else { return Err(InteropError::missing_context(script_id.clone()).into()); }; @@ -504,7 +504,7 @@ impl System for DynamicScriptSystem

{ &state.callback_label, // &self.target_script, &script_id, - Entity::from_raw(0), + None, &self.domain, payload, guard.clone(), @@ -737,7 +737,7 @@ mod test { }); // now dynamically add script system via builder - let mut builder = ScriptSystemBuilder::new("test".into(), "empty_script".into()); + let mut builder = ScriptSystemBuilder::new("test".into(), "empty_script".into(), None); builder.before_system(test_system); let _ = builder diff --git a/crates/bevy_mod_scripting_core/src/commands.rs b/crates/bevy_mod_scripting_core/src/commands.rs index c8f0bb99b5..15718841dc 100644 --- a/crates/bevy_mod_scripting_core/src/commands.rs +++ b/crates/bevy_mod_scripting_core/src/commands.rs @@ -12,7 +12,7 @@ use crate::{ }, extractors::{with_handler_system_state, HandlerContext, WithWorldGuard}, handler::{handle_script_errors, send_callback_response}, - script::{ScriptId, StaticScripts, DisplayProxy, ScriptContextProvider, Domain}, + script::{ScriptId, StaticScripts, DisplayProxy, ScriptContextProvider, Domain, ScriptContext}, IntoScriptPluginParams, }; use bevy::{asset::{Assets, Handle}, ecs::entity::Entity, log::{warn, debug}, prelude::{EntityCommand, Command}}; @@ -44,7 +44,7 @@ impl Command for DeleteScript

{ // first apply unload callback RunScriptCallback::

::new( Handle::Weak(self.id.clone()), - Entity::from_raw(0), + None, self.domain, OnScriptUnloaded::into_callback_label(), vec![], @@ -214,7 +214,7 @@ impl Command for CreateOrUpdateScript

{ RunScriptCallback::

::new( self.id, - Entity::from_raw(0), + None, self.domain, OnScriptLoaded::into_callback_label(), vec![], @@ -243,7 +243,7 @@ impl EntityCommand for CreateOrUpdateScript

{ RunScriptCallback::

::new( self.id, - entity, + Some(entity), self.domain, OnScriptLoaded::into_callback_label(), vec![], @@ -262,7 +262,7 @@ pub struct RunScriptCallback { /// The ID of the script to run the callback on pub id: Handle, /// The entity to use for the callback - pub entity: Entity, + pub entity: Option, /// The domain if any pub domain: Option, /// The callback to run @@ -281,7 +281,7 @@ impl RunScriptCallback

{ /// Creates a new RunCallbackCommand with the given ID, callback and arguments pub fn new( id: Handle, - entity: Entity, + entity: Option, domain: Option, callback: CallbackLabel, args: Vec, @@ -406,7 +406,6 @@ mod test { context::{ContextBuilder, ContextLoadingSettings}, handler::CallbackSettings, runtime::RuntimeContainer, - script::Scripts, }; use super::*; @@ -465,9 +464,7 @@ mod test { Ok(ScriptValue::Unit) }, }) - .insert_resource(Scripts:: { - scripts: Default::default(), - }); + ; app } @@ -485,15 +482,13 @@ mod test { } fn assert_context_and_script(world: &World, id: &str, context: &str, message: &str) { - let scripts = world.get_resource::>().unwrap(); + let scripts = world.get_resource::>().unwrap(); - let script = scripts - .scripts - .get(id) - .unwrap_or_else(|| panic!("Script not found {message}")); + let context = scripts.get(None, &Handle::default(), &None) + .unwrap_or_else(|| panic!("Context not found {message}")); - assert_eq!(id, script.id); - let found_context = script.context.lock(); + // assert_eq!(id, script.id); + let found_context = context.lock(); assert_eq!(*context, *found_context, "{message}"); } diff --git a/crates/bevy_mod_scripting_core/src/context.rs b/crates/bevy_mod_scripting_core/src/context.rs index ea300662af..433e1b102b 100644 --- a/crates/bevy_mod_scripting_core/src/context.rs +++ b/crates/bevy_mod_scripting_core/src/context.rs @@ -25,7 +25,7 @@ pub type ContextInitializer

= /// Initializer run every time before executing or loading/re-loading a script pub type ContextPreHandlingInitializer

= - fn(&Handle, Entity, &mut

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

::C) -> Result<(), ScriptError>; /// Settings concerning the creation and assignment of script contexts as well as their initialization. #[derive(Resource)] diff --git a/crates/bevy_mod_scripting_core/src/extractors.rs b/crates/bevy_mod_scripting_core/src/extractors.rs index 1058e5d9a6..6cfc94a041 100644 --- a/crates/bevy_mod_scripting_core/src/extractors.rs +++ b/crates/bevy_mod_scripting_core/src/extractors.rs @@ -219,13 +219,13 @@ impl<'s, P: IntoScriptPluginParams> HandlerContext<'s, P> { &self, label: &CallbackLabel, script_id: &Handle, - entity: Entity, + entity: Option, domain: &Option, payload: Vec, guard: WorldGuard<'_>, ) -> Result { // find script - let Some(context) = self.script_context.get(Some(entity), &script_id.id(), domain) else { + let Some(context) = self.script_context.get(entity, &script_id.id(), domain) else { return Err(InteropError::missing_context(script_id.clone()).into()); }; @@ -258,7 +258,7 @@ impl<'s, P: IntoScriptPluginParams> HandlerContext<'s, P> { pub fn call( &self, script_id: &Handle, - entity: Entity, + entity: Option, domain: &Option, payload: Vec, guard: WorldGuard<'_>, diff --git a/crates/bevy_mod_scripting_core/src/handler.rs b/crates/bevy_mod_scripting_core/src/handler.rs index 40a0810ff0..e3d1b8d1bc 100644 --- a/crates/bevy_mod_scripting_core/src/handler.rs +++ b/crates/bevy_mod_scripting_core/src/handler.rs @@ -31,7 +31,7 @@ use bevy::{ /// A function that handles a callback event pub type HandlerFn

= fn( args: Vec, - entity: Entity, + entity: Option, script_id: &Handle, callback: &CallbackLabel, context: &mut

::C, @@ -73,7 +73,7 @@ impl CallbackSettings

{ pub fn call( handler: HandlerFn

, args: Vec, - entity: Entity, + entity: Option, script_id: &Handle, callback: &CallbackLabel, script_ctxt: &mut P::C, @@ -155,13 +155,13 @@ pub(crate) fn event_handler_inner( let entity_and_static_scripts = guard.with_global_access(|world| { entity_query_state .iter(world) - .map(|(e, s, d)| (e, s.0.clone(), d.map(|x| x.0.clone()))) + .map(|(e, s, d)| (Some(e), s.0.clone(), d.map(|x| x.0.clone()))) .chain( handler_ctxt .static_scripts .scripts .iter() - .map(|s| (NO_ENTITY, vec![s.clone()], None)), + .map(|s| (None, vec![s.clone()], None)), ) .collect::>() }); @@ -198,7 +198,7 @@ pub(crate) fn event_handler_inner( { continue } - crate::event::Recipients::Entity(target_entity) if target_entity != entity => { + crate::event::Recipients::Entity(target_entity) if entity.map(|e| *target_entity != e).unwrap_or(false) => { continue } crate::event::Recipients::Language(target_language) @@ -215,7 +215,7 @@ pub(crate) fn event_handler_inner( _ => () } - let context_hash = handler_ctxt.script_context.hash((*entity != NO_ENTITY).then_some(*entity), + let context_hash = handler_ctxt.script_context.hash(*entity, &script_id.id(), domain); if let Some(hash) = context_hash { @@ -345,7 +345,7 @@ mod test { context::{ContextBuilder, ContextLoadingSettings}, event::{CallbackLabel, IntoCallbackLabel, ScriptCallbackEvent, ScriptErrorEvent}, runtime::RuntimeContainer, - script::{Script, ScriptComponent, ScriptId, Scripts, StaticScripts}, + script::{ScriptComponent, ScriptId, StaticScripts}, BMSScriptingInfrastructurePlugin, }; diff --git a/crates/languages/bevy_mod_scripting_lua/src/lib.rs b/crates/languages/bevy_mod_scripting_lua/src/lib.rs index f9e625a766..3fc408412b 100644 --- a/crates/languages/bevy_mod_scripting_lua/src/lib.rs +++ b/crates/languages/bevy_mod_scripting_lua/src/lib.rs @@ -162,7 +162,7 @@ fn load_lua_content_into_context( pre_handling_initializers .iter() - .try_for_each(|init| init(script_id, Entity::from_raw(0), context))?; + .try_for_each(|init| init(script_id, None, context))?; context .load(content) @@ -221,7 +221,7 @@ pub fn lua_context_reload( /// The lua handler for events pub fn lua_handler( args: Vec, - entity: bevy::ecs::entity::Entity, + entity: Option, script_id: &Handle, callback_label: &CallbackLabel, context: &mut Lua, diff --git a/crates/languages/bevy_mod_scripting_rhai/src/lib.rs b/crates/languages/bevy_mod_scripting_rhai/src/lib.rs index 539e9b3336..273a854482 100644 --- a/crates/languages/bevy_mod_scripting_rhai/src/lib.rs +++ b/crates/languages/bevy_mod_scripting_rhai/src/lib.rs @@ -197,7 +197,7 @@ fn load_rhai_content_into_context( .try_for_each(|init| init(script, context))?; pre_handling_initializers .iter() - .try_for_each(|init| init(script, Entity::from_raw(0), context))?; + .try_for_each(|init| init(script, None, context))?; runtime.eval_ast_with_scope(&mut context.scope, &context.ast)?; context.ast.clear_statements(); @@ -251,7 +251,7 @@ pub fn rhai_context_reload( /// The rhai callback handler. pub fn rhai_callback_handler( args: Vec, - entity: Entity, + entity: Option, script_id: &Handle, callback: &CallbackLabel, context: &mut RhaiScriptContext, From 318b9f30d9db3728bec775e9712c1040bd1af3c4 Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Sat, 5 Jul 2025 05:27:21 -0400 Subject: [PATCH 33/41] bug: Scripts w/o AssetServer didn't work. Now you can add scripts via `Assets` or the `AssetServer`. --- crates/bevy_mod_scripting_core/src/asset.rs | 73 +++++++++------------ 1 file changed, 30 insertions(+), 43 deletions(-) diff --git a/crates/bevy_mod_scripting_core/src/asset.rs b/crates/bevy_mod_scripting_core/src/asset.rs index 8f402c54fe..c880dde5f5 100644 --- a/crates/bevy_mod_scripting_core/src/asset.rs +++ b/crates/bevy_mod_scripting_core/src/asset.rs @@ -12,7 +12,7 @@ use bevy::{ app::{App, PreUpdate}, asset::{Asset, AssetEvent, AssetId, AssetLoader, AssetPath, Assets, LoadState}, ecs::system::Resource, - log::{debug, info, trace, warn}, + log::{debug, info, trace, warn, error}, prelude::{ Commands, Event, EventReader, EventWriter, IntoSystemConfigs, IntoSystemSetConfigs, Res, ResMut, Added, Query, Local, Handle, AssetServer, Entity, @@ -172,7 +172,12 @@ pub(crate) fn sync_script_data( info!("{}: Loading static script: {:?}", P::LANGUAGE, id); commands.queue(CreateOrUpdateScript::

::new( Handle::Weak(*id), - asset.content.clone(), // Cloning seems bad! + // Since we have the asset, we don't need to clone + // its contents to evaluate it. We can refer to the + // asset. + + // asset.content.clone(), // Cloning seems bad! + None, None, // No domain for static scripts. )); } @@ -202,32 +207,35 @@ pub(crate) fn eval_script( } } 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); + let mut script_failed = false; + // NOTE: Maybe using pop_front_if once stabalized. + let script_ready = script_queue + .front() + .map(|(_, script_id, _)| { + script_assets.contains(script_id.id()) || match asset_server.load_state(&*script_id) { + LoadState::NotLoaded => false, + LoadState::Loading => false, + LoadState::Loaded => true, + LoadState::Failed(e) => { + script_failed = true; + error!("Failed to load script {} for eval.", script_id.display()); + true + } + } + }) + .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 script_failed { + continue; + } 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(), + None, domain_maybe )); } @@ -348,11 +356,6 @@ mod tests { .get(&handle) .unwrap(); - assert_eq!( - asset.asset_path, - AssetPath::from_path(&PathBuf::from("test_assets/test_script.script")) - ); - assert_eq!( String::from_utf8(asset.content.clone().to_vec()).unwrap(), "test script".to_string() @@ -379,8 +382,8 @@ mod tests { .unwrap(); assert_eq!( - asset.asset_path, - AssetPath::from(PathBuf::from("test_assets/test_script.script")) + handle.path().unwrap(), + &AssetPath::from(PathBuf::from("test_assets/test_script.script")) ); assert_eq!( String::from_utf8(asset.content.clone().to_vec()).unwrap(), @@ -388,22 +391,6 @@ mod tests { ); } - #[test] - fn test_script_asset_settings_select_language() { - let settings = make_test_settings(); - - let path = AssetPath::from(Path::new("test.lua")); - assert_eq!(settings.select_script_language(&path), Language::Lua); - assert_eq!( - settings.select_script_language(&AssetPath::from(Path::new("test.rhai"))), - Language::Rhai - ); - assert_eq!( - settings.select_script_language(&AssetPath::from(Path::new("test.blob"))), - Language::Unknown - ); - } - fn run_app_untill_asset_event(app: &mut App, event_kind: AssetEvent) { let checker_system = |mut reader: EventReader>, mut event_target: ResMut| { From e7cbc2fe5bfa2eade33c5e1adcd663def8af2b7f Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Sat, 5 Jul 2025 05:34:41 -0400 Subject: [PATCH 34/41] test: Fix tests. --- crates/bevy_mod_scripting_core/src/handler.rs | 81 +++++++------------ crates/bevy_mod_scripting_core/src/lib.rs | 5 -- .../src/lib.rs | 51 +++++++----- .../test_utils/src/test_data.rs | 9 +-- 4 files changed, 65 insertions(+), 81 deletions(-) diff --git a/crates/bevy_mod_scripting_core/src/handler.rs b/crates/bevy_mod_scripting_core/src/handler.rs index e3d1b8d1bc..bcb06d5e41 100644 --- a/crates/bevy_mod_scripting_core/src/handler.rs +++ b/crates/bevy_mod_scripting_core/src/handler.rs @@ -333,7 +333,7 @@ mod test { use bevy::{ app::{App, Update}, - asset::AssetPlugin, + asset::{Assets, AssetPlugin}, diagnostic::DiagnosticsPlugin, ecs::world::FromWorld, }; @@ -383,8 +383,8 @@ mod test { fn setup_app( runtime: TestRuntime, - scripts: HashMap>, - ) -> App { + scripts: Vec, + ) -> (App, Vec>) { let mut app = App::new(); app.add_event::(); @@ -399,7 +399,12 @@ mod test { }, }); app.add_systems(Update, event_handler::); - app.insert_resource::>(Scripts { scripts }); + let handles = { + let world = app.world_mut(); + let mut script_assets = world.resource_mut::>(); + scripts.iter().map(|s| script_assets.add(s)).collect() + }; + app.insert_resource::>(StaticScripts { scripts: handles.clone() }); app.insert_resource(RuntimeContainer:: { runtime }); app.init_resource::(); app.insert_resource(ContextLoadingSettings:: { @@ -413,22 +418,17 @@ mod test { }); app.finish(); app.cleanup(); - app + (app, handles) } #[test] fn test_handler_emits_response_events() { - let test_script_id = Cow::Borrowed("test_script"); - let test_script = Script { - id: test_script_id.clone(), - asset: None, - context: Arc::new(Mutex::new(TestContext::default())), - }; - let scripts = HashMap::from_iter(vec![(test_script_id.clone(), test_script.clone())]); let runtime = TestRuntime { invocations: vec![].into(), }; - let mut app = setup_app::(runtime, scripts); + let scripts = vec![ScriptAsset::from("")]; + let (mut app, handles) = setup_app::(runtime, scripts); + let test_script_id = &handles[0]; app.world_mut() .spawn(ScriptComponent(vec![test_script_id.clone()])); @@ -455,17 +455,12 @@ mod test { #[test] fn test_handler_called_with_right_args() { - let test_script_id = Cow::Borrowed("test_script"); - let test_script = Script { - id: test_script_id.clone(), - asset: None, - context: Arc::new(Mutex::new(TestContext::default())), - }; - let scripts = HashMap::from_iter(vec![(test_script_id.clone(), test_script.clone())]); + let scripts = vec![ScriptAsset::from("")]; let runtime = TestRuntime { invocations: vec![].into(), }; - let mut app = setup_app::(runtime, scripts); + let (mut app, handles) = setup_app::(runtime, scripts); + let test_script_id = &handles[0]; let test_entity_id = app .world_mut() .spawn(ScriptComponent(vec![test_script_id.clone()])) @@ -479,7 +474,7 @@ mod test { { let test_script = app .world() - .get_resource::>() + .get_resource::>() .unwrap() .scripts .get(&test_script_id) @@ -511,27 +506,15 @@ mod test { #[test] fn test_handler_called_on_right_recipients() { - let test_script_id = Cow::Borrowed("test_script"); - let test_script = Script { - id: test_script_id.clone(), - asset: None, - context: Arc::new(Mutex::new(TestContext::default())), - }; - let scripts = HashMap::from_iter(vec![ - (test_script_id.clone(), test_script.clone()), - ( - "wrong".into(), - Script { - id: "wrong".into(), - asset: None, - context: Arc::new(Mutex::new(TestContext::default())), - }, - ), - ]); - let runtime = TestRuntime { invocations: vec![].into(), }; + let scripts = vec![ScriptAsset::from(""), + ScriptAsset::from("wrong"), + ]; + let (mut app, handles) = setup_app::(runtime, scripts); + let test_script_id = &handles[0]; + let mut app = setup_app::(runtime, scripts); let test_entity_id = app .world_mut() @@ -552,7 +535,7 @@ mod test { app.update(); { - let test_scripts = app.world().get_resource::>().unwrap(); + let test_scripts = app.world().get_resource::>().unwrap(); let test_runtime = app .world() .get_resource::>() @@ -584,20 +567,12 @@ mod test { #[test] fn test_handler_called_for_static_scripts() { - let test_script_id = Cow::Borrowed("test_script"); - - let scripts = HashMap::from_iter(vec![( - test_script_id.clone(), - Script { - id: test_script_id.clone(), - asset: None, - context: Arc::new(Mutex::new(TestContext::default())), - }, - )]); let runtime = TestRuntime { invocations: vec![].into(), }; - let mut app = setup_app::(runtime, scripts); + let scripts = vec![ScriptAsset::from("")]; + let (mut app, handles) = setup_app::(runtime, scripts); + let test_script_id = &handles[0]; app.world_mut().insert_resource(StaticScripts { scripts: vec![test_script_id.clone()].into_iter().collect(), @@ -617,7 +592,7 @@ mod test { app.update(); { - let test_scripts = app.world().get_resource::>().unwrap(); + let test_scripts = app.world().get_resource::>().unwrap(); let test_context = test_scripts .scripts .get(&test_script_id) diff --git a/crates/bevy_mod_scripting_core/src/lib.rs b/crates/bevy_mod_scripting_core/src/lib.rs index ce4f3eb0e4..367a6799a0 100644 --- a/crates/bevy_mod_scripting_core/src/lib.rs +++ b/crates/bevy_mod_scripting_core/src/lib.rs @@ -428,13 +428,8 @@ mod test { #[tokio::test] async fn test_asset_extensions_correctly_accumulate() { let mut app = App::new(); - app.init_resource::(); app.add_plugins(AssetPlugin::default()); - app.world_mut() - .resource_mut::() - .supported_extensions = &["lua", "rhai"]; - BMSScriptingInfrastructurePlugin.finish(&mut app); let asset_loader = app diff --git a/crates/testing_crates/script_integration_test_harness/src/lib.rs b/crates/testing_crates/script_integration_test_harness/src/lib.rs index b119f111ec..aff2c2926e 100644 --- a/crates/testing_crates/script_integration_test_harness/src/lib.rs +++ b/crates/testing_crates/script_integration_test_harness/src/lib.rs @@ -1,6 +1,7 @@ pub mod test_functions; use std::{ + fs, marker::PhantomData, path::PathBuf, time::{Duration, Instant}, @@ -9,7 +10,7 @@ use std::{ use bevy::{ app::{Last, Plugin, PostUpdate, Startup, Update}, - asset::{AssetServer, Handle, AssetPath, AssetId, LoadState}, + asset::{AssetServer, Handle, AssetPath, AssetId, LoadState, Assets}, ecs::{ component::Component, event::{Event, Events}, @@ -23,7 +24,7 @@ use bevy::{ utils::tracing, }; use bevy_mod_scripting_core::{ - asset::ScriptAsset, + asset::{Language, ScriptAsset}, bindings::{ pretty_print::DisplayWithWorld, script_value::ScriptValue, CoreScriptGlobalsPlugin, ReflectAccessId, WorldAccessGuard, WorldGuard, @@ -56,12 +57,10 @@ struct TestCallbackBuilder { } impl TestCallbackBuilder { - fn build<'a>(script_path: impl Into>, expect_response: bool) -> SystemConfigs { - let script_path = script_path.into().into_owned(); + fn build<'a>(script_id: Handle, expect_response: bool) -> SystemConfigs { 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, with_guard, expect_response); @@ -240,23 +239,35 @@ pub fn execute_integration_test<'a, // 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())])); + // The following code did not work, possibly because of the asynchronous + // nature of AssetServer. + // + // ``` + // let handle = app.world_mut().resource_mut::().load(&script_path); + // app.world_mut().spawn(ScriptComponent::new([handle.clone()])); + // ``` + let handle = { + let mut script_dir = manifest_dir.clone(); + script_dir.push("assets"); + script_dir.push(script_id.path()); + // Read the contents and don't do anything async. + let content = fs::read_to_string(&script_dir).map_err(|io| format!("io error {io} for path {script_dir:?}"))?; + let mut script = ScriptAsset::from(content); + script.language = P::LANGUAGE; + app.world_mut().resource_mut::>().add(script) }; - - - app.add_systems(Startup, load_system); + app.world_mut().spawn(ScriptComponent::new([handle.clone()])); app.add_systems( Update, - TestCallbackBuilder::::build(&script_id, expect_callback_response), + TestCallbackBuilder::::build(handle.clone(), expect_callback_response), ); app.add_systems( PostUpdate, - TestCallbackBuilder::::build(&script_id, expect_callback_response), + TestCallbackBuilder::::build(handle.clone(), expect_callback_response), ); app.add_systems( Last, - TestCallbackBuilder::::build(&script_id, expect_callback_response), + TestCallbackBuilder::::build(handle.clone(), expect_callback_response), ); app.add_systems(Update, dummy_update_system); app.add_systems(Startup, dummy_startup_system::); @@ -286,6 +297,10 @@ pub fn execute_integration_test<'a, .collect::>(); if let Some(event) = error_events.into_iter().next() { + // eprintln!("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXx"); + // if ! app.world().resource::().load_state(&handle).is_loaded() { + // continue; + // } return Err(event .error .display_with_world(WorldGuard::new_exclusive(app.world_mut()))); @@ -299,7 +314,7 @@ pub fn execute_integration_test<'a, } fn run_test_callback( - script_id: &ScriptId, + script_id: &Handle, mut with_guard: WithWorldGuard<'_, '_, HandlerContext<'_, P>>, expect_response: bool, ) -> Result { @@ -310,8 +325,8 @@ fn run_test_callback( // } let res = handler_ctxt.call::( - &Handle::Weak(*script_id), - Entity::from_raw(0), + script_id, + None, &None, vec![], guard.clone(), @@ -503,11 +518,11 @@ pub fn run_plugin_script_load_benchmark< let random_id = if is_reload { 0 } else { rng.random::() }; let random_script_id: ScriptId = ScriptId::from(uuid::Builder::from_random_bytes(random_id.to_le_bytes()).into_uuid()); // We manually load the script inside a command. - let content = content.to_string().into_boxed_str(); + let content = content.to_string().into_bytes().into_boxed_slice(); ( CreateOrUpdateScript::

::new( Handle::Weak(random_script_id), - content.clone().into(), + Some(content.clone().into()), None, ), is_reload, diff --git a/crates/testing_crates/test_utils/src/test_data.rs b/crates/testing_crates/test_utils/src/test_data.rs index 030f7dc020..c3115659e4 100644 --- a/crates/testing_crates/test_utils/src/test_data.rs +++ b/crates/testing_crates/test_utils/src/test_data.rs @@ -4,7 +4,6 @@ use std::collections::HashMap; use bevy::asset::AssetPlugin; use bevy::diagnostic::DiagnosticsPlugin; use bevy::ecs::{component::*, world::World}; -use bevy::log::LogPlugin; use bevy::prelude::*; use bevy::reflect::*; @@ -348,10 +347,10 @@ pub fn setup_integration_test(init: F) AssetPlugin::default(), HierarchyPlugin, DiagnosticsPlugin, - LogPlugin { - filter: log_level, - ..Default::default() - }, + // bevy::log::LogPlugin { + // filter: log_level, + // ..Default::default() + // }, )); app } From d2e29c3e853d23e4007ba6d425520a286bfd6b8b Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Sat, 5 Jul 2025 05:35:06 -0400 Subject: [PATCH 35/41] perf: Avoid cloning script content when possible. --- .../bevy_mod_scripting_core/src/commands.rs | 92 ++++++++++++------- .../bevy_mod_scripting_core/src/extractors.rs | 4 +- 2 files changed, 61 insertions(+), 35 deletions(-) diff --git a/crates/bevy_mod_scripting_core/src/commands.rs b/crates/bevy_mod_scripting_core/src/commands.rs index 15718841dc..959aeefa15 100644 --- a/crates/bevy_mod_scripting_core/src/commands.rs +++ b/crates/bevy_mod_scripting_core/src/commands.rs @@ -70,7 +70,7 @@ impl Command for DeleteScript

{ pub struct CreateOrUpdateScript { id: Handle, // It feels like we're using a Box, which requires a clone merely to satisfy the Command trait. - content: Box<[u8]>, + content: 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, @@ -79,7 +79,7 @@ pub struct CreateOrUpdateScript { #[profiling::all_functions] impl CreateOrUpdateScript

{ /// Creates a new CreateOrUpdateScript command with the given ID, content and asset - pub fn new(id: Handle, content: Box<[u8]>, domain: Option) -> Self { + pub fn new(id: Handle, content: Option>, domain: Option) -> Self { Self { id, content, @@ -121,7 +121,7 @@ impl CreateOrUpdateScript

{ id: &Handle, content: &[u8], guard: WorldGuard, - handler_ctxt: &mut HandlerContext

, + handler_ctxt: &HandlerContext

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

::load)( handler_ctxt.context_loading_settings.loader.load, @@ -142,24 +142,33 @@ impl CreateOrUpdateScript

{ pub(crate) fn create_or_update_script( entity: Option, id: &Handle, - content: &[u8], + content: Option<&[u8]>, domain: &Option, guard: WorldGuard, handler_ctxt: &mut HandlerContext

) -> Result<(), ScriptError> { let assignment_strategy = handler_ctxt.context_loading_settings.assignment_strategy; + let Some(content) = content.or_else(|| handler_ctxt.scripts.get(id).map(|script| &*script.content)) else { + warn!("No content for script {} to create or update", id.display()); + return Err(ScriptError::new(InteropError::missing_script( + id.clone(), + ))); + }; let phrase; + let success; 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"; + success = "updated"; 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"; + success = "created"; Self::load_context(id, content, guard.clone(), handler_ctxt) .map(Some) } @@ -174,9 +183,10 @@ impl CreateOrUpdateScript

{ } bevy::log::debug!( - "{}: script {} successfully created or updated", + "{}: script {} successfully {}", P::LANGUAGE, - id.display() + id.display(), + success, ); Ok(())// none until individual context support added. } @@ -204,7 +214,7 @@ impl Command for CreateOrUpdateScript

{ let result = with_handler_system_state( world, |guard, handler_ctxt: &mut HandlerContext

| { - Self::create_or_update_script(None, &self.id, &self.content, &self.domain, + Self::create_or_update_script(None, &self.id, self.content.as_deref(), &self.domain, guard, handler_ctxt) }); @@ -233,7 +243,7 @@ impl EntityCommand for CreateOrUpdateScript

{ 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, + Self::create_or_update_script(Some(entity), &self.id, self.content.as_deref(), &self.domain, guard, handler_ctxt) }); @@ -429,7 +439,7 @@ mod test { init(name, &mut context)?; } for init in pre_run_init { - init(name, Entity::from_raw(0), &mut context)?; + init(name, None, &mut context)?; } Ok(context) }, @@ -439,7 +449,7 @@ mod test { init(name, existing)?; } for init in pre_run_init { - init(name, Entity::from_raw(0), existing)?; + init(name, None, existing)?; } Ok(()) }, @@ -484,13 +494,13 @@ mod test { fn assert_context_and_script(world: &World, id: &str, context: &str, message: &str) { let scripts = world.get_resource::>().unwrap(); - let context = scripts.get(None, &Handle::default(), &None) + let context_arc = scripts.get(None, &Handle::default().id(), &None) .unwrap_or_else(|| panic!("Context not found {message}")); // assert_eq!(id, script.id); - let found_context = context.lock(); + let found_context = context_arc.lock(); - assert_eq!(*context, *found_context, "{message}"); + assert_eq!(context, found_context.as_str(), "{message}"); } fn assert_response_events( @@ -520,8 +530,10 @@ mod test { let mut app = setup_app(); let content = "content".as_bytes().to_vec().into_boxed_slice(); - let command = CreateOrUpdateScript::::new("script".into(), content, None); - command.apply(app.world_mut()); + let handle = Handle::default(); + let command = CreateOrUpdateScript::::new(handle.clone(), content, None); + // command.apply(app.world_mut()); + Command::apply(command, app.world_mut()); // check script assert_context_and_script( @@ -533,8 +545,9 @@ mod test { // update the script let content = "new content".as_bytes().to_vec().into_boxed_slice(); - let command = CreateOrUpdateScript::::new("script".into(), content, None); - command.apply(app.world_mut()); + let script = Handle::default(); + let command = CreateOrUpdateScript::::new(script.clone(), content, None); + Command::apply(command, app.world_mut()); // check script assert_context_and_script( @@ -546,9 +559,10 @@ mod test { // create second script let content = "content2".as_bytes().to_vec().into_boxed_slice(); - let command = CreateOrUpdateScript::::new("script2".into(), content, None); + let script2 = Handle::default(); + let command = CreateOrUpdateScript::::new(script2.clone(), content, None); - command.apply(app.world_mut()); + Command::apply(command, app.world_mut()); // check second script assert_context_and_script( @@ -560,8 +574,9 @@ mod test { // run a specific callback on the first script RunScriptCallback::::new( - "script".into(), - Entity::from_raw(0), + script.clone(), + None, + None, OnScriptLoaded::into_callback_label(), vec![], true, @@ -580,7 +595,7 @@ mod test { app.world_mut(), vec![ScriptCallbackResponseEvent::new( OnScriptLoaded::into_callback_label(), - "script".into(), + script.id(), Ok(ScriptValue::Unit), )] .into_iter(), @@ -588,15 +603,15 @@ mod test { ); // delete both scripts - let command = DeleteScript::::new("script".into()); + let command = DeleteScript::::new(script.id(), None); command.apply(app.world_mut()); - let command = DeleteScript::::new("script2".into()); + let command = DeleteScript::::new(script2.id(), None); command.apply(app.world_mut()); // check that the scripts are gone let scripts = app .world_mut() - .get_resource::>() + .get_resource::() .unwrap(); assert!(scripts.scripts.is_empty()); @@ -607,6 +622,15 @@ mod test { ); } + fn add_script(app: &mut App, content: impl Into) -> Handle { + app.world_mut().resource_mut::>().add(ScriptAsset::from(content.into())) + } + + fn update_script(app: &mut App, handle: AssetId, content: impl Into) { + let mut script_asset = app.world_mut().resource_mut::>().get_mut(handle).unwrap(); + script_asset.bytes = content.into().into_vec().into_boxed_slice(); + } + #[test] fn test_commands_with_global_assigner() { // setup all the resources necessary @@ -620,8 +644,8 @@ mod test { settings.assignment_strategy = crate::context::ContextAssignmentStrategy::Global; // create a script - let content = "content".as_bytes().to_vec().into_boxed_slice(); - let command = CreateOrUpdateScript::::new("script".into(), content, None); + let script = add_script(&mut app, "content"); + let command = CreateOrUpdateScript::::new(script.clone(), None, None); command.apply(app.world_mut()); @@ -635,8 +659,8 @@ mod test { // update the script - let content = "new content".as_bytes().to_vec().into_boxed_slice(); - let command = CreateOrUpdateScript::::new("script".into(), content, None); + update_script(&mut app, script.id(), "new content"); + let command = CreateOrUpdateScript::::new(script.clone(), None, None); command.apply(app.world_mut()); @@ -651,8 +675,8 @@ mod test { // create second script - let content = "content2".as_bytes().to_vec().into_boxed_slice(); - let command = CreateOrUpdateScript::::new("script2".into(), content, None); + let script2 = add_script(&mut app, "content2"); + let command = CreateOrUpdateScript::::new(script2.clone(), None, None); command.apply(app.world_mut()); @@ -671,7 +695,7 @@ mod test { "First script context was not updated on second script insert", ); - let scripts = app.world().get_resource::>().unwrap(); + let scripts = app.world().get_resource::>().unwrap(); assert!(scripts.scripts.len() == 2); // delete first script @@ -695,10 +719,10 @@ mod test { // check that the scripts are gone, and so is the context - let scripts = app.world().get_resource::>().unwrap(); + let scripts = app.world().get_resource::>().unwrap(); assert!(scripts.scripts.is_empty()); - let scripts = app.world().get_resource::>().unwrap(); + let scripts = app.world().get_resource::>().unwrap(); assert_eq!(scripts.scripts.len(), 0, "scripts weren't removed"); assert_response_events( diff --git a/crates/bevy_mod_scripting_core/src/extractors.rs b/crates/bevy_mod_scripting_core/src/extractors.rs index 6cfc94a041..ce9940c532 100644 --- a/crates/bevy_mod_scripting_core/src/extractors.rs +++ b/crates/bevy_mod_scripting_core/src/extractors.rs @@ -5,7 +5,7 @@ use std::ops::{Deref, DerefMut}; use bevy::{ - asset::{LoadState, Handle}, + asset::{LoadState, Handle, Assets}, ecs::{ component::ComponentId, entity::Entity, @@ -160,6 +160,8 @@ pub struct HandlerContext<'s, P: IntoScriptPluginParams> { pub(crate) static_scripts: ResScope<'s, StaticScripts>, /// Script context pub(crate) script_context: ResScope<'s, ScriptContext

>, + /// Scripts + pub(crate) scripts: ResScope<'s, Assets>, } impl<'s, P: IntoScriptPluginParams> HandlerContext<'s, P> { From 2360caef17009fc1d8ae5ee9f1a5371568e35c5e Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Sat, 5 Jul 2025 05:39:45 -0400 Subject: [PATCH 36/41] doc: Document script::Or. --- crates/bevy_mod_scripting_core/src/script/script_context.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/bevy_mod_scripting_core/src/script/script_context.rs b/crates/bevy_mod_scripting_core/src/script/script_context.rs index 89150b491f..f0a9a9d49d 100644 --- a/crates/bevy_mod_scripting_core/src/script/script_context.rs +++ b/crates/bevy_mod_scripting_core/src/script/script_context.rs @@ -90,7 +90,9 @@ impl Default for ScriptContext

{ } } -struct Or(T, U); +/// Compose two ScriptContextProviders in a short-circuit OR relationship. Use T +/// first, failing that use U. +pub struct Or(pub T, pub U); impl, U: ScriptContextProvider

, P: IntoScriptPluginParams> ScriptContextProvider

for Or { #[inline] From 61ac4d496770c4a86cc9e1945cfeb0f2c4fd6c7f Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Sat, 5 Jul 2025 05:50:29 -0400 Subject: [PATCH 37/41] refactor: Unscore unused parameters in context providers. --- .../src/script/domain_context.rs | 14 +++++--------- .../src/script/entity_context.rs | 14 +++++--------- .../src/script/script_context.rs | 8 ++++---- .../src/script/scriptid_context.rs | 16 +++++++--------- .../src/script/shared_context.rs | 8 ++++---- 5 files changed, 25 insertions(+), 35 deletions(-) diff --git a/crates/bevy_mod_scripting_core/src/script/domain_context.rs b/crates/bevy_mod_scripting_core/src/script/domain_context.rs index 69a5c56278..2a7c4b1ba8 100644 --- a/crates/bevy_mod_scripting_core/src/script/domain_context.rs +++ b/crates/bevy_mod_scripting_core/src/script/domain_context.rs @@ -10,17 +10,13 @@ impl Default for DomainContext

{ } 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 hash(&self, _id: Option, _script_id: &ScriptId, domain: &Option) -> Option { + domain.as_ref().map(|d| DefaultHashBuilder::default().hash_one(&d)) } - fn get(&self, id: Option, script_id: &ScriptId, domain: &Option) -> Option<&Arc>> { + 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> { + 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(()) @@ -28,7 +24,7 @@ impl ScriptContextProvider

for DomainContext

{ Err(context) } } - fn contains(&self, id: Option, script_id: &ScriptId, domain: &Option) -> bool { + 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 index 0dde95ea1b..f65add66be 100644 --- a/crates/bevy_mod_scripting_core/src/script/entity_context.rs +++ b/crates/bevy_mod_scripting_core/src/script/entity_context.rs @@ -10,17 +10,13 @@ impl Default for EntityContext

{ } 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 hash(&self, id: Option, _script_id: &ScriptId, _domain: &Option) -> Option { + id.map(|id| DefaultHashBuilder::default().hash_one(&id)) } - fn get(&self, id: Option, script_id: &ScriptId, domain: &Option) -> Option<&Arc>> { + 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> { + 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(()) @@ -28,7 +24,7 @@ impl ScriptContextProvider

for EntityContext

{ Err(context) } } - fn contains(&self, id: Option, script_id: &ScriptId, domain: &Option) -> bool { + 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/script_context.rs b/crates/bevy_mod_scripting_core/src/script/script_context.rs index f0a9a9d49d..ad30f076cf 100644 --- a/crates/bevy_mod_scripting_core/src/script/script_context.rs +++ b/crates/bevy_mod_scripting_core/src/script/script_context.rs @@ -1,9 +1,9 @@ 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 crate::IntoScriptPluginParams; +use bevy::prelude::{Component, Entity}; +use bevy::{asset::Asset, ecs::system::Resource, reflect::Reflect}; use parking_lot::Mutex; -use std::{borrow::Cow, collections::HashMap, ops::Deref, sync::Arc, fmt, marker::PhantomData}; +use std::{borrow::Cow, ops::Deref, sync::Arc}; /// A kind of catch all type for script context selection /// diff --git a/crates/bevy_mod_scripting_core/src/script/scriptid_context.rs b/crates/bevy_mod_scripting_core/src/script/scriptid_context.rs index f4d3bc67ed..266a5d5ebb 100644 --- a/crates/bevy_mod_scripting_core/src/script/scriptid_context.rs +++ b/crates/bevy_mod_scripting_core/src/script/scriptid_context.rs @@ -10,19 +10,17 @@ impl Default for ScriptIdContext

{ } 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 hash(&self, _id: Option, script_id: &ScriptId, _domain: &Option) -> Option { + Some(DefaultHashBuilder::default().hash_one(&script_id)) } - fn get(&self, id: Option, script_id: &ScriptId, domain: &Option) -> Option<&Arc>> { + 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))); + fn insert(&mut self, _id: Option, script_id: &ScriptId, _domain: &Option, context: P::C) -> Result<(), P::C> { + self.0.insert(*script_id, Arc::new(Mutex::new(context))); Ok(()) } - fn contains(&self, id: Option, script_id: &ScriptId, domain: &Option) -> bool { - self.0.contains_key(&script_id) + 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 index b47c099f7a..bc8663cc61 100644 --- a/crates/bevy_mod_scripting_core/src/script/shared_context.rs +++ b/crates/bevy_mod_scripting_core/src/script/shared_context.rs @@ -4,18 +4,18 @@ use super::*; pub struct SharedContext(pub Option>>); impl ScriptContextProvider

for SharedContext

{ - fn hash(&self, id: Option, script_id: &ScriptId, domain: &Option) -> Option { + 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>> { + 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> { + 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 { + fn contains(&self, _id: Option, _script_id: &ScriptId, _domain: &Option) -> bool { self.0.is_some() } } From f95caa22083d4ec9e7e0414bcb1ef1aa5d496d96 Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Sat, 5 Jul 2025 05:51:12 -0400 Subject: [PATCH 38/41] chore: Fix it, Clippy! --- crates/bevy_mod_scripting_core/src/asset.rs | 15 ++-- .../src/bindings/script_system.rs | 4 +- .../bevy_mod_scripting_core/src/commands.rs | 70 ++++++++----------- crates/bevy_mod_scripting_core/src/context.rs | 1 - crates/bevy_mod_scripting_core/src/error.rs | 2 +- .../bevy_mod_scripting_core/src/extractors.rs | 5 +- crates/bevy_mod_scripting_core/src/handler.rs | 4 +- crates/bevy_mod_scripting_core/src/lib.rs | 2 +- .../src/script/domain_context.rs | 2 +- .../src/script/entity_context.rs | 2 +- .../bevy_mod_scripting_core/src/script/mod.rs | 6 +- .../src/script/scriptid_context.rs | 2 +- 12 files changed, 52 insertions(+), 63 deletions(-) diff --git a/crates/bevy_mod_scripting_core/src/asset.rs b/crates/bevy_mod_scripting_core/src/asset.rs index c880dde5f5..8ed971928f 100644 --- a/crates/bevy_mod_scripting_core/src/asset.rs +++ b/crates/bevy_mod_scripting_core/src/asset.rs @@ -3,22 +3,20 @@ use crate::{ StaticScripts, ScriptComponent, - commands::{CreateOrUpdateScript, DeleteScript}, + commands::CreateOrUpdateScript, error::ScriptError, - script::{DisplayProxy, ScriptId, Domain, ScriptDomain}, + script::{DisplayProxy, Domain, ScriptDomain}, IntoScriptPluginParams, ScriptingSystemSet, }; use bevy::{ app::{App, PreUpdate}, - asset::{Asset, AssetEvent, AssetId, AssetLoader, AssetPath, Assets, LoadState}, - ecs::system::Resource, - log::{debug, info, trace, warn, error}, + asset::{Asset, AssetEvent, AssetLoader, Assets, LoadState}, + log::{info, trace, warn, error}, prelude::{ - Commands, Event, EventReader, EventWriter, IntoSystemConfigs, IntoSystemSetConfigs, Res, + Commands, EventReader, IntoSystemConfigs, IntoSystemSetConfigs, Res, ResMut, Added, Query, Local, Handle, AssetServer, Entity, }, reflect::TypePath, - utils::hashbrown::HashMap, }; use std::{borrow::Cow, collections::VecDeque}; use serde::{Deserialize, Serialize}; @@ -212,7 +210,7 @@ pub(crate) fn eval_script( let script_ready = script_queue .front() .map(|(_, script_id, _)| { - script_assets.contains(script_id.id()) || match asset_server.load_state(&*script_id) { + script_assets.contains(script_id.id()) || match asset_server.load_state(script_id) { LoadState::NotLoaded => false, LoadState::Loading => false, LoadState::Loaded => true, @@ -295,6 +293,7 @@ mod tests { use bevy::{ app::{App, Update}, asset::{AssetApp, AssetPlugin, AssetServer, Assets, Handle, LoadState}, + prelude::Resource, MinimalPlugins, }; 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 9994a88cb6..c4e76b8366 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/script_system.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/script_system.rs @@ -18,12 +18,12 @@ use crate::{ extractors::get_all_access_ids, handler::CallbackSettings, runtime::RuntimeContainer, - script::{ScriptId, ScriptContextProvider, ScriptContext, Domain}, + script::{ScriptContextProvider, ScriptContext, Domain}, IntoScriptPluginParams, }; use bevy::{ asset::Handle, - prelude::{Query, AssetServer}, + prelude::AssetServer, ecs::{ archetype::{ArchetypeComponentId, ArchetypeGeneration}, component::{ComponentId, Tick}, diff --git a/crates/bevy_mod_scripting_core/src/commands.rs b/crates/bevy_mod_scripting_core/src/commands.rs index 959aeefa15..7ad7a8e154 100644 --- a/crates/bevy_mod_scripting_core/src/commands.rs +++ b/crates/bevy_mod_scripting_core/src/commands.rs @@ -10,14 +10,13 @@ use crate::{ CallbackLabel, IntoCallbackLabel, OnScriptLoaded, OnScriptUnloaded, ScriptCallbackResponseEvent, }, - extractors::{with_handler_system_state, HandlerContext, WithWorldGuard}, + extractors::{with_handler_system_state, HandlerContext}, handler::{handle_script_errors, send_callback_response}, - script::{ScriptId, StaticScripts, DisplayProxy, ScriptContextProvider, Domain, ScriptContext}, + script::{StaticScripts, DisplayProxy, ScriptContextProvider, Domain}, IntoScriptPluginParams, }; -use bevy::{asset::{Assets, Handle}, ecs::entity::Entity, log::{warn, debug}, prelude::{EntityCommand, Command}}; -use parking_lot::Mutex; -use std::{marker::PhantomData, sync::Arc}; +use bevy::{asset::Handle, ecs::entity::Entity, log::{warn, debug}, prelude::{EntityCommand, Command}}; +use std::marker::PhantomData; /// Deletes a script with the given ID pub struct DeleteScript { @@ -43,7 +42,7 @@ impl Command for DeleteScript

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

::new( - Handle::Weak(self.id.clone()), + Handle::Weak(self.id), None, self.domain, OnScriptUnloaded::into_callback_label(), @@ -105,7 +104,7 @@ impl CreateOrUpdateScript

{ (ContextBuilder::

::reload)( handler_ctxt.context_loading_settings.loader.reload, - &id, + id, content, context, &handler_ctxt.context_loading_settings.context_initializers, @@ -125,7 +124,7 @@ impl CreateOrUpdateScript

{ ) -> Result { let context = (ContextBuilder::

::load)( handler_ctxt.context_loading_settings.loader.load, - &id, + id, content, &handler_ctxt.context_loading_settings.context_initializers, &handler_ctxt @@ -219,20 +218,17 @@ impl Command for CreateOrUpdateScript

{ }); // immediately run command for callback, but only if loading went fine - match result { - Ok(_) => { - - RunScriptCallback::

::new( - self.id, - None, - self.domain, - OnScriptLoaded::into_callback_label(), - vec![], - false, - ) - .apply(world) - } - Err(_) => () + if let Ok(_) = result { + + RunScriptCallback::

::new( + self.id, + None, + self.domain, + OnScriptLoaded::into_callback_label(), + vec![], + false, + ) + .apply(world) } } } @@ -248,20 +244,17 @@ impl EntityCommand for CreateOrUpdateScript

{ }); // immediately run command for callback, but only if loading went fine - match result { - Ok(maybe_script) => { - - RunScriptCallback::

::new( - self.id, - Some(entity), - self.domain, - OnScriptLoaded::into_callback_label(), - vec![], - false, - ) - .apply(world) - } - Err(_) => () + if let Ok(maybe_script) = result { + + RunScriptCallback::

::new( + self.id, + Some(entity), + self.domain, + OnScriptLoaded::into_callback_label(), + vec![], + false, + ) + .apply(world) } } } @@ -404,14 +397,13 @@ impl Command for RemoveStaticScript { #[cfg(test)] mod test { use bevy::{ - app::App, - ecs::event::Events, log::{Level, LogPlugin}, - prelude::{Entity, World}, + prelude::*, }; use crate::{ asset::Language, + script::ScriptContext, bindings::script_value::ScriptValue, context::{ContextBuilder, ContextLoadingSettings}, handler::CallbackSettings, diff --git a/crates/bevy_mod_scripting_core/src/context.rs b/crates/bevy_mod_scripting_core/src/context.rs index 433e1b102b..c592b2e51c 100644 --- a/crates/bevy_mod_scripting_core/src/context.rs +++ b/crates/bevy_mod_scripting_core/src/context.rs @@ -4,7 +4,6 @@ use crate::{ ScriptAsset, bindings::{ThreadWorldContainer, WorldContainer, WorldGuard}, error::{InteropError, ScriptError}, - script::ScriptId, IntoScriptPluginParams, }; use bevy::{ diff --git a/crates/bevy_mod_scripting_core/src/error.rs b/crates/bevy_mod_scripting_core/src/error.rs index c5d3ee4c85..dbad891b60 100644 --- a/crates/bevy_mod_scripting_core/src/error.rs +++ b/crates/bevy_mod_scripting_core/src/error.rs @@ -9,7 +9,7 @@ use crate::{ script_value::ScriptValue, ReflectBaseType, ReflectReference, }, - script::{DisplayProxy, ScriptId}, + script::DisplayProxy, }; use bevy::{ asset::Handle, diff --git a/crates/bevy_mod_scripting_core/src/extractors.rs b/crates/bevy_mod_scripting_core/src/extractors.rs index ce9940c532..bfa6bbc58c 100644 --- a/crates/bevy_mod_scripting_core/src/extractors.rs +++ b/crates/bevy_mod_scripting_core/src/extractors.rs @@ -5,7 +5,7 @@ use std::ops::{Deref, DerefMut}; use bevy::{ - asset::{LoadState, Handle, Assets}, + asset::{Handle, Assets}, ecs::{ component::ComponentId, entity::Entity, @@ -15,7 +15,6 @@ use bevy::{ system::{Local, Resource, SystemParam, SystemState}, world::World, }, - prelude::{AssetServer, Query, Res}, }; use fixedbitset::FixedBitSet; @@ -30,7 +29,7 @@ use crate::{ event::{CallbackLabel, IntoCallbackLabel}, handler::CallbackSettings, runtime::RuntimeContainer, - script::{ScriptId, StaticScripts, ScriptContext, DisplayProxy, ScriptContextProvider, Domain}, + script::{StaticScripts, ScriptContext, DisplayProxy, ScriptContextProvider, Domain}, IntoScriptPluginParams, }; diff --git a/crates/bevy_mod_scripting_core/src/handler.rs b/crates/bevy_mod_scripting_core/src/handler.rs index bcb06d5e41..04901d43e9 100644 --- a/crates/bevy_mod_scripting_core/src/handler.rs +++ b/crates/bevy_mod_scripting_core/src/handler.rs @@ -12,7 +12,7 @@ use crate::{ ScriptErrorEvent, }, extractors::{HandlerContext, WithWorldGuard}, - script::{ScriptComponent, ScriptId, ScriptDomain, Domain, ScriptContextProvider}, + script::{ScriptComponent, ScriptDomain, ScriptContextProvider}, IntoScriptPluginParams, }; use bevy::{ @@ -227,7 +227,7 @@ pub(crate) fn event_handler_inner( let call_result = handler_ctxt.call_dynamic_label( &callback_label, - &script_id, + script_id, *entity, domain, event.args.clone(), diff --git a/crates/bevy_mod_scripting_core/src/lib.rs b/crates/bevy_mod_scripting_core/src/lib.rs index 367a6799a0..6ed4748e68 100644 --- a/crates/bevy_mod_scripting_core/src/lib.rs +++ b/crates/bevy_mod_scripting_core/src/lib.rs @@ -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, StaticScripts, ScriptContext, EntityContext}; +use script::{ScriptComponent, StaticScripts, ScriptContext}; pub mod asset; pub mod bindings; diff --git a/crates/bevy_mod_scripting_core/src/script/domain_context.rs b/crates/bevy_mod_scripting_core/src/script/domain_context.rs index 2a7c4b1ba8..a3618e3478 100644 --- a/crates/bevy_mod_scripting_core/src/script/domain_context.rs +++ b/crates/bevy_mod_scripting_core/src/script/domain_context.rs @@ -11,7 +11,7 @@ impl Default for DomainContext

{ impl ScriptContextProvider

for DomainContext

{ fn hash(&self, _id: Option, _script_id: &ScriptId, domain: &Option) -> Option { - domain.as_ref().map(|d| DefaultHashBuilder::default().hash_one(&d)) + domain.as_ref().map(|d| DefaultHashBuilder::default().hash_one(d)) } fn get(&self, _id: Option, _script_id: &ScriptId, domain: &Option) -> Option<&Arc>> { domain.as_ref().and_then(|id| self.0.get(id)) diff --git a/crates/bevy_mod_scripting_core/src/script/entity_context.rs b/crates/bevy_mod_scripting_core/src/script/entity_context.rs index f65add66be..28d931f428 100644 --- a/crates/bevy_mod_scripting_core/src/script/entity_context.rs +++ b/crates/bevy_mod_scripting_core/src/script/entity_context.rs @@ -11,7 +11,7 @@ impl Default for EntityContext

{ impl ScriptContextProvider

for EntityContext

{ fn hash(&self, id: Option, _script_id: &ScriptId, _domain: &Option) -> Option { - id.map(|id| DefaultHashBuilder::default().hash_one(&id)) + id.map(|id| DefaultHashBuilder::default().hash_one(id)) } fn get(&self, id: Option, _script_id: &ScriptId, _domain: &Option) -> Option<&Arc>> { id.and_then(|id| self.0.get(&id)) diff --git a/crates/bevy_mod_scripting_core/src/script/mod.rs b/crates/bevy_mod_scripting_core/src/script/mod.rs index bf1148066e..db8268f9a8 100644 --- a/crates/bevy_mod_scripting_core/src/script/mod.rs +++ b/crates/bevy_mod_scripting_core/src/script/mod.rs @@ -1,7 +1,7 @@ //! Script related types, functions and components use crate::{asset::ScriptAsset, IntoScriptPluginParams}; -use bevy::prelude::{Component, ReflectComponent, Deref, DerefMut, Entity}; +use bevy::prelude::{Component, ReflectComponent, 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}}; @@ -30,7 +30,7 @@ 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) + write!(f, "path {path}") } else { write!(f, "id {}", self.0.id()) } @@ -40,7 +40,7 @@ impl<'a, A: Asset> fmt::Display for HandleDisplay<'a, A> { 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) + write!(f, "path {path:?}") } else { write!(f, "id {:?}", self.0.id()) } diff --git a/crates/bevy_mod_scripting_core/src/script/scriptid_context.rs b/crates/bevy_mod_scripting_core/src/script/scriptid_context.rs index 266a5d5ebb..9eba613b79 100644 --- a/crates/bevy_mod_scripting_core/src/script/scriptid_context.rs +++ b/crates/bevy_mod_scripting_core/src/script/scriptid_context.rs @@ -11,7 +11,7 @@ impl Default for ScriptIdContext

{ impl ScriptContextProvider

for ScriptIdContext

{ fn hash(&self, _id: Option, script_id: &ScriptId, _domain: &Option) -> Option { - Some(DefaultHashBuilder::default().hash_one(&script_id)) + Some(DefaultHashBuilder::default().hash_one(script_id)) } fn get(&self, _id: Option, script_id: &ScriptId, _domain: &Option) -> Option<&Arc>> { self.0.get(script_id) From 80db901de63a5990c0ca0aaf8a98c45f328f582b Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Sat, 5 Jul 2025 19:18:32 -0400 Subject: [PATCH 39/41] doc: Add note about ScriptContext::hash(). --- crates/bevy_mod_scripting_core/src/script/script_context.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/bevy_mod_scripting_core/src/script/script_context.rs b/crates/bevy_mod_scripting_core/src/script/script_context.rs index ad30f076cf..1929226151 100644 --- a/crates/bevy_mod_scripting_core/src/script/script_context.rs +++ b/crates/bevy_mod_scripting_core/src/script/script_context.rs @@ -24,6 +24,9 @@ pub trait ScriptContextProvider { /// /// Useful for tracking what context will be returned by `get()` without /// requiring that `P::C` impl `Hash` and cheaper too. + /// + /// Note: The existence of the hash does not imply the context exists. It + /// only declares what its hash will be. fn hash(&self, id: Option, script_id: &ScriptId, domain: &Option) -> Option; } From 5a5735a4fb033ad343af20ea7f80e663a6f6caad Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Sun, 6 Jul 2025 05:24:23 -0400 Subject: [PATCH 40/41] test: Fix/refactor tests. --- .../bevy_mod_scripting_core/src/commands.rs | 20 ++++++++--------- .../bevy_mod_scripting_core/src/script/mod.rs | 22 +++++++++++-------- .../bevy_mod_scripting_rhai/src/lib.rs | 2 +- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/crates/bevy_mod_scripting_core/src/commands.rs b/crates/bevy_mod_scripting_core/src/commands.rs index 7ad7a8e154..bae6b1148f 100644 --- a/crates/bevy_mod_scripting_core/src/commands.rs +++ b/crates/bevy_mod_scripting_core/src/commands.rs @@ -523,7 +523,7 @@ mod test { let content = "content".as_bytes().to_vec().into_boxed_slice(); let handle = Handle::default(); - let command = CreateOrUpdateScript::::new(handle.clone(), content, None); + let command = CreateOrUpdateScript::::new(handle.clone(), Some(content), None); // command.apply(app.world_mut()); Command::apply(command, app.world_mut()); @@ -538,7 +538,7 @@ mod test { // update the script let content = "new content".as_bytes().to_vec().into_boxed_slice(); let script = Handle::default(); - let command = CreateOrUpdateScript::::new(script.clone(), content, None); + let command = CreateOrUpdateScript::::new(script.clone(), Some(content), None); Command::apply(command, app.world_mut()); // check script @@ -552,7 +552,7 @@ mod test { // create second script let content = "content2".as_bytes().to_vec().into_boxed_slice(); let script2 = Handle::default(); - let command = CreateOrUpdateScript::::new(script2.clone(), content, None); + let command = CreateOrUpdateScript::::new(script2.clone(), Some(content), None); Command::apply(command, app.world_mut()); @@ -620,7 +620,7 @@ mod test { fn update_script(app: &mut App, handle: AssetId, content: impl Into) { let mut script_asset = app.world_mut().resource_mut::>().get_mut(handle).unwrap(); - script_asset.bytes = content.into().into_vec().into_boxed_slice(); + script_asset.content = content.into().into_bytes().into_boxed_slice(); } #[test] @@ -639,7 +639,7 @@ mod test { let script = add_script(&mut app, "content"); let command = CreateOrUpdateScript::::new(script.clone(), None, None); - command.apply(app.world_mut()); + Command::apply(command, app.world_mut()); // check script assert_context_and_script( @@ -654,7 +654,7 @@ mod test { update_script(&mut app, script.id(), "new content"); let command = CreateOrUpdateScript::::new(script.clone(), None, None); - command.apply(app.world_mut()); + Command::apply(command, app.world_mut()); // check script @@ -670,7 +670,7 @@ mod test { let script2 = add_script(&mut app, "content2"); let command = CreateOrUpdateScript::::new(script2.clone(), None, None); - command.apply(app.world_mut()); + Command::apply(command, app.world_mut()); // check both scripts have the new context @@ -687,13 +687,13 @@ mod test { "First script context was not updated on second script insert", ); - let scripts = app.world().get_resource::>().unwrap(); + let scripts = app.world().get_resource::().unwrap(); assert!(scripts.scripts.len() == 2); // delete first script let command = DeleteScript::::new("script".into()); - command.apply(app.world_mut()); + Command::apply(command, app.world_mut()); // check second script still has the context, and on unload was called assert_context_and_script( @@ -707,7 +707,7 @@ mod test { let command = DeleteScript::::new("script2".into()); - command.apply(app.world_mut()); + Command::apply(command, app.world_mut()); // check that the scripts are gone, and so is the context diff --git a/crates/bevy_mod_scripting_core/src/script/mod.rs b/crates/bevy_mod_scripting_core/src/script/mod.rs index db8268f9a8..63aa8b5fde 100644 --- a/crates/bevy_mod_scripting_core/src/script/mod.rs +++ b/crates/bevy_mod_scripting_core/src/script/mod.rs @@ -128,27 +128,31 @@ mod tests { #[test] fn static_scripts_insert() { let mut static_scripts = StaticScripts::default(); - static_scripts.insert("script1"); + let script1 = Handle::default(); + static_scripts.insert(script1); assert_eq!(static_scripts.scripts.len(), 1); - assert!(static_scripts.scripts.contains("script1")); + assert!(static_scripts.scripts.contains(script1)); } #[test] fn static_scripts_remove() { let mut static_scripts = StaticScripts::default(); - static_scripts.insert("script1"); + let script1 = Handle::default(); + static_scripts.insert(script1); assert_eq!(static_scripts.scripts.len(), 1); - assert!(static_scripts.scripts.contains("script1")); - assert!(static_scripts.remove("script1")); + assert!(static_scripts.scripts.contains(script1)); + assert!(static_scripts.remove(script1)); assert_eq!(static_scripts.scripts.len(), 0); - assert!(!static_scripts.scripts.contains("script1")); + 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")); + let script1 = Handle::default(); + let script2 = Handle::default(); + static_scripts.insert(script1); + assert!(static_scripts.contains(script1)); + assert!(!static_scripts.contains(script2)); } } diff --git a/crates/languages/bevy_mod_scripting_rhai/src/lib.rs b/crates/languages/bevy_mod_scripting_rhai/src/lib.rs index 273a854482..f7d4f45ece 100644 --- a/crates/languages/bevy_mod_scripting_rhai/src/lib.rs +++ b/crates/languages/bevy_mod_scripting_rhai/src/lib.rs @@ -307,7 +307,7 @@ mod test { #[test] fn test_reload_doesnt_overwrite_old_context() { let runtime = RhaiRuntime::new(Engine::new()); - let script_id = ScriptId::from("asd.rhai"); + let script_id = Handle::default(); let initializers: Vec> = vec![]; let pre_handling_initializers: Vec> = vec![]; From 74a88d947824220d29b700619b2eb8ac38e3305c Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Sun, 6 Jul 2025 05:25:14 -0400 Subject: [PATCH 41/41] feature: Remove todo! & impl supported extensions. Implement the ConfigureScriptAssetSettings again. --- crates/bevy_mod_scripting_core/src/asset.rs | 65 ++++++++++++------ crates/bevy_mod_scripting_core/src/handler.rs | 1 - crates/bevy_mod_scripting_core/src/lib.rs | 66 ++++++++----------- 3 files changed, 72 insertions(+), 60 deletions(-) diff --git a/crates/bevy_mod_scripting_core/src/asset.rs b/crates/bevy_mod_scripting_core/src/asset.rs index 8ed971928f..9b1b1251ca 100644 --- a/crates/bevy_mod_scripting_core/src/asset.rs +++ b/crates/bevy_mod_scripting_core/src/asset.rs @@ -3,6 +3,7 @@ use crate::{ StaticScripts, ScriptComponent, + LanguageExtensions, commands::CreateOrUpdateScript, error::ScriptError, script::{DisplayProxy, Domain, ScriptDomain}, @@ -78,11 +79,41 @@ pub struct ScriptSettings { /// A loader for script assets pub struct ScriptAssetLoader { /// The file extensions this loader should handle - pub extensions: &'static [&'static str], + language_extensions: LanguageExtensions, + extensions: &'static [&'static str], /// preprocessor to run on the script before saving the content to an asset pub preprocessor: Option Result<(), ScriptError> + Send + Sync>>, } +impl ScriptAssetLoader { + /// Create a new script asset loader for the given extensions. + pub fn new(language_extensions: LanguageExtensions) -> Self { + let extensions: Vec<&'static str> = language_extensions.keys() + .into_iter() + .map(|x| *x) + .collect(); + let new_arr_static = Vec::leak(extensions); + Self { + language_extensions, + extensions: new_arr_static, + preprocessor: None, + } + } + + // For testing purposes. + pub(crate) fn for_extension(extension: &'static str) -> Self { + let mut langext = LanguageExtensions::default(); + langext.insert(extension, Language::Unknown); + Self::new(langext) + } + + /// Add a preprocessor + pub fn with_preprocessor(mut self, preprocessor: Box Result<(), ScriptError> + Send + Sync>) -> Self { + self.preprocessor = Some(preprocessor); + self + } +} + #[profiling::all_functions] impl AssetLoader for ScriptAssetLoader { type Asset = ScriptAsset; @@ -108,16 +139,13 @@ impl AssetLoader for ScriptAssetLoader { 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 - } - }); + .unwrap_or_else(|| { + let ext = load_context.path().extension().and_then(|e| e.to_str()).unwrap_or_default(); + self.language_extensions.get(ext).cloned().unwrap_or_else(|| { + warn!("Unknown language for {:?}", load_context.path().display()); + Language::Unknown + }) + }); let asset = ScriptAsset { content: content.into_boxed_slice(), language, @@ -292,7 +320,7 @@ mod tests { use bevy::{ app::{App, Update}, - asset::{AssetApp, AssetPlugin, AssetServer, Assets, Handle, LoadState}, + asset::{AssetApp, AssetPlugin, AssetServer, Assets, Handle, LoadState, AssetPath}, prelude::Resource, MinimalPlugins, }; @@ -341,10 +369,7 @@ mod tests { #[test] fn test_asset_loader_loads() { - let loader = ScriptAssetLoader { - extensions: &["script"], - preprocessor: None, - }; + let loader = ScriptAssetLoader::for_extension("script"); let mut app = init_loader_test(loader); let handle = load_asset(&mut app, "test_assets/test_script.script"); @@ -363,13 +388,11 @@ mod tests { #[test] fn test_asset_loader_applies_preprocessor() { - let loader = ScriptAssetLoader { - extensions: &["script"], - preprocessor: Some(Box::new(|content| { + let loader = ScriptAssetLoader::for_extension("script") + .with_preprocessor(Box::new(|content| { content[0] = b'p'; Ok(()) - })), - }; + })); let mut app = init_loader_test(loader); let handle = load_asset(&mut app, "test_assets/test_script.script"); diff --git a/crates/bevy_mod_scripting_core/src/handler.rs b/crates/bevy_mod_scripting_core/src/handler.rs index 04901d43e9..34574f1ad4 100644 --- a/crates/bevy_mod_scripting_core/src/handler.rs +++ b/crates/bevy_mod_scripting_core/src/handler.rs @@ -145,7 +145,6 @@ pub(crate) fn event_handler_inner( 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(); diff --git a/crates/bevy_mod_scripting_core/src/lib.rs b/crates/bevy_mod_scripting_core/src/lib.rs index 6ed4748e68..bd4a52270e 100644 --- a/crates/bevy_mod_scripting_core/src/lib.rs +++ b/crates/bevy_mod_scripting_core/src/lib.rs @@ -5,9 +5,9 @@ use crate::event::ScriptErrorEvent; use asset::{ configure_asset_systems, configure_asset_systems_for_plugin, Language, ScriptAsset, - ScriptAssetLoader, //ScriptAssetSettings, + ScriptAssetLoader, }; -use bevy::prelude::*; +use bevy::{prelude::*, utils::HashMap}; use bindings::{ function::script_function::AppScriptFunctionRegistry, garbage_collector, schedule::AppScheduleRegistry, script_value::ScriptValue, AppReflectAllocator, @@ -281,24 +281,11 @@ 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 = &["lua", "luau", "rhai", "rn"]; - app.register_asset_loader(ScriptAssetLoader { - extensions: asset_settings_extensions, - preprocessor: None, - }); - - // pre-register component id's + // Read extensions. + let language_extensions = app.world_mut().remove_resource::() + .unwrap_or_default(); + app.register_asset_loader(ScriptAssetLoader::new(language_extensions)); + // Pre-register component IDs. pre_register_components(app); DynamicScriptComponentPlugin.finish(app); } @@ -393,31 +380,34 @@ pub trait ConfigureScriptAssetSettings { ) -> &mut Self; } +/// Collect the language extensions supported during initialization. +/// +/// NOTE: This resource is removed after plugin setup. +#[derive(Debug, Resource, Deref, DerefMut)] +pub struct LanguageExtensions(HashMap<&'static str, Language>); + +impl Default for LanguageExtensions { + fn default() -> Self { + LanguageExtensions([("lua", Language::Lua), + ("rhai", Language::Rhai), + ("rn", Language::Rune)].into_iter().collect()) + } +} + impl ConfigureScriptAssetSettings for App { fn add_supported_script_extensions( &mut self, extensions: &[&'static str], language: Language, ) -> &mut Self { - todo!() - // let mut asset_settings = self - // .world_mut() - // .get_resource_or_init::(); + let mut language_extensions = self + .world_mut() + .get_resource_or_init::(); - // let mut new_arr = Vec::from(asset_settings.supported_extensions); - - // new_arr.extend(extensions); - - // let new_arr_static = Vec::leak(new_arr); - - // asset_settings.supported_extensions = new_arr_static; - // for extension in extensions { - // asset_settings - // .extension_to_language_map - // .insert(*extension, language.clone()); - // } - - // self + for extension in extensions { + language_extensions.insert(extension, language.clone()); + } + self } }