From 9d7c1319ea92437f436df7086233431524eab294 Mon Sep 17 00:00:00 2001 From: makspll Date: Sat, 30 Aug 2025 00:10:43 +0100 Subject: [PATCH 1/6] refactor: add world local plugin config --- Cargo.toml | 2 + crates/bevy_mod_scripting_core/Cargo.toml | 1 + crates/bevy_mod_scripting_core/src/asset.rs | 12 +- .../src/bindings/schedule.rs | 3 + .../src/bindings/script_system.rs | 41 ++--- .../src/bindings/world.rs | 13 ++ .../bevy_mod_scripting_core/src/commands.rs | 14 +- crates/bevy_mod_scripting_core/src/config.rs | 93 ++++++++++++ crates/bevy_mod_scripting_core/src/context.rs | 51 +------ crates/bevy_mod_scripting_core/src/event.rs | 1 + .../bevy_mod_scripting_core/src/extractors.rs | 143 ++---------------- crates/bevy_mod_scripting_core/src/handler.rs | 7 +- crates/bevy_mod_scripting_core/src/lib.rs | 20 ++- .../src/script/script_context.rs | 1 + .../bevy_mod_scripting_lua/src/lib.rs | 4 + .../bevy_mod_scripting_rhai/Cargo.toml | 1 + .../bevy_mod_scripting_rhai/src/lib.rs | 5 + .../test_consumer_crate/Cargo.toml | 24 +++ .../test_consumer_crate/src/main.rs | 3 + .../test_utils/src/test_plugin.rs | 2 + tests/script_tests.rs | 10 +- 21 files changed, 212 insertions(+), 239 deletions(-) create mode 100644 crates/bevy_mod_scripting_core/src/config.rs create mode 100644 crates/testing_crates/test_consumer_crate/Cargo.toml create mode 100644 crates/testing_crates/test_consumer_crate/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index e9a301cd9a..95d02c533a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -99,6 +99,7 @@ bevy_mod_scripting_rhai = { workspace = true, optional = true } bevy_mod_scripting_functions = { workspace = true } bevy_mod_scripting_derive = { workspace = true } + [workspace.dependencies] # local crates script_integration_test_harness = { path = "crates/testing_crates/script_integration_test_harness" } @@ -115,6 +116,7 @@ bevy_mod_scripting_rhai = { path = "crates/languages/bevy_mod_scripting_rhai", v bevy_mod_scripting_core = { path = "crates/bevy_mod_scripting_core", version = "0.15.1" } bevy = { version = "0.16.0", default-features = false } bevy_math = { version = "0.16.0", default-features = false, features = ["std"] } +bevy_tasks = { version = "0.16.0", default-features = false } bevy_transform = { version = "0.16.0", default-features = false } bevy_reflect = { version = "0.16.0", default-features = false } bevy_ecs = { version = "0.16.0", default-features = false } diff --git a/crates/bevy_mod_scripting_core/Cargo.toml b/crates/bevy_mod_scripting_core/Cargo.toml index 1a88236531..14ac8bda1b 100644 --- a/crates/bevy_mod_scripting_core/Cargo.toml +++ b/crates/bevy_mod_scripting_core/Cargo.toml @@ -35,6 +35,7 @@ bevy_log = { workspace = true, default-features = false, features = [] } bevy_asset = { workspace = true, default-features = false, features = [] } bevy_diagnostic = { workspace = true, default-features = false, features = [] } bevy_platform = { workspace = true, default-features = false, features = [] } +bevy_tasks = { workspace = true, default-features = false, features = [] } parking_lot = { workspace = true } smallvec = { workspace = true } diff --git a/crates/bevy_mod_scripting_core/src/asset.rs b/crates/bevy_mod_scripting_core/src/asset.rs index d34a7ff79f..f8d84cbeb5 100644 --- a/crates/bevy_mod_scripting_core/src/asset.rs +++ b/crates/bevy_mod_scripting_core/src/asset.rs @@ -18,13 +18,13 @@ use bevy_ecs::{ event::{EventReader, EventWriter}, schedule::IntoScheduleConfigs, system::{Commands, Local, Query, Res}, + world::WorldId, }; use serde::{Deserialize, Serialize}; use crate::{ IntoScriptPluginParams, LanguageExtensions, ScriptComponent, ScriptingSystemSet, StaticScripts, commands::{CreateOrUpdateScript, DeleteScript}, - context::ContextLoadingSettings, error::ScriptError, event::ScriptEvent, script::{ContextKey, DisplayProxy, ScriptAttachment}, @@ -220,7 +220,7 @@ fn handle_script_events( asset_server: Res, mut script_queue: Local, mut commands: Commands, - context_loading_settings: Res>, + world_id: WorldId, ) { for event in events.read() { trace!("{}: Received script event: {:?}", P::LANGUAGE, event); @@ -242,7 +242,7 @@ fn handle_script_events( entity, handle.clone(), )) - .with_responses(context_loading_settings.emit_responses), + .with_responses(P::readonly_configuration(world_id).emit_responses), ); } } @@ -252,7 +252,7 @@ fn handle_script_events( CreateOrUpdateScript::

