Skip to main content

wowlab_engine_adapter_data/
in_memory.rs

1use std::sync::Arc;
2
3use wowlab_engine_domain::dbc::item_matches_consumable_query;
4use wowlab_engine_ports::{
5    DataResolver, EnchantmentRow, PermanentEnchantQuery, ResolverError, SpellId,
6    select_permanent_enchant,
7};
8use wowlab_types::{
9    data::{
10        ChallengeModeHealthFlat, ContentTuningFlat, ContentTuningXDifficultyFlat,
11        ContentTuningXExpectedFlat, CreatureDifficultyFlat, CreatureFlat, ExpansionTraitTreeFlat,
12        ExpectedStatFlat, ExpectedStatModFlat, ItemDamageScalingFlat, ItemDataFlat,
13        ItemScalingData, PermanentEnchantFlat, PowerTypeFlat, SpecDataFlat, SpellDataFlat,
14        SpellEffect, TraitTreeFlat,
15    },
16    sim::{FastMap, IntMap},
17};
18
19use crate::lookup_key::{ExpansionTraitTreeKey, ExpectedStatsKey};
20
21/// In-memory data resolver populated through builder methods.
22#[derive(Debug, Default)]
23pub struct InMemoryResolver {
24    spells: IntMap<i32, SpellDataFlat>,
25    effects: IntMap<i32, Vec<SpellEffect>>,
26    items: IntMap<i32, ItemDataFlat>,
27    specs: IntMap<i32, SpecDataFlat>,
28    trait_trees: IntMap<i32, TraitTreeFlat>,
29    expansion_trait_trees: FastMap<ExpansionTraitTreeKey, ExpansionTraitTreeFlat>,
30    rotation_scripts: FastMap<String, String>,
31    expected_stats: FastMap<ExpectedStatsKey, ExpectedStatFlat>,
32    scaling_data: Option<Arc<ItemScalingData>>,
33    power_types: Vec<PowerTypeFlat>,
34    enchantments: IntMap<i32, EnchantmentRow>,
35    permanent_enchants: Vec<PermanentEnchantFlat>,
36    creatures: IntMap<i32, CreatureFlat>,
37    creature_difficulties: IntMap<i32, Vec<CreatureDifficultyFlat>>,
38    content_tunings: IntMap<i32, ContentTuningFlat>,
39    content_tuning_x_difficulty: IntMap<i32, Vec<ContentTuningXDifficultyFlat>>,
40    content_tuning_x_expected: IntMap<i32, Vec<ContentTuningXExpectedFlat>>,
41    expected_stat_mods: IntMap<i32, ExpectedStatModFlat>,
42    challenge_mode_health: IntMap<i32, ChallengeModeHealthFlat>,
43}
44
45impl InMemoryResolver {
46    /// Creates an empty resolver.
47    #[must_use]
48    pub fn new() -> Self {
49        Self::default()
50    }
51
52    /// Adds or replaces a spell keyed by its ID.
53    #[must_use]
54    pub fn with_spell(mut self, spell: SpellDataFlat) -> Self {
55        self.spells.insert(spell.id, spell);
56
57        self
58    }
59
60    /// Adds or replaces the effect list for a spell and sorts it by zero-based DBC index.
61    #[must_use]
62    pub fn with_effects(mut self, spell_id: i32, mut effects: Vec<SpellEffect>) -> Self {
63        effects.sort_by_key(|e| e.index);
64        self.effects.insert(spell_id, effects);
65
66        self
67    }
68
69    /// Adds or replaces an item keyed by its ID.
70    #[must_use]
71    pub fn with_item(mut self, item: ItemDataFlat) -> Self {
72        self.items.insert(item.id, item);
73
74        self
75    }
76
77    /// Adds or replaces an enchantment keyed by its ID.
78    #[must_use]
79    pub fn with_enchantment(mut self, enchantment: EnchantmentRow) -> Self {
80        self.enchantments.insert(enchantment.id, enchantment);
81
82        self
83    }
84
85    /// Appends one profession-derived permanent-enchant index entry.
86    #[must_use]
87    pub fn with_permanent_enchant(mut self, enchantment: PermanentEnchantFlat) -> Self {
88        self.permanent_enchants.push(enchantment);
89
90        self
91    }
92
93    /// Adds or replaces a specialization keyed by its ID.
94    #[must_use]
95    pub fn with_spec(mut self, spec: SpecDataFlat) -> Self {
96        self.specs.insert(spec.id, spec);
97
98        self
99    }
100
101    /// Adds or replaces a specialization trait tree.
102    #[must_use]
103    pub fn with_trait_tree(mut self, tree: TraitTreeFlat) -> Self {
104        self.trait_trees.insert(tree.spec_id, tree);
105
106        self
107    }
108
109    /// Adds or replaces an expansion-wide trait tree.
110    #[must_use]
111    pub fn with_expansion_trait_tree(mut self, tree: ExpansionTraitTreeFlat) -> Self {
112        self.expansion_trait_trees.insert(
113            ExpansionTraitTreeKey::new(tree.expansion_id, &tree.system),
114            tree,
115        );
116
117        self
118    }
119
120    /// Adds or replaces a rotation script.
121    #[must_use]
122    pub fn with_rotation_script(
123        mut self,
124        id: impl Into<String>,
125        script: impl Into<String>,
126    ) -> Self {
127        self.rotation_scripts.insert(id.into(), script.into());
128
129        self
130    }
131
132    /// Adds or replaces an expected-stat row keyed by expansion and level.
133    #[must_use]
134    pub fn with_expected_stats(mut self, stats: ExpectedStatFlat) -> Self {
135        self.expected_stats
136            .insert(ExpectedStatsKey::new(stats.expansion_id, stats.lvl), stats);
137
138        self
139    }
140
141    /// Replaces the complete item-scaling data set.
142    #[must_use]
143    pub fn with_scaling_data(mut self, data: ItemScalingData) -> Self {
144        self.scaling_data = Some(Arc::new(data));
145
146        self
147    }
148
149    /// Replaces the complete power-type list.
150    #[must_use]
151    pub fn with_power_types(mut self, power_types: Vec<PowerTypeFlat>) -> Self {
152        self.power_types = power_types;
153
154        self
155    }
156
157    /// Adds or replaces a creature keyed by its ID.
158    #[must_use]
159    pub fn with_creature(mut self, creature: CreatureFlat) -> Self {
160        self.creatures.insert(creature.id, creature);
161
162        self
163    }
164
165    /// Appends a difficulty row for its creature.
166    #[must_use]
167    pub fn with_creature_difficulty(mut self, row: CreatureDifficultyFlat) -> Self {
168        self.creature_difficulties
169            .entry(row.creature_id)
170            .or_default()
171            .push(row);
172
173        self
174    }
175
176    /// Adds or replaces a content-tuning row keyed by its ID.
177    #[must_use]
178    pub fn with_content_tuning(mut self, tuning: ContentTuningFlat) -> Self {
179        self.content_tunings.insert(tuning.id, tuning);
180
181        self
182    }
183
184    /// Appends a difficulty mapping for its content-tuning ID.
185    #[must_use]
186    pub fn with_content_tuning_x_difficulty(mut self, row: ContentTuningXDifficultyFlat) -> Self {
187        self.content_tuning_x_difficulty
188            .entry(row.content_tuning_id)
189            .or_default()
190            .push(row);
191
192        self
193    }
194
195    /// Appends an expected-stat mapping for its content-tuning ID.
196    #[must_use]
197    pub fn with_content_tuning_x_expected(mut self, row: ContentTuningXExpectedFlat) -> Self {
198        self.content_tuning_x_expected
199            .entry(row.content_tuning_id)
200            .or_default()
201            .push(row);
202
203        self
204    }
205
206    /// Adds or replaces an expected-stat modifier keyed by its ID.
207    #[must_use]
208    pub fn with_expected_stat_mod(mut self, stat_mod: ExpectedStatModFlat) -> Self {
209        self.expected_stat_mods.insert(stat_mod.id, stat_mod);
210
211        self
212    }
213
214    /// Adds or replaces a challenge-mode health row keyed by keystone level.
215    #[must_use]
216    pub fn with_challenge_mode_health(mut self, health: ChallengeModeHealthFlat) -> Self {
217        self.challenge_mode_health
218            .insert(health.challenge_level, health);
219
220        self
221    }
222
223    fn resolve_spells(&self, spell_ids: &[SpellId]) -> Result<Vec<SpellDataFlat>, ResolverError> {
224        spell_ids
225            .iter()
226            .map(|&id| {
227                self.spells
228                    .get(&id.as_i32())
229                    .cloned()
230                    .ok_or_else(|| ResolverError::spell_not_found(id))
231            })
232            .collect()
233    }
234}
235
236impl DataResolver for InMemoryResolver {
237    async fn get_spell(&self, spell_id: SpellId) -> Result<SpellDataFlat, ResolverError> {
238        self.spells
239            .get(&spell_id.as_i32())
240            .cloned()
241            .ok_or_else(|| ResolverError::spell_not_found(spell_id))
242    }
243
244    async fn get_spells(&self, spell_ids: &[SpellId]) -> Result<Vec<SpellDataFlat>, ResolverError> {
245        self.resolve_spells(spell_ids)
246    }
247
248    async fn get_spell_effect(
249        &self,
250        spell_id: SpellId,
251        effect_index: u8,
252    ) -> Result<SpellEffect, ResolverError> {
253        let effects = self
254            .effects
255            .get(&spell_id.as_i32())
256            .ok_or_else(|| ResolverError::spell_effect_not_found(spell_id, effect_index))?;
257
258        crate::rows::validate_effect_index(spell_id, effect_index, effects)
259    }
260
261    async fn get_spell_effects(
262        &self,
263        spell_id: SpellId,
264    ) -> Result<Vec<SpellEffect>, ResolverError> {
265        if let Some(effects) = self.effects.get(&spell_id.as_i32()) {
266            return Ok(effects.clone());
267        }
268
269        self.spells
270            .get(&spell_id.as_i32())
271            .map(|spell| spell.effects.clone())
272            .ok_or_else(|| ResolverError::spell_not_found(spell_id))
273    }
274
275    async fn get_item(&self, item_id: i32) -> Result<ItemDataFlat, ResolverError> {
276        self.items
277            .get(&item_id)
278            .cloned()
279            .ok_or_else(|| ResolverError::item_not_found(item_id))
280    }
281
282    async fn find_consumable_items(
283        &self,
284        name_token: &str,
285        subclass: i32,
286    ) -> Result<Vec<ItemDataFlat>, ResolverError> {
287        Ok(self
288            .items
289            .values()
290            .filter(|item| item_matches_consumable_query(item, name_token, subclass))
291            .cloned()
292            .collect())
293    }
294
295    async fn get_scaling_data(&self) -> Result<Arc<ItemScalingData>, ResolverError> {
296        self.scaling_data
297            .clone()
298            .ok_or_else(ResolverError::no_scaling_data)
299    }
300
301    async fn get_power_types(&self) -> Result<Vec<PowerTypeFlat>, ResolverError> {
302        Ok(self.power_types.clone())
303    }
304
305    async fn get_spec(&self, spec_id: i32) -> Result<SpecDataFlat, ResolverError> {
306        self.specs
307            .get(&spec_id)
308            .cloned()
309            .ok_or_else(|| ResolverError::spec_not_found(spec_id))
310    }
311
312    async fn get_trait_tree(&self, spec_id: i32) -> Result<TraitTreeFlat, ResolverError> {
313        self.trait_trees
314            .get(&spec_id)
315            .cloned()
316            .ok_or_else(|| ResolverError::trait_tree_not_found(spec_id))
317    }
318
319    async fn get_expansion_trait_tree(
320        &self,
321        expansion_id: i32,
322        system: &str,
323    ) -> Result<ExpansionTraitTreeFlat, ResolverError> {
324        self.expansion_trait_trees
325            .get(&ExpansionTraitTreeKey::new(expansion_id, system))
326            .cloned()
327            .ok_or_else(|| ResolverError::expansion_trait_tree_not_found(expansion_id, system))
328    }
329
330    async fn get_rotation_script(&self, rotation_id: &str) -> Result<String, ResolverError> {
331        self.rotation_scripts
332            .get(rotation_id)
333            .cloned()
334            .ok_or_else(|| ResolverError::rotation_script_not_found(rotation_id))
335    }
336
337    async fn get_spell_overrides(
338        &self,
339        _spec_id: i32,
340    ) -> Result<Vec<(SpellId, SpellId)>, ResolverError> {
341        Ok(vec![])
342    }
343
344    async fn get_specialization_spells(
345        &self,
346        _spec_id: i32,
347    ) -> Result<Vec<SpellId>, ResolverError> {
348        Ok(vec![])
349    }
350
351    async fn get_racial_spells(
352        &self,
353        _race_id: i32,
354        _class_id: i32,
355    ) -> Result<Vec<SpellId>, ResolverError> {
356        Ok(vec![])
357    }
358
359    async fn get_expected_stats(
360        &self,
361        expansion_id: i32,
362        lvl: i32,
363    ) -> Result<ExpectedStatFlat, ResolverError> {
364        self.expected_stats
365            .get(&ExpectedStatsKey::new(expansion_id, lvl))
366            .cloned()
367            .ok_or_else(|| ResolverError::expected_stats_not_found(expansion_id, lvl))
368    }
369
370    async fn get_item_damage_scaling(
371        &self,
372        item_level: i32,
373        weapon_type: &str,
374    ) -> Result<ItemDamageScalingFlat, ResolverError> {
375        Err(ResolverError::item_damage_scaling_not_found(
376            weapon_type,
377            item_level,
378        ))
379    }
380
381    async fn get_enchantment(&self, enchantment_id: i32) -> Result<EnchantmentRow, ResolverError> {
382        self.enchantments
383            .get(&enchantment_id)
384            .cloned()
385            .ok_or_else(|| ResolverError::enchantment_not_found(enchantment_id))
386    }
387
388    async fn find_permanent_enchant(
389        &self,
390        query: &PermanentEnchantQuery,
391    ) -> Result<Option<PermanentEnchantFlat>, ResolverError> {
392        Ok(select_permanent_enchant(&self.permanent_enchants, query))
393    }
394
395    async fn get_creature(&self, creature_id: i32) -> Result<CreatureFlat, ResolverError> {
396        self.creatures
397            .get(&creature_id)
398            .cloned()
399            .ok_or_else(|| ResolverError::creature_not_found(creature_id))
400    }
401
402    async fn get_creature_difficulties(
403        &self,
404        creature_id: i32,
405    ) -> Result<Vec<CreatureDifficultyFlat>, ResolverError> {
406        Ok(self
407            .creature_difficulties
408            .get(&creature_id)
409            .cloned()
410            .unwrap_or_default())
411    }
412
413    async fn get_content_tuning(
414        &self,
415        content_tuning_id: i32,
416    ) -> Result<ContentTuningFlat, ResolverError> {
417        self.content_tunings
418            .get(&content_tuning_id)
419            .cloned()
420            .ok_or_else(|| ResolverError::content_tuning_not_found(content_tuning_id))
421    }
422
423    async fn get_content_tuning_x_difficulty(
424        &self,
425        content_tuning_id: i32,
426    ) -> Result<Vec<ContentTuningXDifficultyFlat>, ResolverError> {
427        Ok(self
428            .content_tuning_x_difficulty
429            .get(&content_tuning_id)
430            .cloned()
431            .unwrap_or_default())
432    }
433
434    async fn get_content_tuning_x_expected(
435        &self,
436        content_tuning_id: i32,
437    ) -> Result<Vec<ContentTuningXExpectedFlat>, ResolverError> {
438        Ok(self
439            .content_tuning_x_expected
440            .get(&content_tuning_id)
441            .cloned()
442            .unwrap_or_default())
443    }
444
445    async fn get_expected_stat_mod(
446        &self,
447        expected_stat_mod_id: i32,
448    ) -> Result<ExpectedStatModFlat, ResolverError> {
449        self.expected_stat_mods
450            .get(&expected_stat_mod_id)
451            .cloned()
452            .ok_or_else(|| ResolverError::expected_stat_mod_not_found(expected_stat_mod_id))
453    }
454
455    async fn get_challenge_mode_health(
456        &self,
457        keystone_level: i32,
458    ) -> Result<ChallengeModeHealthFlat, ResolverError> {
459        self.challenge_mode_health
460            .get(&keystone_level)
461            .cloned()
462            .ok_or_else(|| ResolverError::challenge_mode_health_not_found(keystone_level))
463    }
464}
465
466#[cfg(test)]
467mod tests;