Skip to main content

wowlab_engine_application/simulate_intent/setup/
mod.rs

1//! Resolution of reusable simulation setup inputs.
2
3mod item_spells;
4
5use wowlab_common::sim::intent::SimConfigIntent;
6use wowlab_engine_gamedata::ResolvedGameData;
7use wowlab_engine_ports::{
8    BugSettings, CastLatency, CombatStats, ConsumableFlags, ContentCatalog, DataResolver,
9    DynDataResolver, EngineError, EquippedItem, ExternalBuffConfig, RaidEventConfig,
10    ResolvedEncounter, SpecDescriptor, TalentSelection, WeaponEnchantProc,
11};
12use wowlab_types::{
13    game::{RaceId, SpecId},
14    sim::{EncounterDefinition, Rotation},
15};
16
17use self::item_spells::{ResolvedConsumables, assemble_item_spell_ids};
18use crate::{
19    encounter::resolve_encounter,
20    error::{ApplicationError, ApplicationStage, EngineResultExt as _},
21    game_data::{
22        CharacterContext, PreResolvedEnemyStats, resolve_game_data_with_expected_stats,
23        resolve_stat_passives,
24    },
25    gear::{GearResult, bench_gear_result, compute_stats_from_gear},
26    intent_builder::IntentOverrides,
27    settings::{
28        parse_config, parse_consumable_selection, parse_spec, resolve_bloodlust,
29        resolve_bug_settings, resolve_cast_latency, resolve_consumable_spells,
30        resolve_external_buffs, resolve_race_setting, resolve_raid_events,
31    },
32    talents::loadout::{decode_expansion_talents, decode_loadout_talents},
33    weapon::extract_weapon_enchant_procs,
34};
35
36#[rustfmt::skip]
37#[cfg(test)]
38pub(super) use self::item_spells::{
39    ResolvedConsumables as TestResolvedConsumables,
40    assemble_item_spell_ids as test_assemble_item_spell_ids,
41};
42
43/// Fully resolved sim inputs, owned and `Send + Sync`, ready to build one or more handlers.
44#[derive(Debug)]
45pub struct ResolvedSetup {
46    pub descriptor: &'static SpecDescriptor,
47    pub game_data: ResolvedGameData,
48    pub rotation: Rotation,
49    pub stats: CombatStats,
50    pub talent_selections: Vec<TalentSelection>,
51    pub equipped_items: Vec<EquippedItem>,
52    pub set_bonus_auras: Vec<u32>,
53    pub consumables: ConsumableFlags,
54    pub race: RaceId,
55    pub weapon_enchant_procs: Vec<WeaponEnchantProc>,
56    pub encounter: ResolvedEncounter,
57    pub duration_s: f64,
58    pub bugs: BugSettings,
59    pub cast_latency: CastLatency,
60    pub raid_events: Vec<RaidEventConfig>,
61    pub external_buffs: Vec<ExternalBuffConfig>,
62}
63
64struct ResolvedSettings {
65    race: RaceId,
66    consumables: ResolvedConsumables,
67    raid_events: Vec<RaidEventConfig>,
68    external_buffs: Vec<ExternalBuffConfig>,
69    bugs: BugSettings,
70    cast_latency: CastLatency,
71}
72
73fn encounter_duration(encounter: &EncounterDefinition) -> f64 {
74    encounter
75        .fixed_duration_s
76        .unwrap_or_else(|| wowlab_types::sim::SimTime::MAX.as_secs_f64())
77}
78
79/// Resolve a sim config into owned, reusable [`ResolvedSetup`] inputs.
80pub async fn resolve_setup(
81    catalog: &ContentCatalog,
82    sim_config: &str,
83    resolver: &DynDataResolver<'_>,
84    overrides: &IntentOverrides,
85) -> Result<ResolvedSetup, ApplicationError> {
86    let config = parse_config(sim_config).map_err(|source| {
87        ApplicationError::intent_config(ApplicationStage::ConfigParsing, source)
88    })?;
89    let spec = parse_spec(&config).in_application_stage(ApplicationStage::SpecResolution)?;
90    let descriptor = catalog
91        .descriptor(spec)
92        .in_application_stage(ApplicationStage::SpecResolution)?;
93    let duration_s = encounter_duration(&config.encounter);
94    let encounter = resolve_encounter(config.encounter.clone(), resolver)
95        .await
96        .map_err(|source| {
97            ApplicationError::encounter_resolution(ApplicationStage::EncounterResolution, source)
98        })?;
99    let settings = resolve_settings(&config, resolver, overrides).await?;
100    let rotation = resolve_rotation(&config, resolver, overrides).await?;
101    let talent_selections = resolve_talents(&config, spec, resolver, overrides).await?;
102    let gear_result = resolve_gear(SetupGearInputs {
103        config: &config,
104        descriptor,
105        spec,
106        race: settings.race,
107        resolver,
108        overrides,
109        talent_selections: &talent_selections,
110    })
111    .await?;
112    let mut item_spell_ids =
113        assemble_item_spell_ids(catalog, &gear_result, &settings.consumables, settings.race);
114
115    item_spell_ids.extend(settings.external_buffs.iter().map(|config| config.spell_id));
116    item_spell_ids.extend(talent_selections.iter().map(|talent| talent.spell_id));
117    item_spell_ids.extend(talent_selections.iter().flat_map(|talent| {
118        catalog
119            .expansion_trait_resolve_ids(talent.spell_id)
120            .iter()
121            .copied()
122    }));
123
124    let game_data = resolve_setup_game_data(SetupGameDataInputs {
125        config: &config,
126        descriptor,
127        resolver,
128        talent_selections: &talent_selections,
129        gear_result: &gear_result,
130        race: settings.race,
131        consumables: &settings.consumables,
132        encounter: &encounter,
133        item_spell_ids: &item_spell_ids,
134    })
135    .await?;
136    let weapon_enchant_procs =
137        resolve_weapon_enchants(&config, &game_data, resolver, overrides).await?;
138
139    Ok(ResolvedSetup {
140        descriptor,
141        game_data,
142        rotation,
143        stats: gear_result.stats,
144        talent_selections,
145        equipped_items: gear_result.equipped_items,
146        set_bonus_auras: gear_result.set_bonus_auras,
147        consumables: settings.consumables.flags,
148        race: settings.race,
149        weapon_enchant_procs,
150        encounter,
151        duration_s,
152        bugs: settings.bugs,
153        cast_latency: settings.cast_latency,
154        raid_events: settings.raid_events,
155        external_buffs: settings.external_buffs,
156    })
157}
158
159async fn resolve_settings(
160    config: &SimConfigIntent,
161    resolver: &DynDataResolver<'_>,
162    overrides: &IntentOverrides,
163) -> Result<ResolvedSettings, ApplicationError> {
164    let race = if overrides.no_buffs {
165        RaceId::Human
166    } else {
167        resolve_race_setting(&config.settings)
168            .in_application_stage(ApplicationStage::SettingsResolution)?
169    };
170    let consumables = resolve_consumables(&config.settings, race, resolver, overrides)
171        .await
172        .in_application_stage(ApplicationStage::SettingsResolution)?;
173    let raid_events = resolve_raid_events(&config.settings)
174        .in_application_stage(ApplicationStage::SettingsResolution)?;
175    let external_buffs = resolve_external_buffs(&config.settings)
176        .in_application_stage(ApplicationStage::SettingsResolution)?;
177    let cast_latency = resolve_cast_latency(&config.settings)
178        .in_application_stage(ApplicationStage::SettingsResolution)?;
179
180    Ok(ResolvedSettings {
181        race,
182        consumables,
183        raid_events,
184        external_buffs,
185        bugs: resolve_bug_settings(&config.settings),
186        cast_latency,
187    })
188}
189
190async fn resolve_rotation(
191    config: &SimConfigIntent,
192    resolver: &DynDataResolver<'_>,
193    overrides: &IntentOverrides,
194) -> Result<Rotation, ApplicationError> {
195    if overrides.skip_rotation {
196        return Ok(Rotation::empty());
197    }
198
199    let rotation_id = overrides
200        .rotation_id_override
201        .as_deref()
202        .unwrap_or(&config.rotation_id);
203
204    if rotation_id.is_empty() {
205        return Err(ApplicationError::from_engine(
206            ApplicationStage::RotationResolution,
207            EngineError::intent_validation("rotation_id must not be empty"),
208        ));
209    }
210
211    let rotation_json = resolver
212        .get_rotation_script(rotation_id)
213        .await
214        .in_application_stage(ApplicationStage::RotationResolution)?;
215
216    match wowlab_engine_domain::rotation::parse_and_validate(&rotation_json) {
217        Ok(rotation) => Ok(rotation),
218        Err(source) => Err(ApplicationError::from_engine(
219            ApplicationStage::RotationResolution,
220            EngineError::rotation_compile(source),
221        )),
222    }
223}
224
225async fn resolve_talents(
226    config: &SimConfigIntent,
227    spec: SpecId,
228    resolver: &DynDataResolver<'_>,
229    overrides: &IntentOverrides,
230) -> Result<Vec<TalentSelection>, ApplicationError> {
231    let mut selections = match (overrides.default_stats, config.loadout.as_ref()) {
232        (false, Some(loadout)) => decode_loadout_talents(spec, loadout, resolver)
233            .await
234            .in_application_stage(ApplicationStage::TalentResolution)?,
235        _ => Vec::new(),
236    };
237
238    if !overrides.default_stats {
239        selections.extend(
240            decode_expansion_talents(
241                config.player_expansion_id,
242                &config.expansion_talents,
243                resolver,
244            )
245            .await
246            .in_application_stage(ApplicationStage::TalentResolution)?,
247        );
248    }
249
250    tracing::debug!(
251        spec = spec.slug(),
252        count = selections.len(),
253        "resolved talent selections"
254    );
255
256    Ok(selections)
257}
258
259struct SetupGearInputs<'a, 'resolver> {
260    config: &'a SimConfigIntent,
261    descriptor: &'a SpecDescriptor,
262    spec: SpecId,
263    race: RaceId,
264    resolver: &'a DynDataResolver<'resolver>,
265    overrides: &'a IntentOverrides,
266    talent_selections: &'a [TalentSelection],
267}
268
269async fn resolve_gear(inputs: SetupGearInputs<'_, '_>) -> Result<GearResult, ApplicationError> {
270    let SetupGearInputs {
271        config,
272        descriptor,
273        spec,
274        race,
275        resolver,
276        overrides,
277        talent_selections,
278    } = inputs;
279
280    if overrides.default_stats {
281        return bench_gear_result(overrides.extra_item_id, resolver)
282            .await
283            .in_application_stage(ApplicationStage::GearResolution);
284    }
285
286    let stat_passives = resolve_stat_passives(descriptor, resolver, talent_selections, Some(race))
287        .await
288        .in_application_stage(ApplicationStage::GearResolution)?;
289
290    compute_stats_from_gear(
291        spec,
292        &config.gear,
293        &stat_passives,
294        u32::from(config.player_level),
295        race,
296        resolver,
297    )
298    .await
299    .in_application_stage(ApplicationStage::GearResolution)
300}
301
302struct SetupGameDataInputs<'a, 'resolver> {
303    config: &'a SimConfigIntent,
304    descriptor: &'static SpecDescriptor,
305    resolver: &'a DynDataResolver<'resolver>,
306    talent_selections: &'a [TalentSelection],
307    gear_result: &'a GearResult,
308    race: RaceId,
309    consumables: &'a ResolvedConsumables,
310    encounter: &'a ResolvedEncounter,
311    item_spell_ids: &'a [u32],
312}
313
314async fn resolve_setup_game_data(
315    inputs: SetupGameDataInputs<'_, '_>,
316) -> Result<ResolvedGameData, ApplicationError> {
317    let SetupGameDataInputs {
318        config,
319        descriptor,
320        resolver,
321        talent_selections,
322        gear_result,
323        race,
324        consumables,
325        encounter,
326        item_spell_ids,
327    } = inputs;
328
329    resolve_game_data_with_expected_stats(
330        resolver,
331        crate::GameDataInputs {
332            descriptor,
333            extra_spell_ids: item_spell_ids,
334            talents: talent_selections,
335            set_bonus_auras: &gear_result.set_bonus_auras,
336            character: CharacterContext {
337                main_hand: gear_result.main_hand,
338                race: Some(race),
339                off_hand: gear_result.off_hand,
340                player_expansion_id: config.player_expansion_id,
341                player_level: config.player_level,
342                consumables: consumables.spells.clone(),
343            },
344        },
345        PreResolvedEnemyStats {
346            creature_armor: encounter.primary_enemy().creature_armor(),
347            armor_constant: encounter.primary_enemy().armor_constant(),
348        },
349    )
350    .await
351    .in_application_stage(ApplicationStage::GameDataResolution)
352}
353
354async fn resolve_weapon_enchants(
355    config: &SimConfigIntent,
356    game_data: &ResolvedGameData,
357    resolver: &DynDataResolver<'_>,
358    overrides: &IntentOverrides,
359) -> Result<Vec<WeaponEnchantProc>, ApplicationError> {
360    if overrides.no_buffs {
361        return Ok(Vec::new());
362    }
363
364    extract_weapon_enchant_procs(
365        &config.gear,
366        game_data.game_tables(),
367        game_data.level(),
368        resolver,
369    )
370    .await
371    .in_application_stage(ApplicationStage::WeaponEnchantResolution)
372}
373
374pub(super) async fn resolve_consumables(
375    settings: &std::collections::BTreeMap<String, toml::Value>,
376    race: RaceId,
377    resolver: &DynDataResolver<'_>,
378    overrides: &IntentOverrides,
379) -> Result<ResolvedConsumables, EngineError> {
380    if overrides.no_buffs {
381        return Ok(ResolvedConsumables::default());
382    }
383
384    let selection = parse_consumable_selection(settings)?;
385    let spells = resolve_consumable_spells(resolver, &selection, race).await?;
386    let flags = ConsumableFlags {
387        bloodlust: resolve_bloodlust(settings),
388        pre_pot_tempered: spells.potion.is_some(),
389        flask: spells.flask.is_some(),
390        augment_rune: spells.augment_rune.is_some(),
391    };
392
393    Ok(ResolvedConsumables { flags, spells })
394}