::new(ScriptAttachment::StaticScript( handle.clone(), )) - .with_responses(context_loading_settings.emit_responses), + .with_responses(P::readonly_configuration(world_id).emit_responses), ); } } @@ -260,7 +260,7 @@ fn handle_script_events( ScriptEvent::Detached { key } => { commands.queue( DeleteScript::

::new(key.clone()) - .with_responses(context_loading_settings.emit_responses), + .with_responses(P::readonly_configuration(world_id).emit_responses), ); } ScriptEvent::Attached { key } => { @@ -322,7 +322,7 @@ fn handle_script_events( if language == P::LANGUAGE { commands.queue( CreateOrUpdateScript::

::new(context_key) - .with_responses(context_loading_settings.emit_responses), + .with_responses(P::readonly_configuration(world_id).emit_responses), ); } } diff --git a/crates/bevy_mod_scripting_core/src/bindings/schedule.rs b/crates/bevy_mod_scripting_core/src/bindings/schedule.rs index 7609ecd07c..1c25f5f5a2 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/schedule.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/schedule.rs @@ -192,6 +192,7 @@ impl WorldAccessGuard<'_> { reason = "tests are there but not working currently" )] mod tests { + use crate::config::{GetPluginThreadConfig, ScriptingPluginConfiguration}; use ::{ bevy_app::{App, Plugin, Update}, bevy_ecs::{ @@ -199,7 +200,9 @@ mod tests { schedule::{NodeId, Schedules}, system::IntoSystem, }, + std::{cell::OnceCell, rc::Rc}, }; + use test_utils::make_test_plugin; use super::*; 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 443f32cdcc..2faa7e4cd4 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/script_system.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/script_system.rs @@ -12,7 +12,6 @@ use super::{ use crate::{ IntoScriptPluginParams, bindings::pretty_print::DisplayWithWorld, - context::ContextLoadingSettings, error::{InteropError, ScriptError}, event::CallbackLabel, extractors::get_all_access_ids, @@ -161,12 +160,6 @@ impl ScriptSystemBuilder { let after_systems = self.after.clone(); let system_name = self.name.to_string(); - // let system: DynamicScriptSystem

= - // IntoSystem::<(), (), (IsDynamicScriptSystem

, ())>::into_system(self); - - // dummy node id for now - // let mut reflect_system = ReflectSystem::from_system(&system, NodeId::System(0)); - // this is quite important, by default systems are placed in a set defined by their TYPE, i.e. in this case // all script systems would be the same @@ -208,7 +201,6 @@ impl ScriptSystemBuilder { struct DynamicHandlerContext<'w, P: IntoScriptPluginParams> { script_context: &'w ScriptContext

, - context_loading_settings: &'w ContextLoadingSettings

, runtime_container: &'w RuntimeContainer

, } @@ -220,17 +212,17 @@ 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 - // .query::<&Script

