Skip to content

refactor!: Merge ScriptContexts<T> into Scripts<T> + Remove Sync bound from Contexts #350

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
Mar 7, 2025
Merged
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
2 changes: 1 addition & 1 deletion crates/bevy_mod_scripting_core/src/bindings/schedule.rs
Original file line number Diff line number Diff line change
@@ -334,7 +334,7 @@ impl ScriptSystemBuilder {
bevy::log::debug_once!("First call to script system {}", name);
match handler_ctxt.call_dynamic_label(
&name,
self.script_id.clone(),
&self.script_id,
Entity::from_raw(0),
vec![],
guard.clone(),
398 changes: 184 additions & 214 deletions crates/bevy_mod_scripting_core/src/commands.rs

Large diffs are not rendered by default.

194 changes: 21 additions & 173 deletions crates/bevy_mod_scripting_core/src/context.rs
Original file line number Diff line number Diff line change
@@ -3,73 +3,17 @@
use crate::{
bindings::{ThreadWorldContainer, WorldContainer, WorldGuard},
error::{InteropError, ScriptError},
script::{Script, ScriptId},
script::ScriptId,
IntoScriptPluginParams,
};
use bevy::ecs::{entity::Entity, system::Resource};
use std::{collections::HashMap, sync::atomic::AtomicU32};

/// A trait that all script contexts must implement.
pub trait Context: 'static + Send + Sync {}
impl<T: 'static + Send + Sync> Context for T {}

/// The type of a context id
pub type ContextId = u32;

/// Stores script state for a scripting plugin. Scripts are identified by their `ScriptId`, while contexts are identified by their `ContextId`.
#[derive(Resource)]
pub struct ScriptContexts<P: IntoScriptPluginParams> {
/// The contexts of the scripts
pub contexts: HashMap<ContextId, P::C>,
}

impl<P: IntoScriptPluginParams> Default for ScriptContexts<P> {
fn default() -> Self {
Self {
contexts: Default::default(),
}
}
}

static CONTEXT_ID_COUNTER: AtomicU32 = AtomicU32::new(0);
impl<P: IntoScriptPluginParams> ScriptContexts<P> {
/// Allocates a new ContextId and inserts the context into the map
pub fn insert(&mut self, ctxt: P::C) -> ContextId {
let id = CONTEXT_ID_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
self.contexts.insert(id, ctxt);
id
}

/// Inserts a context with a specific id
pub fn insert_with_id(&mut self, id: ContextId, ctxt: P::C) -> Option<P::C> {
self.contexts.insert(id, ctxt)
}

/// Allocate new context id without inserting a context
pub fn allocate_id(&self) -> ContextId {
CONTEXT_ID_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
}

/// Removes a context from the map
pub fn remove(&mut self, id: ContextId) -> Option<P::C> {
self.contexts.remove(&id)
}

/// Get a reference to a context
pub fn get(&self, id: ContextId) -> Option<&P::C> {
self.contexts.get(&id)
}

/// Get a mutable reference to a context
pub fn get_mut(&mut self, id: ContextId) -> Option<&mut P::C> {
self.contexts.get_mut(&id)
}

/// Check if a context exists
pub fn contains(&self, id: ContextId) -> bool {
self.contexts.contains_key(&id)
}
}
///
/// Contexts are not required to be `Sync` as they are internally stored behind a `Mutex` but they must satisfy `Send` so they can be
/// freely sent between threads.
pub trait Context: 'static + Send {}
impl<T: 'static + Send> Context for T {}

/// Initializer run once after creating a context but before executing it for the first time as well as after re-loading the script
pub type ContextInitializer<P> =
@@ -85,7 +29,7 @@ pub struct ContextLoadingSettings<P: IntoScriptPluginParams> {
/// Defines the strategy used to load and reload contexts
pub loader: ContextBuilder<P>,
/// Defines the strategy used to assign contexts to scripts
pub assigner: ContextAssigner<P>,
pub assignment_strategy: ContextAssignmentStrategy,
/// Initializers run once after creating a context but before executing it for the first time
pub context_initializers: Vec<ContextInitializer<P>>,
/// Initializers run every time before executing or loading a script
@@ -96,7 +40,7 @@ impl<P: IntoScriptPluginParams> Default for ContextLoadingSettings<P> {
fn default() -> Self {
Self {
loader: ContextBuilder::default(),
assigner: ContextAssigner::default(),
assignment_strategy: Default::default(),
context_initializers: Default::default(),
context_pre_handling_initializers: Default::default(),
}
@@ -107,7 +51,7 @@ impl<T: IntoScriptPluginParams> Clone for ContextLoadingSettings<T> {
fn clone(&self) -> Self {
Self {
loader: self.loader.clone(),
assigner: self.assigner.clone(),
assignment_strategy: self.assignment_strategy,
context_initializers: self.context_initializers.clone(),
context_pre_handling_initializers: self.context_pre_handling_initializers.clone(),
}
@@ -119,7 +63,7 @@ pub type ContextLoadFn<P> = fn(
content: &[u8],
context_initializers: &[ContextInitializer<P>],
pre_handling_initializers: &[ContextPreHandlingInitializer<P>],
runtime: &mut <P as IntoScriptPluginParams>::R,
runtime: &<P as IntoScriptPluginParams>::R,
) -> Result<<P as IntoScriptPluginParams>::C, ScriptError>;

/// A strategy for reloading contexts
@@ -129,7 +73,7 @@ pub type ContextReloadFn<P> = fn(
previous_context: &mut <P as IntoScriptPluginParams>::C,
context_initializers: &[ContextInitializer<P>],
pre_handling_initializers: &[ContextPreHandlingInitializer<P>],
runtime: &mut <P as IntoScriptPluginParams>::R,
runtime: &<P as IntoScriptPluginParams>::R,
) -> Result<(), ScriptError>;

/// A strategy for loading and reloading contexts
@@ -160,7 +104,7 @@ impl<P: IntoScriptPluginParams> ContextBuilder<P> {
context_initializers: &[ContextInitializer<P>],
pre_handling_initializers: &[ContextPreHandlingInitializer<P>],
world: WorldGuard,
runtime: &mut P::R,
runtime: &P::R,
) -> Result<P::C, ScriptError> {
WorldGuard::with_existing_static_guard(world.clone(), |world| {
ThreadWorldContainer.set_world(world)?;
@@ -183,7 +127,7 @@ impl<P: IntoScriptPluginParams> ContextBuilder<P> {
context_initializers: &[ContextInitializer<P>],
pre_handling_initializers: &[ContextPreHandlingInitializer<P>],
world: WorldGuard,
runtime: &mut P::R,
runtime: &P::R,
) -> Result<(), ScriptError> {
WorldGuard::with_existing_static_guard(world, |world| {
ThreadWorldContainer.set_world(world)?;
@@ -208,108 +152,12 @@ impl<P: IntoScriptPluginParams> Clone for ContextBuilder<P> {
}
}

/// A strategy for assigning contexts to new and existing but re-loaded scripts as well as for managing old contexts
pub struct ContextAssigner<P: IntoScriptPluginParams> {
/// Assign a context to the script.
/// The assigner can either return `Some(id)` or `None`.
/// Returning None will request the processor to assign a new context id to assign to this script.
///
/// Regardless, whether a script gets a new context id or not, the processor will check if the given context exists.
/// If it does not exist, it will create a new context and assign it to the script.
/// If it does exist, it will NOT create a new context, but assign the existing one to the script, and re-load the context.
///
/// This function is only called once for each script, when it is loaded for the first time.
pub assign: fn(
script_id: &ScriptId,
new_content: &[u8],
contexts: &ScriptContexts<P>,
) -> Option<ContextId>,

/// Handle the removal of the script, if any clean up in contexts is necessary perform it here.
///
/// If you do not clean up the context here, it will stay in the context map!
pub remove: fn(context_id: ContextId, script: &Script, contexts: &mut ScriptContexts<P>),
}

impl<P: IntoScriptPluginParams> ContextAssigner<P> {
/// Create an assigner which re-uses a single global context for all scripts, only use if you know what you're doing.
/// Will not perform any clean up on removal.
pub fn new_global_context_assigner() -> Self {
Self {
assign: |_, _, _| Some(0), // always use the same id in rotation
remove: |_, _, _| {}, // do nothing
}
}

/// Create an assigner which assigns a new context to each script. This is the default strategy.
pub fn new_individual_context_assigner() -> Self {
Self {
assign: |_, _, _| None,
remove: |id, _, c| _ = c.remove(id),
}
}
}

impl<P: IntoScriptPluginParams> Default for ContextAssigner<P> {
fn default() -> Self {
Self::new_individual_context_assigner()
}
}

impl<P: IntoScriptPluginParams> Clone for ContextAssigner<P> {
fn clone(&self) -> Self {
Self {
assign: self.assign,
remove: self.remove,
}
}
}

#[cfg(test)]
mod tests {
use crate::asset::Language;

use super::*;

struct DummyParams;
impl IntoScriptPluginParams for DummyParams {
type C = String;
type R = ();

const LANGUAGE: Language = Language::Lua;

fn build_runtime() -> Self::R {}
}

#[test]
fn test_script_contexts_insert_get() {
let mut contexts: ScriptContexts<DummyParams> = ScriptContexts::default();
let id = contexts.insert("context1".to_string());
assert_eq!(contexts.contexts.get(&id), Some(&"context1".to_string()));
assert_eq!(
contexts.contexts.get_mut(&id),
Some(&mut "context1".to_string())
);
}

#[test]
fn test_script_contexts_allocate_id() {
let contexts: ScriptContexts<DummyParams> = ScriptContexts::default();
let id = contexts.allocate_id();
let next_id = contexts.allocate_id();
assert_eq!(next_id, id + 1);
}

#[test]
fn test_script_contexts_remove() {
let mut contexts: ScriptContexts<DummyParams> = ScriptContexts::default();
let id = contexts.insert("context1".to_string());
let removed = contexts.remove(id);
assert_eq!(removed, Some("context1".to_string()));
assert!(!contexts.contexts.contains_key(&id));

// assert next id is still incremented
let next_id = contexts.allocate_id();
assert_eq!(next_id, id + 1);
}
/// The strategy used in assigning contexts to scripts
#[derive(Default, Clone, Copy)]
pub enum ContextAssignmentStrategy {
/// Assign a new context to each script
#[default]
Individual,
/// Share contexts with all other scripts
Global,
}
30 changes: 9 additions & 21 deletions crates/bevy_mod_scripting_core/src/error.rs
Original file line number Diff line number Diff line change
@@ -8,7 +8,6 @@ use crate::{
script_value::ScriptValue,
ReflectBaseType, ReflectReference,
},
context::ContextId,
script::ScriptId,
};
use bevy::{
@@ -599,9 +598,8 @@ impl InteropError {
}

/// Thrown if the required context for an operation is missing.
pub fn missing_context(context_id: ContextId, script_id: impl Into<ScriptId>) -> Self {
pub fn missing_context(script_id: impl Into<ScriptId>) -> Self {
Self(Arc::new(InteropErrorInner::MissingContext {
context_id,
script_id: script_id.into(),
}))
}
@@ -812,8 +810,6 @@ pub enum InteropErrorInner {
},
/// Thrown if the required context for an operation is missing.
MissingContext {
/// The context that was missing
context_id: ContextId,
/// The script that was attempting to access the context
script_id: ScriptId,
},
@@ -1053,15 +1049,9 @@ impl PartialEq for InteropErrorInner {
},
) => a == c && b == d,
(
InteropErrorInner::MissingContext {
context_id: a,
script_id: b,
},
InteropErrorInner::MissingContext {
context_id: c,
script_id: d,
},
) => a == c && b == d,
InteropErrorInner::MissingContext { script_id: b },
InteropErrorInner::MissingContext { script_id: d },
) => b == d,
(
InteropErrorInner::MissingSchedule { schedule_name: a },
InteropErrorInner::MissingSchedule { schedule_name: b },
@@ -1284,10 +1274,10 @@ macro_rules! argument_count_mismatch_msg {
}

macro_rules! missing_context_for_callback {
($context_id:expr, $script_id:expr) => {
($script_id:expr) => {
format!(
"Missing context with id: {} for script with id: {}. Was the script loaded?.",
$context_id, $script_id
"Missing context for script with id: {}. Was the script loaded?.",
$script_id
)
};
}
@@ -1433,9 +1423,8 @@ impl DisplayWithWorld for InteropErrorInner {
InteropErrorInner::MissingScript { script_id } => {
missing_script_for_callback!(script_id)
},
InteropErrorInner::MissingContext { context_id, script_id } => {
InteropErrorInner::MissingContext { script_id } => {
missing_context_for_callback!(
context_id,
script_id
)
},
@@ -1580,9 +1569,8 @@ impl DisplayWithWorld for InteropErrorInner {
InteropErrorInner::MissingScript { script_id } => {
missing_script_for_callback!(script_id)
},
InteropErrorInner::MissingContext { context_id, script_id } => {
InteropErrorInner::MissingContext { script_id } => {
missing_context_for_callback!(
context_id,
script_id
)
},
56 changes: 17 additions & 39 deletions crates/bevy_mod_scripting_core/src/extractors.rs
Original file line number Diff line number Diff line change
@@ -20,7 +20,7 @@ use crate::{
access_map::ReflectAccessId, pretty_print::DisplayWithWorld, script_value::ScriptValue,
WorldAccessGuard, WorldGuard,
},
context::{ContextLoadingSettings, ScriptContexts},
context::ContextLoadingSettings,
error::{InteropError, ScriptError},
event::{CallbackLabel, IntoCallbackLabel},
handler::CallbackSettings,
@@ -141,11 +141,9 @@ pub struct HandlerContext<'s, P: IntoScriptPluginParams> {
/// Settings for loading contexts
pub(crate) context_loading_settings: ResScope<'s, ContextLoadingSettings<P>>,
/// Scripts
pub(crate) scripts: ResScope<'s, Scripts>,
pub(crate) scripts: ResScope<'s, Scripts<P>>,
/// The runtime container
pub(crate) runtime_container: ResScope<'s, RuntimeContainer<P>>,
/// The script contexts
pub(crate) script_contexts: ResScope<'s, ScriptContexts<P>>,
/// List of static scripts
pub(crate) static_scripts: ResScope<'s, StaticScripts>,
}
@@ -160,17 +158,15 @@ impl<P: IntoScriptPluginParams> HandlerContext<'_, P> {
) -> (
&mut CallbackSettings<P>,
&mut ContextLoadingSettings<P>,
&mut Scripts,
&mut Scripts<P>,
&mut RuntimeContainer<P>,
&mut ScriptContexts<P>,
&mut StaticScripts,
) {
(
&mut self.callback_settings,
&mut self.context_loading_settings,
&mut self.scripts,
&mut self.runtime_container,
&mut self.script_contexts,
&mut self.static_scripts,
)
}
@@ -186,7 +182,7 @@ impl<P: IntoScriptPluginParams> HandlerContext<'_, P> {
}

/// Get the scripts
pub fn scripts(&mut self) -> &mut Scripts {
pub fn scripts(&mut self) -> &mut Scripts<P> {
&mut self.scripts
}

@@ -195,65 +191,47 @@ impl<P: IntoScriptPluginParams> HandlerContext<'_, P> {
&mut self.runtime_container
}

/// Get the script contexts
pub fn script_contexts(&mut self) -> &mut ScriptContexts<P> {
&mut self.script_contexts
}

/// Get the static scripts
pub fn static_scripts(&mut self) -> &mut StaticScripts {
&mut self.static_scripts
}

/// checks if the script is loaded such that it can be executed.
pub fn is_script_fully_loaded(&self, script_id: ScriptId) -> bool {
// check script exists in scripts and contexts
let script = match self.scripts.scripts.get(&script_id) {
Some(script) => script,
None => {
return false;
}
};

self.script_contexts
.contexts
.contains_key(&script.context_id)
self.scripts.scripts.contains_key(&script_id)
}

/// Equivalent to [`Self::call`] but with a dynamically passed in label
pub fn call_dynamic_label(
&mut self,
&self,
label: &CallbackLabel,
script_id: ScriptId,
script_id: &ScriptId,
entity: Entity,
payload: Vec<ScriptValue>,
guard: WorldGuard<'_>,
) -> Result<ScriptValue, ScriptError> {
// find script
let script = match self.scripts.scripts.get(&script_id) {
let script = match self.scripts.scripts.get(script_id) {
Some(script) => script,
None => return Err(InteropError::missing_script(script_id).into()),
};

// find context
let context = match self.script_contexts.contexts.get_mut(&script.context_id) {
Some(context) => context,
None => return Err(InteropError::missing_context(script.context_id, script_id).into()),
None => return Err(InteropError::missing_script(script_id.clone()).into()),
};

// call the script
let handler = self.callback_settings.callback_handler;
let pre_handling_initializers = &self
.context_loading_settings
.context_pre_handling_initializers;
let runtime = &mut self.runtime_container.runtime;
let runtime = &self.runtime_container.runtime;

let mut context = script.context.lock();

CallbackSettings::<P>::call(
handler,
payload,
entity,
&script_id,
script_id,
label,
context,
&mut context,
pre_handling_initializers,
runtime,
guard,
@@ -265,8 +243,8 @@ impl<P: IntoScriptPluginParams> HandlerContext<'_, P> {
/// This will return [`crate::error::InteropErrorInner::MissingScript`] or [`crate::error::InteropErrorInner::MissingContext`] errors while the script is loading.
/// Run [`Self::is_script_fully_loaded`] before calling the script to ensure the script and context were loaded ahead of time.
pub fn call<C: IntoCallbackLabel>(
&mut self,
script_id: ScriptId,
&self,
script_id: &ScriptId,
entity: Entity,
payload: Vec<ScriptValue>,
guard: WorldGuard<'_>,
177 changes: 57 additions & 120 deletions crates/bevy_mod_scripting_core/src/handler.rs
Original file line number Diff line number Diff line change
@@ -30,7 +30,7 @@ pub type HandlerFn<P> = fn(
callback: &CallbackLabel,
context: &mut <P as IntoScriptPluginParams>::C,
pre_handling_initializers: &[ContextPreHandlingInitializer<P>],
runtime: &mut <P as IntoScriptPluginParams>::R,
runtime: &<P as IntoScriptPluginParams>::R,
) -> Result<ScriptValue, ScriptError>;

/// A resource that holds the settings for the callback handler for a specific combination of type parameters
@@ -72,7 +72,7 @@ impl<P: IntoScriptPluginParams> CallbackSettings<P> {
callback: &CallbackLabel,
script_ctxt: &mut P::C,
pre_handling_initializers: &[ContextPreHandlingInitializer<P>],
runtime: &mut P::R,
runtime: &P::R,
world: WorldGuard,
) -> Result<ScriptValue, ScriptError> {
WorldGuard::with_existing_static_guard(world.clone(), |world| {
@@ -194,7 +194,7 @@ pub(crate) fn event_handler_inner<P: IntoScriptPluginParams>(

let call_result = handler_ctxt.call_dynamic_label(
&callback_label,
script_id.clone(),
script_id,
*entity,
event.args.clone(),
guard.clone(),
@@ -206,7 +206,7 @@ pub(crate) fn event_handler_inner<P: IntoScriptPluginParams>(
match e.downcast_interop_inner() {
Some(InteropErrorInner::MissingScript { script_id }) => {
trace_once!(
"{}: Script `{}` on entity `{:?}` is either still loading or doesn't exist, ignoring until the corresponding script is loaded.",
"{}: Script `{}` on entity `{:?}` is either still loading, doesn't exist, or is for another language, ignoring until the corresponding script is loaded.",
P::LANGUAGE,
script_id, entity
);
@@ -256,21 +256,21 @@ pub fn handle_script_errors<I: Iterator<Item = ScriptError> + Clone>(world: Worl
#[cfg(test)]
#[allow(clippy::todo)]
mod test {
use std::{borrow::Cow, collections::HashMap};
use std::{borrow::Cow, collections::HashMap, sync::Arc};

use bevy::{
app::{App, Update},
asset::AssetPlugin,
diagnostic::DiagnosticsPlugin,
ecs::world::FromWorld,
};
use parking_lot::Mutex;
use test_utils::make_test_plugin;

use crate::{
bindings::script_value::ScriptValue,
context::{ContextAssigner, ContextBuilder, ContextLoadingSettings, ScriptContexts},
context::{ContextBuilder, ContextLoadingSettings},
event::{CallbackLabel, IntoCallbackLabel, ScriptCallbackEvent, ScriptErrorEvent},
handler::HandlerFn,
runtime::RuntimeContainer,
script::{Script, ScriptComponent, ScriptId, Scripts, StaticScripts},
};
@@ -286,33 +286,32 @@ mod test {

make_test_plugin!(crate);

fn setup_app<L: IntoCallbackLabel + 'static, P: IntoScriptPluginParams>(
handler_fn: HandlerFn<P>,
runtime: P::R,
contexts: HashMap<u32, P::C>,
scripts: HashMap<ScriptId, Script>,
fn setup_app<L: IntoCallbackLabel + 'static>(
runtime: TestRuntime,
scripts: HashMap<ScriptId, Script<TestPlugin>>,
) -> App {
let mut app = App::new();

app.add_event::<ScriptCallbackEvent>();
app.add_event::<ScriptErrorEvent>();
app.insert_resource::<CallbackSettings<P>>(CallbackSettings {
callback_handler: handler_fn,
app.insert_resource::<CallbackSettings<TestPlugin>>(CallbackSettings {
callback_handler: |args, entity, script, _, ctxt, _, runtime| {
ctxt.invocations.extend(args);
let mut runtime = runtime.invocations.lock();
runtime.push((entity, script.clone()));
Ok(ScriptValue::Unit)
},
});
app.add_systems(Update, event_handler::<L, P>);
app.insert_resource::<Scripts>(Scripts { scripts });
app.insert_resource(RuntimeContainer::<P> { runtime });
app.insert_resource(ScriptContexts::<P> { contexts });
app.add_systems(Update, event_handler::<L, TestPlugin>);
app.insert_resource::<Scripts<TestPlugin>>(Scripts { scripts });
app.insert_resource(RuntimeContainer::<TestPlugin> { runtime });
app.init_resource::<StaticScripts>();
app.insert_resource(ContextLoadingSettings::<P> {
app.insert_resource(ContextLoadingSettings::<TestPlugin> {
loader: ContextBuilder {
load: |_, _, _, _, _| todo!(),
reload: |_, _, _, _, _, _| todo!(),
},
assigner: ContextAssigner {
assign: |_, _, _| todo!(),
remove: |_, _, _| todo!(),
},
assignment_strategy: Default::default(),
context_initializers: vec![],
context_pre_handling_initializers: vec![],
});
@@ -324,32 +323,16 @@ mod test {
#[test]
fn test_handler_called_with_right_args() {
let test_script_id = Cow::Borrowed("test_script");
let test_ctxt_id = 0;
let test_script = Script {
id: test_script_id.clone(),
asset: None,
context_id: test_ctxt_id,
context: Arc::new(Mutex::new(TestContext::default())),
};
let scripts = HashMap::from_iter(vec![(test_script_id.clone(), test_script.clone())]);
let contexts = HashMap::from_iter(vec![(
test_ctxt_id,
TestContext {
invocations: vec![],
},
)]);
let runtime = TestRuntime {
invocations: vec![],
invocations: vec![].into(),
};
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,
);
let mut app = setup_app::<OnTestCallback>(runtime, scripts);
let test_entity_id = app
.world_mut()
.spawn(ScriptComponent(vec![test_script_id.clone()]))
@@ -361,28 +344,29 @@ mod test {
));
app.update();

let test_context = app
let test_script = app
.world()
.get_resource::<ScriptContexts<TestPlugin>>()
.get_resource::<Scripts<TestPlugin>>()
.unwrap()
.scripts
.get(&test_script_id)
.unwrap();

let test_context = test_script.context.lock();

let test_runtime = app
.world()
.get_resource::<RuntimeContainer<TestPlugin>>()
.unwrap();

assert_eq!(
test_context
.contexts
.get(&test_ctxt_id)
.unwrap()
.invocations,
test_context.invocations,
vec![ScriptValue::String("test_args".into())]
);

let runtime_invocations = test_runtime.runtime.invocations.lock();
assert_eq!(
test_runtime
.runtime
.invocations
runtime_invocations
.iter()
.map(|(e, s)| (*e, s.clone()))
.collect::<Vec<_>>(),
@@ -393,11 +377,10 @@ mod test {
#[test]
fn test_handler_called_on_right_recipients() {
let test_script_id = Cow::Borrowed("test_script");
let test_ctxt_id = 0;
let test_script = Script {
id: test_script_id.clone(),
asset: None,
context_id: test_ctxt_id,
context: Arc::new(Mutex::new(TestContext::default())),
};
let scripts = HashMap::from_iter(vec![
(test_script_id.clone(), test_script.clone()),
@@ -406,37 +389,15 @@ mod test {
Script {
id: "wrong".into(),
asset: None,
context_id: 1,
},
),
]);
let contexts = HashMap::from_iter(vec![
(
test_ctxt_id,
TestContext {
invocations: vec![],
},
),
(
1,
TestContext {
invocations: vec![],
context: Arc::new(Mutex::new(TestContext::default())),
},
),
]);

let runtime = TestRuntime {
invocations: vec![],
invocations: vec![].into(),
};
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,
);
let mut app = setup_app::<OnTestCallback>(runtime, scripts);
let test_entity_id = app
.world_mut()
.spawn(ScriptComponent(vec![test_script_id.clone()]))
@@ -456,21 +417,16 @@ mod test {

app.update();

let test_context = app
.world()
.get_resource::<ScriptContexts<TestPlugin>>()
.unwrap();
let test_scripts = app.world().get_resource::<Scripts<TestPlugin>>().unwrap();
let test_runtime = app
.world()
.get_resource::<RuntimeContainer<TestPlugin>>()
.unwrap();

let test_runtime = test_runtime.runtime.invocations.lock();
let script_after = test_scripts.scripts.get(&test_script_id).unwrap();
let context_after = script_after.context.lock();
assert_eq!(
test_context
.contexts
.get(&test_ctxt_id)
.unwrap()
.invocations,
context_after.invocations,
vec![
ScriptValue::String("test_args_script".into()),
ScriptValue::String("test_args_entity".into())
@@ -479,8 +435,6 @@ mod test {

assert_eq!(
test_runtime
.runtime
.invocations
.iter()
.map(|(e, s)| (*e, s.clone()))
.collect::<Vec<_>>(),
@@ -494,35 +448,19 @@ 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![],
context: Arc::new(Mutex::new(TestContext::default())),
},
)]);
let runtime = TestRuntime {
invocations: vec![],
invocations: vec![].into(),
};
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,
);
let mut app = setup_app::<OnTestCallback>(runtime, scripts);

app.world_mut().insert_resource(StaticScripts {
scripts: vec![test_script_id.clone()].into_iter().collect(),
@@ -542,17 +480,16 @@ mod test {

app.update();

let test_context = app
.world()
.get_resource::<ScriptContexts<TestPlugin>>()
.unwrap();
let test_scripts = app.world().get_resource::<Scripts<TestPlugin>>().unwrap();
let test_context = test_scripts
.scripts
.get(&test_script_id)
.unwrap()
.context
.lock();

assert_eq!(
test_context
.contexts
.get(&test_ctxt_id)
.unwrap()
.invocations,
test_context.invocations,
vec![
ScriptValue::String("test_args_script".into()),
ScriptValue::String("test_script_id".into())
20 changes: 10 additions & 10 deletions crates/bevy_mod_scripting_core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -18,8 +18,8 @@ use bindings::{
};
use commands::{AddStaticScript, RemoveStaticScript};
use context::{
Context, ContextAssigner, ContextBuilder, ContextInitializer, ContextLoadingSettings,
ContextPreHandlingInitializer, ScriptContexts,
Context, ContextAssignmentStrategy, ContextBuilder, ContextInitializer, ContextLoadingSettings,
ContextPreHandlingInitializer,
};
use error::ScriptError;
use event::ScriptCallbackEvent;
@@ -83,8 +83,9 @@ pub struct ScriptingPlugin<P: IntoScriptPluginParams> {
pub callback_handler: HandlerFn<P>,
/// The context builder for loading contexts
pub context_builder: ContextBuilder<P>,
/// The context assigner for assigning contexts to scripts.
pub context_assigner: ContextAssigner<P>,

/// The strategy for assigning contexts to scripts
pub context_assignment_strategy: ContextAssignmentStrategy,

/// The asset path to language mapper for the plugin
pub language_mapper: AssetPathToLanguageMapper,
@@ -104,7 +105,7 @@ impl<P: IntoScriptPluginParams> Default for ScriptingPlugin<P> {
runtime_settings: Default::default(),
callback_handler: CallbackSettings::<P>::default().callback_handler,
context_builder: Default::default(),
context_assigner: Default::default(),
context_assignment_strategy: Default::default(),
language_mapper: Default::default(),
context_initializers: Default::default(),
context_pre_handling_initializers: Default::default(),
@@ -119,16 +120,16 @@ impl<P: IntoScriptPluginParams> Plugin for ScriptingPlugin<P> {
.insert_resource::<RuntimeContainer<P>>(RuntimeContainer {
runtime: P::build_runtime(),
})
.init_resource::<ScriptContexts<P>>()
.insert_resource::<CallbackSettings<P>>(CallbackSettings {
callback_handler: self.callback_handler,
})
.insert_resource::<ContextLoadingSettings<P>>(ContextLoadingSettings {
loader: self.context_builder.clone(),
assigner: self.context_assigner.clone(),
assignment_strategy: self.context_assignment_strategy,
context_initializers: self.context_initializers.clone(),
context_pre_handling_initializers: self.context_pre_handling_initializers.clone(),
});
})
.init_resource::<Scripts<P>>();

register_script_plugin_systems::<P>(app);

@@ -227,7 +228,7 @@ impl<P: IntoScriptPluginParams + AsMut<ScriptingPlugin<P>>> ConfigureScriptPlugi
}

fn enable_context_sharing(mut self) -> Self {
self.as_mut().context_assigner = ContextAssigner::new_global_context_assigner();
self.as_mut().context_assignment_strategy = ContextAssignmentStrategy::Global;
self
}
}
@@ -289,7 +290,6 @@ fn once_per_app_init(app: &mut App) {
app.add_event::<ScriptErrorEvent>()
.add_event::<ScriptCallbackEvent>()
.init_resource::<AppReflectAllocator>()
.init_resource::<Scripts>()
.init_resource::<StaticScripts>()
.init_asset::<ScriptAsset>()
.init_resource::<AppScriptFunctionRegistry>()
7 changes: 3 additions & 4 deletions crates/bevy_mod_scripting_core/src/runtime.rs
Original file line number Diff line number Diff line change
@@ -12,8 +12,7 @@ pub trait Runtime: Default + 'static + Send + Sync {}
impl<T: Default + 'static + Send + Sync> Runtime for T {}

/// A function that initializes a runtime.
pub type RuntimeInitializer<P> =
fn(&mut <P as IntoScriptPluginParams>::R) -> Result<(), ScriptError>;
pub type RuntimeInitializer<P> = fn(&<P as IntoScriptPluginParams>::R) -> Result<(), ScriptError>;

#[derive(Resource)]
/// Resource storing settings for a scripting plugin regarding runtime initialization & configuration.
@@ -54,11 +53,11 @@ impl<P: IntoScriptPluginParams> Default for RuntimeContainer<P> {
}

pub(crate) fn initialize_runtime<P: IntoScriptPluginParams>(
mut runtime: ResMut<RuntimeContainer<P>>,
runtime: ResMut<RuntimeContainer<P>>,
settings: Res<RuntimeSettings<P>>,
) -> Result<(), ScriptError> {
for initializer in settings.initializers.iter() {
(initializer)(&mut runtime.runtime)?;
(initializer)(&runtime.runtime)?;
}
Ok(())
}
36 changes: 27 additions & 9 deletions crates/bevy_mod_scripting_core/src/script.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
//! Script related types, functions and components
use crate::{asset::ScriptAsset, context::ContextId};
use crate::{asset::ScriptAsset, IntoScriptPluginParams};
use bevy::{asset::Handle, ecs::system::Resource, reflect::Reflect, utils::HashSet};
use std::{borrow::Cow, collections::HashMap, ops::Deref};
use parking_lot::Mutex;
use std::{borrow::Cow, collections::HashMap, ops::Deref, sync::Arc};

/// A unique identifier for a script, by default corresponds to the path of the asset excluding the asset source.
///
@@ -32,20 +33,37 @@ impl ScriptComponent {
}

/// All the scripts which are currently loaded or loading and their mapping to contexts
#[derive(Resource, Default, Clone)]
pub struct Scripts {
pub(crate) scripts: HashMap<ScriptId, Script>,
#[derive(Resource)]
pub struct Scripts<P: IntoScriptPluginParams> {
pub(crate) scripts: HashMap<ScriptId, Script<P>>,
}

impl<P: IntoScriptPluginParams> Default for Scripts<P> {
fn default() -> Self {
Self {
scripts: Default::default(),
}
}
}

/// A script
#[derive(Clone)]
pub struct Script {
pub struct Script<P: IntoScriptPluginParams> {
/// The id of the script
pub id: ScriptId,
/// the asset holding the content of the script if it comes from an asset
pub asset: Option<Handle<ScriptAsset>>,
/// The id of the context this script is currently assigned to
pub context_id: ContextId,
/// The context of the script, possibly shared with other scripts
pub context: Arc<Mutex<P::C>>,
}

impl<P: IntoScriptPluginParams> Clone for Script<P> {
fn clone(&self) -> Self {
Self {
id: self.id.clone(),
asset: self.asset.clone(),
context: self.context.clone(),
}
}
}

/// A collection of scripts, not associated with any entity.
12 changes: 6 additions & 6 deletions crates/languages/bevy_mod_scripting_lua/src/lib.rs
Original file line number Diff line number Diff line change
@@ -53,7 +53,7 @@ impl Default for LuaScriptingPlugin {
fn default() -> Self {
LuaScriptingPlugin {
scripting_plugin: ScriptingPlugin {
context_assigner: Default::default(),
context_assignment_strategy: Default::default(),
runtime_settings: RuntimeSettings::default(),
callback_handler: lua_handler,
context_builder: ContextBuilder::<LuaScriptingPlugin> {
@@ -187,7 +187,7 @@ pub fn lua_context_load(
content: &[u8],
initializers: &[ContextInitializer<LuaScriptingPlugin>],
pre_handling_initializers: &[ContextPreHandlingInitializer<LuaScriptingPlugin>],
_: &mut (),
_: &(),
) -> Result<Lua, ScriptError> {
#[cfg(feature = "unsafe_lua_modules")]
let mut context = unsafe { Lua::unsafe_new() };
@@ -212,7 +212,7 @@ pub fn lua_context_reload(
old_ctxt: &mut Lua,
initializers: &[ContextInitializer<LuaScriptingPlugin>],
pre_handling_initializers: &[ContextPreHandlingInitializer<LuaScriptingPlugin>],
_: &mut (),
_: &(),
) -> Result<(), ScriptError> {
load_lua_content_into_context(
old_ctxt,
@@ -234,7 +234,7 @@ pub fn lua_handler(
callback_label: &CallbackLabel,
context: &mut Lua,
pre_handling_initializers: &[ContextPreHandlingInitializer<LuaScriptingPlugin>],
_: &mut (),
_: &(),
) -> Result<ScriptValue, bevy_mod_scripting_core::error::ScriptError> {
pre_handling_initializers
.iter()
@@ -285,7 +285,7 @@ mod test {
.as_bytes(),
&initializers,
&pre_handling_initializers,
&mut (),
&(),
)
.unwrap();

@@ -298,7 +298,7 @@ mod test {
&mut old_ctxt,
&initializers,
&pre_handling_initializers,
&mut (),
&(),
)
.unwrap();

1 change: 1 addition & 0 deletions crates/languages/bevy_mod_scripting_rhai/Cargo.toml
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@ bevy = { workspace = true, default-features = false }
rhai = { version = "1.21" }
bevy_mod_scripting_core = { workspace = true, features = ["rhai_impls"] }
strum = { version = "0.26", features = ["derive"] }
parking_lot = "0.12.1"

[dev-dependencies]
script_integration_test_harness = { workspace = true }
35 changes: 21 additions & 14 deletions crates/languages/bevy_mod_scripting_rhai/src/lib.rs
Original file line number Diff line number Diff line change
@@ -23,14 +23,15 @@ use bindings::{
reference::{ReservedKeyword, RhaiReflectReference, RhaiStaticReflectReference},
script_value::{FromDynamic, IntoDynamic},
};
use parking_lot::RwLock;
use rhai::{CallFnOptions, Dynamic, Engine, EvalAltResult, Scope, AST};

pub use rhai;
/// Bindings for rhai.
pub mod bindings;

/// The rhai runtime type.
pub type RhaiRuntime = Engine;
pub type RhaiRuntime = RwLock<Engine>;

/// The rhai context type.
pub struct RhaiScriptContext {
@@ -47,7 +48,7 @@ impl IntoScriptPluginParams for RhaiScriptingPlugin {
const LANGUAGE: Language = Language::Rhai;

fn build_runtime() -> Self::R {
RhaiRuntime::new()
Engine::new().into()
}
}

@@ -67,12 +68,14 @@ impl Default for RhaiScriptingPlugin {
fn default() -> Self {
RhaiScriptingPlugin {
scripting_plugin: ScriptingPlugin {
context_assigner: Default::default(),
context_assignment_strategy: Default::default(),
runtime_settings: RuntimeSettings {
initializers: vec![|runtime: &mut Engine| {
runtime.build_type::<RhaiReflectReference>();
runtime.build_type::<RhaiStaticReflectReference>();
runtime.register_iterator_result::<RhaiReflectReference, _>();
initializers: vec![|runtime: &RhaiRuntime| {
let mut engine = runtime.write();

engine.build_type::<RhaiReflectReference>();
engine.build_type::<RhaiStaticReflectReference>();
engine.register_iterator_result::<RhaiReflectReference, _>();
Ok(())
}],
},
@@ -186,8 +189,10 @@ fn load_rhai_content_into_context(
content: &[u8],
initializers: &[ContextInitializer<RhaiScriptingPlugin>],
pre_handling_initializers: &[ContextPreHandlingInitializer<RhaiScriptingPlugin>],
runtime: &mut RhaiRuntime,
runtime: &RhaiRuntime,
) -> Result<(), ScriptError> {
let runtime = runtime.read();

context.ast = runtime.compile(std::str::from_utf8(content)?)?;
context.ast.set_source(script.to_string());

@@ -209,7 +214,7 @@ pub fn rhai_context_load(
content: &[u8],
initializers: &[ContextInitializer<RhaiScriptingPlugin>],
pre_handling_initializers: &[ContextPreHandlingInitializer<RhaiScriptingPlugin>],
runtime: &mut RhaiRuntime,
runtime: &RhaiRuntime,
) -> Result<RhaiScriptContext, ScriptError> {
let mut context = RhaiScriptContext {
// Using an empty AST as a placeholder.
@@ -234,7 +239,7 @@ pub fn rhai_context_reload(
context: &mut RhaiScriptContext,
initializers: &[ContextInitializer<RhaiScriptingPlugin>],
pre_handling_initializers: &[ContextPreHandlingInitializer<RhaiScriptingPlugin>],
runtime: &mut RhaiRuntime,
runtime: &RhaiRuntime,
) -> Result<(), ScriptError> {
load_rhai_content_into_context(
context,
@@ -255,7 +260,7 @@ pub fn rhai_callback_handler(
callback: &CallbackLabel,
context: &mut RhaiScriptContext,
pre_handling_initializers: &[ContextPreHandlingInitializer<RhaiScriptingPlugin>],
runtime: &mut RhaiRuntime,
runtime: &RhaiRuntime,
) -> Result<ScriptValue, ScriptError> {
pre_handling_initializers
.iter()
@@ -274,6 +279,8 @@ pub fn rhai_callback_handler(
script_id,
args
);
let runtime = runtime.read();

match runtime.call_fn_with_options::<Dynamic>(
options,
&mut context.scope,
@@ -303,7 +310,7 @@ mod test {

#[test]
fn test_reload_doesnt_overwrite_old_context() {
let mut runtime = RhaiRuntime::new();
let runtime = RhaiRuntime::new(Engine::new());
let script_id = ScriptId::from("asd.rhai");
let initializers: Vec<ContextInitializer<RhaiScriptingPlugin>> = vec![];
let pre_handling_initializers: Vec<ContextPreHandlingInitializer<RhaiScriptingPlugin>> =
@@ -315,7 +322,7 @@ mod test {
b"let hello = 2;",
&initializers,
&pre_handling_initializers,
&mut runtime,
&runtime,
)
.unwrap();

@@ -326,7 +333,7 @@ mod test {
&mut context,
&initializers,
&pre_handling_initializers,
&mut runtime,
&runtime,
)
.unwrap();

2 changes: 2 additions & 0 deletions crates/languages/bevy_mod_scripting_rhai/tests/rhai_tests.rs
Original file line number Diff line number Diff line change
@@ -34,6 +34,8 @@ impl Test {
|app| {
app.add_plugins(RhaiScriptingPlugin::default());
app.add_runtime_initializer::<RhaiScriptingPlugin>(|runtime| {
let mut runtime = runtime.write();

runtime.register_fn("assert", |a: Dynamic, b: &str| {
if !a.is::<bool>() {
panic!("Expected a boolean value, but got {:?}", a);
Original file line number Diff line number Diff line change
@@ -167,7 +167,7 @@ fn run_test_callback<P: IntoScriptPluginParams, C: IntoCallbackLabel>(
}

let res = handler_ctxt.call::<C>(
script_id.to_string().into(),
&script_id.to_string().into(),
Entity::from_raw(0),
vec![],
guard.clone(),
5 changes: 3 additions & 2 deletions crates/testing_crates/test_utils/src/test_plugin.rs
Original file line number Diff line number Diff line change
@@ -26,16 +26,17 @@ macro_rules! make_test_plugin {

fn build_runtime() -> Self::R {
TestRuntime {
invocations: vec![],
invocations: vec![].into(),
}
}
}

#[derive(Default)]
struct TestRuntime {
pub invocations: Vec<(Entity, ScriptId)>,
pub invocations: parking_lot::Mutex<Vec<(Entity, ScriptId)>>,
}

#[derive(Default)]
struct TestContext {
pub invocations: Vec<$ident::bindings::script_value::ScriptValue>,
}