diff --git a/crates/bevy_mod_scripting_core/src/commands.rs b/crates/bevy_mod_scripting_core/src/commands.rs index 002f4becc1..4811684d1b 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::{ event::{IntoCallbackLabel, OnScriptLoaded, OnScriptUnloaded}, extractors::{extract_handler_context, yield_handler_context, HandlerContext}, handler::{handle_script_errors, CallbackSettings}, - script::{Script, ScriptId}, + script::{Script, ScriptId, StaticScripts}, IntoScriptPluginParams, }; use bevy::{asset::Handle, log::debug, prelude::Command}; @@ -313,6 +313,46 @@ impl Command for CreateOrUpdateScript

{ } } +/// Adds a static script to the collection of static scripts +pub struct AddStaticScript { + /// The ID of the script to add + id: ScriptId, +} + +impl AddStaticScript { + /// Creates a new AddStaticScript command with the given ID + pub fn new(id: impl Into) -> Self { + Self { id: id.into() } + } +} + +impl Command for AddStaticScript { + fn apply(self, world: &mut bevy::prelude::World) { + let mut static_scripts = world.get_resource_or_init::(); + static_scripts.insert(self.id); + } +} + +/// Removes a static script from the collection of static scripts +pub struct RemoveStaticScript { + /// The ID of the script to remove + id: ScriptId, +} + +impl RemoveStaticScript { + /// Creates a new RemoveStaticScript command with the given ID + pub fn new(id: ScriptId) -> Self { + Self { id } + } +} + +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); + } +} + #[cfg(test)] mod test { use bevy::{ @@ -380,6 +420,7 @@ mod test { .insert_non_send_resource(RuntimeContainer:: { runtime: "Runtime".to_string(), }) + .init_resource::() .insert_resource(CallbackSettings:: { callback_handler: |_, _, _, callback, c, _, _| { c.push_str(format!(" callback-ran-{}", callback).as_str()); @@ -578,4 +619,23 @@ mod test { assert!(contexts.contexts.len() == 1); } + + #[test] + fn test_static_scripts() { + let mut app = setup_app(); + + let world = app.world_mut(); + + let command = AddStaticScript::new("script"); + command.apply(world); + + let static_scripts = world.get_resource::().unwrap(); + assert!(static_scripts.contains("script")); + + let command = RemoveStaticScript::new("script".into()); + command.apply(world); + + let static_scripts = world.get_resource::().unwrap(); + assert!(!static_scripts.contains("script")); + } } diff --git a/crates/bevy_mod_scripting_core/src/extractors.rs b/crates/bevy_mod_scripting_core/src/extractors.rs index fb3f9a9e10..02616d426d 100644 --- a/crates/bevy_mod_scripting_core/src/extractors.rs +++ b/crates/bevy_mod_scripting_core/src/extractors.rs @@ -9,7 +9,7 @@ use crate::{ error::MissingResourceError, handler::CallbackSettings, runtime::RuntimeContainer, - script::Scripts, + script::{Scripts, StaticScripts}, IntoScriptPluginParams, }; @@ -20,6 +20,7 @@ pub(crate) struct HandlerContext { pub scripts: Scripts, pub runtime_container: RuntimeContainer

, pub script_contexts: ScriptContexts

, + pub static_scripts: StaticScripts, } #[profiling::function] pub(crate) fn extract_handler_context( @@ -44,6 +45,9 @@ pub(crate) fn extract_handler_context( let script_contexts = world .remove_non_send_resource::>() .ok_or_else(MissingResourceError::new::>)?; + let static_scripts = world + .remove_resource::() + .ok_or_else(MissingResourceError::new::)?; Ok(HandlerContext { callback_settings, @@ -51,6 +55,7 @@ pub(crate) fn extract_handler_context( scripts, runtime_container, script_contexts, + static_scripts, }) } #[profiling::function] @@ -63,4 +68,5 @@ pub(crate) fn yield_handler_context( world.insert_resource(context.scripts); world.insert_non_send_resource(context.runtime_container); world.insert_non_send_resource(context.script_contexts); + world.insert_resource(context.static_scripts); } diff --git a/crates/bevy_mod_scripting_core/src/handler.rs b/crates/bevy_mod_scripting_core/src/handler.rs index 660154f357..5d1cf77d93 100644 --- a/crates/bevy_mod_scripting_core/src/handler.rs +++ b/crates/bevy_mod_scripting_core/src/handler.rs @@ -112,6 +112,15 @@ pub(crate) fn event_handler_internal>(); for event in events @@ -246,7 +255,7 @@ mod test { event::{CallbackLabel, IntoCallbackLabel, ScriptCallbackEvent, ScriptErrorEvent}, handler::HandlerFn, runtime::RuntimeContainer, - script::{Script, ScriptComponent, ScriptId, Scripts}, + script::{Script, ScriptComponent, ScriptId, Scripts, StaticScripts}, }; use super::*; @@ -298,6 +307,7 @@ mod test { app.insert_resource::(Scripts { scripts }); app.insert_non_send_resource(RuntimeContainer::

