Skip to main content

wowlab_parsers/parsers/spell_desc/
analyzer.rs

1use wowlab_types::spell_desc::SpellDescDependencies;
2
3use super::types::{
4    ConditionalPredicateNode, ExpressionNode, ParsedSpellDescription, SingleConditionNode,
5    SpellDescriptionNode, VariableNode,
6};
7
8/// Extract render dependencies; `self_spell_id` resolves self-references like `$s2`.
9#[must_use]
10pub fn analyze_dependencies(
11    ast: &ParsedSpellDescription,
12    self_spell_id: u32,
13) -> SpellDescDependencies {
14    let mut deps = SpellDescDependencies::new();
15
16    deps.add_spell_id(self_spell_id);
17
18    for node in &ast.nodes {
19        analyze_node(node, self_spell_id, &mut deps);
20    }
21
22    deps
23}
24
25// #t(fn: rust_recursive_fn) bounded AST walk over parsed spell description nodes
26fn analyze_node(node: &SpellDescriptionNode, self_spell_id: u32, deps: &mut SpellDescDependencies) {
27    match node {
28        SpellDescriptionNode::Variable(var) => {
29            analyze_variable(var, self_spell_id, deps);
30        }
31
32        SpellDescriptionNode::ExpressionBlock(block) => {
33            analyze_expression(&block.expression, self_spell_id, deps);
34        }
35
36        SpellDescriptionNode::Conditional(cond) => {
37            for branch in &cond.conditions {
38                analyze_predicate(&branch.predicate, deps);
39
40                for node in &branch.content {
41                    analyze_node(node, self_spell_id, deps);
42                }
43            }
44
45            if let Some(else_branch) = &cond.else_branch {
46                for node in else_branch {
47                    analyze_node(node, self_spell_id, deps);
48                }
49            }
50        }
51
52        SpellDescriptionNode::Text(_)
53        | SpellDescriptionNode::ColorCode(_)
54        | SpellDescriptionNode::Pluralization(_) => {}
55
56        SpellDescriptionNode::Gender(_) => {
57            deps.needs_gender = true;
58        }
59    }
60}
61
62fn analyze_variable(var: &VariableNode, self_spell_id: u32, deps: &mut SpellDescDependencies) {
63    match var {
64        VariableNode::Effect(effect) => {
65            deps.add_effect(self_spell_id, effect.effect_index, effect.var_type.clone());
66        }
67
68        VariableNode::SpellLevel(spell_level) => {
69            deps.add_spell_value(self_spell_id, spell_level.var_type.clone());
70        }
71
72        VariableNode::Player(player) => {
73            deps.add_player_stat(player.var_name().to_owned());
74        }
75
76        VariableNode::CrossSpell(cross) => {
77            if let Some(idx) = cross.effect_index {
78                deps.add_effect(cross.spell_id, idx, cross.var_type.clone());
79            } else {
80                deps.add_spell_value(cross.spell_id, cross.var_type.clone());
81            }
82        }
83
84        VariableNode::Custom(custom) => {
85            deps.add_custom_var(custom.var_name().to_owned());
86        }
87
88        VariableNode::At(at) => match at.var_type.as_str() {
89            "spelldesc" | "spellname" | "spellicon" => {
90                if let Some(spell_id) = at.spell_id {
91                    deps.add_embedded_spell(spell_id);
92                }
93            }
94            _ => {
95                deps.add_player_stat(at.var_type.clone());
96            }
97        },
98
99        VariableNode::Enchant(_) => {}
100
101        VariableNode::Misc(misc) => match misc.var_name.as_str() {
102            "maxcast" | "pctD" | "W" | "B" => {
103                deps.add_player_stat(misc.var_name.clone());
104            }
105            _ => {}
106        },
107    }
108}
109
110// #t(fn: rust_recursive_fn) bounded AST walk over expression tree nodes
111fn analyze_expression(expr: &ExpressionNode, self_spell_id: u32, deps: &mut SpellDescDependencies) {
112    match expr {
113        ExpressionNode::Variable(var) => {
114            analyze_variable(var, self_spell_id, deps);
115        }
116
117        ExpressionNode::Binary(binary) => {
118            analyze_expression(&binary.left, self_spell_id, deps);
119            analyze_expression(&binary.right, self_spell_id, deps);
120        }
121
122        ExpressionNode::Unary(unary) => {
123            analyze_expression(&unary.operand, self_spell_id, deps);
124        }
125
126        ExpressionNode::FunctionCall(call) => {
127            for arg in &call.args {
128                analyze_expression(arg, self_spell_id, deps);
129            }
130        }
131
132        ExpressionNode::Number(_) => {}
133
134        ExpressionNode::Paren(paren) => {
135            analyze_expression(&paren.expression, self_spell_id, deps);
136        }
137    }
138}
139
140fn analyze_predicate(predicate: &ConditionalPredicateNode, deps: &mut SpellDescDependencies) {
141    for condition in &predicate.conditions {
142        match condition {
143            SingleConditionNode::SpellKnown(spell_known) => {
144                deps.add_spell_known_check(spell_known.spell_id());
145            }
146
147            SingleConditionNode::Aura(aura) => {
148                deps.add_aura_check(aura.aura_id());
149            }
150
151            SingleConditionNode::Specialization(spec) => {
152                deps.add_specialization_check(spec.spec_index());
153            }
154
155            SingleConditionNode::PlayerCondition(_) => {}
156
157            SingleConditionNode::Expression(expr_cond) => {
158                for arg in &expr_cond.args {
159                    analyze_expression(arg, 0, deps);
160                }
161            }
162        }
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use googletest::prelude::*;
169    use insta::assert_debug_snapshot;
170    use wowlab_types::spell_desc::{EffectDependency, SpellValueDependency};
171
172    use super::*;
173    use crate::parsers::spell_desc::parse;
174
175    fn deps(input: &str, self_spell_id: u32) -> SpellDescDependencies {
176        analyze_dependencies(&parse(input).ast, self_spell_id)
177    }
178
179    #[gtest]
180    fn self_ref_adds_self_spell_and_effect() -> Result<()> {
181        let d = deps("$s1", 100);
182
183        verify_that!(d.spell_ids, contains(eq(&100u32)))?;
184        verify_that!(
185            d.effects,
186            contains(eq(&EffectDependency {
187                spell_id: 100,
188                effect_index: 1,
189                var_type: "s".to_string(),
190            }))
191        )?;
192
193        Ok(())
194    }
195
196    #[gtest]
197    fn cross_spell_with_effect_index() -> Result<()> {
198        let d = deps("$12345s2", 100);
199
200        verify_that!(d.spell_ids, contains(eq(&12345u32)))?;
201        verify_that!(d.spell_ids, contains(eq(&100u32)))?;
202        verify_that!(
203            d.effects,
204            contains(eq(&EffectDependency {
205                spell_id: 12345,
206                effect_index: 2,
207                var_type: "s".to_string(),
208            }))
209        )?;
210
211        Ok(())
212    }
213
214    #[gtest]
215    fn cross_spell_value_form() -> Result<()> {
216        let d = deps("$12345d", 100);
217
218        verify_that!(d.spell_ids, contains(eq(&12345u32)))?;
219        verify_that!(
220            d.spell_values,
221            contains(eq(&SpellValueDependency {
222                spell_id: 12345,
223                var_type: "d".to_string(),
224            }))
225        )?;
226
227        Ok(())
228    }
229
230    #[gtest]
231    fn gender_node_sets_needs_gender() -> Result<()> {
232        verify_that!(deps("$ghis:her;", 100).needs_gender, eq(true))?;
233        verify_that!(deps("$s1", 100).needs_gender, eq(false))?;
234
235        Ok(())
236    }
237
238    #[gtest]
239    fn conditional_predicates_collected() -> Result<()> {
240        verify_that!(
241            deps("$?s99[a][b]", 100).spell_known_checks,
242            contains(eq(&99u32))
243        )?;
244        verify_that!(deps("$?a88[a][b]", 100).aura_checks, contains(eq(&88u32)))?;
245        verify_that!(
246            deps("$?c1[a][b]", 100).specialization_checks,
247            contains(eq(&1u8))
248        )?;
249
250        Ok(())
251    }
252
253    #[gtest]
254    fn embedded_and_player_stat_at_vars() -> Result<()> {
255        verify_that!(
256            deps("$@spelldesc12345", 100).embedded_spell_ids,
257            contains(eq(&12345u32))
258        )?;
259        verify_that!(
260            deps("$@spellname12345", 100).embedded_spell_ids,
261            contains(eq(&12345u32))
262        )?;
263        verify_that!(
264            deps("$@spellicon12345", 100).embedded_spell_ids,
265            contains(eq(&12345u32))
266        )?;
267        verify_that!(
268            deps("$@somestat", 100).player_stats,
269            contains(eq("somestat"))
270        )?;
271
272        Ok(())
273    }
274
275    #[gtest]
276    fn self_id_always_present() -> Result<()> {
277        verify_that!(deps("plain text", 777).spell_ids, contains(eq(&777u32)))?;
278
279        Ok(())
280    }
281
282    #[gtest]
283    fn complex_description_deps_snapshot() {
284        let d = deps(
285            "$s1 $12345s2 $?s99[known][unknown] $ghis:her; $@spelldesc500",
286            100,
287        );
288
289        assert_debug_snapshot!(d);
290    }
291}