diff --git a/bevy_scripting/assets/scripts/console_integration2.lua b/bevy_scripting/assets/scripts/console_integration2.lua deleted file mode 100644 index a452c4059f..0000000000 --- a/bevy_scripting/assets/scripts/console_integration2.lua +++ /dev/null @@ -1,12 +0,0 @@ -local a = 0 - -function on_update() - - if (a+50) % 100 == 0 then - -- print_to_console()() is defined in console_integration.rs - -- by the api provider - print_to_console(a) - end - - a = a + 1 -end \ No newline at end of file diff --git a/bevy_scripting/examples/console_integration.rs b/bevy_scripting/examples/console_integration.rs index b097465831..2b4e5f5742 100644 --- a/bevy_scripting/examples/console_integration.rs +++ b/bevy_scripting/examples/console_integration.rs @@ -110,13 +110,13 @@ pub fn run_script_cmd( >, ) { if let Some(RunScriptCmd { path, entity }) = log.take() { - info!("Running script: scripts/{}", path); - let handle = server.load::(&format!("scripts/{}", &path)); match entity { Some(e) => { if let Ok(mut scripts) = existing_scripts.get_mut(Entity::from_raw(e)) { + info!("Creating script: scripts/{} {:?}", &path, &entity); + scripts.scripts.push(Script::< as ScriptHost>::ScriptAssetType, >::new::>( @@ -127,6 +127,8 @@ pub fn run_script_cmd( }; } None => { + info!("Creating script: scripts/{}", &path); + commands.spawn().insert(ScriptCollection::< as ScriptHost>::ScriptAssetType, > { @@ -185,6 +187,6 @@ pub struct DeleteScriptCmd { /// the name of the script pub name: String, - /// the entity the script is attached to (only one script can be attached to an entitty as of now) + /// the entity the script is attached to pub entity_id: u32, } diff --git a/bevy_scripting/src/hosts/mod.rs b/bevy_scripting/src/hosts/mod.rs index 41495adc70..d733ed39cf 100644 --- a/bevy_scripting/src/hosts/mod.rs +++ b/bevy_scripting/src/hosts/mod.rs @@ -2,41 +2,111 @@ pub mod rlua_host; use anyhow::Result; use bevy::{asset::Asset, ecs::system::SystemState, prelude::*}; pub use rlua_host::*; -use std::collections::{HashMap, HashSet}; - -pub trait AddScriptHost { - fn add_script_host(&mut self) -> &mut Self; -} +use std::{ + collections::{HashMap, HashSet}, + sync::atomic::{AtomicU32, Ordering}, +}; +/// All code assets share this common interface. +/// When adding a new code asset don't forget to implement asset loading +/// and inserting appropriate systems when registering with the app pub trait CodeAsset: Asset { fn bytes(&self) -> &[u8]; } +/// Implementers can modify a script context in order to enable +/// API access. ScriptHosts call `attach_api` when creating scripts pub trait APIProvider: 'static + Default { + /// The type of script context this api provider handles type Ctx; + + /// provide the given script context with the API permamently fn attach_api(ctx: &Self::Ctx); } +#[derive(Component)] +/// The component storing many scripts. +/// Scripts receive information about the entity they are attached to +/// Scripts have unique identifiers and hence multiple copies of the same script +/// can be attached to the same entity +pub struct ScriptCollection { + pub scripts: Vec>, +} + +#[derive(Default)] +/// A resource storing the script contexts for each script instance. +/// The reason we need this is to split the world borrow in our handle event systems, but this +/// has the added benefit that users don't see the contexts at all, and we can provide +/// generic handling for each new/removed script in one place. +/// +/// We keep this public for now since there is no API for communicating with scripts +/// outside of events. Later this might change. +pub struct ScriptContexts { + /// holds script contexts for all scripts given their instance ids + pub contexts: HashMap, +} + +/// A struct defining an instance of a script asset. +/// Multiple instances of the same script can exist on the same entity (unlike in Unity for example) pub struct Script { + /// a strong handle to the script asset handle: Handle, + + /// the name of the script, usually its file name + relative asset path name: String, + + /// uniquely identifies the script instance (scripts which use the same asset don't necessarily have the same ID) + id: u32, } impl Script { + /// creates a new script instance with the given name and asset handle + /// automatically gives this script instance a unique ID. + /// No two scripts instances ever share the same ID pub fn new(name: String, handle: Handle) -> Self { - Self { handle, name } + static COUNTER: AtomicU32 = AtomicU32::new(0); + Self { + handle, + name, + id: COUNTER.fetch_add(1, Ordering::Relaxed), + } } + #[inline(always)] + /// returns the name of the script pub fn name(&self) -> &str { &self.name } + + #[inline(always)] + /// returns the asset handle which this script is executing pub fn handle(&self) -> &Handle { &self.handle } - fn insert_new_script_context( + #[inline(always)] + /// returns the unique ID of this script instance + pub fn id(&self) -> u32 { + self.id + } + + /// reloads the script by deleting the old context and inserting a new one + /// if the script context never existed, it will after this call. + pub(crate) fn reload_script( + script: &Script, + script_assets: &Res>, + contexts: &mut ResMut>, + ) { + // remove old context + contexts.contexts.remove(&script.id); + + // insert new re-loaded context + Self::insert_new_script_context(script, script_assets, contexts) + } + + /// inserts a new script context for the given script + pub(crate) fn insert_new_script_context( new_script: &Script, - entity: &Entity, script_assets: &Res>, contexts: &mut ResMut>, ) { @@ -53,15 +123,7 @@ impl Script { // allow plugging in an API H::ScriptAPIProvider::attach_api(&ctx); - let name_map = contexts.contexts.entry(entity.id()).or_default(); - - // if the script already exists on an entity, panic - // not allowed at least for now - if name_map.contains_key(&new_script.name) { - panic!("Attempted to attach script: {} to entity which already has another copy of this script attached", &new_script.name); - } - - name_map.insert(new_script.name.clone(), ctx); + contexts.contexts.insert(new_script.id(), ctx); } Err(_e) => { warn! {"Failed to load script: {}", &new_script.name} @@ -71,19 +133,8 @@ impl Script { } } -#[derive(Component)] -pub struct ScriptCollection { - pub scripts: Vec>, -} - -#[derive(Default)] -pub struct ScriptContexts { - /// holds script contexts for all scripts - /// and keeps track of which entities they're attached to - pub contexts: HashMap>, -} - -pub struct CachedScriptEventState<'w, 's, S: Send + Sync + 'static> { +/// system state for exclusive systems dealing with script events +pub(crate) struct CachedScriptEventState<'w, 's, S: Send + Sync + 'static> { event_state: SystemState>, } @@ -95,10 +146,10 @@ impl<'w, 's, S: Send + Sync + 'static> FromWorld for CachedScriptEventState<'w, } } -pub fn script_add_synchronizer( +/// Handles creating contexts for new/modified scripts +pub(crate) fn script_add_synchronizer( query: Query< ( - Entity, &ScriptCollection, ChangeTrackers>, ), @@ -107,12 +158,11 @@ pub fn script_add_synchronizer( mut contexts: ResMut>, script_assets: Res>, ) { - query.for_each(|(entity, new_scripts, tracker)| { + query.for_each(|(new_scripts, tracker)| { if tracker.is_added() { new_scripts.scripts.iter().for_each(|new_script| { Script::::insert_new_script_context( new_script, - &entity, &script_assets, &mut contexts, ) @@ -122,32 +172,26 @@ pub fn script_add_synchronizer( // find out what's changed // we only care about added or removed scripts here // if the script asset gets changed we deal with that elsewhere - // TODO: reduce allocations in this function the change detection here is kinda clunky - let name_map = contexts.contexts.entry(entity.id()).or_default(); - - let previous_scripts = name_map.keys().cloned().collect::>(); - - let current_scripts = new_scripts + let context_ids = contexts.contexts.keys().cloned().collect::>(); + let script_ids = new_scripts .scripts .iter() - .map(|s| s.name.clone()) - .collect::>(); + .map(|s| s.id()) + .collect::>(); - // find new/removed scripts - let removed_scripts = previous_scripts.difference(¤t_scripts); - let added_scripts = current_scripts.difference(&previous_scripts); + let removed_scripts = context_ids.difference(&script_ids); + let added_scripts = script_ids.difference(&context_ids); for r in removed_scripts { - name_map.remove(r); + contexts.contexts.remove(r); } for a in added_scripts { - let script = new_scripts.scripts.iter().find(|e| &e.name == a).unwrap(); + let script = new_scripts.scripts.iter().find(|e| &e.id == a).unwrap(); Script::::insert_new_script_context( script, - &entity, &script_assets, &mut contexts, ) @@ -156,7 +200,8 @@ pub fn script_add_synchronizer( }) } -pub fn script_remove_synchronizer( +/// Handles the removal of script components and their contexts +pub(crate) fn script_remove_synchronizer( query: RemovedComponents>, mut contexts: ResMut>, ) { @@ -170,7 +215,36 @@ pub fn script_remove_synchronizer( }) } -pub fn script_event_handler(world: &mut World) { +/// Reloads hot-reloaded scripts +pub(crate) fn script_hot_reload_handler( + mut events: EventReader>, + scripts: Query<&ScriptCollection>, + script_assets: Res>, + mut contexts: ResMut>, +) { + for e in events.iter() { + match e { + AssetEvent::Modified { handle } => { + // find script using this handle by handle id + for scripts in scripts.iter() { + for script in &scripts.scripts { + if &script.handle == handle { + Script::::reload_script( + script, + &script_assets, + &mut contexts, + ); + } + } + } + } + _ => continue, + } + } +} + +/// Lets the script host handle all script events +pub(crate) fn script_event_handler(world: &mut World) { world.resource_scope( |world, mut cached_state: Mut>| { // we need to clone the events otherwise we cannot perform the subsequent query for scripts @@ -190,16 +264,30 @@ pub fn script_event_handler(world: &mut World) { ); } +/// A script host is the interface between your rust application +/// and the scripts in some interpreted language. pub trait ScriptHost: Send + Sync + 'static { type ScriptContext: Send + Sync + 'static; type ScriptEventType: Send + Sync + Clone + 'static; type ScriptAssetType: CodeAsset; type ScriptAPIProvider: APIProvider; + /// Loads a script in byte array format, the script name can be used + /// to send useful errors. fn load_script(path: &[u8], script_name: &str) -> Result; + + /// the main point of contact with the bevy world. + /// Scripts are called with appropriate events in the event order fn handle_events(world: &mut World, events: &[Self::ScriptEventType]) -> Result<()>; - /// registers the script host with the given app, and stage. - /// all script events generated will be handled at the end of this stage. Ideally place after update + /// Registers the script host with the given app, and stage. + /// all script events generated will be handled at the end of this stage. Ideally place after any game logic + /// which can spawn/remove/modify scripts to avoid frame lag. (typically `CoreStage::Post_Update`) fn register_with_app(app: &mut App, stage: impl StageLabel); } + +/// Trait for app builder notation +pub trait AddScriptHost { + /// registers the given script host with your app + fn add_script_host(&mut self) -> &mut Self; +} diff --git a/bevy_scripting/src/hosts/rlua_host.rs b/bevy_scripting/src/hosts/rlua_host.rs index 952a75ea0c..f37c75ccd8 100644 --- a/bevy_scripting/src/hosts/rlua_host.rs +++ b/bevy_scripting/src/hosts/rlua_host.rs @@ -1,26 +1,22 @@ -use std::ffi::c_void; -use std::marker::PhantomData; - -use std::sync::{Arc, Mutex}; - use crate::{ - script_add_synchronizer, script_event_handler, script_remove_synchronizer, APIProvider, - CachedScriptEventState, CodeAsset, ScriptContexts, ScriptHost, + script_add_synchronizer, script_event_handler, script_hot_reload_handler, + script_remove_synchronizer, APIProvider, CachedScriptEventState, CodeAsset, ScriptContexts, + ScriptHost, }; use anyhow::{anyhow, Result}; use beau_collector::BeauCollector as _; use bevy::asset::{AssetLoader, LoadedAsset}; - -use bevy::prelude::{ - App, ExclusiveSystemDescriptorCoercion, IntoExclusiveSystem, Mut, StageLabel, SystemSet, World, -}; +use bevy::prelude::*; use bevy::reflect::TypeUuid; - use rlua::prelude::*; use rlua::{Context, Function, Lua, MultiValue, ToLua, ToLuaMulti}; +use std::ffi::c_void; +use std::marker::PhantomData; +use std::sync::{Arc, Mutex}; #[derive(Debug, TypeUuid)] #[uuid = "39cadc56-aa9c-4543-8640-a018b74b5052"] +/// A lua code file in bytes pub struct LuaFile { pub bytes: Arc<[u8]>, } @@ -32,6 +28,7 @@ impl CodeAsset for LuaFile { } #[derive(Default)] +/// Asset loader for lua scripts pub struct LuaLoader; impl AssetLoader for LuaLoader { @@ -52,6 +49,7 @@ impl AssetLoader for LuaLoader { } /// defines a value allowed to be passed as lua script arguments for callbacks +/// TODO: expand this #[derive(Clone)] pub enum LuaCallbackArgument { Integer(usize), @@ -64,13 +62,17 @@ impl<'lua> ToLua<'lua> for LuaCallbackArgument { } } } + #[derive(Clone)] +/// A Lua Hook. The result of creating this event will be +/// a call to the lua script with the hook_name and the given arguments pub struct LuaEvent { pub hook_name: String, pub args: Vec, } #[derive(Default)] +/// Rlua script host, enables Lua scripting provided by the Rlua library. pub struct RLuaScriptHost { _ph: PhantomData, } @@ -94,6 +96,7 @@ impl>> ScriptHost for RLuaScriptHost { SystemSet::new() .with_system(script_add_synchronizer::) .with_system(script_remove_synchronizer::) + .with_system(script_hot_reload_handler::) .with_system(script_event_handler::.exclusive_system().at_end()), ); } @@ -120,30 +123,28 @@ impl>> ScriptHost for RLuaScriptHost { world.resource_scope(|world, res: Mut>| { res.contexts .values() - .flat_map(|ctx| { + .map(|ctx| { let world_ptr = LuaLightUserData(world as *mut World as *mut c_void); - ctx.values().map(move |ctx| { - let lua_ctx = ctx.lock().unwrap(); - - lua_ctx.context::<_, Result<()>>(|lua_ctx| { - let globals = lua_ctx.globals(); - globals.set("world", world_ptr)?; - - // 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.iter() { - let f: Function = match globals.get(event.hook_name.clone()) { - Ok(f) => f, - Err(_) => continue, // not subscribed to this event - }; - - f.call::(event.args.clone().to_lua_multi(lua_ctx)?) - .map_err(|e| anyhow!("Runtime LUA error: {}", e))?; - } - - Ok(()) - }) + let lua_ctx = ctx.lock().unwrap(); + + lua_ctx.context::<_, Result<()>>(|lua_ctx| { + let globals = lua_ctx.globals(); + globals.set("world", world_ptr)?; + + // 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.iter() { + let f: Function = match globals.get(event.hook_name.clone()) { + Ok(f) => f, + Err(_) => continue, // not subscribed to this event + }; + + f.call::(event.args.clone().to_lua_multi(lua_ctx)?) + .map_err(|e| anyhow!("Runtime LUA error: {}", e))?; + } + + Ok(()) }) }) .bcollect() diff --git a/bevy_scripting/src/lib.rs b/bevy_scripting/src/lib.rs index 8dd7d53f8f..b982f00a5f 100644 --- a/bevy_scripting/src/lib.rs +++ b/bevy_scripting/src/lib.rs @@ -1,8 +1,12 @@ use bevy::prelude::*; + +/// All script host related things pub mod hosts; + pub use hosts::*; #[derive(Default)] +/// Bevy plugin enabling run-time scripting pub struct ScriptingPlugin; impl Plugin for ScriptingPlugin { diff --git a/readme.md b/readme.md index 8718dfdd62..95a7a0faeb 100644 --- a/readme.md +++ b/readme.md @@ -15,20 +15,19 @@ The API will likely change in the future as more scripting support is rolled out ## State of this crate - [x] Script host interface +- [x] Hot re-loading scripts (on script asset changes, scripts using those assets are re-started) - [x] Rlua integration - [ ] Rhai integration - [x] Customisable Lua API - [x] Event based hooks (i.e. on_update) - [ ] Flexible event scheduling (i.e. allow handling events at different stages rather than a single stage based on the event) - [x] Multiple scripts per entity -- [ ] Multiple instances of the same script on one entity -- [ ] Improved Ergonomics +- [x] Multiple instances of the same script on one entity (unlike Unity) +- [ ] Improved Ergonomics (some types are cumbersome right now) +- [ ] General Bevy API for all script hosts (i.e. Add component, remove component etc.). Blocked by https://github.com/bevyengine/bevy/issues/4474 - [ ] More extensive callback argument type support - [ ] Tests - -As of now script component removals do not work properly just yet - ## Usage ### Installation