Lua support (#16)

* adds Lua language support
* makes the library generic, allowing for easy extension for more languages
* small improvements and fixes
This commit is contained in:
Jarosław Konik 2024-06-16 07:06:09 +02:00 committed by GitHub
parent 7846b556ca
commit 6726e40768
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
104 changed files with 3433 additions and 666 deletions

38
.github/workflows/deploy_book.yml vendored Normal file
View file

@ -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

View file

@ -17,12 +17,12 @@ jobs:
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Clippy - name: Clippy
run: cargo clippy --verbose -- -D warnings run: cargo clippy --all-features --verbose -- -D warnings
- name: Build - name: Build
run: cargo build --verbose run: cargo build --all-features --verbose
- name: Run tests - name: Run tests
run: cargo test --verbose run: cargo test --all-features --verbose
- name: Install cargo-examples - name: Install cargo-examples
run: cargo install cargo-examples run: cargo install cargo-examples
- name: Run all examples - name: Run all examples
run: cargo examples run: cargo examples --features=lua,rhai

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
/target /target
/Cargo.lock /Cargo.lock
.vscode .vscode
rust-analyzer.json

View file

@ -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" repository = "https://github.com/jarkonik/bevy_scriptum"
keywords = ["bevy", "rhai", "scripting", "game", "gamedev"] 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] [dependencies]
bevy = { default-features = false, version = "0.13.0", features = [ bevy = { default-features = false, version = "0.13.0", features = [
"bevy_asset", "bevy_asset",
] } ] }
serde = "1.0.162" 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" thiserror = "1.0.40"
anyhow = "1.0.82" 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"] }

167
README.md
View file

