1use wowlab_engine_domain::dbc::{
2 DungeonDifficultyFlags, GemSubclass, InventoryType, ItemBonusListGroupEntryFlags,
3 ItemBonusType, ItemClass, ItemContext, RaidDifficultyFlags,
4};
5use wowlab_types::{
6 constants::CURRENT_EXPANSION_ID,
7 data::{DifficultyKey, DropSourceKind, InstanceKind, ItemDropScalingFlat},
8 sim::{FastMap, FastSet, IntMap},
9};
10
11use super::journal::transform_all_journal_instances;
12use crate::parsers::dbc::DbcData;
13
14const VAULT_BR_MIN_MPL: i32 = 10;
15const MYTHIC_PLUS_SHARED_BASE_SEQ: i32 = 2;
16const RARE_RAID_GROUP_MOD_SET_ID: i32 = 2967;
17const DEFAULT_BOSS_TIER: i32 = 1;
18const INACTIVE_WORLD_STATE_EXPRESSIONS: &[i32] = &[50187];
19
20#[rustfmt::skip]
21const MYTHIC_PLUS_BUCKETS: &[(DifficultyKey, i32)] = &[
22 (DifficultyKey::MythicPlusTwoThree , 2) ,
24 (DifficultyKey::MythicPlusFour , 4) ,
25 (DifficultyKey::MythicPlusFive , 5) ,
26 (DifficultyKey::MythicPlusSixSeven , 6) ,
27 (DifficultyKey::MythicPlusEightNine, 8) ,
28 (DifficultyKey::MythicPlusTenPlus , 10),
29];
30
31fn list_has_type(dbc: &DbcData, list_id: i32, bonus_type: ItemBonusType) -> bool {
32 dbc.item_bonus
33 .get(&list_id)
34 .is_some_and(|rows| rows.iter().any(|r| r.Type == bonus_type as i32))
35}
36
37fn max_regular_seq_for_group(dbc: &DbcData, group: i32) -> i32 {
38 let mut max = 1;
39
40 if let Some(entries) = dbc.item_bonus_list_group_entry.get(&group) {
41 for entry in entries {
42 if entry.Flags != ItemBonusListGroupEntryFlags::REGULAR.bits() {
43 continue;
44 }
45
46 if !list_has_type(dbc, entry.ItemBonusListID, ItemBonusType::Upgrade) {
47 continue;
48 }
49
50 if entry.SequenceValue > max {
51 max = entry.SequenceValue;
52 }
53 }
54 }
55
56 max
57}
58
59fn walk_tree_nodes(
60 dbc: &DbcData,
61 tree_id: i32,
62) -> Vec<&crate::parsers::dbc::rows::ItemBonusTreeNodeRow> {
63 let mut out: Vec<&crate::parsers::dbc::rows::ItemBonusTreeNodeRow> = Vec::new();
64 let mut seen = FastSet::default();
65
66 seen.insert(tree_id);
67 let mut stack: Vec<&crate::parsers::dbc::rows::ItemBonusTreeNodeRow> = Vec::new();
68
69 if let Some(nodes) = dbc.item_bonus_tree_node.get(&tree_id) {
70 for node in nodes.iter().rev() {
71 stack.push(node);
72 }
73 }
74
75 while let Some(node) = stack.pop() {
76 out.push(node);
77 let child = node.ChildItemBonusTreeID;
78
79 if child > 0
80 && seen.insert(child)
81 && let Some(child_nodes) = dbc.item_bonus_tree_node.get(&child)
82 {
83 for child_node in child_nodes.iter().rev() {
84 stack.push(child_node);
85 }
86 }
87 }
88
89 out
90}
91
92fn node_difficulties_and_seqs(
94 node: &crate::parsers::dbc::rows::ItemBonusTreeNodeRow,
95 source_kind: DropSourceKind,
96 boss_tier: i32,
97 rare_seq: i32,
98) -> Vec<(DifficultyKey, i32)> {
99 let Ok(ctx) = ItemContext::try_from(node.ItemContext) else {
100 return Vec::new();
101 };
102
103 if source_kind == DropSourceKind::Raid {
104 let diff = match ctx {
105 ItemContext::RaidLfr => DifficultyKey::Lfr,
106 ItemContext::RaidNormal => DifficultyKey::Normal,
107 ItemContext::RaidHeroic => DifficultyKey::Heroic,
108 ItemContext::RaidMythic => DifficultyKey::Mythic,
109 _ => return Vec::new(),
110 };
111 let seq = if node.IblGroupPointsModSetID == RARE_RAID_GROUP_MOD_SET_ID {
112 rare_seq
113 } else {
114 boss_tier
115 };
116
117 return vec![(diff, seq)];
118 }
119
120 match ctx {
121 ItemContext::DungeonNormal => vec![(DifficultyKey::Normal, 1)],
122 ItemContext::DungeonHeroic => vec![(DifficultyKey::Heroic, 1)],
123 ItemContext::DungeonMythic => vec![(DifficultyKey::Mythic, 1)],
124 ItemContext::MythicKeystone => {
125 let min_mpl = node.MinMythicPlusLevel;
126 let hi = if node.MaxMythicPlusLevel == 0 {
127 i32::MAX
128 } else {
129 node.MaxMythicPlusLevel
130 };
131 let base_seq = if min_mpl == 0 {
132 MYTHIC_PLUS_SHARED_BASE_SEQ
133 } else {
134 1
135 };
136 let mut out = Vec::new();
137 let mut filtered_index = 0;
138
139 for &(diff, key) in MYTHIC_PLUS_BUCKETS {
140 if key >= min_mpl && key <= hi {
141 out.push((diff, base_seq + filtered_index));
142 filtered_index += 1;
143 }
144 }
145
146 out
147 }
148 ItemContext::MythicKeystoneVault => {
149 let min_mpl = node.MinMythicPlusLevel;
150
151 if min_mpl >= VAULT_BR_MIN_MPL || (min_mpl == 0 && node.MaxMythicPlusLevel == 0) {
152 vec![(DifficultyKey::MythicPlusVaultBr, 1)]
153 } else {
154 Vec::new()
155 }
156 }
157 _ => Vec::new(),
158 }
159}
160
161fn resolve_bonuses(
164 dbc: &DbcData,
165 override_dests: &IntMap<i32, Vec<i32>>,
166 item_id: i32,
167 source_kind: DropSourceKind,
168 boss_tier: i32,
169) -> FastMap<DifficultyKey, (i32, i32)> {
170 let Some(roots) = dbc.item_x_bonus_tree.get(&item_id) else {
171 return FastMap::default();
172 };
173
174 if roots.is_empty() {
175 return FastMap::default();
176 }
177
178 let normal_trees: Vec<i32> = roots.iter().map(|r| r.ItemBonusTreeID).collect();
179
180 let override_count = normal_trees
181 .iter()
182 .filter_map(|tree| override_dests.get(tree))
183 .map(Vec::len)
184 .sum::<usize>();
185 let mut all_trees = Vec::with_capacity(normal_trees.len() + override_count);
186
187 for tree in &normal_trees {
188 if let Some(dests) = override_dests.get(tree) {
189 for &dest in dests {
190 if !all_trees.contains(&dest) {
191 all_trees.push(dest);
192 }
193 }
194 }
195 }
196
197 for tree in &normal_trees {
198 if !all_trees.contains(tree) {
199 all_trees.push(*tree);
200 }
201 }
202
203 let mut upgrade_by_diff = FastMap::default();
204 let mut flair_by_diff = FastMap::default();
205
206 for tree in all_trees {
207 for node in walk_tree_nodes(dbc, tree) {
208 let direct = node.ChildItemBonusListID;
209 let group = node.ChildItemBonusListGroupID;
210 let rare_seq = if group > 0 {
211 max_regular_seq_for_group(dbc, group)
212 } else {
213 1
214 };
215
216 for (diff, seq) in node_difficulties_and_seqs(node, source_kind, boss_tier, rare_seq) {
217 let mut upgrade_candidate: Option<i32> = None;
218 let mut flair_candidate: Option<i32> = None;
219
220 if direct > 0 {
221 if list_has_type(dbc, direct, ItemBonusType::Upgrade) {
222 upgrade_candidate = Some(direct);
223 }
224
225 if list_has_type(dbc, direct, ItemBonusType::Flair) {
226 flair_candidate = Some(direct);
227 }
228 } else if group > 0 {
229 if let Some(entries) = dbc.item_bonus_list_group_entry.get(&group)
230 && let Some(entry) = entries.iter().find(|e| {
231 e.SequenceValue == seq
232 && list_has_type(dbc, e.ItemBonusListID, ItemBonusType::Upgrade)
233 })
234 {
235 upgrade_candidate = Some(entry.ItemBonusListID);
236 }
237 } else {
238 continue;
239 }
240
241 if let Some(upgrade) = upgrade_candidate {
242 upgrade_by_diff.entry(diff).or_insert(upgrade);
243 }
244
245 let current_upgrade = upgrade_by_diff.get(&diff).copied();
246
247 if let Some(flair) = flair_candidate
248 && Some(flair) != current_upgrade
249 && !flair_by_diff.contains_key(&diff)
250 {
251 flair_by_diff.insert(diff, flair);
252 }
253 }
254 }
255 }
256
257 let mut result = FastMap::default();
258
259 result.reserve(upgrade_by_diff.len() + flair_by_diff.len());
260
261 for (&diff, &upgrade) in &upgrade_by_diff {
262 let flair = flair_by_diff.get(&diff).copied().unwrap_or(0);
263
264 result.insert(diff, (upgrade, flair));
265 }
266
267 for (&diff, &flair) in &flair_by_diff {
268 result.entry(diff).or_insert((0, flair));
269 }
270
271 result
272}
273
274fn build_override_dests(dbc: &DbcData) -> IntMap<i32, Vec<i32>> {
275 let mut map: IntMap<i32, Vec<i32>> = IntMap::default();
276
277 for row in dbc.challenge_mode_item_bonus_override.values() {
278 let src = row.SrcItemBonusTreeID;
279 let dst = row.DstItemBonusTreeID;
280
281 if src == 0 || dst == 0 {
282 continue;
283 }
284
285 let dests = map.entry(src).or_default();
286
287 if !dests.contains(&dst) {
288 dests.push(dst);
289 }
290 }
291
292 map
293}
294
295fn build_excluded_item_set(dbc: &DbcData) -> FastSet<i32> {
296 let mut excluded = FastSet::default();
297
298 for item in dbc.item.values() {
299 if item.ClassID == ItemClass::Gem as i32
300 && item.SubclassID == GemSubclass::ArtifactRelic as i32
301 {
302 excluded.insert(item.ID);
303 continue;
304 }
305
306 if item.InventoryType == InventoryType::NonEquippable as i32
307 && item.ClassID != ItemClass::Weapon as i32
308 && item.ClassID != ItemClass::Armor as i32
309 && item.ClassID != ItemClass::Miscellaneous as i32
310 {
311 excluded.insert(item.ID);
312 }
313 }
314
315 excluded
316}
317
318fn build_boss_tier_map(dbc: &DbcData) -> IntMap<i32, i32> {
319 let mut map: IntMap<i32, i32> = IntMap::default();
320
321 for row in dbc.dungeon_encounter.values() {
322 if row.boss_tier > 0 {
323 map.insert(row.ID, row.boss_tier);
324 }
325 }
326
327 map
328}
329
330fn expand_raid_mask(mask: i32) -> Vec<DifficultyKey> {
331 let flags = RaidDifficultyFlags::from_bits_retain(mask);
332 let mut out = Vec::new();
333
334 if flags.contains(RaidDifficultyFlags::LFR) {
335 out.push(DifficultyKey::Lfr);
336 }
337
338 if flags.contains(RaidDifficultyFlags::NORMAL) {
339 out.push(DifficultyKey::Normal);
340 }
341
342 if flags.contains(RaidDifficultyFlags::HEROIC) {
343 out.push(DifficultyKey::Heroic);
344 }
345
346 if flags.contains(RaidDifficultyFlags::MYTHIC) {
347 out.push(DifficultyKey::Mythic);
348 }
349
350 out
351}
352
353fn expand_dungeon_mask(mask: i32) -> Vec<DifficultyKey> {
355 let flags = DungeonDifficultyFlags::from_bits_retain(mask);
356 let mut out = Vec::new();
357
358 if flags.contains(DungeonDifficultyFlags::NORMAL) {
359 out.push(DifficultyKey::Normal);
360 }
361
362 if flags.contains(DungeonDifficultyFlags::HEROIC) {
363 out.push(DifficultyKey::Heroic);
364 }
365
366 if flags.contains(DungeonDifficultyFlags::MYTHIC) {
367 out.extend_from_slice(&[
368 DifficultyKey::Mythic,
369 DifficultyKey::MythicPlusTwoThree,
370 DifficultyKey::MythicPlusFour,
371 DifficultyKey::MythicPlusFive,
372 DifficultyKey::MythicPlusSixSeven,
373 DifficultyKey::MythicPlusEightNine,
374 DifficultyKey::MythicPlusTenPlus,
375 DifficultyKey::MythicPlusVaultBr,
376 ]);
377 }
378
379 out
380}
381
382#[cfg(test)]
383#[expect(
384 clippy::items_after_test_module,
385 reason = "private helper tests remain adjacent to the resolver helpers they exercise"
386)]
387mod tests {
388 use googletest::prelude::*;
389 use rstest::rstest;
390
391 use super::*;
392 use crate::parsers::{
393 dbc::rows::ItemBonusTreeNodeRow,
394 transform::fixtures::{challenge_override_row, dbc, item_bonus_row, item_row},
395 };
396
397 type NodeDifficultyCase = (
398 i32,
399 i32,
400 i32,
401 i32,
402 DropSourceKind,
403 i32,
404 i32,
405 Vec<(DifficultyKey, i32)>,
406 );
407
408 fn node(ctx: i32, mod_set: i32, min: i32, max: i32) -> ItemBonusTreeNodeRow {
409 ItemBonusTreeNodeRow {
410 ID: 1,
411 ItemContext: ctx,
412 ChildItemBonusTreeID: 0,
413 ChildItemBonusListID: 0,
414 ChildItemBonusListGroupID: 0,
415 IblGroupPointsModSetID: mod_set,
416 MinMythicPlusLevel: min,
417 MaxMythicPlusLevel: max,
418 ParentItemBonusTreeID: 0,
419 }
420 }
421
422 #[gtest]
423 #[rstest]
424 #[case::all_bits(-1, vec![
425 DifficultyKey::Lfr,
426 DifficultyKey::Normal,
427 DifficultyKey::Heroic,
428 DifficultyKey::Mythic,
429 ])]
430 #[case::none(0, vec![])]
431 #[case::lfr_only(1, vec![DifficultyKey::Lfr])]
432 #[case::heroic_mythic(0b1100, vec![DifficultyKey::Heroic, DifficultyKey::Mythic])]
433 fn expand_raid_mask_cases(
434 #[case] mask: i32,
435 #[case] expected: Vec<DifficultyKey>,
436 ) -> Result<()> {
437 verify_that!(expand_raid_mask(mask), eq(&expected))
438 }
439
440 #[gtest]
441 #[rstest]
442 #[case::normal(1, vec![DifficultyKey::Normal])]
443 #[case::heroic(2, vec![DifficultyKey::Heroic])]
444 #[case::mythic_expands(4, vec![
445 DifficultyKey::Mythic,
446 DifficultyKey::MythicPlusTwoThree,
447 DifficultyKey::MythicPlusFour,
448 DifficultyKey::MythicPlusFive,
449 DifficultyKey::MythicPlusSixSeven,
450 DifficultyKey::MythicPlusEightNine,
451 DifficultyKey::MythicPlusTenPlus,
452 DifficultyKey::MythicPlusVaultBr,
453 ])]
454 #[case::all_bits(-1, vec![
455 DifficultyKey::Normal,
456 DifficultyKey::Heroic,
457 DifficultyKey::Mythic,
458 DifficultyKey::MythicPlusTwoThree,
459 DifficultyKey::MythicPlusFour,
460 DifficultyKey::MythicPlusFive,
461 DifficultyKey::MythicPlusSixSeven,
462 DifficultyKey::MythicPlusEightNine,
463 DifficultyKey::MythicPlusTenPlus,
464 DifficultyKey::MythicPlusVaultBr,
465 ])]
466 fn expand_dungeon_mask_cases(
467 #[case] mask: i32,
468 #[case] expected: Vec<DifficultyKey>,
469 ) -> Result<()> {
470 verify_that!(expand_dungeon_mask(mask), eq(&expected))
471 }
472
473 #[gtest]
474 #[rstest]
475 #[case::raid_normal_boss_tier((3, 0, 0, 0, DropSourceKind::Raid, 2, 9, vec![(DifficultyKey::Normal, 2)]))]
476 #[case::raid_rare_uses_rare_seq((6, 2967, 0, 0, DropSourceKind::Raid, 2, 9, vec![(DifficultyKey::Mythic, 9)]))]
477 #[case::raid_unknown_ctx((99, 0, 0, 0, DropSourceKind::Raid, 2, 9, vec![]))]
478 #[case::dungeon_heroic((2, 0, 0, 0, DropSourceKind::Dungeon, 1, 1, vec![(DifficultyKey::Heroic, 1)]))]
479 #[case::keystone_full_range((16, 0, 0, 0, DropSourceKind::Dungeon, 1, 1, vec![
480 (DifficultyKey::MythicPlusTwoThree, 2),
481 (DifficultyKey::MythicPlusFour, 3),
482 (DifficultyKey::MythicPlusFive, 4),
483 (DifficultyKey::MythicPlusSixSeven, 5),
484 (DifficultyKey::MythicPlusEightNine, 6),
485 (DifficultyKey::MythicPlusTenPlus, 7),
486 ]))]
487 #[case::keystone_min_five((16, 0, 5, 0, DropSourceKind::Dungeon, 1, 1, vec![
488 (DifficultyKey::MythicPlusFive, 1),
489 (DifficultyKey::MythicPlusSixSeven, 2),
490 (DifficultyKey::MythicPlusEightNine, 3),
491 (DifficultyKey::MythicPlusTenPlus, 4),
492 ]))]
493 #[case::keystone_capped((16, 0, 0, 5, DropSourceKind::Dungeon, 1, 1, vec![
494 (DifficultyKey::MythicPlusTwoThree, 2),
495 (DifficultyKey::MythicPlusFour, 3),
496 (DifficultyKey::MythicPlusFive, 4),
497 ]))]
498 #[case::vault_high((35, 0, 10, 0, DropSourceKind::Dungeon, 1, 1, vec![(DifficultyKey::MythicPlusVaultBr, 1)]))]
499 #[case::vault_zero_zero((35, 0, 0, 0, DropSourceKind::Dungeon, 1, 1, vec![(DifficultyKey::MythicPlusVaultBr, 1)]))]
500 #[case::vault_low((35, 0, 4, 8, DropSourceKind::Dungeon, 1, 1, vec![]))]
501 #[case::unknown_ctx((7, 0, 0, 0, DropSourceKind::Dungeon, 1, 1, vec![]))]
502 fn node_difficulties_and_seqs_cases(#[case] case: NodeDifficultyCase) -> Result<()> {
503 let (ctx, mod_set, min, max, source, boss_tier, rare_seq, expected) = case;
504 let node = node(ctx, mod_set, min, max);
505
506 verify_that!(
507 node_difficulties_and_seqs(&node, source, boss_tier, rare_seq),
508 eq(&expected)
509 )
510 }
511
512 #[gtest]
513 fn list_has_type_matches_only_present_type() -> Result<()> {
514 let mut dbc = dbc();
515
516 dbc.item_bonus
517 .insert(5, vec![item_bonus_row(5, ItemBonusType::Upgrade as i32)]);
518
519 verify_that!(list_has_type(&dbc, 5, ItemBonusType::Upgrade), eq(true))?;
520 verify_that!(list_has_type(&dbc, 5, ItemBonusType::Flair), eq(false))?;
521
522 verify_that!(list_has_type(&dbc, 99, ItemBonusType::Upgrade), eq(false))
523 }
524
525 #[gtest]
526 fn build_override_dests_skips_and_dedups() -> Result<()> {
527 let mut dbc = dbc();
528
529 dbc.challenge_mode_item_bonus_override
530 .insert(1, challenge_override_row(1, 5, 7));
531 dbc.challenge_mode_item_bonus_override
532 .insert(2, challenge_override_row(2, 0, 9));
533 dbc.challenge_mode_item_bonus_override
534 .insert(3, challenge_override_row(3, 5, 7));
535
536 let dests = build_override_dests(&dbc);
537
538 verify_that!(dests.get(&5), some(eq(&vec![7])))?;
539
540 verify_that!(dests.get(&0), none())
541 }
542
543 #[gtest]
544 fn build_excluded_item_set_classifies_items() -> Result<()> {
545 let mut dbc = dbc();
546
547 dbc.item.insert(
548 1,
549 item_row(
550 1,
551 ItemClass::Gem as i32,
552 GemSubclass::ArtifactRelic as i32,
553 InventoryType::NonEquippable as i32,
554 ),
555 );
556 dbc.item.insert(
557 2,
558 item_row(
559 2,
560 ItemClass::Weapon as i32,
561 0,
562 InventoryType::NonEquippable as i32,
563 ),
564 );
565 dbc.item.insert(
566 3,
567 item_row(
568 3,
569 ItemClass::TradeGoods as i32,
570 0,
571 InventoryType::NonEquippable as i32,
572 ),
573 );
574 dbc.item.insert(
575 4,
576 item_row(4, ItemClass::Weapon as i32, 0, InventoryType::Head as i32),
577 );
578
579 let excluded = build_excluded_item_set(&dbc);
580
581 verify_that!(excluded.contains(&1), eq(true))?;
582 verify_that!(excluded.contains(&2), eq(false))?;
583 verify_that!(excluded.contains(&3), eq(true))?;
584
585 verify_that!(excluded.contains(&4), eq(false))
586 }
587}
588
589#[must_use]
593pub fn transform_all_item_drop_scaling(dbc: &DbcData) -> Vec<ItemDropScalingFlat> {
594 type DiffScalingMap = FastMap<DifficultyKey, (i32, i32)>;
595
596 let override_dests = build_override_dests(dbc);
597 let excluded = build_excluded_item_set(dbc);
598 let boss_tier_map = build_boss_tier_map(dbc);
599
600 let mut items_by_encounter: IntMap<
601 i32,
602 Vec<&crate::parsers::dbc::rows::JournalEncounterItemRow>,
603 > = IntMap::default();
604
605 for items in dbc.journal_encounter_item.values() {
606 for item in items {
607 items_by_encounter
608 .entry(item.JournalEncounterID)
609 .or_default()
610 .push(item);
611 }
612 }
613
614 let instances = transform_all_journal_instances(dbc);
615
616 let mut by_item: FastMap<(i32, DropSourceKind), DiffScalingMap> = FastMap::default();
617
618 for instance in &instances {
619 let source_kind = match instance.kind {
620 InstanceKind::Raid => DropSourceKind::Raid,
621 InstanceKind::Dungeon => DropSourceKind::Dungeon,
622 _ => continue,
623 };
624 let expansion = dbc.map.get(&instance.map_id).map_or(0, |m| m.ExpansionID);
625 let strip_legacy = source_kind == DropSourceKind::Dungeon
626 && i64::from(expansion) < i64::from(CURRENT_EXPANSION_ID);
627
628 for encounter in &instance.encounters {
629 let boss_tier = boss_tier_map
630 .get(&encounter.dungeon_encounter_id)
631 .copied()
632 .unwrap_or(DEFAULT_BOSS_TIER);
633 let Some(items) = items_by_encounter.get(&encounter.id) else {
634 continue;
635 };
636
637 for item in items {
638 if INACTIVE_WORLD_STATE_EXPRESSIONS.contains(&item.WorldStateExpressionID) {
639 continue;
640 }
641
642 let item_id = item.ItemID;
643
644 if item_id <= 0 || excluded.contains(&item_id) {
645 continue;
646 }
647
648 let mut expected = match source_kind {
649 DropSourceKind::Raid => expand_raid_mask(item.DifficultyMask),
650 DropSourceKind::Dungeon => expand_dungeon_mask(item.DifficultyMask),
651 _ => continue,
652 };
653
654 if strip_legacy {
655 expected.retain(|diff| {
656 *diff != DifficultyKey::Normal && *diff != DifficultyKey::Heroic
657 });
658 }
659
660 if expected.is_empty() {
661 continue;
662 }
663
664 let resolved =
665 resolve_bonuses(dbc, &override_dests, item_id, source_kind, boss_tier);
666 let entry = by_item.entry((item_id, source_kind)).or_default();
667
668 for diff in expected {
669 let mut pair = if source_kind == DropSourceKind::Dungeon
670 && diff == DifficultyKey::Normal
671 {
672 (0, 0)
673 } else {
674 resolved.get(&diff).copied().unwrap_or((0, 0))
675 };
676
677 if pair.0 == 0 {
678 pair.1 = 0;
679 }
680
681 match entry.get(&diff) {
682 None => {
683 entry.insert(diff, pair);
684 }
685 Some(&(cur_upgrade, cur_flair)) => {
686 let cur_empty = cur_upgrade == 0 && cur_flair == 0;
687 let new_content = pair.0 != 0 || pair.1 != 0;
688
689 if cur_empty && new_content {
690 entry.insert(diff, pair);
691 }
692 }
693 }
694 }
695 }
696 }
697 }
698
699 let mut entries: Vec<((i32, DropSourceKind), DiffScalingMap)> = by_item.into_iter().collect();
700
701 entries.sort_by(|a, b| {
702 a.0.0
703 .cmp(&b.0.0)
704 .then_with(|| a.0.1.as_str().cmp(b.0.1.as_str()))
705 });
706
707 let mut out: Vec<ItemDropScalingFlat> = Vec::new();
708
709 for ((item_id, source_kind), diffs) in entries {
710 if !diffs.values().any(|(upgrade, _)| *upgrade != 0) {
711 continue;
712 }
713
714 let mut diff_list: Vec<(DifficultyKey, (i32, i32))> = diffs.into_iter().collect();
715
716 diff_list.sort_by_key(|(diff, _)| diff.order());
717
718 for (difficulty_key, (upgrade_id, flair_id)) in diff_list {
719 out.push(ItemDropScalingFlat {
720 id: 0,
721 item_id,
722 source_kind,
723 difficulty_key,
724 upgrade_id,
725 flair_id,
726 });
727 }
728 }
729
730 out
731}