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#[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 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#[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 let tech_info =
490 TechnologyDetails::from_attrs(&item.attrs, item.ident.clone(), item.generics.clone());
491
492 let generated_doc = tech_info.generate_doc();
494 let doc_attr: Attribute = syn::parse_quote! {
495 #[doc = #generated_doc]
496 };
497
498 item.attrs.push(doc_attr);
500
501 quote! { #item }.into()
502}