Skip to main content

wowlab_parsers/parsers/transform/
racial.rs

1//! Racial-ability resolution: which spells a race grants, expanded from the racial skill lines.
2
3use wowlab_types::{data::RacialSpellFlat, sim::IntMap};
4
5#[cfg(feature = "dbc")]
6use super::super::dbc::DbcData;
7
8/// `SkillRaceClassInfo.Flags` bit that marks a skill line as a race's racial-ability line.
9const RACIAL_SKILL_LINE_FLAG: i32 = 0x8000;
10
11/// Joins the two signed DBC mask columns into the 64 race bits they encode.
12fn race_mask(low: i32, high: i32) -> u64 {
13    u64::from(u32::from_ne_bytes(low.to_ne_bytes()))
14        | (u64::from(u32::from_ne_bytes(high.to_ne_bytes())) << u32::BITS)
15}
16
17/// Every racial skill line, mapped to the races that own it.
18fn racial_skill_lines(dbc: &DbcData) -> IntMap<i32, u64> {
19    let mut lines = IntMap::default();
20
21    for row in dbc.skill_race_class_info.values().flatten() {
22        if row.Flags & RACIAL_SKILL_LINE_FLAG == 0 {
23            continue;
24        }
25
26        *lines.entry(row.SkillID).or_insert(0) |= race_mask(row.RaceMasks_0, row.RaceMasks_1);
27    }
28
29    lines
30}
31
32/// Every playable race paired with the mask bit it occupies.
33fn playable_race_bits(dbc: &DbcData) -> Vec<(u32, i32)> {
34    dbc.chr_races
35        .values()
36        .filter(|row| row.PlayableRaceBit >= 0)
37        .map(|row| {
38            (
39                wowlab_types::numeric::i32_to_u32_nonnegative(row.PlayableRaceBit),
40                row.ID,
41            )
42        })
43        .collect()
44}
45
46/// Expand every racial skill-line ability into one `RacialSpellFlat` per race that learns it.
47///
48/// A row with no race mask of its own inherits the skill line's, which is how Pandaren are keyed.
49#[must_use]
50pub fn transform_all_racial_spells(dbc: &DbcData) -> Vec<RacialSpellFlat> {
51    let racial_lines = racial_skill_lines(dbc);
52    let races = playable_race_bits(dbc);
53    let mut rows = Vec::new();
54
55    for ability in dbc.skill_line_ability.values().flatten() {
56        let Some(line_race_mask) = racial_lines.get(&ability.SkillLine) else {
57            continue;
58        };
59
60        if ability.Spell == 0 {
61            continue;
62        }
63
64        let mask = match race_mask(ability.RaceMasks_0, ability.RaceMasks_1) {
65            0 => *line_race_mask,
66            own => own,
67        };
68
69        rows.extend(
70            races
71                .iter()
72                .filter(|(bit, _)| mask & 1_u64.checked_shl(*bit).unwrap_or_default() != 0)
73                .map(|(_, race_id)| (*race_id, ability.Spell, ability.ClassMask)),
74        );
75    }
76
77    rows.sort_unstable();
78    rows.dedup();
79
80    rows.into_iter()
81        .map(|(race_id, spell_id, class_mask)| RacialSpellFlat {
82            race_id,
83            spell_id,
84            class_mask,
85        })
86        .collect()
87}
88
89#[cfg(test)]
90mod tests {
91    use googletest::prelude::*;
92
93    use super::*;
94    use crate::parsers::{
95        dbc::rows::{ChrRacesRow, SkillLineAbilityRow, SkillRaceClassInfoRow},
96        transform::fixtures::dbc,
97    };
98
99    /// Orc bit 1, Pandaren (Horde) bit 25, and the racial skill lines they own.
100    fn racial_dbc() -> DbcData {
101        let mut dbc = dbc();
102
103        dbc.chr_races.insert(
104            2,
105            ChrRacesRow {
106                ID: 2,
107                PlayableRaceBit: 1,
108            },
109        );
110        dbc.chr_races.insert(
111            26,
112            ChrRacesRow {
113                ID: 26,
114                PlayableRaceBit: 25,
115            },
116        );
117        dbc.skill_race_class_info.insert(
118            125,
119            vec![SkillRaceClassInfoRow {
120                ID: 70,
121                SkillID: 125,
122                ClassMask: 16383,
123                Flags: 32914,
124                RaceMasks_0: 2,
125                RaceMasks_1: 0,
126            }],
127        );
128        dbc.skill_race_class_info.insert(
129            899,
130            vec![SkillRaceClassInfoRow {
131                ID: 1127,
132                SkillID: 899,
133                ClassMask: 16383,
134                Flags: 32914,
135                RaceMasks_0: 33_554_432,
136                RaceMasks_1: 0,
137            }],
138        );
139        // Cooking is not a racial skill line even though its rows carry race masks.
140        dbc.skill_race_class_info.insert(
141            185,
142            vec![SkillRaceClassInfoRow {
143                ID: 282,
144                SkillID: 185,
145                ClassMask: -1,
146                Flags: 128,
147                RaceMasks_0: -1,
148                RaceMasks_1: -1,
149            }],
150        );
151
152        dbc
153    }
154
155    #[gtest]
156    fn racial_spells_expand_the_row_race_mask() -> Result<()> {
157        let mut dbc = racial_dbc();
158
159        dbc.skill_line_ability.insert(
160            21563,
161            vec![SkillLineAbilityRow {
162                ID: 12041,
163                SkillLine: 125,
164                Spell: 21563,
165                ClassMask: 16383,
166                RaceMasks_0: 2,
167                RaceMasks_1: 0,
168            }],
169        );
170
171        verify_that!(
172            transform_all_racial_spells(&dbc),
173            elements_are![matches_pattern!(RacialSpellFlat {
174                race_id: eq(&2),
175                spell_id: eq(&21563),
176                class_mask: eq(&16383),
177            })]
178        )
179    }
180
181    #[gtest]
182    fn racial_spells_fall_back_to_the_skill_line_race_mask() -> Result<()> {
183        let mut dbc = racial_dbc();
184
185        dbc.skill_line_ability.insert(
186            107_079,
187            vec![SkillLineAbilityRow {
188                ID: 40_000,
189                SkillLine: 899,
190                Spell: 107_079,
191                ClassMask: 0,
192                RaceMasks_0: 0,
193                RaceMasks_1: 0,
194            }],
195        );
196
197        verify_that!(
198            transform_all_racial_spells(&dbc),
199            elements_are![matches_pattern!(RacialSpellFlat {
200                race_id: eq(&26),
201                spell_id: eq(&107_079),
202                class_mask: eq(&0),
203            })]
204        )
205    }
206
207    #[gtest]
208    fn racial_spells_skip_non_racial_skill_lines() -> Result<()> {
209        let mut dbc = racial_dbc();
210
211        dbc.skill_line_ability.insert(
212            33_277,
213            vec![SkillLineAbilityRow {
214                ID: 41_000,
215                SkillLine: 185,
216                Spell: 33_277,
217                ClassMask: 0,
218                RaceMasks_0: 2,
219                RaceMasks_1: 0,
220            }],
221        );
222
223        verify_that!(transform_all_racial_spells(&dbc), elements_are![])
224    }
225}