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