Ruby support (#19)
Some checks are pending
Book / test (push) Waiting to run
Deploy book / deploy (push) Waiting to run
Rust / build (push) Waiting to run

Add support for Ruby language
This commit is contained in:
Jarosław Konik 2025-05-27 17:19:52 +00:00 committed by GitHub
parent f2bb079c60
commit 53479f94b5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
93 changed files with 5129 additions and 305 deletions

54
.github/workflows/book.yml vendored Normal file
View file

@ -0,0 +1,54 @@
name: Book
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Cache Ruby
id: cache-ruby
uses: actions/cache@v4
with:
path: rubies
key: ${{ runner.os }}-ruby
- name: Install Ruby
if: steps.cache-ruby.outputs.cache-hit != 'true'
env:
CC: clang
run: |
url="https://cache.ruby-lang.org/pub/ruby/3.4/ruby-3.4.4.tar.gz"
prefix=`pwd`/rubies/ruby-3.4
mkdir rubies
mkdir ruby_src
curl -sSL $url | tar -xz
cd ruby-3.4.4
mkdir build
cd build
../configure --without-shared --prefix=$prefix
make install
echo $prefix/bin >> $GITHUB_PATH
- name: Add Ruby to PATH
run: |
prefix=`pwd`/rubies/ruby-3.4
echo $prefix/bin >> $GITHUB_PATH
- name: Install latest mdbook
run: |
tag=$(curl 'https://api.github.com/repos/rust-lang/mdbook/releases/latest' | jq -r '.tag_name')
url="https://github.com/rust-lang/mdbook/releases/download/${tag}/mdbook-${tag}-x86_64-unknown-linux-gnu.tar.gz"
mkdir mdbook
curl -sSL $url | tar -xz --directory=./mdbook
echo `pwd`/mdbook >> $GITHUB_PATH
- name: Test Book
run: |
cd book
export CARGO_MANIFEST_DIR=$(pwd)
cargo build
mdbook test -L target/debug/deps/

View file

@ -2,9 +2,9 @@ name: Rust
on:
push:
branches: [ "main" ]
branches: ["main"]
pull_request:
branches: [ "main" ]
branches: ["main"]
env:
CARGO_TERM_COLOR: always
@ -15,10 +15,35 @@ jobs:
env:
RUSTFLAGS: -D warnings
steps:
- uses: actions/checkout@v3
- name: Clippy
run: cargo clippy --all-features --verbose -- -D warnings
- name: Build
run: cargo build --all-features --verbose
- name: Run tests
run: cargo test --all-features --verbose
- uses: actions/checkout@v3
- name: Cache Ruby
id: cache-ruby
uses: actions/cache@v4
with:
path: rubies
key: ${{ runner.os }}-ruby
- name: Install Ruby
if: steps.cache-ruby.outputs.cache-hit != 'true'
env:
CC: clang
run: |
url="https://cache.ruby-lang.org/pub/ruby/3.4/ruby-3.4.4.tar.gz"
prefix=`pwd`/rubies/ruby-3.4
mkdir rubies
mkdir ruby_src
curl -sSL $url | tar -xz
cd ruby-3.4.4
mkdir build
cd build
../configure --without-shared --prefix=$prefix
make install
- name: Add Ruby to PATH
run: |
prefix=`pwd`/rubies/ruby-3.4
echo $prefix/bin >> $GITHUB_PATH
- name: Clippy
run: cargo clippy --all-features --verbose -- -D warnings
- name: Build
run: cargo build --all-features --verbose
- name: Run tests
run: cargo test --all-features --verbose

2
.gitignore vendored
View file

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

View file

@ -2,17 +2,18 @@
name = "bevy_scriptum"
authors = ["Jaroslaw Konik <konikjar@gmail.com>"]
version = "0.8.1"
edition = "2021"
edition = "2024"
license = "MIT OR Apache-2.0"
readme = "README.md"
categories = ["game-development"]
description = "Plugin for Bevy engine that allows you to write some of your game logic in a scripting language"
description = "Plugin for Bevy engine that allows you to write some of your game or application logic in a scripting language"
repository = "https://github.com/jarkonik/bevy_scriptum"
keywords = ["bevy", "rhai", "scripting", "game", "gamedev"]
keywords = ["bevy", "lua", "scripting", "game", "script"]
[features]
lua = ["mlua/luajit"]
lua = ["dep:mlua", "mlua/luajit"]
rhai = ["dep:rhai"]
ruby = ["dep:magnus", "dep:rb-sys"]
[dependencies]
bevy = { default-features = false, version = "0.16", features = ["bevy_asset", "bevy_log"] }
@ -30,6 +31,10 @@ mlua = { version = "0.9.8", features = [
"vendored",
"send",
], optional = true }
magnus = { version = "0.7.1", optional = true }
rb-sys = { version = "*", default-features = false, features = ["link-ruby", "ruby-static"], optional = true }
crossbeam-channel = "0.5.15"
libc = "0.2.172"
[[example]]
name = "call_function_from_rust_rhai"
@ -151,6 +156,61 @@ name = "function_return_value_lua"
path = "examples/lua/function_return_value.rs"
required-features = ["lua"]
[[example]]
name = "call_function_from_rust_ruby"
path = "examples/ruby/call_function_from_rust.rs"
required-features = ["ruby"]
[[example]]
name = "current_entity_ruby"
path = "examples/ruby/current_entity.rs"
required-features = ["ruby"]
[[example]]
name = "custom_type_ruby"
path = "examples/ruby/custom_type.rs"
required-features = ["ruby"]
[[example]]
name = "ecs_ruby"
path = "examples/ruby/ecs.rs"
required-features = ["ruby"]
[[example]]
name = "entity_variable_ruby"
path = "examples/ruby/entity_variable.rs"
required-features = ["ruby"]
[[example]]
name = "function_params_ruby"
path = "examples/ruby/function_params.rs"
required-features = ["ruby"]
[[example]]
name = "function_return_value_ruby"
path = "examples/ruby/function_return_value.rs"
required-features = ["ruby"]
[[example]]
name = "hello_world_ruby"
path = "examples/ruby/hello_world.rs"
required-features = ["ruby"]
[[example]]
name = "multiple_plugins_ruby"
path = "examples/ruby/multiple_plugins.rs"
required-features = ["ruby"]
[[example]]
name = "promises_ruby"
path = "examples/ruby/promises.rs"
required-features = ["ruby"]
[[example]]
name = "side_effects_ruby"
path = "examples/ruby/side_effects.rs"
required-features = ["ruby"]
[dev-dependencies]
tracing-subscriber = "0.3.18"
mlua = { version = "0.9.8", features = ["luajit", "vendored", "send"] }

View file

@ -2,13 +2,18 @@
![demo](demo.gif)
bevy_scriptum is a a plugin for [Bevy](https://bevyengine.org/) that allows you to write some of your game logic in a scripting language.
Currently [Rhai](https://rhai.rs/) and [Lua](https://lua.org/) are supported, but more languages may be added in the future.
bevy_scriptum is a a plugin for [Bevy](https://bevyengine.org/) that allows you to write some of your game or application logic in a scripting language.
### Supported scripting languages/runtimes
Everything you need to know to get started with using this library is contained in the
[bevy_scriptum book](https://jarkonik.github.io/bevy_scriptum/)
| 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 | `ruby` | [link](https://jarkonik.github.io/bevy_scriptum/ruby/ruby.html) |
API docs are available in [docs.rs](https://docs.rs/bevy_scriptum/latest/bevy_scriptum/)
Documentation book is available [here](https://jarkonik.github.io/bevy_scriptum/) 📖
Full API docs are available at [docs.rs](https://docs.rs/bevy_scriptum/latest/bevy_scriptum/) 🧑‍💻
bevy_scriptum's main advantages include:
- low-boilerplate
@ -17,7 +22,7 @@ bevy_scriptum's main advantages include:
- flexibility
- hot-reloading
Scripts are separate files that can be hot-reloaded at runtime. This allows you to quickly iterate on your game logic without having to recompile your game.
Scripts are separate files that can be hot-reloaded at runtime. This allows you to quickly iterate on your game or application logic without having to recompile it.
All you need to do is register callbacks on your Bevy app like this:
```rust
@ -86,33 +91,6 @@ which you can then call in your script like this:
```lua
fun_with_string_param("Hello world!")
```
It is also possible to split the definition of your callback functions up over multiple plugins. This enables you to split up your code by subject and keep the main initialization light and clean.
This can be accomplished by using `add_scripting_api`. Be careful though, `add_scripting` has to be called before adding plugins.
```rust
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;
struct MyPlugin;
impl Plugin for MyPlugin {
fn build(&self, app: &mut App) {
app.add_scripting_api::<LuaRuntime>(|runtime| {
runtime.add_function(String::from("hello_from_my_plugin"), || {
info!("Hello from MyPlugin");
});
});
}
}
App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<LuaRuntime>(|_| {
// nice and clean
})
.add_plugins(MyPlugin)
.run();
```
### Usage

View file

@ -0,0 +1,13 @@
$my_state = {
iterations: 0,
}
def on_update
$my_state[:iterations] += 1
puts("on_update called #{$my_state[:iterations]} times")
if $my_state[:iterations] >= 10
print("calling quit");
quit()
end
end

View file

@ -0,0 +1,3 @@
get_name(Bevy::Entity.current).and_then do |name|
puts(name)
end

View file

@ -0,0 +1,4 @@
# Create a new instance of MyType
my_type = MyType.new()
# Call registered method
puts(my_type.my_method)

View file

@ -0,0 +1 @@
print_player_names

View file

@ -0,0 +1,2 @@
# Bevy::Entity.current can be used to access the entity that is currently being processed
puts("Current entity index: #{Bevy::Entity.current.index}")

View file

@ -0,0 +1,4 @@
fun_with_string_param("hello")
fun_with_i64_param(5)
fun_with_multiple_params(5, "hello")
fun_with_i64_and_array_param(5, [1, 2, "third element"])

View file

@ -0,0 +1,3 @@
def get_value
42
end

View file

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

View file

@ -0,0 +1 @@
hello_from_plugin_a

View file

@ -0,0 +1 @@
hello_from_plugin_b_with_parameters("hello", 42)

View file

@ -0,0 +1,3 @@
get_player_name.and_then do |name|
puts name
end

View file

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

View file

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

View file

@ -0,0 +1,5 @@
index = entity.index
function test_func()
rust_func(index)
end

View file

@ -0,0 +1,2 @@
mark_called()
error()

View file

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

View file

@ -0,0 +1,3 @@
function test_func()
rust_func(Vec3(1.5, 2.5, -3.5))
end

View file

@ -0,0 +1,6 @@
function test_func(vec3)
assert(vec3.x == 1.5)
assert(vec3.y == 2.5)
assert(vec3.z == -3.5)
mark_success()
end

View file

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

View file

@ -0,0 +1,5 @@
let index = entity.index;
fn test_func() {
rust_func(index);
}

View file

@ -0,0 +1,2 @@
mark_called();
throw();

View file

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

View file

@ -0,0 +1,3 @@
fn test_func() {
rust_func(new_vec3(1.5, 2.5, -3.5));
}

View file

@ -0,0 +1,7 @@
fn test_func(vec3) {
if type_of(vec3) != "Vec3" { throw() }
if vec3.x != 1.5 { throw() }
if vec3.y != 2.5 { throw() }
if vec3.z != -3.5 { throw() }
mark_success();
}

View file

@ -0,0 +1,3 @@
def test_func
raise
end

View file

@ -0,0 +1,7 @@
$state = {
'called_with' => nil
}
def test_func(val)
$state['called_with'] = val
end

View file

@ -0,0 +1,3 @@
def test_func
rust_func(Bevy::Entity.current.index)
end

View file

@ -0,0 +1,5 @@
$index = Bevy::Entity.current.index
def test_func
rust_func($index)
end

View file

@ -0,0 +1,2 @@
mark_called
raise

View file

@ -0,0 +1,3 @@
def test_func
rust_func(Bevy::Entity.current)
end

View file

@ -0,0 +1,3 @@
def test_func
rust_func(Bevy::Vec3.new(1.5, 2.5, -3.5))
end

View file

@ -0,0 +1,7 @@
def test_func(vec3)
raise unless vec3.is_a?(Bevy::Vec3)
raise unless vec3.x == 1.5
raise unless vec3.y == 2.5
raise unless vec3.z == -3.5
mark_success
end

View file

@ -0,0 +1,5 @@
def test_func
rust_func.and_then do |x|
raise
end
end

View file

@ -0,0 +1,9 @@
$state = {
'x' => nil
}
def test_func
rust_func.and_then do |x|
$state['x'] = x
end
end

View file

@ -0,0 +1,3 @@
def test_func
rust_func
end

View file

@ -0,0 +1,3 @@
def test_func
rust_func(5, 'test')
end

View file

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

View file

@ -0,0 +1,7 @@
$state = {
'times_called' => 0
}
def test_func
$state['times_called'] += 1
end

View file

@ -0,0 +1,9 @@
$state = {
'a_value' => nil,
'b_value' => nil
}
def test_func(a, b)
$state['a_value'] = a
$state['b_value'] = b
end

View file

@ -0,0 +1,7 @@
$state = {
'a_value' => nil
}
def test_func(a)
$state['a_value'] = a
end

View file

@ -0,0 +1,3 @@
def test_func
spawn_entity
end

1
book/.gitignore vendored
View file

@ -1,2 +1,3 @@
book
doctest_cache
target

2498
book/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

7
book/Cargo.toml Normal file
View file

@ -0,0 +1,7 @@
[package]
name = "bevy_scriptum_book"
publish = false
edition = "2024"
[dependencies]
bevy_scriptum = { path = "../", features = ["ruby", "lua", "rhai"] }

View file

@ -4,9 +4,3 @@ language = "en"
multilingual = false
src = "src"
title = "bevy_scriptum"
[preprocessor.keeper]
command = "mdbook-keeper"
manifest_dir = "../"
externs = ["bevy", "bevy_scriptum"]
build_features = ["lua", "rhai"]

15
book/justfile Normal file
View file

@ -0,0 +1,15 @@
export CARGO_MANIFEST_DIR := `pwd`
build-deps:
cargo clean && cargo build
_test:
mdbook test -L target/debug/deps/
test: build-deps _test
test-watch: build-deps
watchexec --exts md -r just _test
serve:
mdbook serve

View file

@ -2,20 +2,29 @@
- [Introduction](./introduction.md)
- [Runtimes](./runtimes.md)
- [Lua](./lua/lua.md)
- [Installation](./lua/installation.md)
- [Hello World](./lua/hello_world.md)
- [Spawning scripts](./lua/spawning_scripts.md)
- [Calling Rust from Lua](./lua/calling_rust_from_script.md)
- [Calling Lua from Rust](./lua/calling_script_from_rust.md)
- [Interacting with bevy in callbacks](./lua/interacting_with_bevy.md)
- [Builtin types](./lua/builtin_types.md)
- [Builtin variables](./lua/builtin_variables.md)
- [Rhai](./rhai/rhai.md)
- [Installation](./rhai/installation.md)
- [Hello World(TBD)]()
- [Lua](./lua/lua.md)
- [Installation](./lua/installation.md)
- [Hello World](./lua/hello_world.md)
- [Spawning scripts](./lua/spawning_scripts.md)
- [Calling Rust from Lua](./lua/calling_rust_from_script.md)
- [Calling Lua from Rust](./lua/calling_script_from_rust.md)
- [Interacting with bevy in callbacks](./lua/interacting_with_bevy.md)
- [Builtin types](./lua/builtin_types.md)
- [Builtin variables](./lua/builtin_variables.md)
- [Ruby](./ruby/ruby.md)
- [Installation](./ruby/installation.md)
- [Hello World](./ruby/hello_world.md)
- [Spawning scripts](./ruby/spawning_scripts.md)
- [Calling Rust from Ruby](./ruby/calling_rust_from_script.md)
- [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)
- [Rhai](./rhai/rhai.md)
- [Installation](./rhai/installation.md)
- [Hello World(TBD)]()
- [Multiple plugins](./multiple_plugins.md)
- [Multiple runtimes(TBD)]()
- [Implementing custom runtimes(TBD)]()
- [Workflow](./workflow/workflow.md)
- [Live-reload](./workflow/live_reload.md)
- [Live-reload](./workflow/live_reload.md)
- [Bevy support matrix](./bevy_support_matrix.md)

View file

@ -1,9 +1,18 @@
# bevy_scriptum 📜
bevy_scriptum is a a plugin for [Bevy](https://bevyengine.org/) that allows you to write some of your game logic in a scripting language.
Currently [Rhai](https://rhai.rs/) and [Lua](https://lua.org/) are supported, but more languages may be added in the future.
bevy_scriptum is a a plugin for [Bevy](https://bevyengine.org/) that allows you to write some of your game or application logic in a scripting language.
API docs are available in [docs.rs](https://docs.rs/bevy_scriptum/latest/bevy_scriptum/)
## 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 | `ruby` | [link](https://jarkonik.github.io/bevy_scriptum/ruby/ruby.html) |
Documentation book is available [here](https://jarkonik.github.io/bevy_scriptum/) 📖
Full API docs are available at [docs.rs](https://docs.rs/bevy_scriptum/latest/bevy_scriptum/) 🧑‍💻
bevy_scriptum's main advantages include:
- low-boilerplate
@ -12,10 +21,13 @@ bevy_scriptum's main advantages include:
- flexibility
- hot-reloading
Scripts are separate files that can be hot-reloaded at runtime. This allows you to quickly iterate on your game logic without having to recompile your game.
Scripts are separate files that can be hot-reloaded at runtime. This allows you to quickly iterate on your game logic without having to recompile it.
All you need to do is register callbacks on your Bevy app like this:
```rust
```rust,no_run
# extern crate bevy;
# extern crate bevy_scriptum;
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;
@ -38,7 +50,11 @@ hello_bevy()
Every callback function that you expose to the scripting language is also a Bevy system, so you can easily query and mutate ECS components and resources just like you would in a regular Bevy system:
```rust
```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::lua::prelude::*;
@ -64,7 +80,10 @@ fn main() {
```
You can also pass arguments to your callback functions, just like you would in a regular Bevy system - using `In` structs with tuples:
```rust
```rust,no_run
# extern crate bevy;
# extern crate bevy_scriptum;
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;
@ -88,36 +107,6 @@ which you can then call in your script like this:
fun_with_string_param("Hello world!")
```
It is also possible to split the definition of your callback functions up over multiple plugins. This enables you to split up your code by subject and keep the main initialization light and clean.
This can be accomplished by using `add_scripting_api`. Be careful though, `add_scripting` has to be called before adding plugins.
```rust
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;
struct MyPlugin;
impl Plugin for MyPlugin {
fn build(&self, app: &mut App) {
app.add_scripting_api::<LuaRuntime>(|runtime| {
runtime.add_function(String::from("hello_from_my_plugin"), || {
info!("Hello from MyPlugin");
});
});
}
}
// Main
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<LuaRuntime>(|_| {
// nice and clean
})
.add_plugins(MyPlugin)
.run();
}
```
### Usage
Add the following to your `Cargo.toml`:
@ -131,7 +120,10 @@ or execute `cargo add bevy_scriptum --features lua` from your project directory.
You can now start exposing functions to the scripting language. For example, you can expose a function that prints a message to the console:
```rust
```rust,no_run
# extern crate bevy;
# extern crate bevy_scriptum;
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;
@ -159,7 +151,10 @@ my_print("Hello world!")
And spawn an entity with attached `Script` component with a handle to a script source file:
```rust
```rust,no_run
# extern crate bevy;
# extern crate bevy_scriptum;
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;
@ -204,7 +199,10 @@ end)
```
which will print out `John` when used with following exposed function:
```rust
```rust,no_run
# extern crate bevy;
# extern crate bevy_scriptum;
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;

0
book/src/lib.rs Normal file
View file

View file

@ -20,14 +20,17 @@ bevy_scriptum provides following types that can be used in Lua:
### Example Lua usage
```
```lua
my_vec = Vec3(1, 2, 3)
set_translation(entity, my_vec)
```
### Example Rust usage
```rust
```rust,no_run
# extern crate bevy;
# extern crate bevy_scriptum;
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;
@ -69,7 +72,10 @@ pass_to_rust(entity)
### Example Rust usage
```rust
```rust,no_run
# extern crate bevy;
# extern crate bevy_scriptum;
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;

View file

@ -3,7 +3,10 @@
To call a rust function from Lua first you need to register a function
within Rust using builder pattern.
```rust
```rust,no_run
# extern crate bevy;
# extern crate bevy_scriptum;
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;
@ -20,7 +23,10 @@ fn main() {
For example to register a function called `my_rust_func` you can do the following:
```rust
```rust,no_run
# extern crate bevy;
# extern crate bevy_scriptum;
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;
@ -49,7 +55,10 @@ that implements `FromLua`.
Since a registered callback function is a Bevy system, the parameters are passed
to it as `In` struct with tuple, which has to be the first parameter of the closure.
```rust
```rust,no_run
# extern crate bevy;
# extern crate bevy_scriptum;
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;
@ -68,7 +77,10 @@ fn main() {
To make it look nicer you can destructure the `In` struct.
```rust
```rust,no_run
# extern crate bevy;
# extern crate bevy_scriptum;
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;
@ -97,7 +109,10 @@ Any registered rust function that returns a value will retrurn a promise when
called within a script. By calling `:and_then` on the promise you can register
a callback that will receive the value returned from Rust function.
```rust
```rust,no_run
# extern crate bevy;
# extern crate bevy_scriptum;
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;

View file

@ -13,7 +13,10 @@ of the function to call, `LuaScriptData` that has been automatically
attached to entity after an entity with script attached has been spawned
and its script evaluated, the entity and optionally some arguments.
```rust
```rust,no_run
# extern crate bevy;
# extern crate bevy_scriptum;
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;
@ -29,14 +32,15 @@ fn call_lua_on_update_from_rust(
.unwrap();
}
}
fn main() {}
```
We can also pass some arguments by providing a tuple or `Vec` as the last
`call_fn` argument.
```rust
```rust,no_run
# extern crate bevy;
# extern crate bevy_scriptum;
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;
@ -51,8 +55,6 @@ fn call_lua_on_update_from_rust(
.unwrap();
}
}
fn main() {}
```
They will be passed to `on_update` Lua function

View file

@ -12,7 +12,10 @@ a create feature.
You can now start exposing functions to the scripting language. For example, you can expose a function that prints a message to the console:
```rust
```rust,no_run
# extern crate bevy;
# extern crate bevy_scriptum;
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;
@ -40,7 +43,10 @@ my_print("Hello world!")
And spawn an entity with attached `Script` component with a handle to a script source file:
```rust
```rust,no_run
# extern crate bevy;
# extern crate bevy_scriptum;
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;

View file

@ -7,7 +7,11 @@ That allows you to do anything you would do in a Bevy system.
You could for example create a callback system function that prints names
of all entities with `Player` component.
```rust
```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::lua::prelude::*;
@ -42,7 +46,11 @@ 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
```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::lua::prelude::*;
@ -59,7 +67,7 @@ fn main() {
runtime.add_function(
String::from("hurt_player"),
|In((hit_value,)): In<(i32,)>, mut players: Query<&mut Player>| {
let mut player = players.single_mut();
let mut player = players.single_mut().unwrap();
player.health -= hit_value;
},
);

View file

@ -4,6 +4,9 @@ To spawn a Lua 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::lua::prelude::*;
@ -13,8 +16,6 @@ fn my_spawner(mut commands: Commands, assets_server: Res<AssetServer>) {
assets_server.load("my_script.lua"),
));
}
fn main() {}
```
After they scripts have been evaled by bevy_scriptum, the entities that they've
@ -24,6 +25,9 @@ been attached to will get the `Script::<LuaScript>` component stripped and inste
So to query scipted 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::lua::prelude::*;
@ -35,6 +39,4 @@ fn my_system(
// do something with scripted entities
}
}
fn main() {}
```

View file

@ -0,0 +1,34 @@
# Multiple plugins
It is possible to split the definition of your callback functions up over multiple plugins. This enables you to split up your code by subject and keep the main initialization light and clean.
This can be accomplished by using `add_scripting_api`. Be careful though, `add_scripting` has to be called before adding plugins.
```rust,no_run
# extern crate bevy;
# extern crate bevy_scriptum;
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;
struct MyPlugin;
impl Plugin for MyPlugin {
fn build(&self, app: &mut App) {
app.add_scripting_api::<LuaRuntime>(|runtime| {
runtime.add_function(String::from("hello_from_my_plugin"), || {
info!("Hello from MyPlugin");
});
});
}
}
// Main
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<LuaRuntime>(|_| {
// nice and clean
})
.add_plugins(MyPlugin)
.run();
}
```

View file

@ -0,0 +1,95 @@
# Builtin types
bevy_scriptum provides following types that can be used in Ruby:
- `Bevy::Vec3`
- `Bevy::Entity`
## Bevy::Vec3
### Class Methods
- `new(x, y, z)`
- `current`
### Instance Methods
- `x`
- `y`
- `z`
### Example Ruby usage
```ruby
my_vec = Bevy::Vec3.new(1, 2, 3)
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::ruby::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<RubyRuntime>(|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;
}
```
## Bevy::Entity
`Bevy::Entity.current` is currently not available within promise callbacks.
### Constructor
None - instances can only be acquired by using `Bevy::Entity.current`
### Class method
- `index`
### Example Ruby usage
```ruby
puts(Bevy::Entity.current.index)
pass_to_rust(Bevy::Entity.current)
```
### Example Rust usage
```rust,no_run
# extern crate bevy;
# extern crate bevy_scriptum;
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::ruby::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<RubyRuntime>(|runtime| {
runtime.add_function(String::from("pass_to_rust"), |In((entity,)): In<(BevyEntity,)>| {
println!("pass_to_rust called with entity: {:?}", entity);
});
})
.run();
}
```

View file

@ -0,0 +1,133 @@
# Calling Rust from Ruby
To call a rust function from Ruby first you need to register a function
within Rust using builder pattern.
```rust,no_run
# extern crate bevy;
# extern crate bevy_scriptum;
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::ruby::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<RubyRuntime>(|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::ruby::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<RubyRuntime>(|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 Ruby code in your spawned scripts.
```ruby
my_rust_func
```
Since a registered callback function is a Bevy system, the parameters are passed
to it as `In` struct with tuple, which has to be the first parameter of the closure.
```rust,no_run
# extern crate bevy;
# extern crate bevy_scriptum;
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::ruby::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<RubyRuntime>(|runtime| {
runtime.add_function(String::from("func_with_params"), |args: In<(String, i64)>| {
println!("my_rust_func has been called with string {} and i64 {}", args.0.0, args.0.1);
});
})
.run();
}
```
To make it look nicer you can destructure the `In` struct.
```rust,no_run
# extern crate bevy;
# extern crate bevy_scriptum;
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::ruby::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<RubyRuntime>(|runtime| {
runtime.add_function(String::from("func_with_params"), |In((a, b)): In<(String, i64)>| {
println!("my_rust_func has been called with string {} and i64 {}", a, b);
});
})
.run();
}
```
The above function can be called from Ruby
```ruby
func_with_params("abc", 123)
```
## Return value via promise
Any registered rust function that returns a value will retrurn a promise when
called within a script. By calling `:and_then` on the promise you can register
a callback that will receive the value returned from Rust function.
```rust,no_run
# extern crate bevy;
# extern crate bevy_scriptum;
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::ruby::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<RubyRuntime>(|runtime| {
runtime.add_function(String::from("returns_value"), || {
123
});
})
.run();
}
```
```ruby
returns_value.and_then do |value|
puts(value) # 123
end
```

View file

@ -0,0 +1,66 @@
# Calling Ruby from Rust
To call a function defined in Ruby
```ruby
def on_update
end
```
We need to acquire `RubyRuntime` resource within a bevy system.
Then we will be able to call `call_fn` on it, providing the name
of the function to call, `RubyScriptData` that has been automatically
attached to entity after an entity with script attached has been spawned
and its script evaluated, the entity and optionally some arguments.
```rust,no_run
# extern crate bevy;
# extern crate bevy_scriptum;
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::ruby::prelude::*;
fn call_ruby_on_update_from_rust(
mut scripted_entities: Query<(Entity, &mut RubyScriptData)>,
scripting_runtime: ResMut<RubyRuntime>,
) {
for (entity, mut script_data) in &mut scripted_entities {
// calling function named `on_update` defined in Ruby 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::ruby::prelude::*;
fn call_ruby_on_update_from_rust(
mut scripted_entities: Query<(Entity, &mut RubyScriptData)>,
scripting_runtime: ResMut<RubyRuntime>,
) {
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 `on_update` Ruby function
```ruby
def on_update(a, b)
puts(a) # 123
puts(b) # hello
end
```

View file

@ -0,0 +1,72 @@
# Hello World
After you are done installing the required crates, you can start developing
your first game or application using bevy_scriptum.
To start using the library you need to first import some structs and traits
with Rust `use` statements.
For convenience there is a main "prelude" module provided called
`bevy_scriptum::prelude` and a prelude for each runtime you have enabled as
a create feature.
You can now start exposing functions to the scripting language. For example, you can expose a function that prints a message to the console:
```rust,no_run
# extern crate bevy;
# extern crate bevy_scriptum;
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::ruby::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<RubyRuntime>(|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.rb` that calls this function:
```ruby
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::ruby::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<RubyRuntime>(|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::<RubyScript>::new(asset_server.load("script.rb")));
})
.run();
}
```
You should then see `my_print: 'Hello world!'` printed in your console.

View file

@ -0,0 +1,50 @@
# Installation
## Ruby
To build `bevy_scriptum` with Ruby support a Ruby installation is needed to be
present on your development machine.
The easiest way to produce a compatible Ruby installation is to use [rbenv](https://rbenv.org/).
After installing `rbenv` along with its `ruby-build` plugin you can build and
install a Ruby installation that will work with `bevy_scriptum` by executing:
```sh
CC=clang rbenv install 3.4.4
```
Above assumes that you also have `clang` installed on your system.
For `clang` installation instruction consult your
OS vendor provided documentation or [clang official webiste](https://clang.llvm.org).
If you rather not use `rbenv` you are free to supply your own installation of
Ruby provided the following is true about it:
- it is compiled with `clang`
- it is compiled as a static library
- it is accessible as `ruby` within `PATH` or `RUBY` environment variable is set
to path of desired `ruby` binary.
## Main Library
Add the following to your `Cargo.toml`:
```toml
[dependencies]
bevy = "0.16"
bevy_scriptum = { version = "0.8", features = ["ruby"] }
```
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)
Ruby also needs dynamic symbol resolution and since `bevy_scriptum` links Ruby
statically the following `build.rs` file is needed to be present in project
root directory.
```rust
fn main() {
println!("cargo:rustc-link-arg=-rdynamic");
}
```

View 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 `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::ruby::prelude::*;
#[derive(Component)]
struct Player;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<RubyRuntime>(|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:
```ruby
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::ruby::prelude::*;
#[derive(Component)]
struct Player {
health: i32
}
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<RubyRuntime>(|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:
```ruby
hurt_player(5)
```

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

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

View file

@ -0,0 +1,42 @@
# Spawning scripts
To spawn a Ruby 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::ruby::prelude::*;
fn my_spawner(mut commands: Commands, assets_server: Res<AssetServer>) {
commands.spawn(Script::<RubyScript>::new(
assets_server.load("my_script.rb"),
));
}
```
After they scripts have been evaled by bevy_scriptum, the entities that they've
been attached to will get the `Script::<RubyScript>` component stripped and instead
```RubyScriptData``` component will be attached.
So to query scipted 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::ruby::prelude::*;
fn my_system(
mut scripted_entities: Query<(Entity, &mut RubyScriptData)>,
) {
for (entity, mut script_data) in &mut scripted_entities {
// do something with scripted entities
}
}
```

View file

@ -5,22 +5,22 @@
To enable live reload it should be enough to enable `file-watcher` feature
within bevy dependency in `Cargo.toml`
```
```toml
bevy = { version = "0.16", features = ["file_watcher"] }
```
## Init-teardown pattern for game development
## Init-teardown pattern
It is useful to structure your game in a way that would allow making changes to
the scripting code without restarting the game.
It is useful to structure your application in a way that would allow making changes to
the scripting code without restarting the application.
A useful pattern is to hava three functions "init", "update" and "teardown".
- "init" function will take care of starting the game(spawning the player, the level etc)
- "init" function will take care of starting the application(spawning the player, the level etc)
- "update" function will run the main game logic
- "update" function will run the main application logic
- "teardown" function will despawn all the entities so game starts at fresh state.
- "teardown" function will despawn all the entities so application starts at fresh state.
This pattern is very easy to implement in bevy_scriptum. All you need is to define all needed functions
in script:
@ -35,7 +35,7 @@ local function init()
player.entity = spawn_player()
end
-- game logic here, should be called in a bevy system using call_fn
-- application logic here, should be called in a bevy system using call_fn
local function update()
(...)
end
@ -45,7 +45,7 @@ local function teardown()
despawn(player.entity)
end
-- call init to start the game, this will be called on each file-watcher script
-- call init to start the application, this will be called on each file-watcher script
-- reload
init()
```
@ -53,6 +53,9 @@ init()
The function calls can be implemented on Rust side the following way:
```rust
# extern crate bevy;
# extern crate bevy_scriptum;
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;
@ -92,13 +95,14 @@ fn teardown(
}
}
}
fn main() {}
```
And to tie this all together we do the following:
```rust
```rust,no_run
# extern crate bevy;
# extern crate bevy_scriptum;
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;
@ -116,24 +120,25 @@ fn main() {
.run();
}
fn init() {} // Implemented elsewhere
fn update() {} // Implemented elsewhere
fn despawn() {} // Implemented elsewhere
fn teardown() {} // Implemented elsewhere
fn spawn_player() {} // Implemented elsewhere
# fn init() {}
# fn update() {}
# fn despawn() {}
# fn teardown() {}
# fn spawn_player() {}
```
`despawn` can be implemented as:
```rust
# extern crate bevy;
# extern crate bevy_scriptum;
use bevy::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;
fn despawn(In((entity,)): In<(BevyEntity,)>, mut commands: Commands) {
commands.entity(entity.0).despawn();
}
fn main() {} // Implemented elsewhere
```
Implementation of `spawn_player` has been left out as an exercise for the reader.

8
build.rs Normal file
View file

@ -0,0 +1,8 @@
fn main() {
#[cfg(feature = "ruby")]
{
println!("cargo:rustc-link-arg=-rdynamic");
println!("cargo:rustc-link-arg=-lz");
println!("cargo:rustc-link-lib=z");
}
}

View file

@ -4,7 +4,7 @@ use bevy_scriptum::runtimes::lua::prelude::*;
fn main() {
App::new()
// This is just needed for headless console app, not needed for a regular bevy game
// 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 {

View file

@ -4,12 +4,14 @@ use bevy_scriptum::runtimes::rhai::prelude::*;
fn main() {
App::new()
// This is just needed for headless console app, not needed for a regular bevy game
// 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;
.set_runner(move |mut app: App| {
loop {
app.update();
if let Some(exit) = app.should_exit() {
return exit;
}
}
})
.add_plugins(DefaultPlugins)

View file

@ -0,0 +1,33 @@
use bevy::{app::AppExit, prelude::*};
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::ruby::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, startup)
.add_systems(Update, call_ruby_on_update_from_rust)
.add_scripting::<RubyRuntime>(|runtime| {
runtime.add_function(String::from("quit"), |mut exit: EventWriter<AppExit>| {
exit.write(AppExit::Success);
});
})
.run();
}
fn startup(mut commands: Commands, assets_server: Res<AssetServer>) {
commands.spawn(Script::<RubyScript>::new(
assets_server.load("examples/ruby/call_function_from_rust.rb"),
));
}
fn call_ruby_on_update_from_rust(
mut scripted_entities: Query<(Entity, &mut RubyScriptData)>,
scripting_runtime: ResMut<RubyRuntime>,
) {
for (entity, mut script_data) in &mut scripted_entities {
scripting_runtime
.call_fn("on_update", &mut script_data, entity, ())
.unwrap();
}
}

View file

@ -0,0 +1,25 @@
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::ruby::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<RubyRuntime>(|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::<RubyScript>::new(assets_server.load("examples/ruby/current_entity.rb")),
));
}

View file

@ -0,0 +1,54 @@
use bevy::prelude::*;
use bevy_scriptum::ScriptingError;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::ruby::magnus;
use bevy_scriptum::runtimes::ruby::magnus::Module as _;
use bevy_scriptum::runtimes::ruby::magnus::Object as _;
use bevy_scriptum::runtimes::ruby::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<RubyRuntime>(|runtime| {
runtime.add_function(String::from("hello_bevy"), || {
println!("hello bevy, called from script");
});
})
.add_systems(Startup, startup)
.run();
}
#[magnus::wrap(class = "MyType")]
struct MyType {
my_field: u32,
}
impl MyType {
fn new() -> Self {
Self { my_field: 42 }
}
fn my_method(&self) -> u32 {
self.my_field
}
}
fn startup(
mut commands: Commands,
scripting_runtime: ResMut<RubyRuntime>,
assets_server: Res<AssetServer>,
) {
scripting_runtime
.with_engine_send(|ruby| {
let my_type = ruby.define_class("MyType", ruby.class_object())?;
my_type.define_singleton_method("new", magnus::function!(MyType::new, 0))?;
my_type.define_method("my_method", magnus::method!(MyType::my_method, 0))?;
Ok::<(), ScriptingError>(())
})
.unwrap();
commands.spawn(Script::<RubyScript>::new(
assets_server.load("examples/ruby/custom_type.rb"),
));
}

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

@ -0,0 +1,33 @@
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::ruby::prelude::*;
#[derive(Component)]
struct Player;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<RubyRuntime>(|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::<RubyScript>::new(
assets_server.load("examples/ruby/ecs.rb"),
));
}

View file

@ -0,0 +1,17 @@
use bevy::prelude::*;
use bevy_scriptum::runtimes::ruby::prelude::*;
use bevy_scriptum::{prelude::*, BuildScriptingRuntime};
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<RubyRuntime>(|_| {})
.add_systems(Startup, startup)
.run();
}
fn startup(mut commands: Commands, assets_server: Res<AssetServer>) {
commands.spawn(Script::<RubyScript>::new(
assets_server.load("examples/ruby/entity_variable.rb"),
));
}

View file

@ -0,0 +1,52 @@
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::ruby::{RArray, prelude::*};
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<RubyRuntime>(|runtime| {
runtime
.add_function(String::from("fun_without_params"), || {
println!("called without params");
})
.add_function(
String::from("fun_with_string_param"),
|In((x,)): In<(String,)>| {
println!("called with string: '{}'", x);
},
)
.add_function(
String::from("fun_with_i64_param"),
|In((x,)): In<(i64,)>| {
println!("called with i64: {}", x);
},
)
.add_function(
String::from("fun_with_multiple_params"),
|In((x, y)): In<(i64, String)>| {
println!("called with i64: {} and string: '{}'", x, y);
},
)
.add_function(
String::from("fun_with_i64_and_array_param"),
|In((x, y)): In<(i64, RArray)>, runtime: Res<RubyRuntime>| {
runtime.with_engine_send(move |ruby| {
println!(
"called with i64: {} and dynamically typed array: {:?}",
x,
ruby.get_inner(y.0)
);
});
},
);
})
.add_systems(Startup, startup)
.run();
}
fn startup(mut commands: Commands, assets_server: Res<AssetServer>) {
commands.spawn(Script::<RubyScript>::new(
assets_server.load("examples/ruby/function_params.rb"),
));
}

View file

@ -0,0 +1,42 @@
use bevy::{app::AppExit, prelude::*};
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::ruby::prelude::*;
use magnus::TryConvert;
use magnus::value::InnerValue;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, startup)
.add_systems(Update, call_lua_on_update_from_rust)
.add_scripting::<RubyRuntime>(|runtime| {
runtime.add_function(String::from("quit"), |mut exit: EventWriter<AppExit>| {
exit.write(AppExit::Success);
});
})
.run();
}
fn startup(mut commands: Commands, assets_server: Res<AssetServer>) {
commands.spawn(Script::<RubyScript>::new(
assets_server.load("examples/ruby/function_return_value.rb"),
));
}
fn call_lua_on_update_from_rust(
mut scripted_entities: Query<(Entity, &mut RubyScriptData)>,
scripting_runtime: ResMut<RubyRuntime>,
mut exit: EventWriter<AppExit>,
) {
for (entity, mut script_data) in &mut scripted_entities {
let val = scripting_runtime
.call_fn("get_value", &mut script_data, entity, ())
.unwrap()
.0;
scripting_runtime.with_engine(|ruby| {
let val: i32 = TryConvert::try_convert(val.get_inner_with(ruby)).unwrap();
println!("script returned: {}", val);
});
exit.write(AppExit::Success);
}
}

View file

@ -0,0 +1,21 @@
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::ruby::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<RubyRuntime>(|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::<RubyScript>::new(
assets_server.load("examples/ruby/hello_world.rb"),
));
}

View file

@ -0,0 +1,67 @@
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::ruby::prelude::*;
// Plugin A
struct PluginA;
impl Plugin for PluginA {
fn build(&self, app: &mut App) {
app.add_scripting_api::<RubyRuntime>(|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::<RubyScript>::new(
assets_server.load("examples/ruby/multiple_plugins_plugin_a.rb"),
));
}
// Plugin B
struct PluginB;
impl Plugin for PluginB {
fn build(&self, app: &mut App) {
app.add_scripting_api::<RubyRuntime>(|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::<RubyScript>::new(
assets_server.load("examples/lua/multiple_plugins_plugin_b.lua"),
));
}
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::<RubyRuntime>(|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::<RubyScript>::new(
assets_server.load("examples/ruby/hello_world.rb"),
));
}

31
examples/ruby/promises.rs Normal file
View file

@ -0,0 +1,31 @@
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::ruby::prelude::*;
#[derive(Component)]
struct Player;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<RubyRuntime>(|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::<RubyScript>::new(
assets_server.load("examples/ruby/promises.rb"),
));
}

View file

@ -0,0 +1,43 @@
use bevy::{app::AppExit, prelude::*};
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::ruby::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::<RubyRuntime>(|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::<RubyScript>::new(
assets_server.load("examples/ruby/side_effects.rb"),
),));
}
fn print_entity_names_and_quit(query: Query<&Name>, mut exit: EventWriter<AppExit>) {
if !query.is_empty() {
for e in &query {
println!("{}", e);
}
exit.write(AppExit::Success);
}
}

View file

@ -2,7 +2,7 @@ use bevy::prelude::*;
use core::any::TypeId;
use std::sync::{Arc, Mutex};
use crate::{promise::Promise, Runtime};
use crate::{Runtime, promise::Promise};
/// A system that can be used to call a script function.
pub struct CallbackSystem<R: Runtime> {
@ -65,7 +65,7 @@ where
fn into_callback_system(self, world: &mut World) -> CallbackSystem<R>;
}
impl<R: Runtime, Out, FN, Marker> IntoCallbackSystem<R, (), Out, Marker> for FN
impl<R: Runtime, Out: Send + 'static, FN, Marker> IntoCallbackSystem<R, (), Out, Marker> for FN
where
FN: IntoSystem<(), Out, Marker>,
Out: for<'a> IntoRuntimeValueWithEngine<'a, Out, R>,
@ -77,8 +77,10 @@ where
let result = inner_system.run((), world);
inner_system.apply_deferred(world);
let mut runtime = world.get_resource_mut::<R>().expect("No runtime resource");
runtime
.with_engine_mut(move |engine| Out::into_runtime_value_with_engine(result, engine))
runtime.with_engine_send_mut(move |engine| {
Out::into_runtime_value_with_engine(result, engine)
})
};
let system = IntoSystem::into_system(system_fn);
CallbackSystem {
@ -90,27 +92,31 @@ where
macro_rules! impl_tuple {
($($idx:tt $t:tt),+) => {
impl<RN: Runtime, $($t,)+ Out, FN, Marker> IntoCallbackSystem<RN, In<($($t,)+)>, Out, Marker>
impl<RN: Runtime, $($t,)+ Out: Send + 'static, FN, Marker> IntoCallbackSystem<RN, In<($($t,)+)>, Out, Marker>
for FN
where
FN: IntoSystem<In<($($t,)+)>, Out, Marker>,
Out: for<'a> IntoRuntimeValueWithEngine<'a, Out, RN>,
$($t: 'static + for<'a> FromRuntimeValueWithEngine<'a, RN>,)+
$($t: Send + 'static + for<'a> FromRuntimeValueWithEngine<'a, RN>,)+
{
fn into_callback_system(self, world: &mut World) -> CallbackSystem<RN> {
let mut inner_system = IntoSystem::into_system(self);
inner_system.initialize(world);
let system_fn = move |args: In<Vec<RN::Value>>, world: &mut World| {
let mut runtime = world.get_resource_mut::<RN>().expect("No runtime resource");
let args = runtime.with_engine_mut(move |engine| {
(
$($t::from_runtime_value_with_engine(args.get($idx).expect(&format!("Failed to get function argument for index {}", $idx)).clone(), engine), )+
)
});
let args =
runtime.with_engine_send_mut(move |engine| {
(
$($t::from_runtime_value_with_engine(args.get($idx).expect(&format!("Failed to get function argument for index {}", $idx)).clone(), engine), )+
)
});
let result = inner_system.run(args, world);
inner_system.apply_deferred(world);
let mut runtime = world.get_resource_mut::<RN>().expect("No runtime resource");
runtime.with_engine_mut(move |engine| {
runtime.with_engine_send_mut(move |engine| {
Out::into_runtime_value_with_engine(result, engine)
})
};

View file

@ -1,12 +1,18 @@
//! ![demo](demo.gif)
//!
//! bevy_scriptum is a a plugin for [Bevy](https://bevyengine.org/) that allows you to write some of your game logic in a scripting language.
//! Currently [Rhai](https://rhai.rs/) and [Lua](https://lua.org/) are supported, but more languages may be added in the future.
//! bevy_scriptum is a a plugin for [Bevy](https://bevyengine.org/) that allows you to write some of your game or application logic in a scripting language.
//! ## Supported scripting languages/runtimes
//!
//! Everything you need to know to get started with using this library is contained in the
//! [bevy_scriptum book](https://jarkonik.github.io/bevy_scriptum/)
//! | 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 | `ruby` | [link](https://jarkonik.github.io/bevy_scriptum/ruby/ruby.html) |
//!
//! API docs are available in [docs.rs](https://docs.rs/bevy_scriptum/latest/bevy_scriptum/)
//! Documentation book is available [here](https://jarkonik.github.io/bevy_scriptum/) 📖
//!
//! Full API docs are available at [docs.rs](https://docs.rs/bevy_scriptum/latest/bevy_scriptum/) 🧑‍💻
//!
//! bevy_scriptum's main advantages include:
//! - low-boilerplate
@ -15,14 +21,16 @@
//! - flexibility
//! - hot-reloading
//!
//! Scripts are separate files that can be hot-reloaded at runtime. This allows you to quickly iterate on your game logic without having to recompile your game.
//! Scripts are separate files that can be hot-reloaded at runtime. This allows you to quickly iterate on your game or application logic without having to recompile it.
//!
//! All you need to do is register callbacks on your Bevy app like this:
//! ```no_run
//! use bevy::prelude::*;
//! use bevy_scriptum::prelude::*;
//! # #[cfg(feature = "lua")]
//! use bevy_scriptum::runtimes::lua::prelude::*;
//!
//! # #[cfg(feature = "lua")]
//! App::new()
//! .add_plugins(DefaultPlugins)
//! .add_scripting::<LuaRuntime>(|runtime| {
@ -42,11 +50,13 @@
//! ```no_run
//! use bevy::prelude::*;
//! use bevy_scriptum::prelude::*;
//! # #[cfg(feature = "lua")]
//! use bevy_scriptum::runtimes::lua::prelude::*;
//!
//! #[derive(Component)]
//! struct Player;
//!
//! # #[cfg(feature = "lua")]
//! App::new()
//! .add_plugins(DefaultPlugins)
//! .add_scripting::<LuaRuntime>(|runtime| {
@ -66,8 +76,10 @@
//! ```no_run
//! use bevy::prelude::*;
//! use bevy_scriptum::prelude::*;
//! # #[cfg(feature = "lua")]
//! use bevy_scriptum::runtimes::lua::prelude::*;
//!
//! # #[cfg(feature = "lua")]
//! App::new()
//! .add_plugins(DefaultPlugins)
//! .add_scripting::<LuaRuntime>(|runtime| {
@ -84,33 +96,6 @@
//! ```lua
//! fun_with_string_param("Hello world!")
//! ```
//! It is also possible to split the definition of your callback functions up over multiple plugins. This enables you to split up your code by subject and keep the main initialization light and clean.
//! This can be accomplished by using `add_scripting_api`. Be careful though, `add_scripting` has to be called before adding plugins.
//! ```no_run
//! use bevy::prelude::*;
//! use bevy_scriptum::prelude::*;
//! use bevy_scriptum::runtimes::lua::prelude::*;
//!
//! struct MyPlugin;
//! impl Plugin for MyPlugin {
//! fn build(&self, app: &mut App) {
//! app.add_scripting_api::<LuaRuntime>(|runtime| {
//! runtime.add_function(String::from("hello_from_my_plugin"), || {
//! info!("Hello from MyPlugin");
//! });
//! });
//! }
//! }
//!
//! App::new()
//! .add_plugins(DefaultPlugins)
//! .add_scripting::<LuaRuntime>(|_| {
//! // nice and clean
//! })
//! .add_plugins(MyPlugin)
//! .run();
//! ```
//!
//!
//! ## Usage
//!
@ -128,8 +113,10 @@
//! ```no_run
//! use bevy::prelude::*;
//! use bevy_scriptum::prelude::*;
//! # #[cfg(feature = "lua")]
//! use bevy_scriptum::runtimes::lua::prelude::*;
//!
//! # #[cfg(feature = "lua")]
//! App::new()
//! .add_plugins(DefaultPlugins)
//! .add_scripting::<LuaRuntime>(|runtime| {
@ -154,8 +141,10 @@
//! ```no_run
//! use bevy::prelude::*;
//! use bevy_scriptum::prelude::*;
//! # #[cfg(feature = "lua")]
//! use bevy_scriptum::runtimes::lua::prelude::*;
//!
//! # #[cfg(feature = "lua")]
//! App::new()
//! .add_plugins(DefaultPlugins)
//! .add_scripting::<LuaRuntime>(|runtime| {
@ -209,8 +198,10 @@
//! ```
//! use bevy::prelude::*;
//! use bevy_scriptum::prelude::*;
//! # #[cfg(feature = "lua")]
//! use bevy_scriptum::runtimes::lua::prelude::*;
//!
//! # #[cfg(feature = "lua")]
//! App::new()
//! .add_plugins(DefaultPlugins)
//! .add_scripting::<LuaRuntime>(|runtime| {
@ -259,7 +250,11 @@ use std::{
sync::{Arc, Mutex},
};
use bevy::{app::MainScheduleOrder, ecs::{component::Mutable, schedule::ScheduleLabel}, prelude::*};
use bevy::{
app::MainScheduleOrder,
ecs::{component::Mutable, schedule::ScheduleLabel},
prelude::*,
};
use callback::{Callback, IntoCallbackSystem};
use systems::{init_callbacks, log_errors, process_calls};
use thiserror::Error;
@ -275,10 +270,10 @@ const ENTITY_VAR_NAME: &str = "entity";
/// An error that can occur when internal [ScriptingPlugin] systems are being executed
#[derive(Error, Debug)]
pub enum ScriptingError {
#[error("script runtime error: {0}")]
RuntimeError(Box<dyn std::error::Error>),
#[error("script compilation error: {0}")]
CompileError(Box<dyn std::error::Error>),
#[error("script runtime error:\n{0}")]
RuntimeError(String),
#[error("script compilation error:\n{0}")]
CompileError(Box<dyn std::error::Error + Send>),
#[error("no runtime resource present")]
NoRuntimeResource,
#[error("no settings resource present")]
@ -299,11 +294,39 @@ pub trait Runtime: Resource + Default {
/// Provides mutable reference to raw scripting engine instance.
/// Can be used to directly interact with an interpreter to use interfaces
/// that bevy_scriptum does not provided adapters for.
/// Using this function make the closure be executed on another thread for
/// some runtimes. If you need to operate on non-`'static` borrows and/or
/// `!Send` data, you can use `with_engine_mut` - it may not be implemented
/// for some of the runtimes though.
fn with_engine_send_mut<T: Send + 'static>(
&mut self,
f: impl FnOnce(&mut Self::RawEngine) -> T + Send + 'static,
) -> T;
/// Provides immutable reference to raw scripting engine instance.
/// Can be used to directly interact with an interpreter to use interfaces
/// that bevy_scriptum does not provided adapters for.
/// Using this function make the closure be executed on another thread for
/// some runtimes. If you need to operate on non-`'static` borrows and/or
/// `!Send` data, you can use `with_engine` - it may not be implemented
/// for some of the runtimes though.
fn with_engine_send<T: Send + 'static>(
&self,
f: impl FnOnce(&Self::RawEngine) -> T + Send + 'static,
) -> T;
/// Provides mutable reference to raw scripting engine instance.
/// Can be used to directly interact with an interpreter to use interfaces
/// that bevy_scriptum does not provided adapters for.
/// May not be implemented for runtimes which require the closure to pass
/// thread boundary - use `with_engine_send_mut` then.
fn with_engine_mut<T>(&mut self, f: impl FnOnce(&mut Self::RawEngine) -> T) -> T;
/// Provides immutable reference to raw scripting engine instance.
/// Can be used to directly interact with an interpreter to use interfaces
/// that bevy_scriptum does not provided adapters for.
/// May not be implemented for runtimes which require the closure to pass
/// thread boundary - use `with_engine_send` then.
fn with_engine<T>(&self, f: impl FnOnce(&Self::RawEngine) -> T) -> T;
fn eval(
@ -320,12 +343,12 @@ pub trait Runtime: Resource + Default {
name: String,
arg_types: Vec<TypeId>,
f: impl Fn(
Self::CallContext,
Vec<Self::Value>,
) -> Result<Promise<Self::CallContext, Self::Value>, ScriptingError>
+ Send
+ Sync
+ 'static,
Self::CallContext,
Vec<Self::Value>,
) -> Result<Promise<Self::CallContext, Self::Value>, ScriptingError>
+ Send
+ Sync
+ 'static,
) -> Result<(), ScriptingError>;
/// Calls a function by name defined within the runtime in the context of the
@ -336,7 +359,7 @@ pub trait Runtime: Resource + Default {
name: &str,
script_data: &mut Self::ScriptData,
entity: Entity,
args: impl for<'a> FuncArgs<'a, Self::Value, Self>,
args: impl for<'a> FuncArgs<'a, Self::Value, Self> + Send + 'static,
) -> Result<Self::Value, ScriptingError>;
/// Calls a function by value defined within the runtime in the context of the
@ -348,6 +371,10 @@ pub trait Runtime: Resource + Default {
context: &Self::CallContext,
args: Vec<Self::Value>,
) -> Result<Self::Value, ScriptingError>;
fn needs_rdynamic_linking() -> bool {
false
}
}
pub trait FuncArgs<'a, V, R: Runtime> {
@ -410,6 +437,17 @@ impl BuildScriptingRuntime for App {
/// Adds a scripting runtime. Registers required bevy systems that take
/// care of processing and running the scripts.
fn add_scripting<R: Runtime>(&mut self, f: impl Fn(ScriptingRuntimeBuilder<R>)) -> &mut Self {
#[cfg(debug_assertions)]
if R::needs_rdynamic_linking() && !is_rdynamic_linking() {
panic!(
"Missing `-rdynamic`: symbol resolution failed.\n\
It is needed by {:?}.\n\
Please add `println!(\"cargo:rustc-link-arg=-rdynamic\");` to your build.rs\n\
or set `RUSTFLAGS=\"-C link-arg=-rdynamic\"`.",
std::any::type_name::<R>()
);
}
self.world_mut()
.resource_mut::<MainScheduleOrder>()
.insert_after(Update, R::Schedule::default());
@ -472,6 +510,20 @@ impl<R: Runtime> Default for Callbacks<R> {
}
}
#[cfg(debug_assertions)]
pub extern "C" fn is_rdynamic_linking() -> bool {
unsafe {
// Get a function pointer to itself
let addr = is_rdynamic_linking as *const libc::c_void;
let mut info: libc::Dl_info = std::mem::zeroed();
// Try to resolve symbol info
let result = libc::dladdr(addr, &mut info);
result != 0 && !info.dli_sname.is_null()
}
}
pub mod prelude {
pub use crate::{BuildScriptingRuntime as _, Runtime as _, Script};
}

View file

@ -58,7 +58,7 @@ 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"))]
#[cfg(any(feature = "rhai", feature = "lua", feature = "ruby"))]
pub(crate) fn then(&mut self, callback: V) -> Self {
let mut inner = self
.inner

View file

@ -12,10 +12,10 @@ use serde::Deserialize;
use std::sync::{Arc, Mutex};
use crate::{
ENTITY_VAR_NAME, FuncArgs, Runtime, ScriptingError,
assets::GetExtensions,
callback::{FromRuntimeValueWithEngine, IntoRuntimeValueWithEngine},
promise::Promise,
FuncArgs, Runtime, ScriptingError, ENTITY_VAR_NAME,
};
type LuaEngine = Arc<Mutex<Lua>>;
@ -41,6 +41,12 @@ pub struct LuaRuntime {
#[derive(Debug, Clone, Copy)]
pub struct BevyEntity(pub Entity);
impl BevyEntity {
pub fn index(&self) -> u32 {
self.0.index()
}
}
impl UserData for BevyEntity {}
impl FromLua<'_> for BevyEntity {
@ -58,6 +64,24 @@ impl FromLua<'_> for BevyEntity {
#[derive(Debug, Clone, Copy)]
pub struct BevyVec3(pub Vec3);
impl BevyVec3 {
pub fn new(x: f32, y: f32, z: f32) -> Self {
BevyVec3(Vec3 { 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 UserData for BevyVec3 {}
impl FromLua<'_> for BevyVec3 {
@ -163,7 +187,7 @@ impl Runtime for LuaRuntime {
.expect("Error clearing entity global variable");
result
})
.map_err(|e| ScriptingError::RuntimeError(Box::new(e)))?;
.map_err(|e| ScriptingError::RuntimeError(e.to_string()))?;
Ok(LuaScriptData)
}
@ -172,14 +196,14 @@ impl Runtime for LuaRuntime {
name: String,
_arg_types: Vec<std::any::TypeId>,
f: impl Fn(
Self::CallContext,
Vec<Self::Value>,
) -> Result<
crate::promise::Promise<Self::CallContext, Self::Value>,
crate::ScriptingError,
> + Send
+ Sync
+ 'static,
Self::CallContext,
Vec<Self::Value>,
) -> Result<
crate::promise::Promise<Self::CallContext, Self::Value>,
crate::ScriptingError,
> + Send
+ Sync
+ 'static,
) -> Result<(), crate::ScriptingError> {
self.with_engine(|engine| {
let func = engine
@ -212,14 +236,14 @@ impl Runtime for LuaRuntime {
let func = engine
.globals()
.get::<_, Function>(name)
.map_err(|e| ScriptingError::RuntimeError(Box::new(e)))?;
.map_err(|e| ScriptingError::RuntimeError(e.to_string()))?;
let args = args
.parse(engine)
.into_iter()
.map(|a| engine.registry_value::<mlua::Value>(&a.0).unwrap());
let result = func
.call::<_, mlua::Value>(Variadic::from_iter(args))
.map_err(|e| ScriptingError::RuntimeError(Box::new(e)))?;
.map_err(|e| ScriptingError::RuntimeError(e.to_string()))?;
engine
.globals()
.set(ENTITY_VAR_NAME, mlua::Value::Nil)
@ -237,13 +261,13 @@ impl Runtime for LuaRuntime {
self.with_engine(|engine| {
let val = engine
.registry_value::<Function>(&value.0)
.map_err(|e| ScriptingError::RuntimeError(Box::new(e)))?;
.map_err(|e| ScriptingError::RuntimeError(e.to_string()))?;
let args = args
.into_iter()
.map(|a| engine.registry_value::<mlua::Value>(&a.0).unwrap());
let result = val
.call::<_, mlua::Value>(Variadic::from_iter(args))
.map_err(|e| ScriptingError::RuntimeError(Box::new(e)))?;
.map_err(|e| ScriptingError::RuntimeError(e.to_string()))?;
Ok(LuaValue::new(engine, result))
})
}
@ -257,6 +281,20 @@ impl Runtime for LuaRuntime {
let engine = self.engine.lock().unwrap();
f(&engine)
}
fn with_engine_send_mut<T: Send + 'static>(
&mut self,
f: impl FnOnce(&mut Self::RawEngine) -> T + Send + 'static,
) -> T {
self.with_engine_mut(f)
}
fn with_engine_send<T: Send + 'static>(
&self,
f: impl FnOnce(&Self::RawEngine) -> T + Send + 'static,
) -> T {
self.with_engine(f)
}
}
impl<'a, T: IntoLuaMulti<'a>> IntoRuntimeValueWithEngine<'a, T, LuaRuntime> for T {

View file

@ -2,3 +2,5 @@
pub mod lua;
#[cfg(feature = "rhai")]
pub mod rhai;
#[cfg(feature = "ruby")]
pub mod ruby;

View file

@ -10,10 +10,10 @@ use rhai::{CallFnOptions, Dynamic, Engine, FnPtr, Scope, Variant};
use serde::Deserialize;
use crate::{
ENTITY_VAR_NAME, FuncArgs, Runtime, ScriptingError,
assets::GetExtensions,
callback::{FromRuntimeValueWithEngine, IntoRuntimeValueWithEngine},
promise::Promise,
FuncArgs, Runtime, ScriptingError, ENTITY_VAR_NAME,
};
#[derive(Asset, Debug, Deserialize, TypePath)]
@ -50,6 +50,36 @@ pub struct RhaiScriptData {
#[derive(Debug, Clone)]
pub struct RhaiValue(pub rhai::Dynamic);
#[derive(Clone)]
pub struct BevyEntity(pub Entity);
impl BevyEntity {
pub fn index(&self) -> u32 {
self.0.index()
}
}
#[derive(Clone)]
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 RhaiRuntime {
type Schedule = RhaiSchedule;
type ScriptAsset = RhaiScript;
@ -65,7 +95,7 @@ impl Runtime for RhaiRuntime {
entity: Entity,
) -> Result<Self::ScriptData, ScriptingError> {
let mut scope = Scope::new();
scope.push(ENTITY_VAR_NAME, entity);
scope.push(ENTITY_VAR_NAME, BevyEntity(entity));
let engine = &self.engine;
@ -75,9 +105,9 @@ impl Runtime for RhaiRuntime {
engine
.run_ast_with_scope(&mut scope, &ast)
.map_err(|e| ScriptingError::RuntimeError(Box::new(e)))?;
.map_err(|e| ScriptingError::RuntimeError(e.to_string()))?;
scope.remove::<Entity>(ENTITY_VAR_NAME).unwrap();
scope.remove::<BevyEntity>(ENTITY_VAR_NAME).unwrap();
Ok(Self::ScriptData { ast, scope })
}
@ -87,12 +117,12 @@ impl Runtime for RhaiRuntime {
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,
Self::CallContext,
Vec<Self::Value>,
) -> Result<Promise<Self::CallContext, Self::Value>, ScriptingError>
+ Send
+ Sync
+ 'static,
) -> Result<(), ScriptingError> {
self.engine
.register_raw_fn(name, arg_types, move |context, args| {
@ -113,7 +143,7 @@ impl Runtime for RhaiRuntime {
) -> Result<RhaiValue, ScriptingError> {
let ast = script_data.ast.clone();
let scope = &mut script_data.scope;
scope.push(ENTITY_VAR_NAME, entity);
scope.push(ENTITY_VAR_NAME, BevyEntity(entity));
let options = CallFnOptions::new().eval_ast(false);
let args = args
.parse(&self.engine)
@ -123,10 +153,10 @@ impl Runtime for RhaiRuntime {
let result = self
.engine
.call_fn_with_options::<Dynamic>(options, scope, &ast, name, args);
scope.remove::<Entity>(ENTITY_VAR_NAME).unwrap();
scope.remove::<BevyEntity>(ENTITY_VAR_NAME).unwrap();
match result {
Ok(val) => Ok(RhaiValue(val)),
Err(e) => Err(ScriptingError::RuntimeError(Box::new(e))),
Err(e) => Err(ScriptingError::RuntimeError(e.to_string())),
}
}
@ -143,11 +173,11 @@ impl Runtime for RhaiRuntime {
let result = if args.len() == 1 && args.first().unwrap().0.is_unit() {
f.call_raw(ctx, None, [])
.map_err(|e| ScriptingError::RuntimeError(e))?
.map_err(|e| ScriptingError::RuntimeError(e.to_string()))?
} else {
let args = args.into_iter().map(|a| a.0).collect::<Vec<Dynamic>>();
f.call_raw(ctx, None, args)
.map_err(|e| ScriptingError::RuntimeError(e))?
.map_err(|e| ScriptingError::RuntimeError(e.to_string()))?
};
Ok(RhaiValue(result))
@ -160,6 +190,20 @@ impl Runtime for RhaiRuntime {
fn with_engine<T>(&self, f: impl FnOnce(&Self::RawEngine) -> T) -> T {
f(&self.engine)
}
fn with_engine_send_mut<T: Send + 'static>(
&mut self,
f: impl FnOnce(&mut Self::RawEngine) -> T + Send + 'static,
) -> T {
self.with_engine_mut(f)
}
fn with_engine_send<T: Send + 'static>(
&self,
f: impl FnOnce(&Self::RawEngine) -> T + Send + 'static,
) -> T {
self.with_engine(f)
}
}
impl Default for RhaiRuntime {
@ -167,8 +211,8 @@ impl Default for RhaiRuntime {
let mut engine = Engine::new();
engine
.register_type_with_name::<Entity>("Entity")
.register_get("index", |entity: &mut Entity| entity.index());
.register_type_with_name::<BevyEntity>("Entity")
.register_get("index", |entity: &mut BevyEntity| entity.index());
#[allow(deprecated)]
engine
.register_type_with_name::<Promise<rhai::NativeCallContextStore, RhaiValue>>("Promise")
@ -181,13 +225,13 @@ impl Default for RhaiRuntime {
);
engine
.register_type_with_name::<Vec3>("Vec3")
.register_type_with_name::<BevyVec3>("Vec3")
.register_fn("new_vec3", |x: f64, y: f64, z: f64| {
Vec3::new(x as f32, y as f32, z as f32)
BevyVec3(Vec3::new(x as f32, y as f32, z as f32))
})
.register_get("x", |vec: &mut Vec3| vec.x as f64)
.register_get("y", |vec: &mut Vec3| vec.y as f64)
.register_get("z", |vec: &mut Vec3| vec.z as f64);
.register_get("x", |vec: &mut BevyVec3| vec.x() as f64)
.register_get("y", |vec: &mut BevyVec3| vec.y() as f64)
.register_get("z", |vec: &mut BevyVec3| vec.z() as f64);
#[allow(deprecated)]
engine.on_def_var(|_, info, _| Ok(info.name != "entity"));
@ -221,7 +265,7 @@ impl<T: Clone + 'static> FromRuntimeValueWithEngine<'_, RhaiRuntime> for T {
}
pub mod prelude {
pub use super::{RhaiRuntime, RhaiScript, RhaiScriptData};
pub use super::{BevyEntity, BevyVec3, RhaiRuntime, RhaiScript, RhaiScriptData};
}
macro_rules! impl_tuple {

599
src/runtimes/ruby.rs Normal file
View file

@ -0,0 +1,599 @@
use std::{
collections::HashMap,
ffi::CString,
sync::{Arc, Condvar, LazyLock, Mutex},
thread::{self, JoinHandle},
};
use ::magnus::{typed_data::Inspect, value::Opaque};
use bevy::{
asset::Asset,
ecs::{component::Component, entity::Entity, resource::Resource, schedule::ScheduleLabel},
math::Vec3,
reflect::TypePath,
};
use magnus::{
DataType, DataTypeFunctions, IntoValue, Object, RClass, RModule, Ruby, TryConvert, TypedData,
block::Proc,
data_type_builder, function,
value::{Lazy, ReprValue},
};
use magnus::{method, prelude::*};
use rb_sys::{VALUE, ruby_init_stack};
use serde::Deserialize;
use crate::{
FuncArgs, Runtime, ScriptingError,
assets::GetExtensions,
callback::{FromRuntimeValueWithEngine, IntoRuntimeValueWithEngine},
promise::Promise,
};
#[derive(Resource)]
pub struct RubyRuntime {
ruby_thread: Option<RubyThread>,
}
#[derive(ScheduleLabel, Clone, PartialEq, Eq, Debug, Hash, Default)]
pub struct RubySchedule;
#[derive(Asset, Debug, Deserialize, TypePath)]
pub struct RubyScript(pub String);
#[derive(Component)]
pub struct RubyScriptData;
impl GetExtensions for RubyScript {
fn extensions() -> &'static [&'static str] {
&["rb"]
}
}
impl From<String> for RubyScript {
fn from(value: String) -> Self {
Self(value)
}
}
type RubyClosure = Box<dyn FnOnce(Ruby) + Send>;
struct RubyThread {
sender: crossbeam_channel::Sender<RubyClosure>,
handle: Option<JoinHandle<()>>,
}
static RUBY_THREAD: LazyLock<Arc<(Mutex<Option<RubyThread>>, Condvar)>> =
LazyLock::new(|| Arc::new((Mutex::new(Some(RubyThread::spawn())), Condvar::new())));
impl RubyThread {
fn build_ruby_process_argv() -> anyhow::Result<Vec<*mut i8>> {
Ok(vec![
CString::new("ruby")?.into_raw(),
CString::new("-e")?.into_raw(),
CString::new("")?.into_raw(),
])
}
fn spawn() -> Self {
let (sender, receiver) = crossbeam_channel::unbounded::<Box<dyn FnOnce(Ruby) + Send>>();
let handle = thread::spawn(move || {
unsafe {
let mut variable_in_this_stack_frame: VALUE = 0;
ruby_init_stack(&mut variable_in_this_stack_frame as *mut VALUE as *mut _);
rb_sys::ruby_init();
let mut argv =
Self::build_ruby_process_argv().expect("Failed to build ruby process args");
rb_sys::ruby_options(argv.len() as i32, argv.as_mut_ptr());
};
while let Ok(f) = receiver.recv() {
let ruby = Ruby::get().expect("Failed to get a handle to Ruby API");
f(ruby);
}
unsafe {
rb_sys::ruby_finalize();
}
});
RubyThread {
sender,
handle: Some(handle),
}
}
fn execute<T: Send + 'static>(&self, f: Box<dyn FnOnce(Ruby) -> T + Send>) -> T {
let (return_sender, return_receiver) = crossbeam_channel::bounded(0);
self.sender
.send(Box::new(move |ruby| {
return_sender
.send(f(ruby))
.expect("Failed to send callback return value");
}))
.expect("Faild to send execution unit to Ruby thread");
return_receiver
.recv()
.expect("Failed to receive callback return value")
}
}
impl Drop for RubyThread {
fn drop(&mut self) {
let handle = self.handle.take().expect("No Ruby thread to join");
handle.join().expect("Failed to join Ruby thread");
}
}
impl DataTypeFunctions for Promise<(), RubyValue> {}
unsafe impl TypedData for Promise<(), RubyValue> {
fn class(ruby: &Ruby) -> magnus::RClass {
static CLASS: Lazy<RClass> = Lazy::new(|ruby| {
let class = ruby
.define_module("Bevy")
.expect("Failed to define Bevy module")
.define_class("Promise", ruby.class_object())
.expect("Failed to define Bevy::Promise class in Ruby");
class.undef_default_alloc_func();
class
});
ruby.get_inner(&CLASS)
}
fn data_type() -> &'static magnus::DataType {
static DATA_TYPE: DataType =
data_type_builder!(Promise<(), RubyValue>, "Bevy::Promise").build();
&DATA_TYPE
}
}
impl TryConvert for Promise<(), RubyValue> {
fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {
let result: Result<&Self, _> = TryConvert::try_convert(val);
result.cloned()
}
}
fn then(r_self: magnus::Value) -> magnus::Value {
let promise: &Promise<(), RubyValue> =
TryConvert::try_convert(r_self).expect("Couldn't convert self to Promise");
let ruby =
Ruby::get().expect("Failed to get a handle to Ruby API when registering Promise callback");
promise
.clone()
.then(RubyValue::new(
if ruby.block_given() {
ruby.block_proc()
.expect("Failed to create Proc for Promise")
} else {
ruby.proc_new(|ruby, _, _| ruby.qnil().as_value())
}
.as_value(),
))
.into_value()
}
#[derive(Clone, Debug)]
#[magnus::wrap(class = "Bevy::Entity")]
pub struct BevyEntity(pub Entity);
impl BevyEntity {
pub fn index(&self) -> u32 {
self.0.index()
}
}
impl TryConvert for BevyEntity {
fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {
let result: Result<&Self, _> = TryConvert::try_convert(val);
result.cloned()
}
}
#[derive(Clone, Debug)]
#[magnus::wrap(class = "Bevy::Vec3")]
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 TryConvert for BevyVec3 {
fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {
let result: Result<&Self, _> = TryConvert::try_convert(val);
result.cloned()
}
}
impl From<magnus::Error> for ScriptingError {
fn from(value: magnus::Error) -> Self {
ScriptingError::RuntimeError(format!(
"{}\nbacktrace:\n{}\n",
value.inspect(),
value
.value()
.expect("No error value")
.funcall::<_, _, magnus::RArray>("backtrace", ())
.expect("Failed to get backtrace")
.to_vec::<String>()
.expect("Failed to convert backtrace to vec of strings")
.join("\n"),
))
}
}
impl Default for RubyRuntime {
fn default() -> Self {
let (lock, cvar) = &*Arc::clone(&RUBY_THREAD);
let mut ruby_thread = lock.lock().expect("Failed to acquire lock on Ruby thread");
while ruby_thread.is_none() {
ruby_thread = cvar
.wait(ruby_thread)
.expect("Failed to acquire lock on Ruby thread after waiting");
}
let ruby_thread = ruby_thread.take().expect("Ruby thread is not available");
cvar.notify_all();
ruby_thread
.execute(Box::new(|ruby| {
let module = ruby.define_module("Bevy")?;
let entity = module.define_class("Entity", ruby.class_object())?;
entity.class().define_method(
"current",
method!(
|r_self: RClass| { r_self.ivar_get::<_, BevyEntity>("_current") },
0
),
)?;
entity.define_method("index", method!(BevyEntity::index, 0))?;
let promise = module.define_class("Promise", ruby.class_object())?;
promise.define_method("and_then", magnus::method!(then, 0))?;
let vec3 = module.define_class("Vec3", ruby.class_object())?;
vec3.define_singleton_method("new", function!(BevyVec3::new, 3))?;
vec3.define_method("x", method!(BevyVec3::x, 0))?;
vec3.define_method("y", method!(BevyVec3::y, 0))?;
vec3.define_method("z", method!(BevyVec3::z, 0))?;
Ok::<(), ScriptingError>(())
}))
.expect("Failed to define builtin types");
Self {
ruby_thread: Some(ruby_thread),
}
}
}
impl Drop for RubyRuntime {
fn drop(&mut self) {
let (lock, cvar) = &*Arc::clone(&RUBY_THREAD);
let mut ruby_thread = lock
.lock()
.expect("Failed to lock ruby thread while dropping the runtime");
*ruby_thread = self.ruby_thread.take();
cvar.notify_all();
}
}
#[derive(Clone)]
pub struct RubyValue(pub magnus::value::Opaque<magnus::Value>);
impl RubyValue {
fn nil(ruby: &Ruby) -> Self {
Self::new(ruby.qnil().as_value())
}
fn new(value: magnus::Value) -> Self {
Self(magnus::value::Opaque::from(value))
}
}
impl RubyRuntime {
fn execute_in_thread<T: Send + 'static>(
&self,
f: impl FnOnce(&magnus::Ruby) -> T + Send + 'static,
) -> T {
self.ruby_thread
.as_ref()
.expect("No Ruby thread")
.execute(Box::new(move |ruby| f(&ruby)))
}
fn execute_in_thread_mut<T: Send + 'static>(
&self,
f: impl FnOnce(&mut magnus::Ruby) -> T + Send + 'static,
) -> T {
self.ruby_thread
.as_ref()
.expect("No Ruby thread")
.execute(Box::new(move |mut ruby| f(&mut ruby)))
}
fn with_current_entity<T>(ruby: &Ruby, entity: Entity, f: impl FnOnce() -> T) -> T {
let var = ruby
.class_object()
.const_get::<_, RModule>("Bevy")
.expect("Failed to get Bevy module")
.const_get::<_, RClass>("Entity")
.expect("Failed to get Entity class");
var.ivar_set("_current", BevyEntity(entity))
.expect("Failed to set current entity handle");
let result = f();
var.ivar_set("_current", ruby.qnil().as_value())
.expect("Failed to unset current entity handle");
result
}
}
impl Runtime for RubyRuntime {
type Schedule = RubySchedule;
type ScriptAsset = RubyScript;
type ScriptData = RubyScriptData;
type CallContext = ();
type Value = RubyValue;
type RawEngine = magnus::Ruby;
fn with_engine_send_mut<T: Send + 'static>(
&mut self,
f: impl FnOnce(&mut Self::RawEngine) -> T + Send + 'static,
) -> T {
self.execute_in_thread_mut(f)
}
fn with_engine_send<T: Send + 'static>(
&self,
f: impl FnOnce(&Self::RawEngine) -> T + Send + 'static,
) -> T {
self.execute_in_thread(f)
}
fn with_engine_mut<T>(&mut self, _f: impl FnOnce(&mut Self::RawEngine) -> T) -> T {
unimplemented!(
"Ruby runtime requires sending execution to another thread, use `with_engine_mut_send`"
);
}
fn with_engine<T>(&self, _f: impl FnOnce(&Self::RawEngine) -> T) -> T {
unimplemented!(
"Ruby runtime requires sending execution to another thread, use `with_engine_send`"
);
}
fn eval(
&self,
script: &Self::ScriptAsset,
entity: bevy::prelude::Entity,
) -> Result<Self::ScriptData, crate::ScriptingError> {
let script = script.0.clone();
self.execute_in_thread(Box::new(move |ruby: &Ruby| {
Self::with_current_entity(ruby, entity, || {
ruby.eval::<magnus::value::Value>(&script)
.map_err(<magnus::Error as Into<ScriptingError>>::into)
})?;
Ok::<Self::ScriptData, ScriptingError>(RubyScriptData)
}))
}
fn register_fn(
&mut self,
name: String,
_arg_types: Vec<std::any::TypeId>,
f: impl Fn(
Self::CallContext,
Vec<Self::Value>,
) -> Result<
crate::promise::Promise<Self::CallContext, Self::Value>,
crate::ScriptingError,
> + Send
+ Sync
+ 'static,
) -> Result<(), crate::ScriptingError> {
type CallbackClosure = Box<
dyn Fn(
(),
Vec<RubyValue>,
)
-> Result<crate::promise::Promise<(), RubyValue>, crate::ScriptingError>
+ Send,
>;
static RUBY_CALLBACKS: LazyLock<Mutex<HashMap<String, CallbackClosure>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
let mut callbacks = RUBY_CALLBACKS
.lock()
.expect("Failed to lock callbacks static when registering a callback");
callbacks.insert(name.clone(), Box::new(f));
fn callback(args: &[magnus::Value]) -> magnus::Value {
let ruby = magnus::Ruby::get()
.expect("Failed to get a handle to Ruby API while processing callback");
let method_name: magnus::value::StaticSymbol = ruby
.class_object()
.funcall("__method__", ())
.expect("Failed to get currently called method name symbol from Ruby");
let method_name = method_name
.name()
.expect("Failed to convert method symbol to string");
let callbacks = RUBY_CALLBACKS
.lock()
.expect("Failed to lock callbacks when executing a script callback");
let f = callbacks
.get(method_name)
.expect("No callback found to execute with specified name");
let result = f(
(),
args.iter()
.map(|arg| RubyValue::new(arg.into_value()))
.collect(),
)
.expect("failed to call callback");
result.into_value()
}
self.execute_in_thread(Box::new(move |ruby: &Ruby| {
ruby.define_global_function(&name, function!(callback, -1));
RubyValue::nil(ruby)
}));
Ok(())
}
fn call_fn(
&self,
name: &str,
_script_data: &mut Self::ScriptData,
entity: bevy::prelude::Entity,
args: impl for<'a> crate::FuncArgs<'a, Self::Value, Self> + Send + 'static,
) -> Result<Self::Value, crate::ScriptingError> {
let name = name.to_string();
self.execute_in_thread(Box::new(move |ruby: &Ruby| {
let return_value = Self::with_current_entity(ruby, entity, || {
let args: Vec<_> = args
.parse(ruby)
.into_iter()
.map(|a| ruby.get_inner(a.0))
.collect();
ruby.class_object().funcall(name, args.as_slice())
})?;
Ok(RubyValue::new(return_value))
}))
}
fn call_fn_from_value(
&self,
value: &Self::Value,
_context: &Self::CallContext,
args: Vec<Self::Value>,
) -> Result<Self::Value, crate::ScriptingError> {
let value = value.clone();
self.execute_in_thread(move |ruby| {
let f: Proc = TryConvert::try_convert(ruby.get_inner(value.0))?;
let args: Vec<_> = args
.into_iter()
.map(|x| ruby.get_inner(x.0).as_value())
.collect();
let result: magnus::Value = f.funcall("call", args.as_slice())?;
Ok(RubyValue::new(result))
})
}
fn needs_rdynamic_linking() -> bool {
true
}
}
pub mod magnus {
pub use magnus::*;
}
pub mod prelude {
pub use super::{BevyEntity, BevyVec3, RubyRuntime, RubyScript, RubyScriptData};
}
impl<T: TryConvert> FromRuntimeValueWithEngine<'_, RubyRuntime> for T {
fn from_runtime_value_with_engine(value: RubyValue, engine: &magnus::Ruby) -> Self {
let inner = engine.get_inner(value.0);
T::try_convert(inner).expect("Failed to convert from Ruby value to native type")
}
}
impl<T: IntoValue> IntoRuntimeValueWithEngine<'_, T, RubyRuntime> for T {
fn into_runtime_value_with_engine(value: T, _engine: &magnus::Ruby) -> RubyValue {
RubyValue::new(value.into_value())
}
}
impl FuncArgs<'_, RubyValue, RubyRuntime> for () {
fn parse(self, _engine: &magnus::Ruby) -> Vec<RubyValue> {
Vec::new()
}
}
impl<T: IntoValue> FuncArgs<'_, RubyValue, RubyRuntime> for Vec<T> {
fn parse(self, _engine: &magnus::Ruby) -> Vec<RubyValue> {
self.into_iter()
.map(|x| RubyValue::new(x.into_value()))
.collect()
}
}
pub struct RArray(pub Opaque<magnus::RArray>);
impl FromRuntimeValueWithEngine<'_, RubyRuntime> for RArray {
fn from_runtime_value_with_engine(value: RubyValue, engine: &magnus::Ruby) -> Self {
let inner = engine.get_inner(value.0);
let array =
magnus::RArray::try_convert(inner).expect("Failed to convert Ruby value to RArray");
RArray(Opaque::from(array))
}
}
macro_rules! impl_tuple {
($($idx:tt $t:tt),+) => {
impl<'a, $($t: IntoValue,)+> FuncArgs<'a, RubyValue, RubyRuntime>
for ($($t,)+)
{
fn parse(self, _engine: &'a magnus::Ruby) -> Vec<RubyValue> {
vec![
$(RubyValue::new(self.$idx.into_value()), )+
]
}
}
};
}
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);

View file

@ -1,13 +1,13 @@
use bevy::{prelude::*, log::tracing};
use bevy::{log::tracing, prelude::*};
use std::{
fmt::Display,
sync::{Arc, Mutex},
};
use crate::{
Callback, Callbacks, Runtime, ScriptingError,
callback::FunctionCallEvent,
promise::{Promise, PromiseInner},
Callback, Callbacks, Runtime, ScriptingError,
};
use super::components::Script;
@ -52,7 +52,7 @@ pub(crate) fn process_new_scripts<R: Runtime>(
let path = asset_server
.get_path(&script_component.script)
.unwrap_or_default();
tracing::error!("error running script {} {:?}", path, e);
tracing::error!("error running script {} {}", path, e);
}
}
}
@ -107,7 +107,7 @@ pub(crate) fn init_callbacks<R: Runtime>(world: &mut World) -> Result<(), Script
},
);
if let Err(e) = result {
tracing::error!("error registering function: {:?}", e);
tracing::error!("error registering function: {}", e);
}
}
}
@ -158,7 +158,7 @@ pub(crate) fn process_calls<R: Runtime>(world: &mut World) -> Result<(), Scripti
match result {
Ok(_) => {}
Err(e) => {
tracing::error!("error resolving call: {} {:?}", callback.name, e);
tracing::error!("error resolving call: {} {}", callback.name, e);
}
}
}

