Skip to main content

wowlab_engine_application/
game_data.rs

1//! Resolver orchestration that builds [`ResolvedGameData`] for a spec.
2
3mod auras;
4mod expected_stats;
5mod passives;
6mod powers;
7mod resolution_ids;
8mod spells;
9
10#[cfg(test)]
11use std::collections::VecDeque;
12
13use auras::resolve_auras;
14use expected_stats::{apply_expected_stats, resolve_expected_stats};
15use passives::{EquippedWeapons, PassiveData, PassiveInputs, resolve_passives};
16use powers::resolve_scaling_and_power_types;
17use resolution_ids::{ResolvedPassiveSeeds, collect_resolution_ids};
18use spells::{SpellResolutionContext, resolve_spells};
19#[cfg(test)]
20use wowlab_engine_domain::dbc::{AuraSubtypeKind, PowerModifiers};
21#[cfg(test)]
22use wowlab_engine_gamedata::PowerTypeProps;
23use wowlab_engine_gamedata::{ConsumableSpells, ResolvedGameData, ResolvedGameDataBuilder};
24#[cfg(test)]
25use wowlab_engine_ports::SpellId;
26use wowlab_engine_ports::{DynDataResolver, EngineError, SpecDescriptor, TalentSelection};
27#[cfg(test)]
28use wowlab_types::data::{ExpectedStatFlat, PowerTypeFlat, SpellDataFlat, SpellEffect};
29use wowlab_types::{
30    data::WeaponStats,
31    game::RaceId,
32    sim::{FastSet, SpellIdx},
33};
34
35use crate::{ApplicationError, ApplicationStage, error::EngineResultExt as _};
36
37#[rustfmt::skip]
38pub(crate) use passives::{resolve_race_passive_spells, resolve_stat_passives};
39
40/// Character-specific inputs needed while resolving game data.
41#[derive(Clone, Debug)]
42pub struct CharacterContext {
43    pub main_hand: WeaponStats,
44    /// `None` when no character is being resolved, such as spec introspection.
45    pub race: Option<RaceId>,
46    pub off_hand: Option<WeaponStats>,
47    pub player_expansion_id: u32,
48    pub player_level: u16,
49    pub consumables: ConsumableSpells,
50}
51
52fn initialize_builder(character: CharacterContext) -> ResolvedGameDataBuilder {
53    let mut builder = ResolvedGameData::builder();
54
55    builder.set_main_hand(character.main_hand);
56    builder.set_off_hand(character.off_hand);
57    builder.set_level(u32::from(character.player_level));
58    builder.set_consumable_spells(character.consumables);
59
60    builder
61}
62
63enum ExpectedStatsInput {
64    Resolve,
65    PreResolved {
66        creature_armor: f64,
67        armor_constant: f64,
68    },
69}
70
71#[derive(Debug)]
72pub struct GameDataInputs<'a> {
73    pub descriptor: &'a SpecDescriptor,
74    pub extra_spell_ids: &'a [u32],
75    pub talents: &'a [TalentSelection],
76    pub set_bonus_auras: &'a [u32],
77    pub character: CharacterContext,
78}
79
80pub(crate) struct PreResolvedEnemyStats {
81    pub creature_armor: f64,
82    pub armor_constant: f64,
83}
84
85/// Resolve game data for all spells and auras used by a spec.
86pub async fn resolve_game_data(
87    resolver: &DynDataResolver<'_>,
88    inputs: GameDataInputs<'_>,
89) -> Result<ResolvedGameData, ApplicationError> {
90    resolve_game_data_inner(resolver, inputs, ExpectedStatsInput::Resolve)
91        .await
92        .in_application_stage(ApplicationStage::GameDataResolution)
93}
94
95pub(crate) async fn resolve_game_data_with_expected_stats(
96    resolver: &DynDataResolver<'_>,
97    inputs: GameDataInputs<'_>,
98    resolved: PreResolvedEnemyStats,
99) -> Result<ResolvedGameData, EngineError> {
100    resolve_game_data_inner(
101        resolver,
102        inputs,
103        ExpectedStatsInput::PreResolved {
104            creature_armor: resolved.creature_armor,
105            armor_constant: resolved.armor_constant,
106        },
107    )
108    .await
109}
110
111async fn resolve_game_data_inner(
112    resolver: &DynDataResolver<'_>,
113    inputs: GameDataInputs<'_>,
114    expected_stats: ExpectedStatsInput,
115) -> Result<ResolvedGameData, EngineError> {
116    let GameDataInputs {
117        descriptor,
118        extra_spell_ids,
119        talents,
120        set_bonus_auras,
121        character,
122    } = inputs;
123    let mut builder = initialize_builder(character.clone());
124    let power_modifiers = resolve_scaling_and_power_types(&mut builder, resolver).await?;
125    let PassiveData {
126        passives,
127        effect_overrides,
128        crit_chance_scaled,
129        active_aura_spell_ids,
130        overrides,
131        resolvable_spec_spells,
132        class_target_debuffs,
133    } = resolve_passives(
134        &mut builder,
135        resolver,
136        PassiveInputs {
137            descriptor,
138            talents,
139            set_bonus_auras,
140            weapons: EquippedWeapons {
141                main_hand: &character.main_hand,
142                off_hand: character.off_hand.as_ref(),
143            },
144            race: character.race,
145        },
146        &power_modifiers,
147    )
148    .await?;
149
150    let metadata = &descriptor.metadata;
151
152    metadata.validate()?;
153    let (aura_ids, required_spell_ids, mut pending) = collect_resolution_ids(
154        metadata.spell_ids,
155        metadata.aura_ids,
156        extra_spell_ids,
157        talents,
158        &ResolvedPassiveSeeds {
159            overrides: &overrides,
160            spec_spells: &resolvable_spec_spells,
161            class_target_debuffs: &class_target_debuffs,
162        },
163    );
164    let mut resolved = FastSet::default();
165
166    resolve_spells(&mut SpellResolutionContext {
167        builder: &mut builder,
168        resolver,
169        talents,
170        passives: &passives,
171        crit_chance_scaled: &crit_chance_scaled,
172        effect_overrides: &effect_overrides,
173        power_modifiers: &power_modifiers,
174        active_aura_spell_ids: &active_aura_spell_ids,
175        primary_resource_type: i32::from(u8::from(metadata.primary_resource)),
176        secondary_resource_type: metadata
177            .secondary_resource
178            .map(|resource| i32::from(u8::from(resource))),
179        required_spell_ids: &required_spell_ids,
180        pending: &mut pending,
181        resolved: &mut resolved,
182    })
183    .await?;
184
185    for entry in metadata.server_proc_entries {
186        let inserted = builder
187            .insert_server_proc_attributes(SpellIdx::from_raw(entry.spell_id), entry.attributes);
188
189        debug_assert!(
190            inserted,
191            "validated server proc entries reference resolved manifest spells"
192        );
193        tokio::task::yield_now().await;
194    }
195
196    resolve_auras(&mut builder, resolver, &aura_ids, &passives).await?;
197
198    match expected_stats {
199        ExpectedStatsInput::Resolve => {
200            resolve_expected_stats(&mut builder, resolver, &character, &passives).await?;
201        }
202        ExpectedStatsInput::PreResolved {
203            creature_armor,
204            armor_constant,
205        } => apply_expected_stats(&mut builder, creature_armor, armor_constant, &passives),
206    }
207
208    Ok(builder.build())
209}
210
211#[cfg(test)]
212use passives::{
213    apply_declared_passive_effect_overrides, apply_effect_precision_scales, apply_spell_overrides,
214    apply_talent_effect_overrides, mask_spec_conditional_passive_effects,
215    retain_statically_folded_passives,
216};
217#[cfg(test)]
218use powers::{apply_resource_passives, normalized_power_type_props};
219#[cfg(test)]
220use spells::{
221    apply_runtime_talent_effect, insert_effect, insert_spell_properties, resolve_effect_payload,
222    resolve_trigger_chain_payload,
223};
224
225#[cfg(test)]
226#[path = "game_data_tests.rs"]
227mod game_data_tests;