Skip to main content

wowlab_engine_combat/builder/combat_builder/build/
finish.rs

1use wowlab_types::sim::Rotation;
2
3use super::{
4    ActionOverrideRegistration, AuraDefinitionDraft, AuraSubtypeKind, BuiltCombatSystem,
5    CombatBuildError, CombatSystemBuilder, CooldownBypass, CostBypass, EngineError,
6    HeroTalentTreeDesc, ImpactProcRegistration, InitialBufferConfig, RotationCompileInput,
7    SpellGate, SpellIdx, SpellOverride, TriggerProgramRegistration, compile_rotation,
8    ensure_aura_slots, expand_spell_learn_chains, expand_spell_override_chains,
9    lower_builder_state, populate_initial_buffer,
10};
11
12pub(in crate::builder::combat_builder) struct CombatBuildResult(
13    Result<BuiltCombatSystem, CombatBuildError>,
14);
15
16impl CombatBuildResult {
17    pub(in crate::builder::combat_builder) fn into_result(
18        self,
19    ) -> Result<BuiltCombatSystem, CombatBuildError> {
20        self.0
21    }
22}
23
24pub(super) fn apply_hero_tree_spell_gating(
25    trees: &[HeroTalentTreeDesc],
26    selected_tree_names: &[Box<str>],
27    talent_spell_ids: &mut Vec<u32>,
28    selected_talent_spell_ids: &mut Vec<u32>,
29) {
30    for tree in trees {
31        let tree_selected = selected_tree_names
32            .iter()
33            .any(|name| name.eq_ignore_ascii_case(tree.name))
34            || tree
35                .spells
36                .iter()
37                .chain(tree.auras.iter())
38                .any(|&(_, id)| selected_talent_spell_ids.contains(&id));
39
40        for &(_, id) in tree.spells {
41            if !talent_spell_ids.contains(&id) {
42                talent_spell_ids.push(id);
43            }
44
45            if tree_selected && !selected_talent_spell_ids.contains(&id) {
46                selected_talent_spell_ids.push(id);
47            }
48        }
49    }
50}
51
52impl CombatSystemBuilder {
53    // #t(fn: rust_cyclomatic_complexity) sequential build steps have independent validation and registration paths
54    // #t(fn: rust_max_fn_lines) the build method keeps its ordered construction transaction together
55    pub(in crate::builder::combat_builder) fn finish_build(
56        mut self,
57        rotation: &Rotation,
58    ) -> CombatBuildResult {
59        let result = (|| -> Result<BuiltCombatSystem, CombatBuildError> {
60            if let Some(err) = self.pending_error.take() {
61                return Err(err.into());
62            }
63
64            let encounter = self.encounter.take().ok_or_else(|| {
65                EngineError::spec_construction("combat construction requires a resolved encounter")
66            })?;
67
68            let updates: Vec<(String, u32)> = self
69                .spell_ids
70                .iter()
71                .filter_map(|(name, &id)| {
72                    self.game_data
73                        .spell_override(SpellIdx::from_raw(id))
74                        .map(|replacement| (name.clone(), replacement.as_u32()))
75                })
76                .collect();
77
78            for (name, replacement) in updates {
79                self.spell_ids.insert(name, replacement);
80            }
81
82            expand_spell_learn_chains(&mut self.talent_spell_ids, &self.game_data);
83            expand_spell_learn_chains(&mut self.selected_talent_spell_ids, &self.game_data);
84
85            if self.spec_id.is_some() {
86                apply_hero_tree_spell_gating(
87                    self.hero_talent_trees,
88                    &self.selected_hero_trees,
89                    &mut self.talent_spell_ids,
90                    &mut self.selected_talent_spell_ids,
91                );
92
93                for &(name, id) in self
94                    .hero_talent_trees
95                    .iter()
96                    .flat_map(|tree| tree.spells.iter())
97                {
98                    // #t(rust_alloc_in_loop) required for map key
99                    self.spell_ids.entry(name.to_string()).or_insert(id);
100                }
101
102                for &(name, id) in self
103                    .hero_talent_trees
104                    .iter()
105                    .flat_map(|tree| tree.auras.iter())
106                {
107                    // #t(rust_alloc_in_loop) required for map key
108                    self.aura_ids.entry(name.to_string()).or_insert(id);
109                }
110            }
111
112            ActionOverrideRegistration {
113                definition: &mut self,
114            }
115            .apply_dbc_action_overrides();
116            self.register_class_target_debuffs();
117            self.apply_selected_triggered_aura_values();
118            TriggerProgramRegistration {
119                definition: &mut self,
120            }
121            .auto_register_dbc_trigger_programs();
122            ImpactProcRegistration {
123                definition: &mut self,
124            }
125            .auto_register_dbc_impact_procs();
126
127            let gating: Vec<SpellGate> = self
128                .spells
129                .iter()
130                .filter(|s| s.gating_aura_id != 0)
131                .map(|s| SpellGate {
132                    spell_id: s.spell_id,
133                    aura_id: s.gating_aura_id,
134                    min_stacks: s.gating_aura_min_stacks,
135                })
136                .collect();
137
138            let cost_bypass: Vec<CostBypass> = self
139                .spells
140                .iter()
141                .filter(|s| s.cost_bypass_aura_id != 0)
142                .map(|s| CostBypass {
143                    spell_id: s.spell_id,
144                    aura_id: s.cost_bypass_aura_id,
145                })
146                .collect();
147
148            let cooldown_bypass: Vec<CooldownBypass> = self
149                .spells
150                .iter()
151                .filter(|s| s.cooldown_bypass_aura_id != 0)
152                .map(|s| CooldownBypass {
153                    spell_id: s.spell_id,
154                    aura_id: s.cooldown_bypass_aura_id,
155                })
156                .collect();
157
158            let spell_overrides: Vec<SpellOverride> = self
159                .spells
160                .iter()
161                .filter(|spell| spell.override_aura_id != 0 && spell.override_spell_id != 0)
162                .map(|spell| SpellOverride {
163                    spell_id: spell.spell_id,
164                    replacement_spell_id: spell.override_spell_id,
165                    aura_id: spell.override_aura_id,
166                    shares_base_cooldown: spell.override_shares_base_cooldown,
167                })
168                .collect();
169
170            self.hints.primary_resource =
171                (!self.resource_name.is_empty()).then(|| self.resource_name.clone());
172
173            let (engine, mut buffer) = compile_rotation(&RotationCompileInput {
174                rotation,
175                spell_ids: &self.spell_ids,
176                aura_ids: &self.aura_ids,
177                secondary_resource_name: self.secondary_resource_name.as_deref(),
178                hints: &self.hints,
179                gating: &gating,
180                cost_bypass: &cost_bypass,
181                cooldown_bypass: &cooldown_bypass,
182                overrides: &spell_overrides,
183            })?;
184
185            // Dual wield: an equipped off-hand weapon swings on its own timer. The swing loop
186            // maps non-pet defs to hands in order (first = main hand, second = off hand), so a
187            // single-player-def spec gets its off-hand swing synthesized from the main-hand def.
188            let player_aa_count = self.auto_attacks.iter().filter(|aa| !aa.is_pet).count();
189
190            if player_aa_count == 1 && self.game_data.off_hand().is_some_and(|oh| oh.speed_ms > 0) {
191                if let Some(mh_def) = self.auto_attacks.iter().find(|aa| !aa.is_pet).cloned() {
192                    self.auto_attacks.push(mh_def);
193                }
194            }
195
196            let mut names: Vec<Box<str>> = Vec::new();
197            let parts = lower_builder_state(
198                &self.spells,
199                &self.aura_defs,
200                &self.auto_attacks,
201                &self.impact_effect_procs,
202                &mut names,
203            );
204            let resource_max = self.resource_max;
205
206            let talent_ranks = std::mem::take(&mut self.hints.talent_ranks);
207            let mut talent_spell_ids = std::mem::take(&mut self.talent_spell_ids);
208            let mut selected_talent_spell_ids = std::mem::take(&mut self.selected_talent_spell_ids);
209            let selected_replaced_spell_ids = std::mem::take(&mut self.selected_replaced_spell_ids);
210
211            expand_spell_override_chains(&mut talent_spell_ids, &self.game_data);
212            expand_spell_override_chains(&mut selected_talent_spell_ids, &self.game_data);
213
214            let state = self
215                .assemble_state(parts, &selected_talent_spell_ids, encounter)
216                .into_result()?;
217            // Every registered aura gets a buffer slot: `applies_aura`, hooks, and
218            // `on_sim_start` must work even when the rotation never reads the aura
219            // (rotation compilation only allocates APL-referenced slots, and
220            // `apply_aura` silently no-ops on a missing slot).
221
222            for aura in &state.defs.auras {
223                ensure_aura_slots(&state, &mut buffer, aura.aura_id, aura.on);
224            }
225
226            populate_initial_buffer(
227                &state,
228                &mut buffer,
229                &InitialBufferConfig {
230                    resource_max,
231                    talent_ranks: &talent_ranks,
232                    talent_spell_ids: &talent_spell_ids,
233                    selected_talent_spell_ids: &selected_talent_spell_ids,
234                    selected_replaced_spell_ids: &selected_replaced_spell_ids,
235                },
236            );
237
238            Ok(BuiltCombatSystem {
239                state,
240                rotation: engine,
241                buffer,
242                names,
243            })
244        })();
245
246        CombatBuildResult(result)
247    }
248
249    /// Registers the class's self-provided damage-taken debuffs as permanent target auras.
250    ///
251    /// A lone monk still applies Mystic Touch and a lone demon hunter still brands its target.
252    /// `optimal_raid` governs only the external raid provider (`sc_demon_hunter.cpp:2577`).
253    /// The debuff's DBC duration is infinite, so a precombat application is its whole lifecycle.
254    fn register_class_target_debuffs(&mut self) {
255        let debuffs: Vec<SpellIdx> = self.game_data.class_target_debuffs().to_vec();
256
257        for debuff in debuffs {
258            if self
259                .aura_defs
260                .iter()
261                .any(|aura| aura.aura_id == debuff.as_u32())
262            {
263                continue;
264            }
265
266            // #t(rust_alloc_in_loop) each registered aura requires its own stable map key
267            let name = format!("class debuff {}", debuff.as_u32());
268            let draft = match AuraDefinitionDraft::new(&name, debuff.as_u32())
269                .apply_base_from_data(&self.game_data, debuff.as_u32())
270            {
271                Ok(draft) => draft.on_target(),
272                Err(error) => {
273                    self.pending_error = Some(error);
274
275                    return;
276                }
277            };
278            let Ok(index) = u8::try_from(self.aura_defs.len()) else {
279                return;
280            };
281
282            // #t(rust_log_in_loop) each construction-time registration identifies the exact class aura
283            tracing::debug!(
284                aura_id = debuff.as_u32(),
285                local_index = index,
286                "registered self-provided class damage-taken debuff"
287            );
288            self.aura_ids.insert(name, debuff.as_u32());
289            self.aura_defs.push(draft.finalize());
290            self.precombat_auras
291                .push(crate::state::LocalAuraIdx::new(index));
292        }
293    }
294
295    fn apply_selected_triggered_aura_values(&mut self) {
296        let mut forwarded = std::collections::BTreeMap::<u32, Option<(u32, f64)>>::new();
297
298        for &driver_spell_id in &self.selected_talent_spell_ids {
299            let driver = SpellIdx::from_raw(driver_spell_id);
300
301            for effect_index in 1..=self.game_data.max_effect_index(driver) {
302                if self.game_data.effect_aura(driver, effect_index)
303                    != AuraSubtypeKind::TriggerSpellWithValue as i32
304                {
305                    continue;
306                }
307
308                let Some(triggered) = self.game_data.trigger_spell(driver, effect_index) else {
309                    continue;
310                };
311                let value = self.game_data.base_points(driver, effect_index);
312                let entry = forwarded
313                    .entry(triggered.as_u32())
314                    .or_insert(Some((driver_spell_id, value)));
315
316                if entry.is_some_and(|(_, existing)| existing.to_bits() != value.to_bits()) {
317                    *entry = None;
318                }
319            }
320        }
321
322        for (aura_id, source) in forwarded {
323            let Some((driver_spell_id, value)) = source else {
324                // #t(rust_log_in_loop) each conflict identifies the exact forwarded aura value
325                tracing::debug!(
326                    aura_id,
327                    "DBC_TRIGGER_VALUE_CONFLICT: selected drivers forward different values"
328                );
329                continue;
330            };
331            let Some(aura) = self
332                .aura_defs
333                .iter_mut()
334                .find(|aura| aura.aura_id == aura_id)
335            else {
336                continue;
337            };
338            let patched = crate::builder::aura_builder::apply_forwarded_effect_value(
339                aura.aura_id,
340                &mut aura.effects,
341                &self.game_data,
342                value,
343            );
344
345            if patched > 0 {
346                // #t(rust_log_in_loop) each patch identifies the exact driver-to-aura rewrite
347                tracing::debug!(
348                    driver_spell_id,
349                    aura_id,
350                    value,
351                    patched,
352                    "forwarded selected trigger value into registered aura effects"
353                );
354            }
355        }
356    }
357}