1use 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};
35pub mod bindings;
37
38pub type RhaiRuntime = RwLock<Engine>;
40
41pub struct RhaiScriptContext {
43 pub ast: AST,
45 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
74pub trait IntoRhaiError {
76 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
89pub trait IntoInteropError {
91 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}
121pub struct RhaiScriptingPlugin {
123 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 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 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 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 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
298fn 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
335pub 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 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
350pub 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)]
361pub 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 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}