Skip to main content

wowlab_parsers/parsers/transform/loot/
journal.rs

1use wowlab_types::{
2    data::{EncounterEntry, InstanceKind, JournalInstanceFlat, JournalTierFlat, TierInstanceEntry},
3    sim::IntMap,
4};
5
6use super::shared::{
7    encounter_image_path, instance_kind, is_mythic_plus_map, journal_image_path,
8    loadscreen_image_path, loadscreen_seo_path, primary_encounter_creature,
9    resolve_loadscreen_file_data_id,
10};
11use crate::parsers::dbc::DbcData;
12
13const CURRENT_JOURNAL_TIER_ID: i32 = 505;
14
15/// Build the single-row `JournalTierFlat` list for the curated current-season tier.
16pub fn transform_all_journal_tiers(dbc: &DbcData) -> Vec<JournalTierFlat> {
17    let Some(tier) = dbc.journal_tier.get(&CURRENT_JOURNAL_TIER_ID) else {
18        tracing::warn!(
19            tier_id = CURRENT_JOURNAL_TIER_ID,
20            "JournalTier row for current season not found; skipping journal tier emission"
21        );
22
23        return Vec::new();
24    };
25
26    let mut links: Vec<&crate::parsers::dbc::rows::JournalTierXInstanceRow> = dbc
27        .journal_tier_x_instance
28        .values()
29        .flatten()
30        .filter(|link| link.JournalTierID == tier.ID)
31        .collect();
32
33    links.sort_by_key(|link| link.OrderIndex);
34
35    let instances: Vec<TierInstanceEntry> = links
36        .into_iter()
37        .filter_map(|link| {
38            let instance = dbc.journal_instance.get(&link.JournalInstanceID)?;
39            let map_id = instance.MapID;
40            let kind = instance_kind(dbc, map_id, link.OrderIndex, instance.Flags);
41
42            Some(TierInstanceEntry {
43                instance_id: instance.ID,
44                instance_name: instance.Name_lang.clone().unwrap_or_default(),
45                kind,
46                order_index: link.OrderIndex,
47                map_id,
48                is_mythic_plus: is_mythic_plus_map(dbc, map_id),
49            })
50        })
51        .collect();
52
53    vec![JournalTierFlat {
54        id: tier.ID,
55        name: tier.Name_lang.clone().unwrap_or_default(),
56        expansion: tier.Expansion,
57        player_condition_id: tier.PlayerConditionID,
58        instances,
59    }]
60}
61
62/// Build `JournalInstanceFlat` rows for every raid/dungeon journal instance.
63#[must_use]
64pub fn transform_all_journal_instances(dbc: &DbcData) -> Vec<JournalInstanceFlat> {
65    let mut encounters_by_instance: IntMap<
66        i32,
67        Vec<&crate::parsers::dbc::rows::JournalEncounterRow>,
68    > = IntMap::default();
69
70    for encounter in dbc.journal_encounter.values() {
71        encounters_by_instance
72            .entry(encounter.JournalInstanceID)
73            .or_default()
74            .push(encounter);
75    }
76
77    for list in encounters_by_instance.values_mut() {
78        list.sort_by_key(|e| e.OrderIndex);
79    }
80
81    let mut links_by_instance: IntMap<
82        i32,
83        Vec<&crate::parsers::dbc::rows::JournalTierXInstanceRow>,
84    > = IntMap::default();
85
86    for links in dbc.journal_tier_x_instance.values() {
87        for link in links {
88            links_by_instance
89                .entry(link.JournalInstanceID)
90                .or_default()
91                .push(link);
92        }
93    }
94
95    let mut instances: Vec<&crate::parsers::dbc::rows::JournalInstanceRow> = dbc
96        .journal_tier_x_instance
97        .keys()
98        .filter_map(|instance_id| dbc.journal_instance.get(instance_id))
99        .collect();
100
101    instances.sort_by_key(|instance| instance.ID);
102
103    instances
104        .into_iter()
105        .filter_map(|instance| {
106            let links = links_by_instance.get(&instance.ID)?;
107            let primary_link = primary_journal_tier_link(dbc, links)?;
108            let tier = dbc.journal_tier.get(&primary_link.JournalTierID)?;
109            let map_id = instance.MapID;
110            let kind = instance_kind(dbc, map_id, primary_link.OrderIndex, instance.Flags);
111
112            if !matches!(kind, InstanceKind::Raid | InstanceKind::Dungeon) {
113                return None;
114            }
115
116            let instance_name = instance.Name_lang.clone().unwrap_or_default();
117            let loadscreen_file_data_id = resolve_loadscreen_file_data_id(dbc, &instance_name);
118            let encounters: Vec<EncounterEntry> = encounters_by_instance
119                .get(&instance.ID)
120                .map(|list| {
121                    list.iter()
122                        .map(|e| EncounterEntry {
123                            creature_display_info_id: primary_encounter_creature(dbc, e.ID)
124                                .map(|creature| creature.CreatureDisplayInfoID)
125                                .unwrap_or_default(),
126                            id: e.ID,
127                            image_file_data_id: primary_encounter_creature(dbc, e.ID)
128                                .map(|creature| creature.FileDataID)
129                                .unwrap_or_default(),
130                            image_storage_path: encounter_image_path(
131                                primary_encounter_creature(dbc, e.ID)
132                                    .map(|creature| creature.FileDataID)
133                                    .unwrap_or_default(),
134                            ),
135                            name: e.Name_lang.clone().unwrap_or_default(),
136                            order_index: e.OrderIndex,
137                            dungeon_encounter_id: e.DungeonEncounterID,
138                            ui_map_id: e.UiMapID,
139                            ui_model_scene_id: primary_encounter_creature(dbc, e.ID)
140                                .map(|creature| creature.UiModelSceneID)
141                                .unwrap_or_default(),
142                        })
143                        .collect()
144                })
145                .unwrap_or_default();
146
147            Some(JournalInstanceFlat {
148                background_file_data_id: instance.BackgroundFileDataID,
149                background_storage_path: journal_image_path(instance.BackgroundFileDataID),
150                button_file_data_id: instance.ButtonFileDataID,
151                button_small_file_data_id: instance.ButtonSmallFileDataID,
152                button_storage_path: journal_image_path(instance.ButtonFileDataID),
153                id: instance.ID,
154                name: instance_name,
155                description: instance.Description_lang.clone().unwrap_or_default(),
156                lore_file_data_id: instance.LoreFileDataID,
157                lore_storage_path: journal_image_path(instance.LoreFileDataID),
158                loadscreen_file_data_id,
159                loadscreen_seo_storage_path: loadscreen_seo_path(loadscreen_file_data_id),
160                loadscreen_storage_path: loadscreen_image_path(loadscreen_file_data_id),
161                map_id,
162                kind,
163                is_mythic_plus: is_mythic_plus_map(dbc, map_id),
164                is_current_season: links
165                    .iter()
166                    .any(|link| link.JournalTierID == CURRENT_JOURNAL_TIER_ID),
167                tier_id: tier.ID,
168                tier_name: tier.Name_lang.clone().unwrap_or_default(),
169                tier_order_index: primary_link.OrderIndex,
170                encounters,
171            })
172        })
173        .collect()
174}
175
176fn primary_journal_tier_link<'a>(
177    dbc: &DbcData,
178    links: &[&'a crate::parsers::dbc::rows::JournalTierXInstanceRow],
179) -> Option<&'a crate::parsers::dbc::rows::JournalTierXInstanceRow> {
180    links
181        .iter()
182        .copied()
183        .find(|link| link.JournalTierID == CURRENT_JOURNAL_TIER_ID)
184        .or_else(|| {
185            links.iter().copied().max_by_key(|link| {
186                dbc.journal_tier
187                    .get(&link.JournalTierID)
188                    .map(|tier| (tier.Expansion, tier.ID))
189                    .unwrap_or_default()
190            })
191        })
192}