Create an early scripting engine #2

Merged
SeanOMik merged 42 commits from feature/early-scripting into main 2024-03-03 03:28:57 +00:00
4 changed files with 245 additions and 267 deletions
Showing only changes of commit 82c13a7dd2 - Show all commits

View File

@ -120,7 +120,6 @@ impl Iterator for DynamicViewIter {
continue;
}
//let world = unsafe { self.world_ptr.as_ref() };
let world = self.world_ptr.as_ref();
self.fetchers = self.queries.iter()
@ -153,8 +152,6 @@ pub struct ReflectedIterator {
}
impl ReflectedIterator {
#[cfg(feature = "lua")]
pub fn next_lua<'a>(&mut self, lua: &'a mlua::Lua) -> Option<ReflectedRow<'a>> {
@ -167,11 +164,6 @@ impl ReflectedIterator {
.map(|r| NonNull::from(r.deref()));
}
/* let mut row = ReflectedRow {
entity: row.entity,
row: row.item,
}; */
let mut dynamic_row = vec![];
for d in row.item.iter() {
let id = d.info.type_id.as_rust();

View File

@ -1,11 +1,7 @@
pub mod dynamic_iter;
use anyhow::anyhow;
pub use dynamic_iter::*;
pub mod world;
use lyra_game::{game::GameStages, plugin::Plugin};
use lyra_resource::ResourceManager;
use tracing::{debug, debug_span, error, trace};
pub use world::*;
pub mod script;
@ -20,13 +16,15 @@ pub mod wrappers;
pub mod proxy;
pub use proxy::*;
pub mod system;
pub use system::*;
#[cfg(test)]
mod test;
use std::{any::TypeId, ptr::NonNull, sync::Mutex};
use std::{any::TypeId, sync::Mutex};
use lyra_ecs::{
query::{Entities, ResMut, View},
DynamicBundle, World,
};
use lyra_reflect::{FromType, Reflect, TypeRegistry};
@ -38,13 +36,9 @@ 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::{
GameScriptExt, ScriptApiProviders, ScriptBorrow, ScriptContexts, ScriptData,
ScriptDynamicBundle, ScriptError, ScriptHost, ScriptList, ScriptWorldPtr,
};
use self::providers::{LyraEcsApiProvider, LyraMathApiProvider, UtilityApiProvider};
use crate::{ScriptBorrow, ScriptDynamicBundle};
/// A trait used for registering a Lua type with the world.
pub trait RegisterLuaType {
/// Register a type to lua that **is not wrapped**.
fn register_lua_type<'a, T>(&mut self)
@ -113,232 +107,4 @@ impl mlua::UserData for ScriptDynamicBundle {
Ok(())
});
}
}
/// 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() {
if !contexts.has_context(script.id()) {
let script_data = ScriptData {
name: script.name().to_string(),
script_id: script.id(),
entity: en,
};
let script_name = script.name();
let _span = debug_span!("lua", script = script_name).entered();
if let Some(script_res) = &script.res_handle().try_data_ref() {
debug!("Loading script...");
let mut script_ctx =
host.load_script(&script_res.bytes, &script_data, &mut providers)?;
trace!("Finished loading script");
debug!("Setting up script...");
host.setup_script(&script_data, &mut script_ctx, &mut providers)?;
trace!("Finished setting up script");
contexts.add_context(script.id(), script_ctx);
} else {
trace!("Script is not loaded yet, skipping for now");
}
}
}
}
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<ScriptContexts<LuaContext>>,
mut resman: ResMut<ResourceManager>,
view: View<&ScriptList<LuaScript>>,
) -> 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(())
}
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()) {
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(())
}
/// 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;
impl Plugin for LuaScriptingPlugin {
fn setup(&self, game: &mut lyra_game::game::Game) {
let world = game.world();
world.add_resource_default::<TypeRegistry>();
world.add_resource_default::<LuaHost>();
world.add_resource_default::<ScriptApiProviders<LuaHost>>();
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.add_script_api_provider::<LuaHost, _>(UtilityApiProvider);
game.add_script_api_provider::<LuaHost, _>(LyraEcsApiProvider);
game.add_script_api_provider::<LuaHost, _>(LyraMathApiProvider);
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,
&[],
);
}
}
}

View File

@ -0,0 +1,238 @@
use anyhow::anyhow;
use lyra_ecs::{query::{Entities, ResMut, View}, World};
use lyra_game::{game::GameStages, plugin::Plugin};
use lyra_reflect::TypeRegistry;
use lyra_resource::ResourceManager;
use tracing::{debug, debug_span, error, trace};
use crate::{GameScriptExt, ScriptApiProviders, ScriptContexts, ScriptData, ScriptError, ScriptHost, ScriptList, ScriptWorldPtr};
use super::{providers::{LyraEcsApiProvider, LyraMathApiProvider, UtilityApiProvider}, LuaContext, LuaHost, LuaLoader, 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() {
if !contexts.has_context(script.id()) {
let script_data = ScriptData {
name: script.name().to_string(),
script_id: script.id(),
entity: en,
};
let script_name = script.name();
let _span = debug_span!("lua", script = script_name).entered();
if let Some(script_res) = &script.res_handle().try_data_ref() {
debug!("Loading script...");
let mut script_ctx =
host.load_script(&script_res.bytes, &script_data, &mut providers)?;
trace!("Finished loading script");
debug!("Setting up script...");
host.setup_script(&script_data, &mut script_ctx, &mut providers)?;
trace!("Finished setting up script");
contexts.add_context(script.id(), script_ctx);
} else {
trace!("Script is not loaded yet, skipping for now");
}
}
}
}
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<ScriptContexts<LuaContext>>,
mut resman: ResMut<ResourceManager>,
view: View<&ScriptList<LuaScript>>,
) -> 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(())
}
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()) {
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(())
}
/// 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;
impl Plugin for LuaScriptingPlugin {
fn setup(&self, game: &mut lyra_game::game::Game) {
let world = game.world();
world.add_resource_default::<TypeRegistry>();
world.add_resource_default::<LuaHost>();
world.add_resource_default::<ScriptApiProviders<LuaHost>>();
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.add_script_api_provider::<LuaHost, _>(UtilityApiProvider);
game.add_script_api_provider::<LuaHost, _>(LyraEcsApiProvider);
game.add_script_api_provider::<LuaHost, _>(LyraMathApiProvider);
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,
&[],
);
}
}

View File

@ -164,24 +164,6 @@ impl mlua::UserData for ScriptWorldPtr {
let reflect = ty
.call_function::<_, ScriptBorrow>(FN_NAME_INTERNAL_REFLECT_TYPE, ())
.expect("Type does not implement 'reflect_type' properly");
//if let Some(data) = reflect.data {
/* let res = reflect.reflect_branch.as_resource_unchecked();
if let Some(res_ptr) = res.reflect_arc(this.as_mut()) {
let reg_type = this.as_ref().get_type::<RegisteredType>(reflect.reflect_branch.reflect_type_id())
.unwrap();
let proxy = reg_type.get_data::<ReflectLuaProxy>()
.expect("Type does not have ReflectLuaProxy as a TypeData");
//let res = Arc::new(RwLock::new(data.as_any_box()))
(proxy.fn_as_uservalue_ref)(lua, res_ptr)
.and_then(|ud| ud.into_lua(lua))
//Ok(mlua::Value::Nil)
} else {
Ok(mlua::Value::Nil)
} */
let res = reflect.reflect_branch.as_resource_unchecked();
if let Some(res_ptr) = res.reflect_ptr(this.as_mut()) {