Game stats that reset every frame, inspired by immediate mode GUI.
gamedev bevy stats
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Rewrote enum derive.

+97 -158
+97 -158
immediate_stats_macros/src/lib.rs
··· 2 2 mod bevy_butler; 3 3 4 4 use darling::{Error, FromField}; 5 + use proc_macro_error::{emit_call_site_error, emit_warning, proc_macro_error}; 5 6 use proc_macro2::{Span, TokenStream}; 6 - use proc_macro_error::{ 7 - emit_call_site_error, emit_call_site_warning, emit_warning, proc_macro_error, 8 - }; 9 - use quote::{quote, ToTokens}; 7 + use quote::{ToTokens, quote}; 10 8 use syn::spanned::Spanned; 11 - use syn::{parse_macro_input, Data, DataEnum, DeriveInput, Field, Ident, Index}; 9 + use syn::{Data, DeriveInput, Field, Fields, Ident, Index, Variant, parse_macro_input}; 12 10 13 11 #[proc_macro_derive(StatContainer, attributes(stat, stat_ignore, add_component))] 14 12 #[proc_macro_error] 15 13 pub fn stat_container_derive(item: proc_macro::TokenStream) -> proc_macro::TokenStream { 16 14 let tree: DeriveInput = parse_macro_input!(item as DeriveInput); 17 - 18 - let struct_name = &tree.ident; 15 + let ident = &tree.ident; 19 16 20 17 let method_contents = match tree.data.clone() { 21 - Data::Struct(s) => { 22 - s.fields.iter().enumerate().map(|(index, field)| { 23 - reset_field(field, index).unwrap_or_else(Error::write_errors) 24 - }).flatten().collect() 25 - }, 26 - Data::Enum(e) => stat_container_enum(e), 18 + Data::Struct(s) => reset_struct(&s.fields), 19 + Data::Enum(e) => reset_variants(e.variants.iter()), 27 20 Data::Union(_) => { 28 21 emit_call_site_error!("This trait cannot be derived from unions."); 29 22 return proc_macro::TokenStream::new(); ··· 31 24 }; 32 25 33 26 let method = quote! { 34 - impl StatContainer for #struct_name { 27 + impl StatContainer for #ident { 35 28 fn reset_modifiers(&mut self) { 36 29 #method_contents 37 30 } ··· 40 33 41 34 #[cfg(feature = "bevy_butler")] 42 35 { 43 - let systems = bevy_butler::register_systems(tree); 44 - 45 - match systems { 46 - Ok(systems) => quote! { #method #systems }.into(), 47 - Err(e) => e.write_errors().into(), 48 - } 36 + let systems = bevy_butler::register_systems(tree).unwrap_or_else(Error::write_errors); 37 + quote! { #method #systems }.into() 49 38 } 50 39 51 40 #[cfg(not(feature = "bevy_butler"))] ··· 60 49 exclude: bool, 61 50 } 62 51 52 + impl FieldOptions { 53 + /// Returns true if the field is considered a stat. 54 + /// Emits a warning if both the `stat` and `stat_ignore` flags are present. 55 + pub fn is_stat(&self) -> bool { 56 + if self.include && self.exclude { 57 + emit_warning!( 58 + self.ident.span(), 59 + "`stat` attribute is overruled by `stat_ignore` attribute." 60 + ); 61 + } 62 + 63 + (self.include || self.stat_type) && !self.exclude 64 + } 65 + } 66 + 63 67 impl FromField for FieldOptions { 64 68 fn from_field(field: &Field) -> darling::Result<Self> { 65 69 let mut options = FieldOptions { ··· 84 88 } 85 89 } 86 90 87 - /// Returns the code needed to reset a field. `index` is used for tuple fields. 88 - fn reset_field(field: &Field, index: usize) -> darling::Result<TokenStream> { 89 - let options = FieldOptions::from_field(&field)?; 91 + /// Returns the code that can be used to reset a struct's stat fields. 92 + fn reset_struct(fields: &Fields) -> TokenStream { 93 + fields 94 + .iter() 95 + .enumerate() 96 + .map(|(index, field)| reset_struct_field(field, index)) 97 + .flatten() 98 + .collect() 99 + } 90 100 91 - if (options.include || options.stat_type) && !options.exclude { 92 - return Ok(match options.ident { 101 + /// Returns the method call that can be used to reset a stat field. 102 + /// If the field is not a stat, the result will be empty. 103 + /// The `index` is used for tuple/unnamed fields. 104 + fn reset_struct_field(field: &Field, index: usize) -> TokenStream { 105 + let options = FieldOptions::from_field(&field).unwrap(); 106 + 107 + if options.is_stat() { 108 + return match options.ident { 93 109 Some(ident) => { 94 110 quote! { self.#ident.reset_modifiers(); } 95 111 } ··· 97 113 let index = Index::from(index); 98 114 quote! { self.#index.reset_modifiers(); } 99 115 } 100 - }); 116 + }; 101 117 } 102 118 103 - if options.include && options.exclude { 104 - emit_warning!( 105 - field.span(), 106 - "`stat` attribute is overruled by `stat_ignore` attribute." 107 - ); 108 - } 109 - 110 - Ok(TokenStream::new()) 119 + TokenStream::new() 111 120 } 112 121 113 - fn stat_container_enum(e: DataEnum) -> TokenStream { 114 - let mut cases = Vec::new(); 115 - 116 - for variant in e.variants { 117 - let ident = variant.ident; 118 - 119 - match get_members_from_fields(variant.fields.clone()) { 120 - MemberVec::Named(names) => { 121 - cases.push(quote! { 122 - Self::#ident { #(#names,)* .. } => { 123 - #(#names.reset_modifiers();)* 124 - }, 125 - }); 122 + /// Returns a match statement that can be used to reset an enum. 123 + fn reset_variants<'a, T>(variants: T) -> TokenStream 124 + where 125 + T: Iterator<Item = &'a Variant>, 126 + { 127 + variants 128 + .map(|variant| { 129 + let cases = reset_variant(&variant); 130 + quote! { 131 + match self { 132 + #cases 133 + _ => {} 134 + } 126 135 } 127 - MemberVec::Unnamed(nums) => { 128 - let mut variables = Vec::new(); 136 + }) 137 + .flatten() 138 + .collect() 139 + } 129 140 130 - for index in 0..variant.fields.len() { 131 - if nums.contains(&Index::from(index)) { 132 - variables.push(Ident::new( 133 - format!("{}", ('a' as u8 + index as u8) as char).as_str(), 134 - Span::call_site(), 135 - )) 136 - } else { 137 - variables.push(Ident::new("_", Span::call_site())); 138 - } 139 - } 141 + /// Returns a case that can be used to reset the variant. 142 + /// If there are no stat fields, the result will be empty. 143 + fn reset_variant(variant: &Variant) -> TokenStream { 144 + let names: Vec<Ident> = variant 145 + .fields 146 + .iter() 147 + .enumerate() 148 + .filter_map(|(index, field)| { 149 + let options = FieldOptions::from_field(field).unwrap(); 140 150 141 - if variables.is_empty() { 142 - continue; 143 - } 144 - 145 - let used_vars = variables.iter().filter(|x| x.to_string() != "_"); 146 - 147 - cases.push(quote! { 148 - Self::#ident(#(#variables,)*) => { 149 - #(#used_vars.reset_modifiers();)* 150 - }, 151 + if options.is_stat() { 152 + return Some(match options.ident { 153 + Some(ident) => ident, 154 + None => get_ident_from_index(index), 151 155 }); 152 156 } 153 - MemberVec::None => {} 154 - } 155 - } 156 157 157 - if cases.is_empty() { 158 - emit_call_site_warning!( 159 - "Unused derive. Consider adding `#[stat]` to a field that implements `StatContainer`." 160 - ); 161 - } 158 + None 159 + }) 160 + .collect(); 162 161 163 - quote! { 164 - match self { 165 - #(#cases)* 166 - _ => {}, 167 - } 162 + if names.is_empty() { 163 + return TokenStream::new(); 168 164 } 169 - } 170 165 171 - /// The ways to identify fields. 172 - enum MemberVec { 173 - Named(Vec<Ident>), 174 - Unnamed(Vec<Index>), 175 - None, 176 - } 177 - 178 - /// Returns a list of all fields that either have type `Stat` or are tagged with `#[stat]`. 179 - /// If they are tagged with `#[stat_ignore]`, they are removed from the list. 180 - fn get_members_from_fields<T: IntoIterator<Item = Field>>(fields: T) -> MemberVec { 181 - let mut names = Vec::new(); 182 - let mut nums = Vec::new(); 183 - 184 - for (index, field) in fields.into_iter().enumerate() { 185 - // Check if the field is a stat. 186 - let mut is_stat = false; 166 + let is_named = variant.fields.iter().next().unwrap().ident.is_some(); 167 + let ident = &variant.ident; 187 168 188 - // Check if type is `Stat`. 189 - if field.ty.to_token_stream().to_string().contains("Stat") { 190 - is_stat = true; 169 + if is_named { 170 + quote! { 171 + Self::#ident { #(#names,)* .. } => { 172 + #(#names.reset_modifiers();)* 173 + }, 191 174 } 192 - 193 - // Store the `#[stat]` ident. Used for warning when overridden by `#[stat_ignore]`. 194 - let mut explicit_stat: Option<Ident> = None; 195 - 196 - // Iterator over all ident attributes. 197 - let attr_ident_iter = field.attrs.iter().filter_map(|x| x.meta.path().get_ident()); 198 - 199 - // Check for `#[stat]` attribute. 200 - if let Some(attr_ident) = attr_ident_iter.clone().find(|x| x.to_string() == "stat") { 201 - if is_stat { 202 - emit_warning!( 203 - attr_ident, 204 - "Unnecessary `stat` attribute. Fields of type `Stat` are automatically included." 205 - ); 206 - } 207 - 208 - is_stat = true; 209 - explicit_stat = Some(attr_ident.clone()); 210 - } 211 - 212 - // Check for `#[stat_ignore]` attribute. 213 - if attr_ident_iter 214 - .clone() 215 - .find(|x| x.to_string() == "stat_ignore") 216 - .is_some() 217 - { 218 - if let Some(explicit_ident) = &explicit_stat { 219 - emit_warning!( 220 - explicit_ident, 221 - "`stat` attribute is overruled by `stat_ignore` attribute." 222 - ); 223 - } 224 - 225 - is_stat = false; 226 - } 227 - 228 - if !is_stat { 229 - continue; 230 - } 231 - 232 - // Add field to the list. 233 - if let Some(field_ident) = field.ident.clone() { 234 - // (health_base, damage_base, etc.) 235 - names.push(field_ident); 236 - } else { 237 - // Is tuple. 238 - nums.push(Index::from(index)); 175 + } else { 176 + quote! { 177 + Self::#ident ( #(#names,)* .. ) => { 178 + #(#names.reset_modifiers();)* 179 + }, 239 180 } 240 181 } 182 + } 241 183 242 - assert!(names.is_empty() | nums.is_empty()); 243 - 244 - if names.is_empty() { 245 - MemberVec::Unnamed(nums) 246 - } else if nums.is_empty() { 247 - MemberVec::Named(names) 248 - } else { 249 - MemberVec::None 250 - } 184 + /// Generates an alphabetic identifier from an index. 185 + fn get_ident_from_index(index: usize) -> Ident { 186 + Ident::new( 187 + format!("{}", ('a' as u8 + index as u8) as char).as_str(), 188 + Span::call_site(), 189 + ) 251 190 }