Skip to main content

codegen/
gen_items.rs

1use proc_macro2::TokenStream;
2use quote::{format_ident, quote};
3use wowlab_manifest_schema::{ItemManifestEntry, ManifestAuraDef};
4use wowlab_types::sim::FastMap;
5
6use crate::{
7    gen_aura::{AuraEmitOptions, emit_aura_fn},
8    gen_constants::effect_constant,
9    gen_spells::{SpellChainKind, SpellChainOpts, lower_manifest_spell},
10    helpers::{to_display_name, to_snake},
11    rust_source::{
12        FileComment, documented_u32_constant, inspect_link, lit_f64, lit_int, lit_str, render_rust,
13        rust_ident,
14    },
15};
16
17/// True when the item registers only plain on-player equip auras.
18pub(crate) fn is_simple_equip_aura(item: &ItemManifestEntry) -> bool {
19    if !item.spells.is_empty()
20        || item.player_cast_hook.is_some()
21        || item.player_impact_hook.is_some()
22        || item.rppm.is_some()
23        || item.driver_spell_id.is_some()
24        || item.auras.is_empty()
25    {
26        return false;
27    }
28
29    item.auras.values().all(is_plain_equip_aura)
30}
31
32fn is_plain_equip_aura(aura: &ManifestAuraDef) -> bool {
33    aura.on == "player"
34        && aura.duration_ms.is_none()
35        && aura.max_stacks.is_none()
36        && !aura.no_pandemic
37        && aura.refresh_behavior.is_none()
38        && !aura.precombat
39        && !aura.snapshot
40        && aura.damage_mult.is_none()
41        && aura.pet_damage_mult.is_none()
42        && aura.haste_per_stack.is_none()
43        && aura.crit_per_stack.is_none()
44        && aura.crit_rating_per_stack.is_none()
45        && aura.haste_rating_per_stack.is_none()
46        && aura.mastery_rating_per_stack.is_none()
47        && aura.versatility_rating_per_stack.is_none()
48        && aura.primary_stat_per_stack.is_none()
49        && !aura.apply_at_max_stacks
50        && !aura.reverse
51        && !aura.freeze_stacks
52        && aura.tick_behavior.is_none()
53        && aura.tick_stack_change.is_none()
54        && aura.stack_tick_ms.is_none()
55        && !aura.reapply_on_expire
56        && aura.periodic_remove_stack.is_none()
57        && aura.periodic_damage.is_none()
58        && aura.periodic_residual_damage.is_none()
59        && aura.periodic_hasted.is_none()
60        && aura.periodic_resource.is_none()
61        && aura.periodic_resource_drain.is_none()
62        && aura.periodic_apply_aura.is_none()
63}
64
65/// Generate one Rust file for a complex item.
66pub(crate) fn generate_item_file(key: &str, item: &ItemManifestEntry) -> anyhow::Result<String> {
67    let item_snake = to_snake(key);
68    let aura_import = (!item.auras.is_empty()).then(|| quote!(AuraDefinitionDraft,));
69    let spell_import = (!item.spells.is_empty()).then(|| quote!(SpellDefinitionDraft,));
70    let item_id = documented_u32_constant("ITEM_ID", item.id, "item")?;
71    let driver_spell_id = item
72        .driver_spell_id
73        .map(|id| documented_u32_constant("DRIVER_SPELL_ID", id, "spell"))
74        .transpose()?;
75    let constants = item_constants(item)?;
76    let auras = item_aura_functions(item, &item_snake)?;
77    let spells = item_spell_functions(item, &item_snake)?;
78    let register = item_register_function(key, item)?;
79    let preamble = [
80        FileComment::inner_doc(format!(
81            "@generated by codegen-cli from manifests/items.toml ({key}) -- do not edit by hand."
82        ))?,
83        FileComment::line("Generated code: naming follows game data conventions.")?,
84    ];
85    let lint_level = format_ident!("allow");
86
87    Ok(render_rust(
88        quote! {
89            #![#lint_level(non_snake_case, dead_code)]
90
91            use wowlab_engine_combat::{
92                CombatSystemBuilder,
93                #aura_import
94                #spell_import
95            };
96            use wowlab_engine_gamedata::{ItemBudget, ResolvedGameData};
97
98            #item_id
99            #driver_spell_id
100            #constants
101            #auras
102            #spells
103            #register
104        },
105        &preamble,
106    )?)
107}
108
109fn item_constants(item: &ItemManifestEntry) -> anyhow::Result<TokenStream> {
110    let spells = item
111        .spells
112        .iter()
113        .map(|(name, spell)| documented_u32_constant(name, spell.id, "spell"))
114        .collect::<anyhow::Result<Vec<_>>>()?;
115    let auras = item
116        .auras
117        .iter()
118        .map(|(name, aura)| documented_u32_constant(name, aura.id, "spell"))
119        .collect::<anyhow::Result<Vec<_>>>()?;
120    let effects = item
121        .effects
122        .iter()
123        .map(|(name, effect)| {
124            let documentation = lit_str(&inspect_link("spell", effect.spell_id));
125            let constant = effect_constant(name, effect.spell_id, effect.effect)?;
126
127            Ok::<_, anyhow::Error>(quote! {
128                #[doc = #documentation]
129                #constant
130            })
131        })
132        .collect::<anyhow::Result<Vec<_>>>()?;
133    let effects = (!effects.is_empty()).then(|| {
134        quote! {
135            pub(crate) mod EFFECT {
136                #(#effects)*
137            }
138        }
139    });
140
141    Ok(quote! {
142        pub(crate) mod SPELL {
143            #(#spells)*
144        }
145        pub(crate) mod AURA {
146            #(#auras)*
147        }
148        #effects
149    })
150}
151
152fn item_aura_functions(item: &ItemManifestEntry, item_snake: &str) -> anyhow::Result<TokenStream> {
153    let empty_idx_map = FastMap::default();
154    let spell_id_map = item
155        .spells
156        .iter()
157        .map(|(name, spell)| (name.as_str(), spell.id))
158        .collect::<FastMap<_, _>>();
159    let hook_module = format!("items::{item_snake}");
160    let functions = item
161        .auras
162        .iter()
163        .map(|(name, def)| {
164            let options = AuraEmitOptions {
165                spell_group: None,
166                aura_idx_map: &empty_idx_map,
167                spell_id_map: &spell_id_map,
168                emit_default_max_stacks: def.max_stacks.is_none(),
169                has_item_budget: true,
170                hook_module: &hook_module,
171                hook_suffix: "",
172            };
173
174            emit_aura_fn(name, def, &options)
175        })
176        .collect::<anyhow::Result<Vec<_>>>()?;
177
178    Ok(quote!(#(#functions)*))
179}
180
181fn item_spell_functions(item: &ItemManifestEntry, item_snake: &str) -> anyhow::Result<TokenStream> {
182    let hook_module = format!("items::{item_snake}");
183    let functions = item
184        .spells
185        .iter()
186        .map(|(name, def)| {
187            let function_name = format_ident!("spell_{}", to_snake(name));
188            let options = SpellChainOpts {
189                kind: SpellChainKind::Item,
190                inferred_aura: None,
191                hook_module: &hook_module,
192                hook_suffix: "",
193            };
194            let expression = lower_manifest_spell(def, &options)?.into_result_expression()?;
195
196            Ok::<_, anyhow::Error>(quote! {
197                fn #function_name(
198                    s: SpellDefinitionDraft,
199                    data: &ResolvedGameData,
200                ) -> Result<SpellDefinitionDraft, wowlab_engine_combat::BuilderError> {
201                    #expression
202                }
203            })
204        })
205        .collect::<anyhow::Result<Vec<_>>>()?;
206
207    Ok(quote!(#(#functions)*))
208}
209
210fn item_register_function(key: &str, item: &ItemManifestEntry) -> anyhow::Result<TokenStream> {
211    let display = lit_str(&to_display_name(key));
212    let rppm = if let Some(driver_spell_id) = item.driver_spell_id {
213        let driver_spell_id = lit_int(driver_spell_id);
214
215        quote!(, rppm_driver: (ITEM_ID, #driver_spell_id))
216    } else if let Some(rppm) = item.rppm {
217        let rppm = lit_f64(rppm);
218        let haste_scales = item.rppm_haste_scales;
219
220        quote!(, rppm: (ITEM_ID, #rppm, #haste_scales))
221    } else {
222        TokenStream::new()
223    };
224    let aura_rows = item
225        .auras
226        .keys()
227        .map(|name| {
228            let aura = rust_ident(name)?;
229            let function = format_ident!("aura_{}", to_snake(name));
230
231            Ok::<_, syn::Error>(quote!((AURA::#aura, #function)))
232        })
233        .collect::<Result<Vec<_>, _>>()?;
234    let auras = (!aura_rows.is_empty()).then(|| quote!(, auras: (budget) [#(#aura_rows,)*]));
235    let spell_rows = item
236        .spells
237        .keys()
238        .map(|name| {
239            let spell = rust_ident(name)?;
240            let function = format_ident!("spell_{}", to_snake(name));
241
242            Ok::<_, syn::Error>(quote!((SPELL::#spell, #function)))
243        })
244        .collect::<Result<Vec<_>, _>>()?;
245    let spells = (!spell_rows.is_empty()).then(|| quote!(, spells: [#(#spell_rows,)*]));
246    let precombat = item
247        .auras
248        .iter()
249        .filter(|(_, aura)| aura.precombat)
250        .map(|(name, _)| rust_ident(name))
251        .collect::<Result<Vec<_>, _>>()?;
252    let precombat = (!precombat.is_empty()).then(|| quote!(, precombat: [#(AURA::#precombat,)*]));
253    let budget = if item.auras.is_empty() {
254        quote!(_budget)
255    } else {
256        quote!(budget)
257    };
258
259    Ok(quote! {
260        pub(crate) fn register(
261            builder: CombatSystemBuilder,
262            data: &ResolvedGameData,
263            #budget: ItemBudget,
264        ) -> CombatSystemBuilder {
265            crate::items::register_item!(
266                builder,
267                data,
268                #display
269                #rppm
270                #auras
271                #spells
272                #precombat
273            )
274        }
275    })
276}