diff --git a/bevy_mod_scripting/Cargo.toml b/bevy_mod_scripting/Cargo.toml index df85752a07..16f3ddc1f1 100644 --- a/bevy_mod_scripting/Cargo.toml +++ b/bevy_mod_scripting/Cargo.toml @@ -41,4 +41,9 @@ path = "examples/console_integration_rhai.rs" [[example]] name = "complex_game_loop" -path = "examples/complex_game_loop.rs" \ No newline at end of file +path = "examples/complex_game_loop.rs" + + +[[example]] +name = "event_recipients" +path = "examples/event_recipients.rs" \ No newline at end of file diff --git a/bevy_mod_scripting/assets/scripts/event_recipients.lua b/bevy_mod_scripting/assets/scripts/event_recipients.lua new file mode 100644 index 0000000000..69b0b1ca3c --- /dev/null +++ b/bevy_mod_scripting/assets/scripts/event_recipients.lua @@ -0,0 +1,6 @@ + + +function on_event(id) + print(string.format("on_event, script_id: %d, Handling:",script)) + print(string.format("\t-> id: %d", id)) +end \ No newline at end of file diff --git a/bevy_mod_scripting/examples/complex_game_loop.rs b/bevy_mod_scripting/examples/complex_game_loop.rs index edaffe3514..dee10596bd 100644 --- a/bevy_mod_scripting/examples/complex_game_loop.rs +++ b/bevy_mod_scripting/examples/complex_game_loop.rs @@ -2,8 +2,8 @@ use bevy::{core::FixedTimestep, prelude::*}; use bevy_console::ConsolePlugin; use bevy_event_priority::PriorityEventWriter; use bevy_mod_scripting::{ - APIProvider, AddScriptHost, AddScriptHostHandler, LuaEvent, LuaFile, RLuaScriptHost, Script, - ScriptCollection, ScriptingPlugin, + APIProvider, AddScriptHost, AddScriptHostHandler, LuaEvent, LuaFile, RLuaScriptHost, + Recipients, Script, ScriptCollection, ScriptingPlugin, }; use rand::prelude::SliceRandom; use rlua::{Lua, ToLua}; @@ -55,6 +55,7 @@ fn fire_random_event(w: &mut PriorityEventWriter>, events: &[ LuaEvent { hook_name: v.0.to_string(), args: vec![arg], + recipients: Recipients::All, }, v.1, ) diff --git a/bevy_mod_scripting/examples/console_integration_lua.rs b/bevy_mod_scripting/examples/console_integration_lua.rs index 8960fc5b13..888352a8c4 100644 --- a/bevy_mod_scripting/examples/console_integration_lua.rs +++ b/bevy_mod_scripting/examples/console_integration_lua.rs @@ -2,7 +2,7 @@ use bevy::{ecs::event::Events, prelude::*}; use bevy_console::{AddConsoleCommand, ConsoleCommand, ConsolePlugin, PrintConsoleLine}; use bevy_mod_scripting::{ events::PriorityEventWriter, APIProvider, AddScriptHost, AddScriptHostHandler, LuaEvent, - LuaFile, RLuaScriptHost, Script, ScriptCollection, ScriptingPlugin, + LuaFile, RLuaScriptHost, Recipients, Script, ScriptCollection, ScriptingPlugin, }; use rlua::{Lua, ToLua}; use std::sync::Mutex; @@ -51,6 +51,7 @@ pub fn trigger_on_update_lua(mut w: PriorityEventWriter>) { let event = LuaEvent { hook_name: "on_update".to_string(), args: Vec::default(), + recipients: Recipients::All, }; w.send(event, 0); diff --git a/bevy_mod_scripting/examples/console_integration_rhai.rs b/bevy_mod_scripting/examples/console_integration_rhai.rs index 814de85141..f3bb2e432d 100644 --- a/bevy_mod_scripting/examples/console_integration_rhai.rs +++ b/bevy_mod_scripting/examples/console_integration_rhai.rs @@ -1,8 +1,9 @@ use bevy::{ecs::event::Events, prelude::*}; use bevy_console::{AddConsoleCommand, ConsoleCommand, ConsolePlugin, PrintConsoleLine}; use bevy_mod_scripting::{ - events::PriorityEventWriter, APIProvider, AddScriptHost, AddScriptHostHandler, RhaiAPIProvider, - RhaiContext, RhaiEvent, RhaiFile, RhaiScriptHost, Script, ScriptCollection, ScriptingPlugin, + events::PriorityEventWriter, APIProvider, AddScriptHost, AddScriptHostHandler, Recipients, + RhaiAPIProvider, RhaiContext, RhaiEvent, RhaiFile, RhaiScriptHost, Script, ScriptCollection, + ScriptingPlugin, }; use rhai::FuncArgs; @@ -49,6 +50,7 @@ pub fn trigger_on_update_rhai(mut w: PriorityEventWriter ToLua<'lua> for MyLuaArg { + fn to_lua(self, lua: rlua::Context<'lua>) -> rlua::Result> { + self.0.to_lua(lua) + } +} + +#[derive(Default)] +pub struct LuaAPIProvider {} + +/// the custom Lua api, world is provided via a global pointer, +/// and callbacks are defined only once at script creation +impl APIProvider for LuaAPIProvider { + type Ctx = Mutex; + fn attach_api(ctx: &mut Self::Ctx) { + // callbacks can receive any `ToLuaMulti` arguments, here '()' and + // return any `FromLuaMulti` arguments, here a `usize` + // check the Rlua documentation for more details + RLuaScriptHost::::register_api_callback( + "print", + |_ctx, msg: String| { + info!("{}", msg); + Ok(()) + }, + ctx, + ) + } +} + +static COUNTER: AtomicU32 = AtomicU32::new(0); + +/// utility for generating random events from a list +fn fire_random_event(w: &mut PriorityEventWriter>, events: &[ScriptEventData]) { + let mut rng = rand::thread_rng(); + let id = COUNTER.fetch_add(1, Relaxed); + let arg = MyLuaArg(id as usize); + let event = events + .choose(&mut rng) + .map(|v| LuaEvent { + hook_name: v.0.to_string(), + args: vec![arg], + recipients: v.1.clone(), + }) + .unwrap(); + + info!( + "\t - event: {},\t recipients: {:?},\t id: {}", + event.hook_name, event.recipients, id + ); + w.send(event, 0); +} + +fn do_update(mut w: PriorityEventWriter>) { + info!("Update, firing:"); + + let all_events = [ + ScriptEventData("on_event", Recipients::All), + ScriptEventData("on_event", Recipients::ScriptID(0)), + ScriptEventData("on_event", Recipients::ScriptID(1)), + ScriptEventData( + "on_event", + Recipients::ScriptName("scripts/event_recipients.lua".to_owned()), + ), + ]; + + // fire random event, for any stages + fire_random_event(&mut w, &all_events); +} + +#[derive(Clone)] +pub struct ScriptEventData(&'static str, Recipients); + +fn load_our_scripts(server: Res, mut commands: Commands) { + // spawn two identical scripts + // id's will be 0 and 1 + let path = "scripts/event_recipients.lua"; + let handle = server.load::(path); + let scripts = (0..2) + .into_iter() + .map(|_| { + Script::::new::>( + path.to_string(), + handle.clone(), + ) + }) + .collect(); + + commands + .spawn() + .insert(ScriptCollection:: { scripts }); +} + +fn main() -> std::io::Result<()> { + let mut app = App::new(); + + app.add_plugins(DefaultPlugins) + .add_plugin(ScriptingPlugin) + .add_plugin(ConsolePlugin) + .add_startup_system(load_our_scripts) + // randomly fire events for either all scripts, + // the script with id 0 + // or the script with id 1 + .add_system(do_update) + .add_script_handler_stage::, _, 0, 0>( + CoreStage::PostUpdate, + ) + .add_script_host::, _>(CoreStage::PostUpdate); + + app.run(); + + Ok(()) +} diff --git a/bevy_mod_scripting/src/hosts/mod.rs b/bevy_mod_scripting/src/hosts/mod.rs index e624f8c187..2882bd34f3 100644 --- a/bevy_mod_scripting/src/hosts/mod.rs +++ b/bevy_mod_scripting/src/hosts/mod.rs @@ -18,13 +18,57 @@ use std::{ sync::atomic::{AtomicU32, Ordering}, }; +/// Describes the target set of scripts this event should +/// be handled by +#[derive(Clone, Debug)] +pub enum Recipients { + /// Send to all scripts + All, + /// Send only to scripts on the given entity + Entity(Entity), + /// Send to script with the given ID + ScriptID(u32), + // Send to script with the given name + ScriptName(String), +} + +#[derive(Debug)] +pub struct FlatScriptData<'a> { + sid: u32, + entity: Entity, + name: &'a str, +} + +impl Recipients { + /// Returns true if the given script should + pub fn is_recipient(&self, c: &FlatScriptData) -> bool { + match self { + Recipients::All => true, + Recipients::Entity(e) => e == &c.entity, + Recipients::ScriptID(i) => i == &c.sid, + Recipients::ScriptName(n) => n == c.name, + } + } +} + +impl Default for Recipients { + fn default() -> Self { + Self::All + } +} + +pub trait ScriptEvent: Send + Sync + Clone + 'static { + /// Retrieves the recipient scripts for this event + fn recipients(&self) -> &Recipients; +} + /// A script host is the interface between your rust application /// and the scripts in some interpreted language. pub trait ScriptHost: Send + Sync + 'static { /// the type of the persistent script context, representing the execution context of the script type ScriptContext: Send + Sync + 'static; /// the type of events picked up by lua callbacks - type ScriptEvent: Send + Sync + Clone + 'static; + type ScriptEvent: ScriptEvent; /// the type of asset representing the script files for this host type ScriptAsset: CodeAsset; @@ -37,11 +81,12 @@ pub trait ScriptHost: Send + Sync + 'static { fn handle_events<'a>( world: &mut World, events: &[Self::ScriptEvent], - ctxs: impl Iterator, + ctxs: impl Iterator, &'a mut Self::ScriptContext)>, ) -> Result<()>; /// Loads and runs script instantaneously without storing any script data into the world. - /// The script receives the `world` global as normal, but `entity` is set to `u64::MAX` + /// The script receives the `world` global as normal, but `entity` is set to `u64::MAX`. + /// The script id is set to `u32::MAX`. fn run_one_shot( script: &[u8], script_name: &str, @@ -49,10 +94,18 @@ pub trait ScriptHost: Send + Sync + 'static { event: Self::ScriptEvent, ) -> Result<()> { let mut ctx = Self::load_script(script, script_name)?; - let mut entity = Entity::from_bits(u64::MAX); + let entity = Entity::from_bits(u64::MAX); let events = [event; 1]; - let ctx_iter = [(&mut entity, &mut ctx); 1].into_iter(); + let ctx_iter = [( + FlatScriptData { + name: script_name, + sid: u32::MAX, + entity, + }, + &mut ctx, + ); 1] + .into_iter(); Self::handle_events(world, &events, ctx_iter) } @@ -192,7 +245,7 @@ pub struct ScriptCollection { pub struct ScriptContexts { /// holds script contexts for all scripts given their instance ids. /// This also stores contexts which are not fully loaded hence the Option - pub context_entities: HashMap)>, + pub context_entities: HashMap, String)>, } impl Default for ScriptContexts { @@ -205,11 +258,12 @@ impl Default for ScriptContexts { impl ScriptContexts { pub fn script_owner(&self, script_id: u32) -> Option { - self.context_entities.get(&script_id).map(|(e, _c)| *e) + self.context_entities.get(&script_id).map(|(e, _c, _n)| *e) } - pub fn insert_context(&mut self, script_id: u32, entity: Entity, ctx: Option) { - self.context_entities.insert(script_id, (entity, ctx)); + pub fn insert_context(&mut self, fd: FlatScriptData, ctx: Option) { + self.context_entities + .insert(fd.sid, (fd.entity, ctx, fd.name.to_owned())); } pub fn remove_context(&mut self, script_id: u32) { @@ -286,24 +340,30 @@ impl Script { script_assets: &Res>, contexts: &mut ResMut>, ) { + let fd = FlatScriptData { + sid: new_script.id(), + entity, + name: new_script.name(), + }; + let script = match script_assets.get(&new_script.handle) { Some(s) => s, None => { // not loaded yet - contexts.insert_context(new_script.id(), entity, None); + contexts.insert_context(fd, None); return; } }; match H::load_script(script.bytes(), &new_script.name) { Ok(ctx) => { - contexts.insert_context(new_script.id(), entity, Some(ctx)); + contexts.insert_context(fd, Some(ctx)); } Err(e) => { warn! {"Error in loading script {}:\n{}", &new_script.name,e} // this script will now never execute, unless manually reloaded // but contexts are left in a valid state - contexts.insert_context(new_script.id(), entity, None) + contexts.insert_context(fd, None) } } } @@ -356,7 +416,7 @@ pub(crate) fn script_add_synchronizer( let context_ids = contexts .context_entities .iter() - .filter_map(|(sid, (e, _))| if *e == entity { Some(sid) } else { None }) + .filter_map(|(sid, (e, _, _))| if *e == entity { Some(sid) } else { None }) .cloned() .collect::>(); let script_ids = new_scripts @@ -445,11 +505,26 @@ pub(crate) fn script_event_handler>| { - let ctx_iter = ctxts - .as_mut() - .context_entities - .values_mut() - .filter_map(|(e, o)| o.as_mut().map(|v| (e, v))); + let ctx_iter = + ctxts + .as_mut() + .context_entities + .iter_mut() + .filter_map(|(sid, (entity, o, name))| { + let ctx = match o { + Some(v) => v, + None => return None, + }; + + Some(( + FlatScriptData { + sid: *sid, + entity: *entity, + name, + }, + ctx, + )) + }); match H::handle_events(world, &events, ctx_iter) { Ok(_) => {} diff --git a/bevy_mod_scripting/src/hosts/rhai_host/mod.rs b/bevy_mod_scripting/src/hosts/rhai_host/mod.rs index a5738dc9fe..3573767182 100644 --- a/bevy_mod_scripting/src/hosts/rhai_host/mod.rs +++ b/bevy_mod_scripting/src/hosts/rhai_host/mod.rs @@ -2,11 +2,11 @@ pub mod assets; use crate::{ script_add_synchronizer, script_hot_reload_handler, script_remove_synchronizer, APIProvider, - CachedScriptEventState, ScriptContexts, ScriptHost, + CachedScriptEventState, FlatScriptData, Recipients, ScriptContexts, ScriptEvent, ScriptHost, }; use anyhow::anyhow; use beau_collector::BeauCollector as _; -use bevy::prelude::{AddAsset, Entity, SystemSet, World}; +use bevy::prelude::{AddAsset, SystemSet, World}; use bevy_event_priority::AddPriorityEvent; use rhai::*; use std::marker::PhantomData; @@ -39,6 +39,13 @@ pub struct RhaiContext { pub struct RhaiEvent { pub hook_name: String, pub args: A, + pub recipients: Recipients, +} + +impl ScriptEvent for RhaiEvent { + fn recipients(&self) -> &crate::Recipients { + &self.recipients + } } impl> @@ -93,13 +100,19 @@ impl( world: &mut World, events: &[Self::ScriptEvent], - ctxs: impl Iterator, + ctxs: impl Iterator, &'a mut Self::ScriptContext)>, ) -> anyhow::Result<()> { - ctxs.flat_map(|(entity, ctx)| { + ctxs.flat_map(|(fd, ctx)| { ctx.scope.set_value("world", world as *mut World as usize); - ctx.scope.set_value("entity", *entity); + ctx.scope.set_value("entity", fd.entity); + ctx.scope.set_value("script", fd.sid); + + events.iter().map(move |event| { + // check if this script should handle this event + if !event.recipients().is_recipient(&fd) { + return Ok(()); + }; - events.iter().map(|event| { ctx.engine .call_fn( &mut ctx.scope, diff --git a/bevy_mod_scripting/src/hosts/rlua_host/mod.rs b/bevy_mod_scripting/src/hosts/rlua_host/mod.rs index 149688ae59..dea85cd888 100644 --- a/bevy_mod_scripting/src/hosts/rlua_host/mod.rs +++ b/bevy_mod_scripting/src/hosts/rlua_host/mod.rs @@ -2,7 +2,7 @@ pub mod assets; use crate::{ script_add_synchronizer, script_hot_reload_handler, script_remove_synchronizer, APIProvider, - CachedScriptEventState, ScriptContexts, ScriptHost, + CachedScriptEventState, FlatScriptData, Recipients, ScriptContexts, ScriptEvent, ScriptHost, }; use anyhow::{anyhow, Result}; use beau_collector::BeauCollector as _; @@ -26,6 +26,13 @@ impl ToLua<'lua> + Clone + Sync + Send + 'static> LuaArg for T {} pub struct LuaEvent { pub hook_name: String, pub args: Vec, + pub recipients: Recipients, +} + +impl ScriptEvent for LuaEvent { + fn recipients(&self) -> &crate::Recipients { + &self.recipients + } } /// Rlua script host, enables Lua scripting provided by the Rlua library. @@ -135,21 +142,26 @@ impl>> ScriptHost for RLuaScriptHos fn handle_events<'a>( world: &mut World, events: &[Self::ScriptEvent], - ctxs: impl Iterator, + ctxs: impl Iterator, &'a mut Self::ScriptContext)>, ) -> anyhow::Result<()> { - ctxs.map(|(entity, ctx)| { + ctxs.map(|(fd, ctx)| { let world_ptr = world as *mut World as usize; - let lua_ctx = ctx.get_mut().unwrap(); - lua_ctx.context::<_, Result<()>>(|lua_ctx| { + ctx.get_mut().unwrap().context::<_, Result<()>>(|lua_ctx| { let globals = lua_ctx.globals(); globals.set("world", world_ptr)?; - globals.set("entity", entity.to_bits())?; + globals.set("entity", fd.entity.to_bits())?; + globals.set("script", fd.sid)?; // event order is preserved, but scripts can't rely on any temporal // guarantees when it comes to other scripts callbacks, // at least for now for event in events { + // check if this script should handle this event + if !event.recipients().is_recipient(&fd) { + continue; + } + let mut f: Function = match globals.get(event.hook_name.clone()) { Ok(f) => f, Err(_) => continue, // not subscribed to this event diff --git a/readme.md b/readme.md index 3f5a0bbd26..b53e737992 100644 --- a/readme.md +++ b/readme.md @@ -120,6 +120,7 @@ pub fn trigger_on_update_script_callback(mut w: PriorityEventWriter { hook_name: "on_update".to_string(), args: Vec::default(), + recipients: Recipients::All }; w.send(event,0); @@ -152,6 +153,7 @@ pub fn trigger_on_update_rhai(mut w: PriorityEventWriter