codegen/
gen_descriptor.rs1use 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}