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_startup_system_callback = ctx.registry().stash(
135 &ctx,
136 Callback::from_fn(&ctx, move |ctx, _fuel, mut stack| {
137 let (this, closure): (UserData, Closure) = stack.consume(ctx)?;
138 let this = this.downcast_static::<LuaPluginSystemsCell>()?;
139
140 let mut systems = this.borrow_mut();
141 systems
142 .as_loaded_mut()
143 .startup
144 .push((false, ctx.registry().stash(&ctx, closure)));
145
146 Ok(CallbackReturn::Return)
147 }),
148 );
149 let add_system_to_stage_callback = ctx.registry().stash(
150 &ctx,
151 Callback::from_fn(&ctx, move |ctx, _fuel, mut stack| {
152 let (this, stage, closure): (UserData, UserData, Closure) = stack.consume(ctx)?;
153 let this = this.downcast_static::<LuaPluginSystemsCell>()?;
154 let stage = stage.downcast_static::<CoreStage>()?;
155
156 let mut systems = this.borrow_mut();
157 systems
158 .as_loaded_mut()
159 .core_stages
160 .push((*stage, ctx.registry().stash(&ctx, closure)));
161
162 Ok(CallbackReturn::Return)
163 }),
164 );
165
166 metatable
167 .set(
168 ctx,
169 "__index",
170 Callback::from_fn(&ctx, move |ctx, _fuel, mut stack| {
171 let (_this, key): (piccolo::Value, piccolo::String) = stack.consume(ctx)?;
172
173 #[allow(clippy::single_match)]
174 match key.as_bytes() {
175 b"add_system_to_stage" => {
176 stack
177 .push_front(ctx.registry().fetch(&add_system_to_stage_callback).into());
178 }
179 b"add_startup_system" => {
180 stack.push_front(ctx.registry().fetch(&add_startup_system_callback).into());
181 }
182 _ => (),
183 }
184
185 Ok(CallbackReturn::Return)
186 }),
187 )
188 .unwrap();
189
190 metatable
191}
192
193pub type LuaPluginSystemsCell = Arc<AtomicCell<LuaPluginSystemsState>>;
195
196#[derive(Default)]
198pub enum LuaPluginSystemsState {
199 #[default]
201 NotLoaded,
202 Loaded {
204 systems: SendWrapper<LuaPluginSystems>,
205 executor: Arc<ThreadExecutor<'static>>,
206 },
207 Unloaded,
209}
210
211impl LuaPluginSystemsState {
212 pub fn as_loaded(&self) -> &LuaPluginSystems {
214 match self {
215 LuaPluginSystemsState::NotLoaded => panic!("Not loaded"),
216 LuaPluginSystemsState::Loaded { systems, .. } => systems,
217 LuaPluginSystemsState::Unloaded => panic!("Not loaded"),
218 }
219 }
220
221 pub fn as_loaded_mut(&mut self) -> &mut LuaPluginSystems {
223 match self {
224 LuaPluginSystemsState::NotLoaded => panic!("Not loaded"),
225 LuaPluginSystemsState::Loaded { systems, .. } => &mut *systems,
226 LuaPluginSystemsState::Unloaded => panic!("Not loaded"),
227 }
228 }
229}
230
231pub type SystemStageId = Ulid;
233
234#[derive(Default)]
236pub struct LuaPluginSystems {
237 pub startup: Vec<(bool, StashedClosure)>,
239 pub core_stages: Vec<(CoreStage, StashedClosure)>,
241}
242
243struct LuaPluginLoader;
244impl AssetLoader for LuaPluginLoader {
245 fn load(&self, _ctx: AssetLoadCtx, bytes: &[u8]) -> BoxedFuture<anyhow::Result<SchemaBox>> {
246 let bytes = bytes.to_vec();
247 Box::pin(async move {
248 let script = LuaPlugin {
249 source: String::from_utf8(bytes)?,
250 systems: Arc::new(AtomicCell::new(default())),
251 };
252 Ok(SchemaBox::new(script))
253 })
254 }
255}