bevy_tnua_macros/
lib.rs

1use proc_macro2::TokenStream;
2use syn::{DeriveInput, parse::Error, parse_macro_input, spanned::Spanned};
3
4use self::action_slots_derive::codegen::generate_action_slots_derive;
5use self::action_slots_derive::parsed::ParsedActionSlots;
6use self::scheme_derive::codegen::generate_scheme_derive;
7use self::scheme_derive::parsed::ParsedScheme;
8
9mod action_slots_derive;
10mod scheme_derive;
11
12/// Make an enum a control scheme for a Tnua character controller.
13///
14/// This implements the `TnuaScheme` trait for the enum, and also generates the following structs
15/// required for implementing it (replace `{name}` with the name of the control scheme enum):
16///
17/// * `{name}Config` - a struct with the configuration of the basis and all the actions.
18/// * `{name}ActionDiscriminant` - an enum mirroring the control scheme, except all the variants
19///   are units.
20/// * `{name}ActionState` - an enum mirroring the control scheme, except instead of just the input
21///   types each variant contains a `TnuaActionState` which holds the input, configuration and
22///   memory of the action.
23///
24/// The enum itself **must** have a `#[scheme(basis = ...)]` attribute that specifies the basis of
25/// the control scheme (typically `TnuaBuiltinWalk`). The following additional parameters are
26/// allowed on that `scheme` attribute on the enum:
27///
28/// * `#[scheme(serde)]` - derive Serialize and Deserialize on the generated action state enum.
29///   * This is mostly useful with (and will probably fail without) the `serialize` feature enabled
30///     on the bevy-tnua crate.
31///   * The control scheme enum itself will not get these derives automatically - that derive will
32///     need to be added manually.
33///   * With these, and with the `serialize` feature enabled, the `TnuaController` and
34///     `TnuaGhostOverwrites` of the control scheme will also be serializable and deserializable -
35///     allowing networking libraries to synchronize them between machines.
36///   * Even without this setting and without the `serialize` feature on the bevy-tnua crate, the
37///     generated configuration struct and the action discriminant enum will still get these
38///     derives.
39/// * `#[scheme(config_ext = ...)]` - add an extension field to the configuration struct generated
40///   for the control scheme. The field will have the name `ext` and the type specified by this
41///   parameter. This allows adding user-defined settings that the user control systems can utilize
42///   for character control related decisions (e.g. - max number of air actions allowed), and load
43///   these settings from the same asset.
44///
45/// Each variant **must** be a tuple variant, where the first element of the tuple is the action,
46/// followed by zero or more payloads.
47///
48/// Payloads are ignored by Tnua itself - they are for the user systems to keep track of data
49/// related to the actions - except when they are annotated by `#[scheme(modify_basis_config)]`.
50/// Such payloads will modify the configuration when the action they are part of is in effect.
51///
52/// A variant may have a `#[scheme(...)]` attribute, supporting the following parameters:
53///
54/// * `#[scheme(same_trigger(OtherAction)]` - when `OtherAction` is one of the other action
55///   variants (which must not have a `same_trigger` of its own). This will get both actions (as
56///   well as any other action annotated with the same `same_trigger`) to share a slot in Tnua's
57///   feeding mechanism - which means that if one action is fed, all the others are treated as if
58///   alredy fed. Use this for actions that share a button - for example, a regular jump and a
59///   wall-jump. Without this mechanism, if the player holds the jump button and jumps toward a
60///   wall, the moment the user control system detects that the conditions for a wall-jump are met
61///   it'll send the wall-jump action - and since that action was not fed that frame, Tnua will
62///   consider it a new action ("just pressed") and immediately invoke the wall-jump. But if the
63///   wall-jump has `same_trigger` as the jump - Tnua will see that the jump action is still being
64///   fed (even if the action itself is over) and thus the wall-jump will also be considered
65///   "already fed" and won't trigger until the player releases and re-presses the button.
66///
67/// Example:
68///
69/// ```ignore
70/// #[derive(TnuaScheme)]
71/// #[scheme(basis = TnuaBuiltinWalk)]
72/// pub enum ControlScheme {
73///     Jump(TnuaBuiltinJump),
74///     Crouch(
75///         TnuaBuiltinCrouch,
76///         // While this action is in effect, `SlowDownWhileCrouching` will change the
77///         // `TnuaBuiltinWalkConfig` to reduce character speed.
78///         #[scheme(modify_basis_config)] SlowDownWhileCrouching,
79///     ),
80///     WallSlide(
81///         TnuaBuiltinWallSlide,
82///         // This payload has is ignored by Tnua, but user code can use it to tell which wall
83///         // the character is sliding on.
84///         Entity,
85///     ),
86///     // The wall-jump uses the same button as the jump, so we annotate them with `same_trigger`.
87///     #[scheme(same_trigger(Jump))]
88///     // Wall-jump also uses `TnuaBuiltinJump`, but it's a separate variant so that it can have
89///     // its own configuration and so that systems that introspect the current action can tell
90///     // the difference - e.g. the animating system can play a different animation.
91///     WallJump(TnuaBuiltinJump)
92/// }
93/// ```
94///
95#[proc_macro_derive(TnuaScheme, attributes(scheme))]
96pub fn derive_tnua_scheme(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
97    let input = parse_macro_input!(input as DeriveInput);
98    match impl_derive_tnua_scheme(&input) {
99        Ok(output) => output.into(),
100        Err(error) => error.to_compile_error().into(),
101    }
102}
103
104fn impl_derive_tnua_scheme(ast: &syn::DeriveInput) -> Result<TokenStream, Error> {
105    Ok(match &ast.data {
106        syn::Data::Struct(_) => {
107            return Err(Error::new(
108                ast.span(),
109                "TnuaScheme is not supported for structs - only for enums",
110            ));
111        }
112        syn::Data::Enum(data_enum) => {
113            let parsed = ParsedScheme::new(ast, data_enum)?;
114            generate_scheme_derive(&parsed)?
115        }
116        syn::Data::Union(_) => {
117            return Err(Error::new(
118                ast.span(),
119                "TnuaScheme is not supported for unions - only for enums",
120            ));
121        }
122    })
123}
124
125/// Define the behavior of action that can be performed a limited amount of times during certain
126/// durations (e.g. air actions)
127///
128/// This macro must be defined on a struct with a `#[slots(scheme = ...)]` attribute on the struct
129/// itself, pointing to a [`TnuaScheme`] that the slots belong to.
130///
131/// Each field of the struct must have the type [`usize`], and have a `#[slots(...)]` attribute on
132/// it listing the actions (variants of the scheme enum) belonging to that slot.
133///
134/// Not all actions need to be assigned to slots, but every slot needs at least one action assigned
135/// to it.
136///
137/// A single action must not be assigned to more than one slot, but a single slot is allowed to
138/// have multiple actions (`#[slots(Action1, Action2, ...)]`)
139///
140/// The main attribute on the struct can also have a `#[slots(ending(...))]` parameter, listing
141/// actions that end the counting. This is used to signal that the counting should start anew after
142/// these actions, even if the regular conditions for terminating and re-starting the counting
143/// don't occur. For example - when counting air actions, a wall slide should end the counting so
144/// that after jumping from it'd be a new air duration and the player could air-jump again even if
145/// they've exhausted all the air jumps before the wall slide.
146///
147/// Example:
148///
149/// ```ignore
150/// #[derive(Debug, TnuaActionSlots)]
151/// #[slots(scheme = ControlScheme, ending(WallSlide))]
152/// pub struct DemoControlSchemeAirActions {
153///     #[slots(Jump)]
154///     jump: usize,
155///     #[slots(Dash)]
156///     dash: usize,
157///     // Other actions, like `Crouch`
158/// }
159/// ```
160#[proc_macro_derive(TnuaActionSlots, attributes(slots))]
161pub fn derive_tnua_action_slots(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
162    let input = parse_macro_input!(input as DeriveInput);
163    match impl_derive_tnua_action_slots(&input) {
164        Ok(output) => output.into(),
165        Err(error) => error.to_compile_error().into(),
166    }
167}
168
169fn impl_derive_tnua_action_slots(ast: &syn::DeriveInput) -> Result<TokenStream, Error> {
170    generate_action_slots_derive(&ParsedActionSlots::new(ast)?)
171}