Skip to content

Commit 1c342e8

Browse files
authored
feat: add static scripts which do not need to be attached to entities to be run (#253)
* feat: add static scripts * fix failing tests * fix failing handler tests * run cargo fmt
1 parent b322ba4 commit 1c342e8

File tree

7 files changed

+285
-11
lines changed

7 files changed

+285
-11
lines changed

crates/bevy_mod_scripting_core/src/commands.rs

+61-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use crate::{
66
event::{IntoCallbackLabel, OnScriptLoaded, OnScriptUnloaded},
77
extractors::{extract_handler_context, yield_handler_context, HandlerContext},
88
handler::{handle_script_errors, CallbackSettings},
9-
script::{Script, ScriptId},
9+
script::{Script, ScriptId, StaticScripts},
1010
IntoScriptPluginParams,
1111
};
1212
use bevy::{asset::Handle, log::debug, prelude::Command};
@@ -313,6 +313,46 @@ impl<P: IntoScriptPluginParams> Command for CreateOrUpdateScript<P> {
313313
}
314314
}
315315

316+
/// Adds a static script to the collection of static scripts
317+
pub struct AddStaticScript {
318+
/// The ID of the script to add
319+
id: ScriptId,
320+
}
321+
322+
impl AddStaticScript {
323+
/// Creates a new AddStaticScript command with the given ID
324+
pub fn new(id: impl Into<ScriptId>) -> Self {
325+
Self { id: id.into() }
326+
}
327+
}
328+
329+
impl Command for AddStaticScript {
330+
fn apply(self, world: &mut bevy::prelude::World) {
331+
let mut static_scripts = world.get_resource_or_init::<StaticScripts>();
332+
static_scripts.insert(self.id);
333+
}
334+
}
335+
336+
/// Removes a static script from the collection of static scripts
337+
pub struct RemoveStaticScript {
338+
/// The ID of the script to remove
339+
id: ScriptId,
340+
}
341+
342+
impl RemoveStaticScript {
343+
/// Creates a new RemoveStaticScript command with the given ID
344+
pub fn new(id: ScriptId) -> Self {
345+
Self { id }
346+
}
347+
}
348+
349+
impl Command for RemoveStaticScript {
350+
fn apply(self, world: &mut bevy::prelude::World) {
351+
let mut static_scripts = world.get_resource_or_init::<StaticScripts>();
352+
static_scripts.remove(self.id);
353+
}
354+
}
355+
316356
#[cfg(test)]
317357
mod test {
318358
use bevy::{
@@ -380,6 +420,7 @@ mod test {
380420
.insert_non_send_resource(RuntimeContainer::<DummyPlugin> {
381421
runtime: "Runtime".to_string(),
382422
})
423+
.init_resource::<StaticScripts>()
383424
.insert_resource(CallbackSettings::<DummyPlugin> {
384425
callback_handler: |_, _, _, callback, c, _, _| {
385426
c.push_str(format!(" callback-ran-{}", callback).as_str());
@@ -578,4 +619,23 @@ mod test {
578619

579620
assert!(contexts.contexts.len() == 1);
580621
}
622+
623+
#[test]
624+
fn test_static_scripts() {
625+
let mut app = setup_app();
626+
627+
let world = app.world_mut();
628+
629+
let command = AddStaticScript::new("script");
630+
command.apply(world);
631+
632+
let static_scripts = world.get_resource::<StaticScripts>().unwrap();
633+
assert!(static_scripts.contains("script"));
634+
635+
let command = RemoveStaticScript::new("script".into());
636+
command.apply(world);
637+
638+
let static_scripts = world.get_resource::<StaticScripts>().unwrap();
639+
assert!(!static_scripts.contains("script"));
640+
}
581641
}

crates/bevy_mod_scripting_core/src/extractors.rs

+7-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use crate::{
99
error::MissingResourceError,
1010
handler::CallbackSettings,
1111
runtime::RuntimeContainer,
12-
script::Scripts,
12+
script::{Scripts, StaticScripts},
1313
IntoScriptPluginParams,
1414
};
1515

@@ -20,6 +20,7 @@ pub(crate) struct HandlerContext<P: IntoScriptPluginParams> {
2020
pub scripts: Scripts,
2121
pub runtime_container: RuntimeContainer<P>,
2222
pub script_contexts: ScriptContexts<P>,
23+
pub static_scripts: StaticScripts,
2324
}
2425
#[profiling::function]
2526
pub(crate) fn extract_handler_context<P: IntoScriptPluginParams>(
@@ -44,13 +45,17 @@ pub(crate) fn extract_handler_context<P: IntoScriptPluginParams>(
4445
let script_contexts = world
4546
.remove_non_send_resource::<ScriptContexts<P>>()
4647
.ok_or_else(MissingResourceError::new::<ScriptContexts<P>>)?;
48+
let static_scripts = world
49+
.remove_resource::<StaticScripts>()
50+
.ok_or_else(MissingResourceError::new::<StaticScripts>)?;
4751

4852
Ok(HandlerContext {
4953
callback_settings,
5054
context_loading_settings,
5155
scripts,
5256
runtime_container,
5357
script_contexts,
58+
static_scripts,
5459
})
5560
}
5661
#[profiling::function]
@@ -63,4 +68,5 @@ pub(crate) fn yield_handler_context<P: IntoScriptPluginParams>(
6368
world.insert_resource(context.scripts);
6469
world.insert_non_send_resource(context.runtime_container);
6570
world.insert_non_send_resource(context.script_contexts);
71+
world.insert_resource(context.static_scripts);
6672
}

crates/bevy_mod_scripting_core/src/handler.rs

+80-1
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,15 @@ pub(crate) fn event_handler_internal<L: IntoCallbackLabel, P: IntoScriptPluginPa
112112
let entity_scripts = entities
113113
.iter()
114114
.map(|(e, s)| (e, s.0.clone()))
115+
.chain(
116+
// on top of script components we also want to run static scripts
117+
// semantically these are just scripts with no entity, in our case we use an invalid entity index 0
118+
res_ctxt
119+
.static_scripts
120+
.scripts
121+
.iter()
122+
.map(|s| (Entity::from_raw(0), vec![s.clone()])),
123+
)
115124
.collect::<Vec<_>>();
116125

117126
for event in events
@@ -246,7 +255,7 @@ mod test {
246255
event::{CallbackLabel, IntoCallbackLabel, ScriptCallbackEvent, ScriptErrorEvent},
247256
handler::HandlerFn,
248257
runtime::RuntimeContainer,
249-
script::{Script, ScriptComponent, ScriptId, Scripts},
258+
script::{Script, ScriptComponent, ScriptId, Scripts, StaticScripts},
250259
};
251260

252261
use super::*;
@@ -298,6 +307,7 @@ mod test {
298307
app.insert_resource::<Scripts>(Scripts { scripts });
299308
app.insert_non_send_resource(RuntimeContainer::<P> { runtime });
300309
app.insert_non_send_resource(ScriptContexts::<P> { contexts });
310+
app.init_resource::<StaticScripts>();
301311
app.insert_resource(ContextLoadingSettings::<P> {
302312
loader: ContextBuilder {
303313
load: |_, _, _, _, _| todo!(),
@@ -484,4 +494,73 @@ mod test {
484494
]
485495
);
486496
}
497+
498+
#[test]
499+
fn test_handler_called_for_static_scripts() {
500+
let test_script_id = Cow::Borrowed("test_script");
501+
let test_ctxt_id = 0;
502+
503+
let scripts = HashMap::from_iter(vec![(
504+
test_script_id.clone(),
505+
Script {
506+
id: test_script_id.clone(),
507+
asset: None,
508+
context_id: test_ctxt_id,
509+
},
510+
)]);
511+
let contexts = HashMap::from_iter(vec![(
512+
test_ctxt_id,
513+
TestContext {
514+
invocations: vec![],
515+
},
516+
)]);
517+
let runtime = TestRuntime {
518+
invocations: vec![],
519+
};
520+
let mut app = setup_app::<OnTestCallback, TestPlugin>(
521+
|args, entity, script, _, ctxt, _, runtime| {
522+
ctxt.invocations.extend(args);
523+
runtime.invocations.push((entity, script.clone()));
524+
Ok(ScriptValue::Unit)
525+
},
526+
runtime,
527+
contexts,
528+
scripts,
529+
);
530+
531+
app.world_mut().insert_resource(StaticScripts {
532+
scripts: vec![test_script_id.clone()].into_iter().collect(),
533+
});
534+
535+
app.world_mut().send_event(ScriptCallbackEvent::new(
536+
OnTestCallback::into_callback_label(),
537+
vec![ScriptValue::String("test_args_script".into())],
538+
crate::event::Recipients::All,
539+
));
540+
541+
app.world_mut().send_event(ScriptCallbackEvent::new(
542+
OnTestCallback::into_callback_label(),
543+
vec![ScriptValue::String("test_script_id".into())],
544+
crate::event::Recipients::Script(test_script_id.clone()),
545+
));
546+
547+
app.update();
548+
549+
let test_context = app
550+
.world()
551+
.get_non_send_resource::<ScriptContexts<TestPlugin>>()
552+
.unwrap();
553+
554+
assert_eq!(
555+
test_context
556+
.contexts
557+
.get(&test_ctxt_id)
558+
.unwrap()
559+
.invocations,
560+
vec![
561+
ScriptValue::String("test_args_script".into()),
562+
ScriptValue::String("test_script_id".into())
563+
]
564+
);
565+
}
487566
}

crates/bevy_mod_scripting_core/src/lib.rs

+30-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use bindings::{
1313
script_value::ScriptValue, AppReflectAllocator, ReflectAllocator, ReflectReference,
1414
ScriptTypeRegistration,
1515
};
16+
use commands::{AddStaticScript, RemoveStaticScript};
1617
use context::{
1718
Context, ContextAssigner, ContextBuilder, ContextInitializer, ContextLoadingSettings,
1819
ContextPreHandlingInitializer, ScriptContexts,
@@ -21,7 +22,7 @@ use error::ScriptError;
2122
use event::ScriptCallbackEvent;
2223
use handler::{CallbackSettings, HandlerFn};
2324
use runtime::{initialize_runtime, Runtime, RuntimeContainer, RuntimeInitializer, RuntimeSettings};
24-
use script::Scripts;
25+
use script::{ScriptId, Scripts, StaticScripts};
2526

2627
mod extractors;
2728

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

115116
register_script_plugin_systems::<P>(app);
117+
118+
// add extension for the language to the asset loader
116119
once_per_app_init(app);
117120

118121
app.add_supported_script_extensions(self.supported_extensions);
@@ -252,6 +255,7 @@ fn once_per_app_init(app: &mut App) {
252255
.add_event::<ScriptCallbackEvent>()
253256
.init_resource::<AppReflectAllocator>()
254257
.init_resource::<Scripts>()
258+
.init_resource::<StaticScripts>()
255259
.init_asset::<ScriptAsset>()
256260
.init_resource::<AppScriptFunctionRegistry>();
257261

@@ -311,6 +315,31 @@ impl AddRuntimeInitializer for App {
311315
}
312316
}
313317

318+
/// Trait for adding static scripts to an app
319+
pub trait ManageStaticScripts {
320+
/// Registers a script id as a static script.
321+
///
322+
/// Event handlers will run these scripts on top of the entity scripts.
323+
fn add_static_script(&mut self, script_id: impl Into<ScriptId>) -> &mut Self;
324+
325+
/// Removes a script id from the list of static scripts.
326+
///
327+
/// Does nothing if the script id is not in the list.
328+
fn remove_static_script(&mut self, script_id: impl Into<ScriptId>) -> &mut Self;
329+
}
330+
331+
impl ManageStaticScripts for App {
332+
fn add_static_script(&mut self, script_id: impl Into<ScriptId>) -> &mut Self {
333+
AddStaticScript::new(script_id.into()).apply(self.world_mut());
334+
self
335+
}
336+
337+
fn remove_static_script(&mut self, script_id: impl Into<ScriptId>) -> &mut Self {
338+
RemoveStaticScript::new(script_id.into()).apply(self.world_mut());
339+
self
340+
}
341+
}
342+
314343
/// Trait for adding a supported extension to the script asset settings.
315344
///
316345
/// This is only valid in the plugin building phase, as the asset loader will be created in the `finalize` phase.

crates/bevy_mod_scripting_core/src/script.rs

+64-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Script related types, functions and components
22
33
use crate::{asset::ScriptAsset, context::ContextId};
4-
use bevy::{asset::Handle, ecs::system::Resource, reflect::Reflect};
4+
use bevy::{asset::Handle, ecs::system::Resource, reflect::Reflect, utils::HashSet};
55
use std::{borrow::Cow, collections::HashMap, ops::Deref};
66

77
/// 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 {
4747
/// The id of the context this script is currently assigned to
4848
pub context_id: ContextId,
4949
}
50+
51+
/// A collection of scripts, not associated with any entity.
52+
///
53+
/// Useful for `global` or `static` scripts which operate over a larger scope than a single entity.
54+
#[derive(Default, Resource)]
55+
pub struct StaticScripts {
56+
pub(crate) scripts: HashSet<ScriptId>,
57+
}
58+
59+
impl StaticScripts {
60+
/// Inserts a static script into the collection
61+
pub fn insert<S: Into<ScriptId>>(&mut self, script: S) {
62+
self.scripts.insert(script.into());
63+
}
64+
65+
/// Removes a static script from the collection, returning `true` if the script was in the collection, `false` otherwise
66+
pub fn remove<S: Into<ScriptId>>(&mut self, script: S) -> bool {
67+
self.scripts.remove(&script.into())
68+
}
69+
70+
/// Checks if a static script is in the collection
71+
/// Returns `true` if the script is in the collection, `false` otherwise
72+
pub fn contains<S: Into<ScriptId>>(&self, script: S) -> bool {
73+
self.scripts.contains(&script.into())
74+
}
75+
76+
/// Returns an iterator over the static scripts
77+
pub fn iter(&self) -> impl Iterator<Item = &ScriptId> {
78+
self.scripts.iter()
79+
}
80+
}
81+
82+
#[cfg(test)]
83+
mod tests {
84+
use super::*;
85+
86+
#[test]
87+
fn static_scripts_insert() {
88+
let mut static_scripts = StaticScripts::default();
89+
static_scripts.insert("script1");
90+
assert_eq!(static_scripts.scripts.len(), 1);
91+
assert!(static_scripts.scripts.contains("script1"));
92+
}
93+
94+
#[test]
95+
fn static_scripts_remove() {
96+
let mut static_scripts = StaticScripts::default();
97+
static_scripts.insert("script1");
98+
assert_eq!(static_scripts.scripts.len(), 1);
99+
assert!(static_scripts.scripts.contains("script1"));
100+
assert!(static_scripts.remove("script1"));
101+
assert_eq!(static_scripts.scripts.len(), 0);
102+
assert!(!static_scripts.scripts.contains("script1"));
103+
}
104+
105+
#[test]
106+
fn static_scripts_contains() {
107+
let mut static_scripts = StaticScripts::default();
108+
static_scripts.insert("script1");
109+
assert!(static_scripts.contains("script1"));
110+
assert!(!static_scripts.contains("script2"));
111+
}
112+
}

0 commit comments

Comments
 (0)