Skip to main content

codegen/
gen_item_registry.rs

1use proc_macro2::TokenStream;
2use quote::{format_ident, quote};
3use wowlab_manifest_schema::{ItemManifestEntry, ItemsManifest, ManifestAuraDef, ScalarRef};
4
5use crate::{
6    gen_items::is_simple_equip_aura,
7    helpers::{to_display_name, to_snake},
8    rust_source::{FileComment, lit_int, lit_str, render_rust, rust_ident},
9};
10
11pub(crate) fn generate_items_mod_file(manifest: &ItemsManifest) -> anyhow::Result<String> {
12    let all = manifest
13        .items
14        .iter()
15        .map(|(key, item)| (to_snake(key), key.as_str(), item))
16        .collect::<Vec<_>>();
17    let complex = all
18        .iter()
19        .filter(|(_, _, item)| !is_simple_equip_aura(item))
20        .map(|(snake, key, item)| (snake.clone(), *key, *item))
21        .collect::<Vec<_>>();
22    let mut complex_modules = complex
23        .iter()
24        .map(|(module, _, _)| rust_ident(module))
25        .collect::<Result<Vec<_>, _>>()?;
26
27    complex_modules.sort_unstable_by_key(ToString::to_string);
28
29    let simple_items = simple_items_table(&all);
30    let dispatch = items_dispatch_table(&complex)?;
31    let registered = registered_items(&all);
32    let register_equipped = register_equipped_function();
33    let cast_hooks = player_cast_hooks(&all)?;
34    let impact_hooks = player_impact_hooks(&all)?;
35    let lint_level = format_ident!("allow");
36
37    Ok(render_rust(
38        quote! {
39            #![#lint_level(non_snake_case)]
40
41            use wowlab_engine_combat::CombatSystemBuilder;
42            use wowlab_engine_gamedata::ResolvedGameData;
43            use wowlab_engine_ports::ResolveDependencies;
44
45            #(pub(crate) mod #complex_modules;)*
46
47            #simple_items
48            #dispatch
49            #registered
50            #register_equipped
51            #cast_hooks
52            #impact_hooks
53        },
54        &[FileComment::inner_doc(
55            "@generated by codegen-cli from manifests/items.toml -- do not edit by hand.",
56        )?],
57    )?)
58}
59
60fn simple_items_table(items: &[(String, &str, &ItemManifestEntry)]) -> TokenStream {
61    let entries = items
62        .iter()
63        .filter(|(_, _, item)| is_simple_equip_aura(item))
64        .map(|(_, key, item)| {
65            let item_id = lit_int(item.id);
66            let display = lit_str(&to_display_name(key));
67            let aura_ids = item.auras.values().map(|aura| lit_int(aura.id));
68
69            quote!((#item_id, #display, &[#(#aura_ids,)*]))
70        });
71
72    quote! {
73        const SIMPLE_EQUIP_AURA_ITEMS: &[(u32, &str, &[u32])] = &[
74            #(#entries,)*
75        ];
76    }
77}
78
79fn items_dispatch_table(
80    items: &[(String, &str, &ItemManifestEntry)],
81) -> anyhow::Result<TokenStream> {
82    let entries = items
83        .iter()
84        .map(|(module, _, item)| {
85            let item_id = lit_int(item.id);
86            let module = rust_ident(module)?;
87
88            Ok::<_, syn::Error>(quote!((#item_id, #module::register)))
89        })
90        .collect::<Result<Vec<_>, _>>()?;
91
92    Ok(quote! {
93        type ItemRegisterFn = fn(
94            CombatSystemBuilder,
95            &ResolvedGameData,
96            wowlab_engine_gamedata::ItemBudget,
97        ) -> CombatSystemBuilder;
98
99        const ITEMS: &[(u32, ItemRegisterFn)] = &[#(#entries,)*];
100    })
101}
102
103fn aura_scalar_ref_ids(aura: &ManifestAuraDef) -> Vec<u32> {
104    aura.scalar_refs()
105        .filter_map(ScalarRef::effect_spell_id)
106        .collect()
107}
108
109fn registered_items(items: &[(String, &str, &ItemManifestEntry)]) -> TokenStream {
110    let registered = items.iter().map(|(_, key, item)| {
111        let item_id = lit_int(item.id);
112        let key = lit_str(key);
113
114        quote!((#item_id, #key))
115    });
116    let resolve_ids = items.iter().map(|(_, _, item)| {
117        let mut ids = item.auras.values().map(|aura| aura.id).collect::<Vec<_>>();
118
119        ids.extend(item.driver_spell_id);
120        ids.extend(item.effects.values().map(|effect| effect.spell_id));
121
122        for aura in item.auras.values() {
123            ids.extend(aura_scalar_ref_ids(aura));
124        }
125
126        for spell in item.spells.values() {
127            ids.push(spell.id);
128        }
129
130        ids.sort_unstable();
131        ids.dedup();
132
133        let item_id = lit_int(item.id);
134        let ids = ids.into_iter().map(lit_int);
135
136        quote!(ResolveDependencies::new(#item_id, &[#(#ids,)*]))
137    });
138    let use_spells = items.iter().filter_map(|(_, _, item)| {
139        item.spells.get("USE").map(|spell| {
140            let item_id = lit_int(item.id);
141            let spell_id = lit_int(spell.id);
142
143            quote!((#item_id, #spell_id))
144        })
145    });
146    let documentation =
147        lit_str("Generated item spell identifiers and display names in manifest order.");
148
149    quote! {
150        #[doc = #documentation]
151        pub const REGISTERED_ITEMS: &[(u32, &str)] = &[#(#registered,)*];
152
153        pub(crate) const ITEM_RESOLVE_IDS: &[ResolveDependencies] = &[
154            #(#resolve_ids,)*
155        ];
156
157        pub(crate) const ITEM_USE_SPELLS: &[(u32, u32)] = &[#(#use_spells,)*];
158    }
159}
160
161fn register_equipped_function() -> TokenStream {
162    quote! {
163        pub(crate) fn register_equipped(
164            mut builder: CombatSystemBuilder,
165            data: &ResolvedGameData,
166            equipped: &[wowlab_engine_ports::EquippedItem],
167        ) -> Result<CombatSystemBuilder, wowlab_engine_ports::EngineError> {
168            for item in equipped {
169                if let Some(&(_, register)) =
170                    ITEMS.iter().find(|(registered_id, _)| *registered_id == item.item_id)
171                {
172                    builder = register(
173                        builder,
174                        data,
175                        crate::items::item_budget(data, item)?,
176                    );
177                }
178
179                if let Some(&(_, display, aura_ids)) = SIMPLE_EQUIP_AURA_ITEMS
180                    .iter()
181                    .find(|(registered_id, _, _)| *registered_id == item.item_id)
182                {
183                    builder = crate::items::register_simple_equip_auras(
184                        data,
185                        display,
186                        aura_ids,
187                        || builder,
188                    );
189                }
190
191                if let Some(&(_, use_spell)) = ITEM_USE_SPELLS
192                    .iter()
193                    .find(|(registered_id, _)| *registered_id == item.item_id)
194                {
195                    builder = builder.item_use_spell(item.slot, use_spell);
196                }
197            }
198
199            builder = register_player_cast_hooks(builder, equipped);
200            builder = register_player_impact_hooks(builder, data, equipped);
201
202            Ok(builder)
203        }
204    }
205}
206
207fn player_cast_hooks(items: &[(String, &str, &ItemManifestEntry)]) -> anyhow::Result<TokenStream> {
208    let hooks = items
209        .iter()
210        .filter_map(|(module, _, item)| {
211            item.player_cast_hook
212                .as_ref()
213                .map(|hook| (module.as_str(), *item, hook))
214        })
215        .map(|(module, item, hook)| {
216            let item_id = lit_int(item.id);
217            let module = rust_ident(module)?;
218            let hook = rust_ident(hook)?;
219
220            Ok::<_, syn::Error>(quote!((#item_id, crate::hooks::items::#module::#hook)))
221        })
222        .collect::<Result<Vec<_>, _>>()?;
223
224    Ok(if hooks.is_empty() {
225        quote! {
226            fn register_player_cast_hooks(
227                builder: CombatSystemBuilder,
228                _equipped: &[wowlab_engine_ports::EquippedItem],
229            ) -> CombatSystemBuilder {
230                builder
231            }
232        }
233    } else {
234        quote! {
235            const PLAYER_CAST_HOOKS: &[(u32, wowlab_engine_combat::CastHookFn)] = &[
236                #(#hooks,)*
237            ];
238
239            fn register_player_cast_hooks(
240                mut builder: CombatSystemBuilder,
241                equipped: &[wowlab_engine_ports::EquippedItem],
242            ) -> CombatSystemBuilder {
243                for item in equipped {
244                    if let Some(&(_, hook)) = PLAYER_CAST_HOOKS
245                        .iter()
246                        .find(|(registered_id, _)| *registered_id == item.item_id)
247                    {
248                        builder = builder.on_player_cast(hook);
249                    }
250                }
251
252                builder
253            }
254        }
255    })
256}
257
258fn player_impact_hooks(
259    items: &[(String, &str, &ItemManifestEntry)],
260) -> anyhow::Result<TokenStream> {
261    let hooks = items
262        .iter()
263        .filter_map(|(module, _, item)| {
264            item.player_impact_hook
265                .as_ref()
266                .map(|hook| (module.as_str(), *item, hook))
267        })
268        .map(|(module, item, hook)| {
269            let item_id = lit_int(item.id);
270            let driver_spell_id = item.driver_spell_id.map_or_else(
271                || quote!(None),
272                |driver| {
273                    let driver = lit_int(driver);
274
275                    quote!(Some(#driver))
276                },
277            );
278            let module = rust_ident(module)?;
279            let hook_name = rust_ident(&hook.hook)?;
280            let periodic_only = hook.periodic_only;
281            let skip_periodic = hook.skip_periodic;
282            let crit_only = hook.crit_only;
283
284            Ok::<_, syn::Error>(quote! {
285                (
286                    #item_id,
287                    #driver_spell_id,
288                    crate::hooks::items::#module::#hook_name,
289                    #periodic_only,
290                    #skip_periodic,
291                    #crit_only,
292                )
293            })
294        })
295        .collect::<Result<Vec<_>, _>>()?;
296
297    Ok(if hooks.is_empty() {
298        quote! {
299            fn register_player_impact_hooks(
300                builder: CombatSystemBuilder,
301                _data: &ResolvedGameData,
302                _equipped: &[wowlab_engine_ports::EquippedItem],
303            ) -> CombatSystemBuilder {
304                builder
305            }
306        }
307    } else {
308        quote! {
309            type PlayerImpactHookEntry = (
310                u32,
311                Option<u32>,
312                wowlab_engine_combat::ImpactProcFn,
313                bool,
314                bool,
315                bool,
316            );
317
318            const PLAYER_IMPACT_HOOKS: &[PlayerImpactHookEntry] = &[#(#hooks,)*];
319
320            fn register_player_impact_hooks(
321                mut builder: CombatSystemBuilder,
322                data: &ResolvedGameData,
323                equipped: &[wowlab_engine_ports::EquippedItem],
324            ) -> CombatSystemBuilder {
325                for item in equipped {
326                    if let Some(&(
327                        _,
328                        driver_spell_id,
329                        hook,
330                        periodic_only,
331                        skip_periodic,
332                        crit_only,
333                    )) = PLAYER_IMPACT_HOOKS
334                        .iter()
335                        .find(|(registered_id, ..)| *registered_id == item.item_id)
336                    {
337                        builder =
338                            builder.register_impact_proc(wowlab_engine_combat::ImpactProc {
339                                driver: driver_spell_id.map_or_else(
340                                    wowlab_engine_combat::ProcDriver::default,
341                                    |driver| {
342                                        wowlab_engine_combat::ProcDriver::from_spell(
343                                            data,
344                                            driver,
345                                            wowlab_engine_combat::ProcTriggerSide::Caster,
346                                        )
347                                    },
348                                ),
349                                actor_filter:
350                                    wowlab_engine_combat::ImpactActorFilter::Player,
351                                chance: 1.0,
352                                fire: hook,
353                                spell_filter: None,
354                                periodic_only,
355                                skip_periodic,
356                                crit_only,
357                            });
358                    }
359                }
360
361                builder
362            }
363        }
364    })
365}
366
367#[cfg(test)]
368mod tests {
369    use googletest::prelude::*;
370
371    use super::*;
372    use crate::rust_source::contains_source as contains_substring;
373
374    #[gtest]
375    fn player_impact_hook_generates_filtered_impact_proc_registration() -> Result<()> {
376        let manifest: ItemsManifest = toml::from_str(
377            r#"
378                schema_version = 1
379
380                [items.IMPACT_ITEM]
381                id = 42
382                driver_spell_id = 123
383                player_impact_hook = { hook = "on_player_impact", skip_periodic = true }
384            "#,
385        )
386        .or_fail()?;
387        let generated = generate_items_mod_file(&manifest).or_fail()?;
388
389        verify_that!(
390            generated.as_str(),
391            all!(
392                contains_substring("crate::hooks::items::impact_item::on_player_impact"),
393                contains_substring("const PLAYER_IMPACT_HOOKS: &[PlayerImpactHookEntry]"),
394                contains_substring("builder.register_impact_proc(wowlab_engine_combat::ImpactProc"),
395                contains_substring("actor_filter: wowlab_engine_combat::ImpactActorFilter::Player"),
396                contains_substring("wowlab_engine_combat::ProcDriver::from_spell("),
397                contains_substring("wowlab_engine_combat::ProcTriggerSide::Caster"),
398                contains_substring("Result<CombatSystemBuilder, wowlab_engine_ports::EngineError>"),
399                contains_substring("crate::items::item_budget(data, item)?"),
400                contains_substring("Ok(builder)"),
401                contains_substring(
402                    "builder = register_player_impact_hooks(builder, data, equipped);"
403                )
404            )
405        )
406    }
407}