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 documentation =
147 lit_str("Generated item spell identifiers and display names in manifest order.");
148
149 quote! {
150 #[doc = #documentation]
151 pub const REGISTERED_ITEMS: &[(u32, &str)] = &[#(#registered,)*];
152
153 pub(crate) const ITEM_RESOLVE_IDS: &[ResolveDependencies] = &[
154 #(#resolve_ids,)*
155 ];
156
157 pub(crate) const ITEM_USE_SPELLS: &[(u32, u32)] = &[#(#use_spells,)*];
158 }
159}
160
161fn register_equipped_function() -> TokenStream {
162 quote! {
163 pub(crate) fn register_equipped(
164 mut builder: CombatSystemBuilder,
165 data: &ResolvedGameData,
166 equipped: &[wowlab_engine_ports::EquippedItem],
167 ) -> Result<CombatSystemBuilder, wowlab_engine_ports::EngineError> {
168 for item in equipped {
169 if let Some(&(_, register)) =
170 ITEMS.iter().find(|(registered_id, _)| *registered_id == item.item_id)
171 {
172 builder = register(
173 builder,
174 data,
175 crate::items::item_budget(data, item)?,
176 );
177 }
178
179 if let Some(&(_, display, aura_ids)) = SIMPLE_EQUIP_AURA_ITEMS
180 .iter()
181 .find(|(registered_id, _, _)| *registered_id == item.item_id)
182 {
183 builder = crate::items::register_simple_equip_auras(
184 data,
185 display,
186 aura_ids,
187 || builder,
188 );
189 }
190
191 if let Some(&(_, use_spell)) = ITEM_USE_SPELLS
192 .iter()
193 .find(|(registered_id, _)| *registered_id == item.item_id)
194 {
195 builder = builder.item_use_spell(item.slot, use_spell);
196 }
197 }
198
199 builder = register_player_cast_hooks(builder, equipped);
200 builder = register_player_impact_hooks(builder, data, equipped);
201
202 Ok(builder)
203 }
204 }
205}
206
207fn player_cast_hooks(items: &[(String, &str, &ItemManifestEntry)]) -> anyhow::Result<TokenStream> {
208 let hooks = items
209 .iter()
210 .filter_map(|(module, _, item)| {
211 item.player_cast_hook
212 .as_ref()
213 .map(|hook| (module.as_str(), *item, hook))
214 })
215 .map(|(module, item, hook)| {
216 let item_id = lit_int(item.id);
217 let module = rust_ident(module)?;
218 let hook = rust_ident(hook)?;
219
220 Ok::<_, syn::Error>(quote!((#item_id, crate::hooks::items::#module::#hook)))
221 })
222 .collect::<Result<Vec<_>, _>>()?;
223
224 Ok(if hooks.is_empty() {
225 quote! {
226 fn register_player_cast_hooks(
227 builder: CombatSystemBuilder,
228 _equipped: &[wowlab_engine_ports::EquippedItem],
229 ) -> CombatSystemBuilder {
230 builder
231 }
232 }
233 } else {
234 quote! {
235 const PLAYER_CAST_HOOKS: &[(u32, wowlab_engine_combat::CastHookFn)] = &[
236 #(#hooks,)*
237 ];
238
239 fn register_player_cast_hooks(
240 mut builder: CombatSystemBuilder,
241 equipped: &[wowlab_engine_ports::EquippedItem],
242 ) -> CombatSystemBuilder {
243 for item in equipped {
244 if let Some(&(_, hook)) = PLAYER_CAST_HOOKS
245 .iter()
246 .find(|(registered_id, _)| *registered_id == item.item_id)
247 {
248 builder = builder.on_player_cast(hook);
249 }
250 }
251
252 builder
253 }
254 }
255 })
256}
257
258fn player_impact_hooks(
259 items: &[(String, &str, &ItemManifestEntry)],
260) -> anyhow::Result<TokenStream> {
261 let hooks = items
262 .iter()
263 .filter_map(|(module, _, item)| {
264 item.player_impact_hook
265 .as_ref()
266 .map(|hook| (module.as_str(), *item, hook))
267 })
268 .map(|(module, item, hook)| {
269 let item_id = lit_int(item.id);
270 let driver_spell_id = item.driver_spell_id.map_or_else(
271 || quote!(None),
272 |driver| {
273 let driver = lit_int(driver);
274
275 quote!(Some(#driver))
276 },
277 );
278 let module = rust_ident(module)?;
279 let hook_name = rust_ident(&hook.hook)?;
280 let periodic_only = hook.periodic_only;
281 let skip_periodic = hook.skip_periodic;
282 let crit_only = hook.crit_only;
283
284 Ok::<_, syn::Error>(quote! {
285 (
286 #item_id,
287 #driver_spell_id,
288 crate::hooks::items::#module::#hook_name,
289 #periodic_only,
290 #skip_periodic,
291 #crit_only,
292 )
293 })
294 })
295 .collect::<Result<Vec<_>, _>>()?;
296
297 Ok(if hooks.is_empty() {
298 quote! {
299 fn register_player_impact_hooks(
300 builder: CombatSystemBuilder,
301 _data: &ResolvedGameData,
302 _equipped: &[wowlab_engine_ports::EquippedItem],
303 ) -> CombatSystemBuilder {
304 builder
305 }
306 }
307 } else {
308 quote! {
309 type PlayerImpactHookEntry = (
310 u32,
311 Option<u32>,
312 wowlab_engine_combat::ImpactProcFn,
313 bool,
314 bool,
315 bool,
316 );
317
318 const PLAYER_IMPACT_HOOKS: &[PlayerImpactHookEntry] = &[#(#hooks,)*];
319
320 fn register_player_impact_hooks(
321 mut builder: CombatSystemBuilder,
322 data: &ResolvedGameData,
323 equipped: &[wowlab_engine_ports::EquippedItem],
324 ) -> CombatSystemBuilder {
325 for item in equipped {
326 if let Some(&(
327 _,
328 driver_spell_id,
329 hook,
330 periodic_only,
331 skip_periodic,
332 crit_only,
333 )) = PLAYER_IMPACT_HOOKS
334 .iter()
335 .find(|(registered_id, ..)| *registered_id == item.item_id)
336 {
337 builder =
338 builder.register_impact_proc(wowlab_engine_combat::ImpactProc {
339 driver: driver_spell_id.map_or_else(
340 wowlab_engine_combat::ProcDriver::default,
341 |driver| {
342 wowlab_engine_combat::ProcDriver::from_spell(
343 data,
344 driver,
345 wowlab_engine_combat::ProcTriggerSide::Caster,
346 )
347 },
348 ),
349 actor_filter:
350 wowlab_engine_combat::ImpactActorFilter::Player,
351 chance: 1.0,
352 fire: hook,
353 spell_filter: None,
354 periodic_only,
355 skip_periodic,
356 crit_only,
357 });
358 }
359 }
360
361 builder
362 }
363 }
364 })
365}
366
367#[cfg(test)]
368mod tests {
369 use googletest::prelude::*;
370
371 use super::*;
372 use crate::rust_source::contains_source as contains_substring;
373
374 #[gtest]
375 fn player_impact_hook_generates_filtered_impact_proc_registration() -> Result<()> {
376 let manifest: ItemsManifest = toml::from_str(
377 r#"
378 schema_version = 1
379
380 [items.IMPACT_ITEM]
381 id = 42
382 driver_spell_id = 123
383 player_impact_hook = { hook = "on_player_impact", skip_periodic = true }
384 "#,
385 )
386 .or_fail()?;
387 let generated = generate_items_mod_file(&manifest).or_fail()?;
388
389 verify_that!(
390 generated.as_str(),
391 all!(
392 contains_substring("crate::hooks::items::impact_item::on_player_impact"),
393 contains_substring("const PLAYER_IMPACT_HOOKS: &[PlayerImpactHookEntry]"),
394 contains_substring("builder.register_impact_proc(wowlab_engine_combat::ImpactProc"),
395 contains_substring("actor_filter: wowlab_engine_combat::ImpactActorFilter::Player"),
396 contains_substring("wowlab_engine_combat::ProcDriver::from_spell("),
397 contains_substring("wowlab_engine_combat::ProcTriggerSide::Caster"),
398 contains_substring("Result<CombatSystemBuilder, wowlab_engine_ports::EngineError>"),
399 contains_substring("crate::items::item_budget(data, item)?"),
400 contains_substring("Ok(builder)"),
401 contains_substring(
402 "builder = register_player_impact_hooks(builder, data, equipped);"
403 )
404 )
405 )
406 }
407}