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:
- uses: actions/checkout@v3
- name: Clippy
run: cargo clippy --verbose -- -D warnings
run: cargo clippy --all-features --verbose -- -D warnings
- name: Build
run: cargo build --verbose
run: cargo build --all-features --verbose
- name: Run tests
run: cargo test --verbose
run: cargo test --all-features --verbose
- name: Install cargo-examples
run: cargo install cargo-examples
- name: Run all examples
run: cargo examples
run: cargo examples --features=lua,rhai

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
/target
/Cargo.lock
.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"
keywords = ["bevy", "rhai", "scripting", "game", "gamedev"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
lua = ["mlua/luajit"]
rhai = ["dep:rhai"]
[dependencies]
bevy = { default-features = false, version = "0.13.0", features = [
"bevy_asset",
] }
serde = "1.0.162"
rhai = { version = "1.14.0", features = ["sync", "internals", "unchecked"] }
rhai = { version = "1.14.0", features = ["sync", "internals", "unchecked"], optional = true }
thiserror = "1.0.40"
anyhow = "1.0.82"
tracing = "0.1.40"
mlua = { version = "0.9.8", features = ["luajit", "vendored", "send"], optional = true }
[[example]]
name = "call_function_from_rust_rhai"
path = "examples/rhai/call_function_from_rust.rs"
[[example]]
name = "current_entity_rhai"
path = "examples/rhai/current_entity.rs"
[[example]]
name = "custom_type_rhai"
path = "examples/rhai/custom_type.rs"
[[example]]
name = "ecs_rhai"
path = "examples/rhai/ecs.rs"
[[example]]
name = "entity_variable_rhai"
path = "examples/rhai/entity_variable.rs"
[[example]]
name = "function_params_rhai"
path = "examples/rhai/function_params.rs"
[[example]]
name = "hello_world_rhai"
path = "examples/rhai/hello_world.rs"
[[example]]
name = "non_closure_system_rhai"
path = "examples/rhai/non_closure_system.rs"
[[example]]
name = "promises_rhai"
path = "examples/rhai/promises.rs"
[[example]]
name = "side_effects_rhai"
path = "examples/rhai/side_effects.rs"
[[example]]
name = "call_function_from_rust_lua"
path = "examples/lua/call_function_from_rust.rs"
[[example]]
name = "current_entity_lua"
path = "examples/lua/current_entity.rs"
[[example]]
name = "custom_type_lua"
path = "examples/lua/custom_type.rs"
[[example]]
name = "ecs_lua"
path = "examples/lua/ecs.rs"
[[example]]
name = "entity_variable_lua"
path = "examples/lua/entity_variable.rs"
[[example]]
name = "function_params_lua"
path = "examples/lua/function_params.rs"
[[example]]
name = "hello_world_lua"
path = "examples/lua/hello_world.rs"
[[example]]
name = "non_closure_system_lua"
path = "examples/lua/non_closure_system.rs"
[[example]]
name = "promises_lua"
path = "examples/lua/promises.rs"
[[example]]
name = "side_effects_lua"
path = "examples/lua/side_effects.rs"
[dev-dependencies]
tracing-subscriber = "0.3.18"
mlua = { version = "0.9.8", features = ["luajit", "vendored", "send"] }
rhai = { version = "1.14.0", features = ["sync", "internals", "unchecked"] }

167
README.md
View file

@ -1,9 +1,14 @@
# bevy_scriptum 📜
bevy_scriptum is a a plugin for [Bevy](https://bevyengine.org/) that allows you to write some of your game logic in a scripting language.
Currently, only [Rhai](https://rhai.rs/) is supported, but more languages may be added in the future.
Currently [Rhai](https://rhai.rs/) and [Lua](https://lua.org/) are supported, but more languages may be added in the future.
It's main advantages include:
Everything you need to know to get started with using this library is contained in the
[bevy_scriptum book](https://link-to-book.com)
API docs are available in [docs.rs](https://docs.rs/bevy_scriptum/latest/bevy_scriptum/)
bevy_scriptum's main advantages include:
- low-boilerplate
- easy to use
- asynchronicity with a promise-based API
@ -16,17 +21,20 @@ All you need to do is register callbacks on your Bevy app like this:
```rust
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(ScriptingPlugin::default())
.add_script_function(String::from("hello_bevy"), || {
println!("hello bevy, called from script");
});
.add_scripting::<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:
```rhai
hello_bevy();
```lua
hello_bevy()
```
Every callback function that you expose to the scripting language is also a Bevy system, so you can easily query and mutate ECS components and resources just like you would in a regular Bevy system:
@ -34,42 +42,47 @@ Every callback function that you expose to the scripting language is also a Bevy
```rust
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;
#[derive(Component)]
struct Player;
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(ScriptingPlugin::default())
.add_script_function(
String::from("print_player_names"),
|players: Query<&Name, With<Player>>| {
for player in &players {
println!("player name: {}", player);
}
},
);
.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 rhai::ImmutableString;
use bevy_scriptum::runtimes::lua::prelude::*;
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(ScriptingPlugin::default())
.add_script_function(
String::from("fun_with_string_param"),
|In((x,)): In<(ImmutableString,)>| {
println!("called with string: '{}'", x);
},
);
.add_scripting::<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:
```rhai
fun_with_string_param("Hello world!");
```lua
fun_with_string_param("Hello world!")
```
### Usage
@ -78,65 +91,68 @@ Add the following to your `Cargo.toml`:
```toml
[dependencies]
bevy_scriptum = "0.2"
bevy_scriptum = { version = "0.5", features = ["lua"] }
```
or execute `cargo add bevy_scriptum` from your project directory.
Add the following to your `main.rs`:
```rust
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(ScriptingPlugin::default())
.run();
```
or execute `cargo add bevy_scriptum --features lua` from your project directory.
You can now start exposing functions to the scripting language. For example, you can expose a function that prints a message to the console:
```rust
use rhai::ImmutableString;
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(ScriptingPlugin::default())
.add_script_function(
String::from("my_print"),
|In((x,)): In<(ImmutableString,)>| {
println!("my_print: '{}'", x);
},
);
.add_scripting::<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.rhai` that calls this function:
Then you can create a script file in `assets` directory called `script.lua` that calls this function:
```rhai
my_print("Hello world!");
```lua
my_print("Hello world!")
```
And spawn a `Script` component with a handle to a script source file`:
And spawn an entity with attached `Script` component with a handle to a script source file:
```rust
use bevy::prelude::*;
use bevy_scriptum::Script;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;
App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<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::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
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
cargo run --example hello_world
cargo run --example hello_world_lua
```
The examples live in `examples` directory and their corresponding scripts live in `assets/examples` directory within the repository.
@ -144,31 +160,46 @@ The examples live in `examples` directory and their corresponding scripts live i
| bevy version | bevy_scriptum version |
|--------------|----------------------|
| 0.13 | 0.4 |
| 0.13 | 0.4-0.5 |
| 0.12 | 0.3 |
| 0.11 | 0.2 |
| 0.10 | 0.1 |
### Promises - getting return values from scripts
Every function called from script returns a promise that you can call `.then` with a callback function on. This callback function will be called when the promise is resolved, and will be passed the return value of the function called from script. For example:
Every function called from script returns a promise that you can call `:and_then` with a callback function on. This callback function will be called when the promise is resolved, and will be passed the return value of the function called from script. For example:
```rhai
get_player_name().then(|name| {
print(name);
});
```lua
get_player_name():and_then(function(name)
print(name)
end)
```
which will print out `John` when used with following exposed function:
### Access entity from script
```rust
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;
App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<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()` method that returns bevy entity index.
It exposes `index` property that returns bevy entity index.
It is useful for accessing entity's components from scripts.
It can be used in the following way:
```rhai
print("Current entity index: " + entity.index());
```lua
print("Current entity index: " .. entity.index)
```
`entity` variable is currently not available within promise callbacks.
### Contributing
Contributions are welcome! Feel free to open an issue or submit a pull request.

View file

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

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,
// 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_scriptum::{prelude::*, Script};
#[derive(Component)]
struct Comp;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;
fn main() {
App::new()
@ -24,10 +22,11 @@ fn main() {
}
})
.add_plugins(DefaultPlugins)
.add_plugins(ScriptingPlugin)
.add_systems(Startup, startup)
.add_systems(Update, print_entity_names_and_quit)
.add_script_function(String::from("spawn_entity"), spawn_entity)
.add_scripting::<LuaRuntime>(|runtime| {
runtime.add_function(String::from("spawn_entity"), spawn_entity);
})
.run();
}
@ -36,8 +35,8 @@ fn spawn_entity(mut commands: Commands) {
}
fn startup(mut commands: Commands, assets_server: Res<AssetServer>) {
commands.spawn((Script::new(
assets_server.load("examples/side_effects.rhai"),
commands.spawn((Script::<LuaScript>::new(
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_scriptum::{prelude::*, Script, ScriptData, ScriptingRuntime};
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::rhai::prelude::*;
fn main() {
App::new()
@ -21,24 +22,25 @@ fn main() {
}
})
.add_plugins(DefaultPlugins)
.add_plugins(ScriptingPlugin)
.add_systems(Startup, startup)
.add_systems(Update, call_rhai_on_update_from_rust)
.add_script_function(String::from("quit"), |mut exit: EventWriter<AppExit>| {
exit.send(AppExit);
.add_scripting::<RhaiRuntime>(|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::new(
assets_server.load("examples/call_function_from_rust.rhai"),
commands.spawn(Script::<RhaiScript>::new(
assets_server.load("examples/rhai/call_function_from_rust.rhai"),
));
}
fn call_rhai_on_update_from_rust(
mut scripted_entities: Query<(Entity, &mut ScriptData)>,
mut scripting_runtime: ResMut<ScriptingRuntime>,
mut scripted_entities: Query<(Entity, &mut RhaiScriptData)>,
scripting_runtime: ResMut<RhaiRuntime>,
) {
for (entity, mut script_data) in &mut scripted_entities {
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::{
asset::{io::Reader, Asset, AssetLoader, AsyncReadExt as _, LoadContext},
reflect::TypePath,
utils::BoxedFuture,
};
use serde::Deserialize;
/// A script that can be loaded by the [crate::ScriptingPlugin].
#[derive(Asset, Debug, Deserialize, TypePath)]
pub struct RhaiScript(pub String);
/// A loader for script assets.
pub struct ScriptLoader<A: Asset + From<String>> {
_phantom_data: PhantomData<A>,
}
/// A loader for [RhaiScript] assets.
#[derive(Default)]
pub struct RhaiScriptLoader;
impl<A: Asset + From<String>> Default for ScriptLoader<A> {
fn default() -> Self {
Self {
_phantom_data: Default::default(),
}
}
}
impl AssetLoader for RhaiScriptLoader {
type Asset = RhaiScript;
/// Allows providing an allow-list for extensions of AssetLoader for a Script
/// asset
pub trait GetExtensions {
fn extensions() -> &'static [&'static str];
}
impl<A: Asset + From<String> + GetExtensions> AssetLoader for ScriptLoader<A> {
type Asset = A;
type Settings = ();
type Error = anyhow::Error;
@ -23,17 +34,18 @@ impl AssetLoader for RhaiScriptLoader {
reader: &'a mut Reader,
_settings: &'a Self::Settings,
_load_context: &'a mut LoadContext,
) -> BoxedFuture<'a, anyhow::Result<RhaiScript, anyhow::Error>> {
) -> BoxedFuture<'a, anyhow::Result<A, anyhow::Error>> {
Box::pin(async move {
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
let rhai_script = RhaiScript(String::from_utf8(bytes.to_vec())?);
let script_text = String::from_utf8(bytes.to_vec())?;
let rhai_script: A = script_text.into();
Ok(rhai_script)
})
}
fn extensions(&self) -> &[&str] {
&["rhai"]
A::extensions()
}
}

View file

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

View file

@ -1,24 +1,14 @@
use bevy::prelude::*;
use super::assets::RhaiScript;
/// A component that represents a script.
#[derive(Component)]
pub struct Script {
pub script: Handle<RhaiScript>,
pub struct Script<A: Asset> {
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.)
/// and [rhai::AST] which is a cached AST representation of the script.
#[derive(Component)]
pub struct ScriptData {
pub(crate) scope: rhai::Scope<'static>,
pub(crate) ast: rhai::AST,
}
impl Script {
/// Create a new script component from a handle to a [RhaiScript] obtained using [AssetServer].
pub fn new(script: Handle<RhaiScript>) -> Self {
impl<A: Asset> Script<A> {
/// Create a new script component from a handle to a [Script] obtained using [AssetServer].
pub fn new(script: Handle<A>) -> Self {
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.
//! Currently, only [Rhai](https://rhai.rs/) is supported, but more languages may be added in the future.
//! Currently [Rhai](https://rhai.rs/) and [Lua](https://lua.org/) are supported, but more languages may be added in the future.
//!
//! It's main advantages include:
//! Everything you need to know to get started with using this library is contained in the
//! [bevy_scriptum book](https://link-to-book.com)
//!
//! API docs are available in [docs.rs](https://docs.rs/bevy_scriptum/latest/bevy_scriptum/)
//!
//! bevy_scriptum's main advantages include:
//! - low-boilerplate
//! - easy to use
//! - asynchronicity with a promise-based API
@ -14,17 +19,20 @@
//! ```rust
//! use bevy::prelude::*;
//! use bevy_scriptum::prelude::*;
//! use bevy_scriptum::runtimes::lua::prelude::*;
//!
//! App::new()
//! .add_plugins(DefaultPlugins)
//! .add_plugins(ScriptingPlugin::default())
//! .add_script_function(String::from("hello_bevy"), || {
//! println!("hello bevy, called from script");
//! });
//! .add_scripting::<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:
//! ```rhai
//! hello_bevy();
//! ```lua
//! hello_bevy()
//! ```
//!
//! Every callback function that you expose to the scripting language is also a Bevy system, so you can easily query and mutate ECS components and resources just like you would in a regular Bevy system:
@ -32,42 +40,47 @@
//! ```rust
//! use bevy::prelude::*;
//! use bevy_scriptum::prelude::*;
//! use bevy_scriptum::runtimes::lua::prelude::*;
//!
//! #[derive(Component)]
//! struct Player;
//!
//! App::new()
//! .add_plugins(DefaultPlugins)
//! .add_plugins(ScriptingPlugin::default())
//! .add_script_function(
//! String::from("print_player_names"),
//! |players: Query<&Name, With<Player>>| {
//! for player in &players {
//! println!("player name: {}", player);
//! }
//! },
//! );
//! .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 rhai::ImmutableString;
//! use bevy_scriptum::runtimes::lua::prelude::*;
//!
//! App::new()
//! .add_plugins(DefaultPlugins)
//! .add_plugins(ScriptingPlugin::default())
//! .add_script_function(
//! String::from("fun_with_string_param"),
//! |In((x,)): In<(ImmutableString,)>| {
//! println!("called with string: '{}'", x);
//! },
//! );
//! .add_scripting::<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:
//! ```rhai
//! fun_with_string_param("Hello world!");
//! ```lua
//! fun_with_string_param("Hello world!")
//! ```
//!
//! ## Usage
@ -76,65 +89,68 @@
//!
//! ```toml
//! [dependencies]
//! bevy_scriptum = "0.2"
//! bevy_scriptum = { version = "0.5", features = ["lua"] }
//! ```
//!
//! or execute `cargo add bevy_scriptum` from your project directory.
//!
//! Add the following to your `main.rs`:
//!
//! ```rust
//! use bevy::prelude::*;
//! use bevy_scriptum::prelude::*;
//!
//! App::new()
//! .add_plugins(DefaultPlugins)
//! .add_plugins(ScriptingPlugin::default())
//! .run();
//! ```
//! or execute `cargo add bevy_scriptum --features lua` from your project directory.
//!
//! You can now start exposing functions to the scripting language. For example, you can expose a function that prints a message to the console:
//!
//! ```rust
//! use rhai::ImmutableString;
//! use bevy::prelude::*;
//! use bevy_scriptum::prelude::*;
//! use bevy_scriptum::runtimes::lua::prelude::*;
//!
//! App::new()
//! .add_plugins(DefaultPlugins)
//! .add_plugins(ScriptingPlugin::default())
//! .add_script_function(
//! String::from("my_print"),
//! |In((x,)): In<(ImmutableString,)>| {
//! println!("my_print: '{}'", x);
//! },
//! );
//! .add_scripting::<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.rhai` that calls this function:
//! Then you can create a script file in `assets` directory called `script.lua` that calls this function:
//!
//! ```rhai
//! my_print("Hello world!");
//! ```lua
//! my_print("Hello world!")
//! ```
//!
//! And spawn a `Script` component with a handle to a script source file`:
//! And spawn an entity with attached `Script` component with a handle to a script source file:
//!
//! ```rust
//! use bevy::prelude::*;
//! use bevy_scriptum::Script;
//! use bevy_scriptum::prelude::*;
//! use bevy_scriptum::runtimes::lua::prelude::*;
//!
//! App::new()
//! .add_plugins(DefaultPlugins)
//! .add_scripting::<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::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
//!
//! 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
//! cargo run --example hello_world
//! cargo run --example hello_world_lua
//! ```
//! The examples live in `examples` directory and their corresponding scripts live in `assets/examples` directory within the repository.
//!
@ -142,31 +158,46 @@
//!
//! | bevy version | bevy_scriptum version |
//! |--------------|----------------------|
//! | 0.13 | 0.4 |
//! | 0.13 | 0.4-0.5 |
//! | 0.12 | 0.3 |
//! | 0.11 | 0.2 |
//! | 0.10 | 0.1 |
//!
//! ## Promises - getting return values from scripts
//!
//! Every function called from script returns a promise that you can call `.then` with a callback function on. This callback function will be called when the promise is resolved, and will be passed the return value of the function called from script. For example:
//! Every function called from script returns a promise that you can call `:and_then` with a callback function on. This callback function will be called when the promise is resolved, and will be passed the return value of the function called from script. For example:
//!
//! ```rhai
//! get_player_name().then(|name| {
//! print(name);
//! });
//! ```lua
//! get_player_name():and_then(function(name)
//! print(name)
//! end)
//! ```
//! which will print out `John` when used with following exposed function:
//!
//! ```
//! use bevy::prelude::*;
//! use bevy_scriptum::prelude::*;
//! use bevy_scriptum::runtimes::lua::prelude::*;
//!
//! App::new()
//! .add_plugins(DefaultPlugins)
//! .add_scripting::<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()` method that returns bevy entity index.
//! It exposes `index` property that returns bevy entity index.
//! It is useful for accessing entity's components from scripts.
//! It can be used in the following way:
//! ```rhai
//! print("Current entity index: " + entity.index());
//! ```lua
//! print("Current entity index: " .. entity.index)
//! ```
//!
//! `entity` variable is currently not available within promise callbacks.
//!
//! ## Contributing
//!
//! Contributions are welcome! Feel free to open an issue or submit a pull request.
@ -182,19 +213,27 @@ mod components;
mod promise;
mod systems;
use std::sync::{Arc, Mutex};
pub mod runtimes;
pub use crate::components::{Script, ScriptData};
pub use assets::RhaiScript;
pub use crate::components::Script;
use assets::GetExtensions;
use promise::Promise;
use bevy::prelude::*;
use callback::{Callback, RegisterCallbackFunction};
use rhai::{CallFnOptions, Dynamic, Engine, EvalAltResult, FuncArgs, ParseError};
use systems::{init_callbacks, init_engine, log_errors, process_calls};
use std::{
any::TypeId,
fmt::Debug,
hash::Hash,
marker::PhantomData,
sync::{Arc, Mutex},
};
use bevy::{app::MainScheduleOrder, ecs::schedule::ScheduleLabel, prelude::*};
use callback::{Callback, IntoCallbackSystem};
use systems::{init_callbacks, log_errors, process_calls};
use thiserror::Error;
use self::{
assets::RhaiScriptLoader,
assets::ScriptLoader,
systems::{process_new_scripts, reload_scripts},
};
@ -204,126 +243,178 @@ const ENTITY_VAR_NAME: &str = "entity";
#[derive(Error, Debug)]
pub enum ScriptingError {
#[error("script runtime error: {0}")]
RuntimeError(#[from] Box<EvalAltResult>),
RuntimeError(Box<dyn std::error::Error>),
#[error("script compilation error: {0}")]
CompileError(#[from] ParseError),
CompileError(Box<dyn std::error::Error>),
#[error("no runtime resource present")]
NoRuntimeResource,
#[error("no settings resource present")]
NoSettingsResource,
}
#[derive(Default)]
pub struct ScriptingPlugin;
/// Trait that represents a scripting runtime/engine. In practice it is
/// implemented for a scripint language interpreter and the implementor provides
/// function implementations for calling and registering functions within the interpreter.
pub trait Runtime: Resource + Default {
type Schedule: ScheduleLabel + Debug + Clone + Eq + Hash + Default;
type ScriptAsset: Asset + From<String> + GetExtensions;
type ScriptData: Component;
type CallContext: Send + Clone;
type Value: Send + Clone;
type RawEngine;
impl Plugin for ScriptingPlugin {
fn build(&self, app: &mut App) {
app.register_asset_loader(RhaiScriptLoader)
.init_asset::<RhaiScript>()
.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),
),
);
}
}
/// Provides mutable reference to raw scripting engine instance.
/// Can be used to directly interact with an interpreter to use interfaces
/// that bevy_scriptum does not provided adapters for.
fn with_engine_mut<T>(&mut self, f: impl FnOnce(&mut Self::RawEngine) -> T) -> T;
#[derive(Resource, Default)]
pub struct ScriptingRuntime {
engine: Engine,
}
/// Provides immutable reference to raw scripting engine instance.
/// Can be used to directly interact with an interpreter to use interfaces
/// that bevy_scriptum does not provided adapters for.
fn with_engine<T>(&self, f: impl FnOnce(&Self::RawEngine) -> T) -> T;
impl ScriptingRuntime {
/// Get a mutable reference to the internal [rhai::Engine].
pub fn engine_mut(&mut self) -> &mut Engine {
&mut self.engine
}
/// Call a function that is available in the scope of the script.
pub fn call_fn(
&mut self,
function_name: &str,
script_data: &mut ScriptData,
fn eval(
&self,
script: &Self::ScriptAsset,
entity: Entity,
args: impl FuncArgs,
) -> Result<(), ScriptingError> {
let ast = script_data.ast.clone();
let scope = &mut script_data.scope;
scope.push(ENTITY_VAR_NAME, entity);
let options = CallFnOptions::new().eval_ast(false);
let result =
self.engine
.call_fn_with_options::<Dynamic>(options, scope, &ast, function_name, args);
scope.remove::<Entity>(ENTITY_VAR_NAME).unwrap();
if let Err(err) = result {
match *err {
rhai::EvalAltResult::ErrorFunctionNotFound(name, _) if name == function_name => {}
e => Err(Box::new(e))?,
}
) -> Result<Self::ScriptData, ScriptingError>;
/// Registers a new function within the scripting engine. Provided callback
/// function will be called when the function with provided name gets called
/// in script.
fn register_fn(
&mut self,
name: String,
arg_types: Vec<TypeId>,
f: impl Fn(
Self::CallContext,
Vec<Self::Value>,
) -> Result<Promise<Self::CallContext, Self::Value>, ScriptingError>
+ Send
+ Sync
+ 'static,
) -> Result<(), ScriptingError>;
/// Calls a function by name defined within the runtime in the context of the
/// entity that haas been paassed. Can return a dynamically typed value
/// that got returned from the function within a script.
fn call_fn(
&self,
name: &str,
script_data: &mut Self::ScriptData,
entity: Entity,
args: impl for<'a> FuncArgs<'a, Self::Value, Self>,
) -> Result<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.
pub trait AddScriptFunctionAppExt {
fn add_script_function<
Out,
Marker,
A: 'static,
const N: usize,
const X: bool,
R: 'static,
const F: bool,
Args,
>(
&mut self,
/// Registers a function for calling from within a script.
/// Provided function needs to be a valid bevy system and its
/// arguments and return value need to be convertible to runtime
/// value types.
pub fn add_function<In, Out, Marker>(
self,
name: String,
system: impl RegisterCallbackFunction<Out, Marker, A, N, X, R, F, Args>,
) -> &mut Self;
}
fun: impl IntoCallbackSystem<R, In, Out, Marker>,
) -> Self {
let system = fun.into_callback_system(self.world);
/// A resource that stores all the callbacks that were registered using [AddScriptFunctionAppExt::add_script_function].
#[derive(Resource, Default)]
struct Callbacks {
uninitialized_callbacks: Vec<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>();
let mut callbacks_resource = self.world.resource_mut::<Callbacks<R>>();
callbacks_resource.uninitialized_callbacks.push(Callback {
name,
system: Arc::new(Mutex::new(system)),
calls: Arc::new(Mutex::new(vec![])),
});
self
}
}
pub mod prelude {
pub use crate::{AddScriptFunctionAppExt, ScriptingPlugin};
impl BuildScriptingRuntime for App {
/// Adds a scripting runtime. Registers required bevy systems that take
/// care of processing and running the scripts.
fn add_scripting<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};
#[allow(deprecated)]
use rhai::{Dynamic, NativeCallContextStore};
use rhai::{EvalAltResult, FnPtr};
use crate::{Runtime, ScriptingError};
/// A struct that represents a function that will get called when the Promise is resolved.
pub(crate) struct PromiseCallback {
callback: Dynamic,
following_promise: Arc<Mutex<PromiseInner>>,
pub(crate) struct PromiseCallback<C: Send, V: Send> {
callback: V,
following_promise: Arc<Mutex<PromiseInner<C, V>>>,
}
/// Internal representation of a Promise.
pub(crate) struct PromiseInner {
pub(crate) callbacks: Vec<PromiseCallback>,
pub(crate) struct PromiseInner<C: Send, V: Send> {
pub(crate) callbacks: Vec<PromiseCallback<C, V>>,
#[allow(deprecated)]
pub(crate) context_data: NativeCallContextStore,
pub(crate) context: C,
}
/// A struct that represents a Promise.
#[derive(Clone)]
pub struct Promise {
pub(crate) inner: Arc<Mutex<PromiseInner>>,
pub struct Promise<C: Send, V: Send> {
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.
fn resolve(
&mut self,
engine: &mut rhai::Engine,
val: Dynamic,
) -> Result<(), Box<EvalAltResult>> {
fn resolve<R>(&mut self, runtime: &mut R, val: R::Value) -> Result<(), ScriptingError>
where
R: Runtime<Value = V, CallContext = C>,
{
for callback in &self.callbacks {
let f = callback.callback.clone_cast::<FnPtr>();
#[allow(deprecated)]
let context = self.context_data.create_context(engine);
let next_val = if val.is_unit() {
f.call_raw(&context, None, [])?
} else {
f.call_raw(&context, None, [val.clone()])?
};
let next_val =
runtime.call_fn_from_value(&callback.callback, &self.context, vec![val.clone()])?;
callback
.following_promise
.lock()
.unwrap()
.resolve(engine, next_val)?;
.expect("Failed to lock promise mutex")
.resolve(runtime, next_val)?;
}
Ok(())
}
}
impl Promise {
impl<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].
pub(crate) fn resolve(
pub(crate) fn resolve<R>(
&mut self,
engine: &mut rhai::Engine,
val: Dynamic,
) -> Result<(), Box<EvalAltResult>> {
runtime: &mut R,
val: R::Value,
) -> Result<(), ScriptingError>
where
R: Runtime<Value = V, CallContext = C>,
{
if let Ok(mut inner) = self.inner.lock() {
inner.resolve(engine, val)?;
inner.resolve(runtime, val)?;
}
Ok(())
}
/// Register a callback that will be called when the [Promise] is resolved.
pub(crate) fn then(&mut self, callback: rhai::Dynamic) -> rhai::Dynamic {
let mut inner = self.inner.lock().unwrap();
pub(crate) fn then(&mut self, callback: V) -> Self {
let mut inner = self
.inner
.lock()
.expect("Failed to lock inner promise mutex");
let following_inner = Arc::new(Mutex::new(PromiseInner {
callbacks: vec![],
context_data: inner.context_data.clone(),
context: inner.context.clone(),
}));
inner.callbacks.push(PromiseCallback {
following_promise: following_inner.clone(),
callback,
});
Dynamic::from(Promise {
Promise {
inner: following_inner,
})
}
}
}

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