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/// Mirrors `SimC`'s `RacialSpellSet` (`dbc_extract3/dbc/filter.py:29`) and its generator.
49/// A row with no race mask of its own inherits the skill line's, which is how Pandaren are keyed.
50#[must_use]
51pub fn transform_all_racial_spells(dbc: &DbcData) -> Vec<RacialSpellFlat> {
52    let racial_lines = racial_skill_lines(dbc);
53    let races = playable_race_bits(dbc);
54    let mut rows = Vec::new();
55
56    for ability in dbc.skill_line_ability.values().flatten() {
57        let Some(line_race_mask) = racial_lines.get(&ability.SkillLine) else {
58            continue;
59        };
60
61        if ability.Spell == 0 {
62            continue;
63        }
64
65        let mask = match race_mask(ability.RaceMasks_0, ability.RaceMasks_1) {
66            0 => *line_race_mask,
67            own => own,
68        };
69
70        rows.extend(
71            races
72                .iter()
73                .filter(|(bit, _)| mask & 1_u64.checked_shl(*bit).unwrap_or_default() != 0)
74                .map(|(_, race_id)| (*race_id, ability.Spell, ability.ClassMask)),
75        );
76    }
77
78    rows.sort_unstable();
79    rows.dedup();
80
81    rows.into_iter()
82        .map(|(race_id, spell_id, class_mask)| RacialSpellFlat {
83            race_id,
84            spell_id,
85            class_mask,
86        })
87        .collect()
88}
89
90#[cfg(test)]
91mod tests {
92    use googletest::prelude::*;
93
94    use super::*;
95    use crate::parsers::{
96        dbc::rows::{ChrRacesRow, SkillLineAbilityRow, SkillRaceClassInfoRow},
97        transform::fixtures::dbc,
98    };
99
100    /// Orc bit 1, Pandaren (Horde) bit 25, and the racial skill lines they own.
101    fn racial_dbc() -> DbcData {
102        let mut dbc = dbc();
103
104        dbc.chr_races.insert(
105            2,
106            ChrRacesRow {
107                ID: 2,
108                PlayableRaceBit: 1,
109            },
110        );
111        dbc.chr_races.insert(
112            26,
113            ChrRacesRow {
114                ID: 26,
115                PlayableRaceBit: 25,
116            },
117        );
118        dbc.skill_race_class_info.insert(
119            125,
120            vec![SkillRaceClassInfoRow {
121                ID: 70,
122                SkillID: 125,
123                ClassMask: 16383,
124                Flags: 32914,
125                RaceMasks_0: 2,
126                RaceMasks_1: 0,
127            }],
128        );
129        dbc.skill_race_class_info.insert(
130            899,
131            vec![SkillRaceClassInfoRow {
132                ID: 1127,
133                SkillID: 899,
134                ClassMask: 16383,
135                Flags: 32914,
136                RaceMasks_0: 33_554_432,
137                RaceMasks_1: 0,
138            }],
139        );
140        // Cooking is not a racial skill line even though its rows carry race masks.
141        dbc.skill_race_class_info.insert(
142            185,
143            vec![SkillRaceClassInfoRow {
144                ID: 282,
145                SkillID: 185,
146                ClassMask: -1,
147                Flags: 128,
148                RaceMasks_0: -1,
149                RaceMasks_1: -1,
150            }],
151        );
152
153        dbc
154    }
155
156    #[gtest]
157    fn racial_spells_expand_the_row_race_mask() -> Result<()> {
158        let mut dbc = racial_dbc();
159
160        dbc.skill_line_ability.insert(
161            21563,
162            vec![SkillLineAbilityRow {
163                ID: 12041,
164                SkillLine: 125,
165                Spell: 21563,
166                ClassMask: 16383,
167                RaceMasks_0: 2,
168                RaceMasks_1: 0,
169            }],
170        );
171
172        verify_that!(
173            transform_all_racial_spells(&dbc),
174            elements_are![matches_pattern!(RacialSpellFlat {
175                race_id: eq(&2),
176                spell_id: eq(&21563),
177                class_mask: eq(&16383),
178            })]
179        )
180    }
181
182    #[gtest]
183    fn racial_spells_fall_back_to_the_skill_line_race_mask() -> Result<()> {
184        let mut dbc = racial_dbc();
185
186        dbc.skill_line_ability.insert(
187            107_079,
188            vec![SkillLineAbilityRow {
189                ID: 40_000,
190                SkillLine: 899,
191                Spell: 107_079,
192                ClassMask: 0,
193                RaceMasks_0: 0,
194                RaceMasks_1: 0,
195            }],
196        );
197
198        verify_that!(
199            transform_all_racial_spells(&dbc),
200            elements_are![matches_pattern!(RacialSpellFlat {
201                race_id: eq(&26),
202                spell_id: eq(&107_079),
203                class_mask: eq(&0),
204            })]
205        )
206    }
207
208    #[gtest]
209    fn racial_spells_skip_non_racial_skill_lines() -> Result<()> {
210        let mut dbc = racial_dbc();
211
212        dbc.skill_line_ability.insert(
213            33_277,
214            vec![SkillLineAbilityRow {
215                ID: 41_000,
216                SkillLine: 185,
217                Spell: 33_277,
218                ClassMask: 0,
219                RaceMasks_0: 2,
220                RaceMasks_1: 0,
221            }],
222        );
223
224        verify_that!(transform_all_racial_spells(&dbc), elements_are![])
225    }
226}