>(); - let context_loading_settings_res_id = world - .resource_id::>() - .expect("ContextLoadingSettings resource not found"); + let runtime_container_res_id = world .resource_id::>() .expect("RuntimeContainer resource not found"); - access.add_resource_read(context_loading_settings_res_id); + let script_context_res_id = world + .resource_id::>() + .expect("Scripts resource not found"); + access.add_resource_read(runtime_container_res_id); + access.add_resource_read(script_context_res_id); system.add(access); } @@ -243,9 +235,6 @@ impl<'w, P: IntoScriptPluginParams> DynamicHandlerContext<'w, P> { unsafe { Self { script_context: system.get_resource().expect("Scripts resource not found"), - context_loading_settings: system - .get_resource() - .expect("ContextLoadingSettings resource not found"), runtime_container: system .get_resource() .expect("RuntimeContainer resource not found"), @@ -268,22 +257,11 @@ impl<'w, P: IntoScriptPluginParams> DynamicHandlerContext<'w, P> { }; // call the script - let pre_handling_initializers = &self - .context_loading_settings - .context_pre_handling_initializers; let runtime = &self.runtime_container.runtime; let mut context = context.lock(); - P::handle( - payload, - context_key, - label, - &mut context, - pre_handling_initializers, - runtime, - guard, - ) + P::handle(payload, context_key, label, &mut context, runtime, guard) } } @@ -684,7 +662,10 @@ mod test { }; use test_utils::make_test_plugin; - use crate::BMSScriptingInfrastructurePlugin; + use crate::{ + BMSScriptingInfrastructurePlugin, + config::{GetPluginThreadConfig, ScriptingPluginConfiguration}, + }; use super::*; diff --git a/crates/bevy_mod_scripting_core/src/bindings/world.rs b/crates/bevy_mod_scripting_core/src/bindings/world.rs index 2731694abd..6b47eeb79f 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/world.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/world.rs @@ -53,6 +53,7 @@ use bevy_ecs::{ component::Mutable, hierarchy::{ChildOf, Children}, system::Command, + world::WorldId, }; use bevy_platform::collections::HashMap; use bevy_reflect::{TypeInfo, VariantInfo}; @@ -80,6 +81,13 @@ pub struct WorldAccessGuard<'w> { /// stored separate from the contents of the guard invalid: Rc, } +impl WorldAccessGuard<'_> { + /// Returns the id of the world this guard provides access to + pub fn id(&self) -> WorldId { + self.inner.cell.id() + } +} + /// Used to decrease the stack size of [`WorldAccessGuard`] pub(crate) struct WorldAccessGuardInner<'w> { /// Safety: cannot be used unless the scope depth is less than the max valid scope @@ -114,6 +122,11 @@ impl WorldAccessGuard<'static> { } #[profiling::all_functions] impl<'w> WorldAccessGuard<'w> { + /// Returns the id of the world this guard provides access to + fn world_id(&self) -> WorldId { + self.inner.cell.id() + } + /// creates a new guard derived from this one, which if invalidated, will not invalidate the original fn scope(&self) -> Self { let mut new_guard = self.clone(); diff --git a/crates/bevy_mod_scripting_core/src/commands.rs b/crates/bevy_mod_scripting_core/src/commands.rs index e7ca24f216..2ed0cf44b3 100644 --- a/crates/bevy_mod_scripting_core/src/commands.rs +++ b/crates/bevy_mod_scripting_core/src/commands.rs @@ -16,13 +16,13 @@ use crate::{ handler::{handle_script_errors, send_callback_response}, script::{DisplayProxy, ScriptAttachment, StaticScripts}, }; -use ::{ +use bevy_ecs::{system::Command, world::World}; +use bevy_log::{error, info, trace}; +use { bevy_asset::{Assets, Handle}, bevy_ecs::event::Events, bevy_log::{debug, warn}, }; -use bevy_ecs::{system::Command, world::World}; -use bevy_log::{error, info, trace}; /// Detaches a script, invoking the `on_script_unloaded` callback if it exists, and removes the script from the static scripts collection. pub struct DeleteScript { @@ -157,10 +157,6 @@ impl CreateOrUpdateScript

{ attachment, content, context, - &handler_ctxt.context_loading_settings.context_initializers, - &handler_ctxt - .context_loading_settings - .context_pre_handling_initializers, guard.clone(), &handler_ctxt.runtime_container.runtime, ) @@ -176,10 +172,6 @@ impl CreateOrUpdateScript

{ let context = P::load( attachment, content, - &handler_ctxt.context_loading_settings.context_initializers, - &handler_ctxt - .context_loading_settings - .context_pre_handling_initializers, guard.clone(), &handler_ctxt.runtime_container.runtime, )?; diff --git a/crates/bevy_mod_scripting_core/src/config.rs b/crates/bevy_mod_scripting_core/src/config.rs new file mode 100644 index 0000000000..397d8238cc --- /dev/null +++ b/crates/bevy_mod_scripting_core/src/config.rs @@ -0,0 +1,93 @@ +//! Init only configuration and relevant types. + +use bevy_ecs::world::WorldId; + +use crate::{ + IntoScriptPluginParams, + context::{ContextInitializer, ContextPreHandlingInitializer}, +}; + +/// A set of global* configs keyed by the plugin params type. +/// +/// Configuration is immutable after initialization. +/// +/// Configs contained here should be +/// +/// *global meaning stored in thread-locals, i.e. not annoyingly global, but pretty global. +#[derive(Debug, Default)] +pub struct ScriptingPluginConfiguration { + /// callbacks executed before a handler callback is executed every time + pub pre_handling_callbacks: &'static [ContextPreHandlingInitializer

], + /// callbacks executed once after creating a context but before executing it for the first time + pub context_initialization_callbacks: &'static [ContextInitializer

], + /// Whether to emit responses from the core callbacks like `on_script_loaded`. + pub emit_responses: bool, +} + +impl Clone for ScriptingPluginConfiguration

{ + fn clone(&self) -> Self { + *self + } +} + +impl Copy for ScriptingPluginConfiguration

{} + +/// A utility trait for accessing the readonly configuration for types that provide some. +/// +/// This is typically implemented using the `make_plugin_config_static!` macro. +/// +/// The default implementation will allow you to statically retrieve the configuration for a given world id. +/// +/// I.e. this config is not quite thread-local but world-local, meaning it should play nice with tests. +pub trait GetPluginThreadConfig { + /// Get a reference to the readonly configuration. + fn readonly_configuration(world: WorldId) -> ScriptingPluginConfiguration

; + + /// Set the configuration or overwrites it if already set. + fn set_thread_config(world: WorldId, config: ScriptingPluginConfiguration

); +} + +#[macro_export] +/// A macro to implement `WithReadonlyConfiguration` for a given plugin type using thread-local storage. +macro_rules! make_plugin_config_static { + ($ty:ty) => { + // thread_local! { + static CONFIG: std::sync::RwLock< + bevy_platform::prelude::Vec< + // bevy_ecs::world::WorldId, + Option>, + >, + > = std::sync::RwLock::new(bevy_platform::prelude::Vec::new()); + // } + impl GetPluginThreadConfig<$ty> for $ty { + fn readonly_configuration( + world: bevy_ecs::world::WorldId, + ) -> ScriptingPluginConfiguration<$ty> { + CONFIG + .read() + .unwrap() + .get(::sparse_set_index(&world)) + .and_then(|c| *c) + .unwrap_or_else(|| + panic!( + "Configuration for plugin {} not set for world {:?}. Did you add the plugin to the app?", + stringify!($ty), + world + ), + ) + } + + fn set_thread_config( + world: bevy_ecs::world::WorldId, + config: ScriptingPluginConfiguration<$ty>, + ) { + let mut guard = CONFIG.write().unwrap(); + let index = ::sparse_set_index(&world) as usize; + if index >= guard.len() { + guard.resize_with(index + 1, || None); + } + guard[index] = Some(config); + } + } + }; +} diff --git a/crates/bevy_mod_scripting_core/src/context.rs b/crates/bevy_mod_scripting_core/src/context.rs index bef89a315f..fe8278d10f 100644 --- a/crates/bevy_mod_scripting_core/src/context.rs +++ b/crates/bevy_mod_scripting_core/src/context.rs @@ -1,7 +1,5 @@ //! Traits and types for managing script contexts. -use bevy_ecs::resource::Resource; - use crate::{ IntoScriptPluginParams, bindings::{ThreadWorldContainer, WorldContainer, WorldGuard}, @@ -24,37 +22,6 @@ pub type ContextInitializer

= pub type ContextPreHandlingInitializer

