diff --git a/.idea/micro_game_macros.iml b/.idea/micro_game_macros.iml index 9b4cf845b904f4d9ad1fe946a5c783d6f59934f3..6b5fada079efb580e53081e6609acbef960c0110 100644 --- a/.idea/micro_game_macros.iml +++ b/.idea/micro_game_macros.iml @@ -4,6 +4,7 @@ <exclude-output /> <content url="file://$MODULE_DIR$"> <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" /> <excludeFolder url="file://$MODULE_DIR$/target" /> </content> <orderEntry type="inheritedJdk" /> diff --git a/Cargo.toml b/Cargo.toml index 17b589b9929ec36508b2eeb572863daef9b3d731..13536396f4b0164f74f1c1241dbd80f3c6e0a04f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,16 +22,16 @@ syn = "2.0" [dev-dependencies] test-case = "3.3.1" -serde = { version = "1.0", features = ["derive"]} +serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" anyhow = "1.0" - +micro_bevy_world_utils = "0.4.0" [dev-dependencies.bevy] version = "0.13" default-features = false features = [ - "bevy_asset", - "bevy_sprite", - "bevy_core_pipeline", + "bevy_asset", + "bevy_sprite", + "bevy_core_pipeline", ] diff --git a/src/event_system/components.rs b/src/event_system/components.rs new file mode 100644 index 0000000000000000000000000000000000000000..8272ecbffe3d90694334dfd686006bdb5b838a34 --- /dev/null +++ b/src/event_system/components.rs @@ -0,0 +1,162 @@ +use crate::fqpath::*; +use crate::utils::{ident_prefix, ident_suffix, snake_case_ident}; +use proc_macro2::{Ident, TokenStream}; +use quote::{quote, quote_spanned, ToTokens}; +use syn::{ + parse_quote, spanned::Spanned, Attribute, Data, DataEnum, DataStruct, DeriveInput, Field, Meta, + Variant, Visibility, +}; + +macro_rules! err_message { + ($spannable: expr, $($tok:tt)*) => { + return quote_spanned!( + $spannable.span() => + compile_error!($($tok)*); + ) + }; +} + +pub fn event_system(DeriveInput { data, ident, .. }: DeriveInput) -> TokenStream { + let enum_data = match data { + Data::Struct(d) => err_message!( + d.struct_token, + "Can't create an event system from an enum type" + ), + Data::Enum(data_enum) => data_enum, + Data::Union(u) => err_message!( + u.union_token, + "Can't create an asset system from a union type" + ), + }; + + let structs = define_structs(&enum_data); + let root = define_root_enum(&ident, &enum_data); + let helper = define_helper_fn(&ident, &enum_data); + let plugin = define_plugin(&ident, &enum_data); + + quote! { + #structs + #root + #helper + #plugin + } +} + +fn define_structs(data: &DataEnum) -> TokenStream { + data.variants + .iter() + .map( + |Variant { + ident, + attrs, + fields, + .. + }| { + let event_name = ident_suffix(ident, "Event"); + let fields: TokenStream = fields + .iter() + .map( + |fl @ Field { + ident, + ty, + mutability, + .. + }| { + let f = Field { + ident: ident.clone(), + ty: ty.clone(), + vis: parse_quote!(pub), + ..fl.clone() + } + .to_token_stream(); + + quote!(#f,) + }, + ) + .collect(); + + quote! { + #[derive(#FQClone, #FQDebug, #BevyEvent, #FQSerialize, #FQDeserialize)] + pub struct #event_name { + #fields + } + } + }, + ) + .collect() +} + +fn define_root_enum(name: &Ident, data: &DataEnum) -> TokenStream { + let variants: TokenStream = data + .variants + .iter() + .map(|Variant { ident, .. }| { + let event = ident_suffix(ident, "Event"); + quote!(#ident(#event),) + }) + .collect(); + + let conversions: TokenStream = data + .variants + .iter() + .map(|Variant { ident, .. }| { + let event = ident_suffix(ident, "Event"); + quote! { + impl #FQFrom<#event> for #name { + fn from(value: #event) -> Self { + #name::#ident(value) + } + } + } + }) + .collect(); + + quote! { + #[derive(#FQDebug, #FQClone, #BevyEvent, #FQSerialize, #FQDeserialize)] + pub enum #name { + #variants + } + + #conversions + } +} + +fn define_helper_fn(name: &Ident, data: &DataEnum) -> TokenStream { + let variants: TokenStream = data + .variants + .iter() + .map(|Variant { ident, .. }| quote! { #name::#ident(value) => #send_event(world, value), }) + .collect(); + + let fn_name = ident_prefix(&snake_case_ident(name), "dispatch_"); + + quote! { + pub fn #fn_name(world: &mut #BevyWorld, event: #name) { + match event { + #variants + } + } + } +} + +fn define_plugin(name: &Ident, data: &DataEnum) -> TokenStream { + let variants: TokenStream = data + .variants + .iter() + .map(|Variant { ident, .. }| { + let event = ident_suffix(ident, "Event"); + quote!(app.add_event::<#event>();) + }) + .collect(); + + let plugin_name = ident_suffix(name, "Plugin"); + + quote! { + pub struct #plugin_name; + impl #BevyPlugin for #plugin_name { + fn build(&self, app: &mut #BevyApp) { + #variants + } + } + } +} diff --git a/src/event_system/mod.rs b/src/event_system/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..3b8f49f3234d0e24d559584c881f46001b17cc3d --- /dev/null +++ b/src/event_system/mod.rs @@ -0,0 +1,3 @@ +mod components; + +pub use components::event_system; diff --git a/src/fqpath.rs b/src/fqpath.rs index 7d9b5c7b78a6ea980c1b3cedb211d211ef85ecb6..a73d9d7e9381053c6682e24cf31a55575b04a397 100644 --- a/src/fqpath.rs +++ b/src/fqpath.rs @@ -1,3 +1,5 @@ +#![allow(non_camel_case_types)] + use proc_macro2::TokenStream; use quote::{quote, ToTokens}; @@ -38,6 +40,8 @@ fq!(ImportBevyPrelude => use ::bevy::prelude::*); fq!(BevyApp => ::bevy::app::App); fq!(BevyPlugin => ::bevy::app::Plugin); fq!(BevyUpdate => ::bevy::app::Update); +fq!(BevyWorld => ::bevy::ecs::world::World); +fq!(BevyEvent => ::bevy::ecs::event::Event); fq!(BevyRes => ::bevy::ecs::system::Res); fq!(BevyResMut => ::bevy::ecs::system::ResMut); fq!(BevyEventReader => ::bevy::ecs::event::EventReader); @@ -60,5 +64,7 @@ fq!(BevyAddAsset => ::bevy::asset::AssetApp); fq!(BevyAsyncRead => ::bevy::asset::AsyncReadExt); fq!(BevyAssetReader => ::bevy::asset::io::Reader); +fq!(send_event => ::micro_bevy_world_utils::send_event); + #[cfg(feature = "kayak")] fq!(KayakWidget => ::kayak_ui::prelude::Widget); diff --git a/src/lib.rs b/src/lib.rs index 40c8d66a462b081c48fa833ca71eb92aa6ef4820..fbbedcef754c5702ae2acc5534b58ec8bb3885e8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -99,6 +99,7 @@ pub(crate) mod fqpath; pub(crate) mod utils; pub(crate) mod asset_system; +pub(crate) mod event_system; pub(crate) mod json_loader; #[cfg(feature = "kayak")] pub(crate) mod kayak; @@ -286,6 +287,27 @@ pub fn asset_system(_: TokenStream, input: TokenStream) -> TokenStream { asset_system::asset_system(input).into() } +/// +/// ```rust +/// use bevy::prelude::{Entity, EventWriter, IVec2}; +/// use micro_games_macros::event_system; +/// +/// #[event_system] +/// enum ActionEvent { +/// Wait { source: Entity }, +/// Move { source: Entity, to: IVec2 } +/// } +/// +/// pub fn emit_wait_event(mut event_writer: EventWriter<WaitEvent>) { +/// event_writer.send(WaitEvent { source: Entity::from_raw(0) }); +/// } +/// ``` +#[proc_macro_attribute] +pub fn event_system(_: TokenStream, input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + event_system::event_system(input).into() +} + /// Marker attribute, used exclusively by other proc macros #[proc_macro_attribute] #[doc(hidden)] diff --git a/tests/mod.rs b/tests/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..6454e009334875eee9633094c1364aabbfdb6e79 --- /dev/null +++ b/tests/mod.rs @@ -0,0 +1,42 @@ +use micro_games_macros::event_system; +use std::sync::Mutex; + +#[test] +fn event_system_correctly_generates_and_dispatches_events() { + use bevy::prelude::*; + + let mut app = App::new(); + + #[event_system] + enum MyEvents { + Wait { source: Entity }, + Log { source: Entity, mesasge: String }, + } + + /// A hatch to allow us to assert that the system has actually run, so we don't miss an + /// assertion + #[derive(Resource)] + struct HasRun(bool); + app.insert_resource(HasRun(false)); + + app.add_plugins(MyEventsPlugin); + app.add_systems( + Update, + |mut has_run: ResMut<HasRun>, mut events: EventReader<WaitEvent>| { + has_run.0 = true; + let event_length = events.len(); + assert_eq!(event_length, 1); + }, + ); + + dispatch_my_events( + &mut app.world, + MyEvents::Wait(WaitEvent { + source: Entity::from_raw(0), + }), + ); + + app.update(); + + assert!(app.world.resource::<HasRun>().0); +}