Skip to main content

wowlab_parsers/parsers/transform/loot/
mythic_plus.rs

1use 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
13// A complete season has one half-tier and four full crest tiers.
14const 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
100/// Build the single-row `MythicPlusSeasonFlat` list for the current M+ season.
101// #t(fn: rust_max_fn_lines) season aggregation pulls together M+ rewards, display season, scaling, currencies, dungeons, affixes, and floors in one pass
102pub 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}