From e49d69dbc1d73c4331529f22e6a2abd9efb798fc Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Sun, 7 Jan 2024 00:57:19 -0500 Subject: [PATCH] scripting: update scripts on all game stages, create utility api provider --- examples/testbed/scripts/test.lua | 28 ++++-- lyra-scripting/src/host.rs | 21 +++-- lyra-scripting/src/lua/mod.rs | 117 ++++++++++++++++++++----- lyra-scripting/src/lua/modules/mod.rs | 120 ++++++++++++++++++++++++++ lyra-scripting/src/lua/script.rs | 18 ++-- lyra-scripting/src/lua/test.rs | 18 ++-- lyra-scripting/src/lua/world.rs | 8 +- lyra-scripting/src/script.rs | 6 +- lyra-scripting/src/world.rs | 8 ++ 9 files changed, 292 insertions(+), 52 deletions(-) create mode 100644 lyra-scripting/src/lua/modules/mod.rs diff --git a/examples/testbed/scripts/test.lua b/examples/testbed/scripts/test.lua index 102e168..de8a543 100644 --- a/examples/testbed/scripts/test.lua +++ b/examples/testbed/scripts/test.lua @@ -1,7 +1,25 @@ print("Hello World") - -function update() - print("updated") - --printf("I love to eat formatted {}!", "food") - --printf("World is {}", world) + +function on_init() + print("Lua script was initialized!") +end + +function on_first() + print("Lua's first function was called") +end + +function on_pre_update() + print("Lua's pre-update function was called") +end + +function on_update() + print("Lua's update function was called") +end + +function on_post_update() + print("Lua's post-update function was called") +end + +function on_last() + print("Lua's last function was called") end \ No newline at end of file diff --git a/lyra-scripting/src/host.rs b/lyra-scripting/src/host.rs index f422725..b19e316 100644 --- a/lyra-scripting/src/host.rs +++ b/lyra-scripting/src/host.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use lyra_ecs::ResourceObject; +use lyra_ecs::{ResourceObject, Entity}; use crate::ScriptWorldPtr; @@ -29,8 +29,12 @@ impl From for ScriptError { #[derive(Clone, Hash, PartialEq, Eq)] pub struct ScriptData { + /// The script id pub script_id: u64, + /// The name of the script pub name: String, + /// The entity that this script exists on + pub entity: Entity, } /// Provides an API to a scripting context. @@ -41,9 +45,7 @@ pub trait ScriptApiProvider { /// Exposes an API in the provided script context. fn expose_api(&mut self, ctx: &mut Self::ScriptContext) -> Result<(), ScriptError>; - /// Create a script in the script host. - /// - /// This only creates the script for the host, it does not setup the script for execution. See [`ScriptHostProvider::setup_script`]. + /// Setup a script right before its 'init' method is called. fn setup_script(&mut self, data: &ScriptData, ctx: &mut Self::ScriptContext) -> Result<(), ScriptError>; /// A function that is used to update a script's environment. @@ -74,13 +76,18 @@ pub trait ScriptHost: Default + ResourceObject { /// Loads a script and creates a context for it. /// /// Before the script is executed, the API providers are exposed to the script. - fn load_script(&mut self, script: &[u8], script_data: &ScriptData, providers: &mut crate::ScriptApiProviders) -> Result; + fn load_script(&mut self, script: &[u8], script_data: &ScriptData, + providers: &mut crate::ScriptApiProviders) + -> Result; /// Setup a script for execution. - fn setup_script(&mut self, script_data: &ScriptData, ctx: &mut Self::ScriptContext, providers: &mut ScriptApiProviders) -> Result<(), ScriptError>; + fn setup_script(&mut self, script_data: &ScriptData, ctx: &mut Self::ScriptContext, + providers: &mut ScriptApiProviders) -> Result<(), ScriptError>; /// Executes the update step for the script. - fn update_script(&mut self, world: ScriptWorldPtr, script_data: &crate::ScriptData, ctx: &mut Self::ScriptContext, providers: &mut crate::ScriptApiProviders) -> Result<(), ScriptError>; + fn call_script(&mut self, world: ScriptWorldPtr, script_data: &crate::ScriptData, + ctx: &mut Self::ScriptContext, providers: &mut crate::ScriptApiProviders, + function_name: &str) -> Result<(), ScriptError>; } #[derive(Default)] diff --git a/lyra-scripting/src/lua/mod.rs b/lyra-scripting/src/lua/mod.rs index 686baf2..ded46ce 100644 --- a/lyra-scripting/src/lua/mod.rs +++ b/lyra-scripting/src/lua/mod.rs @@ -2,7 +2,7 @@ pub mod dynamic_iter; pub use dynamic_iter::*; pub mod world; -use lyra_game::plugin::Plugin; +use lyra_game::{plugin::Plugin, game::GameStages}; use lyra_resource::ResourceManager; use tracing::{debug, error}; pub use world::*; @@ -13,20 +13,25 @@ pub use script::*; pub mod loader; pub use loader::*; +pub mod modules; +pub use modules::*; + #[cfg(test)] mod test; use std::{ptr::NonNull, sync::Mutex}; -use lyra_ecs::{DynamicBundle, World}; -use lyra_reflect::{Reflect, RegisteredType, FromType, AsRegisteredType}; +use lyra_ecs::{DynamicBundle, World, query::{ResMut, View, Entities}}; +use lyra_reflect::{Reflect, FromType}; use mlua::{Lua, AnyUserDataExt}; +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}; +use crate::{ScriptBorrow, ScriptDynamicBundle, ScriptApiProviders, ScriptContexts, ScriptWorldPtr, ScriptList, ScriptData, ScriptHost, ScriptError, GameScriptExt}; impl<'lua> mlua::FromLua<'lua> for ScriptBorrow { fn from_lua(value: mlua::Value<'lua>, _lua: &'lua Lua) -> mlua::Result { @@ -115,18 +120,24 @@ impl mlua::UserData for ScriptDynamicBundle { } } -/// A system that updates the scripts that exist in the world -pub fn lua_update_scripts(world: &mut World) -> anyhow::Result<()> { - let world_ptr = ScriptWorldPtr::from_ref(&world); - let mut host = world.get_resource_mut::(); - let mut contexts = world.get_resource_mut::>>(); - let mut providers = world.get_resource_mut::>(); +/// +/* fn lua_script_run_func(world: &mut World, function_name: &str) -> anyhow::Result<()> { - for scripts in world.view_iter::<&ScriptList>() { +} */ + +/// 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<()> { + + 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()) { @@ -144,18 +155,37 @@ pub fn lua_update_scripts(world: &mut World) -> anyhow::Result<()> { debug!("Script '{}' is not yet loaded, skipping", script.name()); } } + } + } + + Ok(()) +} - let ctx = contexts.get_context_mut(script.id()).unwrap(); - - debug!("Running 'update' function in script '{}'", script.name()); - match host.update_script(world_ptr.clone(), &script_data, ctx, &mut providers) { - Ok(()) => {}, - Err(e) => match e { - ScriptError::MluaError(m) => { - error!("Script '{}' ran into an error: {}", script.name(), m); +fn lua_call_script_function(world: &mut World, stage_name: &str) -> anyhow::Result<()> { + let world_ptr = ScriptWorldPtr::from_ref(&world); + let mut host = world.get_resource_mut::(); + let mut contexts = world.get_resource_mut::>(); + let mut providers = world.get_resource_mut::>(); + + for (en, scripts) in world.view_iter::<(Entities, &ScriptList)>() { + for script in scripts.iter() { + let script_data = ScriptData { + name: script.name().to_string(), + script_id: script.id(), + entity: en, + }; + + if let Some(ctx) = contexts.get_context_mut(script.id()) { + debug!("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()), }, - ScriptError::Other(_) => return Err(e.into()), - }, + } } } } @@ -163,6 +193,36 @@ pub fn lua_update_scripts(world: &mut World) -> anyhow::Result<()> { Ok(()) } +/// This system executes the 'on_update' function of lua scripts in the world. It is meant to run +/// during the 'GameStages::Update' stage. +pub fn lua_script_update_stage_system(world: &mut World) -> anyhow::Result<()> { + lua_call_script_function(world, "on_update") +} + +/// This system executes the 'on_pre_update' function of lua scripts in the world. It is meant to run +/// during the 'GameStages::PreUpdate' stage. +pub fn lua_script_pre_update_stage_system(world: &mut World) -> anyhow::Result<()> { + lua_call_script_function(world, "on_pre_update") +} + +/// This system executes the 'on_post_update' function of lua scripts in the world. It is meant to run +/// during the 'GameStages::PostUpdate' stage. +pub fn lua_script_post_update_stage_system(world: &mut World) -> anyhow::Result<()> { + lua_call_script_function(world, "on_post_update") +} + +/// This system executes the 'on_first' function of lua scripts in the world. It is meant to run +/// during the 'GameStages::First' stage. +pub fn lua_script_first_stage_system(world: &mut World) -> anyhow::Result<()> { + lua_call_script_function(world, "on_first") +} + +/// This system executes the 'on_last' function of lua scripts in the world. It is meant to run +/// during the 'GameStages::Last' stage. +pub fn lua_script_last_stage_system(world: &mut World) -> anyhow::Result<()> { + lua_call_script_function(world, "on_last") +} + #[derive(Default)] pub struct LuaScriptingPlugin; @@ -172,13 +232,24 @@ impl Plugin for LuaScriptingPlugin { world.add_resource_default::(); world.add_resource_default::>(); - world.add_resource_default::>>(); + world.add_resource_default::>(); 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); - game.with_system("lua_update", lua_update_scripts, &[]); + game.add_script_api_provider::(UtilityApiProvider); + + 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, &[]); } } \ No newline at end of file diff --git a/lyra-scripting/src/lua/modules/mod.rs b/lyra-scripting/src/lua/modules/mod.rs new file mode 100644 index 0000000..1cc50c5 --- /dev/null +++ b/lyra-scripting/src/lua/modules/mod.rs @@ -0,0 +1,120 @@ +use std::sync::{Mutex, Arc}; + +use crate::{ScriptApiProvider, ScriptData}; + +/// This Api provider provides some nice utility functions. +/// +/// Functions: +/// ```lua +/// ---@param str (string) A format string. +/// ---@param ... (any varargs) The variables to format into the string. These values must be +/// primitives, or if UserData, have the '__tostring' meta method +/// function printf(str, ...) +/// ``` +#[derive(Default)] +pub struct UtilityApiProvider; + +impl ScriptApiProvider for UtilityApiProvider { + type ScriptContext = Mutex; + + fn expose_api(&mut self, ctx: &mut Self::ScriptContext) -> Result<(), crate::ScriptError> { + let ctx = ctx.lock().unwrap(); + + fn printf(lua: &mlua::Lua, (mut text, formats): (String, mlua::Variadic)) -> mlua::Result<()> { + let mut formatted = String::new(); + let mut arg_num = 0; + + while let Some(start) = text.find("{}") { + let val_str = match formats.get(arg_num) { + Some(v) => match v { + mlua::Value::Nil => "nil".to_string(), + mlua::Value::Boolean(b) => b.to_string(), + mlua::Value::LightUserData(_) => { + return Err(mlua::Error::RuntimeError(format!("unable to get string representation of LightUserData"))); + }, + mlua::Value::Integer(i) => i.to_string(), + mlua::Value::Number(n) => n.to_string(), + mlua::Value::String(s) => s.to_str().unwrap().to_string(), + mlua::Value::Table(_) => { + return Err(mlua::Error::RuntimeError(format!("unable to get string representation of Table"))); + }, + mlua::Value::Function(_) => { + return Err(mlua::Error::RuntimeError(format!("unable to get string representation of Function"))); + }, + mlua::Value::Thread(_) => { + return Err(mlua::Error::RuntimeError(format!("unable to get string representation of Thread"))); + }, + mlua::Value::UserData(ud) => { + let metatable = ud.get_metatable()?; + if let Ok(tos) = metatable.get::(mlua::MetaMethod::ToString) { + tos.call::<_, String>((ud.clone(),))? + } else { + return Err(mlua::Error::RuntimeError(format!("UserData does not implement MetaMethod '__tostring'"))); + } + }, + mlua::Value::Error(e) => e.to_string(), + }, + None => { + let got_args = arg_num; + + // continue searching for {} to get the number of format spots for the error message. + while let Some(start) = text.find("{}") { + text = text[start + 2..].to_string(); + arg_num += 1; + } + + return Err(mlua::Error::BadArgument { + to: Some("printf".to_string()), + pos: 2, + name: Some("...".to_string()), + cause: Arc::new(mlua::Error::RuntimeError(format!("not enough args \ + given for the amount of format areas in the string. Expected {}, \ + got {}.", arg_num, got_args))) + }) + }, + }; + + formatted = format!("{}{}{}", formatted, &text[0..start], val_str); + + text = text[start + 2..].to_string(); + + arg_num += 1; + } + + if arg_num < formats.len() { + return Err(mlua::Error::BadArgument { + to: Some("printf".to_string()), + pos: 2, + name: Some("...".to_string()), + cause: Arc::new(mlua::Error::RuntimeError(format!("got more args \ + than format areas in the string. Expected {}, got {}.", formats.len(), arg_num))) + }) + } + + formatted = format!("{}{}", formatted, text); + + lua.globals() + .get::<_, mlua::Function>("print") + .unwrap() + .call::<_, ()>(formatted) + .unwrap(); + + Ok(()) + } + + let printf_func = ctx.create_function(printf).unwrap(); + + let globals = ctx.globals(); + globals.set("printf", printf_func).unwrap(); + + Ok(()) + } + + fn setup_script(&mut self, _data: &ScriptData, _ctx: &mut Self::ScriptContext) -> Result<(), crate::ScriptError> { + Ok(()) + } + + fn update_script_environment(&mut self, _world: crate::ScriptWorldPtr, _data: &ScriptData, _ctx: &mut Self::ScriptContext) -> Result<(), crate::ScriptError> { + Ok(()) + } +} \ No newline at end of file diff --git a/lyra-scripting/src/lua/script.rs b/lyra-scripting/src/lua/script.rs index e74efdd..457042f 100644 --- a/lyra-scripting/src/lua/script.rs +++ b/lyra-scripting/src/lua/script.rs @@ -1,8 +1,9 @@ use std::sync::Mutex; +use mlua::IntoLua; use tracing::debug; -use crate::{ScriptHost, ScriptError, ScriptWorldPtr}; +use crate::{ScriptHost, ScriptError, ScriptWorldPtr, ScriptEntity}; #[derive(Default)] pub struct LuaHost; @@ -53,7 +54,7 @@ impl ScriptHost for LuaHost { } let ctx = ctx.lock().expect("Failure to get Lua ScriptContext"); - try_call_lua_function(&ctx, "init")?; + try_call_lua_function(&ctx, "on_init")?; Ok(()) } @@ -61,13 +62,20 @@ impl ScriptHost for LuaHost { /// Runs the update step of the lua script. /// /// It looks for an `update` function with zero parameters in [`the ScriptContext`] and executes it. - fn update_script(&mut self, world: ScriptWorldPtr, script_data: &crate::ScriptData, ctx: &mut Self::ScriptContext, providers: &mut crate::ScriptApiProviders) -> Result<(), ScriptError> { + fn call_script(&mut self, world: ScriptWorldPtr, script_data: &crate::ScriptData, + ctx: &mut Self::ScriptContext, providers: &mut crate::ScriptApiProviders, + function_name: &str) -> Result<(), ScriptError> { for provider in providers.apis.iter_mut() { provider.update_script_environment(world.clone(), script_data, ctx)?; } - + let ctx = ctx.lock().expect("Failure to get Lua ScriptContext"); - try_call_lua_function(&ctx, "update")?; + + let globals = ctx.globals(); + globals.set("world", world.into_lua(&ctx)?)?; + globals.set("entity", ScriptEntity(script_data.entity).into_lua(&ctx)?)?; + + try_call_lua_function(&ctx, function_name)?; Ok(()) } diff --git a/lyra-scripting/src/lua/test.rs b/lyra-scripting/src/lua/test.rs index 7c1f87c..79f72d1 100644 --- a/lyra-scripting/src/lua/test.rs +++ b/lyra-scripting/src/lua/test.rs @@ -1,16 +1,13 @@ -use std::{sync::{Mutex, Arc}, alloc::Layout, ptr::NonNull}; +use std::{sync::{Mutex, Arc}, ptr::NonNull}; -use lyra_ecs::{World, query::{Res, View, Entities, ResMut}, Component, system::{GraphExecutor, IntoSystem}}; +use lyra_ecs::{World, system::{GraphExecutor, IntoSystem}}; use lyra_resource::ResourceManager; use mlua::{IntoLua, AnyUserDataExt}; -use tracing::{debug, error}; -use tracing_subscriber::{layer::SubscriberExt, fmt, filter, util::SubscriberInitExt}; +use tracing_subscriber::{layer::SubscriberExt, fmt, util::SubscriberInitExt}; -use crate::{ScriptHost, ScriptData, ScriptApiProvider, ScriptApiProviders, ScriptError, ScriptWorldPtr, ScriptList, Script, ScriptContexts}; +use crate::{ScriptData, ScriptApiProvider, ScriptApiProviders, ScriptError, ScriptList, Script, ScriptContexts}; -use super::{LuaHost, LuaLoader, LuaScript, lua_update_scripts}; - -use crate::lyra_engine; +use super::{LuaHost, LuaLoader, LuaScript, lua_script_update_stage_system, lua_scripts_create_contexts, LuaContext}; fn enable_tracing() { tracing_subscriber::registry() @@ -148,7 +145,7 @@ pub fn lua_print() { world.add_resource(host); world.add_resource(providers); - world.add_resource(ScriptContexts::>::default()); + world.add_resource(ScriptContexts::::default()); let mut res_loader = ResourceManager::new(); res_loader.register_loader::(); @@ -174,6 +171,7 @@ pub fn lua_print() { world.spawn((scripts,)); let mut exec = GraphExecutor::new(); - exec.insert_system("lua_update_scripts", lua_update_scripts.into_system(), &[]); + exec.insert_system("lua_create_contexts", lua_scripts_create_contexts.into_system(), &[]); + exec.insert_system("lua_update_scripts", lua_script_update_stage_system.into_system(), &["lua_create_contexts"]); exec.execute(NonNull::from(&world), true).unwrap(); } \ No newline at end of file diff --git a/lyra-scripting/src/lua/world.rs b/lyra-scripting/src/lua/world.rs index a6c6552..d1521ca 100644 --- a/lyra-scripting/src/lua/world.rs +++ b/lyra-scripting/src/lua/world.rs @@ -16,7 +16,13 @@ impl<'lua> mlua::FromLua<'lua> for ScriptEntity { } } -impl mlua::UserData for ScriptEntity {} +impl mlua::UserData for ScriptEntity { + fn add_methods<'lua, M: mlua::prelude::LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_meta_method(mlua::MetaMethod::ToString, |_, this, ()| { + Ok(format!("{:?}", this.0)) + }) + } +} #[derive(thiserror::Error, Debug, Clone)] pub enum WorldError { diff --git a/lyra-scripting/src/script.rs b/lyra-scripting/src/script.rs index d93877b..0327834 100644 --- a/lyra-scripting/src/script.rs +++ b/lyra-scripting/src/script.rs @@ -1,8 +1,12 @@ +use std::sync::atomic::{AtomicU64, Ordering}; + use lyra_ecs::Component; use lyra_resource::ResHandle; use crate::lyra_engine; +static SCRIPT_ID_COUNTER: AtomicU64 = AtomicU64::new(0); + #[derive(Clone)] pub struct Script { res: ResHandle, @@ -15,7 +19,7 @@ impl Script { Self { res: script, name: name.to_string(), - id: 0 // TODO: make a counter + id: SCRIPT_ID_COUNTER.fetch_add(1, Ordering::AcqRel) } } diff --git a/lyra-scripting/src/world.rs b/lyra-scripting/src/world.rs index a4e1134..c2b2a1f 100644 --- a/lyra-scripting/src/world.rs +++ b/lyra-scripting/src/world.rs @@ -5,6 +5,14 @@ use lyra_ecs::{world::World, Entity}; #[derive(Clone)] pub struct ScriptEntity(pub Entity); +impl std::ops::Deref for ScriptEntity { + type Target = Entity; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + #[derive(Clone)] pub struct ScriptWorldPtr { pub inner: NonNull,