Skip to main content

codegen/
gen_constants.rs

1use anyhow::Context as _;
2use proc_macro2::TokenStream;
3use quote::{format_ident, quote};
4use wowlab_manifest_schema::Manifest;
5
6use crate::{
7    gen_descriptor::hero_talent_statics,
8    rust_source::{documented_u32_constant, inspect_link, lit_int, lit_str, rust_ident},
9};
10
11pub(crate) fn effect_constant(
12    name: &str,
13    spell_id: u32,
14    effect: u8,
15) -> anyhow::Result<TokenStream> {
16    let name = rust_ident(name).context("effect name is not a Rust identifier")?;
17    let spell_id = lit_int(spell_id);
18    let effect = lit_int(effect);
19
20    Ok(quote! {
21        pub(crate) const #name: (u32, u8) = (#spell_id, #effect);
22    })
23}
24
25pub(crate) fn constants(
26    manifest: &Manifest,
27    damage_namespace: &TokenStream,
28) -> anyhow::Result<TokenStream> {
29    let spells = const_module(
30        "SPELL",
31        manifest
32            .spells
33            .iter()
34            .map(|(name, spell)| (name.as_str(), spell.id)),
35        "spell",
36    )?;
37    let auras = const_module(
38        "AURA",
39        manifest
40            .auras
41            .iter()
42            .map(|(name, aura)| (name.as_str(), aura.id)),
43        "spell",
44    )?;
45    let talents = (!manifest.talents.is_empty())
46        .then(|| {
47            const_module(
48                "TALENT",
49                manifest
50                    .talents
51                    .iter()
52                    .map(|(name, id)| (name.as_str(), *id)),
53                "spell",
54            )
55        })
56        .transpose()?;
57    let set_bonuses = (!manifest.set_bonuses.is_empty())
58        .then(|| {
59            const_module(
60                "SET_BONUS",
61                manifest
62                    .set_bonuses
63                    .iter()
64                    .map(|(name, id)| (name.as_str(), *id)),
65                "spell",
66            )
67        })
68        .transpose()?;
69    let reported_spells = if manifest.reported_spells.is_empty() {
70        TokenStream::new()
71    } else {
72        let entries = manifest
73            .reported_spells
74            .iter()
75            .map(|(name, id)| documented_u32_constant(&name.to_uppercase(), *id, "spell"))
76            .collect::<anyhow::Result<Vec<_>>>()?;
77
78        quote! {
79            pub(crate) mod REPORTED_SPELL {
80                #(#entries)*
81            }
82        }
83    };
84    let hero = hero_constants(manifest)?;
85    let damage = damage_constants(manifest, damage_namespace)?;
86
87    Ok(quote! {
88        #spells
89        #auras
90        #talents
91        #set_bonuses
92        #reported_spells
93        #hero
94        #damage
95    })
96}
97
98pub(crate) fn damage_action_expr(namespace: &TokenStream, spell_id: u32) -> TokenStream {
99    let action = format_ident!("SPELL_{spell_id}");
100
101    quote!(#namespace::ACTION::#action)
102}
103
104pub(crate) fn damage_effect_expr(
105    namespace: &TokenStream,
106    spell_id: u32,
107    effect_index: u8,
108) -> TokenStream {
109    let effect = format_ident!("SPELL_{spell_id}_EFFECT_{effect_index}");
110
111    quote!(#namespace::EFFECT::#effect)
112}
113
114fn damage_constants(
115    manifest: &Manifest,
116    damage_namespace: &TokenStream,
117) -> anyhow::Result<TokenStream> {
118    let action = |name: &str, spell_id: u32| {
119        let name = rust_ident(name)?;
120        let action = damage_action_expr(damage_namespace, spell_id);
121
122        Ok::<_, anyhow::Error>(quote! {
123            pub(crate) const #name: wowlab_engine_combat::DamageActionId =
124                #action;
125        })
126    };
127    let mut spell_coordinates = manifest
128        .spells
129        .iter()
130        .map(|(name, spell)| (name.as_str(), spell.id))
131        .collect::<Vec<_>>();
132
133    for tree in manifest.hero_talents.values() {
134        for (name, spell_id) in &tree.spells {
135            if !spell_coordinates
136                .iter()
137                .any(|(candidate, _)| *candidate == name)
138            {
139                spell_coordinates.push((name.as_str(), *spell_id));
140            }
141        }
142    }
143
144    let spell_entries = spell_coordinates
145        .into_iter()
146        .map(|(name, spell_id)| action(name, spell_id))
147        .collect::<anyhow::Result<Vec<_>>>()?;
148    let aura_entries = manifest
149        .auras
150        .iter()
151        .map(|(name, aura)| action(name, aura.id))
152        .collect::<anyhow::Result<Vec<_>>>()?;
153    let talent_entries = manifest
154        .talents
155        .iter()
156        .map(|(name, spell_id)| action(name, *spell_id))
157        .collect::<anyhow::Result<Vec<_>>>()?;
158    let reported_entries = manifest
159        .reported_spells
160        .iter()
161        .map(|(name, spell_id)| action(&name.to_uppercase(), *spell_id))
162        .collect::<anyhow::Result<Vec<_>>>()?;
163    let effect_entries = manifest
164        .effects
165        .iter()
166        .map(|(name, effect)| {
167            let name = rust_ident(name)?;
168            let effect = damage_effect_expr(damage_namespace, effect.spell_id, effect.effect);
169
170            Ok::<_, anyhow::Error>(quote! {
171                pub(crate) const #name: wowlab_engine_combat::DamageEffectId =
172                    #effect;
173            })
174        })
175        .collect::<anyhow::Result<Vec<_>>>()?;
176
177    Ok(quote! {
178        pub(crate) mod DAMAGE_SPELL {
179            #(#spell_entries)*
180        }
181        pub(crate) mod DAMAGE_AURA {
182            #(#aura_entries)*
183        }
184        pub(crate) mod DAMAGE_TALENT {
185            #(#talent_entries)*
186        }
187        pub(crate) mod DAMAGE_REPORTED {
188            #(#reported_entries)*
189        }
190        pub(crate) mod DAMAGE_EFFECT {
191            #(#effect_entries)*
192        }
193    })
194}
195
196fn const_module<'a>(
197    module: &str,
198    entries: impl Iterator<Item = (&'a str, u32)>,
199    url_path: &str,
200) -> anyhow::Result<TokenStream> {
201    let module = rust_ident(module)?;
202    let entries = entries
203        .map(|(name, id)| documented_u32_constant(name, id, url_path))
204        .collect::<anyhow::Result<Vec<_>>>()?;
205
206    Ok(quote! {
207        pub(crate) mod #module {
208            #(#entries)*
209        }
210    })
211}
212
213fn hero_constants(manifest: &Manifest) -> anyhow::Result<TokenStream> {
214    if manifest.hero_talents.is_empty() {
215        return Ok(TokenStream::new());
216    }
217
218    let trees = manifest
219        .hero_talents
220        .iter()
221        .map(|(tree_name, tree)| {
222            let name = rust_ident(&tree_name.to_uppercase())?;
223            let spells = (!tree.spells.is_empty())
224                .then(|| {
225                    const_module(
226                        "SPELL",
227                        tree.spells.iter().map(|(name, id)| (name.as_str(), *id)),
228                        "spell",
229                    )
230                })
231                .transpose()?;
232            let auras = (!tree.auras.is_empty())
233                .then(|| {
234                    const_module(
235                        "AURA",
236                        tree.auras.iter().map(|(name, id)| (name.as_str(), *id)),
237                        "spell",
238                    )
239                })
240                .transpose()?;
241
242            Ok::<_, anyhow::Error>(quote! {
243                pub(crate) mod #name {
244                    #spells
245                    #auras
246                }
247            })
248        })
249        .collect::<anyhow::Result<Vec<_>>>()?;
250    let statics = hero_talent_statics(manifest);
251
252    Ok(quote! {
253        pub(crate) mod HERO {
254            #(#trees)*
255        }
256
257        #statics
258    })
259}
260
261pub(crate) fn talent_table(manifest: &Manifest) -> anyhow::Result<TokenStream> {
262    let entries = manifest
263        .talents
264        .keys()
265        .map(|name| {
266            let text = lit_str(name);
267            let name = rust_ident(name)?;
268
269            Ok::<_, syn::Error>(quote!((#text, TALENT::#name)))
270        })
271        .collect::<Result<Vec<_>, _>>()?;
272
273    Ok(quote! {
274        static TALENTS: &[(&str, u32)] = &[#(#entries,)*];
275    })
276}
277
278pub(crate) fn local_indices(manifest: &Manifest, module_name: &str) -> anyhow::Result<TokenStream> {
279    let module_name = rust_ident(module_name)?;
280    let auras = manifest
281        .auras
282        .keys()
283        .map(|name| local_index("AURA", name, "LocalAuraIdx", &module_name))
284        .collect::<anyhow::Result<Vec<_>>>()?;
285    let aura_guard = count_guard("AURA", manifest.auras.len());
286    let spells = manifest
287        .spells
288        .keys()
289        .map(|name| local_index("SPELL", name, "LocalSpellIdx", &module_name))
290        .collect::<anyhow::Result<Vec<_>>>()?;
291    let spell_guard = count_guard("SPELL", manifest.spells.len());
292
293    Ok(quote! {
294        #(#auras)*
295        #aura_guard
296        #(#spells)*
297        #spell_guard
298    })
299}
300
301fn local_index(
302    prefix: &str,
303    name: &str,
304    index_type: &str,
305    module_name: &syn::Ident,
306) -> anyhow::Result<TokenStream> {
307    let name = format_ident!("{prefix}_{name}");
308    let index_type = rust_ident(index_type)?;
309
310    Ok(quote! {
311        pub(crate) const #name: #index_type =
312            wowlab_engine_combat::generated_identities::specs::#module_name::#name.bind(
313                wowlab_engine_combat::generated_identities::specs::#module_name::PROOF
314            );
315    })
316}
317
318fn count_guard(prefix: &str, count: usize) -> TokenStream {
319    let name = format_ident!("{prefix}_COUNT");
320    let count = lit_int(count);
321    let message = lit_str(&format!(
322        "{prefix} index ABI: count changed -- update positional hook consumers"
323    ));
324
325    quote! {
326        pub(crate) const #name: usize = #count;
327        const _: () = assert!(#name == #count, #message);
328    }
329}
330
331pub(crate) fn metric_keys(manifest: &Manifest) -> anyhow::Result<TokenStream> {
332    let Some(metric_keys) = manifest
333        .metric_keys
334        .as_ref()
335        .filter(|metric_keys| !metric_keys.keys.is_empty())
336    else {
337        return Ok(TokenStream::new());
338    };
339    let module = rust_ident(&metric_keys.module_name)?;
340    let entries = metric_keys
341        .keys
342        .iter()
343        .enumerate()
344        .map(|(index, key)| {
345            let key = rust_ident(key)?;
346            let index = lit_int(index);
347
348            Ok::<_, syn::Error>(quote!(pub(crate) const #key: u32 = #index;))
349        })
350        .collect::<Result<Vec<_>, _>>()?;
351
352    Ok(quote! {
353        pub(crate) mod #module {
354            #(#entries)*
355        }
356    })
357}
358
359pub(crate) fn effects(manifest: &Manifest) -> anyhow::Result<TokenStream> {
360    let effect_module = if manifest.effects.is_empty() {
361        TokenStream::new()
362    } else {
363        let entries = manifest
364            .effects
365            .iter()
366            .map(|(name, effect)| {
367                let documentation = lit_str(&inspect_link("spell", effect.spell_id));
368                let constant = effect_constant(name, effect.spell_id, effect.effect)?;
369
370                Ok::<_, anyhow::Error>(quote! {
371                    #[doc = #documentation]
372                    #constant
373                })
374            })
375            .collect::<anyhow::Result<Vec<_>>>()?;
376
377        quote! {
378            pub(crate) mod EFFECT {
379                #(#entries)*
380            }
381        }
382    };
383    let reported_spells = manifest.reported_spells.iter().map(|(name, id)| {
384        let name = lit_str(&name.to_lowercase());
385        let id = lit_int(id);
386
387        quote!((#name, #id))
388    });
389    let spell_ids = canonical_damage_spell_ids(manifest);
390
391    let mut aura_ids = manifest
392        .auras
393        .values()
394        .map(|aura| aura.id)
395        .collect::<Vec<_>>();
396
397    for tree in manifest.hero_talents.values() {
398        aura_ids.extend(tree.auras.values().copied());
399    }
400
401    let spell_ids = u32_slice("DECLARED_SPELL_IDS", &spell_ids)?;
402    let aura_ids = u32_slice("DECLARED_AURA_IDS", &aura_ids)?;
403
404    Ok(quote! {
405        #effect_module
406
407        static REPORTED_SPELLS: &[(&str, u32)] = &[#(#reported_spells,)*];
408        #spell_ids
409        #aura_ids
410    })
411}
412
413pub(crate) fn canonical_damage_spell_ids(manifest: &Manifest) -> Vec<u32> {
414    let effect_ids = declared_effect_spell_ids(manifest);
415    let mut ids = manifest
416        .spells
417        .values()
418        .map(|spell| spell.id)
419        .chain(manifest.auras.values().map(|aura| aura.id))
420        .chain(manifest.talents.values().copied())
421        .chain(manifest.auto_attacks.values().map(|attack| attack.spell_id))
422        .chain(effect_ids)
423        .chain(manifest.reported_spells.values().copied())
424        .collect::<Vec<_>>();
425
426    for tree in manifest.hero_talents.values() {
427        ids.extend(tree.spells.values().copied());
428        ids.extend(tree.auras.values().copied());
429    }
430
431    let mut unique = Vec::with_capacity(ids.len());
432
433    for id in ids {
434        if id != 0 && !unique.contains(&id) {
435            unique.push(id);
436        }
437    }
438
439    unique
440}
441
442pub(crate) fn declared_effect_spell_ids(manifest: &Manifest) -> Vec<u32> {
443    let mut ids = manifest
444        .effects
445        .values()
446        .map(|effect| effect.spell_id)
447        .collect::<Vec<_>>();
448
449    ids.extend(
450        manifest
451            .talent_gated_aura_effects
452            .iter()
453            .map(|gate| gate.source.spell_id),
454    );
455    ids.push(manifest.mastery.spell_id);
456
457    let damage_ref_id = |damage: &wowlab_manifest_schema::ManifestDamageDef| match damage {
458        wowlab_manifest_schema::ManifestDamageDef::EffectRef { spell_id, .. } => Some(*spell_id),
459        _ => None,
460    };
461    let scalar_ref_id = wowlab_manifest_schema::ScalarRef::effect_spell_id;
462    let event_effect_id = |effect: &wowlab_manifest_schema::EventEffectDef| match effect {
463        wowlab_manifest_schema::EventEffectDef::Energize { amount, .. } => scalar_ref_id(amount),
464        _ => None,
465    };
466
467    ids.extend(
468        manifest
469            .spells
470            .values()
471            .filter_map(|spell| spell.damage.as_ref())
472            .filter_map(damage_ref_id),
473    );
474    ids.extend(
475        manifest
476            .spells
477            .values()
478            .flat_map(wowlab_manifest_schema::ManifestSpellDef::scalar_refs)
479            .filter_map(wowlab_manifest_schema::ScalarRef::effect_spell_id),
480    );
481    ids.extend(manifest.reported_spells.values().copied());
482    ids.extend(
483        manifest
484            .auras
485            .values()
486            .flat_map(wowlab_manifest_schema::ManifestAuraDef::scalar_refs)
487            .filter_map(wowlab_manifest_schema::ScalarRef::effect_spell_id),
488    );
489
490    for proc in &manifest.impact_procs {
491        ids.extend(scalar_ref_id(&proc.chance_pct));
492        ids.extend(proc.effects.iter().filter_map(event_effect_id));
493    }
494
495    for spell in manifest.spells.values() {
496        let Some(channel) = &spell.channel else {
497            continue;
498        };
499
500        ids.extend(channel.tick_damage.as_ref().and_then(damage_ref_id));
501        ids.extend(channel.tick_cost.as_ref().map(|cost| cost.spell_id));
502    }
503
504    ids.sort_unstable();
505    ids.dedup();
506
507    ids
508}
509
510fn u32_slice(name: &str, ids: &[u32]) -> anyhow::Result<TokenStream> {
511    let name = rust_ident(name)?;
512    let ids = ids.iter().map(lit_int);
513
514    Ok(quote! {
515        static #name: &[u32] = &[#(#ids,)*];
516    })
517}
518
519#[cfg(test)]
520#[path = "gen_constants/tests.rs"]
521mod tests;