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_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
175/// An atomic cell containing the [`LuaPluginSystemsState`].
176pub type LuaPluginSystemsCell = Arc<AtomicCell<LuaPluginSystemsState>>;
177
178/// The load state of the [`LuaPluginSystems
179#[derive(Default)]
180pub enum LuaPluginSystemsState {
181    /// The systems have not been loaded yet.
182    #[default]
183    NotLoaded,
184    /// The systems have been loaded.
185    Loaded {
186        systems: SendWrapper<LuaPluginSystems>,
187        executor: Arc<ThreadExecutor<'static>>,
188    },
189    /// The [`LuaPlugin`] has been dropped and it's systems have been unloaded.
190    Unloaded,
191}
192
193impl LuaPluginSystemsState {
194    /// Helper to get the loaded systems.
195    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    /// Helper to get the loaded systems mutably.
204    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
213/// The ID of a system stage.
214pub type SystemStageId = Ulid;
215
216/// The systems that have been registered by a lua plugin.
217#[derive(Default)]
218pub struct LuaPluginSystems {
219    /// Systems that run in the core stages.
220    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}