From 77e7852af2e5c76ec2776f8dbb1dbbbe4b1bce38 Mon Sep 17 00:00:00 2001 From: Jady Wright Date: Wed, 10 Dec 2025 19:32:29 -0500 Subject: [PATCH] feat: Add `auto_sub_states`, and add `auto_register_state_type` to `auto_states` --- .../docs/proc_attributes/auto_states.md | 2 +- .../docs/proc_attributes/auto_sub_states.md | 51 +++++ .../bevy_auto_plugin_proc_macros/src/lib.rs | 7 + .../src/__private/expand/attr/mod.rs | 19 +- .../src/codegen/tokens.rs | 33 ++++ .../actions/auto_register_state_type.rs | 7 + .../src/macro_api/attributes/mod.rs | 1 + .../attributes/rewrites/auto_states.rs | 11 ++ .../attributes/rewrites/auto_sub_states.rs | 181 ++++++++++++++++++ .../src/macro_api/attributes/rewrites/mod.rs | 1 + src/lib.rs | 3 + tests/e2e/auto_plugin.rs | 101 ++++++++++ 12 files changed, 407 insertions(+), 10 deletions(-) create mode 100644 crates/bevy_auto_plugin_proc_macros/docs/proc_attributes/auto_sub_states.md create mode 100644 crates/bevy_auto_plugin_shared/src/macro_api/attributes/rewrites/auto_sub_states.rs diff --git a/crates/bevy_auto_plugin_proc_macros/docs/proc_attributes/auto_states.md b/crates/bevy_auto_plugin_proc_macros/docs/proc_attributes/auto_states.md index 3499bae6..f740c6d5 100644 --- a/crates/bevy_auto_plugin_proc_macros/docs/proc_attributes/auto_states.md +++ b/crates/bevy_auto_plugin_proc_macros/docs/proc_attributes/auto_states.md @@ -18,7 +18,7 @@ Automatically initializes a state in the app. Passes through any additional reflects listed. If enabled in tandem with `derive` it also includes `#[derive(Reflect)]` - `register` - Enables type registration for the `States` - Same as having `#[auto_register_type]` + Same as having `#[auto_register_type]` and `#[auto_register_state_type]` - `init` - Initializes the `States` with default values Same as having `#[auto_init_state]` diff --git a/crates/bevy_auto_plugin_proc_macros/docs/proc_attributes/auto_sub_states.md b/crates/bevy_auto_plugin_proc_macros/docs/proc_attributes/auto_sub_states.md new file mode 100644 index 00000000..398c3e52 --- /dev/null +++ b/crates/bevy_auto_plugin_proc_macros/docs/proc_attributes/auto_sub_states.md @@ -0,0 +1,51 @@ +Automatically initializes a substate in the app. + +# Parameters +- `plugin = PluginType` - Required. Specifies which plugin should initialize this substate. +- `generics(T1, T2, ...)` - Optional. Specifies concrete types for generic parameters. + When provided, the substates will be registered with these specific generic parameters. +- `derive` | `derive(Debug, Default, ..)` - Optional. Specifies that the macro should handle deriving `SubStates`. + Passes through any additional derives listed. + When enabled, `SubStates` include these additional derives: + - `Debug` + - `Default` + - `Copy` + - `Clone` + - `PartialEq` + - `Eq` + - `Hash` +- `reflect` | `reflect(Debug, Default, ..)` - Optional. Specifies that the macro should handle emitting the single `#[reflect(...)]`. + Passes through any additional reflects listed. + If enabled in tandem with `derive` it also includes `#[derive(Reflect)]` +- `register` - Enables type registration for the `SubStates` + Same as having `#[auto_register_type]` and `#[auto_register_state_type]` +- `init` - Initializes the `SubStates` with default values + Same as having `#[auto_init_sub_state]` + +// Debug, Default, Copy, Clone, PartialEq, Eq, Hash + +# Example +```rust +use bevy::prelude::*; +use bevy_auto_plugin::prelude::*; + +#[derive(AutoPlugin)] +#[auto_plugin(impl_plugin_trait)] +struct MyPlugin; + +#[auto_states(plugin = MyPlugin, derive, reflect, register, init)] +enum AppState { + #[default] + Menu, + InGame +} + +#[auto_sub_states(plugin = MyPlugin, derive, reflect, register, init)] +#[source(AppState = AppState::InGame)] +enum GamePhase { + #[default] + Setup, + Battle, + Conclusion +} +``` \ No newline at end of file diff --git a/crates/bevy_auto_plugin_proc_macros/src/lib.rs b/crates/bevy_auto_plugin_proc_macros/src/lib.rs index c508a707..8dc0487a 100644 --- a/crates/bevy_auto_plugin_proc_macros/src/lib.rs +++ b/crates/bevy_auto_plugin_proc_macros/src/lib.rs @@ -146,6 +146,13 @@ pub fn auto_states(attr: CompilerStream, input: CompilerStream) -> CompilerStrea handle_attribute(expand::attr::auto_states, attr, input) } +/// Automatically registers item as SubStates for bevy app. (See below for additional options) +#[doc = include_str!("../docs/proc_attributes/auto_sub_states.md")] +#[proc_macro_attribute] +pub fn auto_sub_states(attr: CompilerStream, input: CompilerStream) -> CompilerStream { + handle_attribute(expand::attr::auto_sub_states, attr, input) +} + /// Automatically adds the fn as a system for bevy app. (See below for additional options) #[doc = include_str!("../docs/proc_attributes/auto_system.md")] #[proc_macro_attribute] 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 a0ea176d..75cf840f 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 @@ -366,22 +366,23 @@ gen_auto_attribute_outers! { auto_init_resource => InitResourceArgs, auto_insert_resource => InsertResourceArgs, auto_init_state => InitStateArgs, - auto_init_sub_state => InitSubStateArgs, + auto_init_sub_state => InitSubStateArgs, auto_name => NameArgs, auto_register_state_type => RegisterStateTypeArgs, auto_add_system => AddSystemArgs, auto_add_observer => AddObserverArgs, auto_add_plugin => AddPluginArgs, - auto_configure_system_set => ConfigureSystemSetArgs: + auto_configure_system_set => ConfigureSystemSetArgs: parser = ArgParser::Custom(CustomParser::AttrInput(configure_system_set_args_from_attr_input)), } gen_auto_outers! { - auto_component => ComponentArgs, - auto_resource => ResourceArgs, - auto_system => SystemArgs, - auto_event => EventArgs, - auto_message => MessageArgs, - auto_observer => ObserverArgs, - auto_states => StatesArgs, + auto_component => ComponentArgs, + auto_resource => ResourceArgs, + auto_system => SystemArgs, + auto_event => EventArgs, + auto_message => MessageArgs, + auto_observer => ObserverArgs, + auto_states => StatesArgs, + auto_sub_states => SubStatesArgs, } diff --git a/crates/bevy_auto_plugin_shared/src/codegen/tokens.rs b/crates/bevy_auto_plugin_shared/src/codegen/tokens.rs index 2bbae76c..70bbd3c1 100644 --- a/crates/bevy_auto_plugin_shared/src/codegen/tokens.rs +++ b/crates/bevy_auto_plugin_shared/src/codegen/tokens.rs @@ -127,6 +127,11 @@ pub fn derive_states_path() -> NonEmptyPath { parse_quote!(#states::state::States) } +pub fn derive_sub_states_path() -> NonEmptyPath { + let states = crate::__private::paths::state::root_path(); + parse_quote!(#states::state::SubStates) +} + pub fn derive_component<'a>( extra_items: impl IntoIterator, ) -> TokenStream { @@ -196,6 +201,28 @@ pub fn derive_states<'a>(extra_items: impl IntoIterator )], } } +pub fn derive_sub_states<'a>( + extra_items: impl IntoIterator, +) -> ExpandAttrs { + ExpandAttrs { + use_items: vec![crate::__private::paths::state::derive_use_tokens()], + attrs: vec![derive_from( + [ + vec![ + &derive_sub_states_path(), + &parse_quote!(Debug), + &parse_quote!(Default), + &parse_quote!(Clone), + &parse_quote!(PartialEq), + &parse_quote!(Eq), + &parse_quote!(Hash), + ], + extra_items.into_iter().collect::>(), + ] + .concat(), + )], + } +} pub fn derive_reflect() -> TokenStream { let derive_reflect_path = derive_reflect_path(); quote! { #[derive(#derive_reflect_path)] } @@ -209,6 +236,9 @@ pub fn use_bevy_state_app_ext_states() -> syn::ItemUse { pub fn auto_register_type(plugin: NonEmptyPath, args: RegisterTypeArgs) -> TokenStream { ArgsWithPlugin::new(plugin, args).to_token_stream() } +pub fn auto_register_state_type(plugin: NonEmptyPath, args: RegisterStateTypeArgs) -> TokenStream { + ArgsWithPlugin::new(plugin, args).to_token_stream() +} pub fn auto_name(plugin: NonEmptyPath, args: NameArgs) -> TokenStream { ArgsWithPlugin::new(plugin, args).to_token_stream() } @@ -218,6 +248,9 @@ pub fn auto_init_resource(plugin: NonEmptyPath, args: InitResourceArgs) -> Token pub fn auto_init_states(plugin: NonEmptyPath, args: InitStateArgs) -> TokenStream { ArgsWithPlugin::new(plugin, args).to_token_stream() } +pub fn auto_init_sub_states(plugin: NonEmptyPath, args: InitSubStateArgs) -> TokenStream { + ArgsWithPlugin::new(plugin, args).to_token_stream() +} pub fn auto_add_systems(plugin: NonEmptyPath, args: AddSystemArgs) -> TokenStream { ArgsWithPlugin::new(plugin, args).to_token_stream() } diff --git a/crates/bevy_auto_plugin_shared/src/macro_api/attributes/actions/auto_register_state_type.rs b/crates/bevy_auto_plugin_shared/src/macro_api/attributes/actions/auto_register_state_type.rs index 79bf2d3c..52e17fd8 100644 --- a/crates/bevy_auto_plugin_shared/src/macro_api/attributes/actions/auto_register_state_type.rs +++ b/crates/bevy_auto_plugin_shared/src/macro_api/attributes/actions/auto_register_state_type.rs @@ -1,3 +1,4 @@ +use crate::codegen::tokens::ArgsBackToTokens; use crate::codegen::with_target_path::ToTokensWithConcreteTargetPath; use crate::macro_api::attributes::prelude::GenericsArgs; use crate::macro_api::attributes::{AttributeIdent, ItemAttributeArgs}; @@ -46,6 +47,12 @@ impl ToTokensWithConcreteTargetPath for RegisterStateTypeArgs { } } +impl ArgsBackToTokens for RegisterStateTypeArgs { + fn back_to_inner_arg_tokens(&self, tokens: &mut TokenStream) { + tokens.extend(self.generics().to_attribute_arg_tokens()); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/bevy_auto_plugin_shared/src/macro_api/attributes/mod.rs b/crates/bevy_auto_plugin_shared/src/macro_api/attributes/mod.rs index b95700ec..b4dceec6 100644 --- a/crates/bevy_auto_plugin_shared/src/macro_api/attributes/mod.rs +++ b/crates/bevy_auto_plugin_shared/src/macro_api/attributes/mod.rs @@ -39,6 +39,7 @@ pub mod prelude { pub use crate::macro_api::attributes::rewrites::auto_observer::ObserverArgs; pub use crate::macro_api::attributes::rewrites::auto_resource::ResourceArgs; pub use crate::macro_api::attributes::rewrites::auto_states::StatesArgs; + pub use crate::macro_api::attributes::rewrites::auto_sub_states::SubStatesArgs; pub use crate::macro_api::attributes::rewrites::auto_system::SystemArgs; pub use crate::macro_api::attributes::traits::prelude::*; } diff --git a/crates/bevy_auto_plugin_shared/src/macro_api/attributes/rewrites/auto_states.rs b/crates/bevy_auto_plugin_shared/src/macro_api/attributes/rewrites/auto_states.rs index 999c9853..a76ccd9c 100644 --- a/crates/bevy_auto_plugin_shared/src/macro_api/attributes/rewrites/auto_states.rs +++ b/crates/bevy_auto_plugin_shared/src/macro_api/attributes/rewrites/auto_states.rs @@ -36,6 +36,12 @@ impl<'a> From<&'a StatesArgs> for RegisterTypeArgs { } } +impl<'a> From<&'a StatesArgs> for RegisterStateTypeArgs { + fn from(_value: &'a StatesArgs) -> Self { + Self::default() + } +} + impl<'a> From<&'a StatesArgs> for InitStateArgs { fn from(_value: &'a StatesArgs) -> Self { Self::default() @@ -88,6 +94,10 @@ impl RewriteAttribute for StatesArgs { expanded_attrs .attrs .push(tokens::auto_register_type(plugin.clone(), self.into())); + expanded_attrs.attrs.push(tokens::auto_register_state_type( + plugin.clone(), + self.into(), + )); } if self.init { expanded_attrs @@ -159,6 +169,7 @@ mod tests { quote! { #[derive(#derive_reflect_path)] }, quote! { #[reflect(#(#reflect_args),*)] }, tokens::auto_register_type(args.plugin(), (&args.inner).into()), + tokens::auto_register_state_type(args.plugin(), (&args.inner).into()), tokens::auto_init_states(args.plugin(), (&args.inner).into()), ] } diff --git a/crates/bevy_auto_plugin_shared/src/macro_api/attributes/rewrites/auto_sub_states.rs b/crates/bevy_auto_plugin_shared/src/macro_api/attributes/rewrites/auto_sub_states.rs new file mode 100644 index 00000000..77768e3f --- /dev/null +++ b/crates/bevy_auto_plugin_shared/src/macro_api/attributes/rewrites/auto_sub_states.rs @@ -0,0 +1,181 @@ +use crate::__private::attribute::RewriteAttribute; +use crate::codegen::tokens::ArgsBackToTokens; +use crate::codegen::{ExpandAttrs, tokens}; +use crate::macro_api::attributes::AttributeIdent; +use crate::macro_api::attributes::prelude::GenericsArgs; +use crate::macro_api::attributes::prelude::*; +use crate::syntax::ast::flag_or_list::FlagOrList; +use crate::syntax::ast::type_list::TypeList; +use crate::syntax::validated::non_empty_path::NonEmptyPath; +use darling::FromMeta; +use proc_macro2::{Ident, TokenStream as MacroStream, TokenStream}; +use quote::quote; + +#[derive(FromMeta, Debug, Default, Clone, PartialEq, Hash)] +#[darling(derive_syn_parse, default)] +pub struct SubStatesArgs { + pub derive: FlagOrList, + pub reflect: FlagOrList, + pub register: bool, + pub init: bool, +} + +impl GenericsArgs for SubStatesArgs { + fn type_lists(&self) -> &[TypeList] { + &[] + } +} + +impl AttributeIdent for SubStatesArgs { + const IDENT: &'static str = "auto_sub_states"; +} + +impl<'a> From<&'a SubStatesArgs> for RegisterTypeArgs { + fn from(_value: &'a SubStatesArgs) -> Self { + Self::default() + } +} + +impl<'a> From<&'a SubStatesArgs> for RegisterStateTypeArgs { + fn from(_value: &'a SubStatesArgs) -> Self { + Self::default() + } +} + +impl<'a> From<&'a SubStatesArgs> for InitSubStateArgs { + fn from(_value: &'a SubStatesArgs) -> Self { + Self::default() + } +} + +impl ArgsBackToTokens for SubStatesArgs { + fn back_to_inner_arg_tokens(&self, tokens: &mut TokenStream) { + let mut items = vec![]; + items.extend(self.generics().to_attribute_arg_vec_tokens()); + if self.derive.present { + items.push(self.derive.to_outer_tokens("derive")); + } + if self.reflect.present { + items.push(self.reflect.to_outer_tokens("reflect")); + } + if self.register { + items.push(quote!(register)); + } + if self.init { + items.push(quote!(init)); + } + tokens.extend(quote! { #(#items),* }); + } +} + +impl RewriteAttribute for SubStatesArgs { + fn expand_args(&self, plugin: &NonEmptyPath) -> MacroStream { + let mut args = Vec::new(); + args.push(quote! { plugin = #plugin }); + if !self.generics().is_empty() { + args.extend(self.generics().to_attribute_arg_vec_tokens()); + } + quote! { #(#args),* } + } + + fn expand_attrs(&self, plugin: &NonEmptyPath) -> ExpandAttrs { + let mut expanded_attrs = ExpandAttrs::default(); + + if self.derive.present { + expanded_attrs.append(tokens::derive_sub_states(&self.derive.items)); + } + if self.reflect.present { + if self.derive.present { + expanded_attrs.attrs.push(tokens::derive_reflect()); + } + expanded_attrs.append(tokens::reflect(&self.reflect.items)) + } + if self.register { + expanded_attrs + .attrs + .push(tokens::auto_register_type(plugin.clone(), self.into())); + expanded_attrs.attrs.push(tokens::auto_register_state_type( + plugin.clone(), + self.into(), + )); + } + if self.init { + expanded_attrs + .attrs + .push(tokens::auto_init_sub_states(plugin.clone(), self.into())); + } + expanded_attrs + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::macro_api::with_plugin::WithPlugin; + use crate::test_util::combo::combos_one_per_group_or_skip; + use crate::test_util::macros::*; + use darling::ast::NestedMeta; + use internal_test_proc_macro::xtest; + use internal_test_util::{extract_punctuated_paths, vec_spread}; + use quote::ToTokens; + use syn::parse_quote; + + #[xtest] + fn test_expand_back_into_args() -> syn::Result<()> { + for args in combos_one_per_group_or_skip(&[ + vec![quote!(derive), quote!(derive(Debug, Default))], + vec![quote!(reflect), quote!(reflect(Debug, Default))], + vec![quote!(register)], + vec![quote!(init)], + ]) { + println!("checking args: {}", quote! { #(#args),*}); + assert_vec_args_expand!(plugin!(parse_quote!(Test)), SubStatesArgs, args); + } + Ok(()) + } + + #[xtest] + fn test_expand_attrs_global() -> syn::Result<()> { + let extras = extract_punctuated_paths(parse_quote!(A, B)) + .into_iter() + .map(NonEmptyPath::try_from) + .collect::>>()?; + let args: NestedMeta = parse_quote! {_( + plugin = Test, + derive(#(#extras),*), + reflect(#(#extras),*), + register, + init, + )}; + let args = WithPlugin::::from_nested_meta(&args)?; + let derive_attr = tokens::derive_sub_states(&extras); + let derive_reflect_path = tokens::derive_reflect_path(); + let reflect_args = vec_spread![..extras,]; + let reflect_attr = tokens::reflect(reflect_args.iter().map(NonEmptyPath::last_ident)); + println!( + "{}", + args.inner.expand_attrs(&args.plugin()).to_token_stream() + ); + assert_eq!( + args.inner + .expand_attrs(&args.plugin()) + .to_token_stream() + .to_string(), + ExpandAttrs { + use_items: [derive_attr.use_items, reflect_attr.use_items].concat(), + attrs: vec_spread![ + ..derive_attr.attrs, + // TODO: merge these derives + quote! { #[derive(#derive_reflect_path)] }, + quote! { #[reflect(#(#reflect_args),*)] }, + tokens::auto_register_type(args.plugin(), (&args.inner).into()), + tokens::auto_register_state_type(args.plugin(), (&args.inner).into()), + tokens::auto_init_sub_states(args.plugin(), (&args.inner).into()), + ] + } + .to_token_stream() + .to_string() + ); + Ok(()) + } +} diff --git a/crates/bevy_auto_plugin_shared/src/macro_api/attributes/rewrites/mod.rs b/crates/bevy_auto_plugin_shared/src/macro_api/attributes/rewrites/mod.rs index a379c909..4121e3b7 100644 --- a/crates/bevy_auto_plugin_shared/src/macro_api/attributes/rewrites/mod.rs +++ b/crates/bevy_auto_plugin_shared/src/macro_api/attributes/rewrites/mod.rs @@ -4,4 +4,5 @@ pub mod auto_message; pub mod auto_observer; pub mod auto_resource; pub mod auto_states; +pub mod auto_sub_states; pub mod auto_system; diff --git a/src/lib.rs b/src/lib.rs index 588561a4..9f497394 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -62,6 +62,9 @@ pub mod prelude { #[doc(inline)] pub use bevy_auto_plugin_proc_macros::auto_states; + #[doc(inline)] + pub use bevy_auto_plugin_proc_macros::auto_sub_states; + #[doc(inline)] pub use bevy_auto_plugin_proc_macros::auto_system; diff --git a/tests/e2e/auto_plugin.rs b/tests/e2e/auto_plugin.rs index 0d947e89..05dd528e 100644 --- a/tests/e2e/auto_plugin.rs +++ b/tests/e2e/auto_plugin.rs @@ -66,6 +66,16 @@ enum FooState { End, } +#[derive(SubStates, Debug, Default, Copy, Clone, PartialEq, Eq, Hash, Reflect)] +#[auto_init_sub_state(plugin = Test)] +#[auto_register_state_type(plugin = Test)] +#[source(FooState = FooState::Start)] +enum FooSubState { + #[default] + Start, + End, +} + #[auto_states(plugin = Test, derive, register, reflect, init)] enum FooState2 { #[default] @@ -73,6 +83,14 @@ enum FooState2 { End, } +#[auto_sub_states(plugin = Test, derive, register, reflect, init)] +#[source(FooState2 = FooState2::Start)] +enum FooSubState2 { + #[default] + Start, + End, +} + #[auto_add_system(plugin = Test, schedule = Update)] fn foo_system(mut foo_res: ResMut) { foo_res.0 += 1; @@ -280,6 +298,89 @@ fn test_auto_init_state_type_foo_state() { ); } +#[xtest] +fn test_auto_register_state_type_foo_state_2() { + let app = app(); + let type_registry = app.world().resource::().0.clone(); + let type_registry = type_registry.read(); + assert!( + type_registry.contains(type_id_of::>()), + "did not auto register type" + ); + assert!( + type_registry.contains(type_id_of::>()), + "did not auto register type" + ); +} + +#[xtest] +fn test_auto_init_state_type_foo_state_2() { + let app = app(); + assert_eq!( + app.world() + .get_resource::>() + .map(Deref::deref), + Some(&FooState2::Start), + "did not auto init state" + ); +} + +#[xtest] +fn test_auto_register_sub_state_type_foo_sub_state() { + let app = app(); + let type_registry = app.world().resource::().0.clone(); + let type_registry = type_registry.read(); + assert!( + type_registry.contains(type_id_of::>()), + "did not auto register type" + ); + assert!( + type_registry.contains(type_id_of::>()), + "did not auto register type" + ); +} + +#[xtest] +fn test_auto_init_sub_state_type_foo_sub_state() { + let mut app = app(); + app.update(); // needed to figure out we're in the right state + assert_eq!( + app.world() + .get_resource::>() + .map(Deref::deref), + Some(&FooSubState::Start), + "did not auto init substate" + ); +} + +#[xtest] +fn test_auto_register_sub_state_type_foo_sub_state_2() { + let app = app(); + let type_registry = app.world().resource::().0.clone(); + let type_registry = type_registry.read(); + assert!( + type_registry.contains(type_id_of::>()), + "did not auto register type" + ); + assert!( + type_registry.contains(type_id_of::>()), + "did not auto register type" + ); +} + +#[xtest] +fn test_auto_init_sub_state_type_foo_sub_state_2() { + let mut app = app(); + app.update(); // needed to figure out we're in the right state + assert_eq!( + app.world() + .get_resource::>() + .map(Deref::deref), + Some(&FooSubState2::Start), + "did not auto init substate" + ); +} + #[xtest] fn test_auto_add_observer_foo_observer() { let mut app = app();