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}