{ runtime }); app.insert_non_send_resource(ScriptContexts::

{ contexts }); + app.init_resource::(); app.insert_resource(ContextLoadingSettings::

{ loader: ContextBuilder { load: |_, _, _, _, _| todo!(), @@ -484,4 +494,73 @@ mod test { ] ); } + + #[test] + fn test_handler_called_for_static_scripts() { + let test_script_id = Cow::Borrowed("test_script"); + let test_ctxt_id = 0; + + let scripts = HashMap::from_iter(vec![( + test_script_id.clone(), + Script { + id: test_script_id.clone(), + asset: None, + context_id: test_ctxt_id, + }, + )]); + let contexts = HashMap::from_iter(vec![( + test_ctxt_id, + TestContext { + invocations: vec![], + }, + )]); + let runtime = TestRuntime { + invocations: vec![], + }; + let mut app = setup_app::( + |args, entity, script, _, ctxt, _, runtime| { + ctxt.invocations.extend(args); + runtime.invocations.push((entity, script.clone())); + Ok(ScriptValue::Unit) + }, + runtime, + contexts, + scripts, + ); + + app.world_mut().insert_resource(StaticScripts { + scripts: vec![test_script_id.clone()].into_iter().collect(), + }); + + app.world_mut().send_event(ScriptCallbackEvent::new( + OnTestCallback::into_callback_label(), + vec![ScriptValue::String("test_args_script".into())], + crate::event::Recipients::All, + )); + + app.world_mut().send_event(ScriptCallbackEvent::new( + OnTestCallback::into_callback_label(), + vec![ScriptValue::String("test_script_id".into())], + crate::event::Recipients::Script(test_script_id.clone()), + )); + + app.update(); + + let test_context = app + .world() + .get_non_send_resource::>() + .unwrap(); + + assert_eq!( + test_context + .contexts + .get(&test_ctxt_id) + .unwrap() + .invocations, + vec![ + ScriptValue::String("test_args_script".into()), + ScriptValue::String("test_script_id".into()) + ] + ); + } } diff --git a/crates/bevy_mod_scripting_core/src/lib.rs b/crates/bevy_mod_scripting_core/src/lib.rs index 1686801e6a..26ab576891 100644 --- a/crates/bevy_mod_scripting_core/src/lib.rs +++ b/crates/bevy_mod_scripting_core/src/lib.rs @@ -13,6 +13,7 @@ use bindings::{ script_value::ScriptValue, AppReflectAllocator, ReflectAllocator, ReflectReference, ScriptTypeRegistration, }; +use commands::{AddStaticScript, RemoveStaticScript}; use context::{ Context, ContextAssigner, ContextBuilder, ContextInitializer, ContextLoadingSettings, ContextPreHandlingInitializer, ScriptContexts, @@ -21,7 +22,7 @@ use error::ScriptError; use event::ScriptCallbackEvent; use handler::{CallbackSettings, HandlerFn}; use runtime::{initialize_runtime, Runtime, RuntimeContainer, RuntimeInitializer, RuntimeSettings}; -use script::Scripts; +use script::{ScriptId, Scripts, StaticScripts}; mod extractors; @@ -113,6 +114,8 @@ impl Plugin for ScriptingPlugin

{ }); register_script_plugin_systems::

