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                && let Some(&tree_index) = currency_to_tree_index.get(&cost.TraitCurrencyID)
228            {
229                map.insert(group_id, tree_index);
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        && let Some(&(dx, dy)) = offsets.get(&node.TraitSubTreeID)
394    {
395        return (node.PosX + dx, node.PosY + dy);
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            && let Some(name) = &spell.Name_lang
433        {
434            return name.clone();
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        && let Some(spell) = dbc.spell.get(&definition.SpellID)
450        && let Some(desc) = &spell.Description_lang
451    {
452        return desc.clone();
453    }
454
455    String::new()
456}
457
458fn build_subtrees(
459    dbc: &DbcData,
460    valid_sub_tree_ids: &FastSet<i32>,
461    nodes: &[TraitNode],
462) -> Vec<TraitSubTree> {
463    valid_sub_tree_ids
464        .iter()
465        .filter_map(|&sub_tree_id| {
466            let sub_tree = dbc.trait_sub_tree.get(&sub_tree_id)?;
467
468            let icon_file_name = dbc
469                .ui_texture_atlas_element
470                .get(&sub_tree.UiTextureAtlasElementID)
471                .map_or_else(
472                    || derive_sub_tree_icon(nodes, sub_tree_id),
473                    |e| e.Name.clone(),
474                );
475
476            Some(TraitSubTree {
477                id: sub_tree_id,
478                name: sub_tree.Name_lang.clone().unwrap_or_default(),
479                description: sub_tree.Description_lang.clone().unwrap_or_default(),
480                icon_file_name,
481            })
482        })
483        .collect()
484}
485
486fn calculate_point_limits(dbc: &DbcData, all_tree_ids: &[i32]) -> PointLimits {
487    let tree_currencies: Vec<_> = all_tree_ids
488        .iter()
489        .flat_map(|&tid| {
490            dbc.trait_tree_x_trait_currency
491                .get(&tid)
492                .cloned()
493                .unwrap_or_default()
494        })
495        .collect();
496
497    let currency_ids: FastSet<i32> = tree_currencies
498        .iter()
499        .map(|tc| tc.TraitCurrencyID)
500        .collect();
501
502    let currency_totals: IntMap<i32, i32> = currency_ids
503        .iter()
504        .map(|&currency_id| {
505            let total = dbc
506                .trait_currency_source
507                .get(&currency_id)
508                .map_or(0, |sources| sources.iter().map(|s| s.Amount).sum());
509
510            (currency_id, total)
511        })
512        .collect();
513
514    let mut class_limit = 0;
515    let mut spec_limit = 0;
516    let mut hero_limit = 0;
517
518    for tc in tree_currencies {
519        let Some(currency) = dbc.trait_currency.get(&tc.TraitCurrencyID) else {
520            continue;
521        };
522        let total = currency_totals
523            .get(&tc.TraitCurrencyID)
524            .copied()
525            .unwrap_or(0);
526
527        let flags = TraitCurrencyFlags::from_dbc(currency.Flags);
528
529        if flags.contains(TraitCurrencyFlags::CLASS) {
530            class_limit += total;
531        } else if flags.contains(TraitCurrencyFlags::SPECIALIZATION) {
532            spec_limit += total;
533        } else if flags.is_empty() {
534            hero_limit = hero_limit.max(total);
535        }
536    }
537
538    PointLimits {
539        class: class_limit,
540        spec: spec_limit,
541        hero: hero_limit,
542    }
543}
544
545fn calculate_hero_tree_offsets(
546    nodes: &[TraitNodeRow],
547    sub_tree_ids: &[i32],
548) -> IntMap<i32, (i32, i32)> {
549    let mut offsets = IntMap::default();
550
551    for &sub_tree_id in sub_tree_ids {
552        let hero_nodes: Vec<_> = nodes
553            .iter()
554            .filter(|n| n.TraitSubTreeID == sub_tree_id)
555            .collect();
556
557        if hero_nodes.is_empty() {
558            continue;
559        }
560
561        let min_x = hero_nodes.iter().map(|n| n.PosX).min().unwrap_or(0);
562        let min_y = hero_nodes.iter().map(|n| n.PosY).min().unwrap_or(0);
563
564        offsets.insert(sub_tree_id, (TARGET_HERO_X - min_x, TARGET_HERO_Y - min_y));
565    }
566
567    offsets
568}
569
570fn get_icon_file_name(dbc: &DbcData, definition: &TraitDefinitionRow) -> String {
571    let icon_file_data_id = if definition.OverrideIcon > 0 {
572        definition.OverrideIcon
573    } else if definition.SpellID > 0 {
574        dbc.spell_misc
575            .get(&definition.SpellID)
576            .map_or(0, |m| m.SpellIconFileDataID)
577    } else {
578        0
579    };
580
581    super::resolve_icon_file_name(dbc, icon_file_data_id)
582}
583
584fn derive_sub_tree_icon(nodes: &[TraitNode], sub_tree_id: i32) -> String {
585    let mut entries = nodes
586        .iter()
587        .filter(|n| n.sub_tree_id == sub_tree_id)
588        .flat_map(|n| &n.entries);
589    let icon_entry = entries.find(|e| !e.icon_file_name.is_empty());
590
591    icon_entry.map_or_else(
592        || wowlab_types::constants::DEFAULT_ICON_FILENAME.to_string(),
593        |e| e.icon_file_name.clone(),
594    )
595}
596
597fn get_valid_subtrees_for_spec(
598    dbc: &DbcData,
599    tree_nodes: &[TraitNodeRow],
600    spec_id: i32,
601    spec_set_to_specs: &IntMap<i32, Vec<i32>>,
602) -> FastSet<i32> {
603    let mut valid_subtrees = FastSet::default();
604
605    let selection_nodes = tree_nodes
606        .iter()
607        .filter(|n| TraitNodeType::from_dbc(n.Type) == Some(TraitNodeType::SubtreeSelection));
608
609    for node in selection_nodes {
610        if !should_include_node_for_spec(dbc, node.ID, spec_id, spec_set_to_specs) {
611            continue;
612        }
613
614        let entries = dbc
615            .trait_node_x_trait_node_entry
616            .get(&node.ID)
617            .cloned()
618            .unwrap_or_default();
619
620        for x_entry in entries {
621            if let Some(entry) = dbc.trait_node_entry.get(&x_entry.TraitNodeEntryID)
622                && entry.TraitSubTreeID > 0
623            {
624                valid_subtrees.insert(entry.TraitSubTreeID);
625            }
626        }
627    }
628
629    valid_subtrees
630}
631
632fn build_spec_set_lookup(dbc: &DbcData) -> IntMap<i32, Vec<i32>> {
633    let mut map: IntMap<i32, Vec<i32>> = IntMap::default();
634
635    for members in dbc.spec_set_member.values() {
636        for member in members {
637            map.entry(member.SpecSet)
638                .or_default()
639                .push(member.ChrSpecializationID);
640        }
641    }
642
643    map
644}
645
646fn granted_ranks_for_spec(
647    dbc: &DbcData,
648    node_id: i32,
649    spec_id: i32,
650    spec_set_to_specs: &IntMap<i32, Vec<i32>>,
651) -> i32 {
652    let node_conditions = dbc
653        .trait_node_x_trait_cond
654        .get(&node_id)
655        .into_iter()
656        .flatten()
657        .filter_map(|reference| dbc.trait_cond.get(&reference.TraitCondID));
658    let group_conditions = dbc
659        .trait_node_group_x_trait_node
660        .get(&node_id)
661        .into_iter()
662        .flatten()
663        .flat_map(|membership| {
664            dbc.trait_node_group_x_trait_cond
665                .get(&membership.TraitNodeGroupID)
666                .into_iter()
667                .flatten()
668        })
669        .filter_map(|reference| dbc.trait_cond.get(&reference.TraitCondID));
670
671    let starter_conditions = node_conditions
672        .chain(group_conditions)
673        .filter(|condition| {
674            TraitConditionType::try_from(condition.CondType).ok()
675                == Some(TraitConditionType::StarterBuild)
676        })
677        .filter(|condition| {
678            spec_set_to_specs
679                .get(&condition.SpecSetID)
680                .is_some_and(|specs| specs.contains(&spec_id))
681        });
682
683    starter_conditions
684        .map(|condition| condition.GrantedRanks)
685        .max()
686        .unwrap_or(0)
687}
688
689fn should_include_node_for_spec(
690    dbc: &DbcData,
691    node_id: i32,
692    spec_id: i32,
693    spec_set_to_specs: &IntMap<i32, Vec<i32>>,
694) -> bool {
695    let mut has_spec_condition = false;
696    let mut spec_allowed = false;
697
698    let node_conds = dbc
699        .trait_node_x_trait_cond
700        .get(&node_id)
701        .cloned()
702        .unwrap_or_default();
703
704    for node_cond in &node_conds {
705        let Some(cond) = dbc.trait_cond.get(&node_cond.TraitCondID) else {
706            continue;
707        };
708
709        if TraitConditionType::try_from(cond.CondType).ok()
710            != Some(TraitConditionType::Specialization)
711            || cond.SpecSetID <= 0
712        {
713            continue;
714        }
715
716        has_spec_condition = true;
717
718        if let Some(specs) = spec_set_to_specs.get(&cond.SpecSetID)
719            && specs.contains(&spec_id)
720        {
721            spec_allowed = true;
722        }
723    }
724
725    let node_groups = dbc
726        .trait_node_group_x_trait_node
727        .get(&node_id)
728        .cloned()
729        .unwrap_or_default();
730
731    for node_group in &node_groups {
732        let group_cond_refs = dbc
733            .trait_node_group_x_trait_cond
734            .get(&node_group.TraitNodeGroupID)
735            .cloned()
736            .unwrap_or_default();
737
738        for cond_ref in &group_cond_refs {
739            let Some(cond) = dbc.trait_cond.get(&cond_ref.TraitCondID) else {
740                continue;
741            };
742
743            if TraitConditionType::try_from(cond.CondType).ok()
744                != Some(TraitConditionType::Specialization)
745                || cond.SpecSetID <= 0
746            {
747                continue;
748            }
749
750            has_spec_condition = true;
751
752            if let Some(specs) = spec_set_to_specs.get(&cond.SpecSetID)
753                && specs.contains(&spec_id)
754            {
755                spec_allowed = true;
756            }
757        }
758    }
759
760    !has_spec_condition || spec_allowed
761}
762
763#[cfg(test)]
764#[path = "trait/tests.rs"]
765mod tests;