bevy_mod_scripting_rhai/
lib.rs

1//! Rhai scripting language support for Bevy.
2
3use std::{ops::Deref, str::Utf8Error, sync::Arc};
4
5use crate::bindings::script_value::{FromDynamic, IntoDynamic};
6
7use ::{
8    bevy_app::Plugin,
9    bevy_asset::Handle,
10    bevy_ecs::{entity::Entity, world::World},
11};
12use bevy_app::App;
13use bevy_ecs::world::{Mut, WorldId};
14use bevy_log::trace;
15use bevy_mod_scripting_asset::{Language, ScriptAsset};
16use bevy_mod_scripting_bindings::{
17    AppScriptGlobalsRegistry, InteropError, Namespace, PartialReflectExt, ScriptValue,
18    ThreadWorldContainer,
19};
20use bevy_mod_scripting_core::{
21    IntoScriptPluginParams, ScriptingPlugin,
22    callbacks::ScriptCallbacks,
23    config::{GetPluginThreadConfig, ScriptingPluginConfiguration},
24    event::CallbackLabel,
25    make_plugin_config_static,
26    script::ContextPolicy,
27};
28use bevy_mod_scripting_display::DisplayProxy;
29use bevy_mod_scripting_script::ScriptAttachment;
30use bindings::reference::{ReservedKeyword, RhaiReflectReference, RhaiStaticReflectReference};
31use parking_lot::RwLock;
32pub use rhai;
33
34use rhai::{AST, CallFnOptions, Dynamic, Engine, EvalAltResult, FnPtr, ParseError, Scope};
35/// Bindings for rhai.
36pub mod bindings;
37
38/// The rhai runtime type.
39pub type RhaiRuntime = RwLock<Engine>;
40
41/// The rhai context type.
42pub struct RhaiScriptContext {
43    /// The AST of the script
44    pub ast: AST,
45    /// The scope of the script
46    pub scope: Scope<'static>,
47}
48
49make_plugin_config_static!(RhaiScriptingPlugin);
50
51impl IntoScriptPluginParams for RhaiScriptingPlugin {
52    type C = RhaiScriptContext;
53    type R = RhaiRuntime;
54
55    const LANGUAGE: Language = Language::Rhai;
56
57    fn build_runtime() -> Self::R {
58        Engine::new().into()
59    }
60
61    fn handler() -> bevy_mod_scripting_core::handler::HandlerFn<Self> {
62        rhai_callback_handler
63    }
64
65    fn context_loader() -> bevy_mod_scripting_core::context::ContextLoadFn<Self> {
66        rhai_context_load
67    }
68
69    fn context_reloader() -> bevy_mod_scripting_core::context::ContextReloadFn<Self> {
70        rhai_context_reload
71    }
72}
73
74/// A trait for converting types into an [`EvalAltResult`]
75pub trait IntoRhaiError {
76    /// Converts the error into an [`InteropError`]
77    fn into_rhai_error(self) -> Box<EvalAltResult>;
78}
79
80impl IntoRhaiError for InteropError {
81    fn into_rhai_error(self) -> Box<EvalAltResult> {
82        Box::new(rhai::EvalAltResult::ErrorSystem(
83            "ScriptError".to_owned(),
84            Box::new(self),
85        ))
86    }
87}
88
89/// A trait for converting types into an [`InteropError`]
90pub trait IntoInteropError {
91    /// Converts the error into an [`InteropError`]
92    fn into_bms_error(self) -> InteropError;
93}
94
95impl IntoInteropError for Box<EvalAltResult> {
96    fn into_bms_error(self) -> InteropError {
97        match *self {
98            rhai::EvalAltResult::ErrorSystem(message, error) => {
99                if let Some(inner) = error.downcast_ref::<InteropError>() {
100                    inner.clone()
101                } else {
102                    InteropError::external_boxed(error).with_context(message)
103                }
104            }
105            _ => InteropError::external(self),
106        }
107    }
108}
109
110impl IntoInteropError for ParseError {
111    fn into_bms_error(self) -> InteropError {
112        InteropError::external(self)
113    }
114}
115
116impl IntoInteropError for Utf8Error {
117    fn into_bms_error(self) -> InteropError {
118        InteropError::external(self)
119    }
120}
121/// The rhai scripting plugin. Used to add rhai scripting to a bevy app within the context of the BMS framework.
122pub struct RhaiScriptingPlugin {
123    /// The internal scripting plugin
124    pub scripting_plugin: ScriptingPlugin<RhaiScriptingPlugin>,
125}
126
127impl AsMut<ScriptingPlugin<Self>> for RhaiScriptingPlugin {
128    fn as_mut(&mut self) -> &mut ScriptingPlugin<Self> {
129        &mut self.scripting_plugin
130    }
131}
132
133fn register_plugin_globals(ctxt: &mut Engine) {
134    let register_callback_fn = |callback: String, func: FnPtr| {
135        let thread_ctxt = ThreadWorldContainer
136            .try_get_context()
137            .map_err(|e| Box::new(EvalAltResult::ErrorSystem("".to_string(), Box::new(e))))?;
138        let world = thread_ctxt.world;
139        let attachment = thread_ctxt.attachment;
140        world
141            .with_resource_mut(|res: Mut<ScriptCallbacks<RhaiScriptingPlugin>>| {
142                let mut callbacks = res.callbacks.write();
143                callbacks.insert(
144                    (attachment.clone(), callback),
145                    Arc::new(
146                        move |args: Vec<ScriptValue>,
147                              rhai: &mut RhaiScriptContext,
148                              world_id: WorldId| {
149                            let config = RhaiScriptingPlugin::readonly_configuration(world_id);
150                            let pre_handling_callbacks = config.pre_handling_callbacks;
151                            let runtime = config.runtime;
152                            let runtime_guard = runtime.read();
153                            pre_handling_callbacks
154                                .iter()
155                                .try_for_each(|init| init(&attachment, rhai))?;
156
157                            let ret = func
158                                .call::<Dynamic>(&runtime_guard, &rhai.ast, args)
159                                .map_err(IntoInteropError::into_bms_error)?;
160                            ScriptValue::from_dynamic(ret).map_err(IntoInteropError::into_bms_error)
161                        },
162                    ),
163                )
164            })
165            .map_err(|e| Box::new(EvalAltResult::ErrorSystem("".to_string(), Box::new(e))))?;
166        Ok::<_, Box<EvalAltResult>>(())
167    };
168
169    ctxt.register_fn("register_callback", register_callback_fn);
170}
171
172impl Default for RhaiScriptingPlugin {
173    fn default() -> Self {
174        RhaiScriptingPlugin {
175            scripting_plugin: ScriptingPlugin {
176                supported_extensions: vec!["rhai"],
177                runtime_initializers: vec![|runtime| {
178                    let mut engine = runtime.write();
179                    engine.set_max_expr_depths(999, 999);
180                    engine.build_type::<RhaiReflectReference>();
181                    engine.build_type::<RhaiStaticReflectReference>();
182                    engine.register_iterator_result::<RhaiReflectReference, _>();
183                    register_plugin_globals(&mut engine);
184                    Ok(())
185                }],
186                context_initializers: vec![
187                    |_, context| {
188                        context.scope.set_or_push(
189                            "world",
190                            RhaiStaticReflectReference(std::any::TypeId::of::<World>()),
191                        );
192                        Ok(())
193                    },
194                    |_, context| {
195                        // initialize global functions
196                        let world = ThreadWorldContainer.try_get_context()?.world;
197                        let globals_registry =
198                            world.with_resource(|r: &AppScriptGlobalsRegistry| r.clone())?;
199                        let globals_registry = globals_registry.read();
200
201                        for (key, global) in globals_registry.iter() {
202                            match &global.maker {
203                                Some(maker) => {
204                                    let global = (maker)(world.clone())?;
205                                    context.scope.set_or_push(
206                                        key.to_string(),
207                                        global
208                                            .into_dynamic()
209                                            .map_err(IntoInteropError::into_bms_error)?,
210                                    );
211                                }
212                                None => {
213                                    let ref_ = RhaiStaticReflectReference(global.type_id);
214                                    context.scope.set_or_push(key.to_string(), ref_);
215                                }
216                            }
217                        }
218
219                        let mut script_function_registry = world.script_function_registry();
220                        let mut script_function_registry = script_function_registry.write();
221
222                        // iterate all functions, and remap names with reserved keywords
223                        let mut re_insertions = Vec::new();
224                        for (key, function) in script_function_registry.iter_all() {
225                            let name = key.name.clone();
226                            if ReservedKeyword::is_reserved_keyword(&name) {
227                                let new_name = format!("{name}_");
228                                let mut new_function = function.clone();
229                                let new_info =
230                                    function.info.deref().clone().with_name(new_name.clone());
231                                new_function.info = new_info.into();
232                                re_insertions.push((key.namespace, new_name, new_function));
233                            }
234                        }
235                        for (namespace, name, func) in re_insertions {
236                            script_function_registry.raw_insert(namespace, name, func);
237                        }
238
239                        // then go through functions in the global namespace and add them to the lua context
240
241                        for (key, function) in script_function_registry
242                            .iter_all()
243                            .filter(|(k, _)| k.namespace == Namespace::Global)
244                        {
245                            context.scope.set_or_push(
246                                key.name.clone(),
247                                ScriptValue::Function(function.clone())
248                                    .into_dynamic()
249                                    .map_err(IntoInteropError::into_bms_error)?,
250                            );
251                        }
252
253                        Ok(())
254                    },
255                ],
256                context_pre_handling_initializers: vec![|context_key, context| {
257                    let world = ThreadWorldContainer.try_get_context()?.world;
258
259                    if let Some(entity) = context_key.entity() {
260                        context.scope.set_or_push(
261                            "entity",
262                            RhaiReflectReference(<Entity>::allocate(
263                                Box::new(entity),
264                                world.clone(),
265                            )),
266                        );
267                    }
268                    context.scope.set_or_push(
269                        "script_asset",
270                        RhaiReflectReference(<Handle<ScriptAsset>>::allocate(
271                            Box::new(context_key.script().clone()),
272                            world,
273                        )),
274                    );
275
276                    Ok(())
277                }],
278                // already supported by BMS core
279                language: Language::Rhai,
280                context_policy: ContextPolicy::default(),
281                emit_responses: false,
282                processing_pipeline_plugin: Default::default(),
283            },
284        }
285    }
286}
287
288impl Plugin for RhaiScriptingPlugin {
289    fn build(&self, app: &mut App) {
290        self.scripting_plugin.build(app);
291    }
292
293    fn finish(&self, app: &mut App) {
294        self.scripting_plugin.finish(app);
295    }
296}
297
298// NEW helper function to load content into an existing context without clearing previous definitions.
299fn load_rhai_content_into_context(
300    context: &mut RhaiScriptContext,
301    context_key: &ScriptAttachment,
302    content: &[u8],
303    world_id: WorldId,
304) -> Result<(), InteropError> {
305    let config = RhaiScriptingPlugin::readonly_configuration(world_id);
306    let initializers = config.context_initialization_callbacks;
307    let pre_handling_initializers = config.pre_handling_callbacks;
308    let runtime = config.runtime.read();
309
310    context.ast = std::str::from_utf8(content)
311        .map_err(IntoInteropError::into_bms_error)
312        .and_then(|content| {
313            runtime
314                .compile(content)
315                .map_err(IntoInteropError::into_bms_error)
316        })?;
317    context
318        .ast
319        .set_source(context_key.script().display().to_string());
320
321    initializers
322        .iter()
323        .try_for_each(|init| init(context_key, context))?;
324    pre_handling_initializers
325        .iter()
326        .try_for_each(|init| init(context_key, context))?;
327    runtime
328        .eval_ast_with_scope::<()>(&mut context.scope, &context.ast)
329        .map_err(IntoInteropError::into_bms_error)?;
330
331    context.ast.clear_statements();
332    Ok(())
333}
334
335/// Load a rhai context from a script.
336pub fn rhai_context_load(
337    context_key: &ScriptAttachment,
338    content: &[u8],
339    world_id: WorldId,
340) -> Result<RhaiScriptContext, InteropError> {
341    let mut context = RhaiScriptContext {
342        // Using an empty AST as a placeholder.
343        ast: AST::empty(),
344        scope: Scope::new(),
345    };
346    load_rhai_content_into_context(&mut context, context_key, content, world_id)?;
347    Ok(context)
348}
349
350/// Reload a rhai context from a script. New content is appended to the existing context.
351pub fn rhai_context_reload(
352    context_key: &ScriptAttachment,
353    content: &[u8],
354    context: &mut RhaiScriptContext,
355    world_id: WorldId,
356) -> Result<(), InteropError> {
357    load_rhai_content_into_context(context, context_key, content, world_id)
358}
359
360#[allow(clippy::too_many_arguments)]
361/// The rhai callback handler.
362pub fn rhai_callback_handler(
363    args: Vec<ScriptValue>,
364    context_key: &ScriptAttachment,
365    callback: &CallbackLabel,
366    context: &mut RhaiScriptContext,
367    world_id: WorldId,
368) -> Result<ScriptValue, InteropError> {
369    let config = RhaiScriptingPlugin::readonly_configuration(world_id);
370    let pre_handling_initializers = config.pre_handling_callbacks;
371
372    pre_handling_initializers
373        .iter()
374        .try_for_each(|init| init(context_key, context))?;
375
376    // we want the call to be able to impact the scope
377    let options = CallFnOptions::new().rewind_scope(false);
378    let args = args
379        .into_iter()
380        .map(|v| v.into_dynamic())
381        .collect::<Result<Vec<_>, _>>()
382        .map_err(IntoInteropError::into_bms_error)?;
383
384    trace!(
385        "Calling callback {} in context {} with args: {:?}",
386        callback, context_key, args
387    );
388    let runtime = config.runtime.read();
389
390    match runtime.call_fn_with_options::<Dynamic>(
391        options,
392        &mut context.scope,
393        &context.ast,
394        callback.as_ref(),
395        args,
396    ) {
397        Ok(v) => Ok(ScriptValue::from_dynamic(v).map_err(IntoInteropError::into_bms_error)?),
398        Err(e) => {
399            if let EvalAltResult::ErrorFunctionNotFound(_, _) = e.unwrap_inner() {
400                trace!(
401                    "Context {} is not subscribed to callback {} with the provided arguments.",
402                    context_key, callback
403                );
404                Ok(ScriptValue::Unit)
405            } else {
406                Err(e.into_bms_error())
407            }
408        }
409    }
410}