1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
//! ⚠️ **Pre-release, alpha version**: API is bound to change, bugs are to be expected.
//!
//! bevy_scriptum is a a plugin for [Bevy](https://bevyengine.org/) that allows you to write some of your game logic in a scripting language.
//! Currently, only [Rhai](https://rhai.rs/) is supported, but more languages may be added in the future.
//!
//! It's main advantages include:
//! - low-boilerplate
//! - easy to use
//! - asynchronicity with a promise-based API
//! - flexibility
//! - hot-reloading
//!
//! Scripts are separate files that can be hot-reloaded at runtime. This allows you to quickly iterate on your game logic without having to recompile your game.
//!
//! All you need to do is register callbacks on your Bevy app like this:
//! ```rust
//! use bevy::prelude::*;
//! use bevy_scriptum::prelude::*;
//!
//! App::new()
//!     .add_plugins(DefaultPlugins)
//!     .add_plugins(ScriptingPlugin::default())
//!     .add_script_function(String::from("hello_bevy"), || {
//!       println!("hello bevy, called from script");
//!     });
//! ```
//! And you can call them in your scripts like this:
//! ```rhai
//! hello_bevy();
//! ```
//!
//! Every callback function that you expose to the scripting language is also a Bevy system, so you can easily query and mutate ECS components and resources just like you would in a regular Bevy system:
//!
//! ```rust
//! use bevy::prelude::*;
//! use bevy_scriptum::prelude::*;
//!
//! #[derive(Component)]
//! struct Player;
//!
//! App::new()
//!     .add_plugins(DefaultPlugins)
//!     .add_plugins(ScriptingPlugin::default())
//!     .add_script_function(
//!         String::from("print_player_names"),
//!         |players: Query<&Name, With<Player>>| {
//!             for player in &players {
//!                 println!("player name: {}", player);
//!             }
//!         },
//!     );
//! ```
//!
//! You can also pass arguments to your callback functions, just like you would in a regular Bevy system - using `In` structs with tuples:
//! ```rust
//! use bevy::prelude::*;
//! use bevy_scriptum::prelude::*;
//! use rhai::ImmutableString;
//!
//! App::new()
//!     .add_plugins(DefaultPlugins)
//!     .add_plugins(ScriptingPlugin::default())
//!     .add_script_function(
//!         String::from("fun_with_string_param"),
//!         |In((x,)): In<(ImmutableString,)>| {
//!             println!("called with string: '{}'", x);
//!         },
//!     );
//! ```
//! which you can then call in your script like this:
//! ```rhai
//! fun_with_string_param("Hello world!");
//! ```
//!
//! ## Usage
//!
//! Add the following to your `Cargo.toml`:
//!
//! ```toml
//! [dependencies]
//! bevy_scriptum = "0.2"
//! ```
//!
//! or execute `cargo add bevy_scriptum` from your project directory.
//!
//! Add the following to your `main.rs`:
//!
//! ```rust
//! use bevy::prelude::*;
//! use bevy_scriptum::prelude::*;
//!
//! App::new()
//!     .add_plugins(DefaultPlugins)
//!     .add_plugins(ScriptingPlugin::default())
//!     .run();
//! ```
//!
//! You can now start exposing functions to the scripting language. For example, you can expose a function that prints a message to the console:
//!
//! ```rust
//! use rhai::ImmutableString;
//! use bevy::prelude::*;
//! use bevy_scriptum::prelude::*;
//!
//! App::new()
//!     .add_plugins(DefaultPlugins)
//!     .add_plugins(ScriptingPlugin::default())
//!     .add_script_function(
//!         String::from("my_print"),
//!         |In((x,)): In<(ImmutableString,)>| {
//!             println!("my_print: '{}'", x);
//!         },
//!     );
//! ```
//!
//! Then you can create a script file in `assets` directory called `script.rhai` that calls this function:
//!
//! ```rhai
//! my_print("Hello world!");
//! ```
//!
//! And spawn a `Script` component with a handle to a script source file`:
//!
//! ```rust
//! use bevy::prelude::*;
//! use bevy_scriptum::Script;
//!
//! App::new()
//!     .add_systems(Startup,|mut commands: Commands, asset_server: Res<AssetServer>| {
//!         commands.spawn(Script::new(asset_server.load("script.rhai")));
//!     });
//! ```
//!
//! ## Provided examples
//!
//! You can also try running provided examples by cloning this repository and running `cargo run --example <example_name>`.  For example:
//!
//! ```bash
//! cargo run --example hello_world
//! ```
//! The examples live in `examples` directory and their corresponding scripts live in `assets/examples` directory within the repository.
//!
//! ## Bevy compatibility
//!
//! | bevy version | bevy_scriptum version |
//! |--------------|----------------------|
//! | 0.11         | 0.2                  |
//! | 0.10         | 0.1                  |
//!
//! ## Promises - getting return values from scripts
//!
//! Every function called from script returns a promise that you can call `.then` with a callback function on. This callback function will be called when the promise is resolved, and will be passed the return value of the function called from script. For example:
//!
//! ```rhai
//! get_player_name().then(|name| {
//!     print(name);
//! });
//! ```
//!
//! ## Access entity from script
//!
//! A variable called `entity` is automatically available to all scripts - it represents bevy entity that the `Script` component is attached to.
//! It exposes `.index()` method that returns bevy entity index.
//! It is useful for accessing entity's components from scripts.
//! It can be used in the following way:
//! ```rhai
//! print("Current entity index: " + entity.index());
//! ```
//!
//! ## Contributing
//!
//! Contributions are welcome! Feel free to open an issue or submit a pull request.
//!
//! ## License
//!
//! bevy_scriptum is licensed under either of the following, at your option:
//! Apache License, Version 2.0, (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0) or MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)

