Implement JavaScript runtime #3
60 changed files with 1944 additions and 20 deletions
52
Cargo.toml
52
Cargo.toml
|
|
@ -14,6 +14,7 @@ keywords = ["bevy", "lua", "scripting", "game", "script"]
|
||||||
lua = ["dep:mlua", "mlua/luajit"]
|
lua = ["dep:mlua", "mlua/luajit"]
|
||||||
rhai = ["dep:rhai"]
|
rhai = ["dep:rhai"]
|
||||||
ruby = ["dep:magnus", "dep:rb-sys"]
|
ruby = ["dep:magnus", "dep:rb-sys"]
|
||||||
|
javascript = ["dep:deno_core"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bevy = { default-features = false, version = "0.18", features = ["bevy_asset", "bevy_log"] }
|
bevy = { default-features = false, version = "0.18", features = ["bevy_asset", "bevy_log"] }
|
||||||
|
|
@ -33,6 +34,7 @@ mlua = { version = "0.9.8", features = [
|
||||||
], optional = true }
|
], optional = true }
|
||||||
magnus = { version = "0.8.2", optional = true }
|
magnus = { version = "0.8.2", optional = true }
|
||||||
rb-sys = { version = "0.9", default-features = false, features = ["link-ruby", "ruby-static"], 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"
|
crossbeam-channel = "0.5.15"
|
||||||
libc = "0.2.172"
|
libc = "0.2.172"
|
||||||
|
|
||||||
|
|
@ -211,6 +213,56 @@ name = "side_effects_ruby"
|
||||||
path = "examples/ruby/side_effects.rs"
|
path = "examples/ruby/side_effects.rs"
|
||||||
required-features = ["ruby"]
|
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]
|
[dev-dependencies]
|
||||||
tracing-subscriber = "0.3.18"
|
tracing-subscriber = "0.3.18"
|
||||||
mlua = { version = "0.9.8", features = ["luajit", "vendored", "send"] }
|
mlua = { version = "0.9.8", features = ["luajit", "vendored", "send"] }
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ bevy_scriptum is a a plugin for [Bevy](https://bevyengine.org/) that allows you
|
||||||
| 🌙 LuaJIT | `lua` | [link](https://jarkonik.github.io/bevy_scriptum/lua/lua.html) |
|
| 🌙 LuaJIT | `lua` | [link](https://jarkonik.github.io/bevy_scriptum/lua/lua.html) |
|
||||||
| 🌾 Rhai | `rhai` | [link](https://jarkonik.github.io/bevy_scriptum/rhai/rhai.html) |
|
| 🌾 Rhai | `rhai` | [link](https://jarkonik.github.io/bevy_scriptum/rhai/rhai.html) |
|
||||||
| 💎 Ruby(currently only supported on Linux and MacOS) | `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/) 📖
|
Documentation book is available [here](https://jarkonik.github.io/bevy_scriptum/) 📖
|
||||||
|
|
||||||
|
|
|
||||||
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();
|
||||||
|
}
|
||||||
|
|
@ -19,6 +19,15 @@
|
||||||
- [Calling Ruby from Rust](./ruby/calling_script_from_rust.md)
|
- [Calling Ruby from Rust](./ruby/calling_script_from_rust.md)
|
||||||
- [Interacting with bevy in callbacks](./ruby/interacting_with_bevy.md)
|
- [Interacting with bevy in callbacks](./ruby/interacting_with_bevy.md)
|
||||||
- [Builtin types](./ruby/builtin_types.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)
|
- [Rhai](./rhai/rhai.md)
|
||||||
- [Installation](./rhai/installation.md)
|
- [Installation](./rhai/installation.md)
|
||||||
- [Hello World(TBD)]()
|
- [Hello World(TBD)]()
|
||||||
|
|
|
||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
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::prelude::*;
|
||||||
use bevy_scriptum::runtimes::lua::prelude::*;
|
use bevy_scriptum::runtimes::lua::prelude::*;
|
||||||
use bevy_scriptum::{prelude::*, BuildScriptingRuntime};
|
use bevy_scriptum::{BuildScriptingRuntime, prelude::*};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
App::new()
|
App::new()
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,12 @@ fn main() {
|
||||||
.add_scripting::<LuaRuntime>(|builder| {
|
.add_scripting::<LuaRuntime>(|builder| {
|
||||||
builder.add_function(
|
builder.add_function(
|
||||||
String::from("get_player_name"),
|
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)
|
.add_systems(Startup, startup)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy_scriptum::runtimes::rhai::prelude::*;
|
use bevy_scriptum::runtimes::rhai::prelude::*;
|
||||||
use bevy_scriptum::{prelude::*, BuildScriptingRuntime};
|
use bevy_scriptum::{BuildScriptingRuntime, prelude::*};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
App::new()
|
App::new()
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,12 @@ fn main() {
|
||||||
.add_scripting::<RhaiRuntime>(|builder| {
|
.add_scripting::<RhaiRuntime>(|builder| {
|
||||||
builder.add_function(
|
builder.add_function(
|
||||||
String::from("get_player_name"),
|
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)
|
.add_systems(Startup, startup)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy_scriptum::runtimes::ruby::prelude::*;
|
use bevy_scriptum::runtimes::ruby::prelude::*;
|
||||||
use bevy_scriptum::{prelude::*, BuildScriptingRuntime};
|
use bevy_scriptum::{BuildScriptingRuntime, prelude::*};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
App::new()
|
App::new()
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
use bevy::{
|
use bevy::{
|
||||||
asset::{io::Reader, Asset, AssetLoader, LoadContext},
|
asset::{Asset, AssetLoader, LoadContext, io::Reader},
|
||||||
tasks::ConditionalSendFuture,
|
|
||||||
reflect::TypePath,
|
reflect::TypePath,
|
||||||
|
tasks::ConditionalSendFuture,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A loader for script assets.
|
/// A loader for script assets.
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
//! | 🌙 LuaJIT | `lua` | [link](https://jarkonik.github.io/bevy_scriptum/lua/lua.html) |
|
//! | 🌙 LuaJIT | `lua` | [link](https://jarkonik.github.io/bevy_scriptum/lua/lua.html) |
|
||||||
//! | 🌾 Rhai | `rhai` | [link](https://jarkonik.github.io/bevy_scriptum/rhai/rhai.html) |
|
//! | 🌾 Rhai | `rhai` | [link](https://jarkonik.github.io/bevy_scriptum/rhai/rhai.html) |
|
||||||
//! | 💎 Ruby(currently only supported on Linux and MacOS) | `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/) 📖
|
//! Documentation book is available [here](https://jarkonik.github.io/bevy_scriptum/) 📖
|
||||||
//!
|
//!
|
||||||
|
|
@ -266,7 +267,7 @@ use self::{
|
||||||
systems::{process_new_scripts, reload_scripts},
|
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";
|
const ENTITY_VAR_NAME: &str = "entity";
|
||||||
|
|
||||||
/// An error that can occur when internal [ScriptingPlugin] systems are being executed
|
/// 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.
|
/// 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 {
|
pub(crate) fn then(&mut self, callback: V) -> Self {
|
||||||
let mut inner = self
|
let mut inner = self
|
||||||
.inner
|
.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);
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
#[cfg(feature = "javascript")]
|
||||||
|
pub mod javascript;
|
||||||
#[cfg(feature = "lua")]
|
#[cfg(feature = "lua")]
|
||||||
pub mod lua;
|
pub mod lua;
|
||||||
#[cfg(feature = "rhai")]
|
#[cfg(feature = "rhai")]
|
||||||
|
|
|
||||||
134
tests/tests.rs
134
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;
|
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 _;
|
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::*;
|
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::*};
|
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();
|
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)]
|
#[derive(Default, Resource)]
|
||||||
struct TimesCalled {
|
struct TimesCalled {
|
||||||
times_called: u8,
|
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 {
|
fn build_test_app() -> App {
|
||||||
let mut app = App::new();
|
let mut app = App::new();
|
||||||
|
|
||||||
|
|
@ -43,7 +78,12 @@ fn build_test_app() -> 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>(
|
fn run_script<R: Runtime, Out, Marker>(
|
||||||
app: &mut App,
|
app: &mut App,
|
||||||
path: String,
|
path: String,
|
||||||
|
|
@ -60,7 +100,12 @@ fn run_script<R: Runtime, Out, Marker>(
|
||||||
entity_id
|
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>(
|
fn call_script_on_update_from_rust<R: Runtime>(
|
||||||
mut scripted_entities: Query<(Entity, &mut R::ScriptData)>,
|
mut scripted_entities: Query<(Entity, &mut R::ScriptData)>,
|
||||||
scripting_runtime: ResMut<R>,
|
scripting_runtime: ResMut<R>,
|
||||||
|
|
@ -73,7 +118,12 @@ fn call_script_on_update_from_rust<R: Runtime>(
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(feature = "rhai", feature = "lua", feature = "ruby"))]
|
#[cfg(any(
|
||||||
|
feature = "rhai",
|
||||||
|
feature = "lua",
|
||||||
|
feature = "ruby",
|
||||||
|
feature = "javascript"
|
||||||
|
))]
|
||||||
trait AssertStateKeyValue {
|
trait AssertStateKeyValue {
|
||||||
type ScriptData;
|
type ScriptData;
|
||||||
fn assert_state_key_value_i64(world: &World, entity_id: Entity, key: &str, value: i64);
|
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);
|
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 {
|
macro_rules! scripting_tests {
|
||||||
($runtime:ty, $script:literal, $extension:literal, $entity_type: ty, $vec_type: ty) => {
|
($runtime:ty, $script:literal, $extension:literal, $entity_type: ty, $vec_type: ty) => {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
@ -712,3 +767,60 @@ mod ruby_tests {
|
||||||
|
|
||||||
scripting_tests!(RubyRuntime, "ruby", "rb", BevyEntity, BevyVec3);
|
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