1mod 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#[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
79pub 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}