Make ActionSource usable in bsn!; clearer macro errors

- Derive Default on ActionSource so it satisfies bevy's template system
  (Template/FromTemplate are blanket-impl'd only for Clone + Default + Unpin),
  letting it be spawned as a component in a bsn! scene. The bound sits on the
  derived Default impl only, so the Component impl stays generic over every
  GameAction — the fan-in systems keep working for non-Default actions.
- Improve #[derive(GameAction)] diagnostics: each now names the offending
  variant and shows the exact expected syntax (not-an-enum, non-unit variant,
  missing/duplicate/unknown action_kind, malformed action_handler).
- Add a #[cfg(test)] module exercising all four kinds + an #[action_handler]
  each + ActionsPlugin + App::on_action, proving the macro codegen compiles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
main
Elijah Voigt 6 days ago
parent 33c20d47a0
commit cf3a81f4bd

@ -63,7 +63,11 @@ fn expand(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
let Data::Enum(data) = &input.data else {
return Err(syn::Error::new(
input.ident.span(),
"`GameAction` can only be derived for enums",
format!(
"`GameAction` can only be derived for enums, but `{}` is not one.\n\
An action set is a fixed list of variants, e.g. `enum {} {{ #[action_kind(instant)] Jump }}`.",
input.ident, input.ident
),
));
};
@ -78,7 +82,11 @@ fn expand(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
if !matches!(variant.fields, Fields::Unit) {
return Err(syn::Error::new(
variant.fields.span(),
"`GameAction` variants must be fieldless (unit variants)",
format!(
"action variant `{0}` must be a unit variant (no fields).\n\
Actions are plain identifiers used as map keys write `{0}`, not `{0}(..)` or `{0} {{ .. }}`.",
variant.ident
),
));
}
let variant_ident = &variant.ident;
@ -166,13 +174,20 @@ fn parse_kind(variant: &syn::Variant) -> syn::Result<Kind> {
if kind.is_some() {
return Err(syn::Error::new(
attr.span(),
"duplicate `#[action_kind(...)]` attribute",
format!(
"action variant `{}` has more than one `#[action_kind(..)]`; each variant declares exactly one kind",
variant.ident
),
));
}
let name: Ident = attr.parse_args().map_err(|_| {
syn::Error::new(
attr.span(),
"expected `#[action_kind(instant|toggle|axis|scalar)]`",
format!(
"malformed `#[action_kind(..)]` on `{}`: expected a single kind one of \
`instant`, `toggle`, `axis`, or `scalar`, e.g. `#[action_kind(instant)]`",
variant.ident
),
)
})?;
kind = Some(match name.to_string().as_str() {
@ -184,7 +199,8 @@ fn parse_kind(variant: &syn::Variant) -> syn::Result<Kind> {
return Err(syn::Error::new(
name.span(),
format!(
"unknown action kind `{other}`, expected `instant`, `toggle`, `axis`, or `scalar`"
"unknown action kind `{other}` on `{}`; expected `instant`, `toggle`, `axis`, or `scalar`",
variant.ident
),
))
}
@ -195,7 +211,8 @@ fn parse_kind(variant: &syn::Variant) -> syn::Result<Kind> {
syn::Error::new(
variant.ident.span(),
format!(
"variant `{}` needs `#[action_kind(instant|toggle|axis|scalar)]`",
"action variant `{0}` is missing its kind; add one of \
`#[action_kind(instant|toggle|axis|scalar)]`, e.g. `#[action_kind(instant)] {0}`",
variant.ident
),
)
@ -212,7 +229,11 @@ fn parse_handlers(variant: &syn::Variant) -> syn::Result<Vec<Path>> {
let path: Path = attr.parse_args().map_err(|_| {
syn::Error::new(
attr.span(),
"expected `#[action_handler(path::to::system)]`",
format!(
"malformed `#[action_handler(..)]` on `{}`: expected a path to a system \
function, e.g. `#[action_handler(jump)]` or `#[action_handler(sim::step)]`",
variant.ident
),
)
})?;
handlers.push(path);

@ -427,8 +427,18 @@ impl<T: GameAction> ActionState<T> {
/// ([`Toggle`](ActionKind::Toggle)), or `Slider` ([`Scalar`](ActionKind::Scalar)) —
/// the engine wires the widget's events into [`ActionState`] and keeps checkboxes
/// visually in sync.
///
/// To spawn it inside a `bsn!` scene, the action type must be `Default` (Bevy's
/// template system blanket-requires `Clone + Default` for any component in a
/// scene), and the entry needs a turbofish so the generic can be inferred in
/// template position: `ActionSource::<MyAction>(MyAction::Jump)`. `Default` is
/// derived here conditionally, so it is available exactly when the action is.
///
/// Note the `Default` bound lives only on this derived `Default` impl — the
/// `Component` impl stays generic over every [`GameAction`], so the fan-in
/// systems work for actions that are not `Default`.
#[derive(Component, Copy, Clone, Debug, Default)]
pub struct ActionSource<T: GameAction + Default>(pub T);
pub struct ActionSource<T: GameAction>(pub T);
/// The two symmetrical entry points below let any system consume any action:
///
@ -744,3 +754,50 @@ impl<T: GameActionHandlers> Plugin for ActionsPlugin<T> {
T::add_handlers(app);
}
}
#[cfg(test)]
mod tests {
use super::*;
// Exercises the `#[derive(GameAction)]` codegen: every kind, plus an
// `#[action_handler(..)]` per kind wired for its expected signature. If the
// generated `impl GameAction` / `impl GameActionHandlers` (with its
// kind-specific `add_systems`/pipe wiring) fails to type-check, this won't
// compile.
#[derive(Copy, Clone, Eq, PartialEq, Hash, GameAction)]
enum TestAction {
#[action_kind(instant)]
#[action_handler(on_jump)]
Jump,
#[action_kind(toggle)]
#[action_handler(on_grid)]
Grid,
#[action_kind(axis)]
#[action_handler(on_move)]
Move,
#[action_kind(scalar)]
#[action_handler(on_volume)]
Volume,
}
fn on_jump() {}
fn on_grid() {}
fn on_move(_dir: In<Vec2>) {}
fn on_volume(_level: In<f32>) {}
#[test]
fn derive_and_handlers_compile() {
assert_eq!(TestAction::Jump.kind(), ActionKind::Instant);
assert_eq!(TestAction::Move.kind(), ActionKind::Axis);
assert_eq!(TestAction::Volume.kind(), ActionKind::Scalar);
// Registers the generated handlers, exercising the piped/gated wiring
// and the `AppActionExt::on_action` path shape.
let mut app = App::new();
app.add_plugins(
ActionsPlugin::<TestAction>::new()
.bind(TestAction::Jump, [Binding::Key(KeyCode::Space)]),
);
app.on_action(Update, TestAction::Jump, on_jump);
}
}

Loading…
Cancel
Save