Skip to main content

wowlab_parsers/parsers/transform/loot/
shared.rs

1use wowlab_types::data::{DifficultyKind, InstanceKind};
2
3use crate::parsers::dbc::DbcData;
4
5#[rustfmt::skip]
6const PROFESSION_NAMES: &[(i32, &str)] = &[
7    // #t:aligned
8    (0 , "First Aid (legacy)"),
9    (1 , "Blacksmithing")     ,
10    (2 , "Leatherworking")    ,
11    (3 , "Alchemy")           ,
12    (4 , "Herbalism")         ,
13    (5 , "Cooking")           ,
14    (6 , "Mining")            ,
15    (7 , "Tailoring")         ,
16    (8 , "Engineering")       ,
17    (9 , "Enchanting")        ,
18    (10, "Fishing")           ,
19    (11, "Skinning")          ,
20    (12, "Jewelcrafting")     ,
21    (13, "Inscription")       ,
22    (14, "Archaeology")       ,
23];
24
25const WORLD_BOSS_ORDER_INDEX: i32 = 9999;
26
27pub(super) fn profession_name(profession_enum_value: i32) -> &'static str {
28    PROFESSION_NAMES
29        .iter()
30        .find(|(v, _)| *v == profession_enum_value)
31        .map_or("", |(_, name)| *name)
32}
33
34pub(super) const fn difficulty_kind(instance_type: i32) -> DifficultyKind {
35    match instance_type {
36        1 => DifficultyKind::Dungeon,
37        2 => DifficultyKind::Raid,
38        3 => DifficultyKind::Pvp,
39        5 => DifficultyKind::Scenario,
40        _ => DifficultyKind::Unknown,
41    }
42}
43
44pub(super) fn item_display_name(dbc: &DbcData, item_id: i32) -> String {
45    dbc.item_sparse
46        .get(&item_id)
47        .and_then(|s| s.Display_lang.clone())
48        .unwrap_or_default()
49}
50
51pub(super) fn item_level(dbc: &DbcData, item_id: i32) -> i32 {
52    dbc.item_sparse.get(&item_id).map_or(0, |s| s.ItemLevel)
53}
54
55pub(super) fn is_mythic_plus_map(dbc: &DbcData, map_id: i32) -> bool {
56    dbc.map_challenge_mode
57        .values()
58        .any(|row| row.MapID == map_id)
59}
60
61pub(super) fn journal_image_path(file_data_id: i32) -> String {
62    if file_data_id > 0 {
63        format!("journal/images/{file_data_id}.png")
64    } else {
65        String::new()
66    }
67}
68
69pub(super) fn encounter_image_path(file_data_id: i32) -> String {
70    if file_data_id > 0 {
71        format!("journal/encounters/{file_data_id}.png")
72    } else {
73        String::new()
74    }
75}
76
77pub(super) fn loadscreen_image_path(file_data_id: i32) -> String {
78    if file_data_id > 0 {
79        format!("loadscreens/images/{file_data_id}.png")
80    } else {
81        String::new()
82    }
83}
84
85pub(super) fn loadscreen_seo_path(file_data_id: i32) -> String {
86    if file_data_id > 0 {
87        format!("loadscreens/seo/{file_data_id}.png")
88    } else {
89        String::new()
90    }
91}
92
93fn normalized_media_key(raw: &str) -> String {
94    let lower = raw.to_lowercase().replace(".blp", "");
95
96    [
97        "ui-ej-dungeonbutton-",
98        "ui-ej-background-",
99        "ui-ej-lorebg-",
100        "loadscreen_dungeon_",
101        "loadscreen_raid_",
102        "loadscreen_zone_",
103        "loadscreen_",
104        "loadingscreen_",
105        "loadscreen",
106        "loadingscreen",
107    ]
108    .into_iter()
109    .fold(lower, |value, prefix| match value.strip_prefix(prefix) {
110        Some(stripped) => stripped.to_owned(),
111        None => value,
112    })
113    .chars()
114    .filter(char::is_ascii_alphanumeric)
115    .collect()
116}
117
118pub(super) fn resolve_loadscreen_file_data_id(dbc: &DbcData, instance_name: &str) -> i32 {
119    const EXACT_MATCH_SCORE: i32 = 3;
120    const PARTIAL_MATCH_SCORE: i32 = 2;
121
122    let instance_key = normalized_media_key(instance_name);
123
124    if instance_key.is_empty() {
125        return 0;
126    }
127
128    let candidates = dbc.manifest_interface_data.values().filter(|row| {
129        let path = row.FilePath.replace('\\', "/").to_lowercase();
130
131        path.starts_with("interface/glues/loadingscreens/")
132            && row.FileName.to_lowercase().ends_with(".blp")
133    });
134    let matches = candidates.filter_map(|row| {
135        let key = normalized_media_key(&row.FileName);
136        let score = if key == instance_key {
137            EXACT_MATCH_SCORE
138        } else if key.contains(&instance_key) || instance_key.contains(&key) {
139            PARTIAL_MATCH_SCORE
140        } else {
141            0
142        };
143
144        (score > 0).then_some((score, row.ID))
145    });
146
147    matches
148        .max_by_key(|(score, id)| (*score, -*id))
149        .map(|(_, id)| id)
150        .unwrap_or_default()
151}
152
153pub(super) fn primary_encounter_creature(
154    dbc: &DbcData,
155    journal_encounter_id: i32,
156) -> Option<&crate::parsers::dbc::rows::JournalEncounterCreatureRow> {
157    dbc.journal_encounter_creature
158        .get(&journal_encounter_id)?
159        .iter()
160        .filter(|creature| creature.FileDataID > 0)
161        .min_by_key(|creature| creature.OrderIndex)
162}
163
164pub(super) fn instance_kind(
165    dbc: &DbcData,
166    map_id: i32,
167    order_index: i32,
168    flags: i32,
169) -> InstanceKind {
170    const FLAG_OPEN_WORLD_ZONE: i32 = 2;
171
172    if flags == FLAG_OPEN_WORLD_ZONE || order_index == WORLD_BOSS_ORDER_INDEX {
173        InstanceKind::World
174    } else {
175        match dbc.map.get(&map_id).map(|map| map.InstanceType) {
176            Some(1) => InstanceKind::Dungeon,
177            Some(2) => InstanceKind::Raid,
178            _ if is_mythic_plus_map(dbc, map_id) => InstanceKind::Dungeon,
179            _ => InstanceKind::World,
180        }
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use googletest::prelude::*;
187    use rstest::rstest;
188
189    use super::*;
190    use crate::parsers::transform::fixtures::{dbc, manifest_row, map_challenge_mode_row, map_row};
191
192    #[gtest]
193    #[rstest]
194    #[case::blacksmithing(1, "Blacksmithing")]
195    #[case::jewelcrafting(12, "Jewelcrafting")]
196    #[case::legacy_first_aid(0, "First Aid (legacy)")]
197    #[case::unknown(99, "")]
198    fn profession_name_cases(#[case] value: i32, #[case] expected: &str) -> Result<()> {
199        verify_that!(profession_name(value), eq(expected))
200    }
201
202    #[gtest]
203    #[rstest]
204    #[case::dungeon(1, DifficultyKind::Dungeon)]
205    #[case::raid(2, DifficultyKind::Raid)]
206    #[case::pvp(3, DifficultyKind::Pvp)]
207    #[case::scenario(5, DifficultyKind::Scenario)]
208    #[case::gap_four(4, DifficultyKind::Unknown)]
209    #[case::unknown(0, DifficultyKind::Unknown)]
210    fn difficulty_kind_cases(
211        #[case] instance_type: i32,
212        #[case] expected: DifficultyKind,
213    ) -> Result<()> {
214        verify_that!(difficulty_kind(instance_type), eq(expected))
215    }
216
217    #[gtest]
218    fn image_path_helpers() -> Result<()> {
219        verify_that!(journal_image_path(42), eq("journal/images/42.png"))?;
220        verify_that!(journal_image_path(0), eq(""))?;
221        verify_that!(journal_image_path(-1), eq(""))?;
222        verify_that!(encounter_image_path(5), eq("journal/encounters/5.png"))?;
223        verify_that!(loadscreen_image_path(5), eq("loadscreens/images/5.png"))?;
224
225        verify_that!(loadscreen_seo_path(5), eq("loadscreens/seo/5.png"))
226    }
227
228    #[gtest]
229    #[rstest]
230    #[case::plain("Halls", "halls")]
231    #[case::blp_stripped("Foo.blp", "foo")]
232    #[case::prefix_dungeonbutton("UI-EJ-DungeonButton-TheStonevault", "thestonevault")]
233    #[case::loadscreen_prefix("LoadScreen_Dungeon_Foo", "foo")]
234    #[case::non_alnum_removed("A_b-c 1!", "abc1")]
235    #[case::empty("", "")]
236    fn normalized_media_key_cases(#[case] raw: &str, #[case] expected: &str) -> Result<()> {
237        verify_that!(normalized_media_key(raw), eq(expected))
238    }
239
240    #[gtest]
241    fn instance_kind_branches() -> Result<()> {
242        const FLAG_WORLD: i32 = 2;
243        const WORLD_ORDER: i32 = 9999;
244
245        let empty = dbc();
246
247        verify_that!(
248            instance_kind(&empty, 1, 0, FLAG_WORLD),
249            eq(InstanceKind::World)
250        )?;
251        verify_that!(
252            instance_kind(&empty, 1, WORLD_ORDER, 0),
253            eq(InstanceKind::World)
254        )?;
255
256        let mut dungeon = dbc();
257
258        dungeon.map.insert(1, map_row(1, 1));
259        verify_that!(instance_kind(&dungeon, 1, 0, 0), eq(InstanceKind::Dungeon))?;
260
261        let mut raid = dbc();
262
263        raid.map.insert(2, map_row(2, 2));
264        verify_that!(instance_kind(&raid, 2, 0, 0), eq(InstanceKind::Raid))?;
265
266        verify_that!(instance_kind(&empty, 5, 0, 0), eq(InstanceKind::World))?;
267
268        let mut mplus = dbc();
269
270        mplus
271            .map_challenge_mode
272            .insert(1, map_challenge_mode_row(1, 5));
273
274        verify_that!(instance_kind(&mplus, 5, 0, 0), eq(InstanceKind::Dungeon))
275    }
276
277    #[gtest]
278    fn resolve_loadscreen_file_data_id_scoring() -> Result<()> {
279        const DIR: &str = "interface/glues/loadingscreens/";
280        const EXACT: &str = "LoadScreen_Dungeon_TheStonevault.blp";
281        const PARTIAL: &str = "LoadScreen_Dungeon_TheStonevaultExtra.blp";
282
283        let empty = dbc();
284
285        verify_that!(resolve_loadscreen_file_data_id(&empty, ""), eq(0))?;
286        verify_that!(resolve_loadscreen_file_data_id(&empty, "!!!"), eq(0))?;
287
288        let mut exact = dbc();
289
290        exact
291            .manifest_interface_data
292            .insert(50, manifest_row(50, DIR, EXACT));
293        verify_that!(
294            resolve_loadscreen_file_data_id(&exact, "TheStonevault"),
295            eq(50)
296        )?;
297
298        let mut mixed = dbc();
299
300        mixed
301            .manifest_interface_data
302            .insert(50, manifest_row(50, DIR, EXACT));
303        mixed
304            .manifest_interface_data
305            .insert(5, manifest_row(5, DIR, PARTIAL));
306        verify_that!(
307            resolve_loadscreen_file_data_id(&mixed, "TheStonevault"),
308            eq(50)
309        )?;
310
311        let mut ties = dbc();
312
313        ties.manifest_interface_data
314            .insert(20, manifest_row(20, DIR, EXACT));
315        ties.manifest_interface_data
316            .insert(10, manifest_row(10, DIR, EXACT));
317
318        verify_that!(
319            resolve_loadscreen_file_data_id(&ties, "TheStonevault"),
320            eq(10)
321        )
322    }
323}