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 #[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 #[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;