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/// Declares a settings-stage module from a block of items.
224#[proc_macro]
225pub fn settings_mod(input: TokenStream) -> TokenStream {
226    wrap_stage_module("settings", input)
227}
228
229/// Declares a settings-updates-stage module from a block of items.
230#[proc_macro]
231pub fn settings_updates_mod(input: TokenStream) -> TokenStream {
232    wrap_stage_module("settings_updates", input)
233}
234
235/// Declares a settings-final-fixes-stage module from a block of items.
236#[proc_macro]
237pub fn settings_final_fixes_mod(input: TokenStream) -> TokenStream {
238    wrap_stage_module("settings_final_fixes", input)
239}
240
241/// Declares a data/prototype-stage module from a block of items.
242#[proc_macro]
243pub fn data_mod(input: TokenStream) -> TokenStream {
244    wrap_stage_module("data", input)
245}
246
247/// Declares a data-updates-stage module from a block of items.
248#[proc_macro]
249pub fn data_updates_mod(input: TokenStream) -> TokenStream {
250    wrap_stage_module("data_updates", input)
251}
252
253/// Declares a data-final-fixes-stage module from a block of items.
254#[proc_macro]
255pub fn data_final_fixes_mod(input: TokenStream) -> TokenStream {
256    wrap_stage_module("data_final_fixes", input)
257}
258
259/// Declares a control/runtime-stage module from a block of items.
260#[proc_macro]
261pub fn control_mod(input: TokenStream) -> TokenStream {
262    wrap_stage_module("control", input)
263}
264
265/// Declares a shared-stage module from a block of items.
266#[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/// Declare mod settings in a single, concise block.
284///
285/// # Example
286/// ```ignore
287/// use factorio_rs::prelude::*;
288///
289/// factorio_rs::mod_settings! {
290///     prefix = "ms",
291///
292///     startup {
293///         casual_mode: bool = false,
294///         adjacency_enabled: bool = true,
295///     }
296///
297///     runtime_global {
298///         max_entities: i64 = 100,
299///     }
300/// }
301/// ```
302///
303/// Access in control stage:
304/// ```ignore
305/// let enabled = settings.startup.get::<bool>(Settings::CASUAL_MODE);
306/// ```
307#[proc_macro]
308pub fn mod_settings(input: TokenStream) -> TokenStream {
309    let ModSettingsInput { prefix, groups } = parse_macro_input!(input as ModSettingsInput);
310
311    // Collect all settings in order.
312    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            // `pub const CASUAL_MODE: &'static str = "ms-casual-mode";`
328            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        // &'static str or &str
510        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
523/// Convert a `snake_case` ident to a `proc_macro2::Ident`.
524fn 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
529/// Build the Lua setting name: `{prefix}-{kebab-case}` or just `{kebab-case}`.
530fn 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/// Declare Factorio locale entries in Rust.
539///
540/// Keys that reference associated constants (e.g. `Settings::CASUAL_MODE`) are
541/// type-checked by rustc. The frontend resolves them to the constant's string
542/// value when assembling `locale/<lang>/*.cfg`.
543///
544/// # Example
545/// ```ignore
546/// factorio_rs::locale! {
547///     file = "settings",
548///
549///     en {
550///         mod_setting_name {
551///             Settings::CASUAL_MODE = "Casual mode",
552///         }
553///         mod_setting_description {
554///             Settings::CASUAL_MODE = "Relax some rules.",
555///         }
556///         "my-mod-messages" {
557///             "hello" = "Hello engineer!",
558///         }
559///     }
560/// }
561/// ```
562#[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(value, "locale values must be a single line")
579                        .to_compile_error()
580                        .into();
581                }
582            }
583        }
584    }
585
586    TokenStream::from(quote::quote! {
587        const _: () = {
588            #( #checks )*
589        };
590    })
591}
592
593struct LocaleInput {
594    #[allow(dead_code)]
595    file: Option<String>,
596    languages: Vec<LocaleLanguageBlock>,
597}
598
599struct LocaleLanguageBlock {
600    #[allow(dead_code)]
601    lang: String,
602    categories: Vec<LocaleCategoryBlock>,
603}
604
605struct LocaleCategoryBlock {
606    #[allow(dead_code)]
607    name: String,
608    entries: Vec<LocaleEntry>,
609}
610
611struct LocaleEntry {
612    key: LocaleKey,
613    value: LitStr,
614}
615
616enum LocaleKey {
617    Path(Path),
618    #[allow(dead_code)]
619    Literal(String),
620}
621
622impl Parse for LocaleInput {
623    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
624        let mut file = None;
625        if input.peek(syn::Ident) {
626            let fork = input.fork();
627            let kw: syn::Ident = fork.parse()?;
628            if kw == "file" && fork.peek(Token![=]) {
629                let _: syn::Ident = input.parse()?;
630                let _: Token![=] = input.parse()?;
631                let lit: LitStr = input.parse()?;
632                file = Some(lit.value());
633                let _: Option<Token![,]> = input.parse()?;
634            }
635        }
636
637        let mut languages = Vec::new();
638        while !input.is_empty() {
639            languages.push(input.parse()?);
640        }
641
642        if languages.is_empty() {
643            return Err(input.error("expected at least one language block such as `en { ... }`"));
644        }
645
646        Ok(Self { file, languages })
647    }
648}
649
650impl Parse for LocaleLanguageBlock {
651    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
652        let lang = if input.peek(LitStr) {
653            input.parse::<LitStr>()?.value()
654        } else {
655            input.parse::<syn::Ident>()?.to_string()
656        };
657
658        let content;
659        syn::braced!(content in input);
660
661        let mut categories = Vec::new();
662        while !content.is_empty() {
663            categories.push(content.parse()?);
664        }
665
666        Ok(Self { lang, categories })
667    }
668}
669
670impl Parse for LocaleCategoryBlock {
671    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
672        let name = if input.peek(LitStr) {
673            input.parse::<LitStr>()?.value()
674        } else {
675            let ident: syn::Ident = input.parse()?;
676            ident.to_string().replace('_', "-")
677        };
678
679        let content;
680        syn::braced!(content in input);
681
682        let mut entries = Vec::new();
683        while !content.is_empty() {
684            entries.push(content.parse()?);
685        }
686
687        Ok(Self { name, entries })
688    }
689}
690
691impl Parse for LocaleEntry {
692    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
693        let key = if input.peek(LitStr) {
694            LocaleKey::Literal(input.parse::<LitStr>()?.value())
695        } else {
696            LocaleKey::Path(input.parse()?)
697        };
698        let _: Token![=] = input.parse()?;
699        let value: LitStr = input.parse()?;
700        let _: Option<Token![,]> = input.parse()?;
701        Ok(Self { key, value })
702    }
703}