@ -1,9 +1,14 @@
# bevy_scriptum 📜 # 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. 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 - low-boilerplate
- easy to use - easy to use
- asynchronicity with a promise-based API - 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 ```rust
use bevy::prelude::*; use bevy::prelude::*;
use bevy_scriptum::prelude::*; use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;
App::new() App::new()
.add_plugins(DefaultPlugins) .add_plugins(DefaultPlugins)
.add_plugins(ScriptingPlugin::default()) .add_scripting::<LuaRuntime>(|runtime| {
.add_script_function(String::from("hello_bevy"), || { runtime.add_function(String::from("hello_bevy"), || {
println!("hello bevy, called from script"); println!("hello bevy, called from script");
}); });
})
.run();
``` ```
And you can call them in your scripts like this: And you can call them in your scripts like this:
```rhai ```lua
hello_bevy(); 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: 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 ```rust
use bevy::prelude::*; use bevy::prelude::*;
use bevy_scriptum::prelude::*; use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;
#[derive(Component)] #[derive(Component)]
struct Player; struct Player;
App::new() App::new()
.add_plugins(DefaultPlugins) .add_plugins(DefaultPlugins)
.add_plugins(ScriptingPlugin::default()) .add_scripting::<LuaRuntime>(|runtime| {
.add_script_function( runtime.add_function(
String::from("print_player_names"), String::from("print_player_names"),
|players: Query<&Name, With<Player>>| { |players: Query<&Name, With<Player>>| {
for player in &players { for player in &players {
println!("player name: {}", player); 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: You can also pass arguments to your callback functions, just like you would in a regular Bevy system - using `In` structs with tuples:
```rust ```rust
use bevy::prelude::*; use bevy::prelude::*;
use bevy_scriptum::prelude::*; use bevy_scriptum::prelude::*;
use rhai::ImmutableString; use bevy_scriptum::runtimes::lua::prelude::*;
App::new() App::new()
.add_plugins(DefaultPlugins) .add_plugins(DefaultPlugins)
.add_plugins(ScriptingPlugin::default()) .add_scripting::<LuaRuntime>(|runtime| {
.add_script_function( runtime.add_function(
String::from("fun_with_string_param"), String::from("fun_with_string_param"),
|In((x,)): In<(ImmutableString,)>| { |In((x,)): In<(String,)>| {
println!("called with string: '{}'", x); println!("called with string: '{}'", x);
}, },
); );
})
.run();
``` ```
which you can then call in your script like this: which you can then call in your script like this:
```rhai ```lua
fun_with_string_param("Hello world!"); fun_with_string_param("Hello world!")
``` ```
### Usage ### Usage
@ -78,65 +91,68 @@ Add the following to your `Cargo.toml`:
```toml ```toml
[dependencies] [dependencies]
bevy_scriptum = "0.2" bevy_scriptum = { version = "0.5", features = ["lua"] }
``` ```
or execute `cargo add bevy_scriptum` from your project directory. or execute `cargo add bevy_scriptum --features lua` from your project directory.
Add the following to your `main.rs`:
```rust
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(ScriptingPlugin::default())
.run();
```
You can now start exposing functions to the scripting language. For example, you can expose a function that prints a message to the console: 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 ```rust
use rhai::ImmutableString;
use bevy::prelude::*; use bevy::prelude::*;
use bevy_scriptum::prelude::*; use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;
App::new() App::new()
.add_plugins(DefaultPlugins) .add_plugins(DefaultPlugins)
.add_plugins(ScriptingPlugin::default()) .add_scripting::<LuaRuntime>(|runtime| {
.add_script_function( runtime.add_function(
String::from("my_print"), String::from("my_print"),
|In((x,)): In<(ImmutableString,)>| { |In((x,)): In<(String,)>| {
println!("my_print: '{}'", x); 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 ```lua
my_print("Hello world!"); 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 ```rust
use bevy::prelude::*; use bevy::prelude::*;
use bevy_scriptum::Script; use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;
App::new() App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<LuaRuntime>(|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<AssetServer>| { .add_systems(Startup,|mut commands: Commands, asset_server: Res<AssetServer>| {
commands.spawn(Script::new(asset_server.load("script.rhai"))); commands.spawn(Script::<LuaScript>::new(asset_server.load("script.lua")));
}); })
.run();
``` ```
You should then see `my_print: 'Hello world!'` printed in your console.
### Provided examples ### Provided examples
You can also try running provided examples by cloning this repository and running `cargo run --example <example_name>`. For example: You can also try running provided examples by cloning this repository and running `cargo run --example <example_name>_<language_name>`. For example:
```bash ```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. 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 | | bevy version | bevy_scriptum version |
|--------------|----------------------| |--------------|----------------------|
| 0.13 | 0.4 | | 0.13 | 0.4-0.5 |
| 0.12 | 0.3 | | 0.12 | 0.3 |
| 0.11 | 0.2 | | 0.11 | 0.2 |
| 0.10 | 0.1 | | 0.10 | 0.1 |
### Promises - getting return values from scripts ### 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 ```lua
get_player_name().then(|name| { get_player_name():and_then(function(name)
print(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::<LuaRuntime>(|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. 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 is useful for accessing entity's components from scripts.
It can be used in the following way: It can be used in the following way:
```rhai ```lua
print("Current entity index: " + entity.index()); print("Current entity index: " .. entity.index)
``` ```
`entity` variable is currently not available within promise callbacks.
### Contributing ### Contributing
Contributions are welcome! Feel free to open an issue or submit a pull request. Contributions are welcome! Feel free to open an issue or submit a pull request.

View file

@ -7,8 +7,7 @@ currently being supported with security updates.
| Version | Supported | | Version | Supported |
| ------- | ------------------ | | ------- | ------------------ |
| 0.1 | :x: | | 0.5 | :white_check_mark: |
| 0.2 | :white_check_mark: |
## Reporting a Vulnerability ## Reporting a Vulnerability

View file

@ -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

View file

@ -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)

View file

@ -0,0 +1,4 @@
-- Create a new instance of MyType
my_type = MyType();
-- Call registered method
print(my_type:my_method())

View file

@ -0,0 +1 @@
print_player_names()

View file

@ -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)

View file

@ -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" })

View file

@ -0,0 +1,3 @@
get_player_name():and_then(function(name)
print(name)
end);

View file

@ -1,3 +1,3 @@
// entity is a global variable that is set to the entity that is currently being processed, // entity is a global variable that is set to the entity that is currently being processed,
// it is automatically available in all scripts // it is automatically available in all scripts
print("Current entity index: " + entity.index()); print("Current entity index: " + entity.index);

View file

@ -0,0 +1 @@
hello_bevy();

View file

@ -0,0 +1 @@
spawn_entity();

View file

@ -0,0 +1,3 @@
function test_func()
print("abc" + 5)
end

View file

@ -0,0 +1,7 @@
State = {
called_with = nil
}
function test_func(x)
called_with = x
end

View file

@ -0,0 +1,5 @@
function test_func()
rust_func():and_then(function(x)
print("abc" + 5)
end)
end

View file

@ -0,0 +1,9 @@
State = {
x = nil
}
function test_func()
rust_func():and_then(function(x)
State.x = x
end)
end

View file

@ -0,0 +1,3 @@
function test_func()
rust_func()
end

View file

@ -0,0 +1,3 @@
function test_func()
rust_func(5, "test")
end

View file

@ -0,0 +1,3 @@
function test_func()
rust_func(5)
end

View file

@ -0,0 +1,7 @@
State = {
times_called = 0
}
function test_func()
State.times_called = State.times_called + 1;
end

View file

@ -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

View file

@ -0,0 +1,7 @@
State = {
a_value = nil
}
function test_func(a)
State.a_value = a
end

View file

@ -0,0 +1,3 @@
function test_func()
spawn_entity()
end

View file

@ -0,0 +1,3 @@
fn test_func() {
print("abc" * 5)
}

View file

@ -0,0 +1,2 @@
fn test_func(x) {
}

View file

@ -0,0 +1,5 @@
fn test_func() {
rust_func().then(|x| {
print("abc" * 5)
})
}

View file

@ -0,0 +1,9 @@
let state = #{
x: 0
};
fn test_func() {
rust_func().then(|x| {
state.x = x;
})
}

View file

@ -0,0 +1,3 @@
fn test_func() {
rust_func();
}

View file

@ -0,0 +1,3 @@
fn test_func() {
rust_func(5, "test");
}

View file

@ -0,0 +1,3 @@
fn test_func() {
rust_func(5);
}

View file

@ -0,0 +1,7 @@
let state = #{
times_called: 0
};
fn test_func() {
state.times_called += 1;
}

View file

@ -0,0 +1,9 @@
let state = #{
a_value: (),
b_value: ()
};
fn test_func(a, b) {
state.a_value = a;
state.b_value = b;
}

View file

@ -0,0 +1,7 @@
let state = #{
a_value: ()
};
fn test_func(a) {
state.a_value = a
}

View file

@ -0,0 +1,3 @@
fn test_func() {
spawn_entity()
}

2
book/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
book
doctest_cache

12
book/book.toml Normal file
View file

@ -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"]

21
book/src/SUMMARY.md Normal file
View file

@ -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)

View file

@ -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 |

210
book/src/introduction.md Normal file
View file

@ -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::<LuaRuntime>(|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::<LuaRuntime>(|runtime| {
runtime.add_function(
String::from("print_player_names"),
|players: Query<&Name, With<Player>>| {
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::<LuaRuntime>(|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::<LuaRuntime>(|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::<LuaRuntime>(|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<AssetServer>| {
commands.spawn(Script::<LuaScript>::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 <example_name>_<language_name>`. 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::<LuaRuntime>(|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)

View file

@ -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::<LuaRuntime>(|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::<LuaRuntime>(|runtime| {
runtime.add_function(String::from("pass_to_rust"), |In((entity,)): In<(BevyEntity,)>| {
println!("pass_to_rust called with entity: {:?}", entity);
});
})
.run();
}
```

View file

@ -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.

View file

@ -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::<LuaRuntime>(|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::<LuaRuntime>(|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::<LuaRuntime>(|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::<LuaRuntime>(|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::<LuaRuntime>(|runtime| {
runtime.add_function(String::from("returns_value"), || {
123
});
})
.run();
}
```
```lua
returns_value():and_then(function (value)
print(value) -- 123
end)
```

View file

@ -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<LuaRuntime>,
) {
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<LuaRuntime>,
) {
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`.

View file

@ -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::<LuaRuntime>(|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::<LuaRuntime>(|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<AssetServer>| {
commands.spawn(Script::<LuaScript>::new(asset_server.load("script.lua")));
})
.run();
}
```
You should then see `my_print: 'Hello world!'` printed in your console.

View file

@ -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)

View file

@ -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::<LuaRuntime>(|runtime| {
runtime.add_function(
String::from("print_player_names"),
|players: Query<&Name, With<Player>>| {
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::<LuaRuntime>(|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)
```

3
book/src/lua/lua.md Normal file
View file

@ -0,0 +1,3 @@
# Lua
This chapter demonstrates how to work with bevy_scriptum when using Lua language runtime.

View file

@ -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<AssetServer>) {
commands.spawn(Script::<LuaScript>::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::<LuaScript>` 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() {}
```

View file

@ -0,0 +1 @@
# Hello World

View file

@ -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)

3
book/src/rhai/rhai.md Normal file
View file

@ -0,0 +1,3 @@
# Rhai
This chapter demonstrates how to work with bevy_scriptum when using Rhai language runtime.

3
book/src/runtimes.md Normal file
View file

@ -0,0 +1,3 @@
# Runtimes
This chapter demonstrates how to work with bevy_scriptum when using a specific runtime.

View file

@ -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<AssetServer>) {
commands.spawn(Script::<LuaScript>::new(
assets_server.load("scripts/game.lua"),
));
}
fn update(
mut scripted_entities: Query<(Entity, &mut LuaScriptData)>,
scripting_runtime: ResMut<LuaRuntime>,
) {
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<AssetEvent<LuaScript>>,
scripting_runtime: ResMut<LuaRuntime>,
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::<LuaRuntime>(|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.

View file

@ -0,0 +1,3 @@
# Workflow
Demonstration of useful approaches when working with bevy_scriptum.

View file

@ -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<AssetServer>) {
commands.spawn((
Name::from("MyEntityName"),
Script::new(assets_server.load("examples/current_entity.rhai")),
));
}

View file

@ -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<ScriptingRuntime>,
assets_server: Res<AssetServer>,
) {
let engine = scripting_runtime.engine_mut();
engine
.register_type_with_name::<MyType>("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")));
}

View file

@ -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<Player>>| {
for player in &players {
println!("player name: {}", player);
}
},
)
.add_systems(Startup, startup)
.run();
}
fn startup(mut commands: Commands, assets_server: Res<AssetServer>) {
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")));
}

View file

@ -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<AssetServer>) {
commands.spawn(Script::new(
assets_server.load("examples/entity_variable.rhai"),
));
}

View file

@ -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<AssetServer>) {
commands.spawn(Script::new(
assets_server.load("examples/function_params.rhai"),
));
}

View file

@ -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<AssetServer>) {
commands.spawn(Script::new(assets_server.load("examples/hello_world.rhai")));
}

View file

@ -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::<AppExit>::default();
loop {
if let Some(app_exit_events) = app.world.get_resource_mut::<Events<AppExit>>() {
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::<LuaRuntime>(|runtime| {
runtime.add_function(String::from("quit"), |mut exit: EventWriter<AppExit>| {
exit.send(AppExit);
});
})
.run();
}
fn startup(mut commands: Commands, assets_server: Res<AssetServer>) {
commands.spawn(Script::<LuaScript>::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<LuaRuntime>,
) {
for (entity, mut script_data) in &mut scripted_entities {
scripting_runtime
.call_fn("on_update", &mut script_data, entity, ())
.unwrap();
}
}

View file

@ -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::<LuaRuntime>(|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<AssetServer>) {
commands.spawn((
Name::from("MyEntityName"),
Script::<LuaScript>::new(assets_server.load("examples/lua/current_entity.lua")),
));
}

View file

@ -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::<LuaRuntime>(|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<LuaRuntime>,
assets_server: Res<AssetServer>,
) {
scripting_runtime.with_engine_mut(|engine| {
engine
.register_userdata_type::<MyType>(|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::<LuaScript>::new(
assets_server.load("examples/lua/custom_type.lua"),
));
}

33
examples/lua/ecs.rs Normal file
View file

@ -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::<LuaRuntime>(|runtime| {
runtime.add_function(
String::from("print_player_names"),
|players: Query<&Name, With<Player>>| {
for player in &players {
println!("player name: {}", player);
}
},
);
})
.add_systems(Startup, startup)
.run();
}
fn startup(mut commands: Commands, assets_server: Res<AssetServer>) {
commands.spawn((Player, Name::new("John")));
commands.spawn((Player, Name::new("Mary")));
commands.spawn((Player, Name::new("Alice")));
commands.spawn(Script::<LuaScript>::new(
assets_server.load("examples/lua/ecs.lua"),
));
}

View file

@ -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::<LuaRuntime>(|_| {})
.add_systems(Startup, startup)
.run();
}
fn startup(mut commands: Commands, assets_server: Res<AssetServer>) {
commands.spawn(Script::<LuaScript>::new(
assets_server.load("examples/lua/entity_variable.lua"),
));
}

View file

@ -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::<LuaRuntime>(|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<LuaRuntime>| {
runtime.with_engine(|engine| {
println!(
"called with i64: {} and dynamically typed array: [{:?}]",
x,
engine
.registry_value::<mlua::Table>(&y)
.unwrap()
.pairs::<usize, mlua::Value>()
.map(|pair| pair.unwrap())
.map(|(_, v)| format!("{:?}", v))
.collect::<Vec<String>>()
.join(",")
);
});
},
);
})
.add_systems(Startup, startup)
.run();
}
fn startup(mut commands: Commands, assets_server: Res<AssetServer>) {
commands.spawn(Script::<LuaScript>::new(
assets_server.load("examples/lua/function_params.lua"),
));
}

View file

@ -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::<LuaRuntime>(|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<AssetServer>) {
commands.spawn(Script::<LuaScript>::new(
assets_server.load("examples/lua/hello_world.lua"),
));
}

View file

@ -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::<LuaRuntime>(|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<AssetServer>) {
commands.spawn(Script::<LuaScript>::new(
assets_server.load("examples/lua/hello_world.lua"),
));
}
fn hello_bevy_callback_system() {
println!("hello bevy, called from script");
}

26
examples/lua/promises.rs Normal file
View file

@ -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::<LuaRuntime>(|builder| {
builder.add_function(
String::from("get_player_name"),
|player_names: Query<&Name, With<Player>>| player_names.single().to_string(),
);
})
.add_systems(Startup, startup)
.run();
}
fn startup(mut commands: Commands, assets_server: Res<AssetServer>) {
commands.spawn((Player, Name::new("John")));
commands.spawn(Script::<LuaScript>::new(
assets_server.load("examples/lua/promises.lua"),
));
}

View file

@ -1,8 +1,6 @@
use bevy::{app::AppExit, ecs::event::ManualEventReader, prelude::*}; use bevy::{app::AppExit, ecs::event::ManualEventReader, prelude::*};
use bevy_scriptum::{prelude::*, Script}; use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;
#[derive(Component)]
struct Comp;
fn main() { fn main() {
App::new() App::new()
@ -24,10 +22,11 @@ fn main() {
} }
}) })
.add_plugins(DefaultPlugins) .add_plugins(DefaultPlugins)
.add_plugins(ScriptingPlugin)
.add_systems(Startup, startup) .add_systems(Startup, startup)
.add_systems(Update, print_entity_names_and_quit) .add_systems(Update, print_entity_names_and_quit)
.add_script_function(String::from("spawn_entity"), spawn_entity) .add_scripting::<LuaRuntime>(|runtime| {
runtime.add_function(String::from("spawn_entity"), spawn_entity);
})
.run(); .run();
} }
@ -36,8 +35,8 @@ fn spawn_entity(mut commands: Commands) {
} }
fn startup(mut commands: Commands, assets_server: Res<AssetServer>) { fn startup(mut commands: Commands, assets_server: Res<AssetServer>) {
commands.spawn((Script::new( commands.spawn((Script::<LuaScript>::new(
assets_server.load("examples/side_effects.rhai"), assets_server.load("examples/lua/side_effects.lua"),
),)); ),));
} }

View file

@ -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<AssetServer>) {
commands.spawn(Script::new(assets_server.load("examples/hello_world.rhai")));
}
fn hello_bevy_callback_system() {
println!("hello bevy, called from script");
}

View file

@ -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>>| player_names.single().to_string(),
)
.add_systems(Startup, startup)
.run();
}
fn startup(mut commands: Commands, assets_server: Res<AssetServer>) {
commands.spawn((Player, Name::new("John")));
commands.spawn(Script::new(assets_server.load("examples/promises.rhai")));
}

