Skip to main content

wowlab_parsers/parsers/transform/loot/
crafted.rs

1use 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/// Build crafted-product rows, emitting one per quality variant.
74// #t(fn: rust_alloc_in_loop) row construction allocates owned Strings per emitted product
75// #t(fn: rust_clone_in_loop) shared crafted_item_name / profession_name cloned into each variant row
76#[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/// 1:1 transform of `Profession` rows + name lookup.
129#[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}