@ -1,31 +1,37 @@
//! Proc-macros for the `engine` crate.
//!
//! Currently provides [`GameAction`](macro@GameAction), a derive that annotates
//! each enum variant with its [`ActionKind`] via `#[action_kind(...)]`.
//! Provides [`GameAction`](macro@GameAction), a derive that annotates each enum
//! variant with its [`ActionKind`] via `#[action_kind(...)]` and, optionally, a
//! system to run for it via `#[action_handler(...)]`.
use proc_macro ::TokenStream ;
use proc_macro_crate ::{ crate_name , FoundCrate } ;
use quote ::{ format_ident , quote } ;
use syn ::{ spanned ::Spanned , Data , DeriveInput , Fields , Ident };
use syn ::{ spanned ::Spanned , Data , DeriveInput , Fields , Ident , Path };
/// Derives `engine::GameAction` for a fieldless enum.
/// Derives `engine::GameAction` (and `engine::GameActionHandlers`) for a
/// fieldless enum.
///
/// Each variant must be annotated with `#[action_kind(...)]` — one of `instant`,
/// `toggle`, `axis`, or `scalar`:
/// `toggle`, `axis`, or `scalar` — and may carry any number of
/// `#[action_handler(path::to::system)]` attributes:
///
/// ```ignore
/// #[derive(Copy, Clone, Eq, PartialEq, Hash, GameAction)]
/// enum Action {
/// #[action_kind(instant)]
/// #[action_kind(instant)] #[action_handler(jump)]
/// Jump,
/// #[action_kind( toggle)]
/// Grid ,
/// #[action_kind( axis)] #[action_handler(move_player)] // fn(In<Vec2>, ..)
/// Move ,
/// }
/// ```
///
/// This expands to an `impl GameAction for Action` whose `kind` matches each
/// variant to its declared [`ActionKind`].
#[ proc_macro_derive(GameAction, attributes(action_kind)) ]
/// This expands to an `impl GameAction` whose `kind` maps each variant to its
/// declared [`ActionKind`], plus an `impl GameActionHandlers` whose `add_handlers`
/// registers each handler wired for its kind — a gated plain system for
/// `instant`/`toggle`, a typed pipe (`axis_value`/`scalar_value`) plus gate for
/// `axis`/`scalar`. `ActionsPlugin` calls `add_handlers` for you.
#[ proc_macro_derive(GameAction, attributes(action_kind, action_handler)) ]
pub fn derive_game_action ( input : TokenStream ) -> TokenStream {
let input = syn ::parse_macro_input ! ( input as DeriveInput ) ;
expand ( input )
@ -33,6 +39,26 @@ pub fn derive_game_action(input: TokenStream) -> TokenStream {
. into ( )
}
#[ derive(Clone, Copy) ]
enum Kind {
Instant ,
Toggle ,
Axis ,
Scalar ,
}
impl Kind {
/// The `ActionKind` expression this maps to.
fn to_tokens ( self , engine : & proc_macro2 ::TokenStream ) -> proc_macro2 ::TokenStream {
match self {
Kind ::Instant = > quote ! ( #engine ::ActionKind ::Instant ) ,
Kind ::Toggle = > quote ! ( #engine ::ActionKind ::Toggle ) ,
Kind ::Axis = > quote ! ( #engine ::ActionKind ::Axis ) ,
Kind ::Scalar = > quote ! ( #engine ::ActionKind ::Scalar ) ,
}
}
}
fn expand ( input : DeriveInput ) -> syn ::Result < proc_macro2 ::TokenStream > {
let Data ::Enum ( data ) = & input . data else {
return Err ( syn ::Error ::new (
@ -45,40 +71,93 @@ fn expand(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
let ident = & input . ident ;
let ( impl_generics , ty_generics , where_clause ) = input . generics . split_for_impl ( ) ;
let arms = data
. variants
. iter ( )
. map ( | variant | {
if ! matches! ( variant . fields , Fields ::Unit ) {
return Err ( syn ::Error ::new (
variant . fields . span ( ) ,
"`GameAction` variants must be fieldless (unit variants)" ,
) ) ;
let mut kind_arms = Vec ::new ( ) ;
let mut handler_regs = Vec ::new ( ) ;
for variant in & data . variants {
if ! matches! ( variant . fields , Fields ::Unit ) {
return Err ( syn ::Error ::new (
variant . fields . span ( ) ,
"`GameAction` variants must be fieldless (unit variants)" ,
) ) ;
}
let variant_ident = & variant . ident ;
let kind = parse_kind ( variant ) ? ;
let kind_tokens = kind . to_tokens ( & engine ) ;
kind_arms . push ( quote ! ( Self ::#variant_ident = > #kind_tokens ) ) ;
for handler in parse_handlers ( variant ) ? {
handler_regs . push ( gen_handler ( & engine , variant_ident , kind , & handler ) ) ;
}
}
// Keep the generated `add_handlers` warning-clean whether or not there are
// any handlers: no unused `app` param and no unused trait imports.
let handlers_body = if handler_regs . is_empty ( ) {
quote ! {
fn add_handlers ( _app : & mut #engine ::App ) { }
}
} else {
quote ! {
fn add_handlers ( app : & mut #engine ::App ) {
// Bring the builder methods into scope without polluting names.
#[ allow(unused_imports) ]
use #engine ::IntoScheduleConfigs as _ ;
#[ allow(unused_imports) ]
use #engine ::IntoSystem as _ ;
#( #handler_regs ) *
}
let variant_ident = & variant . ident ;
let kind = parse_kind ( variant , & engine ) ? ;
Ok ( quote ! ( Self ::#variant_ident = > #kind ) )
} )
. collect ::< syn ::Result < Vec < _ > > > ( ) ? ;
}
} ;
Ok ( quote ! {
impl #impl_generics #engine ::GameAction for #ident #ty_generics #where_clause {
fn kind ( self ) -> #engine ::ActionKind {
match self {
#( #arms , ) *
#( #kind_ arms, ) *
}
}
}
impl #impl_generics #engine ::GameActionHandlers for #ident #ty_generics #where_clause {
#handlers_body
}
} )
}
/// Parses the single required `#[action_kind(instant|toggle)]` attribute on a
/// variant into the corresponding `ActionKind` expression.
fn parse_kind (
variant : & syn ::Variant ,
/// Emits one `add_systems` call for a variant's handler, wired for its kind.
fn gen_handler (
engine : & proc_macro2 ::TokenStream ,
) -> syn ::Result < proc_macro2 ::TokenStream > {
let mut kind : Option < proc_macro2 ::TokenStream > = None ;
variant_ident : & Ident ,
kind : Kind ,
handler : & Path ,
) -> proc_macro2 ::TokenStream {
let action = quote ! ( Self ::#variant_ident ) ;
match kind {
// No payload: gate a plain system on the action being active.
Kind ::Instant | Kind ::Toggle = > quote ! {
app . add_systems ( #engine ::Update , #handler . run_if ( #engine ::on_action ( #action ) ) ) ;
} ,
// Vec2 payload: pipe it in, gated so it runs while the axis is active.
Kind ::Axis = > quote ! {
app . add_systems (
#engine ::Update ,
#engine ::axis_value ( #action ) . pipe ( #handler ) . run_if ( #engine ::on_action ( #action ) ) ,
) ;
} ,
// f32 payload: pipe it in, gated so it runs when the slider changes.
Kind ::Scalar = > quote ! {
app . add_systems (
#engine ::Update ,
#engine ::scalar_value ( #action ) . pipe ( #handler ) . run_if ( #engine ::on_action ( #action ) ) ,
) ;
} ,
}
}
/// Parses the single required `#[action_kind(...)]` attribute on a variant.
fn parse_kind ( variant : & syn ::Variant ) -> syn ::Result < Kind > {
let mut kind : Option < Kind > = None ;
for attr in & variant . attrs {
if ! attr . path ( ) . is_ident ( "action_kind" ) {
@ -90,20 +169,20 @@ fn parse_kind(
"duplicate `#[action_kind(...)]` attribute" ,
) ) ;
}
let variant_ name: Ident = attr . parse_args ( ) . map_err ( | _ | {
let name: Ident = attr . parse_args ( ) . map_err ( | _ | {
syn ::Error ::new (
attr . span ( ) ,
"expected `#[action_kind(instant )]` or `#[action_kind(toggle )]`",
"expected `#[action_kind(instant |toggle|axis|scalar )]`",
)
} ) ? ;
kind = Some ( match variant_ name. to_string ( ) . as_str ( ) {
"instant" = > quote! ( #engine ::Action Kind::Instant ) ,
"toggle" = > quote! ( #engine ::Action Kind::Toggle ) ,
"axis" = > quote! ( #engine ::Action Kind::Axis ) ,
"scalar" = > quote! ( #engine ::Action Kind::Scalar ) ,
kind = Some ( match name. to_string ( ) . as_str ( ) {
"instant" = > Kind::Instant ,
"toggle" = > Kind::Toggle ,
"axis" = > Kind::Axis ,
"scalar" = > Kind::Scalar ,
other = > {
return Err ( syn ::Error ::new (
variant_ name. span ( ) ,
name. span ( ) ,
format! (
"unknown action kind `{other}`, expected `instant`, `toggle`, `axis`, or `scalar`"
) ,
@ -116,13 +195,31 @@ fn parse_kind(
syn ::Error ::new (
variant . ident . span ( ) ,
format! (
"variant `{}` needs `#[action_kind(instant )]` or `#[action_kind(toggle )]`",
"variant `{}` needs `#[action_kind(instant |toggle|axis|scalar )]`",
variant . ident
) ,
)
} )
}
/// Parses every `#[action_handler(path)]` attribute on a variant (zero or more).
fn parse_handlers ( variant : & syn ::Variant ) -> syn ::Result < Vec < Path > > {
let mut handlers = Vec ::new ( ) ;
for attr in & variant . attrs {
if ! attr . path ( ) . is_ident ( "action_handler" ) {
continue ;
}
let path : Path = attr . parse_args ( ) . map_err ( | _ | {
syn ::Error ::new (
attr . span ( ) ,
"expected `#[action_handler(path::to::system)]`" ,
)
} ) ? ;
handlers . push ( path ) ;
}
Ok ( handlers )
}
/// Resolves the path to the `engine` crate so the derive works whether it is
/// invoked from a game, from `engine`'s own tests, or under a renamed dependency.
fn engine_path ( ) -> proc_macro2 ::TokenStream {