From c2087b142bd36a0fc240b396f5af187a1de2e701 Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Mon, 19 May 2025 07:19:26 -0400 Subject: [PATCH 01/11] feature: Warn on unknown language. --- crates/bevy_mod_scripting_core/src/asset.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/bevy_mod_scripting_core/src/asset.rs b/crates/bevy_mod_scripting_core/src/asset.rs index 5b122ab7f8..c077ce82e6 100644 --- a/crates/bevy_mod_scripting_core/src/asset.rs +++ b/crates/bevy_mod_scripting_core/src/asset.rs @@ -225,6 +225,14 @@ pub(crate) fn dispatch_script_asset_events( 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 metadata = ScriptMetadata { asset_id: *id, script_id, From 7cda1782193a5a91002e89659b9425395dc3df8a Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Wed, 21 May 2025 06:50:25 -0400 Subject: [PATCH 02/11] feature: Add on_script_reloaded callback. --- .../bevy_mod_scripting_core/src/commands.rs | 40 +++++++++++++++++-- crates/bevy_mod_scripting_core/src/event.rs | 1 + docs/src/ScriptingReference/core-callbacks.md | 24 ++++++++++- 3 files changed, 61 insertions(+), 4 deletions(-) diff --git a/crates/bevy_mod_scripting_core/src/commands.rs b/crates/bevy_mod_scripting_core/src/commands.rs index 57b37195a9..ca888daa85 100644 --- a/crates/bevy_mod_scripting_core/src/commands.rs +++ b/crates/bevy_mod_scripting_core/src/commands.rs @@ -6,7 +6,7 @@ use crate::{ context::ContextBuilder, error::{InteropError, ScriptError}, event::{ - CallbackLabel, IntoCallbackLabel, OnScriptLoaded, OnScriptUnloaded, + CallbackLabel, IntoCallbackLabel, OnScriptLoaded, OnScriptUnloaded, OnScriptReloaded, ScriptCallbackResponseEvent, }, extractors::{with_handler_system_state, HandlerContext}, @@ -150,6 +150,7 @@ impl CreateOrUpdateScript

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

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

| { @@ -194,6 +195,28 @@ 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); + } else { + match handler_ctxt.call_dynamic_label( + &OnScriptReloaded::into_callback_label(), + &self.id, + Entity::from_raw(0), + vec![ScriptValue::Bool(true)], + guard.clone(), + ) { + Ok(state) => { + reload_state = Some(state); + } + Err(err) => { + handle_script_errors( + guard.clone(), + vec![err + .with_script(self.id.clone()) + .with_context(P::LANGUAGE) + .with_context("saving reload state")] + .into_iter(), + ); + } + } } bevy::log::debug!("{}: reloading script with id: {}", P::LANGUAGE, self.id); self.reload_context(guard.clone(), handler_ctxt) @@ -235,14 +258,25 @@ impl Command for CreateOrUpdateScript

{ // immediately run command for callback, but only if loading went fine if success { RunScriptCallback::

::new( - self.id, + self.id.clone(), Entity::from_raw(0), OnScriptLoaded::into_callback_label(), vec![], false, ) - .apply(world) + .apply(world); + + if let Some(state) = reload_state { + RunScriptCallback::

::new( + self.id, + Entity::from_raw(0), + OnScriptReloaded::into_callback_label(), + vec![ScriptValue::Bool(false), state], + false, + ).apply(world); + } } + } } diff --git a/crates/bevy_mod_scripting_core/src/event.rs b/crates/bevy_mod_scripting_core/src/event.rs index 7d9c5c3da7..5146e52134 100644 --- a/crates/bevy_mod_scripting_core/src/event.rs +++ b/crates/bevy_mod_scripting_core/src/event.rs @@ -75,6 +75,7 @@ macro_rules! callback_labels { callback_labels!( OnScriptLoaded => "on_script_loaded", OnScriptUnloaded => "on_script_unloaded", + OnScriptReloaded => "on_script_reloaded", ); /// A trait for types that can be converted into a callback label diff --git a/docs/src/ScriptingReference/core-callbacks.md b/docs/src/ScriptingReference/core-callbacks.md index b8f81a7b95..150badeaab 100644 --- a/docs/src/ScriptingReference/core-callbacks.md +++ b/docs/src/ScriptingReference/core-callbacks.md @@ -2,9 +2,10 @@ On top of callbacks which are registered by your application, BMS provides a set of core callbacks which are always available. -The two core callbacks are: +The three core callbacks are: - `on_script_loaded` - `on_script_unloaded` +- `on_script_reloaded` ## `on_script_loaded` @@ -30,3 +31,24 @@ function on_script_unloaded() print("Goodbye world") end ``` + +## `on_script_reloaded` + +This will be called twice: right before and after a script is reloaded. + +The first parameter `save` informs you whether it is time to save a value or restore it. + +Before the reload, it is called with one argument: `true`. After the script is reloaded, it is called with two parameters: the first is `false` and the second is value returned from before. + +```lua +mode = 1 +function on_script_reloaded(save, value) + if save then + print("Before I go, take this.") + return mode + else + print("I'm back. Where was I?") + mode = value + end +end +``` From d59839ebd6a8615afebcc6df9405707b57f937ed Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Wed, 21 May 2025 07:07:00 -0400 Subject: [PATCH 03/11] style: Reformat. --- crates/bevy_mod_scripting_core/src/commands.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bevy_mod_scripting_core/src/commands.rs b/crates/bevy_mod_scripting_core/src/commands.rs index ca888daa85..2d75719edb 100644 --- a/crates/bevy_mod_scripting_core/src/commands.rs +++ b/crates/bevy_mod_scripting_core/src/commands.rs @@ -6,7 +6,7 @@ use crate::{ context::ContextBuilder, error::{InteropError, ScriptError}, event::{ - CallbackLabel, IntoCallbackLabel, OnScriptLoaded, OnScriptUnloaded, OnScriptReloaded, + CallbackLabel, IntoCallbackLabel, OnScriptLoaded, OnScriptReloaded, OnScriptUnloaded, ScriptCallbackResponseEvent, }, extractors::{with_handler_system_state, HandlerContext}, @@ -273,10 +273,10 @@ impl Command for CreateOrUpdateScript

{ OnScriptReloaded::into_callback_label(), vec![ScriptValue::Bool(false), state], false, - ).apply(world); + ) + .apply(world); } } - } } From 2d83d1eb3f08a2358787ce5d70d0a35714bdcb0d Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Mon, 26 May 2025 00:32:12 -0400 Subject: [PATCH 04/11] feature: Reload hook for non-shared context. --- .../bevy_mod_scripting_core/src/commands.rs | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/crates/bevy_mod_scripting_core/src/commands.rs b/crates/bevy_mod_scripting_core/src/commands.rs index 2d75719edb..8fddc9855b 100644 --- a/crates/bevy_mod_scripting_core/src/commands.rs +++ b/crates/bevy_mod_scripting_core/src/commands.rs @@ -212,7 +212,7 @@ impl Command for CreateOrUpdateScript