= fn(&ScriptAttachment, &mut

::C) -> Result<(), ScriptError>; -/// Settings concerning the creation and assignment of script contexts as well as their initialization. -#[derive(Resource)] -pub struct ContextLoadingSettings { - /// Whether to emit responses from core script_callbacks like `on_script_loaded` or `on_script_unloaded`. - /// By default, this is `false` and responses are not emitted. - pub emit_responses: bool, - /// Initializers run once after creating a context but before executing it for the first time - pub context_initializers: Vec>, - /// Initializers run every time before executing or loading a script - pub context_pre_handling_initializers: Vec>, -} - -impl Default for ContextLoadingSettings

{ - fn default() -> Self { - Self { - emit_responses: false, - context_initializers: Default::default(), - context_pre_handling_initializers: Default::default(), - } - } -} - -impl Clone for ContextLoadingSettings { - fn clone(&self) -> Self { - Self { - emit_responses: self.emit_responses, - context_initializers: self.context_initializers.clone(), - context_pre_handling_initializers: self.context_pre_handling_initializers.clone(), - } - } -} /// A strategy for loading contexts pub type ContextLoadFn

= fn( attachment: &ScriptAttachment, @@ -82,8 +49,6 @@ pub trait ScriptingLoader { fn load( attachment: &ScriptAttachment, content: &[u8], - context_initializers: &[ContextInitializer

], - pre_handling_initializers: &[ContextPreHandlingInitializer

], world: WorldGuard, runtime: &P::R, ) -> Result; @@ -93,8 +58,6 @@ pub trait ScriptingLoader { attachment: &ScriptAttachment, content: &[u8], previous_context: &mut P::C, - context_initializers: &[ContextInitializer

], - pre_handling_initializers: &[ContextPreHandlingInitializer

], world: WorldGuard, runtime: &P::R, ) -> Result<(), ScriptError>; @@ -104,18 +67,17 @@ impl ScriptingLoader

for P { fn load( attachment: &ScriptAttachment, content: &[u8], - context_initializers: &[ContextInitializer

], - pre_handling_initializers: &[ContextPreHandlingInitializer

], world: WorldGuard, runtime: &P::R, ) -> Result { WorldGuard::with_existing_static_guard(world.clone(), |world| { + let world_id = world.id(); ThreadWorldContainer.set_world(world)?; Self::context_loader()( attachment, content, - context_initializers, - pre_handling_initializers, + P::readonly_configuration(world_id).context_initialization_callbacks, + P::readonly_configuration(world_id).pre_handling_callbacks, runtime, ) }) @@ -125,19 +87,18 @@ impl ScriptingLoader

for P { attachment: &ScriptAttachment, content: &[u8], previous_context: &mut P::C, - context_initializers: &[ContextInitializer

], - pre_handling_initializers: &[ContextPreHandlingInitializer

], world: WorldGuard, runtime: &P::R, ) -> Result<(), ScriptError> { WorldGuard::with_existing_static_guard(world, |world| { + let world_id = world.id(); ThreadWorldContainer.set_world(world)?; Self::context_reloader()( attachment, content, previous_context, - context_initializers, - pre_handling_initializers, + P::readonly_configuration(world_id).context_initialization_callbacks, + P::readonly_configuration(world_id).pre_handling_callbacks, runtime, ) }) diff --git a/crates/bevy_mod_scripting_core/src/event.rs b/crates/bevy_mod_scripting_core/src/event.rs index d06c3312f4..4a44df8b7b 100644 --- a/crates/bevy_mod_scripting_core/src/event.rs +++ b/crates/bevy_mod_scripting_core/src/event.rs @@ -421,6 +421,7 @@ mod test { use super::FORBIDDEN_KEYWORDS; use crate::{ bindings::ScriptValue, + config::{GetPluginThreadConfig, ScriptingPluginConfiguration}, event::Recipients, script::{ContextPolicy, ScriptAttachment, ScriptContext}, }; diff --git a/crates/bevy_mod_scripting_core/src/extractors.rs b/crates/bevy_mod_scripting_core/src/extractors.rs index 38c9e4f966..b1aa74f329 100644 --- a/crates/bevy_mod_scripting_core/src/extractors.rs +++ b/crates/bevy_mod_scripting_core/src/extractors.rs @@ -2,16 +2,12 @@ //! //! These are designed to be used to pipe inputs into other systems which require them, while handling any configuration erorrs nicely. #![allow(deprecated)] -use std::{ - ops::{Deref, DerefMut}, - sync::Arc, -}; +use std::sync::Arc; use bevy_ecs::{ archetype::Archetype, component::{ComponentId, Tick}, query::{Access, AccessConflicts}, - resource::Resource, storage::SparseSetIndex, system::{SystemMeta, SystemParam, SystemParamValidationError}, world::{DeferredWorld, World, unsafe_world_cell::UnsafeWorldCell}, @@ -25,7 +21,6 @@ use crate::{ WorldAccessGuard, WorldGuard, access_map::ReflectAccessId, pretty_print::DisplayWithWorld, script_value::ScriptValue, }, - context::ContextLoadingSettings, error::{InteropError, ScriptError}, event::{CallbackLabel, IntoCallbackLabel}, handler::ScriptingHandler, @@ -51,75 +46,8 @@ 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. -/// -/// 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. -/// -/// # 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` -pub(crate) struct ResScope<'state, T: Resource + Default>(pub &'state mut T); - -impl Deref for ResScope<'_, T> { - type Target = T; - - fn deref(&self) -> &Self::Target { - self.0 - } -} - -impl DerefMut for ResScope<'_, T> { - fn deref_mut(&mut self) -> &mut Self::Target { - self.0 - } -} - -unsafe impl SystemParam for ResScope<'_, T> { - type State = (T, bool); - - type Item<'world, 'state> = ResScope<'state, T>; - - fn init_state(_world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - system_meta.set_has_deferred(); - (T::default(), false) - } - - unsafe fn get_param<'world, 'state>( - state: &'state mut Self::State, - _system_meta: &SystemMeta, - world: UnsafeWorldCell<'world>, - _change_tick: Tick, - ) -> Self::Item<'world, 'state> { - state.1 = true; - if let Some(mut r) = unsafe { world.get_resource_mut::() } { - std::mem::swap(&mut state.0, &mut r); - } - ResScope(&mut state.0) - } - - fn apply(state: &mut Self::State, _system_meta: &SystemMeta, world: &mut World) { - if state.1 { - world.insert_resource(std::mem::take(&mut state.0)); - state.1 = false; - } - } -} - /// Context for systems which handle events for scripts pub struct HandlerContext { - /// Settings for loading contexts - pub(crate) context_loading_settings: ContextLoadingSettings

, /// The runtime container pub(crate) runtime_container: RuntimeContainer

, /// List of static scripts @@ -133,7 +61,6 @@ impl HandlerContext

{ /// Every call to this function must be paired with a call to [`Self::release`]. pub fn yoink(world: &mut World) -> Self { Self { - context_loading_settings: world.remove_resource().unwrap_or_default(), runtime_container: world.remove_resource().unwrap_or_default(), static_scripts: world.remove_resource().unwrap_or_default(), script_context: world.remove_resource().unwrap_or_default(), @@ -144,7 +71,6 @@ impl HandlerContext

{ /// Only call this if you have previously yoinked the handler context from the world. pub fn release(self, world: &mut World) { // insert the handler context back into the world - world.insert_resource(self.context_loading_settings); world.insert_resource(self.runtime_container); world.insert_resource(self.static_scripts); world.insert_resource(self.script_context); @@ -154,23 +80,8 @@ impl HandlerContext

{ /// /// Useful if you are needing multiple resources from the handler context. /// Otherwise the borrow checker will prevent you from borrowing the handler context mutably multiple times. - pub fn destructure( - &mut self, - ) -> ( - &mut ContextLoadingSettings

, - &mut RuntimeContainer

, - &mut StaticScripts, - ) { - ( - &mut self.context_loading_settings, - &mut self.runtime_container, - &mut self.static_scripts, - ) - } - - /// Get the context loading settings - pub fn context_loading_settings(&mut self) -> &mut ContextLoadingSettings

{ - &mut self.context_loading_settings + pub fn destructure(&mut self) -> (&mut RuntimeContainer

, &mut StaticScripts) { + (&mut self.runtime_container, &mut self.static_scripts) } /// Get the runtime container @@ -211,22 +122,11 @@ impl HandlerContext

{ }; // call the script - let pre_handling_initializers = &self - .context_loading_settings - .context_pre_handling_initializers; let runtime = &self.runtime_container.runtime; let mut context = context.lock(); - P::handle( - payload, - context_key, - label, - &mut context, - pre_handling_initializers, - runtime, - guard, - ) + P::handle(payload, context_key, label, &mut context, runtime, guard) } /// Invoke a callback in a script immediately. @@ -397,17 +297,19 @@ pub(crate) fn get_all_access_ids(access: &Access) -> Vec<(ReflectAc #[cfg(test)] mod test { - use ::{ + use crate::config::{GetPluginThreadConfig, ScriptingPluginConfiguration}; + use bevy_ecs::resource::Resource; + use test_utils::make_test_plugin; + + use { bevy_app::{App, Plugin, Update}, bevy_ecs::{ component::Component, entity::Entity, event::{Event, EventReader}, - system::{Query, ResMut, SystemState}, - world::FromWorld, + system::{Query, ResMut}, }, }; - use test_utils::make_test_plugin; use super::*; @@ -484,29 +386,4 @@ mod test { app.finish(); app.update(); } - - #[test] - pub fn resscope_reinserts_resource() { - // apply deffered system should be inserted after the system automatically - let mut app = App::new(); - - app.insert_resource(Res); - app.add_systems(Update, |_: ResScope| {}); - - app.update(); - - // check the resources are re-inserted - assert!(app.world().contains_resource::()); - } - - #[test] - pub fn rescope_does_not_remove_until_system_call() { - let mut world = World::new(); - world.insert_resource(Res); - - // this will call init, and that should't remove the resource - assert!(world.contains_resource::()); - SystemState::>::from_world(&mut world); - assert!(world.contains_resource::()); - } } diff --git a/crates/bevy_mod_scripting_core/src/handler.rs b/crates/bevy_mod_scripting_core/src/handler.rs index 96d796a0e2..589768f0e9 100644 --- a/crates/bevy_mod_scripting_core/src/handler.rs +++ b/crates/bevy_mod_scripting_core/src/handler.rs @@ -1,5 +1,5 @@ //! Contains the logic for handling script callback events -use ::{ +use { bevy_ecs::{ event::EventCursor, event::Events, @@ -46,7 +46,6 @@ pub trait ScriptingHandler { context_key: &ScriptAttachment, callback: &CallbackLabel, script_ctxt: &mut P::C, - pre_handling_initializers: &[ContextPreHandlingInitializer

], runtime: &P::R, world: WorldGuard, ) -> Result; @@ -59,18 +58,18 @@ impl ScriptingHandler

for P { context_key: &ScriptAttachment, callback: &CallbackLabel, script_ctxt: &mut P::C, - pre_handling_initializers: &[ContextPreHandlingInitializer

], runtime: &P::R, world: WorldGuard, ) -> Result { WorldGuard::with_existing_static_guard(world.clone(), |world| { + let world_id = world.id(); ThreadWorldContainer.set_world(world)?; Self::handler()( args, context_key, callback, script_ctxt, - pre_handling_initializers, + P::readonly_configuration(world_id).pre_handling_callbacks, runtime, ) }) diff --git a/crates/bevy_mod_scripting_core/src/lib.rs b/crates/bevy_mod_scripting_core/src/lib.rs index b74775169a..55956e1e79 100644 --- a/crates/bevy_mod_scripting_core/src/lib.rs +++ b/crates/bevy_mod_scripting_core/src/lib.rs @@ -4,6 +4,7 @@ use crate::{ bindings::MarkAsCore, + config::{GetPluginThreadConfig, ScriptingPluginConfiguration}, context::{ContextLoadFn, ContextReloadFn}, event::ScriptErrorEvent, }; @@ -28,7 +29,7 @@ use bindings::{ garbage_collector, schedule::AppScheduleRegistry, script_value::ScriptValue, }; use commands::{AddStaticScript, RemoveStaticScript}; -use context::{Context, ContextInitializer, ContextLoadingSettings, ContextPreHandlingInitializer}; +use context::{Context, ContextInitializer, ContextPreHandlingInitializer}; use error::ScriptError; use event::{ScriptCallbackEvent, ScriptCallbackResponseEvent, ScriptEvent}; use handler::HandlerFn; @@ -39,6 +40,7 @@ use std::ops::{Deref, DerefMut}; pub mod asset; pub mod bindings; pub mod commands; +pub mod config; pub mod context; pub mod docgen; pub mod error; @@ -72,7 +74,7 @@ pub enum ScriptingSystemSet { /// When implementing a new scripting plugin, also ensure the following implementations exist: /// - [`Plugin`] for the plugin, both [`Plugin::build`] and [`Plugin::finish`] methods need to be dispatched to the underlying [`ScriptingPlugin`] struct /// - [`AsMut`] for the plugin struct -pub trait IntoScriptPluginParams: 'static { +pub trait IntoScriptPluginParams: 'static + GetPluginThreadConfig { /// The language of the scripts const LANGUAGE: Language; /// The context type used for the scripts @@ -151,13 +153,17 @@ impl Plugin for ScriptingPlugin

{ app.insert_resource(self.runtime_settings.clone()) .insert_resource::>(RuntimeContainer { runtime: P::build_runtime(), - }) - .insert_resource::>(ContextLoadingSettings { - context_initializers: self.context_initializers.clone(), - context_pre_handling_initializers: self.context_pre_handling_initializers.clone(), - emit_responses: self.emit_responses, }); + // initialize thread local configs + let config = ScriptingPluginConfiguration::

{ + pre_handling_callbacks: Vec::leak(self.context_pre_handling_initializers.clone()), + context_initialization_callbacks: Vec::leak(self.context_initializers.clone()), + emit_responses: self.emit_responses, + }; + + P::set_thread_config(app.world().id(), config); + app.insert_resource(ScriptContext::

::new(self.context_policy.clone())); register_script_plugin_systems::

(app); 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 3d66b9bf0a..3d42763519 100644 --- a/crates/bevy_mod_scripting_core/src/script/script_context.rs +++ b/crates/bevy_mod_scripting_core/src/script/script_context.rs @@ -346,6 +346,7 @@ impl Default for ScriptContext

{ #[cfg(test)] mod tests { + use crate::config::{GetPluginThreadConfig, ScriptingPluginConfiguration}; use bevy_app::{App, Plugin}; use bevy_asset::AssetIndex; use test_utils::make_test_plugin; diff --git a/crates/languages/bevy_mod_scripting_lua/src/lib.rs b/crates/languages/bevy_mod_scripting_lua/src/lib.rs index 423fb5cb11..8419924bc2 100644 --- a/crates/languages/bevy_mod_scripting_lua/src/lib.rs +++ b/crates/languages/bevy_mod_scripting_lua/src/lib.rs @@ -13,9 +13,11 @@ use bevy_mod_scripting_core::{ ThreadWorldContainer, WorldContainer, function::namespace::Namespace, globals::AppScriptGlobalsRegistry, script_value::ScriptValue, }, + config::{GetPluginThreadConfig, ScriptingPluginConfiguration}, context::{ContextInitializer, ContextPreHandlingInitializer}, error::ScriptError, event::CallbackLabel, + make_plugin_config_static, reflection_extensions::PartialReflectExt, runtime::RuntimeSettings, script::{ContextPolicy, ScriptAttachment}, @@ -30,6 +32,8 @@ use mlua::{Function, IntoLua, Lua, MultiValue}; /// Bindings for lua. pub mod bindings; +make_plugin_config_static!(LuaScriptingPlugin); + impl IntoScriptPluginParams for LuaScriptingPlugin { type C = Lua; type R = (); diff --git a/crates/languages/bevy_mod_scripting_rhai/Cargo.toml b/crates/languages/bevy_mod_scripting_rhai/Cargo.toml index 6fd78d878e..ceb249a506 100644 --- a/crates/languages/bevy_mod_scripting_rhai/Cargo.toml +++ b/crates/languages/bevy_mod_scripting_rhai/Cargo.toml @@ -20,6 +20,7 @@ bevy_ecs = { workspace = true, default-features = false, features = [] } bevy_asset = { workspace = true, default-features = false, features = [] } bevy_app = { workspace = true, default-features = false, features = [] } bevy_log = { workspace = true, default-features = false, features = [] } +bevy_platform = { workspace = true, default-features = false, features = [] } rhai = { workspace = true, features = ["std"] } bevy_mod_scripting_core = { workspace = true, features = ["rhai_impls"] } strum = { workspace = true, features = ["derive"] } diff --git a/crates/languages/bevy_mod_scripting_rhai/src/lib.rs b/crates/languages/bevy_mod_scripting_rhai/src/lib.rs index ca713fdcf4..68c7786f57 100644 --- a/crates/languages/bevy_mod_scripting_rhai/src/lib.rs +++ b/crates/languages/bevy_mod_scripting_rhai/src/lib.rs @@ -16,9 +16,11 @@ use bevy_mod_scripting_core::{ ThreadWorldContainer, WorldContainer, function::namespace::Namespace, globals::AppScriptGlobalsRegistry, script_value::ScriptValue, }, + config::{GetPluginThreadConfig, ScriptingPluginConfiguration}, context::{ContextInitializer, ContextPreHandlingInitializer}, error::ScriptError, event::CallbackLabel, + make_plugin_config_static, reflection_extensions::PartialReflectExt, runtime::RuntimeSettings, script::{ContextPolicy, DisplayProxy, ScriptAttachment}, @@ -29,6 +31,7 @@ use bindings::{ }; use parking_lot::RwLock; pub use rhai; + use rhai::{AST, CallFnOptions, Dynamic, Engine, EvalAltResult, Scope}; /// Bindings for rhai. pub mod bindings; @@ -44,6 +47,8 @@ pub struct RhaiScriptContext { pub scope: Scope<'static>, } +make_plugin_config_static!(RhaiScriptingPlugin); + impl IntoScriptPluginParams for RhaiScriptingPlugin { type C = RhaiScriptContext; type R = RhaiRuntime; diff --git a/crates/testing_crates/test_consumer_crate/Cargo.toml b/crates/testing_crates/test_consumer_crate/Cargo.toml new file mode 100644 index 0000000000..454df9eec6 --- /dev/null +++ b/crates/testing_crates/test_consumer_crate/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "test_consumer_crate" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "test_consumer_crate" +path = "src/main.rs" + +[dependencies] +bevy = { version = "0.16", default-features = false, features = [ + "bevy_gltf", + "bevy_winit", + "bevy_render", + "png", + "web", + "webgpu", +] } +bevy_mod_scripting = { default-features = false, features = [ + "rhai", +], path = "../../../../bevy_mod_scripting" } + +[target.wasm32-unknown-unknown] +runner = "wasm-server-runner" diff --git a/crates/testing_crates/test_consumer_crate/src/main.rs b/crates/testing_crates/test_consumer_crate/src/main.rs new file mode 100644 index 0000000000..e7a11a969c --- /dev/null +++ b/crates/testing_crates/test_consumer_crate/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/crates/testing_crates/test_utils/src/test_plugin.rs b/crates/testing_crates/test_utils/src/test_plugin.rs index b1c25aacc3..c4c71ed79b 100644 --- a/crates/testing_crates/test_utils/src/test_plugin.rs +++ b/crates/testing_crates/test_utils/src/test_plugin.rs @@ -18,6 +18,8 @@ macro_rules! make_test_plugin { } } + $ident::make_plugin_config_static!(TestPlugin); + impl $ident::IntoScriptPluginParams for TestPlugin { type C = TestContext; type R = TestRuntime; diff --git a/tests/script_tests.rs b/tests/script_tests.rs index 0134360599..737896b47c 100644 --- a/tests/script_tests.rs +++ b/tests/script_tests.rs @@ -26,9 +26,12 @@ impl TestExecutor for Test { let scenario = Scenario::from_scenario_file(&script_asset_path, &scenario_path) .map_err(|e| format!("{e:?}"))?; // print whole error from anyhow including source and backtrace - execute_integration_test(scenario)?; + // do this in a separate thread to isolate the thread locals - Ok(()) + match execute_integration_test(scenario) { + Ok(_) => Ok(()), + Err(e) => Err(Failed::from(format!("{e:?}"))), // print whole error from anyhow including source and backtrace + } } fn name(&self) -> String { @@ -48,8 +51,9 @@ impl TestExecutor for Test { // or filter using the prefix "lua test -" fn main() { // Parse command line arguments - let args = Arguments::from_args(); + let mut args = Arguments::from_args(); let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + args.test_threads = Some(1); // force single-threaded to avoid issues with thread-local storage let tests = discover_all_tests(manifest_dir, |p| p.script_asset_path.starts_with("tests")) .into_iter() From 09eeba02e74ca8133595256f62461190f442871f Mon Sep 17 00:00:00 2001 From: makspll Date: Sat, 30 Aug 2025 00:14:46 +0100 Subject: [PATCH 2/6] remove test consumer crate --- .../test_consumer_crate/Cargo.toml | 24 ------------------- .../test_consumer_crate/src/main.rs | 3 --- 2 files changed, 27 deletions(-) delete mode 100644 crates/testing_crates/test_consumer_crate/Cargo.toml delete mode 100644 crates/testing_crates/test_consumer_crate/src/main.rs diff --git a/crates/testing_crates/test_consumer_crate/Cargo.toml b/crates/testing_crates/test_consumer_crate/Cargo.toml deleted file mode 100644 index 454df9eec6..0000000000 --- a/crates/testing_crates/test_consumer_crate/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "test_consumer_crate" -version = "0.1.0" -edition = "2024" - -[[bin]] -name = "test_consumer_crate" -path = "src/main.rs" - -[dependencies] -bevy = { version = "0.16", default-features = false, features = [ - "bevy_gltf", - "bevy_winit", - "bevy_render", - "png", - "web", - "webgpu", -] } -bevy_mod_scripting = { default-features = false, features = [ - "rhai", -], path = "../../../../bevy_mod_scripting" } - -[target.wasm32-unknown-unknown] -runner = "wasm-server-runner" diff --git a/crates/testing_crates/test_consumer_crate/src/main.rs b/crates/testing_crates/test_consumer_crate/src/main.rs deleted file mode 100644 index e7a11a969c..0000000000 --- a/crates/testing_crates/test_consumer_crate/src/main.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - println!("Hello, world!"); -} From 292a1ac82d3ac628d18de68922ad3aaddc1ac800 Mon Sep 17 00:00:00 2001 From: makspll Date: Sat, 30 Aug 2025 00:17:01 +0100 Subject: [PATCH 3/6] remove bevy_tasks --- Cargo.toml | 1 - crates/bevy_mod_scripting_core/Cargo.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 95d02c533a..9623dedce2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -116,7 +116,6 @@ bevy_mod_scripting_rhai = { path = "crates/languages/bevy_mod_scripting_rhai", v bevy_mod_scripting_core = { path = "crates/bevy_mod_scripting_core", version = "0.15.1" } bevy = { version = "0.16.0", default-features = false } bevy_math = { version = "0.16.0", default-features = false, features = ["std"] } -bevy_tasks = { version = "0.16.0", default-features = false } bevy_transform = { version = "0.16.0", default-features = false } bevy_reflect = { version = "0.16.0", default-features = false } bevy_ecs = { version = "0.16.0", default-features = false } diff --git a/crates/bevy_mod_scripting_core/Cargo.toml b/crates/bevy_mod_scripting_core/Cargo.toml index 14ac8bda1b..1a88236531 100644 --- a/crates/bevy_mod_scripting_core/Cargo.toml +++ b/crates/bevy_mod_scripting_core/Cargo.toml @@ -35,7 +35,6 @@ bevy_log = { workspace = true, default-features = false, features = [] } bevy_asset = { workspace = true, default-features = false, features = [] } bevy_diagnostic = { workspace = true, default-features = false, features = [] } bevy_platform = { workspace = true, default-features = false, features = [] } -bevy_tasks = { workspace = true, default-features = false, features = [] } parking_lot = { workspace = true } smallvec = { workspace = true } From cb75011b91cb68ecbb7263bdae3cd212319eb510 Mon Sep 17 00:00:00 2001 From: makspll Date: Sat, 30 Aug 2025 00:17:17 +0100 Subject: [PATCH 4/6] remove space --- Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 9623dedce2..e9a301cd9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -99,7 +99,6 @@ bevy_mod_scripting_rhai = { workspace = true, optional = true } bevy_mod_scripting_functions = { workspace = true } bevy_mod_scripting_derive = { workspace = true } - [workspace.dependencies] # local crates script_integration_test_harness = { path = "crates/testing_crates/script_integration_test_harness" } From 5bdb5e2065965e98879bd37a10dc19998e89c707 Mon Sep 17 00:00:00 2001 From: Maksymilian Mozolewski Date: Sat, 30 Aug 2025 00:27:58 +0100 Subject: [PATCH 5/6] Update crates/bevy_mod_scripting_core/src/config.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- crates/bevy_mod_scripting_core/src/config.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/crates/bevy_mod_scripting_core/src/config.rs b/crates/bevy_mod_scripting_core/src/config.rs index 397d8238cc..acb534a0f3 100644 --- a/crates/bevy_mod_scripting_core/src/config.rs +++ b/crates/bevy_mod_scripting_core/src/config.rs @@ -51,14 +51,11 @@ pub trait GetPluginThreadConfig { /// A macro to implement `WithReadonlyConfiguration` for a given plugin type using thread-local storage. macro_rules! make_plugin_config_static { ($ty:ty) => { - // thread_local! { static CONFIG: std::sync::RwLock< bevy_platform::prelude::Vec< - // bevy_ecs::world::WorldId, Option>, >, > = std::sync::RwLock::new(bevy_platform::prelude::Vec::new()); - // } impl GetPluginThreadConfig<$ty> for $ty { fn readonly_configuration( world: bevy_ecs::world::WorldId, From 2eee454e9af43a7da6ce120bba7c5eb5dc1f84a4 Mon Sep 17 00:00:00 2001 From: Maksymilian Mozolewski Date: Sat, 30 Aug 2025 00:28:44 +0100 Subject: [PATCH 6/6] Update crates/bevy_mod_scripting_core/src/bindings/world.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- crates/bevy_mod_scripting_core/src/bindings/world.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/crates/bevy_mod_scripting_core/src/bindings/world.rs b/crates/bevy_mod_scripting_core/src/bindings/world.rs index 6b47eeb79f..6ab9512bd4 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/world.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/world.rs @@ -122,11 +122,6 @@ impl WorldAccessGuard<'static> { } #[profiling::all_functions] impl<'w> WorldAccessGuard<'w> { - /// Returns the id of the world this guard provides access to - fn world_id(&self) -> WorldId { - self.inner.cell.id() - } - /// creates a new guard derived from this one, which if invalidated, will not invalidate the original fn scope(&self) -> Self { let mut new_guard = self.clone();