531 lines
18 KiB
Rust
531 lines
18 KiB
Rust
//! 
|
|
//!
|
|
//! 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
|
|
//!
|
|
//! | 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
|
|
//! - easy to use
|
|
//! - asynchronicity with a promise-based API
|
|
//! - flexibility
|
|
//! - hot-reloading
|
|
//!
|
|
//! Scripts are separate files that can be hot-reloaded at runtime. This allows you to quickly iterate on your game 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| {
|
|
//! runtime.add_function(String::from("hello_bevy"), || {
|
|
//! println!("hello bevy, called from script");
|
|
//! });
|
|
//! })
|
|
//! .run();
|
|
//! ```
|
|
//! And you can call them in your scripts like this:
|
|
//! ```lua
|
|
//! hello_bevy()
|
|
//! ```
|
|
//!
|
|
//! Every callback function that you expose to the scripting language is also a Bevy system, so you can easily query and mutate ECS components and resources just like you would in a regular Bevy system:
|
|
//!
|
|
//! ```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| {
|
|
//! runtime.add_function(
|
|
//! String::from("print_player_names"),
|
|
//! |players: Query<&Name, With<Player>>| {
|
|
//! for player in &players {
|
|
//! println!("player name: {}", player);
|
|
//! }
|
|
//! },
|
|
//! );
|
|
//! })
|
|
//! .run();
|
|
//! ```
|
|
//!
|
|
//! You can also pass arguments to your callback functions, just like you would in a regular Bevy system - using `In` structs with tuples:
|
|
//! ```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| {
|
|
//! runtime.add_function(
|
|
//! String::from("fun_with_string_param"),
|
|
//! |In((x,)): In<(String,)>| {
|
|
//! println!("called with string: '{}'", x);
|
|
//! },
|
|
//! );
|
|
//! })
|
|
//! .run();
|
|
//! ```
|
|
//! which you can then call in your script like this:
|
|
//! ```lua
|
|
//! fun_with_string_param("Hello world!")
|
|
//! ```
|
|
//!
|
|
//! ## Usage
|
|
//!
|
|
//! Add the following to your `Cargo.toml`:
|
|
//!
|
|
//! ```toml
|
|
//! [dependencies]
|
|
//! bevy_scriptum = { version = "0.9", features = ["lua"] }
|
|
//! ```
|
|
//!
|
|
//! or execute `cargo add bevy_scriptum --features lua` from your project directory.
|
|
//!
|
|
//! You can now start exposing functions to the scripting language. For example, you can expose a function that prints a message to the console:
|
|
//!
|
|
//! ```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| {
|
|
//! runtime.add_function(
|
|
//! String::from("my_print"),
|
|
//! |In((x,)): In<(String,)>| {
|
|
//! println!("my_print: '{}'", x);
|
|
//! },
|
|
//! );
|
|
//! })
|
|
//! .run();
|
|
//! ```
|
|
//!
|
|
//! Then you can create a script file in `assets` directory called `script.lua` that calls this function:
|
|
//!
|
|
//! ```lua
|
|
//! my_print("Hello world!")
|
|
//! ```
|
|
//!
|
|
//! And spawn an entity with attached `Script` component with a handle to a script source file:
|
|
//!
|
|
//! ```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| {
|
|
//! runtime.add_function(
|
|
//! String::from("my_print"),
|
|
//! |In((x,)): In<(String,)>| {
|
|
//! println!("my_print: '{}'", x);
|
|
//! },
|
|
//! );
|
|
//! })
|
|
//! .add_systems(Startup,|mut commands: Commands, asset_server: Res<AssetServer>| {
|
|
//! commands.spawn(Script::<LuaScript>::new(asset_server.load("script.lua")));
|
|
//! })
|
|
//! .run();
|
|
//! ```
|
|
//!
|
|
//! You should then see `my_print: 'Hello world!'` printed in your console.
|
|
//!
|
|
//! ## Provided examples
|
|
//!
|
|
//! You can also try running provided examples by cloning this repository and running `cargo run --example <example_name>_<language_name>`. For example:
|
|
//!
|
|
//! ```bash
|
|
//! cargo run --example hello_world_lua
|
|
//! ```
|
|
//! The examples live in `examples` directory and their corresponding scripts live in `assets/examples` directory within the repository.
|
|
//!
|
|
//! ## Bevy compatibility
|
|
//!
|
|
//! | bevy version | bevy_scriptum version |
|
|
//! |--------------|-----------------------|
|
|
//! | 0.16 | 0.8-0.9 |
|
|
//! | 0.15 | 0.7 |
|
|
//! | 0.14 | 0.6 |
|
|
//! | 0.13 | 0.4-0.5 |
|
|
//! | 0.12 | 0.3 |
|
|
//! | 0.11 | 0.2 |
|
|
//! | 0.10 | 0.1 |
|
|
//!
|
|
//! ## Promises - getting return values from scripts
|
|
//!
|
|
//! Every function called from script returns a promise that you can call `:and_then` with a callback function on. This callback function will be called when the promise is resolved, and will be passed the return value of the function called from script. For example:
|
|
//!
|
|
//! ```lua
|
|
//! get_player_name():and_then(function(name)
|
|
//! print(name)
|
|
//! end)
|
|
//! ```
|
|
//! which will print out `John` when used with following exposed function:
|
|
//!
|
|
//! ```
|
|
//! 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| {
|
|
//! runtime.add_function(String::from("get_player_name"), || String::from("John"));
|
|
//! });
|
|
//! ````
|
|
//!
|
|
//! ## Access entity from script
|
|
//!
|
|
//! A variable called `entity` is automatically available to all scripts - it represents bevy entity that the `Script` component is attached to.
|
|
//! It exposes `index` property that returns bevy entity index.
|
|
//! It is useful for accessing entity's components from scripts.
|
|
//! It can be used in the following way:
|
|
//! ```lua
|
|
//! print("Current entity index: " .. entity.index)
|
|
//! ```
|
|
//!
|
|
//! `entity` variable is currently not available within promise callbacks.
|
|
//!
|
|
//! ## Contributing
|
|
//!
|
|
//! Contributions are welcome! Feel free to open an issue or submit a pull request.
|
|
//!
|
|
//! ## License
|
|
//!
|
|
//! bevy_scriptum is licensed under either of the following, at your option:
|
|
//! Apache License, Version 2.0, (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0) or MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
|
|
|
|
mod assets;
|
|
mod callback;
|
|
mod components;
|
|
mod promise;
|
|
mod systems;
|
|
|
|
pub mod runtimes;
|
|
|
|
pub use crate::components::Script;
|
|
use assets::GetExtensions;
|
|
use promise::Promise;
|
|
|
|
use std::{
|
|
any::TypeId,
|
|
fmt::Debug,
|
|
hash::Hash,
|
|
marker::PhantomData,
|
|
sync::{Arc, Mutex},
|
|
};
|
|
|
|
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;
|
|
|
|
use self::{
|
|
assets::ScriptLoader,
|
|
systems::{process_new_scripts, reload_scripts},
|
|
};
|
|
|
|
#[cfg(any(feature = "rhai", feature = "lua"))]
|
|
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:\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")]
|
|
NoSettingsResource,
|
|
}
|
|
|
|
/// Trait that represents a scripting runtime/engine. In practice it is
|
|
/// implemented for a scripint language interpreter and the implementor provides
|
|
/// function implementations for calling and registering functions within the interpreter.
|
|
pub trait Runtime: Resource + Default {
|
|
type Schedule: ScheduleLabel + Debug + Clone + Eq + Hash + Default;
|
|
type ScriptAsset: Asset + From<String> + GetExtensions;
|
|
type ScriptData: Component<Mutability = Mutable>;
|
|
type CallContext: Send + Clone;
|
|
type Value: Send + Clone;
|
|
type RawEngine;
|
|
|
|
/// 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(
|
|
&self,
|
|
script: &Self::ScriptAsset,
|
|
entity: Entity,
|
|
) -> Result<Self::ScriptData, ScriptingError>;
|
|
|
|
/// Registers a new function within the scripting engine. Provided callback
|
|
/// function will be called when the function with provided name gets called
|
|
/// in script.
|
|
fn register_fn(
|
|
&mut self,
|
|
name: String,
|
|
arg_types: Vec<TypeId>,
|
|
f: impl Fn(
|
|
Self::CallContext,
|
|
Vec<Self::Value>,
|
|
) -> Result<Promise<Self::CallContext, Self::Value>, ScriptingError>
|
|
+ Send
|
|
+ Sync
|
|
+ 'static,
|
|
) -> Result<(), ScriptingError>;
|
|
|
|
/// Calls a function by name defined within the runtime in the context of the
|
|
/// entity that haas been paassed. Can return a dynamically typed value
|
|
/// that got returned from the function within a script.
|
|
fn call_fn(
|
|
&self,
|
|
name: &str,
|
|
script_data: &mut Self::ScriptData,
|
|
entity: Entity,
|
|
args: impl for<'a> FuncArgs<'a, Self::Value, Self> + Send + 'static,
|
|
) -> Result<Self::Value, ScriptingError>;
|
|
|
|
/// Calls a function by value defined within the runtime in the context of the
|
|
/// entity that haas been paassed. Can return a dynamically typed value
|
|
/// that got returned from the function within a script.
|
|
fn call_fn_from_value(
|
|
&self,
|
|
value: &Self::Value,
|
|
context: &Self::CallContext,
|
|
args: Vec<Self::Value>,
|
|
) -> Result<Self::Value, ScriptingError>;
|
|
|
|
fn needs_rdynamic_linking() -> bool {
|
|
false
|
|
}
|
|
|
|
fn resume(&self, fiber: &Self::Value, value: &Self::Value);
|
|
}
|
|
|
|
pub trait FuncArgs<'a, V, R: Runtime> {
|
|
fn parse(self, engine: &'a R::RawEngine) -> Vec<V>;
|
|
}
|
|
|
|
/// An extension trait for [App] that allows to setup a scripting runtime `R`.
|
|
pub trait BuildScriptingRuntime {
|
|
/// Returns a "runtime" type than can be used to setup scripting runtime(
|
|
/// add scripting functions etc.).
|
|
fn add_scripting<R: Runtime>(&mut self, f: impl Fn(ScriptingRuntimeBuilder<R>)) -> &mut Self;
|
|
|
|
/// Returns a "runtime" type that can be used to add additional scripting functions from plugins etc.
|
|
fn add_scripting_api<R: Runtime>(
|
|
&mut self,
|
|
f: impl Fn(ScriptingRuntimeBuilder<R>),
|
|
) -> &mut Self;
|
|
}
|
|
|
|
pub struct ScriptingRuntimeBuilder<'a, R: Runtime> {
|
|
_phantom_data: PhantomData<R>,
|
|
world: &'a mut World,
|
|
}
|
|
|
|
impl<'a, R: Runtime> ScriptingRuntimeBuilder<'a, R> {
|
|
fn new(world: &'a mut World) -> Self {
|
|
Self {
|
|
_phantom_data: PhantomData,
|
|
world,
|
|
}
|
|
}
|
|
|
|
/// Registers a function for calling from within a script.
|
|
/// Provided function needs to be a valid bevy system and its
|
|
/// arguments and return value need to be convertible to runtime
|
|
/// value types.
|
|
pub fn add_function<In, Out, Marker>(
|
|
self,
|
|
name: String,
|
|
fun: impl IntoCallbackSystem<R, In, Out, Marker>,
|
|
) -> Self
|
|
where
|
|
In: SystemInput,
|
|
{
|
|
let system = fun.into_callback_system(self.world);
|
|
|
|
let mut callbacks_resource = self.world.resource_mut::<Callbacks<R>>();
|
|
|
|
callbacks_resource.uninitialized_callbacks.push(Callback {
|
|
name,
|
|
system: Arc::new(Mutex::new(system)),
|
|
calls: Arc::new(Mutex::new(vec![])),
|
|
});
|
|
|
|
self
|
|
}
|
|
}
|
|
|
|
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());
|
|
|
|
self.register_asset_loader(ScriptLoader::<R::ScriptAsset>::default())
|
|
.init_schedule(R::Schedule::default())
|
|
.init_asset::<R::ScriptAsset>()
|
|
.init_resource::<Callbacks<R>>()
|
|
.insert_resource(R::default())
|
|
.add_systems(
|
|
R::Schedule::default(),
|
|
(
|
|
reload_scripts::<R>,
|
|
process_calls::<R>
|
|
.pipe(log_errors)
|
|
.after(process_new_scripts::<R>),
|
|
init_callbacks::<R>.pipe(log_errors),
|
|
process_new_scripts::<R>
|
|
.pipe(log_errors)
|
|
.after(init_callbacks::<R>),
|
|
),
|
|
);
|
|
|
|
let runtime = ScriptingRuntimeBuilder::<R>::new(self.world_mut());
|
|
|
|
f(runtime);
|
|
|
|
self
|
|
}
|
|
|
|
/// Adds a way to add additional accesspoints to the scripting runtime. For example from plugins to add
|
|
/// for example additional lua functions to the runtime.
|
|
///
|
|
/// Be careful with calling this though, make sure that the `add_scripting` call is already called before calling this function.
|
|
fn add_scripting_api<R: Runtime>(
|
|
&mut self,
|
|
f: impl Fn(ScriptingRuntimeBuilder<R>),
|
|
) -> &mut Self {
|
|
let runtime = ScriptingRuntimeBuilder::<R>::new(self.world_mut());
|
|
|
|
f(runtime);
|
|
|
|
self
|
|
}
|
|
}
|
|
|
|
/// A resource that stores all the callbacks that were registered using [AddScriptFunctionAppExt::add_function].
|
|
#[derive(Resource)]
|
|
struct Callbacks<R: Runtime> {
|
|
uninitialized_callbacks: Vec<Callback<R>>,
|
|
callbacks: Mutex<Vec<Callback<R>>>,
|
|
}
|
|
|
|
impl<R: Runtime> Default for Callbacks<R> {
|
|
fn default() -> Self {
|
|
Self {
|
|
uninitialized_callbacks: Default::default(),
|
|
callbacks: Default::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[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};
|
|
}
|