{ vec![err .with_script(self.id.clone()) .with_context(P::LANGUAGE) - .with_context("saving reload state")] + .with_context("saving reload state (shared-context)")] .into_iter(), ); } @@ -222,6 +222,27 @@ impl Command for CreateOrUpdateScript

{ self.reload_context(guard.clone(), handler_ctxt) } None => { + match handler_ctxt.call_dynamic_label( + &OnScriptReloaded::into_callback_label(), + &self.id, + Entity::from_raw(0), + vec![ScriptValue::Bool(true)], + guard.clone(), + ) { + Ok(state) => { + reload_state = Some(state); + } + Err(err) => { + handle_script_errors( + guard.clone(), + vec![err + .with_script(self.id.clone()) + .with_context(P::LANGUAGE) + .with_context("saving reload state")] + .into_iter(), + ); + } + } bevy::log::debug!("{}: loading script with id: {}", P::LANGUAGE, self.id); self.load_context(guard.clone(), handler_ctxt) } From f70b5c9db576a90e8f6a23c5eab7da7468ce975c Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Tue, 24 Jun 2025 04:04:58 -0400 Subject: [PATCH 05/11] feature: Report error but not for missing script. --- .../bevy_mod_scripting_core/src/commands.rs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/crates/bevy_mod_scripting_core/src/commands.rs b/crates/bevy_mod_scripting_core/src/commands.rs index 8fddc9855b..861364526e 100644 --- a/crates/bevy_mod_scripting_core/src/commands.rs +++ b/crates/bevy_mod_scripting_core/src/commands.rs @@ -233,14 +233,17 @@ impl Command for CreateOrUpdateScript

