diff --git a/.github/workflows/bevy_mod_scripting.yml b/.github/workflows/bevy_mod_scripting.yml index 0a050e75e1..4b4e3490c0 100644 --- a/.github/workflows/bevy_mod_scripting.yml +++ b/.github/workflows/bevy_mod_scripting.yml @@ -85,20 +85,20 @@ jobs: matrix: run_args: ${{fromJson(needs.generate-job-matrix.outputs.matrix)}} steps: - - name: Free Disk Space (Ubuntu) - if: runner.os == 'Linux' - uses: jlumbroso/free-disk-space@main - with: - tool-cache: false - android: true - dotnet: true - haskell: true - large-packages: true - docker-images: true - swap-storage: true - # - if: runner.os == 'linux' - # run: | - # sudo rm -rf /usr/share/dotnet; sudo rm -rf /opt/ghc; sudo rm -rf "/usr/local/share/boost"; sudo rm -rf "$AGENT_TOOLSDIRECTORY" + # - name: Free Disk Space (Ubuntu) + # if: runner.os == 'Linux' + # uses: jlumbroso/free-disk-space@main + # with: + # tool-cache: false + # android: true + # dotnet: true + # haskell: true + # large-packages: true + # docker-images: true + # swap-storage: true + # # - if: runner.os == 'linux' + # # run: | + # # sudo rm -rf /usr/share/dotnet; sudo rm -rf /opt/ghc; sudo rm -rf "/usr/local/share/boost"; sudo rm -rf "$AGENT_TOOLSDIRECTORY" - name: Checkout if: ${{ needs.check-needs-run.outputs.any-changes == 'true' }} uses: actions/checkout@v4 @@ -120,10 +120,19 @@ jobs: run: | cargo xtask init + - name: Setup GPU Drivers + if: ${{ needs.check-needs-run.outputs.any-changes == 'true' && matrix.run_args.requires_gpu }} + run: | + sudo add-apt-repository ppa:kisak/turtle -y + sudo apt-get install --no-install-recommends libxkbcommon-x11-0 xvfb libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers - name: Check - if: ${{ needs.check-needs-run.outputs.any-changes == 'true' }} + if: ${{ needs.check-needs-run.outputs.any-changes == 'true' && !matrix.run_args.requires_gpu }} run: | ${{ matrix.run_args.command }} + - name: Check With virtual X11 server + if: ${{ needs.check-needs-run.outputs.any-changes == 'true' && matrix.run_args.requires_gpu }} + run: | + xvfb-run ${{ matrix.run_args.command }} - name: Upload coverage artifact if: ${{ needs.check-needs-run.outputs.any-changes == 'true' && matrix.run_args.generates_coverage }} diff --git a/crates/bevy_mod_scripting_core/src/bindings/globals/core.rs b/crates/bevy_mod_scripting_core/src/bindings/globals/core.rs new file mode 100644 index 0000000000..e0006e84c0 --- /dev/null +++ b/crates/bevy_mod_scripting_core/src/bindings/globals/core.rs @@ -0,0 +1,40 @@ +//! Core globals exposed by the BMS framework + +use bevy::{app::Plugin, ecs::reflect::AppTypeRegistry}; + +use super::AppScriptGlobalsRegistry; + +/// A plugin introducing core globals for the BMS framework +pub struct CoreScriptGlobalsPlugin; + +impl Plugin for CoreScriptGlobalsPlugin { + fn build(&self, app: &mut bevy::app::App) { + let global_registry = app + .world_mut() + .get_resource_or_init::() + .clone(); + let type_registry = app + .world_mut() + .get_resource_or_init::() + .clone(); + let mut global_registry = global_registry.write(); + let type_registry = type_registry.read(); + + // find all reflectable types without generics + for registration in type_registry.iter() { + if !registration.type_info().generics().is_empty() { + continue; + } + + if let Some(global_name) = registration.type_info().type_path_table().ident() { + let documentation = "A reference to the type, allowing you to call static methods."; + global_registry.register_static_documented_dynamic( + registration.type_id(), + None, + global_name.into(), + documentation.into(), + ) + } + } + } +} diff --git a/crates/bevy_mod_scripting_core/src/bindings/globals/mod.rs b/crates/bevy_mod_scripting_core/src/bindings/globals/mod.rs new file mode 100644 index 0000000000..22ed1f0199 --- /dev/null +++ b/crates/bevy_mod_scripting_core/src/bindings/globals/mod.rs @@ -0,0 +1,266 @@ +//! Contains abstractions for exposing "globals" to scripts, in a language-agnostic way. + +use super::{ + function::arg_meta::{ScriptReturn, TypedScriptReturn}, + script_value::ScriptValue, + WorldGuard, +}; +use crate::{docgen::typed_through::ThroughTypeInfo, error::InteropError}; +use bevy::{ecs::system::Resource, utils::hashbrown::HashMap}; +use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard}; +use std::{any::TypeId, borrow::Cow, sync::Arc}; + +pub mod core; + +/// A send + sync wrapper around the [`ScriptGlobalsRegistry`]. +#[derive(Default, Resource, Clone)] +pub struct AppScriptGlobalsRegistry(Arc>); + +impl AppScriptGlobalsRegistry { + /// Returns a reference to the inner [`ScriptGlobalsRegistry`]. + pub fn read(&self) -> RwLockReadGuard { + self.0.read() + } + + /// Returns a mutable reference to the inner [`ScriptGlobalsRegistry`]. + pub fn write(&self) -> RwLockWriteGuard { + self.0.write() + } +} + +/// A function that creates a global variable. +pub type ScriptGlobalMakerFn = + dyn Fn(WorldGuard) -> Result + 'static + Send + Sync; + +/// A global variable that can be exposed to scripts. +pub struct ScriptGlobal { + /// The function that creates the global variable. + /// if not present, this is assumed to be a static global, one that + /// cannot be instantiated, but carries type information. + pub maker: Option>>, + /// The documentation for the global variable. + pub documentation: Option>, + /// The type ID of the global variable. + pub type_id: TypeId, + /// Rich type information the global variable. + pub type_information: Option, +} + +/// A registry of global variables that can be exposed to scripts. +#[derive(Default)] +pub struct ScriptGlobalsRegistry { + globals: HashMap, ScriptGlobal>, +} + +impl ScriptGlobalsRegistry { + /// Gets the global with the given name + pub fn get(&self, name: &str) -> Option<&ScriptGlobal> { + self.globals.get(name) + } + + /// Gets the global with the given name mutably + pub fn get_mut(&mut self, name: &str) -> Option<&mut ScriptGlobal> { + self.globals.get_mut(name) + } + + /// Counts the number of globals in the registry + pub fn len(&self) -> usize { + self.globals.len() + } + + /// Checks if the registry is empty + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Iterates over the globals in the registry + pub fn iter(&self) -> impl Iterator, &ScriptGlobal)> { + self.globals.iter() + } + + /// Iterates over the globals in the registry mutably + pub fn iter_mut(&mut self) -> impl Iterator, &mut ScriptGlobal)> { + self.globals.iter_mut() + } + + fn type_erase_maker< + T: ScriptReturn, + F: Fn(WorldGuard) -> Result + Send + Sync + 'static, + >( + maker: F, + ) -> Arc> { + Arc::new(move |world| T::into_script(maker(world.clone())?, world)) + } + + /// Inserts a global into the registry, returns the previous value if it existed + pub fn register< + T: ScriptReturn + 'static, + F: Fn(WorldGuard) -> Result + 'static + Send + Sync, + >( + &mut self, + name: Cow<'static, str>, + maker: F, + ) -> Option { + self.globals.insert( + name, + ScriptGlobal { + maker: Some(Self::type_erase_maker(maker)), + documentation: None, + type_id: TypeId::of::(), + type_information: None, + }, + ) + } + + /// Inserts a global into the registry, returns the previous value if it existed. + /// + /// This is a version of [`Self::register`] which stores type information regarding the global. + pub fn register_documented< + T: TypedScriptReturn + 'static, + F: Fn(WorldGuard) -> Result + 'static + Send + Sync, + >( + &mut self, + name: Cow<'static, str>, + maker: F, + documentation: Cow<'static, str>, + ) -> Option { + self.globals.insert( + name, + ScriptGlobal { + maker: Some(Self::type_erase_maker(maker)), + documentation: Some(documentation), + type_id: TypeId::of::(), + type_information: Some(T::through_type_info()), + }, + ) + } + + /// Registers a static global into the registry. + pub fn register_static(&mut self, name: Cow<'static, str>) { + self.globals.insert( + name, + ScriptGlobal { + maker: None, + documentation: None, + type_id: TypeId::of::(), + type_information: None, + }, + ); + } + + /// Registers a static global into the registry. + /// + /// This is a version of [`Self::register_static`] which stores rich type information regarding the global. + pub fn register_static_documented( + &mut self, + name: Cow<'static, str>, + documentation: Cow<'static, str>, + ) { + self.globals.insert( + name, + ScriptGlobal { + maker: None, + documentation: Some(documentation), + type_id: TypeId::of::(), + type_information: Some(T::through_type_info()), + }, + ); + } + + /// Registers a static global into the registry. + /// + /// This is a version of [`Self::register_static_documented`] which does not require compile time type knowledge. + pub fn register_static_documented_dynamic( + &mut self, + type_id: TypeId, + type_information: Option, + name: Cow<'static, str>, + documentation: Cow<'static, str>, + ) { + self.globals.insert( + name, + ScriptGlobal { + maker: None, + documentation: Some(documentation), + type_id, + type_information, + }, + ); + } +} + +#[cfg(test)] +mod test { + use bevy::ecs::world::World; + + use super::*; + + #[test] + fn test_script_globals_registry() { + let mut registry = ScriptGlobalsRegistry::default(); + + let maker = |_: WorldGuard| Ok(ScriptValue::from(42)); + let maker2 = |_: WorldGuard| Ok(ScriptValue::from(43)); + + assert_eq!(registry.len(), 0); + assert!(registry.is_empty()); + + assert!(registry.register(Cow::Borrowed("foo"), maker).is_none()); + assert_eq!(registry.len(), 1); + + assert_eq!( + (registry.get("foo").unwrap().maker.clone().unwrap())(WorldGuard::new( + &mut World::new() + )) + .unwrap(), + ScriptValue::from(42) + ); + + assert!(registry.register(Cow::Borrowed("foo"), maker2).is_some()); + assert_eq!(registry.len(), 1); + + assert_eq!( + (registry.get("foo").unwrap().maker.clone().unwrap())(WorldGuard::new( + &mut World::new() + )) + .unwrap(), + ScriptValue::from(43) + ); + } + + #[test] + fn test_documentation_is_stored() { + let mut registry = ScriptGlobalsRegistry::default(); + + let maker = |_: WorldGuard| Ok(ScriptValue::from(42)); + + assert!(registry + .register_documented(Cow::Borrowed("foo"), maker, Cow::Borrowed("This is a test")) + .is_none()); + + let global = registry.get("foo").unwrap(); + assert_eq!(global.documentation.as_deref(), Some("This is a test")); + } + + #[test] + fn test_static_globals() { + let mut registry = ScriptGlobalsRegistry::default(); + + registry.register_static::(Cow::Borrowed("foo")); + + let global = registry.get("foo").unwrap(); + assert!(global.maker.is_none()); + assert_eq!(global.type_id, TypeId::of::()); + + // the same but documented + registry.register_static_documented::( + Cow::Borrowed("bar"), + Cow::Borrowed("This is a test"), + ); + + let global = registry.get("bar").unwrap(); + assert!(global.maker.is_none()); + assert_eq!(global.type_id, TypeId::of::()); + assert_eq!(global.documentation.as_deref(), Some("This is a test")); + } +} diff --git a/crates/bevy_mod_scripting_core/src/bindings/mod.rs b/crates/bevy_mod_scripting_core/src/bindings/mod.rs index b41c2da725..9dd2de5e70 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/mod.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/mod.rs @@ -3,6 +3,7 @@ pub mod access_map; pub mod allocator; pub mod function; +pub mod globals; pub mod pretty_print; pub mod query; pub mod reference; diff --git a/crates/bevy_mod_scripting_core/src/lib.rs b/crates/bevy_mod_scripting_core/src/lib.rs index 43bddb9ec3..dcdefea841 100644 --- a/crates/bevy_mod_scripting_core/src/lib.rs +++ b/crates/bevy_mod_scripting_core/src/lib.rs @@ -9,9 +9,12 @@ use asset::{ }; use bevy::prelude::*; use bindings::{ - function::script_function::AppScriptFunctionRegistry, garbage_collector, - schedule::AppScheduleRegistry, script_value::ScriptValue, AppReflectAllocator, - ReflectAllocator, ReflectReference, ScriptTypeRegistration, + function::script_function::AppScriptFunctionRegistry, + garbage_collector, + globals::{core::CoreScriptGlobalsPlugin, AppScriptGlobalsRegistry}, + schedule::AppScheduleRegistry, + script_value::ScriptValue, + AppReflectAllocator, ReflectAllocator, ReflectReference, ScriptTypeRegistration, }; use commands::{AddStaticScript, RemoveStaticScript}; use context::{ @@ -290,6 +293,7 @@ fn once_per_app_init(app: &mut App) { .init_resource::() .init_asset::() .init_resource::() + .init_resource::() .insert_resource(AppScheduleRegistry::new()); app.add_systems( @@ -297,6 +301,8 @@ fn once_per_app_init(app: &mut App) { ((garbage_collector).in_set(ScriptingSystemSet::GarbageCollection),), ); + app.add_plugins(CoreScriptGlobalsPlugin); + configure_asset_systems(app); } diff --git a/crates/bevy_mod_scripting_derive/src/derive/mod.rs b/crates/bevy_mod_scripting_derive/src/derive/mod.rs index 3ae391a536..9c55d9f3d2 100644 --- a/crates/bevy_mod_scripting_derive/src/derive/mod.rs +++ b/crates/bevy_mod_scripting_derive/src/derive/mod.rs @@ -1,4 +1,138 @@ mod into_script; +mod script_bindings; +mod script_globals; mod typed_through; -pub use self::{into_script::into_script, typed_through::typed_through}; +use proc_macro2::{Span, TokenStream}; +use quote::{quote_spanned, ToTokens}; +use syn::{Ident, ImplItemFn, ItemImpl}; + +pub use self::{ + into_script::into_script, script_bindings::script_bindings, script_globals::script_globals, + typed_through::typed_through, +}; + +pub(crate) fn impl_fn_to_namespace_builder_registration(fun: &ImplItemFn) -> TokenStream { + process_impl_fn( + fun, + Ident::new("register_documented", Span::call_site()), + true, + ) +} + +pub(crate) fn impl_fn_to_global_registry_registration(fun: &ImplItemFn) -> TokenStream { + process_impl_fn( + fun, + Ident::new("register_documented", Span::call_site()), + false, + ) +} + +/// checks if the impl contains at least one public function +pub(crate) fn is_public_impl(fun: &ItemImpl) -> bool { + for i in &fun.items { + match i { + syn::ImplItem::Fn(impl_item_fn) => { + if matches!(impl_item_fn.vis, syn::Visibility::Public(..)) { + return true; + } + } + _ => continue, + } + } + + false +} + +/// Converts an impl block function into a function registration, i.e. a closure which will be used to register this function, as well as +/// the target function reference and other metadata +fn process_impl_fn( + fun: &ImplItemFn, + generated_name: Ident, + include_arg_names: bool, +) -> TokenStream { + let args = &fun.sig.inputs; + let fun_span = fun.sig.ident.span(); + + let args_names = match include_arg_names { + true => { + let args = args.iter().map(|arg| match arg { + syn::FnArg::Receiver(_) => syn::LitStr::new("self", Span::call_site()), + syn::FnArg::Typed(pat_type) => { + syn::LitStr::new(&stringify_pat_type(&pat_type.pat), Span::call_site()) + } + }); + + quote_spanned!(fun_span=> + &[#(#args),*] + ) + } + false => Default::default(), + }; + + let body = &fun.block; + let docstring = parse_docstring(fun.attrs.iter()) + .map(|s| syn::LitStr::new(&s, Span::call_site())) + .unwrap_or(syn::LitStr::new("", Span::call_site())); + let fun_name = syn::LitStr::new(&fun.sig.ident.to_string(), Span::call_site()); + let out_type = match &fun.sig.output { + syn::ReturnType::Default => quote_spanned! {fun_span=> + () + }, + syn::ReturnType::Type(_, ty) => quote_spanned! {fun_span=> + #ty + }, + }; + quote_spanned! {fun_span=> + .#generated_name( + #fun_name, + |#args| { + let output: #out_type = {#body}; + output + }, + #docstring, + #args_names + ) + } +} + +pub(crate) fn stringify_pat_type(pat_type: &syn::Pat) -> String { + match pat_type { + syn::Pat::Ident(pat_ident) => pat_ident.ident.to_string(), + syn::Pat::Type(pat_type) => stringify_pat_type(&pat_type.pat), + + p => p.to_token_stream().to_string(), + } +} + +/// Ideally we'd be doing something like rustdoc: https://github.com/rust-lang/rust/blob/124cc92199ffa924f6b4c7cc819a85b65e0c3984/compiler/rustc_resolve/src/rustdoc.rs#L102 +/// but that is too much complexity, stripping the first space should be good enough for now. +pub(crate) fn parse_docstring<'a>( + attrs: impl Iterator, +) -> Option { + let docs = attrs.filter_map(|attr| { + if attr.path().is_ident("doc") { + if let syn::Meta::NameValue(meta_name_value) = &attr.meta { + if let syn::Expr::Lit(expr_lit) = &meta_name_value.value { + if let syn::Lit::Str(lit_str) = &expr_lit.lit { + if lit_str.value().len() > 1 { + return Some(lit_str.value()[1..].to_string()); + } else { + return Some(lit_str.value()); + } + } + } + } + }; + + None + }); + + // join with newline + let docs = docs.collect::>(); + if docs.is_empty() { + return None; + } + + Some(docs.join("\n")) +} diff --git a/crates/bevy_mod_scripting_derive/src/derive/script_bindings.rs b/crates/bevy_mod_scripting_derive/src/derive/script_bindings.rs new file mode 100644 index 0000000000..8dd1569519 --- /dev/null +++ b/crates/bevy_mod_scripting_derive/src/derive/script_bindings.rs @@ -0,0 +1,141 @@ +use proc_macro2::{Span, TokenStream}; +use quote::{format_ident, quote_spanned}; +use syn::{spanned::Spanned, ItemImpl}; + +use super::{impl_fn_to_namespace_builder_registration, is_public_impl}; + +pub fn script_bindings( + args: proc_macro::TokenStream, + input: proc_macro::TokenStream, +) -> proc_macro::TokenStream { + let args = syn::parse_macro_input!(args as Args); + + 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(); + + let type_ident_with_generics = &impl_block.self_ty; + let mut function_registrations = Vec::with_capacity(impl_block.items.len()); + for i in &impl_block.items { + match i { + syn::ImplItem::Fn(impl_item_fn) => { + let fun = impl_fn_to_namespace_builder_registration(impl_item_fn); + function_registrations.push(fun); + } + _ => continue, + } + } + + let visibility = match is_public_impl(&impl_block) { + true => quote_spanned! {impl_span=> + pub + }, + false => quote_spanned! {impl_span=> + pub(crate) + }, + }; + + let impl_block = match args.remote { + true => TokenStream::default(), + false => quote_spanned! {impl_span=> + #impl_block + }, + }; + + let bms_core_path = &args.bms_core_path; + + let function_name = format_ident!("register_{}", args.name); + let builder_function_name = if args.unregistered { + format_ident!("new_unregistered") + } else { + format_ident!("new") + }; + + let out = quote_spanned! {impl_span=> + #visibility fn #function_name(world: &mut bevy::ecs::world::World) { + #bms_core_path::bindings::function::namespace::NamespaceBuilder::<#type_ident_with_generics>::#builder_function_name(world) + #(#function_registrations)*; + } + + #impl_block + }; + + out.into() +} + +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 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_core_path: syn::Path, + /// If true will use `new_unregistered` instead of `new` for the namespace builder + pub unregistered: bool, +} + +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 bms_core_path = + syn::Path::from(syn::Ident::new("bevy_mod_scripting", Span::call_site())); + bms_core_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::Path(path) => { + if path.is_ident("remote") { + remote = true; + continue; + } else if path.is_ident("unregistered") { + unregistered = true; + continue; + } + } + syn::Meta::NameValue(name_value) => { + if name_value.path.is_ident("bms_core_path") { + if let syn::Expr::Lit(path) = &name_value.value { + if let syn::Lit::Str(lit_str) = &path.lit { + bms_core_path = syn::parse_str(&lit_str.value())?; + continue; + } + } + } else if name_value.path.is_ident("name") { + if let syn::Expr::Lit(path) = &name_value.value { + if 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; + } + } + + unknown_spans.push((pair.span(), "Unknown argument to script_bindings")); + } + + if !unknown_spans.is_empty() { + return Err(syn::Error::new(unknown_spans[0].0, unknown_spans[0].1)); + } + + Ok(Self { + remote, + bms_core_path, + name, + unregistered, + }) + } +} diff --git a/crates/bevy_mod_scripting_derive/src/derive/script_globals.rs b/crates/bevy_mod_scripting_derive/src/derive/script_globals.rs new file mode 100644 index 0000000000..8ada9ee4f0 --- /dev/null +++ b/crates/bevy_mod_scripting_derive/src/derive/script_globals.rs @@ -0,0 +1,111 @@ +use proc_macro2::Span; +use quote::{format_ident, quote_spanned}; +use syn::{spanned::Spanned, ItemImpl}; + +use super::{impl_fn_to_global_registry_registration, is_public_impl}; + +pub fn script_globals( + args: proc_macro::TokenStream, + input: proc_macro::TokenStream, +) -> proc_macro::TokenStream { + let args = syn::parse_macro_input!(args as Args); + + let impl_block = syn::parse_macro_input!(input as ItemImpl); + let impl_span = impl_block.span(); + let mut function_registrations = Vec::with_capacity(impl_block.items.len()); + + for i in &impl_block.items { + match i { + syn::ImplItem::Fn(impl_item_fn) => { + let fun = impl_fn_to_global_registry_registration(impl_item_fn); + function_registrations.push(fun); + } + _ => continue, + } + } + + let function_name = format_ident!("register_{}", args.name); + let bms_core_path = &args.bms_core_path; + + let visibility = match is_public_impl(&impl_block) { + true => quote_spanned! {impl_span=> + pub + }, + false => quote_spanned! {impl_span=> + pub(crate) + }, + }; + + let out = quote_spanned! {impl_span=> + #visibility fn #function_name(world: &mut bevy::ecs::world::World) { + + let registry = world.get_resource_or_init::<#bms_core_path::bindings::globals::AppScriptGlobalsRegistry>(); + let mut registry = registry.write(); + + registry + #(#function_registrations)*; + } + }; + + out.into() +} + +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_core_path: syn::Path, +} + +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_core_path = + syn::Path::from(syn::Ident::new("bevy_mod_scripting", Span::call_site())); + bms_core_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_core_path") { + if let syn::Expr::Lit(path) = &name_value.value { + if let syn::Lit::Str(lit_str) = &path.lit { + bms_core_path = syn::parse_str(&lit_str.value())?; + continue; + } + } + } else if name_value.path.is_ident("name") { + if let syn::Expr::Lit(path) = &name_value.value { + if 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; + } + } + + unknown_spans.push((pair.span(), "Unknown argument to script_globals")); + } + + if !unknown_spans.is_empty() { + return Err(syn::Error::new(unknown_spans[0].0, unknown_spans[0].1)); + } + + Ok(Self { + bms_core_path, + name, + }) + } +} diff --git a/crates/bevy_mod_scripting_derive/src/lib.rs b/crates/bevy_mod_scripting_derive/src/lib.rs index 7e1ea2e391..c453a096f4 100644 --- a/crates/bevy_mod_scripting_derive/src/lib.rs +++ b/crates/bevy_mod_scripting_derive/src/lib.rs @@ -1,9 +1,5 @@ //! Derive macros for BMS -use proc_macro2::{Span, TokenStream}; -use quote::{format_ident, quote_spanned, ToTokens}; -use syn::{spanned::Spanned, ImplItemFn, ItemImpl}; - mod derive; #[proc_macro_derive(TypedThrough)] @@ -20,6 +16,8 @@ pub fn into_script(input: proc_macro::TokenStream) -> proc_macro::TokenStream { /// 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. +/// /// Does not support generics. /// /// Arguments: @@ -32,201 +30,22 @@ pub fn script_bindings( args: proc_macro::TokenStream, input: proc_macro::TokenStream, ) -> proc_macro::TokenStream { - let args = syn::parse_macro_input!(args as Args); - - 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(); - - let type_ident_with_generics = &impl_block.self_ty; - let mut function_registrations = Vec::with_capacity(impl_block.items.len()); - for i in &impl_block.items { - match i { - syn::ImplItem::Fn(impl_item_fn) => { - let fun = process_impl_fn(impl_item_fn); - function_registrations.push(fun); - } - _ => continue, - } - } - - let impl_block = match args.remote { - true => TokenStream::default(), - false => quote_spanned! {impl_span=> - #impl_block - }, - }; - - let bms_core_path = &args.bms_core_path; - - let function_name = format_ident!("register_{}", args.name); - let builder_function_name = if args.unregistered { - format_ident!("new_unregistered") - } else { - format_ident!("new") - }; - - let out = quote_spanned! {impl_span=> - fn #function_name(world: &mut bevy::ecs::world::World) { - #bms_core_path::bindings::function::namespace::NamespaceBuilder::<#type_ident_with_generics>::#builder_function_name(world) - #(#function_registrations)*; - } - - #impl_block - }; - - out.into() -} - -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 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_core_path: syn::Path, - /// If true will use `new_unregistered` instead of `new` for the namespace builder - pub unregistered: bool, -} - -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 bms_core_path = - syn::Path::from(syn::Ident::new("bevy_mod_scripting", Span::call_site())); - bms_core_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::Path(path) => { - if path.is_ident("remote") { - remote = true; - continue; - } else if path.is_ident("unregistered") { - unregistered = true; - continue; - } - } - syn::Meta::NameValue(name_value) => { - if name_value.path.is_ident("bms_core_path") { - if let syn::Expr::Lit(path) = &name_value.value { - if let syn::Lit::Str(lit_str) = &path.lit { - bms_core_path = syn::parse_str(&lit_str.value())?; - continue; - } - } - } else if name_value.path.is_ident("name") { - if let syn::Expr::Lit(path) = &name_value.value { - if 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; - } - } - - unknown_spans.push((pair.span(), "Unknown argument to script_bindings")); - } - - if !unknown_spans.is_empty() { - return Err(syn::Error::new(unknown_spans[0].0, unknown_spans[0].1)); - } - - Ok(Self { - remote, - bms_core_path, - name, - unregistered, - }) - } + derive::script_bindings(args, input) } -fn stringify_pat_type(pat_type: &syn::Pat) -> String { - match pat_type { - syn::Pat::Ident(pat_ident) => pat_ident.ident.to_string(), - syn::Pat::Type(pat_type) => stringify_pat_type(&pat_type.pat), - - p => p.to_token_stream().to_string(), - } -} - -/// Converts an impl block function into a function registration, i.e. a closure which will be used to register this function, as well as -/// the target function reference and other metadata -fn process_impl_fn(fun: &ImplItemFn) -> TokenStream { - let args = &fun.sig.inputs; - let args_names = args.iter().map(|arg| match arg { - syn::FnArg::Receiver(_) => syn::LitStr::new("self", Span::call_site()), - syn::FnArg::Typed(pat_type) => { - syn::LitStr::new(&stringify_pat_type(&pat_type.pat), Span::call_site()) - } - }); - let body = &fun.block; - let docstring = parse_docstring(fun.attrs.iter()) - .map(|s| syn::LitStr::new(&s, Span::call_site())) - .unwrap_or(syn::LitStr::new("", Span::call_site())); - let fun_name = syn::LitStr::new(&fun.sig.ident.to_string(), Span::call_site()); - let fun_span = fun.sig.ident.span(); - let out_type = match &fun.sig.output { - syn::ReturnType::Default => quote_spanned! {fun_span=> - () - }, - syn::ReturnType::Type(_, ty) => quote_spanned! {fun_span=> - #ty - }, - }; - quote_spanned! {fun_span=> - .register_documented( - #fun_name, - |#args| { - let output: #out_type = {#body}; - output - }, - #docstring, - &[#(#args_names),*] - ) - } -} - -/// Ideally we'd be doing something like rustdoc: https://github.com/rust-lang/rust/blob/124cc92199ffa924f6b4c7cc819a85b65e0c3984/compiler/rustc_resolve/src/rustdoc.rs#L102 -/// but that is too much complexity, stripping the first space should be good enough for now. -fn parse_docstring<'a>(attrs: impl Iterator) -> Option { - let docs = attrs.filter_map(|attr| { - if attr.path().is_ident("doc") { - if let syn::Meta::NameValue(meta_name_value) = &attr.meta { - if let syn::Expr::Lit(expr_lit) = &meta_name_value.value { - if let syn::Lit::Str(lit_str) = &expr_lit.lit { - if lit_str.value().len() > 1 { - return Some(lit_str.value()[1..].to_string()); - } else { - return Some(lit_str.value()); - } - } - } - } - }; - - None - }); - - // join with newline - let docs = docs.collect::>(); - if docs.is_empty() { - return None; - } - - Some(docs.join("\n")) +/// Derive macro for generating script globals from an impl block. +/// +/// Generates a registration function with visibility determined by the highest visibility in the impl block. +/// +/// Does not support generics. +/// +/// Arguments: +/// - `name`: the name to use to suffix the generated function, i.e. `test_fn` will generate `register_test_fn. Defaults to `globals` +/// - `bms_core_path`: If set the path to override bms imports, normally only used internally +#[proc_macro_attribute] +pub fn script_globals( + args: proc_macro::TokenStream, + input: proc_macro::TokenStream, +) -> proc_macro::TokenStream { + derive::script_globals(args, input) } diff --git a/crates/bevy_mod_scripting_functions/src/core.rs b/crates/bevy_mod_scripting_functions/src/core.rs index 95e29711c6..92a549381f 100644 --- a/crates/bevy_mod_scripting_functions/src/core.rs +++ b/crates/bevy_mod_scripting_functions/src/core.rs @@ -834,12 +834,13 @@ impl GlobalNamespace { )) } - /// Creates a new script function builder + /// Creates a new script system builder, which can be used to add new systems to the world. + /// /// Arguments: - /// * `callback`: The functio name in the script, this system will call - /// * `script_id`: The id of the script + /// * `callback`: The function name in the script this system should call when run. + /// * `script_id`: The id of the script this system will execute when run. /// Returns: - /// * `builder`: The new system builder + /// * `builder`: The system builder fn system_builder( callback: String, script_id: String, diff --git a/crates/ladfile_builder/src/lib.rs b/crates/ladfile_builder/src/lib.rs index 2544d5ab80..6f8c3490ab 100644 --- a/crates/ladfile_builder/src/lib.rs +++ b/crates/ladfile_builder/src/lib.rs @@ -155,6 +155,22 @@ impl<'t> LadFileBuilder<'t> { self } + /// An untyped version of [`Self::add_instance`]. + /// + /// Adds a global instance to the LAD file. + pub fn add_instance_dynamic( + &mut self, + key: impl Into>, + is_static: bool, + type_id: TypeId, + ) -> &mut Self { + let type_id = self.lad_id_from_type_id(type_id); + self.file + .globals + .insert(key.into(), LadInstance { type_id, is_static }); + self + } + /// Add a type definition to the LAD file. /// /// Equivalent to calling [`Self::add_type_info`] with `T::type_info()`. diff --git a/crates/ladfile_builder/src/plugin.rs b/crates/ladfile_builder/src/plugin.rs index 58e132a414..4f4ba30e1f 100644 --- a/crates/ladfile_builder/src/plugin.rs +++ b/crates/ladfile_builder/src/plugin.rs @@ -9,8 +9,9 @@ use bevy::{ system::{Res, Resource}, }, }; -use bevy_mod_scripting_core::bindings::function::{ - namespace::Namespace, script_function::AppScriptFunctionRegistry, +use bevy_mod_scripting_core::bindings::{ + function::{namespace::Namespace, script_function::AppScriptFunctionRegistry}, + globals::AppScriptGlobalsRegistry, }; use crate::LadFileBuilder; @@ -18,6 +19,7 @@ use crate::LadFileBuilder; /// Plugin which enables the generation of LAD files at runtime for the purposes of creating documentation and other goodies. /// /// When added, will automatically generate a LAD file on the Startup schedule +#[derive(Default)] pub struct ScriptingDocgenPlugin(LadFileSettings); #[derive(Resource, Clone)] @@ -35,13 +37,13 @@ pub struct LadFileSettings { pub pretty: bool, } -impl Default for ScriptingDocgenPlugin { +impl Default for LadFileSettings { fn default() -> Self { - Self(LadFileSettings { + Self { path: PathBuf::from("bindings.lad.json"), description: "", pretty: true, - }) + } } } @@ -56,13 +58,16 @@ impl ScriptingDocgenPlugin { } } -fn generate_lad_file( - type_registry: Res, - function_registry: Res, - settings: Res, +/// The function used to generate a ladfile from pre-populated type, function and global registries +pub fn generate_lad_file( + type_registry: &AppTypeRegistry, + function_registry: &AppScriptFunctionRegistry, + global_registry: &AppScriptGlobalsRegistry, + settings: &LadFileSettings, ) { let type_registry = type_registry.read(); let function_registry = function_registry.read(); + let global_registry = global_registry.read(); let mut builder = LadFileBuilder::new(&type_registry); builder .set_description(settings.description) @@ -92,6 +97,12 @@ fn generate_lad_file( builder.add_function_info(function.info.clone()); } + // find global instances + + for (key, global) in global_registry.iter() { + builder.add_instance_dynamic(key.to_string(), global.maker.is_none(), global.type_id); + } + let file = builder.build(); let mut path = PathBuf::from("assets"); @@ -117,9 +128,23 @@ fn generate_lad_file( } } +fn generate_lad_file_system( + type_registry: Res, + function_registry: Res, + global_registry: Res, + settings: Res, +) { + generate_lad_file( + &type_registry, + &function_registry, + &global_registry, + &settings, + ); +} + impl Plugin for ScriptingDocgenPlugin { fn build(&self, app: &mut App) { app.insert_resource(self.0.clone()); - app.add_systems(Startup, generate_lad_file); + app.add_systems(Startup, generate_lad_file_system); } } diff --git a/crates/languages/bevy_mod_scripting_lua/src/lib.rs b/crates/languages/bevy_mod_scripting_lua/src/lib.rs index 6b6a58de0a..81bf11d048 100644 --- a/crates/languages/bevy_mod_scripting_lua/src/lib.rs +++ b/crates/languages/bevy_mod_scripting_lua/src/lib.rs @@ -7,8 +7,8 @@ use bevy::{ use bevy_mod_scripting_core::{ asset::{AssetPathToLanguageMapper, Language}, bindings::{ - function::namespace::Namespace, script_value::ScriptValue, ThreadWorldContainer, - WorldContainer, + function::namespace::Namespace, globals::AppScriptGlobalsRegistry, + script_value::ScriptValue, ThreadWorldContainer, WorldContainer, }, context::{ContextBuilder, ContextInitializer, ContextPreHandlingInitializer}, error::ScriptError, @@ -79,24 +79,23 @@ impl Default for LuaScriptingPlugin { |_script_id, context: &mut Lua| { // set static globals let world = ThreadWorldContainer.try_get_world()?; - let type_registry = world.type_registry(); - let type_registry = type_registry.read(); - - for registration in type_registry.iter() { - // only do this for non generic types - // we don't want to see `Vec:function()` in lua - if !registration.type_info().generics().is_empty() { - continue; - } - - if let Some(global_name) = - registration.type_info().type_path_table().ident() - { - let ref_ = LuaStaticReflectReference(registration.type_id()); - context - .globals() - .set(global_name, ref_) - .map_err(ScriptError::from_mlua_error)?; + let globals_registry = + world.with_resource(|r: &AppScriptGlobalsRegistry| r.clone())?; + let globals_registry = globals_registry.read(); + + for (key, global) in globals_registry.iter() { + match &global.maker { + Some(maker) => { + // non-static global + let global = (maker)(world.clone())?; + context + .globals() + .set(key.to_string(), LuaScriptValue::from(global))? + } + None => { + let ref_ = LuaStaticReflectReference(global.type_id); + context.globals().set(key.to_string(), ref_)? + } } } diff --git a/crates/languages/bevy_mod_scripting_rhai/src/lib.rs b/crates/languages/bevy_mod_scripting_rhai/src/lib.rs index 576e86ecd6..7c3a6a39c5 100644 --- a/crates/languages/bevy_mod_scripting_rhai/src/lib.rs +++ b/crates/languages/bevy_mod_scripting_rhai/src/lib.rs @@ -8,8 +8,8 @@ use bevy::{ use bevy_mod_scripting_core::{ asset::{AssetPathToLanguageMapper, Language}, bindings::{ - function::namespace::Namespace, script_value::ScriptValue, ThreadWorldContainer, - WorldContainer, + function::namespace::Namespace, globals::AppScriptGlobalsRegistry, + script_value::ScriptValue, ThreadWorldContainer, WorldContainer, }, context::{ContextBuilder, ContextInitializer, ContextPreHandlingInitializer}, error::ScriptError, @@ -95,21 +95,22 @@ impl Default for RhaiScriptingPlugin { |_, context: &mut RhaiScriptContext| { // initialize global functions let world = ThreadWorldContainer.try_get_world()?; - let type_registry = world.type_registry(); - let type_registry = type_registry.read(); + let globals_registry = + world.with_resource(|r: &AppScriptGlobalsRegistry| r.clone())?; + let globals_registry = globals_registry.read(); - for registration in type_registry.iter() { - // only do this for non generic types - // we don't want to see `Vec:function()` in lua - if !registration.type_info().generics().is_empty() { - continue; - } - - if let Some(global_name) = - registration.type_info().type_path_table().ident() - { - let ref_ = RhaiStaticReflectReference(registration.type_id()); - context.scope.set_or_push(global_name, ref_); + for (key, global) in globals_registry.iter() { + match &global.maker { + Some(maker) => { + let global = (maker)(world.clone())?; + context + .scope + .set_or_push(key.to_string(), global.into_dynamic()?); + } + None => { + let ref_ = RhaiStaticReflectReference(global.type_id); + context.scope.set_or_push(key.to_string(), ref_); + } } } diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs index 0a36eb4bbc..35a5fb72e9 100644 --- a/crates/xtask/src/main.rs +++ b/crates/xtask/src/main.rs @@ -387,6 +387,7 @@ impl App { ), os: os.to_string(), generates_coverage: self.global_args.coverage, + requires_gpu: matches!(self.subcmd, Xtasks::Docs { .. }), } } } @@ -645,6 +646,8 @@ struct CiMatrixRow { os: String, /// If this run produces lcov files generates_coverage: bool, + /// If this step requires a gpu + requires_gpu: bool, } impl Xtasks { diff --git a/examples/docgen.rs b/examples/docgen.rs index e77efa8671..0e0c256c11 100644 --- a/examples/docgen.rs +++ b/examples/docgen.rs @@ -1,20 +1,66 @@ -use bevy::{app::App, asset::AssetPlugin, hierarchy::HierarchyPlugin, MinimalPlugins}; +use bevy::ecs::reflect::AppTypeRegistry; +use bevy::{app::App, DefaultPlugins}; use bevy_mod_scripting::ScriptFunctionsPlugin; -use ladfile_builder::plugin::ScriptingDocgenPlugin; +use bevy_mod_scripting_core::bindings::function::script_function::AppScriptFunctionRegistry; +use bevy_mod_scripting_core::bindings::globals::core::CoreScriptGlobalsPlugin; +use bevy_mod_scripting_core::bindings::globals::AppScriptGlobalsRegistry; +use ladfile_builder::plugin::{generate_lad_file, LadFileSettings, ScriptingDocgenPlugin}; fn main() -> std::io::Result<()> { let mut app = App::new(); - // headless bevy - app.add_plugins((MinimalPlugins, AssetPlugin::default(), HierarchyPlugin)); + // headless bevy, kinda, I want to include as many plugins as I can which actually + // provide reflected type definitions, but exclude anything that runs rendering stuff. + app.add_plugins(DefaultPlugins); // docgen + scripting - app.add_plugins((ScriptFunctionsPlugin, ScriptingDocgenPlugin::default())); + app.add_plugins(( + // normally the global plugin is included as part of each scripting plugin, here we just take + // the definitions by themselves + CoreScriptGlobalsPlugin, + ScriptFunctionsPlugin, + )); - // run once - app.cleanup(); - app.finish(); - app.update(); + // there are two ways to generate the ladfile - // bah bye + // 1. add the docgen plugin and run your app as normal + app.add_plugins(ScriptingDocgenPlugin::default()); + // running the app once like below would do the trick + // app.cleanup(); + // app.finish(); + // app.update(); + + // or 2. manually trigger the system + // this is what we do here as we're running this example in GHA + + let type_registry = app + .world() + .get_resource::() + .unwrap() + .clone(); + let function_registry = app + .world() + .get_resource::() + .unwrap() + .clone(); + let global_registry = app + .world() + .get_resource::() + .unwrap() + .clone(); + + let settings = LadFileSettings { + description: "Core BMS framework bindings", + ..Default::default() + }; + + generate_lad_file( + &type_registry, + &function_registry, + &global_registry, + &settings, + ); + + // bah bye, the generated file will be found in assets/ + // this can then be passed to various backends to generate docs, and other declaration files Ok(()) }