From fc4a525182cf16250db1f236cc40f8d4315eb19f Mon Sep 17 00:00:00 2001 From: Jaroslaw Konik Date: Wed, 21 Jun 2023 16:12:49 +0200 Subject: [PATCH] First commit --- .github/workflows/rust.yml | 28 ++ .gitignore | 3 + Cargo.toml | 14 + LICENSE-APACHE | 176 ++++++++++ LICENSE-MIT | 19 ++ README.md | 179 ++++++++++ README.tpl | 3 + assets/examples/call_function_from_rust.rhai | 13 + assets/examples/current_entity.rhai | 13 + assets/examples/custom_type.rhai | 5 + assets/examples/ecs.rhai | 1 + assets/examples/entity_variable.rhai | 3 + assets/examples/function_params.rhai | 4 + assets/examples/hello_world.rhai | 1 + assets/examples/promises.rhai | 3 + examples/call_function_from_rust.rs | 44 +++ examples/current_entity.rs | 23 ++ examples/custom_type.rs | 37 +++ examples/ecs.rs | 29 ++ examples/entity_variable.rs | 16 + examples/function_params.rs | 47 +++ examples/hello_world.rs | 17 + examples/non_closure_system.rs | 19 ++ examples/promises.rs | 22 ++ src/assets.rs | 33 ++ src/callback.rs | 123 +++++++ src/components.rs | 24 ++ src/lib.rs | 326 +++++++++++++++++++ src/promise.rs | 83 +++++ src/systems.rs | 185 +++++++++++ 30 files changed, 1493 insertions(+) create mode 100644 .github/workflows/rust.yml create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 LICENSE-APACHE create mode 100644 LICENSE-MIT create mode 100644 README.md create mode 100644 README.tpl create mode 100644 assets/examples/call_function_from_rust.rhai create mode 100644 assets/examples/current_entity.rhai create mode 100644 assets/examples/custom_type.rhai create mode 100644 assets/examples/ecs.rhai create mode 100644 assets/examples/entity_variable.rhai create mode 100644 assets/examples/function_params.rhai create mode 100644 assets/examples/hello_world.rhai create mode 100644 assets/examples/promises.rhai create mode 100644 examples/call_function_from_rust.rs create mode 100644 examples/current_entity.rs create mode 100644 examples/custom_type.rs create mode 100644 examples/ecs.rs create mode 100644 examples/entity_variable.rs create mode 100644 examples/function_params.rs create mode 100644 examples/hello_world.rs create mode 100644 examples/non_closure_system.rs create mode 100644 examples/promises.rs create mode 100644 src/assets.rs create mode 100644 src/callback.rs create mode 100644 src/components.rs create mode 100644 src/lib.rs create mode 100644 src/promise.rs create mode 100644 src/systems.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 00000000..ef0a7455 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,28 @@ +name: Rust + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Clippy + run: cargo clippy --verbose -- -D warnings + - name: Build + run: cargo build --verbose + - name: Run tests + run: cargo test --verbose + - name: Install cargo-examples + run: cargo install cargo-examples + - name: Run all examples + run: cargo examples diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..5b70a898 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +/Cargo.lock +.vscode diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..ac96ef25 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "bevy_scriptum" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bevy = { default-features = false, version = "0.10.1", features = [ + "bevy_asset", +] } +serde = "1.0.162" +rhai = { version = "1.14.0", features = ["sync", "internals", "unchecked"] } +thiserror = "1.0.40" diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 00000000..d9a10c0d --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 00000000..9cf10627 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,19 @@ +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..5886142e --- /dev/null +++ b/README.md @@ -0,0 +1,179 @@ +# bevy_scriptum 📜 + +⚠️ **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. + +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_plugin(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_plugin(ScriptingPlugin::default()) + .add_script_function( + String::from("print_player_names"), + |players: Query<&Name, With>| { + 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_plugin(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!"); +``` + +Currently, only [Rhai](https://rhai.rs/) is supported, but more languages may be added in the future. + +### Usage + +Add the following to your `Cargo.toml`: + +```toml +[dependencies] +bevy_scriptum = "0.1" +``` + +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_plugin(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_plugin(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_startup_system(|mut commands: Commands, asset_server: Res| { + 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 `. 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.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) diff --git a/README.tpl b/README.tpl new file mode 100644 index 00000000..eed1980c --- /dev/null +++ b/README.tpl @@ -0,0 +1,3 @@ +# {{crate}} 📜 + +{{readme}} diff --git a/assets/examples/call_function_from_rust.rhai b/assets/examples/call_function_from_rust.rhai new file mode 100644 index 00000000..7c287c21 --- /dev/null +++ b/assets/examples/call_function_from_rust.rhai @@ -0,0 +1,13 @@ +let my_state = #{ + iterations: 0, +}; + +fn on_update() { + my_state.iterations += 1; + print("on_update called " + my_state.iterations + " times"); + + if (my_state.iterations >= 10) { + print("calling quit"); + quit(); + } +} diff --git a/assets/examples/current_entity.rhai b/assets/examples/current_entity.rhai new file mode 100644 index 00000000..24561e71 --- /dev/null +++ b/assets/examples/current_entity.rhai @@ -0,0 +1,13 @@ +// entity is a global variable that is set to the entity that is currently being processed, +// it is automatically available in all scripts + +// get name of the entity using registered function +get_name(entity).then(|name| { + print(name); +}); + +// Rhai also supports calling functions with the dot operator +entity.get_name().then(|name| { + print(name); +}) + diff --git a/assets/examples/custom_type.rhai b/assets/examples/custom_type.rhai new file mode 100644 index 00000000..438dc939 --- /dev/null +++ b/assets/examples/custom_type.rhai @@ -0,0 +1,5 @@ +// Create a new instance of MyType +let my_type = new_my_type(); +// Call registered method +print(my_type.my_method()); + diff --git a/assets/examples/ecs.rhai b/assets/examples/ecs.rhai new file mode 100644 index 00000000..04d5c5cf --- /dev/null +++ b/assets/examples/ecs.rhai @@ -0,0 +1 @@ +print_player_names(); diff --git a/assets/examples/entity_variable.rhai b/assets/examples/entity_variable.rhai new file mode 100644 index 00000000..963eaff4 --- /dev/null +++ b/assets/examples/entity_variable.rhai @@ -0,0 +1,3 @@ +// entity is a global variable that is set to the entity that is currently being processed, +// it is automatically available in all scripts +print("Current entity index: " + entity.index()); diff --git a/assets/examples/function_params.rhai b/assets/examples/function_params.rhai new file mode 100644 index 00000000..acc5226d --- /dev/null +++ b/assets/examples/function_params.rhai @@ -0,0 +1,4 @@ +fun_with_string_param("hello"); +fun_with_i64_param(5); +fun_with_multiple_params(5, "hello"); +fun_with_i64_and_array_param(5, [1, 2, "third element"]); diff --git a/assets/examples/hello_world.rhai b/assets/examples/hello_world.rhai new file mode 100644 index 00000000..bd18b19a --- /dev/null +++ b/assets/examples/hello_world.rhai @@ -0,0 +1 @@ +hello_bevy(); diff --git a/assets/examples/promises.rhai b/assets/examples/promises.rhai new file mode 100644 index 00000000..29b4450b --- /dev/null +++ b/assets/examples/promises.rhai @@ -0,0 +1,3 @@ +get_player_name().then(|name| { + print(name); +}); diff --git a/examples/call_function_from_rust.rs b/examples/call_function_from_rust.rs new file mode 100644 index 00000000..4bbf19ef --- /dev/null +++ b/examples/call_function_from_rust.rs @@ -0,0 +1,44 @@ +use bevy::{app::AppExit, ecs::event::ManualEventReader, prelude::*}; +use bevy_scriptum::{prelude::*, Script, ScriptData, ScriptingRuntime}; + +fn main() { + App::new() + // This is just needed for headless console app, not needed for a regular bevy game + // that uses a winit window + .set_runner(move |mut app: App| { + let mut app_exit_event_reader = ManualEventReader::::default(); + loop { + if let Some(app_exit_events) = app.world.get_resource_mut::>() { + if let Some(_) = app_exit_event_reader.iter(&app_exit_events).last() { + break; + } + } + app.update(); + } + }) + .add_plugins(DefaultPlugins) + .add_plugin(ScriptingPlugin::default()) + .add_startup_system(startup) + .add_system(call_rhai_on_update_from_rust) + .add_script_function(String::from("quit"), |mut exit: EventWriter| { + exit.send(AppExit); + }) + .run(); +} + +fn startup(mut commands: Commands, assets_server: Res) { + commands.spawn(Script::new( + assets_server.load("examples/call_function_from_rust.rhai"), + )); +} + +fn call_rhai_on_update_from_rust( + mut scripted_entities: Query<(Entity, &mut ScriptData)>, + mut scripting_runtime: ResMut, +) { + for (entity, mut script_data) in &mut scripted_entities { + scripting_runtime + .call_fn("on_update", &mut script_data, entity, ()) + .unwrap(); + } +} diff --git a/examples/current_entity.rs b/examples/current_entity.rs new file mode 100644 index 00000000..a56a22c5 --- /dev/null +++ b/examples/current_entity.rs @@ -0,0 +1,23 @@ +use bevy::{prelude::*}; +use bevy_scriptum::{prelude::*, Script}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugin(ScriptingPlugin::default()) + .add_script_function( + String::from("get_name"), + |In((entity,)): In<(Entity,)>, names: Query<&Name>| { + names.get(entity).unwrap().to_string() + }, + ) + .add_startup_system(startup) + .run(); +} + +fn startup(mut commands: Commands, assets_server: Res) { + commands.spawn(( + Name::from("MyEntityName"), + Script::new(assets_server.load("examples/current_entity.rhai")), + )); +} diff --git a/examples/custom_type.rs b/examples/custom_type.rs new file mode 100644 index 00000000..d4406ead --- /dev/null +++ b/examples/custom_type.rs @@ -0,0 +1,37 @@ +use bevy::{prelude::*}; +use bevy_scriptum::{prelude::*, Script, ScriptingRuntime}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugin(ScriptingPlugin::default()) + .add_script_function(String::from("hello_bevy"), || { + println!("hello bevy, called from script"); + }) + .add_startup_system(startup) + .run(); +} + +#[derive(Clone)] +struct MyType { + my_field: u32, +} + +fn startup( + mut commands: Commands, + mut scripting_runtime: ResMut, + assets_server: Res, +) { + let engine = scripting_runtime.engine_mut(); + + engine + .register_type_with_name::("MyType") + // Register a method on MyType + .register_fn("my_method", |my_type_instance: &mut MyType| { + my_type_instance.my_field + }) + // Register a "constructor" for MyType + .register_fn("new_my_type", || MyType { my_field: 42 }); + + commands.spawn(Script::new(assets_server.load("examples/custom_type.rhai"))); +} diff --git a/examples/ecs.rs b/examples/ecs.rs new file mode 100644 index 00000000..03cb9617 --- /dev/null +++ b/examples/ecs.rs @@ -0,0 +1,29 @@ +use bevy::{prelude::*}; +use bevy_scriptum::{prelude::*, Script}; + +#[derive(Component)] +struct Player; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugin(ScriptingPlugin::default()) + .add_script_function( + String::from("print_player_names"), + |players: Query<&Name, With>| { + for player in &players { + println!("player name: {}", player); + } + }, + ) + .add_startup_system(startup) + .run(); +} + +fn startup(mut commands: Commands, assets_server: Res) { + commands.spawn((Player, Name::new("John"))); + commands.spawn((Player, Name::new("Mary"))); + commands.spawn((Player, Name::new("Alice"))); + + commands.spawn(Script::new(assets_server.load("examples/ecs.rhai"))); +} diff --git a/examples/entity_variable.rs b/examples/entity_variable.rs new file mode 100644 index 00000000..803c13ba --- /dev/null +++ b/examples/entity_variable.rs @@ -0,0 +1,16 @@ +use bevy::{prelude::*}; +use bevy_scriptum::{prelude::*, Script}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugin(ScriptingPlugin::default()) + .add_startup_system(startup) + .run(); +} + +fn startup(mut commands: Commands, assets_server: Res) { + commands.spawn(Script::new( + assets_server.load("examples/entity_variable.rhai"), + )); +} diff --git a/examples/function_params.rs b/examples/function_params.rs new file mode 100644 index 00000000..0f547009 --- /dev/null +++ b/examples/function_params.rs @@ -0,0 +1,47 @@ +use bevy::{prelude::*}; +use bevy_scriptum::{prelude::*, Script}; +use rhai::ImmutableString; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugin(ScriptingPlugin::default()) + .add_script_function(String::from("fun_without_params"), || { + println!("called without params"); + }) + .add_script_function( + String::from("fun_with_string_param"), + |In((x,)): In<(ImmutableString,)>| { + println!("called with string: '{}'", x); + }, + ) + .add_script_function( + String::from("fun_with_i64_param"), + |In((x,)): In<(i64,)>| { + println!("called with i64: {}", x); + }, + ) + .add_script_function( + String::from("fun_with_multiple_params"), + |In((x, y)): In<(i64, ImmutableString)>| { + println!("called with i64: {} and string: '{}'", x, y); + }, + ) + .add_script_function( + String::from("fun_with_i64_and_array_param"), + |In((x, y)): In<(i64, rhai::Array)>| { + println!( + "called with i64: {} and dynamically typed array: '{:?}'", + x, y + ); + }, + ) + .add_startup_system(startup) + .run(); +} + +fn startup(mut commands: Commands, assets_server: Res) { + commands.spawn(Script::new( + assets_server.load("examples/function_params.rhai"), + )); +} diff --git a/examples/hello_world.rs b/examples/hello_world.rs new file mode 100644 index 00000000..49ce919c --- /dev/null +++ b/examples/hello_world.rs @@ -0,0 +1,17 @@ +use bevy::{prelude::*}; +use bevy_scriptum::{prelude::*, Script}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugin(ScriptingPlugin::default()) + .add_script_function(String::from("hello_bevy"), || { + println!("hello bevy, called from script"); + }) + .add_startup_system(startup) + .run(); +} + +fn startup(mut commands: Commands, assets_server: Res) { + commands.spawn(Script::new(assets_server.load("examples/hello_world.rhai"))); +} diff --git a/examples/non_closure_system.rs b/examples/non_closure_system.rs new file mode 100644 index 00000000..b1f33b45 --- /dev/null +++ b/examples/non_closure_system.rs @@ -0,0 +1,19 @@ +use bevy::{prelude::*}; +use bevy_scriptum::{prelude::*, Script}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugin(ScriptingPlugin::default()) + .add_script_function(String::from("hello_bevy"), hello_bevy_callback_system) + .add_startup_system(startup) + .run(); +} + +fn startup(mut commands: Commands, assets_server: Res) { + commands.spawn(Script::new(assets_server.load("examples/hello_world.rhai"))); +} + +fn hello_bevy_callback_system() { + println!("hello bevy, called from script"); +} diff --git a/examples/promises.rs b/examples/promises.rs new file mode 100644 index 00000000..7d655d98 --- /dev/null +++ b/examples/promises.rs @@ -0,0 +1,22 @@ +use bevy::{prelude::*}; +use bevy_scriptum::{prelude::*, Script}; + +#[derive(Component)] +struct Player; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugin(ScriptingPlugin::default()) + .add_script_function( + String::from("get_player_name"), + |player_names: Query<&Name, With>| player_names.single().to_string(), + ) + .add_startup_system(startup) + .run(); +} + +fn startup(mut commands: Commands, assets_server: Res) { + commands.spawn((Player, Name::new("John"))); + commands.spawn(Script::new(assets_server.load("examples/promises.rhai"))); +} diff --git a/src/assets.rs b/src/assets.rs new file mode 100644 index 00000000..938f9d61 --- /dev/null +++ b/src/assets.rs @@ -0,0 +1,33 @@ +use bevy::{ + asset::{AssetLoader, LoadContext, LoadedAsset}, + reflect::TypeUuid, + utils::BoxedFuture, +}; +use serde::Deserialize; + +/// A script that can be loaded by the [crate::ScriptingPlugin]. +#[derive(Debug, Deserialize, TypeUuid)] +#[uuid = "3ed4b68b-4f5d-4d82-96f6-5194e358921a"] +pub struct RhaiScript(pub String); + +/// A loader for [RhaiScript] assets. +#[derive(Default)] +pub struct RhaiScriptLoader; + +impl AssetLoader for RhaiScriptLoader { + fn load<'a>( + &'a self, + bytes: &'a [u8], + load_context: &'a mut LoadContext, + ) -> BoxedFuture<'a, Result<(), bevy::asset::Error>> { + Box::pin(async move { + let rhai_script = RhaiScript(String::from_utf8(bytes.to_vec())?); + load_context.set_default_asset(LoadedAsset::new(rhai_script)); + Ok(()) + }) + } + + fn extensions(&self) -> &[&str] { + &["rhai"] + } +} diff --git a/src/callback.rs b/src/callback.rs new file mode 100644 index 00000000..88cfea94 --- /dev/null +++ b/src/callback.rs @@ -0,0 +1,123 @@ +use bevy::prelude::*; +use core::any::TypeId; +use std::sync::{Arc, Mutex}; + +use rhai::{Dynamic, Variant}; + +use crate::promise::Promise; + +/// A system that can be used to call a script function. +pub struct CallbackSystem { + pub(crate) system: Box, Out = Dynamic>>, + pub(crate) arg_types: Vec, +} + +pub(crate) struct FunctionCallEvent { + pub(crate) params: Vec, + pub(crate) promise: Promise, +} + +/// A struct representing a Bevy system that can be called from a script. +#[derive(Clone)] +pub(crate) struct Callback { + pub(crate) name: String, + pub(crate) system: Arc>, + pub(crate) calls: Arc>>, +} + +impl CallbackSystem { + pub(crate) fn call(&mut self, call: &FunctionCallEvent, world: &mut World) -> Dynamic { + self.system.run(call.params.clone(), world) + } +} + +/// Trait that alllows to convert a script callback function into a Bevy [`System`]. +pub trait RegisterCallbackFunction< + Out, + Marker, + A: 'static, + const N: usize, + const X: bool, + R: 'static, + const F: bool, + Args, +>: IntoSystem +{ + /// Convert this function into a [CallbackSystem]. + #[must_use] + fn into_callback_system(self, world: &mut World) -> CallbackSystem; +} + +impl RegisterCallbackFunction for FN +where + FN: IntoSystem<(), Out, Marker>, + Out: Sync + Variant + Clone, +{ + fn into_callback_system(self, world: &mut World) -> CallbackSystem { + let mut inner_system = IntoSystem::into_system(self); + inner_system.initialize(world); + let system_fn = move |_args: In>, world: &mut World| { + Dynamic::from(inner_system.run((), world)) + }; + let system = IntoSystem::into_system(system_fn); + CallbackSystem { + arg_types: vec![], + system: Box::new(system), + } + } +} + +macro_rules! impl_tuple { + ($($idx:tt $t:tt),+) => { + impl<$($t,)+ Out, FN, Marker> RegisterCallbackFunction + for FN + where + FN: IntoSystem<($($t,)+), Out, Marker>, + Out: Sync + Variant + Clone, + $($t: 'static + Clone,)+ + { + fn into_callback_system(self, world: &mut World) -> CallbackSystem { + let mut inner_system = IntoSystem::into_system(self); + inner_system.initialize(world); + let system_fn = move |args: In>, world: &mut World| { + let args = ( + $(args.0.get($idx).unwrap().clone_cast::<$t>(), )+ + ); + Dynamic::from(inner_system.run(args, world)) + }; + let system = IntoSystem::into_system(system_fn); + CallbackSystem { + arg_types: vec![$(TypeId::of::<$t>(),)+], + system: Box::new(system), + } + } + } + }; +} + +impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H, 8 I, 9 J, 10 K, 11 L, 12 M, 13 N, 14 O, 15 P, 16 Q, 17 R, 18 S, 19 T, 20 U, 21 V, 22 W, 23 X, 24 Y, 25 Z); +impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H, 8 I, 9 J, 10 K, 11 L, 12 M, 13 N, 14 O, 15 P, 16 Q, 17 R, 18 S, 19 T, 20 U, 21 V, 22 W, 23 X, 24 Y); +impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H, 8 I, 9 J, 10 K, 11 L, 12 M, 13 N, 14 O, 15 P, 16 Q, 17 R, 18 S, 19 T, 20 U, 21 V, 22 W, 23 X); +impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H, 8 I, 9 J, 10 K, 11 L, 12 M, 13 N, 14 O, 15 P, 16 Q, 17 R, 18 S, 19 T, 20 U, 21 V, 22 W); +impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H, 8 I, 9 J, 10 K, 11 L, 12 M, 13 N, 14 O, 15 P, 16 Q, 17 R, 18 S, 19 T, 20 U, 21 V); +impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H, 8 I, 9 J, 10 K, 11 L, 12 M, 13 N, 14 O, 15 P, 16 Q, 17 R, 18 S, 19 T, 20 U); +impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H, 8 I, 9 J, 10 K, 11 L, 12 M, 13 N, 14 O, 15 P, 16 Q, 17 R, 18 S, 19 T); +impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H, 8 I, 9 J, 10 K, 11 L, 12 M, 13 N, 14 O, 15 P, 16 Q, 17 R, 18 S); +impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H, 8 I, 9 J, 10 K, 11 L, 12 M, 13 N, 14 O, 15 P, 16 Q, 17 R); +impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H, 8 I, 9 J, 10 K, 11 L, 12 M, 13 N, 14 O, 15 P, 16 Q); +impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H, 8 I, 9 J, 10 K, 11 L, 12 M, 13 N, 14 O, 15 P); +impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H, 8 I, 9 J, 10 K, 11 L, 12 M, 13 N, 14 O); +impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H, 8 I, 9 J, 10 K, 11 L, 12 M, 13 N); +impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H, 8 I, 9 J, 10 K, 11 L, 12 M); +impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H, 8 I, 9 J, 10 K, 11 L); +impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H, 8 I, 9 J, 10 K); +impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H, 8 I, 9 J); +impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H, 8 I); +impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H); +impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G); +impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F); +impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E); +impl_tuple!(0 A, 1 B, 2 C, 3 D); +impl_tuple!(0 A, 1 B, 2 C); +impl_tuple!(0 A, 1 B); +impl_tuple!(0 A); diff --git a/src/components.rs b/src/components.rs new file mode 100644 index 00000000..95ed2c84 --- /dev/null +++ b/src/components.rs @@ -0,0 +1,24 @@ +use bevy::prelude::*; + +use super::assets::RhaiScript; + +/// A component that represents a script. +#[derive(Component)] +pub struct Script { + pub script: Handle, +} + +/// A component that represents the data of a script. It stores the [rhai::Scope](basically the state of the script, any declared variable etc.) +/// and [rhai::AST] which is a cached AST representation of the script. +#[derive(Component)] +pub struct ScriptData { + pub(crate) scope: rhai::Scope<'static>, + pub(crate) ast: rhai::AST, +} + +impl Script { + /// Create a new script component from a handle to a [RhaiScript] obtained using [AssetServer]. + pub fn new(script: Handle) -> Self { + Self { script } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 00000000..1f6a9ed9 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,326 @@ +//! ⚠️ **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. +//! +//! 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_plugin(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_plugin(ScriptingPlugin::default()) +//! .add_script_function( +//! String::from("print_player_names"), +//! |players: Query<&Name, With>| { +//! 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_plugin(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!"); +//! ``` +//! +//! Currently, only [Rhai](https://rhai.rs/) is supported, but more languages may be added in the future. +//! +//! ## Usage +//! +//! Add the following to your `Cargo.toml`: +//! +//! ```toml +//! [dependencies] +//! bevy_scriptum = "0.1" +//! ``` +//! +//! 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_plugin(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_plugin(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_startup_system(|mut commands: Commands, asset_server: Res| { +//! 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 `. 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.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), + #[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::() + .init_asset_loader::() + .init_resource::() + .insert_resource(ScriptingRuntime::default()) + .add_startup_system(init_engine.pipe(log_errors)) + .add_systems(( + 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::(options, scope, &ast, function_name, args); + scope.remove::(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, + ) -> &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, + callbacks: Mutex>, +} + +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, + ) -> &mut Self { + let system = system.into_callback_system(&mut self.world); + let mut callbacks_resource = self.world.resource_mut::(); + + 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}; +} diff --git a/src/promise.rs b/src/promise.rs new file mode 100644 index 00000000..fb9cd7a4 --- /dev/null +++ b/src/promise.rs @@ -0,0 +1,83 @@ +use std::sync::{Arc, Mutex}; + +#[allow(deprecated)] +use rhai::{Dynamic, NativeCallContextStore}; +use rhai::{EvalAltResult, FnPtr}; + +// TODO: This should not be public +/// A struct that represents a function that will get called when the Promise is resolved. +pub(crate) struct PromiseCallback { + callback: Dynamic, + following_promise: Arc>, +} + +// TODO: This should not be public +/// Internal representation of a Promise. +pub(crate) struct PromiseInner { + pub(crate) callbacks: Vec, + #[allow(deprecated)] + pub(crate) context_data: NativeCallContextStore, +} + +/// A struct that represents a Promise. +#[derive(Clone)] +pub struct Promise { + pub(crate) inner: Arc>, +} + +impl PromiseInner { + /// Resolve the Promise. This will call all the callbacks that were added to the Promise. + fn resolve( + &mut self, + engine: &mut rhai::Engine, + val: Dynamic, + ) -> Result<(), Box> { + for callback in &self.callbacks { + let f = callback.callback.clone_cast::(); + #[allow(deprecated)] + let context = self.context_data.create_context(engine); + let next_val = if val.is_unit() { + f.call_raw(&context, None, [])? + } else { + f.call_raw(&context, None, [val.clone()])? + }; + callback + .following_promise + .lock() + .unwrap() + .resolve(engine, next_val)?; + } + Ok(()) + } +} + +impl Promise { + /// Acquire [Mutex] for writing the promise and resolve it. Call will be forwarded to [PromiseInner::resolve]. + pub(crate) fn resolve( + &mut self, + engine: &mut rhai::Engine, + val: Dynamic, + ) -> Result<(), Box> { + if let Ok(mut inner) = self.inner.lock() { + inner.resolve(engine, val)?; + } + Ok(()) + } + + /// Register a callback that will be called when the [Promise] is resolved. + pub(crate) fn then(&mut self, callback: rhai::Dynamic) -> rhai::Dynamic { + let mut inner = self.inner.lock().unwrap(); + let following_inner = Arc::new(Mutex::new(PromiseInner { + callbacks: vec![], + context_data: inner.context_data.clone(), + })); + + inner.callbacks.push(PromiseCallback { + following_promise: following_inner.clone(), + callback, + }); + Dynamic::from(Promise { + inner: following_inner, + }) + } +} diff --git a/src/systems.rs b/src/systems.rs new file mode 100644 index 00000000..9666c6ae --- /dev/null +++ b/src/systems.rs @@ -0,0 +1,185 @@ +use bevy::{prelude::*, utils::tracing}; +use rhai::Scope; +use std::{ + fmt::Display, + sync::{Arc, Mutex}, +}; + +use crate::{ + callback::FunctionCallEvent, + components::ScriptData, + promise::{Promise, PromiseInner}, + Callback, Callbacks, ScriptingError, ENTITY_VAR_NAME, +}; + +use super::{assets::RhaiScript, components::Script, ScriptingRuntime}; + +/// Initialize the scripting engine. Adds built-in types and functions. +pub(crate) fn init_engine(world: &mut World) -> Result<(), ScriptingError> { + let mut scripting_runtime = world + .get_resource_mut::() + .ok_or(ScriptingError::NoRuntimeResource)?; + + let engine = &mut scripting_runtime.engine; + + engine + .register_type_with_name::("Entity") + .register_fn("index", |entity: &mut Entity| entity.index()); + engine + .register_type_with_name::("Promise") + .register_fn("then", Promise::then); + engine + .register_type_with_name::("Vec3") + .register_fn("new_vec3", |x: f64, y: f64, z: f64| { + Vec3::new(x as f32, y as f32, z as f32) + }) + .register_get("x", |vec: &mut Vec3| vec.x as f64) + .register_get("y", |vec: &mut Vec3| vec.y as f64) + .register_get("z", |vec: &mut Vec3| vec.z as f64); + #[allow(deprecated)] + engine.on_def_var(|_, info, _| Ok(info.name != "entity")); + + Ok(()) +} + +/// Reloads scripts when they are modified. +pub(crate) fn reload_scripts( + mut commands: Commands, + mut ev_asset: EventReader>, + mut scripts: Query<(Entity, &mut Script)>, +) { + for ev in ev_asset.iter() { + if let AssetEvent::Modified { handle } = ev { + for (entity, script) in &mut scripts { + if script.script == *handle { + commands.entity(entity).remove::(); + } + } + } + } +} + +/// Processes new scripts. Evaluates them and stores the [rhai::Scope] and cached [rhai::AST] in [ScriptData]. +pub(crate) fn process_new_scripts( + mut commands: Commands, + mut added_scripted_entities: Query<(Entity, &mut Script), Without>, + scripting_runtime: ResMut, + scripts: Res>, +) -> Result<(), ScriptingError> { + for (entity, script_component) in &mut added_scripted_entities { + trace!("process_new_scripts: evaulating a new script"); + if let Some(script) = scripts.get(&script_component.script) { + let mut scope = Scope::new(); + + scope.push(ENTITY_VAR_NAME, entity); + + let engine = &scripting_runtime.engine; + + let ast = engine + .compile_with_scope(&scope, script.0.as_str()) + .map_err(ScriptingError::CompileError)?; + + engine + .run_ast_with_scope(&mut scope, &ast) + .map_err(ScriptingError::RuntimeError)?; + + scope.remove::(ENTITY_VAR_NAME).unwrap(); + + commands.entity(entity).insert(ScriptData { ast, scope }); + } + } + Ok(()) +} + +/// Initializes callbacks. Registers them in the scripting engine. +pub(crate) fn init_callbacks(world: &mut World) -> Result<(), ScriptingError> { + let mut callbacks_resource = world + .get_resource_mut::() + .ok_or(ScriptingError::NoSettingsResource)?; + + let mut callbacks = callbacks_resource + .uninitialized_callbacks + .drain(..) + .collect::>(); + + for callback in callbacks.iter_mut() { + if let Ok(mut system) = callback.system.lock() { + system.system.initialize(world); + + let mut scripting_runtime = world + .get_resource_mut::() + .ok_or(ScriptingError::NoRuntimeResource)?; + + trace!("init_callbacks: registering callback: '{}'", callback.name); + let engine = &mut scripting_runtime.engine; + let callback = callback.clone(); + engine.register_raw_fn( + callback.name, + system.arg_types.clone(), + move |context, args| { + #[allow(deprecated)] + let context_data = context.store_data(); + let promise = Promise { + inner: Arc::new(Mutex::new(PromiseInner { + callbacks: vec![], + context_data, + })), + }; + + let mut calls = callback.calls.lock().unwrap(); + calls.push(FunctionCallEvent { + promise: promise.clone(), + params: args.iter_mut().map(|arg| arg.clone()).collect(), + }); + Ok(promise) + }, + ); + } + } + + let callbacks_resource = world + .get_resource_mut::() + .ok_or(ScriptingError::NoSettingsResource)?; + callbacks_resource + .callbacks + .lock() + .unwrap() + .append(&mut callbacks.clone()); + + Ok(()) +} + +/// Processes calls. Calls the user-defined callback systems +pub(crate) fn process_calls(world: &mut World) -> Result<(), ScriptingError> { + let callbacks_resource = world + .get_resource::() + .ok_or(ScriptingError::NoSettingsResource)?; + + let callbacks = callbacks_resource.callbacks.lock().unwrap().clone(); + + for callback in callbacks.into_iter() { + let calls = callback + .calls + .lock() + .unwrap() + .drain(..) + .collect::>(); + for mut call in calls { + trace!("process_calls: calling '{}'", callback.name); + let mut system = callback.system.lock().unwrap(); + let val = system.call(&call, world); + let mut runtime = world + .get_resource_mut::() + .ok_or(ScriptingError::NoRuntimeResource)?; + call.promise.resolve(&mut runtime.engine, val)?; + } + } + Ok(()) +} + +/// Error logging system +pub fn log_errors(In(res): In>) { + if let Err(error) = res { + tracing::error!("{}", error); + } +}