Add early scripting system with lua
ci/woodpecker/push/debug Pipeline failed Details

This commit is contained in:
SeanOMik 2024-01-04 20:52:47 -05:00
parent 0f062217ca
commit 6caf235a6f
Signed by: SeanOMik
GPG Key ID: FEC9E2FC15235964
16 changed files with 1137 additions and 5 deletions

21
.vscode/launch.json vendored
View File

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

53
Cargo.lock generated
View File

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

View File

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

View File

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

25
lyra-scripting/Cargo.toml Normal file
View File

@ -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" ] }

114
lyra-scripting/src/host.rs Normal file
View File

@ -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<mlua::Error> for ScriptError {
fn from(value: mlua::Error) -> Self {
ScriptError::MluaError(value)
}
}
impl From<anyhow::Error> 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<T: ScriptHost> {
pub apis: Vec<Box<dyn ScriptApiProvider<ScriptContext = T::ScriptContext>>>,
}
impl<T: ScriptHost> ScriptApiProviders<T> {
pub fn add_provider<P>(&mut self, provider: P)
where
P: ScriptApiProvider<ScriptContext = T::ScriptContext> + '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<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>;
/// 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>;
}
#[derive(Default)]
pub struct ScriptContexts<T> {
contexts: HashMap<u64, T>,
}
impl<T> ScriptContexts<T> {
pub fn new(contexts: HashMap<u64, T>) -> 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)
}
}

60
lyra-scripting/src/lib.rs Normal file
View File

@ -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<Box<dyn Reflect>>,
}
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,
}
}
}

View File

@ -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<ComponentColumn>,
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<lyra_ecs::query::dynamic::FetchDynamicType<'a>> 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<QueryDynamicType>,
fetchers: Vec<FetchDynamicType>,
archetypes: Vec<NonNull<Archetype>>,
next_archetype: usize,
component_indices: Range<u64>,
}
impl<'a> From<lyra_ecs::query::dynamic::DynamicViewIter<'a>> 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<DynamicType>;
fn next(&mut self) -> Option<Self::Item> {
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<NonNull<TypeRegistry>>
}
impl ReflectedIterator {
#[cfg(feature = "lua")]
pub fn next_lua<'a>(&mut self, lua: &'a mlua::Lua) -> Option<Vec<( (&'a ReflectLuaProxy, NonNull<u8>), 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::<TypeRegistry>()
.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::<ReflectLuaProxy>()
.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
}
}
}

View File

@ -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<u8>,
}
#[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<std::sync::Arc<dyn lyra_resource::ResourceStorage>, 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<u8>, offset: usize, length: usize) -> Result<std::sync::Arc<dyn lyra_resource::ResourceStorage>, 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))
}
}

View File

@ -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<Self> {
match value {
mlua::Value::UserData(ud) => Ok(ud.borrow::<Self>()?.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<mlua::AnyUserData<'lua>>;
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<mlua::AnyUserData<'lua>> {
let this = this.as_any().downcast_ref::<T>().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::<T>().unwrap();
let apply = apply.borrow::<T>()?;
*this = apply.clone();
Ok(())
}
}
#[derive(Clone)]
pub struct ReflectLuaProxy {
fn_as_uservalue: for<'a> fn(lua: &'a Lua, this_ptr: NonNull<u8>) -> mlua::Result<mlua::AnyUserData<'a>>,
fn_apply: for<'a> fn(lua: &'a Lua, this_ptr: NonNull<u8>, apply: &'a mlua::AnyUserData<'a>) -> mlua::Result<()>,
}
impl<'a, T: Reflect + LuaProxy + Clone + mlua::FromLua<'a> + mlua::UserData> FromType<T> for ReflectLuaProxy {
fn from_type() -> Self {
Self {
fn_as_uservalue: |lua, this| -> mlua::Result<mlua::AnyUserData> {
let this = unsafe { this.cast::<T>().as_ref() };
<T as LuaProxy>::as_lua_value(lua, this)
},
fn_apply: |lua, ptr, apply| {
let this = unsafe { ptr.cast::<T>().as_mut() };
<T as LuaProxy>::apply(lua, this, apply)
}
}
}
}
impl<'lua> mlua::FromLua<'lua> for ScriptDynamicBundle {
fn from_lua(value: mlua::Value<'lua>, _lua: &'lua Lua) -> mlua::Result<Self> {
match value {
mlua::Value::UserData(ud) => Ok(ud.borrow::<Self>()?.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(())
});
}
}

View File

@ -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<mlua::Lua>;
fn load_script(&mut self, script: &[u8], script_data: &crate::ScriptData, providers: &mut crate::ScriptApiProviders<Self>) -> Result<Self::ScriptContext, crate::ScriptError> {
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<Self>) -> 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<Self>) -> 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(())
}
}

View File

@ -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<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) => {
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::<LuaHost>::default();
providers.add_provider(test_provider);
let host = LuaHost::default();
world.add_resource(host);
world.add_resource(providers);
world.add_resource(ScriptContexts::<Mutex<mlua::Lua>>::default());
let mut res_loader = ResourceManager::new();
res_loader.register_loader::<LuaLoader>();
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::<LuaScript>("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::<LuaHost>();
let mut contexts = world.get_resource_mut::<ScriptContexts<Mutex<mlua::Lua>>>();
let mut providers = world.get_resource_mut::<ScriptApiProviders<LuaHost>>();
for scripts in world.view_iter::<&ScriptList<LuaScript>>() {
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(())
}

View File

@ -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<Self> {
match value {
mlua::Value::UserData(ud) => Ok(ud.borrow::<Self>()?.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<mlua::AnyUserData>| {
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::<mlua::Result<Vec<mlua::Value>>>()?;
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<mlua::AnyUserData>)| {
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::<TypeRegistry>();
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::<mlua::Result<Vec<mlua::Value>>>()?;
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::<ReflectLuaProxy>().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(())
});
}
}

View File

@ -0,0 +1,57 @@
use lyra_ecs::Component;
use lyra_resource::ResHandle;
use crate::lyra_engine;
#[derive(Clone)]
pub struct Script<T> {
res: ResHandle<T>,
name: String,
id: u64
}
impl<T> Script<T> {
pub fn new(name: &str, script: ResHandle<T>) -> Self {
Self {
res: script,
name: name.to_string(),
id: 0 // TODO: make a counter
}
}
pub fn res_handle(&self) -> ResHandle<T> {
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<T: 'static>(Vec<Script<T>>);
impl<T> ScriptList<T> {
pub fn new(list: Vec<Script<T>>) -> Self {
Self(list)
}
}
impl<T> std::ops::Deref for ScriptList<T> {
type Target = Vec<Script<T>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T> std::ops::DerefMut for ScriptList<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}

View File

@ -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<World>,
}
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<World>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}

View File

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