Compare commits
	
		
			5 commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| d572faf95a | |||
| bd0c5e184e | |||
| dfd96746fe | |||
| 8a3661a77a | |||
| 57f865178a | 
					 10 changed files with 163 additions and 43 deletions
				
			
		|  | @ -1,7 +1,7 @@ | |||
| [package] | ||||
| name = "bevy_scriptum" | ||||
| authors = ["Jaroslaw Konik <konikjar@gmail.com>"] | ||||
| version = "0.9.1" | ||||
| version = "0.9.0" | ||||
| edition = "2024" | ||||
| license = "MIT OR Apache-2.0" | ||||
| readme = "README.md" | ||||
|  | @ -35,6 +35,7 @@ magnus = { version = "0.7.1", optional = true } | |||
| rb-sys = { version = "0.9", default-features = false, features = ["link-ruby", "ruby-static"], optional = true } | ||||
| crossbeam-channel = "0.5.15" | ||||
| libc = "0.2.172" | ||||
| tempfile = "3.20.0" | ||||
| 
 | ||||
| [[example]] | ||||
| name = "call_function_from_rust_rhai" | ||||
|  |  | |||
							
								
								
									
										27
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								README.md
									
									
									
									
									
								
							|  | @ -3,19 +3,21 @@ | |||
|  | ||||
| 
 | ||||
| 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) | | ||||
| | 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 | ||||
|  | @ -25,6 +27,7 @@ bevy_scriptum's main advantages include: | |||
| 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 | ||||
| use bevy::prelude::*; | ||||
| use bevy_scriptum::prelude::*; | ||||
|  | @ -39,7 +42,9 @@ App::new() | |||
|     }) | ||||
|     .run(); | ||||
| ``` | ||||
| 
 | ||||
| And you can call them in your scripts like this: | ||||
| 
 | ||||
| ```lua | ||||
| hello_bevy() | ||||
| ``` | ||||
|  | @ -70,6 +75,7 @@ App::new() | |||
| ``` | ||||
| 
 | ||||
| You can also pass arguments to your callback functions, just like you would in a regular Bevy system - using `In` structs with tuples: | ||||
| 
 | ||||
| ```rust | ||||
| use bevy::prelude::*; | ||||
| use bevy_scriptum::prelude::*; | ||||
|  | @ -87,7 +93,9 @@ App::new() | |||
|     }) | ||||
|     .run(); | ||||
| ``` | ||||
| 
 | ||||
| which you can then call in your script like this: | ||||
| 
 | ||||
| ```lua | ||||
| fun_with_string_param("Hello world!") | ||||
| ``` | ||||
|  | @ -156,17 +164,18 @@ 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: | ||||
| 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                   | | ||||
|  | @ -184,6 +193,7 @@ get_player_name():and_then(function(name) | |||
|     print(name) | ||||
| end) | ||||
| ``` | ||||
| 
 | ||||
| which will print out `John` when used with following exposed function: | ||||
| 
 | ||||
| ```rust | ||||
|  | @ -196,7 +206,7 @@ App::new() | |||
|    .add_scripting::<LuaRuntime>(|runtime| { | ||||
|            runtime.add_function(String::from("get_player_name"), || String::from("John")); | ||||
|    }); | ||||
| ```` | ||||
| ``` | ||||
| 
 | ||||
| ## Access entity from script | ||||
| 
 | ||||
|  | @ -204,6 +214,7 @@ A variable called `entity` is automatically available to all scripts - it repres | |||
| 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) | ||||
| ``` | ||||
|  |  | |||
|  | @ -1,3 +1,21 @@ | |||
| get_player_name.and_then do |name| | ||||
|   puts name | ||||
| def async_fun | ||||
|   async do | ||||
|     a = get_player_name | ||||
|     b = a | ||||
|     puts '0' | ||||
|     puts a.await | ||||
|     puts '1' | ||||
|     u = get_player_name | ||||
|     puts b.await | ||||
|     puts '2' | ||||
|     z = get_player_name | ||||
|     puts z | ||||
|     puts z.await | ||||
|     puts "end" | ||||
|   end | ||||
| end | ||||
| 
 | ||||
| async_fun.await | ||||
| puts "after await" | ||||
| 
 | ||||
