Compare commits
3 commits
c5510c9075
...
36cc907f0e
| Author | SHA1 | Date | |
|---|---|---|---|
| 36cc907f0e | |||
|
|
4fc2b47834 | ||
|
|
7fac9f7cfc |
71 changed files with 1993 additions and 67 deletions
56
Cargo.toml
56
Cargo.toml
|
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "bevy_scriptum"
|
||||
authors = ["Jaroslaw Konik <konikjar@gmail.com>"]
|
||||
version = "0.10.0"
|
||||
version = "0.11.0"
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "README.md"
|
||||
|
|
@ -14,9 +14,10 @@ keywords = ["bevy", "lua", "scripting", "game", "script"]
|
|||
lua = ["dep:mlua", "mlua/luajit"]
|
||||
rhai = ["dep:rhai"]
|
||||
ruby = ["dep:magnus", "dep:rb-sys"]
|
||||
javascript = ["dep:deno_core"]
|
||||
|
||||
[dependencies]
|
||||
bevy = { default-features = false, version = "0.17", features = ["bevy_asset", "bevy_log"] }
|
||||
bevy = { default-features = false, version = "0.18", features = ["bevy_asset", "bevy_log"] }
|
||||
serde = "1.0.162"
|
||||
rhai = { version = "1.14.0", features = [
|
||||
"sync",
|
||||
|
|
@ -33,6 +34,7 @@ mlua = { version = "0.9.8", features = [
|
|||
], optional = true }
|
||||
magnus = { version = "0.8.2", optional = true }
|
||||
rb-sys = { version = "0.9", default-features = false, features = ["link-ruby", "ruby-static"], optional = true }
|
||||
deno_core = { version = "0.403", optional = true }
|
||||
crossbeam-channel = "0.5.15"
|
||||
libc = "0.2.172"
|
||||
|
||||
|
|
@ -211,6 +213,56 @@ name = "side_effects_ruby"
|
|||
path = "examples/ruby/side_effects.rs"
|
||||
required-features = ["ruby"]
|
||||
|
||||
[[example]]
|
||||
name = "call_function_from_rust_js"
|
||||
path = "examples/js/call_function_from_rust.rs"
|
||||
required-features = ["javascript"]
|
||||
|
||||
[[example]]
|
||||
name = "current_entity_js"
|
||||
path = "examples/js/current_entity.rs"
|
||||
required-features = ["javascript"]
|
||||
|
||||
[[example]]
|
||||
name = "ecs_js"
|
||||
path = "examples/js/ecs.rs"
|
||||
required-features = ["javascript"]
|
||||
|
||||
[[example]]
|
||||
name = "entity_variable_js"
|
||||
path = "examples/js/entity_variable.rs"
|
||||
required-features = ["javascript"]
|
||||
|
||||
[[example]]
|
||||
name = "function_params_js"
|
||||
path = "examples/js/function_params.rs"
|
||||
required-features = ["javascript"]
|
||||
|
||||
[[example]]
|
||||
name = "hello_world_js"
|
||||
path = "examples/js/hello_world.rs"
|
||||
required-features = ["javascript"]
|
||||
|
||||
[[example]]
|
||||
name = "multiple_plugins_js"
|
||||
path = "examples/js/multiple_plugins.rs"
|
||||
required-features = ["javascript"]
|
||||
|
||||
[[example]]
|
||||
name = "non_closure_system_js"
|
||||
path = "examples/js/non_closure_system.rs"
|
||||
required-features = ["javascript"]
|
||||
|
||||
[[example]]
|
||||
name = "promises_js"
|
||||
path = "examples/js/promises.rs"
|
||||
required-features = ["javascript"]
|
||||
|
||||
[[example]]
|
||||
name = "side_effects_js"
|
||||
path = "examples/js/side_effects.rs"
|
||||
required-features = ["javascript"]
|
||||
|
||||
[dev-dependencies]
|
||||
tracing-subscriber = "0.3.18"
|
||||
mlua = { version = "0.9.8", features = ["luajit", "vendored", "send"] }
|
||||
|
|
|
|||
|
|
@ -6,10 +6,11 @@ bevy_scriptum is a a plugin for [Bevy](https://bevyengine.org/) that allows you
|
|||
### Supported scripting languages/runtimes
|
||||
|
||||
| language/runtime | cargo feature | documentation chapter |
|
||||
| ------------------------------------------ | ------------- | --------------------------------------------------------------- |
|
||||
| ---------------------------------------------------- | ------------- | --------------------------------------------------------------- |
|
||||
| 🌙 LuaJIT | `lua` | [link](https://jarkonik.github.io/bevy_scriptum/lua/lua.html) |
|
||||
| 🌾 Rhai | `rhai` | [link](https://jarkonik.github.io/bevy_scriptum/rhai/rhai.html) |
|
||||
| 💎 Ruby(currently only supported on Linux) | `ruby` | [link](https://jarkonik.github.io/bevy_scriptum/ruby/ruby.html) |
|
||||
| 💎 Ruby(currently only supported on Linux and MacOS) | `ruby` | [link](https://jarkonik.github.io/bevy_scriptum/ruby/ruby.html) |
|
||||
| 🟨 JavaScript (V8 via deno_core) | `javascript` | [link](https://jarkonik.github.io/bevy_scriptum/javascript/javascript.html) |
|
||||
|
||||
Documentation book is available [here](https://jarkonik.github.io/bevy_scriptum/) 📖
|
||||
|
||||
|
|
@ -98,7 +99,7 @@ Add the following to your `Cargo.toml`:
|
|||
|
||||
```toml
|
||||
[dependencies]
|
||||
bevy_scriptum = { version = "0.10", features = ["lua"] }
|
||||
bevy_scriptum = { version = "0.11", features = ["lua"] }
|
||||
```
|
||||
|
||||
or execute `cargo add bevy_scriptum --features lua` from your project directory.
|
||||
|
|
@ -167,6 +168,7 @@ The examples live in `examples` directory and their corresponding scripts live i
|
|||
|
||||
| bevy version | bevy_scriptum version |
|
||||
|--------------|-----------------------|
|
||||
| 0.18 | 0.11 |
|
||||
| 0.17 | 0.10 |
|
||||
| 0.16 | 0.8-0.9 |
|
||||
| 0.15 | 0.7 |
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ currently being supported with security updates.
|
|||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 0.10 | :white_check_mark: |
|
||||
| 0.11 | :white_check_mark: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
|
|
|
|||
13
assets/examples/js/call_function_from_rust.js
Normal file
13
assets/examples/js/call_function_from_rust.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
var 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) {
|
||||
print("calling quit");
|
||||
quit();
|
||||
}
|
||||
}
|
||||
7
assets/examples/js/current_entity.js
Normal file
7
assets/examples/js/current_entity.js
Normal 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);
|
||||
});
|
||||
1
assets/examples/js/ecs.js
Normal file
1
assets/examples/js/ecs.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
print_player_names();
|
||||
3
assets/examples/js/entity_variable.js
Normal file
3
assets/examples/js/entity_variable.js
Normal 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);
|
||||
4
assets/examples/js/function_params.js
Normal file
4
assets/examples/js/function_params.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
fun_without_params();
|
||||
fun_with_string_param("hello");
|
||||
fun_with_i64_param(5);
|
||||
fun_with_multiple_params(5, "hello");
|
||||
1
assets/examples/js/hello_world.js
Normal file
1
assets/examples/js/hello_world.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
hello_bevy();
|
||||
1
assets/examples/js/multiple_plugins_plugin_a.js
Normal file
1
assets/examples/js/multiple_plugins_plugin_a.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
hello_from_plugin_a();
|
||||
1
assets/examples/js/multiple_plugins_plugin_b.js
Normal file
1
assets/examples/js/multiple_plugins_plugin_b.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
hello_from_plugin_b_with_parameters("hello", 42);
|
||||
3
assets/examples/js/promises.js
Normal file
3
assets/examples/js/promises.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
get_player_name().and_then(function (name) {
|
||||
print(name);
|
||||
});
|
||||
1
assets/examples/js/side_effects.js
Normal file
1
assets/examples/js/side_effects.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
spawn_entity();
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
function test_func() {
|
||||
throw new Error("intentional runtime error");
|
||||
}
|
||||
7
assets/tests/js/call_script_function_with_params.js
Normal file
7
assets/tests/js/call_script_function_with_params.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
var State = {
|
||||
called_with: null,
|
||||
};
|
||||
|
||||
function test_func(x) {
|
||||
State.called_with = x;
|
||||
}
|
||||
3
assets/tests/js/entity_variable.js
Normal file
3
assets/tests/js/entity_variable.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
function test_func() {
|
||||
rust_func(entity.index);
|
||||
}
|
||||
5
assets/tests/js/entity_variable_eval.js
Normal file
5
assets/tests/js/entity_variable_eval.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
var index = entity.index;
|
||||
|
||||
function test_func() {
|
||||
rust_func(index);
|
||||
}
|
||||
2
assets/tests/js/eval_that_causes_runtime_error.js
Normal file
2
assets/tests/js/eval_that_causes_runtime_error.js
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
mark_called();
|
||||
throw new Error("intentional runtime error");
|
||||
3
assets/tests/js/pass_entity_from_script.js
Normal file
3
assets/tests/js/pass_entity_from_script.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
function test_func() {
|
||||
rust_func(entity);
|
||||
}
|
||||
3
assets/tests/js/pass_vec3_from_script.js
Normal file
3
assets/tests/js/pass_vec3_from_script.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
function test_func() {
|
||||
rust_func(Vec3(1.5, 2.5, -3.5));
|
||||
}
|
||||
6
assets/tests/js/pass_vec3_to_script.js
Normal file
6
assets/tests/js/pass_vec3_to_script.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
function test_func(vec3) {
|
||||
if (vec3.x !== 1.5 || vec3.y !== 2.5 || vec3.z !== -3.5) {
|
||||
throw new Error("unexpected Vec3 components");
|
||||
}
|
||||
mark_success();
|
||||
}
|
||||
5
assets/tests/js/promise_runtime_error.js
Normal file
5
assets/tests/js/promise_runtime_error.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
function test_func() {
|
||||
rust_func().and_then(function (x) {
|
||||
throw new Error("intentional runtime error");
|
||||
});
|
||||
}
|
||||
9
assets/tests/js/return_via_promise.js
Normal file
9
assets/tests/js/return_via_promise.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
var State = {
|
||||
x: null,
|
||||
};
|
||||
|
||||
function test_func() {
|
||||
rust_func().and_then(function (x) {
|
||||
State.x = x;
|
||||
});
|
||||
}
|
||||
3
assets/tests/js/rust_function_gets_called_from_script.js
Normal file
3
assets/tests/js/rust_function_gets_called_from_script.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
function test_func() {
|
||||
rust_func();
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
function test_func() {
|
||||
rust_func(5, "test");
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
function test_func() {
|
||||
rust_func(5);
|
||||
}
|
||||
7
assets/tests/js/script_function_gets_called_from_rust.js
Normal file
7
assets/tests/js/script_function_gets_called_from_rust.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
var State = {
|
||||
times_called: 0,
|
||||
};
|
||||
|
||||
function test_func() {
|
||||
State.times_called = State.times_called + 1;
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
var State = {
|
||||
a_value: null,
|
||||
b_value: null,
|
||||
};
|
||||
|
||||
function test_func(a, b) {
|
||||
State.a_value = a;
|
||||
State.b_value = b;
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
var State = {
|
||||
a_value: null,
|
||||
};
|
||||
|
||||
function test_func(a) {
|
||||
State.a_value = a;
|
||||
}
|
||||
3
assets/tests/js/side_effects.js
Normal file
3
assets/tests/js/side_effects.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
function test_func() {
|
||||
spawn_entity();
|
||||
}
|
||||
31
book/Cargo.lock
generated
31
book/Cargo.lock
generated
|
|
@ -685,9 +685,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
|||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.10.1"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
||||
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
|
|
@ -1211,18 +1211,18 @@ checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
|||
|
||||
[[package]]
|
||||
name = "lua-src"
|
||||
version = "550.0.0"
|
||||
version = "547.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e836dc8ae16806c9bdcf42003a88da27d163433e3f9684c52f0301258004a4fb"
|
||||
checksum = "1edaf29e3517b49b8b746701e5648ccb5785cde1c119062cbabbc5d5cd115e42"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "luajit-src"
|
||||
version = "210.6.6+707c12b"
|
||||
version = "210.5.12+a4f56a4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a86cc925d4053d0526ae7f5bc765dbd0d7a5d1a63d43974f4966cb349ca63295"
|
||||
checksum = "b3a8e7962a5368d5f264d045a5a255e90f9aa3fc1941ae15a8d2940d42cac671"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"which",
|
||||
|
|
@ -1274,29 +1274,25 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
|||
|
||||
[[package]]
|
||||
name = "mlua"
|
||||
version = "0.11.6"
|
||||
version = "0.9.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccd36acfa49ce6ee56d1307a061dd302c564eee757e6e4cd67eb4f7204846fab"
|
||||
checksum = "d111deb18a9c9bd33e1541309f4742523bfab01d276bfa9a27519f6de9c11dc7"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"either",
|
||||
"libc",
|
||||
"mlua-sys",
|
||||
"num-traits",
|
||||
"parking_lot",
|
||||
"once_cell",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mlua-sys"
|
||||
version = "0.10.0"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f1c3a7fc7580227ece249fd90aa2fa3b39eb2b49d3aec5e103b3e85f2c3dfc8"
|
||||
checksum = "380c1f7e2099cafcf40e51d3a9f20a346977587aa4d012eae1f043149a728a93"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"lua-src",
|
||||
"luajit-src",
|
||||
"pkg-config",
|
||||
|
|
@ -2289,10 +2285,11 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "which"
|
||||
version = "8.0.0"
|
||||
version = "7.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d"
|
||||
checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762"
|
||||
dependencies = [
|
||||
"either",
|
||||
"env_home",
|
||||
"rustix",
|
||||
"winsafe",
|
||||
|
|
|
|||
|
|
@ -19,6 +19,15 @@
|
|||
- [Calling Ruby from Rust](./ruby/calling_script_from_rust.md)
|
||||
- [Interacting with bevy in callbacks](./ruby/interacting_with_bevy.md)
|
||||
- [Builtin types](./ruby/builtin_types.md)
|
||||
- [JavaScript](./javascript/javascript.md)
|
||||
- [Installation](./javascript/installation.md)
|
||||
- [Hello World](./javascript/hello_world.md)
|
||||
- [Spawning scripts](./javascript/spawning_scripts.md)
|
||||
- [Calling Rust from JavaScript](./javascript/calling_rust_from_script.md)
|
||||
- [Calling JavaScript from Rust](./javascript/calling_script_from_rust.md)
|
||||
- [Interacting with bevy in callbacks](./javascript/interacting_with_bevy.md)
|
||||
- [Builtin types](./javascript/builtin_types.md)
|
||||
- [Builtin variables](./javascript/builtin_variables.md)
|
||||
- [Rhai](./rhai/rhai.md)
|
||||
- [Installation](./rhai/installation.md)
|
||||
- [Hello World(TBD)]()
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
| bevy version | bevy_scriptum version |
|
||||
| ------------ | --------------------- |
|
||||
| 0.18 | 0.11 |
|
||||
| 0.17 | 0.10 |
|
||||
| 0.16 | 0.8-0.9 |
|
||||
| 0.15 | 0.7 |
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ Add the following to your `Cargo.toml`:
|
|||
|
||||
```toml
|
||||
[dependencies]
|
||||
bevy_scriptum = { version = "0.10", features = ["lua"] }
|
||||
bevy_scriptum = { version = "0.11", features = ["lua"] }
|
||||
```
|
||||
|
||||
or execute `cargo add bevy_scriptum --features lua` from your project directory.
|
||||
|
|
|
|||
85
book/src/javascript/builtin_types.md
Normal file
85
book/src/javascript/builtin_types.md
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
# Builtin types
|
||||
|
||||
bevy_scriptum provides the following types that can be used in JavaScript:
|
||||
|
||||
- `Vec3`
|
||||
- `Entity`
|
||||
|
||||
## Vec3
|
||||
|
||||
A `Vec3` is represented as a plain object with `x`, `y` and `z` number
|
||||
properties. A global `Vec3(x, y, z)` constructor function is available to all
|
||||
scripts.
|
||||
|
||||
### Example JavaScript usage
|
||||
|
||||
```js
|
||||
var my_vec = Vec3(1, 2, 3);
|
||||
print(my_vec.x, my_vec.y, my_vec.z);
|
||||
set_translation(entity, my_vec);
|
||||
```
|
||||
|
||||
### Example Rust usage
|
||||
|
||||
```rust,no_run
|
||||
# extern crate bevy;
|
||||
# extern crate bevy_scriptum;
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy_scriptum::prelude::*;
|
||||
use bevy_scriptum::runtimes::javascript::prelude::*;
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_scripting::<JsRuntime>(|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;
|
||||
}
|
||||
```
|
||||
|
||||
## Entity
|
||||
|
||||
The currently processed entity is available through the global `entity`
|
||||
variable. It exposes an `index` property and can be passed back to Rust
|
||||
functions that accept a `BevyEntity` argument.
|
||||
|
||||
`entity` is currently not available within promise callbacks.
|
||||
|
||||
### Example JavaScript usage
|
||||
|
||||
```js
|
||||
print(entity.index);
|
||||
pass_to_rust(entity);
|
||||
```
|
||||
|
||||
### Example Rust usage
|
||||
|
||||
```rust,no_run
|
||||
# extern crate bevy;
|
||||
# extern crate bevy_scriptum;
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy_scriptum::prelude::*;
|
||||
use bevy_scriptum::runtimes::javascript::prelude::*;
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_scripting::<JsRuntime>(|runtime| {
|
||||
runtime.add_function(String::from("pass_to_rust"), |In((entity,)): In<(BevyEntity,)>| {
|
||||
println!("pass_to_rust called with entity: {:?}", entity);
|
||||
});
|
||||
})
|
||||
.run();
|
||||
}
|
||||
```
|
||||
14
book/src/javascript/builtin_variables.md
Normal file
14
book/src/javascript/builtin_variables.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Builtin variables
|
||||
|
||||
## entity
|
||||
|
||||
A variable called `entity` is automatically available to all scripts - it
|
||||
represents the bevy entity that the `Script` component is attached to.
|
||||
It exposes an `index` property that returns the bevy entity index.
|
||||
It is useful for accessing the entity's components from scripts.
|
||||
It can be used in the following way:
|
||||
```js
|
||||
print("Current entity index: " + entity.index);
|
||||
```
|
||||
|
||||
The `entity` variable is currently not available within promise callbacks.
|
||||
113
book/src/javascript/calling_rust_from_script.md
Normal file
113
book/src/javascript/calling_rust_from_script.md
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
# Calling Rust from JavaScript
|
||||
|
||||
To call a rust function from JavaScript first you need to register a function
|
||||
within Rust using the builder pattern.
|
||||
|
||||
```rust,no_run
|
||||
# extern crate bevy;
|
||||
# extern crate bevy_scriptum;
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy_scriptum::prelude::*;
|
||||
use bevy_scriptum::runtimes::javascript::prelude::*;
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_scripting::<JsRuntime>(|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,no_run
|
||||
# extern crate bevy;
|
||||
# extern crate bevy_scriptum;
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy_scriptum::prelude::*;
|
||||
use bevy_scriptum::runtimes::javascript::prelude::*;
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_scripting::<JsRuntime>(|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 JavaScript code in your
|
||||
spawned scripts.
|
||||
|
||||
```js
|
||||
my_rust_func();
|
||||
```
|
||||
|
||||
Since a registered callback function is a Bevy system, the parameters are passed
|
||||
to it as an `In` struct with a tuple, which has to be the first parameter of the
|
||||
closure.
|
||||
|
||||
```rust,no_run
|
||||
# extern crate bevy;
|
||||
# extern crate bevy_scriptum;
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy_scriptum::prelude::*;
|
||||
use bevy_scriptum::runtimes::javascript::prelude::*;
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_scripting::<JsRuntime>(|runtime| {
|
||||
runtime.add_function(String::from("func_with_params"), |In((a, b)): In<(String, i64)>| {
|
||||
println!("func_with_params has been called with string {} and i64 {}", a, b);
|
||||
});
|
||||
})
|
||||
.run();
|
||||
}
|
||||
```
|
||||
|
||||
The above function can be called from JavaScript
|
||||
|
||||
```js
|
||||
func_with_params("abc", 123);
|
||||
```
|
||||
|
||||
## Return value via promise
|
||||
|
||||
Any registered rust function that returns a value will return 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 the Rust function.
|
||||
|
||||
```rust,no_run
|
||||
# extern crate bevy;
|
||||
# extern crate bevy_scriptum;
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy_scriptum::prelude::*;
|
||||
use bevy_scriptum::runtimes::javascript::prelude::*;
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_scripting::<JsRuntime>(|runtime| {
|
||||
runtime.add_function(String::from("returns_value"), || {
|
||||
123
|
||||
});
|
||||
})
|
||||
.run();
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
returns_value().and_then(function (value) {
|
||||
print(value); // 123
|
||||
});
|
||||
```
|
||||
66
book/src/javascript/calling_script_from_rust.md
Normal file
66
book/src/javascript/calling_script_from_rust.md
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# Calling JavaScript from Rust
|
||||
|
||||
To call a function defined in JavaScript
|
||||
|
||||
```js
|
||||
function on_update() {
|
||||
}
|
||||
```
|
||||
|
||||
We need to acquire the `JsRuntime` resource within a bevy system.
|
||||
Then we will be able to call `call_fn` on it, providing the name
|
||||
of the function to call, the `JsScriptData` that has been automatically
|
||||
attached to the entity after an entity with a script attached has been spawned
|
||||
and its script evaluated, the entity and optionally some arguments.
|
||||
|
||||
```rust,no_run
|
||||
# extern crate bevy;
|
||||
# extern crate bevy_scriptum;
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy_scriptum::prelude::*;
|
||||
use bevy_scriptum::runtimes::javascript::prelude::*;
|
||||
|
||||
fn call_js_on_update_from_rust(
|
||||
mut scripted_entities: Query<(Entity, &mut JsScriptData)>,
|
||||
scripting_runtime: ResMut<JsRuntime>,
|
||||
) {
|
||||
for (entity, mut script_data) in &mut scripted_entities {
|
||||
// calling function named `on_update` defined in JavaScript script
|
||||
scripting_runtime
|
||||
.call_fn("on_update", &mut script_data, entity, ())
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
We can also pass some arguments by providing a tuple or `Vec` as the last
|
||||
`call_fn` argument.
|
||||
|
||||
```rust,no_run
|
||||
# extern crate bevy;
|
||||
# extern crate bevy_scriptum;
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy_scriptum::prelude::*;
|
||||
use bevy_scriptum::runtimes::javascript::prelude::*;
|
||||
|
||||
fn call_js_on_update_from_rust(
|
||||
mut scripted_entities: Query<(Entity, &mut JsScriptData)>,
|
||||
scripting_runtime: ResMut<JsRuntime>,
|
||||
) {
|
||||
for (entity, mut script_data) in &mut scripted_entities {
|
||||
scripting_runtime
|
||||
.call_fn("on_update", &mut script_data, entity, (123, String::from("hello")))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
They will be passed to the `on_update` JavaScript function
|
||||
```js
|
||||
function on_update(a, b) {
|
||||
print(a); // 123
|
||||
print(b); // hello
|
||||
}
|
||||
```
|
||||
75
book/src/javascript/hello_world.md
Normal file
75
book/src/javascript/hello_world.md
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# 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 crate 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,no_run
|
||||
# extern crate bevy;
|
||||
# extern crate bevy_scriptum;
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy_scriptum::prelude::*;
|
||||
use bevy_scriptum::runtimes::javascript::prelude::*;
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_scripting::<JsRuntime>(|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.js` that
|
||||
calls this function:
|
||||
|
||||
```js
|
||||
my_print("Hello world!");
|
||||
```
|
||||
|
||||
And spawn an entity with attached `Script` component with a handle to a script
|
||||
source file:
|
||||
|
||||
```rust,no_run
|
||||
# extern crate bevy;
|
||||
# extern crate bevy_scriptum;
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy_scriptum::prelude::*;
|
||||
use bevy_scriptum::runtimes::javascript::prelude::*;
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_scripting::<JsRuntime>(|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::<JsScript>::new(asset_server.load("script.js")));
|
||||
})
|
||||
.run();
|
||||
}
|
||||
```
|
||||
|
||||
You should then see `my_print: 'Hello world!'` printed in your console.
|
||||
18
book/src/javascript/installation.md
Normal file
18
book/src/javascript/installation.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Installation
|
||||
|
||||
JavaScript support links against V8 which is downloaded as a prebuilt static
|
||||
library by the `v8` crate during the build, so the first build may take a while
|
||||
and requires network access.
|
||||
|
||||
## Main Library
|
||||
|
||||
Add the following to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
bevy = "0.18"
|
||||
bevy_scriptum = { version = "0.11", features = ["javascript"] }
|
||||
```
|
||||
|
||||
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)
|
||||
83
book/src/javascript/interacting_with_bevy.md
Normal file
83
book/src/javascript/interacting_with_bevy.md
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
# 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 a `Player` component.
|
||||
|
||||
```rust,no_run
|
||||
# extern crate bevy;
|
||||
# extern crate bevy_ecs;
|
||||
# extern crate bevy_scriptum;
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy_scriptum::prelude::*;
|
||||
use bevy_scriptum::runtimes::javascript::prelude::*;
|
||||
|
||||
#[derive(Component)]
|
||||
struct Player;
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_scripting::<JsRuntime>(|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:
|
||||
|
||||
```js
|
||||
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,no_run
|
||||
# extern crate bevy;
|
||||
# extern crate bevy_ecs;
|
||||
# extern crate bevy_scriptum;
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy_scriptum::prelude::*;
|
||||
use bevy_scriptum::runtimes::javascript::prelude::*;
|
||||
|
||||
#[derive(Component)]
|
||||
struct Player {
|
||||
health: i32
|
||||
}
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_scripting::<JsRuntime>(|runtime| {
|
||||
runtime.add_function(
|
||||
String::from("hurt_player"),
|
||||
|In((hit_value,)): In<(i32,)>, mut players: Query<&mut Player>| {
|
||||
let mut player = players.single_mut().unwrap();
|
||||
player.health -= hit_value;
|
||||
},
|
||||
);
|
||||
})
|
||||
.run();
|
||||
}
|
||||
```
|
||||
|
||||
And it could be called in script like:
|
||||
|
||||
```js
|
||||
hurt_player(5);
|
||||
```
|
||||
11
book/src/javascript/javascript.md
Normal file
11
book/src/javascript/javascript.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# JavaScript
|
||||
|
||||
This chapter demonstrates how to work with bevy_scriptum when using the
|
||||
JavaScript language runtime.
|
||||
|
||||
JavaScript support is powered by the [V8](https://v8.dev/) engine through
|
||||
[`deno_core`](https://crates.io/crates/deno_core). Scripts run synchronously -
|
||||
the JavaScript event loop is not pumped, so APIs such as `setTimeout` or native
|
||||
`Promise`s are not available. Return values from registered Rust functions are
|
||||
delivered through bevy_scriptum's own promise object (see
|
||||
[Calling Rust from JavaScript](./calling_rust_from_script.md)).
|
||||
42
book/src/javascript/spawning_scripts.md
Normal file
42
book/src/javascript/spawning_scripts.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# Spawning scripts
|
||||
|
||||
To spawn a JavaScript script you will need to get a handle to a script asset
|
||||
using bevy's `AssetServer`.
|
||||
|
||||
```rust
|
||||
# extern crate bevy;
|
||||
# extern crate bevy_scriptum;
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy_scriptum::prelude::*;
|
||||
use bevy_scriptum::runtimes::javascript::prelude::*;
|
||||
|
||||
fn my_spawner(mut commands: Commands, assets_server: Res<AssetServer>) {
|
||||
commands.spawn(Script::<JsScript>::new(
|
||||
assets_server.load("my_script.js"),
|
||||
));
|
||||
}
|
||||
```
|
||||
|
||||
After the scripts have been evaled by bevy_scriptum, the entities that they've
|
||||
been attached to will get the `Script::<JsScript>` component stripped and instead
|
||||
a `JsScriptData` component will be attached.
|
||||
|
||||
So to query scripted entities you could do something like:
|
||||
|
||||
```rust
|
||||
# extern crate bevy;
|
||||
# extern crate bevy_scriptum;
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy_scriptum::prelude::*;
|
||||
use bevy_scriptum::runtimes::javascript::prelude::*;
|
||||
|
||||
fn my_system(
|
||||
mut scripted_entities: Query<(Entity, &mut JsScriptData)>,
|
||||
) {
|
||||
for (entity, mut script_data) in &mut scripted_entities {
|
||||
// do something with scripted entities
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -4,8 +4,8 @@ Add the following to your `Cargo.toml`:
|
|||
|
||||
```toml
|
||||
[dependencies]
|
||||
bevy = "0.17"
|
||||
bevy_scriptum = { version = "0.10", features = ["lua"] }
|
||||
bevy = "0.18"
|
||||
bevy_scriptum = { version = "0.11", features = ["lua"] }
|
||||
```
|
||||
|
||||
If you need a different version of bevy you need to use a matching bevy_scriptum
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ Add the following to your `Cargo.toml`:
|
|||
|
||||
```toml
|
||||
[dependencies]
|
||||
bevy = "0.17"
|
||||
bevy_scriptum = { version = "0.10", features = ["rhai"] }
|
||||
bevy = "0.18"
|
||||
bevy_scriptum = { version = "0.11", features = ["rhai"] }
|
||||
```
|
||||
|
||||
If you need a different version of bevy you need to use a matching bevy_scriptum
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Installation
|
||||
|
||||
Ruby is currently only supported on Linux.
|
||||
Ruby is currently only supported on Linux ana MacOS.
|
||||
|
||||
## Ruby
|
||||
|
||||
|
|
@ -45,8 +45,8 @@ Add the following to your `Cargo.toml`:
|
|||
|
||||
```toml
|
||||
[dependencies]
|
||||
bevy = "0.17"
|
||||
bevy_scriptum = { version = "0.10", features = ["ruby"] }
|
||||
bevy = "0.18"
|
||||
bevy_scriptum = { version = "0.11", features = ["ruby"] }
|
||||
```
|
||||
|
||||
If you need a different version of bevy you need to use a matching bevy_scriptum
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ To enable live reload it should be enough to enable `file-watcher` feature
|
|||
within bevy dependency in `Cargo.toml`
|
||||
|
||||
```toml
|
||||
bevy = { version = "0.17", features = ["file_watcher"] }
|
||||
bevy = { version = "0.18", features = ["file_watcher"] }
|
||||
```
|
||||
|
||||
## Init-teardown pattern
|
||||
|
|
|
|||
33
examples/js/call_function_from_rust.rs
Normal file
33
examples/js/call_function_from_rust.rs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
use bevy::{app::AppExit, prelude::*};
|
||||
use bevy_scriptum::prelude::*;
|
||||
use bevy_scriptum::runtimes::javascript::prelude::*;
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_systems(Startup, startup)
|
||||
.add_systems(Update, call_js_on_update_from_rust)
|
||||
.add_scripting::<JsRuntime>(|runtime| {
|
||||
runtime.add_function(String::from("quit"), |mut exit: MessageWriter<AppExit>| {
|
||||
exit.write(AppExit::Success);
|
||||
});
|
||||
})
|
||||
.run();
|
||||
}
|
||||
|
||||
fn startup(mut commands: Commands, assets_server: Res<AssetServer>) {
|
||||
commands.spawn(Script::<JsScript>::new(
|
||||
assets_server.load("examples/js/call_function_from_rust.js"),
|
||||
));
|
||||
}
|
||||
|
||||
fn call_js_on_update_from_rust(
|
||||
mut scripted_entities: Query<(Entity, &mut JsScriptData)>,
|
||||
scripting_runtime: ResMut<JsRuntime>,
|
||||
) {
|
||||
for (entity, mut script_data) in &mut scripted_entities {
|
||||
scripting_runtime
|
||||
.call_fn("on_update", &mut script_data, entity, ())
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
25
examples/js/current_entity.rs
Normal file
25
examples/js/current_entity.rs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
use bevy::prelude::*;
|
||||
use bevy_scriptum::prelude::*;
|
||||
use bevy_scriptum::runtimes::javascript::prelude::*;
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_scripting::<JsRuntime>(|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::<JsScript>::new(assets_server.load("examples/js/current_entity.js")),
|
||||
));
|
||||
}
|
||||
33
examples/js/ecs.rs
Normal file
33
examples/js/ecs.rs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
use bevy::prelude::*;
|
||||
use bevy_scriptum::prelude::*;
|
||||
use bevy_scriptum::runtimes::javascript::prelude::*;
|
||||
|
||||
#[derive(Component)]
|
||||
struct Player;
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_scripting::<JsRuntime>(|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::<JsScript>::new(
|
||||
assets_server.load("examples/js/ecs.js"),
|
||||
));
|
||||
}
|
||||
17
examples/js/entity_variable.rs
Normal file
17
examples/js/entity_variable.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
use bevy::prelude::*;
|
||||
use bevy_scriptum::runtimes::javascript::prelude::*;
|
||||
use bevy_scriptum::{BuildScriptingRuntime, prelude::*};
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_scripting::<JsRuntime>(|_| {})
|
||||
.add_systems(Startup, startup)
|
||||
.run();
|
||||
}
|
||||
|
||||
fn startup(mut commands: Commands, assets_server: Res<AssetServer>) {
|
||||
commands.spawn(Script::<JsScript>::new(
|
||||
assets_server.load("examples/js/entity_variable.js"),
|
||||
));
|
||||
}
|
||||
40
examples/js/function_params.rs
Normal file
40
examples/js/function_params.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
use bevy::prelude::*;
|
||||
use bevy_scriptum::prelude::*;
|
||||
use bevy_scriptum::runtimes::javascript::prelude::*;
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_scripting::<JsRuntime>(|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_systems(Startup, startup)
|
||||
.run();
|
||||
}
|
||||
|
||||
fn startup(mut commands: Commands, assets_server: Res<AssetServer>) {
|
||||
commands.spawn(Script::<JsScript>::new(
|
||||
assets_server.load("examples/js/function_params.js"),
|
||||
));
|
||||
}
|
||||
21
examples/js/hello_world.rs
Normal file
21
examples/js/hello_world.rs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
use bevy::prelude::*;
|
||||
use bevy_scriptum::prelude::*;
|
||||
use bevy_scriptum::runtimes::javascript::prelude::*;
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_scripting::<JsRuntime>(|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::<JsScript>::new(
|
||||
assets_server.load("examples/js/hello_world.js"),
|
||||
));
|
||||
}
|
||||
67
examples/js/multiple_plugins.rs
Normal file
67
examples/js/multiple_plugins.rs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
use bevy::prelude::*;
|
||||
use bevy_scriptum::prelude::*;
|
||||
use bevy_scriptum::runtimes::javascript::prelude::*;
|
||||
|
||||
// Plugin A
|
||||
struct PluginA;
|
||||
impl Plugin for PluginA {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_scripting_api::<JsRuntime>(|runtime| {
|
||||
runtime.add_function(String::from("hello_from_plugin_a"), || {
|
||||
info!("Hello from Plugin A");
|
||||
});
|
||||
})
|
||||
.add_systems(Startup, plugin_a_startup);
|
||||
}
|
||||
}
|
||||
|
||||
fn plugin_a_startup(mut commands: Commands, assets_server: Res<AssetServer>) {
|
||||
commands.spawn(Script::<JsScript>::new(
|
||||
assets_server.load("examples/js/multiple_plugins_plugin_a.js"),
|
||||
));
|
||||
}
|
||||
|
||||
// Plugin B
|
||||
struct PluginB;
|
||||
impl Plugin for PluginB {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_scripting_api::<JsRuntime>(|runtime| {
|
||||
runtime.add_function(
|
||||
String::from("hello_from_plugin_b_with_parameters"),
|
||||
hello_from_b,
|
||||
);
|
||||
})
|
||||
.add_systems(Startup, plugin_b_startup);
|
||||
}
|
||||
}
|
||||
|
||||
fn plugin_b_startup(mut commands: Commands, assets_server: Res<AssetServer>) {
|
||||
commands.spawn(Script::<JsScript>::new(
|
||||
assets_server.load("examples/js/multiple_plugins_plugin_b.js"),
|
||||
));
|
||||
}
|
||||
|
||||
fn hello_from_b(In((text, x)): In<(String, i32)>) {
|
||||
info!("{} from Plugin B: {}", text, x);
|
||||
}
|
||||
|
||||
// Main
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_scripting::<JsRuntime>(|runtime| {
|
||||
runtime.add_function(String::from("hello_bevy"), || {
|
||||
info!("hello bevy, called from script");
|
||||
});
|
||||
})
|
||||
.add_systems(Startup, main_startup)
|
||||
.add_plugins(PluginA)
|
||||
.add_plugins(PluginB)
|
||||
.run();
|
||||
}
|
||||
|
||||
fn main_startup(mut commands: Commands, assets_server: Res<AssetServer>) {
|
||||
commands.spawn(Script::<JsScript>::new(
|
||||
assets_server.load("examples/js/hello_world.js"),
|
||||
));
|
||||
}
|
||||
23
examples/js/non_closure_system.rs
Normal file
23
examples/js/non_closure_system.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
use bevy::prelude::*;
|
||||
use bevy_scriptum::prelude::*;
|
||||
use bevy_scriptum::runtimes::javascript::prelude::*;
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_scripting::<JsRuntime>(|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::<JsScript>::new(
|
||||
assets_server.load("examples/js/hello_world.js"),
|
||||
));
|
||||
}
|
||||
|
||||
fn hello_bevy_callback_system() {
|
||||
println!("hello bevy, called from script");
|
||||
}
|
||||
31
examples/js/promises.rs
Normal file
31
examples/js/promises.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
use bevy::prelude::*;
|
||||
use bevy_scriptum::prelude::*;
|
||||
use bevy_scriptum::runtimes::javascript::prelude::*;
|
||||
|
||||
#[derive(Component)]
|
||||
struct Player;
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_scripting::<JsRuntime>(|builder| {
|
||||
builder.add_function(
|
||||
String::from("get_player_name"),
|
||||
|player_names: Query<&Name, With<Player>>| {
|
||||
player_names
|
||||
.single()
|
||||
.expect("Missing player_names")
|
||||
.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::<JsScript>::new(
|
||||
assets_server.load("examples/js/promises.js"),
|
||||
));
|
||||
}
|
||||
43
examples/js/side_effects.rs
Normal file
43
examples/js/side_effects.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
use bevy::{app::AppExit, prelude::*};
|
||||
use bevy_scriptum::prelude::*;
|
||||
use bevy_scriptum::runtimes::javascript::prelude::*;
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
// This is just needed for headless console app, not needed for a regular bevy application
|
||||
// that uses a winit window
|
||||
.set_runner(move |mut app: App| {
|
||||
loop {
|
||||
app.update();
|
||||
if let Some(exit) = app.should_exit() {
|
||||
return exit;
|
||||
}
|
||||
}
|
||||
})
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_systems(Startup, startup)
|
||||
.add_systems(Update, print_entity_names_and_quit)
|
||||
.add_scripting::<JsRuntime>(|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::<JsScript>::new(
|
||||
assets_server.load("examples/js/side_effects.js"),
|
||||
),));
|
||||
}
|
||||
|
||||
fn print_entity_names_and_quit(query: Query<&Name>, mut exit: MessageWriter<AppExit>) {
|
||||
if !query.is_empty() {
|
||||
for e in &query {
|
||||
println!("{}", e);
|
||||
}
|
||||
exit.write(AppExit::Success);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
use bevy::prelude::*;
|
||||
use bevy_scriptum::runtimes::lua::prelude::*;
|
||||
use bevy_scriptum::{prelude::*, BuildScriptingRuntime};
|
||||
use bevy_scriptum::{BuildScriptingRuntime, prelude::*};
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
|
|
|
|||
|
|
@ -11,7 +11,12 @@ fn main() {
|
|||
.add_scripting::<LuaRuntime>(|builder| {
|
||||
builder.add_function(
|
||||
String::from("get_player_name"),
|
||||
|player_names: Query<&Name, With<Player>>| player_names.single().expect("Missing player_names").to_string(),
|
||||
|player_names: Query<&Name, With<Player>>| {
|
||||
player_names
|
||||
.single()
|
||||
.expect("Missing player_names")
|
||||
.to_string()
|
||||
},
|
||||
);
|
||||
})
|
||||
.add_systems(Startup, startup)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use bevy::prelude::*;
|
||||
use bevy_scriptum::runtimes::rhai::prelude::*;
|
||||
use bevy_scriptum::{prelude::*, BuildScriptingRuntime};
|
||||
use bevy_scriptum::{BuildScriptingRuntime, prelude::*};
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
|
|
|
|||
|
|
@ -11,7 +11,12 @@ fn main() {
|
|||
.add_scripting::<RhaiRuntime>(|builder| {
|
||||
builder.add_function(
|
||||
String::from("get_player_name"),
|
||||
|player_names: Query<&Name, With<Player>>| player_names.single().expect("Missing player_names").to_string(),
|
||||
|player_names: Query<&Name, With<Player>>| {
|
||||
player_names
|
||||
.single()
|
||||
.expect("Missing player_names")
|
||||
.to_string()
|
||||
},
|
||||
);
|
||||
})
|
||||
.add_systems(Startup, startup)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use bevy::prelude::*;
|
||||
use bevy_scriptum::runtimes::ruby::prelude::*;
|
||||
use bevy_scriptum::{prelude::*, BuildScriptingRuntime};
|
||||
use bevy_scriptum::{BuildScriptingRuntime, prelude::*};
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
use std::marker::PhantomData;
|
||||
|
||||
use bevy::{
|
||||
asset::{io::Reader, Asset, AssetLoader, LoadContext},
|
||||
asset::{Asset, AssetLoader, LoadContext, io::Reader},
|
||||
reflect::TypePath,
|
||||
tasks::ConditionalSendFuture,
|
||||
};
|
||||
|
||||
/// A loader for script assets.
|
||||
#[derive(TypePath)]
|
||||
pub struct ScriptLoader<A: Asset + From<String>> {
|
||||
_phantom_data: PhantomData<A>,
|
||||
}
|
||||
|
|
|
|||
10
src/lib.rs
10
src/lib.rs
|
|
@ -5,10 +5,11 @@
|
|||
//! ## Supported scripting languages/runtimes
|
||||
//!
|
||||
//! | language/runtime | cargo feature | documentation chapter |
|
||||
//! | ------------------------------------------ | ------------- | --------------------------------------------------------------- |
|
||||
//! | ---------------------------------------------------- | ------------- | --------------------------------------------------------------- |
|
||||
//! | 🌙 LuaJIT | `lua` | [link](https://jarkonik.github.io/bevy_scriptum/lua/lua.html) |
|
||||
//! | 🌾 Rhai | `rhai` | [link](https://jarkonik.github.io/bevy_scriptum/rhai/rhai.html) |
|
||||
//! | 💎 Ruby(currently only supported on Linux) | `ruby` | [link](https://jarkonik.github.io/bevy_scriptum/ruby/ruby.html) |
|
||||
//! | 💎 Ruby(currently only supported on Linux and MacOS) | `ruby` | [link](https://jarkonik.github.io/bevy_scriptum/ruby/ruby.html) |
|
||||
//! | 🟨 JavaScript (V8 via deno_core) | `javascript` | [link](https://jarkonik.github.io/bevy_scriptum/javascript/javascript.html) |
|
||||
//!
|
||||
//! Documentation book is available [here](https://jarkonik.github.io/bevy_scriptum/) 📖
|
||||
//!
|
||||
|
|
@ -103,7 +104,7 @@
|
|||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! bevy_scriptum = { version = "0.10", features = ["lua"] }
|
||||
//! bevy_scriptum = { version = "0.11", features = ["lua"] }
|
||||
//! ```
|
||||
//!
|
||||
//! or execute `cargo add bevy_scriptum --features lua` from your project directory.
|
||||
|
|
@ -176,6 +177,7 @@
|
|||
//!
|
||||
//! | bevy version | bevy_scriptum version |
|
||||
//! |--------------|-----------------------|
|
||||
//! | 0.18 | 0.11 |
|
||||
//! | 0.17 | 0.10 |
|
||||
//! | 0.16 | 0.8-0.9 |
|
||||
//! | 0.15 | 0.7 |
|
||||
|
|
@ -265,7 +267,7 @@ use self::{
|
|||
systems::{process_new_scripts, reload_scripts},
|
||||
};
|
||||
|
||||
#[cfg(any(feature = "rhai", feature = "lua"))]
|
||||
#[cfg(any(feature = "rhai", feature = "lua", feature = "javascript"))]
|
||||
const ENTITY_VAR_NAME: &str = "entity";
|
||||
|
||||
/// An error that can occur when internal [ScriptingPlugin] systems are being executed
|
||||
|
|
|
|||
|
|
@ -58,7 +58,12 @@ impl<C: Clone + Send + 'static, V: Send + Clone> Promise<C, V> {
|
|||
}
|
||||
|
||||
/// Register a callback that will be called when the [Promise] is resolved.
|
||||
#[cfg(any(feature = "rhai", feature = "lua", feature = "ruby"))]
|
||||
#[cfg(any(
|
||||
feature = "rhai",
|
||||
feature = "lua",
|
||||
feature = "ruby",
|
||||
feature = "javascript"
|
||||
))]
|
||||
pub(crate) fn then(&mut self, callback: V) -> Self {
|
||||
let mut inner = self
|
||||
.inner
|
||||
|
|
|
|||
776
src/runtimes/javascript.rs
Normal file
776
src/runtimes/javascript.rs
Normal file
|
|
@ -0,0 +1,776 @@
|
|||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
collections::HashMap,
|
||||
rc::Rc,
|
||||
sync::{Arc, Mutex},
|
||||
thread::{self, JoinHandle},
|
||||
};
|
||||
|
||||
use bevy::{
|
||||
asset::Asset,
|
||||
ecs::{component::Component, entity::Entity, resource::Resource, schedule::ScheduleLabel},
|
||||
math::Vec3,
|
||||
reflect::TypePath,
|
||||
};
|
||||
use deno_core::{JsRuntime as DenoRuntime, RuntimeOptions};
|
||||
|
||||
pub use deno_core::v8;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
ENTITY_VAR_NAME, FuncArgs, Runtime, ScriptingError,
|
||||
assets::GetExtensions,
|
||||
callback::{FromRuntimeValueWithEngine, IntoRuntimeValueWithEngine},
|
||||
promise::Promise,
|
||||
};
|
||||
|
||||
/// Boxed registered callback function as stored on the JavaScript thread.
|
||||
type CallbackFn =
|
||||
dyn Fn((), Vec<JsValue>) -> Result<Promise<(), JsValue>, ScriptingError> + 'static;
|
||||
|
||||
/// A queue of value ids that have been dropped on another thread and need to be
|
||||
/// removed from the value registry the next time the JavaScript thread runs.
|
||||
type FreeQueue = Arc<Mutex<Vec<u64>>>;
|
||||
|
||||
thread_local! {
|
||||
/// Registry of live `v8::Global` values, keyed by the id carried in a [JsValue].
|
||||
static REGISTRY: RefCell<HashMap<u64, v8::Global<v8::Value>>> = RefCell::new(HashMap::new());
|
||||
/// Registry of pending promises, keyed by the id stored in the script-side
|
||||
/// promise object.
|
||||
static PROMISES: RefCell<HashMap<u64, Promise<(), JsValue>>> = RefCell::new(HashMap::new());
|
||||
/// Registered Rust callback functions, keyed by their script-visible name.
|
||||
static CALLBACKS: RefCell<HashMap<String, Rc<CallbackFn>>> = RefCell::new(HashMap::new());
|
||||
static NEXT_VALUE_ID: Cell<u64> = const { Cell::new(0) };
|
||||
static NEXT_PROMISE_ID: Cell<u64> = const { Cell::new(0) };
|
||||
static FREE_QUEUE: FreeQueue = Arc::new(Mutex::new(Vec::new()));
|
||||
}
|
||||
|
||||
/// A handle to a value living inside the JavaScript engine.
|
||||
///
|
||||
/// The actual `v8::Global` is kept in a thread-local registry on the JavaScript
|
||||
/// thread - this handle only carries an id (plus a shared free-list so it can be
|
||||
/// cleaned up when dropped from any thread), which makes it cheap to `Clone` and
|
||||
/// safe to `Send`.
|
||||
#[derive(Clone)]
|
||||
pub struct JsValue(Arc<JsValueInner>);
|
||||
|
||||
struct JsValueInner {
|
||||
id: u64,
|
||||
free: FreeQueue,
|
||||
}
|
||||
|
||||
impl Drop for JsValueInner {
|
||||
fn drop(&mut self) {
|
||||
if let Ok(mut queue) = self.free.lock() {
|
||||
queue.push(self.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes any values that were dropped on other threads from the registry.
|
||||
/// Must be called on the JavaScript thread while the isolate is entered.
|
||||
fn drain_free_queue() {
|
||||
let ids: Vec<u64> = FREE_QUEUE.with(|queue| {
|
||||
queue
|
||||
.lock()
|
||||
.map(|mut queue| queue.drain(..).collect())
|
||||
.unwrap_or_default()
|
||||
});
|
||||
if !ids.is_empty() {
|
||||
REGISTRY.with(|registry| {
|
||||
let mut registry = registry.borrow_mut();
|
||||
for id in ids {
|
||||
registry.remove(&id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Stores a `v8::Local` value in the registry and returns a [JsValue] handle for it.
|
||||
fn store_value(scope: &mut v8::PinScope, local: v8::Local<v8::Value>) -> JsValue {
|
||||
let global = v8::Global::new(scope, local);
|
||||
let id = NEXT_VALUE_ID.with(|next| {
|
||||
let id = next.get();
|
||||
next.set(id + 1);
|
||||
id
|
||||
});
|
||||
REGISTRY.with(|registry| registry.borrow_mut().insert(id, global));
|
||||
let free = FREE_QUEUE.with(|queue| queue.clone());
|
||||
JsValue(Arc::new(JsValueInner { id, free }))
|
||||
}
|
||||
|
||||
/// Loads the `v8::Local` value referenced by a [JsValue] handle.
|
||||
fn load_value<'s>(scope: &mut v8::PinScope<'s, '_>, value: &JsValue) -> v8::Local<'s, v8::Value> {
|
||||
let global = REGISTRY
|
||||
.with(|registry| registry.borrow().get(&value.0.id).cloned())
|
||||
.expect("JsValue referenced a value that is no longer in the registry");
|
||||
v8::Local::new(scope, global)
|
||||
}
|
||||
|
||||
fn v8_string<'s>(scope: &mut v8::PinScope<'s, '_>, value: &str) -> v8::Local<'s, v8::Value> {
|
||||
v8::String::new(scope, value)
|
||||
.expect("Failed to allocate JavaScript string")
|
||||
.into()
|
||||
}
|
||||
|
||||
fn global_object<'s>(scope: &mut v8::PinScope<'s, '_>) -> v8::Local<'s, v8::Object> {
|
||||
scope.get_current_context().global(scope)
|
||||
}
|
||||
|
||||
fn set_entity(scope: &mut v8::PinScope, entity: Entity) {
|
||||
let global = global_object(scope);
|
||||
let key = v8_string(scope, ENTITY_VAR_NAME);
|
||||
let value = BevyEntity(entity).to_v8(scope);
|
||||
global.set(scope, key, value);
|
||||
}
|
||||
|
||||
fn clear_entity(scope: &mut v8::PinScope) {
|
||||
let global = global_object(scope);
|
||||
let key = v8_string(scope, ENTITY_VAR_NAME);
|
||||
let undefined: v8::Local<v8::Value> = v8::undefined(scope).into();
|
||||
global.set(scope, key, undefined);
|
||||
}
|
||||
|
||||
/// Wraps a bevy_scriptum [Promise] into a script-side object exposing `and_then`.
|
||||
fn promise_to_v8<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
promise: Promise<(), JsValue>,
|
||||
) -> v8::Local<'s, v8::Value> {
|
||||
let id = NEXT_PROMISE_ID.with(|next| {
|
||||
let id = next.get();
|
||||
next.set(id + 1);
|
||||
id
|
||||
});
|
||||
PROMISES.with(|promises| promises.borrow_mut().insert(id, promise));
|
||||
|
||||
let object = v8::Object::new(scope);
|
||||
let key = v8_string(scope, "and_then");
|
||||
let data: v8::Local<v8::Value> = v8::Number::new(scope, id as f64).into();
|
||||
let function = v8::Function::builder(and_then_callback)
|
||||
.data(data)
|
||||
.build(scope)
|
||||
.expect("Failed to create and_then function");
|
||||
object.set(scope, key, function.into());
|
||||
object.into()
|
||||
}
|
||||
|
||||
/// `and_then` method on script-side promise objects.
|
||||
fn and_then_callback(
|
||||
scope: &mut v8::PinScope,
|
||||
args: v8::FunctionCallbackArguments,
|
||||
mut rv: v8::ReturnValue<v8::Value>,
|
||||
) {
|
||||
let id = args
|
||||
.data()
|
||||
.number_value(scope)
|
||||
.expect("Promise object is missing its id") as u64;
|
||||
let callback = store_value(scope, args.get(0));
|
||||
|
||||
let following = PROMISES.with(|promises| {
|
||||
let mut promises = promises.borrow_mut();
|
||||
let promise = promises
|
||||
.get_mut(&id)
|
||||
.expect("Promise referenced by script no longer exists");
|
||||
promise.then(callback)
|
||||
});
|
||||
|
||||
let object = promise_to_v8(scope, following);
|
||||
rv.set(object);
|
||||
}
|
||||
|
||||
/// Callback invoked by V8 for every registered Rust function.
|
||||
fn native_callback(
|
||||
scope: &mut v8::PinScope,
|
||||
args: v8::FunctionCallbackArguments,
|
||||
mut rv: v8::ReturnValue<v8::Value>,
|
||||
) {
|
||||
let name = args.data().to_rust_string_lossy(scope);
|
||||
|
||||
let mut params = Vec::with_capacity(args.length() as usize);
|
||||
for i in 0..args.length() {
|
||||
params.push(store_value(scope, args.get(i)));
|
||||
}
|
||||
|
||||
let callback = CALLBACKS.with(|callbacks| callbacks.borrow().get(&name).cloned());
|
||||
if let Some(callback) = callback {
|
||||
let promise = callback((), params).expect("Failed to call registered function");
|
||||
let object = promise_to_v8(scope, promise);
|
||||
rv.set(object);
|
||||
}
|
||||
}
|
||||
|
||||
fn print_callback(
|
||||
scope: &mut v8::PinScope,
|
||||
args: v8::FunctionCallbackArguments,
|
||||
_rv: v8::ReturnValue<v8::Value>,
|
||||
) {
|
||||
let mut parts = Vec::with_capacity(args.length() as usize);
|
||||
for i in 0..args.length() {
|
||||
parts.push(args.get(i).to_rust_string_lossy(scope));
|
||||
}
|
||||
println!("{}", parts.join(" "));
|
||||
}
|
||||
|
||||
/// `Vec3(x, y, z)` global constructor available to all scripts.
|
||||
fn vec3_callback(
|
||||
scope: &mut v8::PinScope,
|
||||
args: v8::FunctionCallbackArguments,
|
||||
mut rv: v8::ReturnValue<v8::Value>,
|
||||
) {
|
||||
let component = |scope: &mut v8::PinScope, index: i32| {
|
||||
args.get(index).number_value(scope).unwrap_or(0.0) as f32
|
||||
};
|
||||
let x = component(scope, 0);
|
||||
let y = component(scope, 1);
|
||||
let z = component(scope, 2);
|
||||
let value = BevyVec3::new(x, y, z).to_v8(scope);
|
||||
rv.set(value);
|
||||
}
|
||||
|
||||
/// Registers a global function on the engine's global object.
|
||||
fn register_global_function(
|
||||
scope: &mut v8::PinScope,
|
||||
name: &str,
|
||||
callback: impl v8::MapFnTo<v8::FunctionCallback>,
|
||||
) {
|
||||
let global = global_object(scope);
|
||||
let key = v8_string(scope, name);
|
||||
let function = v8::Function::new(scope, callback)
|
||||
.unwrap_or_else(|| panic!("Failed to create `{name}` function"));
|
||||
global.set(scope, key, function.into());
|
||||
}
|
||||
|
||||
/// Calls a global script function by name with the provided arguments.
|
||||
fn call_global_fn(
|
||||
scope: &mut v8::PinScope,
|
||||
name: &str,
|
||||
args: &[JsValue],
|
||||
) -> Result<JsValue, ScriptingError> {
|
||||
let global = global_object(scope);
|
||||
let key = v8_string(scope, name);
|
||||
let value = global
|
||||
.get(scope, key)
|
||||
.ok_or_else(|| ScriptingError::RuntimeError(format!("function `{name}` not found")))?;
|
||||
let function = value
|
||||
.try_cast::<v8::Function>()
|
||||
.map_err(|_| ScriptingError::RuntimeError(format!("`{name}` is not a function")))?;
|
||||
let arguments: Vec<v8::Local<v8::Value>> =
|
||||
args.iter().map(|arg| load_value(scope, arg)).collect();
|
||||
call_function(scope, function, &arguments)
|
||||
}
|
||||
|
||||
/// Calls a `v8::Function` with the provided arguments, capturing runtime errors.
|
||||
fn call_function(
|
||||
scope: &mut v8::PinScope,
|
||||
function: v8::Local<v8::Function>,
|
||||
arguments: &[v8::Local<v8::Value>],
|
||||
) -> Result<JsValue, ScriptingError> {
|
||||
let recv: v8::Local<v8::Value> = v8::undefined(scope).into();
|
||||
v8::tc_scope!(let scope, scope);
|
||||
match function.call(scope, recv, arguments) {
|
||||
Some(result) => Ok(store_value(scope, result)),
|
||||
None => {
|
||||
let message = scope
|
||||
.exception()
|
||||
.map(|exception| exception.to_rust_string_lossy(scope))
|
||||
.unwrap_or_else(|| String::from("unknown JavaScript error"));
|
||||
Err(ScriptingError::RuntimeError(message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The JavaScript engine - a thin wrapper around a deno_core [JsRuntime] that
|
||||
/// allows obtaining a V8 scope through a shared reference (the rest of
|
||||
/// bevy_scriptum only hands out `&RawEngine`).
|
||||
pub struct JsEngine {
|
||||
runtime: RefCell<DenoRuntime>,
|
||||
}
|
||||
|
||||
impl JsEngine {
|
||||
fn new() -> Self {
|
||||
let engine = Self {
|
||||
runtime: RefCell::new(DenoRuntime::new(RuntimeOptions::default())),
|
||||
};
|
||||
engine.with_scope(|scope| {
|
||||
register_global_function(scope, "print", print_callback);
|
||||
register_global_function(scope, "Vec3", vec3_callback);
|
||||
});
|
||||
engine
|
||||
}
|
||||
|
||||
/// Runs `f` with a V8 handle/context scope ready for use.
|
||||
pub fn with_scope<R>(&self, f: impl FnOnce(&mut v8::PinScope) -> R) -> R {
|
||||
let mut runtime = self.runtime.borrow_mut();
|
||||
let context = runtime.main_context();
|
||||
let isolate = &mut *runtime.v8_isolate();
|
||||
v8::scope!(let scope, isolate);
|
||||
let context = v8::Local::new(scope, context);
|
||||
let scope = &mut v8::ContextScope::new(scope, context);
|
||||
drain_free_queue();
|
||||
f(scope)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
pub struct JsRuntime {
|
||||
thread: Option<JsThread>,
|
||||
}
|
||||
|
||||
impl Default for JsRuntime {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
thread: Some(JsThread::spawn()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl JsRuntime {
|
||||
fn execute_in_thread<T: Send + 'static>(
|
||||
&self,
|
||||
f: impl FnOnce(&mut JsEngine) -> T + Send + 'static,
|
||||
) -> T {
|
||||
self.thread
|
||||
.as_ref()
|
||||
.expect("JavaScript thread is gone")
|
||||
.execute(Box::new(f))
|
||||
}
|
||||
|
||||
/// Runs `f` with a V8 scope on the JavaScript thread. Useful for directly
|
||||
/// interacting with the engine, e.g. from tests.
|
||||
pub fn with_scope<T: Send + 'static>(
|
||||
&self,
|
||||
f: impl FnOnce(&mut v8::PinScope) -> T + Send + 'static,
|
||||
) -> T {
|
||||
self.execute_in_thread(move |engine| engine.with_scope(f))
|
||||
}
|
||||
}
|
||||
|
||||
type JsClosure = Box<dyn FnOnce(&mut JsEngine) + Send>;
|
||||
|
||||
/// A dedicated thread owning the (non-`Send`) V8 isolate.
|
||||
struct JsThread {
|
||||
sender: Option<crossbeam_channel::Sender<JsClosure>>,
|
||||
handle: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl JsThread {
|
||||
fn spawn() -> Self {
|
||||
let (sender, receiver) = crossbeam_channel::unbounded::<JsClosure>();
|
||||
|
||||
let handle = thread::spawn(move || {
|
||||
let mut engine = JsEngine::new();
|
||||
while let Ok(f) = receiver.recv() {
|
||||
f(&mut engine);
|
||||
}
|
||||
});
|
||||
|
||||
JsThread {
|
||||
sender: Some(sender),
|
||||
handle: Some(handle),
|
||||
}
|
||||
}
|
||||
|
||||
fn execute<T: Send + 'static>(&self, f: Box<dyn FnOnce(&mut JsEngine) -> T + Send>) -> T {
|
||||
let (return_sender, return_receiver) = crossbeam_channel::bounded(1);
|
||||
self.sender
|
||||
.as_ref()
|
||||
.expect("JavaScript thread sender is gone")
|
||||
.send(Box::new(move |engine| {
|
||||
let _ = return_sender.send(f(engine));
|
||||
}))
|
||||
.expect("Failed to send execution unit to JavaScript thread");
|
||||
return_receiver
|
||||
.recv()
|
||||
.expect("Failed to receive callback return value from JavaScript thread")
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for JsThread {
|
||||
fn drop(&mut self) {
|
||||
drop(self.sender.take());
|
||||
if let Some(handle) = self.handle.take() {
|
||||
let _ = handle.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(ScheduleLabel, Clone, PartialEq, Eq, Debug, Hash, Default)]
|
||||
pub struct JsSchedule;
|
||||
|
||||
#[derive(Asset, Debug, Deserialize, TypePath)]
|
||||
pub struct JsScript(pub String);
|
||||
|
||||
impl GetExtensions for JsScript {
|
||||
fn extensions() -> &'static [&'static str] {
|
||||
&["js"]
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for JsScript {
|
||||
fn from(value: String) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct JsScriptData;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct BevyEntity(pub Entity);
|
||||
|
||||
impl BevyEntity {
|
||||
pub fn index(&self) -> u32 {
|
||||
self.0.index_u32()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct BevyVec3(pub Vec3);
|
||||
|
||||
impl BevyVec3 {
|
||||
pub fn new(x: f32, y: f32, z: f32) -> Self {
|
||||
Self(Vec3::new(x, y, z))
|
||||
}
|
||||
|
||||
pub fn x(&self) -> f32 {
|
||||
self.0.x
|
||||
}
|
||||
|
||||
pub fn y(&self) -> f32 {
|
||||
self.0.y
|
||||
}
|
||||
|
||||
pub fn z(&self) -> f32 {
|
||||
self.0.z
|
||||
}
|
||||
}
|
||||
|
||||
impl Runtime for JsRuntime {
|
||||
type Schedule = JsSchedule;
|
||||
type ScriptAsset = JsScript;
|
||||
type ScriptData = JsScriptData;
|
||||
type CallContext = ();
|
||||
type Value = JsValue;
|
||||
type RawEngine = JsEngine;
|
||||
|
||||
fn with_engine_send_mut<T: Send + 'static>(
|
||||
&mut self,
|
||||
f: impl FnOnce(&mut Self::RawEngine) -> T + Send + 'static,
|
||||
) -> T {
|
||||
self.execute_in_thread(f)
|
||||
}
|
||||
|
||||
fn with_engine_send<T: Send + 'static>(
|
||||
&self,
|
||||
f: impl FnOnce(&Self::RawEngine) -> T + Send + 'static,
|
||||
) -> T {
|
||||
self.execute_in_thread(move |engine| f(engine))
|
||||
}
|
||||
|
||||
fn with_engine_mut<T>(&mut self, _f: impl FnOnce(&mut Self::RawEngine) -> T) -> T {
|
||||
unimplemented!(
|
||||
"JavaScript runtime requires sending execution to another thread, use `with_engine_send_mut`"
|
||||
);
|
||||
}
|
||||
|
||||
fn with_engine<T>(&self, _f: impl FnOnce(&Self::RawEngine) -> T) -> T {
|
||||
unimplemented!(
|
||||
"JavaScript runtime requires sending execution to another thread, use `with_engine_send`"
|
||||
);
|
||||
}
|
||||
|
||||
fn eval(
|
||||
&self,
|
||||
script: &Self::ScriptAsset,
|
||||
entity: Entity,
|
||||
) -> Result<Self::ScriptData, ScriptingError> {
|
||||
let source = script.0.clone();
|
||||
self.execute_in_thread(move |engine| {
|
||||
engine.with_scope(|scope| set_entity(scope, entity));
|
||||
let result = engine
|
||||
.runtime
|
||||
.borrow_mut()
|
||||
.execute_script("[bevy_scriptum]", source);
|
||||
engine.with_scope(clear_entity);
|
||||
result
|
||||
.map(|_| JsScriptData)
|
||||
.map_err(|e| ScriptingError::RuntimeError(e.to_string()))
|
||||
})
|
||||
}
|
||||
|
||||
fn register_fn(
|
||||
&mut self,
|
||||
name: String,
|
||||
_arg_types: Vec<std::any::TypeId>,
|
||||
f: impl Fn(
|
||||
Self::CallContext,
|
||||
Vec<Self::Value>,
|
||||
) -> Result<Promise<Self::CallContext, Self::Value>, ScriptingError>
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
) -> Result<(), ScriptingError> {
|
||||
self.execute_in_thread(move |engine| {
|
||||
CALLBACKS.with(|callbacks| {
|
||||
callbacks.borrow_mut().insert(name.clone(), Rc::new(f));
|
||||
});
|
||||
engine.with_scope(|scope| {
|
||||
let global = global_object(scope);
|
||||
let key = v8_string(scope, &name);
|
||||
let data = v8_string(scope, &name);
|
||||
let function = v8::Function::builder(native_callback)
|
||||
.data(data)
|
||||
.build(scope)
|
||||
.expect("Failed to create registered function");
|
||||
global.set(scope, key, function.into());
|
||||
});
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn call_fn(
|
||||
&self,
|
||||
name: &str,
|
||||
_script_data: &mut Self::ScriptData,
|
||||
entity: Entity,
|
||||
args: impl for<'a> FuncArgs<'a, Self::Value, Self> + Send + 'static,
|
||||
) -> Result<Self::Value, ScriptingError> {
|
||||
let name = name.to_string();
|
||||
self.execute_in_thread(move |engine| {
|
||||
let args = args.parse(engine);
|
||||
engine.with_scope(|scope| {
|
||||
set_entity(scope, entity);
|
||||
let result = call_global_fn(scope, &name, &args);
|
||||
clear_entity(scope);
|
||||
result
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn call_fn_from_value(
|
||||
&self,
|
||||
value: &Self::Value,
|
||||
_context: &Self::CallContext,
|
||||
args: Vec<Self::Value>,
|
||||
) -> Result<Self::Value, ScriptingError> {
|
||||
let value = value.clone();
|
||||
self.execute_in_thread(move |engine| {
|
||||
engine.with_scope(|scope| {
|
||||
let local = load_value(scope, &value);
|
||||
let function = local.try_cast::<v8::Function>().map_err(|_| {
|
||||
ScriptingError::RuntimeError(String::from("value is not a function"))
|
||||
})?;
|
||||
let arguments: Vec<v8::Local<v8::Value>> =
|
||||
args.iter().map(|arg| load_value(scope, arg)).collect();
|
||||
call_function(scope, function, &arguments)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Conversion from a Rust value into a V8 value.
|
||||
trait ToV8 {
|
||||
fn to_v8<'s>(self, scope: &mut v8::PinScope<'s, '_>) -> v8::Local<'s, v8::Value>;
|
||||
}
|
||||
|
||||
/// Conversion from a V8 value into a Rust value.
|
||||
trait FromV8 {
|
||||
fn from_v8(scope: &mut v8::PinScope, value: v8::Local<v8::Value>) -> Self;
|
||||
}
|
||||
|
||||
macro_rules! impl_number {
|
||||
($($t:ty),*) => {$(
|
||||
impl ToV8 for $t {
|
||||
fn to_v8<'s>(self, scope: &mut v8::PinScope<'s, '_>) -> v8::Local<'s, v8::Value> {
|
||||
v8::Number::new(scope, self as f64).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromV8 for $t {
|
||||
fn from_v8(scope: &mut v8::PinScope, value: v8::Local<v8::Value>) -> Self {
|
||||
value.number_value(scope).unwrap_or(0.0) as $t
|
||||
}
|
||||
}
|
||||
)*};
|
||||
}
|
||||
|
||||
impl_number!(i8, i16, i32, i64, isize, u8, u16, u32, u64, usize, f32, f64);
|
||||
|
||||
impl ToV8 for String {
|
||||
fn to_v8<'s>(self, scope: &mut v8::PinScope<'s, '_>) -> v8::Local<'s, v8::Value> {
|
||||
v8_string(scope, &self)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromV8 for String {
|
||||
fn from_v8(scope: &mut v8::PinScope, value: v8::Local<v8::Value>) -> Self {
|
||||
value.to_rust_string_lossy(scope)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToV8 for bool {
|
||||
fn to_v8<'s>(self, scope: &mut v8::PinScope<'s, '_>) -> v8::Local<'s, v8::Value> {
|
||||
v8::Boolean::new(scope, self).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromV8 for bool {
|
||||
fn from_v8(scope: &mut v8::PinScope, value: v8::Local<v8::Value>) -> Self {
|
||||
value.boolean_value(scope)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToV8 for () {
|
||||
fn to_v8<'s>(self, scope: &mut v8::PinScope<'s, '_>) -> v8::Local<'s, v8::Value> {
|
||||
v8::undefined(scope).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromV8 for () {
|
||||
fn from_v8(_scope: &mut v8::PinScope, _value: v8::Local<v8::Value>) -> Self {}
|
||||
}
|
||||
|
||||
impl ToV8 for BevyEntity {
|
||||
fn to_v8<'s>(self, scope: &mut v8::PinScope<'s, '_>) -> v8::Local<'s, v8::Value> {
|
||||
let object = v8::Object::new(scope);
|
||||
|
||||
let key = v8_string(scope, "index");
|
||||
let value: v8::Local<v8::Value> =
|
||||
v8::Integer::new_from_unsigned(scope, self.index()).into();
|
||||
object.set(scope, key, value);
|
||||
|
||||
let key = v8_string(scope, "__entity_bits");
|
||||
let value: v8::Local<v8::Value> = v8::BigInt::new_from_u64(scope, self.0.to_bits()).into();
|
||||
object.set(scope, key, value);
|
||||
|
||||
object.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromV8 for BevyEntity {
|
||||
fn from_v8(scope: &mut v8::PinScope, value: v8::Local<v8::Value>) -> Self {
|
||||
let object = value.to_object(scope).expect("entity is not an object");
|
||||
let key = v8_string(scope, "__entity_bits");
|
||||
let bits = object
|
||||
.get(scope, key)
|
||||
.expect("entity object is missing `__entity_bits`")
|
||||
.try_cast::<v8::BigInt>()
|
||||
.expect("`__entity_bits` is not a BigInt");
|
||||
let (bits, _) = bits.u64_value();
|
||||
BevyEntity(Entity::from_bits(bits))
|
||||
}
|
||||
}
|
||||
|
||||
impl ToV8 for BevyVec3 {
|
||||
fn to_v8<'s>(self, scope: &mut v8::PinScope<'s, '_>) -> v8::Local<'s, v8::Value> {
|
||||
let object = v8::Object::new(scope);
|
||||
for (name, component) in [("x", self.0.x), ("y", self.0.y), ("z", self.0.z)] {
|
||||
let key = v8_string(scope, name);
|
||||
let value: v8::Local<v8::Value> = v8::Number::new(scope, component as f64).into();
|
||||
object.set(scope, key, value);
|
||||
}
|
||||
object.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromV8 for BevyVec3 {
|
||||
fn from_v8(scope: &mut v8::PinScope, value: v8::Local<v8::Value>) -> Self {
|
||||
let object = value.to_object(scope).expect("Vec3 is not an object");
|
||||
let read = |scope: &mut v8::PinScope, name: &str| {
|
||||
let key = v8_string(scope, name);
|
||||
object
|
||||
.get(scope, key)
|
||||
.and_then(|value| value.number_value(scope))
|
||||
.unwrap_or(0.0) as f32
|
||||
};
|
||||
let x = read(scope, "x");
|
||||
let y = read(scope, "y");
|
||||
let z = read(scope, "z");
|
||||
BevyVec3(Vec3::new(x, y, z))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: ToV8> IntoRuntimeValueWithEngine<'a, T, JsRuntime> for T {
|
||||
fn into_runtime_value_with_engine(value: T, engine: &'a JsEngine) -> JsValue {
|
||||
engine.with_scope(|scope| {
|
||||
let local = value.to_v8(scope);
|
||||
store_value(scope, local)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: FromV8> FromRuntimeValueWithEngine<'a, JsRuntime> for T {
|
||||
fn from_runtime_value_with_engine(value: JsValue, engine: &'a JsEngine) -> Self {
|
||||
engine.with_scope(|scope| {
|
||||
let local = load_value(scope, &value);
|
||||
T::from_v8(scope, local)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl FuncArgs<'_, JsValue, JsRuntime> for () {
|
||||
fn parse(self, _engine: &JsEngine) -> Vec<JsValue> {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ToV8> FuncArgs<'_, JsValue, JsRuntime> for Vec<T> {
|
||||
fn parse(self, engine: &JsEngine) -> Vec<JsValue> {
|
||||
engine.with_scope(|scope| {
|
||||
self.into_iter()
|
||||
.map(|value| {
|
||||
let local = value.to_v8(scope);
|
||||
store_value(scope, local)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub mod prelude {
|
||||
pub use super::{BevyEntity, BevyVec3, JsRuntime, JsScript, JsScriptData};
|
||||
}
|
||||
|
||||
macro_rules! impl_tuple {
|
||||
($($idx:tt $t:tt),+) => {
|
||||
impl<$($t: ToV8,)+> FuncArgs<'_, JsValue, JsRuntime> for ($($t,)+) {
|
||||
fn parse(self, engine: &JsEngine) -> Vec<JsValue> {
|
||||
engine.with_scope(|scope| {
|
||||
vec![
|
||||
$({
|
||||
let local = self.$idx.to_v8(scope);
|
||||
store_value(scope, local)
|
||||
},)+
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
|
|
@ -43,7 +43,7 @@ pub struct BevyEntity(pub Entity);
|
|||
|
||||
impl BevyEntity {
|
||||
pub fn index(&self) -> u32 {
|
||||
self.0.index()
|
||||
self.0.index_u32()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -104,7 +104,7 @@ impl Default for LuaRuntime {
|
|||
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()));
|
||||
typ.add_field_method_get("index", |_, entity| Ok(entity.index()));
|
||||
})
|
||||
.expect("Failed to register BevyEntity userdata type");
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#[cfg(feature = "javascript")]
|
||||
pub mod javascript;
|
||||
#[cfg(feature = "lua")]
|
||||
pub mod lua;
|
||||
#[cfg(feature = "rhai")]
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ pub struct BevyEntity(pub Entity);
|
|||
|
||||
impl BevyEntity {
|
||||
pub fn index(&self) -> u32 {
|
||||
self.0.index()
|
||||
self.0.index_u32()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -180,7 +180,7 @@ pub struct BevyEntity(pub Entity);
|
|||
|
||||
impl BevyEntity {
|
||||
pub fn index(&self) -> u32 {
|
||||
self.0.index()
|
||||
self.0.index_u32()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
140
tests/tests.rs
140
tests/tests.rs
|
|
@ -1,17 +1,47 @@
|
|||
#[cfg(any(feature = "rhai", feature = "lua", feature = "ruby"))]
|
||||
#[cfg(any(
|
||||
feature = "rhai",
|
||||
feature = "lua",
|
||||
feature = "ruby",
|
||||
feature = "javascript"
|
||||
))]
|
||||
use std::sync::OnceLock;
|
||||
|
||||
#[cfg(any(feature = "rhai", feature = "lua", feature = "ruby"))]
|
||||
#[cfg(any(
|
||||
feature = "rhai",
|
||||
feature = "lua",
|
||||
feature = "ruby",
|
||||
feature = "javascript"
|
||||
))]
|
||||
use bevy::ecs::system::RunSystemOnce as _;
|
||||
#[cfg(any(feature = "rhai", feature = "lua", feature = "ruby"))]
|
||||
#[cfg(any(
|
||||
feature = "rhai",
|
||||
feature = "lua",
|
||||
feature = "ruby",
|
||||
feature = "javascript"
|
||||
))]
|
||||
use bevy::prelude::*;
|
||||
#[cfg(any(feature = "rhai", feature = "lua", feature = "ruby"))]
|
||||
#[cfg(any(
|
||||
feature = "rhai",
|
||||
feature = "lua",
|
||||
feature = "ruby",
|
||||
feature = "javascript"
|
||||
))]
|
||||
use bevy_scriptum::{FuncArgs, Runtime, prelude::*};
|
||||
|
||||
#[cfg(any(feature = "rhai", feature = "lua", feature = "ruby"))]
|
||||
#[cfg(any(
|
||||
feature = "rhai",
|
||||
feature = "lua",
|
||||
feature = "ruby",
|
||||
feature = "javascript"
|
||||
))]
|
||||
static TRACING_SUBSCRIBER: OnceLock<()> = OnceLock::new();
|
||||
|
||||
#[cfg(any(feature = "rhai", feature = "lua", feature = "ruby"))]
|
||||
#[cfg(any(
|
||||
feature = "rhai",
|
||||
feature = "lua",
|
||||
feature = "ruby",
|
||||
feature = "javascript"
|
||||
))]
|
||||
#[derive(Default, Resource)]
|
||||
struct TimesCalled {
|
||||
times_called: u8,
|
||||
|
|
@ -29,7 +59,12 @@ macro_rules! assert_n_times_called {
|
|||
};
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "rhai", feature = "lua", feature = "ruby"))]
|
||||
#[cfg(any(
|
||||
feature = "rhai",
|
||||
feature = "lua",
|
||||
feature = "ruby",
|
||||
feature = "javascript"
|
||||
))]
|
||||
fn build_test_app() -> App {
|
||||
let mut app = App::new();
|
||||
|
||||
|
|
@ -43,7 +78,12 @@ fn build_test_app() -> App {
|
|||
app
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "rhai", feature = "lua", feature = "ruby"))]
|
||||
#[cfg(any(
|
||||
feature = "rhai",
|
||||
feature = "lua",
|
||||
feature = "ruby",
|
||||
feature = "javascript"
|
||||
))]
|
||||
fn run_script<R: Runtime, Out, Marker>(
|
||||
app: &mut App,
|
||||
path: String,
|
||||
|
|
@ -60,7 +100,12 @@ fn run_script<R: Runtime, Out, Marker>(
|
|||
entity_id
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "rhai", feature = "lua", feature = "ruby"))]
|
||||
#[cfg(any(
|
||||
feature = "rhai",
|
||||
feature = "lua",
|
||||
feature = "ruby",
|
||||
feature = "javascript"
|
||||
))]
|
||||
fn call_script_on_update_from_rust<R: Runtime>(
|
||||
mut scripted_entities: Query<(Entity, &mut R::ScriptData)>,
|
||||
scripting_runtime: ResMut<R>,
|
||||
|
|
@ -73,7 +118,12 @@ fn call_script_on_update_from_rust<R: Runtime>(
|
|||
.unwrap();
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "rhai", feature = "lua", feature = "ruby"))]
|
||||
#[cfg(any(
|
||||
feature = "rhai",
|
||||
feature = "lua",
|
||||
feature = "ruby",
|
||||
feature = "javascript"
|
||||
))]
|
||||
trait AssertStateKeyValue {
|
||||
type ScriptData;
|
||||
fn assert_state_key_value_i64(world: &World, entity_id: Entity, key: &str, value: i64);
|
||||
|
|
@ -81,7 +131,12 @@ trait AssertStateKeyValue {
|
|||
fn assert_state_key_value_string(world: &World, entity_id: Entity, key: &str, value: &str);
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "rhai", feature = "lua", feature = "ruby"))]
|
||||
#[cfg(any(
|
||||
feature = "rhai",
|
||||
feature = "lua",
|
||||
feature = "ruby",
|
||||
feature = "javascript"
|
||||
))]
|
||||
macro_rules! scripting_tests {
|
||||
($runtime:ty, $script:literal, $extension:literal, $entity_type: ty, $vec_type: ty) => {
|
||||
use super::*;
|
||||
|
|
@ -441,7 +496,7 @@ macro_rules! scripting_tests {
|
|||
|
||||
assert_eq!(
|
||||
app.world().get_resource::<State>().unwrap().index,
|
||||
entity.index()
|
||||
entity.index_u32()
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -473,7 +528,7 @@ macro_rules! scripting_tests {
|
|||
|
||||
assert_eq!(
|
||||
app.world().get_resource::<State>().unwrap().index,
|
||||
Some(entity.index())
|
||||
Some(entity.index_u32())
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -505,7 +560,7 @@ macro_rules! scripting_tests {
|
|||
|
||||
assert_eq!(
|
||||
app.world().get_resource::<State>().unwrap().index,
|
||||
Some(entity.index())
|
||||
Some(entity.index_u32())
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -712,3 +767,60 @@ mod ruby_tests {
|
|||
|
||||
scripting_tests!(RubyRuntime, "ruby", "rb", BevyEntity, BevyVec3);
|
||||
}
|
||||
|
||||
#[cfg(feature = "javascript")]
|
||||
mod javascript_tests {
|
||||
use bevy::prelude::*;
|
||||
use bevy_scriptum::runtimes::javascript::{prelude::*, v8};
|
||||
|
||||
fn with_state_value<T: Send + 'static>(
|
||||
world: &World,
|
||||
key: &str,
|
||||
f: impl FnOnce(&mut v8::PinScope, v8::Local<v8::Value>) -> T + Send + 'static,
|
||||
) -> T {
|
||||
let runtime = world.get_resource::<JsRuntime>().unwrap();
|
||||
let key = key.to_string();
|
||||
runtime.with_scope(move |scope| {
|
||||
let global = scope.get_current_context().global(scope);
|
||||
let state_key: v8::Local<v8::Value> = v8::String::new(scope, "State").unwrap().into();
|
||||
let state = global
|
||||
.get(scope, state_key)
|
||||
.unwrap()
|
||||
.to_object(scope)
|
||||
.unwrap();
|
||||
let value_key: v8::Local<v8::Value> = v8::String::new(scope, &key).unwrap().into();
|
||||
let value = state.get(scope, value_key).unwrap();
|
||||
f(scope, value)
|
||||
})
|
||||
}
|
||||
|
||||
impl AssertStateKeyValue for JsRuntime {
|
||||
type ScriptData = JsScriptData;
|
||||
|
||||
fn assert_state_key_value_i64(world: &World, _entity_id: Entity, key: &str, value: i64) {
|
||||
let actual = with_state_value(world, key, |scope, val| {
|
||||
val.number_value(scope).unwrap() as i64
|
||||
});
|
||||
assert_eq!(actual, value);
|
||||
}
|
||||
|
||||
fn assert_state_key_value_i32(world: &World, _entity_id: Entity, key: &str, value: i32) {
|
||||
let actual = with_state_value(world, key, |scope, val| {
|
||||
val.number_value(scope).unwrap() as i32
|
||||
});
|
||||
assert_eq!(actual, value);
|
||||
}
|
||||
|
||||
fn assert_state_key_value_string(
|
||||
world: &World,
|
||||
_entity_id: Entity,
|
||||
key: &str,
|
||||
value: &str,
|
||||
) {
|
||||
let actual = with_state_value(world, key, |scope, val| val.to_rust_string_lossy(scope));
|
||||
assert_eq!(actual, value);
|
||||
}
|
||||
}
|
||||
|
||||
scripting_tests!(JsRuntime, "js", "js", BevyEntity, BevyVec3);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue