wowlab_parsers/parsers/transform/loot/
crafted.rs1use wowlab_engine_domain::dbc::{SpellEffectKind, spell_effect_kind};
2use wowlab_types::{
3 data::{CraftedProductFlat, Profession},
4 sim::IntMap,
5};
6
7use super::shared::{item_display_name, item_level, profession_name};
8use crate::parsers::dbc::DbcData;
9
10struct CraftedRecipeMaps {
11 by_crafting_data_id: IntMap<i32, i32>,
12 by_item_id: IntMap<i32, i32>,
13}
14
15fn build_recipe_maps(dbc: &DbcData) -> CraftedRecipeMaps {
16 let mut by_crafting_data_id: IntMap<i32, i32> = IntMap::default();
17 let mut by_item_id: IntMap<i32, i32> = IntMap::default();
18
19 for effects in dbc.spell_effect.values() {
20 for effect in effects {
21 match spell_effect_kind(effect.Effect) {
22 Some(SpellEffectKind::ModifiedCrafting) => {
23 by_crafting_data_id
24 .entry(effect.EffectMiscValue_0)
25 .or_insert(effect.SpellID);
26 }
27 Some(SpellEffectKind::CreateItem | SpellEffectKind::CreateHeirloom) => {
28 by_item_id
29 .entry(effect.EffectItemType)
30 .or_insert(effect.SpellID);
31 }
32 _ => {}
33 }
34 }
35 }
36
37 CraftedRecipeMaps {
38 by_crafting_data_id,
39 by_item_id,
40 }
41}
42
43fn resolve_recipe_spell(
44 maps: &CraftedRecipeMaps,
45 crafting_data_id: i32,
46 crafted_item_id: i32,
47) -> Option<i32> {
48 if let Some(spell) = maps.by_crafting_data_id.get(&crafting_data_id).copied() {
49 return Some(spell);
50 }
51
52 if crafted_item_id == 0 {
53 return None;
54 }
55
56 maps.by_item_id.get(&crafted_item_id).copied()
57}
58
59fn resolve_profession(dbc: &DbcData, recipe_spell_id: i32) -> Option<(i32, i32)> {
60 let abilities = dbc.skill_line_ability.get(&recipe_spell_id)?;
61
62 for ability in abilities {
63 for prof in dbc.profession.values() {
64 if prof.SkillLineID == ability.SkillLine {
65 return Some((prof.ID, prof.ProfessionEnumValue));
66 }
67 }
68 }
69
70 None
71}
72
73#[must_use]
77pub fn transform_all_crafted_products(dbc: &DbcData) -> Vec<CraftedProductFlat> {
78 let recipe_maps = build_recipe_maps(dbc);
79 let mut out: Vec<CraftedProductFlat> = Vec::new();
80
81 for crafting_data in dbc.crafting_data.values() {
82 let Some(recipe_spell_id) =
83 resolve_recipe_spell(&recipe_maps, crafting_data.ID, crafting_data.CraftedItemID)
84 else {
85 continue;
86 };
87
88 let Some((profession_id, profession_enum_value)) = resolve_profession(dbc, recipe_spell_id)
89 else {
90 continue;
91 };
92
93 let crafted_item_name = item_display_name(dbc, crafting_data.CraftedItemID);
94 let profession_label = profession_name(profession_enum_value).to_string();
95
96 let Some(variants) = dbc.crafting_data_item_quality.get(&crafting_data.ID) else {
97 continue;
98 };
99
100 for variant in variants {
101 let Some(modified) = dbc.modified_crafting_item.get(&variant.ItemID) else {
102 continue;
103 };
104 let Some(quality) = dbc.crafting_quality.get(&modified.CraftingQualityID) else {
105 continue;
106 };
107
108 out.push(CraftedProductFlat {
109 id: 0,
110 crafting_data_id: crafting_data.ID,
111 crafted_item_id: crafting_data.CraftedItemID,
112 crafted_item_name: crafted_item_name.clone(),
113 quality_tier: quality.QualityTier,
114 quality_item_id: variant.ItemID,
115 quality_item_name: item_display_name(dbc, variant.ItemID),
116 quality_item_level: item_level(dbc, variant.ItemID),
117 profession_id,
118 profession_name: profession_label.clone(),
119 profession_enum_value,
120 item_bonus_tree_id: crafting_data.ItemBonusTreeID,
121 });
122 }
123 }
124
125 out
126}
127
128#[must_use]
130pub fn transform_all_professions(dbc: &DbcData) -> Vec<Profession> {
131 dbc.profession
132 .values()
133 .map(|row| Profession {
134 id: row.ID,
135 skill_line_id: row.SkillLineID,
136 profession_enum_value: row.ProfessionEnumValue,
137 name: profession_name(row.ProfessionEnumValue).to_string(),
138 })
139 .collect()
140}
141
142#[cfg(test)]
143mod tests {
144 use googletest::prelude::*;
145
146 use super::*;
147 use crate::parsers::{
148 dbc::rows::SpellEffectRow,
149 transform::fixtures::{dbc, profession_row, skill_line_ability_row, spell_effect_row},
150 };
151
152 #[gtest]
153 fn resolve_recipe_spell_priority() -> Result<()> {
154 let mut by_crafting_data_id: IntMap<i32, i32> = IntMap::default();
155 let mut by_item_id: IntMap<i32, i32> = IntMap::default();
156
157 by_crafting_data_id.insert(1, 1000);
158 by_item_id.insert(1, 2000);
159 by_item_id.insert(5, 3000);
160 let maps = CraftedRecipeMaps {
161 by_crafting_data_id,
162 by_item_id,
163 };
164
165 verify_that!(resolve_recipe_spell(&maps, 1, 1), some(eq(1000)))?;
166 verify_that!(resolve_recipe_spell(&maps, 99, 0), none())?;
167 verify_that!(resolve_recipe_spell(&maps, 99, 5), some(eq(3000)))?;
168
169 verify_that!(resolve_recipe_spell(&maps, 99, 88), none())
170 }
171
172 #[gtest]
173 fn build_recipe_maps_keeps_first_and_classifies_effects() -> Result<()> {
174 let mut dbc = dbc();
175
176 dbc.spell_effect.insert(
177 1,
178 vec![
179 SpellEffectRow {
180 EffectMiscValue_0: 50,
181 ..spell_effect_row(1000, SpellEffectKind::ModifiedCrafting as i32, 0)
182 },
183 SpellEffectRow {
184 EffectMiscValue_0: 50,
185 ..spell_effect_row(9999, SpellEffectKind::ModifiedCrafting as i32, 0)
186 },
187 SpellEffectRow {
188 EffectItemType: 77,
189 ..spell_effect_row(2000, SpellEffectKind::CreateItem as i32, 0)
190 },
191 SpellEffectRow {
192 EffectItemType: 88,
193 ..spell_effect_row(3000, SpellEffectKind::CreateHeirloom as i32, 0)
194 },
195 ],
196 );
197
198 let maps = build_recipe_maps(&dbc);
199
200 verify_that!(maps.by_crafting_data_id.get(&50), some(eq(&1000)))?;
201 verify_that!(maps.by_item_id.get(&77), some(eq(&2000)))?;
202
203 verify_that!(maps.by_item_id.get(&88), some(eq(&3000)))
204 }
205
206 #[gtest]
207 fn resolve_profession_joins_skill_line() -> Result<()> {
208 let mut matched = dbc();
209
210 matched
211 .skill_line_ability
212 .insert(500, vec![skill_line_ability_row(1, 164, 500)]);
213 matched.profession.insert(7, profession_row(7, 164, 2));
214
215 verify_that!(resolve_profession(&matched, 500), some(eq((7, 2))))?;
216 verify_that!(resolve_profession(&matched, 999), none())?;
217
218 let mut unmatched = dbc();
219
220 unmatched
221 .skill_line_ability
222 .insert(600, vec![skill_line_ability_row(2, 999, 600)]);
223 unmatched.profession.insert(7, profession_row(7, 164, 2));
224
225 verify_that!(resolve_profession(&unmatched, 600), none())
226 }
227}