diff --git a/Cargo.toml b/Cargo.toml index b7f6daba9dd563c5d5548788f7ffe0d2bdc0be24..8c8466f2ad5272483cda92bb7fc41dc71b40ec5b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "micro_games_macros" -version = "0.5.0" +version = "0.6.0" edition = "2021" authors = ["Louis Capitanchik <contact@louiscap.co>"] description = "Utility macros to make it easier to build complex systems with Bevy" diff --git a/README.md b/README.md index 5ed9ce50a1ca8b2b4a728b16a43e77932cf53436..c01ecb81c39da67f8d08c22222ec20664f34cbec 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,14 @@ [](https://docs.rs/micro_games_macros) [](https://crates.io/crates/micro_games_macros) -A collection of utility macros for building games +A collection of utility macros for building games. While this library should theoretically work with +any version of Bevy that includes the required traits and structs for a given macro, it is worth checking the +version of bevy listed as a dev dependency in `Cargo.toml`, as that will be the version tested against. -**Current Version Support: 0.5.x -> Bevy 0.14** +This works because the library does not directly depend on bevy, but instead just generates fully qualified paths +to derives, traits, and structs that will resolve to the version of Bevy used in your downstream project + +**Current Version Support: 0.6.x -> Bevy 0.14** ## Macros @@ -82,4 +87,21 @@ struct MyComponent; // ... Other Kayak Setup ... +``` + +### Tag Finder + +Create a system param for checking whether an entity has one of a specified number of tags. The tag finder will also +create the tag components, which can use either table or sparse storage + +```rust +use micro_games_macros::tag_finder; + +#[tag_finder] +pub struct TagFinder { + player: Player, + item: Item, + #[sparse] + hover: Hover, +} ``` \ No newline at end of file diff --git a/src/fqpath.rs b/src/fqpath.rs index 6ff2d4e47499b1747e3d48557a9a13f1a5f61ac7..5442aedaeafe42750845b963d72e5cd066dd89e0 100644 --- a/src/fqpath.rs +++ b/src/fqpath.rs @@ -20,6 +20,7 @@ fq!(FQVec => ::std::vec::Vec); fq!(FQString => ::std::string::String); fq!(FQHashMap => ::std::collections::HashMap); fq!(FQClone => ::core::clone::Clone); +fq!(FQCopy => ::core::marker::Copy); fq!(FQDebug => ::core::fmt::Debug); fq!(FQDisplay => ::core::fmt::Display); fq!(FQDefault => ::core::default::Default); @@ -44,9 +45,13 @@ 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!(BevyWith => ::bevy::ecs::query::With); +fq!(BevyQuery => ::bevy::ecs::system::Query); +fq!(BevyEntity => ::bevy::ecs::entity::Entity); fq!(BevyEventReader => ::bevy::ecs::event::EventReader); fq!(BevySystemParam => ::bevy::ecs::system::SystemParam); fq!(BevyResource => ::bevy::ecs::system::Resource); +fq!(BevyComponent => ::bevy::ecs::component::Component); fq!(BevyTypePath=> ::bevy::reflect::TypePath); fq!(BevyTypeUuid => ::bevy::reflect::TypeUuid); fq!(BevyDeref => ::bevy::prelude::Deref); diff --git a/src/lib.rs b/src/lib.rs index d1e93e3e6843ae058557d2a70e1e61969ac71140..7a1f6cba1f6e04b273f995b819acff5da1dffe2f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -104,6 +104,7 @@ pub(crate) mod json_loader; #[cfg(feature = "kayak")] pub(crate) mod kayak; pub(crate) mod std_traits; +pub(crate) mod tag_finder; /// Generate loader and handler implementations for keyed JSON resources. The asset must implement `bevy::asset::Asset`, as well as /// `serde::Deserialize` and `serde::Serialize` @@ -369,3 +370,48 @@ pub fn derive_from_inner(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); std_traits::from_inner::derive(input).into() } + +/// Create a TagFinder type, used for checking what tag components might contain +/// +/// ## Examples +/// +/// Spawns a number of entities, and then perform different behaviour based on tags +/// +/// ``` +/// # use bevy::prelude::*; +/// # use micro_games_macros::tag_finder; +/// +/// #[tag_finder] +/// struct TagFinder { +/// ally: Ally, +/// enemy: Enemy, +/// } +/// +/// pub fn spawn_entities(mut commands: Commands) { +/// commands.spawn((Ally, SpatialBundle::default())); +/// commands.spawn((Enemy, SpatialBundle::default())); +/// commands.spawn((Enemy, SpatialBundle::default())); +/// } +/// +/// pub fn my_checking_system(query: Query<(Entity, &Transform)>, tags: TagFinder) { +/// for (e, t) in &query { +/// if tags.is_ally(e) { +/// println!("Celebrate"); +/// } else if tags.is_enemy(e) { +/// println!("Run away") +/// } +/// } +/// } +/// ``` +#[proc_macro_attribute] +pub fn tag_finder(_: TokenStream, input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + tag_finder::tag_finder(input).into() +} + +/// Marker attribute, used exclusively by other proc macros +#[proc_macro_attribute] +#[doc(hidden)] +pub fn sparse(_: TokenStream, input: TokenStream) -> TokenStream { + input +} diff --git a/src/tag_finder/components.rs b/src/tag_finder/components.rs new file mode 100644 index 0000000000000000000000000000000000000000..bab22d65c1e80e0bb6227beafb25c2cc71a43eff --- /dev/null +++ b/src/tag_finder/components.rs @@ -0,0 +1,85 @@ +use crate::fqpath::{ + BevyComponent, BevyEntity, BevyQuery, BevySystemParam, BevyWith, FQClone, FQCopy, FQDebug, + FQDefault, +}; +use crate::utils::{ident_prefix, only_named_struct, only_struct}; +use proc_macro2::TokenStream; +use quote::{quote, quote_spanned}; +use syn::spanned::Spanned; +use syn::{DeriveInput, Field}; + +macro_rules! or_return { + ($expr: expr) => { + match $expr { + Ok(value) => value, + Err(err) => return err, + } + }; +} + +pub fn tag_finder(input: DeriveInput) -> TokenStream { + let struct_data = or_return!(only_struct(&input)); + let _ = or_return!(only_named_struct(struct_data)); + + let struct_fields: TokenStream = struct_data.fields.iter().map(map_named_field).collect(); + let struct_methods: TokenStream = struct_data.fields.iter().map(map_named_method).collect(); + let tag_types: TokenStream = struct_data.fields.iter().map(map_tag_types).collect(); + + let DeriveInput { vis, ident, .. } = input; + + quote! { + #tag_types + + #[derive(#BevySystemParam)] + #vis struct #ident <'w, 's> { + #struct_fields + } + + impl <'w, 's> #ident<'w, 's> { + #struct_methods + } + } +} + +fn map_tag_types(field: &Field) -> TokenStream { + let Field { ty, attrs, .. } = field; + + let is_sparse = attrs.iter().any(|attr| attr.path().is_ident("sparse")); + + if is_sparse { + quote! { + #[derive(#FQCopy, #FQClone, #FQDefault, #FQDebug, #BevyComponent)] + #[component(storage = "SparseSet")] + pub struct #ty; + } + } else { + quote! { + #[derive(#FQCopy, #FQClone, #FQDefault, #FQDebug, #BevyComponent)] + pub struct #ty; + } + } +} + +fn map_named_field(field: &Field) -> TokenStream { + let Field { ident, ty, .. } = field; + quote! { + #ident: #BevyQuery<'w, 's, (), #BevyWith<#ty>>, + } +} + +fn map_named_method(field: &Field) -> TokenStream { + let Field { ident, ty, .. } = field; + match ident { + None => { + quote_spanned! { ty.span() => compile_error!("Unable to map field with type {} that does not have a name"); } + } + Some(ident) => { + let name = ident_prefix(ident, "is_"); + quote! { + pub fn #name(&self, entity: #BevyEntity) -> bool { + self.#ident.contains(entity) + } + } + } + } +} diff --git a/src/tag_finder/mod.rs b/src/tag_finder/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..29f71a5807d633cb61ea0df4b6f68032e51a47ee --- /dev/null +++ b/src/tag_finder/mod.rs @@ -0,0 +1,3 @@ +mod components; + +pub use components::tag_finder; diff --git a/src/utils.rs b/src/utils.rs index bcd173f9e8c8621583cdce176fe6914c35f19ba0..59539ebf40db85c6b1c06f13367af89335ccfb10 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,7 +2,10 @@ use proc_macro2::{Ident, TokenStream, TokenTree}; use quote::{quote, quote_spanned, ToTokens}; use std::collections::HashMap; use std::fmt::Display; -use syn::{spanned::Spanned, Attribute, DataStruct, Field, Meta, MetaNameValue, Visibility}; +use syn::{ + spanned::Spanned, Attribute, Data, DataStruct, DeriveInput, Field, Fields, FieldsNamed, Meta, + MetaNameValue, Visibility, +}; /// Convert a list of characters to snake case pub fn snake_case(chars: impl Iterator<Item = char>) -> String { @@ -160,6 +163,30 @@ pub fn module_wrapped( } } +pub fn only_struct<'a>(input: &'a DeriveInput) -> Result<&'a DataStruct, TokenStream> { + match &input.data { + Data::Struct(at) => Ok(at), + Data::Enum(en) => Err( + quote_spanned!(en.enum_token.span() => compile_error!("Unsupported for Enum, only for Struct");), + ), + Data::Union(un) => Err( + quote_spanned!(un.union_token.span() => compile_error!("Unsupported for Union, only for Struct");), + ), + } +} + +pub fn only_named_struct(data: &DataStruct) -> Result<&FieldsNamed, TokenStream> { + match &data.fields { + Fields::Named(named) => Ok(named), + Fields::Unnamed(uf) => Err( + quote_spanned!(uf.paren_token.span => compile_error!("Unsupported for tuple structs, must use named structs");), + ), + Fields::Unit => Err( + quote_spanned!(data.semi_token.span() => compile_error!("Unsupported for unit structs, must use named structs");), + ), + } +} + #[cfg(test)] mod tests { use crate::utils::{snake_case, unquote};