bevy_ergo_plugin 0.2.1

Macros to make building bevy plugins more ergonomic
Documentation
//! Macros to make building bevy plugins more ergonomic (in my opinion).\
//!\
//! 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`)\
//! This crate's purpose is to replace that API with a more ergonomic one, using attribute macros as markers for system parameters.\
//! 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.\
//!\
//! 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.\
//! 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.\
//!\
//! The other macros are the aforementioned parameter markers to be put on your system definitions.\
//! For any params not included by a specific marker, you can use the [macro@sysparam] marker to define custom behavior.\
//!\
//! 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.  
//!\
//! 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.\
//!\
//! **This crate has not been thoroughly tested, so there will probably be weird bugs I don't know about.**
//!# Example
//! adapted from <https://github.com/bevyengine/bevy/blob/latest/examples/ecs/run_conditions.rs>\
//!
//!    use bevy::prelude::*;
//!    use bevy_ergo_plugin::*;
//!    
//!    fn main() {
//!        println!();
//!        println!("For the first 2 seconds you will not be able to increment the counter");
//!        println!("Once that time has passed you can press space, enter, left mouse, right mouse or touch the screen to increment the counter");
//!        println!();
//!    
//!        App::new()
//!            .add_plugins(DefaultPlugins)
//!            .add_plugin(Game)
//!            .run();
//!    }
//!    #[derive(Resource, Default)]
//!    pub struct InputCounter(usize);
//!    
//!    pub struct Game;
//!    #[bevy_plugin]
//!    impl Game {
//!        #[resource_exists(InputCounter)]
//!        #[run_if(Game::has_user_input)]
//!        pub fn increment_input_counter(mut counter: ResMut<InputCounter>) {
//!            counter.0 += 1;
//!        }
//!    
//!        #[run_if(resource_exists::<InputCounter>().and_then(
//!            |counter: Res<InputCounter>| counter.is_changed() && !counter.is_added()
//!        ))]
//!        pub fn print_input_counter(counter: Res<InputCounter>) {
//!            println!("Input counter: {}", counter.0);
//!        }
//!    
//!        #[run_if(Game::time_passed(2.0))]
//!        #[run_if(not(Game::time_passed(2.5)))]
//!        pub fn print_time_message() {
//!            println!(
//!                "It has been more than 2 seconds since the program started and less than 2.5 seconds"
//!            );
//!        }
//!    
//!        #[do_not_add]
//!        pub fn has_user_input(
//!            keyboard_input: Res<Input<KeyCode>>,
//!            mouse_button_input: Res<Input<MouseButton>>,
//!            touch_input: Res<Touches>,
//!        ) -> bool {
//!            keyboard_input.just_pressed(KeyCode::Space)
//!                || keyboard_input.just_pressed(KeyCode::Return)
//!                || mouse_button_input.just_pressed(MouseButton::Left)
//!                || mouse_button_input.just_pressed(MouseButton::Right)
//!                || touch_input.any_just_pressed()
//!        }
//!    
//!        #[do_not_add]
//!        pub fn time_passed(t: f32) -> impl FnMut(Local<f32>, Res<Time>) -> bool {
//!            move |mut timer: Local<f32>, time: Res<Time>| {
//!                *timer += time.delta_seconds();
//!                *timer >= t
//!            }
//!        }
//!    
//!        pub fn build(&self, app: &mut App) {
//!            app.init_resource::<InputCounter>();
//!        }
//!    }
//!
extern crate proc_macro;

use itertools::Itertools;
use proc_macro::TokenStream;
use quote::{quote, ToTokens};
use syn::{parse_macro_input, Attribute, ImplItem, ItemImpl, Meta};

// macro to:
//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
// we need these because attribute macros dont have helper attributes like derive macros do
//2: store the quotes along with the name of the helper.
// this implementation is pretty janky but it avoids a bunch of boilerplate i dont wanna look at or write
// i.e: name => Some(quote!{the actual thing}) for every single param i define
macro_rules! fake_helpers{
    ($len:literal, $(($name:ident, $val:expr)),+) => {
        $(///system parameter marker attribute that generates\
        ///`.add_system(
        #[doc=stringify!(system_name.$val)]
        ///)`
        #[proc_macro_attribute]
        pub fn $name(_attr: TokenStream, input: TokenStream) -> TokenStream {
            input
        })
        +
        const PARAM_HELPERS: [(&'static str, &'static str ); $len]= [
            $((stringify!($name), stringify!($val))),+
        ];

     }
}

