bevy_mod_scripting_rune/
lib.rs

1use std::{marker::PhantomData, sync::Arc};
2
3use bevy::prelude::*;
4use bevy_mod_scripting_core::{
5    prelude::*,
6    systems::{self, CachedScriptState},
7    world::{WorldPointer, WorldPointerGuard},
8};
9use prelude::{RuneDocFragment, RuneFile, RuneLoader};
10use rune::{
11    runtime::{Args, RuntimeContext, VmError, VmResult},
12    Context, Diagnostics, Source, Sources, Unit, Vm,
13};
14
15mod assets;
16mod docs;
17
18pub mod prelude {
19    pub use crate::{
20        assets::{RuneFile, RuneLoader},
21        docs::RuneDocFragment,
22        RuneArgs, RuneEvent, RuneScriptContext, RuneScriptHost,
23    };
24    pub use rune::{self, runtime::Args, Context};
25}
26
27/// Super trait adding additional bounds to Rune's `Args` trait.
28/// It's gets automatically implemented for any type that implments `Args`,
29/// so you should never have to manually implement it.
30pub trait RuneArgs: Args + Clone + Send + Sync + 'static {}
31
32impl<T: Args + Clone + Send + Sync + 'static> RuneArgs for T {}
33
34/// A Rune script hook.
35#[derive(Debug, Clone, Event)]
36pub struct RuneEvent<A: RuneArgs> {
37    /// The name of the Rune function to call.
38    pub hook_name: String,
39    /// The arguments to supply the function being invoked. If you
40    /// don't need any arguments, `()` is a good default value.
41    pub args: A,
42    /// The target set of scripts that should handle this event.
43    pub recipients: Recipients,
44}
45
46impl<A: RuneArgs> ScriptEvent for RuneEvent<A> {
47    fn recipients(&self) -> &Recipients {
48        &self.recipients
49    }
50}
51
52/// A cached Rune Vm used to execute units.
53struct RuneVm(Vm);
54
55impl Default for RuneVm {
56    fn default() -> Self {
57        Self(Vm::new(
58            Arc::new(RuntimeContext::default()),
59            Arc::new(Unit::default()),
60        ))
61    }
62}
63
64/// Script context for a rune script.
65pub struct RuneScriptContext {
66    pub unit: Arc<Unit>,
67    pub runtime_context: Arc<RuntimeContext>,
68}
69
70#[derive(Resource)]
71/// Rune script host. Enables Rune scripting.
72pub struct RuneScriptHost<A: RuneArgs> {
73    _ph: PhantomData<A>,
74}
75
76impl<A: RuneArgs> Default for RuneScriptHost<A> {
77    fn default() -> Self {
78        Self {
79            _ph: Default::default(),
80        }
81    }
82}
83
84impl<A: RuneArgs> RuneScriptHost<A> {
85    /// Helper function to handle errors from a Rune virtual machine.
86    ///
87    #[cold]
88    fn handle_rune_error(world: WorldPointer, error: VmError, script_data: &ScriptData<'_>) {
89        let mut world = world.write();
90        let mut state: CachedScriptState<Self> = world.remove_resource().unwrap();
91
92        let (_, mut error_wrt, _) = state.event_state.get_mut(&mut world);
93
94        let error = ScriptError::RuntimeError {
95            script: script_data.name.to_owned(),
96            msg: error.to_string(),
97        };
98
99        error!("{}", error);
100
101        error_wrt.send(ScriptErrorEvent { error });
102        world.insert_resource(state);
103    }
104}
105
106impl<A: RuneArgs> ScriptHost for RuneScriptHost<A> {
107    type ScriptContext = RuneScriptContext;
108
109    type ScriptEvent = RuneEvent<A>;
110
111    type ScriptAsset = RuneFile;
112
113    type APITarget = Context;
114
115    type DocTarget = RuneDocFragment;
116
117    fn register_with_app_in_set(
118        app: &mut App,
119        schedule: impl bevy::ecs::schedule::ScheduleLabel,
120        set: impl SystemSet,
121    ) {
122        app.add_priority_event::<Self::ScriptEvent>()
123            .init_asset::<RuneFile>()
124            .init_asset_loader::<RuneLoader>()
125            .init_resource::<CachedScriptState<Self>>()
126            .init_resource::<ScriptContexts<Self::ScriptContext>>()
127            .init_resource::<APIProviders<Self>>()
128            .register_type::<ScriptCollection<Self::ScriptAsset>>()
129            .register_type::<Script<Self::ScriptAsset>>()
130            .register_type::<Handle<RuneFile>>()
131            // Add a cached Vm as a non-send resource.
132            .insert_non_send_resource(RuneVm::default())
133            // handle script insertions removal first
134            // then update their contexts later on script asset changes
135            .add_systems(
136                schedule,
137                (
138                    systems::script_add_synchronizer::<Self>,
139                    systems::script_remove_synchronizer::<Self>,
140                    systems::script_hot_reload_handler::<Self>,
141                )
142                    .chain()
143                    .in_set(set),
144            );
145    }
146
147    fn load_script(
148        &mut self,
149        script: &[u8],
150        script_data: &ScriptData,
151        providers: &mut APIProviders<Self>,
152    ) -> Result<Self::ScriptContext, ScriptError> {
153        let mut context = rune_modules::default_context().map_err(ScriptError::new_other)?;
154
155        // Rune requires that we tell it what modules and types we'll be using before
156        // it compiles a file.
157        providers.attach_all(&mut context).unwrap();
158
159        let mut diagnostics = Diagnostics::new();
160
161        let mut sources = Sources::new();
162        sources
163            .insert(
164                Source::new(
165                    script_data.name,
166                    std::str::from_utf8(script).expect("Slice is not UTF-8"),
167                )
168                .map_err(|msg| ScriptError::FailedToLoad {
169                    script: script_data.name.into(),
170                    msg: msg.to_string(),
171                })?,
172            )
173            .map_err(|msg| ScriptError::FailedToLoad {
174                script: script_data.name.into(),
175                msg: msg.to_string(),
176            })?;
177
178        let result = rune::prepare(&mut sources)
179            .with_context(&context)
180            .with_diagnostics(&mut diagnostics)
181            .build();
182
183        if !diagnostics.is_empty() {
184            let mut writer = rune::termcolor::Buffer::no_color();
185
186            diagnostics
187                .emit(&mut writer, &sources)
188                .expect("Failed to write diagnostics to buffer");
189
190            return Err(ScriptError::SyntaxError {
191                script: script_data.name.into(),
192                msg: std::str::from_utf8(writer.as_slice())
193                    .expect("Slice was not UTF-8")
194                    .to_owned(),
195            });
196        }
197
198        let unit = result.expect("Failed to build Rune unit.");
199
200        let runtime_ctx = context
201            .runtime()
202            .expect("Failed to create Rune runtime context.");
203
204        Ok(RuneScriptContext {
205            unit: Arc::new(unit),
206            runtime_context: Arc::new(runtime_ctx),
207        })
208    }
209
210    fn setup_script(
211        &mut self,
212        script_data: &ScriptData,
213        ctx: &mut Self::ScriptContext,
214        providers: &mut APIProviders<Self>,
215    ) -> Result<(), ScriptError> {
216        providers.setup_all(script_data, ctx)
217    }
218
219    fn handle_events<'a>(
220        &mut self,
221        world: &mut World,
222        events: &[Self::ScriptEvent],
223        ctxs: impl Iterator<Item = (ScriptData<'a>, &'a mut Self::ScriptContext)>,
224        providers: &mut APIProviders<Self>,
225    ) {
226        // Grab the cached Vm.
227        let RuneVm(mut vm) = world.remove_non_send_resource::<RuneVm>().unwrap(/* invariant */);
228
229        {
230            // Safety:
231            // - we have &mut World access
232            // - we do not use the original reference again anywhere in this block.
233            // - the guard is dropped at the end of this block.
234            let world = unsafe { WorldPointerGuard::new(world) };
235
236            ctxs.for_each(|(script_data, ctx)| {
237                providers
238                    .setup_runtime_all(world.clone(), &script_data, ctx)
239                    .expect("Could not setup script runtime");
240
241                for event in events {
242                    if !event.recipients().is_recipient(&script_data) {
243                        continue;
244                    }
245
246                    // Swap out the old context and old unit with the new ones.
247                    *vm.context_mut() = Arc::clone(&ctx.runtime_context);
248                    *vm.unit_mut() = Arc::clone(&ctx.unit);
249
250                    let mut exec = match vm.execute([event.hook_name.as_str()], event.args.clone())
251                    {
252                        Ok(exec) => exec,
253                        Err(error) => {
254                            Self::handle_rune_error(world.clone(), error, &script_data);
255                            continue;
256                        }
257                    };
258
259                    if let VmResult::Err(error) = exec.complete() {
260                        Self::handle_rune_error(world.clone(), error, &script_data);
261                    }
262                }
263            });
264
265            // explictly release the pointer to world.
266            drop(world);
267        }
268
269        world.insert_non_send_resource(RuneVm(vm));
270    }
271}