bevy_mod_scripting_core/
lib.rs

1//! Core functionality for the bevy_mod_scripting framework.
2//!
3//! Contains language agnostic systems and types for handling scripting in bevy.
4
5use crate::event::ScriptErrorEvent;
6use asset::{
7    configure_asset_systems, configure_asset_systems_for_plugin, AssetPathToLanguageMapper,
8    Language, ScriptAsset, ScriptAssetLoader, ScriptAssetSettings,
9};
10use bevy::prelude::*;
11use bindings::{
12    function::script_function::AppScriptFunctionRegistry,
13    garbage_collector,
14    globals::{core::CoreScriptGlobalsPlugin, AppScriptGlobalsRegistry},
15    schedule::AppScheduleRegistry,
16    script_value::ScriptValue,
17    AppReflectAllocator, ReflectAllocator, ReflectReference, ScriptTypeRegistration,
18};
19use commands::{AddStaticScript, RemoveStaticScript};
20use context::{
21    Context, ContextAssigner, ContextBuilder, ContextInitializer, ContextLoadingSettings,
22    ContextPreHandlingInitializer, ScriptContexts,
23};
24use error::ScriptError;
25use event::ScriptCallbackEvent;
26use handler::{CallbackSettings, HandlerFn};
27use runtime::{initialize_runtime, Runtime, RuntimeContainer, RuntimeInitializer, RuntimeSettings};
28use script::{ScriptId, Scripts, StaticScripts};
29
30pub mod asset;
31pub mod bindings;
32pub mod commands;
33pub mod context;
34pub mod docgen;
35pub mod error;
36pub mod event;
37pub mod extractors;
38pub mod handler;
39pub mod reflection_extensions;
40pub mod runtime;
41pub mod script;
42
43#[derive(SystemSet, Hash, Debug, Eq, PartialEq, Clone)]
44/// Labels for various BMS systems
45pub enum ScriptingSystemSet {
46    /// Systems which handle the processing of asset events for script assets, and dispatching internal script asset events
47    ScriptAssetDispatch,
48    /// Systems which read incoming internal script asset events and produce script lifecycle commands
49    ScriptCommandDispatch,
50    /// Systems which read incoming script asset events and remove metadata for removed assets
51    ScriptMetadataRemoval,
52
53    /// One time runtime initialization systems
54    RuntimeInitialization,
55
56    /// Systems which handle the garbage collection of allocated values
57    GarbageCollection,
58}
59
60/// Types which act like scripting plugins, by selecting a context and runtime
61/// Each individual combination of context and runtime has specific infrastructure built for it and does not interact with other scripting plugins
62///
63/// When implementing a new scripting plugin, also ensure the following implementations exist:
64/// - [`Plugin`] for the plugin, both [`Plugin::build`] and [`Plugin::finish`] methods need to be dispatched to the underlying [`ScriptingPlugin`] struct
65/// - [`AsMut<ScriptingPlugin<Self>`] for the plugin struct
66pub trait IntoScriptPluginParams: 'static {
67    /// The language of the scripts
68    const LANGUAGE: Language;
69    /// The context type used for the scripts
70    type C: Context;
71    /// The runtime type used for the scripts
72    type R: Runtime;
73
74    /// Build the runtime
75    fn build_runtime() -> Self::R;
76}
77
78/// Bevy plugin enabling scripting within the bevy mod scripting framework
79pub struct ScriptingPlugin<P: IntoScriptPluginParams> {
80    /// Settings for the runtime
81    pub runtime_settings: RuntimeSettings<P>,
82    /// The handler used for executing callbacks in scripts
83    pub callback_handler: HandlerFn<P>,
84    /// The context builder for loading contexts
85    pub context_builder: ContextBuilder<P>,
86    /// The context assigner for assigning contexts to scripts.
87    pub context_assigner: ContextAssigner<P>,
88
89    /// The asset path to language mapper for the plugin
90    pub language_mapper: AssetPathToLanguageMapper,
91
92    /// initializers for the contexts, run when loading the script
93    pub context_initializers: Vec<ContextInitializer<P>>,
94    /// initializers for the contexts run every time before handling events
95    pub context_pre_handling_initializers: Vec<ContextPreHandlingInitializer<P>>,
96
97    /// Supported extensions to be added to the asset settings without the dot
98    pub supported_extensions: &'static [&'static str],
99}
100
101impl<P: IntoScriptPluginParams> Default for ScriptingPlugin<P> {
102    fn default() -> Self {
103        Self {
104            runtime_settings: Default::default(),
105            callback_handler: CallbackSettings::<P>::default().callback_handler,
106            context_builder: Default::default(),
107            context_assigner: Default::default(),
108            language_mapper: Default::default(),
109            context_initializers: Default::default(),
110            context_pre_handling_initializers: Default::default(),
111            supported_extensions: Default::default(),
112        }
113    }
114}
115
116impl<P: IntoScriptPluginParams> Plugin for ScriptingPlugin<P> {
117    fn build(&self, app: &mut bevy::prelude::App) {
118        app.insert_resource(self.runtime_settings.clone())
119            .insert_resource::<RuntimeContainer<P>>(RuntimeContainer {
120                runtime: P::build_runtime(),
121            })
122            .init_resource::<ScriptContexts<P>>()
123            .insert_resource::<CallbackSettings<P>>(CallbackSettings {
124                callback_handler: self.callback_handler,
125            })
126            .insert_resource::<ContextLoadingSettings<P>>(ContextLoadingSettings {
127                loader: self.context_builder.clone(),
128                assigner: self.context_assigner.clone(),
129                context_initializers: self.context_initializers.clone(),
130                context_pre_handling_initializers: self.context_pre_handling_initializers.clone(),
131            });
132
133        register_script_plugin_systems::<P>(app);
134
135        // add extension for the language to the asset loader
136        once_per_app_init(app);
137
138        app.add_supported_script_extensions(self.supported_extensions);
139
140        app.world_mut()
141            .resource_mut::<ScriptAssetSettings>()
142            .as_mut()
143            .script_language_mappers
144            .push(self.language_mapper);
145
146        register_types(app);
147    }
148
149    fn finish(&self, app: &mut App) {
150        once_per_app_finalize(app);
151    }
152}
153
154impl<P: IntoScriptPluginParams> ScriptingPlugin<P> {
155    /// Adds a context initializer to the plugin
156    ///
157    /// Initializers will be run every time a context is loaded or re-loaded and before any events are handled
158    pub fn add_context_initializer(&mut self, initializer: ContextInitializer<P>) -> &mut Self {
159        self.context_initializers.push(initializer);
160        self
161    }
162
163    /// Adds a context pre-handling initializer to the plugin.
164    ///
165    /// Initializers will be run every time before handling events and after the context is loaded or re-loaded.
166    pub fn add_context_pre_handling_initializer(
167        &mut self,
168        initializer: ContextPreHandlingInitializer<P>,
169    ) -> &mut Self {
170        self.context_pre_handling_initializers.push(initializer);
171        self
172    }
173
174    /// Adds a runtime initializer to the plugin.
175    ///
176    /// Initializers will be run after the runtime is created, but before any contexts are loaded.
177    pub fn add_runtime_initializer(&mut self, initializer: RuntimeInitializer<P>) -> &mut Self {
178        self.runtime_settings.initializers.push(initializer);
179        self
180    }
181}
182
183/// Utility trait for configuring all scripting plugins.
184pub trait ConfigureScriptPlugin {
185    /// The type of the plugin to configure
186    type P: IntoScriptPluginParams;
187
188    /// Add a context initializer to the plugin
189    fn add_context_initializer(self, initializer: ContextInitializer<Self::P>) -> Self;
190
191    /// Add a context pre-handling initializer to the plugin
192    fn add_context_pre_handling_initializer(
193        self,
194        initializer: ContextPreHandlingInitializer<Self::P>,
195    ) -> Self;
196
197    /// Add a runtime initializer to the plugin
198    fn add_runtime_initializer(self, initializer: RuntimeInitializer<Self::P>) -> Self;
199
200    /// Switch the context assigning strategy to a global context assigner.
201    ///
202    /// This means that all scripts will share the same context. This is useful for when you want to share data between scripts easilly.
203    /// Be careful however as this also means that scripts can interfere with each other in unexpected ways! Including overwriting each other's handlers.
204    fn enable_context_sharing(self) -> Self;
205}
206
207impl<P: IntoScriptPluginParams + AsMut<ScriptingPlugin<P>>> ConfigureScriptPlugin for P {
208    type P = P;
209
210    fn add_context_initializer(mut self, initializer: ContextInitializer<Self::P>) -> Self {
211        self.as_mut().add_context_initializer(initializer);
212        self
213    }
214
215    fn add_context_pre_handling_initializer(
216        mut self,
217        initializer: ContextPreHandlingInitializer<Self::P>,
218    ) -> Self {
219        self.as_mut()
220            .add_context_pre_handling_initializer(initializer);
221        self
222    }
223
224    fn add_runtime_initializer(mut self, initializer: RuntimeInitializer<Self::P>) -> Self {
225        self.as_mut().add_runtime_initializer(initializer);
226        self
227    }
228
229    fn enable_context_sharing(mut self) -> Self {
230        self.as_mut().context_assigner = ContextAssigner::new_global_context_assigner();
231        self
232    }
233}
234
235fn once_per_app_finalize(app: &mut App) {
236    #[derive(Resource)]
237    struct BMSFinalized;
238
239    if app.world().contains_resource::<BMSFinalized>() {
240        return;
241    }
242    app.insert_resource(BMSFinalized);
243
244    // read extensions from asset settings
245    let asset_settings_extensions = app
246        .world_mut()
247        .get_resource_or_init::<ScriptAssetSettings>()
248        .supported_extensions;
249
250    // convert extensions to static array
251    bevy::log::info!(
252        "Initializing BMS with Supported extensions: {:?}",
253        asset_settings_extensions
254    );
255
256    app.register_asset_loader(ScriptAssetLoader {
257        extensions: asset_settings_extensions,
258        preprocessor: None,
259    });
260
261    // pre-register component id's
262    pre_register_componnents(app);
263}
264
265/// Ensures all types with `ReflectComponent` type data are pre-registered with component ID's
266fn pre_register_componnents(app: &mut App) {
267    let type_registry = app
268        .world_mut()
269        .get_resource_or_init::<AppTypeRegistry>()
270        .clone();
271    let type_registry = type_registry.read();
272
273    let world = app.world_mut();
274    for (_, data) in type_registry.iter_with_data::<ReflectComponent>() {
275        data.register_component(world);
276    }
277}
278
279// One of registration of things that need to be done only once per app
280fn once_per_app_init(app: &mut App) {
281    #[derive(Resource)]
282    struct BMSInitialized;
283
284    if app.world().contains_resource::<BMSInitialized>() {
285        return;
286    }
287    app.insert_resource(BMSInitialized);
288
289    app.add_event::<ScriptErrorEvent>()
290        .add_event::<ScriptCallbackEvent>()
291        .init_resource::<AppReflectAllocator>()
292        .init_resource::<Scripts>()
293        .init_resource::<StaticScripts>()
294        .init_asset::<ScriptAsset>()
295        .init_resource::<AppScriptFunctionRegistry>()
296        .init_resource::<AppScriptGlobalsRegistry>()
297        .insert_resource(AppScheduleRegistry::new());
298
299    app.add_systems(
300        PostUpdate,
301        ((garbage_collector).in_set(ScriptingSystemSet::GarbageCollection),),
302    );
303
304    app.add_plugins(CoreScriptGlobalsPlugin);
305
306    configure_asset_systems(app);
307}
308
309/// Systems registered per-language
310fn register_script_plugin_systems<P: IntoScriptPluginParams>(app: &mut App) {
311    app.add_systems(
312        PostStartup,
313        (initialize_runtime::<P>.pipe(|e: In<Result<(), ScriptError>>| {
314            if let Err(e) = e.0 {
315                error!("Error initializing runtime: {:?}", e);
316            }
317        }))
318        .in_set(ScriptingSystemSet::RuntimeInitialization),
319    );
320
321    configure_asset_systems_for_plugin::<P>(app);
322}
323
324/// Register all types that need to be accessed via reflection
325fn register_types(app: &mut App) {
326    app.register_type::<ScriptValue>();
327    app.register_type::<ScriptTypeRegistration>();
328    app.register_type::<ReflectReference>();
329}
330
331/// Trait for adding a runtime initializer to an app
332pub trait AddRuntimeInitializer {
333    /// Adds a runtime initializer to the app
334    fn add_runtime_initializer<P: IntoScriptPluginParams>(
335        &mut self,
336        initializer: RuntimeInitializer<P>,
337    ) -> &mut Self;
338}
339
340impl AddRuntimeInitializer for App {
341    fn add_runtime_initializer<P: IntoScriptPluginParams>(
342        &mut self,
343        initializer: RuntimeInitializer<P>,
344    ) -> &mut Self {
345        if !self.world_mut().contains_resource::<RuntimeSettings<P>>() {
346            self.world_mut().init_resource::<RuntimeSettings<P>>();
347        }
348        self.world_mut()
349            .resource_mut::<RuntimeSettings<P>>()
350            .as_mut()
351            .initializers
352            .push(initializer);
353        self
354    }
355}
356
357/// Trait for adding static scripts to an app
358pub trait ManageStaticScripts {
359    /// Registers a script id as a static script.
360    ///
361    /// Event handlers will run these scripts on top of the entity scripts.
362    fn add_static_script(&mut self, script_id: impl Into<ScriptId>) -> &mut Self;
363
364    /// Removes a script id from the list of static scripts.
365    ///
366    /// Does nothing if the script id is not in the list.
367    fn remove_static_script(&mut self, script_id: impl Into<ScriptId>) -> &mut Self;
368}
369
370impl ManageStaticScripts for App {
371    fn add_static_script(&mut self, script_id: impl Into<ScriptId>) -> &mut Self {
372        AddStaticScript::new(script_id.into()).apply(self.world_mut());
373        self
374    }
375
376    fn remove_static_script(&mut self, script_id: impl Into<ScriptId>) -> &mut Self {
377        RemoveStaticScript::new(script_id.into()).apply(self.world_mut());
378        self
379    }
380}
381
382/// Trait for adding a supported extension to the script asset settings.
383///
384/// This is only valid in the plugin building phase, as the asset loader will be created in the `finalize` phase.
385/// Any changes to the asset settings after that will not be reflected in the asset loader.
386pub trait ConfigureScriptAssetSettings {
387    /// Adds a supported extension to the asset settings
388    fn add_supported_script_extensions(&mut self, extensions: &[&'static str]) -> &mut Self;
389}
390
391impl ConfigureScriptAssetSettings for App {
392    fn add_supported_script_extensions(&mut self, extensions: &[&'static str]) -> &mut Self {
393        let mut asset_settings = self
394            .world_mut()
395            .get_resource_or_init::<ScriptAssetSettings>();
396
397        let mut new_arr = Vec::from(asset_settings.supported_extensions);
398
399        new_arr.extend(extensions);
400
401        let new_arr_static = Vec::leak(new_arr);
402
403        asset_settings.supported_extensions = new_arr_static;
404
405        self
406    }
407}
408
409#[cfg(test)]
410mod test {
411    use super::*;
412
413    #[tokio::test]
414    async fn test_asset_extensions_correctly_accumulate() {
415        let mut app = App::new();
416        app.init_resource::<ScriptAssetSettings>();
417        app.add_plugins(AssetPlugin::default());
418
419        app.world_mut()
420            .resource_mut::<ScriptAssetSettings>()
421            .supported_extensions = &["lua", "rhai"];
422
423        once_per_app_finalize(&mut app);
424
425        let asset_loader = app
426            .world()
427            .get_resource::<AssetServer>()
428            .expect("Asset loader not found");
429
430        asset_loader
431            .get_asset_loader_with_extension("lua")
432            .await
433            .expect("Lua loader not found");
434
435        asset_loader
436            .get_asset_loader_with_extension("rhai")
437            .await
438            .expect("Rhai loader not found");
439    }
440
441    #[test]
442    fn test_reflect_component_is_preregistered_in_app_finalize() {
443        let mut app = App::new();
444
445        app.add_plugins(AssetPlugin::default());
446
447        #[derive(Component, Reflect)]
448        #[reflect(Component)]
449        struct Comp;
450
451        app.register_type::<Comp>();
452
453        assert!(app.world_mut().component_id::<Comp>().is_none());
454
455        once_per_app_finalize(&mut app);
456
457        assert!(app.world_mut().component_id::<Comp>().is_some());
458    }
459}