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