View file

@ -1,5 +1,6 @@
use bevy::{app::AppExit, ecs::event::ManualEventReader, prelude::*}; 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() { fn main() {
App::new() App::new()
@ -21,24 +22,25 @@ fn main() {
} }
}) })
.add_plugins(DefaultPlugins) .add_plugins(DefaultPlugins)
.add_plugins(ScriptingPlugin)
.add_systems(Startup, startup) .add_systems(Startup, startup)
.add_systems(Update, call_rhai_on_update_from_rust) .add_systems(Update, call_rhai_on_update_from_rust)
.add_script_function(String::from("quit"), |mut exit: EventWriter<AppExit>| { .add_scripting::<RhaiRuntime>(|runtime| {
exit.send(AppExit); runtime.add_function(String::from("quit"), |mut exit: EventWriter<AppExit>| {
exit.send(AppExit);
});
}) })
.run(); .run();
} }
fn startup(mut commands: Commands, assets_server: Res<AssetServer>) { fn startup(mut commands: Commands, assets_server: Res<AssetServer>) {
commands.spawn(Script::new( commands.spawn(Script::<RhaiScript>::new(
assets_server.load("examples/call_function_from_rust.rhai"), assets_server.load("examples/rhai/call_function_from_rust.rhai"),
)); ));
} }
fn call_rhai_on_update_from_rust( fn call_rhai_on_update_from_rust(
mut scripted_entities: Query<(Entity, &mut ScriptData)>, mut scripted_entities: Query<(Entity, &mut RhaiScriptData)>,
mut scripting_runtime: ResMut<ScriptingRuntime>, scripting_runtime: ResMut<RhaiRuntime>,
) { ) {
for (entity, mut script_data) in &mut scripted_entities { for (entity, mut script_data) in &mut scripted_entities {
scripting_runtime scripting_runtime

View file

@ -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::<RhaiRuntime>(|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<AssetServer>) {
commands.spawn((
Name::from("MyEntityName"),
Script::<RhaiScript>::new(assets_server.load("examples/rhai/current_entity.rhai")),
));
}

View file

@ -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::<RhaiRuntime>(|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<RhaiRuntime>,
assets_server: Res<AssetServer>,
) {
scripting_runtime.with_engine_mut(|engine| {
engine
.register_type_with_name::<MyType>("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::<RhaiScript>::new(
assets_server.load("examples/rhai/custom_type.rhai"),
));
}

33
examples/rhai/ecs.rs Normal file
View file

@ -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::<RhaiRuntime>(|runtime| {
runtime.add_function(
String::from("print_player_names"),
|players: Query<&Name, With<Player>>| {
for player in &players {
println!("player name: {}", player);
}
},
);
})
.add_systems(Startup, startup)
.run();
}
fn startup(mut commands: Commands, assets_server: Res<AssetServer>) {
commands.spawn((Player, Name::new("John")));
commands.spawn((Player, Name::new("Mary")));
commands.spawn((Player, Name::new("Alice")));
commands.spawn(Script::<RhaiScript>::new(
assets_server.load("examples/rhai/ecs.rhai"),
));
}

View file

@ -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::<RhaiRuntime>(|_| {})
.add_systems(Startup, startup)
.run();
}
fn startup(mut commands: Commands, assets_server: Res<AssetServer>) {
commands.spawn(Script::<RhaiScript>::new(
assets_server.load("examples/rhai/entity_variable.rhai"),
));
}

View file

@ -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::<RhaiRuntime>(|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<AssetServer>) {
commands.spawn(Script::<RhaiScript>::new(
assets_server.load("examples/rhai/function_params.rhai"),
));
}

View file

@ -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::<RhaiRuntime>(|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<AssetServer>) {
commands.spawn(Script::<RhaiScript>::new(
assets_server.load("examples/rhai/hello_world.rhai"),
));
}

View file

@ -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::<RhaiRuntime>(|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<AssetServer>) {
commands.spawn(Script::<RhaiScript>::new(
assets_server.load("examples/rhai/hello_world.rhai"),
));
}
fn hello_bevy_callback_system() {
println!("hello bevy, called from script");
}

26
examples/rhai/promises.rs Normal file
View file

@ -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::<RhaiRuntime>(|builder| {
builder.add_function(
String::from("get_player_name"),
|player_names: Query<&Name, With<Player>>| player_names.single().to_string(),
);
})
.add_systems(Startup, startup)
.run();
}
fn startup(mut commands: Commands, assets_server: Res<AssetServer>) {
commands.spawn((Player, Name::new("John")));
commands.spawn(Script::<RhaiScript>::new(
assets_server.load("examples/rhai/promises.rhai"),
));
}

View file

@ -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::<AppExit>::default();
loop {
if let Some(app_exit_events) = app.world.get_resource_mut::<Events<AppExit>>() {
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::<RhaiRuntime>(|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<AssetServer>) {
commands.spawn((Script::<RhaiScript>::new(
assets_server.load("examples/rhai/side_effects.rhai"),
),));
}
fn print_entity_names_and_quit(query: Query<&Name>, mut exit: EventWriter<AppExit>) {
if !query.is_empty() {
for e in &query {
println!("{}", e);
}
exit.send(AppExit);
}
}

View file

@ -1,20 +1,31 @@
use std::marker::PhantomData;
use bevy::{ use bevy::{
asset::{io::Reader, Asset, AssetLoader, AsyncReadExt as _, LoadContext}, asset::{io::Reader, Asset, AssetLoader, AsyncReadExt as _, LoadContext},
reflect::TypePath,
utils::BoxedFuture, utils::BoxedFuture,
}; };
use serde::Deserialize;
/// A script that can be loaded by the [crate::ScriptingPlugin]. /// A loader for script assets.
#[derive(Asset, Debug, Deserialize, TypePath)] pub struct ScriptLoader<A: Asset + From<String>> {
pub struct RhaiScript(pub String); _phantom_data: PhantomData<A>,
}
/// A loader for [RhaiScript] assets. impl<A: Asset + From<String>> Default for ScriptLoader<A> {
#[derive(Default)] fn default() -> Self {
pub struct RhaiScriptLoader; Self {
_phantom_data: Default::default(),
}
}
}
impl AssetLoader for RhaiScriptLoader { /// Allows providing an allow-list for extensions of AssetLoader for a Script
type Asset = RhaiScript; /// asset
pub trait GetExtensions {
fn extensions() -> &'static [&'static str];
}
impl<A: Asset + From<String> + GetExtensions> AssetLoader for ScriptLoader<A> {
type Asset = A;
type Settings = (); type Settings = ();
type Error = anyhow::Error; type Error = anyhow::Error;
@ -23,17 +34,18 @@ impl AssetLoader for RhaiScriptLoader {
reader: &'a mut Reader, reader: &'a mut Reader,
_settings: &'a Self::Settings, _settings: &'a Self::Settings,
_load_context: &'a mut LoadContext, _load_context: &'a mut LoadContext,
) -> BoxedFuture<'a, anyhow::Result<RhaiScript, anyhow::Error>> { ) -> BoxedFuture<'a, anyhow::Result<A, anyhow::Error>> {
Box::pin(async move { Box::pin(async move {
let mut bytes = Vec::new(); let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?; 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) Ok(rhai_script)
}) })
} }
fn extensions(&self) -> &[&str] { fn extensions(&self) -> &[&str] {
&["rhai"] A::extensions()
} }
} }