{ reload_state = Some(state); } Err(err) => { - handle_script_errors( - guard.clone(), - vec![err - .with_script(self.id.clone()) - .with_context(P::LANGUAGE) - .with_context("saving reload state")] - .into_iter(), - ); + let missing_script = err.downcast_interop_inner().map(|e| matches!(e, crate::error::InteropErrorInner::MissingScript { .. })).unwrap_or(false); + if !missing_script { + handle_script_errors( + guard.clone(), + vec![err + .with_script(self.id.clone()) + .with_context(P::LANGUAGE) + .with_context("saving reload state")] + .into_iter(), + ); + } } } bevy::log::debug!("{}: loading script with id: {}", P::LANGUAGE, self.id); From c355a3499e41f9d6a45445a31430b562f2fbeacc Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Tue, 24 Jun 2025 04:53:56 -0400 Subject: [PATCH 06/11] refactor: Use 2-arity for on_script_reloaded(). Two arguments every time. The second argument may be Unit/nil. --- crates/bevy_mod_scripting_core/src/commands.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_mod_scripting_core/src/commands.rs b/crates/bevy_mod_scripting_core/src/commands.rs index 861364526e..4ab4900c2c 100644 --- a/crates/bevy_mod_scripting_core/src/commands.rs +++ b/crates/bevy_mod_scripting_core/src/commands.rs @@ -200,7 +200,7 @@ impl Command for CreateOrUpdateScript

