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}