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_attribute]
237pub fn export(args: TokenStream, input: TokenStream) -> TokenStream {
238 if !args.is_empty() {
239 let _ = parse_macro_input!(args as ExportAttributeArgs);
240 }
241 input
242}
243
244#[allow(dead_code)]
246enum ExportInterfaceArg {
247 Default,
249 Named(LitStr),
251}
252
253struct ExportAttributeArgs {
254 #[allow(dead_code)]
255 interface: Option<ExportInterfaceArg>,
256}
257
258impl Parse for ExportAttributeArgs {
259 fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
260 if input.is_empty() {
261 return Ok(Self { interface: None });
262 }
263 let keyword: syn::Ident = input.parse()?;
264 if keyword != "interface" {
265 return Err(syn::Error::new(
266 keyword.span(),
267 "expected `interface` or `interface = \"...\"`",
268 ));
269 }
270 if input.peek(Token![=]) {
271 input.parse::<Token![=]>()?;
272 Ok(Self {
273 interface: Some(ExportInterfaceArg::Named(input.parse()?)),
274 })
275 } else {
276 Ok(Self {
277 interface: Some(ExportInterfaceArg::Default),
278 })
279 }
280 }
281}
282
283#[proc_macro]
285pub fn settings_mod(input: TokenStream) -> TokenStream {
286 wrap_stage_module("settings", input)
287}
288
289#[proc_macro]
291pub fn settings_updates_mod(input: TokenStream) -> TokenStream {
292 wrap_stage_module("settings_updates", input)
293}
294
295#[proc_macro]
297pub fn settings_final_fixes_mod(input: TokenStream) -> TokenStream {
298 wrap_stage_module("settings_final_fixes", input)
299}
300
301#[proc_macro]
303pub fn data_mod(input: TokenStream) -> TokenStream {
304 wrap_stage_module("data", input)
305}
306
307#[proc_macro]
309pub fn data_updates_mod(input: TokenStream) -> TokenStream {
310 wrap_stage_module("data_updates", input)
311}
312
313#[proc_macro]
315pub fn data_final_fixes_mod(input: TokenStream) -> TokenStream {
316 wrap_stage_module("data_final_fixes", input)
317}
318
319#[proc_macro]
321pub fn control_mod(input: TokenStream) -> TokenStream {
322 wrap_stage_module("control", input)
323}
324
325#[proc_macro]
327pub fn shared_mod(input: TokenStream) -> TokenStream {
328 wrap_stage_module("shared", input)
329}
330
331fn wrap_stage_module(stage: &str, input: TokenStream) -> TokenStream {
332 let module_name = syn::Ident::new(
333 &format!("__factorio_{stage}"),
334 proc_macro2::Span::call_site(),
335 );
336 let items = proc_macro2::TokenStream::from(input);
337 TokenStream::from(quote::quote! {
338 #[doc(hidden)]
339 mod #module_name { #items }
340 })
341}
342
343#[proc_macro]
368pub fn mod_settings(input: TokenStream) -> TokenStream {
369 let ModSettingsInput { prefix, groups } = parse_macro_input!(input as ModSettingsInput);
370
371 let mut const_defs = Vec::<TokenStream2>::new();
373 let mut extend_items = Vec::<TokenStream2>::new();
374
375 for group in &groups {
376 let setting_type_str = match group.stage {
377 SettingStage::Startup => "startup",
378 SettingStage::RuntimeGlobal => "runtime-global",
379 SettingStage::RuntimePerUser => "runtime-per-user",
380 };
381
382 for entry in &group.entries {
383 let const_name = screaming_to_const_ident(&entry.ident);
384 let lua_name = build_lua_name(prefix.as_deref(), &entry.ident.to_string());
385 let default_expr = &entry.default;
386
387 const_defs.push(quote::quote! {
389 pub const #const_name: &'static str = #lua_name;
390 });
391
392 let lua_name_lit = lua_name.as_str();
393 let item_expr = match entry.setting_type {
394 SettingType::Bool => quote::quote! {
395 BoolSetting {
396 name: #lua_name_lit,
397 setting_type: #setting_type_str,
398 default_value: #default_expr,
399 }
400 },
401 SettingType::Int => quote::quote! {
402 IntSetting {
403 name: #lua_name_lit,
404 setting_type: #setting_type_str,
405 default_value: #default_expr,
406 minimum_value: None,
407 maximum_value: None,
408 }
409 },
410 SettingType::Double => quote::quote! {
411 DoubleSetting {
412 name: #lua_name_lit,
413 setting_type: #setting_type_str,
414 default_value: #default_expr,
415 minimum_value: None,
416 maximum_value: None,
417 }
418 },
419 SettingType::Str => quote::quote! {
420 StringSetting {
421 name: #lua_name_lit,
422 setting_type: #setting_type_str,
423 default_value: #default_expr,
424 hidden: false,
425 }
426 },
427 };
428 extend_items.push(item_expr);
429 }
430 }
431
432 TokenStream::from(quote::quote! {
433 pub struct Settings;
434
435 impl Settings {
436 #( #const_defs )*
437 }
438
439 pub fn register() {
440 data.extend([
441 #( #extend_items, )*
442 ]);
443 }
444 })
445}
446
447struct ModSettingsInput {
448 prefix: Option<String>,
449 groups: Vec<SettingGroup>,
450}
451
452struct SettingGroup {
453 stage: SettingStage,
454 entries: Vec<SettingEntry>,
455}
456
457struct SettingEntry {
458 ident: syn::Ident,
459 setting_type: SettingType,
460 default: Expr,
461}
462
463#[derive(Clone, Copy)]
464enum SettingStage {
465 Startup,
466 RuntimeGlobal,
467 RuntimePerUser,
468}
469
470#[derive(Clone, Copy)]
471enum SettingType {
472 Bool,
473 Int,
474 Double,
475 Str,
476}
477
478impl Parse for ModSettingsInput {
479 fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
480 let mut prefix: Option<String> = None;
481 if input.peek(syn::Ident) {
482 let fork = input.fork();
483 let kw: syn::Ident = fork.parse()?;
484 if kw == "prefix" && fork.peek(Token![=]) {
485 let _: syn::Ident = input.parse()?;
486 let _: Token![=] = input.parse()?;
487 let lit: LitStr = input.parse()?;
488 prefix = Some(lit.value());
489 let _: Option<Token![,]> = input.parse()?;
490 }
491 }
492
493 let mut groups = Vec::new();
494 while !input.is_empty() {
495 groups.push(input.parse::<SettingGroup>()?);
496 }
497
498 Ok(Self { prefix, groups })
499 }
500}
501
502impl Parse for SettingGroup {
503 fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
504 let stage_kw: syn::Ident = input.parse()?;
505 let stage = match stage_kw.to_string().as_str() {
506 "startup" => SettingStage::Startup,
507 "runtime_global" => SettingStage::RuntimeGlobal,
508 "runtime_per_user" => SettingStage::RuntimePerUser,
509 other => {
510 return Err(syn::Error::new(
511 stage_kw.span(),
512 format!(
513 "unknown setting stage `{other}`; expected `startup`, `runtime_global`, or `runtime_per_user`"
514 ),
515 ));
516 }
517 };
518
519 let content;
520 syn::braced!(content in input);
521
522 let mut entries = Vec::new();
523 while !content.is_empty() {
524 entries.push(content.parse::<SettingEntry>()?);
525 }
526
527 Ok(Self { stage, entries })
528 }
529}
530
531impl Parse for SettingEntry {
532 fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
533 let ident: syn::Ident = input.parse()?;
534 let _: Token![:] = input.parse()?;
535 let ty: Type = input.parse()?;
536 let _: Token![=] = input.parse()?;
537 let default: Expr = input.parse()?;
538 let _: Option<Token![,]> = input.parse()?;
539
540 let setting_type = type_to_setting_type(&ty).ok_or_else(|| {
541 syn::Error::new_spanned(
542 &ty,
543 "unsupported setting type; use `bool`, `i64`, `f64`, or `&'static str`",
544 )
545 })?;
546
547 Ok(Self {
548 ident,
549 setting_type,
550 default,
551 })
552 }
553}
554
555fn type_to_setting_type(ty: &Type) -> Option<SettingType> {
556 match ty {
557 Type::Path(tp) => {
558 let ident = tp.path.get_ident()?.to_string();
559 match ident.as_str() {
560 "bool" => Some(SettingType::Bool),
561 "i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" | "usize" => {
562 Some(SettingType::Int)
563 }
564 "f32" | "f64" => Some(SettingType::Double),
565 "String" => Some(SettingType::Str),
566 _ => None,
567 }
568 }
569 Type::Reference(tr) => {
571 if let Type::Path(tp) = tr.elem.as_ref()
572 && tp.path.is_ident("str")
573 {
574 Some(SettingType::Str)
575 } else {
576 None
577 }
578 }
579 _ => None,
580 }
581}
582
583fn screaming_to_const_ident(ident: &syn::Ident) -> proc_macro2::Ident {
585 let upper = ident.to_string().to_uppercase();
586 proc_macro2::Ident::new(&upper, ident.span())
587}
588
589fn build_lua_name(prefix: Option<&str>, snake: &str) -> String {
591 let kebab = snake.replace('_', "-");
592 match prefix {
593 Some(p) => format!("{p}-{kebab}"),
594 None => kebab,
595 }
596}
597
598#[proc_macro]
623pub fn locale(input: TokenStream) -> TokenStream {
624 let LocaleInput { languages, .. } = parse_macro_input!(input as LocaleInput);
625
626 let mut checks = Vec::<TokenStream2>::new();
627 for lang in &languages {
628 for category in &lang.categories {
629 for entry in &category.entries {
630 if let LocaleKey::Path(path) = &entry.key {
631 checks.push(quote::quote! {
632 let _: &'static str = #path;
633 });
634 }
635 let value = &entry.value;
636 let value_text = value.value();
637 if value_text.contains('\n') || value_text.contains('\r') {
638 return syn::Error::new_spanned(value, "locale values must be a single line")
639 .to_compile_error()
640 .into();
641 }
642 }
643 }
644 }
645
646 TokenStream::from(quote::quote! {
647 const _: () = {
648 #( #checks )*
649 };
650 })
651}
652
653struct LocaleInput {
654 #[allow(dead_code)]
655 file: Option<String>,
656 languages: Vec<LocaleLanguageBlock>,
657}
658
659struct LocaleLanguageBlock {
660 #[allow(dead_code)]
661 lang: String,
662 categories: Vec<LocaleCategoryBlock>,
663}
664
665struct LocaleCategoryBlock {
666 #[allow(dead_code)]
667 name: String,
668 entries: Vec<LocaleEntry>,
669}
670
671struct LocaleEntry {
672 key: LocaleKey,
673 value: LitStr,
674}
675
676enum LocaleKey {
677 Path(Path),
678 #[allow(dead_code)]
679 Literal(String),
680}
681
682impl Parse for LocaleInput {
683 fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
684 let mut file = None;
685 if input.peek(syn::Ident) {
686 let fork = input.fork();
687 let kw: syn::Ident = fork.parse()?;
688 if kw == "file" && fork.peek(Token![=]) {
689 let _: syn::Ident = input.parse()?;
690 let _: Token![=] = input.parse()?;
691 let lit: LitStr = input.parse()?;
692 file = Some(lit.value());
693 let _: Option<Token![,]> = input.parse()?;
694 }
695 }
696
697 let mut languages = Vec::new();
698 while !input.is_empty() {
699 languages.push(input.parse()?);
700 }
701
702 if languages.is_empty() {
703 return Err(input.error("expected at least one language block such as `en { ... }`"));
704 }
705
706 Ok(Self { file, languages })
707 }
708}
709
710impl Parse for LocaleLanguageBlock {
711 fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
712 let lang = if input.peek(LitStr) {
713 input.parse::<LitStr>()?.value()
714 } else {
715 input.parse::<syn::Ident>()?.to_string()
716 };
717
718 let content;
719 syn::braced!(content in input);
720
721 let mut categories = Vec::new();
722 while !content.is_empty() {
723 categories.push(content.parse()?);
724 }
725
726 Ok(Self { lang, categories })
727 }
728}
729
730impl Parse for LocaleCategoryBlock {
731 fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
732 let name = if input.peek(LitStr) {
733 input.parse::<LitStr>()?.value()
734 } else {
735 let ident: syn::Ident = input.parse()?;
736 ident.to_string().replace('_', "-")
737 };
738
739 let content;
740 syn::braced!(content in input);
741
742 let mut entries = Vec::new();
743 while !content.is_empty() {
744 entries.push(content.parse()?);
745 }
746
747 Ok(Self { name, entries })
748 }
749}
750
751impl Parse for LocaleEntry {
752 fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
753 let key = if input.peek(LitStr) {
754 LocaleKey::Literal(input.parse::<LitStr>()?.value())
755 } else {
756 LocaleKey::Path(input.parse()?)
757 };
758 let _: Token![=] = input.parse()?;
759 let value: LitStr = input.parse()?;
760 let _: Option<Token![,]> = input.parse()?;
761 Ok(Self { key, value })
762 }
763}