mod assets;
mod callback;
mod components;
mod promise;
mod systems;

use std::sync::{Arc, Mutex};

pub use crate::components::{Script, ScriptData};
pub use assets::RhaiScript;

use bevy::prelude::*;
use callback::{Callback, RegisterCallbackFunction};
use rhai::{CallFnOptions, Dynamic, Engine, EvalAltResult, FuncArgs, ParseError};
use systems::{init_callbacks, init_engine, log_errors, process_calls};
use thiserror::Error;

use self::{
    assets::RhaiScriptLoader,
    systems::{process_new_scripts, reload_scripts},
};

const ENTITY_VAR_NAME: &str = "entity";

/// An error that can occur when internal [ScriptingPlugin] systems are being executed
#[derive(Error, Debug)]
pub enum ScriptingError {
    #[error("script runtime error: {0}")]
    RuntimeError(#[from] Box<EvalAltResult>),
    #[error("script compilation error: {0}")]
    CompileError(#[from] ParseError),
    #[error("no runtime resource present")]
    NoRuntimeResource,
    #[error("no settings resource present")]
    NoSettingsResource,
}

#[derive(Default)]
pub struct ScriptingPlugin;

impl Plugin for ScriptingPlugin {
    fn build(&self, app: &mut App) {
        app.add_asset::<RhaiScript>()
            .init_asset_loader::<RhaiScriptLoader>()
            .init_resource::<Callbacks>()
            .insert_resource(ScriptingRuntime::default())
            .add_systems(Startup, init_engine.pipe(log_errors))
            .add_systems(
                Update,
                (
                    reload_scripts,
                    process_calls.pipe(log_errors).after(process_new_scripts),
                    init_callbacks.pipe(log_errors),
                    process_new_scripts.pipe(log_errors).after(init_callbacks),
                ),
            );
    }
}

#[derive(Resource, Default)]
pub struct ScriptingRuntime {
    engine: Engine,
}

impl ScriptingRuntime {
    /// Get a  mutable reference to the internal [rhai::Engine].
    pub fn engine_mut(&mut self) -> &mut Engine {
        &mut self.engine
    }

    /// Call a function that is available in the scope of the script.
    pub fn call_fn(
        &mut self,
        function_name: &str,
        script_data: &mut ScriptData,
        entity: Entity,
        args: impl FuncArgs,
    ) -> Result<(), ScriptingError> {
        let ast = script_data.ast.clone();
        let scope = &mut script_data.scope;
        scope.push(ENTITY_VAR_NAME, entity);
        let options = CallFnOptions::new().eval_ast(false);
        let result =
            self.engine
                .call_fn_with_options::<Dynamic>(options, scope, &ast, function_name, args);
        scope.remove::<Entity>(ENTITY_VAR_NAME).unwrap();
        if let Err(err) = result {
            match *err {
                rhai::EvalAltResult::ErrorFunctionNotFound(name, _) if name == function_name => {}
                e => Err(Box::new(e))?,
            }
        }
        Ok(())
    }
}

/// An extension trait for [App] that allows to register a script function.
pub trait AddScriptFunctionAppExt {
    fn add_script_function<
        Out,
        Marker,
        A: 'static,
        const N: usize,
        const X: bool,
        R: 'static,
        const F: bool,
        Args,
    >(
        &mut self,
        name: String,
        system: impl RegisterCallbackFunction<Out, Marker, A, N, X, R, F, Args>,
    ) -> &mut Self;
}

/// A resource that stores all the callbacks that were registered using [AddScriptFunctionAppExt::add_script_function].
#[derive(Resource, Default)]
struct Callbacks {
    uninitialized_callbacks: Vec<Callback>,
    callbacks: Mutex<Vec<Callback>>,
}

impl AddScriptFunctionAppExt for App {
    fn add_script_function<
        Out,
        Marker,
        A: 'static,
        const N: usize,
        const X: bool,
        R: 'static,
        const F: bool,
        Args,
    >(
        &mut self,
        name: String,
        system: impl RegisterCallbackFunction<Out, Marker, A, N, X, R, F, Args>,
    ) -> &mut Self {
        let system = system.into_callback_system(&mut self.world);
        let mut callbacks_resource = self.world.resource_mut::<Callbacks>();

        callbacks_resource.uninitialized_callbacks.push(Callback {
            name,
            system: Arc::new(Mutex::new(system)),
            calls: Arc::new(Mutex::new(vec![])),
        });
        self
    }
}

pub mod prelude {
    pub use crate::{AddScriptFunctionAppExt, ScriptingPlugin};
}