1use std::sync::Arc;
2
3use bevy_tasks::ThreadExecutor;
4use futures_lite::future::Boxed as BoxedFuture;
5use piccolo::{
6 Callback, CallbackReturn, Closure, Context, Executor, StashedClosure, Table, UserData,
7};
8use send_wrapper::SendWrapper;
9
10use crate::prelude::*;
11
12#[derive(HasSchema)]
16#[schema(no_clone, no_default)]
17#[type_data(asset_loader("lua", LuaScriptLoader))]
18pub struct LuaScript {
19 pub source: String,
21}
22
23struct LuaScriptLoader;
25impl AssetLoader for LuaScriptLoader {
26 fn load(&self, _ctx: AssetLoadCtx, bytes: &[u8]) -> BoxedFuture<anyhow::Result<SchemaBox>> {
27 let bytes = bytes.to_vec();
28 Box::pin(async move {
29 let script = LuaScript {
30 source: String::from_utf8(bytes)?,
31 };
32 Ok(SchemaBox::new(script))
33 })
34 }
35}
36
37#[derive(HasSchema)]
42#[schema(no_clone, no_default)]
43#[type_data(asset_loader("plugin.lua", LuaPluginLoader))]
44pub struct LuaPlugin {
45 pub source: String,
47 pub systems: LuaPluginSystemsCell,
49}
50impl Drop for LuaPlugin {
51 fn drop(&mut self) {
52 match std::mem::take(&mut *self.systems.borrow_mut()) {
53 LuaPluginSystemsState::NotLoaded => (),
54 LuaPluginSystemsState::Loaded { systems, executor } => {
57 #[cfg(not(target_arch = "wasm32"))]
58 executor.spawn(async move { drop(systems) }).detach();
59 #[cfg(target_arch = "wasm32")]
60 wasm_bindgen_futures::spawn_local(async move { drop(systems) });
61 #[cfg(target_arch = "wasm32")]
62 let _ = executor;
63 }
64 LuaPluginSystemsState::Unloaded => (),
65 }
66 }
67}
68
69impl LuaPlugin {
70 pub fn has_loaded(&self) -> bool {
72 matches!(*self.systems.borrow(), LuaPluginSystemsState::Loaded { .. })
73 }
74
75 pub fn load(
77 &self,
78 executor: Arc<ThreadExecutor<'static>>,
79 lua: &mut piccolo::Lua,
80 ) -> Result<(), anyhow::Error> {
81 if !self.has_loaded() {
82 *self.systems.borrow_mut() = LuaPluginSystemsState::Loaded {
83 systems: SendWrapper::new(default()),
84 executor,
85 };
86 self.load_impl(lua)
87 } else {
88 Ok(())
89 }
90 }
91 fn load_impl(&self, lua: &mut piccolo::Lua) -> Result<(), anyhow::Error> {
92 let executor = lua.try_enter(|ctx| {
93 let env = ctx.singletons().get(ctx, super::bindings::env);
94
95 let session_var = UserData::new_static(&ctx, self.systems.clone());
96 session_var.set_metatable(&ctx, Some(ctx.singletons().get(ctx, session_metatable)));
97 env.set(ctx, "session", session_var)?;
98
99 let closure = Closure::load_with_env(ctx, None, self.source.as_bytes(), env)?;
101 let ex = Executor::start(ctx, closure.into(), ());
102 Ok(ctx.registry().stash(&ctx, ex))
103 })?;
104
105 lua.execute::<()>(&executor)?;
106
107 Ok(())
108 }
109}
110
111fn session_metatable(ctx: Context) -> Table {
112 let metatable = Table::new(&ctx);
113
114 metatable
115 .set(
116 ctx,
117 "__tostring",
118 Callback::from_fn(&ctx, |ctx, _fuel, mut stack| {
119 stack.push_front(
120 piccolo::String::from_static(&ctx, "Session { add_system_to_stage }").into(),
121 );
122 Ok(CallbackReturn::Return)
123 }),
124 )
125 .unwrap();
126 metatable
127 .set(
128 ctx,
129 "__newindex",
130 ctx.singletons().get(ctx, super::bindings::no_newindex),
131 )
132 .unwrap();
133
134 let add_system_to_stage_callback = ctx.registry().stash(
135 &ctx,
136 Callback::from_fn(&ctx, move |ctx, _fuel, mut stack| {
137 let (this, stage, closure): (UserData, UserData, Closure) = stack.consume(ctx)?;
138 let this = this.downcast_static::<LuaPluginSystemsCell>()?;
139 let stage = stage.downcast_static::<CoreStage>()?;
140
141 let mut systems = this.borrow_mut();
142 systems
143 .as_loaded_mut()
144 .core_stages
145 .push((*stage, ctx.registry().stash(&ctx, closure)));
146
147 Ok(CallbackReturn::Return)
148 }),
149 );
150
151 metatable
152 .set(
153 ctx,
154 "__index",
155 Callback::from_fn(&ctx, move |ctx, _fuel, mut stack| {
156 let (_this, key): (piccolo::Value, piccolo::String) = stack.consume(ctx)?;
157
158 #[allow(clippy::single_match)]
159 match key.as_bytes() {
160 b"add_system_to_stage" => {
161 stack
162 .push_front(ctx.registry().fetch(&add_system_to_stage_callback).into());
163 }
164 _ => (),
165 }
166
167 Ok(CallbackReturn::Return)
168 }),
169 )
170 .unwrap();
171
172 metatable
173}
174
175pub type LuaPluginSystemsCell = Arc<AtomicCell<LuaPluginSystemsState>>;
177
178#[derive(Default)]
180pub enum LuaPluginSystemsState {
181 #[default]
183 NotLoaded,
184 Loaded {
186 systems: SendWrapper<LuaPluginSystems>,
187 executor: Arc<ThreadExecutor<'static>>,
188 },
189 Unloaded,
191}
192
193impl LuaPluginSystemsState {
194 pub fn as_loaded(&self) -> &LuaPluginSystems {
196 match self {
197 LuaPluginSystemsState::NotLoaded => panic!("Not loaded"),
198 LuaPluginSystemsState::Loaded { systems, .. } => systems,
199 LuaPluginSystemsState::Unloaded => panic!("Not loaded"),
200 }
201 }
202
203 pub fn as_loaded_mut(&mut self) -> &mut LuaPluginSystems {
205 match self {
206 LuaPluginSystemsState::NotLoaded => panic!("Not loaded"),
207 LuaPluginSystemsState::Loaded { systems, .. } => &mut *systems,
208 LuaPluginSystemsState::Unloaded => panic!("Not loaded"),
209 }
210 }
211}
212
213pub type SystemStageId = Ulid;
215
216#[derive(Default)]
218pub struct LuaPluginSystems {
219 pub core_stages: Vec<(CoreStage, StashedClosure)>,
221}
222
223struct LuaPluginLoader;
224impl AssetLoader for LuaPluginLoader {
225 fn load(&self, _ctx: AssetLoadCtx, bytes: &[u8]) -> BoxedFuture<anyhow::Result<SchemaBox>> {
226 let bytes = bytes.to_vec();
227 Box::pin(async move {
228 let script = LuaPlugin {
229 source: String::from_utf8(bytes)?,
230 systems: Arc::new(AtomicCell::new(default())),
231 };
232 Ok(SchemaBox::new(script))
233 })
234 }
235}