Skip to content

feat: add static scripts which do not need to be attached to entities to be run #253

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Feb 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 61 additions & 1 deletion crates/bevy_mod_scripting_core/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -313,6 +313,46 @@ impl<P: IntoScriptPluginParams> Command for CreateOrUpdateScript<P> {
}
}

/// 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<ScriptId>) -> 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::<StaticScripts>();
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::<StaticScripts>();
static_scripts.remove(self.id);
}
}

#[cfg(test)]
mod test {
use bevy::{
Expand Down Expand Up @@ -380,6 +420,7 @@ mod test {
.insert_non_send_resource(RuntimeContainer::<DummyPlugin> {
runtime: "Runtime".to_string(),
})
.init_resource::<StaticScripts>()
.insert_resource(CallbackSettings::<DummyPlugin> {
callback_handler: |_, _, _, callback, c, _, _| {
c.push_str(format!(" callback-ran-{}", callback).as_str());
Expand Down Expand Up @@ -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::<StaticScripts>().unwrap();
assert!(static_scripts.contains("script"));

let command = RemoveStaticScript::new("script".into());
command.apply(world);

let static_scripts = world.get_resource::<StaticScripts>().unwrap();
assert!(!static_scripts.contains("script"));
}
}
8 changes: 7 additions & 1 deletion crates/bevy_mod_scripting_core/src/extractors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::{
error::MissingResourceError,
handler::CallbackSettings,
runtime::RuntimeContainer,
script::Scripts,
script::{Scripts, StaticScripts},
IntoScriptPluginParams,
};

Expand All @@ -20,6 +20,7 @@ pub(crate) struct HandlerContext<P: IntoScriptPluginParams> {
pub scripts: Scripts,
pub runtime_container: RuntimeContainer<P>,
pub script_contexts: ScriptContexts<P>,
pub static_scripts: StaticScripts,
}
#[profiling::function]
pub(crate) fn extract_handler_context<P: IntoScriptPluginParams>(
Expand All @@ -44,13 +45,17 @@ pub(crate) fn extract_handler_context<P: IntoScriptPluginParams>(
let script_contexts = world
.remove_non_send_resource::<ScriptContexts<P>>()
.ok_or_else(MissingResourceError::new::<ScriptContexts<P>>)?;
let static_scripts = world
.remove_resource::<StaticScripts>()
.ok_or_else(MissingResourceError::new::<StaticScripts>)?;

Ok(HandlerContext {
callback_settings,
context_loading_settings,
scripts,
runtime_container,
script_contexts,
static_scripts,
})
}
#[profiling::function]
Expand All @@ -63,4 +68,5 @@ pub(crate) fn yield_handler_context<P: IntoScriptPluginParams>(
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);
}
81 changes: 80 additions & 1 deletion crates/bevy_mod_scripting_core/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,15 @@ pub(crate) fn event_handler_internal<L: IntoCallbackLabel, P: IntoScriptPluginPa
let entity_scripts = entities
.iter()
.map(|(e, s)| (e, s.0.clone()))
.chain(
// on top of script components we also want to run static scripts
// semantically these are just scripts with no entity, in our case we use an invalid entity index 0
res_ctxt
.static_scripts
.scripts
.iter()
.map(|s| (Entity::from_raw(0), vec![s.clone()])),
)
.collect::<Vec<_>>();

for event in events
Expand Down Expand Up @@ -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::*;
Expand Down Expand Up @@ -298,6 +307,7 @@ mod test {
app.insert_resource::<Scripts>(Scripts { scripts });
app.insert_non_send_resource(RuntimeContainer::<P> { runtime });
app.insert_non_send_resource(ScriptContexts::<P> { contexts });
app.init_resource::<StaticScripts>();
app.insert_resource(ContextLoadingSettings::<P> {
loader: ContextBuilder {
load: |_, _, _, _, _| todo!(),
Expand Down Expand Up @@ -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::<OnTestCallback, TestPlugin>(
|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::<ScriptContexts<TestPlugin>>()
.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())
]
);
}
}
31 changes: 30 additions & 1 deletion crates/bevy_mod_scripting_core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;

Expand Down Expand Up @@ -113,6 +114,8 @@ impl<P: IntoScriptPluginParams> Plugin for ScriptingPlugin<P> {
});

register_script_plugin_systems::<P>(app);

// add extension for the language to the asset loader
once_per_app_init(app);

app.add_supported_script_extensions(self.supported_extensions);
Expand Down Expand Up @@ -252,6 +255,7 @@ fn once_per_app_init(app: &mut App) {
.add_event::<ScriptCallbackEvent>()
.init_resource::<AppReflectAllocator>()
.init_resource::<Scripts>()
.init_resource::<StaticScripts>()
.init_asset::<ScriptAsset>()
.init_resource::<AppScriptFunctionRegistry>();

Expand Down Expand Up @@ -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<ScriptId>) -> &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<ScriptId>) -> &mut Self;
}

impl ManageStaticScripts for App {
fn add_static_script(&mut self, script_id: impl Into<ScriptId>) -> &mut Self {
AddStaticScript::new(script_id.into()).apply(self.world_mut());
self
}

fn remove_static_script(&mut self, script_id: impl Into<ScriptId>) -> &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.
Expand Down
65 changes: 64 additions & 1 deletion crates/bevy_mod_scripting_core/src/script.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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<ScriptId>,
}

impl StaticScripts {
/// Inserts a static script into the collection
pub fn insert<S: Into<ScriptId>>(&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<S: Into<ScriptId>>(&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<S: Into<ScriptId>>(&self, script: S) -> bool {
self.scripts.contains(&script.into())
}

/// Returns an iterator over the static scripts
pub fn iter(&self) -> impl Iterator<Item = &ScriptId> {
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"));
}
}
Loading
Loading