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;