diff --git a/.github/workflows/deploy_book.yml b/.github/workflows/deploy_book.yml new file mode 100644 index 0000000..2d0f6a1 --- /dev/null +++ b/.github/workflows/deploy_book.yml @@ -0,0 +1,38 @@ +name: Deploy book +on: + push: + branches: + - main + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: write # To push a branch + pages: write # To push to a GitHub Pages site + id-token: write # To update the deployment status + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install latest mdbook + run: | + tag=$(curl 'https://api.github.com/repos/rust-lang/mdbook/releases/latest' | jq -r '.tag_name') + url="https://github.com/rust-lang/mdbook/releases/download/${tag}/mdbook-${tag}-x86_64-unknown-linux-gnu.tar.gz" + mkdir mdbook + curl -sSL $url | tar -xz --directory=./mdbook + echo `pwd`/mdbook >> $GITHUB_PATH + - name: Build Book + run: | + cd book + mdbook build + - name: Setup Pages + uses: actions/configure-pages@v4 + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + # Upload entire repository + path: 'book' + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index ef0a745..5110b5c 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -17,12 +17,12 @@ jobs: steps: - uses: actions/checkout@v3 - name: Clippy - run: cargo clippy --verbose -- -D warnings + run: cargo clippy --all-features --verbose -- -D warnings - name: Build - run: cargo build --verbose + run: cargo build --all-features --verbose - name: Run tests - run: cargo test --verbose + run: cargo test --all-features --verbose - name: Install cargo-examples run: cargo install cargo-examples - name: Run all examples - run: cargo examples + run: cargo examples --features=lua,rhai diff --git a/.gitignore b/.gitignore index 5b70a89..a7c83b7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target /Cargo.lock .vscode +rust-analyzer.json diff --git a/Cargo.toml b/Cargo.toml index cb3dc0c..0136531 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,13 +10,102 @@ description = "Plugin for Bevy engine that allows you to write some of your game repository = "https://github.com/jarkonik/bevy_scriptum" keywords = ["bevy", "rhai", "scripting", "game", "gamedev"] -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +lua = ["mlua/luajit"] +rhai = ["dep:rhai"] [dependencies] bevy = { default-features = false, version = "0.13.0", features = [ "bevy_asset", ] } serde = "1.0.162" -rhai = { version = "1.14.0", features = ["sync", "internals", "unchecked"] } +rhai = { version = "1.14.0", features = ["sync", "internals", "unchecked"], optional = true } thiserror = "1.0.40" anyhow = "1.0.82" +tracing = "0.1.40" +mlua = { version = "0.9.8", features = ["luajit", "vendored", "send"], optional = true } + +[[example]] +name = "call_function_from_rust_rhai" +path = "examples/rhai/call_function_from_rust.rs" + +[[example]] +name = "current_entity_rhai" +path = "examples/rhai/current_entity.rs" + +[[example]] +name = "custom_type_rhai" +path = "examples/rhai/custom_type.rs" + +[[example]] +name = "ecs_rhai" +path = "examples/rhai/ecs.rs" + +[[example]] +name = "entity_variable_rhai" +path = "examples/rhai/entity_variable.rs" + +[[example]] +name = "function_params_rhai" +path = "examples/rhai/function_params.rs" + +[[example]] +name = "hello_world_rhai" +path = "examples/rhai/hello_world.rs" + +[[example]] +name = "non_closure_system_rhai" +path = "examples/rhai/non_closure_system.rs" + +[[example]] +name = "promises_rhai" +path = "examples/rhai/promises.rs" + +[[example]] +name = "side_effects_rhai" +path = "examples/rhai/side_effects.rs" + +[[example]] +name = "call_function_from_rust_lua" +path = "examples/lua/call_function_from_rust.rs" + +[[example]] +name = "current_entity_lua" +path = "examples/lua/current_entity.rs" + +[[example]] +name = "custom_type_lua" +path = "examples/lua/custom_type.rs" + +[[example]] +name = "ecs_lua" +path = "examples/lua/ecs.rs" + +[[example]] +name = "entity_variable_lua" +path = "examples/lua/entity_variable.rs" + +[[example]] +name = "function_params_lua" +path = "examples/lua/function_params.rs" + +[[example]] +name = "hello_world_lua" +path = "examples/lua/hello_world.rs" + +[[example]] +name = "non_closure_system_lua" +path = "examples/lua/non_closure_system.rs" + +[[example]] +name = "promises_lua" +path = "examples/lua/promises.rs" + +[[example]] +name = "side_effects_lua" +path = "examples/lua/side_effects.rs" + +[dev-dependencies] +tracing-subscriber = "0.3.18" +mlua = { version = "0.9.8", features = ["luajit", "vendored", "send"] } +rhai = { version = "1.14.0", features = ["sync", "internals", "unchecked"] } diff --git a/README.md b/README.md index 683b880..be0eb18 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,14 @@ # bevy_scriptum 📜 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. +Currently [Rhai](https://rhai.rs/) and [Lua](https://lua.org/) are supported, but more languages may be added in the future. -It's main advantages include: +Everything you need to know to get started with using this library is contained in the +[bevy_scriptum book](https://link-to-book.com) + +API docs are available in [docs.rs](https://docs.rs/bevy_scriptum/latest/bevy_scriptum/) + +bevy_scriptum's main advantages include: - low-boilerplate - easy to use - asynchronicity with a promise-based API @@ -16,17 +21,20 @@ All you need to do is register callbacks on your Bevy app like this: ```rust use bevy::prelude::*; use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::lua::prelude::*; App::new() .add_plugins(DefaultPlugins) - .add_plugins(ScriptingPlugin::default()) - .add_script_function(String::from("hello_bevy"), || { - println!("hello bevy, called from script"); - }); + .add_scripting::(|runtime| { + runtime.add_function(String::from("hello_bevy"), || { + println!("hello bevy, called from script"); + }); + }) + .run(); ``` And you can call them in your scripts like this: -```rhai -hello_bevy(); +```lua +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: @@ -34,42 +42,47 @@ Every callback function that you expose to the scripting language is also a Bevy ```rust use bevy::prelude::*; use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::lua::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>| { - for player in &players { - println!("player name: {}", player); - } - }, - ); + .add_scripting::(|runtime| { + runtime.add_function( + String::from("print_player_names"), + |players: Query<&Name, With>| { + for player in &players { + println!("player name: {}", player); + } + }, + ); + }) + .run(); ``` 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; +use bevy_scriptum::runtimes::lua::prelude::*; 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); - }, - ); + .add_scripting::(|runtime| { + runtime.add_function( + String::from("fun_with_string_param"), + |In((x,)): In<(String,)>| { + println!("called with string: '{}'", x); + }, + ); + }) + .run(); ``` which you can then call in your script like this: -```rhai -fun_with_string_param("Hello world!"); +```lua +fun_with_string_param("Hello world!") ``` ### Usage @@ -78,65 +91,68 @@ Add the following to your `Cargo.toml`: ```toml [dependencies] -bevy_scriptum = "0.2" +bevy_scriptum = { version = "0.5", features = ["lua"] } ``` -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(); -``` +or execute `cargo add bevy_scriptum --features lua` from your project directory. 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::*; +use bevy_scriptum::runtimes::lua::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); - }, - ); + .add_scripting::(|runtime| { + runtime.add_function( + String::from("my_print"), + |In((x,)): In<(String,)>| { + println!("my_print: '{}'", x); + }, + ); + }) + .run(); ``` -Then you can create a script file in `assets` directory called `script.rhai` that calls this function: +Then you can create a script file in `assets` directory called `script.lua` that calls this function: -```rhai -my_print("Hello world!"); +```lua +my_print("Hello world!") ``` -And spawn a `Script` component with a handle to a script source file`: +And spawn an entity with attached `Script` component with a handle to a script source file: ```rust use bevy::prelude::*; -use bevy_scriptum::Script; +use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::lua::prelude::*; App::new() + .add_plugins(DefaultPlugins) + .add_scripting::(|runtime| { + runtime.add_function( + String::from("my_print"), + |In((x,)): In<(String,)>| { + println!("my_print: '{}'", x); + }, + ); + }) .add_systems(Startup,|mut commands: Commands, asset_server: Res| { - commands.spawn(Script::new(asset_server.load("script.rhai"))); - }); + commands.spawn(Script::::new(asset_server.load("script.lua"))); + }) + .run(); ``` +You should then see `my_print: 'Hello world!'` printed in your console. + ### Provided examples -You can also try running provided examples by cloning this repository and running `cargo run --example `. For example: +You can also try running provided examples by cloning this repository and running `cargo run --example _`. For example: ```bash -cargo run --example hello_world +cargo run --example hello_world_lua ``` The examples live in `examples` directory and their corresponding scripts live in `assets/examples` directory within the repository. @@ -144,31 +160,46 @@ The examples live in `examples` directory and their corresponding scripts live i | bevy version | bevy_scriptum version | |--------------|----------------------| -| 0.13 | 0.4 | +| 0.13 | 0.4-0.5 | | 0.12 | 0.3 | | 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: +Every function called from script returns a promise that you can call `:and_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); -}); +```lua +get_player_name():and_then(function(name) + print(name) +end) ``` +which will print out `John` when used with following exposed function: -### Access entity from script +```rust +use bevy::prelude::*; +use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::lua::prelude::*; + +App::new() + .add_plugins(DefaultPlugins) + .add_scripting::(|runtime| { + runtime.add_function(String::from("get_player_name"), || String::from("John")); + }); +```` + +## 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 exposes `index` property 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()); +```lua +print("Current entity index: " .. entity.index) ``` +`entity` variable is currently not available within promise callbacks. + ### Contributing Contributions are welcome! Feel free to open an issue or submit a pull request. diff --git a/SECURITY.md b/SECURITY.md index cf96c44..526e30c 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -7,8 +7,7 @@ currently being supported with security updates. | Version | Supported | | ------- | ------------------ | -| 0.1 | :x: | -| 0.2 | :white_check_mark: | +| 0.5 | :white_check_mark: | ## Reporting a Vulnerability diff --git a/assets/examples/lua/call_function_from_rust.lua b/assets/examples/lua/call_function_from_rust.lua new file mode 100644 index 0000000..6cc40c3 --- /dev/null +++ b/assets/examples/lua/call_function_from_rust.lua @@ -0,0 +1,13 @@ +local my_state = { + iterations = 0, +} + +function on_update() + my_state.iterations = my_state.iterations + 1; + print("on_update called " .. my_state.iterations .. " times") + + if my_state.iterations >= 10 then + print("calling quit"); + quit(); + end +end diff --git a/assets/examples/lua/current_entity.lua b/assets/examples/lua/current_entity.lua new file mode 100644 index 0000000..e854eaf --- /dev/null +++ b/assets/examples/lua/current_entity.lua @@ -0,0 +1,7 @@ +-- 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 +get_name(entity):and_then(function(name) + print(name) +end) diff --git a/assets/examples/lua/custom_type.lua b/assets/examples/lua/custom_type.lua new file mode 100644 index 0000000..a24a23a --- /dev/null +++ b/assets/examples/lua/custom_type.lua @@ -0,0 +1,4 @@ +-- Create a new instance of MyType +my_type = MyType(); +-- Call registered method +print(my_type:my_method()) diff --git a/assets/examples/lua/ecs.lua b/assets/examples/lua/ecs.lua new file mode 100644 index 0000000..6746905 --- /dev/null +++ b/assets/examples/lua/ecs.lua @@ -0,0 +1 @@ +print_player_names() diff --git a/assets/examples/lua/entity_variable.lua b/assets/examples/lua/entity_variable.lua new file mode 100644 index 0000000..e89847b --- /dev/null +++ b/assets/examples/lua/entity_variable.lua @@ -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/lua/function_params.lua b/assets/examples/lua/function_params.lua new file mode 100644 index 0000000..d9292b6 --- /dev/null +++ b/assets/examples/lua/function_params.lua @@ -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/lua/hello_world.lua similarity index 100% rename from assets/examples/hello_world.rhai rename to assets/examples/lua/hello_world.lua diff --git a/assets/examples/lua/promises.lua b/assets/examples/lua/promises.lua new file mode 100644 index 0000000..e4b1cc2 --- /dev/null +++ b/assets/examples/lua/promises.lua @@ -0,0 +1,3 @@ +get_player_name():and_then(function(name) + print(name) +end); diff --git a/assets/examples/side_effects.rhai b/assets/examples/lua/side_effects.lua similarity index 100% rename from assets/examples/side_effects.rhai rename to assets/examples/lua/side_effects.lua diff --git a/assets/examples/call_function_from_rust.rhai b/assets/examples/rhai/call_function_from_rust.rhai similarity index 100% rename from assets/examples/call_function_from_rust.rhai rename to assets/examples/rhai/call_function_from_rust.rhai diff --git a/assets/examples/current_entity.rhai b/assets/examples/rhai/current_entity.rhai similarity index 100% rename from assets/examples/current_entity.rhai rename to assets/examples/rhai/current_entity.rhai diff --git a/assets/examples/custom_type.rhai b/assets/examples/rhai/custom_type.rhai similarity index 100% rename from assets/examples/custom_type.rhai rename to assets/examples/rhai/custom_type.rhai diff --git a/assets/examples/ecs.rhai b/assets/examples/rhai/ecs.rhai similarity index 100% rename from assets/examples/ecs.rhai rename to assets/examples/rhai/ecs.rhai diff --git a/assets/examples/entity_variable.rhai b/assets/examples/rhai/entity_variable.rhai similarity index 73% rename from assets/examples/entity_variable.rhai rename to assets/examples/rhai/entity_variable.rhai index 963eaff..9353229 100644 --- a/assets/examples/entity_variable.rhai +++ b/assets/examples/rhai/entity_variable.rhai @@ -1,3 +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()); +print("Current entity index: " + entity.index); diff --git a/assets/examples/function_params.rhai b/assets/examples/rhai/function_params.rhai similarity index 100% rename from assets/examples/function_params.rhai rename to assets/examples/rhai/function_params.rhai diff --git a/assets/examples/rhai/hello_world.rhai b/assets/examples/rhai/hello_world.rhai new file mode 100644 index 0000000..bd18b19 --- /dev/null +++ b/assets/examples/rhai/hello_world.rhai @@ -0,0 +1 @@ +hello_bevy(); diff --git a/assets/examples/promises.rhai b/assets/examples/rhai/promises.rhai similarity index 100% rename from assets/examples/promises.rhai rename to assets/examples/rhai/promises.rhai diff --git a/assets/examples/rhai/side_effects.rhai b/assets/examples/rhai/side_effects.rhai new file mode 100644 index 0000000..fbd0c56 --- /dev/null +++ b/assets/examples/rhai/side_effects.rhai @@ -0,0 +1 @@ +spawn_entity(); diff --git a/assets/tests/lua/call_script_function_that_causes_runtime_error.lua b/assets/tests/lua/call_script_function_that_causes_runtime_error.lua new file mode 100644 index 0000000..6d5e858 --- /dev/null +++ b/assets/tests/lua/call_script_function_that_causes_runtime_error.lua @@ -0,0 +1,3 @@ +function test_func() + print("abc" + 5) +end diff --git a/assets/tests/lua/call_script_function_with_params.lua b/assets/tests/lua/call_script_function_with_params.lua new file mode 100644 index 0000000..dd6ac42 --- /dev/null +++ b/assets/tests/lua/call_script_function_with_params.lua @@ -0,0 +1,7 @@ +State = { + called_with = nil +} + +function test_func(x) + called_with = x +end diff --git a/assets/tests/lua/promise_runtime_error.lua b/assets/tests/lua/promise_runtime_error.lua new file mode 100644 index 0000000..74b2210 --- /dev/null +++ b/assets/tests/lua/promise_runtime_error.lua @@ -0,0 +1,5 @@ +function test_func() + rust_func():and_then(function(x) + print("abc" + 5) + end) +end diff --git a/assets/tests/lua/return_via_promise.lua b/assets/tests/lua/return_via_promise.lua new file mode 100644 index 0000000..8794e9a --- /dev/null +++ b/assets/tests/lua/return_via_promise.lua @@ -0,0 +1,9 @@ +State = { + x = nil +} + +function test_func() + rust_func():and_then(function(x) + State.x = x + end) +end diff --git a/assets/tests/lua/rust_function_gets_called_from_script.lua b/assets/tests/lua/rust_function_gets_called_from_script.lua new file mode 100644 index 0000000..acb79ea --- /dev/null +++ b/assets/tests/lua/rust_function_gets_called_from_script.lua @@ -0,0 +1,3 @@ +function test_func() + rust_func() +end diff --git a/assets/tests/lua/rust_function_gets_called_from_script_with_multiple_params.lua b/assets/tests/lua/rust_function_gets_called_from_script_with_multiple_params.lua new file mode 100644 index 0000000..d663b01 --- /dev/null +++ b/assets/tests/lua/rust_function_gets_called_from_script_with_multiple_params.lua @@ -0,0 +1,3 @@ +function test_func() + rust_func(5, "test") +end diff --git a/assets/tests/lua/rust_function_gets_called_from_script_with_param.lua b/assets/tests/lua/rust_function_gets_called_from_script_with_param.lua new file mode 100644 index 0000000..d753e39 --- /dev/null +++ b/assets/tests/lua/rust_function_gets_called_from_script_with_param.lua @@ -0,0 +1,3 @@ +function test_func() + rust_func(5) +end diff --git a/assets/tests/lua/script_function_gets_called_from_rust.lua b/assets/tests/lua/script_function_gets_called_from_rust.lua new file mode 100644 index 0000000..6618f3e --- /dev/null +++ b/assets/tests/lua/script_function_gets_called_from_rust.lua @@ -0,0 +1,7 @@ +State = { + times_called = 0 +} + +function test_func() + State.times_called = State.times_called + 1; +end diff --git a/assets/tests/lua/script_function_gets_called_from_rust_with_multiple_params.lua b/assets/tests/lua/script_function_gets_called_from_rust_with_multiple_params.lua new file mode 100644 index 0000000..d27b39d --- /dev/null +++ b/assets/tests/lua/script_function_gets_called_from_rust_with_multiple_params.lua @@ -0,0 +1,9 @@ +State = { + a_value = nil, + b_value = nil +} + +function test_func(a, b) + State.a_value = a + State.b_value = b +end diff --git a/assets/tests/lua/script_function_gets_called_from_rust_with_single_param.lua b/assets/tests/lua/script_function_gets_called_from_rust_with_single_param.lua new file mode 100644 index 0000000..f43238b --- /dev/null +++ b/assets/tests/lua/script_function_gets_called_from_rust_with_single_param.lua @@ -0,0 +1,7 @@ +State = { + a_value = nil +} + +function test_func(a) + State.a_value = a +end diff --git a/assets/tests/lua/side_effects.lua b/assets/tests/lua/side_effects.lua new file mode 100644 index 0000000..cb64783 --- /dev/null +++ b/assets/tests/lua/side_effects.lua @@ -0,0 +1,3 @@ +function test_func() + spawn_entity() +end diff --git a/assets/tests/rhai/call_script_function_that_causes_runtime_error.rhai b/assets/tests/rhai/call_script_function_that_causes_runtime_error.rhai new file mode 100644 index 0000000..50e1e7c --- /dev/null +++ b/assets/tests/rhai/call_script_function_that_causes_runtime_error.rhai @@ -0,0 +1,3 @@ +fn test_func() { + print("abc" * 5) +} diff --git a/assets/tests/rhai/call_script_function_with_params.rhai b/assets/tests/rhai/call_script_function_with_params.rhai new file mode 100644 index 0000000..29d9255 --- /dev/null +++ b/assets/tests/rhai/call_script_function_with_params.rhai @@ -0,0 +1,2 @@ +fn test_func(x) { +} diff --git a/assets/tests/rhai/promise_runtime_error.rhai b/assets/tests/rhai/promise_runtime_error.rhai new file mode 100644 index 0000000..d6eef0c --- /dev/null +++ b/assets/tests/rhai/promise_runtime_error.rhai @@ -0,0 +1,5 @@ +fn test_func() { + rust_func().then(|x| { + print("abc" * 5) + }) +} diff --git a/assets/tests/rhai/return_via_promise.rhai b/assets/tests/rhai/return_via_promise.rhai new file mode 100644 index 0000000..5277a19 --- /dev/null +++ b/assets/tests/rhai/return_via_promise.rhai @@ -0,0 +1,9 @@ +let state = #{ + x: 0 +}; + +fn test_func() { + rust_func().then(|x| { + state.x = x; + }) +} diff --git a/assets/tests/rhai/rust_function_gets_called_from_script.rhai b/assets/tests/rhai/rust_function_gets_called_from_script.rhai new file mode 100644 index 0000000..a62123e --- /dev/null +++ b/assets/tests/rhai/rust_function_gets_called_from_script.rhai @@ -0,0 +1,3 @@ +fn test_func() { + rust_func(); +} diff --git a/assets/tests/rhai/rust_function_gets_called_from_script_with_multiple_params.rhai b/assets/tests/rhai/rust_function_gets_called_from_script_with_multiple_params.rhai new file mode 100644 index 0000000..05e1ea9 --- /dev/null +++ b/assets/tests/rhai/rust_function_gets_called_from_script_with_multiple_params.rhai @@ -0,0 +1,3 @@ +fn test_func() { + rust_func(5, "test"); +} diff --git a/assets/tests/rhai/rust_function_gets_called_from_script_with_param.rhai b/assets/tests/rhai/rust_function_gets_called_from_script_with_param.rhai new file mode 100644 index 0000000..2b433fa --- /dev/null +++ b/assets/tests/rhai/rust_function_gets_called_from_script_with_param.rhai @@ -0,0 +1,3 @@ +fn test_func() { + rust_func(5); +} diff --git a/assets/tests/rhai/script_function_gets_called_from_rust.rhai b/assets/tests/rhai/script_function_gets_called_from_rust.rhai new file mode 100644 index 0000000..d059590 --- /dev/null +++ b/assets/tests/rhai/script_function_gets_called_from_rust.rhai @@ -0,0 +1,7 @@ +let state = #{ + times_called: 0 +}; + +fn test_func() { + state.times_called += 1; +} diff --git a/assets/tests/rhai/script_function_gets_called_from_rust_with_multiple_params.rhai b/assets/tests/rhai/script_function_gets_called_from_rust_with_multiple_params.rhai new file mode 100644 index 0000000..c7f9ca0 --- /dev/null +++ b/assets/tests/rhai/script_function_gets_called_from_rust_with_multiple_params.rhai @@ -0,0 +1,9 @@ +let state = #{ + a_value: (), + b_value: () +}; + +fn test_func(a, b) { + state.a_value = a; + state.b_value = b; +} diff --git a/assets/tests/rhai/script_function_gets_called_from_rust_with_single_param.rhai b/assets/tests/rhai/script_function_gets_called_from_rust_with_single_param.rhai new file mode 100644 index 0000000..844a139 --- /dev/null +++ b/assets/tests/rhai/script_function_gets_called_from_rust_with_single_param.rhai @@ -0,0 +1,7 @@ +let state = #{ + a_value: () +}; + +fn test_func(a) { + state.a_value = a +} diff --git a/assets/tests/rhai/side_effects.rhai b/assets/tests/rhai/side_effects.rhai new file mode 100644 index 0000000..e964512 --- /dev/null +++ b/assets/tests/rhai/side_effects.rhai @@ -0,0 +1,3 @@ +fn test_func() { + spawn_entity() +} diff --git a/book/.gitignore b/book/.gitignore new file mode 100644 index 0000000..517ed82 --- /dev/null +++ b/book/.gitignore @@ -0,0 +1,2 @@ +book +doctest_cache diff --git a/book/book.toml b/book/book.toml new file mode 100644 index 0000000..35d684f --- /dev/null +++ b/book/book.toml @@ -0,0 +1,12 @@ +[book] +authors = ["Jaroslaw Konik"] +language = "en" +multilingual = false +src = "src" +title = "bevy_scriptum" + +[preprocessor.keeper] +command = "mdbook-keeper" +manifest_dir = "../" +externs = ["bevy", "bevy_scriptum"] +build_features = ["lua", "rhai"] diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md new file mode 100644 index 0000000..cfaddce --- /dev/null +++ b/book/src/SUMMARY.md @@ -0,0 +1,21 @@ +# Summary + +- [Introduction](./introduction.md) +- [Runtimes](./runtimes.md) + - [Lua](./lua/lua.md) + - [Installation](./lua/installation.md) + - [Hello World](./lua/hello_world.md) + - [Spawning scripts](./lua/spawning_scripts.md) + - [Calling Rust from Lua](./lua/calling_rust_from_script.md) + - [Calling Lua from Rust](./lua/calling_script_from_rust.md) + - [Interacting with bevy in callbacks](./lua/interacting_with_bevy.md) + - [Builtin types](./lua/builtin_types.md) + - [Builtin variables](./lua/builtin_variables.md) + - [Rhai](./rhai/rhai.md) + - [Installation](./rhai/installation.md) + - [Hello World(TBD)]() +- [Multiple runtimes(TBD)]() +- [Implementing custom runtimes(TBD)]() +- [Workflow](./workflow/workflow.md) + - [Live-reload](./workflow/live_reload.md) +- [Bevy support matrix](./bevy_support_matrix.md) diff --git a/book/src/bevy_support_matrix.md b/book/src/bevy_support_matrix.md new file mode 100644 index 0000000..fa95ff6 --- /dev/null +++ b/book/src/bevy_support_matrix.md @@ -0,0 +1,8 @@ +# Bevy support matrix + +| bevy version | bevy_scriptum version | +|--------------|----------------------| +| 0.13 | 0.4-0.5 | +| 0.12 | 0.3 | +| 0.11 | 0.2 | +| 0.10 | 0.1 | diff --git a/book/src/introduction.md b/book/src/introduction.md new file mode 100644 index 0000000..d617e75 --- /dev/null +++ b/book/src/introduction.md @@ -0,0 +1,210 @@ +# bevy_scriptum 📜 + +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 [Rhai](https://rhai.rs/) and [Lua](https://lua.org/) are supported, but more languages may be added in the future. + +API docs are available in [docs.rs](https://docs.rs/bevy_scriptum/latest/bevy_scriptum/) + +bevy_scriptum'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::*; +use bevy_scriptum::runtimes::lua::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_scripting::(|runtime| { + runtime.add_function(String::from("hello_bevy"), || { + println!("hello bevy, called from script"); + }); + }) + .run(); +} +``` +And you can call them in your scripts like this: +```lua +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::*; +use bevy_scriptum::runtimes::lua::prelude::*; + +#[derive(Component)] +struct Player; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_scripting::(|runtime| { + runtime.add_function( + String::from("print_player_names"), + |players: Query<&Name, With>| { + for player in &players { + println!("player name: {}", player); + } + }, + ); + }) + .run(); +} +``` + +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 bevy_scriptum::runtimes::lua::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_scripting::(|runtime| { + runtime.add_function( + String::from("fun_with_string_param"), + |In((x,)): In<(String,)>| { + println!("called with string: '{}'", x); + }, + ); + }) + .run(); +} +``` +which you can then call in your script like this: +```lua +fun_with_string_param("Hello world!") +``` + +### Usage + +Add the following to your `Cargo.toml`: + +```toml +[dependencies] +bevy_scriptum = { version = "0.5", features = ["lua"] } +``` + +or execute `cargo add bevy_scriptum --features lua` from your project directory. + +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 bevy::prelude::*; +use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::lua::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_scripting::(|runtime| { + runtime.add_function( + String::from("my_print"), + |In((x,)): In<(String,)>| { + println!("my_print: '{}'", x); + }, + ); + }) + .run(); +} +``` + +Then you can create a script file in `assets` directory called `script.lua` that calls this function: + +```lua +my_print("Hello world!") +``` + +And spawn an entity with attached `Script` component with a handle to a script source file: + +```rust +use bevy::prelude::*; +use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::lua::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_scripting::(|runtime| { + runtime.add_function( + String::from("my_print"), + |In((x,)): In<(String,)>| { + println!("my_print: '{}'", x); + }, + ); + }) + .add_systems(Startup,|mut commands: Commands, asset_server: Res| { + commands.spawn(Script::::new(asset_server.load("script.lua"))); + }) + .run(); +} +``` + +You should then see `my_print: 'Hello world!'` printed in your console. + +### 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_lua +``` +The examples live in `examples` directory and their corresponding scripts live in `assets/examples` directory within the repository. + +### Promises - getting return values from scripts + +Every function called from script returns a promise that you can call `:and_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: + +```lua +get_player_name():and_then(function(name) + print(name) +end) +``` +which will print out `John` when used with following exposed function: + +```rust +use bevy::prelude::*; +use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::lua::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_scripting::(|runtime| { + runtime.add_function(String::from("get_player_name"), || String::from("John")); + }); +} +```` + +## 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` property that returns bevy entity index. +It is useful for accessing entity's components from scripts. +It can be used in the following way: +```lua +print("Current entity index: " .. entity.index) +``` + +`entity` variable is currently not available within promise callbacks. + +### 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/book/src/lua/builtin_types.md b/book/src/lua/builtin_types.md new file mode 100644 index 0000000..e376a33 --- /dev/null +++ b/book/src/lua/builtin_types.md @@ -0,0 +1,87 @@ +# Builtin types + +bevy_scriptum provides following types that can be used in Lua: + +- ```Vec3``` +- ```BevyEntity``` + +## Vec3 + +### Constructor + +`Vec3(x: number, y: number, z: number)` + +### Properties + +- `x: number` +- `y: number` +- `z: number` + + +### Example Lua usage + +``` +my_vec = Vec3(1, 2, 3) +set_translation(entity, my_vec) +``` + +### Example Rust usage + +```rust +use bevy::prelude::*; +use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::lua::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_scripting::(|runtime| { + runtime.add_function(String::from("set_translation"), set_translation); + }) + .run(); +} + +fn set_translation( + In((entity, translation)): In<(BevyEntity, BevyVec3)>, + mut entities: Query<&mut Transform>, +) { + let mut transform = entities.get_mut(entity.0).unwrap(); + transform.translation = translation.0; +} +``` + +## BevyEntity + +### Constructor + +None - instances can only be acquired by using built-in `entity` global variable. + +### Properties + +- `index: integer` + +### Example Lua usage + +```lua +print(entity.index) +pass_to_rust(entity) +``` + +### Example Rust usage + +```rust +use bevy::prelude::*; +use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::lua::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_scripting::(|runtime| { + runtime.add_function(String::from("pass_to_rust"), |In((entity,)): In<(BevyEntity,)>| { + println!("pass_to_rust called with entity: {:?}", entity); + }); + }) + .run(); +} +``` diff --git a/book/src/lua/builtin_variables.md b/book/src/lua/builtin_variables.md new file mode 100644 index 0000000..558aa39 --- /dev/null +++ b/book/src/lua/builtin_variables.md @@ -0,0 +1,13 @@ +# Builtin variables + +## entity + +A variable called `entity` is automatically available to all scripts - it represents bevy entity that the `Script` component is attached to. +It exposes `index` property that returns bevy entity index. +It is useful for accessing entity's components from scripts. +It can be used in the following way: +```lua +print("Current entity index: " .. entity.index) +``` + +`entity` variable is currently not available within promise callbacks. diff --git a/book/src/lua/calling_rust_from_script.md b/book/src/lua/calling_rust_from_script.md new file mode 100644 index 0000000..3b7321e --- /dev/null +++ b/book/src/lua/calling_rust_from_script.md @@ -0,0 +1,121 @@ +# Calling Rust from Lua + +To call a rust function from Lua first you need to register a function +within Rust using builder pattern. + +```rust +use bevy::prelude::*; +use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::lua::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_scripting::(|runtime| { + // `runtime` is a builder that you can use to register functions + }) + .run(); +} +``` + +For example to register a function called `my_rust_func` you can do the following: + +```rust +use bevy::prelude::*; +use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::lua::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_scripting::(|runtime| { + runtime.add_function(String::from("my_rust_func"), || { + println!("my_rust_func has been called"); + }); + }) + .run(); +} +``` + +After you do that the function will be available to Lua code in your spawned scripts. + +```lua +my_rust_func() +``` + +Registered functions can also take parameters. A parameter can be any type +that implements `FromLua`. + +Since a registered callback function is a Bevy system, the parameters are passed +to it as `In` struct with tuple, which has to be the first parameter of the closure. + +```rust +use bevy::prelude::*; +use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::lua::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_scripting::(|runtime| { + runtime.add_function(String::from("func_with_params"), |args: In<(String, i64)>| { + println!("my_rust_func has been called with string {} and i64 {}", args.0.0, args.0.1); + }); + }) + .run(); +} +``` + +To make it look nicer you can destructure the `In` struct. + +```rust +use bevy::prelude::*; +use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::lua::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_scripting::(|runtime| { + runtime.add_function(String::from("func_with_params"), |In((a, b)): In<(String, i64)>| { + println!("my_rust_func has been called with string {} and i64 {}", a, b); + }); + }) + .run(); +} +``` + +The above function can be called from Lua + +```lua +func_with_params("abc", 123) +``` + +## Return value via promise + +Any registered rust function that returns a value will retrurn a promise when +called within a script. By calling `:and_then` on the promise you can register +a callback that will receive the value returned from Rust function. + +```rust +use bevy::prelude::*; +use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::lua::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_scripting::(|runtime| { + runtime.add_function(String::from("returns_value"), || { + 123 + }); + }) + .run(); +} +``` + +```lua +returns_value():and_then(function (value) + print(value) -- 123 +end) +``` diff --git a/book/src/lua/calling_script_from_rust.md b/book/src/lua/calling_script_from_rust.md new file mode 100644 index 0000000..b543c2c --- /dev/null +++ b/book/src/lua/calling_script_from_rust.md @@ -0,0 +1,67 @@ +# Calling Lua from Rust + +To call a function defined in Lua + +```lua +function on_update() +end +``` + +We need to acquire `LuaRuntime` resource within a bevy system. +Then we will be able to call `call_fn` on it, providing the name +of the function to call, `LuaScriptData` that has been automatically +attached to entity after an entity with script attached has been spawned +and its script evaluated, the entity and optionally some arguments. + +```rust +use bevy::prelude::*; +use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::lua::prelude::*; + +fn call_lua_on_update_from_rust( + mut scripted_entities: Query<(Entity, &mut LuaScriptData)>, + scripting_runtime: ResMut, +) { + for (entity, mut script_data) in &mut scripted_entities { + // calling function named `on_update` defined in lua script + scripting_runtime + .call_fn("on_update", &mut script_data, entity, ()) + .unwrap(); + } +} + +fn main() {} +``` + +We can also pass some arguments by providing a tuple or `Vec` as the last +`call_fn` argument. + +```rust +use bevy::prelude::*; +use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::lua::prelude::*; + +fn call_lua_on_update_from_rust( + mut scripted_entities: Query<(Entity, &mut LuaScriptData)>, + scripting_runtime: ResMut, +) { + for (entity, mut script_data) in &mut scripted_entities { + scripting_runtime + .call_fn("on_update", &mut script_data, entity, (123, String::from("hello"))) + .unwrap(); + } +} + +fn main() {} +``` + +They will be passed to `on_update` Lua function +```lua +function on_update(a, b) + print(a) -- 123 + print(b) -- hello +end +``` + +Any type that implements `IntoLua` can be passed as an argument withing the +tuple in `call_fn`. diff --git a/book/src/lua/hello_world.md b/book/src/lua/hello_world.md new file mode 100644 index 0000000..469f551 --- /dev/null +++ b/book/src/lua/hello_world.md @@ -0,0 +1,66 @@ +# Hello World + +After you are done installing the required crates, you can start developing +your first game or application using bevy_scriptum. + +To start using the library you need to first import some structs and traits +with Rust `use` statements. + +For convenience there is a main "prelude" module provided called +`bevy_scriptum::prelude` and a prelude for each runtime you have enabled as +a create feature. + +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 bevy::prelude::*; +use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::lua::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_scripting::(|runtime| { + runtime.add_function( + String::from("my_print"), + |In((x,)): In<(String,)>| { + println!("my_print: '{}'", x); + }, + ); + }) + .run(); +} +``` + +Then you can create a script file in `assets` directory called `script.lua` that calls this function: + +```lua +my_print("Hello world!") +``` + +And spawn an entity with attached `Script` component with a handle to a script source file: + +```rust +use bevy::prelude::*; +use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::lua::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_scripting::(|runtime| { + runtime.add_function( + String::from("my_print"), + |In((x,)): In<(String,)>| { + println!("my_print: '{}'", x); + }, + ); + }) + .add_systems(Startup,|mut commands: Commands, asset_server: Res| { + commands.spawn(Script::::new(asset_server.load("script.lua"))); + }) + .run(); +} +``` + +You should then see `my_print: 'Hello world!'` printed in your console. diff --git a/book/src/lua/installation.md b/book/src/lua/installation.md new file mode 100644 index 0000000..09e4bc7 --- /dev/null +++ b/book/src/lua/installation.md @@ -0,0 +1,12 @@ +# Installation + +Add the following to your `Cargo.toml`: + +```toml +[dependencies] +bevy = "0.13" +bevy_scriptum = { version = "0.5", features = ["lua"] } +``` + +If you need a different version of bevy you need to use a matching bevy_scriptum +version according to the [bevy support matrix](../bevy_support_matrix.md) diff --git a/book/src/lua/interacting_with_bevy.md b/book/src/lua/interacting_with_bevy.md new file mode 100644 index 0000000..6e7637d --- /dev/null +++ b/book/src/lua/interacting_with_bevy.md @@ -0,0 +1,75 @@ +# Interacting with bevy in callbacks + +Every registered function is also just a regular Bevy system. + +That allows you to do anything you would do in a Bevy system. + +You could for example create a callback system function that prints names +of all entities with `Player` component. + +```rust +use bevy::prelude::*; +use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::lua::prelude::*; + +#[derive(Component)] +struct Player; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_scripting::(|runtime| { + runtime.add_function( + String::from("print_player_names"), + |players: Query<&Name, With>| { + for player in &players { + println!("player name: {}", player); + } + }, + ); + }) + .run(); +} +``` + +In script: + +```lua +print_player_names() +``` + +You can use functions that interact with Bevy entities and resources and +take arguments at the same time. It could be used for example to mutate a +component. + +```rust +use bevy::prelude::*; +use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::lua::prelude::*; + +#[derive(Component)] +struct Player { + health: i32 +} + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_scripting::(|runtime| { + runtime.add_function( + String::from("hurt_player"), + |In((hit_value,)): In<(i32,)>, mut players: Query<&mut Player>| { + let mut player = players.single_mut(); + player.health -= hit_value; + }, + ); + }) + .run(); +} +``` + +And it could be called in script like: + +```lua +hurt_player(5) +``` diff --git a/book/src/lua/lua.md b/book/src/lua/lua.md new file mode 100644 index 0000000..891ffd4 --- /dev/null +++ b/book/src/lua/lua.md @@ -0,0 +1,3 @@ +# Lua + +This chapter demonstrates how to work with bevy_scriptum when using Lua language runtime. diff --git a/book/src/lua/spawning_scripts.md b/book/src/lua/spawning_scripts.md new file mode 100644 index 0000000..41a49b4 --- /dev/null +++ b/book/src/lua/spawning_scripts.md @@ -0,0 +1,40 @@ +# Spawning scripts + +To spawn a Lua script you will need to get a handle to a script asset using +bevy's `AssetServer`. + +```rust +use bevy::prelude::*; +use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::lua::prelude::*; + +fn my_spawner(mut commands: Commands, assets_server: Res) { + commands.spawn(Script::::new( + assets_server.load("my_script.lua"), + )); +} + +fn main() {} +``` + +After they scripts have been evaled by bevy_scriptum, the entities that they've +been attached to will get the `Script::` component stripped and instead +```LuaScriptData``` component will be attached. + +So to query scipted entities you could do something like: + +```rust +use bevy::prelude::*; +use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::lua::prelude::*; + +fn my_system( + mut scripted_entities: Query<(Entity, &mut LuaScriptData)>, +) { + for (entity, mut script_data) in &mut scripted_entities { + // do something with scripted entities + } +} + +fn main() {} +``` diff --git a/book/src/rhai/hello_world.md b/book/src/rhai/hello_world.md new file mode 100644 index 0000000..2965834 --- /dev/null +++ b/book/src/rhai/hello_world.md @@ -0,0 +1 @@ +# Hello World diff --git a/book/src/rhai/installation.md b/book/src/rhai/installation.md new file mode 100644 index 0000000..f76e899 --- /dev/null +++ b/book/src/rhai/installation.md @@ -0,0 +1,12 @@ +# Installation + +Add the following to your `Cargo.toml`: + +```toml +[dependencies] +bevy = "0.13" +bevy_scriptum = { version = "0.5", features = ["rhai"] } +``` + +If you need a different version of bevy you need to use a matching bevy_scriptum +version according to the [bevy support matrix](../bevy_support_matrix.md) diff --git a/book/src/rhai/rhai.md b/book/src/rhai/rhai.md new file mode 100644 index 0000000..db10014 --- /dev/null +++ b/book/src/rhai/rhai.md @@ -0,0 +1,3 @@ +# Rhai + +This chapter demonstrates how to work with bevy_scriptum when using Rhai language runtime. diff --git a/book/src/runtimes.md b/book/src/runtimes.md new file mode 100644 index 0000000..31f0439 --- /dev/null +++ b/book/src/runtimes.md @@ -0,0 +1,3 @@ +# Runtimes + +This chapter demonstrates how to work with bevy_scriptum when using a specific runtime. diff --git a/book/src/workflow/live_reload.md b/book/src/workflow/live_reload.md new file mode 100644 index 0000000..e300307 --- /dev/null +++ b/book/src/workflow/live_reload.md @@ -0,0 +1,139 @@ +# Live-reload + +## Bevy included support + +To enable life reload it should be enough to enable `file-watcher` feature +within bevy dependency in `Cargo.toml` + +``` +bevy = { version = "0.13", features = ["file_watcher"] } +``` + +## Init-teardown pattern for game development + +It is useful to structure your game in a way that would allow making changes to +the scripting code without restarting the game. + +A useful pattern is to hava three functions "init", "update" and "teardown". + +- "init" function will take care of starting the game(spawning the player, the level etc) + +- "update" function will run the main game logic + +- "teardown" function will despawn all the entities so game starts at fresh state. + +This pattern is very easy to implement in bevy_scriptum. All you need is to define all needed functions +in script: + +```lua +player = { + entity = nil +} + +-- spawning all needed entities +local function init() + player.entity = spawn_player() +end + +-- game logic here, should be called in a bevy system using call_fn +local function update() + (...) +end + +-- despawning entities and possible other cleanup logic needed +local function teardown() + despawn(player.entity) +end + +-- call init to start the game, this will be called on each file-watcher script +-- reload +init() +``` + +The function calls can be implemented on Rust side the following way: + +```rust +use bevy::prelude::*; +use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::lua::prelude::*; +use bevy_scriptum::runtimes::lua::BevyVec3; + +fn init(mut commands: Commands, assets_server: Res) { + commands.spawn(Script::::new( + assets_server.load("scripts/game.lua"), + )); +} + + +fn update( + mut scripted_entities: Query<(Entity, &mut LuaScriptData)>, + scripting_runtime: ResMut, +) { + for (entity, mut script_data) in &mut scripted_entities { + scripting_runtime + .call_fn("update", &mut script_data, entity, ()) + .unwrap(); + } +} + + +fn teardown( + mut ev_asset: EventReader>, + scripting_runtime: ResMut, + mut scripted_entities: Query<(Entity, &mut LuaScriptData)>, +) { + for event in ev_asset.read() { + if let AssetEvent::Modified { .. } = event { + for (entity, mut script_data) in &mut scripted_entities { + scripting_runtime + .call_fn("teardown", &mut script_data, entity, ()) + .unwrap(); + } + } + } +} + +fn main() {} +``` + +And to tie this all together we do the following: + +```rust +use bevy::prelude::*; +use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::lua::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_scripting::(|builder| { + builder + .add_function(String::from("spawn_player"), spawn_player) + .add_function(String::from("despawn"), despawn); + }) + .add_systems(Startup, init) + .add_systems(Update, (update, teardown)) + .run(); +} + +fn init() {} // Implemented elsewhere +fn update() {} // Implemented elsewhere +fn despawn() {} // Implemented elsewhere +fn teardown() {} // Implemented elsewhere +fn spawn_player() {} // Implemented elsewhere +``` + +`despawn` can be implemented as: + +```rust +use bevy::prelude::*; +use bevy_scriptum::runtimes::lua::prelude::*; + +fn despawn(In((entity,)): In<(BevyEntity,)>, mut commands: Commands) { + commands.entity(entity.0).despawn(); +} + +fn main() {} // Implemented elsewhere +``` + +Implementation of `spawn_player` has been left out as an exercise for the reader. diff --git a/book/src/workflow/workflow.md b/book/src/workflow/workflow.md new file mode 100644 index 0000000..36811cd --- /dev/null +++ b/book/src/workflow/workflow.md @@ -0,0 +1,3 @@ +# Workflow + +Demonstration of useful approaches when working with bevy_scriptum. diff --git a/examples/current_entity.rs b/examples/current_entity.rs deleted file mode 100644 index ff6dd5b..0000000 --- a/examples/current_entity.rs +++ /dev/null @@ -1,23 +0,0 @@ -use bevy::prelude::*; -use bevy_scriptum::{prelude::*, Script}; - -fn main() { - App::new() - .add_plugins(DefaultPlugins) - .add_plugins(ScriptingPlugin) - .add_script_function( - String::from("get_name"), - |In((entity,)): In<(Entity,)>, names: Query<&Name>| { - names.get(entity).unwrap().to_string() - }, - ) - .add_systems(Startup, 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 deleted file mode 100644 index b918dd1..0000000 --- a/examples/custom_type.rs +++ /dev/null @@ -1,37 +0,0 @@ -use bevy::prelude::*; -use bevy_scriptum::{prelude::*, Script, ScriptingRuntime}; - -fn main() { - App::new() - .add_plugins(DefaultPlugins) - .add_plugins(ScriptingPlugin) - .add_script_function(String::from("hello_bevy"), || { - println!("hello bevy, called from script"); - }) - .add_systems(Startup, 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 deleted file mode 100644 index 37cb2fb..0000000 --- a/examples/ecs.rs +++ /dev/null @@ -1,29 +0,0 @@ -use bevy::prelude::*; -use bevy_scriptum::{prelude::*, Script}; - -#[derive(Component)] -struct Player; - -fn main() { - App::new() - .add_plugins(DefaultPlugins) - .add_plugins(ScriptingPlugin) - .add_script_function( - String::from("print_player_names"), - |players: Query<&Name, With>| { - for player in &players { - println!("player name: {}", player); - } - }, - ) - .add_systems(Startup, 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 deleted file mode 100644 index 7b64f8c..0000000 --- a/examples/entity_variable.rs +++ /dev/null @@ -1,16 +0,0 @@ -use bevy::prelude::*; -use bevy_scriptum::{prelude::*, Script}; - -fn main() { - App::new() - .add_plugins(DefaultPlugins) - .add_plugins(ScriptingPlugin) - .add_systems(Startup, 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 deleted file mode 100644 index 251a875..0000000 --- a/examples/function_params.rs +++ /dev/null @@ -1,47 +0,0 @@ -use bevy::prelude::*; -use bevy_scriptum::{prelude::*, Script}; -use rhai::ImmutableString; - -fn main() { - App::new() - .add_plugins(DefaultPlugins) - .add_plugins(ScriptingPlugin) - .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_systems(Startup, 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 deleted file mode 100644 index 66da4f6..0000000 --- a/examples/hello_world.rs +++ /dev/null @@ -1,17 +0,0 @@ -use bevy::prelude::*; -use bevy_scriptum::{prelude::*, Script}; - -fn main() { - App::new() - .add_plugins(DefaultPlugins) - .add_plugins(ScriptingPlugin) - .add_script_function(String::from("hello_bevy"), || { - println!("hello bevy, called from script"); - }) - .add_systems(Startup, 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/lua/call_function_from_rust.rs b/examples/lua/call_function_from_rust.rs new file mode 100644 index 0000000..2ec78c8 --- /dev/null +++ b/examples/lua/call_function_from_rust.rs @@ -0,0 +1,50 @@ +use bevy::{app::AppExit, ecs::event::ManualEventReader, prelude::*}; +use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::lua::prelude::*; + +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 app_exit_event_reader + .read(&app_exit_events) + .last() + .is_some() + { + break; + } + } + app.update(); + } + }) + .add_plugins(DefaultPlugins) + .add_systems(Startup, startup) + .add_systems(Update, call_lua_on_update_from_rust) + .add_scripting::(|runtime| { + runtime.add_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/lua/call_function_from_rust.lua"), + )); +} + +fn call_lua_on_update_from_rust( + mut scripted_entities: Query<(Entity, &mut LuaScriptData)>, + 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/lua/current_entity.rs b/examples/lua/current_entity.rs new file mode 100644 index 0000000..f0a9504 --- /dev/null +++ b/examples/lua/current_entity.rs @@ -0,0 +1,25 @@ +use bevy::prelude::*; +use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::lua::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_scripting::(|runtime| { + runtime.add_function( + String::from("get_name"), + |In((BevyEntity(entity),)): In<(BevyEntity,)>, names: Query<&Name>| { + names.get(entity).unwrap().to_string() + }, + ); + }) + .add_systems(Startup, startup) + .run(); +} + +fn startup(mut commands: Commands, assets_server: Res) { + commands.spawn(( + Name::from("MyEntityName"), + Script::::new(assets_server.load("examples/lua/current_entity.lua")), + )); +} diff --git a/examples/lua/custom_type.rs b/examples/lua/custom_type.rs new file mode 100644 index 0000000..ce8c025 --- /dev/null +++ b/examples/lua/custom_type.rs @@ -0,0 +1,50 @@ +use bevy::prelude::*; +use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::lua::prelude::*; +use mlua::{UserData, UserDataMethods}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_scripting::(|runtime| { + runtime.add_function(String::from("hello_bevy"), || { + println!("hello bevy, called from script"); + }); + }) + .add_systems(Startup, startup) + .run(); +} + +#[derive(Clone)] +struct MyType { + my_field: u32, +} + +impl UserData for MyType {} + +fn startup( + mut commands: Commands, + mut scripting_runtime: ResMut, + assets_server: Res, +) { + scripting_runtime.with_engine_mut(|engine| { + engine + .register_userdata_type::(|typ| { + // Register a method on MyType + typ.add_method("my_method", |_, my_type_instance: &MyType, ()| { + Ok(my_type_instance.my_field) + }) + }) + .unwrap(); + + // Register a "constructor" for MyType + let my_type_constructor = engine + .create_function(|_, ()| Ok(MyType { my_field: 42 })) + .unwrap(); + engine.globals().set("MyType", my_type_constructor).unwrap(); + }); + + commands.spawn(Script::::new( + assets_server.load("examples/lua/custom_type.lua"), + )); +} diff --git a/examples/lua/ecs.rs b/examples/lua/ecs.rs new file mode 100644 index 0000000..386b50d --- /dev/null +++ b/examples/lua/ecs.rs @@ -0,0 +1,33 @@ +use bevy::prelude::*; +use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::lua::prelude::*; + +#[derive(Component)] +struct Player; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_scripting::(|runtime| { + runtime.add_function( + String::from("print_player_names"), + |players: Query<&Name, With>| { + for player in &players { + println!("player name: {}", player); + } + }, + ); + }) + .add_systems(Startup, 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/lua/ecs.lua"), + )); +} diff --git a/examples/lua/entity_variable.rs b/examples/lua/entity_variable.rs new file mode 100644 index 0000000..0be6b24 --- /dev/null +++ b/examples/lua/entity_variable.rs @@ -0,0 +1,17 @@ +use bevy::prelude::*; +use bevy_scriptum::runtimes::lua::prelude::*; +use bevy_scriptum::{prelude::*, BuildScriptingRuntime}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_scripting::(|_| {}) + .add_systems(Startup, startup) + .run(); +} + +fn startup(mut commands: Commands, assets_server: Res) { + commands.spawn(Script::::new( + assets_server.load("examples/lua/entity_variable.lua"), + )); +} diff --git a/examples/lua/function_params.rs b/examples/lua/function_params.rs new file mode 100644 index 0000000..ba17f31 --- /dev/null +++ b/examples/lua/function_params.rs @@ -0,0 +1,59 @@ +use bevy::prelude::*; +use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::lua::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_scripting::(|runtime| { + runtime + .add_function(String::from("fun_without_params"), || { + println!("called without params"); + }) + .add_function( + String::from("fun_with_string_param"), + |In((x,)): In<(String,)>| { + println!("called with string: '{}'", x); + }, + ) + .add_function( + String::from("fun_with_i64_param"), + |In((x,)): In<(i64,)>| { + println!("called with i64: {}", x); + }, + ) + .add_function( + String::from("fun_with_multiple_params"), + |In((x, y)): In<(i64, String)>| { + println!("called with i64: {} and string: '{}'", x, y); + }, + ) + .add_function( + String::from("fun_with_i64_and_array_param"), + |In((x, y)): In<(i64, mlua::RegistryKey)>, runtime: Res| { + runtime.with_engine(|engine| { + println!( + "called with i64: {} and dynamically typed array: [{:?}]", + x, + engine + .registry_value::(&y) + .unwrap() + .pairs::() + .map(|pair| pair.unwrap()) + .map(|(_, v)| format!("{:?}", v)) + .collect::>() + .join(",") + ); + }); + }, + ); + }) + .add_systems(Startup, startup) + .run(); +} + +fn startup(mut commands: Commands, assets_server: Res) { + commands.spawn(Script::::new( + assets_server.load("examples/lua/function_params.lua"), + )); +} diff --git a/examples/lua/hello_world.rs b/examples/lua/hello_world.rs new file mode 100644 index 0000000..fa31b94 --- /dev/null +++ b/examples/lua/hello_world.rs @@ -0,0 +1,21 @@ +use bevy::prelude::*; +use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::lua::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_scripting::(|runtime| { + runtime.add_function(String::from("hello_bevy"), || { + println!("hello bevy, called from script"); + }); + }) + .add_systems(Startup, startup) + .run(); +} + +fn startup(mut commands: Commands, assets_server: Res) { + commands.spawn(Script::::new( + assets_server.load("examples/lua/hello_world.lua"), + )); +} diff --git a/examples/lua/non_closure_system.rs b/examples/lua/non_closure_system.rs new file mode 100644 index 0000000..4e9e71c --- /dev/null +++ b/examples/lua/non_closure_system.rs @@ -0,0 +1,23 @@ +use bevy::prelude::*; +use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::lua::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_scripting::(|runtime| { + runtime.add_function(String::from("hello_bevy"), hello_bevy_callback_system); + }) + .add_systems(Startup, startup) + .run(); +} + +fn startup(mut commands: Commands, assets_server: Res) { + commands.spawn(Script::::new( + assets_server.load("examples/lua/hello_world.lua"), + )); +} + +fn hello_bevy_callback_system() { + println!("hello bevy, called from script"); +} diff --git a/examples/lua/promises.rs b/examples/lua/promises.rs new file mode 100644 index 0000000..b1095a4 --- /dev/null +++ b/examples/lua/promises.rs @@ -0,0 +1,26 @@ +use bevy::prelude::*; +use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::lua::prelude::*; + +#[derive(Component)] +struct Player; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_scripting::(|builder| { + builder.add_function( + String::from("get_player_name"), + |player_names: Query<&Name, With>| player_names.single().to_string(), + ); + }) + .add_systems(Startup, startup) + .run(); +} + +fn startup(mut commands: Commands, assets_server: Res) { + commands.spawn((Player, Name::new("John"))); + commands.spawn(Script::::new( + assets_server.load("examples/lua/promises.lua"), + )); +} diff --git a/examples/side_effects.rs b/examples/lua/side_effects.rs similarity index 80% rename from examples/side_effects.rs rename to examples/lua/side_effects.rs index 08525a5..12d9003 100644 --- a/examples/side_effects.rs +++ b/examples/lua/side_effects.rs @@ -1,8 +1,6 @@ use bevy::{app::AppExit, ecs::event::ManualEventReader, prelude::*}; -use bevy_scriptum::{prelude::*, Script}; - -#[derive(Component)] -struct Comp; +use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::lua::prelude::*; fn main() { App::new() @@ -24,10 +22,11 @@ fn main() { } }) .add_plugins(DefaultPlugins) - .add_plugins(ScriptingPlugin) .add_systems(Startup, startup) .add_systems(Update, print_entity_names_and_quit) - .add_script_function(String::from("spawn_entity"), spawn_entity) + .add_scripting::(|runtime| { + runtime.add_function(String::from("spawn_entity"), spawn_entity); + }) .run(); } @@ -36,8 +35,8 @@ fn spawn_entity(mut commands: Commands) { } fn startup(mut commands: Commands, assets_server: Res) { - commands.spawn((Script::new( - assets_server.load("examples/side_effects.rhai"), + commands.spawn((Script::::new( + assets_server.load("examples/lua/side_effects.lua"), ),)); } diff --git a/examples/non_closure_system.rs b/examples/non_closure_system.rs deleted file mode 100644 index 903b713..0000000 --- a/examples/non_closure_system.rs +++ /dev/null @@ -1,19 +0,0 @@ -use bevy::prelude::*; -use bevy_scriptum::{prelude::*, Script}; - -fn main() { - App::new() - .add_plugins(DefaultPlugins) - .add_plugins(ScriptingPlugin) - .add_script_function(String::from("hello_bevy"), hello_bevy_callback_system) - .add_systems(Startup, 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 deleted file mode 100644 index b1ac3c7..0000000 --- a/examples/promises.rs +++ /dev/null @@ -1,22 +0,0 @@ -use bevy::prelude::*; -use bevy_scriptum::{prelude::*, Script}; - -#[derive(Component)] -struct Player; - -fn main() { - App::new() - .add_plugins(DefaultPlugins) - .add_plugins(ScriptingPlugin) - .add_script_function( - String::from("get_player_name"), - |player_names: Query<&Name, With>| player_names.single().to_string(), - ) - .add_systems(Startup, 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/examples/call_function_from_rust.rs b/examples/rhai/call_function_from_rust.rs similarity index 71% rename from examples/call_function_from_rust.rs rename to examples/rhai/call_function_from_rust.rs index 797136a..fb174ca 100644 --- a/examples/call_function_from_rust.rs +++ b/examples/rhai/call_function_from_rust.rs @@ -1,5 +1,6 @@ use bevy::{app::AppExit, ecs::event::ManualEventReader, prelude::*}; -use bevy_scriptum::{prelude::*, Script, ScriptData, ScriptingRuntime}; +use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::rhai::prelude::*; fn main() { App::new() @@ -21,24 +22,25 @@ fn main() { } }) .add_plugins(DefaultPlugins) - .add_plugins(ScriptingPlugin) .add_systems(Startup, startup) .add_systems(Update, call_rhai_on_update_from_rust) - .add_script_function(String::from("quit"), |mut exit: EventWriter| { - exit.send(AppExit); + .add_scripting::(|runtime| { + runtime.add_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"), + commands.spawn(Script::::new( + assets_server.load("examples/rhai/call_function_from_rust.rhai"), )); } fn call_rhai_on_update_from_rust( - mut scripted_entities: Query<(Entity, &mut ScriptData)>, - mut scripting_runtime: ResMut, + mut scripted_entities: Query<(Entity, &mut RhaiScriptData)>, + scripting_runtime: ResMut, ) { for (entity, mut script_data) in &mut scripted_entities { scripting_runtime diff --git a/examples/rhai/current_entity.rs b/examples/rhai/current_entity.rs new file mode 100644 index 0000000..d177371 --- /dev/null +++ b/examples/rhai/current_entity.rs @@ -0,0 +1,25 @@ +use bevy::prelude::*; +use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::rhai::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_scripting::(|runtime| { + runtime.add_function( + String::from("get_name"), + |In((entity,)): In<(Entity,)>, names: Query<&Name>| { + names.get(entity).unwrap().to_string() + }, + ); + }) + .add_systems(Startup, startup) + .run(); +} + +fn startup(mut commands: Commands, assets_server: Res) { + commands.spawn(( + Name::from("MyEntityName"), + Script::::new(assets_server.load("examples/rhai/current_entity.rhai")), + )); +} diff --git a/examples/rhai/custom_type.rs b/examples/rhai/custom_type.rs new file mode 100644 index 0000000..dfcceb9 --- /dev/null +++ b/examples/rhai/custom_type.rs @@ -0,0 +1,41 @@ +use bevy::prelude::*; +use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::rhai::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_scripting::(|runtime| { + runtime.add_function(String::from("hello_bevy"), || { + println!("hello bevy, called from script"); + }); + }) + .add_systems(Startup, startup) + .run(); +} + +#[derive(Clone)] +struct MyType { + my_field: u32, +} + +fn startup( + mut commands: Commands, + mut scripting_runtime: ResMut, + assets_server: Res, +) { + scripting_runtime.with_engine_mut(|engine| { + 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/rhai/custom_type.rhai"), + )); +} diff --git a/examples/rhai/ecs.rs b/examples/rhai/ecs.rs new file mode 100644 index 0000000..85bb6ca --- /dev/null +++ b/examples/rhai/ecs.rs @@ -0,0 +1,33 @@ +use bevy::prelude::*; +use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::rhai::prelude::*; + +#[derive(Component)] +struct Player; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_scripting::(|runtime| { + runtime.add_function( + String::from("print_player_names"), + |players: Query<&Name, With>| { + for player in &players { + println!("player name: {}", player); + } + }, + ); + }) + .add_systems(Startup, 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/rhai/ecs.rhai"), + )); +} diff --git a/examples/rhai/entity_variable.rs b/examples/rhai/entity_variable.rs new file mode 100644 index 0000000..d7e70bc --- /dev/null +++ b/examples/rhai/entity_variable.rs @@ -0,0 +1,17 @@ +use bevy::prelude::*; +use bevy_scriptum::runtimes::rhai::prelude::*; +use bevy_scriptum::{prelude::*, BuildScriptingRuntime}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_scripting::(|_| {}) + .add_systems(Startup, startup) + .run(); +} + +fn startup(mut commands: Commands, assets_server: Res) { + commands.spawn(Script::::new( + assets_server.load("examples/rhai/entity_variable.rhai"), + )); +} diff --git a/examples/rhai/function_params.rs b/examples/rhai/function_params.rs new file mode 100644 index 0000000..2c1820e --- /dev/null +++ b/examples/rhai/function_params.rs @@ -0,0 +1,50 @@ +use bevy::prelude::*; +use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::rhai::prelude::*; +use rhai::ImmutableString; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_scripting::(|runtime| { + runtime + .add_function(String::from("fun_without_params"), || { + println!("called without params"); + }) + .add_function( + String::from("fun_with_string_param"), + |In((x,)): In<(ImmutableString,)>| { + println!("called with string: '{}'", x); + }, + ) + .add_function( + String::from("fun_with_i64_param"), + |In((x,)): In<(i64,)>| { + println!("called with i64: {}", x); + }, + ) + .add_function( + String::from("fun_with_multiple_params"), + |In((x, y)): In<(i64, ImmutableString)>| { + println!("called with i64: {} and string: '{}'", x, y); + }, + ) + .add_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_systems(Startup, startup) + .run(); +} + +fn startup(mut commands: Commands, assets_server: Res) { + commands.spawn(Script::::new( + assets_server.load("examples/rhai/function_params.rhai"), + )); +} diff --git a/examples/rhai/hello_world.rs b/examples/rhai/hello_world.rs new file mode 100644 index 0000000..f15c0cd --- /dev/null +++ b/examples/rhai/hello_world.rs @@ -0,0 +1,21 @@ +use bevy::prelude::*; +use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::rhai::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_scripting::(|runtime| { + runtime.add_function(String::from("hello_bevy"), || { + println!("hello bevy, called from script"); + }); + }) + .add_systems(Startup, startup) + .run(); +} + +fn startup(mut commands: Commands, assets_server: Res) { + commands.spawn(Script::::new( + assets_server.load("examples/rhai/hello_world.rhai"), + )); +} diff --git a/examples/rhai/non_closure_system.rs b/examples/rhai/non_closure_system.rs new file mode 100644 index 0000000..29e3944 --- /dev/null +++ b/examples/rhai/non_closure_system.rs @@ -0,0 +1,23 @@ +use bevy::prelude::*; +use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::rhai::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_scripting::(|runtime| { + runtime.add_function(String::from("hello_bevy"), hello_bevy_callback_system); + }) + .add_systems(Startup, startup) + .run(); +} + +fn startup(mut commands: Commands, assets_server: Res) { + commands.spawn(Script::::new( + assets_server.load("examples/rhai/hello_world.rhai"), + )); +} + +fn hello_bevy_callback_system() { + println!("hello bevy, called from script"); +} diff --git a/examples/rhai/promises.rs b/examples/rhai/promises.rs new file mode 100644 index 0000000..8b2a5d8 --- /dev/null +++ b/examples/rhai/promises.rs @@ -0,0 +1,26 @@ +use bevy::prelude::*; +use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::rhai::prelude::*; + +#[derive(Component)] +struct Player; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_scripting::(|builder| { + builder.add_function( + String::from("get_player_name"), + |player_names: Query<&Name, With>| player_names.single().to_string(), + ); + }) + .add_systems(Startup, startup) + .run(); +} + +fn startup(mut commands: Commands, assets_server: Res) { + commands.spawn((Player, Name::new("John"))); + commands.spawn(Script::::new( + assets_server.load("examples/rhai/promises.rhai"), + )); +} diff --git a/examples/rhai/side_effects.rs b/examples/rhai/side_effects.rs new file mode 100644 index 0000000..6964c01 --- /dev/null +++ b/examples/rhai/side_effects.rs @@ -0,0 +1,50 @@ +use bevy::{app::AppExit, ecs::event::ManualEventReader, prelude::*}; +use bevy_scriptum::prelude::*; +use bevy_scriptum::runtimes::rhai::prelude::*; + +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 app_exit_event_reader + .read(&app_exit_events) + .last() + .is_some() + { + break; + } + } + app.update(); + } + }) + .add_plugins(DefaultPlugins) + .add_systems(Startup, startup) + .add_systems(Update, print_entity_names_and_quit) + .add_scripting::(|runtime| { + runtime.add_function(String::from("spawn_entity"), spawn_entity); + }) + .run(); +} + +fn spawn_entity(mut commands: Commands) { + commands.spawn(Name::new("SpawnedEntity")); +} + +fn startup(mut commands: Commands, assets_server: Res) { + commands.spawn((Script::::new( + assets_server.load("examples/rhai/side_effects.rhai"), + ),)); +} + +fn print_entity_names_and_quit(query: Query<&Name>, mut exit: EventWriter) { + if !query.is_empty() { + for e in &query { + println!("{}", e); + } + exit.send(AppExit); + } +} diff --git a/src/assets.rs b/src/assets.rs index 842d8a7..118c1eb 100644 --- a/src/assets.rs +++ b/src/assets.rs @@ -1,20 +1,31 @@ +use std::marker::PhantomData; + use bevy::{ asset::{io::Reader, Asset, AssetLoader, AsyncReadExt as _, LoadContext}, - reflect::TypePath, utils::BoxedFuture, }; -use serde::Deserialize; -/// A script that can be loaded by the [crate::ScriptingPlugin]. -#[derive(Asset, Debug, Deserialize, TypePath)] -pub struct RhaiScript(pub String); +/// A loader for script assets. +pub struct ScriptLoader> { + _phantom_data: PhantomData, +} -/// A loader for [RhaiScript] assets. -#[derive(Default)] -pub struct RhaiScriptLoader; +impl> Default for ScriptLoader { + fn default() -> Self { + Self { + _phantom_data: Default::default(), + } + } +} -impl AssetLoader for RhaiScriptLoader { - type Asset = RhaiScript; +/// Allows providing an allow-list for extensions of AssetLoader for a Script +/// asset +pub trait GetExtensions { + fn extensions() -> &'static [&'static str]; +} + +impl + GetExtensions> AssetLoader for ScriptLoader { + type Asset = A; type Settings = (); type Error = anyhow::Error; @@ -23,17 +34,18 @@ impl AssetLoader for RhaiScriptLoader { reader: &'a mut Reader, _settings: &'a Self::Settings, _load_context: &'a mut LoadContext, - ) -> BoxedFuture<'a, anyhow::Result> { + ) -> BoxedFuture<'a, anyhow::Result> { Box::pin(async move { let mut bytes = Vec::new(); reader.read_to_end(&mut bytes).await?; - let rhai_script = RhaiScript(String::from_utf8(bytes.to_vec())?); + let script_text = String::from_utf8(bytes.to_vec())?; + let rhai_script: A = script_text.into(); Ok(rhai_script) }) } fn extensions(&self) -> &[&str] { - &["rhai"] + A::extensions() } } diff --git a/src/callback.rs b/src/callback.rs index 47b7ce8..19ea2d0 100644 --- a/src/callback.rs +++ b/src/callback.rs @@ -2,64 +2,80 @@ use bevy::prelude::*; use core::any::TypeId; use std::sync::{Arc, Mutex}; -use rhai::{Dynamic, Variant}; - -use crate::promise::Promise; +use crate::{promise::Promise, Runtime}; /// A system that can be used to call a script function. -pub struct CallbackSystem { - pub(crate) system: Box, Out = Dynamic>>, +pub struct CallbackSystem { + pub(crate) system: Box, Out = R::Value>>, pub(crate) arg_types: Vec, } -pub(crate) struct FunctionCallEvent { - pub(crate) params: Vec, - pub(crate) promise: Promise, +pub(crate) struct FunctionCallEvent { + pub(crate) params: Vec, + pub(crate) promise: Promise, } +type Calls = Arc>>>; + /// A struct representing a Bevy system that can be called from a script. -#[derive(Clone)] -pub(crate) struct Callback { +pub(crate) struct Callback { pub(crate) name: String, - pub(crate) system: Arc>, - pub(crate) calls: Arc>>, + pub(crate) system: Arc>>, + pub(crate) calls: Calls, } -impl CallbackSystem { - pub(crate) fn call(&mut self, call: &FunctionCallEvent, world: &mut World) -> Dynamic { +impl Clone for Callback { + fn clone(&self) -> Self { + Callback { + name: self.name.clone(), + system: self.system.clone(), + calls: self.calls.clone(), + } + } +} + +impl CallbackSystem { + pub(crate) fn call( + &mut self, + call: &FunctionCallEvent, + world: &mut World, + ) -> R::Value { 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; +/// Allows converting to a wrapper type that the library uses internally for data +pub(crate) trait IntoRuntimeValueWithEngine<'a, V, R: Runtime> { + fn into_runtime_value_with_engine(value: V, engine: &'a R::RawEngine) -> R::Value; } -impl RegisterCallbackFunction for FN +/// Allows converting from a wrapper type that the library uses internally for data to underlying +/// concrete type. +pub(crate) trait FromRuntimeValueWithEngine<'a, R: Runtime> { + fn from_runtime_value_with_engine(value: R::Value, engine: &'a R::RawEngine) -> Self; +} + +/// Trait that alllows to convert a script callback function into a Bevy [`System`]. +pub trait IntoCallbackSystem: IntoSystem { + /// Convert this function into a [CallbackSystem]. + #[must_use] + fn into_callback_system(self, world: &mut World) -> CallbackSystem; +} + +impl IntoCallbackSystem for FN where FN: IntoSystem<(), Out, Marker>, - Out: Sync + Variant + Clone, + Out: for<'a> IntoRuntimeValueWithEngine<'a, Out, R>, { - fn into_callback_system(self, world: &mut World) -> CallbackSystem { + 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 system_fn = move |_args: In>, world: &mut World| { let result = inner_system.run((), world); inner_system.apply_deferred(world); - Dynamic::from(result) + let mut runtime = world.get_resource_mut::().expect("No runtime resource"); + runtime + .with_engine_mut(move |engine| Out::into_runtime_value_with_engine(result, engine)) }; let system = IntoSystem::into_system(system_fn); CallbackSystem { @@ -71,23 +87,29 @@ where macro_rules! impl_tuple { ($($idx:tt $t:tt),+) => { - impl<$($t,)+ Out, FN, Marker> RegisterCallbackFunction + impl IntoCallbackSystem for FN where FN: IntoSystem<($($t,)+), Out, Marker>, - Out: Sync + Variant + Clone, - $($t: 'static + Clone,)+ + Out: for<'a> IntoRuntimeValueWithEngine<'a, Out, RN>, + $($t: 'static + for<'a> FromRuntimeValueWithEngine<'a, RN>,)+ { - fn into_callback_system(self, world: &mut World) -> CallbackSystem { + 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>(), )+ - ); + let system_fn = move |args: In>, world: &mut World| { + let mut runtime = world.get_resource_mut::().expect("No runtime resource"); + let args = runtime.with_engine_mut(move |engine| { + ( + $($t::from_runtime_value_with_engine(args.get($idx).expect(&format!("Failed to get function argument for index {}", $idx)).clone(), engine), )+ + ) + }); let result = inner_system.run(args, world); inner_system.apply_deferred(world); - Dynamic::from(result) + let mut runtime = world.get_resource_mut::().expect("No runtime resource"); + runtime.with_engine_mut(move |engine| { + Out::into_runtime_value_with_engine(result, engine) + }) }; let system = IntoSystem::into_system(system_fn); CallbackSystem { diff --git a/src/components.rs b/src/components.rs index 95ed2c8..a8c8473 100644 --- a/src/components.rs +++ b/src/components.rs @@ -1,24 +1,14 @@ use bevy::prelude::*; -use super::assets::RhaiScript; - /// A component that represents a script. #[derive(Component)] -pub struct Script { - pub script: Handle, +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 { +impl Script { + /// Create a new script component from a handle to a [Script] obtained using [AssetServer]. + pub fn new(script: Handle) -> Self { Self { script } } } diff --git a/src/lib.rs b/src/lib.rs index 15790bc..654507c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,12 @@ //! 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. +//! Currently [Rhai](https://rhai.rs/) and [Lua](https://lua.org/) are supported, but more languages may be added in the future. //! -//! It's main advantages include: +//! Everything you need to know to get started with using this library is contained in the +//! [bevy_scriptum book](https://link-to-book.com) +//! +//! API docs are available in [docs.rs](https://docs.rs/bevy_scriptum/latest/bevy_scriptum/) +//! +//! bevy_scriptum's main advantages include: //! - low-boilerplate //! - easy to use //! - asynchronicity with a promise-based API @@ -14,17 +19,20 @@ //! ```rust //! use bevy::prelude::*; //! use bevy_scriptum::prelude::*; +//! use bevy_scriptum::runtimes::lua::prelude::*; //! //! App::new() //! .add_plugins(DefaultPlugins) -//! .add_plugins(ScriptingPlugin::default()) -//! .add_script_function(String::from("hello_bevy"), || { -//! println!("hello bevy, called from script"); -//! }); +//! .add_scripting::(|runtime| { +//! runtime.add_function(String::from("hello_bevy"), || { +//! println!("hello bevy, called from script"); +//! }); +//! }) +//! .run(); //! ``` //! And you can call them in your scripts like this: -//! ```rhai -//! hello_bevy(); +//! ```lua +//! 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: @@ -32,42 +40,47 @@ //! ```rust //! use bevy::prelude::*; //! use bevy_scriptum::prelude::*; +//! use bevy_scriptum::runtimes::lua::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>| { -//! for player in &players { -//! println!("player name: {}", player); -//! } -//! }, -//! ); +//! .add_scripting::(|runtime| { +//! runtime.add_function( +//! String::from("print_player_names"), +//! |players: Query<&Name, With>| { +//! for player in &players { +//! println!("player name: {}", player); +//! } +//! }, +//! ); +//! }) +//! .run(); //! ``` //! //! 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; +//! use bevy_scriptum::runtimes::lua::prelude::*; //! //! 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); -//! }, -//! ); +//! .add_scripting::(|runtime| { +//! runtime.add_function( +//! String::from("fun_with_string_param"), +//! |In((x,)): In<(String,)>| { +//! println!("called with string: '{}'", x); +//! }, +//! ); +//! }) +//! .run(); //! ``` //! which you can then call in your script like this: -//! ```rhai -//! fun_with_string_param("Hello world!"); +//! ```lua +//! fun_with_string_param("Hello world!") //! ``` //! //! ## Usage @@ -76,65 +89,68 @@ //! //! ```toml //! [dependencies] -//! bevy_scriptum = "0.2" +//! bevy_scriptum = { version = "0.5", features = ["lua"] } //! ``` //! -//! 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(); -//! ``` +//! or execute `cargo add bevy_scriptum --features lua` from your project directory. //! //! 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::*; +//! use bevy_scriptum::runtimes::lua::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); -//! }, -//! ); +//! .add_scripting::(|runtime| { +//! runtime.add_function( +//! String::from("my_print"), +//! |In((x,)): In<(String,)>| { +//! println!("my_print: '{}'", x); +//! }, +//! ); +//! }) +//! .run(); //! ``` //! -//! Then you can create a script file in `assets` directory called `script.rhai` that calls this function: +//! Then you can create a script file in `assets` directory called `script.lua` that calls this function: //! -//! ```rhai -//! my_print("Hello world!"); +//! ```lua +//! my_print("Hello world!") //! ``` //! -//! And spawn a `Script` component with a handle to a script source file`: +//! And spawn an entity with attached `Script` component with a handle to a script source file: //! //! ```rust //! use bevy::prelude::*; -//! use bevy_scriptum::Script; +//! use bevy_scriptum::prelude::*; +//! use bevy_scriptum::runtimes::lua::prelude::*; //! //! App::new() +//! .add_plugins(DefaultPlugins) +//! .add_scripting::(|runtime| { +//! runtime.add_function( +//! String::from("my_print"), +//! |In((x,)): In<(String,)>| { +//! println!("my_print: '{}'", x); +//! }, +//! ); +//! }) //! .add_systems(Startup,|mut commands: Commands, asset_server: Res| { -//! commands.spawn(Script::new(asset_server.load("script.rhai"))); -//! }); +//! commands.spawn(Script::::new(asset_server.load("script.lua"))); +//! }) +//! .run(); //! ``` //! +//! You should then see `my_print: 'Hello world!'` printed in your console. +//! //! ## Provided examples //! -//! You can also try running provided examples by cloning this repository and running `cargo run --example `. For example: +//! You can also try running provided examples by cloning this repository and running `cargo run --example _`. For example: //! //! ```bash -//! cargo run --example hello_world +//! cargo run --example hello_world_lua //! ``` //! The examples live in `examples` directory and their corresponding scripts live in `assets/examples` directory within the repository. //! @@ -142,31 +158,46 @@ //! //! | bevy version | bevy_scriptum version | //! |--------------|----------------------| -//! | 0.13 | 0.4 | +//! | 0.13 | 0.4-0.5 | //! | 0.12 | 0.3 | //! | 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: +//! Every function called from script returns a promise that you can call `:and_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); -//! }); +//! ```lua +//! get_player_name():and_then(function(name) +//! print(name) +//! end) //! ``` +//! which will print out `John` when used with following exposed function: +//! +//! ``` +//! use bevy::prelude::*; +//! use bevy_scriptum::prelude::*; +//! use bevy_scriptum::runtimes::lua::prelude::*; +//! +//! App::new() +//! .add_plugins(DefaultPlugins) +//! .add_scripting::(|runtime| { +//! runtime.add_function(String::from("get_player_name"), || String::from("John")); +//! }); +//! ```` //! //! ## 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 exposes `index` property 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()); +//! ```lua +//! print("Current entity index: " .. entity.index) //! ``` //! +//! `entity` variable is currently not available within promise callbacks. +//! //! ## Contributing //! //! Contributions are welcome! Feel free to open an issue or submit a pull request. @@ -182,19 +213,27 @@ mod components; mod promise; mod systems; -use std::sync::{Arc, Mutex}; +pub mod runtimes; -pub use crate::components::{Script, ScriptData}; -pub use assets::RhaiScript; +pub use crate::components::Script; +use assets::GetExtensions; +use promise::Promise; -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 std::{ + any::TypeId, + fmt::Debug, + hash::Hash, + marker::PhantomData, + sync::{Arc, Mutex}, +}; + +use bevy::{app::MainScheduleOrder, ecs::schedule::ScheduleLabel, prelude::*}; +use callback::{Callback, IntoCallbackSystem}; +use systems::{init_callbacks, log_errors, process_calls}; use thiserror::Error; use self::{ - assets::RhaiScriptLoader, + assets::ScriptLoader, systems::{process_new_scripts, reload_scripts}, }; @@ -204,126 +243,178 @@ const ENTITY_VAR_NAME: &str = "entity"; #[derive(Error, Debug)] pub enum ScriptingError { #[error("script runtime error: {0}")] - RuntimeError(#[from] Box), + RuntimeError(Box), #[error("script compilation error: {0}")] - CompileError(#[from] ParseError), + CompileError(Box), #[error("no runtime resource present")] NoRuntimeResource, #[error("no settings resource present")] NoSettingsResource, } -#[derive(Default)] -pub struct ScriptingPlugin; +/// Trait that represents a scripting runtime/engine. In practice it is +/// implemented for a scripint language interpreter and the implementor provides +/// function implementations for calling and registering functions within the interpreter. +pub trait Runtime: Resource + Default { + type Schedule: ScheduleLabel + Debug + Clone + Eq + Hash + Default; + type ScriptAsset: Asset + From + GetExtensions; + type ScriptData: Component; + type CallContext: Send + Clone; + type Value: Send + Clone; + type RawEngine; -impl Plugin for ScriptingPlugin { - fn build(&self, app: &mut App) { - app.register_asset_loader(RhaiScriptLoader) - .init_asset::() - .init_resource::() - .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), - ), - ); - } -} + /// Provides mutable reference to raw scripting engine instance. + /// Can be used to directly interact with an interpreter to use interfaces + /// that bevy_scriptum does not provided adapters for. + fn with_engine_mut(&mut self, f: impl FnOnce(&mut Self::RawEngine) -> T) -> T; -#[derive(Resource, Default)] -pub struct ScriptingRuntime { - engine: Engine, -} + /// Provides immutable reference to raw scripting engine instance. + /// Can be used to directly interact with an interpreter to use interfaces + /// that bevy_scriptum does not provided adapters for. + fn with_engine(&self, f: impl FnOnce(&Self::RawEngine) -> T) -> T; -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, + fn eval( + &self, + script: &Self::ScriptAsset, 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))?, - } + ) -> Result; + + /// Registers a new function within the scripting engine. Provided callback + /// function will be called when the function with provided name gets called + /// in script. + fn register_fn( + &mut self, + name: String, + arg_types: Vec, + f: impl Fn( + Self::CallContext, + Vec, + ) -> Result, ScriptingError> + + Send + + Sync + + 'static, + ) -> Result<(), ScriptingError>; + + /// Calls a function by name defined within the runtime in the context of the + /// entity that haas been paassed. Can return a dynamically typed value + /// that got returned from the function within a script. + fn call_fn( + &self, + name: &str, + script_data: &mut Self::ScriptData, + entity: Entity, + args: impl for<'a> FuncArgs<'a, Self::Value, Self>, + ) -> Result; + + /// Calls a function by value defined within the runtime in the context of the + /// entity that haas been paassed. Can return a dynamically typed value + /// that got returned from the function within a script. + fn call_fn_from_value( + &self, + value: &Self::Value, + context: &Self::CallContext, + args: Vec, + ) -> Result; +} + +pub trait FuncArgs<'a, V, R: Runtime> { + fn parse(self, engine: &'a R::RawEngine) -> Vec; +} + +/// An extension trait for [App] that allows to setup a scripting runtime `R`. +pub trait BuildScriptingRuntime { + /// Returns a "runtime" type than can be used to setup scripting runtime( + /// add scripting functions etc.). + fn add_scripting(&mut self, f: impl Fn(ScriptingRuntimeBuilder)) -> &mut Self; +} + +pub struct ScriptingRuntimeBuilder<'a, R: Runtime> { + _phantom_data: PhantomData, + world: &'a mut World, +} + +impl<'a, R: Runtime> ScriptingRuntimeBuilder<'a, R> { + fn new(world: &'a mut World) -> Self { + Self { + _phantom_data: PhantomData, + world, } - 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, + /// Registers a function for calling from within a script. + /// Provided function needs to be a valid bevy system and its + /// arguments and return value need to be convertible to runtime + /// value types. + pub fn add_function( + self, name: String, - system: impl RegisterCallbackFunction, - ) -> &mut Self; -} + fun: impl IntoCallbackSystem, + ) -> Self { + let system = fun.into_callback_system(self.world); -/// 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::(); + 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}; +impl BuildScriptingRuntime for App { + /// Adds a scripting runtime. Registers required bevy systems that take + /// care of processing and running the scripts. + fn add_scripting(&mut self, f: impl Fn(ScriptingRuntimeBuilder)) -> &mut Self { + self.world + .resource_mut::() + .insert_after(Update, R::Schedule::default()); + + self.register_asset_loader(ScriptLoader::::default()) + .init_schedule(R::Schedule::default()) + .init_asset::() + .init_resource::>() + .insert_resource(R::default()) + .add_systems( + R::Schedule::default(), + ( + 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::), + ), + ); + + let runtime = ScriptingRuntimeBuilder::::new(&mut self.world); + + f(runtime); + + self + } +} + +/// A resource that stores all the callbacks that were registered using [AddScriptFunctionAppExt::add_function]. +#[derive(Resource)] +struct Callbacks { + uninitialized_callbacks: Vec>, + callbacks: Mutex>>, +} + +impl Default for Callbacks { + fn default() -> Self { + Self { + uninitialized_callbacks: Default::default(), + callbacks: Default::default(), + } + } +} + +pub mod prelude { + pub use crate::{BuildScriptingRuntime as _, Runtime as _, Script}; } diff --git a/src/promise.rs b/src/promise.rs index 0040570..d7bacc9 100644 --- a/src/promise.rs +++ b/src/promise.rs @@ -1,81 +1,80 @@ use std::sync::{Arc, Mutex}; -#[allow(deprecated)] -use rhai::{Dynamic, NativeCallContextStore}; -use rhai::{EvalAltResult, FnPtr}; +use crate::{Runtime, ScriptingError}; /// A struct that represents a function that will get called when the Promise is resolved. -pub(crate) struct PromiseCallback { - callback: Dynamic, - following_promise: Arc>, +pub(crate) struct PromiseCallback { + callback: V, + following_promise: Arc>>, } /// Internal representation of a Promise. -pub(crate) struct PromiseInner { - pub(crate) callbacks: Vec, +pub(crate) struct PromiseInner { + pub(crate) callbacks: Vec>, #[allow(deprecated)] - pub(crate) context_data: NativeCallContextStore, + pub(crate) context: C, } /// A struct that represents a Promise. #[derive(Clone)] -pub struct Promise { - pub(crate) inner: Arc>, +pub struct Promise { + pub(crate) inner: Arc>>, } -impl PromiseInner { +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> { + fn resolve(&mut self, runtime: &mut R, val: R::Value) -> Result<(), ScriptingError> + where + R: Runtime, + { 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()])? - }; + let next_val = + runtime.call_fn_from_value(&callback.callback, &self.context, vec![val.clone()])?; + callback .following_promise .lock() - .unwrap() - .resolve(engine, next_val)?; + .expect("Failed to lock promise mutex") + .resolve(runtime, next_val)?; } Ok(()) } } -impl Promise { +impl Promise { /// Acquire [Mutex] for writing the promise and resolve it. Call will be forwarded to [PromiseInner::resolve]. - pub(crate) fn resolve( + pub(crate) fn resolve( &mut self, - engine: &mut rhai::Engine, - val: Dynamic, - ) -> Result<(), Box> { + runtime: &mut R, + val: R::Value, + ) -> Result<(), ScriptingError> + where + R: Runtime, + { if let Ok(mut inner) = self.inner.lock() { - inner.resolve(engine, val)?; + inner.resolve(runtime, 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(); + pub(crate) fn then(&mut self, callback: V) -> Self { + let mut inner = self + .inner + .lock() + .expect("Failed to lock inner promise mutex"); let following_inner = Arc::new(Mutex::new(PromiseInner { callbacks: vec![], - context_data: inner.context_data.clone(), + context: inner.context.clone(), })); inner.callbacks.push(PromiseCallback { following_promise: following_inner.clone(), callback, }); - Dynamic::from(Promise { + + Promise { inner: following_inner, - }) + } } } diff --git a/src/runtimes/lua.rs b/src/runtimes/lua.rs new file mode 100644 index 0000000..4b240cf --- /dev/null +++ b/src/runtimes/lua.rs @@ -0,0 +1,335 @@ +use bevy::{ + asset::Asset, + ecs::{component::Component, entity::Entity, schedule::ScheduleLabel, system::Resource}, + math::Vec3, + reflect::TypePath, +}; +use mlua::{ + FromLua, Function, IntoLua, IntoLuaMulti, Lua, RegistryKey, UserData, UserDataFields, + UserDataMethods, Variadic, +}; +use serde::Deserialize; +use std::sync::{Arc, Mutex}; + +use crate::{ + assets::GetExtensions, + callback::{FromRuntimeValueWithEngine, IntoRuntimeValueWithEngine}, + promise::Promise, + FuncArgs, Runtime, ScriptingError, ENTITY_VAR_NAME, +}; + +type LuaEngine = Arc>; + +#[derive(Clone)] +pub struct LuaValue(Arc); + +impl LuaValue { + fn new<'a, T: IntoLua<'a>>(engine: &'a Lua, value: T) -> Self { + Self(Arc::new( + engine + .create_registry_value(value) + .expect("Error creating a registry key for value"), + )) + } +} + +#[derive(Resource)] +pub struct LuaRuntime { + engine: LuaEngine, +} + +#[derive(Debug, Clone, Copy)] +pub struct BevyEntity(pub Entity); + +impl UserData for BevyEntity {} + +impl FromLua<'_> for BevyEntity { + fn from_lua( + value: mlua::prelude::LuaValue<'_>, + _lua: &'_ Lua, + ) -> mlua::prelude::LuaResult { + match value { + mlua::Value::UserData(ud) => Ok(*ud.borrow::()?), + _ => panic!("got {:?} instead of BevyEntity", value), + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct BevyVec3(pub Vec3); + +impl UserData for BevyVec3 {} + +impl FromLua<'_> for BevyVec3 { + fn from_lua( + value: mlua::prelude::LuaValue<'_>, + _lua: &'_ Lua, + ) -> mlua::prelude::LuaResult { + match value { + mlua::Value::UserData(ud) => Ok(*ud.borrow::()?), + _ => panic!("got {:?} instead of BevyVec3", value), + } + } +} + +impl Default for LuaRuntime { + fn default() -> Self { + let engine = LuaEngine::default(); + + { + let engine = engine.lock().expect("Failed to lock engine"); + engine + .register_userdata_type::(|typ| { + typ.add_field_method_get("index", |_, entity| Ok(entity.0.index())); + }) + .expect("Failed to register BevyEntity userdata type"); + + engine + .register_userdata_type::>(|typ| { + typ.add_method_mut("and_then", |engine, promise, callback: Function| { + Ok(Promise::then(promise, LuaValue::new(engine, callback))) + }); + }) + .expect("Failed to register Promise userdata type"); + + engine + .register_userdata_type::(|typ| { + typ.add_field_method_get("x", |_engine, vec| Ok(vec.0.x)); + typ.add_field_method_get("y", |_engine, vec| Ok(vec.0.y)); + typ.add_field_method_get("z", |_engine, vec| Ok(vec.0.z)); + }) + .expect("Failed to register BevyVec3 userdata type"); + let vec3_constructor = engine + .create_function(|_, (x, y, z)| Ok(BevyVec3(Vec3::new(x, y, z)))) + .expect("Failed to create Vec3 constructor"); + engine + .globals() + .set("Vec3", vec3_constructor) + .expect("Failed to set Vec3 global"); + } + + Self { engine } + } +} + +#[derive(ScheduleLabel, Clone, PartialEq, Eq, Debug, Hash, Default)] +pub struct LuaSchedule; + +#[derive(Asset, Debug, Deserialize, TypePath)] +pub struct LuaScript(pub String); + +impl GetExtensions for LuaScript { + fn extensions() -> &'static [&'static str] { + &["lua"] + } +} + +impl From for LuaScript { + fn from(value: String) -> Self { + Self(value) + } +} + +#[derive(Component)] +pub struct LuaScriptData; + +impl Runtime for LuaRuntime { + type Schedule = LuaSchedule; + + type ScriptAsset = LuaScript; + + type ScriptData = LuaScriptData; + + type CallContext = (); + + type Value = LuaValue; + + type RawEngine = Lua; + + fn eval( + &self, + script: &Self::ScriptAsset, + entity: bevy::prelude::Entity, + ) -> Result { + self.with_engine(|engine| { + engine + .globals() + .set(ENTITY_VAR_NAME, BevyEntity(entity)) + .expect("Error setting entity global variable"); + let result = engine.load(&script.0).exec(); + engine + .globals() + .set(ENTITY_VAR_NAME, mlua::Value::Nil) + .expect("Error clearing entity global variable"); + result + }) + .map_err(|e| ScriptingError::RuntimeError(Box::new(e)))?; + Ok(LuaScriptData) + } + + fn register_fn( + &mut self, + name: String, + _arg_types: Vec, + f: impl Fn( + Self::CallContext, + Vec, + ) -> Result< + crate::promise::Promise, + crate::ScriptingError, + > + Send + + Sync + + 'static, + ) -> Result<(), crate::ScriptingError> { + self.with_engine(|engine| { + let func = engine + .create_function(move |engine, args: Variadic| { + let args = { args.into_iter().map(|x| LuaValue::new(engine, x)).collect() }; + let result = f((), args).unwrap(); + Ok(result) + }) + .unwrap(); + engine + .globals() + .set(name, func) + .expect("Error registering function in global lua scope"); + }); + Ok(()) + } + + fn call_fn( + &self, + name: &str, + _script_data: &mut Self::ScriptData, + entity: bevy::prelude::Entity, + args: impl for<'a> FuncArgs<'a, Self::Value, Self>, + ) -> Result { + self.with_engine(|engine| { + engine + .globals() + .set(ENTITY_VAR_NAME, BevyEntity(entity)) + .expect("Error setting entity global variable"); + let func = engine + .globals() + .get::<_, Function>(name) + .map_err(|e| ScriptingError::RuntimeError(Box::new(e)))?; + let args = args + .parse(engine) + .into_iter() + .map(|a| engine.registry_value::(&a.0).unwrap()); + let result = func + .call::<_, mlua::Value>(Variadic::from_iter(args)) + .map_err(|e| ScriptingError::RuntimeError(Box::new(e)))?; + engine + .globals() + .set(ENTITY_VAR_NAME, mlua::Value::Nil) + .expect("Error clearing entity global variable"); + Ok(LuaValue::new(engine, result)) + }) + } + + fn call_fn_from_value( + &self, + value: &Self::Value, + _context: &Self::CallContext, + args: Vec, + ) -> Result { + self.with_engine(|engine| { + let val = engine + .registry_value::(&value.0) + .map_err(|e| ScriptingError::RuntimeError(Box::new(e)))?; + let args = args + .into_iter() + .map(|a| engine.registry_value::(&a.0).unwrap()); + let result = val + .call::<_, mlua::Value>(Variadic::from_iter(args)) + .map_err(|e| ScriptingError::RuntimeError(Box::new(e)))?; + Ok(LuaValue::new(engine, result)) + }) + } + + fn with_engine_mut(&mut self, f: impl FnOnce(&mut Self::RawEngine) -> T) -> T { + let mut engine = self.engine.lock().unwrap(); + f(&mut engine) + } + + fn with_engine(&self, f: impl FnOnce(&Self::RawEngine) -> T) -> T { + let engine = self.engine.lock().unwrap(); + f(&engine) + } +} + +impl<'a, T: IntoLuaMulti<'a>> IntoRuntimeValueWithEngine<'a, T, LuaRuntime> for T { + fn into_runtime_value_with_engine(value: T, engine: &'a Lua) -> LuaValue { + let mut iter = value.into_lua_multi(engine).unwrap().into_iter(); + if iter.len() > 1 { + unimplemented!("Returning multiple values from function"); + } + LuaValue(Arc::new(engine.create_registry_value(iter.next()).unwrap())) + } +} + +impl<'a, T: FromLua<'a>> FromRuntimeValueWithEngine<'a, LuaRuntime> for T { + fn from_runtime_value_with_engine(value: LuaValue, engine: &'a Lua) -> Self { + engine.registry_value(&value.0).unwrap() + } +} + +impl FuncArgs<'_, LuaValue, LuaRuntime> for () { + fn parse(self, _engine: &Lua) -> Vec { + Vec::new() + } +} + +impl<'a, T: IntoLua<'a>> FuncArgs<'a, LuaValue, LuaRuntime> for Vec { + fn parse(self, engine: &'a Lua) -> Vec { + self.into_iter().map(|x| LuaValue::new(engine, x)).collect() + } +} + +impl UserData for Promise<(), LuaValue> {} + +pub mod prelude { + pub use super::{BevyEntity, BevyVec3, LuaRuntime, LuaScript, LuaScriptData}; +} + +macro_rules! impl_tuple { + ($($idx:tt $t:tt),+) => { + impl<'a, $($t: IntoLua<'a>,)+> FuncArgs<'a, LuaValue, LuaRuntime> + for ($($t,)+) + { + fn parse(self, engine: &'a Lua) -> Vec { + vec![ + $(LuaValue::new(engine, self.$idx), )+ + ] + } + } + }; +} + +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/runtimes/mod.rs b/src/runtimes/mod.rs new file mode 100644 index 0000000..c01166f --- /dev/null +++ b/src/runtimes/mod.rs @@ -0,0 +1,4 @@ +#[cfg(feature = "lua")] +pub mod lua; +#[cfg(feature = "rhai")] +pub mod rhai; diff --git a/src/runtimes/rhai.rs b/src/runtimes/rhai.rs new file mode 100644 index 0000000..8e34fbd --- /dev/null +++ b/src/runtimes/rhai.rs @@ -0,0 +1,266 @@ +use std::fmt::Debug; + +use bevy::{ + asset::Asset, + ecs::{component::Component, entity::Entity, schedule::ScheduleLabel, system::Resource}, + math::Vec3, + reflect::TypePath, +}; +use rhai::{CallFnOptions, Dynamic, Engine, FnPtr, Scope, Variant}; +use serde::Deserialize; + +use crate::{ + assets::GetExtensions, + callback::{FromRuntimeValueWithEngine, IntoRuntimeValueWithEngine}, + promise::Promise, + FuncArgs, Runtime, ScriptingError, ENTITY_VAR_NAME, +}; + +#[derive(Asset, Debug, Deserialize, TypePath)] +pub struct RhaiScript(pub String); + +impl GetExtensions for RhaiScript { + fn extensions() -> &'static [&'static str] { + &["rhai"] + } +} + +impl From for RhaiScript { + fn from(value: String) -> Self { + Self(value) + } +} + +#[derive(Resource)] +pub struct RhaiRuntime { + engine: rhai::Engine, +} + +#[derive(ScheduleLabel, Clone, PartialEq, Eq, Debug, Hash, Default)] +pub struct RhaiSchedule; + +/// 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 RhaiScriptData { + pub scope: rhai::Scope<'static>, + pub(crate) ast: rhai::AST, +} + +#[derive(Debug, Clone)] +pub struct RhaiValue(rhai::Dynamic); + +impl Runtime for RhaiRuntime { + type Schedule = RhaiSchedule; + type ScriptAsset = RhaiScript; + type ScriptData = RhaiScriptData; + #[allow(deprecated)] + type CallContext = rhai::NativeCallContextStore; + type Value = RhaiValue; + type RawEngine = rhai::Engine; + + fn eval( + &self, + script: &Self::ScriptAsset, + entity: Entity, + ) -> Result { + let mut scope = Scope::new(); + scope.push(ENTITY_VAR_NAME, entity); + + let engine = &self.engine; + + let ast = engine + .compile_with_scope(&scope, script.0.as_str()) + .map_err(|e| ScriptingError::CompileError(Box::new(e)))?; + + engine + .run_ast_with_scope(&mut scope, &ast) + .map_err(|e| ScriptingError::RuntimeError(Box::new(e)))?; + + scope.remove::(ENTITY_VAR_NAME).unwrap(); + + Ok(Self::ScriptData { ast, scope }) + } + + fn register_fn( + &mut self, + name: String, + arg_types: Vec, + f: impl Fn( + Self::CallContext, + Vec, + ) -> Result, ScriptingError> + + Send + + Sync + + 'static, + ) -> Result<(), ScriptingError> { + self.engine + .register_raw_fn(name, arg_types, move |context, args| { + let args = args.iter_mut().map(|arg| RhaiValue(arg.clone())).collect(); + #[allow(deprecated)] + let promise = f(context.store_data(), args).unwrap(); + Ok(promise) + }); + Ok(()) + } + + fn call_fn( + &self, + name: &str, + script_data: &mut Self::ScriptData, + entity: Entity, + args: impl for<'a> FuncArgs<'a, Self::Value, Self>, + ) -> Result { + 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 args = args + .parse(&self.engine) + .into_iter() + .map(|a| a.0) + .collect::>(); + let result = self + .engine + .call_fn_with_options::(options, scope, &ast, name, args); + scope.remove::(ENTITY_VAR_NAME).unwrap(); + match result { + Ok(val) => Ok(RhaiValue(val)), + Err(e) => Err(ScriptingError::RuntimeError(Box::new(e))), + } + } + + fn call_fn_from_value( + &self, + value: &Self::Value, + context: &Self::CallContext, + args: Vec, + ) -> Result { + let f = value.0.clone_cast::(); + + #[allow(deprecated)] + let ctx = &context.create_context(&self.engine); + + let result = if args.len() == 1 && args.first().unwrap().0.is_unit() { + f.call_raw(ctx, None, []) + .map_err(|e| ScriptingError::RuntimeError(e))? + } else { + let args = args.into_iter().map(|a| a.0).collect::>(); + f.call_raw(ctx, None, args) + .map_err(|e| ScriptingError::RuntimeError(e))? + }; + + Ok(RhaiValue(result)) + } + + fn with_engine_mut(&mut self, f: impl FnOnce(&mut Self::RawEngine) -> T) -> T { + f(&mut self.engine) + } + + fn with_engine(&self, f: impl FnOnce(&Self::RawEngine) -> T) -> T { + f(&self.engine) + } +} + +impl Default for RhaiRuntime { + fn default() -> Self { + let mut engine = Engine::new(); + + engine + .register_type_with_name::("Entity") + .register_get("index", |entity: &mut Entity| entity.index()); + #[allow(deprecated)] + engine + .register_type_with_name::>("Promise") + .register_fn( + "then", + |promise: &mut Promise, + callback: rhai::Dynamic| { + Promise::then(promise, RhaiValue(callback)); + }, + ); + + 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")); + + RhaiRuntime { engine } + } +} + +impl<'a, T: Clone + Variant> IntoRuntimeValueWithEngine<'a, T, RhaiRuntime> for T { + fn into_runtime_value_with_engine(value: T, _engine: &'a rhai::Engine) -> RhaiValue { + RhaiValue(Dynamic::from(value)) + } +} + +impl FuncArgs<'_, RhaiValue, RhaiRuntime> for () { + fn parse(self, _engnie: &rhai::Engine) -> Vec { + Vec::new() + } +} +impl FuncArgs<'_, RhaiValue, RhaiRuntime> for Vec { + fn parse(self, _engine: &rhai::Engine) -> Vec { + self.into_iter() + .map(|v| RhaiValue(Dynamic::from(v))) + .collect() + } +} + +impl FromRuntimeValueWithEngine<'_, RhaiRuntime> for T { + fn from_runtime_value_with_engine(value: RhaiValue, _engine: &rhai::Engine) -> Self { + value.0.clone_cast() + } +} + +pub mod prelude { + pub use super::{RhaiRuntime, RhaiScript, RhaiScriptData}; +} + +macro_rules! impl_tuple { + ($($idx:tt $t:tt),+) => { + impl<$($t: Clone +Variant,)+> FuncArgs<'_, RhaiValue, RhaiRuntime> + for ($($t,)+) + { + fn parse(self, _engine: &rhai::Engine) -> Vec { + vec![ + $(RhaiValue(Dynamic::from(self.$idx)), )+ + ] + } + } + }; +} + +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/systems.rs b/src/systems.rs index dae433b..54904e9 100644 --- a/src/systems.rs +++ b/src/systems.rs @@ -1,5 +1,4 @@ use bevy::{prelude::*, utils::tracing}; -use rhai::Scope; use std::{ fmt::Display, sync::{Arc, Mutex}, @@ -7,171 +6,161 @@ use std::{ use crate::{ callback::FunctionCallEvent, - components::ScriptData, promise::{Promise, PromiseInner}, - Callback, Callbacks, ScriptingError, ENTITY_VAR_NAME, + Callback, Callbacks, Runtime, ScriptingError, }; -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(()) -} +use super::components::Script; /// Reloads scripts when they are modified. -pub(crate) fn reload_scripts( +pub(crate) fn reload_scripts( mut commands: Commands, - mut ev_asset: EventReader>, - mut scripts: Query<(Entity, &mut Script)>, + mut ev_asset: EventReader>, + mut scripts: Query<(Entity, &mut Script)>, ) { for ev in ev_asset.read() { if let AssetEvent::Modified { id } = ev { for (entity, script) in &mut scripts { if script.script.id() == *id { - commands.entity(entity).remove::(); + 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( +/// Processes new scripts. Evaluates them and stores the script data in the entity. +#[allow(clippy::type_complexity)] +pub(crate) fn process_new_scripts( mut commands: Commands, - mut added_scripted_entities: Query<(Entity, &mut Script), Without>, - scripting_runtime: ResMut, - scripts: Res>, + mut added_scripted_entities: Query< + (Entity, &mut Script), + Without, + >, + scripting_runtime: ResMut, + scripts: Res>, + asset_server: Res, ) -> Result<(), ScriptingError> { for (entity, script_component) in &mut added_scripted_entities { - trace!("process_new_scripts: evaulating a new script"); + tracing::trace!("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 }); + match scripting_runtime.eval(script, entity) { + Ok(script_data) => { + commands.entity(entity).insert(script_data); + } + Err(e) => { + let path = asset_server + .get_path(&script_component.script) + .unwrap_or_default(); + tracing::error!("error running script {} {:?}", path, e); + } + } } } Ok(()) } /// Initializes callbacks. Registers them in the scripting engine. -pub(crate) fn init_callbacks(world: &mut World) -> Result<(), ScriptingError> { +pub(crate) fn init_callbacks(world: &mut World) -> Result<(), ScriptingError> { let mut callbacks_resource = world - .get_resource_mut::() + .get_resource_mut::>() .ok_or(ScriptingError::NoSettingsResource)?; let mut callbacks = callbacks_resource .uninitialized_callbacks .drain(..) - .collect::>(); + .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::() + .get_resource_mut::() .ok_or(ScriptingError::NoRuntimeResource)?; - trace!("init_callbacks: registering callback: '{}'", callback.name); - let engine = &mut scripting_runtime.engine; + tracing::trace!("init_callbacks: registering callback: '{}'", callback.name); + let callback = callback.clone(); - engine.register_raw_fn( + + let result = scripting_runtime.register_fn( callback.name, system.arg_types.clone(), - move |context, args| { - #[allow(deprecated)] - let context_data = context.store_data(); + move |context, params| { let promise = Promise { inner: Arc::new(Mutex::new(PromiseInner { callbacks: vec![], - context_data, + context, })), }; - let mut calls = callback.calls.lock().unwrap(); + let mut calls = callback + .calls + .lock() + .expect("Failed to lock callback calls mutex"); + calls.push(FunctionCallEvent { promise: promise.clone(), - params: args.iter_mut().map(|arg| arg.clone()).collect(), + params, }); Ok(promise) }, ); + if let Err(e) = result { + tracing::error!("error registering function: {:?}", e); + } } } let callbacks_resource = world - .get_resource_mut::() + .get_resource_mut::>() .ok_or(ScriptingError::NoSettingsResource)?; callbacks_resource .callbacks .lock() - .unwrap() + .expect("Failed to lock callbacks mutex") .append(&mut callbacks.clone()); Ok(()) } /// Processes calls. Calls the user-defined callback systems -pub(crate) fn process_calls(world: &mut World) -> Result<(), ScriptingError> { +pub(crate) fn process_calls(world: &mut World) -> Result<(), ScriptingError> { let callbacks_resource = world - .get_resource::() + .get_resource::>() .ok_or(ScriptingError::NoSettingsResource)?; - let callbacks = callbacks_resource.callbacks.lock().unwrap().clone(); + let callbacks = callbacks_resource + .callbacks + .lock() + .expect("Failed to lock callbacks mutex") + .clone(); for callback in callbacks.into_iter() { let calls = callback .calls .lock() - .unwrap() + .expect("Failed to lock callback calls mutex") .drain(..) - .collect::>(); + .collect::>>(); for mut call in calls { - trace!("process_calls: calling '{}'", callback.name); - let mut system = callback.system.lock().unwrap(); + tracing::trace!("process_calls: calling '{}'", callback.name); + let mut system = callback + .system + .lock() + .expect("Failed to lock callback system mutex"); let val = system.call(&call, world); let mut runtime = world - .get_resource_mut::() + .get_resource_mut::() .ok_or(ScriptingError::NoRuntimeResource)?; - call.promise.resolve(&mut runtime.engine, val)?; + + let result = call.promise.resolve(runtime.as_mut(), val); + match result { + Ok(_) => {} + Err(e) => { + tracing::error!("error resolving call: {} {:?}", callback.name, e); + } + } } } Ok(()) diff --git a/tests/tests.rs b/tests/tests.rs new file mode 100644 index 0000000..5b18f4d --- /dev/null +++ b/tests/tests.rs @@ -0,0 +1,472 @@ +use std::sync::OnceLock; + +use bevy::ecs::system::RunSystemOnce as _; +use bevy::prelude::*; +use bevy_scriptum::{prelude::*, FuncArgs, Runtime}; +use mlua::Table; + +static TRACING_SUBSCRIBER: OnceLock<()> = OnceLock::new(); + +fn build_test_app() -> App { + let mut app = App::new(); + + if std::env::var("RUST_TEST_LOG").is_ok() { + TRACING_SUBSCRIBER.get_or_init(|| { + tracing_subscriber::fmt().init(); + }); + } + + app.add_plugins((AssetPlugin::default(), TaskPoolPlugin::default())); + app +} + +fn run_script( + app: &mut App, + path: String, + system: impl IntoSystem<(), Out, Marker>, +) -> Entity { + let asset_server = app.world.get_resource_mut::().unwrap(); + let asset = asset_server.load::(path); + + let entity_id = app.world.spawn(Script::new(asset)).id(); + app.update(); // let `ScriptData` resources be added to entities + app.world.run_system_once(system); + app.update(); // let callbacks be executed + + entity_id +} + +fn call_script_on_update_from_rust( + mut scripted_entities: Query<(Entity, &mut R::ScriptData)>, + scripting_runtime: ResMut, +) where + (): for<'a> FuncArgs<'a, R::Value, R>, +{ + let (entity, mut script_data) = scripted_entities.single_mut(); + scripting_runtime + .call_fn("test_func", &mut script_data, entity, ()) + .unwrap(); +} + +trait AssertStateKeyValue { + type ScriptData; + fn assert_state_key_value_i64(world: &World, entity_id: Entity, key: &str, value: i64); + fn assert_state_key_value_i32(world: &World, entity_id: Entity, key: &str, value: i32); + fn assert_state_key_value_string(world: &World, entity_id: Entity, key: &str, value: &str); +} + +macro_rules! scripting_tests { + ($runtime:ty, $script:literal, $extension:literal) => { + use super::*; + + #[test] + fn call_script_function_with_params() { + let mut app = build_test_app(); + + app.add_scripting::<$runtime>(|_| {}); + + run_script::<$runtime, _, _>( + &mut app, + format!( + "tests/{}/call_script_function_with_params.{}", + $script, $extension + ) + .to_string(), + |mut scripted_entities: Query<(Entity, &mut <$runtime as Runtime>::ScriptData)>, + scripting_runtime: ResMut<$runtime>| { + let (entity, mut script_data) = scripted_entities.single_mut(); + scripting_runtime + .call_fn("test_func", &mut script_data, entity, vec![1]) + .unwrap(); + }, + ); + } + + #[test] + fn rust_function_gets_called_from_script_with_param() { + let mut app = build_test_app(); + + #[derive(Default, Resource)] + struct IntResource { + my_int: i64, + } + + app.world.init_resource::(); + + app.add_scripting::<$runtime>(|runtime| { + runtime.add_function( + String::from("rust_func"), + |In((x,)): In<(i64,)>, mut res: ResMut| { + res.my_int = x; + }, + ); + }); + + run_script::<$runtime, _, _>( + &mut app, + format!( + "tests/{}/rust_function_gets_called_from_script_with_param.{}", + $script, $extension + ) + .to_string(), + call_script_on_update_from_rust::<$runtime>, + ); + + assert_eq!(app.world.get_resource::().unwrap().my_int, 5); + } + + #[test] + fn rust_function_gets_called_from_script_with_multiple_params() { + let mut app = build_test_app(); + + #[derive(Default, Resource)] + struct TestResource { + a: i64, + b: String, + } + + app.world.init_resource::(); + + app.add_scripting::<$runtime>(|runtime| { + runtime.add_function( + String::from("rust_func"), + |In((a, b)): In<(i64, String)>, mut res: ResMut| { + res.a = a; + res.b = b; + }, + ); + }); + + run_script::<$runtime, _, _>( + &mut app, + format!( + "tests/{}/rust_function_gets_called_from_script_with_multiple_params.{}", + $script, $extension + ) + .to_string(), + call_script_on_update_from_rust::<$runtime>, + ); + + assert_eq!(app.world.get_resource::().unwrap().a, 5); + assert_eq!( + app.world.get_resource::().unwrap().b, + String::from("test") + ); + } + + #[test] + fn test_script_function_gets_called_from_rust_with_single_param() { + let mut app = build_test_app(); + + app.add_scripting::<$runtime>(|_| {}); + + let entity_id = run_script::<$runtime, _, _>( + &mut app, + format!( + "tests/{}/script_function_gets_called_from_rust_with_single_param.{}", + $script, $extension + ) + .to_string(), + |mut scripted_entities: Query<(Entity, &mut <$runtime as Runtime>::ScriptData)>, + scripting_runtime: ResMut<$runtime>| { + let (entity, mut script_data) = scripted_entities.single_mut(); + scripting_runtime + .call_fn("test_func", &mut script_data, entity, vec![1]) + .unwrap(); + }, + ); + + <$runtime>::assert_state_key_value_i32(&app.world, entity_id, "a_value", 1i32); + } + + #[test] + fn test_script_function_gets_called_from_rust_with_heterogenous_params() { + let mut app = build_test_app(); + + app.add_scripting::<$runtime>(|_| {}); + + let entity_id = run_script::<$runtime, _, _>( + &mut app, + format!( + "tests/{}/script_function_gets_called_from_rust_with_multiple_params.{}", + $script, $extension + ) + .to_string(), + |mut scripted_entities: Query<(Entity, &mut <$runtime as Runtime>::ScriptData)>, + scripting_runtime: ResMut<$runtime>| { + let (entity, mut script_data) = scripted_entities.single_mut(); + scripting_runtime + .call_fn( + "test_func", + &mut script_data, + entity, + (1, String::from("abc")), + ) + .unwrap(); + }, + ); + + <$runtime>::assert_state_key_value_i32(&app.world, entity_id, "a_value", 1i32); + <$runtime>::assert_state_key_value_string( + &app.world, + entity_id, + "b_value", + &String::from("abc"), + ); + } + + #[test] + fn test_script_function_gets_called_from_rust_with_multiple_params() { + let mut app = build_test_app(); + + app.add_scripting::<$runtime>(|_| {}); + + let entity_id = run_script::<$runtime, _, _>( + &mut app, + format!( + "tests/{}/script_function_gets_called_from_rust_with_multiple_params.{}", + $script, $extension + ) + .to_string(), + |mut scripted_entities: Query<(Entity, &mut <$runtime as Runtime>::ScriptData)>, + scripting_runtime: ResMut<$runtime>| { + let (entity, mut script_data) = scripted_entities.single_mut(); + scripting_runtime + .call_fn("test_func", &mut script_data, entity, vec![1, 2]) + .unwrap(); + }, + ); + + <$runtime>::assert_state_key_value_i32(&app.world, entity_id, "a_value", 1i32); + <$runtime>::assert_state_key_value_i32(&app.world, entity_id, "b_value", 2i32); + } + + #[test] + fn test_call_script_function_that_casues_runtime_error() { + let mut app = build_test_app(); + + app.add_scripting::<$runtime>(|_| {}); + + run_script::<$runtime, _, _>( + &mut app, + format!( + "tests/{}/call_script_function_that_causes_runtime_error.{}", + $script, $extension + ) + .to_string(), + |mut scripted_entities: Query<(Entity, &mut <$runtime as Runtime>::ScriptData)>, + scripting_runtime: ResMut<$runtime>| { + let (entity, mut script_data) = scripted_entities.single_mut(); + let result = + scripting_runtime.call_fn("test_func", &mut script_data, entity, ()); + assert!(result.is_err()); + }, + ); + } + + #[test] + fn test_call_script_function_that_does_not_exist() { + let mut app = build_test_app(); + + app.add_scripting::<$runtime>(|_| {}); + + run_script::<$runtime, _, _>( + &mut app, + format!( + "tests/{}/call_script_function_that_causes_runtime_error.{}", + $script, $extension + ) + .to_string(), + |mut scripted_entities: Query<(Entity, &mut <$runtime as Runtime>::ScriptData)>, + scripting_runtime: ResMut<$runtime>| { + let (entity, mut script_data) = scripted_entities.single_mut(); + let result = + scripting_runtime.call_fn("does_not_exist", &mut script_data, entity, ()); + assert!(result.is_err()); + }, + ); + } + + #[test] + fn test_script_function_gets_called_from_rust() { + let mut app = build_test_app(); + + app.add_scripting::<$runtime>(|_| {}); + + let entity_id = run_script::<$runtime, _, _>( + &mut app, + format!( + "tests/{}/script_function_gets_called_from_rust.{}", + $script, $extension + ) + .to_string(), + call_script_on_update_from_rust::<$runtime>, + ); + + <$runtime>::assert_state_key_value_i64(&app.world, entity_id, "times_called", 1i64); + } + + #[test] + fn test_promise() { + let mut app = build_test_app(); + + app.add_scripting::<$runtime>(|runtime| { + runtime.add_function(String::from("rust_func"), || 123); + }); + + let entity_id = run_script::<$runtime, _, _>( + &mut app, + format!("tests/{}/return_via_promise.{}", $script, $extension).to_string(), + call_script_on_update_from_rust::<$runtime>, + ); + + <$runtime>::assert_state_key_value_i32(&app.world, entity_id, "x", 123i32); + } + + #[test] + fn test_promise_runtime_error_does_not_panic() { + let mut app = build_test_app(); + + app.add_scripting::<$runtime>(|runtime| { + runtime.add_function(String::from("rust_func"), || 123); + }); + + run_script::<$runtime, _, _>( + &mut app, + format!("tests/{}/promise_runtime_error.{}", $script, $extension).to_string(), + call_script_on_update_from_rust::<$runtime>, + ); + } + + #[test] + fn test_side_effects() { + let mut app = build_test_app(); + + #[derive(Component)] + struct MyTag; + + app.add_scripting::<$runtime>(|runtime| { + runtime.add_function(String::from("spawn_entity"), |mut commands: Commands| { + commands.spawn(MyTag); + }); + }); + + run_script::<$runtime, _, _>( + &mut app, + format!("tests/{}/side_effects.{}", $script, $extension).to_string(), + call_script_on_update_from_rust::<$runtime>, + ); + + app.world.run_system_once(|tagged: Query<&MyTag>| { + tagged.single(); + }); + } + + #[test] + fn test_rust_function_gets_called_from_script() { + let mut app = build_test_app(); + + #[derive(Default, Resource)] + struct TimesCalled { + times_called: u8, + } + + app.world.init_resource::(); + + app.add_scripting::<$runtime>(|runtime| { + runtime.add_function(String::from("rust_func"), |mut res: ResMut| { + res.times_called += 1; + }); + }); + + run_script::<$runtime, _, _>( + &mut app, + format!( + "tests/{}/rust_function_gets_called_from_script.{}", + $script, $extension + ) + .to_string(), + call_script_on_update_from_rust::<$runtime>, + ); + + assert_eq!( + app.world + .get_resource::() + .unwrap() + .times_called, + 1 + ); + } + }; +} + +#[cfg(feature = "rhai")] +mod rhai_tests { + use bevy::prelude::*; + use bevy_scriptum::runtimes::rhai::prelude::*; + + impl AssertStateKeyValue for RhaiRuntime { + type ScriptData = RhaiScriptData; + + fn assert_state_key_value_i64(world: &World, entity_id: Entity, key: &str, value: i64) { + let script_data = world.get::(entity_id).unwrap(); + let state = script_data.scope.get_value::("state").unwrap(); + assert_eq!(state[key].clone_cast::(), value); + } + + fn assert_state_key_value_i32(world: &World, entity_id: Entity, key: &str, value: i32) { + let script_data = world.get::(entity_id).unwrap(); + let state = script_data.scope.get_value::("state").unwrap(); + assert_eq!(state[key].clone_cast::(), value); + } + + fn assert_state_key_value_string(world: &World, entity_id: Entity, key: &str, value: &str) { + let script_data = world.get::(entity_id).unwrap(); + let state = script_data.scope.get_value::("state").unwrap(); + assert_eq!(state[key].clone_cast::(), value); + } + } + + scripting_tests!(RhaiRuntime, "rhai", "rhai"); +} + +#[cfg(feature = "lua")] +mod lua_tests { + use bevy::prelude::*; + use bevy_scriptum::runtimes::lua::prelude::*; + + impl AssertStateKeyValue for LuaRuntime { + type ScriptData = LuaScriptData; + + fn assert_state_key_value_i64(world: &World, _entity_id: Entity, key: &str, value: i64) { + let runtime = world.get_resource::().unwrap(); + runtime.with_engine(|engine| { + let state = engine.globals().get::<_, Table>("State").unwrap(); + assert_eq!(state.get::<_, i64>(key).unwrap(), value); + }); + } + + fn assert_state_key_value_i32(world: &World, _entity_id: Entity, key: &str, value: i32) { + let runtime = world.get_resource::().unwrap(); + runtime.with_engine(|engine| { + let state = engine.globals().get::<_, Table>("State").unwrap(); + assert_eq!(state.get::<_, i32>(key).unwrap(), value); + }); + } + + fn assert_state_key_value_string( + world: &World, + _entity_id: Entity, + key: &str, + value: &str, + ) { + let runtime = world.get_resource::().unwrap(); + runtime.with_engine(|engine| { + let state = engine.globals().get::<_, Table>("State").unwrap(); + assert_eq!(state.get::<_, String>(key).unwrap(), value); + }); + } + } + + scripting_tests!(LuaRuntime, "lua", "lua"); +}