wowlab_parsers/parsers/transform/loot/
mythic_plus.rs1use wowlab_types::{
2 data::{
3 KeyFloorEntry, KeyLevelRewardEntry, MythicPlusSeasonFlat, TrackedAffixEntry,
4 TrackedDungeonEntry,
5 },
6 sim::FastSet,
7};
8
9use crate::parsers::dbc::DbcData;
10
11const VALORSTONES_CURRENCY_ID: i32 = 3008;
12
13const EXPECTED_CREST_CURRENCY_COUNT: usize = 5;
15
16fn current_mythic_plus_season_id(dbc: &DbcData) -> Option<i32> {
17 dbc.mythic_plus_season_reward_levels
18 .values()
19 .flatten()
20 .map(|row| row.MythicPlusSeasonID)
21 .max()
22}
23
24fn pick_display_season_id(dbc: &DbcData, season_id: i32, expansion_level: i32) -> Option<i32> {
25 let mut display_max_id: Option<i32> = None;
26 let mut display_max_count: u32 = 0;
27
28 for row in dbc.display_season.values() {
29 if row.ExpansionID != expansion_level {
30 continue;
31 }
32
33 match display_max_id {
34 Some(curr) if curr == row.ID => {
35 display_max_count += 1;
36 }
37 Some(curr) if row.ID > curr => {
38 display_max_id = Some(row.ID);
39 display_max_count = 1;
40 }
41 Some(_) => {}
42 None => {
43 display_max_id = Some(row.ID);
44 display_max_count = 1;
45 }
46 }
47 }
48
49 let Some(display_id) = display_max_id else {
50 tracing::warn!(
51 season_id,
52 expansion_level,
53 "no DisplaySeason rows match ExpansionLevel; skipping mythic_plus_seasons emission"
54 );
55
56 return None;
57 };
58
59 if display_max_count > 1 {
60 tracing::warn!(
61 season_id,
62 display_season_id = display_id,
63 "multiple DisplaySeason rows tie at the same max ID; data corruption, skipping mythic_plus_seasons emission"
64 );
65
66 return None;
67 }
68
69 Some(display_id)
70}
71
72fn collect_crest_currencies(dbc: &DbcData, scaling_id: i32) -> (Vec<i32>, bool) {
73 let mut crest_set = FastSet::default();
74 let mut has_valorstones = false;
75
76 if let Some(entries) = dbc.item_group_ilvl_scaling_entry.get(&scaling_id) {
77 for entry in entries {
78 if entry.CurrencyTypeID == VALORSTONES_CURRENCY_ID {
79 has_valorstones = true;
80 continue;
81 }
82
83 if entry.CurrencyTypeID != 0 {
84 crest_set.insert(entry.CurrencyTypeID);
85 }
86 }
87 }
88
89 let mut crest_currencies: Vec<i32> = crest_set.into_iter().collect();
90
91 crest_currencies.sort_by_key(|id| {
92 dbc.currency_types
93 .get(id)
94 .map_or(i32::MAX, |c| c.OrderIndex)
95 });
96
97 (crest_currencies, has_valorstones)
98}
99
100pub fn transform_all_mythic_plus_seasons(dbc: &DbcData) -> Vec<MythicPlusSeasonFlat> {
103 let Some(season_id) = current_mythic_plus_season_id(dbc) else {
104 return Vec::new();
105 };
106 let mut rewards: Vec<&crate::parsers::dbc::rows::MythicPlusSeasonRewardLevelsRow> = dbc
107 .mythic_plus_season_reward_levels
108 .get(&season_id)
109 .map(|v| v.iter().collect())
110 .unwrap_or_default();
111
112 rewards.sort_by_key(|r| r.DifficultyLevel);
113
114 let key_rewards: Vec<KeyLevelRewardEntry> = rewards
115 .into_iter()
116 .map(|r| KeyLevelRewardEntry {
117 key_level: r.DifficultyLevel,
118 activity_tier_id: r.ActivityTierID,
119 weekly_reward_level: r.WeeklyRewardLevel,
120 end_of_run_reward_level: r.EndOfRunRewardLevel,
121 })
122 .collect();
123
124 let Some(header) = dbc.mythic_plus_season.get(&season_id) else {
125 tracing::warn!(
126 season_id,
127 "MythicPlusSeason header row missing; skipping mythic_plus_seasons emission"
128 );
129
130 return Vec::new();
131 };
132
133 let Some(display_season_id) = pick_display_season_id(dbc, season_id, header.ExpansionLevel)
134 else {
135 return Vec::new();
136 };
137 let Some(display_season) = dbc.display_season.get(&display_season_id) else {
138 tracing::warn!(
139 season_id,
140 display_season_id,
141 "DisplaySeason row vanished between scan and lookup; skipping mythic_plus_seasons emission"
142 );
143
144 return Vec::new();
145 };
146 let display_season_name = display_season.display_name_lang.clone().unwrap_or_default();
147 let display_season_index = display_season.Season;
148 let delves_season_id = display_season.DelvesSeasonID;
149
150 let Some(item_group_ilvl_scaling_id) = dbc
151 .item_bonus_list_group
152 .values()
153 .filter(|row| row.SequenceSpellID != 0)
154 .map(|row| row.ItemGroupIlvlScalingID)
155 .max()
156 else {
157 tracing::warn!(
158 season_id,
159 "no ItemBonusListGroup rows with SequenceSpellID != 0; skipping mythic_plus_seasons emission"
160 );
161
162 return Vec::new();
163 };
164
165 let mut bonus_list_groups: Vec<i32> = dbc
166 .item_bonus_list_group
167 .values()
168 .filter(|row| row.ItemGroupIlvlScalingID == item_group_ilvl_scaling_id)
169 .map(|row| row.ID)
170 .collect();
171
172 bonus_list_groups.sort_unstable();
173
174 let (crest_currencies, has_valorstones) =
175 collect_crest_currencies(dbc, item_group_ilvl_scaling_id);
176
177 if crest_currencies.len() != EXPECTED_CREST_CURRENCY_COUNT {
178 tracing::warn!(
179 season_id,
180 item_group_ilvl_scaling_id,
181 count = crest_currencies.len(),
182 expected = EXPECTED_CREST_CURRENCY_COUNT,
183 "unexpected number of crest currencies; skipping mythic_plus_seasons emission"
184 );
185
186 return Vec::new();
187 }
188
189 let valorstones_currency_id = if has_valorstones {
190 VALORSTONES_CURRENCY_ID
191 } else {
192 0
193 };
194
195 let tracked_dungeons: Vec<TrackedDungeonEntry> = dbc
196 .mythic_plus_season_tracked_map
197 .get(&display_season_id)
198 .map(|entries| {
199 entries
200 .iter()
201 .filter_map(|tracked| {
202 let challenge = dbc.map_challenge_mode.get(&tracked.MapChallengeModeID)?;
203 let map_id = challenge.MapID;
204 let instance = dbc
205 .journal_instance
206 .values()
207 .find(|inst| inst.MapID == map_id)?;
208
209 Some(TrackedDungeonEntry {
210 map_challenge_mode_id: tracked.MapChallengeModeID,
211 map_id,
212 instance_name: instance.Name_lang.clone().unwrap_or_default(),
213 })
214 })
215 .collect()
216 })
217 .unwrap_or_default();
218
219 let tracked_affixes: Vec<TrackedAffixEntry> = dbc
220 .mythic_plus_season_tracked_affix
221 .get(&display_season_id)
222 .map(|entries| {
223 entries
224 .iter()
225 .map(|row| TrackedAffixEntry {
226 keystone_affix_id: row.KeystoneAffixID,
227 bonus_rating: row.BonusRating,
228 })
229 .collect()
230 })
231 .unwrap_or_default();
232
233 let key_floors: Vec<KeyFloorEntry> = dbc
234 .mythic_plus_season_key_floor
235 .get(&display_season_id)
236 .map(|entries| {
237 entries
238 .iter()
239 .map(|row| KeyFloorEntry {
240 key_floor: row.KeyFloor,
241 player_condition_id: row.PlayerConditionID,
242 })
243 .collect()
244 })
245 .unwrap_or_default();
246
247 vec![MythicPlusSeasonFlat {
248 id: season_id,
249 milestone_season: header.MilestoneSeason,
250 start_time_event: header.StartTimeEvent,
251 expansion_level: header.ExpansionLevel,
252 heroic_lfg_dungeon_min_gear: header.HeroicLFGDungeonMinGear,
253 display_season_id,
254 display_season_name,
255 display_season_index,
256 delves_season_id,
257 item_group_ilvl_scaling_id,
258 bonus_list_groups,
259 crest_currencies,
260 valorstones_currency_id,
261 tracked_dungeons,
262 tracked_affixes,
263 key_floors,
264 key_rewards,
265 }]
266}