(app); + + // add extension for the language to the asset loader once_per_app_init(app); app.add_supported_script_extensions(self.supported_extensions); @@ -252,6 +255,7 @@ fn once_per_app_init(app: &mut App) { .add_event::() .init_resource::() .init_resource::() + .init_resource::() .init_asset::() .init_resource::(); @@ -311,6 +315,31 @@ impl AddRuntimeInitializer for App { } } +/// Trait for adding static scripts to an app +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; + + /// 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; +} + +impl ManageStaticScripts for App { + 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 { + RemoveStaticScript::new(script_id.into()).apply(self.world_mut()); + self + } +} + /// Trait for adding a supported extension to the script asset settings. /// /// This is only valid in the plugin building phase, as the asset loader will be created in the `finalize` phase. diff --git a/crates/bevy_mod_scripting_core/src/script.rs b/crates/bevy_mod_scripting_core/src/script.rs index a4ffb9cc52..a69018f240 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, context::ContextId}; -use bevy::{asset::Handle, ecs::system::Resource, reflect::Reflect}; +use bevy::{asset::Handle, ecs::system::Resource, reflect::Reflect, utils::HashSet}; use std::{borrow::Cow, collections::HashMap, ops::Deref}; /// A unique identifier for a script, by default corresponds to the path of the asset excluding the asset source. @@ -47,3 +47,66 @@ pub struct Script { /// The id of the context this script is currently assigned to pub context_id: ContextId, } + +/// A collection of scripts, not associated with any entity. +/// +/// Useful for `global` or `static` scripts which operate over a larger scope than a single entity. +#[derive(Default, Resource)] +pub struct StaticScripts { + pub(crate) scripts: HashSet, +} + +impl StaticScripts { + /// Inserts a static script into the collection + pub fn insert>(&mut self, script: S) { + self.scripts.insert(script.into()); + } + + /// Removes a static script from the collection, returning `true` if the script was in the collection, `false` otherwise + pub fn remove>(&mut self, script: S) -> bool { + self.scripts.remove(&script.into()) + } + + /// Checks if a static script is in the collection + /// Returns `true` if the script is in the collection, `false` otherwise + pub fn contains>(&self, script: S) -> bool { + self.scripts.contains(&script.into()) + } + + /// Returns an iterator over the static scripts + pub fn iter(&self) -> impl Iterator { + self.scripts.iter() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn static_scripts_insert() { + let mut static_scripts = StaticScripts::default(); + static_scripts.insert("script1"); + assert_eq!(static_scripts.scripts.len(), 1); + assert!(static_scripts.scripts.contains("script1")); + } + + #[test] + fn static_scripts_remove() { + let mut static_scripts = StaticScripts::default(); + static_scripts.insert("script1"); + assert_eq!(static_scripts.scripts.len(), 1); + assert!(static_scripts.scripts.contains("script1")); + assert!(static_scripts.remove("script1")); + assert_eq!(static_scripts.scripts.len(), 0); + assert!(!static_scripts.scripts.contains("script1")); + } + + #[test] + fn static_scripts_contains() { + let mut static_scripts = StaticScripts::default(); + static_scripts.insert("script1"); + assert!(static_scripts.contains("script1")); + assert!(!static_scripts.contains("script2")); + } +} diff --git a/docs/src/Summary/running-scripts.md b/docs/src/Summary/running-scripts.md index de174af077..a68df6701f 100644 --- a/docs/src/Summary/running-scripts.md +++ b/docs/src/Summary/running-scripts.md @@ -1,12 +1,35 @@ # Attaching Scripts -Once you have scripts discovered and loaded, you'll want to run them. At the moment BMS supports one method of triggering scripts, and that is by attaching them to entities via `ScriptComponent`'s and then sending script event's which trigger callbacks on the scripts. + +Once you have scripts discovered and loaded, you'll want to run them. + +At the moment BMS supports two methods of making scripts runnable: +- Attaching them to entities via `ScriptComponent`'s +- Adding static scripts + +And then sending script event's which trigger callbacks on the scripts. + +## Attaching scripts to entities In order to attach a script and make it runnable simply add a `ScriptComponent` to an entity ```rust,ignore commands.entity(my_entity).insert(ScriptComponent::new(vec!["my_script.lua", "my_other_script.lua"])); ``` +When this script is run the `entity` global will represent the entity the script is attached to. This allows you to interact with the entity in your script easilly. + +## Making static scripts runnable + +Some scripts do not require attaching to an entity. You can run these scripts by loading them first as you would with any other script, then either adding them at app level via `add_static_script` or by issuing a `AddStaticScript` command like so: + +```rust,ignore + commands.queue(AddStaticScript::new("my_static_script.lua")); +``` + +The script will then be run as any other script but without being attached to any entity. and as such the `entity` global will always represent an invalid entity. + +Note: Internally these scripts are attached to a dummy entity and as such you can think of them as being attached to an entity with an id of `0`. + # Running Scripts Scripts can run logic either when loaded or when triggered by an event. For example the script: diff --git a/examples/game_of_life.rs b/examples/game_of_life.rs index 5717a720f4..a0c22a7c22 100644 --- a/examples/game_of_life.rs +++ b/examples/game_of_life.rs @@ -20,6 +20,7 @@ use bevy_mod_scripting_core::{ script_value::ScriptValue, }, callback_labels, + commands::AddStaticScript, event::ScriptCallbackEvent, handler::event_handler, script::ScriptComponent, @@ -53,15 +54,24 @@ fn run_script_cmd( ) { if let Some(Ok(command)) = log.take() { match command { - GameOfLifeCommand::Start { language } => { + GameOfLifeCommand::Start { + language, + 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 ); - commands.spawn(ScriptComponent::new(vec![format!( - "scripts/game_of_life.{language}" - )])); + + let script_path = format!("scripts/game_of_life.{}", language); + if !use_static_script { + bevy::log::info!("Spawning an entity with ScriptComponent"); + commands.spawn(ScriptComponent::new(vec![script_path])); + } else { + bevy::log::info!("Using static script instead of spawning an entity"); + commands.queue(AddStaticScript::new(script_path)) + } } GameOfLifeCommand::Stop => { // we can simply drop the handle, or manually delete, I'll just drop the handle @@ -87,8 +97,12 @@ pub enum GameOfLifeCommand { /// Start the game of life by spawning an entity with the game_of_life.{language} script Start { /// The language to use for the script, i.e. "lua" or "rhai" - #[clap(short, long, default_value = "lua")] + #[clap(default_value = "lua")] language: String, + + /// Whether to use a static script instead of spawning an entity with the script + #[clap(short, long, default_value = "false")] + use_static_script: bool, }, /// Stop the game of life by dropping a handle to the game_of_life script Stop,