diff --git a/CHANGELOG.md b/CHANGELOG.md index fdc1c660..b5be6ab1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -160,4 +160,5 @@ ## v0.10.0 - Add deterministic ordering for auto plugin registry entries, preserving definition order. - Add optional `after_build` flag to all `auto_*` macros to enable injecting tokens at the end of plugin build function body instead of the start. -- Add support for `insert` in `auto_resource` \ No newline at end of file +- Add support for `insert` in `auto_resource` +- Add `auto_plugin_build_hook` and `AutoPluginBuildHook` to run custom hooks during plugin build. diff --git a/README.md b/README.md index 9bd9d756..a696854a 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,34 @@ There is `auto_plugin` arguments if your plugin has generics. See [tests](tests/e2e) for other examples +### Custom Build Hooks (Third-Party Integration) + +You can extend this pattern to third-party crates by using `#[auto_plugin_build_hook]` as a building block +for your own macros. For example, a user might want to automatically call `bevy_replicon` `app.replicate::()` for each annotated type. + +```rust,ignore +use bevy::prelude::*; +use bevy_auto_plugin::prelude::*; +use bevy_replicon::prelude::*; + +#[derive(AutoPlugin)] +#[auto_plugin(impl_plugin_trait)] +struct MyPlugin; + +struct ReplicateHook; + +impl AutoPluginBuildHook for ReplicateHook { + fn on_build(&self, app: &mut App) { + app.replicate::(); + } +} + +#[derive(Component)] +#[auto_plugin_build_hook(plugin = MyPlugin, hook = ReplicateHook)] +struct MyReplicatedThing; +``` + + ### Expanded If you were looking to cherry-pick certain functionality like `auto_name` or `auto_register_type` for example you could use them individually: @@ -260,4 +288,4 @@ at your option. This means you can select the license you prefer. ### Your Contributions Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be -dual-licensed as above, without any additional terms or conditions. \ No newline at end of file +dual-licensed as above, without any additional terms or conditions. diff --git a/crates/bevy_auto_plugin_proc_macros/src/lib.rs b/crates/bevy_auto_plugin_proc_macros/src/lib.rs index 84d9b058..4d8b9eae 100644 --- a/crates/bevy_auto_plugin_proc_macros/src/lib.rs +++ b/crates/bevy_auto_plugin_proc_macros/src/lib.rs @@ -155,3 +155,9 @@ pub fn auto_run_on_build(attr: CompilerStream, input: CompilerStream) -> Compile pub fn auto_bind_plugin(attr: CompilerStream, input: CompilerStream) -> CompilerStream { handle_attribute(expand::attr::auto_bind_plugin::auto_bind_plugin_outer, attr, input) } + +#[doc = include_str!(concat!(env!("OUT_DIR"), "/docs/proc_attributes/actions/auto_plugin_build_hook.md"))] +#[proc_macro_attribute] +pub fn auto_plugin_build_hook(attr: CompilerStream, input: CompilerStream) -> CompilerStream { + handle_attribute(expand::attr::auto_plugin_build_hook, attr, input) +} diff --git a/crates/bevy_auto_plugin_shared/Cargo.toml b/crates/bevy_auto_plugin_shared/Cargo.toml index f5aa45e9..7ff69a92 100644 --- a/crates/bevy_auto_plugin_shared/Cargo.toml +++ b/crates/bevy_auto_plugin_shared/Cargo.toml @@ -45,4 +45,5 @@ anyhow = { workspace = true } # used in crate resolve tests bevy_ecs = { workspace = true } bevy_reflect = { workspace = true } -bevy_state = { workspace = true } \ No newline at end of file +bevy_state = { workspace = true } +bevy_auto_plugin = { path = "../../.", default-features = false } \ No newline at end of file diff --git a/crates/bevy_auto_plugin_shared/src/__private/expand/attr/mod.rs b/crates/bevy_auto_plugin_shared/src/__private/expand/attr/mod.rs index 05d79be5..528c5ce0 100644 --- a/crates/bevy_auto_plugin_shared/src/__private/expand/attr/mod.rs +++ b/crates/bevy_auto_plugin_shared/src/__private/expand/attr/mod.rs @@ -42,6 +42,7 @@ gen_action_outers! { auto_add_observer => IaAddObserver, auto_add_plugin => IaAddPlugin, auto_configure_system_set => IaConfigureSystemSet, + auto_plugin_build_hook => IaAutoPluginBuildHook, } gen_rewrite_outers! { diff --git a/crates/bevy_auto_plugin_shared/src/lib.rs b/crates/bevy_auto_plugin_shared/src/lib.rs index ad233530..567df6d6 100644 --- a/crates/bevy_auto_plugin_shared/src/lib.rs +++ b/crates/bevy_auto_plugin_shared/src/lib.rs @@ -20,3 +20,52 @@ pub fn _initialize() { __wasm_call_ctors(); } } + +/// Hook invoked during a plugin's build for a specific target type `T`. +/// +/// Register a hook by annotating the target type with [`bevy_auto_plugin::prelude::auto_plugin_build_hook`]. +/// +/// The `hook` expression is evaluated when the plugin builds. Use `after_build` on the +/// attribute to run the hook after the build body instead of before. +/// +/// # Example +/// ```rust +/// use bevy_app::prelude::*; +/// use bevy_ecs::prelude::*; +/// use bevy_auto_plugin::prelude::*; +/// +/// #[derive(AutoPlugin)] +/// #[auto_plugin(impl_plugin_trait)] +/// struct MyPlugin; +/// +/// struct SpawnComponentHook; +/// +/// impl AutoPluginBuildHook for SpawnComponentHook where T: Component + Default { +/// fn on_build(&self, app: &mut App) { +/// app.world_mut().spawn(T::default()); +/// } +/// } +/// +/// #[auto_component(plugin = MyPlugin, derive(Default))] +/// #[auto_plugin_build_hook(plugin = MyPlugin, hook = SpawnComponentHook)] +/// struct MyComponent; +/// ``` +/// Generates the equivalent to: +/// ```rust +/// use bevy_app::prelude::*; +/// use bevy_ecs::prelude::*; +/// +/// #[derive(Component, Default)] +/// struct MyComponent; +/// +/// struct MyPlugin; +/// impl Plugin for MyPlugin { +/// fn build(&self, app: &mut App) { +/// app.world_mut().spawn(MyComponent); +/// } +/// } +/// ``` +pub trait AutoPluginBuildHook { + /// Called during plugin build for the target type `T`. + fn on_build(&self, app: &mut bevy_app::App); +} diff --git a/crates/bevy_auto_plugin_shared/src/macro_api/attributes/actions/auto_plugin_build_hook.rs b/crates/bevy_auto_plugin_shared/src/macro_api/attributes/actions/auto_plugin_build_hook.rs new file mode 100644 index 00000000..43d121aa --- /dev/null +++ b/crates/bevy_auto_plugin_shared/src/macro_api/attributes/actions/auto_plugin_build_hook.rs @@ -0,0 +1,53 @@ +use crate::macro_api::prelude::*; +use darling::FromMeta; +use proc_macro2::TokenStream; +use quote::{ + ToTokens, + quote, +}; +use syn::Expr; + +#[derive(FromMeta, Debug, Clone, PartialEq, Hash)] +#[darling(derive_syn_parse)] +pub struct AutoPluginBuildHookArgs { + hook: Expr, +} + +impl AttributeIdent for AutoPluginBuildHookArgs { + const IDENT: &'static str = "auto_plugin_hook"; +} + +pub type IaAutoPluginBuildHook = ItemAttribute< + Composed, + AllowStructOrEnum, +>; +pub type AutoPluginBuildHookAppMutEmitter = AppMutationEmitter; +pub type AutoPluginBuildHookAttrEmitter = AttrEmitter; + +impl EmitAppMutationTokens for AutoPluginBuildHookAppMutEmitter { + fn to_app_mutation_tokens( + &self, + tokens: &mut TokenStream, + app_param: &syn::Ident, + ) -> syn::Result<()> { + let custom = &self.args.args.base.hook; + let concrete_paths = self.args.concrete_paths()?; + for concrete_path in concrete_paths { + tokens.extend(quote! { + let instance = #custom; + ::bevy_auto_plugin::__private::shared::AutoPluginBuildHook::< #concrete_path >::on_build(&instance, #app_param); + }); + } + Ok(()) + } +} + +impl ToTokens for AutoPluginBuildHookAttrEmitter { + fn to_tokens(&self, tokens: &mut TokenStream) { + let args = self.args.args.extra_args(); + tokens.extend(quote! { + #(#args),* + }); + *tokens = self.wrap_as_attr(tokens); + } +} diff --git a/crates/bevy_auto_plugin_shared/src/macro_api/attributes/actions/mod.rs b/crates/bevy_auto_plugin_shared/src/macro_api/attributes/actions/mod.rs index 137d11e9..bbd3a320 100644 --- a/crates/bevy_auto_plugin_shared/src/macro_api/attributes/actions/mod.rs +++ b/crates/bevy_auto_plugin_shared/src/macro_api/attributes/actions/mod.rs @@ -8,6 +8,7 @@ mod auto_init_state; mod auto_init_sub_state; mod auto_insert_resource; mod auto_name; +mod auto_plugin_build_hook; mod auto_register_state_type; mod auto_register_type; mod auto_run_on_build; @@ -24,6 +25,7 @@ pub mod prelude { pub use auto_init_sub_state::*; pub use auto_insert_resource::*; pub use auto_name::*; + pub use auto_plugin_build_hook::*; pub use auto_register_state_type::*; pub use auto_register_type::*; pub use auto_run_on_build::*; diff --git a/crates/bevy_auto_plugin_shared/src/macro_api/context/macro_paths.rs b/crates/bevy_auto_plugin_shared/src/macro_api/context/macro_paths.rs index a2e7150a..758d5868 100644 --- a/crates/bevy_auto_plugin_shared/src/macro_api/context/macro_paths.rs +++ b/crates/bevy_auto_plugin_shared/src/macro_api/context/macro_paths.rs @@ -32,6 +32,7 @@ pub struct MacroPaths { pub emit_auto_name_macro: syn::Path, /// resolved absolute path to `auto_configure_system_set` pub emit_configure_system_set_macro: syn::Path, + pub emit_auto_plugin_hook_macro: syn::Path, } impl Default for MacroPaths { @@ -51,6 +52,7 @@ impl Default for MacroPaths { emit_run_on_build_macro: parse_quote!( ::bevy_auto_plugin::prelude::auto_run_on_build ), emit_auto_name_macro: parse_quote!( ::bevy_auto_plugin::prelude::auto_name ), emit_configure_system_set_macro: parse_quote!( ::bevy_auto_plugin::prelude::auto_configure_system_set ), + emit_auto_plugin_hook_macro: parse_quote!( ::bevy_auto_plugin::prelude::auto_plugin_hook ), } } } @@ -136,3 +138,9 @@ impl MacroPathProvider for ConfigureSystemSetArgs { &context.macros.emit_configure_system_set_macro } } + +impl MacroPathProvider for AutoPluginBuildHookArgs { + fn macro_path(context: &Context) -> &syn::Path { + &context.macros.emit_auto_plugin_hook_macro + } +} diff --git a/docs/proc_attributes/actions/auto_plugin_build_hook.md b/docs/proc_attributes/actions/auto_plugin_build_hook.md new file mode 100644 index 00000000..a876ecad --- /dev/null +++ b/docs/proc_attributes/actions/auto_plugin_build_hook.md @@ -0,0 +1,51 @@ +Registers a build hook to run custom logic for a type when a plugin builds. + +# Parameters +- `plugin = PluginType` - Required. Specifies which plugin should run this hook. +- `hook = Expr` - Required. Expression that constructs a value implementing `AutoPluginBuildHook` for the target type. +- `after_build` - Optional. Injects this macro's tokens at the end of the plugin build instead of the start. +- `generics(T1, T2, ...)` - Optional. Specifies concrete types for generic parameters. + When provided, the hook is run for each of these specific generic parameters. + Note: Clippy will complain if you have duplicate generic type names. For those you can use named generics: `generics(T1 = ..., T2 = ...)`. + +# Example +```rust +use bevy::prelude::*; +use bevy_auto_plugin::prelude::*; + +#[derive(AutoPlugin)] +#[auto_plugin(impl_plugin_trait)] +struct MyPlugin; + +struct MyHook; + +impl AutoPluginBuildHook for MyHook { + fn on_build(&self, _app: &mut App) { + // custom logic for T + } +} + +#[derive(Component)] +#[auto_plugin_build_hook(plugin = MyPlugin, hook = MyHook)] +struct Foo; +``` + +# Example (with generics) +```rust +use bevy::prelude::*; +use bevy_auto_plugin::prelude::*; + +#[derive(AutoPlugin)] +#[auto_plugin(impl_plugin_trait)] +struct MyPlugin; + +struct MyHook; + +impl AutoPluginBuildHook for MyHook { + fn on_build(&self, _app: &mut App) {} +} + +#[derive(Component)] +#[auto_plugin_build_hook(plugin = MyPlugin, hook = MyHook, generics(u32), generics(bool))] +struct Foo(T); +``` diff --git a/src/lib.rs b/src/lib.rs index 0b4c45f1..c6dd0640 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -106,6 +106,34 @@ //! #[apply(CopyDefaultComponentTemplate!)] //! struct BarComponent; //! ``` +//! +//! ### Custom Build Hooks (Third-Party Integration) +//! You can use `#[auto_plugin_build_hook]` as a building block for third-party +//! APIs that require `App` calls (for example, `bevy_replicon`'s `app.replicate::()`). +//! This makes it easy to build your own macros for external crates without +//! writing boilerplate in every plugin build. +//! +//! ```rust,ignore +//! use bevy::prelude::*; +//! use bevy_auto_plugin::prelude::*; +//! use bevy_replicon::prelude::*; +//! +//! #[derive(AutoPlugin)] +//! #[auto_plugin(impl_plugin_trait)] +//! struct MyPlugin; +//! +//! struct ReplicateHook; +//! +//! impl AutoPluginBuildHook for ReplicateHook { +//! fn on_build(&self, app: &mut App) { +//! app.replicate::(); +//! } +//! } +//! +//! #[derive(Component)] +//! #[auto_plugin_build_hook(plugin = MyPlugin, hook = ReplicateHook)] +//! struct NetTransform; +//! ``` /// Private Re-exports #[doc(hidden)] @@ -189,4 +217,10 @@ pub mod prelude { #[doc = include_str!("../docs/proc_attributes/actions/auto_configure_system_set.md")] pub use bevy_auto_plugin_proc_macros::auto_configure_system_set; + + #[doc(inline)] + pub use super::__private::shared::AutoPluginBuildHook; + + #[doc = include_str!("../docs/proc_attributes/actions/auto_plugin_build_hook.md")] + pub use bevy_auto_plugin_proc_macros::auto_plugin_build_hook; } diff --git a/tests/e2e/actions/auto_plugin_build_hook.rs b/tests/e2e/actions/auto_plugin_build_hook.rs new file mode 100644 index 00000000..694a4c1f --- /dev/null +++ b/tests/e2e/actions/auto_plugin_build_hook.rs @@ -0,0 +1,87 @@ +use bevy_app::prelude::*; +use bevy_auto_plugin::prelude::*; +use bevy_ecs::prelude::*; +use internal_test_proc_macro::xtest; +use std::{ + any::TypeId, + collections::{ + HashMap, + HashSet, + }, +}; + +#[derive(AutoPlugin)] +#[auto_plugin(impl_plugin_trait)] +struct TestPlugin; + +#[derive(Resource, Debug, Default, PartialEq)] +struct Counter(HashMap<&'static str, HashMap>>); + +struct MyCustomHookA; + +impl AutoPluginBuildHook for MyCustomHookA { + fn on_build(&self, app: &mut App) { + app.world_mut() + .resource_mut::() + .0 + .entry("A") + .or_default() + .entry(TypeId::of::()) + .or_default() + .insert(std::any::type_name::()); + } +} + +struct MyCustomHookB(&'static str); + +impl AutoPluginBuildHook for MyCustomHookB { + fn on_build(&self, app: &mut App) { + let mut counter = app.world_mut().resource_mut::(); + let set = counter.0.entry("B").or_default().entry(TypeId::of::()).or_default(); + set.insert(std::any::type_name::()); + set.insert(self.0); + } +} + +#[derive(Component, Debug)] +#[auto_plugin_build_hook(plugin = TestPlugin, hook = MyCustomHookA)] +#[auto_plugin_build_hook(plugin = TestPlugin, hook = MyCustomHookB("foo"))] +struct TestA; + +#[derive(Component, Debug)] +#[auto_plugin_build_hook(plugin = TestPlugin, hook = MyCustomHookB("bar"))] +#[auto_plugin_build_hook(plugin = TestPlugin, hook = MyCustomHookA)] +struct TestB; + +fn app() -> App { + let mut app = internal_test_util::create_minimal_app(); + app.init_resource::(); + app.add_plugins(TestPlugin); + app +} + +#[xtest] +fn test_auto_plugin_hook() { + let app = app(); + let counter = app.world().get_resource::().expect("counter resource missing"); + + let hook_a_map = counter.0.get("A").expect("Hook A entry missing"); + assert_eq!( + hook_a_map.get(&TypeId::of::()).expect("Hook A - TestA entry missing"), + &HashSet::from([std::any::type_name::()]) + ); + assert_eq!( + hook_a_map.get(&TypeId::of::()).expect("Hook A - TestB entry missing"), + &HashSet::from([std::any::type_name::()]) + ); + + let hook_b_map = counter.0.get("B").expect("Hook B entry missing"); + assert_eq!( + hook_b_map.get(&TypeId::of::()).expect("Hook B - TestA entry missing"), + &HashSet::from([std::any::type_name::(), "foo"]) + ); + assert_eq!( + hook_b_map.get(&TypeId::of::()).expect("Hook B - TestB entry missing"), + &HashSet::from([std::any::type_name::(), "bar"]) + ); +} diff --git a/tests/e2e/actions/mod.rs b/tests/e2e/actions/mod.rs index 3a595627..97f03286 100644 --- a/tests/e2e/actions/mod.rs +++ b/tests/e2e/actions/mod.rs @@ -23,6 +23,7 @@ mod auto_insert_resource; mod auto_insert_resource_with_generics; mod auto_name; mod auto_name_with_generics; +mod auto_plugin_build_hook; mod auto_plugin_default_param; mod auto_plugin_default_param_method; mod auto_plugin_param;