//! ![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(currently only supported on Linux) | `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::(|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::(|runtime| { //! runtime.add_function( //! String::from("print_player_names"), //! |players: Query<&Name, With>| { //! 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::(|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::(|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::(|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| { //! commands.spawn(Script::::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 _`. 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::(|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), #[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 + GetExtensions; type ScriptData: Component; 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( &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( &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(&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(&self, f: impl FnOnce(&Self::RawEngine) -> T) -> T; fn eval( &self, script: &Self::ScriptAsset, entity: Entity, ) -> Result; /// 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, f: impl Fn( Self::CallContext, Vec, ) -> Result, 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; /// 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, ) -> Result; fn needs_rdynamic_linking() -> bool { false } } pub trait FuncArgs<'a, V, R: Runtime> { fn parse(self, engine: &'a R::RawEngine) -> Vec; } /// 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(&mut self, f: impl Fn(ScriptingRuntimeBuilder)) -> &mut Self; /// Returns a "runtime" type that can be used to add additional scripting functions from plugins etc. fn add_scripting_api( &mut self, f: impl Fn(ScriptingRuntimeBuilder), ) -> &mut Self; } pub struct ScriptingRuntimeBuilder<'a, R: Runtime> { _phantom_data: PhantomData, 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( self, name: String, fun: impl IntoCallbackSystem, ) -> Self where In: SystemInput, { let system = fun.into_callback_system(self.world); let mut callbacks_resource = self.world.resource_mut::>(); 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(&mut self, f: impl Fn(ScriptingRuntimeBuilder)) -> &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::() ); } self.world_mut() .resource_mut::() .insert_after(Update, R::Schedule::default()); self.register_asset_loader(ScriptLoader::::default()) .init_schedule(R::Schedule::default()) .init_asset::() .init_resource::>() .insert_resource(R::default()) .add_systems( R::Schedule::default(), ( reload_scripts::, process_calls:: .pipe(log_errors) .after(process_new_scripts::), init_callbacks::.pipe(log_errors), process_new_scripts:: .pipe(log_errors) .after(init_callbacks::), ), ); let runtime = ScriptingRuntimeBuilder::::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( &mut self, f: impl Fn(ScriptingRuntimeBuilder), ) -> &mut Self { let runtime = ScriptingRuntimeBuilder::::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 { uninitialized_callbacks: Vec>, callbacks: Mutex>>, } impl Default for Callbacks { fn default() -> Self { Self { uninitialized_callbacks: Default::default(), callbacks: Default::default(), } } } #[cfg(all(debug_assertions, unix))] 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() } } #[cfg(any(not(debug_assertions), not(unix)))] pub extern "C" fn is_rdynamic_linking() -> bool { // On Windows or in release builds, return a default value true } pub mod prelude { pub use crate::{BuildScriptingRuntime as _, Runtime as _, Script}; }