1use rakata_extract::{tables, TwoDaCache};
27use rakata_formats::twoda::TwoDa;
28
29use crate::uti::{Uti, UtiProperty};
30
31#[derive(Debug, Clone, PartialEq, Eq)]
48pub enum DecodedProperty {
49 AbilityBonus {
54 property_id: u16,
57 subtype_id: u16,
60 cost_table: u8,
62 cost_value: u16,
65 },
66 SaveBonus {
78 property_id: u16,
81 subtype_id: u16,
83 cost_table: u8,
85 cost_value: u16,
88 },
89 SaveBonusSpecific {
97 property_id: u16,
101 subtype_id: u16,
103 cost_table: u8,
105 cost_value: u16,
108 },
109 SavePenalty {
115 property_id: u16,
118 subtype_id: u16,
120 cost_table: u8,
122 cost_value: u16,
125 },
126 SavePenaltySpecific {
136 property_id: u16,
140 subtype_id: u16,
142 cost_table: u8,
144 cost_value: u16,
147 },
148 DamageBonus {
158 property_id: u16,
161 subtype_id: u16,
163 cost_table: u8,
165 cost_value: u16,
168 },
169 DamageImmunity {
174 property_id: u16,
177 subtype_id: u16,
179 cost_table: u8,
181 cost_value: u16,
184 },
185 DamageResistance {
194 property_id: u16,
197 subtype_id: u16,
199 cost_table: u8,
201 cost_value: u16,
204 },
205 DamageRacialGroup {
210 property_id: u16,
213 subtype_id: u16,
215 cost_table: u8,
217 cost_value: u16,
220 },
221 DamageAlignmentGroup {
226 property_id: u16,
229 subtype_id: u16,
231 cost_table: u8,
233 cost_value: u16,
236 },
237 EnhancementRacialGroup {
246 property_id: u16,
249 subtype_id: u16,
251 cost_table: u8,
253 cost_value: u16,
256 },
257 EnhancementAlignmentGroup {
264 property_id: u16,
267 subtype_id: u16,
269 cost_table: u8,
271 cost_value: u16,
274 },
275 AttackBonusAlignmentGroup {
280 property_id: u16,
283 subtype_id: u16,
285 cost_table: u8,
287 cost_value: u16,
290 },
291 TrueSeeing {
296 property_id: u16,
299 subtype_id: u16,
302 cost_table: u8,
304 cost_value: u16,
307 },
308 Light {
317 property_id: u16,
320 subtype_id: u16,
323 cost_table: u8,
325 cost_value: u16,
328 param1: u8,
330 param1_value: u8,
333 },
334 AcBonus {
342 property_id: u16,
345 subtype_id: u16,
348 cost_table: u8,
350 cost_value: u16,
353 },
354 EnhancementBonus {
358 property_id: u16,
361 subtype_id: u16,
364 cost_table: u8,
366 cost_value: u16,
369 },
370 AttackBonus {
375 property_id: u16,
378 subtype_id: u16,
381 cost_table: u8,
383 cost_value: u16,
386 },
387 Keen {
391 property_id: u16,
394 subtype_id: u16,
397 cost_table: u8,
399 cost_value: u16,
402 },
403 MassiveCriticals {
408 property_id: u16,
411 subtype_id: u16,
414 cost_table: u8,
416 cost_value: u16,
419 },
420 BlasterBoltDeflectIncrease {
429 property_id: u16,
433 subtype_id: u16,
436 cost_table: u8,
438 cost_value: u16,
441 },
442 MonsterDamage {
450 property_id: u16,
453 subtype_id: u16,
456 cost_table: u8,
458 cost_value: u16,
461 },
462 BonusFeats {
468 property_id: u16,
471 subtype_id: u16,
473 cost_table: u8,
475 cost_value: u16,
478 },
479 Immunity {
484 property_id: u16,
487 subtype_id: u16,
489 cost_table: u8,
491 cost_value: u16,
494 },
495 Skill {
504 property_id: u16,
507 subtype_id: u16,
509 cost_table: u8,
511 cost_value: u16,
514 },
515 AttackPenalty {
520 property_id: u16,
523 subtype_id: u16,
526 cost_table: u8,
528 cost_value: u16,
531 },
532 DamagePenalty {
537 property_id: u16,
540 subtype_id: u16,
543 cost_table: u8,
545 cost_value: u16,
548 },
549 MagicResistBonus {
553 property_id: u16,
556 subtype_id: u16,
559 cost_table: u8,
561 cost_value: u16,
564 },
565 DamageNone {
571 property_id: u16,
574 subtype_id: u16,
577 cost_table: u8,
579 cost_value: u16,
582 },
583 Regeneration {
591 property_id: u16,
594 subtype_id: u16,
597 cost_table: u8,
599 cost_value: u16,
602 },
603 RegenerationForcePoints {
608 property_id: u16,
611 subtype_id: u16,
614 cost_table: u8,
616 cost_value: u16,
619 },
620 Disguise {
625 property_id: u16,
628 subtype_id: u16,
630 cost_table: u8,
632 cost_value: u16,
635 },
636 UseLimitationFeat {
645 property_id: u16,
648 subtype_id: u16,
650 cost_table: u8,
652 cost_value: u16,
655 },
656 UseLimitationRacial {
660 property_id: u16,
663 subtype_id: u16,
665 cost_table: u8,
667 cost_value: u16,
670 },
671 UseLimitationAlignmentGroup {
676 property_id: u16,
679 subtype_id: u16,
681 cost_table: u8,
683 cost_value: u16,
686 },
687 CastSpell {
704 property_id: u16,
707 subtype_id: u16,
709 cost_table: u8,
711 cost_value: u16,
714 useable: bool,
719 uses_per_day: Option<u8>,
723 },
724 Trap {
732 property_id: u16,
735 subtype_id: u16,
737 cost_table: u8,
739 cost_value: u16,
742 useable: bool,
744 uses_per_day: Option<u8>,
746 },
747 ThievesTools {
758 property_id: u16,
761 subtype_id: u16,
764 cost_table: u8,
766 cost_value: u16,
769 useable: bool,
771 uses_per_day: Option<u8>,
773 },
774 ComputerSpike {
785 property_id: u16,
788 subtype_id: u16,
791 cost_table: u8,
793 cost_value: u16,
796 useable: bool,
798 uses_per_day: Option<u8>,
800 },
801 OnHit {
812 property_id: u16,
815 subtype_id: u16,
817 cost_table: u8,
819 cost_value: u16,
822 param1: u8,
826 param1_value: u8,
829 },
830 Unknown {
835 property_id: u16,
838 property_label: Option<String>,
842 subtype: u16,
845 cost_table: u8,
847 cost_value: u16,
850 param1: u8,
852 param1_value: u8,
855 },
856}
857
858impl DecodedProperty {
859 pub fn subtype_label(&self, cache: &mut TwoDaCache) -> Option<String> {
878 let (property_id, subtype) = match self {
879 DecodedProperty::AbilityBonus {
880 property_id,
881 subtype_id,
882 ..
883 }
884 | DecodedProperty::SaveBonus {
885 property_id,
886 subtype_id,
887 ..
888 }
889 | DecodedProperty::SaveBonusSpecific {
890 property_id,
891 subtype_id,
892 ..
893 }
894 | DecodedProperty::SavePenalty {
895 property_id,
896 subtype_id,
897 ..
898 }
899 | DecodedProperty::SavePenaltySpecific {
900 property_id,
901 subtype_id,
902 ..
903 }
904 | DecodedProperty::DamageBonus {
905 property_id,
906 subtype_id,
907 ..
908 }
909 | DecodedProperty::DamageImmunity {
910 property_id,
911 subtype_id,
912 ..
913 }
914 | DecodedProperty::DamageResistance {
915 property_id,
916 subtype_id,
917 ..
918 }
919 | DecodedProperty::AcBonus {
920 property_id,
921 subtype_id,
922 ..
923 }
924 | DecodedProperty::EnhancementBonus {
925 property_id,
926 subtype_id,
927 ..
928 }
929 | DecodedProperty::OnHit {
930 property_id,
931 subtype_id,
932 ..
933 }
934 | DecodedProperty::CastSpell {
935 property_id,
936 subtype_id,
937 ..
938 }
939 | DecodedProperty::Trap {
940 property_id,
941 subtype_id,
942 ..
943 }
944 | DecodedProperty::ThievesTools {
945 property_id,
946 subtype_id,
947 ..
948 }
949 | DecodedProperty::ComputerSpike {
950 property_id,
951 subtype_id,
952 ..
953 }
954 | DecodedProperty::UseLimitationFeat {
955 property_id,
956 subtype_id,
957 ..
958 }
959 | DecodedProperty::UseLimitationRacial {
960 property_id,
961 subtype_id,
962 ..
963 }
964 | DecodedProperty::UseLimitationAlignmentGroup {
965 property_id,
966 subtype_id,
967 ..
968 }
969 | DecodedProperty::DamageRacialGroup {
970 property_id,
971 subtype_id,
972 ..
973 }
974 | DecodedProperty::DamageAlignmentGroup {
975 property_id,
976 subtype_id,
977 ..
978 }
979 | DecodedProperty::EnhancementRacialGroup {
980 property_id,
981 subtype_id,
982 ..
983 }
984 | DecodedProperty::EnhancementAlignmentGroup {
985 property_id,
986 subtype_id,
987 ..
988 }
989 | DecodedProperty::AttackBonusAlignmentGroup {
990 property_id,
991 subtype_id,
992 ..
993 }
994 | DecodedProperty::TrueSeeing {
995 property_id,
996 subtype_id,
997 ..
998 }
999 | DecodedProperty::Light {
1000 property_id,
1001 subtype_id,
1002 ..
1003 }
1004 | DecodedProperty::AttackBonus {
1005 property_id,
1006 subtype_id,
1007 ..
1008 }
1009 | DecodedProperty::Keen {
1010 property_id,
1011 subtype_id,
1012 ..
1013 }
1014 | DecodedProperty::MassiveCriticals {
1015 property_id,
1016 subtype_id,
1017 ..
1018 }
1019 | DecodedProperty::BlasterBoltDeflectIncrease {
1020 property_id,
1021 subtype_id,
1022 ..
1023 }
1024 | DecodedProperty::MonsterDamage {
1025 property_id,
1026 subtype_id,
1027 ..
1028 }
1029 | DecodedProperty::BonusFeats {
1030 property_id,
1031 subtype_id,
1032 ..
1033 }
1034 | DecodedProperty::Immunity {
1035 property_id,
1036 subtype_id,
1037 ..
1038 }
1039 | DecodedProperty::Skill {
1040 property_id,
1041 subtype_id,
1042 ..
1043 }
1044 | DecodedProperty::AttackPenalty {
1045 property_id,
1046 subtype_id,
1047 ..
1048 }
1049 | DecodedProperty::DamagePenalty {
1050 property_id,
1051 subtype_id,
1052 ..
1053 }
1054 | DecodedProperty::MagicResistBonus {
1055 property_id,
1056 subtype_id,
1057 ..
1058 }
1059 | DecodedProperty::DamageNone {
1060 property_id,
1061 subtype_id,
1062 ..
1063 }
1064 | DecodedProperty::Regeneration {
1065 property_id,
1066 subtype_id,
1067 ..
1068 }
1069 | DecodedProperty::RegenerationForcePoints {
1070 property_id,
1071 subtype_id,
1072 ..
1073 }
1074 | DecodedProperty::Disguise {
1075 property_id,
1076 subtype_id,
1077 ..
1078 } => (*property_id, *subtype_id),
1079 DecodedProperty::Unknown {
1080 property_id,
1081 subtype,
1082 ..
1083 } => (*property_id, *subtype),
1084 };
1085 let subtype_resref = {
1086 let propdef = cache.twoda(tables::ITEMPROPDEF).ok()?;
1087 propdef
1088 .cell(usize::from(property_id), "SubTypeResRef")?
1089 .to_string()
1090 };
1091 if subtype_resref.is_empty() {
1092 return None;
1093 }
1094 let subtype_table = cache.twoda(&subtype_resref).ok()?;
1095 subtype_table
1096 .cell(usize::from(subtype), "label")
1097 .map(str::to_string)
1098 }
1099}
1100
1101#[derive(Debug, Clone)]
1116pub struct UtiProjection<'a> {
1117 uti: &'a Uti,
1118 decoded_properties: Vec<DecodedProperty>,
1119}
1120
1121#[derive(Debug)]
1139pub struct UtiSnapshot<'a> {
1140 uti: &'a Uti,
1141 decoded_properties: Vec<DecodedProperty>,
1142 base_item_info: Option<BaseItemInfo>,
1143 resolved_magnitudes: Vec<Option<i32>>,
1151}
1152
1153#[derive(Debug, Clone, Copy, Default)]
1157struct BaseItemInfo {
1158 weapon_wield: u8,
1161 stacking: u8,
1165 equip_slot_mask: Option<u32>,
1169 model_type: Option<u8>,
1173}
1174
1175impl BaseItemInfo {
1176 fn from_row(table: &TwoDa, base_item: i32) -> Option<Self> {
1180 let row = usize::try_from(base_item).ok()?;
1181 if row >= table.rows.len() {
1182 return None;
1183 }
1184 Some(Self {
1185 weapon_wield: parse_u8_cell(table, row, "weaponwield").unwrap_or(0),
1186 stacking: parse_u8_cell(table, row, "stacking").unwrap_or(0),
1187 equip_slot_mask: parse_hex_u32_cell(table, row, "equipableslots"),
1188 model_type: parse_u8_cell(table, row, "modeltype"),
1189 })
1190 }
1191}
1192
1193fn parse_u8_cell(table: &TwoDa, row: usize, column: &str) -> Option<u8> {
1194 table.cell(row, column).and_then(|s| s.trim().parse().ok())
1195}
1196
1197fn parse_i32_cell(table: &TwoDa, row: usize, column: &str) -> Option<i32> {
1198 table.cell(row, column).and_then(|s| s.trim().parse().ok())
1199}
1200
1201fn parse_hex_u32_cell(table: &TwoDa, row: usize, column: &str) -> Option<u32> {
1202 let raw = table.cell(row, column)?.trim();
1203 let stripped = raw
1204 .strip_prefix("0x")
1205 .or_else(|| raw.strip_prefix("0X"))
1206 .unwrap_or(raw);
1207 u32::from_str_radix(stripped, 16).ok()
1208}
1209
1210fn resolve_magnitude(prop: &DecodedProperty, cache: &mut TwoDaCache) -> Option<i32> {
1218 match prop {
1219 DecodedProperty::DamageBonus { cost_value, .. } => Some(i32::from(*cost_value)),
1224 DecodedProperty::AbilityBonus { cost_value, .. } => {
1229 let table = cache.twoda("iprp_bonuscost").ok()?;
1230 parse_i32_cell(table, usize::from(*cost_value), "Value")
1231 }
1232 DecodedProperty::DamageImmunity {
1237 cost_table,
1238 cost_value,
1239 ..
1240 } => resolve_dynamic_magnitude(cache, *cost_table, *cost_value, "Value"),
1241 _ => None,
1242 }
1243}
1244
1245fn resolve_dynamic_magnitude(
1253 cache: &mut TwoDaCache,
1254 cost_table: u8,
1255 cost_value: u16,
1256 column: &str,
1257) -> Option<i32> {
1258 let cost_2da_name = {
1263 let costtable = cache.twoda("iprp_costtable").ok()?;
1264 costtable
1265 .cell(usize::from(cost_table), "Name")?
1266 .trim()
1267 .to_lowercase()
1268 };
1269 if cost_2da_name.is_empty() {
1270 return None;
1271 }
1272 let table = cache.twoda(&cost_2da_name).ok()?;
1273 parse_i32_cell(table, usize::from(cost_value), column)
1274}
1275
1276impl<'a> UtiProjection<'a> {
1277 pub fn properties(&self) -> &[DecodedProperty] {
1284 &self.decoded_properties
1285 }
1286
1287 pub fn is_armor(&self) -> bool {
1296 self.uti.is_armor()
1297 }
1298
1299 pub fn snapshot(&self, cache: &mut TwoDaCache) -> UtiSnapshot<'a> {
1307 let base_item_info = cache
1308 .twoda(tables::BASEITEMS)
1309 .ok()
1310 .and_then(|table| BaseItemInfo::from_row(table, self.uti.base_item));
1311 let resolved_magnitudes = self
1312 .decoded_properties
1313 .iter()
1314 .map(|prop| resolve_magnitude(prop, cache))
1315 .collect();
1316 UtiSnapshot {
1317 uti: self.uti,
1318 decoded_properties: self.decoded_properties.clone(),
1319 base_item_info,
1320 resolved_magnitudes,
1321 }
1322 }
1323}
1324
1325impl<'a> UtiSnapshot<'a> {
1326 pub fn properties(&self) -> &[DecodedProperty] {
1333 &self.decoded_properties
1334 }
1335
1336 pub fn is_armor(&self) -> bool {
1345 self.uti.is_armor()
1346 }
1347
1348 pub fn is_weapon(&self) -> bool {
1355 self.base_item_info
1356 .as_ref()
1357 .is_some_and(|b| b.weapon_wield > 0)
1358 }
1359
1360 pub fn is_consumable(&self) -> bool {
1368 self.base_item_info.as_ref().is_some_and(|b| b.stacking > 1)
1369 }
1370
1371 pub fn equip_slot_mask(&self) -> Option<u32> {
1379 self.base_item_info.as_ref().and_then(|b| b.equip_slot_mask)
1380 }
1381
1382 pub fn model_type(&self) -> Option<u8> {
1391 self.base_item_info.as_ref().and_then(|b| b.model_type)
1392 }
1393
1394 pub fn has_property_kind(&self, filter: PropertyKindFilter) -> bool {
1403 self.decoded_properties
1404 .iter()
1405 .any(|prop| filter.matches(prop))
1406 }
1407
1408 pub fn ability_bonuses(&self) -> impl Iterator<Item = (u16, i32)> + '_ {
1422 self.iter_resolved_magnitudes(|prop| match prop {
1423 DecodedProperty::AbilityBonus { subtype_id, .. } => Some(*subtype_id),
1424 _ => None,
1425 })
1426 }
1427
1428 pub fn damage_bonuses(&self) -> impl Iterator<Item = (u16, i32)> + '_ {
1439 self.iter_resolved_magnitudes(|prop| match prop {
1440 DecodedProperty::DamageBonus { subtype_id, .. } => Some(*subtype_id),
1441 _ => None,
1442 })
1443 }
1444
1445 pub fn damage_immunities(&self) -> impl Iterator<Item = (u16, i32)> + '_ {
1461 self.iter_resolved_magnitudes(|prop| match prop {
1462 DecodedProperty::DamageImmunity { subtype_id, .. } => Some(*subtype_id),
1463 _ => None,
1464 })
1465 }
1466
1467 fn iter_resolved_magnitudes<F>(&self, pick_subtype: F) -> impl Iterator<Item = (u16, i32)> + '_
1474 where
1475 F: Fn(&DecodedProperty) -> Option<u16> + 'static,
1476 {
1477 self.decoded_properties
1478 .iter()
1479 .zip(self.resolved_magnitudes.iter())
1480 .filter_map(move |(prop, magnitude)| {
1481 let subtype = pick_subtype(prop)?;
1482 let value = (*magnitude)?;
1483 Some((subtype, value))
1484 })
1485 }
1486}
1487
1488#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1505pub enum PropertyKindFilter {
1506 Damage,
1510 Ability,
1512 Save,
1516 Attack,
1519 Enhancement,
1522 UseLimitation,
1525 Active,
1529}
1530
1531impl PropertyKindFilter {
1532 fn matches(self, prop: &DecodedProperty) -> bool {
1533 match self {
1534 Self::Damage => matches!(
1535 prop,
1536 DecodedProperty::DamageBonus { .. }
1537 | DecodedProperty::DamageImmunity { .. }
1538 | DecodedProperty::DamageResistance { .. }
1539 | DecodedProperty::DamageRacialGroup { .. }
1540 | DecodedProperty::DamageAlignmentGroup { .. }
1541 | DecodedProperty::DamagePenalty { .. }
1542 ),
1543 Self::Ability => matches!(prop, DecodedProperty::AbilityBonus { .. }),
1544 Self::Save => matches!(
1545 prop,
1546 DecodedProperty::SaveBonus { .. }
1547 | DecodedProperty::SaveBonusSpecific { .. }
1548 | DecodedProperty::SavePenalty { .. }
1549 | DecodedProperty::SavePenaltySpecific { .. }
1550 ),
1551 Self::Attack => matches!(
1552 prop,
1553 DecodedProperty::AttackBonus { .. }
1554 | DecodedProperty::AttackBonusAlignmentGroup { .. }
1555 | DecodedProperty::AttackPenalty { .. }
1556 ),
1557 Self::Enhancement => matches!(
1558 prop,
1559 DecodedProperty::EnhancementBonus { .. }
1560 | DecodedProperty::EnhancementRacialGroup { .. }
1561 | DecodedProperty::EnhancementAlignmentGroup { .. }
1562 ),
1563 Self::UseLimitation => matches!(
1564 prop,
1565 DecodedProperty::UseLimitationFeat { .. }
1566 | DecodedProperty::UseLimitationRacial { .. }
1567 | DecodedProperty::UseLimitationAlignmentGroup { .. }
1568 ),
1569 Self::Active => matches!(
1570 prop,
1571 DecodedProperty::CastSpell { .. }
1572 | DecodedProperty::Trap { .. }
1573 | DecodedProperty::ThievesTools { .. }
1574 | DecodedProperty::ComputerSpike { .. }
1575 ),
1576 }
1577 }
1578}
1579
1580impl Uti {
1581 pub fn project(&self, itempropdef: Option<&TwoDa>) -> UtiProjection<'_> {
1596 let decoded_properties = self
1597 .properties
1598 .iter()
1599 .map(|p| {
1600 let label = itempropdef
1601 .and_then(|table| table.cell(usize::from(p.property_name), "label"))
1602 .map(str::to_string);
1603 decode_property(p, label)
1604 })
1605 .collect();
1606 UtiProjection {
1607 uti: self,
1608 decoded_properties,
1609 }
1610 }
1611
1612 pub fn snapshot(&self, cache: &mut TwoDaCache) -> UtiSnapshot<'_> {
1627 let projection = {
1631 let propdef = cache.twoda(tables::ITEMPROPDEF).ok();
1632 self.project(propdef)
1633 };
1634 projection.snapshot(cache)
1635 }
1636}
1637
1638fn decode_property(p: &UtiProperty, label: Option<String>) -> DecodedProperty {
1646 match label.as_deref() {
1647 Some("Ability") => DecodedProperty::AbilityBonus {
1648 property_id: p.property_name,
1649 subtype_id: p.subtype,
1650 cost_table: p.cost_table,
1651 cost_value: p.cost_value,
1652 },
1653 Some("ImprovedSavingThrows") => DecodedProperty::SaveBonus {
1654 property_id: p.property_name,
1655 subtype_id: p.subtype,
1656 cost_table: p.cost_table,
1657 cost_value: p.cost_value,
1658 },
1659 Some("ImprovedSavingThrowsSpecific") => DecodedProperty::SaveBonusSpecific {
1660 property_id: p.property_name,
1661 subtype_id: p.subtype,
1662 cost_table: p.cost_table,
1663 cost_value: p.cost_value,
1664 },
1665 Some("ReducedSavingThrows") => DecodedProperty::SavePenalty {
1666 property_id: p.property_name,
1667 subtype_id: p.subtype,
1668 cost_table: p.cost_table,
1669 cost_value: p.cost_value,
1670 },
1671 Some("ReducedSpecificSavingThrow") => DecodedProperty::SavePenaltySpecific {
1672 property_id: p.property_name,
1673 subtype_id: p.subtype,
1674 cost_table: p.cost_table,
1675 cost_value: p.cost_value,
1676 },
1677 Some("Damage") => DecodedProperty::DamageBonus {
1678 property_id: p.property_name,
1679 subtype_id: p.subtype,
1680 cost_table: p.cost_table,
1681 cost_value: p.cost_value,
1682 },
1683 Some("DamageImmunity") => DecodedProperty::DamageImmunity {
1684 property_id: p.property_name,
1685 subtype_id: p.subtype,
1686 cost_table: p.cost_table,
1687 cost_value: p.cost_value,
1688 },
1689 Some("DamageResist") => DecodedProperty::DamageResistance {
1690 property_id: p.property_name,
1691 subtype_id: p.subtype,
1692 cost_table: p.cost_table,
1693 cost_value: p.cost_value,
1694 },
1695 Some("Armor") => DecodedProperty::AcBonus {
1696 property_id: p.property_name,
1697 subtype_id: p.subtype,
1698 cost_table: p.cost_table,
1699 cost_value: p.cost_value,
1700 },
1701 Some("Enhancement") => DecodedProperty::EnhancementBonus {
1702 property_id: p.property_name,
1703 subtype_id: p.subtype,
1704 cost_table: p.cost_table,
1705 cost_value: p.cost_value,
1706 },
1707 Some("OnHit") => DecodedProperty::OnHit {
1708 property_id: p.property_name,
1709 subtype_id: p.subtype,
1710 cost_table: p.cost_table,
1711 cost_value: p.cost_value,
1712 param1: p.param1,
1713 param1_value: p.param1_value,
1714 },
1715 Some("CastSpell") => DecodedProperty::CastSpell {
1716 property_id: p.property_name,
1717 subtype_id: p.subtype,
1718 cost_table: p.cost_table,
1719 cost_value: p.cost_value,
1720 useable: active_useable(p.useable),
1721 uses_per_day: active_uses_per_day(p.uses_per_day),
1722 },
1723 Some("Trap") => DecodedProperty::Trap {
1724 property_id: p.property_name,
1725 subtype_id: p.subtype,
1726 cost_table: p.cost_table,
1727 cost_value: p.cost_value,
1728 useable: active_useable(p.useable),
1729 uses_per_day: active_uses_per_day(p.uses_per_day),
1730 },
1731 Some("ThievesTools") => DecodedProperty::ThievesTools {
1732 property_id: p.property_name,
1733 subtype_id: p.subtype,
1734 cost_table: p.cost_table,
1735 cost_value: p.cost_value,
1736 useable: active_useable(p.useable),
1737 uses_per_day: active_uses_per_day(p.uses_per_day),
1738 },
1739 Some("Computer_Spike") => DecodedProperty::ComputerSpike {
1740 property_id: p.property_name,
1741 subtype_id: p.subtype,
1742 cost_table: p.cost_table,
1743 cost_value: p.cost_value,
1744 useable: active_useable(p.useable),
1745 uses_per_day: active_uses_per_day(p.uses_per_day),
1746 },
1747 Some("Use_Limitation_Feat") => DecodedProperty::UseLimitationFeat {
1748 property_id: p.property_name,
1749 subtype_id: p.subtype,
1750 cost_table: p.cost_table,
1751 cost_value: p.cost_value,
1752 },
1753 Some("UseLimitationRacial") => DecodedProperty::UseLimitationRacial {
1754 property_id: p.property_name,
1755 subtype_id: p.subtype,
1756 cost_table: p.cost_table,
1757 cost_value: p.cost_value,
1758 },
1759 Some("UseLimitationAlignmentGroup") => DecodedProperty::UseLimitationAlignmentGroup {
1760 property_id: p.property_name,
1761 subtype_id: p.subtype,
1762 cost_table: p.cost_table,
1763 cost_value: p.cost_value,
1764 },
1765 Some("DamageRacialGroup") => DecodedProperty::DamageRacialGroup {
1766 property_id: p.property_name,
1767 subtype_id: p.subtype,
1768 cost_table: p.cost_table,
1769 cost_value: p.cost_value,
1770 },
1771 Some("DamageAlignmentGroup") => DecodedProperty::DamageAlignmentGroup {
1772 property_id: p.property_name,
1773 subtype_id: p.subtype,
1774 cost_table: p.cost_table,
1775 cost_value: p.cost_value,
1776 },
1777 Some("EnhancementRacialGroup") => DecodedProperty::EnhancementRacialGroup {
1778 property_id: p.property_name,
1779 subtype_id: p.subtype,
1780 cost_table: p.cost_table,
1781 cost_value: p.cost_value,
1782 },
1783 Some("EnhancementAlignmentGroup") => DecodedProperty::EnhancementAlignmentGroup {
1784 property_id: p.property_name,
1785 subtype_id: p.subtype,
1786 cost_table: p.cost_table,
1787 cost_value: p.cost_value,
1788 },
1789 Some("AttackBonusAlignmentGroup") => DecodedProperty::AttackBonusAlignmentGroup {
1790 property_id: p.property_name,
1791 subtype_id: p.subtype,
1792 cost_table: p.cost_table,
1793 cost_value: p.cost_value,
1794 },
1795 Some("True_Seeing") => DecodedProperty::TrueSeeing {
1796 property_id: p.property_name,
1797 subtype_id: p.subtype,
1798 cost_table: p.cost_table,
1799 cost_value: p.cost_value,
1800 },
1801 Some("Light") => DecodedProperty::Light {
1802 property_id: p.property_name,
1803 subtype_id: p.subtype,
1804 cost_table: p.cost_table,
1805 cost_value: p.cost_value,
1806 param1: p.param1,
1807 param1_value: p.param1_value,
1808 },
1809 Some("AttackBonus") => DecodedProperty::AttackBonus {
1810 property_id: p.property_name,
1811 subtype_id: p.subtype,
1812 cost_table: p.cost_table,
1813 cost_value: p.cost_value,
1814 },
1815 Some("Keen") => DecodedProperty::Keen {
1816 property_id: p.property_name,
1817 subtype_id: p.subtype,
1818 cost_table: p.cost_table,
1819 cost_value: p.cost_value,
1820 },
1821 Some("Massive_Criticals") => DecodedProperty::MassiveCriticals {
1822 property_id: p.property_name,
1823 subtype_id: p.subtype,
1824 cost_table: p.cost_table,
1825 cost_value: p.cost_value,
1826 },
1827 Some("Blaster_Bolt_Deflect_Increase") => DecodedProperty::BlasterBoltDeflectIncrease {
1828 property_id: p.property_name,
1829 subtype_id: p.subtype,
1830 cost_table: p.cost_table,
1831 cost_value: p.cost_value,
1832 },
1833 Some("Monster_damage") => DecodedProperty::MonsterDamage {
1834 property_id: p.property_name,
1835 subtype_id: p.subtype,
1836 cost_table: p.cost_table,
1837 cost_value: p.cost_value,
1838 },
1839 Some("BonusFeats") => DecodedProperty::BonusFeats {
1840 property_id: p.property_name,
1841 subtype_id: p.subtype,
1842 cost_table: p.cost_table,
1843 cost_value: p.cost_value,
1844 },
1845 Some("Immunity") => DecodedProperty::Immunity {
1846 property_id: p.property_name,
1847 subtype_id: p.subtype,
1848 cost_table: p.cost_table,
1849 cost_value: p.cost_value,
1850 },
1851 Some("Skill") => DecodedProperty::Skill {
1852 property_id: p.property_name,
1853 subtype_id: p.subtype,
1854 cost_table: p.cost_table,
1855 cost_value: p.cost_value,
1856 },
1857 Some("AttackPenalty") => DecodedProperty::AttackPenalty {
1858 property_id: p.property_name,
1859 subtype_id: p.subtype,
1860 cost_table: p.cost_table,
1861 cost_value: p.cost_value,
1862 },
1863 Some("DamagePenalty") => DecodedProperty::DamagePenalty {
1864 property_id: p.property_name,
1865 subtype_id: p.subtype,
1866 cost_table: p.cost_table,
1867 cost_value: p.cost_value,
1868 },
1869 Some("ImprovedMagicResist") => DecodedProperty::MagicResistBonus {
1870 property_id: p.property_name,
1871 subtype_id: p.subtype,
1872 cost_table: p.cost_table,
1873 cost_value: p.cost_value,
1874 },
1875 Some("DamageNone") => DecodedProperty::DamageNone {
1876 property_id: p.property_name,
1877 subtype_id: p.subtype,
1878 cost_table: p.cost_table,
1879 cost_value: p.cost_value,
1880 },
1881 Some("Regeneration") => DecodedProperty::Regeneration {
1882 property_id: p.property_name,
1883 subtype_id: p.subtype,
1884 cost_table: p.cost_table,
1885 cost_value: p.cost_value,
1886 },
1887 Some("Regeneration_Force_Points") => DecodedProperty::RegenerationForcePoints {
1888 property_id: p.property_name,
1889 subtype_id: p.subtype,
1890 cost_table: p.cost_table,
1891 cost_value: p.cost_value,
1892 },
1893 Some("Disguise") => DecodedProperty::Disguise {
1894 property_id: p.property_name,
1895 subtype_id: p.subtype,
1896 cost_table: p.cost_table,
1897 cost_value: p.cost_value,
1898 },
1899 _ => DecodedProperty::Unknown {
1916 property_id: p.property_name,
1917 property_label: label,
1918 subtype: p.subtype,
1919 cost_table: p.cost_table,
1920 cost_value: p.cost_value,
1921 param1: p.param1,
1922 param1_value: p.param1_value,
1923 },
1924 }
1925}
1926
1927fn active_useable(raw: Option<bool>) -> bool {
1934 raw.unwrap_or(true)
1935}
1936
1937fn active_uses_per_day(raw: Option<u8>) -> Option<u8> {
1942 match raw {
1943 Some(0xFF) | None => None,
1944 Some(value) => Some(value),
1945 }
1946}
1947
1948#[cfg(test)]
1949mod tests {
1950 use super::*;
1951 use crate::uti::UtiProperty;
1952 use rakata_core::{ResourceType, ResourceTypeCode};
1953 use rakata_extract::{OverrideSource, Resolver, ResolverSourceRef};
1954 use rakata_formats::twoda::{write_twoda_to_vec, TwoDa, TwoDaRow};
1955
1956 fn property(property_name: u16, subtype: u16) -> UtiProperty {
1957 UtiProperty {
1958 cost_table: 1,
1959 cost_value: 5,
1960 param1: 0xFF,
1961 param1_value: 0,
1962 property_name,
1963 subtype,
1964 chance_appear: 100,
1965 useable: None,
1966 uses_per_day: None,
1967 upgrade_type: None,
1968 }
1969 }
1970
1971 fn itempropdef_with_labels(labels: &[(usize, &str)]) -> TwoDa {
1972 let max_row = labels.iter().map(|(idx, _)| *idx).max().unwrap_or(0);
1973 let mut rows = Vec::with_capacity(max_row + 1);
1974 for row_index in 0..=max_row {
1975 let label = labels
1976 .iter()
1977 .find(|(idx, _)| *idx == row_index)
1978 .map(|(_, label)| (*label).to_string())
1979 .unwrap_or_default();
1980 rows.push(TwoDaRow {
1981 label: row_index.to_string(),
1982 cells: vec![label],
1983 });
1984 }
1985 TwoDa {
1986 headers: vec!["label".to_string()],
1987 rows,
1988 }
1989 }
1990
1991 fn override_with_2da(name: &str, table: &TwoDa) -> OverrideSource {
1992 let bytes = write_twoda_to_vec(table).expect("write 2da fixture");
1993 let mut overrides = OverrideSource::new();
1994 overrides
1995 .add_entry(
1996 name,
1997 ResourceTypeCode::from(ResourceType::TwoDa),
1998 bytes,
1999 "test",
2000 )
2001 .expect("add override entry");
2002 overrides
2003 }
2004
2005 #[test]
2006 fn decodes_every_property_into_unknown_variant() {
2007 let uti = Uti {
2008 properties: vec![property(0, 1), property(7, 12), property(45, 0)],
2009 ..Uti::default()
2010 };
2011 let resolver = Resolver::new();
2012 let mut cache = TwoDaCache::new(&resolver);
2013
2014 let view = uti.snapshot(&mut cache);
2015 assert_eq!(view.properties().len(), 3);
2016 assert!(view
2017 .decoded_properties
2018 .iter()
2019 .all(|p| matches!(p, DecodedProperty::Unknown { .. })));
2020 }
2021
2022 #[test]
2023 fn unknown_carries_raw_fields_through_unchanged() {
2024 let raw = property(0, 0);
2025 let mut shaped = raw.clone();
2026 shaped.property_name = 7;
2027 shaped.subtype = 12;
2028 shaped.cost_table = 3;
2029 shaped.cost_value = 99;
2030 shaped.param1 = 4;
2031 shaped.param1_value = 200;
2032 let uti = Uti {
2033 properties: vec![shaped.clone()],
2034 ..Uti::default()
2035 };
2036 let resolver = Resolver::new();
2037 let mut cache = TwoDaCache::new(&resolver);
2038
2039 let view = uti.snapshot(&mut cache);
2040 let DecodedProperty::Unknown {
2041 property_id,
2042 subtype,
2043 cost_table,
2044 cost_value,
2045 param1,
2046 param1_value,
2047 ..
2048 } = &view.properties()[0]
2049 else {
2050 panic!("expected Unknown variant for unloaded itempropdef");
2051 };
2052 assert_eq!(*property_id, 7);
2053 assert_eq!(*subtype, 12);
2054 assert_eq!(*cost_table, 3);
2055 assert_eq!(*cost_value, 99);
2056 assert_eq!(*param1, 4);
2057 assert_eq!(*param1_value, 200);
2058 }
2059
2060 #[test]
2061 fn property_label_resolves_into_unknown_for_untyped_kinds() {
2062 let table = itempropdef_with_labels(&[(0, "Test_Kind_A"), (7, "Test_Kind_B")]);
2065 let overrides = override_with_2da("itempropdef", &table);
2066 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2067 let mut cache = TwoDaCache::new(&resolver);
2068
2069 let uti = Uti {
2070 properties: vec![property(7, 0), property(0, 0)],
2071 ..Uti::default()
2072 };
2073 let view = uti.snapshot(&mut cache);
2074
2075 let DecodedProperty::Unknown { property_label, .. } = &view.properties()[0] else {
2076 panic!("expected Unknown variant for synthetic label");
2077 };
2078 assert_eq!(property_label.as_deref(), Some("Test_Kind_B"));
2079 let DecodedProperty::Unknown { property_label, .. } = &view.properties()[1] else {
2080 panic!("expected Unknown variant for synthetic label");
2081 };
2082 assert_eq!(property_label.as_deref(), Some("Test_Kind_A"));
2083 }
2084
2085 #[test]
2086 fn property_label_is_none_when_row_is_absent() {
2087 let table = itempropdef_with_labels(&[(0, "Test_Kind_A"), (1, "Test_Kind_B")]);
2091 let overrides = override_with_2da("itempropdef", &table);
2092 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2093 let mut cache = TwoDaCache::new(&resolver);
2094
2095 let uti = Uti {
2096 properties: vec![property(99, 0)],
2097 ..Uti::default()
2098 };
2099 let view = uti.snapshot(&mut cache);
2100
2101 let DecodedProperty::Unknown { property_label, .. } = &view.properties()[0] else {
2102 panic!("expected Unknown variant for absent row");
2103 };
2104 assert!(property_label.is_none());
2105 }
2106
2107 #[test]
2108 fn property_label_is_none_when_itempropdef_is_missing() {
2109 let resolver = Resolver::new();
2112 let mut cache = TwoDaCache::new(&resolver);
2113
2114 let uti = Uti {
2115 properties: vec![property(0, 0), property(7, 0)],
2116 ..Uti::default()
2117 };
2118 let view = uti.snapshot(&mut cache);
2119
2120 for prop in view.properties() {
2121 let DecodedProperty::Unknown { property_label, .. } = prop else {
2122 panic!("expected Unknown variant when itempropdef is missing");
2123 };
2124 assert!(property_label.is_none());
2125 }
2126 }
2127
2128 #[test]
2129 fn mod_added_property_label_surfaces_via_unknown() {
2130 let table = itempropdef_with_labels(&[(200, "FakeMod_GrantsCookies")]);
2134 let overrides = override_with_2da("itempropdef", &table);
2135 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2136 let mut cache = TwoDaCache::new(&resolver);
2137
2138 let uti = Uti {
2139 properties: vec![property(200, 5)],
2140 ..Uti::default()
2141 };
2142 let view = uti.snapshot(&mut cache);
2143
2144 let DecodedProperty::Unknown {
2145 property_id,
2146 property_label,
2147 subtype,
2148 ..
2149 } = &view.properties()[0]
2150 else {
2151 panic!("expected Unknown variant for mod-added label");
2152 };
2153 assert_eq!(*property_id, 200);
2154 assert_eq!(property_label.as_deref(), Some("FakeMod_GrantsCookies"));
2155 assert_eq!(*subtype, 5);
2156 }
2157
2158 #[test]
2159 fn empty_property_list_decodes_to_empty_view() {
2160 let uti = Uti::default();
2161 let resolver = Resolver::new();
2162 let mut cache = TwoDaCache::new(&resolver);
2163
2164 let view = uti.snapshot(&mut cache);
2165 assert!(view.properties().is_empty());
2166 }
2167
2168 #[test]
2169 fn ability_bonus_label_routes_to_typed_variant() {
2170 let table = itempropdef_with_labels(&[(0, "Ability")]);
2174 let overrides = override_with_2da("itempropdef", &table);
2175 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2176 let mut cache = TwoDaCache::new(&resolver);
2177
2178 let mut shaped = property(0, 2);
2179 shaped.cost_table = 1;
2180 shaped.cost_value = 4;
2181 let uti = Uti {
2182 properties: vec![shaped],
2183 ..Uti::default()
2184 };
2185 let view = uti.snapshot(&mut cache);
2186
2187 let DecodedProperty::AbilityBonus {
2188 property_id,
2189 subtype_id,
2190 cost_table,
2191 cost_value,
2192 } = &view.properties()[0]
2193 else {
2194 panic!("expected AbilityBonus variant for `Ability` label");
2195 };
2196 assert_eq!(*property_id, 0);
2197 assert_eq!(*subtype_id, 2); assert_eq!(*cost_table, 1);
2199 assert_eq!(*cost_value, 4);
2200 }
2201
2202 #[test]
2203 fn ability_bonus_preserves_raw_subtype_for_mod_extended_rows() {
2204 let table = itempropdef_with_labels(&[(0, "Ability")]);
2208 let overrides = override_with_2da("itempropdef", &table);
2209 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2210 let mut cache = TwoDaCache::new(&resolver);
2211
2212 let uti = Uti {
2213 properties: vec![property(0, 99)],
2214 ..Uti::default()
2215 };
2216 let view = uti.snapshot(&mut cache);
2217
2218 let DecodedProperty::AbilityBonus { subtype_id, .. } = &view.properties()[0] else {
2219 panic!("expected AbilityBonus variant for mod-extended subtype");
2220 };
2221 assert_eq!(*subtype_id, 99);
2222 }
2223
2224 #[test]
2225 fn ability_bonus_subtype_label_resolves_via_iprp_abilities() {
2226 let propdef = itempropdef_with_subtypes(&[(0, "Ability", "iprp_abilities")]);
2229 let abilities = subtype_2da(&[
2230 (0, "STR"),
2231 (1, "DEX"),
2232 (2, "CON"),
2233 (3, "INT"),
2234 (4, "WIS"),
2235 (5, "CHA"),
2236 ]);
2237 let mut overrides = OverrideSource::new();
2238 add_2da_entry(&mut overrides, "itempropdef", &propdef);
2239 add_2da_entry(&mut overrides, "iprp_abilities", &abilities);
2240 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2241 let mut cache = TwoDaCache::new(&resolver);
2242
2243 let prop = DecodedProperty::AbilityBonus {
2244 property_id: 0,
2245 subtype_id: 3,
2246 cost_table: 0,
2247 cost_value: 0,
2248 };
2249 assert_eq!(prop.subtype_label(&mut cache).as_deref(), Some("INT"));
2250 }
2251
2252 #[test]
2253 fn save_bonus_label_routes_to_typed_variant() {
2254 let table = itempropdef_with_labels(&[(26, "ImprovedSavingThrows")]);
2258 let overrides = override_with_2da("itempropdef", &table);
2259 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2260 let mut cache = TwoDaCache::new(&resolver);
2261
2262 let mut shaped = property(26, 1);
2263 shaped.cost_table = 2;
2264 shaped.cost_value = 3;
2265 let uti = Uti {
2266 properties: vec![shaped],
2267 ..Uti::default()
2268 };
2269 let view = uti.snapshot(&mut cache);
2270
2271 let DecodedProperty::SaveBonus {
2272 property_id,
2273 subtype_id,
2274 cost_table,
2275 cost_value,
2276 } = &view.properties()[0]
2277 else {
2278 panic!("expected SaveBonus variant for `ImprovedSavingThrows` label");
2279 };
2280 assert_eq!(*property_id, 26);
2281 assert_eq!(*subtype_id, 1);
2282 assert_eq!(*cost_table, 2);
2283 assert_eq!(*cost_value, 3);
2284 }
2285
2286 #[test]
2287 fn save_bonus_preserves_raw_subtype_for_mod_extended_rows() {
2288 let table = itempropdef_with_labels(&[(26, "ImprovedSavingThrows")]);
2292 let overrides = override_with_2da("itempropdef", &table);
2293 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2294 let mut cache = TwoDaCache::new(&resolver);
2295
2296 let uti = Uti {
2297 properties: vec![property(26, 200)],
2298 ..Uti::default()
2299 };
2300 let view = uti.snapshot(&mut cache);
2301
2302 let DecodedProperty::SaveBonus { subtype_id, .. } = &view.properties()[0] else {
2303 panic!("expected SaveBonus variant for mod-extended subtype");
2304 };
2305 assert_eq!(*subtype_id, 200);
2306 }
2307
2308 #[test]
2309 fn save_bonus_does_not_match_save_penalty_or_specific_kinds() {
2310 let table = itempropdef_with_labels(&[
2317 (26, "ImprovedSavingThrows"),
2318 (27, "ImprovedSavingThrowsSpecific"),
2319 (33, "ReducedSavingThrows"),
2320 (34, "ReducedSpecificSavingThrow"),
2321 ]);
2322 let overrides = override_with_2da("itempropdef", &table);
2323 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2324 let mut cache = TwoDaCache::new(&resolver);
2325
2326 let uti = Uti {
2327 properties: vec![
2328 property(26, 0),
2329 property(27, 0),
2330 property(33, 0),
2331 property(34, 0),
2332 ],
2333 ..Uti::default()
2334 };
2335 let view = uti.snapshot(&mut cache);
2336
2337 assert!(matches!(
2338 view.properties()[0],
2339 DecodedProperty::SaveBonus { .. }
2340 ));
2341 assert!(matches!(
2342 view.properties()[1],
2343 DecodedProperty::SaveBonusSpecific { .. }
2344 ));
2345 assert!(matches!(
2346 view.properties()[2],
2347 DecodedProperty::SavePenalty { .. }
2348 ));
2349 assert!(matches!(
2350 view.properties()[3],
2351 DecodedProperty::SavePenaltySpecific { .. }
2352 ));
2353 }
2354
2355 #[test]
2356 fn save_bonus_subtype_label_resolves_via_iprp_saveelement() {
2357 let propdef =
2360 itempropdef_with_subtypes(&[(26, "ImprovedSavingThrows", "iprp_saveelement")]);
2361 let saveelement = subtype_2da(&[(0, "Universal"), (1, "Acid"), (2, "Cold")]);
2362 let mut overrides = OverrideSource::new();
2363 add_2da_entry(&mut overrides, "itempropdef", &propdef);
2364 add_2da_entry(&mut overrides, "iprp_saveelement", &saveelement);
2365 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2366 let mut cache = TwoDaCache::new(&resolver);
2367
2368 let prop = DecodedProperty::SaveBonus {
2369 property_id: 26,
2370 subtype_id: 2,
2371 cost_table: 0,
2372 cost_value: 0,
2373 };
2374 assert_eq!(prop.subtype_label(&mut cache).as_deref(), Some("Cold"));
2375 }
2376
2377 #[test]
2378 fn save_throw_family_subtype_labels_resolve_via_their_2das() {
2379 let propdef = itempropdef_with_subtypes(&[
2385 (26, "ImprovedSavingThrows", "iprp_saveelement"),
2386 (27, "ImprovedSavingThrowsSpecific", "iprp_savingthrow"),
2387 (33, "ReducedSavingThrows", "iprp_saveelement"),
2388 (34, "ReducedSpecificSavingThrow", "iprp_savingthrow"),
2389 ]);
2390 let saveelement = subtype_2da(&[(0, "Universal"), (1, "Acid"), (2, "Cold")]);
2391 let savingthrow = subtype_2da(&[(0, "Fortitude"), (1, "Reflex"), (2, "Will")]);
2392
2393 let mut overrides = OverrideSource::new();
2394 add_2da_entry(&mut overrides, "itempropdef", &propdef);
2395 add_2da_entry(&mut overrides, "iprp_saveelement", &saveelement);
2396 add_2da_entry(&mut overrides, "iprp_savingthrow", &savingthrow);
2397 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2398 let mut cache = TwoDaCache::new(&resolver);
2399
2400 let specific_bonus = DecodedProperty::SaveBonusSpecific {
2401 property_id: 27,
2402 subtype_id: 2,
2403 cost_table: 0,
2404 cost_value: 0,
2405 };
2406 assert_eq!(
2407 specific_bonus.subtype_label(&mut cache).as_deref(),
2408 Some("Will")
2409 );
2410
2411 let universal_penalty = DecodedProperty::SavePenalty {
2412 property_id: 33,
2413 subtype_id: 1,
2414 cost_table: 0,
2415 cost_value: 0,
2416 };
2417 assert_eq!(
2418 universal_penalty.subtype_label(&mut cache).as_deref(),
2419 Some("Acid")
2420 );
2421
2422 let specific_penalty = DecodedProperty::SavePenaltySpecific {
2423 property_id: 34,
2424 subtype_id: 0,
2425 cost_table: 0,
2426 cost_value: 0,
2427 };
2428 assert_eq!(
2429 specific_penalty.subtype_label(&mut cache).as_deref(),
2430 Some("Fortitude")
2431 );
2432 }
2433
2434 #[test]
2435 fn damage_bonus_label_routes_to_typed_variant() {
2436 let table = itempropdef_with_labels(&[(11, "Damage")]);
2440 let overrides = override_with_2da("itempropdef", &table);
2441 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2442 let mut cache = TwoDaCache::new(&resolver);
2443
2444 let mut shaped = property(11, 5);
2445 shaped.cost_table = 4;
2446 shaped.cost_value = 2;
2447 let uti = Uti {
2448 properties: vec![shaped],
2449 ..Uti::default()
2450 };
2451 let view = uti.snapshot(&mut cache);
2452
2453 let DecodedProperty::DamageBonus {
2454 property_id,
2455 subtype_id,
2456 cost_table,
2457 cost_value,
2458 } = &view.properties()[0]
2459 else {
2460 panic!("expected DamageBonus variant for `Damage` label");
2461 };
2462 assert_eq!(*property_id, 11);
2463 assert_eq!(*subtype_id, 5);
2464 assert_eq!(*cost_table, 4);
2465 assert_eq!(*cost_value, 2);
2466 }
2467
2468 #[test]
2469 fn damage_bonus_does_not_match_vulnerability_or_unrelated_kinds() {
2470 let table = itempropdef_with_labels(&[(18, "Damage_Vulnerability")]);
2478 let overrides = override_with_2da("itempropdef", &table);
2479 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2480 let mut cache = TwoDaCache::new(&resolver);
2481
2482 let uti = Uti {
2483 properties: vec![property(18, 0)],
2484 ..Uti::default()
2485 };
2486 let view = uti.snapshot(&mut cache);
2487
2488 let DecodedProperty::Unknown { property_label, .. } = &view.properties()[0] else {
2489 panic!("expected Unknown variant for `Damage_Vulnerability`");
2490 };
2491 assert_eq!(property_label.as_deref(), Some("Damage_Vulnerability"));
2492 }
2493
2494 #[test]
2495 fn damage_immunity_label_routes_to_typed_variant() {
2496 let table = itempropdef_with_labels(&[(14, "DamageImmunity")]);
2497 let overrides = override_with_2da("itempropdef", &table);
2498 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2499 let mut cache = TwoDaCache::new(&resolver);
2500
2501 let uti = Uti {
2502 properties: vec![property(14, 7)],
2503 ..Uti::default()
2504 };
2505 let view = uti.snapshot(&mut cache);
2506
2507 let DecodedProperty::DamageImmunity {
2508 property_id,
2509 subtype_id,
2510 ..
2511 } = &view.properties()[0]
2512 else {
2513 panic!("expected DamageImmunity variant for `DamageImmunity` label");
2514 };
2515 assert_eq!(*property_id, 14);
2516 assert_eq!(*subtype_id, 7);
2517 }
2518
2519 #[test]
2520 fn damage_resistance_label_routes_to_typed_variant() {
2521 let table = itempropdef_with_labels(&[(17, "DamageResist")]);
2524 let overrides = override_with_2da("itempropdef", &table);
2525 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2526 let mut cache = TwoDaCache::new(&resolver);
2527
2528 let uti = Uti {
2529 properties: vec![property(17, 3)],
2530 ..Uti::default()
2531 };
2532 let view = uti.snapshot(&mut cache);
2533
2534 let DecodedProperty::DamageResistance {
2535 property_id,
2536 subtype_id,
2537 ..
2538 } = &view.properties()[0]
2539 else {
2540 panic!("expected DamageResistance variant for `DamageResist` label");
2541 };
2542 assert_eq!(*property_id, 17);
2543 assert_eq!(*subtype_id, 3);
2544 }
2545
2546 #[test]
2547 fn damage_resistance_does_not_match_damage_reduced() {
2548 let table = itempropdef_with_labels(&[(16, "DamageReduced")]);
2553 let overrides = override_with_2da("itempropdef", &table);
2554 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2555 let mut cache = TwoDaCache::new(&resolver);
2556
2557 let uti = Uti {
2558 properties: vec![property(16, 0)],
2559 ..Uti::default()
2560 };
2561 let view = uti.snapshot(&mut cache);
2562
2563 let DecodedProperty::Unknown { property_label, .. } = &view.properties()[0] else {
2564 panic!("expected Unknown variant for `DamageReduced`");
2565 };
2566 assert_eq!(property_label.as_deref(), Some("DamageReduced"));
2567 }
2568
2569 #[test]
2570 fn damage_family_subtype_labels_resolve_via_iprp_damagetype() {
2571 let propdef = itempropdef_with_subtypes(&[
2575 (11, "Damage", "iprp_damagetype"),
2576 (14, "DamageImmunity", "iprp_damagetype"),
2577 (17, "DamageResist", "iprp_damagetype"),
2578 ]);
2579 let damagetype = subtype_2da(&[
2580 (0, "Bludgeoning"),
2581 (1, "Slashing"),
2582 (2, "Piercing"),
2583 (5, "Acid"),
2584 (6, "Cold"),
2585 ]);
2586 let mut overrides = OverrideSource::new();
2587 add_2da_entry(&mut overrides, "itempropdef", &propdef);
2588 add_2da_entry(&mut overrides, "iprp_damagetype", &damagetype);
2589 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2590 let mut cache = TwoDaCache::new(&resolver);
2591
2592 let bonus = DecodedProperty::DamageBonus {
2593 property_id: 11,
2594 subtype_id: 5,
2595 cost_table: 0,
2596 cost_value: 0,
2597 };
2598 assert_eq!(bonus.subtype_label(&mut cache).as_deref(), Some("Acid"));
2599
2600 let immunity = DecodedProperty::DamageImmunity {
2601 property_id: 14,
2602 subtype_id: 6,
2603 cost_table: 0,
2604 cost_value: 0,
2605 };
2606 assert_eq!(immunity.subtype_label(&mut cache).as_deref(), Some("Cold"));
2607
2608 let resistance = DecodedProperty::DamageResistance {
2609 property_id: 17,
2610 subtype_id: 1,
2611 cost_table: 0,
2612 cost_value: 0,
2613 };
2614 assert_eq!(
2615 resistance.subtype_label(&mut cache).as_deref(),
2616 Some("Slashing")
2617 );
2618 }
2619
2620 #[test]
2621 fn ac_bonus_label_routes_to_typed_variant() {
2622 let table = itempropdef_with_labels(&[(1, "Armor")]);
2627 let overrides = override_with_2da("itempropdef", &table);
2628 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2629 let mut cache = TwoDaCache::new(&resolver);
2630
2631 let mut shaped = property(1, 0);
2632 shaped.cost_table = 2;
2633 shaped.cost_value = 5;
2634 let uti = Uti {
2635 properties: vec![shaped],
2636 ..Uti::default()
2637 };
2638 let view = uti.snapshot(&mut cache);
2639
2640 let DecodedProperty::AcBonus {
2641 property_id,
2642 subtype_id,
2643 cost_table,
2644 cost_value,
2645 } = &view.properties()[0]
2646 else {
2647 panic!("expected AcBonus variant for `Armor` label");
2648 };
2649 assert_eq!(*property_id, 1);
2650 assert_eq!(*subtype_id, 0);
2651 assert_eq!(*cost_table, 2);
2652 assert_eq!(*cost_value, 5);
2653 }
2654
2655 #[test]
2656 fn ac_bonus_does_not_match_armor_conditional_kinds() {
2657 let table = itempropdef_with_labels(&[
2662 (2, "ArmorAlignmentGroup"),
2663 (3, "ArmorDamageType"),
2664 (4, "ArmorRacialGroup"),
2665 ]);
2666 let overrides = override_with_2da("itempropdef", &table);
2667 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2668 let mut cache = TwoDaCache::new(&resolver);
2669
2670 let uti = Uti {
2671 properties: vec![property(2, 0), property(3, 0), property(4, 0)],
2672 ..Uti::default()
2673 };
2674 let view = uti.snapshot(&mut cache);
2675
2676 for prop in view.properties() {
2677 assert!(
2678 matches!(prop, DecodedProperty::Unknown { .. }),
2679 "expected Unknown for non-Armor label, got {prop:?}"
2680 );
2681 }
2682 }
2683
2684 #[test]
2685 fn enhancement_bonus_label_routes_to_typed_variant() {
2686 let table = itempropdef_with_labels(&[(5, "Enhancement")]);
2687 let overrides = override_with_2da("itempropdef", &table);
2688 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2689 let mut cache = TwoDaCache::new(&resolver);
2690
2691 let mut shaped = property(5, 0);
2692 shaped.cost_table = 2;
2693 shaped.cost_value = 3;
2694 let uti = Uti {
2695 properties: vec![shaped],
2696 ..Uti::default()
2697 };
2698 let view = uti.snapshot(&mut cache);
2699
2700 let DecodedProperty::EnhancementBonus {
2701 property_id,
2702 cost_table,
2703 cost_value,
2704 ..
2705 } = &view.properties()[0]
2706 else {
2707 panic!("expected EnhancementBonus variant for `Enhancement` label");
2708 };
2709 assert_eq!(*property_id, 5);
2710 assert_eq!(*cost_table, 2);
2711 assert_eq!(*cost_value, 3);
2712 }
2713
2714 #[test]
2715 fn enhancement_bonus_does_not_match_its_conditional_siblings() {
2716 let table = itempropdef_with_labels(&[
2721 (6, "EnhancementAlignmentGroup"),
2722 (7, "EnhancementRacialGroup"),
2723 ]);
2724 let overrides = override_with_2da("itempropdef", &table);
2725 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2726 let mut cache = TwoDaCache::new(&resolver);
2727
2728 let uti = Uti {
2729 properties: vec![property(6, 0), property(7, 0)],
2730 ..Uti::default()
2731 };
2732 let view = uti.snapshot(&mut cache);
2733
2734 assert!(matches!(
2735 view.properties()[0],
2736 DecodedProperty::EnhancementAlignmentGroup { .. }
2737 ));
2738 assert!(matches!(
2739 view.properties()[1],
2740 DecodedProperty::EnhancementRacialGroup { .. }
2741 ));
2742 }
2743
2744 #[test]
2745 fn on_hit_label_routes_to_typed_variant_with_param_fields() {
2746 let table = itempropdef_with_labels(&[(32, "OnHit")]);
2751 let overrides = override_with_2da("itempropdef", &table);
2752 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2753 let mut cache = TwoDaCache::new(&resolver);
2754
2755 let mut shaped = property(32, 4);
2756 shaped.cost_table = 25;
2757 shaped.cost_value = 1;
2758 shaped.param1 = 1;
2759 shaped.param1_value = 7;
2760 let uti = Uti {
2761 properties: vec![shaped],
2762 ..Uti::default()
2763 };
2764 let view = uti.snapshot(&mut cache);
2765
2766 let DecodedProperty::OnHit {
2767 property_id,
2768 subtype_id,
2769 cost_table,
2770 cost_value,
2771 param1,
2772 param1_value,
2773 } = &view.properties()[0]
2774 else {
2775 panic!("expected OnHit variant for `OnHit` label");
2776 };
2777 assert_eq!(*property_id, 32);
2778 assert_eq!(*subtype_id, 4);
2779 assert_eq!(*cost_table, 25);
2780 assert_eq!(*cost_value, 1);
2781 assert_eq!(*param1, 1);
2782 assert_eq!(*param1_value, 7);
2783 }
2784
2785 #[test]
2786 fn on_hit_does_not_match_on_monster_hit() {
2787 let table = itempropdef_with_labels(&[(48, "OnMonsterHit")]);
2790 let overrides = override_with_2da("itempropdef", &table);
2791 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2792 let mut cache = TwoDaCache::new(&resolver);
2793
2794 let uti = Uti {
2795 properties: vec![property(48, 0)],
2796 ..Uti::default()
2797 };
2798 let view = uti.snapshot(&mut cache);
2799
2800 let DecodedProperty::Unknown { property_label, .. } = &view.properties()[0] else {
2801 panic!("expected Unknown variant for `OnMonsterHit`");
2802 };
2803 assert_eq!(property_label.as_deref(), Some("OnMonsterHit"));
2804 }
2805
2806 #[test]
2807 fn on_hit_subtype_label_resolves_via_iprp_onhit() {
2808 let propdef = itempropdef_with_subtypes(&[(32, "OnHit", "iprp_onhit")]);
2809 let onhit = subtype_2da(&[(0, "Sleep"), (1, "Daze"), (4, "Stun")]);
2810 let mut overrides = OverrideSource::new();
2811 add_2da_entry(&mut overrides, "itempropdef", &propdef);
2812 add_2da_entry(&mut overrides, "iprp_onhit", &onhit);
2813 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2814 let mut cache = TwoDaCache::new(&resolver);
2815
2816 let prop = DecodedProperty::OnHit {
2817 property_id: 32,
2818 subtype_id: 4,
2819 cost_table: 0,
2820 cost_value: 0,
2821 param1: 0,
2822 param1_value: 0,
2823 };
2824 assert_eq!(prop.subtype_label(&mut cache).as_deref(), Some("Stun"));
2825 }
2826
2827 #[test]
2828 fn ac_bonus_subtype_label_returns_none_for_subtypeless_property() {
2829 let propdef = itempropdef_with_subtypes(&[(1, "Armor", "")]);
2833 let overrides = override_with_2da("itempropdef", &propdef);
2834 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2835 let mut cache = TwoDaCache::new(&resolver);
2836
2837 let prop = DecodedProperty::AcBonus {
2838 property_id: 1,
2839 subtype_id: 0,
2840 cost_table: 2,
2841 cost_value: 5,
2842 };
2843 assert!(prop.subtype_label(&mut cache).is_none());
2844 }
2845
2846 fn active_property(property_name: u16, subtype: u16) -> UtiProperty {
2847 let mut p = property(property_name, subtype);
2851 p.useable = None;
2852 p.uses_per_day = None;
2853 p
2854 }
2855
2856 #[test]
2857 fn cast_spell_label_routes_to_typed_variant_with_active_defaults() {
2858 let table = itempropdef_with_labels(&[(10, "CastSpell")]);
2863 let overrides = override_with_2da("itempropdef", &table);
2864 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2865 let mut cache = TwoDaCache::new(&resolver);
2866
2867 let mut shaped = active_property(10, 42);
2868 shaped.cost_table = 3;
2869 shaped.cost_value = 7;
2870 let uti = Uti {
2871 properties: vec![shaped],
2872 ..Uti::default()
2873 };
2874 let view = uti.snapshot(&mut cache);
2875
2876 let DecodedProperty::CastSpell {
2877 property_id,
2878 subtype_id,
2879 cost_table,
2880 cost_value,
2881 useable,
2882 uses_per_day,
2883 } = &view.properties()[0]
2884 else {
2885 panic!("expected CastSpell variant for `CastSpell` label");
2886 };
2887 assert_eq!(*property_id, 10);
2888 assert_eq!(*subtype_id, 42);
2889 assert_eq!(*cost_table, 3);
2890 assert_eq!(*cost_value, 7);
2891 assert!(*useable);
2892 assert!(uses_per_day.is_none());
2893 }
2894
2895 #[test]
2896 fn cast_spell_preserves_explicit_useable_false() {
2897 let table = itempropdef_with_labels(&[(10, "CastSpell")]);
2901 let overrides = override_with_2da("itempropdef", &table);
2902 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2903 let mut cache = TwoDaCache::new(&resolver);
2904
2905 let mut shaped = active_property(10, 0);
2906 shaped.useable = Some(false);
2907 let uti = Uti {
2908 properties: vec![shaped],
2909 ..Uti::default()
2910 };
2911 let view = uti.snapshot(&mut cache);
2912
2913 let DecodedProperty::CastSpell { useable, .. } = &view.properties()[0] else {
2914 panic!("expected CastSpell variant");
2915 };
2916 assert!(!*useable);
2917 }
2918
2919 #[test]
2920 fn cast_spell_coalesces_uses_per_day_sentinel_into_none() {
2921 let table = itempropdef_with_labels(&[(10, "CastSpell")]);
2925 let overrides = override_with_2da("itempropdef", &table);
2926 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2927 let mut cache = TwoDaCache::new(&resolver);
2928
2929 let mut sentinel = active_property(10, 0);
2930 sentinel.uses_per_day = Some(0xFF);
2931 let mut explicit = active_property(10, 0);
2932 explicit.uses_per_day = Some(3);
2933 let uti = Uti {
2934 properties: vec![sentinel, explicit],
2935 ..Uti::default()
2936 };
2937 let view = uti.snapshot(&mut cache);
2938
2939 let DecodedProperty::CastSpell { uses_per_day, .. } = &view.properties()[0] else {
2940 panic!("expected CastSpell variant for sentinel");
2941 };
2942 assert!(uses_per_day.is_none());
2943
2944 let DecodedProperty::CastSpell { uses_per_day, .. } = &view.properties()[1] else {
2945 panic!("expected CastSpell variant for explicit cap");
2946 };
2947 assert_eq!(*uses_per_day, Some(3));
2948 }
2949
2950 #[test]
2951 fn cast_spell_subtype_label_resolves_via_spells_2da() {
2952 let propdef = itempropdef_with_subtypes(&[(10, "CastSpell", "spells")]);
2953 let spells = subtype_2da(&[(0, "Cure_Wounds"), (5, "Force_Push")]);
2954 let mut overrides = OverrideSource::new();
2955 add_2da_entry(&mut overrides, "itempropdef", &propdef);
2956 add_2da_entry(&mut overrides, "spells", &spells);
2957 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2958 let mut cache = TwoDaCache::new(&resolver);
2959
2960 let prop = DecodedProperty::CastSpell {
2961 property_id: 10,
2962 subtype_id: 5,
2963 cost_table: 0,
2964 cost_value: 0,
2965 useable: true,
2966 uses_per_day: None,
2967 };
2968 assert_eq!(
2969 prop.subtype_label(&mut cache).as_deref(),
2970 Some("Force_Push")
2971 );
2972 }
2973
2974 #[test]
2975 fn trap_label_routes_to_typed_variant() {
2976 let table = itempropdef_with_labels(&[(46, "Trap")]);
2977 let overrides = override_with_2da("itempropdef", &table);
2978 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2979 let mut cache = TwoDaCache::new(&resolver);
2980
2981 let mut shaped = active_property(46, 2);
2982 shaped.uses_per_day = Some(1);
2983 let uti = Uti {
2984 properties: vec![shaped],
2985 ..Uti::default()
2986 };
2987 let view = uti.snapshot(&mut cache);
2988
2989 let DecodedProperty::Trap {
2990 property_id,
2991 subtype_id,
2992 useable,
2993 uses_per_day,
2994 ..
2995 } = &view.properties()[0]
2996 else {
2997 panic!("expected Trap variant for `Trap` label");
2998 };
2999 assert_eq!(*property_id, 46);
3000 assert_eq!(*subtype_id, 2);
3001 assert!(*useable);
3002 assert_eq!(*uses_per_day, Some(1));
3003 }
3004
3005 #[test]
3006 fn trap_subtype_label_resolves_via_traps_2da() {
3007 let propdef = itempropdef_with_subtypes(&[(46, "Trap", "traps")]);
3008 let traps = subtype_2da(&[(0, "Minor_Frag"), (3, "Deadly_Plasma")]);
3009 let mut overrides = OverrideSource::new();
3010 add_2da_entry(&mut overrides, "itempropdef", &propdef);
3011 add_2da_entry(&mut overrides, "traps", &traps);
3012 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3013 let mut cache = TwoDaCache::new(&resolver);
3014
3015 let prop = DecodedProperty::Trap {
3016 property_id: 46,
3017 subtype_id: 3,
3018 cost_table: 0,
3019 cost_value: 0,
3020 useable: true,
3021 uses_per_day: Some(1),
3022 };
3023 assert_eq!(
3024 prop.subtype_label(&mut cache).as_deref(),
3025 Some("Deadly_Plasma")
3026 );
3027 }
3028
3029 #[test]
3030 fn thieves_tools_label_routes_to_typed_variant_with_active_defaults() {
3031 let table = itempropdef_with_labels(&[(37, "ThievesTools")]);
3036 let overrides = override_with_2da("itempropdef", &table);
3037 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3038 let mut cache = TwoDaCache::new(&resolver);
3039
3040 let mut shaped = active_property(37, 0);
3041 shaped.cost_table = 1;
3042 shaped.cost_value = 4;
3043 shaped.uses_per_day = Some(0xFF);
3044 let uti = Uti {
3045 properties: vec![shaped],
3046 ..Uti::default()
3047 };
3048 let view = uti.snapshot(&mut cache);
3049
3050 let DecodedProperty::ThievesTools {
3051 property_id,
3052 cost_table,
3053 cost_value,
3054 useable,
3055 uses_per_day,
3056 ..
3057 } = &view.properties()[0]
3058 else {
3059 panic!("expected ThievesTools variant for `ThievesTools` label");
3060 };
3061 assert_eq!(*property_id, 37);
3062 assert_eq!(*cost_table, 1);
3063 assert_eq!(*cost_value, 4);
3064 assert!(*useable);
3065 assert!(uses_per_day.is_none());
3066 }
3067
3068 #[test]
3069 fn thieves_tools_subtype_label_returns_none_for_subtypeless_property() {
3070 let propdef = itempropdef_with_subtypes(&[(37, "ThievesTools", "")]);
3074 let overrides = override_with_2da("itempropdef", &propdef);
3075 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3076 let mut cache = TwoDaCache::new(&resolver);
3077
3078 let prop = DecodedProperty::ThievesTools {
3079 property_id: 37,
3080 subtype_id: 0,
3081 cost_table: 1,
3082 cost_value: 4,
3083 useable: true,
3084 uses_per_day: None,
3085 };
3086 assert!(prop.subtype_label(&mut cache).is_none());
3087 }
3088
3089 #[test]
3090 fn computer_spike_label_routes_to_typed_variant() {
3091 let table = itempropdef_with_labels(&[(53, "Computer_Spike")]);
3097 let overrides = override_with_2da("itempropdef", &table);
3098 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3099 let mut cache = TwoDaCache::new(&resolver);
3100
3101 let mut shaped = active_property(53, 0);
3102 shaped.cost_table = 1;
3103 shaped.cost_value = 5;
3104 shaped.useable = Some(true);
3105 shaped.uses_per_day = Some(2);
3106 let uti = Uti {
3107 properties: vec![shaped],
3108 ..Uti::default()
3109 };
3110 let view = uti.snapshot(&mut cache);
3111
3112 let DecodedProperty::ComputerSpike {
3113 property_id,
3114 cost_table,
3115 cost_value,
3116 useable,
3117 uses_per_day,
3118 ..
3119 } = &view.properties()[0]
3120 else {
3121 panic!("expected ComputerSpike variant for `Computer_Spike` label");
3122 };
3123 assert_eq!(*property_id, 53);
3124 assert_eq!(*cost_table, 1);
3125 assert_eq!(*cost_value, 5);
3126 assert!(*useable);
3127 assert_eq!(*uses_per_day, Some(2));
3128 }
3129
3130 #[test]
3131 fn computer_spike_subtype_label_returns_none_for_subtypeless_property() {
3132 let propdef = itempropdef_with_subtypes(&[(53, "Computer_Spike", "")]);
3133 let overrides = override_with_2da("itempropdef", &propdef);
3134 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3135 let mut cache = TwoDaCache::new(&resolver);
3136
3137 let prop = DecodedProperty::ComputerSpike {
3138 property_id: 53,
3139 subtype_id: 0,
3140 cost_table: 1,
3141 cost_value: 5,
3142 useable: true,
3143 uses_per_day: Some(2),
3144 };
3145 assert!(prop.subtype_label(&mut cache).is_none());
3146 }
3147
3148 #[test]
3149 fn attack_bonus_label_routes_to_typed_variant() {
3150 let table = itempropdef_with_labels(&[(38, "AttackBonus")]);
3153 let overrides = override_with_2da("itempropdef", &table);
3154 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3155 let mut cache = TwoDaCache::new(&resolver);
3156
3157 let mut shaped = property(38, 0);
3158 shaped.cost_table = 2;
3159 shaped.cost_value = 3;
3160 let uti = Uti {
3161 properties: vec![shaped],
3162 ..Uti::default()
3163 };
3164 let view = uti.snapshot(&mut cache);
3165
3166 let DecodedProperty::AttackBonus {
3167 property_id,
3168 cost_table,
3169 cost_value,
3170 ..
3171 } = &view.properties()[0]
3172 else {
3173 panic!("expected AttackBonus variant for `AttackBonus` label");
3174 };
3175 assert_eq!(*property_id, 38);
3176 assert_eq!(*cost_table, 2);
3177 assert_eq!(*cost_value, 3);
3178 }
3179
3180 #[test]
3181 fn attack_bonus_does_not_match_its_conditional_siblings() {
3182 let table = itempropdef_with_labels(&[
3189 (39, "AttackBonusAlignmentGroup"),
3190 (40, "AttackBonusRacialGroup"),
3191 ]);
3192 let overrides = override_with_2da("itempropdef", &table);
3193 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3194 let mut cache = TwoDaCache::new(&resolver);
3195
3196 let uti = Uti {
3197 properties: vec![property(39, 0), property(40, 0)],
3198 ..Uti::default()
3199 };
3200 let view = uti.snapshot(&mut cache);
3201
3202 assert!(matches!(
3203 view.properties()[0],
3204 DecodedProperty::AttackBonusAlignmentGroup { .. }
3205 ));
3206 let DecodedProperty::Unknown { property_label, .. } = &view.properties()[1] else {
3207 panic!("expected Unknown for `AttackBonusRacialGroup`");
3208 };
3209 assert_eq!(property_label.as_deref(), Some("AttackBonusRacialGroup"));
3210 }
3211
3212 #[test]
3213 fn keen_label_routes_to_typed_variant() {
3214 let table = itempropdef_with_labels(&[(28, "Keen")]);
3215 let overrides = override_with_2da("itempropdef", &table);
3216 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3217 let mut cache = TwoDaCache::new(&resolver);
3218
3219 let uti = Uti {
3220 properties: vec![property(28, 0)],
3221 ..Uti::default()
3222 };
3223 let view = uti.snapshot(&mut cache);
3224
3225 let DecodedProperty::Keen { property_id, .. } = &view.properties()[0] else {
3226 panic!("expected Keen variant for `Keen` label");
3227 };
3228 assert_eq!(*property_id, 28);
3229 }
3230
3231 #[test]
3232 fn massive_criticals_label_routes_to_typed_variant() {
3233 let table = itempropdef_with_labels(&[(49, "Massive_Criticals")]);
3237 let overrides = override_with_2da("itempropdef", &table);
3238 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3239 let mut cache = TwoDaCache::new(&resolver);
3240
3241 let uti = Uti {
3242 properties: vec![property(49, 0)],
3243 ..Uti::default()
3244 };
3245 let view = uti.snapshot(&mut cache);
3246
3247 let DecodedProperty::MassiveCriticals { property_id, .. } = &view.properties()[0] else {
3248 panic!("expected MassiveCriticals variant for `Massive_Criticals` label");
3249 };
3250 assert_eq!(*property_id, 49);
3251 }
3252
3253 #[test]
3254 fn blaster_bolt_deflect_increase_label_routes_to_typed_variant() {
3255 let table = itempropdef_with_labels(&[(55, "Blaster_Bolt_Deflect_Increase")]);
3256 let overrides = override_with_2da("itempropdef", &table);
3257 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3258 let mut cache = TwoDaCache::new(&resolver);
3259
3260 let uti = Uti {
3261 properties: vec![property(55, 0)],
3262 ..Uti::default()
3263 };
3264 let view = uti.snapshot(&mut cache);
3265
3266 let DecodedProperty::BlasterBoltDeflectIncrease { property_id, .. } = &view.properties()[0]
3267 else {
3268 panic!("expected BlasterBoltDeflectIncrease variant for the vanilla label");
3269 };
3270 assert_eq!(*property_id, 55);
3271 }
3272
3273 #[test]
3274 fn blaster_bolt_deflect_does_not_absorb_vanilla_typo_sibling() {
3275 let table = itempropdef_with_labels(&[(56, "Blaster_Bolt_Defect_Decrease")]);
3282 let overrides = override_with_2da("itempropdef", &table);
3283 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3284 let mut cache = TwoDaCache::new(&resolver);
3285
3286 let uti = Uti {
3287 properties: vec![property(56, 0)],
3288 ..Uti::default()
3289 };
3290 let view = uti.snapshot(&mut cache);
3291
3292 let DecodedProperty::Unknown { property_label, .. } = &view.properties()[0] else {
3293 panic!("expected Unknown for the vanilla-typo sibling");
3294 };
3295 assert_eq!(
3296 property_label.as_deref(),
3297 Some("Blaster_Bolt_Defect_Decrease")
3298 );
3299 }
3300
3301 #[test]
3302 fn monster_damage_label_routes_to_typed_variant() {
3303 let table = itempropdef_with_labels(&[(51, "Monster_damage")]);
3306 let overrides = override_with_2da("itempropdef", &table);
3307 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3308 let mut cache = TwoDaCache::new(&resolver);
3309
3310 let uti = Uti {
3311 properties: vec![property(51, 0)],
3312 ..Uti::default()
3313 };
3314 let view = uti.snapshot(&mut cache);
3315
3316 let DecodedProperty::MonsterDamage { property_id, .. } = &view.properties()[0] else {
3317 panic!("expected MonsterDamage variant for `Monster_damage` label");
3318 };
3319 assert_eq!(*property_id, 51);
3320 }
3321
3322 #[test]
3323 fn subtypeless_single_magnitude_variants_short_circuit_subtype_label() {
3324 let propdef = itempropdef_with_subtypes(&[
3328 (28, "Keen", ""),
3329 (38, "AttackBonus", ""),
3330 (49, "Massive_Criticals", ""),
3331 (51, "Monster_damage", ""),
3332 (55, "Blaster_Bolt_Deflect_Increase", ""),
3333 ]);
3334 let overrides = override_with_2da("itempropdef", &propdef);
3335 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3336 let mut cache = TwoDaCache::new(&resolver);
3337
3338 for prop in [
3339 DecodedProperty::Keen {
3340 property_id: 28,
3341 subtype_id: 0,
3342 cost_table: 0,
3343 cost_value: 0,
3344 },
3345 DecodedProperty::AttackBonus {
3346 property_id: 38,
3347 subtype_id: 0,
3348 cost_table: 0,
3349 cost_value: 0,
3350 },
3351 DecodedProperty::MassiveCriticals {
3352 property_id: 49,
3353 subtype_id: 0,
3354 cost_table: 0,
3355 cost_value: 0,
3356 },
3357 DecodedProperty::MonsterDamage {
3358 property_id: 51,
3359 subtype_id: 0,
3360 cost_table: 0,
3361 cost_value: 0,
3362 },
3363 DecodedProperty::BlasterBoltDeflectIncrease {
3364 property_id: 55,
3365 subtype_id: 0,
3366 cost_table: 0,
3367 cost_value: 0,
3368 },
3369 ] {
3370 assert!(
3371 prop.subtype_label(&mut cache).is_none(),
3372 "expected None subtype_label for subtypeless variant, got {prop:?}"
3373 );
3374 }
3375 }
3376
3377 #[test]
3378 fn bonus_feats_label_routes_to_typed_variant() {
3379 let table = itempropdef_with_labels(&[(9, "BonusFeats")]);
3380 let overrides = override_with_2da("itempropdef", &table);
3381 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3382 let mut cache = TwoDaCache::new(&resolver);
3383
3384 let uti = Uti {
3385 properties: vec![property(9, 12)],
3386 ..Uti::default()
3387 };
3388 let view = uti.snapshot(&mut cache);
3389
3390 let DecodedProperty::BonusFeats {
3391 property_id,
3392 subtype_id,
3393 ..
3394 } = &view.properties()[0]
3395 else {
3396 panic!("expected BonusFeats variant for `BonusFeats` label");
3397 };
3398 assert_eq!(*property_id, 9);
3399 assert_eq!(*subtype_id, 12);
3400 }
3401
3402 #[test]
3403 fn immunity_label_routes_to_typed_variant() {
3404 let table = itempropdef_with_labels(&[(24, "Immunity")]);
3405 let overrides = override_with_2da("itempropdef", &table);
3406 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3407 let mut cache = TwoDaCache::new(&resolver);
3408
3409 let uti = Uti {
3410 properties: vec![property(24, 5)],
3411 ..Uti::default()
3412 };
3413 let view = uti.snapshot(&mut cache);
3414
3415 let DecodedProperty::Immunity {
3416 property_id,
3417 subtype_id,
3418 ..
3419 } = &view.properties()[0]
3420 else {
3421 panic!("expected Immunity variant for `Immunity` label");
3422 };
3423 assert_eq!(*property_id, 24);
3424 assert_eq!(*subtype_id, 5);
3425 }
3426
3427 #[test]
3428 fn skill_label_routes_to_typed_variant() {
3429 let table = itempropdef_with_labels(&[(36, "Skill")]);
3430 let overrides = override_with_2da("itempropdef", &table);
3431 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3432 let mut cache = TwoDaCache::new(&resolver);
3433
3434 let uti = Uti {
3435 properties: vec![property(36, 2)],
3436 ..Uti::default()
3437 };
3438 let view = uti.snapshot(&mut cache);
3439
3440 let DecodedProperty::Skill {
3441 property_id,
3442 subtype_id,
3443 ..
3444 } = &view.properties()[0]
3445 else {
3446 panic!("expected Skill variant for `Skill` label");
3447 };
3448 assert_eq!(*property_id, 36);
3449 assert_eq!(*subtype_id, 2);
3450 }
3451
3452 #[test]
3453 fn skill_does_not_match_decreased_skill_sibling() {
3454 let table = itempropdef_with_labels(&[(21, "DecreasedSkill")]);
3459 let overrides = override_with_2da("itempropdef", &table);
3460 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3461 let mut cache = TwoDaCache::new(&resolver);
3462
3463 let uti = Uti {
3464 properties: vec![property(21, 0)],
3465 ..Uti::default()
3466 };
3467 let view = uti.snapshot(&mut cache);
3468
3469 let DecodedProperty::Unknown { property_label, .. } = &view.properties()[0] else {
3470 panic!("expected Unknown for `DecreasedSkill`");
3471 };
3472 assert_eq!(property_label.as_deref(), Some("DecreasedSkill"));
3473 }
3474
3475 #[test]
3476 fn misc_2da_backed_singleton_subtype_labels_resolve_via_their_2das() {
3477 let propdef = itempropdef_with_subtypes(&[
3481 (9, "BonusFeats", "feat"),
3482 (24, "Immunity", "iprp_immunity"),
3483 (36, "Skill", "skills"),
3484 ]);
3485 let feats = subtype_2da(&[(0, "Toughness"), (12, "Force_Sensitive")]);
3486 let immunities = subtype_2da(&[(0, "Mind_Affecting"), (5, "Paralysis")]);
3487 let skills = subtype_2da(&[(0, "Computer_Use"), (2, "Persuade")]);
3488
3489 let mut overrides = OverrideSource::new();
3490 add_2da_entry(&mut overrides, "itempropdef", &propdef);
3491 add_2da_entry(&mut overrides, "feat", &feats);
3492 add_2da_entry(&mut overrides, "iprp_immunity", &immunities);
3493 add_2da_entry(&mut overrides, "skills", &skills);
3494 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3495 let mut cache = TwoDaCache::new(&resolver);
3496
3497 let feat = DecodedProperty::BonusFeats {
3498 property_id: 9,
3499 subtype_id: 12,
3500 cost_table: 0,
3501 cost_value: 0,
3502 };
3503 assert_eq!(
3504 feat.subtype_label(&mut cache).as_deref(),
3505 Some("Force_Sensitive")
3506 );
3507
3508 let immunity = DecodedProperty::Immunity {
3509 property_id: 24,
3510 subtype_id: 5,
3511 cost_table: 0,
3512 cost_value: 0,
3513 };
3514 assert_eq!(
3515 immunity.subtype_label(&mut cache).as_deref(),
3516 Some("Paralysis")
3517 );
3518
3519 let skill = DecodedProperty::Skill {
3520 property_id: 36,
3521 subtype_id: 2,
3522 cost_table: 0,
3523 cost_value: 0,
3524 };
3525 assert_eq!(skill.subtype_label(&mut cache).as_deref(), Some("Persuade"));
3526 }
3527
3528 #[test]
3529 fn use_limitation_feat_label_routes_to_typed_variant() {
3530 let table = itempropdef_with_labels(&[(57, "Use_Limitation_Feat")]);
3535 let overrides = override_with_2da("itempropdef", &table);
3536 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3537 let mut cache = TwoDaCache::new(&resolver);
3538
3539 let uti = Uti {
3540 properties: vec![property(57, 42)],
3541 ..Uti::default()
3542 };
3543 let view = uti.snapshot(&mut cache);
3544
3545 let DecodedProperty::UseLimitationFeat {
3546 property_id,
3547 subtype_id,
3548 ..
3549 } = &view.properties()[0]
3550 else {
3551 panic!("expected UseLimitationFeat variant for `Use_Limitation_Feat` label");
3552 };
3553 assert_eq!(*property_id, 57);
3554 assert_eq!(*subtype_id, 42);
3555 }
3556
3557 #[test]
3558 fn use_limitation_racial_label_routes_to_typed_variant() {
3559 let table = itempropdef_with_labels(&[(45, "UseLimitationRacial")]);
3560 let overrides = override_with_2da("itempropdef", &table);
3561 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3562 let mut cache = TwoDaCache::new(&resolver);
3563
3564 let uti = Uti {
3565 properties: vec![property(45, 3)],
3566 ..Uti::default()
3567 };
3568 let view = uti.snapshot(&mut cache);
3569
3570 let DecodedProperty::UseLimitationRacial {
3571 property_id,
3572 subtype_id,
3573 ..
3574 } = &view.properties()[0]
3575 else {
3576 panic!("expected UseLimitationRacial variant for `UseLimitationRacial` label");
3577 };
3578 assert_eq!(*property_id, 45);
3579 assert_eq!(*subtype_id, 3);
3580 }
3581
3582 #[test]
3583 fn use_limitation_alignment_group_label_routes_to_typed_variant() {
3584 let table = itempropdef_with_labels(&[(43, "UseLimitationAlignmentGroup")]);
3585 let overrides = override_with_2da("itempropdef", &table);
3586 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3587 let mut cache = TwoDaCache::new(&resolver);
3588
3589 let uti = Uti {
3590 properties: vec![property(43, 2)],
3591 ..Uti::default()
3592 };
3593 let view = uti.snapshot(&mut cache);
3594
3595 let DecodedProperty::UseLimitationAlignmentGroup {
3596 property_id,
3597 subtype_id,
3598 ..
3599 } = &view.properties()[0]
3600 else {
3601 panic!(
3602 "expected UseLimitationAlignmentGroup variant for \
3603 `UseLimitationAlignmentGroup` label"
3604 );
3605 };
3606 assert_eq!(*property_id, 43);
3607 assert_eq!(*subtype_id, 2);
3608 }
3609
3610 #[test]
3611 fn use_limitation_does_not_match_unused_class_sibling() {
3612 let table = itempropdef_with_labels(&[(44, "UseLimitationClass")]);
3617 let overrides = override_with_2da("itempropdef", &table);
3618 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3619 let mut cache = TwoDaCache::new(&resolver);
3620
3621 let uti = Uti {
3622 properties: vec![property(44, 0)],
3623 ..Uti::default()
3624 };
3625 let view = uti.snapshot(&mut cache);
3626
3627 let DecodedProperty::Unknown { property_label, .. } = &view.properties()[0] else {
3628 panic!("expected Unknown variant for `UseLimitationClass`");
3629 };
3630 assert_eq!(property_label.as_deref(), Some("UseLimitationClass"));
3631 }
3632
3633 #[test]
3634 fn damage_racial_group_label_routes_to_typed_variant() {
3635 let table = itempropdef_with_labels(&[(13, "DamageRacialGroup")]);
3636 let overrides = override_with_2da("itempropdef", &table);
3637 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3638 let mut cache = TwoDaCache::new(&resolver);
3639
3640 let mut shaped = property(13, 3);
3641 shaped.cost_table = 4;
3642 shaped.cost_value = 2;
3643 let uti = Uti {
3644 properties: vec![shaped],
3645 ..Uti::default()
3646 };
3647 let view = uti.snapshot(&mut cache);
3648
3649 let DecodedProperty::DamageRacialGroup {
3650 property_id,
3651 subtype_id,
3652 cost_table,
3653 cost_value,
3654 } = &view.properties()[0]
3655 else {
3656 panic!("expected DamageRacialGroup variant for `DamageRacialGroup` label");
3657 };
3658 assert_eq!(*property_id, 13);
3659 assert_eq!(*subtype_id, 3);
3660 assert_eq!(*cost_table, 4);
3661 assert_eq!(*cost_value, 2);
3662 }
3663
3664 #[test]
3665 fn damage_alignment_group_label_routes_to_typed_variant() {
3666 let table = itempropdef_with_labels(&[(12, "DamageAlignmentGroup")]);
3667 let overrides = override_with_2da("itempropdef", &table);
3668 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3669 let mut cache = TwoDaCache::new(&resolver);
3670
3671 let uti = Uti {
3672 properties: vec![property(12, 4)],
3673 ..Uti::default()
3674 };
3675 let view = uti.snapshot(&mut cache);
3676
3677 let DecodedProperty::DamageAlignmentGroup {
3678 property_id,
3679 subtype_id,
3680 ..
3681 } = &view.properties()[0]
3682 else {
3683 panic!("expected DamageAlignmentGroup variant for `DamageAlignmentGroup` label");
3684 };
3685 assert_eq!(*property_id, 12);
3686 assert_eq!(*subtype_id, 4);
3687 }
3688
3689 #[test]
3690 fn enhancement_racial_group_label_routes_to_typed_variant() {
3691 let table = itempropdef_with_labels(&[(7, "EnhancementRacialGroup")]);
3692 let overrides = override_with_2da("itempropdef", &table);
3693 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3694 let mut cache = TwoDaCache::new(&resolver);
3695
3696 let uti = Uti {
3697 properties: vec![property(7, 3)],
3698 ..Uti::default()
3699 };
3700 let view = uti.snapshot(&mut cache);
3701
3702 let DecodedProperty::EnhancementRacialGroup {
3703 property_id,
3704 subtype_id,
3705 ..
3706 } = &view.properties()[0]
3707 else {
3708 panic!("expected EnhancementRacialGroup variant for `EnhancementRacialGroup` label");
3709 };
3710 assert_eq!(*property_id, 7);
3711 assert_eq!(*subtype_id, 3);
3712 }
3713
3714 #[test]
3715 fn conditional_bonus_family_subtype_labels_resolve_via_their_2das() {
3716 let propdef = itempropdef_with_subtypes(&[
3721 (7, "EnhancementRacialGroup", "racialtypes"),
3722 (12, "DamageAlignmentGroup", "iprp_aligngrp"),
3723 (13, "DamageRacialGroup", "racialtypes"),
3724 ]);
3725 let racial = subtype_2da(&[(0, "Human"), (3, "Wookiee")]);
3726 let aligngrp = subtype_2da(&[(0, "Lawful_Good"), (4, "Chaotic_Evil")]);
3727
3728 let mut overrides = OverrideSource::new();
3729 add_2da_entry(&mut overrides, "itempropdef", &propdef);
3730 add_2da_entry(&mut overrides, "racialtypes", &racial);
3731 add_2da_entry(&mut overrides, "iprp_aligngrp", &aligngrp);
3732 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3733 let mut cache = TwoDaCache::new(&resolver);
3734
3735 let damage_racial = DecodedProperty::DamageRacialGroup {
3736 property_id: 13,
3737 subtype_id: 3,
3738 cost_table: 0,
3739 cost_value: 0,
3740 };
3741 assert_eq!(
3742 damage_racial.subtype_label(&mut cache).as_deref(),
3743 Some("Wookiee")
3744 );
3745
3746 let damage_alignment = DecodedProperty::DamageAlignmentGroup {
3747 property_id: 12,
3748 subtype_id: 4,
3749 cost_table: 0,
3750 cost_value: 0,
3751 };
3752 assert_eq!(
3753 damage_alignment.subtype_label(&mut cache).as_deref(),
3754 Some("Chaotic_Evil")
3755 );
3756
3757 let enhancement_racial = DecodedProperty::EnhancementRacialGroup {
3758 property_id: 7,
3759 subtype_id: 3,
3760 cost_table: 0,
3761 cost_value: 0,
3762 };
3763 assert_eq!(
3764 enhancement_racial.subtype_label(&mut cache).as_deref(),
3765 Some("Wookiee")
3766 );
3767 }
3768
3769 #[test]
3770 fn use_limitation_family_subtype_labels_resolve_via_their_2das() {
3771 let propdef = itempropdef_with_subtypes(&[
3775 (43, "UseLimitationAlignmentGroup", "iprp_aligngrp"),
3776 (45, "UseLimitationRacial", "racialtypes"),
3777 (57, "Use_Limitation_Feat", "feat"),
3778 ]);
3779 let aligngrp = subtype_2da(&[(0, "Lawful_Good"), (4, "Chaotic_Evil")]);
3780 let racial = subtype_2da(&[(0, "Human"), (3, "Wookiee")]);
3781 let feats = subtype_2da(&[(0, "Toughness"), (42, "Force_Sensitive")]);
3782
3783 let mut overrides = OverrideSource::new();
3784 add_2da_entry(&mut overrides, "itempropdef", &propdef);
3785 add_2da_entry(&mut overrides, "iprp_aligngrp", &aligngrp);
3786 add_2da_entry(&mut overrides, "racialtypes", &racial);
3787 add_2da_entry(&mut overrides, "feat", &feats);
3788 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3789 let mut cache = TwoDaCache::new(&resolver);
3790
3791 let alignment = DecodedProperty::UseLimitationAlignmentGroup {
3792 property_id: 43,
3793 subtype_id: 4,
3794 cost_table: 0,
3795 cost_value: 0,
3796 };
3797 assert_eq!(
3798 alignment.subtype_label(&mut cache).as_deref(),
3799 Some("Chaotic_Evil")
3800 );
3801
3802 let racial = DecodedProperty::UseLimitationRacial {
3803 property_id: 45,
3804 subtype_id: 3,
3805 cost_table: 0,
3806 cost_value: 0,
3807 };
3808 assert_eq!(racial.subtype_label(&mut cache).as_deref(), Some("Wookiee"));
3809
3810 let feat = DecodedProperty::UseLimitationFeat {
3811 property_id: 57,
3812 subtype_id: 42,
3813 cost_table: 0,
3814 cost_value: 0,
3815 };
3816 assert_eq!(
3817 feat.subtype_label(&mut cache).as_deref(),
3818 Some("Force_Sensitive")
3819 );
3820 }
3821
3822 #[test]
3823 fn low_volume_catchall_variants_route_to_typed_variants() {
3824 let table = itempropdef_with_labels(&[
3830 (8, "AttackPenalty"),
3831 (15, "DamagePenalty"),
3832 (25, "ImprovedMagicResist"),
3833 (31, "DamageNone"),
3834 (35, "Regeneration"),
3835 (54, "Regeneration_Force_Points"),
3836 (59, "Disguise"),
3837 ]);
3838 let overrides = override_with_2da("itempropdef", &table);
3839 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3840 let mut cache = TwoDaCache::new(&resolver);
3841
3842 let uti = Uti {
3843 properties: vec![
3844 property(8, 0),
3845 property(15, 0),
3846 property(25, 0),
3847 property(31, 0),
3848 property(35, 0),
3849 property(54, 0),
3850 property(59, 7),
3851 ],
3852 ..Uti::default()
3853 };
3854 let view = uti.snapshot(&mut cache);
3855
3856 assert!(matches!(
3857 view.properties()[0],
3858 DecodedProperty::AttackPenalty { .. }
3859 ));
3860 assert!(matches!(
3861 view.properties()[1],
3862 DecodedProperty::DamagePenalty { .. }
3863 ));
3864 assert!(matches!(
3865 view.properties()[2],
3866 DecodedProperty::MagicResistBonus { .. }
3867 ));
3868 assert!(matches!(
3869 view.properties()[3],
3870 DecodedProperty::DamageNone { .. }
3871 ));
3872 assert!(matches!(
3873 view.properties()[4],
3874 DecodedProperty::Regeneration { .. }
3875 ));
3876 assert!(matches!(
3877 view.properties()[5],
3878 DecodedProperty::RegenerationForcePoints { .. }
3879 ));
3880 let DecodedProperty::Disguise { subtype_id, .. } = &view.properties()[6] else {
3881 panic!("expected Disguise variant for `Disguise` label");
3882 };
3883 assert_eq!(*subtype_id, 7);
3884 }
3885
3886 #[test]
3887 fn low_volume_subtypeless_variants_short_circuit_subtype_label() {
3888 let propdef = itempropdef_with_subtypes(&[
3891 (8, "AttackPenalty", ""),
3892 (15, "DamagePenalty", ""),
3893 (25, "ImprovedMagicResist", ""),
3894 (31, "DamageNone", ""),
3895 (35, "Regeneration", ""),
3896 (54, "Regeneration_Force_Points", ""),
3897 ]);
3898 let overrides = override_with_2da("itempropdef", &propdef);
3899 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3900 let mut cache = TwoDaCache::new(&resolver);
3901
3902 for prop in [
3903 DecodedProperty::AttackPenalty {
3904 property_id: 8,
3905 subtype_id: 0,
3906 cost_table: 0,
3907 cost_value: 0,
3908 },
3909 DecodedProperty::DamagePenalty {
3910 property_id: 15,
3911 subtype_id: 0,
3912 cost_table: 0,
3913 cost_value: 0,
3914 },
3915 DecodedProperty::MagicResistBonus {
3916 property_id: 25,
3917 subtype_id: 0,
3918 cost_table: 0,
3919 cost_value: 0,
3920 },
3921 DecodedProperty::DamageNone {
3922 property_id: 31,
3923 subtype_id: 0,
3924 cost_table: 0,
3925 cost_value: 0,
3926 },
3927 DecodedProperty::Regeneration {
3928 property_id: 35,
3929 subtype_id: 0,
3930 cost_table: 0,
3931 cost_value: 0,
3932 },
3933 DecodedProperty::RegenerationForcePoints {
3934 property_id: 54,
3935 subtype_id: 0,
3936 cost_table: 0,
3937 cost_value: 0,
3938 },
3939 ] {
3940 assert!(
3941 prop.subtype_label(&mut cache).is_none(),
3942 "expected None subtype_label for subtypeless variant, got {prop:?}"
3943 );
3944 }
3945 }
3946
3947 #[test]
3948 fn disguise_subtype_label_resolves_via_appearance_2da() {
3949 let propdef = itempropdef_with_subtypes(&[(59, "Disguise", "appearance")]);
3950 let appearance = subtype_2da(&[(0, "Human"), (7, "Tusken_Raider")]);
3951 let mut overrides = OverrideSource::new();
3952 add_2da_entry(&mut overrides, "itempropdef", &propdef);
3953 add_2da_entry(&mut overrides, "appearance", &appearance);
3954 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3955 let mut cache = TwoDaCache::new(&resolver);
3956
3957 let prop = DecodedProperty::Disguise {
3958 property_id: 59,
3959 subtype_id: 7,
3960 cost_table: 0,
3961 cost_value: 0,
3962 };
3963 assert_eq!(
3964 prop.subtype_label(&mut cache).as_deref(),
3965 Some("Tusken_Raider")
3966 );
3967 }
3968
3969 #[test]
3970 fn newly_visible_alignment_and_special_variants_route_to_typed() {
3971 let table = itempropdef_with_labels(&[
3979 (6, "EnhancementAlignmentGroup"),
3980 (29, "Light"),
3981 (39, "AttackBonusAlignmentGroup"),
3982 (47, "True_Seeing"),
3983 ]);
3984 let overrides = override_with_2da("itempropdef", &table);
3985 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3986 let mut cache = TwoDaCache::new(&resolver);
3987
3988 let mut light_prop = property(29, 0);
3989 light_prop.param1 = 9;
3990 light_prop.param1_value = 4;
3991 let uti = Uti {
3992 properties: vec![property(6, 2), light_prop, property(39, 3), property(47, 0)],
3993 ..Uti::default()
3994 };
3995 let view = uti.snapshot(&mut cache);
3996
3997 let DecodedProperty::EnhancementAlignmentGroup {
3998 subtype_id: ealignment_subtype,
3999 ..
4000 } = &view.properties()[0]
4001 else {
4002 panic!("expected EnhancementAlignmentGroup for row 6");
4003 };
4004 assert_eq!(*ealignment_subtype, 2);
4005
4006 let DecodedProperty::Light {
4007 param1,
4008 param1_value,
4009 ..
4010 } = &view.properties()[1]
4011 else {
4012 panic!("expected Light for row 29");
4013 };
4014 assert_eq!(*param1, 9);
4015 assert_eq!(*param1_value, 4);
4016
4017 let DecodedProperty::AttackBonusAlignmentGroup {
4018 subtype_id: ab_alignment_subtype,
4019 ..
4020 } = &view.properties()[2]
4021 else {
4022 panic!("expected AttackBonusAlignmentGroup for row 39");
4023 };
4024 assert_eq!(*ab_alignment_subtype, 3);
4025
4026 assert!(matches!(
4027 view.properties()[3],
4028 DecodedProperty::TrueSeeing { .. }
4029 ));
4030 }
4031
4032 #[test]
4033 fn alignment_group_variants_subtype_labels_resolve_via_iprp_aligngrp() {
4034 let propdef = itempropdef_with_subtypes(&[
4040 (6, "EnhancementAlignmentGroup", "iprp_aligngrp"),
4041 (39, "AttackBonusAlignmentGroup", "iprp_aligngrp"),
4042 ]);
4043 let aligngrp = subtype_2da(&[(0, "Lawful_Good"), (2, "Neutral"), (4, "Chaotic_Evil")]);
4044
4045 let mut overrides = OverrideSource::new();
4046 add_2da_entry(&mut overrides, "itempropdef", &propdef);
4047 add_2da_entry(&mut overrides, "iprp_aligngrp", &aligngrp);
4048 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
4049 let mut cache = TwoDaCache::new(&resolver);
4050
4051 let enhancement = DecodedProperty::EnhancementAlignmentGroup {
4052 property_id: 6,
4053 subtype_id: 4,
4054 cost_table: 0,
4055 cost_value: 0,
4056 };
4057 assert_eq!(
4058 enhancement.subtype_label(&mut cache).as_deref(),
4059 Some("Chaotic_Evil")
4060 );
4061
4062 let attack = DecodedProperty::AttackBonusAlignmentGroup {
4063 property_id: 39,
4064 subtype_id: 0,
4065 cost_table: 0,
4066 cost_value: 0,
4067 };
4068 assert_eq!(
4069 attack.subtype_label(&mut cache).as_deref(),
4070 Some("Lawful_Good")
4071 );
4072 }
4073
4074 #[test]
4075 fn light_and_true_seeing_short_circuit_subtype_label() {
4076 let propdef = itempropdef_with_subtypes(&[(29, "Light", ""), (47, "True_Seeing", "")]);
4079 let overrides = override_with_2da("itempropdef", &propdef);
4080 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
4081 let mut cache = TwoDaCache::new(&resolver);
4082
4083 let light = DecodedProperty::Light {
4084 property_id: 29,
4085 subtype_id: 0,
4086 cost_table: 0,
4087 cost_value: 0,
4088 param1: 9,
4089 param1_value: 4,
4090 };
4091 assert!(light.subtype_label(&mut cache).is_none());
4092
4093 let true_seeing = DecodedProperty::TrueSeeing {
4094 property_id: 47,
4095 subtype_id: 0,
4096 cost_table: 0,
4097 cost_value: 0,
4098 };
4099 assert!(true_seeing.subtype_label(&mut cache).is_none());
4100 }
4101
4102 #[test]
4103 fn deferred_zero_use_rows_stay_in_unknown_by_design() {
4104 let table = itempropdef_with_labels(&[
4112 (2, "ArmorAlignmentGroup"),
4113 (16, "DamageReduced"),
4114 (19, "DecreaseAbilityScore"),
4115 (22, "DamageMelee"),
4116 (40, "AttackBonusRacialGroup"),
4117 (44, "UseLimitationClass"),
4118 (48, "OnMonsterHit"),
4119 (56, "Blaster_Bolt_Defect_Decrease"),
4120 ]);
4121 let overrides = override_with_2da("itempropdef", &table);
4122 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
4123 let mut cache = TwoDaCache::new(&resolver);
4124
4125 let uti = Uti {
4126 properties: vec![
4127 property(2, 0),
4128 property(16, 0),
4129 property(19, 0),
4130 property(22, 0),
4131 property(40, 0),
4132 property(44, 0),
4133 property(48, 0),
4134 property(56, 0),
4135 ],
4136 ..Uti::default()
4137 };
4138 let view = uti.snapshot(&mut cache);
4139
4140 for prop in view.properties() {
4141 assert!(
4142 matches!(prop, DecodedProperty::Unknown { .. }),
4143 "expected Unknown for deferred row, got {prop:?}"
4144 );
4145 }
4146 }
4147
4148 #[test]
4149 fn properties_accessor_preserves_source_order() {
4150 let uti = Uti {
4151 properties: vec![property(7, 0), property(0, 0), property(45, 0)],
4152 ..Uti::default()
4153 };
4154 let resolver = Resolver::new();
4155 let mut cache = TwoDaCache::new(&resolver);
4156
4157 let view = uti.snapshot(&mut cache);
4158 let ids: Vec<u16> = view
4159 .properties()
4160 .iter()
4161 .map(|prop| match prop {
4162 DecodedProperty::AbilityBonus { property_id, .. }
4163 | DecodedProperty::SaveBonus { property_id, .. }
4164 | DecodedProperty::SaveBonusSpecific { property_id, .. }
4165 | DecodedProperty::SavePenalty { property_id, .. }
4166 | DecodedProperty::SavePenaltySpecific { property_id, .. }
4167 | DecodedProperty::DamageBonus { property_id, .. }
4168 | DecodedProperty::DamageImmunity { property_id, .. }
4169 | DecodedProperty::DamageResistance { property_id, .. }
4170 | DecodedProperty::AcBonus { property_id, .. }
4171 | DecodedProperty::EnhancementBonus { property_id, .. }
4172 | DecodedProperty::OnHit { property_id, .. }
4173 | DecodedProperty::CastSpell { property_id, .. }
4174 | DecodedProperty::Trap { property_id, .. }
4175 | DecodedProperty::ThievesTools { property_id, .. }
4176 | DecodedProperty::ComputerSpike { property_id, .. }
4177 | DecodedProperty::UseLimitationFeat { property_id, .. }
4178 | DecodedProperty::UseLimitationRacial { property_id, .. }
4179 | DecodedProperty::UseLimitationAlignmentGroup { property_id, .. }
4180 | DecodedProperty::DamageRacialGroup { property_id, .. }
4181 | DecodedProperty::DamageAlignmentGroup { property_id, .. }
4182 | DecodedProperty::EnhancementRacialGroup { property_id, .. }
4183 | DecodedProperty::EnhancementAlignmentGroup { property_id, .. }
4184 | DecodedProperty::AttackBonusAlignmentGroup { property_id, .. }
4185 | DecodedProperty::TrueSeeing { property_id, .. }
4186 | DecodedProperty::Light { property_id, .. }
4187 | DecodedProperty::AttackBonus { property_id, .. }
4188 | DecodedProperty::Keen { property_id, .. }
4189 | DecodedProperty::MassiveCriticals { property_id, .. }
4190 | DecodedProperty::BlasterBoltDeflectIncrease { property_id, .. }
4191 | DecodedProperty::MonsterDamage { property_id, .. }
4192 | DecodedProperty::BonusFeats { property_id, .. }
4193 | DecodedProperty::Immunity { property_id, .. }
4194 | DecodedProperty::Skill { property_id, .. }
4195 | DecodedProperty::AttackPenalty { property_id, .. }
4196 | DecodedProperty::DamagePenalty { property_id, .. }
4197 | DecodedProperty::MagicResistBonus { property_id, .. }
4198 | DecodedProperty::DamageNone { property_id, .. }
4199 | DecodedProperty::Regeneration { property_id, .. }
4200 | DecodedProperty::RegenerationForcePoints { property_id, .. }
4201 | DecodedProperty::Disguise { property_id, .. }
4202 | DecodedProperty::Unknown { property_id, .. } => *property_id,
4203 })
4204 .collect();
4205 assert_eq!(ids, vec![7, 0, 45]);
4206 }
4207
4208 #[test]
4209 fn is_armor_delegates_to_source_uti_for_armor_base_item() {
4210 let uti = Uti {
4212 base_item: 35,
4213 ..Uti::default()
4214 };
4215 let resolver = Resolver::new();
4216 let mut cache = TwoDaCache::new(&resolver);
4217
4218 let view = uti.snapshot(&mut cache);
4219 assert!(view.is_armor());
4220 assert_eq!(view.is_armor(), uti.is_armor());
4222 }
4223
4224 #[test]
4225 fn is_armor_returns_false_for_non_armor_base_item() {
4226 let uti = Uti {
4228 base_item: 0,
4229 ..Uti::default()
4230 };
4231 let resolver = Resolver::new();
4232 let mut cache = TwoDaCache::new(&resolver);
4233
4234 let view = uti.snapshot(&mut cache);
4235 assert!(!view.is_armor());
4236 }
4237
4238 fn baseitems_with_rows(rows: &[(usize, &str, &str, &str, &str)]) -> TwoDa {
4243 let max_row = rows.iter().map(|(idx, ..)| *idx).max().unwrap_or(0);
4244 let mut table_rows = Vec::with_capacity(max_row + 1);
4245 for row_index in 0..=max_row {
4246 let cells = rows
4247 .iter()
4248 .find(|(idx, ..)| *idx == row_index)
4249 .map(|(_, ww, st, eq, mt)| {
4250 vec![
4251 (*ww).to_string(),
4252 (*st).to_string(),
4253 (*eq).to_string(),
4254 (*mt).to_string(),
4255 ]
4256 })
4257 .unwrap_or_else(|| vec![String::new(); 4]);
4258 table_rows.push(TwoDaRow {
4259 label: row_index.to_string(),
4260 cells,
4261 });
4262 }
4263 TwoDa {
4264 headers: vec![
4265 "weaponwield".to_string(),
4266 "stacking".to_string(),
4267 "equipableslots".to_string(),
4268 "modeltype".to_string(),
4269 ],
4270 rows: table_rows,
4271 }
4272 }
4273
4274 #[test]
4275 fn combat_equip_queries_default_to_safe_values_without_baseitems() {
4276 let uti = Uti {
4279 base_item: 2,
4280 ..Uti::default()
4281 };
4282 let resolver = Resolver::new();
4283 let mut cache = TwoDaCache::new(&resolver);
4284
4285 let view = uti.snapshot(&mut cache);
4286 assert!(!view.is_weapon());
4287 assert!(!view.is_consumable());
4288 assert!(view.equip_slot_mask().is_none());
4289 assert!(view.model_type().is_none());
4290 }
4291
4292 #[test]
4293 fn combat_equip_queries_default_when_base_item_row_is_absent() {
4294 let table = baseitems_with_rows(&[(2, "2", "1", "0x00030", "0")]);
4297 let overrides = override_with_2da("baseitems", &table);
4298 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
4299 let mut cache = TwoDaCache::new(&resolver);
4300
4301 let uti = Uti {
4302 base_item: 99,
4303 ..Uti::default()
4304 };
4305 let view = uti.snapshot(&mut cache);
4306 assert!(!view.is_weapon());
4307 assert!(!view.is_consumable());
4308 assert!(view.equip_slot_mask().is_none());
4309 assert!(view.model_type().is_none());
4310 }
4311
4312 #[test]
4313 fn is_weapon_reads_weaponwield_column() {
4314 let table =
4317 baseitems_with_rows(&[(2, "2", "1", "0x00030", "0"), (35, "", "1", "0x00018", "0")]);
4318 let overrides = override_with_2da("baseitems", &table);
4319 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
4320 let mut cache = TwoDaCache::new(&resolver);
4321
4322 let weapon = Uti {
4323 base_item: 2,
4324 ..Uti::default()
4325 };
4326 assert!(weapon.snapshot(&mut cache).is_weapon());
4327
4328 let armor = Uti {
4329 base_item: 35,
4330 ..Uti::default()
4331 };
4332 assert!(!armor.snapshot(&mut cache).is_weapon());
4333 }
4334
4335 #[test]
4336 fn is_consumable_reads_stacking_column() {
4337 let table = baseitems_with_rows(&[
4341 (60, "0", "99", "0x40000", "0"),
4342 (35, "0", "1", "0x00018", "0"),
4343 ]);
4344 let overrides = override_with_2da("baseitems", &table);
4345 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
4346 let mut cache = TwoDaCache::new(&resolver);
4347
4348 let stim = Uti {
4349 base_item: 60,
4350 ..Uti::default()
4351 };
4352 assert!(stim.snapshot(&mut cache).is_consumable());
4353
4354 let armor = Uti {
4355 base_item: 35,
4356 ..Uti::default()
4357 };
4358 assert!(!armor.snapshot(&mut cache).is_consumable());
4359 }
4360
4361 #[test]
4362 fn equip_slot_mask_parses_hex_string_with_or_without_prefix() {
4363 let table = baseitems_with_rows(&[
4367 (2, "2", "1", "0x00030", "0"),
4368 (3, "2", "1", "0X00030", "0"),
4369 (4, "2", "1", "00030", "0"),
4370 ]);
4371 let overrides = override_with_2da("baseitems", &table);
4372 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
4373 let mut cache = TwoDaCache::new(&resolver);
4374
4375 for base in [2, 3, 4] {
4376 let uti = Uti {
4377 base_item: base,
4378 ..Uti::default()
4379 };
4380 assert_eq!(uti.snapshot(&mut cache).equip_slot_mask(), Some(0x0030));
4381 }
4382 }
4383
4384 #[test]
4385 fn model_type_reads_numeric_column() {
4386 let table = baseitems_with_rows(&[
4387 (2, "2", "1", "0x00030", "0"),
4388 (38, "0", "1", "0x00018", "1"),
4389 (75, "0", "1", "0x00400", "2"),
4390 ]);
4391 let overrides = override_with_2da("baseitems", &table);
4392 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
4393 let mut cache = TwoDaCache::new(&resolver);
4394
4395 for (base, expected) in [(2, 0_u8), (38, 1), (75, 2)] {
4396 let uti = Uti {
4397 base_item: base,
4398 ..Uti::default()
4399 };
4400 assert_eq!(uti.snapshot(&mut cache).model_type(), Some(expected));
4401 }
4402 }
4403
4404 #[test]
4405 fn has_property_kind_matches_each_family() {
4406 let table = itempropdef_with_labels(&[
4409 (0, "Ability"),
4410 (10, "CastSpell"),
4411 (11, "Damage"),
4412 (26, "ImprovedSavingThrows"),
4413 (38, "AttackBonus"),
4414 (5, "Enhancement"),
4415 (57, "Use_Limitation_Feat"),
4416 ]);
4417 let overrides = override_with_2da("itempropdef", &table);
4418 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
4419 let mut cache = TwoDaCache::new(&resolver);
4420
4421 let uti = Uti {
4422 properties: vec![
4423 property(0, 0),
4424 property(10, 0),
4425 property(11, 0),
4426 property(26, 0),
4427 property(38, 0),
4428 property(5, 0),
4429 property(57, 0),
4430 ],
4431 ..Uti::default()
4432 };
4433 let view = uti.snapshot(&mut cache);
4434
4435 assert!(view.has_property_kind(PropertyKindFilter::Ability));
4436 assert!(view.has_property_kind(PropertyKindFilter::Active));
4437 assert!(view.has_property_kind(PropertyKindFilter::Damage));
4438 assert!(view.has_property_kind(PropertyKindFilter::Save));
4439 assert!(view.has_property_kind(PropertyKindFilter::Attack));
4440 assert!(view.has_property_kind(PropertyKindFilter::Enhancement));
4441 assert!(view.has_property_kind(PropertyKindFilter::UseLimitation));
4442 }
4443
4444 #[test]
4445 fn has_property_kind_returns_false_for_unrepresented_families() {
4446 let table = itempropdef_with_labels(&[(0, "Ability")]);
4449 let overrides = override_with_2da("itempropdef", &table);
4450 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
4451 let mut cache = TwoDaCache::new(&resolver);
4452
4453 let uti = Uti {
4454 properties: vec![property(0, 0)],
4455 ..Uti::default()
4456 };
4457 let view = uti.snapshot(&mut cache);
4458
4459 assert!(view.has_property_kind(PropertyKindFilter::Ability));
4460 for filter in [
4461 PropertyKindFilter::Damage,
4462 PropertyKindFilter::Save,
4463 PropertyKindFilter::Attack,
4464 PropertyKindFilter::Enhancement,
4465 PropertyKindFilter::UseLimitation,
4466 PropertyKindFilter::Active,
4467 ] {
4468 assert!(
4469 !view.has_property_kind(filter),
4470 "unexpected match for {filter:?}"
4471 );
4472 }
4473 }
4474
4475 #[test]
4476 fn has_property_kind_damage_covers_full_damage_family() {
4477 let table = itempropdef_with_labels(&[
4482 (11, "Damage"),
4483 (12, "DamageAlignmentGroup"),
4484 (13, "DamageRacialGroup"),
4485 (14, "DamageImmunity"),
4486 (15, "DamagePenalty"),
4487 (17, "DamageResist"),
4488 ]);
4489 let overrides = override_with_2da("itempropdef", &table);
4490 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
4491 let mut cache = TwoDaCache::new(&resolver);
4492
4493 for row in [11_u16, 12, 13, 14, 15, 17] {
4494 let uti = Uti {
4495 properties: vec![property(row, 0)],
4496 ..Uti::default()
4497 };
4498 let view = uti.snapshot(&mut cache);
4499 assert!(
4500 view.has_property_kind(PropertyKindFilter::Damage),
4501 "expected Damage filter to match row {row}"
4502 );
4503 }
4504 }
4505
4506 #[test]
4507 fn has_property_kind_does_not_match_unknown_variants() {
4508 let table = itempropdef_with_labels(&[(200, "FakeMod_GrantsCookies")]);
4511 let overrides = override_with_2da("itempropdef", &table);
4512 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
4513 let mut cache = TwoDaCache::new(&resolver);
4514
4515 let uti = Uti {
4516 properties: vec![property(200, 0)],
4517 ..Uti::default()
4518 };
4519 let view = uti.snapshot(&mut cache);
4520
4521 for filter in [
4522 PropertyKindFilter::Damage,
4523 PropertyKindFilter::Ability,
4524 PropertyKindFilter::Save,
4525 PropertyKindFilter::Attack,
4526 PropertyKindFilter::Enhancement,
4527 PropertyKindFilter::UseLimitation,
4528 PropertyKindFilter::Active,
4529 ] {
4530 assert!(
4531 !view.has_property_kind(filter),
4532 "Unknown variant should not match {filter:?}"
4533 );
4534 }
4535 }
4536
4537 #[test]
4538 fn has_property_kind_returns_false_for_empty_property_list() {
4539 let uti = Uti::default();
4540 let resolver = Resolver::new();
4541 let mut cache = TwoDaCache::new(&resolver);
4542 let view = uti.snapshot(&mut cache);
4543
4544 for filter in [
4545 PropertyKindFilter::Damage,
4546 PropertyKindFilter::Ability,
4547 PropertyKindFilter::Save,
4548 PropertyKindFilter::Attack,
4549 PropertyKindFilter::Enhancement,
4550 PropertyKindFilter::UseLimitation,
4551 PropertyKindFilter::Active,
4552 ] {
4553 assert!(!view.has_property_kind(filter));
4554 }
4555 }
4556
4557 #[test]
4558 fn combat_equip_queries_return_field_defaults_for_unparseable_cells() {
4559 let table = baseitems_with_rows(&[(2, "junk", "stuff", "not-hex", "wat")]);
4563 let overrides = override_with_2da("baseitems", &table);
4564 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
4565 let mut cache = TwoDaCache::new(&resolver);
4566
4567 let uti = Uti {
4568 base_item: 2,
4569 ..Uti::default()
4570 };
4571 let view = uti.snapshot(&mut cache);
4572 assert!(!view.is_weapon());
4573 assert!(!view.is_consumable());
4574 assert!(view.equip_slot_mask().is_none());
4575 assert!(view.model_type().is_none());
4576 }
4577
4578 fn itempropdef_with_subtypes(rows: &[(usize, &str, &str)]) -> TwoDa {
4583 let max_row = rows.iter().map(|(idx, _, _)| *idx).max().unwrap_or(0);
4584 let mut table_rows = Vec::with_capacity(max_row + 1);
4585 for row_index in 0..=max_row {
4586 let (label, subtype_resref) = rows
4587 .iter()
4588 .find(|(idx, _, _)| *idx == row_index)
4589 .map(|(_, label, sr)| ((*label).to_string(), (*sr).to_string()))
4590 .unwrap_or_default();
4591 table_rows.push(TwoDaRow {
4592 label: row_index.to_string(),
4593 cells: vec![label, subtype_resref],
4594 });
4595 }
4596 TwoDa {
4597 headers: vec!["label".to_string(), "SubTypeResRef".to_string()],
4598 rows: table_rows,
4599 }
4600 }
4601
4602 fn subtype_2da(labels: &[(usize, &str)]) -> TwoDa {
4605 itempropdef_with_labels(labels)
4609 }
4610
4611 fn add_2da_entry(overrides: &mut OverrideSource, name: &str, table: &TwoDa) {
4612 let bytes = write_twoda_to_vec(table).expect("write 2da fixture");
4613 overrides
4614 .add_entry(
4615 name,
4616 ResourceTypeCode::from(ResourceType::TwoDa),
4617 bytes,
4618 "test",
4619 )
4620 .expect("add override entry");
4621 }
4622
4623 #[test]
4624 fn subtype_label_resolves_full_chain_for_known_property() {
4625 let propdef = itempropdef_with_subtypes(&[
4628 (0, "Ability", "iprp_abilities"),
4629 (7, "Damage", "iprp_damagecost"),
4630 ]);
4631 let damagecost = subtype_2da(&[(0, "Bludgeoning"), (5, "Acid")]);
4632
4633 let mut overrides = OverrideSource::new();
4634 add_2da_entry(&mut overrides, "itempropdef", &propdef);
4635 add_2da_entry(&mut overrides, "iprp_damagecost", &damagecost);
4636 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
4637 let mut cache = TwoDaCache::new(&resolver);
4638
4639 let prop = DecodedProperty::Unknown {
4640 property_id: 7,
4641 property_label: Some("Damage".to_string()),
4642 subtype: 5,
4643 cost_table: 0,
4644 cost_value: 0,
4645 param1: 0,
4646 param1_value: 0,
4647 };
4648 assert_eq!(prop.subtype_label(&mut cache).as_deref(), Some("Acid"));
4649 }
4650
4651 #[test]
4652 fn subtype_label_returns_none_for_property_with_empty_subtype_resref() {
4653 let propdef = itempropdef_with_subtypes(&[(7, "Light", "")]);
4656 let mut overrides = OverrideSource::new();
4657 add_2da_entry(&mut overrides, "itempropdef", &propdef);
4658 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
4659 let mut cache = TwoDaCache::new(&resolver);
4660
4661 let prop = DecodedProperty::Unknown {
4662 property_id: 7,
4663 property_label: Some("Light".to_string()),
4664 subtype: 0,
4665 cost_table: 0,
4666 cost_value: 0,
4667 param1: 0,
4668 param1_value: 0,
4669 };
4670 assert!(prop.subtype_label(&mut cache).is_none());
4671 }
4672
4673 #[test]
4674 fn subtype_label_returns_none_when_subtype_table_resref_is_missing() {
4675 let propdef = itempropdef_with_subtypes(&[(0, "Ability", "iprp_abilities")]);
4678 let mut overrides = OverrideSource::new();
4679 add_2da_entry(&mut overrides, "itempropdef", &propdef);
4680 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
4681 let mut cache = TwoDaCache::new(&resolver);
4682
4683 let prop = DecodedProperty::Unknown {
4684 property_id: 99,
4685 property_label: None,
4686 subtype: 0,
4687 cost_table: 0,
4688 cost_value: 0,
4689 param1: 0,
4690 param1_value: 0,
4691 };
4692 assert!(prop.subtype_label(&mut cache).is_none());
4693 }
4694
4695 #[test]
4696 fn subtype_label_returns_none_when_subtype_table_cannot_be_loaded() {
4697 let propdef = itempropdef_with_subtypes(&[(7, "Damage", "iprp_damagecost")]);
4700 let mut overrides = OverrideSource::new();
4701 add_2da_entry(&mut overrides, "itempropdef", &propdef);
4702 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
4703 let mut cache = TwoDaCache::new(&resolver);
4704
4705 let prop = DecodedProperty::Unknown {
4706 property_id: 7,
4707 property_label: Some("Damage".to_string()),
4708 subtype: 0,
4709 cost_table: 0,
4710 cost_value: 0,
4711 param1: 0,
4712 param1_value: 0,
4713 };
4714 assert!(prop.subtype_label(&mut cache).is_none());
4715 }
4716
4717 #[test]
4718 fn subtype_label_returns_none_when_subtype_row_is_out_of_bounds() {
4719 let propdef = itempropdef_with_subtypes(&[(7, "Damage", "iprp_damagecost")]);
4721 let damagecost = subtype_2da(&[(0, "Bludgeoning"), (1, "Slashing")]);
4722 let mut overrides = OverrideSource::new();
4723 add_2da_entry(&mut overrides, "itempropdef", &propdef);
4724 add_2da_entry(&mut overrides, "iprp_damagecost", &damagecost);
4725 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
4726 let mut cache = TwoDaCache::new(&resolver);
4727
4728 let prop = DecodedProperty::Unknown {
4729 property_id: 7,
4730 property_label: Some("Damage".to_string()),
4731 subtype: 99,
4732 cost_table: 0,
4733 cost_value: 0,
4734 param1: 0,
4735 param1_value: 0,
4736 };
4737 assert!(prop.subtype_label(&mut cache).is_none());
4738 }
4739
4740 #[test]
4741 fn subtype_label_returns_none_when_itempropdef_is_missing() {
4742 let resolver = Resolver::new();
4744 let mut cache = TwoDaCache::new(&resolver);
4745
4746 let prop = DecodedProperty::Unknown {
4747 property_id: 7,
4748 property_label: None,
4749 subtype: 5,
4750 cost_table: 0,
4751 cost_value: 0,
4752 param1: 0,
4753 param1_value: 0,
4754 };
4755 assert!(prop.subtype_label(&mut cache).is_none());
4756 }
4757
4758 #[test]
4759 fn subtype_label_resolves_mod_extended_subtype_row() {
4760 let propdef = itempropdef_with_subtypes(&[(7, "Damage", "iprp_damagecost")]);
4764 let damagecost = subtype_2da(&[(200, "FakeMod_PsionicBurn")]);
4765 let mut overrides = OverrideSource::new();
4766 add_2da_entry(&mut overrides, "itempropdef", &propdef);
4767 add_2da_entry(&mut overrides, "iprp_damagecost", &damagecost);
4768 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
4769 let mut cache = TwoDaCache::new(&resolver);
4770
4771 let prop = DecodedProperty::Unknown {
4772 property_id: 7,
4773 property_label: Some("Damage".to_string()),
4774 subtype: 200,
4775 cost_table: 0,
4776 cost_value: 0,
4777 param1: 0,
4778 param1_value: 0,
4779 };
4780 assert_eq!(
4781 prop.subtype_label(&mut cache).as_deref(),
4782 Some("FakeMod_PsionicBurn")
4783 );
4784 }
4785
4786 #[test]
4789 fn project_with_itempropdef_dispatches_typed_variants() {
4790 let uti = Uti {
4793 properties: vec![property(0, 2)],
4794 ..Uti::default()
4795 };
4796 let propdef = itempropdef_with_labels(&[(0, "Ability")]);
4797
4798 let projection = uti.project(Some(&propdef));
4799 assert!(matches!(
4800 projection.properties()[0],
4801 DecodedProperty::AbilityBonus { subtype_id: 2, .. }
4802 ));
4803 }
4804
4805 #[test]
4806 fn project_without_itempropdef_falls_back_to_unknown_no_label() {
4807 let uti = Uti {
4812 properties: vec![property(0, 2), property(11, 4)],
4813 ..Uti::default()
4814 };
4815
4816 let projection = uti.project(None);
4817 let labels: Vec<Option<&str>> = projection
4818 .properties()
4819 .iter()
4820 .map(|prop| match prop {
4821 DecodedProperty::Unknown { property_label, .. } => property_label.as_deref(),
4822 _ => panic!("expected every property to land in Unknown without itempropdef"),
4823 })
4824 .collect();
4825 assert_eq!(labels, vec![None, None]);
4826 }
4827
4828 #[test]
4829 fn projection_snapshot_loads_baseitems_for_snapshot_queries() {
4830 let uti = Uti {
4834 base_item: 0,
4835 ..Uti::default()
4836 };
4837 let baseitems = baseitems_with_rows(&[(0, "5", "0", "0x0010", "3")]);
4838 let overrides = override_with_2da("baseitems", &baseitems);
4839 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
4840 let mut cache = TwoDaCache::new(&resolver);
4841
4842 let projection = uti.project(None);
4843 let snapshot = projection.snapshot(&mut cache);
4844
4845 assert!(snapshot.is_weapon());
4846 assert_eq!(snapshot.equip_slot_mask(), Some(0x0010));
4847 assert_eq!(snapshot.model_type(), Some(3));
4848 }
4849
4850 #[test]
4851 fn one_projection_feeds_independent_snapshots_per_scope() {
4852 let uti = Uti {
4858 base_item: 0,
4859 ..Uti::default()
4860 };
4861
4862 let vanilla_baseitems = baseitems_with_rows(&[(0, "5", "0", "0x0010", "3")]);
4863 let vanilla_overrides = override_with_2da("baseitems", &vanilla_baseitems);
4864 let vanilla_resolver =
4865 Resolver::new().with_source(ResolverSourceRef::Override(&vanilla_overrides));
4866 let mut vanilla_cache = TwoDaCache::new(&vanilla_resolver);
4867
4868 let mod_baseitems = baseitems_with_rows(&[(0, "0", "10", "0x0000", "0")]);
4871 let mod_overrides = override_with_2da("baseitems", &mod_baseitems);
4872 let mod_resolver = Resolver::new().with_source(ResolverSourceRef::Override(&mod_overrides));
4873 let mut mod_cache = TwoDaCache::new(&mod_resolver);
4874
4875 let projection = uti.project(None);
4876 let vanilla = projection.snapshot(&mut vanilla_cache);
4877 let modded = projection.snapshot(&mut mod_cache);
4878
4879 assert!(vanilla.is_weapon());
4880 assert!(!modded.is_weapon());
4881 assert!(modded.is_consumable(), "mod row marks the item stackable");
4882 assert!(!vanilla.is_consumable());
4883 }
4884
4885 #[test]
4886 fn snapshot_sugar_matches_project_then_snapshot() {
4887 let uti = Uti {
4892 base_item: 0,
4893 properties: vec![property(0, 2)],
4894 ..Uti::default()
4895 };
4896 let propdef = itempropdef_with_labels(&[(0, "Ability")]);
4897 let baseitems = baseitems_with_rows(&[(0, "5", "0", "0x0010", "3")]);
4898 let mut overrides = OverrideSource::new();
4899 add_2da_entry(&mut overrides, "itempropdef", &propdef);
4900 add_2da_entry(&mut overrides, "baseitems", &baseitems);
4901 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
4902
4903 let mut sugar_cache = TwoDaCache::new(&resolver);
4904 let sugar = uti.snapshot(&mut sugar_cache);
4905
4906 let mut explicit_cache = TwoDaCache::new(&resolver);
4907 let projection = {
4908 let propdef_ref = explicit_cache.twoda(tables::ITEMPROPDEF).ok();
4909 uti.project(propdef_ref)
4910 };
4911 let explicit = projection.snapshot(&mut explicit_cache);
4912
4913 assert_eq!(sugar.properties(), explicit.properties());
4914 assert_eq!(sugar.is_weapon(), explicit.is_weapon());
4915 assert_eq!(sugar.is_consumable(), explicit.is_consumable());
4916 assert_eq!(sugar.equip_slot_mask(), explicit.equip_slot_mask());
4917 assert_eq!(sugar.model_type(), explicit.model_type());
4918 }
4919
4920 fn iprp_costtable_with_entries(rows: &[(usize, &str)]) -> TwoDa {
4926 let max_row = rows.iter().map(|(idx, _)| *idx).max().unwrap_or(0);
4927 let mut table_rows = Vec::with_capacity(max_row + 1);
4928 for row_index in 0..=max_row {
4929 let name = rows
4930 .iter()
4931 .find(|(idx, _)| *idx == row_index)
4932 .map(|(_, name)| (*name).to_string())
4933 .unwrap_or_default();
4934 table_rows.push(TwoDaRow {
4935 label: row_index.to_string(),
4936 cells: vec![name],
4937 });
4938 }
4939 TwoDa {
4940 headers: vec!["Name".to_string()],
4941 rows: table_rows,
4942 }
4943 }
4944
4945 fn cost_value_2da(rows: &[(usize, i32)]) -> TwoDa {
4950 let max_row = rows.iter().map(|(idx, _)| *idx).max().unwrap_or(0);
4951 let mut table_rows = Vec::with_capacity(max_row + 1);
4952 for row_index in 0..=max_row {
4953 let value = rows
4954 .iter()
4955 .find(|(idx, _)| *idx == row_index)
4956 .map(|(_, value)| value.to_string())
4957 .unwrap_or_default();
4958 table_rows.push(TwoDaRow {
4959 label: row_index.to_string(),
4960 cells: vec![value],
4961 });
4962 }
4963 TwoDa {
4964 headers: vec!["Value".to_string()],
4965 rows: table_rows,
4966 }
4967 }
4968
4969 fn property_with_cost(
4974 property_name: u16,
4975 subtype: u16,
4976 cost_table: u8,
4977 cost_value: u16,
4978 ) -> UtiProperty {
4979 UtiProperty {
4980 cost_table,
4981 cost_value,
4982 param1: 0xFF,
4983 param1_value: 0,
4984 property_name,
4985 subtype,
4986 chance_appear: 100,
4987 useable: None,
4988 uses_per_day: None,
4989 upgrade_type: None,
4990 }
4991 }
4992
4993 #[test]
4994 fn damage_bonuses_yields_cost_value_as_magnitude() {
4995 let uti = Uti {
4999 properties: vec![
5000 property_with_cost(11, 3, 4, 7),
5001 property_with_cost(11, 5, 4, 12),
5002 ],
5003 ..Uti::default()
5004 };
5005 let propdef = itempropdef_with_labels(&[(11, "Damage")]);
5006 let overrides = override_with_2da("itempropdef", &propdef);
5007 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
5008 let mut cache = TwoDaCache::new(&resolver);
5009
5010 let snapshot = uti.snapshot(&mut cache);
5011 let yielded: Vec<(u16, i32)> = snapshot.damage_bonuses().collect();
5012 assert_eq!(yielded, vec![(3, 7), (5, 12)]);
5013 }
5014
5015 #[test]
5016 fn ability_bonuses_resolve_magnitude_from_iprp_bonuscost_value() {
5017 let uti = Uti {
5022 properties: vec![
5023 property_with_cost(0, 2, 99, 4), property_with_cost(0, 4, 99, 7), ],
5026 ..Uti::default()
5027 };
5028 let propdef = itempropdef_with_labels(&[(0, "Ability")]);
5029 let bonuscost = cost_value_2da(&[(4, 2), (7, 5)]);
5030 let mut overrides = OverrideSource::new();
5031 add_2da_entry(&mut overrides, "itempropdef", &propdef);
5032 add_2da_entry(&mut overrides, "iprp_bonuscost", &bonuscost);
5033 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
5034 let mut cache = TwoDaCache::new(&resolver);
5035
5036 let snapshot = uti.snapshot(&mut cache);
5037 let yielded: Vec<(u16, i32)> = snapshot.ability_bonuses().collect();
5038 assert_eq!(yielded, vec![(2, 2), (4, 5)]);
5039 }
5040
5041 #[test]
5042 fn ability_bonuses_yield_nothing_when_iprp_bonuscost_missing() {
5043 let uti = Uti {
5048 properties: vec![property_with_cost(0, 2, 1, 4)],
5049 ..Uti::default()
5050 };
5051 let propdef = itempropdef_with_labels(&[(0, "Ability")]);
5052 let overrides = override_with_2da("itempropdef", &propdef);
5053 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
5054 let mut cache = TwoDaCache::new(&resolver);
5055
5056 let snapshot = uti.snapshot(&mut cache);
5057 assert_eq!(snapshot.ability_bonuses().count(), 0);
5058 }
5059
5060 #[test]
5061 fn damage_immunities_walk_dynamic_cost_table_dispatch() {
5062 let uti = Uti {
5067 properties: vec![property_with_cost(14, 8, 5, 3)],
5068 ..Uti::default()
5069 };
5070 let propdef = itempropdef_with_labels(&[(14, "DamageImmunity")]);
5071 let costtable = iprp_costtable_with_entries(&[(5, "IPRP_IMMUNCOST")]);
5073 let immuncost = cost_value_2da(&[(3, 50)]);
5074 let mut overrides = OverrideSource::new();
5075 add_2da_entry(&mut overrides, "itempropdef", &propdef);
5076 add_2da_entry(&mut overrides, "iprp_costtable", &costtable);
5077 add_2da_entry(&mut overrides, "iprp_immuncost", &immuncost);
5078 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
5079 let mut cache = TwoDaCache::new(&resolver);
5080
5081 let snapshot = uti.snapshot(&mut cache);
5082 let yielded: Vec<(u16, i32)> = snapshot.damage_immunities().collect();
5083 assert_eq!(yielded, vec![(8, 50)]);
5084 }
5085
5086 #[test]
5087 fn damage_immunities_follow_mod_extended_cost_table_index() {
5088 let uti = Uti {
5093 properties: vec![property_with_cost(14, 8, 26, 1)],
5094 ..Uti::default()
5095 };
5096 let propdef = itempropdef_with_labels(&[(14, "DamageImmunity")]);
5097 let costtable = iprp_costtable_with_entries(&[(26, "mod_immuncost")]);
5098 let modded_cost = cost_value_2da(&[(1, 75)]);
5099 let mut overrides = OverrideSource::new();
5100 add_2da_entry(&mut overrides, "itempropdef", &propdef);
5101 add_2da_entry(&mut overrides, "iprp_costtable", &costtable);
5102 add_2da_entry(&mut overrides, "mod_immuncost", &modded_cost);
5103 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
5104 let mut cache = TwoDaCache::new(&resolver);
5105
5106 let snapshot = uti.snapshot(&mut cache);
5107 let yielded: Vec<(u16, i32)> = snapshot.damage_immunities().collect();
5108 assert_eq!(yielded, vec![(8, 75)]);
5109 }
5110
5111 #[test]
5112 fn iterators_only_yield_matching_kind() {
5113 let uti = Uti {
5116 properties: vec![
5117 property_with_cost(0, 2, 1, 4), property_with_cost(11, 3, 4, 7), property_with_cost(14, 8, 5, 3), ],
5121 ..Uti::default()
5122 };
5123 let propdef =
5124 itempropdef_with_labels(&[(0, "Ability"), (11, "Damage"), (14, "DamageImmunity")]);
5125 let bonuscost = cost_value_2da(&[(4, 2)]);
5126 let costtable = iprp_costtable_with_entries(&[(5, "iprp_immuncost")]);
5127 let immuncost = cost_value_2da(&[(3, 50)]);
5128 let mut overrides = OverrideSource::new();
5129 add_2da_entry(&mut overrides, "itempropdef", &propdef);
5130 add_2da_entry(&mut overrides, "iprp_bonuscost", &bonuscost);
5131 add_2da_entry(&mut overrides, "iprp_costtable", &costtable);
5132 add_2da_entry(&mut overrides, "iprp_immuncost", &immuncost);
5133 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
5134 let mut cache = TwoDaCache::new(&resolver);
5135
5136 let snapshot = uti.snapshot(&mut cache);
5137 assert_eq!(snapshot.ability_bonuses().collect::<Vec<_>>(), vec![(2, 2)]);
5138 assert_eq!(snapshot.damage_bonuses().collect::<Vec<_>>(), vec![(3, 7)]);
5139 assert_eq!(
5140 snapshot.damage_immunities().collect::<Vec<_>>(),
5141 vec![(8, 50)]
5142 );
5143 }
5144
5145 #[test]
5146 fn one_projection_yields_diverging_magnitudes_per_scope() {
5147 let uti = Uti {
5152 properties: vec![property_with_cost(0, 2, 1, 4)],
5153 ..Uti::default()
5154 };
5155 let propdef = itempropdef_with_labels(&[(0, "Ability")]);
5156
5157 let vanilla_bonus = cost_value_2da(&[(4, 2)]);
5158 let mut vanilla_overrides = OverrideSource::new();
5159 add_2da_entry(&mut vanilla_overrides, "itempropdef", &propdef);
5160 add_2da_entry(&mut vanilla_overrides, "iprp_bonuscost", &vanilla_bonus);
5161 let vanilla_resolver =
5162 Resolver::new().with_source(ResolverSourceRef::Override(&vanilla_overrides));
5163
5164 let mod_bonus = cost_value_2da(&[(4, 6)]); let mut mod_overrides = OverrideSource::new();
5166 add_2da_entry(&mut mod_overrides, "itempropdef", &propdef);
5167 add_2da_entry(&mut mod_overrides, "iprp_bonuscost", &mod_bonus);
5168 let mod_resolver = Resolver::new().with_source(ResolverSourceRef::Override(&mod_overrides));
5169
5170 let mut vanilla_cache = TwoDaCache::new(&vanilla_resolver);
5171 let mut mod_cache = TwoDaCache::new(&mod_resolver);
5172
5173 let projection = {
5174 let propdef_ref = vanilla_cache.twoda(tables::ITEMPROPDEF).ok();
5175 uti.project(propdef_ref)
5176 };
5177 let vanilla = projection.snapshot(&mut vanilla_cache);
5178 let modded = projection.snapshot(&mut mod_cache);
5179
5180 assert_eq!(vanilla.ability_bonuses().collect::<Vec<_>>(), vec![(2, 2)]);
5181 assert_eq!(modded.ability_bonuses().collect::<Vec<_>>(), vec![(2, 6)]);
5182 }
5183}