1use wowlab_engine_domain::dbc::InventoryType;
2use wowlab_types::data::{
3 ItemClassification, ItemDataFlat, ItemDropSource, ItemEffect, ItemSetBonus, ItemSetInfo,
4 ItemStat,
5};
6
7#[cfg(feature = "dbc")]
8use super::{super::dbc::DbcData, super::errors::TransformError};
9
10macro_rules! array_fields {
11 ($base:ident . $prefix:ident, [ $($n:literal),+ $(,)? ]) => {
12 [ $( $crate::_private::paste::paste!( $base.[<$prefix _ $n>] ) ),+ ]
13 };
14 (vec $base:ident . $prefix:ident, [ $($n:literal),+ $(,)? ]) => {
15 ::std::vec![ $( $crate::_private::paste::paste!( $base.[<$prefix _ $n>] ) ),+ ]
16 };
17}
18
19#[rustfmt::skip]
20const EXPANSION_NAMES: &[(i32, &str)] = &[
21 (0 , "Classic") ,
23 (1 , "The Burning Crusade") ,
24 (2 , "Wrath of the Lich King"),
25 (3 , "Cataclysm") ,
26 (4 , "Mists of Pandaria") ,
27 (5 , "Warlords of Draenor") ,
28 (6 , "Legion") ,
29 (7 , "Battle for Azeroth") ,
30 (8 , "Shadowlands") ,
31 (9 , "Dragonflight") ,
32 (10, "The War Within") ,
33];
34
35fn combine_allowable_race(low: Option<i64>, high: Option<i64>) -> i64 {
36 const WORD_BITS: u32 = 32;
37 let [byte_0, byte_1, byte_2, byte_3, ..] = low.unwrap_or(-1).to_le_bytes();
38 let low = i64::from(u32::from_le_bytes([byte_0, byte_1, byte_2, byte_3]));
39 let high = high.unwrap_or(-1);
40
41 (high << WORD_BITS) | low
42}
43
44fn decode_difficulty_mask(mask: i32) -> Vec<i32> {
46 const MAX_DIFFICULTY_ID: i32 = 31;
47
48 if mask == -1 || mask == 0 {
49 return Vec::new();
50 }
51
52 (1..=MAX_DIFFICULTY_ID)
53 .filter(|id| mask & (1 << (id - 1)) != 0)
54 .collect()
55}
56
57fn get_expansion_name(id: i32) -> &'static str {
58 EXPANSION_NAMES
59 .iter()
60 .find(|(i, _)| *i == id)
61 .map_or("Unknown", |(_, name)| *name)
62}
63
64fn get_inventory_type_name(id: i32) -> &'static str {
65 InventoryType::try_from(id)
66 .ok()
67 .map_or("Unknown", InventoryType::display_name)
68}
69
70pub fn transform_item(dbc: &DbcData, item_id: i32) -> Result<ItemDataFlat, TransformError> {
77 let item = dbc
78 .item
79 .get(&item_id)
80 .ok_or_else(|| TransformError::item_not_found(item_id))?;
81
82 let sparse = dbc.item_sparse.get(&item_id);
83
84 let mut icon_file_data_id = item.IconFileDataID;
85
86 if icon_file_data_id == 0
87 && let Some(modified_appearance) = dbc.item_modified_appearance.get(&item_id)
88 && let Some(appearance) = dbc
89 .item_appearance
90 .get(&modified_appearance.ItemAppearanceID)
91 && appearance.DefaultIconFileDataID > 0
92 {
93 icon_file_data_id = appearance.DefaultIconFileDataID;
94 }
95
96 let file_name = super::resolve_icon_file_name(dbc, icon_file_data_id);
97
98 let effect_links = dbc.item_x_item_effect.get(&item_id);
99 let effects: Vec<ItemEffect> = effect_links
100 .map(|links| {
101 links
102 .iter()
103 .filter_map(|link| {
104 dbc.item_effect
105 .get(&link.ItemEffectID)
106 .map(|effect| ItemEffect {
107 spell_id: effect.SpellID,
108 trigger_type: effect.TriggerType,
109 charges: effect.Charges,
110 cooldown: effect.CoolDownMSec,
111 category_cooldown: effect.CategoryCoolDownMSec,
112 })
113 })
114 .collect()
115 })
116 .unwrap_or_default();
117
118 let stats: Vec<ItemStat> = sparse
119 .map(|s| {
120 let stat_types = array_fields!(s.StatModifierBonusStat, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
121 let stat_values = array_fields!(s.StatPercentEditor, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
122
123 stat_types
124 .iter()
125 .zip(stat_values.iter())
126 .filter(|&(&t, _)| t != -1 && t != 0)
127 .map(|(&t, &v)| ItemStat {
128 stat_type: t,
129 value: v,
130 })
131 .collect()
132 })
133 .unwrap_or_default();
134
135 let expansion_id = sparse.map_or(0, |s| s.ExpansionID);
136 let item_class_row = dbc.item_class_by_class_id.get(&item.ClassID);
137 let item_sub_classes = dbc.item_sub_class_by_class_id.get(&item.ClassID);
138 let item_sub_class_row =
139 item_sub_classes.and_then(|scs| scs.iter().find(|sc| sc.SubClassID == item.SubclassID));
140
141 let classification = Some(ItemClassification {
142 class_id: item.ClassID,
143 class_name: item_class_row
144 .and_then(|c| c.ClassName_lang.clone())
145 .unwrap_or_else(|| "Unknown".to_string())
146 .into(),
147 subclass_id: item.SubclassID,
148 subclass_name: item_sub_class_row
149 .and_then(|sc| sc.DisplayName_lang.clone())
150 .unwrap_or_else(|| "Unknown".to_string())
151 .into(),
152 inventory_type: item.InventoryType,
153 inventory_type_name: get_inventory_type_name(item.InventoryType).into(),
154 expansion_id,
155 expansion_name: get_expansion_name(expansion_id).into(),
156 });
157
158 let sockets: Vec<i32> = sparse
159 .map(|s| {
160 array_fields!(s.SocketType, [0, 1, 2])
161 .into_iter()
162 .filter(|&t| t > 0)
163 .collect()
164 })
165 .unwrap_or_default();
166
167 let flags: Vec<i32> = sparse
168 .map(|s| array_fields!(vec s.Flags, [0, 1, 2, 3, 4]))
169 .unwrap_or_default();
170
171 let item_set_id = sparse.map_or(0, |s| s.ItemSet);
172 let set_info = if item_set_id > 0 {
173 dbc.item_set.get(&item_set_id).map(|item_set| {
174 let item_ids: Vec<i32> = array_fields!(
175 item_set.ItemID,
176 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
177 )
178 .into_iter()
179 .filter(|&id| id > 0)
180 .collect();
181
182 let bonuses: Vec<ItemSetBonus> = dbc
183 .item_set_spell
184 .get(&item_set_id)
185 .map(|spells| {
186 spells
187 .iter()
188 .map(|spell| ItemSetBonus {
189 threshold: spell.Threshold,
190 spell_id: spell.SpellID,
191 spec_id: spell.ChrSpecID,
192 })
193 .collect()
194 })
195 .unwrap_or_default();
196
197 ItemSetInfo {
198 set_id: item_set_id,
199 set_name: item_set
200 .Name_lang
201 .clone()
202 .unwrap_or_else(|| "Unknown Set".to_string())
203 .into(),
204 item_ids,
205 bonuses,
206 }
207 })
208 } else {
209 None
210 };
211
212 let drop_sources: Vec<ItemDropSource> = dbc
213 .journal_encounter_item
214 .get(&item_id)
215 .map(|encounter_items| {
216 encounter_items
217 .iter()
218 .filter_map(|encounter_item| {
219 let encounter = dbc
220 .journal_encounter
221 .get(&encounter_item.JournalEncounterID)?;
222 let instance = dbc.journal_instance.get(&encounter.JournalInstanceID);
223
224 Some(ItemDropSource {
225 instance_id: instance.map_or(0, |i| i.ID),
226 instance_name: instance
227 .and_then(|i| i.Name_lang.clone())
228 .unwrap_or_else(|| "Unknown".to_string())
229 .into(),
230 encounter_id: encounter.ID,
231 encounter_name: encounter
232 .Name_lang
233 .clone()
234 .unwrap_or_else(|| "Unknown".to_string())
235 .into(),
236 difficulty_ids: decode_difficulty_mask(encounter_item.DifficultyMask),
237 })
238 })
239 .collect()
240 })
241 .unwrap_or_default();
242
243 Ok(ItemDataFlat {
244 id: item_id,
245 name: sparse
246 .and_then(|s| s.Display_lang.clone())
247 .unwrap_or_default()
248 .into(),
249 description: sparse
250 .and_then(|s| s.Description_lang.clone())
251 .unwrap_or_default(),
252 file_name: file_name.into(),
253 item_level: sparse.map_or(0, |s| s.ItemLevel),
254 quality: sparse.map_or(0, |s| s.OverallQualityID),
255 required_level: sparse.map_or(0, |s| s.RequiredLevel),
256 binding: sparse.map_or(0, |s| s.Bonding),
257 buy_price: sparse.map_or(0, |s| s.BuyPrice),
258 sell_price: sparse.map_or(0, |s| s.SellPrice),
259 max_count: sparse.map_or(0, |s| s.MaxCount),
260 stackable: sparse.map_or(1, |s| s.Stackable),
261 speed: sparse.map_or(0, |s| s.ItemDelay),
262 class_id: item.ClassID,
263 subclass_id: item.SubclassID,
264 inventory_type: item.InventoryType,
265 classification,
266 stats,
267 effects,
268 sockets,
269 socket_bonus_enchant_id: sparse.map_or(0, |s| s.SocketMatchEnchantmentID),
270 flags,
271 allowable_class: sparse.map_or(-1, |s| s.AllowableClass),
272 allowable_race: sparse.map_or(-1, |s| {
273 combine_allowable_race(s.AllowableRaces_0, s.AllowableRaces_1)
274 }),
275 expansion_id,
276 item_set_id,
277 set_info,
278 drop_sources,
279 dmg_variance: sparse.map_or(0.0, |s| s.DmgVariance),
280 gem_properties: sparse.map_or(0, |s| s.GemProperties),
281 modified_crafting_reagent_item_id: item.ModifiedCraftingReagentItemID,
282 })
283}
284
285#[must_use]
287pub fn transform_all_items(dbc: &DbcData) -> Vec<ItemDataFlat> {
288 super::transform_all(dbc.item.keys().copied(), "item", |item_id| {
289 transform_item(dbc, item_id)
290 })
291}
292
293#[cfg(test)]
294mod tests {
295 use googletest::prelude::*;
296 use rstest::rstest;
297
298 use super::*;
299
300 #[gtest]
301 #[rstest]
302 #[case::both_none(None, None, -1)]
303 #[case::low_zero_high_zero(Some(0), Some(0), 0)]
304 #[case::low_only(Some(5), Some(0), 5)]
305 #[case::high_only(Some(0), Some(1), 4_294_967_296)]
306 #[case::low_negative_masked(Some(-1), Some(0), 4_294_967_295)]
307 #[case::none_low_some_high(None, Some(0), 4_294_967_295)]
308 fn combine_allowable_race_cases(
309 #[case] low: Option<i64>,
310 #[case] high: Option<i64>,
311 #[case] expected: i64,
312 ) -> Result<()> {
313 verify_that!(combine_allowable_race(low, high), eq(expected))
314 }
315
316 #[gtest]
317 #[rstest]
318 #[case::zero(0, vec![])]
319 #[case::minus_one(-1, vec![])]
320 #[case::bit0(0b1, vec![1])]
321 #[case::bit0_and_2(0b101, vec![1, 3])]
322 #[case::high_bit(1 << 30, vec![31])]
323 #[case::mixed(0b1010, vec![2, 4])]
324 fn decode_difficulty_mask_cases(#[case] mask: i32, #[case] expected: Vec<i32>) -> Result<()> {
325 verify_that!(decode_difficulty_mask(mask), eq(&expected))
326 }
327
328 #[gtest]
329 #[rstest]
330 #[case::classic(0, "Classic")]
331 #[case::tww(10, "The War Within")]
332 #[case::unknown(99, "Unknown")]
333 #[case::negative(-1, "Unknown")]
334 fn get_expansion_name_cases(#[case] id: i32, #[case] expected: &str) -> Result<()> {
335 verify_that!(get_expansion_name(id), eq(expected))
336 }
337
338 #[gtest]
339 #[rstest]
340 #[case::head(1, "Head")]
341 #[case::two_hand(17, "Two-Hand")]
342 #[case::relic(28, "Relic")]
343 #[case::gap_27(27, "Unknown")]
344 #[case::unknown(99, "Unknown")]
345 fn get_inventory_type_name_cases(#[case] id: i32, #[case] expected: &str) -> Result<()> {
346 verify_that!(get_inventory_type_name(id), eq(expected))
347 }
348}