{ &OnScriptReloaded::into_callback_label(), &self.id, Entity::from_raw(0), - vec![ScriptValue::Bool(true)], + vec![ScriptValue::Bool(true), ScriptValue::Unit], guard.clone(), ) { Ok(state) => { @@ -226,7 +226,7 @@ impl Command for CreateOrUpdateScript

{ &OnScriptReloaded::into_callback_label(), &self.id, Entity::from_raw(0), - vec![ScriptValue::Bool(true)], + vec![ScriptValue::Bool(true), ScriptValue::Unit], guard.clone(), ) { Ok(state) => { From 3cf7d09187229a3fd01aaa2708204d5119ff8228 Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Tue, 24 Jun 2025 05:10:33 -0400 Subject: [PATCH 07/11] doc: Clarify on_script_reloaded behavior. --- docs/src/ScriptingReference/core-callbacks.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/src/ScriptingReference/core-callbacks.md b/docs/src/ScriptingReference/core-callbacks.md index 150badeaab..9caa831522 100644 --- a/docs/src/ScriptingReference/core-callbacks.md +++ b/docs/src/ScriptingReference/core-callbacks.md @@ -36,9 +36,12 @@ end This will be called twice: right before and after a script is reloaded. -The first parameter `save` informs you whether it is time to save a value or restore it. +The first parameter `save` informs whether it is time to save a value or restore it. -Before the reload, it is called with one argument: `true`. After the script is reloaded, it is called with two parameters: the first is `false` and the second is value returned from before. +Before the script reload, `on_script_reloaded` is called with two arguments: +`true`, `nil`. The value returned is kept. After the script reload, +`on_script_reloaded` is called with two arguments: `false` and the value +returned from the preceding call. ```lua mode = 1 @@ -52,3 +55,5 @@ function on_script_reloaded(save, value) end end ``` + +Using `on_script_reloaded` one can make a script reload event not disrupt the current script state. From f776374ece5f65707da2cad29b88ad7b2e2d9aa0 Mon Sep 17 00:00:00 2001 From: makspll Date: Wed, 2 Jul 2025 22:27:11 +0100 Subject: [PATCH 08/11] move stuff around, rename things --- .../bevy_mod_scripting_core/src/commands.rs | 441 ++++++++++-------- crates/bevy_mod_scripting_core/src/event.rs | 7 +- crates/xtask/src/main.rs | 4 +- 3 files changed, 254 insertions(+), 198 deletions(-) diff --git a/crates/bevy_mod_scripting_core/src/commands.rs b/crates/bevy_mod_scripting_core/src/commands.rs index 5a54cf1f39..6a70eabf5f 100644 --- a/crates/bevy_mod_scripting_core/src/commands.rs +++ b/crates/bevy_mod_scripting_core/src/commands.rs @@ -88,6 +88,7 @@ impl CreateOrUpdateScript

{ guard: WorldGuard, handler_ctxt: &HandlerContext

, ) -> Result<(), ScriptError> { + bevy::log::debug!("{}: reloading script with id: {}", P::LANGUAGE, self.id); let existing_script = match handler_ctxt.scripts.scripts.get(&self.id) { Some(script) => script, None => { @@ -121,6 +122,8 @@ impl CreateOrUpdateScript

{ guard: WorldGuard, handler_ctxt: &mut HandlerContext

, ) -> Result<(), ScriptError> { + bevy::log::debug!("{}: loading script with id: {}", P::LANGUAGE, self.id); + let context = (ContextBuilder::

::load)( handler_ctxt.context_loading_settings.loader.load, &self.id, @@ -145,162 +148,174 @@ impl CreateOrUpdateScript

{ ); Ok(()) } + + fn before_load( + &self, + world: WorldGuard, + handler_ctxt: &mut HandlerContext

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

::new( + self.id.clone(), + Entity::from_raw(0), + OnScriptUnloaded::into_callback_label(), + vec![], + false, + ) + .with_context(P::LANGUAGE) + .with_context("saving reload state") + .run_with_handler(world, handler_ctxt) + .ok(); + } + + None + } + + fn after_load( + &self, + world: WorldGuard, + handler_ctxt: &mut HandlerContext

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

::new( + self.id.clone(), + Entity::from_raw(0), + OnScriptLoaded::into_callback_label(), + vec![], + false, + ) + .with_context(P::LANGUAGE) + .with_context("on loaded callback") + .run_with_handler(world.clone(), handler_ctxt); + + if is_reload { + let state = script_state.unwrap_or(ScriptValue::Unit); + let _ = RunScriptCallback::

::new( + self.id.clone(), + Entity::from_raw(0), + OnScriptReloaded::into_callback_label(), + vec![state], + false, + ) + .with_context(P::LANGUAGE) + .with_context("on reloaded callback") + .run_with_handler(world, handler_ctxt); + } + } + + fn handle_global_context( + &self, + guard: WorldGuard, + handler_ctxt: &mut HandlerContext

, + ) -> (Result<(), ScriptError>, Option, bool) { + let existing_context = handler_ctxt + .scripts + .scripts + .values() + .next() + .map(|s| s.context.clone()); + + debug!( + "{}: CreateOrUpdateScript command applying to global context (script_id: {}, new context?: {}, new script?: {})", + P::LANGUAGE, + self.id, + existing_context.is_none(), + !handler_ctxt.scripts.scripts.contains_key(&self.id) + ); + + let is_reload = existing_context.is_some(); + + if let Some(context) = existing_context { + // point all new scripts to the shared context + handler_ctxt.scripts.scripts.insert( + self.id.clone(), + Script { + id: self.id.clone(), + asset: self.asset.clone(), + context, + }, + ); + } + + let script_state = self.before_load(guard.clone(), handler_ctxt, is_reload); + + let result = if is_reload { + self.reload_context(guard, handler_ctxt) + } else { + self.load_context(guard, handler_ctxt) + }; + + (result, script_state, is_reload) + } + + fn handle_individual_context( + &self, + guard: WorldGuard, + handler_ctxt: &mut HandlerContext

, + ) -> (Result<(), ScriptError>, Option, bool) { + let is_new_script = !handler_ctxt.scripts.scripts.contains_key(&self.id); + let is_reload = !is_new_script; + + debug!( + "{}: CreateOrUpdateScript command applying (script_id: {}, new context?: {}, new script?: {})", + P::LANGUAGE, + self.id, + is_new_script, + !handler_ctxt.scripts.scripts.contains_key(&self.id) + ); + + let script_state = self.before_load(guard.clone(), handler_ctxt, is_reload); + let result = if is_new_script { + self.load_context(guard, handler_ctxt) + } else { + self.reload_context(guard, handler_ctxt) + }; + (result, script_state, is_reload) + } } #[profiling::all_functions] impl Command for CreateOrUpdateScript

{ fn apply(self, world: &mut bevy::prelude::World) { - let mut reload_state = None; - 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 assigned_shared_context = - match handler_ctxt.context_loading_settings.assignment_strategy { - crate::context::ContextAssignmentStrategy::Individual => None, - crate::context::ContextAssignmentStrategy::Global => { - let is_new_context = handler_ctxt.scripts.scripts.is_empty(); - if !is_new_context { - handler_ctxt - .scripts - .scripts - .values() - .next() - .map(|s| s.context.clone()) - } else { - None - } - } - }; - - debug!( - "{}: CreateOrUpdateScript command applying (script_id: {}, new context?: {}, new script?: {})", - P::LANGUAGE, - self.id, - assigned_shared_context.is_none(), - is_new_script - ); - - let result = match &assigned_shared_context { - Some(assigned_shared_context) => { - if is_new_script { - // this will happen when sharing contexts - // make a new script with the shared context - let script = Script { - id: self.id.clone(), - asset: self.asset.clone(), - context: assigned_shared_context.clone(), - }; - // it can potentially be loaded but without a successful script reload but that - // leaves us in an okay state - handler_ctxt.scripts.scripts.insert(self.id.clone(), script); - } else { - match handler_ctxt.call_dynamic_label( - &OnScriptReloaded::into_callback_label(), - &self.id, - Entity::from_raw(0), - vec![ScriptValue::Bool(true), ScriptValue::Unit], - guard.clone(), - ) { - Ok(state) => { - reload_state = Some(state); - } - Err(err) => { - handle_script_errors( - guard.clone(), - vec![err - .with_script(self.id.clone()) - .with_context(P::LANGUAGE) - .with_context("saving reload state (shared-context)")] - .into_iter(), - ); - } - } - } - bevy::log::debug!("{}: reloading script with id: {}", P::LANGUAGE, self.id); - self.reload_context(guard.clone(), handler_ctxt) + with_handler_system_state(world, |guard, handler_ctxt: &mut HandlerContext

| { + let (result, script_state, is_reload) = + match handler_ctxt.context_loading_settings.assignment_strategy { + crate::context::ContextAssignmentStrategy::Global => { + self.handle_global_context(guard.clone(), handler_ctxt) } - None => { - match handler_ctxt.call_dynamic_label( - &OnScriptReloaded::into_callback_label(), - &self.id, - Entity::from_raw(0), - vec![ScriptValue::Bool(true), ScriptValue::Unit], - guard.clone(), - ) { - Ok(state) => { - reload_state = Some(state); - } - Err(err) => { - let missing_script = err.downcast_interop_inner().map(|e| matches!(e, crate::error::InteropErrorInner::MissingScript { .. })).unwrap_or(false); - if !missing_script { - handle_script_errors( - guard.clone(), - vec![err - .with_script(self.id.clone()) - .with_context(P::LANGUAGE) - .with_context("saving reload state")] - .into_iter(), - ); - } - } - } - bevy::log::debug!("{}: loading script with id: {}", P::LANGUAGE, self.id); - self.load_context(guard.clone(), handler_ctxt) + crate::context::ContextAssignmentStrategy::Individual => { + self.handle_individual_context(guard.clone(), handler_ctxt) } }; - let phrase = if assigned_shared_context.is_some() { - "reloading" - } else { - "loading" - }; - - if let Err(err) = result { - handle_script_errors( - guard, - vec![err - .with_script(self.id.clone()) - .with_context(P::LANGUAGE) - .with_context(phrase)] - .into_iter(), - ); - return false; - } - - bevy::log::debug!( - "{}: script with id: {} successfully created or updated", - P::LANGUAGE, - self.id + if let Err(err) = result { + handle_script_errors( + guard, + vec![err + .with_script(self.id.clone()) + .with_context(P::LANGUAGE) + .with_context(if is_reload { + "reloading an existing script or context" + } else { + "loading a new script or context" + })] + .into_iter(), ); + return; // don't run after_load if there was an error + } - true - }, - ); + bevy::log::debug!( + "{}: script with id: {} successfully created or updated", + P::LANGUAGE, + self.id + ); - // immediately run command for callback, but only if loading went fine - if success { - RunScriptCallback::

::new( - self.id.clone(), - Entity::from_raw(0), - OnScriptLoaded::into_callback_label(), - vec![], - false, - ) - .apply(world); - - if let Some(state) = reload_state { - RunScriptCallback::

::new( - self.id, - Entity::from_raw(0), - OnScriptReloaded::into_callback_label(), - vec![ScriptValue::Bool(false), state], - false, - ) - .apply(world); - } - } + self.after_load(guard, handler_ctxt, script_state, is_reload); + }); } } @@ -313,7 +328,7 @@ pub struct RunScriptCallback { /// The callback to run pub callback: CallbackLabel, /// optional context passed down to errors - pub context: Option<&'static str>, + pub context: Vec, /// The arguments to pass to the callback pub args: Vec, /// Whether the callback should emit a response event @@ -334,7 +349,7 @@ impl RunScriptCallback

{ Self { id, entity, - context: None, + context: Default::default(), callback, args, trigger_response, @@ -343,55 +358,73 @@ impl RunScriptCallback

{ } /// Sets the context for the command, makes produced errors more useful. - pub fn with_context(mut self, context: &'static str) -> Self { - self.context = Some(context); + pub fn with_context(mut self, context: impl ToString) -> Self { + self.context.push(context.to_string()); self } -} -impl Command for RunScriptCallback

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

| { - if !handler_ctxt.is_script_fully_loaded(self.id.clone()) { - bevy::log::error!( - "{}: Cannot apply callback command, as script does not exist: {}. Ignoring.", - P::LANGUAGE, - self.id - ); - return; - } + /// Equivalent to [`Self::run`], but usable in the case where you already have a [`HandlerContext`]. + pub fn run_with_handler( + self, + guard: WorldGuard, + handler_ctxt: &mut HandlerContext

, + ) -> Result { + if !handler_ctxt.is_script_fully_loaded(self.id.clone()) { + bevy::log::error!( + "{}: Cannot apply callback {} command, as script does not exist: {}. Ignoring.", + P::LANGUAGE, + self.callback, + self.id + ); + return Err(ScriptError::new(InteropError::missing_script( + self.id.clone(), + ))); + } - let result = handler_ctxt.call_dynamic_label( - &self.callback, - &self.id, - self.entity, - self.args, + let result = handler_ctxt.call_dynamic_label( + &self.callback, + &self.id, + self.entity, + self.args, + guard.clone(), + ); + + if self.trigger_response { + send_callback_response( guard.clone(), + ScriptCallbackResponseEvent::new(self.callback, self.id.clone(), result.clone()), ); + } - if self.trigger_response { - send_callback_response( - guard.clone(), - ScriptCallbackResponseEvent::new( - self.callback, - self.id.clone(), - result.clone(), - ), - ); + if let Err(err) = &result { + let mut error_with_context = err.clone().with_script(self.id).with_context(P::LANGUAGE); + for ctxt in &self.context { + error_with_context = error_with_context.with_context(ctxt); } - if let Err(err) = result { - let mut error_with_context = err.with_script(self.id).with_context(P::LANGUAGE); - if let Some(ctxt) = self.context { - error_with_context = error_with_context.with_context(ctxt); - } + handle_script_errors(guard, vec![error_with_context].into_iter()); + } - handle_script_errors(guard, vec![error_with_context].into_iter()); - } + result + } + + /// Equivalent to running the command, but also returns the result of the callback. + /// + /// The returned error will already be handled and logged. + pub fn run(self, world: &mut bevy::prelude::World) -> Result { + with_handler_system_state(world, |guard, handler_ctxt: &mut HandlerContext

| { + self.run_with_handler(guard, handler_ctxt) }) } } +impl Command for RunScriptCallback

{ + fn apply(self, world: &mut bevy::prelude::World) { + // internals handle this + let _ = self.run(world); + } +} + /// Adds a static script to the collection of static scripts pub struct AddStaticScript { /// The ID of the script to add @@ -477,13 +510,15 @@ mod test { Ok(context) }, reload: |name, new, existing, init, pre_run_init, _| { - *existing = String::from_utf8_lossy(new).into(); + let mut new = String::from_utf8_lossy(new).to_string(); for init in init { - init(name, existing)?; + init(name, &mut new)?; } for init in pre_run_init { - init(name, Entity::from_raw(0), existing)?; + init(name, Entity::from_raw(0), &mut new)?; } + existing.push_str(" | "); + existing.push_str(&new); Ok(()) }, }, @@ -536,8 +571,11 @@ mod test { assert_eq!(id, script.id); let found_context = script.context.lock(); - - assert_eq!(*context, *found_context, "{message}"); + pretty_assertions::assert_eq!( + *context, + *found_context, + "expected context != actual context. {message}" + ); } fn assert_response_events( @@ -571,10 +609,12 @@ mod test { command.apply(app.world_mut()); // check script + let loaded_script_expected_content = + "content initialized pre-handling-initialized callback-ran-on_script_loaded"; assert_context_and_script( app.world_mut(), "script", - "content initialized pre-handling-initialized callback-ran-on_script_loaded", + loaded_script_expected_content, "Initial script creation failed", ); @@ -584,10 +624,13 @@ mod test { command.apply(app.world_mut()); // check script + let reloaded_script_expected_content = format!("{loaded_script_expected_content} callback-ran-on_script_unloaded \ + | new content initialized pre-handling-initialized callback-ran-on_script_loaded callback-ran-on_script_reloaded"); + assert_context_and_script( app.world_mut(), "script", - "new content initialized pre-handling-initialized callback-ran-on_script_loaded", + &reloaded_script_expected_content, "Script update failed", ); @@ -619,7 +662,7 @@ mod test { assert_context_and_script( app.world_mut(), "script", - "new content initialized pre-handling-initialized callback-ran-on_script_loaded callback-ran-on_script_loaded", + &format!("{reloaded_script_expected_content} callback-ran-on_script_loaded"), "Script callback failed", ); // assert events sent @@ -673,10 +716,12 @@ mod test { command.apply(app.world_mut()); // check script + let loaded_script_expected_content = + "content initialized pre-handling-initialized callback-ran-on_script_loaded"; assert_context_and_script( app.world(), "script", - "content initialized pre-handling-initialized callback-ran-on_script_loaded", + loaded_script_expected_content, "Initial script creation failed", ); @@ -689,10 +734,13 @@ mod test { // check script + let second_loaded_script_expected_content = + format!("{loaded_script_expected_content} callback-ran-on_script_unloaded \ + | new content initialized pre-handling-initialized callback-ran-on_script_loaded callback-ran-on_script_reloaded"); assert_context_and_script( app.world(), "script", - "new content initialized pre-handling-initialized callback-ran-on_script_loaded", + &second_loaded_script_expected_content, "Script update failed", ); @@ -704,17 +752,20 @@ mod test { command.apply(app.world_mut()); // check both scripts have the new context - + let third_loaded_script_expected_content = format!( + "{second_loaded_script_expected_content} callback-ran-on_script_unloaded \ + | content2 initialized pre-handling-initialized callback-ran-on_script_loaded callback-ran-on_script_reloaded", + ); assert_context_and_script( app.world(), "script2", - "content2 initialized pre-handling-initialized callback-ran-on_script_loaded", + &third_loaded_script_expected_content, "second script context was not created correctly", ); assert_context_and_script( app.world(), "script", - "content2 initialized pre-handling-initialized callback-ran-on_script_loaded", + &third_loaded_script_expected_content, "First script context was not updated on second script insert", ); @@ -730,7 +781,7 @@ mod test { assert_context_and_script( app.world(), "script2", - "content2 initialized pre-handling-initialized callback-ran-on_script_loaded callback-ran-on_script_unloaded", + &format!("{third_loaded_script_expected_content} callback-ran-on_script_unloaded"), "first script unload didn't have the desired effect", ); diff --git a/crates/bevy_mod_scripting_core/src/event.rs b/crates/bevy_mod_scripting_core/src/event.rs index 9952a7a00a..be2096c6b3 100644 --- a/crates/bevy_mod_scripting_core/src/event.rs +++ b/crates/bevy_mod_scripting_core/src/event.rs @@ -57,9 +57,10 @@ impl CallbackLabel { #[macro_export] /// Creates a set of callback labels macro_rules! callback_labels { - ($($name:ident => $label:expr),* $(,)?) => { + ($($(#[doc = $doc:expr])* $name:ident => $label:expr),* $(,)?) => { $( + $(#[doc = $doc])* #[doc = "A callback label for the event: "] #[doc = stringify!($label)] pub struct $name; @@ -73,8 +74,12 @@ macro_rules! callback_labels { } callback_labels!( + /// Fired when a script is successfully loaded OnScriptLoaded => "on_script_loaded", + /// Fired when a script is unloaded before a reload, if a value is returned, it will be passed to the `on_script_reloaded` callback OnScriptUnloaded => "on_script_unloaded", + /// Fired when a script is reloaded (loaded after being unloaded) + /// This callback receives the value returned by the `on_script_unloaded` callback if any were returned OnScriptReloaded => "on_script_reloaded", ); diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs index 1b664a20ab..c037fce18c 100644 --- a/crates/xtask/src/main.rs +++ b/crates/xtask/src/main.rs @@ -1211,10 +1211,10 @@ impl Xtasks { match kind { CheckKind::All => { let err_main = Self::check_main_workspace(app_settings.clone(), ide_mode); - let err_codegen = Self::check_codegen_crate(app_settings.clone(), ide_mode); + // let err_codegen = Self::check_codegen_crate(app_settings.clone(), ide_mode); err_main?; - err_codegen?; + // err_codegen?; } CheckKind::Main => { Self::check_main_workspace(app_settings, ide_mode)?; From 3f3fa283f2ac60bf9d3804a5c393ea43419e5214 Mon Sep 17 00:00:00 2001 From: Maksymilian Mozolewski Date: Wed, 2 Jul 2025 22:29:46 +0100 Subject: [PATCH 09/11] undo bad change --- crates/xtask/src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs index c037fce18c..1b664a20ab 100644 --- a/crates/xtask/src/main.rs +++ b/crates/xtask/src/main.rs @@ -1211,10 +1211,10 @@ impl Xtasks { match kind { CheckKind::All => { let err_main = Self::check_main_workspace(app_settings.clone(), ide_mode); - // let err_codegen = Self::check_codegen_crate(app_settings.clone(), ide_mode); + let err_codegen = Self::check_codegen_crate(app_settings.clone(), ide_mode); err_main?; - // err_codegen?; + err_codegen?; } CheckKind::Main => { Self::check_main_workspace(app_settings, ide_mode)?; From 975df3bb51f21795ea44c9c76ec3b83da56a68e1 Mon Sep 17 00:00:00 2001 From: Maksymilian Mozolewski Date: Wed, 2 Jul 2025 22:37:12 +0100 Subject: [PATCH 10/11] Update core-callbacks.md --- docs/src/ScriptingReference/core-callbacks.md | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/docs/src/ScriptingReference/core-callbacks.md b/docs/src/ScriptingReference/core-callbacks.md index 9caa831522..0fb4a55653 100644 --- a/docs/src/ScriptingReference/core-callbacks.md +++ b/docs/src/ScriptingReference/core-callbacks.md @@ -29,31 +29,26 @@ This callback will not have access to the `entity` variable, as when the script ```lua function on_script_unloaded() print("Goodbye world") + return "house key" end ``` ## `on_script_reloaded` -This will be called twice: right before and after a script is reloaded. +Called right after `on_script_loaded` but only if the script was reloaded. +The callback is passed a state argument, this state is exactly what is returned by the script through `on_script_unloaded` before a reload happens. -The first parameter `save` informs whether it is time to save a value or restore it. - -Before the script reload, `on_script_reloaded` is called with two arguments: -`true`, `nil`. The value returned is kept. After the script reload, -`on_script_reloaded` is called with two arguments: `false` and the value -returned from the preceding call. +This callback does not have access to the `entity` variable. ```lua mode = 1 -function on_script_reloaded(save, value) - if save then - print("Before I go, take this.") - return mode +function on_script_reloaded(value) + if value then + print("I'm back. Thanks for the keys!") else - print("I'm back. Where was I?") - mode = value + print('I have not saved any state before unloading') end end ``` -Using `on_script_reloaded` one can make a script reload event not disrupt the current script state. +Using `on_script_reloaded` one can make a script reload preserve its current state. From 4a716719408973aeb9beb779499d0ca8058e3a7e Mon Sep 17 00:00:00 2001 From: makspll Date: Thu, 3 Jul 2025 19:14:56 +0100 Subject: [PATCH 11/11] update docs --- docs/src/ScriptingReference/core-callbacks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/ScriptingReference/core-callbacks.md b/docs/src/ScriptingReference/core-callbacks.md index 0fb4a55653..07d3a1e328 100644 --- a/docs/src/ScriptingReference/core-callbacks.md +++ b/docs/src/ScriptingReference/core-callbacks.md @@ -22,7 +22,7 @@ end ## `on_script_unloaded` -This will be called right before a script is unloaded. This is a good place to clean up any resources that your script has allocated. Note this is not called when a script is reloaded, only when it is being removed from the system. +This will be called right before a script is unloaded. This is a good place to clean up any resources that your script has allocated. This is called both before a script is removed as well as before a script is reloaded. If you want to preserve the state of your script across reloads, you can return a value from this callback, which will be passed to `on_script_reloaded` when the script is reloaded. This callback will not have access to the `entity` variable, as when the script is being unloaded it might not be attached to an entity.