diff --git a/Cargo.toml b/Cargo.toml index 4574b2c0db..f187c006ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -345,6 +345,12 @@ name = "script_loading" path = "examples/script_loading.rs" required-features = ["lua54"] + +[[example]] +name = "custom_conversions" +path = "examples/custom_conversions.rs" +required-features = ["lua54"] + [workspace.lints.clippy] panic = "deny" unwrap_used = "deny" diff --git a/crates/bevy_mod_scripting_asset/src/script_asset.rs b/crates/bevy_mod_scripting_asset/src/script_asset.rs index 2f1c408662..acfe06de17 100644 --- a/crates/bevy_mod_scripting_asset/src/script_asset.rs +++ b/crates/bevy_mod_scripting_asset/src/script_asset.rs @@ -29,4 +29,10 @@ impl ScriptAsset { pub fn new(s: impl Into) -> Self { s.into().into() } + + /// Returns a new script asset with the language provided + pub fn with_language(mut self, language: Language) -> Self { + self.language = language; + self + } } diff --git a/crates/bevy_mod_scripting_bindings/src/lib.rs b/crates/bevy_mod_scripting_bindings/src/lib.rs index 477795c7a6..91049ef96e 100644 --- a/crates/bevy_mod_scripting_bindings/src/lib.rs +++ b/crates/bevy_mod_scripting_bindings/src/lib.rs @@ -22,6 +22,7 @@ pub use error::*; pub use function::*; pub use globals::*; // pub use pretty_print::*; +pub use bevy_mod_scripting_world::*; pub use conversions::*; pub use path::*; pub use query::*; diff --git a/crates/bevy_mod_scripting_core/src/derive_tests/mod.rs b/crates/bevy_mod_scripting_core/src/derive_tests/mod.rs new file mode 100644 index 0000000000..7420e19b3e --- /dev/null +++ b/crates/bevy_mod_scripting_core/src/derive_tests/mod.rs @@ -0,0 +1,89 @@ +use bevy_ecs::world::World; +use bevy_mod_scripting_derive::{ + ArgMeta, FromScript, GetTypeDependencies, IntoScript, TypedThrough, script_bindings, +}; +use bevy_reflect::{Reflect, TypeRegistry, Typed}; + +#[derive(Clone, TypedThrough, GetTypeDependencies, ArgMeta, IntoScript, FromScript, Reflect)] +// this is not required in third party crates, unless they only depend on these directly +#[typed_through(bms_bindings_path = "bevy_mod_scripting_bindings")] +#[get_type_dependencies(bms_bindings_path = "bevy_mod_scripting_bindings")] +#[into_script(bms_bindings_path = "bevy_mod_scripting_bindings")] +#[from_script(bms_bindings_path = "bevy_mod_scripting_bindings")] +#[arg_meta(bms_bindings_path = "bevy_mod_scripting_bindings")] +pub struct MyThing(usize); + +#[script_bindings(remote, bms_bindings_path = "bevy_mod_scripting_bindings")] +impl MyThing { + pub fn test(thing: MyThing) -> MyThing { + thing + } +} +mod tests { + use std::any::TypeId; + + use bevy_ecs::world::World; + use bevy_mod_scripting_bindings::{ + AppScriptFunctionRegistry, FunctionCallContext, GetTypeDependencies, ReflectReference, + ThroughTypeInfo, TypedThrough, WorldExtensions, + }; + use bevy_mod_scripting_world::WorldAccessGuard; + use bevy_reflect::TypeRegistry; + + use crate::derive_tests::{MyThing, register_functions}; + + #[test] + pub fn typed_through_represents_self() { + let info = MyThing::through_type_info(); + assert!( + matches!(info, ThroughTypeInfo::TypeInfo(info) if info.type_id() == TypeId::of::()) + ); + } + + #[test] + pub fn type_dependency_registers_only_self() { + let mut registry = TypeRegistry::new(); + let prev_count = registry.iter().count(); + ::register_type_dependencies(&mut registry); + assert!(registry.contains(TypeId::of::())); + let next_count = registry.iter().count(); + assert_eq!(next_count - prev_count, 1) + } + + #[test] + pub fn bindings_function_works() { + let mut world = World::new(); + + world.init_resource::(); + + let cache = WorldAccessGuard::setup_cache(&world, Default::default()); + WorldAccessGuard::with_static_guard(&mut world, cache, |world| { + let world = &world; + world + .with_world_mut_access(|w| { + register_functions(w); + }) + .unwrap(); + let func = world + .lookup_function([TypeId::of::()], "test") + .unwrap(); + let allocator = world.allocator(); + let mut allocator = allocator.write(); + let allocated = ReflectReference::new_allocated(MyThing(42), &mut allocator); + drop(allocator); + let out = func + .call( + vec![allocated.into()], + FunctionCallContext::new(bevy_mod_scripting_asset::Language::Lua), + ) + .unwrap(); + let out: MyThing = match out { + bevy_mod_scripting_bindings::ScriptValue::Reference(reflect_reference) => { + reflect_reference.downcast(world.clone()).unwrap() + } + _ => panic!("invalid return"), + }; + assert_eq!(out.0, 42) + }); + } +} diff --git a/crates/bevy_mod_scripting_core/src/event.rs b/crates/bevy_mod_scripting_core/src/event.rs index 73f93cb824..257e883b76 100644 --- a/crates/bevy_mod_scripting_core/src/event.rs +++ b/crates/bevy_mod_scripting_core/src/event.rs @@ -209,6 +209,8 @@ pub struct ScriptCallbackEvent { pub args: Vec, /// Whether the callback should emit a response event pub trigger_response: bool, + /// How many times was this re-queued, when 0 it's not been processed + pub iteration: usize, } impl ScriptCallbackEvent { @@ -225,6 +227,7 @@ impl ScriptCallbackEvent { args, recipients, trigger_response: false, + iteration: 0, } } @@ -268,6 +271,11 @@ impl ScriptCallbackEvent { pub fn new_for_all_contexts>(label: L, args: Vec) -> Self { Self::new(label, args, Recipients::AllContexts, None) } + + pub(crate) fn with_incremented_iteration(mut self) -> Self { + self.iteration += 1; + self + } } /// Event published when a script completes a callback and a response is requested. diff --git a/crates/bevy_mod_scripting_core/src/handler.rs b/crates/bevy_mod_scripting_core/src/handler.rs index 0c708f328b..7fcb936f72 100644 --- a/crates/bevy_mod_scripting_core/src/handler.rs +++ b/crates/bevy_mod_scripting_core/src/handler.rs @@ -145,15 +145,21 @@ pub(crate) fn event_handler_inner( event.recipients, Recipients::ScriptEntity(_, _) | Recipients::StaticScript(_) ); - + let might_not_have_reached_pipeline_if_new = event.iteration == 0; for (attachment, ctxt) in recipients { // we don't issue callbacks to scripts which are currently loading/unloading/reloading let ctxt = if let Some(ctxt) = ctxt.as_loaded() { ctxt - } else if highly_specific && ctxt.is_loading_or_reloading() { + // due to how the pipeline runs, we might see scripts which have just been + // loaded, and attached, but the pipeline hasn't run yet + // we want to give those one frame to trigger + } else if highly_specific && ctxt.is_loading_or_reloading() + || might_not_have_reached_pipeline_if_new + { + println!("REQUEING"); // events with high specificity, have their callbacks re-queued in this case // i.e. we don't want `on_update` to queue up, but a directed `on_collision_with_entity` callback will have - events_to_requeue.push(event.clone()); + events_to_requeue.push(event.clone().with_incremented_iteration()); continue; } else { continue; diff --git a/crates/bevy_mod_scripting_core/src/lib.rs b/crates/bevy_mod_scripting_core/src/lib.rs index feffe99332..46f224ce60 100644 --- a/crates/bevy_mod_scripting_core/src/lib.rs +++ b/crates/bevy_mod_scripting_core/src/lib.rs @@ -36,6 +36,9 @@ pub mod callbacks; pub mod commands; pub mod config; pub mod context; +#[cfg(test)] +mod derive_tests; + pub mod error; pub mod event; pub mod extractors; diff --git a/crates/bevy_mod_scripting_derive/src/derive/arg_meta.rs b/crates/bevy_mod_scripting_derive/src/derive/arg_meta.rs new file mode 100644 index 0000000000..ed1af680a5 --- /dev/null +++ b/crates/bevy_mod_scripting_derive/src/derive/arg_meta.rs @@ -0,0 +1,50 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::DeriveInput; + +use crate::derive::SharedArgs; +#[derive(Default)] +struct Args { + shared_args: SharedArgs, +} + +impl Args { + fn parse(attrs: &[syn::Attribute]) -> syn::Result { + let mut shared_args = SharedArgs::default(); + + for attr in attrs { + if attr.path().is_ident("arg_meta") { + attr.parse_nested_meta(|meta| { + // delegate everything to SharedArgs + if shared_args.apply_nested_meta(&meta)? { + return Ok(()); + } + + Err(meta.error("Unknown argument to get_type_dependencies")) + })?; + } + } + + Ok(Self { shared_args }) + } +} +pub fn arg_meta(input: TokenStream) -> TokenStream { + let (args, ident, generics) = match syn::parse2::(input) { + Ok(derive_input) => { + let args = match Args::parse(&derive_input.attrs) { + Ok(args) => args, + Err(error) => return error.to_compile_error(), + }; + (args, derive_input.ident, derive_input.generics) + } + Err(err) => return err.to_compile_error(), + }; + + let (impl_generics, type_generics, where_clause) = generics.split_for_impl(); + + let bms_bindings_path = args.shared_args.bms_bindings_path; + quote! { + impl #impl_generics #bms_bindings_path::ArgMeta for #ident #type_generics #where_clause { + } + } +} diff --git a/crates/bevy_mod_scripting_derive/src/derive/debug_with_type_info.rs b/crates/bevy_mod_scripting_derive/src/derive/debug_with_type_info.rs index fffe5c1db2..803d6277f3 100644 --- a/crates/bevy_mod_scripting_derive/src/derive/debug_with_type_info.rs +++ b/crates/bevy_mod_scripting_derive/src/derive/debug_with_type_info.rs @@ -1,37 +1,36 @@ use proc_macro2::{Span, TokenStream}; use quote::format_ident; -use syn::{DeriveInput, Ident, parse_macro_input, parse_quote}; +use syn::{DeriveInput, Ident, parse_macro_input}; +use crate::derive::SharedArgs; + +#[derive(Default)] struct Args { - bms_display_path: syn::Path, remote: bool, + shared_args: SharedArgs, } impl Args { fn parse(input: &[syn::Attribute]) -> syn::Result { let mut args = Args { - bms_display_path: parse_quote!(::bevy_mod_scripting::display), + shared_args: Default::default(), remote: false, }; for attr in input { if attr.path().is_ident("debug_with_type_info") { attr.parse_nested_meta(|meta| { - if meta.path.is_ident("bms_display_path") { - let value = meta.value()?; - let string: syn::LitStr = value.parse()?; - args.bms_display_path = string.parse()?; - Ok(()) - } else if meta.path.is_ident("remote") { + if meta.path.is_ident("remote") { args.remote = true; - Ok(()) - } else { - Err(syn::Error::new_spanned( - meta.path, - "unknown attribute, allowed: bms_display_path,remote", - )) + return Ok(()); + } + + if args.shared_args.apply_nested_meta(&meta)? { + return Ok(()); } - })? + + Err(meta.error("Unknown argument to debug_with_type_info")) + })?; } } @@ -48,7 +47,7 @@ pub fn debug_with_type_info(input: proc_macro::TokenStream) -> proc_macro::Token Err(e) => return e.to_compile_error().into(), }; - let bms_display_path = &args.bms_display_path; + let bms_display_path = &args.shared_args.bms_display_path; // use DebugStruct, DebugTuple, DebugMap, DebugList etc from bevy_mod_scripting_display // from the trait DebugWithTypeInfoBuilder trait on the formatter in implementing Debug i.e.: diff --git a/crates/bevy_mod_scripting_derive/src/derive/from_script.rs b/crates/bevy_mod_scripting_derive/src/derive/from_script.rs new file mode 100644 index 0000000000..12efe3a352 --- /dev/null +++ b/crates/bevy_mod_scripting_derive/src/derive/from_script.rs @@ -0,0 +1,61 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::DeriveInput; + +use crate::derive::SharedArgs; + +#[derive(Default)] +struct Args { + shared_args: SharedArgs, +} + +impl Args { + fn parse(attrs: &[syn::Attribute]) -> syn::Result { + let mut shared_args = SharedArgs::default(); + + for attr in attrs { + if attr.path().is_ident("from_script") { + attr.parse_nested_meta(|meta| { + if shared_args.apply_nested_meta(&meta)? { + return Ok(()); + } + + Err(meta.error("Unknown argument to into_script")) + })?; + } + } + + Ok(Self { shared_args }) + } +} + +pub fn from_script(input: TokenStream) -> TokenStream { + let (args, ident, generics) = match syn::parse2::(input) { + Ok(derive_input) => { + let args = match Args::parse(&derive_input.attrs) { + Ok(args) => args, + Err(error) => return error.to_compile_error(), + }; + (args, derive_input.ident, derive_input.generics) + } + Err(err) => return err.to_compile_error(), + }; + + let (impl_generics, type_generics, where_clause) = generics.split_for_impl(); + let bms_bindings_path = args.shared_args.bms_bindings_path; + quote! { + + impl #impl_generics #bms_bindings_path::FromScript for #ident #type_generics #where_clause { + type This<'w> = #ident #type_generics; + fn from_script( + value: #bms_bindings_path::ScriptValue, + world: #bms_bindings_path::WorldGuard<'_>, + ) -> Result, #bms_bindings_path::InteropError> + where + Self: Sized, + { + #bms_bindings_path::V::::from_script(value, world).map(#bms_bindings_path::V::into_inner) + } + } + } +} diff --git a/crates/bevy_mod_scripting_derive/src/derive/get_type_dependencies.rs b/crates/bevy_mod_scripting_derive/src/derive/get_type_dependencies.rs index 5df56c40dd..c5c7765f41 100644 --- a/crates/bevy_mod_scripting_derive/src/derive/get_type_dependencies.rs +++ b/crates/bevy_mod_scripting_derive/src/derive/get_type_dependencies.rs @@ -2,6 +2,8 @@ use proc_macro2::TokenStream; use quote::{ToTokens, quote_spanned}; use syn::{DeriveInput, WhereClause, parse_quote, parse_quote_spanned}; +use crate::derive::SharedArgs; + /// Generate a GetTypeDependencies impl like below: /// For type: /// @@ -36,7 +38,7 @@ fn get_type_dependencies_from_input(derive_input: DeriveInput) -> TokenStream { Err(error) => return error.to_compile_error(), }; - let bms_core = &args.bms_bindings_path; + let bms_bindings_path = &args.shared_args.bms_bindings_path; let (impl_generics, type_generics, impl_where) = derive_input.generics.split_for_impl(); @@ -89,7 +91,7 @@ fn get_type_dependencies_from_input(derive_input: DeriveInput) -> TokenStream { quote_spanned! {derive_input.ident.span()=> #[automatically_derived] #[allow(clippy::needless_lifetimes)] - impl #impl_generics #bms_core::GetTypeDependencies for #name #type_generics #impl_where + impl #impl_generics #bms_bindings_path::GetTypeDependencies for #name #type_generics #impl_where { type Underlying = #underlying; fn register_type_dependencies(registry: &mut TypeRegistry) { @@ -110,51 +112,47 @@ pub fn get_type_dependencies(input: TokenStream) -> TokenStream { get_type_dependencies_from_input(derive_input) } +#[derive(Default)] struct Args { - bms_bindings_path: syn::Path, underlying: Option, dont_recurse: bool, - // bounds: Vec, + shared_args: SharedArgs, } impl Args { fn parse(attrs: &[syn::Attribute]) -> syn::Result { - let mut bms_bindings_path = parse_quote!(::bevy_mod_scripting::bindings); let mut underlying = None; let mut dont_recurse = false; + let mut shared_args = SharedArgs::default(); for attr in attrs { - // find attr with name `get_type_dependencies` - // then parse its meta if attr.path().is_ident("get_type_dependencies") { attr.parse_nested_meta(|meta| { - if meta.path.is_ident("bms_bindings_path") { - let value = meta.value()?; - let string: syn::LitStr = value.parse()?; - bms_bindings_path = string.parse()?; - Ok(()) - } else if meta.path.is_ident("underlying") { + if meta.path.is_ident("underlying") { let value = meta.value()?; let string: syn::LitStr = value.parse()?; underlying = Some(string.parse()?); - Ok(()) - } else if meta.path.is_ident("dont_recurse") { + return Ok(()); + } + + if meta.path.is_ident("dont_recurse") { dont_recurse = true; - Ok(()) - } else { - Err(syn::Error::new_spanned( - meta.path, - "unknown attribute, allowed: bms_bindings_path, underlying", - )) + return Ok(()); + } + + if shared_args.apply_nested_meta(&meta)? { + return Ok(()); } + + Err(meta.error("Unknown argument to get_type_dependencies")) })?; } } Ok(Self { - bms_bindings_path, underlying, dont_recurse, + shared_args, }) } } diff --git a/crates/bevy_mod_scripting_derive/src/derive/into_script.rs b/crates/bevy_mod_scripting_derive/src/derive/into_script.rs index 6879f5d58d..66959b003b 100644 --- a/crates/bevy_mod_scripting_derive/src/derive/into_script.rs +++ b/crates/bevy_mod_scripting_derive/src/derive/into_script.rs @@ -2,21 +2,52 @@ use proc_macro2::TokenStream; use quote::quote; use syn::DeriveInput; +use crate::derive::SharedArgs; + +#[derive(Default)] +struct Args { + shared_args: SharedArgs, +} + +impl Args { + fn parse(attrs: &[syn::Attribute]) -> syn::Result { + let mut shared_args = SharedArgs::default(); + + for attr in attrs { + if attr.path().is_ident("into_script") { + attr.parse_nested_meta(|meta| { + if shared_args.apply_nested_meta(&meta)? { + return Ok(()); + } + + Err(meta.error("Unknown argument to into_script")) + })?; + } + } + + Ok(Self { shared_args }) + } +} + pub fn into_script(input: TokenStream) -> TokenStream { - let (ident, generics) = match syn::parse2(input) { - Ok(DeriveInput { - ident, generics, .. - }) => (ident, generics), + let (args, ident, generics) = match syn::parse2::(input) { + Ok(derive_input) => { + let args = match Args::parse(&derive_input.attrs) { + Ok(args) => args, + Err(error) => return error.to_compile_error(), + }; + (args, derive_input.ident, derive_input.generics) + } Err(err) => return err.to_compile_error(), }; let (impl_generics, type_generics, where_clause) = generics.split_for_impl(); - + let bms_bindings_path = args.shared_args.bms_bindings_path; quote! { - impl #impl_generics ::bevy_mod_scripting::bindings::function::into::IntoScript for #ident #type_generics #where_clause { - fn into_script(self, world: ::bevy_mod_scripting::bindings::WorldGuard) -> Result<::bevy_mod_scripting::bindings::script_value::ScriptValue, ::bevy_mod_scripting::bindings::error::InteropError> { - ::bevy_mod_scripting::bindings::function::into::IntoScript::into_script( - ::bevy_mod_scripting::bindings::function::from::Val(self), + impl #impl_generics #bms_bindings_path::IntoScript for #ident #type_generics #where_clause { + fn into_script(self, world: #bms_bindings_path::WorldGuard) -> Result<#bms_bindings_path::ScriptValue, #bms_bindings_path::InteropError> { + #bms_bindings_path::function::into::IntoScript::into_script( + #bms_bindings_path::V(self), world, ) } diff --git a/crates/bevy_mod_scripting_derive/src/derive/mod.rs b/crates/bevy_mod_scripting_derive/src/derive/mod.rs index 60a3c7b9b7..8194777b58 100644 --- a/crates/bevy_mod_scripting_derive/src/derive/mod.rs +++ b/crates/bevy_mod_scripting_derive/src/derive/mod.rs @@ -1,4 +1,6 @@ +mod arg_meta; mod debug_with_type_info; +mod from_script; mod get_type_dependencies; mod into_script; mod script_bindings; @@ -10,11 +12,78 @@ use quote::{ToTokens, quote_spanned}; use syn::{Ident, ImplItemFn, ItemImpl}; pub use self::{ - debug_with_type_info::debug_with_type_info, get_type_dependencies::get_type_dependencies, - into_script::into_script, script_bindings::script_bindings, script_globals::script_globals, - typed_through::typed_through, + arg_meta::arg_meta, debug_with_type_info::debug_with_type_info, from_script::from_script, + get_type_dependencies::get_type_dependencies, into_script::into_script, + script_bindings::script_bindings, script_globals::script_globals, typed_through::typed_through, }; +#[allow(dead_code)] +pub(crate) struct SharedArgs { + /// If set the path to override bms bindings root path + pub bms_bindings_path: syn::Path, + /// If set the path to override bms core root path + pub bms_core_path: syn::Path, + /// If set the path to override bms display root path + pub bms_display_path: syn::Path, +} + +impl Default for SharedArgs { + fn default() -> Self { + let bms_path = syn::Path::from(syn::Ident::new("bevy_mod_scripting", Span::call_site())); + + let mut bms_bindings_path = bms_path.clone(); + bms_bindings_path.segments.push(syn::PathSegment { + ident: syn::Ident::new("bindings", Span::call_site()), + arguments: syn::PathArguments::None, + }); + let mut bms_core_path = bms_path.clone(); + bms_core_path.segments.push(syn::PathSegment { + ident: syn::Ident::new("core", Span::call_site()), + arguments: syn::PathArguments::None, + }); + let mut bms_display_path = bms_path.clone(); + bms_display_path.segments.push(syn::PathSegment { + ident: syn::Ident::new("display", Span::call_site()), + arguments: syn::PathArguments::None, + }); + Self { + bms_bindings_path, + bms_core_path, + bms_display_path, + } + } +} + +impl SharedArgs { + const BMS_BINDINGS_PATH: &'static str = "bms_bindings_path"; + const BMS_CORE_PATH: &'static str = "bms_core_path"; + const BMS_DISPLAY_PATH: &'static str = "bms_display_path"; + pub fn apply_nested_meta(&mut self, meta: &syn::meta::ParseNestedMeta) -> syn::Result { + if meta.path.is_ident(Self::BMS_BINDINGS_PATH) { + let value = meta.value()?; + let lit: syn::LitStr = value.parse()?; + self.bms_bindings_path = syn::parse_str(&lit.value())?; + return Ok(true); + } + + if meta.path.is_ident(Self::BMS_CORE_PATH) { + let value = meta.value()?; + let lit: syn::LitStr = value.parse()?; + self.bms_core_path = syn::parse_str(&lit.value())?; + return Ok(true); + } + + if meta.path.is_ident(Self::BMS_DISPLAY_PATH) { + let value = meta.value()?; + let lit: syn::LitStr = value.parse()?; + self.bms_display_path = syn::parse_str(&lit.value())?; + return Ok(true); + } + + Ok(false) + } +} + pub(crate) fn impl_fn_to_namespace_builder_registration(fun: &ImplItemFn) -> TokenStream { process_impl_fn( fun, diff --git a/crates/bevy_mod_scripting_derive/src/derive/script_bindings.rs b/crates/bevy_mod_scripting_derive/src/derive/script_bindings.rs index 8bc9debc5b..adc245b456 100644 --- a/crates/bevy_mod_scripting_derive/src/derive/script_bindings.rs +++ b/crates/bevy_mod_scripting_derive/src/derive/script_bindings.rs @@ -2,14 +2,17 @@ use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote_spanned}; use syn::{ItemImpl, spanned::Spanned}; +use crate::derive::SharedArgs; + use super::{impl_fn_to_namespace_builder_registration, is_public_impl}; pub fn script_bindings( - args: proc_macro::TokenStream, + args_stream: proc_macro::TokenStream, input: proc_macro::TokenStream, ) -> proc_macro::TokenStream { - let args = syn::parse_macro_input!(args as Args); - + let mut args = Args::default(); + let parser = syn::meta::parser(|meta| args.apply_nested_meta(&meta)); + syn::parse_macro_input!(args_stream with parser); let impl_block = syn::parse_macro_input!(input as ItemImpl); let impl_span = impl_block.span(); // let (impl_generics, ty_generics, where_clause) = impl_block.generics.split_for_impl(); @@ -42,7 +45,7 @@ pub fn script_bindings( }, }; - let bms_bindings_path = &args.bms_bindings_path; + let bms_bindings_path = &args.shared_args.bms_bindings_path; let function_name = format_ident!("register_{}", args.name); let builder_function_name = if args.unregistered { @@ -112,8 +115,6 @@ struct Args { pub name: syn::Ident, /// If true the original impl block will be ignored, and only the function registrations will be generated pub remote: bool, - /// If set the path to override bms imports - pub bms_bindings_path: syn::Path, /// If true will use `new_unregistered` instead of `new` for the namespace builder pub unregistered: bool, /// If true registers a marker type against the type registry to state that the type is generated (if unregistered is not set) @@ -125,88 +126,68 @@ struct Args { /// If true will register into the [`DummyScriptFunctionRegistry`] instead of the full one. /// This is useful for documenting functions without actually making them available, if you're exposing them another way. pub use_dummy_registry: bool, + + pub shared_args: SharedArgs, } -impl syn::parse::Parse for Args { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - // parse separated key-value pairs - let pairs = - syn::punctuated::Punctuated::::parse_terminated(input)?; - - let mut name = syn::Ident::new("functions", Span::call_site()); - let mut remote = false; - let mut unregistered = false; - let mut generated = false; - let mut core = false; - let mut significant = false; - let mut use_dummy_registry = false; - let mut bms_bindings_path = - syn::Path::from(syn::Ident::new("bevy_mod_scripting", Span::call_site())); - bms_bindings_path.segments.push(syn::PathSegment { - ident: syn::Ident::new("bindings", Span::call_site()), - arguments: syn::PathArguments::None, - }); - let mut unknown_spans = Vec::default(); - for pair in pairs { - match &pair { - syn::Meta::Path(path) => { - if path.is_ident("remote") { - remote = true; - continue; - } else if path.is_ident("unregistered") { - unregistered = true; - continue; - } else if path.is_ident("generated") { - generated = true; - continue; - } else if path.is_ident("core") { - core = true; - continue; - } else if path.is_ident("significant") { - significant = true; - continue; - } else if path.is_ident("use_dummy_registry") { - use_dummy_registry = true; - continue; - } - } - syn::Meta::NameValue(name_value) => { - if name_value.path.is_ident("bms_bindings_path") - && let syn::Expr::Lit(path) = &name_value.value - && let syn::Lit::Str(lit_str) = &path.lit - { - bms_bindings_path = syn::parse_str(&lit_str.value())?; - continue; - } else if name_value.path.is_ident("name") - && let syn::Expr::Lit(path) = &name_value.value - && let syn::Lit::Str(lit_str) = &path.lit - { - name = syn::parse_str(&lit_str.value())?; - continue; - } - } - _ => { - unknown_spans.push((pair.span(), "Unsupported meta kind for script_bindings")); - continue; - } - } +impl Default for Args { + fn default() -> Self { + Self { + name: syn::Ident::new("functions", Span::call_site()), + remote: Default::default(), + unregistered: Default::default(), + generated: Default::default(), + core: Default::default(), + significant: Default::default(), + use_dummy_registry: Default::default(), + shared_args: Default::default(), + } + } +} + +impl Args { + pub fn apply_nested_meta(&mut self, meta: &syn::meta::ParseNestedMeta) -> syn::Result<()> { + if meta.path.is_ident("name") { + let value = meta.value()?; + let lit: syn::LitStr = value.parse()?; + self.name = syn::parse_str(&lit.value())?; + return Ok(()); + } + + if meta.path.is_ident("remote") { + self.remote = true; + return Ok(()); + } + + if meta.path.is_ident("unregistered") { + self.unregistered = true; + return Ok(()); + } + + if meta.path.is_ident("generated") { + self.generated = true; + return Ok(()); + } + + if meta.path.is_ident("core") { + self.core = true; + return Ok(()); + } + + if meta.path.is_ident("significant") { + self.significant = true; + return Ok(()); + } - unknown_spans.push((pair.span(), "Unknown argument to script_bindings")); + if meta.path.is_ident("use_dummy_registry") { + self.use_dummy_registry = true; + return Ok(()); } - if !unknown_spans.is_empty() { - return Err(syn::Error::new(unknown_spans[0].0, unknown_spans[0].1)); + if self.shared_args.apply_nested_meta(meta)? { + return Ok(()); } - Ok(Self { - remote, - bms_bindings_path, - name, - unregistered, - generated, - core, - significant, - use_dummy_registry, - }) + Err(meta.error("Unknown argument to script_bindings")) } } diff --git a/crates/bevy_mod_scripting_derive/src/derive/script_globals.rs b/crates/bevy_mod_scripting_derive/src/derive/script_globals.rs index ed2931751f..668d463ff4 100644 --- a/crates/bevy_mod_scripting_derive/src/derive/script_globals.rs +++ b/crates/bevy_mod_scripting_derive/src/derive/script_globals.rs @@ -2,13 +2,17 @@ use proc_macro2::Span; use quote::{format_ident, quote_spanned}; use syn::{ItemImpl, spanned::Spanned}; +use crate::derive::SharedArgs; + use super::{impl_fn_to_global_registry_registration, is_public_impl}; pub fn script_globals( - args: proc_macro::TokenStream, + args_stream: proc_macro::TokenStream, input: proc_macro::TokenStream, ) -> proc_macro::TokenStream { - let args = syn::parse_macro_input!(args as Args); + let mut args = Args::default(); + let parser = syn::meta::parser(|meta| args.apply_nested_meta(&meta)); + syn::parse_macro_input!(args_stream with parser); let impl_block = syn::parse_macro_input!(input as ItemImpl); let impl_span = impl_block.span(); @@ -25,7 +29,7 @@ pub fn script_globals( } let function_name = format_ident!("register_{}", args.name); - let bms_bindings_path = &args.bms_bindings_path; + let bms_bindings_path = &args.shared_args.bms_bindings_path; let visibility = match is_public_impl(&impl_block) { true => quote_spanned! {impl_span=> @@ -56,57 +60,32 @@ pub fn script_globals( struct Args { /// The name to use to suffix the generated function, i.e. `test_fn` will generate `register_test_fn pub name: syn::Ident, - /// If set the path to override bms imports - pub bms_bindings_path: syn::Path, + pub shared_args: SharedArgs, } -impl syn::parse::Parse for Args { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - // parse separated key-value pairs - let pairs = - syn::punctuated::Punctuated::::parse_terminated(input)?; - - let mut name = syn::Ident::new("functions", Span::call_site()); - let mut bms_bindings_path = - syn::Path::from(syn::Ident::new("bevy_mod_scripting", Span::call_site())); - bms_bindings_path.segments.push(syn::PathSegment { - ident: syn::Ident::new("core", Span::call_site()), - arguments: syn::PathArguments::None, - }); - let mut unknown_spans = Vec::default(); - for pair in pairs { - match &pair { - syn::Meta::NameValue(name_value) => { - if name_value.path.is_ident("bms_bindings_path") - && let syn::Expr::Lit(path) = &name_value.value - && let syn::Lit::Str(lit_str) = &path.lit - { - bms_bindings_path = syn::parse_str(&lit_str.value())?; - continue; - } else if name_value.path.is_ident("name") - && let syn::Expr::Lit(path) = &name_value.value - && let syn::Lit::Str(lit_str) = &path.lit - { - name = syn::parse_str(&lit_str.value())?; - continue; - } - } - _ => { - unknown_spans.push((pair.span(), "Unsupported meta kind for script_globals")); - continue; - } - } +impl Default for Args { + fn default() -> Self { + Self { + name: syn::Ident::new("functions", Span::call_site()), + shared_args: SharedArgs::default(), + } + } +} - unknown_spans.push((pair.span(), "Unknown argument to script_globals")); +impl Args { + pub fn apply_nested_meta(&mut self, meta: &syn::meta::ParseNestedMeta) -> syn::Result<()> { + if meta.path.is_ident("name") { + let value = meta.value()?; + let lit: syn::LitStr = value.parse()?; + self.name = syn::parse_str(&lit.value())?; + return Ok(()); } - if !unknown_spans.is_empty() { - return Err(syn::Error::new(unknown_spans[0].0, unknown_spans[0].1)); + // delegate shared args + if self.shared_args.apply_nested_meta(meta)? { + return Ok(()); } - Ok(Self { - bms_bindings_path, - name, - }) + Err(meta.error("Unknown argument to script_globals")) } } diff --git a/crates/bevy_mod_scripting_derive/src/derive/typed_through.rs b/crates/bevy_mod_scripting_derive/src/derive/typed_through.rs index 2236fb242e..fcedae8681 100644 --- a/crates/bevy_mod_scripting_derive/src/derive/typed_through.rs +++ b/crates/bevy_mod_scripting_derive/src/derive/typed_through.rs @@ -2,21 +2,52 @@ use proc_macro2::TokenStream; use quote::quote; use syn::DeriveInput; +use crate::derive::SharedArgs; +#[derive(Default)] +struct Args { + shared_args: SharedArgs, +} + +impl Args { + fn parse(attrs: &[syn::Attribute]) -> syn::Result { + let mut shared_args = SharedArgs::default(); + + for attr in attrs { + if attr.path().is_ident("get_type_dependencies") { + attr.parse_nested_meta(|meta| { + // delegate everything to SharedArgs + if shared_args.apply_nested_meta(&meta)? { + return Ok(()); + } + + Err(meta.error("Unknown argument to get_type_dependencies")) + })?; + } + } + + Ok(Self { shared_args }) + } +} pub fn typed_through(input: TokenStream) -> TokenStream { - let (ident, generics) = match syn::parse2(input) { - Ok(DeriveInput { - ident, generics, .. - }) => (ident, generics), + let (args, ident, generics) = match syn::parse2::(input) { + Ok(derive_input) => { + let args = match Args::parse(&derive_input.attrs) { + Ok(args) => args, + Err(error) => return error.to_compile_error(), + }; + (args, derive_input.ident, derive_input.generics) + } Err(err) => return err.to_compile_error(), }; let (impl_generics, type_generics, where_clause) = generics.split_for_impl(); let turbofish = type_generics.as_turbofish(); + let bms_bindings_path = args.shared_args.bms_bindings_path; quote! { - impl #impl_generics ::bevy_mod_scripting::bindings::docgen::typed_through::TypedThrough for #ident #type_generics #where_clause { - fn through_type_info() -> ::bevy_mod_scripting::core::docgen::typed_through::ThroughTypeInfo { - ::bevy_mod_scripting::bindings::docgen::typed_through::ThroughTypeInfo::TypeInfo(#ident #turbofish ::type_info()) + impl #impl_generics #bms_bindings_path::TypedThrough for #ident #type_generics #where_clause { + fn through_type_info() -> #bms_bindings_path::ThroughTypeInfo { + #bms_bindings_path::ThroughTypeInfo::TypeInfo(#ident #turbofish ::type_info()) } } } diff --git a/crates/bevy_mod_scripting_derive/src/lib.rs b/crates/bevy_mod_scripting_derive/src/lib.rs index 8f931cc005..c9bdc9ac58 100644 --- a/crates/bevy_mod_scripting_derive/src/lib.rs +++ b/crates/bevy_mod_scripting_derive/src/lib.rs @@ -2,18 +2,30 @@ mod derive; -#[proc_macro_derive(TypedThrough)] +#[proc_macro_derive(TypedThrough, attributes(typed_through))] /// Default implementation for the `TypedThrough` trait pub fn typed_through(input: proc_macro::TokenStream) -> proc_macro::TokenStream { derive::typed_through(input.into()).into() } -#[proc_macro_derive(IntoScript)] +#[proc_macro_derive(ArgMeta, attributes(arg_meta))] +/// Default implementation for the `TypedThrough` trait +pub fn arg_meta(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + derive::arg_meta(input.into()).into() +} + +#[proc_macro_derive(IntoScript, attributes(into_script))] /// Default implementation for the `IntoScript` trait pub fn into_script(input: proc_macro::TokenStream) -> proc_macro::TokenStream { derive::into_script(input.into()).into() } +#[proc_macro_derive(FromScript, attributes(from_script))] +/// Default implementation for the `IntoScript` trait +pub fn from_script(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + derive::from_script(input.into()).into() +} + /// Derive macro for generating script bindings from an impl block. /// /// Generates a registration function with visibility determined by the highest visibility in the impl block. diff --git a/docs/src/ReleaseNotes/0.20.0.md b/docs/src/ReleaseNotes/0.20.0.md index 34432bcf43..772abb9450 100644 --- a/docs/src/ReleaseNotes/0.20.0.md +++ b/docs/src/ReleaseNotes/0.20.0.md @@ -64,4 +64,28 @@ the `CachedRegistry` trait was introduced to store registries such as: etc. in a type-erased way. -This allowed `bevy_mod_scripting_display` to directly rely on world types, and removed a lot of awkward bridging code. \ No newline at end of file +This allowed `bevy_mod_scripting_display` to directly rely on world types, and removed a lot of awkward bridging code. + + +# More Derive Macros +The following macros derive macros are now available: `ArgMeta`,`FromScript`. + +This means for types you want to clone into scripts you can make convenient wrappers like so: + +```rust +#[derive(Clone, TypedThrough, GetTypeDependencies, ArgMeta, IntoScript, FromScript, Reflect)] +pub struct MyThing(usize); + + +#[script_bindings(remote)] +impl MyThing { + // Can now be used directly without V<> wrapper, acts exactly like V + pub fn test(thing: MyThing) -> MyThing { + thing + } +} +``` + +And overriding either the into or from implementations if you wish easilly. + + diff --git a/docs/src/Summary/controlling-script-bindings.md b/docs/src/Summary/controlling-script-bindings.md index 2961cb4509..74e6bc6029 100644 --- a/docs/src/Summary/controlling-script-bindings.md +++ b/docs/src/Summary/controlling-script-bindings.md @@ -84,6 +84,29 @@ pub fn main() { Note the documentation will automatically be picked up and stored for the purposes of reflection and documentation generation, including argument/return type specific docs. +## Deriving FromScript and IntoScript + +Generally all reflect types are wrapped in: `R` `V` or `M` when writing script bindings. However some types may opt in to implement `FromScript` or `IntoScript` directly. + +Doing this requires implementing a set of other traits which tell the runtime how to manage your type in conversion and function dispatch. + +The most trivial way to allow your type to be used directly in bindings is to apply derive macros like so: + +```rust +#[derive(Clone, TypedThrough, GetTypeDependencies, ArgMeta, IntoScript, FromScript, Reflect)] +pub struct MyThing(usize); + + +#[script_bindings(remote)] +impl MyThing { + // Can now be used directly without V<> wrapper, acts exactly like V + pub fn test(thing: MyThing) -> MyThing { + thing + } +} +``` + +If you wish to customize the `FromScript` implementation, simply remove that macro and implement it yourself. See [the custom conversions example](https://github.com/makspll/bevy_mod_scripting/blob/main/examples/docgen.rs) for more information. ## Context Arguments diff --git a/examples/custom_conversions.rs b/examples/custom_conversions.rs new file mode 100644 index 0000000000..2f6346bfb0 --- /dev/null +++ b/examples/custom_conversions.rs @@ -0,0 +1,149 @@ +// The standard way of interacting with reflect types is via R V and M wrappers. +// These do the following under the hood: +// - Claim the necessary access from underlying references +// - downcast, construct or clone the reflect value as needed to generate a reference +// You may want your types to convert from more than just references, i.e. primitives or perhaps from a variety other types. + +// you can do this by implementing FromScript and IntoScript as needed + +use std::any::TypeId; + +use bevy::reflect::Typed; +use bevy::{prelude::*, reflect::TypeRegistry}; +use bevy_mod_scripting::prelude::*; +use bevy_mod_scripting_bindings::{ + AppReflectAllocator, FromScript, InteropError, ReflectReference, WorldExtensions, +}; + +#[derive(Clone, TypedThrough, GetTypeDependencies, ArgMeta, IntoScript, Reflect)] +pub enum MyFunkyArgumentType { + Nil, + Number(usize), + HandleToSprite(Handle), +} + +impl FromScript for MyFunkyArgumentType { + // generally this is always Self, unless you are implementing complex wrapper types + type This<'w> = Self; + + fn from_script( + value: ScriptValue, + world: bevy_mod_scripting_bindings::WorldGuard<'_>, + ) -> std::result::Result, bevy_mod_scripting_bindings::InteropError> + where + Self: Sized, + { + match value { + ScriptValue::Unit => Ok(Self::Nil), + ScriptValue::Bool(_) => todo!(), + ScriptValue::Integer(i) => Ok(Self::Number(i as usize)), + ScriptValue::Float(f) => Ok(Self::Number(f as usize)), + ScriptValue::Reference(reflect_reference) => { + // references are backed by an allocator over reflected values + + // you can use with_reflect if you don't want to clone the value, otherwise use downcast + reflect_reference.with_reflect(world, |my_val| { + // and here we have a normal reflect reference + let me = my_val + .try_downcast_ref::() + .ok_or_else(|| InteropError::str("expected myfinkyargumenttype"))?; + + Ok(me.clone()) + })? + } + // you can probably make this error better, this will just show "v != MyFunkyType" but this is the quickest way to get this working nicely. + _ => Err(InteropError::value_mismatch(TypeId::of::(), value)), + } + } +} + +// you can do the same thing for IntoScript + +#[derive(Clone, TypedThrough, GetTypeDependencies, ArgMeta, FromScript, Reflect)] +pub enum MyFunkyReturnType { + Nil, + Number(usize), + HandleToSprite(Handle), +} + +impl bevy_mod_scripting::bindings::IntoScript for MyFunkyReturnType { + fn into_script( + self, + world: bevy_mod_scripting_bindings::WorldGuard, + ) -> std::result::Result { + match self { + // note if we return primitives, the type is no longer callable! + // i.e. these get converted to primitive lua types + MyFunkyReturnType::Nil => Ok(ScriptValue::Unit), + MyFunkyReturnType::Number(n) => Ok(ScriptValue::Integer(n as i64)), + // references stay callabke though, in this case we change the type + MyFunkyReturnType::HandleToSprite(handle) => { + // here in order to create a reference to this handle, we must allocate it first + let allocator = world.allocator(); + let mut allocator_guard = allocator.write(); + let allocated = ReflectReference::new_allocated(handle, &mut allocator_guard); + Ok(allocated.into()) + } + } + } +} + +// these can both participate in bindings now + +#[script_bindings(remote, unregistered)] +impl World { + pub fn my_funky_function(a: MyFunkyArgumentType) -> MyFunkyReturnType { + match a { + MyFunkyArgumentType::Nil => MyFunkyReturnType::Nil, + MyFunkyArgumentType::Number(n) => MyFunkyReturnType::Number(n), + MyFunkyArgumentType::HandleToSprite(handle) => { + MyFunkyReturnType::HandleToSprite(handle) + } + } + } +} +callback_labels!(OnExample => "on_example"); +pub fn main() { + let mut app = App::new(); + // required for bindings to actually be visibly + register_functions(app.world_mut()); + + app.add_plugins(DefaultPlugins) + .add_plugins(BMSPlugin) + .add_systems( + Startup, + move |mut commands: Commands, + mut script_assets: ResMut>, + mut callbacks: MessageWriter, + allocator: ResMut| { + let content = r#" + function on_example(my_funky_argument_payload) + print("input:", my_funky_argument_payload) + local output = world.my_funky_function(my_funky_argument_payload) + print("output:", output) + return output + end + "#; + let script_asset = ScriptAsset::new(content).with_language(Language::Lua); + let handle = script_assets.add(script_asset); + let attachment = ScriptAttachment::StaticScript(handle.clone()); + + let mut allocator = allocator.write(); + let reference_payload = + ReflectReference::new_allocated(MyFunkyArgumentType::Number(2), &mut allocator); + + commands.queue(AttachScript::::new(attachment.clone())); + callbacks.write( + ScriptCallbackEvent::new_for_static_script( + OnExample, + vec![reference_payload.into()], + handle, + ) + .with_response(), + ); + }, + ) + .add_systems(Update, event_handler::); + + app.run(); +} diff --git a/src/prelude.rs b/src/prelude.rs index c3c0e6d7b6..4fd705e181 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -23,3 +23,5 @@ pub use bevy_mod_scripting_lua::LuaScriptingPlugin; pub use bevy_mod_scripting_rhai::RhaiScriptingPlugin; pub use crate::{BMSPlugin, ScriptFunctionsPlugin}; + +pub use bevy_mod_scripting_derive::*;