Skip to main content

factorio_macros/
lib.rs

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/// Marks a control-stage function as a Factorio event handler.
11///
12/// The event is inferred from the handler name and first parameter type
13/// (`OnBuiltEntityEvent`). Filters are validated at compile time via a generated
14/// const expression.
15///
16/// # Examples
17///
18/// Without filter:
19/// ```ignore
20/// #[factorio_rs::event]
21/// pub fn on_singleplayer_init(event: OnSingleplayerInitEvent) {}
22/// ```
23///
24/// With filter (filter expression is type-checked at compile time):
25/// ```ignore
26/// #[factorio_rs::event(filter = [OnBuiltEntityFilter::type_("inserter")])]
27/// pub fn on_built_entity(event: OnBuiltEntityEvent) {}
28/// ```
29#[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        // `filter = [...]` without an explicit event type
115        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/// Marks a file or inline `mod` as shared-stage code for transpilation.
216///
217/// Shared modules may be required by any other stage.
218#[proc_macro_attribute]
219pub fn shared(_args: TokenStream, input: TokenStream) -> TokenStream {
220    input
221}
222
223/// Publishes a function (or every `pub fn` in a module) as part of this mod's
224/// cross-mod API.
225///
226/// Control-stage exports are registered with Factorio `remote.add_interface`.
227/// Shared-stage exports remain requireable module functions and are included in
228/// the generated `api/` stub crate.
229///
230/// Optional remote interface:
231/// - `#[factorio_rs::export(interface)]` - remote using the mod name
232/// - `#[factorio_rs::export(interface = "my_iface")]` - remote on a custom name
233///
234/// On a `mod` item, every public function inside inherits the export without
235/// needing a per-fn attribute.
236#[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/// Parsed `#[export(...)]` interface argument (validation-only today).
245#[allow(dead_code)]
246enum ExportInterfaceArg {
247    /// `#[export(interface)]` — remote using the mod-name default.
248    Default,
249    /// `#[export(interface = "name")]`.
250    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/// Declares a settings-stage module from a block of items.
284#[proc_macro]
285pub fn settings_mod(input: TokenStream) -> TokenStream {
286    wrap_stage_module("settings", input)
287}
288
289/// Declares a settings-updates-stage module from a block of items.
290#[proc_macro]
291pub fn settings_updates_mod(input: TokenStream) -> TokenStream {
292    wrap_stage_module("settings_updates", input)
293}
294
295/// Declares a settings-final-fixes-stage module from a block of items.
296#[proc_macro]
297pub fn settings_final_fixes_mod(input: TokenStream) -> TokenStream {
298    wrap_stage_module("settings_final_fixes", input)
299}
300
301/// Declares a data/prototype-stage module from a block of items.
302#[proc_macro]
303pub fn data_mod(input: TokenStream) -> TokenStream {
304    wrap_stage_module("data", input)
305}
306
307/// Declares a data-updates-stage module from a block of items.
308#[proc_macro]
309pub fn data_updates_mod(input: TokenStream) -> TokenStream {
310    wrap_stage_module("data_updates", input)
311}
312
313/// Declares a data-final-fixes-stage module from a block of items.
314#[proc_macro]
315pub fn data_final_fixes_mod(input: TokenStream) -> TokenStream {
316    wrap_stage_module("data_final_fixes", input)
317}
318
319/// Declares a control/runtime-stage module from a block of items.
320#[proc_macro]
321pub fn control_mod(input: TokenStream) -> TokenStream {
322    wrap_stage_module("control", input)
323}
324
325/// Declares a shared-stage module from a block of items.
326#[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/// Declare mod settings in a single, concise block.
344///
345/// # Example
346/// ```ignore
347/// use factorio_rs::prelude::*;
348///
349/// factorio_rs::mod_settings! {
350///     prefix = "ms",
351///
352///     startup {
353///         casual_mode: bool = false,
354///         adjacency_enabled: bool = true,
355///     }
356///
357///     runtime_global {
358///         max_entities: i64 = 100,
359///     }
360/// }
361/// ```
362///
363/// Access in control stage:
364/// ```ignore
365/// let enabled = settings.startup.get::<bool>(Settings::CASUAL_MODE);
366/// ```
367#[proc_macro]
368pub fn mod_settings(input: TokenStream) -> TokenStream {
369    let ModSettingsInput { prefix, groups } = parse_macro_input!(input as ModSettingsInput);
370
371    // Collect all settings in order.
372    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            // `pub const CASUAL_MODE: &'static str = "ms-casual-mode";`
388            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        // &'static str or &str
570        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
583/// Convert a `snake_case` ident to a `proc_macro2::Ident`.
584fn 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
589/// Build the Lua setting name: `{prefix}-{kebab-case}` or just `{kebab-case}`.
590fn 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/// Declare Factorio locale entries in Rust.
599///
600/// Keys that reference associated constants (e.g. `Settings::CASUAL_MODE`) are
601/// type-checked by rustc. The frontend resolves them to the constant's string
602/// value when assembling `locale/<lang>/*.cfg`.
603///
604/// # Example
605/// ```ignore
606/// factorio_rs::locale! {
607///     file = "settings",
608///
609///     en {
610///         mod_setting_name {
611///             Settings::CASUAL_MODE = "Casual mode",
612///         }
613///         mod_setting_description {
614///             Settings::CASUAL_MODE = "Relax some rules.",
615///         }
616///         "my-mod-messages" {
617///             "hello" = "Hello engineer!",
618///         }
619///     }
620/// }
621/// ```
622#[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}