havok_types_derive_proc_macros/
mod.rs

1extern crate proc_macro;
2
3use proc_macro::TokenStream;
4use quote::{ToTokens, quote};
5use syn::{Ident, Token, braced, parse::Parse, punctuated::Punctuated, token::PathSep};
6
7/// Automatically implement [`core::str::FromStr`], [`core::fmt::Display`] in string notation according
8/// to Havok's `hkFlags<Enum, SizeType>` XML notation.
9///
10/// In addition, [`core::default::Default`] is implemented.
11/// - [Playground](https://play.rust-lang.org/?version=stable&mode=release&edition=2024&gist=da155784893d0200a8d62c04c8c8da16)
12///
13/// # Note
14/// This attribute macro is automatically implemented by parsing `bitflags!` macro, so it cannot be implemented in a normal structure.
15#[proc_macro_attribute]
16pub fn impl_flags_methods(_attr: TokenStream, input: TokenStream) -> TokenStream {
17    let bit_flags = syn::parse_macro_input!(input as BitFlagsMacro);
18    let struct_ = &bit_flags.struct_;
19    let struct_type = struct_.ty.to_token_stream().to_string();
20    let struct_ident = &struct_.ident;
21    let struct_name_str = struct_.ident.to_string();
22
23    let mut str_push_matchers = Vec::new();
24    let mut matchers = Vec::new();
25    let mut field_names = Vec::new();
26    for field in &struct_.fields {
27        let field_ident = &field.name;
28        let flag_str = field.name.to_string();
29
30        str_push_matchers.push(quote! {
31            #struct_ident::#field_ident => flags.push(#flag_str.into()),
32        });
33
34        matchers.push(quote! {
35            s if s.eq_ignore_ascii_case(#flag_str) => flags |= #struct_ident::#field_ident,
36        });
37
38        field_names.push(flag_str);
39    }
40
41    let err_msg = format!(
42        "Invalid {struct_name_str}({struct_type}): {{err}} '{{unknown}}'. Expected one or more values separated by '|', or custom bits. Valid values are: {}.",
43        field_names.join(", ")
44    );
45    // INFO: hkxcmd is setting the RoleFlags of cow\behaviors\quadrupedbehavior to 0xfffff300 (-3328)
46    // by wrapping the input hexadecimal number that is greater than i16::MAX.
47
48    quote! {
49        #bit_flags
50
51        impl Default for #struct_ident {
52            #[inline]
53            fn default() -> Self {
54                Self::empty()
55            }
56        }
57
58        impl core::fmt::Display for #struct_ident {
59            fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
60                if self.is_empty() {
61                    return write!(f, "0");
62                }
63
64                let mut flags: Vec<std::borrow::Cow<'_, str>> = Vec::new();
65                for flag in self.iter() {
66                    match flag {
67                        #(#str_push_matchers)*
68                        remain => flags.push(format!("{:#X}", remain.bits() as u32).into()),
69                    };
70                }
71
72                write!(f, "{}", flags.join("|"))
73            }
74        }
75
76        // The FromStr implementation is used to deserialize XML.
77        impl core::str::FromStr for #struct_ident {
78            type Err = String;
79
80            fn from_str(s: &str) -> Result<Self, Self::Err> {
81                if s == "0" {
82                    return Ok(#struct_ident::empty());
83                }
84
85                let mut flags = #struct_ident::empty();
86                for token in s.split('|') {
87                    let token = token.trim();
88
89                    // Skip XML comment.
90                    // hkFlags contains `<! -- UNKNOWN BITS -->`, so it must be parsed error-free.
91                    let token = if let Some(start) = token.find("<!--") {
92                        if let Some(end) = token[start..].find("-->") {
93                            let before_comment = token[..start].trim();
94                            let after_comment = token[(start + end + 3)..].trim();
95                            if !before_comment.is_empty() {
96                                before_comment
97                            } else {
98                                after_comment
99                            }
100                        } else {
101                            token[..start].trim() // use pre-comment if `-->` is not there
102                        }
103                    } else {
104                        token
105                    };
106
107                    if token.is_empty() {
108                        continue;
109                    }
110
111                    match token {
112                        #(#matchers)*
113                        unknown => {
114                            let bits = havok_types::parse_int::ParseNumber::parse_wrapping(unknown).map_err(|err| {
115                                let name = #struct_name_str;
116                                format!(#err_msg)
117                            })?;
118                            flags |= #struct_ident::from_bits_retain(bits);
119                        }
120                    }
121                }
122
123                Ok(flags)
124            }
125        }
126    }
127    .into()
128}
129
130/// bitflags::bitflags! {
131#[derive(Debug, Clone, PartialEq, Eq)]
132struct BitFlagsMacro {
133    crate_name: Ident,
134    path_seq_token: PathSep,
135    macro_name: Ident,
136    exclamation_token: Token![!],
137    brace_token: syn::token::Brace,
138    struct_: Struct,
139}
140
141impl Parse for BitFlagsMacro {
142    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
143        // bitflags::bitflags! {
144        let crate_name = input.parse::<syn::Ident>()?;
145        let path_seq_token = input.parse::<syn::Token![::]>()?;
146        let macro_name = input.parse::<syn::Ident>()?;
147        let exclamation_token = input.parse::<syn::Token![!]>()?;
148        let content;
149        let brace_token: syn::token::Brace = braced!(content in input);
150
151        Ok(Self {
152            crate_name,
153            path_seq_token,
154            macro_name,
155            exclamation_token,
156            brace_token,
157            struct_: Struct::parse(&content)?,
158        })
159    }
160}
161
162/// We have to implement this to use `quote!` to generate Rust code.
163impl quote::ToTokens for BitFlagsMacro {
164    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
165        self.crate_name.to_tokens(tokens);
166        self.path_seq_token.to_tokens(tokens);
167        self.macro_name.to_tokens(tokens);
168        self.exclamation_token.to_tokens(tokens);
169        self.brace_token
170            .surround(tokens, |tokens| self.struct_.to_tokens(tokens));
171    }
172}
173
174/// ///
175/// pub struct <name>: <type> {
176#[derive(Debug, Clone, PartialEq, Eq)]
177struct Struct {
178    attrs: Vec<syn::Attribute>,
179    vis: syn::Visibility,
180    struct_token: syn::Token![struct],
181    ident: syn::Ident,
182    colon_token: syn::Token![:],
183    ty: syn::Type,
184    brace_token: syn::token::Brace,
185    fields: Punctuated<Field, Token![;]>,
186}
187
188impl Parse for Struct {
189    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
190        // ///
191        // pub struct <name>: <type> {
192        let content;
193        let attrs: Vec<syn::Attribute> = input.call(syn::Attribute::parse_outer)?;
194        let vis: syn::Visibility = input.parse()?;
195        let struct_token = input.parse::<syn::Token![struct]>()?;
196        let ident = input.parse::<syn::Ident>()?;
197        let colon_token = input.parse::<syn::Token![:]>()?;
198        let ty = input.parse::<syn::Type>()?;
199        let brace_token: syn::token::Brace = braced!(content in input);
200
201        Ok(Self {
202            attrs,
203            vis,
204            struct_token,
205            ident,
206            colon_token,
207            ty,
208            brace_token,
209            fields: content.parse_terminated(Field::parse, Token![;])?,
210        })
211    }
212}
213
214/// We have to implement this to use `quote!` to generate Rust code.
215impl ToTokens for Struct {
216    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
217        for attr in &self.attrs {
218            attr.to_tokens(tokens);
219        }
220        self.vis.to_tokens(tokens);
221        self.struct_token.to_tokens(tokens);
222        self.ident.to_tokens(tokens);
223        self.colon_token.to_tokens(tokens);
224        self.ty.to_tokens(tokens);
225        self.brace_token
226            .surround(tokens, |tokens| self.fields.to_tokens(tokens))
227    }
228}
229
230/// #[doc] or `///`
231/// const <NAME> = <value>;
232#[derive(Debug, Clone, PartialEq, Eq)]
233struct Field {
234    attrs: Vec<syn::Attribute>,
235    const_token: Token![const],
236    eq_token: Token![=],
237    name: Ident,
238    value: syn::Expr,
239}
240
241impl Parse for Field {
242    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
243        let attrs: Vec<syn::Attribute> = input.call(syn::Attribute::parse_outer)?;
244        let const_token = input.parse::<syn::Token![const]>()?;
245        let name = input.parse::<syn::Ident>()?;
246        let eq_token = input.parse::<syn::Token![=]>()?;
247        let value: syn::Expr = input.parse()?;
248
249        Ok(Field {
250            attrs,
251            const_token,
252            eq_token,
253            name,
254            value,
255        })
256    }
257}
258
259/// We have to implement this to use `quote!` to generate Rust code.
260impl ToTokens for Field {
261    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
262        for attr in &self.attrs {
263            attr.to_tokens(tokens);
264        }
265        self.const_token.to_tokens(tokens);
266        self.name.to_tokens(tokens);
267        self.eq_token.to_tokens(tokens);
268        self.value.to_tokens(tokens);
269    }
270}