Skip to main content

rustorio_derive/
lib.rs

1use proc_macro_crate::FoundCrate;
2use proc_macro2::{Span, TokenStream};
3use quote::{ToTokens, quote};
4use syn::{
5    Attribute, DeriveInput, Generics, Ident, ItemStruct, LitInt, Token, Type, parenthesized,
6    parse::{Parse, ParseStream},
7    parse_macro_input,
8    punctuated::Punctuated,
9};
10
11struct Crate;
12
13impl ToTokens for Crate {
14    fn to_tokens(&self, tokens: &mut TokenStream) {
15        let found_crate =
16            proc_macro_crate::crate_name("rustorio-engine").expect("Failed to get crate name");
17        match found_crate {
18            FoundCrate::Itself => quote! {crate}.to_tokens(tokens),
19            FoundCrate::Name(name) => {
20                let crate_ident = Ident::new(&name, Span::call_site());
21                quote! {::#crate_ident}.to_tokens(tokens);
22            }
23        }
24    }
25}
26
27struct RecipeItemAttrArgs(LitInt, Type);
28
29impl Parse for RecipeItemAttrArgs {
30    fn parse(input: ParseStream) -> syn::Result<Self> {
31        let content;
32        parenthesized!(content in input);
33        let amount = content.parse()?;
34        let _ = content.parse::<Token![,]>()?;
35        let ty = content.parse()?;
36        Ok(Self(amount, ty))
37    }
38}
39
40struct RecipeItemsAttr(Punctuated<RecipeItemAttrArgs, Token![,]>);
41
42impl Parse for RecipeItemsAttr {
43    fn parse(input: ParseStream) -> syn::Result<Self> {
44        Ok(Self(
45            input.parse_terminated(RecipeItemAttrArgs::parse, Token![,])?,
46        ))
47    }
48}
49
50struct RecipeItemList {
51    item_list: Vec<(u32, Type)>,
52    item_type_ident: Ident,
53    amount_const_ident: Ident,
54}
55
56impl RecipeItemList {
57    fn new(
58        attr: &Attribute,
59        attr_name: &str,
60        item_type_name: &str,
61        amount_const_name: &str,
62    ) -> Self {
63        let Ok(inner) = attr.parse_args::<RecipeItemsAttr>() else {
64            panic!("Invalid \"{attr_name}\" args");
65        };
66
67        let per_type = inner
68            .0
69            .iter()
70            .map(|RecipeItemAttrArgs(lit, ty)| {
71                let amount = lit
72                    .base10_parse::<u32>()
73                    .unwrap_or_else(|_| panic!("Invalid amount in \"{attr_name}\" args"));
74                (amount, ty.to_owned())
75            })
76            .collect::<Vec<_>>();
77        let item_type_ident = Ident::new(item_type_name, Span::call_site());
78        let amount_const_ident = Ident::new(amount_const_name, Span::call_site());
79
80        Self {
81            item_list: per_type,
82            item_type_ident,
83            amount_const_ident,
84        }
85    }
86
87    fn new_inputs(attr: &Attribute) -> Self {
88        Self::new(attr, "recipe_inputs", "Inputs", "INPUT_AMOUNTS")
89    }
90
91    fn new_outputs(attr: &Attribute) -> Self {
92        Self::new(attr, "recipe_outputs", "Outputs", "OUTPUT_AMOUNTS")
93    }
94
95    fn generate_recipe_direction(&self, amount_type_name: &str) -> TokenStream {
96        let RecipeItemList {
97            item_list,
98            item_type_ident,
99            amount_const_ident,
100        } = self;
101
102        let amount_type_ident = Ident::new(amount_type_name, Span::call_site());
103        let amount_types = item_list.iter().map(|_| quote! {u32}).collect::<Vec<_>>();
104        let amounts = item_list.iter().map(|(amount, _)| amount);
105
106        let recipe_items = item_list
107            .iter()
108            .map(|(_, ty)| quote! {#Crate::resources::Resource<#ty>});
109
110        quote! {
111            type #item_type_ident = (#(#recipe_items,)*);
112
113            type #amount_type_ident = (#(#amount_types,)*);
114            const #amount_const_ident: (#(#amount_types,)*) = (#(#amounts,)*);
115        }
116    }
117
118    fn generate_recipe_new_method(
119        &self,
120        new_fn_name: &str,
121        implementing_trait: TokenStream,
122    ) -> TokenStream {
123        let RecipeItemList {
124            item_list,
125            item_type_ident,
126            amount_const_ident: _,
127        } = self;
128
129        let new_fn_ident = Ident::new(new_fn_name, Span::call_site());
130        let new_values = item_list
131            .iter()
132            .map(|_| quote! {#Crate::resources::Resource::new_empty()});
133
134        quote! {
135            fn #new_fn_ident() -> <Self as #implementing_trait>::#item_type_ident {
136                (#(#new_values,)*)
137            }
138        }
139    }
140
141    fn generate_recipe_new_bundle_method(&self, new_fn_name: &str) -> TokenStream {
142        let RecipeItemList {
143            item_list,
144            item_type_ident: _,
145            amount_const_ident: _,
146        } = self;
147
148        let new_fn_ident = Ident::new(new_fn_name, Span::call_site());
149        let new_values = item_list
150            .iter()
151            .map(|(amount, ty)| quote! {#Crate::resources::bundle::<#ty, #amount>()});
152
153        quote! {
154            fn #new_fn_ident() -> <Self as RecipeEx>::OutputBundle {
155                (#(#new_values,)*)
156            }
157        }
158    }
159
160    fn generate_recipe_iter_method(
161        &self,
162        iter_fn_name: &str,
163        implementing_trait: TokenStream,
164    ) -> TokenStream {
165        let RecipeItemList {
166            item_list,
167            item_type_ident,
168            amount_const_ident,
169        } = self;
170
171        let iter_fn_ident = Ident::new(iter_fn_name, Span::call_site());
172        let iter_values = item_list
173            .iter()
174            .enumerate()
175            .map(|(i, (_amount, resource_type))| {
176                let i = LitInt::new(&i.to_string(), Span::call_site());
177                quote! {(
178                    <#resource_type as #Crate::ResourceType>::NAME,
179                    <Self as #implementing_trait>::#amount_const_ident.#i,
180                    #Crate::resources::resource_amount_mut(&mut items.#i)
181                )}
182            });
183
184        quote! {
185            fn #iter_fn_ident(
186                items: &mut <Self as #implementing_trait>::#item_type_ident
187            ) -> impl Iterator<Item = (&'static str, u32, &mut u32)> {
188                [#(#iter_values,)*].into_iter()
189            }
190        }
191    }
192
193    fn generate_bundle_type(&self) -> TokenStream {
194        let RecipeItemList {
195            item_list,
196            item_type_ident: _,
197            amount_const_ident: _,
198        } = self;
199
200        let bundle_items = item_list
201            .iter()
202            .map(|(amount, ty)| quote! {#Crate::resources::Bundle<#ty, #amount>});
203
204        quote! {
205            (#(#bundle_items,)*)
206        }
207    }
208}
209
210struct RecipeDetails {
211    name: Ident,
212    generics: Generics,
213
214    inputs: RecipeItemList,
215    outputs: RecipeItemList,
216    ticks: LitInt,
217}
218
219impl RecipeDetails {
220    fn from_input(input: DeriveInput) -> Self {
221        Self::from_attrs(&input.attrs, input.ident, input.generics)
222    }
223
224    fn from_attrs(attrs: &[Attribute], name: Ident, generics: Generics) -> Self {
225        let mut inputs = None;
226        let mut outputs = None;
227        let mut ticks = None;
228        for attr in attrs {
229            if attr.path().is_ident("recipe_inputs") {
230                inputs = Some(RecipeItemList::new_inputs(attr));
231            } else if attr.path().is_ident("recipe_outputs") {
232                outputs = Some(RecipeItemList::new_outputs(attr));
233            } else if attr.path().is_ident("recipe_ticks") {
234                ticks = Some(
235                    attr.parse_args::<LitInt>()
236                        .expect("Invalid \"recipe_ticks\" value"),
237                );
238            }
239        }
240        let inputs = inputs.expect("Missing \"recipe_inputs\" attribute");
241        let outputs = outputs.expect("Missing \"recipe_outputs\" attribute");
242        let ticks = ticks.expect("Missing \"recipe_ticks\" attribute");
243
244        Self {
245            name,
246            generics,
247            inputs,
248            outputs,
249            ticks,
250        }
251    }
252
253    fn generate_doc(&self) -> String {
254        let mut doc_lines = Vec::new();
255
256        doc_lines.push("### Input".to_string());
257        for (amount, ty) in &self.inputs.item_list {
258            let type_str = quote! { #ty }.to_string();
259            doc_lines.push(format!("- [`{type_str}`] :  {amount}\n"));
260        }
261        doc_lines.push("### Output".to_string());
262        for (amount, ty) in &self.outputs.item_list {
263            let type_str = quote! { #ty }.to_string();
264            doc_lines.push(format!("- [`{type_str}`] :  {amount}\n"));
265        }
266        doc_lines.push("### Time".to_string());
267
268        doc_lines.push(format!("- **Ticks**: {}\n", self.ticks));
269
270        doc_lines.join("\n")
271    }
272
273    fn recipe_impl(&self) -> TokenStream {
274        let implementing_trait_path = quote! {#Crate::recipe::Recipe};
275        let inputs_stream = self.inputs.generate_recipe_direction("InputAmountsType");
276        let outputs_stream = self.outputs.generate_recipe_direction("OutputAmountsType");
277
278        let new_inputs_method_stream = self
279            .inputs
280            .generate_recipe_new_method("new_inputs", implementing_trait_path.clone());
281        let new_outputs_method_stream = self
282            .outputs
283            .generate_recipe_new_method("new_outputs", implementing_trait_path.clone());
284
285        let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl();
286
287        let name = &self.name;
288        let ticks = &self.ticks;
289        quote! {
290            impl #impl_generics #Crate::recipe::Recipe for #name #ty_generics #where_clause {
291                const TIME: u64 = #ticks;
292
293                #new_inputs_method_stream
294                #new_outputs_method_stream
295
296                #inputs_stream
297                #outputs_stream
298            }
299        }
300    }
301
302    fn recipe_ex_impl(&self) -> TokenStream {
303        let implementing_trait_path = quote! {#Crate::recipe::Recipe};
304        let input_bundle_type = self.inputs.generate_bundle_type();
305        let output_bundle_type = self.outputs.generate_bundle_type();
306        let new_output_bundle_method_stream = self
307            .outputs
308            .generate_recipe_new_bundle_method("new_output_bundle");
309        let iter_inputs_method_stream = self
310            .inputs
311            .generate_recipe_iter_method("iter_inputs", implementing_trait_path.clone());
312        let iter_outputs_method_stream = self
313            .outputs
314            .generate_recipe_iter_method("iter_outputs", implementing_trait_path.clone());
315        let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl();
316        let name = &self.name;
317        quote! {
318            impl #impl_generics #Crate::recipe::RecipeEx for #name #ty_generics #where_clause {
319                type InputBundle = #input_bundle_type;
320                type OutputBundle = #output_bundle_type;
321                #new_output_bundle_method_stream
322                #iter_inputs_method_stream
323                #iter_outputs_method_stream
324            }
325        }
326    }
327}
328
329#[proc_macro_derive(Recipe, attributes(recipe_inputs, recipe_outputs, recipe_ticks))]
330pub fn derive_recipe(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
331    let input = parse_macro_input!(input as DeriveInput);
332    let recipe_info = RecipeDetails::from_input(input);
333    let output = recipe_info.recipe_impl();
334    proc_macro::TokenStream::from(output)
335}
336
337#[proc_macro_derive(RecipeEx, attributes(recipe_inputs, recipe_outputs, recipe_ticks))]
338pub fn derive_recipe_ex(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
339    let input = parse_macro_input!(input as DeriveInput);
340    let recipe_info = RecipeDetails::from_input(input);
341    let output = recipe_info.recipe_ex_impl();
342    proc_macro::TokenStream::from(output)
343}
344
345/// Generates documentation for a recipe based on its inputs and outputs.
346/// The generated documentation is appended to any existing documentation on the struct.
347#[proc_macro_attribute]
348pub fn recipe_doc(
349    _args: proc_macro::TokenStream,
350    input: proc_macro::TokenStream,
351) -> proc_macro::TokenStream {
352    let mut item = parse_macro_input!(input as ItemStruct);
353    let recipe_info =
354        RecipeDetails::from_attrs(&item.attrs, item.ident.clone(), item.generics.clone());
355
356    let generated_doc = recipe_info.generate_doc();
357    let doc_attr: Attribute = syn::parse_quote! {
358        #[doc = #generated_doc]
359    };
360
361    // Insert the generated doc at the beginning of the attributes
362    item.attrs.push(doc_attr);
363
364    quote! { #item }.into()
365}
366
367struct TechnologyDetails {
368    name: Ident,
369    generics: Generics,
370    research_inputs: RecipeItemList,
371    point_recipe_time: LitInt,
372    research_point_cost: LitInt,
373}
374
375impl TechnologyDetails {
376    fn from_derive(input: DeriveInput) -> Self {
377        Self::from_attrs(&input.attrs, input.ident, input.generics)
378    }
379
380    fn from_attrs(attrs: &[Attribute], name: Ident, generics: Generics) -> Self {
381        let mut research_inputs = None;
382        let mut research_point_cost = None;
383        let mut research_ticks = None;
384        for attr in attrs {
385            if attr.path().is_ident("research_inputs") {
386                research_inputs = Some(RecipeItemList::new_inputs(attr));
387            } else if attr.path().is_ident("research_point_cost") {
388                research_point_cost = Some(
389                    attr.parse_args::<LitInt>()
390                        .expect("Invalid \"research_point_cost\" value"),
391                );
392            } else if attr.path().is_ident("research_ticks") {
393                research_ticks = Some(
394                    attr.parse_args::<LitInt>()
395                        .expect("Invalid \"research_ticks\" value"),
396                );
397            }
398        }
399        let research_inputs = research_inputs.expect("Missing \"research_inputs\" attribute");
400        let research_point_cost =
401            research_point_cost.expect("Missing \"research_point_cost\" attribute");
402        let research_ticks = research_ticks.expect("Missing \"research_ticks\" attribute");
403
404        Self {
405            name,
406            generics,
407            research_inputs,
408            research_point_cost,
409            point_recipe_time: research_ticks,
410        }
411    }
412
413    fn generate_doc(&self) -> String {
414        let mut doc_lines = Vec::new();
415
416        doc_lines.push("### Cost".to_string());
417        for (amount, ty) in &self.research_inputs.item_list {
418            let type_str = quote! { #ty }.to_string();
419            doc_lines.push(format!("- [`{type_str}`] :  {amount}\n"));
420        }
421
422        doc_lines.push(format!("**Ticks**: {}\n", self.point_recipe_time));
423
424        doc_lines.push(format!(
425            "**Research points required**: {}",
426            self.research_point_cost
427        ));
428
429        doc_lines.join("\n")
430    }
431
432    fn technology_impl(&self) -> TokenStream {
433        let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl();
434        let name = &self.name;
435
436        let inputs_stream = self
437            .research_inputs
438            .generate_recipe_direction("InputAmountsType");
439        let research_point_cost = &self.research_point_cost;
440        let point_recipe_time = &self.point_recipe_time;
441
442        let input_bundle_type = self.research_inputs.generate_bundle_type();
443
444        let implementing_trait_path = quote! {#Crate::research::TechnologyEx};
445
446        let new_inputs_method_stream = self
447            .research_inputs
448            .generate_recipe_new_method("new_inputs", implementing_trait_path.clone());
449
450        let iter_inputs_method_stream = self
451            .research_inputs
452            .generate_recipe_iter_method("iter_inputs", implementing_trait_path.clone());
453
454        quote! {
455            impl #impl_generics #Crate::research::TechnologyEx for #name #ty_generics #where_clause {
456                #inputs_stream
457                const POINT_RECIPE_TIME: u64 = #point_recipe_time;
458                const REQUIRED_RESEARCH_POINTS_EX: u32 = #research_point_cost;
459                type InputBundle = #input_bundle_type;
460
461                #new_inputs_method_stream
462                #iter_inputs_method_stream
463            }
464        }
465    }
466}
467
468#[proc_macro_derive(
469    TechnologyEx,
470    attributes(research_inputs, research_point_cost, research_ticks)
471)]
472pub fn derive_technology(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
473    let input = parse_macro_input!(input as DeriveInput);
474    let tech_info = TechnologyDetails::from_derive(input);
475    let output = tech_info.technology_impl();
476    proc_macro::TokenStream::from(output)
477}
478
479/// Generates documentation for a technology struct based on its `research_inputs` and `research_ticks` attributes.
480/// The generated documentation is appended to any existing documentation on the struct.
481#[proc_macro_attribute]
482pub fn technology_doc(
483    _args: proc_macro::TokenStream,
484    input: proc_macro::TokenStream,
485) -> proc_macro::TokenStream {
486    let mut item = parse_macro_input!(input as ItemStruct);
487
488    // Parse the technology details from the struct's attributes
489    let tech_info =
490        TechnologyDetails::from_attrs(&item.attrs, item.ident.clone(), item.generics.clone());
491
492    // Generate the documentation
493    let generated_doc = tech_info.generate_doc();
494    let doc_attr: Attribute = syn::parse_quote! {
495        #[doc = #generated_doc]
496    };
497
498    // Insert the generated doc at the beginning of the attributes
499    item.attrs.push(doc_attr);
500
501    quote! { #item }.into()
502}