From db77ca43881c9a6cceb6725295b7918638882c5f Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Mon, 15 Jan 2024 23:22:21 -0500 Subject: [PATCH] scripting: lua script auto-reloading --- Cargo.lock | 24 +++ examples/testbed/scripts/test.lua | 2 +- examples/testbed/src/main.rs | 1 + lyra-game/src/render/light/mod.rs | 4 +- lyra-resource/Cargo.toml | 1 + lyra-resource/src/lib.rs | 5 +- lyra-resource/src/resource.rs | 10 +- lyra-resource/src/resource_manager.rs | 49 +++-- lyra-scripting/src/host.rs | 12 +- lyra-scripting/src/lua/mod.rs | 261 +++++++++++++++++++------- 10 files changed, 279 insertions(+), 90 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7b962c2..ad8a285 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -836,6 +836,15 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "file-id" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6584280525fb2059cba3db2c04abf947a1a29a45ddae89f3870f8281704fafc9" +dependencies = [ + "windows-sys 0.48.0", +] + [[package]] name = "filetime" version = "0.2.23" @@ -1587,6 +1596,7 @@ dependencies = [ "infer", "mime", "notify", + "notify-debouncer-full", "percent-encoding", "thiserror", "tracing", @@ -1831,6 +1841,20 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "notify-debouncer-full" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f5dab59c348b9b50cf7f261960a20e389feb2713636399cd9082cd4b536154" +dependencies = [ + "crossbeam-channel", + "file-id", + "log", + "notify", + "parking_lot", + "walkdir", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" diff --git a/examples/testbed/scripts/test.lua b/examples/testbed/scripts/test.lua index bd2bd06..ea5bdc6 100644 --- a/examples/testbed/scripts/test.lua +++ b/examples/testbed/scripts/test.lua @@ -16,7 +16,7 @@ function on_update() --print("Lua's update function was called") world:view(function (t) - print("Found entity at " .. tostring(t)) + --print("Found entity at a really cool place: " .. tostring(t)) t.translation = t.translation + Vec3.new(0, 0.0008, 0) return t diff --git a/examples/testbed/src/main.rs b/examples/testbed/src/main.rs index 66ecc00..0dabc2f 100644 --- a/examples/testbed/src/main.rs +++ b/examples/testbed/src/main.rs @@ -306,6 +306,7 @@ async fn main() { let world = game.world(); let mut res_man = world.get_resource_mut::(); let script = res_man.request::("scripts/test.lua").unwrap(); + res_man.watch("scripts/test.lua", false).unwrap(); drop(res_man); let script = Script::new("test.lua", script); diff --git a/lyra-game/src/render/light/mod.rs b/lyra-game/src/render/light/mod.rs index fb4728b..adbe0b5 100644 --- a/lyra-game/src/render/light/mod.rs +++ b/lyra-game/src/render/light/mod.rs @@ -8,8 +8,6 @@ pub use spotlight::*; use std::{collections::{VecDeque, HashMap}, marker::PhantomData}; -use tracing::debug; - use std::mem; use crate::math::Transform; @@ -174,7 +172,7 @@ impl LightUniformBuffers { if !self.point_lights.has_light(entity) || light_epoch == world_tick || transform_epoch == world_tick { let uniform = PointLightUniform::from_bundle(&point_light, &transform); self.point_lights.update_or_add(&mut self.lights_uniform.point_lights, entity, uniform); - debug!("Updated point light"); + //debug!("Updated point light"); } } diff --git a/lyra-resource/Cargo.toml b/lyra-resource/Cargo.toml index ffdef11..0f6adf3 100644 --- a/lyra-resource/Cargo.toml +++ b/lyra-resource/Cargo.toml @@ -17,6 +17,7 @@ image = "0.24.7" infer = { version = "0.15.0", default-features = false } mime = "0.3.17" notify = "6.1.1" +notify-debouncer-full = "0.3.1" #notify = { version = "6.1.1", default-features = false, features = [ "fsevent-sys", "macos_fsevent" ]} # disables crossbeam-channel percent-encoding = "2.3.0" thiserror = "1.0.48" diff --git a/lyra-resource/src/lib.rs b/lyra-resource/src/lib.rs index f78b8ab..65fe735 100644 --- a/lyra-resource/src/lib.rs +++ b/lyra-resource/src/lib.rs @@ -16,4 +16,7 @@ pub use model::*; pub mod material; pub use material::*; -pub(crate) mod util; \ No newline at end of file +pub(crate) mod util; + +pub use crossbeam::channel as channel; +pub use notify; \ No newline at end of file diff --git a/lyra-resource/src/resource.rs b/lyra-resource/src/resource.rs index d36c82f..ef85222 100644 --- a/lyra-resource/src/resource.rs +++ b/lyra-resource/src/resource.rs @@ -27,6 +27,7 @@ pub(crate) struct Resource { pub(crate) version: usize, pub(crate) state: ResourceState, uuid: Uuid, + pub(crate) is_watched: bool, } /// A handle to a resource. @@ -54,6 +55,7 @@ impl ResHandle { version: 0, state: ResourceState::Ready, uuid: Uuid::new_v4(), + is_watched: false, }; Self { @@ -61,7 +63,13 @@ impl ResHandle { } } - /// Returns a boolean indicated if this resource is loaded + /// Returns a boolean indicating if this resource's path is being watched. + pub fn is_watched(&self) -> bool { + let d = self.data.read().expect("Resource mutex was poisoned!"); + d.is_watched + } + + /// Returns a boolean indicating if this resource is loaded pub fn is_loaded(&self) -> bool { let d = self.data.read().expect("Resource mutex was poisoned!"); d.state == ResourceState::Ready diff --git a/lyra-resource/src/resource_manager.rs b/lyra-resource/src/resource_manager.rs index 593c0d4..dcf7e8b 100644 --- a/lyra-resource/src/resource_manager.rs +++ b/lyra-resource/src/resource_manager.rs @@ -1,7 +1,8 @@ -use std::{sync::{Arc, RwLock}, collections::{HashMap, VecDeque}, any::Any, thread::{Thread, JoinHandle}, rc::Rc, path::Path, ops::Deref}; +use std::{sync::{Arc, RwLock}, collections::HashMap, any::Any, path::Path, time::Duration}; -use crossbeam::channel::{Receiver, Sender}; +use crossbeam::channel::Receiver; use notify::{Watcher, RecommendedWatcher}; +use notify_debouncer_full::{DebouncedEvent, FileIdMap}; use thiserror::Error; use crate::{resource::ResHandle, loader::{ResourceLoader, LoaderError, image::ImageLoader, model::ModelLoader}}; @@ -11,10 +12,11 @@ pub trait ResourceStorage: Send + Sync + Any + 'static { fn as_any_mut(&mut self) -> &mut dyn Any; fn as_arc_any(self: Arc) -> Arc; fn as_box_any(self: Box) -> Box; + fn set_watched(&self, watched: bool); } /// Implements this trait for anything that fits the type bounds -impl ResourceStorage for T { +impl ResourceStorage for ResHandle { fn as_any(&self) -> &dyn Any { self } @@ -30,6 +32,11 @@ impl ResourceStorage for T { fn as_box_any(self: Box) -> Box { self } + + fn set_watched(&self, watched: bool) { + let mut w = self.data.write().unwrap(); + w.is_watched = watched; + } } #[derive(Error, Debug)] @@ -55,8 +62,8 @@ impl From for RequestError { /// A struct that pub struct ResourceWatcher { - watcher: Arc>, - events_recv: Receiver>, + debouncer: Arc>>, + events_recv: Receiver, Vec>>, } pub struct ResourceManager { @@ -176,32 +183,40 @@ impl ResourceManager { } /// Start watching a path for changes. Returns a mspc channel that will send events - pub fn watch(&mut self, path: &str, recursive: bool) -> notify::Result>> { + pub fn watch(&mut self, path: &str, recursive: bool) -> notify::Result, Vec>>> { let (send, recv) = crossbeam::channel::bounded(15); - let mut watcher = RecommendedWatcher::new(send, notify::Config::default())?; + let mut watcher = notify_debouncer_full::new_debouncer(Duration::from_millis(1000), None, send)?; let recurse_mode = match recursive { true => notify::RecursiveMode::Recursive, false => notify::RecursiveMode::NonRecursive, }; - watcher.watch(path.as_ref(), recurse_mode)?; + watcher.watcher().watch(path.as_ref(), recurse_mode)?; let watcher = Arc::new(RwLock::new(watcher)); let watcher = ResourceWatcher { - watcher, + debouncer: watcher, events_recv: recv.clone(), }; self.watchers.insert(path.to_string(), watcher); + let res = self.resources.get(&path.to_string()) + .expect("The path that was watched has not been loaded as a resource yet"); + res.set_watched(true); + Ok(recv) } /// Stops watching a path pub fn stop_watching(&mut self, path: &str) -> notify::Result<()> { if let Some(watcher) = self.watchers.get(path) { - let mut watcher = watcher.watcher.write().unwrap(); - watcher.unwatch(Path::new(path))?; + let mut watcher = watcher.debouncer.write().unwrap(); + watcher.watcher().unwatch(Path::new(path))?; + + // unwrap is safe since only loaded resources can be watched + let res = self.resources.get(&path.to_string()).unwrap(); + res.set_watched(false); } Ok(()) @@ -209,7 +224,7 @@ impl ResourceManager { /// Returns a mspc receiver for watcher events of a specific path. The path must already /// be watched with [`ResourceManager::watch`] for this to return `Some`. - pub fn watcher_event_recv(&self, path: &str) -> Option>> { + pub fn watcher_event_recv(&self, path: &str) -> Option, Vec>>> { self.watchers.get(&path.to_string()) .map(|w| w.events_recv.clone()) } @@ -242,8 +257,9 @@ impl ResourceManager { let res_lock = &resource.data; let mut res_lock = res_lock.write().unwrap(); let version = res_lock.version; - // safe since loaded was JUST loaded, it will be unlocked and not poisoned - *res_lock = loaded; + + res_lock.data = loaded.data; + res_lock.state = loaded.state; res_lock.version = version + 1; } @@ -336,9 +352,6 @@ mod tests { std::fs::write(image_path, image_bytes).unwrap(); - println!("Event kind: {:?}", event.kind); - - // for some reason, - assert!(event.kind.is_remove() || event.kind.is_modify()); + assert!(event.iter().any(|ev| ev.kind.is_remove() || ev.kind.is_modify())); } } \ No newline at end of file diff --git a/lyra-scripting/src/host.rs b/lyra-scripting/src/host.rs index b9cb4b9..e25a56d 100644 --- a/lyra-scripting/src/host.rs +++ b/lyra-scripting/src/host.rs @@ -43,7 +43,9 @@ pub trait ScriptApiProvider { type ScriptContext; /// Prepare the ECS world for this api. Things like registering types with the type registry happen here. - fn prepare_world(&mut self, world: &mut World) {} + fn prepare_world(&mut self, world: &mut World) { + let _ = world; // remove compiler warning + } /// Exposes an API in the provided script context. fn expose_api(&mut self, ctx: &mut Self::ScriptContext) -> Result<(), ScriptError>; @@ -120,4 +122,12 @@ impl ScriptContexts { pub fn has_context(&self, script_id: u64) -> bool { self.contexts.contains_key(&script_id) } + + pub fn remove_context(&mut self, script_id: u64) -> Option { + self.contexts.remove(&script_id) + } + + pub fn len(&self) -> usize { + self.contexts.len() + } } \ No newline at end of file diff --git a/lyra-scripting/src/lua/mod.rs b/lyra-scripting/src/lua/mod.rs index d159a38..7c9f6af 100644 --- a/lyra-scripting/src/lua/mod.rs +++ b/lyra-scripting/src/lua/mod.rs @@ -1,8 +1,9 @@ pub mod dynamic_iter; +use anyhow::anyhow; pub use dynamic_iter::*; pub mod world; -use lyra_game::{plugin::Plugin, game::GameStages}; +use lyra_game::{game::GameStages, plugin::Plugin}; use lyra_resource::ResourceManager; use tracing::{debug, error, trace}; pub use world::*; @@ -19,34 +20,49 @@ pub mod wrappers; #[cfg(test)] mod test; -use std::{ptr::NonNull, sync::Mutex, any::TypeId}; +use std::{any::TypeId, ptr::NonNull, sync::Mutex}; -use lyra_ecs::{DynamicBundle, World, query::{ResMut, View, Entities}}; -use lyra_reflect::{Reflect, FromType, RegisteredType, TypeRegistry}; +use lyra_ecs::{ + query::{Entities, ResMut, View}, + DynamicBundle, World, +}; +use lyra_reflect::{FromType, Reflect, TypeRegistry}; -use mlua::{Lua, AnyUserDataExt}; +use mlua::{AnyUserDataExt, Lua}; pub type LuaContext = Mutex; pub const FN_NAME_INTERNAL_REFLECT_TYPE: &str = "__lyra_internal_reflect_type"; pub const FN_NAME_INTERNAL_REFLECT: &str = "__lyra_internal_reflect"; -use crate::{ScriptBorrow, ScriptDynamicBundle, ScriptApiProviders, ScriptContexts, ScriptWorldPtr, ScriptList, ScriptData, ScriptHost, ScriptError, GameScriptExt}; +use crate::{ + GameScriptExt, ScriptApiProviders, ScriptBorrow, ScriptContexts, ScriptData, + ScriptDynamicBundle, ScriptError, ScriptHost, ScriptList, ScriptWorldPtr, +}; -use self::providers::{UtilityApiProvider, LyraMathApiProvider, LyraEcsApiProvider}; +use self::providers::{LyraEcsApiProvider, LyraMathApiProvider, UtilityApiProvider}; pub trait RegisterLuaType { /// Register a lua type that **is not wrapped**. - fn register_lua_type<'a, T: Reflect + LuaProxy + Clone + mlua::FromLua<'a> + mlua::UserData>(&mut self); + fn register_lua_type<'a, T: Reflect + LuaProxy + Clone + mlua::FromLua<'a> + mlua::UserData>( + &mut self, + ); /// Registers a wrapped lua type. /// You provide the wrapper as `W`, and the type that the wrapper wraps, as `T`. - fn register_lua_wrapper<'a, W: Reflect + LuaProxy + LuaWrapper + Clone + mlua::FromLua<'a> + mlua::UserData>(&mut self); + fn register_lua_wrapper< + 'a, + W: Reflect + LuaProxy + LuaWrapper + Clone + mlua::FromLua<'a> + mlua::UserData, + >( + &mut self, + ); } impl RegisterLuaType for World { - fn register_lua_type<'a, T: Reflect + LuaProxy + Clone + mlua::FromLua<'a> + mlua::UserData>(&mut self) { + fn register_lua_type<'a, T: Reflect + LuaProxy + Clone + mlua::FromLua<'a> + mlua::UserData>( + &mut self, + ) { let mut registry = self.get_resource_mut::(); - + let type_id = TypeId::of::(); let reg_type = registry.get_type_or_default(type_id); @@ -54,9 +70,14 @@ impl RegisterLuaType for World { //reg_type.add_data(>::from_type()); } - fn register_lua_wrapper<'a, W: Reflect + LuaProxy + LuaWrapper + Clone + mlua::FromLua<'a> + mlua::UserData>(&mut self) { + fn register_lua_wrapper< + 'a, + W: Reflect + LuaProxy + LuaWrapper + Clone + mlua::FromLua<'a> + mlua::UserData, + >( + &mut self, + ) { let mut registry = self.get_resource_mut::(); - + let reg_type = registry.get_type_or_default(W::wrapped_type_id()); reg_type.add_data(>::from_type()); } @@ -84,17 +105,31 @@ pub trait LuaWrapper { } pub trait LuaProxy { - fn as_lua_value<'lua>(lua: &'lua mlua::Lua, this: &dyn Reflect) -> mlua::Result>; - fn apply(lua: &mlua::Lua, this: &mut dyn Reflect, apply: &mlua::AnyUserData) -> mlua::Result<()>; + fn as_lua_value<'lua>( + lua: &'lua mlua::Lua, + this: &dyn Reflect, + ) -> mlua::Result>; + fn apply( + lua: &mlua::Lua, + this: &mut dyn Reflect, + apply: &mlua::AnyUserData, + ) -> mlua::Result<()>; } impl<'a, T: Reflect + Clone + mlua::FromLua<'a> + mlua::UserData> LuaProxy for T { - fn as_lua_value<'lua>(lua: &'lua mlua::Lua, this: &dyn Reflect) -> mlua::Result> { + fn as_lua_value<'lua>( + lua: &'lua mlua::Lua, + this: &dyn Reflect, + ) -> mlua::Result> { let this = this.as_any().downcast_ref::().unwrap(); lua.create_userdata(this.clone()) } - fn apply(_lua: &mlua::Lua, this: &mut dyn Reflect, apply: &mlua::AnyUserData) -> mlua::Result<()> { + fn apply( + _lua: &mlua::Lua, + this: &mut dyn Reflect, + apply: &mlua::AnyUserData, + ) -> mlua::Result<()> { let this = this.as_any_mut().downcast_mut::().unwrap(); let apply = apply.borrow::()?; @@ -106,11 +141,18 @@ impl<'a, T: Reflect + Clone + mlua::FromLua<'a> + mlua::UserData> LuaProxy for T #[derive(Clone)] pub struct ReflectLuaProxy { - fn_as_uservalue: for<'a> fn(lua: &'a Lua, this_ptr: NonNull) -> mlua::Result>, - fn_apply: for<'a> fn(lua: &'a Lua, this_ptr: NonNull, apply: &'a mlua::AnyUserData<'a>) -> mlua::Result<()>, + fn_as_uservalue: + for<'a> fn(lua: &'a Lua, this_ptr: NonNull) -> mlua::Result>, + fn_apply: for<'a> fn( + lua: &'a Lua, + this_ptr: NonNull, + apply: &'a mlua::AnyUserData<'a>, + ) -> mlua::Result<()>, } -impl<'a, T: Reflect + LuaProxy + Clone + mlua::FromLua<'a> + mlua::UserData> FromType for ReflectLuaProxy { +impl<'a, T: Reflect + LuaProxy + Clone + mlua::FromLua<'a> + mlua::UserData> FromType + for ReflectLuaProxy +{ fn from_type() -> Self { Self { fn_as_uservalue: |lua, this| -> mlua::Result { @@ -120,7 +162,7 @@ impl<'a, T: Reflect + LuaProxy + Clone + mlua::FromLua<'a> + mlua::UserData> Fro fn_apply: |lua, ptr, apply| { let this = unsafe { ptr.cast::().as_mut() }; ::apply(lua, this, apply) - } + }, } } } @@ -129,7 +171,11 @@ impl<'lua> mlua::FromLua<'lua> for ScriptDynamicBundle { fn from_lua(value: mlua::Value<'lua>, _lua: &'lua Lua) -> mlua::Result { match value { mlua::Value::UserData(ud) => Ok(ud.borrow::()?.clone()), - mlua::Value::Nil => Err(mlua::Error::FromLuaConversionError { from: "Nil", to: "DynamicBundle", message: Some("Value was nil".to_string()) }), + mlua::Value::Nil => Err(mlua::Error::FromLuaConversionError { + from: "Nil", + to: "DynamicBundle", + message: Some("Value was nil".to_string()), + }), _ => panic!(), } } @@ -137,9 +183,7 @@ impl<'lua> mlua::FromLua<'lua> for ScriptDynamicBundle { impl mlua::UserData for ScriptDynamicBundle { fn add_methods<'lua, M: mlua::prelude::LuaUserDataMethods<'lua, Self>>(methods: &mut M) { - methods.add_function("new", |_, ()| { - Ok(ScriptDynamicBundle(DynamicBundle::new())) - }); + methods.add_function("new", |_, ()| Ok(ScriptDynamicBundle(DynamicBundle::new()))); methods.add_method_mut("push", |_, this, (comp,): (mlua::AnyUserData,)| { let script_brw = comp.call_method::<_, ScriptBorrow>(FN_NAME_INTERNAL_REFLECT, ())?; @@ -154,44 +198,84 @@ impl mlua::UserData for ScriptDynamicBundle { } } -/// -/* fn lua_script_run_func(world: &mut World, function_name: &str) -> anyhow::Result<()> { - -} */ - /// A system that creates the script contexts in the world as new scripts are found -pub fn lua_scripts_create_contexts(mut host: ResMut, - mut contexts: ResMut>, - mut providers: ResMut>, - view: View<(Entities, &ScriptList)>, - ) -> anyhow::Result<()> { - +pub fn lua_scripts_create_contexts( + mut host: ResMut, + mut contexts: ResMut>, + mut providers: ResMut>, + view: View<(Entities, &ScriptList)>, +) -> anyhow::Result<()> { for (en, scripts) in view.into_iter() { for script in scripts.iter() { - let script_data = ScriptData { - name: script.name().to_string(), - script_id: script.id(), - entity: en, - }; - if !contexts.has_context(script.id()) { + let script_data = ScriptData { + name: script.name().to_string(), + script_id: script.id(), + entity: en, + }; + if let Some(script_res) = &script.res_handle().try_data_ref() { debug!("Loading script '{}'...", script.name()); - let mut script_ctx = host.load_script(&script_res.bytes, &script_data, &mut providers).unwrap(); - debug!("Finished loading script '{}'", script.name()); + let mut script_ctx = + host.load_script(&script_res.bytes, &script_data, &mut providers)?; + trace!("Finished loading script '{}'", script.name()); debug!("Setting up script '{}'...", script.name()); - host.setup_script(&script_data, &mut script_ctx, &mut providers).unwrap(); - debug!("Finished setting up script '{}'...", script.name()); + host.setup_script(&script_data, &mut script_ctx, &mut providers)?; + trace!("Finished setting up script '{}'...", script.name()); contexts.add_context(script.id(), script_ctx); } else { - debug!("Script '{}' is not yet loaded, skipping", script.name()); + trace!("Script '{}' is not loaded yet, skipping for now", script.name()); } } } } - + + Ok(()) +} + +/// A system that triggers a reload of watched script resources. +/// +/// Note: This only works if the script is watched. See [`lyra_resource::ResourceManager::watch`]. +pub fn lua_scripts_reload_system( + mut contexts: ResMut>, + mut resman: ResMut, + view: View<&ScriptList>, +) -> anyhow::Result<()> { + for scripts in view.into_iter() { + for script in scripts.iter() { + let handle = script.res_handle(); + if handle.is_watched() { + let handle_path = handle.path(); + let watch_recv = resman.watcher_event_recv(&handle_path).unwrap(); + + match watch_recv.try_recv() { + Ok(ev) => { + let evs = + ev.map_err(|e| anyhow!("Script watcher ran into errors: {:?}", e))?; + + if evs.iter().any(|ev| ev.event.kind.is_modify()) { + debug!( + "Detected change of '{}' script, triggering reload", + handle_path + ); + + contexts.remove_context(script.id()).unwrap(); + resman.reload(handle)?; + } + } + Err(e) => match e { + lyra_resource::channel::TryRecvError::Empty => {} + lyra_resource::channel::TryRecvError::Disconnected => { + resman.stop_watching(&handle_path).unwrap(); + } + }, + } + } + } + } + Ok(()) } @@ -210,20 +294,31 @@ fn lua_call_script_function(world: &mut World, stage_name: &str) -> anyhow::Resu }; if let Some(ctx) = contexts.get_context_mut(script.id()) { - trace!("Running '{}' function in script '{}'", stage_name, script.name()); - match host.call_script(world_ptr.clone(), &script_data, ctx, &mut providers, stage_name) { - Ok(()) => {}, + trace!( + "Running '{}' function in script '{}'", + stage_name, + script.name() + ); + + match host.call_script( + world_ptr.clone(), + &script_data, + ctx, + &mut providers, + stage_name, + ) { + Ok(()) => {} Err(e) => match e { ScriptError::MluaError(m) => { error!("Script '{}' ran into an error: {}", script.name(), m); - }, + } ScriptError::Other(_) => return Err(e.into()), }, } } } } - + Ok(()) } @@ -270,7 +365,8 @@ impl Plugin for LuaScriptingPlugin { world.add_resource_default::>(); world.add_resource_default::>(); - let mut loader = world.try_get_resource_mut::() + let mut loader = world + .try_get_resource_mut::() .expect("Add 'ResourceManager' to the world before trying to add this plugin"); loader.register_loader::(); drop(loader); @@ -279,15 +375,50 @@ impl Plugin for LuaScriptingPlugin { game.add_script_api_provider::(LyraEcsApiProvider); game.add_script_api_provider::(LyraMathApiProvider); - game - .add_system_to_stage(GameStages::First, "lua_create_contexts", lua_scripts_create_contexts, &[]) - .add_system_to_stage(GameStages::First, "lua_first_stage", lua_script_first_stage_system, &["lua_create_contexts"]) - // cannot depend on 'lua_create_contexts' since it will cause a panic. - // the staged executor separates the executor of a single stage so this system - // cannot depend on the other one. - .add_system_to_stage(GameStages::PreUpdate, "lua_pre_update", lua_script_pre_update_stage_system, &[]) - .add_system_to_stage(GameStages::Update, "lua_update", lua_script_update_stage_system, &[]) - .add_system_to_stage(GameStages::PostUpdate, "lua_post_update", lua_script_post_update_stage_system, &[]) - .add_system_to_stage(GameStages::Last, "lua_last_stage", lua_script_last_stage_system, &[]); + game.add_system_to_stage( + GameStages::First, + "lua_create_contexts", + lua_scripts_create_contexts, + &[], + ) + .add_system_to_stage( + GameStages::First, + "lua_reload_scripts", + lua_scripts_reload_system, + &["lua_create_contexts"], + ) + .add_system_to_stage( + GameStages::First, + "lua_first_stage", + lua_script_first_stage_system, + &["lua_reload_scripts"], + ) + // cannot depend on 'lua_create_contexts' since it will cause a panic. + // the staged executor separates the executor of a single stage so this system + // cannot depend on the other one. + .add_system_to_stage( + GameStages::PreUpdate, + "lua_pre_update", + lua_script_pre_update_stage_system, + &[], + ) + .add_system_to_stage( + GameStages::Update, + "lua_update", + lua_script_update_stage_system, + &[], + ) + .add_system_to_stage( + GameStages::PostUpdate, + "lua_post_update", + lua_script_post_update_stage_system, + &[], + ) + .add_system_to_stage( + GameStages::Last, + "lua_last_stage", + lua_script_last_stage_system, + &[], + ); } -} \ No newline at end of file +}