Skip to main content

codegen/
gen_barrel.rs

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
63// #t(fn: rust_max_fn_lines) the identity catalog is one deterministic code-generation transaction whose namespaces must remain positionally synchronized.
64pub(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
454/// Emit the top-level `generated/mod.rs`.
455pub(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
479/// Emits a module containing generated shared-manifest binding modules.
480pub(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
503/// Emit `generated/specs/mod.rs`.
504pub(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}