Skip to main content

wowlab_engine_adapter_data/local/csv/
mod.rs

1use std::sync::Arc;
2
3use parking_lot::Mutex;
4use wowlab_engine_domain::dbc::item_matches_consumable_query;
5use wowlab_engine_ports::{
6    DataResolver, EnchantmentRow, PermanentEnchantQuery, ResolverError, SpellId,
7    select_permanent_enchant,
8};
9use wowlab_fs::path::PathBuf;
10use wowlab_parsers::{
11    DbcData, transform_all_content_tuning_x_difficulty, transform_all_content_tuning_x_expected,
12    transform_all_content_tunings, transform_all_creature_difficulties, transform_all_creatures,
13    transform_all_curve_points, transform_all_curves, transform_all_enchantments,
14    transform_all_expansion_trait_trees, transform_all_expected_stat_mods,
15    transform_all_expected_stats, transform_all_gem_properties, transform_all_item_bonuses,
16    transform_all_item_damage_scaling, transform_all_item_offset_curves,
17    transform_all_item_scaling_configs, transform_all_item_squish_eras, transform_all_items,
18    transform_all_permanent_enchants, transform_all_power_types, transform_all_racial_spells,
19    transform_all_rand_prop_points, transform_all_specialization_spells, transform_all_specs,
20    transform_all_spells, transform_all_trait_trees, transform_challenge_mode_health,
21    transform_combat_ratings, transform_combat_ratings_mult_by_ilvl, transform_hp_per_sta,
22    transform_spell_scaling,
23};
24use wowlab_types::{
25    data::{
26        ChallengeModeHealthFlat, ContentTuningFlat, ContentTuningXDifficultyFlat,
27        ContentTuningXExpectedFlat, CreatureDifficultyFlat, CreatureFlat, EnchantmentFlat,
28        ExpansionTraitTreeFlat, ExpectedStatFlat, ExpectedStatModFlat, ItemDamageScalingFlat,
29        ItemDataFlat, ItemScalingData, PermanentEnchantFlat, PowerTypeFlat, RacialSpellFlat,
30        SpecDataFlat, SpecializationSpellFlat, SpellDataFlat, SpellEffect, TraitTreeFlat,
31    },
32    sim::{FastMap, IntMap},
33};
34
35use crate::lookup_key::ExpectedStatsKey;
36
37type ItemDamageMap = FastMap<String, IntMap<i32, ItemDamageScalingFlat>>;
38
39type ExpectedStatMap = FastMap<ExpectedStatsKey, ExpectedStatFlat>;
40
41type GroupedMap<T> = IntMap<i32, Vec<T>>;
42
43macro_rules! csv_lazy_slots {
44    (
45        categories {
46            $(
47                $category_slot:ident, $category_reader:ident: $category_value:ty
48                    => $category_transform:path, $category_key:expr
49            ),* $(,)?
50        }
51        grouped {
52            $(
53                $grouped_slot:ident, $grouped_reader:ident: $grouped_value:ty
54                    => $grouped_transform:path, $grouped_key:expr
55            ),* $(,)?
56        }
57        slots {
58            $( $slot:ident: $value:ty ),* $(,)?
59        }
60    ) => {
61        /// Resolver backed by lazily cached DBC-derived CSV files.
62        #[derive(Debug)]
63        pub struct LocalCsvResolver {
64            data_dir: PathBuf,
65            $(
66                $category_slot: Mutex<Option<IntMap<i32, $category_value>>>,
67            )*
68            $(
69                $grouped_slot: Mutex<Option<GroupedMap<$grouped_value>>>,
70            )*
71            $(
72                $slot: Mutex<Option<$value>>,
73            )*
74        }
75
76        impl LocalCsvResolver {
77            fn empty(data_dir: PathBuf) -> Self {
78                Self {
79                    data_dir,
80                    $(
81                        $category_slot: Mutex::new(None),
82                    )*
83                    $(
84                        $grouped_slot: Mutex::new(None),
85                    )*
86                    $(
87                        $slot: Mutex::new(None),
88                    )*
89                }
90            }
91
92            $(
93                fn $category_reader<R>(
94                    &self,
95                    access: impl FnOnce(&IntMap<i32, $category_value>) -> R,
96                ) -> Result<R, ResolverError> {
97                    self.read_category(
98                        &self.$category_slot,
99                        $category_transform,
100                        $category_key,
101                        access,
102                    )
103                }
104            )*
105
106            $(
107                fn $grouped_reader<R>(
108                    &self,
109                    access: impl FnOnce(&GroupedMap<$grouped_value>) -> R,
110                ) -> Result<R, ResolverError> {
111                    self.read_grouped(
112                        &self.$grouped_slot,
113                        $grouped_transform,
114                        $grouped_key,
115                        access,
116                    )
117                }
118            )*
119        }
120    };
121}
122
123csv_lazy_slots! {
124    categories {
125        spells, read_spells: SpellDataFlat => transform_all_spells, |row: &SpellDataFlat| row.id,
126        items, read_items: ItemDataFlat => transform_all_items, |row: &ItemDataFlat| row.id,
127        specs, read_specs: SpecDataFlat => transform_all_specs, |row: &SpecDataFlat| row.id,
128        traits, read_traits: TraitTreeFlat => transform_all_trait_trees, |row: &TraitTreeFlat| row.spec_id,
129        enchantments, read_enchantments: EnchantmentFlat => transform_all_enchantments, |row: &EnchantmentFlat| row.id,
130        creatures, read_creatures: CreatureFlat => transform_all_creatures, |row: &CreatureFlat| row.id,
131        content_tunings, read_content_tunings: ContentTuningFlat => transform_all_content_tunings, |row: &ContentTuningFlat| row.id,
132        expected_stat_mods, read_expected_stat_mods: ExpectedStatModFlat => transform_all_expected_stat_mods, |row: &ExpectedStatModFlat| row.id,
133        challenge_mode_health, read_challenge_mode_health: ChallengeModeHealthFlat => transform_challenge_mode_health, |row: &ChallengeModeHealthFlat| row.challenge_level,
134    }
135    grouped {
136        spec_spells, read_spec_spells: SpecializationSpellFlat => transform_all_specialization_spells, |row: &SpecializationSpellFlat| row.spec_id,
137        racial_spells, read_racial_spells: RacialSpellFlat => transform_all_racial_spells, |row: &RacialSpellFlat| row.race_id,
138        creature_difficulties, read_creature_difficulties: CreatureDifficultyFlat => transform_all_creature_difficulties, |row: &CreatureDifficultyFlat| row.creature_id,
139        content_tuning_x_difficulty, read_content_tuning_x_difficulty: ContentTuningXDifficultyFlat => transform_all_content_tuning_x_difficulty, |row: &ContentTuningXDifficultyFlat| row.content_tuning_id,
140        content_tuning_x_expected, read_content_tuning_x_expected: ContentTuningXExpectedFlat => transform_all_content_tuning_x_expected, |row: &ContentTuningXExpectedFlat| row.content_tuning_id,
141    }
142    slots {
143        dbc: Arc<DbcData>,
144        expansion_traits: Vec<ExpansionTraitTreeFlat>,
145        scaling_data: Arc<ItemScalingData>,
146        power_types: Arc<[PowerTypeFlat]>,
147        item_damage: ItemDamageMap,
148        expected_stats: ExpectedStatMap,
149        permanent_enchants: Arc<[PermanentEnchantFlat]>,
150    }
151}
152
153impl LocalCsvResolver {
154    /// Creates a resolver rooted at a directory of DBC-derived CSV files.
155    #[must_use]
156    pub fn new(data_dir: impl Into<PathBuf>) -> Self {
157        Self::empty(data_dir.into())
158    }
159
160    fn read_category<T, R>(
161        &self,
162        slot: &Mutex<Option<IntMap<i32, T>>>,
163        transform: fn(&DbcData) -> Vec<T>,
164        key_of: fn(&T) -> i32,
165        access: impl FnOnce(&IntMap<i32, T>) -> R,
166    ) -> Result<R, ResolverError> {
167        let mut guard = slot.lock();
168
169        if guard.is_none() {
170            let dbc = self.shared_dbc()?;
171            let items = transform(&dbc);
172            let mut map = IntMap::with_capacity_and_hasher(
173                items.len(),
174                std::hash::BuildHasherDefault::default(),
175            );
176
177            for item in items {
178                map.insert(key_of(&item), item);
179            }
180
181            *guard = Some(map);
182        }
183
184        let map = guard.as_ref().ok_or_else(|| {
185            ResolverError::decode(
186                "category cache populated above",
187                std::io::Error::other("category slot empty after populate"),
188            )
189        })?;
190
191        Ok(access(map))
192    }
193
194    fn read_grouped<T, R>(
195        &self,
196        slot: &Mutex<Option<GroupedMap<T>>>,
197        transform: fn(&DbcData) -> Vec<T>,
198        key_of: fn(&T) -> i32,
199        access: impl FnOnce(&GroupedMap<T>) -> R,
200    ) -> Result<R, ResolverError> {
201        let mut guard = slot.lock();
202        let map = if let Some(map) = guard.as_ref() {
203            map
204        } else {
205            let dbc = self.shared_dbc()?;
206            let mut grouped: GroupedMap<T> = IntMap::default();
207
208            for item in transform(&dbc) {
209                grouped.entry(key_of(&item)).or_default().push(item);
210            }
211
212            guard.insert(grouped)
213        };
214
215        Ok(access(map))
216    }
217
218    fn shared_dbc(&self) -> Result<Arc<DbcData>, ResolverError> {
219        let mut guard = self.dbc.lock();
220
221        if let Some(existing) = guard.as_ref() {
222            return Ok(Arc::clone(existing));
223        }
224
225        let dbc = DbcData::load_all(&self.data_dir)
226            .map_err(|error| ResolverError::decode("load DBC CSVs", error))?;
227        let arc = Arc::new(dbc);
228
229        *guard = Some(Arc::clone(&arc));
230
231        Ok(arc)
232    }
233
234    fn ensure_scaling_data(&self) -> Result<Arc<ItemScalingData>, ResolverError> {
235        let mut guard = self.scaling_data.lock();
236
237        if let Some(existing) = guard.as_ref() {
238            return Ok(Arc::clone(existing));
239        }
240
241        let dbc = self.shared_dbc()?;
242        let data = Arc::new(ItemScalingData::from_flat(
243            transform_all_item_bonuses(&dbc),
244            transform_all_curves(&dbc),
245            transform_all_curve_points(&dbc),
246            transform_all_rand_prop_points(&dbc),
247            transform_all_item_scaling_configs(&dbc),
248            transform_all_item_offset_curves(&dbc),
249            transform_all_item_squish_eras(&dbc),
250            transform_combat_ratings(&dbc),
251            transform_hp_per_sta(&dbc),
252            transform_spell_scaling(&dbc),
253            transform_combat_ratings_mult_by_ilvl(&dbc),
254            transform_all_gem_properties(&dbc),
255        ));
256
257        *guard = Some(Arc::clone(&data));
258
259        Ok(data)
260    }
261
262    fn ensure_power_types(&self) -> Result<Arc<[PowerTypeFlat]>, ResolverError> {
263        let mut guard = self.power_types.lock();
264
265        if let Some(existing) = guard.as_ref() {
266            return Ok(Arc::clone(existing));
267        }
268
269        let dbc = self.shared_dbc()?;
270        let data: Arc<[PowerTypeFlat]> = transform_all_power_types(&dbc).into();
271
272        *guard = Some(Arc::clone(&data));
273
274        Ok(data)
275    }
276
277    fn ensure_item_damage<R>(
278        &self,
279        access: impl FnOnce(&ItemDamageMap) -> R,
280    ) -> Result<R, ResolverError> {
281        let mut guard = self.item_damage.lock();
282        let map = if let Some(map) = guard.as_ref() {
283            map
284        } else {
285            let dbc = self.shared_dbc()?;
286            let mut by_type: ItemDamageMap = FastMap::default();
287
288            for mut row in transform_all_item_damage_scaling(&dbc) {
289                let weapon_type = std::mem::take(&mut row.weapon_type);
290
291                by_type
292                    .entry(weapon_type)
293                    .or_default()
294                    .insert(row.item_level, row);
295            }
296
297            guard.insert(by_type)
298        };
299
300        Ok(access(map))
301    }
302
303    fn ensure_expected_stats<R>(
304        &self,
305        access: impl FnOnce(&ExpectedStatMap) -> R,
306    ) -> Result<R, ResolverError> {
307        let mut guard = self.expected_stats.lock();
308        let map = if let Some(map) = guard.as_ref() {
309            map
310        } else {
311            let dbc = self.shared_dbc()?;
312            let mut by_key: ExpectedStatMap = FastMap::default();
313
314            for row in transform_all_expected_stats(&dbc) {
315                by_key.insert(ExpectedStatsKey::new(row.expansion_id, row.lvl), row);
316            }
317
318            guard.insert(by_key)
319        };
320
321        Ok(access(map))
322    }
323
324    fn ensure_permanent_enchants(&self) -> Result<Arc<[PermanentEnchantFlat]>, ResolverError> {
325        let mut guard = self.permanent_enchants.lock();
326
327        if let Some(existing) = guard.as_ref() {
328            return Ok(Arc::clone(existing));
329        }
330
331        let dbc = self.shared_dbc()?;
332        let entries: Arc<[PermanentEnchantFlat]> = transform_all_permanent_enchants(&dbc).into();
333
334        *guard = Some(Arc::clone(&entries));
335
336        Ok(entries)
337    }
338}
339
340impl DataResolver for LocalCsvResolver {
341    async fn get_spell(&self, spell_id: SpellId) -> Result<SpellDataFlat, ResolverError> {
342        self.read_spells(|map| map.get(&spell_id.as_i32()).cloned())?
343            .ok_or_else(|| ResolverError::spell_not_found(spell_id))
344    }
345
346    async fn get_spells(&self, spell_ids: &[SpellId]) -> Result<Vec<SpellDataFlat>, ResolverError> {
347        self.read_spells(|map| {
348            let mut result = Vec::with_capacity(spell_ids.len());
349
350            for &id in spell_ids {
351                let spell = map
352                    .get(&id.as_i32())
353                    .cloned()
354                    .ok_or_else(|| ResolverError::spell_not_found(id))?;
355
356                result.push(spell);
357            }
358
359            Ok(result)
360        })?
361    }
362
363    async fn get_spell_effect(
364        &self,
365        spell_id: SpellId,
366        effect_index: u8,
367    ) -> Result<SpellEffect, ResolverError> {
368        self.read_spells(|map| {
369            let spell = map
370                .get(&spell_id.as_i32())
371                .ok_or_else(|| ResolverError::spell_not_found(spell_id))?;
372
373            crate::rows::validate_effect_index(spell_id, effect_index, &spell.effects)
374        })?
375    }
376
377    async fn get_spell_effects(
378        &self,
379        spell_id: SpellId,
380    ) -> Result<Vec<SpellEffect>, ResolverError> {
381        self.read_spells(|map| {
382            map.get(&spell_id.as_i32())
383                .map(|spell| spell.effects.clone())
384                .ok_or_else(|| ResolverError::spell_not_found(spell_id))
385        })?
386    }
387
388    async fn get_item(&self, item_id: i32) -> Result<ItemDataFlat, ResolverError> {
389        self.read_items(|map| map.get(&item_id).cloned())?
390            .ok_or_else(|| ResolverError::item_not_found(item_id))
391    }
392
393    async fn find_consumable_items(
394        &self,
395        name_token: &str,
396        subclass: i32,
397    ) -> Result<Vec<ItemDataFlat>, ResolverError> {
398        self.read_items(|map| {
399            map.values()
400                .filter(|item| item_matches_consumable_query(item, name_token, subclass))
401                .cloned()
402                .collect()
403        })
404    }
405
406    async fn get_scaling_data(&self) -> Result<Arc<ItemScalingData>, ResolverError> {
407        self.ensure_scaling_data()
408    }
409
410    async fn get_item_damage_scaling(
411        &self,
412        item_level: i32,
413        weapon_type: &str,
414    ) -> Result<ItemDamageScalingFlat, ResolverError> {
415        self.ensure_item_damage(|by_type| {
416            by_type
417                .get(weapon_type)
418                .and_then(|by_ilvl| by_ilvl.get(&item_level))
419                .cloned()
420        })?
421        .ok_or_else(|| ResolverError::item_damage_scaling_not_found(weapon_type, item_level))
422    }
423
424    async fn get_expected_stats(
425        &self,
426        expansion_id: i32,
427        lvl: i32,
428    ) -> Result<ExpectedStatFlat, ResolverError> {
429        self.ensure_expected_stats(|by_key| {
430            by_key
431                .get(&ExpectedStatsKey::new(expansion_id, lvl))
432                .cloned()
433        })?
434        .ok_or_else(|| ResolverError::expected_stats_not_found(expansion_id, lvl))
435    }
436
437    async fn get_enchantment(&self, enchantment_id: i32) -> Result<EnchantmentRow, ResolverError> {
438        self.read_enchantments(|map| map.get(&enchantment_id).map(crate::rows::enchantment_row))?
439            .ok_or_else(|| ResolverError::enchantment_not_found(enchantment_id))
440    }
441
442    async fn find_permanent_enchant(
443        &self,
444        query: &PermanentEnchantQuery,
445    ) -> Result<Option<PermanentEnchantFlat>, ResolverError> {
446        let entries = self.ensure_permanent_enchants()?;
447
448        Ok(select_permanent_enchant(&entries, query))
449    }
450
451    async fn get_power_types(&self) -> Result<Vec<PowerTypeFlat>, ResolverError> {
452        Ok(self.ensure_power_types()?.to_vec())
453    }
454
455    async fn get_spec(&self, spec_id: i32) -> Result<SpecDataFlat, ResolverError> {
456        self.read_specs(|map| map.get(&spec_id).cloned())?
457            .ok_or_else(|| ResolverError::spec_not_found(spec_id))
458    }
459
460    async fn get_trait_tree(&self, spec_id: i32) -> Result<TraitTreeFlat, ResolverError> {
461        self.read_traits(|map| map.get(&spec_id).cloned())?
462            .ok_or_else(|| ResolverError::trait_tree_not_found(spec_id))
463    }
464
465    async fn get_expansion_trait_tree(
466        &self,
467        expansion_id: i32,
468        system: &str,
469    ) -> Result<ExpansionTraitTreeFlat, ResolverError> {
470        let mut guard = self.expansion_traits.lock();
471
472        if guard.is_none() {
473            let dbc = self.shared_dbc()?;
474
475            *guard = Some(transform_all_expansion_trait_trees(&dbc));
476        }
477
478        guard
479            .as_ref()
480            .and_then(|trees| {
481                trees
482                    .iter()
483                    .find(|tree| tree.expansion_id == expansion_id && tree.system == system)
484            })
485            .cloned()
486            .ok_or_else(|| ResolverError::expansion_trait_tree_not_found(expansion_id, system))
487    }
488
489    async fn get_spell_overrides(
490        &self,
491        spec_id: i32,
492    ) -> Result<Vec<(SpellId, SpellId)>, ResolverError> {
493        self.read_spec_spells(|map| {
494            crate::rows::spell_override_pairs(
495                map.get(&spec_id)
496                    .into_iter()
497                    .flatten()
498                    .map(|r| (r.overrides_spell_id, r.spell_id)),
499            )
500        })
501    }
502
503    async fn get_specialization_spells(&self, spec_id: i32) -> Result<Vec<SpellId>, ResolverError> {
504        self.read_spec_spells(|map| {
505            map.get(&spec_id)
506                .into_iter()
507                .flatten()
508                .map(|r| SpellId::new(r.spell_id))
509                .collect()
510        })
511    }
512
513    async fn get_racial_spells(
514        &self,
515        race_id: i32,
516        class_id: i32,
517    ) -> Result<Vec<SpellId>, ResolverError> {
518        self.read_racial_spells(|map| {
519            crate::rows::racial_spell_ids(
520                map.get(&race_id)
521                    .into_iter()
522                    .flatten()
523                    .map(|r| (r.spell_id, r.class_mask)),
524                class_id,
525            )
526        })
527    }
528
529    async fn get_creature(&self, creature_id: i32) -> Result<CreatureFlat, ResolverError> {
530        self.read_creatures(|map| map.get(&creature_id).cloned())?
531            .ok_or_else(|| ResolverError::creature_not_found(creature_id))
532    }
533
534    async fn get_creature_difficulties(
535        &self,
536        creature_id: i32,
537    ) -> Result<Vec<CreatureDifficultyFlat>, ResolverError> {
538        self.read_creature_difficulties(|map| map.get(&creature_id).cloned().unwrap_or_default())
539    }
540
541    async fn get_content_tuning(
542        &self,
543        content_tuning_id: i32,
544    ) -> Result<ContentTuningFlat, ResolverError> {
545        self.read_content_tunings(|map| map.get(&content_tuning_id).cloned())?
546            .ok_or_else(|| ResolverError::content_tuning_not_found(content_tuning_id))
547    }
548
549    async fn get_content_tuning_x_difficulty(
550        &self,
551        content_tuning_id: i32,
552    ) -> Result<Vec<ContentTuningXDifficultyFlat>, ResolverError> {
553        self.read_content_tuning_x_difficulty(|map| {
554            map.get(&content_tuning_id).cloned().unwrap_or_default()
555        })
556    }
557
558    async fn get_content_tuning_x_expected(
559        &self,
560        content_tuning_id: i32,
561    ) -> Result<Vec<ContentTuningXExpectedFlat>, ResolverError> {
562        self.read_content_tuning_x_expected(|map| {
563            map.get(&content_tuning_id).cloned().unwrap_or_default()
564        })
565    }
566
567    async fn get_expected_stat_mod(
568        &self,
569        expected_stat_mod_id: i32,
570    ) -> Result<ExpectedStatModFlat, ResolverError> {
571        self.read_expected_stat_mods(|map| map.get(&expected_stat_mod_id).cloned())?
572            .ok_or_else(|| ResolverError::expected_stat_mod_not_found(expected_stat_mod_id))
573    }
574
575    async fn get_challenge_mode_health(
576        &self,
577        keystone_level: i32,
578    ) -> Result<ChallengeModeHealthFlat, ResolverError> {
579        self.read_challenge_mode_health(|map| map.get(&keystone_level).cloned())?
580            .ok_or_else(|| ResolverError::challenge_mode_health_not_found(keystone_level))
581    }
582
583    async fn get_rotation_script(&self, rotation_id: &str) -> Result<String, ResolverError> {
584        super::read_rotation_from_disk(&self.data_dir, rotation_id)
585    }
586}
587
588#[cfg(test)]
589mod tests;