fake_helpers!(
    34,
    (sysparam, arg),
    (startup, on_startup()),
    (run_if, run_if(arg)),
    (run_once, run_if(run_once())),
    (before, before(arg)),
    (after, after(arg)),
    (pipe, pipe(arg)),
    (dbg, pipe(system_adapter::dbg)),
    (error, pipe(system_adapter::error)),
    (ignore, pipe(system_adapter::ignore)),
    (info, pipe(system_adapter::info)),
    (unwrap, pipe(system_adapter::unwrap)),
    (warn, pipe(system_adapter::warn)),
    (not, run_if(not(arg))),
    (any_with_component, run_if(any_with_component::<arg>())),
    (resource_added, run_if(resource_added::<arg>())),
    (resource_changed, run_if(resource_changed::<arg>())),
    (
        resource_changed_or_removed,
        run_if(resource_changed_or_removed::<arg>())
    ),
    (resource_equals, run_if(resource_equals(arg))),
    (resource_exists, run_if(resource_exists::<arg>())),
    (
        resource_exists_and_changed,
        run_if(resource_exists_and_changed::<arg>())
    ),
    (
        resource_exists_and_equals,
        run_if(resource_exists_and_equals(arg))
    ),
    (resource_removed, run_if(resource_removed::<arg>())),
    (on_event, run_if(on_event::<arg>())),
    (on_enter, in_schedule(OnEnter(arg))),
    (on_exit, in_schedule(OnExit(arg))),
    (on_update, in_set(OnUpdate(arg))),
    (in_set, in_set(arg)),
    (in_base_set, in_base_set(arg)),
    (in_schedule, in_schedule(arg)),
    (in_state, run_if(in_state(arg))),
    (state_changed, run_if(state_changed::<arg>())),
    (state_exists, run_if(state_exists::<arg>())),
    (
        state_exists_and_equals,
        run_if(state_exists_and_equals(arg))
    )
);

//manually creating another fake helper outside of our list to mark a function as not a system
/// a marker attribute to exclude a function from being added
#[proc_macro_attribute]
pub fn do_not_add(_attr: TokenStream, input: TokenStream) -> TokenStream {
    input
}

//this only gets the end of the path, so there will be name collisions with similarly named macros from other sources.
//might fix later if it becomes a problem
fn get_fake_helper_name(attr: &Attribute) -> String {
    attr.meta
        .path()
        .segments
        .last()
        .map(|a| a.ident.clone())
        .unwrap()
        .to_string()
}

/**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.
    Use the marker attributes on your systems to add parameters.
    A function named `build` can be used to add custom functionality to the `Plugin::build` in the generated `Plugin` implementation.
*/
#[proc_macro_attribute]
pub fn bevy_plugin(_attr: TokenStream, input: TokenStream) -> TokenStream {
    let mut input = parse_macro_input!(input as ItemImpl);

    let ty = input.self_ty.clone();

    // filter out associated function named "build", to add its contents to our build function in the generated Plugin implementation
    let mut custom_build = quote! {};
    input.items = input
        .items
        .iter()
        .cloned()
        .filter(|item| {
            if let ImplItem::Fn(func) = item {
                let ident = func.sig.ident.clone();
                if ident.to_string() == "build" {
                    custom_build = func.block.clone().into_token_stream();
                    return false;
                }
            }
            true
        })
        .collect_vec();

    let systems = input
        .items
        .iter()
        .filter_map(|item| {
            if let ImplItem::Fn(func) = item {
                // filter out fns marked to not add
                if func
                    .attrs
                    .iter()
                    .any(|attr| get_fake_helper_name(attr) == "do_not_add")
                {
                    return None;
                }
                Some(func)
            } else {
                None
            }
        })
        .map(|system| {
            // get the name of the function
            let ident = system.sig.ident.clone();

            let extensions = &system
                .attrs
                .iter()
                .filter_map(|attr| {
                    // get the name of the fake helper
                    let path = get_fake_helper_name(attr);

                    // get the contents of the fake helper if there are any, otherwise make empty (in that case we arent using them anyway)
                    let params = if let Meta::List(meta_list) = attr.meta.clone() {
                        meta_list.tokens
                    } else {
                        Default::default()
                    };

                    // jank to make the string quotes work
                    // we convert the parsed param contents back into a string in order to manually insert into the quote with string replace
                    // we will later re-parse the string quote back into a TokenStream
                    PARAM_HELPERS
                        .iter()
                        .find(|(name, _)| path.as_str() == *name)
                        .map(|(_, val)| val.replace("arg", params.to_string().as_str()))
                })
                .join(".");

            if extensions.len() > 0 {
                //parse extensions string quote into a TokenStream, then use quote! macro
                let extensions =
                    syn::parse_str::<proc_macro2::TokenStream>(extensions.as_str()).unwrap();
                quote! { .add_system(#ty::#ident.#extensions)}
            } else if system.attrs.len() == 0 {
                quote!(.add_system(#ty::#ident))
            } else {
                Default::default()
            }
        })
        .collect_vec();

    let add_systems = if systems.len() > 0 {
        quote!(app #(#systems)*;)
    } else {
        Default::default()
    };

    let output = quote! {
        #input

        impl Plugin for #ty {
            fn build(&self, app: &mut App) {
                #add_systems
                #custom_build
            }
        }
    };

    TokenStream::from(output)
}