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;