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 damage_identities = items.iter().map(|(module, _, item)| {
147        let module = format_ident!("{module}");
148        let item_id = lit_int(item.id);
149
150        quote! {
151            (#item_id, wowlab_engine_combat::generated_identities::items::#module::damage::ALL)
152        }
153    });
154    let documentation =
155        lit_str("Generated item spell identifiers and display names in manifest order.");
156
157    quote! {
158        #[doc = #documentation]
159        pub const REGISTERED_ITEMS: &[(u32, &str)] = &[#(#registered,)*];
160
161        pub(crate) const ITEM_RESOLVE_IDS: &[ResolveDependencies] = &[
162            #(#resolve_ids,)*
163        ];
164
165        pub(crate) const ITEM_USE_SPELLS: &[(u32, u32)] = &[#(#use_spells,)*];
166        pub(crate) const ITEM_DAMAGE_IDENTITIES: &[(u32, &[wowlab_engine_combat::DamageActionId])] =
167            &[#(#damage_identities,)*];
168    }
169}
170
171fn register_equipped_function() -> TokenStream {
172    quote! {
173        pub(crate) fn register_equipped(
174            mut builder: CombatSystemBuilder,
175            data: &ResolvedGameData,
176            equipped: &[wowlab_engine_ports::EquippedItem],
177        ) -> Result<CombatSystemBuilder, wowlab_engine_ports::EngineError> {
178            for item in equipped {
179                if let Some((_, identities)) = ITEM_DAMAGE_IDENTITIES
180                    .iter()
181                    .find(|(item_id, _)| *item_id == item.item_id)
182                {
183                    builder = builder.effect_identities(identities);
184                }
185
186                if let Some(&(_, register)) =
187                    ITEMS.iter().find(|(registered_id, _)| *registered_id == item.item_id)
188                {
189                    builder = register(
190                        builder,
191                        data,
192                        crate::items::item_budget(data, item)?,
193                    );
194                }
195
196                if let Some(&(_, display, aura_ids)) = SIMPLE_EQUIP_AURA_ITEMS
197                    .iter()
198                    .find(|(registered_id, _, _)| *registered_id == item.item_id)
199                {
200                    builder = crate::items::register_simple_equip_auras(
201                        data,
202                        display,
203                        aura_ids,
204                        || builder,
205                    );
206                }
207
208                if let Some(&(_, use_spell)) = ITEM_USE_SPELLS
209                    .iter()
210                    .find(|(registered_id, _)| *registered_id == item.item_id)
211                {
212                    builder = builder.item_use_spell(item.slot, use_spell);
213                }
214            }
215
216            builder = register_player_cast_hooks(builder, equipped);
217            builder = register_player_impact_hooks(builder, data, equipped);
218
219            Ok(builder)
220        }
221    }
222}
223
224fn player_cast_hooks(items: &[(String, &str, &ItemManifestEntry)]) -> anyhow::Result<TokenStream> {
225    let hooks = items
226        .iter()
227        .filter_map(|(module, _, item)| {
228            item.player_cast_hook
229                .as_ref()
230                .map(|hook| (module.as_str(), *item, hook))
231        })
232        .map(|(module, item, hook)| {
233            let item_id = lit_int(item.id);
234            let module = rust_ident(module)?;
235            let hook = rust_ident(hook)?;
236
237            Ok::<_, syn::Error>(quote!((#item_id, crate::hooks::items::#module::#hook)))
238        })
239        .collect::<Result<Vec<_>, _>>()?;
240
241    Ok(if hooks.is_empty() {
242        quote! {
243            fn register_player_cast_hooks(
244                builder: CombatSystemBuilder,
245                _equipped: &[wowlab_engine_ports::EquippedItem],
246            ) -> CombatSystemBuilder {
247                builder
248            }
249        }
250    } else {
251        quote! {
252            const PLAYER_CAST_HOOKS: &[(u32, wowlab_engine_combat::CastHookFn)] = &[
253                #(#hooks,)*
254            ];
255
256            fn register_player_cast_hooks(
257                mut builder: CombatSystemBuilder,
258                equipped: &[wowlab_engine_ports::EquippedItem],
259            ) -> CombatSystemBuilder {
260                for item in equipped {
261                    if let Some(&(_, hook)) = PLAYER_CAST_HOOKS
262                        .iter()
263                        .find(|(registered_id, _)| *registered_id == item.item_id)
264                    {
265                        builder = builder.on_player_cast(hook);
266                    }
267                }
268
269                builder
270            }
271        }
272    })
273}
274
275fn player_impact_hooks(
276    items: &[(String, &str, &ItemManifestEntry)],
277) -> anyhow::Result<TokenStream> {
278    let hooks = items
279        .iter()
280        .filter_map(|(module, _, item)| {
281            item.player_impact_hook
282                .as_ref()
283                .map(|hook| (module.as_str(), *item, hook))
284        })
285        .map(|(module, item, hook)| {
286            let item_id = lit_int(item.id);
287            let driver_spell_id = item.driver_spell_id.map_or_else(
288                || quote!(None),
289                |driver| {
290                    let driver = lit_int(driver);
291
292                    quote!(Some(#driver))
293                },
294            );
295            let module = rust_ident(module)?;
296            let hook_name = rust_ident(&hook.hook)?;
297            let periodic_only = hook.periodic_only;
298            let skip_periodic = hook.skip_periodic;
299            let crit_only = hook.crit_only;
300
301            Ok::<_, syn::Error>(quote! {
302                (
303                    #item_id,
304                    #driver_spell_id,
305                    crate::hooks::items::#module::#hook_name,
306                    #periodic_only,
307                    #skip_periodic,
308                    #crit_only,
309                )
310            })
311        })
312        .collect::<Result<Vec<_>, _>>()?;
313
314    Ok(if hooks.is_empty() {
315        quote! {
316            fn register_player_impact_hooks(
317                builder: CombatSystemBuilder,
318                _data: &ResolvedGameData,
319                _equipped: &[wowlab_engine_ports::EquippedItem],
320            ) -> CombatSystemBuilder {
321                builder
322            }
323        }
324    } else {
325        quote! {
326            type PlayerImpactHookEntry = (
327                u32,
328                Option<u32>,
329                wowlab_engine_combat::ImpactProcFn,
330                bool,
331                bool,
332                bool,
333            );
334
335            const PLAYER_IMPACT_HOOKS: &[PlayerImpactHookEntry] = &[#(#hooks,)*];
336
337            fn register_player_impact_hooks(
338                mut builder: CombatSystemBuilder,
339                data: &ResolvedGameData,
340                equipped: &[wowlab_engine_ports::EquippedItem],
341            ) -> CombatSystemBuilder {
342                for item in equipped {
343                    if let Some(&(
344                        _,
345                        driver_spell_id,
346                        hook,
347                        periodic_only,
348                        skip_periodic,
349                        crit_only,
350                    )) = PLAYER_IMPACT_HOOKS
351                        .iter()
352                        .find(|(registered_id, ..)| *registered_id == item.item_id)
353                    {
354                        builder =
355                            builder.register_impact_proc(wowlab_engine_combat::ImpactProc {
356                                driver: driver_spell_id.map_or_else(
357                                    wowlab_engine_combat::ProcDriver::default,
358                                    |driver| {
359                                        wowlab_engine_combat::ProcDriver::from_spell(
360                                            data,
361                                            driver,
362                                            wowlab_engine_combat::ProcTriggerSide::Caster,
363                                        )
364                                    },
365                                ),
366                                actor_filter:
367                                    wowlab_engine_combat::ImpactActorFilter::Player,
368                                chance: 1.0,
369                                fire: hook,
370                                spell_filter: None,
371                                periodic_only,
372                                skip_periodic,
373                                crit_only,
374                            });
375                    }
376                }
377
378                builder
379            }
380        }
381    })
382}
383
384#[cfg(test)]
385mod tests {
386    use googletest::prelude::*;
387
388    use super::*;
389    use crate::rust_source::contains_source as contains_substring;
390
391    #[gtest]
392    fn player_impact_hook_generates_filtered_impact_proc_registration() -> Result<()> {
393        let manifest: ItemsManifest = toml::from_str(
394            r#"
395                schema_version = 1
396
397                [items.IMPACT_ITEM]
398                id = 42
399                driver_spell_id = 123
400                player_impact_hook = { hook = "on_player_impact", skip_periodic = true }
401            "#,
402        )
403        .or_fail()?;
404        let generated = generate_items_mod_file(&manifest).or_fail()?;
405
406        verify_that!(
407            generated.as_str(),
408            all!(
409                contains_substring("crate::hooks::items::impact_item::on_player_impact"),
410                contains_substring("const PLAYER_IMPACT_HOOKS: &[PlayerImpactHookEntry]"),
411                contains_substring("builder.register_impact_proc(wowlab_engine_combat::ImpactProc"),
412                contains_substring("actor_filter: wowlab_engine_combat::ImpactActorFilter::Player"),
413                contains_substring("wowlab_engine_combat::ProcDriver::from_spell("),
414                contains_substring("wowlab_engine_combat::ProcTriggerSide::Caster"),
415                contains_substring("Result<CombatSystemBuilder, wowlab_engine_ports::EngineError>"),
416                contains_substring("crate::items::item_budget(data, item)?"),
417                contains_substring("Ok(builder)"),
418                contains_substring(
419                    "builder = register_player_impact_hooks(builder, data, equipped);"
420                )
421            )
422        )
423    }
424}