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(manifest: &Manifest) -> anyhow::Result<TokenStream> {
26    let spells = const_module(
27        "SPELL",
28        manifest
29            .spells
30            .iter()
31            .map(|(name, spell)| (name.as_str(), spell.id)),
32        "spell",
33    )?;
34    let auras = const_module(
35        "AURA",
36        manifest
37            .auras
38            .iter()
39            .map(|(name, aura)| (name.as_str(), aura.id)),
40        "spell",
41    )?;
42    let talents = (!manifest.talents.is_empty())
43        .then(|| {
44            const_module(
45                "TALENT",
46                manifest
47                    .talents
48                    .iter()
49                    .map(|(name, id)| (name.as_str(), *id)),
50                "spell",
51            )
52        })
53        .transpose()?;
54    let set_bonuses = (!manifest.set_bonuses.is_empty())
55        .then(|| {
56            const_module(
57                "SET_BONUS",
58                manifest
59                    .set_bonuses
60                    .iter()
61                    .map(|(name, id)| (name.as_str(), *id)),
62                "spell",
63            )
64        })
65        .transpose()?;
66    let reported_spells = if manifest.reported_spells.is_empty() {
67        TokenStream::new()
68    } else {
69        let entries = manifest
70            .reported_spells
71            .iter()
72            .map(|(name, id)| documented_u32_constant(&name.to_uppercase(), *id, "spell"))
73            .collect::<anyhow::Result<Vec<_>>>()?;
74
75        quote! {
76            pub(crate) mod REPORTED_SPELL {
77                #(#entries)*
78            }
79        }
80    };
81    let hero = hero_constants(manifest)?;
82
83    Ok(quote! {
84        #spells
85        #auras
86        #talents
87        #set_bonuses
88        #reported_spells
89        #hero
90    })
91}
92
93fn const_module<'a>(
94    module: &str,
95    entries: impl Iterator<Item = (&'a str, u32)>,
96    url_path: &str,
97) -> anyhow::Result<TokenStream> {
98    let module = rust_ident(module)?;
99    let entries = entries
100        .map(|(name, id)| documented_u32_constant(name, id, url_path))
101        .collect::<anyhow::Result<Vec<_>>>()?;
102
103    Ok(quote! {
104        pub(crate) mod #module {
105            #(#entries)*
106        }
107    })
108}
109
110fn hero_constants(manifest: &Manifest) -> anyhow::Result<TokenStream> {
111    if manifest.hero_talents.is_empty() {
112        return Ok(TokenStream::new());
113    }
114
115    let trees = manifest
116        .hero_talents
117        .iter()
118        .map(|(tree_name, tree)| {
119            let name = rust_ident(&tree_name.to_uppercase())?;
120            let spells = (!tree.spells.is_empty())
121                .then(|| {
122                    const_module(
123                        "SPELL",
124                        tree.spells.iter().map(|(name, id)| (name.as_str(), *id)),
125                        "spell",
126                    )
127                })
128                .transpose()?;
129            let auras = (!tree.auras.is_empty())
130                .then(|| {
131                    const_module(
132                        "AURA",
133                        tree.auras.iter().map(|(name, id)| (name.as_str(), *id)),
134                        "spell",
135                    )
136                })
137                .transpose()?;
138
139            Ok::<_, anyhow::Error>(quote! {
140                pub(crate) mod #name {
141                    #spells
142                    #auras
143                }
144            })
145        })
146        .collect::<anyhow::Result<Vec<_>>>()?;
147    let statics = hero_talent_statics(manifest);
148
149    Ok(quote! {
150        pub(crate) mod HERO {
151            #(#trees)*
152        }
153
154        #statics
155    })
156}
157
158pub(crate) fn talent_table(manifest: &Manifest) -> anyhow::Result<TokenStream> {
159    let entries = manifest
160        .talents
161        .keys()
162        .map(|name| {
163            let text = lit_str(name);
164            let name = rust_ident(name)?;
165
166            Ok::<_, syn::Error>(quote!((#text, TALENT::#name)))
167        })
168        .collect::<Result<Vec<_>, _>>()?;
169
170    Ok(quote! {
171        static TALENTS: &[(&str, u32)] = &[#(#entries,)*];
172    })
173}
174
175pub(crate) fn local_indices(manifest: &Manifest) -> anyhow::Result<TokenStream> {
176    let auras = manifest
177        .auras
178        .keys()
179        .enumerate()
180        .map(|(index, name)| local_index("AURA", name, "LocalAuraIdx", index))
181        .collect::<anyhow::Result<Vec<_>>>()?;
182    let aura_guard = count_guard("AURA", manifest.auras.len());
183    let spells = manifest
184        .spells
185        .keys()
186        .enumerate()
187        .map(|(index, name)| local_index("SPELL", name, "LocalSpellIdx", index))
188        .collect::<anyhow::Result<Vec<_>>>()?;
189    let spell_guard = count_guard("SPELL", manifest.spells.len());
190
191    Ok(quote! {
192        #(#auras)*
193        #aura_guard
194        #(#spells)*
195        #spell_guard
196    })
197}
198
199fn local_index(
200    prefix: &str,
201    name: &str,
202    index_type: &str,
203    index: usize,
204) -> anyhow::Result<TokenStream> {
205    let name = format_ident!("{prefix}_{name}");
206    let index_type = rust_ident(index_type)?;
207    let index = lit_int(index);
208
209    Ok(quote! {
210        pub(crate) const #name: #index_type = #index_type::new(#index);
211    })
212}
213
214fn count_guard(prefix: &str, count: usize) -> TokenStream {
215    let name = format_ident!("{prefix}_COUNT");
216    let count = lit_int(count);
217    let message = lit_str(&format!(
218        "{prefix} index ABI: count changed -- update positional hook consumers"
219    ));
220
221    quote! {
222        pub(crate) const #name: usize = #count;
223        const _: () = assert!(#name == #count, #message);
224    }
225}
226
227pub(crate) fn metric_keys(manifest: &Manifest) -> anyhow::Result<TokenStream> {
228    let Some(metric_keys) = manifest
229        .metric_keys
230        .as_ref()
231        .filter(|metric_keys| !metric_keys.keys.is_empty())
232    else {
233        return Ok(TokenStream::new());
234    };
235    let module = rust_ident(&metric_keys.module_name)?;
236    let entries = metric_keys
237        .keys
238        .iter()
239        .enumerate()
240        .map(|(index, key)| {
241            let key = rust_ident(key)?;
242            let index = lit_int(index);
243
244            Ok::<_, syn::Error>(quote!(pub(crate) const #key: u32 = #index;))
245        })
246        .collect::<Result<Vec<_>, _>>()?;
247
248    Ok(quote! {
249        pub(crate) mod #module {
250            #(#entries)*
251        }
252    })
253}
254
255pub(crate) fn effects(manifest: &Manifest) -> anyhow::Result<TokenStream> {
256    let effect_module = if manifest.effects.is_empty() {
257        TokenStream::new()
258    } else {
259        let entries = manifest
260            .effects
261            .iter()
262            .map(|(name, effect)| {
263                let documentation = lit_str(&inspect_link("spell", effect.spell_id));
264                let constant = effect_constant(name, effect.spell_id, effect.effect)?;
265
266                Ok::<_, anyhow::Error>(quote! {
267                    #[doc = #documentation]
268                    #constant
269                })
270            })
271            .collect::<anyhow::Result<Vec<_>>>()?;
272
273        quote! {
274            pub(crate) mod EFFECT {
275                #(#entries)*
276            }
277        }
278    };
279    let reported_spells = manifest.reported_spells.iter().map(|(name, id)| {
280        let name = lit_str(&name.to_lowercase());
281        let id = lit_int(id);
282
283        quote!((#name, #id))
284    });
285    let effect_ids = declared_effect_spell_ids(manifest);
286    let mut spell_ids = manifest
287        .spells
288        .values()
289        .map(|spell| spell.id)
290        .chain(manifest.auras.values().map(|aura| aura.id))
291        .chain(manifest.auto_attacks.values().map(|attack| attack.spell_id))
292        .chain(effect_ids)
293        .chain(manifest.reported_spells.values().copied())
294        .collect::<Vec<_>>();
295
296    for tree in manifest.hero_talents.values() {
297        spell_ids.extend(tree.spells.values().copied());
298        spell_ids.extend(tree.auras.values().copied());
299    }
300
301    let mut aura_ids = manifest
302        .auras
303        .values()
304        .map(|aura| aura.id)
305        .collect::<Vec<_>>();
306
307    for tree in manifest.hero_talents.values() {
308        aura_ids.extend(tree.auras.values().copied());
309    }
310
311    let spell_ids = u32_slice("DECLARED_SPELL_IDS", &spell_ids)?;
312    let aura_ids = u32_slice("DECLARED_AURA_IDS", &aura_ids)?;
313
314    Ok(quote! {
315        #effect_module
316
317        static REPORTED_SPELLS: &[(&str, u32)] = &[#(#reported_spells,)*];
318        #spell_ids
319        #aura_ids
320    })
321}
322
323pub(crate) fn declared_effect_spell_ids(manifest: &Manifest) -> Vec<u32> {
324    let mut ids = manifest
325        .effects
326        .values()
327        .map(|effect| effect.spell_id)
328        .collect::<Vec<_>>();
329
330    ids.extend(
331        manifest
332            .talent_gated_aura_effects
333            .iter()
334            .map(|gate| gate.source.spell_id),
335    );
336    ids.push(manifest.mastery.spell_id);
337
338    let damage_ref_id = |damage: &wowlab_manifest_schema::ManifestDamageDef| match damage {
339        wowlab_manifest_schema::ManifestDamageDef::EffectRef { spell_id, .. } => Some(*spell_id),
340        _ => None,
341    };
342    let scalar_ref_id = wowlab_manifest_schema::ScalarRef::effect_spell_id;
343    let event_effect_id = |effect: &wowlab_manifest_schema::EventEffectDef| match effect {
344        wowlab_manifest_schema::EventEffectDef::Energize { amount, .. } => scalar_ref_id(amount),
345        _ => None,
346    };
347
348    ids.extend(
349        manifest
350            .spells
351            .values()
352            .filter_map(|spell| spell.damage.as_ref())
353            .filter_map(damage_ref_id),
354    );
355    ids.extend(
356        manifest
357            .spells
358            .values()
359            .flat_map(wowlab_manifest_schema::ManifestSpellDef::scalar_refs)
360            .filter_map(wowlab_manifest_schema::ScalarRef::effect_spell_id),
361    );
362    ids.extend(manifest.reported_spells.values().copied());
363    ids.extend(
364        manifest
365            .auras
366            .values()
367            .flat_map(wowlab_manifest_schema::ManifestAuraDef::scalar_refs)
368            .filter_map(wowlab_manifest_schema::ScalarRef::effect_spell_id),
369    );
370
371    for proc in &manifest.impact_procs {
372        ids.extend(scalar_ref_id(&proc.chance_pct));
373        ids.extend(proc.effects.iter().filter_map(event_effect_id));
374    }
375
376    for spell in manifest.spells.values() {
377        let Some(channel) = &spell.channel else {
378            continue;
379        };
380
381        ids.extend(channel.tick_damage.as_ref().and_then(damage_ref_id));
382        ids.extend(channel.tick_cost.as_ref().map(|cost| cost.spell_id));
383    }
384
385    ids.sort_unstable();
386    ids.dedup();
387
388    ids
389}
390
391fn u32_slice(name: &str, ids: &[u32]) -> anyhow::Result<TokenStream> {
392    let name = rust_ident(name)?;
393    let ids = ids.iter().map(lit_int);
394
395    Ok(quote! {
396        static #name: &[u32] = &[#(#ids,)*];
397    })
398}
399
400#[cfg(test)]
401#[path = "gen_constants/tests.rs"]
402mod tests;