| quit | ||||
|  |  | |||
|  | @ -1,7 +1,5 @@ | |||
| # Installation | ||||
| 
 | ||||
| Ruby is currently only supported on Linux. | ||||
| 
 | ||||
| ## Ruby | ||||
| 
 | ||||
| To build `bevy_scriptum` with Ruby support a Ruby installation is needed to be | ||||
|  |  | |||
|  | @ -1,4 +1,3 @@ | |||
| # Ruby | ||||
| 
 | ||||
| This chapter demonstrates how to work with bevy_scriptum when using Ruby language runtime. | ||||
| Ruby is currently only supported on Linux. | ||||
|  |  | |||
|  | @ -9,15 +9,19 @@ 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() | ||||
|                 }, | ||||
|             ); | ||||
|             builder | ||||
|                 .add_function( | ||||
|                     String::from("get_player_name"), | ||||
|                     |player_names: Query<&Name, With<Player>>| { | ||||
|                         player_names | ||||
|                             .single() | ||||
|                             .expect("Missing player_names") | ||||
|                             .to_string() | ||||
|                     }, | ||||
|                 ) | ||||
|                 .add_function(String::from("quit"), |mut exit: EventWriter<AppExit>| { | ||||
|                     exit.write(AppExit::Success); | ||||
|                 }); | ||||
|         }) | ||||
|         .add_systems(Startup, startup) | ||||
|         .run(); | ||||
|  |  | |||
							
								
								
									
										20
									
								
								src/lib.rs
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								src/lib.rs
									
									
									
									
									
								
							|  | @ -4,11 +4,11 @@ | |||
| 
 | ||||
| //! ## 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) |
 | ||||
| //! | 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/) 📖
 | ||||
| //!
 | ||||
|  | @ -375,6 +375,8 @@ pub trait Runtime: Resource + Default { | |||
|     fn needs_rdynamic_linking() -> bool { | ||||
|         false | ||||
|     } | ||||
| 
 | ||||
|     fn resume(&self, fiber: &Self::Value, value: &Self::Value); | ||||
| } | ||||
| 
 | ||||
| pub trait FuncArgs<'a, V, R: Runtime> { | ||||
|  | @ -510,7 +512,7 @@ impl<R: Runtime> Default for Callbacks<R> { | |||
|     } | ||||
| } | ||||
| 
 | ||||
| #[cfg(all(debug_assertions, unix))] | ||||
| #[cfg(debug_assertions)] | ||||
| pub extern "C" fn is_rdynamic_linking() -> bool { | ||||
|     unsafe { | ||||
|         // Get a function pointer to itself
 | ||||
|  | @ -524,12 +526,6 @@ pub extern "C" fn is_rdynamic_linking() -> bool { | |||
|     } | ||||
| } | ||||
| 
 | ||||
| #[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}; | ||||
| } | ||||
|  |  | |||
|  | @ -13,6 +13,8 @@ pub(crate) struct PromiseInner<C: Send, V: Send> { | |||
|     pub(crate) callbacks: Vec<PromiseCallback<C, V>>, | ||||
|     #[allow(deprecated)] | ||||
|     pub(crate) context: C, | ||||
|     pub(crate) resolved_value: Option<V>, | ||||
|     pub(crate) fibers: Vec<V>, // TODO: should htis be vec or option
 | ||||
| } | ||||
| 
 | ||||
| /// A struct that represents a Promise.
 | ||||
