From 0ce86f7ed319e8381e9d623caa78c6b68a56011e Mon Sep 17 00:00:00 2001
From: Louis Capitanchik <contact@louiscap.co>
Date: Sun, 5 May 2024 17:19:59 +0100
Subject: [PATCH] Add event_system for generating all the scaffolding for
 network compatible event broadcasting

---
 .idea/micro_game_macros.iml    |   1 +
 Cargo.toml                     |  10 +-
 src/event_system/components.rs | 162 +++++++++++++++++++++++++++++++++
 src/event_system/mod.rs        |   3 +
 src/fqpath.rs                  |   6 ++
 src/lib.rs                     |  22 +++++
 tests/mod.rs                   |  42 +++++++++
 7 files changed, 241 insertions(+), 5 deletions(-)
 create mode 100644 src/event_system/components.rs
 create mode 100644 src/event_system/mod.rs
 create mode 100644 tests/mod.rs

diff --git a/.idea/micro_game_macros.iml b/.idea/micro_game_macros.iml
index 9b4cf84..6b5fada 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 17b589b..1353639 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 0000000..8272ecb
--- /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 0000000..3b8f49f
--- /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 7d9b5c7..a73d9d7 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 40c8d66..fbbedce 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 0000000..6454e00
--- /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);
+}
-- 
GitLab