diff --git a/.vscode/launch.json b/.vscode/launch.json index ad599e0..238c8c0 100755 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -41,6 +41,27 @@ "args": [], "cwd": "${workspaceFolder}" }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'lyra-scripting'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=lyra-scripting", + "--", + "--nocapture" + ], + "filter": { + "name": "lyra-scripting", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}/lyra-scripting" + }, { "type": "lldb", "request": "launch", diff --git a/Cargo.lock b/Cargo.lock index c6b5c4b..01cbba6 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,9 +90,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.75" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +checksum = "ca87830a3e3fb156dc96cfbd31cb620265dd053be734723f22b760d6cc3c3051" [[package]] name = "arrayref" @@ -369,6 +369,16 @@ dependencies = [ "log", ] +[[package]] +name = "bstr" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "bumpalo" version = "3.13.0" @@ -1300,6 +1310,7 @@ dependencies = [ "lyra-ecs", "lyra-reflect", "lyra-resource", + "lyra-scripting", "quote", "syn 2.0.42", "tracing", @@ -1346,6 +1357,20 @@ dependencies = [ "uuid", ] +[[package]] +name = "lyra-scripting" +version = "0.1.0" +dependencies = [ + "anyhow", + "lyra-ecs", + "lyra-reflect", + "lyra-resource", + "mlua", + "thiserror", + "tracing", + "tracing-subscriber", +] + [[package]] name = "mach2" version = "0.4.1" @@ -1445,6 +1470,30 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mlua" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c81f8ac20188feb5461a73eabb22a34dd09d6d58513535eb587e46bff6ba250" +dependencies = [ + "bstr", + "mlua-sys", + "num-traits", + "once_cell", + "rustc-hash", +] + +[[package]] +name = "mlua-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc29228347d6bdc9e613dc95c69df2817f755434ee0f7f3b27b57755fe238b7f" +dependencies = [ + "cc", + "cfg-if", + "pkg-config", +] + [[package]] name = "naga" version = "0.11.1" diff --git a/Cargo.toml b/Cargo.toml index 6f5a56d..4080bd4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,15 +5,21 @@ edition = "2021" [workspace] members = [ + "examples/testbed", "lyra-resource", "lyra-ecs", - "examples/testbed" -, "lyra-reflect"] + "lyra-reflect", + "lyra-scripting" +] + +[features] +scripting = ["dep:lyra-scripting"] [dependencies] lyra-resource = { path = "lyra-resource", version = "0.0.1" } lyra-ecs = { path = "lyra-ecs" } lyra-reflect = { path = "lyra-reflect" } +lyra-scripting = { path = "lyra-scripting", optional = true } winit = "0.28.1" tracing = "0.1.37" diff --git a/examples/testbed/Cargo.toml b/examples/testbed/Cargo.toml index 54a8a77..efe8e25 100644 --- a/examples/testbed/Cargo.toml +++ b/examples/testbed/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -lyra-engine = { path = "../../", version = "0.0.1" } +lyra-engine = { path = "../../", version = "0.0.1", features = ["scripting"] } #lyra-ecs = { path = "../../lyra-ecs"} anyhow = "1.0.75" async-std = "1.12.0" diff --git a/lyra-scripting/Cargo.toml b/lyra-scripting/Cargo.toml new file mode 100644 index 0000000..6ec74cc --- /dev/null +++ b/lyra-scripting/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "lyra-scripting" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +default = ["lua"] +lua = ["dep:mlua"] + +[dependencies] +lyra-ecs = { path = "../lyra-ecs" } +lyra-reflect = { path = "../lyra-reflect" } +lyra-resource = { path = "../lyra-resource" } +thiserror = "1.0.50" +anyhow = "1.0.77" +tracing = "0.1.37" + +# enabled with lua feature +mlua = { version = "0.9.2", features = ["lua54"], optional = true } # luajit maybe? + + +[dev-dependencies] +tracing-subscriber = { version = "0.3.16", features = [ "tracing-log" ] } \ No newline at end of file diff --git a/lyra-scripting/src/host.rs b/lyra-scripting/src/host.rs new file mode 100644 index 0000000..b453389 --- /dev/null +++ b/lyra-scripting/src/host.rs @@ -0,0 +1,114 @@ +use std::{sync::{Arc, RwLock}, error::Error, collections::HashMap}; + +use anyhow::anyhow; +use lyra_ecs::{query::Res, ResourceObject}; + +use crate::ScriptWorldPtr; + +#[derive(Debug, thiserror::Error)] +pub enum ScriptError { + #[error("{0}")] + #[cfg(feature = "lua")] + MluaError(mlua::Error), + + #[error("{0}")] + Other(anyhow::Error), +} + +#[cfg(feature = "lua")] +impl From for ScriptError { + fn from(value: mlua::Error) -> Self { + ScriptError::MluaError(value) + } +} + +impl From for ScriptError { + fn from(value: anyhow::Error) -> Self { + ScriptError::Other(value) + } +} + +#[derive(Clone, Hash, PartialEq, Eq)] +pub struct ScriptData { + pub script_id: u64, + pub name: String, +} + +/// Provides an API to a scripting context. +pub trait ScriptApiProvider { + /// The type used as the script's context. + type ScriptContext; + + /// 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`]. + fn setup_script(&mut self, data: &ScriptData, ctx: &mut Self::ScriptContext) -> Result<(), ScriptError>; + + /// A function that is used to update a script's environment. + /// + /// This is used to update stuff like the world pointer in the script context. + fn update_script_environment(&mut self, world: ScriptWorldPtr, data: &ScriptData, ctx: &mut Self::ScriptContext) -> Result<(), ScriptError>; +} + +/// A storage for a [`ScriptHost`]'s api providers +#[derive(Default)] +pub struct ScriptApiProviders { + pub apis: Vec>>, +} + +impl ScriptApiProviders { + pub fn add_provider

(&mut self, provider: P) + where + P: ScriptApiProvider + 'static + { + self.apis.push(Box::new(provider)); + } +} + +pub trait ScriptHost: Default + ResourceObject { + /// The type used as the script's context. + type ScriptContext; + + /// 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; + + /// Setup a script for execution. + 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>; +} + +#[derive(Default)] +pub struct ScriptContexts { + contexts: HashMap, +} + +impl ScriptContexts { + pub fn new(contexts: HashMap) -> Self { + Self { + contexts, + } + } + + pub fn add_context(&mut self, script_id: u64, context: T) { + self.contexts.insert(script_id, context); + } + + pub fn get_context(&self, script_id: u64) -> Option<&T> { + self.contexts.get(&script_id) + } + + pub fn get_context_mut(&mut self, script_id: u64) -> Option<&mut T> { + self.contexts.get_mut(&script_id) + } + + pub fn has_context(&self, script_id: u64) -> bool { + self.contexts.contains_key(&script_id) + } +} \ No newline at end of file diff --git a/lyra-scripting/src/lib.rs b/lyra-scripting/src/lib.rs new file mode 100644 index 0000000..3ebe522 --- /dev/null +++ b/lyra-scripting/src/lib.rs @@ -0,0 +1,60 @@ + +#[cfg(feature = "lua")] +pub mod lua; + +pub mod world; +pub use world::*; + +pub mod wrap; +pub use wrap::*; + +pub mod host; +pub use host::*; + +pub mod script; +pub use script::*; + +#[allow(unused_imports)] +pub(crate) mod lyra_engine { + pub use lyra_ecs as ecs; +} + +use lyra_reflect::{ReflectedComponent, Reflect}; + +#[derive(Clone)] +pub enum ReflectBranch { + Component(ReflectedComponent), +} + +impl ReflectBranch { + /// Gets self as a [`ReflectedComponent`]. + /// + /// # Panics + /// If `self` is not a variant of [`ReflectBranch::Component`]. + pub fn as_component_unchecked(&self) -> &ReflectedComponent { + match self { + ReflectBranch::Component(c) => c, + _ => panic!("`self` is not an instance of `ReflectBranch::Component`") + } + } + + pub fn is_component(&self) -> bool { + matches!(self, ReflectBranch::Component(_)) + } +} + +pub struct ScriptBorrow { + reflect_branch: ReflectBranch, + data: Option>, +} + +impl Clone for ScriptBorrow { + fn clone(&self) -> Self { + let data = self.data.as_ref().map(|b| b.clone_inner()); + + Self { + reflect_branch: self.reflect_branch.clone(), + data, + } + } +} \ No newline at end of file diff --git a/lyra-scripting/src/lua/dynamic_iter.rs b/lyra-scripting/src/lua/dynamic_iter.rs new file mode 100644 index 0000000..5b6b300 --- /dev/null +++ b/lyra-scripting/src/lua/dynamic_iter.rs @@ -0,0 +1,166 @@ +use std::{ptr::NonNull, ops::{Range, Deref}}; + +use lyra_ecs::{ComponentColumn, ComponentInfo, Archetype, ArchetypeId, ArchetypeEntityId, query::dynamic::{DynamicType, QueryDynamicType}, query::Fetch}; +use lyra_reflect::TypeRegistry; + +#[cfg(feature = "lua")] +use super::ReflectLuaProxy; + +use crate::ScriptWorldPtr; + +/// A reimplementation of lyra_ecs::FetchDynamicType that doesn't store references and +/// uses a pointer instead. +/// This makes it easier to expose to Lua +#[derive(Clone)] +pub struct FetchDynamicType { + col: NonNull, + info: ComponentInfo, +} + +impl<'a> Fetch<'a> for FetchDynamicType { + type Item = DynamicType; + + fn dangling() -> Self { + unreachable!() + } + + unsafe fn get_item(&mut self, entity: ArchetypeEntityId) -> Self::Item { + let ptr = unsafe { self.col.as_ref().borrow_ptr() }; + let ptr = NonNull::new_unchecked(ptr.as_ptr() + .add(entity.0 as usize * self.info.layout.size)); + + DynamicType { + info: self.info, + ptr, + } + } +} + +impl<'a> From> for FetchDynamicType { + fn from(value: lyra_ecs::query::dynamic::FetchDynamicType<'a>) -> Self { + Self { + col: NonNull::from(value.col), + info: value.info, + } + } +} + +#[derive(Clone)] +pub struct DynamicViewIter { + world_ptr: ScriptWorldPtr, + queries: Vec, + fetchers: Vec, + archetypes: Vec>, + next_archetype: usize, + component_indices: Range, +} + +impl<'a> From> for DynamicViewIter { + fn from(value: lyra_ecs::query::dynamic::DynamicViewIter<'a>) -> Self { + Self { + world_ptr: ScriptWorldPtr::from_ref(value.world), + queries: value.queries, + fetchers: value.fetchers.into_iter().map(|f| FetchDynamicType::from(f)).collect(), + archetypes: value.archetypes.into_iter().map(|a| NonNull::from(a)).collect(), + next_archetype: value.next_archetype, + component_indices: value.component_indices, + } + } +} + +impl Iterator for DynamicViewIter { + type Item = Vec; + + fn next(&mut self) -> Option { + loop { + if let Some(entity_index) = self.component_indices.next() { + let mut fetch_res = vec![]; + + for fetcher in self.fetchers.iter_mut() { + let entity_index = ArchetypeEntityId(entity_index); + if !fetcher.can_visit_item(entity_index) { + break; + } else { + let i = unsafe { fetcher.get_item(entity_index) }; + fetch_res.push(i); + } + } + + if fetch_res.len() != self.fetchers.len() { + continue; + } + + return Some(fetch_res); + } else { + if self.next_archetype >= self.archetypes.len() { + return None; // ran out of archetypes to go through + } + + let arch_id = self.next_archetype; + self.next_archetype += 1; + let arch = unsafe { self.archetypes.get_unchecked(arch_id).as_ref() }; + + if arch.entities().len() == 0 { + continue; + } + + if self.queries.iter().any(|q| !q.can_visit_archetype(arch)) { + continue; + } + + //let world = unsafe { self.world_ptr.as_ref() }; + let world = self.world_ptr.as_ref(); + + self.fetchers = self.queries.iter() + .map(|q| unsafe { q.fetch(world, ArchetypeId(arch_id as u64), arch) } ) + .map(|f| FetchDynamicType::from(f)) + .collect(); + self.component_indices = 0..arch.entities().len() as u64; + } + } + } +} + +pub struct ReflectedIterator { + pub world: ScriptWorldPtr, + pub dyn_view: DynamicViewIter, + pub reflected_components: Option> +} + +impl ReflectedIterator { + + + #[cfg(feature = "lua")] + pub fn next_lua<'a>(&mut self, lua: &'a mlua::Lua) -> Option), mlua::AnyUserData<'a>)>> { + + let n = self.dyn_view.next(); + + if let Some(row) = n { + if self.reflected_components.is_none() { + let world = self.world.as_ref(); + self.reflected_components = world.try_get_resource::() + .map(|r| NonNull::from(r.deref())); + } + + let mut dynamic_row = Vec::new(); + for d in row.iter() { + let id = d.info.type_id.as_rust(); + let reflected_components = + unsafe { self.reflected_components.as_ref().unwrap().as_ref() }; + + let reg_type = reflected_components.get_type(id) + .expect("Could not find type for dynamic view!"); + let proxy = reg_type.get_data::() + .expect("Type does not have ReflectLuaProxy as a TypeData"); + + let userdata = (proxy.fn_as_uservalue)(lua, d.ptr).unwrap(); + + dynamic_row.push(( (proxy, d.ptr), userdata)); + } + + Some(dynamic_row) + } else { + None + } + } +} \ No newline at end of file diff --git a/lyra-scripting/src/lua/loader.rs b/lyra-scripting/src/lua/loader.rs new file mode 100644 index 0000000..95aee98 --- /dev/null +++ b/lyra-scripting/src/lua/loader.rs @@ -0,0 +1,43 @@ +use std::sync::Arc; + +use lyra_resource::{ResourceLoader, Resource}; + +#[derive(Debug, Clone)] +pub struct LuaScript { + /// The byte contents of the script. + pub(crate) bytes: Vec, +} + +#[derive(Default, Clone)] +pub struct LuaLoader; + +impl ResourceLoader for LuaLoader { + fn extensions(&self) -> &[&str] { + &[".lua"] + } + + fn mime_types(&self) -> &[&str] { + &["text/lua"] + } + + fn load(&self, _resource_manager: &mut lyra_resource::ResourceManager, path: &str) -> Result, lyra_resource::LoaderError> { + let bytes = std::fs::read(path)?; + + let s = Resource::with_data(path, LuaScript { + bytes + }); + + Ok(Arc::new(s)) + } + + fn load_bytes(&self, _resource_manager: &mut lyra_resource::ResourceManager, bytes: Vec, offset: usize, length: usize) -> Result, lyra_resource::LoaderError> { + let end = offset + length; + let bytes = bytes[offset..end].to_vec(); + + let s = Resource::with_data("from bytes", LuaScript { + bytes + }); + + Ok(Arc::new(s)) + } +} \ No newline at end of file diff --git a/lyra-scripting/src/lua/mod.rs b/lyra-scripting/src/lua/mod.rs new file mode 100644 index 0000000..1e678f0 --- /dev/null +++ b/lyra-scripting/src/lua/mod.rs @@ -0,0 +1,113 @@ +pub mod dynamic_iter; +pub use dynamic_iter::*; + +pub mod world; +pub use world::*; + +pub mod script; +pub use script::*; + +pub mod loader; +pub use loader::*; + +#[cfg(test)] +mod test; + +use std::ptr::NonNull; + +use lyra_ecs::DynamicBundle; +use lyra_reflect::{Reflect, RegisteredType, FromType, AsRegisteredType}; + +use mlua::{Lua, AnyUserDataExt}; + +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}; + +impl<'lua> mlua::FromLua<'lua> for ScriptBorrow { + fn from_lua(value: mlua::Value<'lua>, _lua: &'lua Lua) -> mlua::Result { + match value { + mlua::Value::UserData(ud) => Ok(ud.borrow::()?.clone()), + _ => unreachable!(), + } + } +} + +impl mlua::UserData for ScriptBorrow {} + +pub fn reflect_user_data(ud: &mlua::AnyUserData) -> ScriptBorrow { + ud.call_method::<_, ScriptBorrow>(FN_NAME_INTERNAL_REFLECT, ()) + .expect("Type does not implement '__internal_reflect' properly") +} + +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<()>; +} + +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> { + 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<()> { + let this = this.as_any_mut().downcast_mut::().unwrap(); + let apply = apply.borrow::()?; + + *this = apply.clone(); + + Ok(()) + } +} + +#[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<()>, +} + +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 { + let this = unsafe { this.cast::().as_ref() }; + ::as_lua_value(lua, this) + }, + fn_apply: |lua, ptr, apply| { + let this = unsafe { ptr.cast::().as_mut() }; + ::apply(lua, this, apply) + } + } + } +} + +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()) }), + _ => panic!(), + } + } +} + +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_method_mut("push", |_, this, (comp,): (mlua::AnyUserData,)| { + let script_brw = comp.call_method::<_, ScriptBorrow>(FN_NAME_INTERNAL_REFLECT, ())?; + let reflect = script_brw.reflect_branch.as_component_unchecked(); + + let refl_data = script_brw.data.unwrap(); + let refl_data = refl_data.as_ref(); + reflect.bundle_insert(&mut this.0, refl_data); + + Ok(()) + }); + } +} \ No newline at end of file diff --git a/lyra-scripting/src/lua/script.rs b/lyra-scripting/src/lua/script.rs new file mode 100644 index 0000000..e74efdd --- /dev/null +++ b/lyra-scripting/src/lua/script.rs @@ -0,0 +1,74 @@ +use std::sync::Mutex; + +use tracing::debug; + +use crate::{ScriptHost, ScriptError, ScriptWorldPtr}; + +#[derive(Default)] +pub struct LuaHost; + +fn try_call_lua_function(lua: &mlua::Lua, fn_name: &str) -> Result<(), ScriptError> { + let globals = lua.globals(); + + match globals.get::<_, mlua::Function>(fn_name) { + Ok(init_fn) => { + init_fn.call(()) + .map_err(ScriptError::MluaError)?; + }, + Err(mlua::Error::FromLuaConversionError { from: "nil", to: "function", message: None }) => { + debug!("Function '{}' was not found, ignoring...", fn_name) + // ignore + }, + Err(e) => { + return Err(ScriptError::MluaError(e)); + }, + } + + Ok(()) +} + +impl ScriptHost for LuaHost { + type ScriptContext = Mutex; + + fn load_script(&mut self, script: &[u8], script_data: &crate::ScriptData, providers: &mut crate::ScriptApiProviders) -> Result { + let mut ctx = Mutex::new(mlua::Lua::new()); + + for provider in providers.apis.iter_mut() { + provider.expose_api(&mut ctx)?; + } + + let lua = ctx.lock().unwrap(); + lua.load(script) + .set_name(&script_data.name) + .exec() + .map_err(|e| ScriptError::MluaError(e))?; + drop(lua); + + Ok(ctx) + } + + fn setup_script(&mut self, script_data: &crate::ScriptData, ctx: &mut Self::ScriptContext, providers: &mut crate::ScriptApiProviders) -> Result<(), ScriptError> { + for provider in providers.apis.iter_mut() { + provider.setup_script(script_data, ctx)?; + } + + let ctx = ctx.lock().expect("Failure to get Lua ScriptContext"); + try_call_lua_function(&ctx, "init")?; + + Ok(()) + } + + /// 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> { + 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")?; + + Ok(()) + } +} \ No newline at end of file diff --git a/lyra-scripting/src/lua/test.rs b/lyra-scripting/src/lua/test.rs new file mode 100644 index 0000000..398d7ce --- /dev/null +++ b/lyra-scripting/src/lua/test.rs @@ -0,0 +1,219 @@ +use std::{sync::{Mutex, Arc}, alloc::Layout, ptr::NonNull}; + +use lyra_ecs::{World, query::{Res, View, Entities, ResMut}, Component, 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 crate::{ScriptHost, ScriptData, ScriptApiProvider, ScriptApiProviders, ScriptError, ScriptWorldPtr, ScriptList, Script, ScriptContexts}; + +use super::{LuaHost, LuaLoader, LuaScript}; + +use crate::lyra_engine; + +fn enable_tracing() { + tracing_subscriber::registry() + .with(fmt::layer().with_writer(std::io::stdout)) + .init(); +} + +#[derive(Default)] +struct PrintfProvider; + +impl ScriptApiProvider for PrintfProvider { + 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) => { + if let Ok(tos) = ud.get::<_, mlua::Function>(mlua::MetaMethod::ToString.to_string()) { + tos.call::<_, String>(())? + } 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;// - 1; + + // 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> { + let ctx = ctx.lock().unwrap(); + let globals = ctx.globals(); + + let world_lua = world.into_lua(&ctx) + .map_err(ScriptError::MluaError)?; + globals.set("world", world_lua) + .map_err(ScriptError::MluaError)?; + + Ok(()) + } +} + +/// Tests a simple lua script that just prints some test +#[test] +pub fn lua_print() { + enable_tracing(); + + let mut world = World::new(); + + let test_provider = PrintfProvider::default(); + let mut providers = ScriptApiProviders::::default(); + providers.add_provider(test_provider); + + let host = LuaHost::default(); + + world.add_resource(host); + world.add_resource(providers); + world.add_resource(ScriptContexts::>::default()); + + let mut res_loader = ResourceManager::new(); + res_loader.register_loader::(); + + let script = + r#" + print("Hello World") + + function update() + print("updated") + printf("I love to eat formatted {}!", "food") + --printf("World is {}", world) + end + "#; + let script = script.as_bytes(); + + let script = res_loader.load_bytes::("test_script.lua", + "text/lua", script.to_vec(), 0, script.len()).unwrap(); + let script = Script::new("text_script.lua", script); + + let scripts = ScriptList::new(vec![script]); + + world.spawn((scripts,)); + + let mut exec = GraphExecutor::new(); + exec.insert_system("lua_update_scripts", lua_update_scripts.into_system(), &[]); + exec.execute(NonNull::from(&world), true).unwrap(); +} + +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::>(); + + for scripts in world.view_iter::<&ScriptList>() { + for script in scripts.iter() { + let script_data = ScriptData { + name: script.name().to_string(), + script_id: script.id(), + }; + + if !contexts.has_context(script.id()) { + if let Some(script_res) = &script.res_handle().data { + let mut script_ctx = host.load_script(&script_res.bytes, &script_data, &mut providers).unwrap(); + host.setup_script(&script_data, &mut script_ctx, &mut providers).unwrap(); + contexts.add_context(script.id(), script_ctx); + } else { + debug!("Script '{}' is not yet loaded, skipping", script.name()); + } + } + + let ctx = contexts.get_context_mut(script.id()).unwrap(); + + 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); + }, + ScriptError::Other(_) => return Err(e.into()), + }, + } + } + } + + Ok(()) +} \ No newline at end of file diff --git a/lyra-scripting/src/lua/world.rs b/lyra-scripting/src/lua/world.rs new file mode 100644 index 0000000..a6c6552 --- /dev/null +++ b/lyra-scripting/src/lua/world.rs @@ -0,0 +1,131 @@ +use lyra_ecs::query::dynamic::QueryDynamicType; +use lyra_reflect::TypeRegistry; +use mlua::{AnyUserDataExt, IntoLua, IntoLuaMulti}; + +use crate::{ScriptWorldPtr, ScriptEntity, ScriptDynamicBundle, ScriptBorrow}; + +use super::{ReflectedIterator, DynamicViewIter, FN_NAME_INTERNAL_REFLECT_TYPE, reflect_user_data, ReflectLuaProxy}; + +impl<'lua> mlua::FromLua<'lua> for ScriptEntity { + fn from_lua(value: mlua::Value<'lua>, _lua: &'lua mlua::Lua) -> mlua::Result { + match value { + mlua::Value::UserData(ud) => Ok(ud.borrow::()?.clone()), + mlua::Value::Nil => Err(mlua::Error::FromLuaConversionError { from: "Nil", to: "ScriptEntity", message: Some("Value was nil".to_string()) }), + _ => panic!(), + } + } +} + +impl mlua::UserData for ScriptEntity {} + +#[derive(thiserror::Error, Debug, Clone)] +pub enum WorldError { + #[error("{0}")] + LuaInvalidUsage(String), +} + +impl mlua::UserData for ScriptWorldPtr { + fn add_methods<'lua, M: mlua::prelude::LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_method_mut("spawn", |_, this, (bundle,): (ScriptDynamicBundle,)| { + let world = unsafe { this.inner.as_mut() }; + + Ok(ScriptEntity(world.spawn(bundle.0))) + }); + + methods.add_method("view_iter", |lua, this, queries: mlua::Variadic| { + let world = unsafe { this.inner.as_ref() }; + let mut view = world.dynamic_view(); + + for comp in queries.into_iter() { + let script_brw = comp.call_function::<_, ScriptBorrow>(FN_NAME_INTERNAL_REFLECT_TYPE, ()) + .expect("Type does not implement '__internal_reflect_type' properly"); + let refl_comp = script_brw.reflect_branch.as_component_unchecked(); + + let dyn_type = QueryDynamicType::from_info(refl_comp.info); + view.push(dyn_type); + } + + let iter = view.into_iter(); + let mut reflected_iter = ReflectedIterator { + world: this.clone(), + dyn_view: DynamicViewIter::from(iter), + reflected_components: None, + }; + + let f = lua.create_function_mut(move |lua, ()| { + if let Some(row) = reflected_iter.next_lua(lua) { + let row = row.into_iter().map(|(_, ud)| ud.into_lua(lua)) + .collect::>>()?; + Ok(mlua::MultiValue::from_vec(row)) + } else { + Ok(mlua::Value::Nil.into_lua_multi(lua)?) + } + })?; + + Ok(f) + }); + + methods.add_method("view", |lua, this, (system, queries): (mlua::Function, mlua::Variadic)| { + if queries.is_empty() { + panic!("No components were provided!"); + } + + let world = unsafe { this.inner.as_ref() }; + let mut view = world.dynamic_view(); + + for comp in queries.into_iter() { + let reflect = comp.call_function::<_, ScriptBorrow>(FN_NAME_INTERNAL_REFLECT_TYPE, ()) + .expect("Type does not implement 'reflect_type' properly"); + let refl_comp = reflect.reflect_branch.as_component_unchecked(); + + let dyn_type = QueryDynamicType::from_info(refl_comp.info); + view.push(dyn_type); + } + + let iter = view.into_iter(); + let mut reflected_iter = ReflectedIterator { + world: this.clone(), + dyn_view: DynamicViewIter::from(iter), + reflected_components: None, + }; + + let reg = this.as_ref().get_resource::(); + + while let Some(row) = reflected_iter.next_lua(lua) { + let (reflects, values): (Vec<(_, _)>, Vec<_>) = row.into_iter().unzip(); + + let value_row: Vec<_> = values.into_iter().map(|ud| ud.into_lua(lua)).collect::>>()?; + let mult_val = mlua::MultiValue::from_vec(value_row); + let res: mlua::MultiValue = system.call(mult_val)?; + + // if values were returned, find the type in the type registry, and apply the new values + if res.len() <= reflects.len() { + for (i, comp) in res.into_iter().enumerate() { + let (_proxy, ptr) = reflects[i]; + + match comp.as_userdata() { + Some(ud) => { + let lua_comp = reflect_user_data(ud); + let refl_comp = lua_comp.reflect_branch.as_component_unchecked(); + let lua_typeid = refl_comp.info.type_id.as_rust(); + let reg_type = reg.get_type(lua_typeid).unwrap(); + + let proxy = reg_type.get_data::().unwrap(); + (proxy.fn_apply)(lua, ptr, ud)? + } + None => { + panic!("A userdata value was not returned!"); + } + } + } + } else { + let msg = format!("Too many arguments were returned from the World view! + At most, the expected number of results is {}.", reflects.len()); + return Err(mlua::Error::external(WorldError::LuaInvalidUsage(msg))); + } + } + + Ok(()) + }); + } +} \ No newline at end of file diff --git a/lyra-scripting/src/script.rs b/lyra-scripting/src/script.rs new file mode 100644 index 0000000..d93877b --- /dev/null +++ b/lyra-scripting/src/script.rs @@ -0,0 +1,57 @@ +use lyra_ecs::Component; +use lyra_resource::ResHandle; + +use crate::lyra_engine; + +#[derive(Clone)] +pub struct Script { + res: ResHandle, + name: String, + id: u64 +} + +impl Script { + pub fn new(name: &str, script: ResHandle) -> Self { + Self { + res: script, + name: name.to_string(), + id: 0 // TODO: make a counter + } + } + + pub fn res_handle(&self) -> ResHandle { + self.res.clone() + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn id(&self) -> u64 { + self.id + } +} + +/// A list of scripts +#[derive(Clone, Default, Component)] +pub struct ScriptList(Vec>); + +impl ScriptList { + pub fn new(list: Vec>) -> Self { + Self(list) + } +} + +impl std::ops::Deref for ScriptList { + type Target = Vec>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for ScriptList { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} \ No newline at end of file diff --git a/lyra-scripting/src/world.rs b/lyra-scripting/src/world.rs new file mode 100644 index 0000000..b786612 --- /dev/null +++ b/lyra-scripting/src/world.rs @@ -0,0 +1,42 @@ +use std::ptr::NonNull; + +use lyra_ecs::{world::World, Entity}; + +use mlua::{prelude::{LuaUserData, LuaAnyUserData, LuaValue, LuaResult, Lua, LuaError}, Variadic, AnyUserDataExt, FromLua, IntoLua, MultiValue, IntoLuaMulti}; + +use crate::ScriptBorrow; + +#[derive(Clone)] +pub struct ScriptEntity(pub Entity); + +#[derive(Clone)] +pub struct ScriptWorldPtr { + pub inner: NonNull, +} + +impl ScriptWorldPtr { + /// Creates a world pointer from a world borrow. + pub fn from_ref(world: &World) -> Self { + Self { + inner: NonNull::from(world), + } + } + + /// Returns a borrow to the world from the ptr. + pub fn as_ref(&self) -> &World { + unsafe { self.inner.as_ref() } + } + + /// Returns a mutable borrow to the world from the ptr. + pub fn as_mut(&mut self) -> &mut World { + unsafe { self.inner.as_mut() } + } +} + +impl std::ops::Deref for ScriptWorldPtr { + type Target = NonNull; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} \ No newline at end of file diff --git a/lyra-scripting/src/wrap.rs b/lyra-scripting/src/wrap.rs new file mode 100644 index 0000000..d0c6c76 --- /dev/null +++ b/lyra-scripting/src/wrap.rs @@ -0,0 +1,12 @@ +use lyra_ecs::DynamicBundle; + +#[derive(Clone)] +pub struct ScriptDynamicBundle(pub DynamicBundle); + +impl std::ops::Deref for ScriptDynamicBundle { + type Target = DynamicBundle; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} \ No newline at end of file