1use anyhow::Context as _;
2use proc_macro2::TokenStream;
3use quote::{format_ident, quote};
4use wowlab_manifest_schema::{ItemsManifest, Manifest, ScalarRef, SharedBindings};
5
6use crate::{
7 gen_constants::canonical_damage_spell_ids,
8 helpers::to_snake,
9 rust_source::{FileComment, lit_int, render_rust, rust_ident},
10};
11
12const GENERATED_BANNER: &str = "@generated by codegen-cli -- do not edit by hand.";
13
14fn damage_identity_module(
15 brand: u32,
16 mut action_ids: Vec<u32>,
17 mut effects: Vec<(u32, u8)>,
18) -> TokenStream {
19 action_ids.retain(|spell_id| *spell_id != 0);
20 action_ids.sort_unstable();
21 action_ids.dedup();
22 effects.retain(|(spell_id, _)| *spell_id != 0);
23 effects.sort_unstable();
24 effects.dedup();
25 let brand = lit_int(brand);
26 let actions = action_ids.iter().copied().map(|spell_id| {
27 let name = format_ident!("SPELL_{spell_id}");
28 let spell_id = lit_int(spell_id);
29
30 quote!(pub const #name: crate::DamageActionId = crate::DamageActionId::new(#brand, #spell_id);)
31 });
32 let action_values = action_ids.iter().copied().map(|spell_id| {
33 let name = format_ident!("SPELL_{spell_id}");
34
35 quote!(ACTION::#name)
36 });
37 let effect_values = effects.into_iter().map(|(spell_id, effect_index)| {
38 let name = format_ident!("SPELL_{spell_id}_EFFECT_{effect_index}");
39 let action = format_ident!("SPELL_{spell_id}");
40 let effect_index = lit_int(effect_index);
41
42 quote! {
43 pub const #name: crate::DamageEffectId =
44 crate::DamageEffectId::new(super::ACTION::#action, #effect_index);
45 }
46 });
47
48 quote! {
49 pub mod damage {
50 pub const ALL: &[crate::DamageActionId] = &[#(#action_values,)*];
51
52 pub mod ACTION {
53 #(#actions)*
54 }
55
56 pub mod EFFECT {
57 #(#effect_values)*
58 }
59 }
60 }
61}
62
63pub(crate) fn generate_identity_catalog(
65 specs: &[(String, Manifest)],
66 items: &ItemsManifest,
67 shared: &[(String, SharedBindings)],
68) -> anyhow::Result<String> {
69 let modules = specs
70 .iter()
71 .enumerate()
72 .map(|(brand, (module_name, manifest))| {
73 let module_name = rust_ident(module_name)?;
74 let brand_value = u32::try_from(brand + 1)?;
75 let brand = lit_int(brand_value);
76 let spec_id = lit_int(manifest.spec.id);
77 let auras = manifest
78 .auras
79 .keys()
80 .enumerate()
81 .map(|(index, name)| {
82 let name = format_ident!("AURA_{name}");
83 let index = lit_int(index);
84
85 quote!(pub const #name: SpecAuraBinding = SpecAuraBinding::new(#brand, #index);)
86 })
87 .collect::<Vec<_>>();
88 let aura_values = manifest.auras.keys().map(|name| {
89 let name = format_ident!("AURA_{name}");
90
91 quote!(#name)
92 });
93 let spells = manifest
94 .spells
95 .keys()
96 .enumerate()
97 .map(|(index, name)| {
98 let name = format_ident!("SPELL_{name}");
99 let index = lit_int(index);
100
101 quote!(pub const #name: SpecSpellBinding = SpecSpellBinding::new(#brand, #index);)
102 })
103 .collect::<Vec<_>>();
104 let spell_values = manifest.spells.keys().map(|name| {
105 let name = format_ident!("SPELL_{name}");
106
107 quote!(#name)
108 });
109 let damage = damage_identity_module(
110 brand_value,
111 canonical_damage_spell_ids(manifest),
112 manifest
113 .effects
114 .values()
115 .map(|effect| (effect.spell_id, effect.effect))
116 .collect(),
117 );
118
119 Ok::<_, anyhow::Error>(quote! {
120 pub mod #module_name {
121 use super::{SpecAuraBinding, SpecIdentityProof, SpecSpellBinding};
122 pub const PROOF: SpecIdentityProof = SpecIdentityProof::new(#brand, #spec_id);
123 #(#auras)*
124 #(#spells)*
125 #damage
126
127 #[cfg(test)]
128 #[googletest::gtest]
129 fn every_generated_binding_matches_its_proof() {
130 for binding in [#(#aura_values,)*] {
131 assert!(PROOF.owns_aura(binding));
132 }
133 for binding in [#(#spell_values,)*] {
134 assert!(PROOF.owns_spell(binding));
135 }
136 }
137 }
138 })
139 })
140 .collect::<anyhow::Result<Vec<_>>>()?;
141 let mut binding_index = 0_u16;
142 let mut spell_binding_index = 0_u16;
143 let mut rppm_binding_index = 0_u16;
144 let mut rppm_drivers = Vec::new();
145 let item_modules = items
146 .items
147 .iter()
148 .enumerate()
149 .map(|(item_index, (key, item))| {
150 let module_name = rust_ident(&to_snake(key))?;
151 let auras = item
152 .auras
153 .keys()
154 .map(|name| {
155 let name = rust_ident(name)?;
156 let index = lit_int(binding_index);
157
158 binding_index = binding_index.checked_add(1).context("item aura binding count exceeds u16")?;
159
160 Ok::<_, anyhow::Error>(quote!(pub const #name: crate::AuraBinding = crate::AuraBinding::new(#index);))
161 })
162 .collect::<anyhow::Result<Vec<_>>>()?;
163 let spells = item
164 .spells
165 .keys()
166 .map(|name| {
167 let name = rust_ident(&format!("SPELL_{name}"))?;
168 let index = lit_int(spell_binding_index);
169
170 spell_binding_index = spell_binding_index.checked_add(1).context("item spell binding count exceeds u16")?;
171
172 Ok::<_, anyhow::Error>(quote!(pub const #name: crate::SpellBinding = crate::SpellBinding::new(#index);))
173 })
174 .collect::<anyhow::Result<Vec<_>>>()?;
175
176 let rppm = (item.rppm.is_some() || item.driver_spell_id.is_some()).then(|| {
177 let index = lit_int(rppm_binding_index);
178
179 rppm_binding_index += 1;
180 rppm_drivers.push(lit_int(item.driver_spell_id.unwrap_or(0)));
181
182 quote!(pub const RPPM: crate::RppmBinding = crate::RppmBinding::new(#index);)
183 });
184 let mut damage_ids = item
185 .spells
186 .values()
187 .map(|spell| spell.id)
188 .chain(item.auras.values().map(|aura| aura.id))
189 .chain(item.effects.values().map(|effect| effect.spell_id))
190 .chain(item.auras.values().flat_map(|aura| {
191 aura.scalar_refs().filter_map(ScalarRef::effect_spell_id)
192 }))
193 .chain(item.driver_spell_id)
194 .filter(|spell_id| *spell_id != 0)
195 .collect::<Vec<_>>();
196
197 damage_ids.sort_unstable();
198 damage_ids.dedup();
199 let damage = damage_identity_module(
200 10_000 + u32::try_from(item_index)?,
201 damage_ids,
202 item.effects
203 .values()
204 .map(|effect| (effect.spell_id, effect.effect))
205 .collect(),
206 );
207
208 Ok::<_, anyhow::Error>(quote! {
209 pub mod #module_name {
210 #(#auras)*
211 #(#spells)*
212 #rppm
213 #damage
214 }
215 })
216 })
217 .collect::<anyhow::Result<Vec<_>>>()?;
218 let shared_modules = shared
219 .iter()
220 .enumerate()
221 .map(|(shared_index, (module_name, bindings))| {
222 let module_name = rust_ident(module_name)?;
223 let mut damage_ids = bindings
224 .reported_spells
225 .values()
226 .copied()
227 .chain(bindings.effects.values().map(|effect| effect.spell_id))
228 .filter(|spell_id| *spell_id != 0)
229 .collect::<Vec<_>>();
230
231 damage_ids.sort_unstable();
232 damage_ids.dedup();
233 let damage = damage_identity_module(
234 20_000 + u32::try_from(shared_index)?,
235 damage_ids,
236 bindings
237 .effects
238 .values()
239 .map(|effect| (effect.spell_id, effect.effect))
240 .collect(),
241 );
242
243 Ok::<_, anyhow::Error>(quote! {
244 pub mod #module_name {
245 #damage
246 }
247 })
248 })
249 .collect::<anyhow::Result<Vec<_>>>()?;
250 let shared_damage_values = shared
251 .iter()
252 .flat_map(|(module_name, bindings)| {
253 let module_name = rust_ident(module_name).expect("validated shared module name");
254 let mut ids = bindings
255 .reported_spells
256 .values()
257 .copied()
258 .chain(bindings.effects.values().map(|effect| effect.spell_id))
259 .filter(|spell_id| *spell_id != 0)
260 .collect::<Vec<_>>();
261
262 ids.sort_unstable();
263 ids.dedup();
264
265 ids.into_iter().map(move |spell_id| {
266 let action = format_ident!("SPELL_{spell_id}");
267
268 quote!(#module_name::damage::ACTION::#action)
269 })
270 })
271 .collect::<Vec<_>>();
272 let omnium_names = [
273 "VOID_TOUCHED_ORBS_COUNTER",
274 "LINGERING_DAMAGE",
275 "CRITICAL_POWER_BUFF",
276 "BURNING_HASTE_BUFF",
277 "MASTERFUL_CUNNING_BUFF",
278 "VERSATILE_WARRIOR_BUFF",
279 "ECHOES_DEBUFF",
280 ];
281 let omnium_bindings = omnium_names
282 .into_iter()
283 .map(|name| {
284 let name = rust_ident(name)?;
285 let index = lit_int(binding_index);
286
287 binding_index = binding_index
288 .checked_add(1)
289 .context("Omnium aura binding count exceeds u16")?;
290
291 Ok::<_, anyhow::Error>(
292 quote!(pub const #name: crate::AuraBinding = crate::AuraBinding::new(#index);),
293 )
294 })
295 .collect::<anyhow::Result<Vec<_>>>()?;
296 let omnium_damage = damage_identity_module(
297 30_000,
298 vec![1_286_716, 1_286_970, 1_287_663, 1_303_048],
299 Vec::new(),
300 );
301 let system_rppm_bindings = [
302 "SHADOW_OF_NATHREZA",
303 "SUDDEN_DEATH",
304 "FROSTREAPER",
305 "DANCE_OF_CHI_JI",
306 "CORPSECALLER",
307 "TOTEMIC_REBOUND",
308 "UNLEASHED_FIRE",
309 ]
310 .into_iter()
311 .enumerate()
312 .map(|(index, name)| {
313 let name = rust_ident(name)?;
314 let index = lit_int(index);
315
316 Ok::<_, anyhow::Error>(quote! {
317 pub const #name: crate::SystemRppmBinding = crate::SystemRppmBinding::new(#index);
318 })
319 })
320 .collect::<anyhow::Result<Vec<_>>>()?;
321 let proc_categories = [
322 ("STRIKE_AS_ONE", 1_251_717_u32),
323 ("ALNSIGHT", 1_266_686),
324 ("SLAYERS_DOMINANCE", 444_767),
325 ("REAP_THE_STORM", 444_775),
326 ("WILD_STRIKES", 382_946),
327 ("RUSH_OF_LIGHT", 407_067),
328 ("RIGHTEOUS_CAUSE", 402_912),
329 ("ART_OF_WAR", 406_064),
330 ("SHADOW_TECHNIQUES", 196_911),
331 ("WISDOM_OF_THE_WALL", 1_272_821),
332 ];
333 let proc_category_bindings = proc_categories
334 .iter()
335 .enumerate()
336 .map(|(index, (name, _))| {
337 let name = rust_ident(name)?;
338 let index = lit_int(index);
339
340 Ok::<_, anyhow::Error>(quote! {
341 pub const #name: crate::ProcCategoryBinding = crate::ProcCategoryBinding::new(#index);
342 })
343 })
344 .collect::<anyhow::Result<Vec<_>>>()?;
345 let proc_category_all = proc_categories
346 .iter()
347 .map(|(name, driver)| {
348 let name = rust_ident(name)?;
349 let driver = lit_int(*driver);
350
351 Ok::<_, anyhow::Error>(quote!((#name, #driver)))
352 })
353 .collect::<anyhow::Result<Vec<_>>>()?;
354 let periodic_drivers = [
355 ("DEATHMARK", 360_194_u32),
356 ("OVERWHELMING_BLADES", 1_238_507_u32),
357 ("UNHINGED", 1_257_068_u32),
358 ];
359 let periodic_driver_bindings = periodic_drivers
360 .iter()
361 .enumerate()
362 .map(|(index, (name, _))| {
363 let name = rust_ident(name)?;
364 let index = lit_int(index);
365
366 Ok::<_, anyhow::Error>(quote! {
367 pub const #name: crate::PeriodicDriverBinding = crate::PeriodicDriverBinding::new(#index);
368 })
369 })
370 .collect::<anyhow::Result<Vec<_>>>()?;
371 let periodic_driver_all = periodic_drivers
372 .iter()
373 .map(|(_, driver)| lit_int(*driver))
374 .collect::<Vec<_>>();
375 let proc_counters = [
376 "STORMKEEPER",
377 "SHADOW_TECHNIQUES",
378 "DANSE_MACABRE",
379 "SUPERCHARGER",
380 "DARKENING_HORIZON",
381 "COLLAPSING_VOID",
382 "IMMINENT_DEMISE",
383 "UNHINGED",
384 "DEADLY_PURSUIT",
385 "BEAST_MASTERY_HOWL",
386 "SURVIVAL_HOWL",
387 "LAVA_SURGE_ATTEMPT",
388 "MASTER_OF_WARFARE_ATTEMPT",
389 "SLAYERS_DOMINANCE_ATTEMPT",
390 "VOIDFALL_ATTEMPT",
391 ];
392 let proc_counter_bindings = proc_counters
393 .iter()
394 .enumerate()
395 .map(|(index, name)| {
396 let name = rust_ident(name)?;
397 let index = lit_int(index);
398
399 Ok::<_, anyhow::Error>(quote! {
400 pub const #name: crate::ProcCounterBinding = crate::ProcCounterBinding::new(#index);
401 })
402 })
403 .collect::<anyhow::Result<Vec<_>>>()?;
404 let proc_counter_count = lit_int(proc_counters.len());
405
406 render_rust(
407 quote! {
408 pub mod specs {
409 use crate::{SpecAuraBinding, SpecIdentityProof, SpecSpellBinding};
410 #(#modules)*
411 }
412
413 pub mod items {
414 #(#item_modules)*
415 pub(crate) const RPPM_DRIVERS: &[u32] = &[#(#rppm_drivers,)*];
416 }
417
418 pub mod shared {
419 use crate::DamageActionId;
420 #(#shared_modules)*
421 pub const ALL: &[DamageActionId] = &[#(#shared_damage_values,)*];
422 }
423
424 pub mod omnium {
425 #(#omnium_bindings)*
426 #omnium_damage
427 }
428
429 pub mod system_rppm {
430 #(#system_rppm_bindings)*
431 }
432
433 pub mod proc_category {
434 #(#proc_category_bindings)*
435 pub(crate) const ALL: &[(crate::ProcCategoryBinding, u32)] = &[#(#proc_category_all,)*];
436 }
437
438 pub mod periodic_driver {
439 #(#periodic_driver_bindings)*
440 pub(crate) const ALL: &[u32] = &[#(#periodic_driver_all,)*];
441 }
442
443 pub mod proc_counter {
444 #(#proc_counter_bindings)*
445 pub(crate) const COUNT: usize = #proc_counter_count;
446 }
447
448 },
449 &[FileComment::line(GENERATED_BANNER)?],
450 )
451 .context("rendering canonical generated identity catalog")
452}
453
454pub(crate) fn generate_top_mod() -> anyhow::Result<String> {
456 let lint_level = format_ident!("allow");
457
458 render_rust(
459 quote! {
460 #![#lint_level(
461 dead_code,
462 clippy::allow_attributes_without_reason,
463 clippy::doc_markdown,
464 clippy::fn_to_numeric_cast_any,
465 clippy::must_use_candidate,
466 clippy::needless_pass_by_value,
467 reason = "generated bindings preserve manifest names and registry ABI shapes"
468 )]
469
470 pub(crate) mod items;
471 pub(crate) mod shared;
472 pub(crate) mod specs;
473 },
474 &[FileComment::inner_doc(GENERATED_BANNER)?],
475 )
476 .context("rendering top-level generated module")
477}
478
479pub(crate) fn generate_shared_mod(module_names: &[String]) -> anyhow::Result<String> {
481 let mut sorted_names = module_names.to_vec();
482
483 sorted_names.sort();
484
485 let modules = sorted_names
486 .iter()
487 .map(|name| rust_ident(name))
488 .collect::<Result<Vec<_>, _>>()
489 .context("shared module name is not a Rust identifier")?;
490 let lint_level = format_ident!("allow");
491
492 render_rust(
493 quote! {
494 #![#lint_level(dead_code)]
495
496 #(pub(crate) mod #modules;)*
497 },
498 &[FileComment::inner_doc(GENERATED_BANNER)?],
499 )
500 .context("rendering shared generated module")
501}
502
503pub(crate) fn generate_specs_mod(spec_names: &[String]) -> anyhow::Result<String> {
505 let mut sorted_names = spec_names.to_vec();
506
507 sorted_names.sort();
508
509 let modules = sorted_names
510 .iter()
511 .map(|name| rust_ident(name))
512 .collect::<Result<Vec<_>, _>>()
513 .context("spec module name is not a Rust identifier")?;
514 let descriptors = spec_names
515 .iter()
516 .map(|name| rust_ident(name))
517 .collect::<Result<Vec<_>, _>>()
518 .context("spec descriptor module name is not a Rust identifier")?;
519 let lint_level = format_ident!("allow");
520
521 render_rust(
522 quote! {
523 #![#lint_level(dead_code)]
524
525 #(pub(crate) mod #modules;)*
526
527 use wowlab_engine_ports::SpecDescriptor;
528
529 pub(crate) const SPEC_DESCRIPTORS: &[SpecDescriptor] = &[
530 #(#descriptors::SPEC_DESCRIPTOR,)*
531 ];
532
533 #[doc(hidden)]
534 #[inline(never)]
535 pub(crate) fn force_link_generated_specs() -> usize {
536 std::hint::black_box(SPEC_DESCRIPTORS.as_ptr() as usize)
537 }
538 },
539 &[FileComment::inner_doc(GENERATED_BANNER)?],
540 )
541 .context("rendering spec generated module")
542}