Skip to main content

codegen/
gen_aura.rs

1//! Emission of `aura_<name>` builder functions from manifest aura definitions.
2
3use anyhow::{Context, bail};
4use proc_macro2::TokenStream;
5use quote::{format_ident, quote};
6use wowlab_manifest_schema::{Manifest, ManifestAuraDef, SpellGroupRule};
7use wowlab_types::sim::FastMap;
8
9use crate::{
10    gen_aura_behavior::{apply_aura_consume_effects, apply_refresh_behavior, apply_tick_behavior},
11    gen_aura_modifiers::{
12        apply_per_stack_scalars, apply_rating_effects, apply_spell_scoped_effects,
13    },
14    gen_aura_periodic::{
15        apply_aura_duration, apply_periodic_damage, apply_periodic_hook_source,
16        apply_periodic_hooks, periodic_behavior_spell_id,
17    },
18    gen_emit::{
19        require_get, scalar_ref_call, spell_idx, validated_max_stacks_call,
20        validated_milliseconds_call,
21    },
22    helpers::to_snake,
23    rust_source::{ExpressionRequirements, expression_requirements, lit_int, result_expression},
24};
25
26pub(crate) fn aura_functions(
27    manifest: &Manifest,
28    module_name: &str,
29) -> anyhow::Result<TokenStream> {
30    let aura_count = manifest
31        .spell_groups
32        .iter()
33        .map(|group| group.auras.len())
34        .sum();
35    let mut spell_group_map = FastMap::default();
36
37    spell_group_map.reserve(aura_count);
38
39    for (group_index, group) in manifest.spell_groups.iter().enumerate() {
40        let group_index =
41            u8::try_from(group_index).context("manifest has more than 256 spell groups")?;
42
43        for aura_name in &group.auras {
44            spell_group_map.insert(aura_name.as_str(), (group_index, group.rule));
45        }
46    }
47
48    let mut aura_idx_map = FastMap::default();
49
50    aura_idx_map.reserve(manifest.auras.len());
51
52    for (index, name) in manifest.auras.keys().enumerate() {
53        let index = u8::try_from(index).context("manifest has more than 256 auras")?;
54
55        aura_idx_map.insert(name.as_str(), index);
56    }
57
58    let spell_id_map = manifest_spell_id_map(manifest)?;
59    let functions = manifest
60        .auras
61        .iter()
62        .map(|(name, def)| {
63            let options = AuraEmitOptions {
64                spell_group: spell_group_map.get(name.as_str()).copied(),
65                aura_idx_map: &aura_idx_map,
66                spell_id_map: &spell_id_map,
67                emit_default_max_stacks: def.max_stacks.is_none(),
68                has_item_budget: false,
69                hook_module: module_name,
70                hook_suffix: "_hook",
71            };
72
73            emit_aura_fn(name, def, &options)
74        })
75        .collect::<anyhow::Result<Vec<_>>>()?;
76
77    Ok(quote!(#(#functions)*))
78}
79
80fn manifest_spell_id_map(manifest: &Manifest) -> anyhow::Result<FastMap<&str, u32>> {
81    let mut ids: FastMap<&str, u32> = manifest
82        .spells
83        .iter()
84        .map(|(name, spell)| (name.as_str(), spell.id))
85        .collect();
86
87    for (name, id) in &manifest.reported_spells {
88        ids.entry(name).or_insert(*id);
89    }
90
91    let hero_spell_count = manifest
92        .hero_talents
93        .values()
94        .map(|tree| tree.spells.len())
95        .sum();
96    let mut hero_ids = FastMap::default();
97
98    hero_ids.reserve(hero_spell_count);
99
100    for tree in manifest.hero_talents.values() {
101        for (name, id) in &tree.spells {
102            if let Some(previous) = hero_ids.insert(name.as_str(), *id)
103                && previous != *id
104                && !ids.contains_key(name.as_str())
105            {
106                bail!("hero spell binding {name:?} is ambiguous between ids {previous} and {id}");
107            }
108
109            ids.entry(name).or_insert(*id);
110        }
111    }
112
113    Ok(ids)
114}
115
116/// Controls item- and spec-specific aura emission.
117pub(crate) struct AuraEmitOptions<'a> {
118    pub spell_group: Option<(u8, SpellGroupRule)>,
119    pub aura_idx_map: &'a FastMap<&'a str, u8>,
120    pub spell_id_map: &'a FastMap<&'a str, u32>,
121    pub emit_default_max_stacks: bool,
122    pub has_item_budget: bool,
123    pub hook_module: &'a str,
124    pub hook_suffix: &'a str,
125}
126
127struct GeneratedAuraBuilderCode {
128    expression: TokenStream,
129    requirements: ExpressionRequirements,
130    _stage: AuraBuilderStage,
131}
132
133struct AuraBuilderStage;
134
135impl GeneratedAuraBuilderCode {
136    fn uses_data(&self) -> bool {
137        self.requirements.data
138    }
139
140    fn uses_budget(&self) -> bool {
141        self.requirements.budget
142    }
143
144    fn into_result_expression(self) -> anyhow::Result<TokenStream> {
145        Ok(result_expression(self.expression)?)
146    }
147
148    #[cfg(test)]
149    fn body_string(&self) -> String {
150        self.expression.to_string()
151    }
152}
153
154fn lower_manifest_aura(
155    def: &ManifestAuraDef,
156    options: &AuraEmitOptions<'_>,
157) -> anyhow::Result<GeneratedAuraBuilderCode> {
158    let id = def.id;
159    let spell_id = spell_idx(id);
160    let mut expression = match def.on.as_str() {
161        "target" => quote!(a.on_target()),
162        "player" => quote!(a.on_player()),
163        "pet" => quote!(a.on_pet()),
164        other => bail!("unsupported aura target {other:?} on aura {id}"),
165    };
166
167    expression = apply_core_aura_options(expression, def, options, &spell_id)?;
168    expression = apply_tick_behavior(expression, def)?;
169
170    if let Some(remove_stack) = &def.periodic_remove_stack {
171        let tick_ms = validated_milliseconds_call(
172            &remove_stack.tick_ms,
173            id,
174            "periodic_remove_stack.tick_ms",
175        )?;
176
177        expression = quote!(#expression.periodic_remove_stack(#tick_ms));
178    }
179
180    if let Some(value) = &def.primary_stat_percent_per_stack {
181        let value = scalar_ref_call(value)?;
182
183        expression = quote!(#expression.primary_pct_per_stack(#value)?);
184    }
185
186    if let Some(value) = &def.attack_power_percent_per_stack {
187        let value = scalar_ref_call(value)?;
188
189        expression = quote!(#expression.attack_power_pct_per_stack(#value)?);
190    }
191
192    expression = apply_spell_scoped_effects(expression, def, options)?;
193    expression = apply_periodic_damage(expression, def, id, options.has_item_budget)?;
194
195    if let Some(resource) = &def.periodic_resource {
196        let tick_ms =
197            validated_milliseconds_call(&resource.tick_ms, id, "periodic_resource.tick_ms")?;
198        let amount = scalar_ref_call(&resource.amount)?;
199
200        expression = quote!(#expression.periodic_resource(#tick_ms, #amount));
201    }
202
203    if let Some(drain) = &def.periodic_resource_drain {
204        let tick_ms =
205            validated_milliseconds_call(&drain.tick_ms, id, "periodic_resource_drain.tick_ms")?;
206        let amount = scalar_ref_call(&drain.amount)?;
207
208        expression = quote!(#expression.periodic_resource_drain(#tick_ms, #amount));
209    }
210
211    if let Some(apply) = &def.periodic_apply_aura {
212        let index = options
213            .aura_idx_map
214            .get(apply.aura.as_str())
215            .copied()
216            .ok_or_else(|| {
217                anyhow::anyhow!(
218                    "periodic_apply_aura.aura {:?} not found in [auras.*]",
219                    apply.aura
220                )
221            })?;
222        let tick_ms =
223            validated_milliseconds_call(&apply.tick_ms, id, "periodic_apply_aura.tick_ms")?;
224        let index = lit_int(index);
225
226        expression = quote!(#expression.periodic_apply_aura(#tick_ms, #index));
227    }
228
229    expression = apply_periodic_hook_source(expression, def)?;
230
231    if let Some(source_spell_id) = periodic_behavior_spell_id(def) {
232        let source_spell_id = lit_int(source_spell_id);
233
234        expression = quote!(#expression.apply_periodic_behavior_from_data(data, #source_spell_id)?);
235    }
236
237    expression = apply_periodic_hooks(expression, def, options)?;
238    expression = apply_aura_consume_effects(expression, def, options)?;
239
240    if let Some((group_index, rule)) = options.spell_group {
241        let group_index = lit_int(group_index);
242        let rule = match rule {
243            SpellGroupRule::Exclusive => {
244                quote!(wowlab_types::combat::SpellGroupRule::Exclusive)
245            }
246        };
247
248        expression = quote!(#expression.spell_group(#group_index, #rule));
249    }
250
251    let requirements = expression_requirements(expression.clone())?;
252
253    Ok(GeneratedAuraBuilderCode {
254        expression,
255        requirements,
256        _stage: AuraBuilderStage,
257    })
258}
259
260fn apply_core_aura_options(
261    mut expression: TokenStream,
262    def: &ManifestAuraDef,
263    options: &AuraEmitOptions<'_>,
264    spell_id: &TokenStream,
265) -> anyhow::Result<TokenStream> {
266    let id = def.id;
267
268    expression = apply_aura_duration(expression, def)?;
269
270    if options.emit_default_max_stacks {
271        let max_stacks = require_get("aura_max_stacks", spell_id);
272
273        expression = quote!(#expression.max_stacks(#max_stacks));
274    }
275
276    expression = apply_refresh_behavior(expression, def)?;
277
278    if def.snapshot {
279        expression = quote!(#expression.snapshot());
280    }
281
282    if let Some(value) = &def.forwarded_effect_value {
283        let value = scalar_ref_call(value)?;
284
285        expression = quote!(#expression.forwarded_effect_value(data, #value)?);
286    }
287
288    if let Some(multiplier) = &def.damage_mult {
289        let multiplier = scalar_ref_call(multiplier)?;
290
291        expression = quote!(#expression.damage_multiplier(#multiplier)?);
292    }
293
294    if let Some(multiplier) = &def.damage_mult_stacking {
295        let initial = scalar_ref_call(&multiplier.initial)?;
296        let per_stack = scalar_ref_call(&multiplier.per_stack)?;
297
298        expression = quote!(#expression.damage_multiplier_stacking(#initial, #per_stack)?);
299    }
300
301    if let Some(multiplier) = &def.pet_damage_mult {
302        let multiplier = scalar_ref_call(multiplier)?;
303
304        expression = quote!(#expression.pet_damage_multiplier(#multiplier)?);
305    }
306
307    if let Some(stacks) = &def.max_stacks {
308        let stacks = validated_max_stacks_call(stacks, id, "aura.max_stacks")?;
309
310        expression = quote!(#expression.max_stacks(#stacks));
311    }
312
313    expression = apply_per_stack_scalars(expression, def)?;
314    expression = apply_rating_effects(expression, def, options)?;
315
316    if def.apply_at_max_stacks {
317        expression = quote!(#expression.apply_at_max_stacks());
318    }
319
320    if def.reverse {
321        expression = quote!(#expression.reverse());
322    }
323
324    if def.freeze_stacks {
325        expression = quote!(#expression.freeze_stacks());
326    }
327
328    if def.async_stacks {
329        expression = quote!(#expression.async_stacks());
330    }
331
332    if def.periodic_damage_scales_with_stacks {
333        expression = quote!(#expression.periodic_damage_scales_with_stacks());
334    }
335
336    if def.reapply_on_expire {
337        expression = quote!(#expression.reapply_on_expire());
338    }
339
340    Ok(expression)
341}
342
343/// Emit a single `aura_<name>` builder function.
344pub(crate) fn emit_aura_fn(
345    name: &str,
346    def: &ManifestAuraDef,
347    options: &AuraEmitOptions<'_>,
348) -> anyhow::Result<TokenStream> {
349    let function_name = format_ident!("aura_{}", to_snake(name));
350    let generated = lower_manifest_aura(def, options)?;
351    let data_parameter = if generated.uses_data() {
352        quote!(data)
353    } else {
354        quote!(_data)
355    };
356    let uses_budget = generated.uses_budget();
357    let expression = generated.into_result_expression()?;
358
359    Ok(if options.has_item_budget {
360        let budget_parameter = if uses_budget {
361            quote!(budget)
362        } else {
363            quote!(_budget)
364        };
365
366        quote! {
367            fn #function_name(
368                a: AuraDefinitionDraft,
369                #data_parameter: &ResolvedGameData,
370                #budget_parameter: ItemBudget,
371            ) -> Result<AuraDefinitionDraft, wowlab_engine_combat::BuilderError> {
372                #expression
373            }
374        }
375    } else {
376        quote! {
377            fn #function_name(
378                a: AuraDefinitionDraft,
379                #data_parameter: &ResolvedGameData,
380            ) -> Result<AuraDefinitionDraft, wowlab_engine_combat::BuilderError> {
381                #expression
382            }
383        }
384    })
385}
386
387#[cfg(test)]
388#[path = "gen_aura/periodic_tests.rs"]
389mod periodic_tests;
390
391#[cfg(test)]
392#[path = "gen_aura/tests.rs"]
393mod tests;