bevy_scriptum/src/lib.rs
Jaroslaw Konik dfd96746fe
Some checks failed
Book / test (pull_request) Has been cancelled
Rust / build (pull_request) Has been cancelled
works
2025-05-30 07:00:00 +02:00

531 lines
18 KiB
Rust

//! ![demo](demo.gif)
//!
//! 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};
}