Skip to main content

codegen/
gen_descriptor.rs

1use anyhow::Context;
2use proc_macro2::TokenStream;
3use quote::{format_ident, quote};
4use wowlab_manifest_schema::{Manifest, ManifestProcAttribute};
5use wowlab_types::combat::ResourceType;
6
7use crate::{
8    helpers::to_display_name,
9    rust_source::{lit_f64, lit_int, lit_str, rust_ident},
10};
11
12pub(crate) fn hero_talent_statics(manifest: &Manifest) -> TokenStream {
13    let trees = manifest.hero_talents.iter().map(|(tree_name, tree)| {
14        let upper = tree_name.to_uppercase();
15        let spells_name = format_ident!("HERO_TREE_{upper}_SPELLS");
16        let auras_name = format_ident!("HERO_TREE_{upper}_AURAS");
17        let spell_names = tree.spells.keys().map(|name| lit_str(name));
18        let spell_ids = tree.spells.values().map(lit_int);
19        let aura_names = tree.auras.keys().map(|name| lit_str(name));
20        let aura_ids = tree.auras.values().map(lit_int);
21
22        quote! {
23            static #spells_name: &[(&str, u32)] = &[
24                #((#spell_names, #spell_ids),)*
25            ];
26            static #auras_name: &[(&str, u32)] = &[
27                #((#aura_names, #aura_ids),)*
28            ];
29        }
30    });
31
32    quote!(#(#trees)*)
33}
34
35pub(crate) fn descriptor(
36    manifest: &Manifest,
37    spec_variant: &str,
38    display_name: &str,
39) -> anyhow::Result<TokenStream> {
40    let rotation_fields = manifest
41        .rotation_schema
42        .as_ref()
43        .into_iter()
44        .flat_map(|schema| schema.fields.iter())
45        .map(|field| lit_str(field));
46    let primary_resource = rust_ident(&resource_variant(manifest.resource.type_id)?)?;
47    let secondary_resource = manifest
48        .secondary_resource
49        .as_ref()
50        .map(|resource| resource_variant(resource.type_id))
51        .transpose()?
52        .map(|resource| rust_ident(&resource))
53        .transpose()?;
54    let secondary_resource = secondary_resource.map_or_else(
55        || quote!(None),
56        |resource| quote!(Some(ResourceType::#resource)),
57    );
58    let server_proc_entries = server_proc_entries(manifest)?;
59    let talents = if manifest.talents.is_empty() {
60        quote!(&[])
61    } else {
62        quote!(TALENTS)
63    };
64    let hero_talent_trees = hero_talent_tree_descriptors(manifest);
65    let masked_passive_effects = manifest.masked_passive_effects.iter().map(|masked| {
66        let spell_id = lit_int(masked.spell_id);
67        let effect = lit_int(masked.effect);
68
69        quote!((#spell_id, #effect))
70    });
71    let passive_effect_overrides = passive_effect_overrides(manifest)?;
72    let spec_variant = rust_ident(spec_variant)?;
73    let display_name = lit_str(display_name);
74
75    Ok(quote! {
76        static DECLARED_METADATA: DeclaredSpecMetadata = DeclaredSpecMetadata {
77            primary_resource: ResourceType::#primary_resource,
78            secondary_resource: #secondary_resource,
79            spell_ids: DECLARED_SPELL_IDS,
80            aura_ids: DECLARED_AURA_IDS,
81            server_proc_entries: #server_proc_entries,
82            talents: #talents,
83            rotation_field_schema: &[#(#rotation_fields,)*],
84            metrics_plugin_id: None,
85            hero_talent_trees: #hero_talent_trees,
86            reported_spells: REPORTED_SPELLS,
87            masked_passive_effects: &[#(#masked_passive_effects,)*],
88            passive_effect_overrides: #passive_effect_overrides,
89        };
90
91        pub(super) const SPEC_DESCRIPTOR: SpecDescriptor = SpecDescriptor {
92            spec_id: SpecId::#spec_variant,
93            display_name: #display_name,
94            metadata: DECLARED_METADATA,
95            handler_factory: new_handler,
96        };
97    })
98}
99
100fn server_proc_entries(manifest: &Manifest) -> anyhow::Result<TokenStream> {
101    let spells = manifest
102        .spells
103        .values()
104        .map(|spell| (spell.id, spell.proc_attributes.as_slice()));
105    let auras = manifest
106        .auras
107        .values()
108        .map(|aura| (aura.id, aura.proc_attributes.as_slice()));
109    let entries = spells
110        .chain(auras)
111        .filter(|(_, attributes)| !attributes.is_empty())
112        .map(|(spell_id, proc_attributes)| {
113            let spell_id = lit_int(spell_id);
114            let mut attributes = proc_attributes.iter().map(|attribute| {
115                let variant = format_ident!("{}", proc_attribute_variant(*attribute)?);
116
117                Ok::<_, anyhow::Error>(quote!(ProcAttributes::#variant.bits()))
118            });
119            let first = attributes
120                .next()
121                .transpose()?
122                .context("server proc entry has no attributes")?;
123            let attributes = attributes.try_fold(first, |left, right| {
124                let right = right?;
125
126                Ok::<_, anyhow::Error>(quote!(#left | #right))
127            })?;
128
129            Ok::<_, anyhow::Error>(quote! {
130                DeclaredServerProcEntry {
131                    spell_id: #spell_id,
132                    attributes: #attributes,
133                }
134            })
135        })
136        .collect::<anyhow::Result<Vec<_>>>()?;
137
138    Ok(quote!(&[#(#entries,)*]))
139}
140
141fn hero_talent_tree_descriptors(manifest: &Manifest) -> TokenStream {
142    let trees = manifest.hero_talents.keys().map(|tree_name| {
143        let upper = tree_name.to_uppercase();
144        let display = lit_str(&to_display_name(tree_name));
145        let spells = format_ident!("HERO_TREE_{upper}_SPELLS");
146        let auras = format_ident!("HERO_TREE_{upper}_AURAS");
147
148        quote! {
149            HeroTalentTreeDesc {
150                name: #display,
151                spells: #spells,
152                auras: #auras,
153            }
154        }
155    });
156
157    quote!(&[#(#trees,)*])
158}
159
160fn passive_effect_overrides(manifest: &Manifest) -> anyhow::Result<TokenStream> {
161    let overrides = manifest
162        .passive_effect_overrides
163        .iter()
164        .map(|override_def| {
165            anyhow::ensure!(
166                override_def.from.is_some() != override_def.value.is_some(),
167                "passive_effect_overrides row for spell {} effect {} must set exactly one of `from` and `value`",
168                override_def.spell_id,
169                override_def.effect
170            );
171
172            let spell_id = lit_int(override_def.spell_id);
173            let effect = lit_int(override_def.effect);
174            let source = override_def.from.as_ref().map_or_else(
175                || quote!(None),
176                |from| {
177                    let spell_id = lit_int(from.spell_id);
178                    let effect = lit_int(from.effect);
179
180                    quote!(Some((#spell_id, #effect)))
181                },
182            );
183            let value = lit_f64(override_def.value.unwrap_or_default());
184            let multiplier = lit_f64(override_def.multiplier.unwrap_or(1.0));
185
186            Ok::<_, anyhow::Error>(quote! {
187                wowlab_engine_ports::DeclaredPassiveEffectOverride {
188                    spell_id: #spell_id,
189                    effect: #effect,
190                    source: #source,
191                    value: #value,
192                    multiplier: #multiplier,
193                }
194            })
195        })
196        .collect::<anyhow::Result<Vec<_>>>()?;
197
198    Ok(quote!(&[#(#overrides,)*]))
199}
200
201fn proc_attribute_variant(attribute: ManifestProcAttribute) -> anyhow::Result<&'static str> {
202    let variant = match attribute {
203        ManifestProcAttribute::RequireExperienceOrHonor => "REQUIRE_EXPERIENCE_OR_HONOR",
204        ManifestProcAttribute::TriggeredCanProc => "TRIGGERED_CAN_PROC",
205        ManifestProcAttribute::RequirePowerCost => "REQUIRE_POWER_COST",
206        ManifestProcAttribute::RequireSpellModifier => "REQUIRE_SPELL_MODIFIER",
207        ManifestProcAttribute::UseStacksForCharges => "USE_STACKS_FOR_CHARGES",
208        ManifestProcAttribute::ReduceProcAboveLevel60 => "REDUCE_PROC_ABOVE_LEVEL_60",
209        ManifestProcAttribute::CannotProcFromItemCast => "CANNOT_PROC_FROM_ITEM_CAST",
210        _ => anyhow::bail!("unsupported manifest proc attribute"),
211    };
212
213    Ok(variant)
214}
215
216pub(crate) fn resource_variant(type_id: u8) -> anyhow::Result<String> {
217    ResourceType::try_from(type_id)
218        .map(|resource| format!("{resource:?}"))
219        .with_context(|| format!("unknown resource type ID {type_id}"))
220}
221
222pub(crate) fn smoke_test() -> TokenStream {
223    quote!(wowlab_engine_combat::smoke_test!(build_combat_system);)
224}