Skip to main content

codegen/
gen_emit.rs

1//! Shared low-level typed emission primitives for spell, aura, and item generators.
2
3use anyhow::bail;
4use proc_macro2::TokenStream;
5use quote::{format_ident, quote};
6use wowlab_manifest_schema::{DamageSchoolKind, ScalarField, ScalarRef, SpellScalarField};
7
8use crate::rust_source::{lit_f64, lit_int, lit_str};
9
10/// Emit the engine damage flags for a manifest damage school.
11pub(crate) fn damage_flags(school: DamageSchoolKind) -> TokenStream {
12    match school {
13        DamageSchoolKind::Physical => quote!(wowlab_engine_combat::DamageFlags::PHYSICAL),
14        DamageSchoolKind::Magic => quote!(wowlab_engine_combat::DamageFlags::empty()),
15    }
16}
17
18/// Emit a typed spell-index constructor for a raw spell ID.
19pub(crate) fn spell_idx(spell_id: u32) -> TokenStream {
20    let spell_id = lit_int(spell_id);
21
22    quote!(wowlab_types::sim::SpellIdx::from_raw(#spell_id))
23}
24
25pub(crate) fn effect_lookup(data: &TokenStream, spell_id: u32, effect_index: u8) -> TokenStream {
26    let spell_id = spell_idx(spell_id);
27    let effect_index = lit_int(effect_index);
28
29    quote! {
30        wowlab_engine_domain::dbc::EffectLookup::new(
31            #data,
32            wowlab_types::sim::EffectRef::new(#spell_id, #effect_index),
33        )
34    }
35}
36
37/// Emit a `data.require_<field>(<argument>)?` accessor call.
38pub(crate) fn require_get(field: &str, argument: &TokenStream) -> TokenStream {
39    let method = format_ident!("require_{field}");
40
41    quote!(data.#method(#argument)?)
42}
43
44/// Emit a [`ScalarRef`] as either a literal or a game-data accessor.
45pub(crate) fn scalar_ref_call(scalar: &ScalarRef) -> anyhow::Result<TokenStream> {
46    scalar_ref_call_with_data(scalar, &quote!(data))
47}
48
49/// Emit a [`ScalarRef`] using the provided game-data expression as the accessor receiver.
50pub(crate) fn scalar_ref_call_with_data(
51    scalar: &ScalarRef,
52    data: &TokenStream,
53) -> anyhow::Result<TokenStream> {
54    let (mut value, multiplier, offset, round_decimals) = match scalar {
55        ScalarRef::Literal(value) => {
56            let value = lit_f64(*value);
57
58            return Ok(quote!(#value));
59        }
60        ScalarRef::EffectRef {
61            spell_id,
62            effect,
63            field,
64            multiplier,
65            offset,
66            round_decimals,
67        } => {
68            let spell_id = spell_idx(*spell_id);
69            let effect = lit_int(effect);
70            let method = match field {
71                ScalarField::BasePoints => format_ident!("base_points"),
72                ScalarField::ApCoef => format_ident!("ap_coef"),
73                ScalarField::SpCoef => format_ident!("sp_coef"),
74                ScalarField::Amplitude => format_ident!("amplitude"),
75                ScalarField::Period => format_ident!("period"),
76                ScalarField::Coefficient => format_ident!("coefficient"),
77                other => bail!("unknown scalar field: {other:?}"),
78            };
79
80            (
81                quote!(#data.#method(#spell_id, #effect)),
82                multiplier,
83                offset,
84                round_decimals,
85            )
86        }
87        ScalarRef::SpellRef {
88            spell_id,
89            field,
90            multiplier,
91            offset,
92            round_decimals,
93        } => {
94            let spell_id = spell_idx(*spell_id);
95            let (method, cast) = match field {
96                SpellScalarField::DurationMs => (format_ident!("require_aura_duration_ms"), true),
97                SpellScalarField::MaxStacks => (format_ident!("require_aura_max_stacks"), true),
98                SpellScalarField::Cooldown => (format_ident!("require_cooldown_s"), false),
99                SpellScalarField::Cost => (format_ident!("require_cost"), false),
100                SpellScalarField::CastTimeMs => (format_ident!("require_cast_time_ms"), true),
101                SpellScalarField::Charges => (format_ident!("require_charges"), true),
102                SpellScalarField::ChargeCd => (format_ident!("require_charge_cd_s"), false),
103                SpellScalarField::GcdMs => (format_ident!("require_gcd_ms"), true),
104                SpellScalarField::ProcCategoryRecoveryMs => {
105                    (format_ident!("require_proc_category_recovery_ms"), true)
106                }
107                other => bail!("unknown spell scalar field: {other:?}"),
108            };
109            let call = quote!(#data.#method(#spell_id)?);
110
111            (
112                if cast { quote!(f64::from(#call)) } else { call },
113                multiplier,
114                offset,
115                round_decimals,
116            )
117        }
118    };
119
120    if let Some(multiplier) = multiplier {
121        let multiplier = lit_f64(*multiplier);
122
123        value = quote!((#value) * #multiplier);
124    }
125
126    if let Some(offset) = offset {
127        let offset = lit_f64(*offset);
128
129        value = quote!((#value) + #offset);
130    }
131
132    if let Some(decimals) = round_decimals {
133        let scale = lit_f64(10_f64.powi(i32::from(*decimals)));
134
135        value = quote!(((#value) * #scale).round() / #scale);
136    }
137
138    Ok(value)
139}
140
141/// Emit a scalar as a validated integral millisecond value.
142pub(crate) fn validated_milliseconds_call(
143    value: &ScalarRef,
144    owner_id: u32,
145    field: &str,
146) -> anyhow::Result<TokenStream> {
147    validated_scalar_call("validated_milliseconds", value, owner_id, field)
148}
149
150fn validated_scalar_call(
151    validator: &str,
152    value: &ScalarRef,
153    owner_id: u32,
154    field: &str,
155) -> anyhow::Result<TokenStream> {
156    let validator = format_ident!("{validator}");
157    let value = scalar_ref_call(value)?;
158    let owner_id = lit_int(owner_id);
159    let field = lit_str(field);
160
161    Ok(quote! {
162        wowlab_engine_combat::#validator(#value, #owner_id, #field)?
163    })
164}
165
166/// Emit a scalar as a validated non-negative value.
167pub(crate) fn validated_nonnegative_call(
168    value: &ScalarRef,
169    owner_id: u32,
170    field: &str,
171) -> anyhow::Result<TokenStream> {
172    validated_scalar_call("validated_nonnegative", value, owner_id, field)
173}
174
175/// Emit a scalar as a validated aura stack cap.
176pub(crate) fn validated_max_stacks_call(
177    value: &ScalarRef,
178    aura_id: u32,
179    field: &str,
180) -> anyhow::Result<TokenStream> {
181    validated_scalar_call("validated_max_stacks", value, aura_id, field)
182}
183
184/// Emit a scalar as a validated channel tick count.
185pub(crate) fn validated_channel_tick_count_call(
186    value: &ScalarRef,
187    spell_id: u32,
188    field: &str,
189) -> anyhow::Result<TokenStream> {
190    validated_scalar_call("validated_channel_tick_count", value, spell_id, field)
191}
192
193/// Emit a scalar as a validated spell charge count.
194pub(crate) fn validated_charges_call(
195    value: &ScalarRef,
196    spell_id: u32,
197    field: &str,
198) -> anyhow::Result<TokenStream> {
199    validated_scalar_call("validated_charges", value, spell_id, field)
200}
201
202#[cfg(test)]
203mod tests {
204    use googletest::prelude::*;
205
206    use super::*;
207
208    #[gtest]
209    fn scalar_transform_emits_multiplier_then_offset_then_rounding() -> Result<()> {
210        let scalar = ScalarRef::EffectRef {
211            spell_id: 42,
212            effect: 1,
213            field: ScalarField::BasePoints,
214            multiplier: Some(10.0),
215            offset: Some(0.004),
216            round_decimals: Some(2),
217        };
218
219        let expected = quote!(
220            ((((data.base_points(wowlab_types::sim::SpellIdx::from_raw(42), 1)) * 10.0) + 0.004)
221                * 100.0)
222                .round()
223                / 100.0
224        )
225        .to_string();
226
227        verify_that!(
228            scalar_ref_call(&scalar).or_fail()?.to_string(),
229            eq(&expected)
230        )
231    }
232
233    #[gtest]
234    fn spell_property_ref_emits_required_accessor_and_affine_transform() -> Result<()> {
235        let scalar = ScalarRef::SpellRef {
236            spell_id: 42,
237            field: SpellScalarField::DurationMs,
238            multiplier: Some(0.8),
239            offset: Some(1.0),
240            round_decimals: Some(0),
241        };
242
243        let emitted = scalar_ref_call(&scalar).or_fail()?.to_string();
244
245        verify_that!(
246            emitted,
247            all!(
248                contains_substring("data . require_aura_duration_ms"),
249                contains_substring("* 0.8"),
250                contains_substring("+ 1.0"),
251                contains_substring(". round ()")
252            )
253        )
254    }
255
256    #[gtest]
257    fn scalar_ref_accepts_an_explicit_game_data_receiver() -> Result<()> {
258        let scalar = ScalarRef::EffectRef {
259            spell_id: 42,
260            effect: 1,
261            field: ScalarField::BasePoints,
262            multiplier: None,
263            offset: None,
264            round_decimals: None,
265        };
266
267        verify_that!(
268            scalar_ref_call_with_data(&scalar, &quote!(params.game_data))
269                .or_fail()?
270                .to_string(),
271            eq(
272                "params . game_data . base_points (wowlab_types :: sim :: SpellIdx :: from_raw (42) , 1)"
273            )
274        )
275    }
276}