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
23pub 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 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 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 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 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 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(|¤cy_id| {
505 let total = dbc
506 .trait_currency_source
507 .get(¤cy_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;