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