bones_scripting/lua/
asset.rs

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/// A Lua script asset.
13///
14/// Lua scripts can be run easily with the [`LuaEngine`] resource.
15#[derive(HasSchema)]
16#[schema(no_clone, no_default)]
17#[type_data(asset_loader("lua", LuaScriptLoader))]
18pub struct LuaScript {
19    /// The lua source for the script.
20    pub source: String,
21}
22
23/// Asset loader for [`LuaScript`].
24struct 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/// A lua plugin asset.
38///
39/// This differs from [`LuaScript`] in that loaded [`LuaPlugin`]s will be automatically registered
40/// and run by the bones framework and [`LuaScript`] must be manually triggered by your systems.
41#[derive(HasSchema)]
42#[schema(no_clone, no_default)]
43#[type_data(asset_loader("plugin.lua", LuaPluginLoader))]
44pub struct LuaPlugin {
45    /// The lua source of the script.
46    pub source: String,
47    /// The lua closures, registered by the script, to run in different system stages.
48    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            // Systems, due to the `SendWrapper` for the lua `Closures` must be dropped on
55            // the lua executor thread
56            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    /// Whether or not the plugin has loaded it's systems.
71    pub fn has_loaded(&self) -> bool {
72        matches!(*self.systems.borrow(), LuaPluginSystemsState::Loaded { .. })
73    }
74
75    /// Load the lua plugin's systems.
76    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            // TODO: Provide a meaningfull name to loaded scripts.
100            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
193/// An atomic cell containing the [`LuaPluginSystemsState`].
194pub type LuaPluginSystemsCell = Arc<AtomicCell<LuaPluginSystemsState>>;
195
196/// The load state of the [`LuaPluginSystems
197#[derive(Default)]
198pub enum LuaPluginSystemsState {
199    /// The systems have not been loaded yet.
200    #[default]
201    NotLoaded,
202    /// The systems have been loaded.
203    Loaded {
204        systems: SendWrapper<LuaPluginSystems>,
205        executor: Arc<ThreadExecutor<'static>>,
206    },
207    /// The [`LuaPlugin`] has been dropped and it's systems have been unloaded.
208    Unloaded,
209}
210
211impl LuaPluginSystemsState {
212    /// Helper to get the loaded systems.
213    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    /// Helper to get the loaded systems mutably.
222    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
231/// The ID of a system stage.
232pub type SystemStageId = Ulid;
233
234/// The systems that have been registered by a lua plugin.
235#[derive(Default)]
236pub struct LuaPluginSystems {
237    /// Startup systems. The bool indicates whether the system has been run yet.
238    pub startup: Vec<(bool, StashedClosure)>,
239    /// Systems that run in the core stages.
240    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}