View file

@ -1,17 +1,35 @@
#[cfg(any(feature = "rhai", feature = "lua"))]
#[cfg(any(feature = "rhai", feature = "lua", feature = "ruby"))]
use std::sync::OnceLock;
#[cfg(any(feature = "rhai", feature = "lua"))]
#[cfg(any(feature = "rhai", feature = "lua", feature = "ruby"))]
use bevy::ecs::system::RunSystemOnce as _;
#[cfg(any(feature = "rhai", feature = "lua"))]
#[cfg(any(feature = "rhai", feature = "lua", feature = "ruby"))]
use bevy::prelude::*;
#[cfg(any(feature = "rhai", feature = "lua"))]
use bevy_scriptum::{prelude::*, FuncArgs, Runtime};
#[cfg(any(feature = "rhai", feature = "lua", feature = "ruby"))]
use bevy_scriptum::{FuncArgs, Runtime, prelude::*};
#[cfg(any(feature = "rhai", feature = "lua"))]
#[cfg(any(feature = "rhai", feature = "lua", feature = "ruby"))]
static TRACING_SUBSCRIBER: OnceLock<()> = OnceLock::new();
#[cfg(any(feature = "rhai", feature = "lua"))]
#[cfg(any(feature = "rhai", feature = "lua", feature = "ruby"))]
#[derive(Default, Resource)]
struct TimesCalled {
times_called: u8,
}
macro_rules! assert_n_times_called {
($app: expr, $count: expr) => {
assert_eq!(
$app.world()
.get_resource::<TimesCalled>()
.unwrap()
.times_called,
$count
);
};
}
#[cfg(any(feature = "rhai", feature = "lua", feature = "ruby"))]
fn build_test_app() -> App {
let mut app = App::new();
@ -25,7 +43,7 @@ fn build_test_app() -> App {
app
}
#[cfg(any(feature = "rhai", feature = "lua"))]
#[cfg(any(feature = "rhai", feature = "lua", feature = "ruby"))]
fn run_script<R: Runtime, Out, Marker>(
app: &mut App,
path: String,
@ -42,7 +60,7 @@ fn run_script<R: Runtime, Out, Marker>(
entity_id
}
#[cfg(any(feature = "rhai", feature = "lua"))]
#[cfg(any(feature = "rhai", feature = "lua", feature = "ruby"))]
fn call_script_on_update_from_rust<R: Runtime>(
mut scripted_entities: Query<(Entity, &mut R::ScriptData)>,
scripting_runtime: ResMut<R>,
@ -55,7 +73,7 @@ fn call_script_on_update_from_rust<R: Runtime>(
.unwrap();
}
#[cfg(any(feature = "rhai", feature = "lua"))]
#[cfg(any(feature = "rhai", feature = "lua", feature = "ruby"))]
trait AssertStateKeyValue {
type ScriptData;
fn assert_state_key_value_i64(world: &World, entity_id: Entity, key: &str, value: i64);
@ -63,9 +81,9 @@ trait AssertStateKeyValue {
fn assert_state_key_value_string(world: &World, entity_id: Entity, key: &str, value: &str);
}
#[cfg(any(feature = "rhai", feature = "lua"))]
#[cfg(any(feature = "rhai", feature = "lua", feature = "ruby"))]
macro_rules! scripting_tests {
($runtime:ty, $script:literal, $extension:literal) => {
($runtime:ty, $script:literal, $extension:literal, $entity_type: ty, $vec_type: ty) => {
use super::*;
#[test]
@ -189,7 +207,7 @@ macro_rules! scripting_tests {
}
#[test]
fn test_script_function_gets_called_from_rust_with_heterogenous_params() {
fn test_script_function_gets_called_from_rust_with_multiple_params() {
let mut app = build_test_app();
app.add_scripting::<$runtime>(|_| {});
@ -225,33 +243,34 @@ macro_rules! scripting_tests {
}
#[test]
fn test_script_function_gets_called_from_rust_with_multiple_params() {
fn eval_that_casues_runtime_error_doesnt_panic() {
let mut app = build_test_app();
app.add_scripting::<$runtime>(|_| {});
app.add_scripting::<$runtime>(|r| {
r.add_function(
String::from("mark_called"),
|mut times_called: ResMut<TimesCalled>| {
times_called.times_called += 1;
},
);
})
.init_resource::<TimesCalled>();
let entity_id = run_script::<$runtime, _, _>(
run_script::<$runtime, _, _>(
&mut app,
format!(
"tests/{}/script_function_gets_called_from_rust_with_multiple_params.{}",
"tests/{}/eval_that_causes_runtime_error.{}",
$script, $extension
)
.to_string(),
|mut scripted_entities: Query<(Entity, &mut <$runtime as Runtime>::ScriptData)>,
scripting_runtime: ResMut<$runtime>| {
let (entity, mut script_data) = scripted_entities.single_mut().unwrap();
scripting_runtime
.call_fn("test_func", &mut script_data, entity, vec![1, 2])
.unwrap();
},
|| {},
);
<$runtime>::assert_state_key_value_i32(&app.world(), entity_id, "a_value", 1i32);
<$runtime>::assert_state_key_value_i32(&app.world(), entity_id, "b_value", 2i32);
assert_n_times_called!(app, 2);
}
#[test]
fn test_call_script_function_that_casues_runtime_error() {
fn call_script_function_that_casues_runtime_error() {
let mut app = build_test_app();
app.add_scripting::<$runtime>(|_| {});
@ -274,18 +293,14 @@ macro_rules! scripting_tests {
}
#[test]
fn test_call_script_function_that_does_not_exist() {
fn call_script_function_that_does_not_exist() {
let mut app = build_test_app();
app.add_scripting::<$runtime>(|_| {});
run_script::<$runtime, _, _>(
&mut app,
format!(
"tests/{}/call_script_function_that_causes_runtime_error.{}",
$script, $extension
)
.to_string(),
format!("tests/{}/side_effects.{}", $script, $extension).to_string(),
|mut scripted_entities: Query<(Entity, &mut <$runtime as Runtime>::ScriptData)>,
scripting_runtime: ResMut<$runtime>| {
let (entity, mut script_data) = scripted_entities.single_mut().unwrap();
@ -297,7 +312,7 @@ macro_rules! scripting_tests {
}
#[test]
fn test_script_function_gets_called_from_rust() {
fn script_function_gets_called_from_rust() {
let mut app = build_test_app();
app.add_scripting::<$runtime>(|_| {});
@ -316,7 +331,7 @@ macro_rules! scripting_tests {
}
#[test]
fn test_promise() {
fn return_via_promise() {
let mut app = build_test_app();
app.add_scripting::<$runtime>(|runtime| {
@ -333,7 +348,7 @@ macro_rules! scripting_tests {
}
#[test]
fn test_promise_runtime_error_does_not_panic() {
fn promise_runtime_error() {
let mut app = build_test_app();
app.add_scripting::<$runtime>(|runtime| {
@ -348,7 +363,7 @@ macro_rules! scripting_tests {
}
#[test]
fn test_side_effects() {
fn side_effects() {
let mut app = build_test_app();
#[derive(Component)]
@ -374,14 +389,9 @@ macro_rules! scripting_tests {
}
#[test]
fn test_rust_function_gets_called_from_script() {
fn rust_function_gets_called_from_script() {
let mut app = build_test_app();
#[derive(Default, Resource)]
struct TimesCalled {
times_called: u8,
}
app.world_mut().init_resource::<TimesCalled>();
app.add_scripting::<$runtime>(|runtime| {
@ -400,13 +410,172 @@ macro_rules! scripting_tests {
call_script_on_update_from_rust::<$runtime>,
);
assert_eq!(
app.world()
.get_resource::<TimesCalled>()
.unwrap()
.times_called,
1
assert_n_times_called!(app, 1);
}
#[test]
fn entity_variable_index_is_available_in_callback() {
let mut app = build_test_app();
#[derive(Default, Resource)]
struct State {
index: u32,
}
app.world_mut().init_resource::<State>();
app.add_scripting::<$runtime>(|runtime| {
runtime.add_function(
String::from("rust_func"),
|In((index,)): In<(u32,)>, mut res: ResMut<State>| {
res.index = index;
},
);
});
let entity = run_script::<$runtime, _, _>(
&mut app,
format!("tests/{}/entity_variable.{}", $script, $extension).to_string(),
call_script_on_update_from_rust::<$runtime>,
);
assert_eq!(
app.world().get_resource::<State>().unwrap().index,
entity.index()
);
}
#[test]
fn entity_variable_index_is_available_in_eval() {
let mut app = build_test_app();
#[derive(Default, Resource)]
struct State {
index: Option<u32>,
}
app.world_mut().init_resource::<State>();
app.add_scripting::<$runtime>(|runtime| {
runtime.add_function(
String::from("rust_func"),
|In((index,)): In<(u32,)>, mut res: ResMut<State>| {
res.index = Some(index);
},
);
});
let entity = run_script::<$runtime, _, _>(
&mut app,
format!("tests/{}/entity_variable_eval.{}", $script, $extension).to_string(),
call_script_on_update_from_rust::<$runtime>,
);
assert_eq!(
app.world().get_resource::<State>().unwrap().index,
Some(entity.index())
);
}
#[test]
fn pass_entity_from_script() {
let mut app = build_test_app();
#[derive(Default, Resource)]
struct State {
index: Option<u32>,
}
app.world_mut().init_resource::<State>();
app.add_scripting::<$runtime>(|runtime| {
runtime.add_function(
String::from("rust_func"),
|In((entity,)): In<($entity_type,)>, mut res: ResMut<State>| {
res.index = Some(entity.index());
},
);
});
let entity = run_script::<$runtime, _, _>(
&mut app,
format!("tests/{}/pass_entity_from_script.{}", $script, $extension).to_string(),
call_script_on_update_from_rust::<$runtime>,
);
assert_eq!(
app.world().get_resource::<State>().unwrap().index,
Some(entity.index())
);
}
#[test]
fn pass_vec3_from_script() {
let mut app = build_test_app();
#[derive(Default, Resource)]
struct State {
success: bool,
}
app.world_mut().init_resource::<State>();
app.add_scripting::<$runtime>(|runtime| {
runtime.add_function(
String::from("rust_func"),
|In((v,)): In<($vec_type,)>, mut res: ResMut<State>| {
assert_eq!(v.x(), 1.5);
assert_eq!(v.y(), 2.5);
assert_eq!(v.z(), -3.5);
res.success = true
},
);
});
run_script::<$runtime, _, _>(
&mut app,
format!("tests/{}/pass_vec3_from_script.{}", $script, $extension).to_string(),
call_script_on_update_from_rust::<$runtime>,
);
assert!(app.world().get_resource::<State>().unwrap().success);
}
#[test]
fn pass_vec3_to_script() {
let mut app = build_test_app();
#[derive(Default, Resource)]
struct State {
success: bool,
}
app.world_mut().init_resource::<State>();
app.add_scripting::<$runtime>(|runtime| {
runtime.add_function(String::from("mark_success"), |mut res: ResMut<State>| {
res.success = true
});
});
run_script::<$runtime, _, _>(
&mut app,
format!("tests/{}/pass_vec3_to_script.{}", $script, $extension).to_string(),
|mut scripted_entities: Query<(Entity, &mut <$runtime as Runtime>::ScriptData)>,
scripting_runtime: ResMut<$runtime>| {
let (entity, mut script_data) = scripted_entities.single_mut().unwrap();
scripting_runtime
.call_fn(
"test_func",
&mut script_data,
entity,
(<$vec_type>::new(1.5, 2.5, -3.5),),
)
.unwrap();
},
);
assert!(app.world().get_resource::<State>().unwrap().success);
}
};
}
@ -438,7 +607,7 @@ mod rhai_tests {
}
}
scripting_tests!(RhaiRuntime, "rhai", "rhai");
scripting_tests!(RhaiRuntime, "rhai", "rhai", BevyEntity, BevyVec3);
}
#[cfg(feature = "lua")]
@ -480,5 +649,66 @@ mod lua_tests {
}
}
scripting_tests!(LuaRuntime, "lua", "lua");
scripting_tests!(LuaRuntime, "lua", "lua", BevyEntity, BevyVec3);
}
#[cfg(feature = "ruby")]
mod ruby_tests {
use bevy::prelude::*;
use bevy_scriptum::runtimes::ruby::{RubyScriptData, prelude::*};
use magnus::value::ReprValue;
impl AssertStateKeyValue for RubyRuntime {
type ScriptData = RubyScriptData;
fn assert_state_key_value_i64(world: &World, _entity_id: Entity, key: &str, value: i64) {
let runtime = world.get_resource::<RubyRuntime>().unwrap();
let key = key.to_string();
runtime.with_engine_send(move |engine| {
let state: magnus::value::Value = engine.eval("$state").unwrap();
let res: i64 = state.funcall_public("[]", (key,)).unwrap();
assert_eq!(res, value)
})
}
fn assert_state_key_value_i32(world: &World, _entity_id: Entity, key: &str, value: i32) {
let runtime = world.get_resource::<RubyRuntime>().unwrap();
let key = key.to_string();
runtime.with_engine_send(move |engine| {
let state: magnus::value::Value = engine.eval("$state").unwrap();
let res: i32 = state.funcall_public("[]", (key,)).unwrap();
assert_eq!(res, value)
})
}
fn assert_state_key_value_string(
world: &World,
_entity_id: Entity,
key: &str,
value: &str,
) {
let runtime = world.get_resource::<RubyRuntime>().unwrap();
let key = key.to_string();
let value = value.to_string();
runtime.with_engine_send(move |engine| {
let state: magnus::value::Value = engine.eval("$state").unwrap();
let res: String = state.funcall_public("[]", (key,)).unwrap();
assert_eq!(res, value);
});
}
}
#[test]
fn test_symbol_inspection() {
let mut app = build_test_app();
app.add_scripting::<RubyRuntime>(|_| {});
let runtime = app.world().get_resource::<RubyRuntime>().unwrap();
runtime.with_engine_send(|engine| {
let symbol_string: String = engine.eval(":test_symbol.inspect").unwrap();
assert_eq!(symbol_string, ":test_symbol")
});
}
scripting_tests!(RubyRuntime, "ruby", "rb", BevyEntity, BevyVec3);
}