|  | @ -51,12 +53,31 @@ impl<C: Clone + Send + 'static, V: Send + Clone> Promise<C, V> { | |||
|     where | ||||
|         R: Runtime<Value = V, CallContext = C>, | ||||
|     { | ||||
|         let mut fibers: Vec<V> = vec![]; | ||||
|         if let Ok(mut inner) = self.inner.lock() { | ||||
|             inner.resolve(runtime, val)?; | ||||
|             inner.resolved_value = Some(val.clone()); | ||||
|             inner.resolve(runtime, val.clone())?; | ||||
| 
 | ||||
|             for fiber in inner.fibers.drain(..) { | ||||
|                 fibers.push(fiber); | ||||
|             } | ||||
|         } | ||||
|         for fiber in fibers { | ||||
|             runtime.resume(&fiber, &val.clone()); | ||||
|         } | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     /// Register a fiber that will be resumed when the [Promise] is resolved.
 | ||||
|     #[cfg(any(feature = "rhai", feature = "lua", feature = "ruby"))] | ||||
|     pub(crate) fn await_promise(&mut self, fiber: V) { | ||||
|         let mut inner = self | ||||
|             .inner | ||||
|             .lock() | ||||
|             .expect("Failed to lock inner promise mutex"); | ||||
|         inner.fibers.push(fiber); | ||||
|     } | ||||
| 
 | ||||
|     /// Register a callback that will be called when the [Promise] is resolved.
 | ||||
|     #[cfg(any(feature = "rhai", feature = "lua", feature = "ruby"))] | ||||
|     pub(crate) fn then(&mut self, callback: V) -> Self { | ||||
|  | @ -65,8 +86,10 @@ impl<C: Clone + Send + 'static, V: Send + Clone> Promise<C, V> { | |||
|             .lock() | ||||
|             .expect("Failed to lock inner promise mutex"); | ||||
|         let following_inner = Arc::new(Mutex::new(PromiseInner { | ||||
|             fibers: vec![], | ||||
|             callbacks: vec![], | ||||
|             context: inner.context.clone(), | ||||
|             resolved_value: None, | ||||
|         })); | ||||
| 
 | ||||
|         inner.callbacks.push(PromiseCallback { | ||||
|  |  | |||
|  | @ -1,11 +1,16 @@ | |||
| use std::{ | ||||
|     collections::HashMap, | ||||
|     ffi::CString, | ||||
|     io::Write, | ||||
|     sync::{Arc, Condvar, LazyLock, Mutex}, | ||||
|     thread::{self, JoinHandle}, | ||||
| }; | ||||
| 
 | ||||
| use ::magnus::{typed_data::Inspect, value::Opaque}; | ||||
| use ::magnus::{ | ||||
|     Fiber, | ||||
|     typed_data::Inspect, | ||||
|     value::{self, Opaque}, | ||||
| }; | ||||
| use bevy::{ | ||||
|     asset::Asset, | ||||
|     ecs::{component::Component, entity::Entity, resource::Resource, schedule::ScheduleLabel}, | ||||
|  | @ -19,7 +24,7 @@ use magnus::{ | |||
|     value::{Lazy, ReprValue}, | ||||
| }; | ||||
| use magnus::{method, prelude::*}; | ||||
| use rb_sys::{VALUE, ruby_init_stack}; | ||||
| use rb_sys::{VALUE, rb_load, ruby_init_stack}; | ||||
| use serde::Deserialize; | ||||
| 
 | ||||
| use crate::{ | ||||
|  | @ -174,6 +179,19 @@ fn then(r_self: magnus::Value) -> magnus::Value { | |||
|         .into_value() | ||||
| } | ||||
| 
 | ||||
| fn await_promise(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"); | ||||
|     let fiber = Opaque::from(ruby.fiber_current().as_value()); | ||||
|     if let Some(value) = &promise.inner.try_lock().unwrap().resolved_value { | ||||
|         return ruby.get_inner(value.0); | ||||
|     } | ||||
|     promise.clone().await_promise(RubyValue(fiber)).into_value(); | ||||
|     ruby.fiber_yield::<_, magnus::Value>(()).unwrap() | ||||
| } | ||||
| 
 | ||||
| #[derive(Clone, Debug)] | ||||
| #[magnus::wrap(class = "Bevy::Entity")] | ||||
| pub struct BevyEntity(pub Entity); | ||||
|  | @ -195,6 +213,19 @@ impl TryConvert for BevyEntity { | |||
| #[magnus::wrap(class = "Bevy::Vec3")] | ||||
| pub struct BevyVec3(pub Vec3); | ||||
| 
 | ||||
| pub fn async_function() { | ||||
|     let ruby = Ruby::get().unwrap(); | ||||
|     let fiber = ruby | ||||
|         .fiber_new_from_fn(Default::default(), move |ruby, _args, _block| { | ||||
|             let p = ruby.block_proc().unwrap(); | ||||
|             p.call::<_, value::Value>(()).unwrap(); | ||||
| 
 | ||||
|             Ok(()) | ||||
|         }) | ||||
|         .unwrap(); | ||||
|     fiber.resume::<_, magnus::Value>(()).unwrap(); | ||||
| } | ||||
| 
 | ||||
| impl BevyVec3 { | ||||
|     pub fn new(x: f32, y: f32, z: f32) -> Self { | ||||
|         Self(Vec3::new(x, y, z)) | ||||
|  | @ -266,12 +297,15 @@ impl Default for RubyRuntime { | |||
| 
 | ||||
|                 let promise = module.define_class("Promise", ruby.class_object())?; | ||||
|                 promise.define_method("and_then", magnus::method!(then, 0))?; | ||||
|                 promise.define_method("await", magnus::method!(await_promise, 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))?; | ||||
| 
 | ||||
|                 ruby.define_global_function("async", function!(async_function, 0)); | ||||
|                 Ok::<(), ScriptingError>(()) | ||||
|             })) | ||||
|             .expect("Failed to define builtin types"); | ||||
|  | @ -392,10 +426,35 @@ impl Runtime for RubyRuntime { | |||
|     ) -> 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) | ||||
|             })?; | ||||
|             let p = Opaque::from(ruby.proc_from_fn(move |ruby, _args, _block| { | ||||
|                 Self::with_current_entity(ruby, entity, || { | ||||
|                     let mut tmpfile = tempfile::NamedTempFile::new().unwrap(); | ||||
|                     tmpfile.write(script.as_bytes()).unwrap(); | ||||
|                     unsafe { | ||||
|                         let file = rb_sys::rb_str_new_cstr( | ||||
|                             CString::new(tmpfile.path().to_str().unwrap()) | ||||
|                                 .unwrap() | ||||
|                                 .into_raw(), | ||||
|                         ); | ||||
|                         rb_load(file, 0); | ||||
|                     }; | ||||
|                     // ruby.eval::<magnus::value::Value>(&script)
 | ||||
|                     //     .map_err(<magnus::Error as Into<ScriptingError>>::into)
 | ||||
|                     Ok::<(), ScriptingError>(()) | ||||
|                 }) | ||||
|                 .unwrap(); | ||||
|             })); | ||||
|             let fiber = ruby | ||||
|                 .fiber_new_from_fn(Default::default(), move |ruby, _args, _block| { | ||||
|                     let p = ruby.get_inner(p); | ||||
| 
 | ||||
|                     p.call::<_, value::Value>(()).unwrap(); | ||||
| 
 | ||||
|                     Ok(()) | ||||
|                 }) | ||||
|                 .unwrap(); | ||||
|             fiber.resume::<_, value::Value>(()).unwrap(); | ||||
| 
 | ||||
|             Ok::<Self::ScriptData, ScriptingError>(RubyScriptData) | ||||
|         })) | ||||
|     } | ||||
|  | @ -509,6 +568,15 @@ impl Runtime for RubyRuntime { | |||
|     fn needs_rdynamic_linking() -> bool { | ||||
|         true | ||||
|     } | ||||
| 
 | ||||
|     fn resume(&self, fiber: &Self::Value, value: &Self::Value) { | ||||
|         let fiber = fiber.clone(); | ||||
|         let value = value.clone(); | ||||
|         self.execute_in_thread(move |ruby| { | ||||
|             let fiber: Fiber = TryConvert::try_convert(ruby.get_inner(fiber.0)).unwrap(); | ||||
|             fiber.resume::<_, magnus::Value>((value.0,)); | ||||
|         }); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub mod magnus { | ||||
|  |  | |||
|  | @ -89,6 +89,8 @@ pub(crate) fn init_callbacks<R: Runtime>(world: &mut World) -> Result<(), Script | |||
|                 move |context, params| { | ||||
|                     let promise = Promise { | ||||
|                         inner: Arc::new(Mutex::new(PromiseInner { | ||||
|                             resolved_value: None, | ||||
|                             fibers: vec![], | ||||
|                             callbacks: vec![], | ||||
|                             context, | ||||
|                         })), | ||||
|  | @ -100,7 +102,7 @@ pub(crate) fn init_callbacks<R: Runtime>(world: &mut World) -> Result<(), Script | |||
|                         .expect("Failed to lock callback calls mutex"); | ||||
| 
 | ||||
|                     calls.push(FunctionCallEvent { | ||||
|                         promise: promise.clone(), | ||||
|                         promise: promise.clone(), // TODO: dont clone?
 | ||||
|                         params, | ||||
|                     }); | ||||
|                     Ok(promise) | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue