use std::sync::Arc;
use bevy_tasks::ThreadExecutor;
use futures_lite::future::Boxed as BoxedFuture;
use piccolo::{
Callback, CallbackReturn, Closure, Context, Executor, StashedClosure, Table, UserData,
};
use send_wrapper::SendWrapper;
use crate::prelude::*;
#[derive(HasSchema)]
#[schema(no_clone, no_default)]
#[type_data(asset_loader("lua", LuaScriptLoader))]
pub struct LuaScript {
pub source: String,
}
struct LuaScriptLoader;
impl AssetLoader for LuaScriptLoader {
fn load(&self, _ctx: AssetLoadCtx, bytes: &[u8]) -> BoxedFuture<anyhow::Result<SchemaBox>> {
let bytes = bytes.to_vec();
Box::pin(async move {
let script = LuaScript {
source: String::from_utf8(bytes)?,
};
Ok(SchemaBox::new(script))
})
}
}
#[derive(HasSchema)]
#[schema(no_clone, no_default)]
#[type_data(asset_loader("plugin.lua", LuaPluginLoader))]
pub struct LuaPlugin {
pub source: String,
pub systems: LuaPluginSystemsCell,
}
impl Drop for LuaPlugin {
fn drop(&mut self) {
match std::mem::take(&mut *self.systems.borrow_mut()) {
LuaPluginSystemsState::NotLoaded => (),
LuaPluginSystemsState::Loaded { systems, executor } => {
#[cfg(not(target_arch = "wasm32"))]
executor.spawn(async move { drop(systems) }).detach();
#[cfg(target_arch = "wasm32")]
wasm_bindgen_futures::spawn_local(async move { drop(systems) });
#[cfg(target_arch = "wasm32")]
let _ = executor;
}
LuaPluginSystemsState::Unloaded => (),
}
}
}
impl LuaPlugin {
pub fn has_loaded(&self) -> bool {
matches!(*self.systems.borrow(), LuaPluginSystemsState::Loaded { .. })
}
pub fn load(
&self,
executor: Arc<ThreadExecutor<'static>>,
lua: &mut piccolo::Lua,
) -> Result<(), anyhow::Error> {
if !self.has_loaded() {
*self.systems.borrow_mut() = LuaPluginSystemsState::Loaded {
systems: SendWrapper::new(default()),
executor,
};
self.load_impl(lua)
} else {
Ok(())
}
}
fn load_impl(&self, lua: &mut piccolo::Lua) -> Result<(), anyhow::Error> {
let executor = lua.try_enter(|ctx| {
let env = ctx.singletons().get(ctx, super::bindings::env);
let session_var = UserData::new_static(&ctx, self.systems.clone());
session_var.set_metatable(&ctx, Some(ctx.singletons().get(ctx, session_metatable)));
env.set(ctx, "session", session_var)?;
let closure = Closure::load_with_env(ctx, None, self.source.as_bytes(), env)?;
let ex = Executor::start(ctx, closure.into(), ());
Ok(ctx.registry().stash(&ctx, ex))
})?;
lua.execute::<()>(&executor)?;
Ok(())
}
}
fn session_metatable(ctx: Context) -> Table {
let metatable = Table::new(&ctx);
metatable
.set(
ctx,
"__tostring",
Callback::from_fn(&ctx, |ctx, _fuel, mut stack| {
stack.push_front(
piccolo::String::from_static(&ctx, "Session { add_system_to_stage }").into(),
);
Ok(CallbackReturn::Return)
}),
)
.unwrap();
metatable
.set(
ctx,
"__newindex",
ctx.singletons().get(ctx, super::bindings::no_newindex),
)
.unwrap();
let add_startup_system_callback = ctx.registry().stash(
&ctx,
Callback::from_fn(&ctx, move |ctx, _fuel, mut stack| {
let (this, closure): (UserData, Closure) = stack.consume(ctx)?;
let this = this.downcast_static::<LuaPluginSystemsCell>()?;
let mut systems = this.borrow_mut();
systems
.as_loaded_mut()
.startup
.push((false, ctx.registry().stash(&ctx, closure)));
Ok(CallbackReturn::Return)
}),
);
let add_system_to_stage_callback = ctx.registry().stash(
&ctx,
Callback::from_fn(&ctx, move |ctx, _fuel, mut stack| {
let (this, stage, closure): (UserData, UserData, Closure) = stack.consume(ctx)?;
let this = this.downcast_static::<LuaPluginSystemsCell>()?;
let stage = stage.downcast_static::<CoreStage>()?;
let mut systems = this.borrow_mut();
systems
.as_loaded_mut()
.core_stages
.push((*stage, ctx.registry().stash(&ctx, closure)));
Ok(CallbackReturn::Return)
}),
);
metatable
.set(
ctx,
"__index",
Callback::from_fn(&ctx, move |ctx, _fuel, mut stack| {
let (_this, key): (piccolo::Value, piccolo::String) = stack.consume(ctx)?;
#[allow(clippy::single_match)]
match key.as_bytes() {
b"add_system_to_stage" => {
stack
.push_front(ctx.registry().fetch(&add_system_to_stage_callback).into());
}
b"add_startup_system" => {
stack.push_front(ctx.registry().fetch(&add_startup_system_callback).into());
}
_ => (),
}
Ok(CallbackReturn::Return)
}),
)
.unwrap();
metatable
}
pub type LuaPluginSystemsCell = Arc<AtomicCell<LuaPluginSystemsState>>;
#[derive(Default)]
pub enum LuaPluginSystemsState {
#[default]
NotLoaded,
Loaded {
systems: SendWrapper<LuaPluginSystems>,
executor: Arc<ThreadExecutor<'static>>,
},
Unloaded,
}
impl LuaPluginSystemsState {
pub fn as_loaded(&self) -> &LuaPluginSystems {
match self {
LuaPluginSystemsState::NotLoaded => panic!("Not loaded"),
LuaPluginSystemsState::Loaded { systems, .. } => systems,
LuaPluginSystemsState::Unloaded => panic!("Not loaded"),
}
}
pub fn as_loaded_mut(&mut self) -> &mut LuaPluginSystems {
match self {
LuaPluginSystemsState::NotLoaded => panic!("Not loaded"),
LuaPluginSystemsState::Loaded { systems, .. } => &mut *systems,
LuaPluginSystemsState::Unloaded => panic!("Not loaded"),
}
}
}
pub type SystemStageId = Ulid;
#[derive(Default)]
pub struct LuaPluginSystems {
pub startup: Vec<(bool, StashedClosure)>,
pub core_stages: Vec<(CoreStage, StashedClosure)>,
}
struct LuaPluginLoader;
impl AssetLoader for LuaPluginLoader {
fn load(&self, _ctx: AssetLoadCtx, bytes: &[u8]) -> BoxedFuture<anyhow::Result<SchemaBox>> {
let bytes = bytes.to_vec();
Box::pin(async move {
let script = LuaPlugin {
source: String::from_utf8(bytes)?,
systems: Arc::new(AtomicCell::new(default())),
};
Ok(SchemaBox::new(script))
})
}
}