View file

@ -2,64 +2,80 @@ use bevy::prelude::*;
use core::any::TypeId; use core::any::TypeId;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use rhai::{Dynamic, Variant}; use crate::{promise::Promise, Runtime};
use crate::promise::Promise;
/// A system that can be used to call a script function. /// A system that can be used to call a script function.
pub struct CallbackSystem { pub struct CallbackSystem<R: Runtime> {
pub(crate) system: Box<dyn System<In = Vec<Dynamic>, Out = Dynamic>>, pub(crate) system: Box<dyn System<In = Vec<R::Value>, Out = R::Value>>,
pub(crate) arg_types: Vec<TypeId>, pub(crate) arg_types: Vec<TypeId>,
} }
pub(crate) struct FunctionCallEvent { pub(crate) struct FunctionCallEvent<C: Send, V: Send> {
pub(crate) params: Vec<Dynamic>, pub(crate) params: Vec<V>,
pub(crate) promise: Promise, pub(crate) promise: Promise<C, V>,
} }
type Calls<C, V> = Arc<Mutex<Vec<FunctionCallEvent<C, V>>>>;
/// A struct representing a Bevy system that can be called from a script. /// A struct representing a Bevy system that can be called from a script.
#[derive(Clone)] pub(crate) struct Callback<R: Runtime> {
pub(crate) struct Callback {
pub(crate) name: String, pub(crate) name: String,
pub(crate) system: Arc<Mutex<CallbackSystem>>, pub(crate) system: Arc<Mutex<CallbackSystem<R>>>,
pub(crate) calls: Arc<Mutex<Vec<FunctionCallEvent>>>, pub(crate) calls: Calls<R::CallContext, R::Value>,
} }
impl CallbackSystem { impl<R: Runtime> Clone for Callback<R> {
pub(crate) fn call(&mut self, call: &FunctionCallEvent, world: &mut World) -> Dynamic { fn clone(&self) -> Self {
Callback {
name: self.name.clone(),
system: self.system.clone(),
calls: self.calls.clone(),
}
}
}
impl<R: Runtime> CallbackSystem<R> {
pub(crate) fn call(
&mut self,
call: &FunctionCallEvent<R::CallContext, R::Value>,
world: &mut World,
) -> R::Value {
self.system.run(call.params.clone(), world) self.system.run(call.params.clone(), world)
} }
} }
/// Trait that alllows to convert a script callback function into a Bevy [`System`]. /// Allows converting to a wrapper type that the library uses internally for data
pub trait RegisterCallbackFunction< pub(crate) trait IntoRuntimeValueWithEngine<'a, V, R: Runtime> {
Out, fn into_runtime_value_with_engine(value: V, engine: &'a R::RawEngine) -> R::Value;
Marker,
A: 'static,
const N: usize,
const X: bool,
R: 'static,
const F: bool,
Args,
>: IntoSystem<Args, Out, Marker>
{
/// Convert this function into a [CallbackSystem].
#[must_use]
fn into_callback_system(self, world: &mut World) -> CallbackSystem;
} }
impl<Out, FN, Marker> RegisterCallbackFunction<Out, Marker, (), 1, false, Dynamic, false, ()> 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<R: Runtime, In, Out, Marker>: IntoSystem<In, Out, Marker> {
/// Convert this function into a [CallbackSystem].
#[must_use]
fn into_callback_system(self, world: &mut World) -> CallbackSystem<R>;
}
impl<R: Runtime, Out, FN, Marker> IntoCallbackSystem<R, (), Out, Marker> for FN
where where
FN: IntoSystem<(), Out, Marker>, 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<R> {
let mut inner_system = IntoSystem::into_system(self); let mut inner_system = IntoSystem::into_system(self);
inner_system.initialize(world); inner_system.initialize(world);
let system_fn = move |_args: In<Vec<Dynamic>>, world: &mut World| { let system_fn = move |_args: In<Vec<R::Value>>, world: &mut World| {
let result = inner_system.run((), world); let result = inner_system.run((), world);
inner_system.apply_deferred(world); inner_system.apply_deferred(world);
Dynamic::from(result) let mut runtime = world.get_resource_mut::<R>().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); let system = IntoSystem::into_system(system_fn);
CallbackSystem { CallbackSystem {
@ -71,23 +87,29 @@ where
macro_rules! impl_tuple { macro_rules! impl_tuple {
($($idx:tt $t:tt),+) => { ($($idx:tt $t:tt),+) => {
impl<$($t,)+ Out, FN, Marker> RegisterCallbackFunction<Out, Marker, ($($t,)+), 1, false, Dynamic, false, ($($t,)+)> impl<RN: Runtime, $($t,)+ Out, FN, Marker> IntoCallbackSystem<RN, ($($t,)+), Out, Marker>
for FN for FN
where where
FN: IntoSystem<($($t,)+), Out, Marker>, FN: IntoSystem<($($t,)+), Out, Marker>,
Out: Sync + Variant + Clone, Out: for<'a> IntoRuntimeValueWithEngine<'a, Out, RN>,
$($t: 'static + Clone,)+ $($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<RN> {
let mut inner_system = IntoSystem::into_system(self); let mut inner_system = IntoSystem::into_system(self);
inner_system.initialize(world); inner_system.initialize(world);
let system_fn = move |args: In<Vec<Dynamic>>, world: &mut World| { let system_fn = move |args: In<Vec<RN::Value>>, world: &mut World| {
let args = ( let mut runtime = world.get_resource_mut::<RN>().expect("No runtime resource");
$(args.0.get($idx).unwrap().clone_cast::<$t>(), )+ 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); let result = inner_system.run(args, world);
inner_system.apply_deferred(world); inner_system.apply_deferred(world);
Dynamic::from(result) let mut runtime = world.get_resource_mut::<RN>().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); let system = IntoSystem::into_system(system_fn);
CallbackSystem { CallbackSystem {

View file

@ -1,24 +1,14 @@
use bevy::prelude::*; use bevy::prelude::*;
use super::assets::RhaiScript;
/// A component that represents a script. /// A component that represents a script.
#[derive(Component)] #[derive(Component)]
pub struct Script { pub struct Script<A: Asset> {
pub script: Handle<RhaiScript>, pub script: Handle<A>,
} }
/// A component that represents the data of a script. It stores the [rhai::Scope](basically the state of the script, any declared variable etc.) impl<A: Asset> Script<A> {
/// and [rhai::AST] which is a cached AST representation of the script. /// Create a new script component from a handle to a [Script] obtained using [AssetServer].
#[derive(Component)] pub fn new(script: Handle<A>) -> Self {
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<RhaiScript>) -> Self {
Self { script } Self { script }
} }
} }

View file

@ -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. //! 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 //! - low-boilerplate
//! - easy to use //! - easy to use
//! - asynchronicity with a promise-based API //! - asynchronicity with a promise-based API
@ -14,17 +19,20 @@
//! ```rust //! ```rust
//! use bevy::prelude::*; //! use bevy::prelude::*;
//! use bevy_scriptum::prelude::*; //! use bevy_scriptum::prelude::*;
//! use bevy_scriptum::runtimes::lua::prelude::*;
//! //!
//! App::new() //! App::new()
//! .add_plugins(DefaultPlugins) //! .add_plugins(DefaultPlugins)
//! .add_plugins(ScriptingPlugin::default()) //! .add_scripting::<LuaRuntime>(|runtime| {
//! .add_script_function(String::from("hello_bevy"), || { //! runtime.add_function(String::from("hello_bevy"), || {
//! println!("hello bevy, called from script"); //! println!("hello bevy, called from script");
//! }); //! });
//! })
//! .run();
//! ``` //! ```
//! And you can call them in your scripts like this: //! And you can call them in your scripts like this:
//! ```rhai //! ```lua
//! hello_bevy(); //! 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: //! 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 //! ```rust
//! use bevy::prelude::*; //! use bevy::prelude::*;
//! use bevy_scriptum::prelude::*; //! use bevy_scriptum::prelude::*;
//! use bevy_scriptum::runtimes::lua::prelude::*;
//! //!
//! #[derive(Component)] //! #[derive(Component)]
//! struct Player; //! struct Player;
//! //!
//! App::new() //! App::new()
//! .add_plugins(DefaultPlugins) //! .add_plugins(DefaultPlugins)
//! .add_plugins(ScriptingPlugin::default()) //! .add_scripting::<LuaRuntime>(|runtime| {
//! .add_script_function( //! runtime.add_function(
//! String::from("print_player_names"), //! String::from("print_player_names"),
//! |players: Query<&Name, With<Player>>| { //! |players: Query<&Name, With<Player>>| {
//! for player in &players { //! for player in &players {
//! println!("player name: {}", player); //! 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: //! You can also pass arguments to your callback functions, just like you would in a regular Bevy system - using `In` structs with tuples:
//! ```rust //! ```rust
//! use bevy::prelude::*; //! use bevy::prelude::*;
//! use bevy_scriptum::prelude::*; //! use bevy_scriptum::prelude::*;
//! use rhai::ImmutableString; //! use bevy_scriptum::runtimes::lua::prelude::*;
//! //!
//! App::new() //! App::new()
//! .add_plugins(DefaultPlugins) //! .add_plugins(DefaultPlugins)
//! .add_plugins(ScriptingPlugin::default()) //! .add_scripting::<LuaRuntime>(|runtime| {
//! .add_script_function( //! runtime.add_function(
//! String::from("fun_with_string_param"), //! String::from("fun_with_string_param"),
//! |In((x,)): In<(ImmutableString,)>| { //! |In((x,)): In<(String,)>| {
//! println!("called with string: '{}'", x); //! println!("called with string: '{}'", x);
//! }, //! },
//! ); //! );
//! })
//! .run();
//! ``` //! ```
//! which you can then call in your script like this: //! which you can then call in your script like this:
//! ```rhai //! ```lua
//! fun_with_string_param("Hello world!"); //! fun_with_string_param("Hello world!")
//! ``` //! ```
//! //!
//! ## Usage //! ## Usage
@ -76,65 +89,68 @@
//! //!
//! ```toml //! ```toml
//! [dependencies] //! [dependencies]
//! bevy_scriptum = "0.2" //! bevy_scriptum = { version = "0.5", features = ["lua"] }
//! ``` //! ```
//! //!
//! or execute `cargo add bevy_scriptum` from your project directory. //! or execute `cargo add bevy_scriptum --features lua` from your project directory.
//!
//! Add the following to your `main.rs`:
//!
//! ```rust
//! use bevy::prelude::*;
//! use bevy_scriptum::prelude::*;
//!
//! App::new()
//! .add_plugins(DefaultPlugins)
//! .add_plugins(ScriptingPlugin::default())
//! .run();
//! ```
//! //!
//! You can now start exposing functions to the scripting language. For example, you can expose a function that prints a message to the console: //! 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 //! ```rust
//! use rhai::ImmutableString;
//! use bevy::prelude::*; //! use bevy::prelude::*;
//! use bevy_scriptum::prelude::*; //! use bevy_scriptum::prelude::*;
//! use bevy_scriptum::runtimes::lua::prelude::*;
//! //!
//! App::new() //! App::new()
//! .add_plugins(DefaultPlugins) //! .add_plugins(DefaultPlugins)
//! .add_plugins(ScriptingPlugin::default()) //! .add_scripting::<LuaRuntime>(|runtime| {
//! .add_script_function( //! runtime.add_function(
//! String::from("my_print"), //! String::from("my_print"),
//! |In((x,)): In<(ImmutableString,)>| { //! |In((x,)): In<(String,)>| {
//! println!("my_print: '{}'", x); //! 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 //! ```lua
//! my_print("Hello world!"); //! 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 //! ```rust
//! use bevy::prelude::*; //! use bevy::prelude::*;
//! use bevy_scriptum::Script; //! use bevy_scriptum::prelude::*;
//! use bevy_scriptum::runtimes::lua::prelude::*;
//! //!
//! App::new() //! App::new()
//! .add_plugins(DefaultPlugins)
//! .add_scripting::<LuaRuntime>(|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<AssetServer>| { //! .add_systems(Startup,|mut commands: Commands, asset_server: Res<AssetServer>| {
//! commands.spawn(Script::new(asset_server.load("script.rhai"))); //! commands.spawn(Script::<LuaScript>::new(asset_server.load("script.lua")));
//! }); //! })
//! .run();
//! ``` //! ```
//! //!
//! You should then see `my_print: 'Hello world!'` printed in your console.
//!
//! ## Provided examples //! ## Provided examples
//! //!
//! You can also try running provided examples by cloning this repository and running `cargo run --example <example_name>`. For example: //! You can also try running provided examples by cloning this repository and running `cargo run --example <example_name>_<language_name>`. For example:
//! //!
//! ```bash //! ```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. //! 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 | //! | bevy version | bevy_scriptum version |
//! |--------------|----------------------| //! |--------------|----------------------|
//! | 0.13 | 0.4 | //! | 0.13 | 0.4-0.5 |
//! | 0.12 | 0.3 | //! | 0.12 | 0.3 |
//! | 0.11 | 0.2 | //! | 0.11 | 0.2 |
//! | 0.10 | 0.1 | //! | 0.10 | 0.1 |
//! //!
//! ## Promises - getting return values from scripts //! ## 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 //! ```lua
//! get_player_name().then(|name| { //! get_player_name():and_then(function(name)
//! print(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::<LuaRuntime>(|runtime| {
//! runtime.add_function(String::from("get_player_name"), || String::from("John"));
//! });
//! ````
//! //!
//! ## Access entity from script //! ## 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. //! 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 is useful for accessing entity's components from scripts.
//! It can be used in the following way: //! It can be used in the following way:
//! ```rhai //! ```lua
//! print("Current entity index: " + entity.index()); //! print("Current entity index: " .. entity.index)
//! ``` //! ```
//! //!
//! `entity` variable is currently not available within promise callbacks.
//!
//! ## Contributing //! ## Contributing
//! //!
//! Contributions are welcome! Feel free to open an issue or submit a pull request. //! Contributions are welcome! Feel free to open an issue or submit a pull request.
@ -182,19 +213,27 @@ mod components;
mod promise; mod promise;
mod systems; mod systems;
use std::sync::{Arc, Mutex}; pub mod runtimes;
pub use crate::components::{Script, ScriptData}; pub use crate::components::Script;
pub use assets::RhaiScript; use assets::GetExtensions;
use promise::Promise;
use bevy::prelude::*; use std::{
use callback::{Callback, RegisterCallbackFunction}; any::TypeId,
use rhai::{CallFnOptions, Dynamic, Engine, EvalAltResult, FuncArgs, ParseError}; fmt::Debug,
use systems::{init_callbacks, init_engine, log_errors, process_calls}; 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 thiserror::Error;
use self::{ use self::{
assets::RhaiScriptLoader, assets::ScriptLoader,
systems::{process_new_scripts, reload_scripts}, systems::{process_new_scripts, reload_scripts},
}; };
@ -204,126 +243,178 @@ const ENTITY_VAR_NAME: &str = "entity";
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum ScriptingError { pub enum ScriptingError {
#[error("script runtime error: {0}")] #[error("script runtime error: {0}")]
RuntimeError(#[from] Box<EvalAltResult>), RuntimeError(Box<dyn std::error::Error>),
#[error("script compilation error: {0}")] #[error("script compilation error: {0}")]
CompileError(#[from] ParseError), CompileError(Box<dyn std::error::Error>),
#[error("no runtime resource present")] #[error("no runtime resource present")]
NoRuntimeResource, NoRuntimeResource,
#[error("no settings resource present")] #[error("no settings resource present")]
NoSettingsResource, NoSettingsResource,
} }
#[derive(Default)] /// Trait that represents a scripting runtime/engine. In practice it is
pub struct ScriptingPlugin; /// 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<String> + GetExtensions;
type ScriptData: Component;
type CallContext: Send + Clone;
type Value: Send + Clone;
type RawEngine;
impl Plugin for ScriptingPlugin { /// Provides mutable reference to raw scripting engine instance.
fn build(&self, app: &mut App) { /// Can be used to directly interact with an interpreter to use interfaces
app.register_asset_loader(RhaiScriptLoader) /// that bevy_scriptum does not provided adapters for.
.init_asset::<RhaiScript>() fn with_engine_mut<T>(&mut self, f: impl FnOnce(&mut Self::RawEngine) -> T) -> T;
.init_resource::<Callbacks>()
.insert_resource(ScriptingRuntime::default())
.add_systems(Startup, init_engine.pipe(log_errors))
.add_systems(
Update,
(
reload_scripts,
process_calls.pipe(log_errors).after(process_new_scripts),
init_callbacks.pipe(log_errors),
process_new_scripts.pipe(log_errors).after(init_callbacks),
),
);
}
}
#[derive(Resource, Default)] /// Provides immutable reference to raw scripting engine instance.
pub struct ScriptingRuntime { /// Can be used to directly interact with an interpreter to use interfaces
engine: Engine, /// that bevy_scriptum does not provided adapters for.
} fn with_engine<T>(&self, f: impl FnOnce(&Self::RawEngine) -> T) -> T;
impl ScriptingRuntime { fn eval(
/// Get a mutable reference to the internal [rhai::Engine]. &self,
pub fn engine_mut(&mut self) -> &mut Engine { script: &Self::ScriptAsset,
&mut self.engine
}
/// Call a function that is available in the scope of the script.
pub fn call_fn(
&mut self,
function_name: &str,
script_data: &mut ScriptData,
entity: Entity, entity: Entity,
args: impl FuncArgs, ) -> Result<Self::ScriptData, ScriptingError>;
) -> Result<(), ScriptingError> {
let ast = script_data.ast.clone(); /// Registers a new function within the scripting engine. Provided callback
let scope = &mut script_data.scope; /// function will be called when the function with provided name gets called
scope.push(ENTITY_VAR_NAME, entity); /// in script.
let options = CallFnOptions::new().eval_ast(false); fn register_fn(
let result = &mut self,
self.engine name: String,
.call_fn_with_options::<Dynamic>(options, scope, &ast, function_name, args); arg_types: Vec<TypeId>,
scope.remove::<Entity>(ENTITY_VAR_NAME).unwrap(); f: impl Fn(
if let Err(err) = result { Self::CallContext,
match *err { Vec<Self::Value>,
rhai::EvalAltResult::ErrorFunctionNotFound(name, _) if name == function_name => {} ) -> Result<Promise<Self::CallContext, Self::Value>, ScriptingError>
e => Err(Box::new(e))?, + 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<Self::Value, ScriptingError>;
/// 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<Self::Value>,
) -> Result<Self::Value, ScriptingError>;
}
pub trait FuncArgs<'a, V, R: Runtime> {
fn parse(self, engine: &'a R::RawEngine) -> Vec<V>;
}
/// 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<R: Runtime>(&mut self, f: impl Fn(ScriptingRuntimeBuilder<R>)) -> &mut Self;
}
pub struct ScriptingRuntimeBuilder<'a, R: Runtime> {
_phantom_data: PhantomData<R>,
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. /// Registers a function for calling from within a script.
pub trait AddScriptFunctionAppExt { /// Provided function needs to be a valid bevy system and its
fn add_script_function< /// arguments and return value need to be convertible to runtime
Out, /// value types.
Marker, pub fn add_function<In, Out, Marker>(
A: 'static, self,
const N: usize,
const X: bool,
R: 'static,
const F: bool,
Args,
>(
&mut self,
name: String, name: String,
system: impl RegisterCallbackFunction<Out, Marker, A, N, X, R, F, Args>, fun: impl IntoCallbackSystem<R, In, Out, Marker>,
) -> &mut Self; ) -> Self {
} let system = fun.into_callback_system(self.world);
/// A resource that stores all the callbacks that were registered using [AddScriptFunctionAppExt::add_script_function]. let mut callbacks_resource = self.world.resource_mut::<Callbacks<R>>();
#[derive(Resource, Default)]
struct Callbacks {
uninitialized_callbacks: Vec<Callback>,
callbacks: Mutex<Vec<Callback>>,
}
impl AddScriptFunctionAppExt for App {
fn add_script_function<
Out,
Marker,
A: 'static,
const N: usize,
const X: bool,
R: 'static,
const F: bool,
Args,
>(
&mut self,
name: String,
system: impl RegisterCallbackFunction<Out, Marker, A, N, X, R, F, Args>,
) -> &mut Self {
let system = system.into_callback_system(&mut self.world);
let mut callbacks_resource = self.world.resource_mut::<Callbacks>();
callbacks_resource.uninitialized_callbacks.push(Callback { callbacks_resource.uninitialized_callbacks.push(Callback {
name, name,
system: Arc::new(Mutex::new(system)), system: Arc::new(Mutex::new(system)),
calls: Arc::new(Mutex::new(vec![])), calls: Arc::new(Mutex::new(vec![])),
}); });
self self
} }
} }
pub mod prelude { impl BuildScriptingRuntime for App {
pub use crate::{AddScriptFunctionAppExt, ScriptingPlugin}; /// Adds a scripting runtime. Registers required bevy systems that take
/// care of processing and running the scripts.
fn add_scripting<R: Runtime>(&mut self, f: impl Fn(ScriptingRuntimeBuilder<R>)) -> &mut Self {
self.world
.resource_mut::<MainScheduleOrder>()
.insert_after(Update, R::Schedule::default());
self.register_asset_loader(ScriptLoader::<R::ScriptAsset>::default())
.init_schedule(R::Schedule::default())
.init_asset::<R::ScriptAsset>()
.init_resource::<Callbacks<R>>()
.insert_resource(R::default())
.add_systems(
R::Schedule::default(),
(
reload_scripts::<R>,
process_calls::<R>
.pipe(log_errors)
.after(process_new_scripts::<R>),
init_callbacks::<R>.pipe(log_errors),
process_new_scripts::<R>
.pipe(log_errors)
.after(init_callbacks::<R>),
),
);
let runtime = ScriptingRuntimeBuilder::<R>::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<R: Runtime> {
uninitialized_callbacks: Vec<Callback<R>>,
callbacks: Mutex<Vec<Callback<R>>>,
}
impl<R: Runtime> Default for Callbacks<R> {
fn default() -> Self {
Self {
uninitialized_callbacks: Default::default(),
callbacks: Default::default(),
}
}
}
pub mod prelude {
pub use crate::{BuildScriptingRuntime as _, Runtime as _, Script};
} }

View file

@ -1,81 +1,80 @@
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
#[allow(deprecated)] use crate::{Runtime, ScriptingError};
use rhai::{Dynamic, NativeCallContextStore};
use rhai::{EvalAltResult, FnPtr};
/// A struct that represents a function that will get called when the Promise is resolved. /// A struct that represents a function that will get called when the Promise is resolved.
pub(crate) struct PromiseCallback { pub(crate) struct PromiseCallback<C: Send, V: Send> {
callback: Dynamic, callback: V,
following_promise: Arc<Mutex<PromiseInner>>, following_promise: Arc<Mutex<PromiseInner<C, V>>>,
} }
/// Internal representation of a Promise. /// Internal representation of a Promise.
pub(crate) struct PromiseInner { pub(crate) struct PromiseInner<C: Send, V: Send> {
pub(crate) callbacks: Vec<PromiseCallback>, pub(crate) callbacks: Vec<PromiseCallback<C, V>>,
#[allow(deprecated)] #[allow(deprecated)]
pub(crate) context_data: NativeCallContextStore, pub(crate) context: C,
} }
/// A struct that represents a Promise. /// A struct that represents a Promise.
#[derive(Clone)] #[derive(Clone)]
pub struct Promise { pub struct Promise<C: Send, V: Send> {
pub(crate) inner: Arc<Mutex<PromiseInner>>, pub(crate) inner: Arc<Mutex<PromiseInner<C, V>>>,
} }
impl PromiseInner { impl<C: Send, V: Send + Clone> PromiseInner<C, V> {
/// Resolve the Promise. This will call all the callbacks that were added to the Promise. /// Resolve the Promise. This will call all the callbacks that were added to the Promise.
fn resolve( fn resolve<R>(&mut self, runtime: &mut R, val: R::Value) -> Result<(), ScriptingError>
&mut self, where
engine: &mut rhai::Engine, R: Runtime<Value = V, CallContext = C>,
val: Dynamic, {
) -> Result<(), Box<EvalAltResult>> {
for callback in &self.callbacks { for callback in &self.callbacks {
let f = callback.callback.clone_cast::<FnPtr>(); let next_val =
#[allow(deprecated)] runtime.call_fn_from_value(&callback.callback, &self.context, vec![val.clone()])?;
let context = self.context_data.create_context(engine);
let next_val = if val.is_unit() {
f.call_raw(&context, None, [])?
} else {
f.call_raw(&context, None, [val.clone()])?
};
callback callback
.following_promise .following_promise
.lock() .lock()
.unwrap() .expect("Failed to lock promise mutex")
.resolve(engine, next_val)?; .resolve(runtime, next_val)?;
} }
Ok(()) Ok(())
} }
} }
impl Promise { impl<C: Clone + Send + 'static, V: Send + Clone> Promise<C, V> {
/// Acquire [Mutex] for writing the promise and resolve it. Call will be forwarded to [PromiseInner::resolve]. /// Acquire [Mutex] for writing the promise and resolve it. Call will be forwarded to [PromiseInner::resolve].
pub(crate) fn resolve( pub(crate) fn resolve<R>(
&mut self, &mut self,
engine: &mut rhai::Engine, runtime: &mut R,
val: Dynamic, val: R::Value,
) -> Result<(), Box<EvalAltResult>> { ) -> Result<(), ScriptingError>
where
R: Runtime<Value = V, CallContext = C>,
{
if let Ok(mut inner) = self.inner.lock() { if let Ok(mut inner) = self.inner.lock() {
inner.resolve(engine, val)?; inner.resolve(runtime, val)?;
} }
Ok(()) Ok(())
} }
/// Register a callback that will be called when the [Promise] is resolved. /// Register a callback that will be called when the [Promise] is resolved.
pub(crate) fn then(&mut self, callback: rhai::Dynamic) -> rhai::Dynamic { pub(crate) fn then(&mut self, callback: V) -> Self {
let mut inner = self.inner.lock().unwrap(); let mut inner = self
.inner
.lock()
.expect("Failed to lock inner promise mutex");
let following_inner = Arc::new(Mutex::new(PromiseInner { let following_inner = Arc::new(Mutex::new(PromiseInner {
callbacks: vec![], callbacks: vec![],
context_data: inner.context_data.clone(), context: inner.context.clone(),
})); }));
inner.callbacks.push(PromiseCallback { inner.callbacks.push(PromiseCallback {
following_promise: following_inner.clone(), following_promise: following_inner.clone(),
callback, callback,
}); });
Dynamic::from(Promise {
Promise {
inner: following_inner, inner: following_inner,
}) }
} }
} }

335
src/runtimes/lua.rs Normal file
View file

@ -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<Mutex<Lua>>;
#[derive(Clone)]
pub struct LuaValue(Arc<RegistryKey>);
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<Self> {
match value {
mlua::Value::UserData(ud) => Ok(*ud.borrow::<Self>()?),
_ => 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<Self> {
match value {
mlua::Value::UserData(ud) => Ok(*ud.borrow::<Self>()?),
_ => 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::<BevyEntity>(|typ| {
typ.add_field_method_get("index", |_, entity| Ok(entity.0.index()));
})
.expect("Failed to register BevyEntity userdata type");
engine
.register_userdata_type::<Promise<(), LuaValue>>(|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::<BevyVec3>(|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<String> 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::ScriptData, crate::ScriptingError> {
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<std::any::TypeId>,
f: impl Fn(
Self::CallContext,
Vec<Self::Value>,
) -> Result<
crate::promise::Promise<Self::CallContext, Self::Value>,
crate::ScriptingError,
> + Send
+ Sync
+ 'static,
) -> Result<(), crate::ScriptingError> {
self.with_engine(|engine| {
let func = engine
.create_function(move |engine, args: Variadic<mlua::Value>| {
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::Value, crate::ScriptingError> {
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::<mlua::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<Self::Value>,
) -> Result<Self::Value, crate::ScriptingError> {
self.with_engine(|engine| {
let val = engine
.registry_value::<Function>(&value.0)
.map_err(|e| ScriptingError::RuntimeError(Box::new(e)))?;
let args = args
.into_iter()
.map(|a| engine.registry_value::<mlua::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<T>(&mut self, f: impl FnOnce(&mut Self::RawEngine) -> T) -> T {
let mut engine = self.engine.lock().unwrap();
f(&mut engine)
}
fn with_engine<T>(&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<LuaValue> {
Vec::new()
}
}
impl<'a, T: IntoLua<'a>> FuncArgs<'a, LuaValue, LuaRuntime> for Vec<T> {
fn parse(self, engine: &'a Lua) -> Vec<LuaValue> {
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<LuaValue> {
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);

Some files were not shown because too many files have changed in this diff Show more