Skip to main content

bevy_ergo_plugin/
lib.rs

1//! Macros to make building bevy plugins more ergonomic (in my opinion).\
2//!\
3//! Bevy's API puts adding a system separate from its implementation. Separatng the system's run conditions and other system parameters from its definition adds an extra layer of indirection, harming readability and adding boilerplate (`.add_system`)\
4//! This crate's purpose is to replace that API with a more ergonomic one, using attribute macros as markers for system parameters.\
5//! Not only does this allow for more readable system run conditions, but it also gives us fallible systems with logging much more cleanly, while still using bevy's built-ins.\
6//!\
7//! Putting the [macro@bevy_plugin] attribute on the `impl` block of a struct will turn that struct into a Bevy Plugin, which registers its associated functions as systems.\
8//! If you want to add extra functionality to your plugin's `Plugin::build` (like adding an asset or registering a component), the contents of any associated function named `build` in a [macro@bevy_plugin] attributed `impl` block will be inserted into the generated `Plugin::build` implementation.\
9//!\
10//! The other macros are the aforementioned parameter markers to be put on your system definitions.\
11//! For any params not included by a specific marker, you can use the [macro@sysparam] marker to define custom behavior.\
12//!\
13//! Multiple parameter markers on a system do stack onto a single `add_system` call, so you can add multiple run conditions and have it work as expected.  
14//!\
15//! This crate is basically just doing code generation, so it doesn't depend on bevy itself. It should work with any bevy version that has the functions you're generating.\
16//!\
17//! **This crate has not been thoroughly tested, so there will probably be weird bugs I don't know about.**
18//!# Example
19//! adapted from <https://github.com/bevyengine/bevy/blob/latest/examples/ecs/run_conditions.rs>\
20//!
21//!    use bevy::prelude::*;
22//!    use bevy_ergo_plugin::*;
23//!    
24//!    fn main() {
25//!        println!();
26//!        println!("For the first 2 seconds you will not be able to increment the counter");
27//!        println!("Once that time has passed you can press space, enter, left mouse, right mouse or touch the screen to increment the counter");
28//!        println!();
29//!    
30//!        App::new()
31//!            .add_plugins(DefaultPlugins)
32//!            .add_plugin(Game)
33//!            .run();
34//!    }
35//!    #[derive(Resource, Default)]
36//!    pub struct InputCounter(usize);
37//!    
38//!    pub struct Game;
39//!    #[bevy_plugin]
40//!    impl Game {
41//!        #[resource_exists(InputCounter)]
42//!        #[run_if(Game::has_user_input)]
43//!        pub fn increment_input_counter(mut counter: ResMut<InputCounter>) {
44//!            counter.0 += 1;
45//!        }
46//!    
47//!        #[run_if(resource_exists::<InputCounter>().and_then(
48//!            |counter: Res<InputCounter>| counter.is_changed() && !counter.is_added()
49//!        ))]
50//!        pub fn print_input_counter(counter: Res<InputCounter>) {
51//!            println!("Input counter: {}", counter.0);
52//!        }
53//!    
54//!        #[run_if(Game::time_passed(2.0))]
55//!        #[run_if(not(Game::time_passed(2.5)))]
56//!        pub fn print_time_message() {
57//!            println!(
58//!                "It has been more than 2 seconds since the program started and less than 2.5 seconds"
59//!            );
60//!        }
61//!    
62//!        #[do_not_add]
63//!        pub fn has_user_input(
64//!            keyboard_input: Res<Input<KeyCode>>,
65//!            mouse_button_input: Res<Input<MouseButton>>,
66//!            touch_input: Res<Touches>,
67//!        ) -> bool {
68//!            keyboard_input.just_pressed(KeyCode::Space)
69//!                || keyboard_input.just_pressed(KeyCode::Return)
70//!                || mouse_button_input.just_pressed(MouseButton::Left)
71//!                || mouse_button_input.just_pressed(MouseButton::Right)
72//!                || touch_input.any_just_pressed()
73//!        }
74//!    
75//!        #[do_not_add]
76//!        pub fn time_passed(t: f32) -> impl FnMut(Local<f32>, Res<Time>) -> bool {
77//!            move |mut timer: Local<f32>, time: Res<Time>| {
78//!                *timer += time.delta_seconds();
79//!                *timer >= t
80//!            }
81//!        }
82//!    
83//!        pub fn build(&self, app: &mut App) {
84//!            app.init_resource::<InputCounter>();
85//!        }
86//!    }
87//!
88extern crate proc_macro;
89
90use itertools::Itertools;
91use proc_macro::TokenStream;
92use quote::{quote, ToTokens};
93use syn::{parse_macro_input, Attribute, ImplItem, ItemImpl, Meta};
94
95// macro to:
96//1: create a simple attribute macro that doesn't do anything, the autobuild macro will look at these to generate params to add to the systems
97// we need these because attribute macros dont have helper attributes like derive macros do
98//2: store the quotes along with the name of the helper.
99// this implementation is pretty janky but it avoids a bunch of boilerplate i dont wanna look at or write
100// i.e: name => Some(quote!{the actual thing}) for every single param i define
101macro_rules! fake_helpers{
102    ($len:literal, $(($name:ident, $val:expr)),+) => {
103        $(///system parameter marker attribute that generates\
104        ///`.add_system(
105        #[doc=stringify!(system_name.$val)]
106        ///)`
107        #[proc_macro_attribute]
108        pub fn $name(_attr: TokenStream, input: TokenStream) -> TokenStream {
109            input
110        })
111        +
112        const PARAM_HELPERS: [(&'static str, &'static str ); $len]= [
113            $((stringify!($name), stringify!($val))),+
114        ];
115
116     }
117}
118
119fake_helpers!(
120    34,
121    (sysparam, arg),
122    (startup, on_startup()),
123    (run_if, run_if(arg)),
124    (run_once, run_if(run_once())),
125    (before, before(arg)),
126    (after, after(arg)),
127    (pipe, pipe(arg)),
128    (dbg, pipe(system_adapter::dbg)),
129    (error, pipe(system_adapter::error)),
130    (ignore, pipe(system_adapter::ignore)),
131    (info, pipe(system_adapter::info)),
132    (unwrap, pipe(system_adapter::unwrap)),
133    (warn, pipe(system_adapter::warn)),
134    (not, run_if(not(arg))),
135    (any_with_component, run_if(any_with_component::<arg>())),
136    (resource_added, run_if(resource_added::<arg>())),
137    (resource_changed, run_if(resource_changed::<arg>())),
138    (
139        resource_changed_or_removed,
140        run_if(resource_changed_or_removed::<arg>())
141    ),
142    (resource_equals, run_if(resource_equals(arg))),
143    (resource_exists, run_if(resource_exists::<arg>())),
144    (
145        resource_exists_and_changed,
146        run_if(resource_exists_and_changed::<arg>())
147    ),
148    (
149        resource_exists_and_equals,
150        run_if(resource_exists_and_equals(arg))
151    ),
152    (resource_removed, run_if(resource_removed::<arg>())),
153    (on_event, run_if(on_event::<arg>())),
154    (on_enter, in_schedule(OnEnter(arg))),
155    (on_exit, in_schedule(OnExit(arg))),
156    (on_update, in_set(OnUpdate(arg))),
157    (in_set, in_set(arg)),
158    (in_base_set, in_base_set(arg)),
159    (in_schedule, in_schedule(arg)),
160    (in_state, run_if(in_state(arg))),
161    (state_changed, run_if(state_changed::<arg>())),
162    (state_exists, run_if(state_exists::<arg>())),
163    (
164        state_exists_and_equals,
165        run_if(state_exists_and_equals(arg))
166    )
167);
168
169//manually creating another fake helper outside of our list to mark a function as not a system
170/// a marker attribute to exclude a function from being added
171#[proc_macro_attribute]
172pub fn do_not_add(_attr: TokenStream, input: TokenStream) -> TokenStream {
173    input
174}
175
176//this only gets the end of the path, so there will be name collisions with similarly named macros from other sources.
177//might fix later if it becomes a problem
178fn get_fake_helper_name(attr: &Attribute) -> String {
179    attr.meta
180        .path()
181        .segments
182        .last()
183        .map(|a| a.ident.clone())
184        .unwrap()
185        .to_string()
186}
187
188/**Put this attribute on an `impl` block of a struct to turn the struct into a plugin where the functions in the `impl` block are systems.
189    Use the marker attributes on your systems to add parameters.
190    A function named `build` can be used to add custom functionality to the `Plugin::build` in the generated `Plugin` implementation.
191*/
192#[proc_macro_attribute]
193pub fn bevy_plugin(_attr: TokenStream, input: TokenStream) -> TokenStream {
194    let mut input = parse_macro_input!(input as ItemImpl);
195
196    let ty = input.self_ty.clone();
197
198    // filter out associated function named "build", to add its contents to our build function in the generated Plugin implementation
199    let mut custom_build = quote! {};
200    input.items = input
201        .items
202        .iter()
203        .cloned()
204        .filter(|item| {
205            if let ImplItem::Fn(func) = item {
206                let ident = func.sig.ident.clone();
207                if ident.to_string() == "build" {
208                    custom_build = func.block.clone().into_token_stream();
209                    return false;
210                }
211            }
212            true
213        })
214        .collect_vec();
215
216    let systems = input
217        .items
218        .iter()
219        .filter_map(|item| {
220            if let ImplItem::Fn(func) = item {
221                // filter out fns marked to not add
222                if func
223                    .attrs
224                    .iter()
225                    .any(|attr| get_fake_helper_name(attr) == "do_not_add")
226                {
227                    return None;
228                }
229                Some(func)
230            } else {
231                None
232            }
233        })
234        .map(|system| {
235            // get the name of the function
236            let ident = system.sig.ident.clone();
237
238            let extensions = &system
239                .attrs
240                .iter()
241                .filter_map(|attr| {
242                    // get the name of the fake helper
243                    let path = get_fake_helper_name(attr);
244
245                    // get the contents of the fake helper if there are any, otherwise make empty (in that case we arent using them anyway)
246                    let params = if let Meta::List(meta_list) = attr.meta.clone() {
247                        meta_list.tokens
248                    } else {
249                        Default::default()
250                    };
251
252                    // jank to make the string quotes work
253                    // we convert the parsed param contents back into a string in order to manually insert into the quote with string replace
254                    // we will later re-parse the string quote back into a TokenStream
255                    PARAM_HELPERS
256                        .iter()
257                        .find(|(name, _)| path.as_str() == *name)
258                        .map(|(_, val)| val.replace("arg", params.to_string().as_str()))
259                })
260                .join(".");
261
262            if extensions.len() > 0 {
263                //parse extensions string quote into a TokenStream, then use quote! macro
264                let extensions =
265                    syn::parse_str::<proc_macro2::TokenStream>(extensions.as_str()).unwrap();
266                quote! { .add_system(#ty::#ident.#extensions)}
267            } else if system.attrs.len() == 0 {
268                quote!(.add_system(#ty::#ident))
269            } else {
270                Default::default()
271            }
272        })
273        .collect_vec();
274
275    let add_systems = if systems.len() > 0 {
276        quote!(app #(#systems)*;)
277    } else {
278        Default::default()
279    };
280
281    let output = quote! {
282        #input
283
284        impl Plugin for #ty {
285            fn build(&self, app: &mut App) {
286                #add_systems
287                #custom_build
288            }
289        }
290    };
291
292    TokenStream::from(output)
293}