Skip to main content

wowlab_parsers/parsers/transform/
trait.rs

1use wowlab_engine_domain::dbc::{
2    TraitConditionType, TraitCurrencyFlags, TraitEffectOperation, TraitNodeType, TraitTreeIndex,
3};
4use wowlab_types::{
5    data::{
6        PointLimits, TraitEdge, TraitEffectPoints, TraitNode, TraitNodeEntry, TraitSubTree,
7        TraitTreeFlat,
8    },
9    sim::{FastSet, IntMap},
10};
11
12#[cfg(feature = "dbc")]
13use super::super::dbc::rows::{
14    TraitDefinitionRow, TraitEdgeRow, TraitNodeRow, TraitNodeXTraitNodeEntryRow,
15    TraitTreeLoadoutRow,
16};
17#[cfg(feature = "dbc")]
18use super::{super::dbc::DbcData, super::errors::TransformError};
19
20const TARGET_HERO_X: i32 = 7500;
21const TARGET_HERO_Y: i32 = 1200;
22
23/// Transform a specialization into its complete talent tree structure.
24///
25/// # Errors
26///
27/// Returns [`TransformError`] when the specialization or its required talent-tree data is absent.
28pub fn transform_trait_tree(dbc: &DbcData, spec_id: i32) -> Result<TraitTreeFlat, TransformError> {
29    let spec = dbc
30        .chr_specialization
31        .get(&spec_id)
32        .ok_or_else(|| TransformError::spec_not_found(spec_id))?;
33
34    let class_id = spec
35        .ClassID
36        .ok_or_else(|| TransformError::spec_not_found(spec_id))?;
37    let chr_class = dbc
38        .chr_classes
39        .get(&class_id)
40        .ok_or_else(|| TransformError::class_not_found(class_id))?;
41
42    let all_loadouts = dbc
43        .trait_tree_loadout_by_spec
44        .get(&spec_id)
45        .ok_or_else(|| TransformError::loadout_not_found(spec_id))?;
46
47    let all_tree_ids = select_live_tree_ids(dbc, all_loadouts);
48
49    // IntMap iteration order is nondeterministic.
50    let tree_id = all_tree_ids.iter().copied().min().unwrap_or(0);
51
52    let loadouts: Vec<_> = all_loadouts
53        .iter()
54        .filter(|l| all_tree_ids.contains(&l.TraitTreeID))
55        .cloned()
56        .collect();
57
58    let order_index_map = build_order_index_map(dbc, &loadouts);
59
60    let all_tree_nodes: Vec<_> = all_tree_ids
61        .iter()
62        .flat_map(|&tid| {
63            dbc.trait_node_by_tree
64                .get(&tid)
65                .cloned()
66                .unwrap_or_default()
67        })
68        .collect();
69
70    let spec_set_to_specs = build_spec_set_lookup(dbc);
71
72    let tree_nodes: Vec<_> = all_tree_nodes
73        .iter()
74        .filter(|node| {
75            node.TraitSubTreeID > 0
76                || TraitNodeType::from_dbc(node.Type) == Some(TraitNodeType::SubtreeSelection)
77                || should_include_node_for_spec(dbc, node.ID, spec_id, &spec_set_to_specs)
78        })
79        .cloned()
80        .collect();
81
82    let node_ids: Vec<i32> = tree_nodes.iter().map(|n| n.ID).collect();
83    let node_id_set: FastSet<i32> = node_ids.iter().copied().collect();
84
85    let valid_sub_tree_ids =
86        get_valid_subtrees_for_spec(dbc, &all_tree_nodes, spec_id, &spec_set_to_specs);
87
88    // #t(block: rust_unnecessary_collect) collect into FastSet intentionally deduplicates sub-tree IDs
89    let all_sub_tree_ids: Vec<i32> = tree_nodes
90        .iter()
91        .map(|n| n.TraitSubTreeID)
92        .filter(|&id| id > 0)
93        .collect::<FastSet<_>>()
94        .into_iter()
95        .collect();
96
97    let all_edges = collect_edges(dbc, &node_ids, &node_id_set);
98
99    let indexes = TraitNodeIndexes {
100        node_entries: build_node_entries_map(dbc, &tree_nodes),
101        hero_tree_offsets: calculate_hero_tree_offsets(&tree_nodes, &all_sub_tree_ids),
102        group_to_tree_index: build_group_tree_index_map(dbc, tree_id),
103        order_index: order_index_map,
104        spec_set_to_specs,
105    };
106
107    let mut nodes: Vec<TraitNode> = tree_nodes
108        .iter()
109        .filter_map(|node| build_talent_node(dbc, node, &indexes, spec_id, None))
110        .collect();
111
112    nodes.sort_by_key(|n| n.id);
113
114    let mut edges: Vec<TraitEdge> = all_edges
115        .iter()
116        .map(|e| TraitEdge {
117            id: e.ID,
118            from_node_id: e.LeftTraitNodeID,
119            to_node_id: e.RightTraitNodeID,
120            visual_style: e.VisualStyle,
121        })
122        .collect();
123
124    edges.sort_by_key(|e| e.id);
125
126    let mut sub_trees = build_subtrees(dbc, &valid_sub_tree_ids, &nodes);
127
128    sub_trees.sort_by_key(|s| s.id);
129
130    let point_limits = calculate_point_limits(dbc, &all_tree_ids);
131
132    let mut all_node_ids: Vec<i32> = all_tree_nodes
133        .iter()
134        .filter(|n| n.TraitTreeID == tree_id)
135        .map(|n| n.ID)
136        .collect();
137
138    all_node_ids.sort_unstable();
139
140    Ok(TraitTreeFlat {
141        spec_id,
142        spec_name: spec.Name_lang.clone().unwrap_or_default(),
143        class_name: chr_class.Name_lang.clone().unwrap_or_default(),
144        tree_id,
145        all_node_ids,
146        nodes,
147        edges,
148        sub_trees,
149        point_limits,
150    })
151}
152
153fn select_live_tree_ids(dbc: &DbcData, loadouts: &[TraitTreeLoadoutRow]) -> Vec<i32> {
154    // #t(block: rust_unnecessary_collect) collect into FastSet intentionally deduplicates tree IDs
155    let referenced: Vec<i32> = loadouts
156        .iter()
157        .map(|l| l.TraitTreeID)
158        .collect::<FastSet<_>>()
159        .into_iter()
160        .collect();
161
162    let live: Vec<i32> = referenced
163        .iter()
164        .copied()
165        .filter(|tid| dbc.skill_line_x_trait_tree.contains_key(tid))
166        .collect();
167
168    if live.is_empty() { referenced } else { live }
169}
170
171fn build_order_index_map(dbc: &DbcData, loadouts: &[TraitTreeLoadoutRow]) -> IntMap<i32, i32> {
172    let mut entries: Vec<_> = loadouts
173        .iter()
174        .flat_map(|loadout| {
175            dbc.trait_tree_loadout_entry
176                .get(&loadout.ID)
177                .cloned()
178                .unwrap_or_default()
179        })
180        .collect();
181
182    entries.sort_by_key(|e| e.OrderIndex);
183
184    entries
185        .iter()
186        .map(|e| (e.SelectedTraitNodeID, e.OrderIndex))
187        .collect()
188}
189
190pub(super) fn collect_edges(
191    dbc: &DbcData,
192    node_ids: &[i32],
193    node_id_set: &FastSet<i32>,
194) -> Vec<TraitEdgeRow> {
195    node_ids
196        .iter()
197        .flat_map(|&node_id| dbc.trait_edge.get(&node_id).cloned().unwrap_or_default())
198        .filter(|e| {
199            node_id_set.contains(&e.LeftTraitNodeID) && node_id_set.contains(&e.RightTraitNodeID)
200        })
201        .collect()
202}
203
204fn build_group_tree_index_map(dbc: &DbcData, primary_tree_id: i32) -> IntMap<i32, i32> {
205    let mut map = IntMap::default();
206
207    let mut currency_to_tree_index: IntMap<i32, i32> = IntMap::default();
208
209    if let Some(tree_currencies) = dbc.trait_tree_x_trait_currency.get(&primary_tree_id) {
210        for tc in tree_currencies {
211            if let Some(currency) = dbc.trait_currency.get(&tc.TraitCurrencyID) {
212                let flags = TraitCurrencyFlags::from_dbc(currency.Flags);
213
214                if flags.contains(TraitCurrencyFlags::CLASS) {
215                    currency_to_tree_index.insert(tc.TraitCurrencyID, TraitTreeIndex::Class as i32);
216                } else if flags.contains(TraitCurrencyFlags::SPECIALIZATION) {
217                    currency_to_tree_index
218                        .insert(tc.TraitCurrencyID, TraitTreeIndex::Specialization as i32);
219                }
220            }
221        }
222    }
223
224    for (&group_id, cost_links) in &dbc.trait_node_group_x_trait_cost {
225        for cost_link in cost_links {
226            if let Some(cost) = dbc.trait_cost.get(&cost_link.TraitCostID) {
227                if let Some(&tree_index) = currency_to_tree_index.get(&cost.TraitCurrencyID) {
228                    map.insert(group_id, tree_index);
229                }
230            }
231        }
232    }
233
234    map
235}
236
237pub(super) fn build_node_entries_map(
238    dbc: &DbcData,
239    tree_nodes: &[TraitNodeRow],
240) -> IntMap<i32, Vec<TraitNodeXTraitNodeEntryRow>> {
241    tree_nodes
242        .iter()
243        .map(|n| {
244            let entries = dbc
245                .trait_node_x_trait_node_entry
246                .get(&n.ID)
247                .cloned()
248                .unwrap_or_default();
249
250            let mut sorted = entries;
251            // DBC may assign the same selection index to multiple entries.
252
253            sorted.sort_by_key(|entry| {
254                let selection_index = if entry.Index >= 0 {
255                    entry.Index
256                } else {
257                    -entry.TraitNodeEntryID
258                };
259
260                (selection_index, entry.TraitNodeEntryID)
261            });
262
263            (n.ID, sorted)
264        })
265        .collect()
266}
267
268pub(super) struct TraitNodeIndexes {
269    pub(super) node_entries: IntMap<i32, Vec<TraitNodeXTraitNodeEntryRow>>,
270    pub(super) hero_tree_offsets: IntMap<i32, (i32, i32)>,
271    pub(super) group_to_tree_index: IntMap<i32, i32>,
272    pub(super) order_index: IntMap<i32, i32>,
273    pub(super) spec_set_to_specs: IntMap<i32, Vec<i32>>,
274}
275
276pub(super) fn build_talent_node(
277    dbc: &DbcData,
278    node: &TraitNodeRow,
279    indexes: &TraitNodeIndexes,
280    spec_id: i32,
281    tree_index_override: Option<i32>,
282) -> Option<TraitNode> {
283    let x_entries = indexes.node_entries.get(&node.ID)?;
284
285    let mut entries: Vec<TraitNodeEntry> = Vec::new();
286
287    for x_entry in x_entries {
288        let entry = dbc.trait_node_entry.get(&x_entry.TraitNodeEntryID)?;
289
290        // #t(block: rust_clone_in_loop) need owned String from borrowed DBC data
291        if entry.TraitDefinitionID <= 0 {
292            let sub_tree_name = dbc
293                .trait_sub_tree
294                .get(&entry.TraitSubTreeID)
295                .and_then(|st| st.Name_lang.clone())
296                .unwrap_or_default();
297
298            entries.push(TraitNodeEntry {
299                id: entry.ID,
300                definition_id: 0,
301                spell_id: 0,
302                override_spell_id: 0,
303                replaces_spell_id: 0,
304                sub_tree_id: entry.TraitSubTreeID,
305                max_ranks: entry.MaxRanks,
306                name: sub_tree_name,
307                description: String::new(),
308                icon_file_name: String::new(),
309                effect_points: Vec::new(),
310            });
311            continue;
312        }
313
314        let definition = dbc.trait_definition.get(&entry.TraitDefinitionID)?;
315
316        let icon_file_name = get_icon_file_name(dbc, definition);
317        let name = get_talent_name(dbc, definition);
318        let description = get_talent_description(dbc, definition);
319
320        entries.push(TraitNodeEntry {
321            id: entry.ID,
322            definition_id: definition.ID,
323            spell_id: definition.SpellID,
324            override_spell_id: definition.VisibleSpellID,
325            replaces_spell_id: definition.OverridesSpellID,
326            sub_tree_id: entry.TraitSubTreeID,
327            max_ranks: entry.MaxRanks,
328            name,
329            description,
330            icon_file_name,
331            effect_points: trait_effect_points(dbc, definition.ID),
332        });
333    }
334
335    if entries.is_empty() {
336        return None;
337    }
338
339    let max_ranks = if TraitNodeType::from_dbc(node.Type) == Some(TraitNodeType::Tiered) {
340        entries.iter().map(|e| e.max_ranks).sum()
341    } else {
342        entries[0].max_ranks
343    };
344
345    let (pos_x, pos_y) = apply_hero_offset(node, &indexes.hero_tree_offsets);
346    let tree_index = tree_index_override
347        .unwrap_or_else(|| determine_tree_index(dbc, node, &indexes.group_to_tree_index));
348
349    Some(TraitNode {
350        id: node.ID,
351        pos_x,
352        pos_y,
353        max_ranks,
354        granted_ranks: granted_ranks_for_spec(dbc, node.ID, spec_id, &indexes.spec_set_to_specs),
355        node_type: node.Type,
356        tree_index,
357        order_index: indexes.order_index.get(&node.ID).copied().unwrap_or(-1),
358        sub_tree_id: node.TraitSubTreeID,
359        entries,
360    })
361}
362
363fn trait_effect_points(dbc: &DbcData, definition_id: i32) -> Vec<TraitEffectPoints> {
364    let mut effects: Vec<_> = dbc
365        .trait_definition_effect_points
366        .get(&definition_id)
367        .into_iter()
368        .flatten()
369        .filter_map(|effect| {
370            let operation = TraitEffectOperation::from_dbc(effect.OperationType)?;
371            let mut points = dbc.curve_point.get(&effect.CurveID)?.clone();
372
373            points.sort_by_key(|point| point.OrderIndex);
374
375            Some(TraitEffectPoints {
376                effect_index: effect.EffectIndex,
377                operation,
378                rank_values: points
379                    .into_iter()
380                    .map(|point| (point.Pos_0, point.Pos_1))
381                    .collect(),
382            })
383        })
384        .collect();
385
386    effects.sort_by_key(|effect| effect.effect_index);
387
388    effects
389}
390
391fn apply_hero_offset(node: &TraitNodeRow, offsets: &IntMap<i32, (i32, i32)>) -> (i32, i32) {
392    if node.TraitSubTreeID > 0 {
393        if let Some(&(dx, dy)) = offsets.get(&node.TraitSubTreeID) {
394            return (node.PosX + dx, node.PosY + dy);
395        }
396    }
397
398    (node.PosX, node.PosY)
399}
400
401fn determine_tree_index(
402    dbc: &DbcData,
403    node: &TraitNodeRow,
404    group_to_tree_index: &IntMap<i32, i32>,
405) -> i32 {
406    if TraitNodeType::from_dbc(node.Type) == Some(TraitNodeType::SubtreeSelection) {
407        return TraitTreeIndex::Selection as i32;
408    }
409
410    if node.TraitSubTreeID > 0 {
411        return TraitTreeIndex::Hero as i32;
412    }
413
414    if let Some(memberships) = dbc.trait_node_group_x_trait_node.get(&node.ID) {
415        for m in memberships {
416            if let Some(&idx) = group_to_tree_index.get(&m.TraitNodeGroupID) {
417                return idx;
418            }
419        }
420    }
421
422    TraitTreeIndex::Class as i32
423}
424
425fn get_talent_name(dbc: &DbcData, definition: &TraitDefinitionRow) -> String {
426    if let Some(name) = &definition.OverrideName_lang {
427        return name.clone();
428    }
429
430    if definition.SpellID > 0 {
431        if let Some(spell) = dbc.spell_name.get(&definition.SpellID) {
432            if let Some(name) = &spell.Name_lang {
433                return name.clone();
434            }
435        }
436
437        return format!("Spell {}", definition.SpellID);
438    }
439
440    "Unknown Talent".to_string()
441}
442
443fn get_talent_description(dbc: &DbcData, definition: &TraitDefinitionRow) -> String {
444    if let Some(desc) = &definition.OverrideDescription_lang {
445        return desc.clone();
446    }
447
448    if definition.SpellID > 0 {
449        if let Some(spell) = dbc.spell.get(&definition.SpellID) {
450            if let Some(desc) = &spell.Description_lang {
451                return desc.clone();
452            }
453        }
454    }
455
456    String::new()
457}
458
459fn build_subtrees(
460    dbc: &DbcData,
461    valid_sub_tree_ids: &FastSet<i32>,
462    nodes: &[TraitNode],
463) -> Vec<TraitSubTree> {
464    valid_sub_tree_ids
465        .iter()
466        .filter_map(|&sub_tree_id| {
467            let sub_tree = dbc.trait_sub_tree.get(&sub_tree_id)?;
468
469            let icon_file_name = dbc
470                .ui_texture_atlas_element
471                .get(&sub_tree.UiTextureAtlasElementID)
472                .map_or_else(
473                    || derive_sub_tree_icon(nodes, sub_tree_id),
474                    |e| e.Name.clone(),
475                );
476
477            Some(TraitSubTree {
478                id: sub_tree_id,
479                name: sub_tree.Name_lang.clone().unwrap_or_default(),
480                description: sub_tree.Description_lang.clone().unwrap_or_default(),
481                icon_file_name,
482            })
483        })
484        .collect()
485}
486
487fn calculate_point_limits(dbc: &DbcData, all_tree_ids: &[i32]) -> PointLimits {
488    let tree_currencies: Vec<_> = all_tree_ids
489        .iter()
490        .flat_map(|&tid| {
491            dbc.trait_tree_x_trait_currency
492                .get(&tid)
493                .cloned()
494                .unwrap_or_default()
495        })
496        .collect();
497
498    let currency_ids: FastSet<i32> = tree_currencies
499        .iter()
500        .map(|tc| tc.TraitCurrencyID)
501        .collect();
502
503    let currency_totals: IntMap<i32, i32> = currency_ids
504        .iter()
505        .map(|&currency_id| {
506            let total = dbc
507                .trait_currency_source
508                .get(&currency_id)
509                .map_or(0, |sources| sources.iter().map(|s| s.Amount).sum());
510
511            (currency_id, total)
512        })
513        .collect();
514
515    let mut class_limit = 0;
516    let mut spec_limit = 0;
517    let mut hero_limit = 0;
518
519    for tc in tree_currencies {
520        let Some(currency) = dbc.trait_currency.get(&tc.TraitCurrencyID) else {
521            continue;
522        };
523        let total = currency_totals
524            .get(&tc.TraitCurrencyID)
525            .copied()
526            .unwrap_or(0);
527
528        let flags = TraitCurrencyFlags::from_dbc(currency.Flags);
529
530        if flags.contains(TraitCurrencyFlags::CLASS) {
531            class_limit += total;
532        } else if flags.contains(TraitCurrencyFlags::SPECIALIZATION) {
533            spec_limit += total;
534        } else if flags.is_empty() {
535            hero_limit = hero_limit.max(total);
536        }
537    }
538
539    PointLimits {
540        class: class_limit,
541        spec: spec_limit,
542        hero: hero_limit,
543    }
544}
545
546fn calculate_hero_tree_offsets(
547    nodes: &[TraitNodeRow],
548    sub_tree_ids: &[i32],
549) -> IntMap<i32, (i32, i32)> {
550    let mut offsets = IntMap::default();
551
552    for &sub_tree_id in sub_tree_ids {
553        let hero_nodes: Vec<_> = nodes
554            .iter()
555            .filter(|n| n.TraitSubTreeID == sub_tree_id)
556            .collect();
557
558        if hero_nodes.is_empty() {
559            continue;
560        }
561
562        let min_x = hero_nodes.iter().map(|n| n.PosX).min().unwrap_or(0);
563        let min_y = hero_nodes.iter().map(|n| n.PosY).min().unwrap_or(0);
564
565        offsets.insert(sub_tree_id, (TARGET_HERO_X - min_x, TARGET_HERO_Y - min_y));
566    }
567
568    offsets
569}
570
571fn get_icon_file_name(dbc: &DbcData, definition: &TraitDefinitionRow) -> String {
572    let icon_file_data_id = if definition.OverrideIcon > 0 {
573        definition.OverrideIcon
574    } else if definition.SpellID > 0 {
575        dbc.spell_misc
576            .get(&definition.SpellID)
577            .map_or(0, |m| m.SpellIconFileDataID)
578    } else {
579        0
580    };
581
582    super::resolve_icon_file_name(dbc, icon_file_data_id)
583}
584
585fn derive_sub_tree_icon(nodes: &[TraitNode], sub_tree_id: i32) -> String {
586    let mut entries = nodes
587        .iter()
588        .filter(|n| n.sub_tree_id == sub_tree_id)
589        .flat_map(|n| &n.entries);
590    let icon_entry = entries.find(|e| !e.icon_file_name.is_empty());
591
592    icon_entry.map_or_else(
593        || wowlab_types::constants::DEFAULT_ICON_FILENAME.to_string(),
594        |e| e.icon_file_name.clone(),
595    )
596}
597
598fn get_valid_subtrees_for_spec(
599    dbc: &DbcData,
600    tree_nodes: &[TraitNodeRow],
601    spec_id: i32,
602    spec_set_to_specs: &IntMap<i32, Vec<i32>>,
603) -> FastSet<i32> {
604    let mut valid_subtrees = FastSet::default();
605
606    let selection_nodes = tree_nodes
607        .iter()
608        .filter(|n| TraitNodeType::from_dbc(n.Type) == Some(TraitNodeType::SubtreeSelection));
609
610    for node in selection_nodes {
611        if !should_include_node_for_spec(dbc, node.ID, spec_id, spec_set_to_specs) {
612            continue;
613        }
614
615        let entries = dbc
616            .trait_node_x_trait_node_entry
617            .get(&node.ID)
618            .cloned()
619            .unwrap_or_default();
620
621        for x_entry in entries {
622            if let Some(entry) = dbc.trait_node_entry.get(&x_entry.TraitNodeEntryID) {
623                if entry.TraitSubTreeID > 0 {
624                    valid_subtrees.insert(entry.TraitSubTreeID);
625                }
626            }
627        }
628    }
629
630    valid_subtrees
631}
632
633fn build_spec_set_lookup(dbc: &DbcData) -> IntMap<i32, Vec<i32>> {
634    let mut map: IntMap<i32, Vec<i32>> = IntMap::default();
635
636    for members in dbc.spec_set_member.values() {
637        for member in members {
638            map.entry(member.SpecSet)
639                .or_default()
640                .push(member.ChrSpecializationID);
641        }
642    }
643
644    map
645}
646
647fn granted_ranks_for_spec(
648    dbc: &DbcData,
649    node_id: i32,
650    spec_id: i32,
651    spec_set_to_specs: &IntMap<i32, Vec<i32>>,
652) -> i32 {
653    let node_conditions = dbc
654        .trait_node_x_trait_cond
655        .get(&node_id)
656        .into_iter()
657        .flatten()
658        .filter_map(|reference| dbc.trait_cond.get(&reference.TraitCondID));
659    let group_conditions = dbc
660        .trait_node_group_x_trait_node
661        .get(&node_id)
662        .into_iter()
663        .flatten()
664        .flat_map(|membership| {
665            dbc.trait_node_group_x_trait_cond
666                .get(&membership.TraitNodeGroupID)
667                .into_iter()
668                .flatten()
669        })
670        .filter_map(|reference| dbc.trait_cond.get(&reference.TraitCondID));
671
672    let starter_conditions = node_conditions
673        .chain(group_conditions)
674        .filter(|condition| {
675            TraitConditionType::try_from(condition.CondType).ok()
676                == Some(TraitConditionType::StarterBuild)
677        })
678        .filter(|condition| {
679            spec_set_to_specs
680                .get(&condition.SpecSetID)
681                .is_some_and(|specs| specs.contains(&spec_id))
682        });
683
684    starter_conditions
685        .map(|condition| condition.GrantedRanks)
686        .max()
687        .unwrap_or(0)
688}
689
690fn should_include_node_for_spec(
691    dbc: &DbcData,
692    node_id: i32,
693    spec_id: i32,
694    spec_set_to_specs: &IntMap<i32, Vec<i32>>,
695) -> bool {
696    let mut has_spec_condition = false;
697    let mut spec_allowed = false;
698
699    let node_conds = dbc
700        .trait_node_x_trait_cond
701        .get(&node_id)
702        .cloned()
703        .unwrap_or_default();
704
705    for node_cond in &node_conds {
706        let Some(cond) = dbc.trait_cond.get(&node_cond.TraitCondID) else {
707            continue;
708        };
709
710        if TraitConditionType::try_from(cond.CondType).ok()
711            != Some(TraitConditionType::Specialization)
712            || cond.SpecSetID <= 0
713        {
714            continue;
715        }
716
717        has_spec_condition = true;
718
719        if let Some(specs) = spec_set_to_specs.get(&cond.SpecSetID) {
720            if specs.contains(&spec_id) {
721                spec_allowed = true;
722            }
723        }
724    }
725
726    let node_groups = dbc
727        .trait_node_group_x_trait_node
728        .get(&node_id)
729        .cloned()
730        .unwrap_or_default();
731
732    for node_group in &node_groups {
733        let group_cond_refs = dbc
734            .trait_node_group_x_trait_cond
735            .get(&node_group.TraitNodeGroupID)
736            .cloned()
737            .unwrap_or_default();
738
739        for cond_ref in &group_cond_refs {
740            let Some(cond) = dbc.trait_cond.get(&cond_ref.TraitCondID) else {
741                continue;
742            };
743
744            if TraitConditionType::try_from(cond.CondType).ok()
745                != Some(TraitConditionType::Specialization)
746                || cond.SpecSetID <= 0
747            {
748                continue;
749            }
750
751            has_spec_condition = true;
752
753            if let Some(specs) = spec_set_to_specs.get(&cond.SpecSetID) {
754                if specs.contains(&spec_id) {
755                    spec_allowed = true;
756                }
757            }
758        }
759    }
760
761    !has_spec_condition || spec_allowed
762}
763
764#[cfg(test)]
765#[path = "trait/tests.rs"]
766mod tests;