1use proc_macro::TokenStream;
2use proc_macro2::TokenStream as TokenStream2;
3use syn::{
4 Expr, ItemFn, LitStr, Path, Token, Type,
5 parse::{Parse, ParseStream},
6 parse_macro_input,
7 spanned::Spanned,
8};
9
10#[proc_macro_attribute]
30pub fn event(args: TokenStream, input: TokenStream) -> TokenStream {
31 let event_args = parse_macro_input!(args as EventAttributeArgs);
32 let function = parse_macro_input!(input as ItemFn);
33
34 let marker_type = match (&event_args.event, event_marker_from_param(&function)) {
35 (Some(path), _) => path
36 .segments
37 .last()
38 .map(|segment| segment.ident.to_string()),
39 (None, Some(marker)) => Some(marker),
40 (None, None) => None,
41 };
42
43 let Some(type_name) = marker_type else {
44 return syn::Error::new_spanned(
45 &function.sig,
46 "expected an event parameter such as `event: OnBuiltEntityEvent`",
47 )
48 .to_compile_error()
49 .into();
50 };
51
52 let Some(event_name) = lookup_event_name(&type_name) else {
53 let span = event_args
54 .event
55 .as_ref()
56 .map_or_else(|| function.sig.span(), Spanned::span);
57 return syn::Error::new(span, format!("unsupported event type `{type_name}`"))
58 .to_compile_error()
59 .into();
60 };
61
62 if let Some(filter) = &event_args.filter
63 && lookup_event_filter_type(&type_name).is_none()
64 {
65 return syn::Error::new_spanned(
66 filter,
67 format!("event `{type_name}` does not support filters"),
68 )
69 .to_compile_error()
70 .into();
71 }
72
73 if function.sig.ident != event_name {
74 return syn::Error::new_spanned(
75 &function.sig.ident,
76 format!("event handler must be named `{event_name}`"),
77 )
78 .to_compile_error()
79 .into();
80 }
81
82 let filter_check: TokenStream2 =
83 event_args
84 .filter
85 .as_ref()
86 .map_or_else(TokenStream2::new, |filter_expr| {
87 quote::quote! {
88 const _: () = { let _ = #filter_expr; };
89 }
90 });
91
92 TokenStream::from(quote::quote! {
93 #[allow(dead_code)]
94 #function
95
96 #filter_check
97 })
98}
99
100struct EventAttributeArgs {
101 event: Option<Path>,
102 filter: Option<Expr>,
103}
104
105impl Parse for EventAttributeArgs {
106 fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
107 if input.is_empty() {
108 return Ok(Self {
109 event: None,
110 filter: None,
111 });
112 }
113
114 if input.peek(syn::Ident) && input.peek2(Token![=]) {
116 let keyword: syn::Ident = input.parse()?;
117 if keyword != "filter" {
118 return Err(syn::Error::new(
119 keyword.span(),
120 "expected `filter` or an event type such as `OnBuiltEntity`",
121 ));
122 }
123 input.parse::<Token![=]>()?;
124 return Ok(Self {
125 event: None,
126 filter: Some(input.parse::<Expr>()?),
127 });
128 }
129
130 let event: Path = input.parse()?;
131 let filter = if input.peek(Token![,]) {
132 input.parse::<Token![,]>()?;
133 let keyword: syn::Ident = input.parse()?;
134 if keyword != "filter" {
135 return Err(syn::Error::new(
136 keyword.span(),
137 "expected `filter` after event type",
138 ));
139 }
140 input.parse::<Token![=]>()?;
141 Some(input.parse::<Expr>()?)
142 } else {
143 None
144 };
145
146 Ok(Self {
147 event: Some(event),
148 filter,
149 })
150 }
151}
152
153fn event_marker_from_param(function: &ItemFn) -> Option<String> {
154 let syn::FnArg::Typed(pat_type) = function.sig.inputs.first()? else {
155 return None;
156 };
157 event_marker_from_type(&pat_type.ty)
158}
159
160fn event_marker_from_type(ty: &Type) -> Option<String> {
161 let syn::Type::Path(type_path) = ty else {
162 return None;
163 };
164 let segments = &type_path.path.segments;
165 if segments.len() == 1 {
166 let ident = segments[0].ident.to_string();
167 return ident.strip_suffix("Event").map(str::to_string);
168 }
169 None
170}
171
172fn lookup_event_name(type_name: &str) -> Option<&'static str> {
173 include!(concat!(env!("OUT_DIR"), "/event_lookup.rs"))
174}
175
176fn lookup_event_filter_type(type_name: &str) -> Option<&'static str> {
177 include!(concat!(env!("OUT_DIR"), "/event_filter_lookup.rs"))
178}
179
180#[proc_macro_attribute]
181pub fn settings(_args: TokenStream, input: TokenStream) -> TokenStream {
182 input
183}
184
185#[proc_macro_attribute]
186pub fn settings_updates(_args: TokenStream, input: TokenStream) -> TokenStream {
187 input
188}
189
190#[proc_macro_attribute]
191pub fn settings_final_fixes(_args: TokenStream, input: TokenStream) -> TokenStream {
192 input
193}
194
195#[proc_macro_attribute]
196pub fn data(_args: TokenStream, input: TokenStream) -> TokenStream {
197 input
198}
199
200#[proc_macro_attribute]
201pub fn data_updates(_args: TokenStream, input: TokenStream) -> TokenStream {
202 input
203}
204
205#[proc_macro_attribute]
206pub fn data_final_fixes(_args: TokenStream, input: TokenStream) -> TokenStream {
207 input
208}
209
210#[proc_macro_attribute]
211pub fn control(_args: TokenStream, input: TokenStream) -> TokenStream {
212 input
213}
214
215#[proc_macro_attribute]
219pub fn shared(_args: TokenStream, input: TokenStream) -> TokenStream {
220 input
221}
222
223#[proc_macro]
225pub fn settings_mod(input: TokenStream) -> TokenStream {
226 wrap_stage_module("settings", input)
227}
228
229#[proc_macro]
231pub fn settings_updates_mod(input: TokenStream) -> TokenStream {
232 wrap_stage_module("settings_updates", input)
233}
234
235#[proc_macro]
237pub fn settings_final_fixes_mod(input: TokenStream) -> TokenStream {
238 wrap_stage_module("settings_final_fixes", input)
239}
240
241#[proc_macro]
243pub fn data_mod(input: TokenStream) -> TokenStream {
244 wrap_stage_module("data", input)
245}
246
247#[proc_macro]
249pub fn data_updates_mod(input: TokenStream) -> TokenStream {
250 wrap_stage_module("data_updates", input)
251}
252
253#[proc_macro]
255pub fn data_final_fixes_mod(input: TokenStream) -> TokenStream {
256 wrap_stage_module("data_final_fixes", input)
257}
258
259#[proc_macro]
261pub fn control_mod(input: TokenStream) -> TokenStream {
262 wrap_stage_module("control", input)
263}
264
265#[proc_macro]
267pub fn shared_mod(input: TokenStream) -> TokenStream {
268 wrap_stage_module("shared", input)
269}
270
271fn wrap_stage_module(stage: &str, input: TokenStream) -> TokenStream {
272 let module_name = syn::Ident::new(
273 &format!("__factorio_{stage}"),
274 proc_macro2::Span::call_site(),
275 );
276 let items = proc_macro2::TokenStream::from(input);
277 TokenStream::from(quote::quote! {
278 #[doc(hidden)]
279 mod #module_name { #items }
280 })
281}
282
283#[proc_macro]
308pub fn mod_settings(input: TokenStream) -> TokenStream {
309 let ModSettingsInput { prefix, groups } = parse_macro_input!(input as ModSettingsInput);
310
311 let mut const_defs = Vec::<TokenStream2>::new();
313 let mut extend_items = Vec::<TokenStream2>::new();
314
315 for group in &groups {
316 let setting_type_str = match group.stage {
317 SettingStage::Startup => "startup",
318 SettingStage::RuntimeGlobal => "runtime-global",
319 SettingStage::RuntimePerUser => "runtime-per-user",
320 };
321
322 for entry in &group.entries {
323 let const_name = screaming_to_const_ident(&entry.ident);
324 let lua_name = build_lua_name(prefix.as_deref(), &entry.ident.to_string());
325 let default_expr = &entry.default;
326
327 const_defs.push(quote::quote! {
329 pub const #const_name: &'static str = #lua_name;
330 });
331
332 let lua_name_lit = lua_name.as_str();
333 let item_expr = match entry.setting_type {
334 SettingType::Bool => quote::quote! {
335 BoolSetting {
336 name: #lua_name_lit,
337 setting_type: #setting_type_str,
338 default_value: #default_expr,
339 }
340 },
341 SettingType::Int => quote::quote! {
342 IntSetting {
343 name: #lua_name_lit,
344 setting_type: #setting_type_str,
345 default_value: #default_expr,
346 minimum_value: None,
347 maximum_value: None,
348 }
349 },
350 SettingType::Double => quote::quote! {
351 DoubleSetting {
352 name: #lua_name_lit,
353 setting_type: #setting_type_str,
354 default_value: #default_expr,
355 minimum_value: None,
356 maximum_value: None,
357 }
358 },
359 SettingType::Str => quote::quote! {
360 StringSetting {
361 name: #lua_name_lit,
362 setting_type: #setting_type_str,
363 default_value: #default_expr,
364 hidden: false,
365 }
366 },
367 };
368 extend_items.push(item_expr);
369 }
370 }
371
372 TokenStream::from(quote::quote! {
373 pub struct Settings;
374
375 impl Settings {
376 #( #const_defs )*
377 }
378
379 pub fn register() {
380 data.extend([
381 #( #extend_items, )*
382 ]);
383 }
384 })
385}
386
387struct ModSettingsInput {
388 prefix: Option<String>,
389 groups: Vec<SettingGroup>,
390}
391
392struct SettingGroup {
393 stage: SettingStage,
394 entries: Vec<SettingEntry>,
395}
396
397struct SettingEntry {
398 ident: syn::Ident,
399 setting_type: SettingType,
400 default: Expr,
401}
402
403#[derive(Clone, Copy)]
404enum SettingStage {
405 Startup,
406 RuntimeGlobal,
407 RuntimePerUser,
408}
409
410#[derive(Clone, Copy)]
411enum SettingType {
412 Bool,
413 Int,
414 Double,
415 Str,
416}
417
418impl Parse for ModSettingsInput {
419 fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
420 let mut prefix: Option<String> = None;
421 if input.peek(syn::Ident) {
422 let fork = input.fork();
423 let kw: syn::Ident = fork.parse()?;
424 if kw == "prefix" && fork.peek(Token![=]) {
425 let _: syn::Ident = input.parse()?;
426 let _: Token![=] = input.parse()?;
427 let lit: LitStr = input.parse()?;
428 prefix = Some(lit.value());
429 let _: Option<Token![,]> = input.parse()?;
430 }
431 }
432
433 let mut groups = Vec::new();
434 while !input.is_empty() {
435 groups.push(input.parse::<SettingGroup>()?);
436 }
437
438 Ok(Self { prefix, groups })
439 }
440}
441
442impl Parse for SettingGroup {
443 fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
444 let stage_kw: syn::Ident = input.parse()?;
445 let stage = match stage_kw.to_string().as_str() {
446 "startup" => SettingStage::Startup,
447 "runtime_global" => SettingStage::RuntimeGlobal,
448 "runtime_per_user" => SettingStage::RuntimePerUser,
449 other => {
450 return Err(syn::Error::new(
451 stage_kw.span(),
452 format!(
453 "unknown setting stage `{other}`; expected `startup`, `runtime_global`, or `runtime_per_user`"
454 ),
455 ));
456 }
457 };
458
459 let content;
460 syn::braced!(content in input);
461
462 let mut entries = Vec::new();
463 while !content.is_empty() {
464 entries.push(content.parse::<SettingEntry>()?);
465 }
466
467 Ok(Self { stage, entries })
468 }
469}
470
471impl Parse for SettingEntry {
472 fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
473 let ident: syn::Ident = input.parse()?;
474 let _: Token![:] = input.parse()?;
475 let ty: Type = input.parse()?;
476 let _: Token![=] = input.parse()?;
477 let default: Expr = input.parse()?;
478 let _: Option<Token![,]> = input.parse()?;
479
480 let setting_type = type_to_setting_type(&ty).ok_or_else(|| {
481 syn::Error::new_spanned(
482 &ty,
483 "unsupported setting type; use `bool`, `i64`, `f64`, or `&'static str`",
484 )
485 })?;
486
487 Ok(Self {
488 ident,
489 setting_type,
490 default,
491 })
492 }
493}
494
495fn type_to_setting_type(ty: &Type) -> Option<SettingType> {
496 match ty {
497 Type::Path(tp) => {
498 let ident = tp.path.get_ident()?.to_string();
499 match ident.as_str() {
500 "bool" => Some(SettingType::Bool),
501 "i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" | "usize" => {
502 Some(SettingType::Int)
503 }
504 "f32" | "f64" => Some(SettingType::Double),
505 "String" => Some(SettingType::Str),
506 _ => None,
507 }
508 }
509 Type::Reference(tr) => {
511 if let Type::Path(tp) = tr.elem.as_ref()
512 && tp.path.is_ident("str")
513 {
514 Some(SettingType::Str)
515 } else {
516 None
517 }
518 }
519 _ => None,
520 }
521}
522
523fn screaming_to_const_ident(ident: &syn::Ident) -> proc_macro2::Ident {
525 let upper = ident.to_string().to_uppercase();
526 proc_macro2::Ident::new(&upper, ident.span())
527}
528
529fn build_lua_name(prefix: Option<&str>, snake: &str) -> String {
531 let kebab = snake.replace('_', "-");
532 match prefix {
533 Some(p) => format!("{p}-{kebab}"),
534 None => kebab,
535 }
536}
537
538#[proc_macro]
563pub fn locale(input: TokenStream) -> TokenStream {
564 let LocaleInput { languages, .. } = parse_macro_input!(input as LocaleInput);
565
566 let mut checks = Vec::<TokenStream2>::new();
567 for lang in &languages {
568 for category in &lang.categories {
569 for entry in &category.entries {
570 if let LocaleKey::Path(path) = &entry.key {
571 checks.push(quote::quote! {
572 let _: &'static str = #path;
573 });
574 }
575 let value = &entry.value;
576 let value_text = value.value();
577 if value_text.contains('\n') || value_text.contains('\r') {
578 return syn::Error::new_spanned(
579 value,
580 "locale values must be a single line",
581 )
582 .to_compile_error()
583 .into();
584 }
585 }
586 }
587 }
588
589 TokenStream::from(quote::quote! {
590 const _: () = {
591 #( #checks )*
592 };
593 })
594}
595
596struct LocaleInput {
597 #[allow(dead_code)]
598 file: Option<String>,
599 languages: Vec<LocaleLanguageBlock>,
600}
601
602struct LocaleLanguageBlock {
603 #[allow(dead_code)]
604 lang: String,
605 categories: Vec<LocaleCategoryBlock>,
606}
607
608struct LocaleCategoryBlock {
609 #[allow(dead_code)]
610 name: String,
611 entries: Vec<LocaleEntry>,
612}
613
614struct LocaleEntry {
615 key: LocaleKey,
616 value: LitStr,
617}
618
619enum LocaleKey {
620 Path(Path),
621 #[allow(dead_code)]
622 Literal(String),
623}
624
625impl Parse for LocaleInput {
626 fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
627 let mut file = None;
628 if input.peek(syn::Ident) {
629 let fork = input.fork();
630 let kw: syn::Ident = fork.parse()?;
631 if kw == "file" && fork.peek(Token![=]) {
632 let _: syn::Ident = input.parse()?;
633 let _: Token![=] = input.parse()?;
634 let lit: LitStr = input.parse()?;
635 file = Some(lit.value());
636 let _: Option<Token![,]> = input.parse()?;
637 }
638 }
639
640 let mut languages = Vec::new();
641 while !input.is_empty() {
642 languages.push(input.parse()?);
643 }
644
645 if languages.is_empty() {
646 return Err(input.error("expected at least one language block such as `en { ... }`"));
647 }
648
649 Ok(Self { file, languages })
650 }
651}
652
653impl Parse for LocaleLanguageBlock {
654 fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
655 let lang = if input.peek(LitStr) {
656 input.parse::<LitStr>()?.value()
657 } else {
658 input.parse::<syn::Ident>()?.to_string()
659 };
660
661 let content;
662 syn::braced!(content in input);
663
664 let mut categories = Vec::new();
665 while !content.is_empty() {
666 categories.push(content.parse()?);
667 }
668
669 Ok(Self { lang, categories })
670 }
671}
672
673impl Parse for LocaleCategoryBlock {
674 fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
675 let name = if input.peek(LitStr) {
676 input.parse::<LitStr>()?.value()
677 } else {
678 let ident: syn::Ident = input.parse()?;
679 ident.to_string().replace('_', "-")
680 };
681
682 let content;
683 syn::braced!(content in input);
684
685 let mut entries = Vec::new();
686 while !content.is_empty() {
687 entries.push(content.parse()?);
688 }
689
690 Ok(Self { name, entries })
691 }
692}
693
694impl Parse for LocaleEntry {
695 fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
696 let key = if input.peek(LitStr) {
697 LocaleKey::Literal(input.parse::<LitStr>()?.value())
698 } else {
699 LocaleKey::Path(input.parse()?)
700 };
701 let _: Token![=] = input.parse()?;
702 let value: LitStr = input.parse()?;
703 let _: Option<Token![,]> = input.parse()?;
704 Ok(Self { key, value })
705 }
706}