scripting: update scripts on all game stages, create utility api provider
ci/woodpecker/push/debug Pipeline failed Details

This commit is contained in:
SeanOMik 2024-01-07 00:57:19 -05:00
parent 13ad671a55
commit e49d69dbc1
Signed by: SeanOMik
GPG Key ID: FEC9E2FC15235964
9 changed files with 292 additions and 52 deletions

View File

@ -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

View File

@ -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<anyhow::Error> 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<Self>) -> Result<Self::ScriptContext, ScriptError>;
fn load_script(&mut self, script: &[u8], script_data: &ScriptData,
providers: &mut crate::ScriptApiProviders<Self>)
-> Result<Self::ScriptContext, ScriptError>;
/// Setup a script for execution.
fn setup_script(&mut self, script_data: &ScriptData, ctx: &mut Self::ScriptContext, providers: &mut ScriptApiProviders<Self>) -> Result<(), ScriptError>;
fn setup_script(&mut self, script_data: &ScriptData, ctx: &mut Self::ScriptContext,
providers: &mut ScriptApiProviders<Self>) -> 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<Self>) -> Result<(), ScriptError>;
fn call_script(&mut self, world: ScriptWorldPtr, script_data: &crate::ScriptData,
ctx: &mut Self::ScriptContext, providers: &mut crate::ScriptApiProviders<Self>,
function_name: &str) -> Result<(), ScriptError>;
}
#[derive(Default)]

View File

@ -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<mlua::Lua>;
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<Self> {
@ -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::<LuaHost>();
let mut contexts = world.get_resource_mut::<ScriptContexts<Mutex<mlua::Lua>>>();
let mut providers = world.get_resource_mut::<ScriptApiProviders<LuaHost>>();
///
/* fn lua_script_run_func(world: &mut World, function_name: &str) -> anyhow::Result<()> {
for scripts in world.view_iter::<&ScriptList<LuaScript>>() {
} */
/// A system that creates the script contexts in the world as new scripts are found
pub fn lua_scripts_create_contexts(mut host: ResMut<LuaHost>,
mut contexts: ResMut<ScriptContexts<LuaContext>>,
mut providers: ResMut<ScriptApiProviders<LuaHost>>,
view: View<(Entities, &ScriptList<LuaScript>)>,
) -> 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::<LuaHost>();
let mut contexts = world.get_resource_mut::<ScriptContexts<LuaContext>>();
let mut providers = world.get_resource_mut::<ScriptApiProviders<LuaHost>>();
for (en, scripts) in world.view_iter::<(Entities, &ScriptList<LuaScript>)>() {
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::<LuaHost>();
world.add_resource_default::<ScriptApiProviders<LuaHost>>();
world.add_resource_default::<ScriptContexts<Mutex<mlua::Lua>>>();
world.add_resource_default::<ScriptContexts<LuaContext>>();
let mut loader = world.try_get_resource_mut::<ResourceManager>()
.expect("Add 'ResourceManager' to the world before trying to add this plugin");
loader.register_loader::<LuaLoader>();
drop(loader);
game.with_system("lua_update", lua_update_scripts, &[]);
game.add_script_api_provider::<LuaHost, _>(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, &[]);
}
}

View File

@ -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<mlua::Lua>;
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::Value>)) -> 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::Function>(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(())
}
}

View File

@ -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<Self>) -> Result<(), ScriptError> {
fn call_script(&mut self, world: ScriptWorldPtr, script_data: &crate::ScriptData,
ctx: &mut Self::ScriptContext, providers: &mut crate::ScriptApiProviders<Self>,
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(())
}

View File

@ -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::<Mutex<mlua::Lua>>::default());
world.add_resource(ScriptContexts::<LuaContext>::default());
let mut res_loader = ResourceManager::new();
res_loader.register_loader::<LuaLoader>();
@ -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();
}

View File

@ -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 {

View File

@ -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<T> {
res: ResHandle<T>,
@ -15,7 +19,7 @@ impl<T> Script<T> {
Self {
res: script,
name: name.to_string(),
id: 0 // TODO: make a counter
id: SCRIPT_ID_COUNTER.fetch_add(1, Ordering::AcqRel)
}
}

View File

@ -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<World>,