1use proc_macro2::TokenStream;
2use quote::{format_ident, quote};
3use wowlab_manifest_schema::{ItemManifestEntry, ItemsManifest, ManifestAuraDef, ScalarRef};
4
5use crate::{
6 gen_items::is_simple_equip_aura,
7 helpers::{to_display_name, to_snake},
8 rust_source::{FileComment, lit_int, lit_str, render_rust, rust_ident},
9};
10
11pub(crate) fn generate_items_mod_file(manifest: &ItemsManifest) -> anyhow::Result<String> {
12 let all = manifest
13 .items
14 .iter()
15 .map(|(key, item)| (to_snake(key), key.as_str(), item))
16 .collect::<Vec<_>>();
17 let complex = all
18 .iter()
19 .filter(|(_, _, item)| !is_simple_equip_aura(item))
20 .map(|(snake, key, item)| (snake.clone(), *key, *item))
21 .collect::<Vec<_>>();
22 let mut complex_modules = complex
23 .iter()
24 .map(|(module, _, _)| rust_ident(module))
25 .collect::<Result<Vec<_>, _>>()?;
26
27 complex_modules.sort_unstable_by_key(ToString::to_string);
28
29 let simple_items = simple_items_table(&all);
30 let dispatch = items_dispatch_table(&complex)?;
31 let registered = registered_items(&all);
32 let register_equipped = register_equipped_function();
33 let cast_hooks = player_cast_hooks(&all)?;
34 let impact_hooks = player_impact_hooks(&all)?;
35 let lint_level = format_ident!("allow");
36
37 Ok(render_rust(
38 quote! {
39 #![#lint_level(non_snake_case)]
40
41 use wowlab_engine_combat::CombatSystemBuilder;
42 use wowlab_engine_gamedata::ResolvedGameData;
43 use wowlab_engine_ports::ResolveDependencies;
44
45 #(pub(crate) mod #complex_modules;)*
46
47 #simple_items
48 #dispatch
49 #registered
50 #register_equipped
51 #cast_hooks
52 #impact_hooks
53 },
54 &[FileComment::inner_doc(
55 "@generated by codegen-cli from manifests/items.toml -- do not edit by hand.",
56 )?],
57 )?)
58}
59
60fn simple_items_table(items: &[(String, &str, &ItemManifestEntry)]) -> TokenStream {
61 let entries = items
62 .iter()
63 .filter(|(_, _, item)| is_simple_equip_aura(item))
64 .map(|(_, key, item)| {
65 let item_id = lit_int(item.id);
66 let display = lit_str(&to_display_name(key));
67 let aura_ids = item.auras.values().map(|aura| lit_int(aura.id));
68
69 quote!((#item_id, #display, &[#(#aura_ids,)*]))
70 });
71
72 quote! {
73 const SIMPLE_EQUIP_AURA_ITEMS: &[(u32, &str, &[u32])] = &[
74 #(#entries,)*
75 ];
76 }
77}
78
79fn items_dispatch_table(
80 items: &[(String, &str, &ItemManifestEntry)],
81) -> anyhow::Result<TokenStream> {
82 let entries = items
83 .iter()
84 .map(|(module, _, item)| {
85 let item_id = lit_int(item.id);
86 let module = rust_ident(module)?;
87
88 Ok::<_, syn::Error>(quote!((#item_id, #module::register)))
89 })
90 .collect::<Result<Vec<_>, _>>()?;
91
92 Ok(quote! {
93 type ItemRegisterFn = fn(
94 CombatSystemBuilder,
95 &ResolvedGameData,
96 wowlab_engine_gamedata::ItemBudget,
97 ) -> CombatSystemBuilder;
98
99 const ITEMS: &[(u32, ItemRegisterFn)] = &[#(#entries,)*];
100 })
101}
102
103fn aura_scalar_ref_ids(aura: &ManifestAuraDef) -> Vec<u32> {
104 aura.scalar_refs()
105 .filter_map(ScalarRef::effect_spell_id)
106 .collect()
107}
108
109fn registered_items(items: &[(String, &str, &ItemManifestEntry)]) -> TokenStream {
110 let registered = items.iter().map(|(_, key, item)| {
111 let item_id = lit_int(item.id);
112 let key = lit_str(key);
113
114 quote!((#item_id, #key))
115 });
116 let resolve_ids = items.iter().map(|(_, _, item)| {
117 let mut ids = item.auras.values().map(|aura| aura.id).collect::<Vec<_>>();
118
119 ids.extend(item.driver_spell_id);
120 ids.extend(item.effects.values().map(|effect| effect.spell_id));
121
122 for aura in item.auras.values() {
123 ids.extend(aura_scalar_ref_ids(aura));
124 }
125
126 for spell in item.spells.values() {
127 ids.push(spell.id);
128 }
129
130 ids.sort_unstable();
131 ids.dedup();
132
133 let item_id = lit_int(item.id);
134 let ids = ids.into_iter().map(lit_int);
135
136 quote!(ResolveDependencies::new(#item_id, &[#(#ids,)*]))
137 });
138 let use_spells = items.iter().filter_map(|(_, _, item)| {
139 item.spells.get("USE").map(|spell| {
140 let item_id = lit_int(item.id);
141 let spell_id = lit_int(spell.id);
142
143 quote!((#item_id, #spell_id))
144 })
145 });
146 let damage_identities = items.iter().map(|(module, _, item)| {
147 let module = format_ident!("{module}");
148 let item_id = lit_int(item.id);
149
150 quote! {
151 (#item_id, wowlab_engine_combat::generated_identities::items::#module::damage::ALL)
152 }
153 });
154 let documentation =
155 lit_str("Generated item spell identifiers and display names in manifest order.");
156
157 quote! {
158 #[doc = #documentation]
159 pub const REGISTERED_ITEMS: &[(u32, &str)] = &[#(#registered,)*];
160
161 pub(crate) const ITEM_RESOLVE_IDS: &[ResolveDependencies] = &[
162 #(#resolve_ids,)*
163 ];
164
165 pub(crate) const ITEM_USE_SPELLS: &[(u32, u32)] = &[#(#use_spells,)*];
166 pub(crate) const ITEM_DAMAGE_IDENTITIES: &[(u32, &[wowlab_engine_combat::DamageActionId])] =
167 &[#(#damage_identities,)*];
168 }
169}
170
171fn register_equipped_function() -> TokenStream {
172 quote! {
173 pub(crate) fn register_equipped(
174 mut builder: CombatSystemBuilder,
175 data: &ResolvedGameData,
176 equipped: &[wowlab_engine_ports::EquippedItem],
177 ) -> Result<CombatSystemBuilder, wowlab_engine_ports::EngineError> {
178 for item in equipped {
179 if let Some((_, identities)) = ITEM_DAMAGE_IDENTITIES
180 .iter()
181 .find(|(item_id, _)| *item_id == item.item_id)
182 {
183 builder = builder.effect_identities(identities);
184 }
185
186 if let Some(&(_, register)) =
187 ITEMS.iter().find(|(registered_id, _)| *registered_id == item.item_id)
188 {
189 builder = register(
190 builder,
191 data,
192 crate::items::item_budget(data, item)?,
193 );
194 }
195
196 if let Some(&(_, display, aura_ids)) = SIMPLE_EQUIP_AURA_ITEMS
197 .iter()
198 .find(|(registered_id, _, _)| *registered_id == item.item_id)
199 {
200 builder = crate::items::register_simple_equip_auras(
201 data,
202 display,
203 aura_ids,
204 || builder,
205 );
206 }
207
208 if let Some(&(_, use_spell)) = ITEM_USE_SPELLS
209 .iter()
210 .find(|(registered_id, _)| *registered_id == item.item_id)
211 {
212 builder = builder.item_use_spell(item.slot, use_spell);
213 }
214 }
215
216 builder = register_player_cast_hooks(builder, equipped);
217 builder = register_player_impact_hooks(builder, data, equipped);
218
219 Ok(builder)
220 }
221 }
222}
223
224fn player_cast_hooks(items: &[(String, &str, &ItemManifestEntry)]) -> anyhow::Result<TokenStream> {
225 let hooks = items
226 .iter()
227 .filter_map(|(module, _, item)| {
228 item.player_cast_hook
229 .as_ref()
230 .map(|hook| (module.as_str(), *item, hook))
231 })
232 .map(|(module, item, hook)| {
233 let item_id = lit_int(item.id);
234 let module = rust_ident(module)?;
235 let hook = rust_ident(hook)?;
236
237 Ok::<_, syn::Error>(quote!((#item_id, crate::hooks::items::#module::#hook)))
238 })
239 .collect::<Result<Vec<_>, _>>()?;
240
241 Ok(if hooks.is_empty() {
242 quote! {
243 fn register_player_cast_hooks(
244 builder: CombatSystemBuilder,
245 _equipped: &[wowlab_engine_ports::EquippedItem],
246 ) -> CombatSystemBuilder {
247 builder
248 }
249 }
250 } else {
251 quote! {
252 const PLAYER_CAST_HOOKS: &[(u32, wowlab_engine_combat::CastHookFn)] = &[
253 #(#hooks,)*
254 ];
255
256 fn register_player_cast_hooks(
257 mut builder: CombatSystemBuilder,
258 equipped: &[wowlab_engine_ports::EquippedItem],
259 ) -> CombatSystemBuilder {
260 for item in equipped {
261 if let Some(&(_, hook)) = PLAYER_CAST_HOOKS
262 .iter()
263 .find(|(registered_id, _)| *registered_id == item.item_id)
264 {
265 builder = builder.on_player_cast(hook);
266 }
267 }
268
269 builder
270 }
271 }
272 })
273}
274
275fn player_impact_hooks(
276 items: &[(String, &str, &ItemManifestEntry)],
277) -> anyhow::Result<TokenStream> {
278 let hooks = items
279 .iter()
280 .filter_map(|(module, _, item)| {
281 item.player_impact_hook
282 .as_ref()
283 .map(|hook| (module.as_str(), *item, hook))
284 })
285 .map(|(module, item, hook)| {
286 let item_id = lit_int(item.id);
287 let driver_spell_id = item.driver_spell_id.map_or_else(
288 || quote!(None),
289 |driver| {
290 let driver = lit_int(driver);
291
292 quote!(Some(#driver))
293 },
294 );
295 let module = rust_ident(module)?;
296 let hook_name = rust_ident(&hook.hook)?;
297 let periodic_only = hook.periodic_only;
298 let skip_periodic = hook.skip_periodic;
299 let crit_only = hook.crit_only;
300
301 Ok::<_, syn::Error>(quote! {
302 (
303 #item_id,
304 #driver_spell_id,
305 crate::hooks::items::#module::#hook_name,
306 #periodic_only,
307 #skip_periodic,
308 #crit_only,
309 )
310 })
311 })
312 .collect::<Result<Vec<_>, _>>()?;
313
314 Ok(if hooks.is_empty() {
315 quote! {
316 fn register_player_impact_hooks(
317 builder: CombatSystemBuilder,
318 _data: &ResolvedGameData,
319 _equipped: &[wowlab_engine_ports::EquippedItem],
320 ) -> CombatSystemBuilder {
321 builder
322 }
323 }
324 } else {
325 quote! {
326 type PlayerImpactHookEntry = (
327 u32,
328 Option<u32>,
329 wowlab_engine_combat::ImpactProcFn,
330 bool,
331 bool,
332 bool,
333 );
334
335 const PLAYER_IMPACT_HOOKS: &[PlayerImpactHookEntry] = &[#(#hooks,)*];
336
337 fn register_player_impact_hooks(
338 mut builder: CombatSystemBuilder,
339 data: &ResolvedGameData,
340 equipped: &[wowlab_engine_ports::EquippedItem],
341 ) -> CombatSystemBuilder {
342 for item in equipped {
343 if let Some(&(
344 _,
345 driver_spell_id,
346 hook,
347 periodic_only,
348 skip_periodic,
349 crit_only,
350 )) = PLAYER_IMPACT_HOOKS
351 .iter()
352 .find(|(registered_id, ..)| *registered_id == item.item_id)
353 {
354 builder =
355 builder.register_impact_proc(wowlab_engine_combat::ImpactProc {
356 driver: driver_spell_id.map_or_else(
357 wowlab_engine_combat::ProcDriver::default,
358 |driver| {
359 wowlab_engine_combat::ProcDriver::from_spell(
360 data,
361 driver,
362 wowlab_engine_combat::ProcTriggerSide::Caster,
363 )
364 },
365 ),
366 actor_filter:
367 wowlab_engine_combat::ImpactActorFilter::Player,
368 chance: 1.0,
369 fire: hook,
370 spell_filter: None,
371 periodic_only,
372 skip_periodic,
373 crit_only,
374 });
375 }
376 }
377
378 builder
379 }
380 }
381 })
382}
383
384#[cfg(test)]
385mod tests {
386 use googletest::prelude::*;
387
388 use super::*;
389 use crate::rust_source::contains_source as contains_substring;
390
391 #[gtest]
392 fn player_impact_hook_generates_filtered_impact_proc_registration() -> Result<()> {
393 let manifest: ItemsManifest = toml::from_str(
394 r#"
395 schema_version = 1
396
397 [items.IMPACT_ITEM]
398 id = 42
399 driver_spell_id = 123
400 player_impact_hook = { hook = "on_player_impact", skip_periodic = true }
401 "#,
402 )
403 .or_fail()?;
404 let generated = generate_items_mod_file(&manifest).or_fail()?;
405
406 verify_that!(
407 generated.as_str(),
408 all!(
409 contains_substring("crate::hooks::items::impact_item::on_player_impact"),
410 contains_substring("const PLAYER_IMPACT_HOOKS: &[PlayerImpactHookEntry]"),
411 contains_substring("builder.register_impact_proc(wowlab_engine_combat::ImpactProc"),
412 contains_substring("actor_filter: wowlab_engine_combat::ImpactActorFilter::Player"),
413 contains_substring("wowlab_engine_combat::ProcDriver::from_spell("),
414 contains_substring("wowlab_engine_combat::ProcTriggerSide::Caster"),
415 contains_substring("Result<CombatSystemBuilder, wowlab_engine_ports::EngineError>"),
416 contains_substring("crate::items::item_budget(data, item)?"),
417 contains_substring("Ok(builder)"),
418 contains_substring(
419 "builder = register_player_impact_hooks(builder, data, equipped);"
420 )
421 )
422 )
423 }
424}