1use wowlab_types::data::{DifficultyKind, InstanceKind};
2
3use crate::parsers::dbc::DbcData;
4
5#[rustfmt::skip]
6const PROFESSION_NAMES: &[(i32, &str)] = &[
7 (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}