1use rakata_core::tables;
27use rakata_formats::twoda::{TwoDa, TwoDaSource};
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 impl TwoDaSource) -> 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)?;
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)?;
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 UtiResolved<'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 impl TwoDaSource) -> 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")?;
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 impl TwoDaSource,
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")?;
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)?;
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 resolve(&self, cache: &mut impl TwoDaSource) -> UtiResolved<'a> {
1307 let base_item_info = cache
1308 .twoda(tables::BASEITEMS)
1309 .and_then(|table| BaseItemInfo::from_row(table, self.uti.base_item));
1310 let resolved_magnitudes = self
1311 .decoded_properties
1312 .iter()
1313 .map(|prop| resolve_magnitude(prop, cache))
1314 .collect();
1315 UtiResolved {
1316 uti: self.uti,
1317 decoded_properties: self.decoded_properties.clone(),
1318 base_item_info,
1319 resolved_magnitudes,
1320 }
1321 }
1322}
1323
1324impl<'a> UtiResolved<'a> {
1325 pub fn properties(&self) -> &[DecodedProperty] {
1332 &self.decoded_properties
1333 }
1334
1335 pub fn is_armor(&self) -> bool {
1344 self.uti.is_armor()
1345 }
1346
1347 pub fn is_weapon(&self) -> bool {
1354 self.base_item_info
1355 .as_ref()
1356 .is_some_and(|b| b.weapon_wield > 0)
1357 }
1358
1359 pub fn is_consumable(&self) -> bool {
1367 self.base_item_info.as_ref().is_some_and(|b| b.stacking > 1)
1368 }
1369
1370 pub fn equip_slot_mask(&self) -> Option<u32> {
1378 self.base_item_info.as_ref().and_then(|b| b.equip_slot_mask)
1379 }
1380
1381 pub fn model_type(&self) -> Option<u8> {
1390 self.base_item_info.as_ref().and_then(|b| b.model_type)
1391 }
1392
1393 pub fn has_property_kind(&self, filter: PropertyKindFilter) -> bool {
1402 self.decoded_properties
1403 .iter()
1404 .any(|prop| filter.matches(prop))
1405 }
1406
1407 pub fn ability_bonuses(&self) -> impl Iterator<Item = (u16, i32)> + '_ {
1421 self.iter_resolved_magnitudes(|prop| match prop {
1422 DecodedProperty::AbilityBonus { subtype_id, .. } => Some(*subtype_id),
1423 _ => None,
1424 })
1425 }
1426
1427 pub fn damage_bonuses(&self) -> impl Iterator<Item = (u16, i32)> + '_ {
1438 self.iter_resolved_magnitudes(|prop| match prop {
1439 DecodedProperty::DamageBonus { subtype_id, .. } => Some(*subtype_id),
1440 _ => None,
1441 })
1442 }
1443
1444 pub fn damage_immunities(&self) -> impl Iterator<Item = (u16, i32)> + '_ {
1460 self.iter_resolved_magnitudes(|prop| match prop {
1461 DecodedProperty::DamageImmunity { subtype_id, .. } => Some(*subtype_id),
1462 _ => None,
1463 })
1464 }
1465
1466 fn iter_resolved_magnitudes<F>(&self, pick_subtype: F) -> impl Iterator<Item = (u16, i32)> + '_
1473 where
1474 F: Fn(&DecodedProperty) -> Option<u16> + 'static,
1475 {
1476 self.decoded_properties
1477 .iter()
1478 .zip(self.resolved_magnitudes.iter())
1479 .filter_map(move |(prop, magnitude)| {
1480 let subtype = pick_subtype(prop)?;
1481 let value = (*magnitude)?;
1482 Some((subtype, value))
1483 })
1484 }
1485}
1486
1487#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1504pub enum PropertyKindFilter {
1505 Damage,
1509 Ability,
1511 Save,
1515 Attack,
1518 Enhancement,
1521 UseLimitation,
1524 Active,
1528}
1529
1530impl PropertyKindFilter {
1531 fn matches(self, prop: &DecodedProperty) -> bool {
1532 match self {
1533 Self::Damage => matches!(
1534 prop,
1535 DecodedProperty::DamageBonus { .. }
1536 | DecodedProperty::DamageImmunity { .. }
1537 | DecodedProperty::DamageResistance { .. }
1538 | DecodedProperty::DamageRacialGroup { .. }
1539 | DecodedProperty::DamageAlignmentGroup { .. }
1540 | DecodedProperty::DamagePenalty { .. }
1541 ),
1542 Self::Ability => matches!(prop, DecodedProperty::AbilityBonus { .. }),
1543 Self::Save => matches!(
1544 prop,
1545 DecodedProperty::SaveBonus { .. }
1546 | DecodedProperty::SaveBonusSpecific { .. }
1547 | DecodedProperty::SavePenalty { .. }
1548 | DecodedProperty::SavePenaltySpecific { .. }
1549 ),
1550 Self::Attack => matches!(
1551 prop,
1552 DecodedProperty::AttackBonus { .. }
1553 | DecodedProperty::AttackBonusAlignmentGroup { .. }
1554 | DecodedProperty::AttackPenalty { .. }
1555 ),
1556 Self::Enhancement => matches!(
1557 prop,
1558 DecodedProperty::EnhancementBonus { .. }
1559 | DecodedProperty::EnhancementRacialGroup { .. }
1560 | DecodedProperty::EnhancementAlignmentGroup { .. }
1561 ),
1562 Self::UseLimitation => matches!(
1563 prop,
1564 DecodedProperty::UseLimitationFeat { .. }
1565 | DecodedProperty::UseLimitationRacial { .. }
1566 | DecodedProperty::UseLimitationAlignmentGroup { .. }
1567 ),
1568 Self::Active => matches!(
1569 prop,
1570 DecodedProperty::CastSpell { .. }
1571 | DecodedProperty::Trap { .. }
1572 | DecodedProperty::ThievesTools { .. }
1573 | DecodedProperty::ComputerSpike { .. }
1574 ),
1575 }
1576 }
1577}
1578
1579impl Uti {
1580 pub fn project(&self, itempropdef: Option<&TwoDa>) -> UtiProjection<'_> {
1595 let decoded_properties = self
1596 .properties
1597 .iter()
1598 .map(|p| {
1599 let label = itempropdef
1600 .and_then(|table| table.cell(usize::from(p.property_name), "label"))
1601 .map(str::to_string);
1602 decode_property(p, label)
1603 })
1604 .collect();
1605 UtiProjection {
1606 uti: self,
1607 decoded_properties,
1608 }
1609 }
1610
1611 pub fn resolve(&self, cache: &mut impl TwoDaSource) -> UtiResolved<'_> {
1626 let projection = {
1630 let propdef = cache.twoda(tables::ITEMPROPDEF);
1631 self.project(propdef)
1632 };
1633 projection.resolve(cache)
1634 }
1635}
1636
1637fn decode_property(p: &UtiProperty, label: Option<String>) -> DecodedProperty {
1645 match label.as_deref() {
1646 Some("Ability") => DecodedProperty::AbilityBonus {
1647 property_id: p.property_name,
1648 subtype_id: p.subtype,
1649 cost_table: p.cost_table,
1650 cost_value: p.cost_value,
1651 },
1652 Some("ImprovedSavingThrows") => DecodedProperty::SaveBonus {
1653 property_id: p.property_name,
1654 subtype_id: p.subtype,
1655 cost_table: p.cost_table,
1656 cost_value: p.cost_value,
1657 },
1658 Some("ImprovedSavingThrowsSpecific") => DecodedProperty::SaveBonusSpecific {
1659 property_id: p.property_name,
1660 subtype_id: p.subtype,
1661 cost_table: p.cost_table,
1662 cost_value: p.cost_value,
1663 },
1664 Some("ReducedSavingThrows") => DecodedProperty::SavePenalty {
1665 property_id: p.property_name,
1666 subtype_id: p.subtype,
1667 cost_table: p.cost_table,
1668 cost_value: p.cost_value,
1669 },
1670 Some("ReducedSpecificSavingThrow") => DecodedProperty::SavePenaltySpecific {
1671 property_id: p.property_name,
1672 subtype_id: p.subtype,
1673 cost_table: p.cost_table,
1674 cost_value: p.cost_value,
1675 },
1676 Some("Damage") => DecodedProperty::DamageBonus {
1677 property_id: p.property_name,
1678 subtype_id: p.subtype,
1679 cost_table: p.cost_table,
1680 cost_value: p.cost_value,
1681 },
1682 Some("DamageImmunity") => DecodedProperty::DamageImmunity {
1683 property_id: p.property_name,
1684 subtype_id: p.subtype,
1685 cost_table: p.cost_table,
1686 cost_value: p.cost_value,
1687 },
1688 Some("DamageResist") => DecodedProperty::DamageResistance {
1689 property_id: p.property_name,
1690 subtype_id: p.subtype,
1691 cost_table: p.cost_table,
1692 cost_value: p.cost_value,
1693 },
1694 Some("Armor") => DecodedProperty::AcBonus {
1695 property_id: p.property_name,
1696 subtype_id: p.subtype,
1697 cost_table: p.cost_table,
1698 cost_value: p.cost_value,
1699 },
1700 Some("Enhancement") => DecodedProperty::EnhancementBonus {
1701 property_id: p.property_name,
1702 subtype_id: p.subtype,
1703 cost_table: p.cost_table,
1704 cost_value: p.cost_value,
1705 },
1706 Some("OnHit") => DecodedProperty::OnHit {
1707 property_id: p.property_name,
1708 subtype_id: p.subtype,
1709 cost_table: p.cost_table,
1710 cost_value: p.cost_value,
1711 param1: p.param1,
1712 param1_value: p.param1_value,
1713 },
1714 Some("CastSpell") => DecodedProperty::CastSpell {
1715 property_id: p.property_name,
1716 subtype_id: p.subtype,
1717 cost_table: p.cost_table,
1718 cost_value: p.cost_value,
1719 useable: active_useable(p.useable),
1720 uses_per_day: active_uses_per_day(p.uses_per_day),
1721 },
1722 Some("Trap") => DecodedProperty::Trap {
1723 property_id: p.property_name,
1724 subtype_id: p.subtype,
1725 cost_table: p.cost_table,
1726 cost_value: p.cost_value,
1727 useable: active_useable(p.useable),
1728 uses_per_day: active_uses_per_day(p.uses_per_day),
1729 },
1730 Some("ThievesTools") => DecodedProperty::ThievesTools {
1731 property_id: p.property_name,
1732 subtype_id: p.subtype,
1733 cost_table: p.cost_table,
1734 cost_value: p.cost_value,
1735 useable: active_useable(p.useable),
1736 uses_per_day: active_uses_per_day(p.uses_per_day),
1737 },
1738 Some("Computer_Spike") => DecodedProperty::ComputerSpike {
1739 property_id: p.property_name,
1740 subtype_id: p.subtype,
1741 cost_table: p.cost_table,
1742 cost_value: p.cost_value,
1743 useable: active_useable(p.useable),
1744 uses_per_day: active_uses_per_day(p.uses_per_day),
1745 },
1746 Some("Use_Limitation_Feat") => DecodedProperty::UseLimitationFeat {
1747 property_id: p.property_name,
1748 subtype_id: p.subtype,
1749 cost_table: p.cost_table,
1750 cost_value: p.cost_value,
1751 },
1752 Some("UseLimitationRacial") => DecodedProperty::UseLimitationRacial {
1753 property_id: p.property_name,
1754 subtype_id: p.subtype,
1755 cost_table: p.cost_table,
1756 cost_value: p.cost_value,
1757 },
1758 Some("UseLimitationAlignmentGroup") => DecodedProperty::UseLimitationAlignmentGroup {
1759 property_id: p.property_name,
1760 subtype_id: p.subtype,
1761 cost_table: p.cost_table,
1762 cost_value: p.cost_value,
1763 },
1764 Some("DamageRacialGroup") => DecodedProperty::DamageRacialGroup {
1765 property_id: p.property_name,
1766 subtype_id: p.subtype,
1767 cost_table: p.cost_table,
1768 cost_value: p.cost_value,
1769 },
1770 Some("DamageAlignmentGroup") => DecodedProperty::DamageAlignmentGroup {
1771 property_id: p.property_name,
1772 subtype_id: p.subtype,
1773 cost_table: p.cost_table,
1774 cost_value: p.cost_value,
1775 },
1776 Some("EnhancementRacialGroup") => DecodedProperty::EnhancementRacialGroup {
1777 property_id: p.property_name,
1778 subtype_id: p.subtype,
1779 cost_table: p.cost_table,
1780 cost_value: p.cost_value,
1781 },
1782 Some("EnhancementAlignmentGroup") => DecodedProperty::EnhancementAlignmentGroup {
1783 property_id: p.property_name,
1784 subtype_id: p.subtype,
1785 cost_table: p.cost_table,
1786 cost_value: p.cost_value,
1787 },
1788 Some("AttackBonusAlignmentGroup") => DecodedProperty::AttackBonusAlignmentGroup {
1789 property_id: p.property_name,
1790 subtype_id: p.subtype,
1791 cost_table: p.cost_table,
1792 cost_value: p.cost_value,
1793 },
1794 Some("True_Seeing") => DecodedProperty::TrueSeeing {
1795 property_id: p.property_name,
1796 subtype_id: p.subtype,
1797 cost_table: p.cost_table,
1798 cost_value: p.cost_value,
1799 },
1800 Some("Light") => DecodedProperty::Light {
1801 property_id: p.property_name,
1802 subtype_id: p.subtype,
1803 cost_table: p.cost_table,
1804 cost_value: p.cost_value,
1805 param1: p.param1,
1806 param1_value: p.param1_value,
1807 },
1808 Some("AttackBonus") => DecodedProperty::AttackBonus {
1809 property_id: p.property_name,
1810 subtype_id: p.subtype,
1811 cost_table: p.cost_table,
1812 cost_value: p.cost_value,
1813 },
1814 Some("Keen") => DecodedProperty::Keen {
1815 property_id: p.property_name,
1816 subtype_id: p.subtype,
1817 cost_table: p.cost_table,
1818 cost_value: p.cost_value,
1819 },
1820 Some("Massive_Criticals") => DecodedProperty::MassiveCriticals {
1821 property_id: p.property_name,
1822 subtype_id: p.subtype,
1823 cost_table: p.cost_table,
1824 cost_value: p.cost_value,
1825 },
1826 Some("Blaster_Bolt_Deflect_Increase") => DecodedProperty::BlasterBoltDeflectIncrease {
1827 property_id: p.property_name,
1828 subtype_id: p.subtype,
1829 cost_table: p.cost_table,
1830 cost_value: p.cost_value,
1831 },
1832 Some("Monster_damage") => DecodedProperty::MonsterDamage {
1833 property_id: p.property_name,
1834 subtype_id: p.subtype,
1835 cost_table: p.cost_table,
1836 cost_value: p.cost_value,
1837 },
1838 Some("BonusFeats") => DecodedProperty::BonusFeats {
1839 property_id: p.property_name,
1840 subtype_id: p.subtype,
1841 cost_table: p.cost_table,
1842 cost_value: p.cost_value,
1843 },
1844 Some("Immunity") => DecodedProperty::Immunity {
1845 property_id: p.property_name,
1846 subtype_id: p.subtype,
1847 cost_table: p.cost_table,
1848 cost_value: p.cost_value,
1849 },
1850 Some("Skill") => DecodedProperty::Skill {
1851 property_id: p.property_name,
1852 subtype_id: p.subtype,
1853 cost_table: p.cost_table,
1854 cost_value: p.cost_value,
1855 },
1856 Some("AttackPenalty") => DecodedProperty::AttackPenalty {
1857 property_id: p.property_name,
1858 subtype_id: p.subtype,
1859 cost_table: p.cost_table,
1860 cost_value: p.cost_value,
1861 },
1862 Some("DamagePenalty") => DecodedProperty::DamagePenalty {
1863 property_id: p.property_name,
1864 subtype_id: p.subtype,
1865 cost_table: p.cost_table,
1866 cost_value: p.cost_value,
1867 },
1868 Some("ImprovedMagicResist") => DecodedProperty::MagicResistBonus {
1869 property_id: p.property_name,
1870 subtype_id: p.subtype,
1871 cost_table: p.cost_table,
1872 cost_value: p.cost_value,
1873 },
1874 Some("DamageNone") => DecodedProperty::DamageNone {
1875 property_id: p.property_name,
1876 subtype_id: p.subtype,
1877 cost_table: p.cost_table,
1878 cost_value: p.cost_value,
1879 },
1880 Some("Regeneration") => DecodedProperty::Regeneration {
1881 property_id: p.property_name,
1882 subtype_id: p.subtype,
1883 cost_table: p.cost_table,
1884 cost_value: p.cost_value,
1885 },
1886 Some("Regeneration_Force_Points") => DecodedProperty::RegenerationForcePoints {
1887 property_id: p.property_name,
1888 subtype_id: p.subtype,
1889 cost_table: p.cost_table,
1890 cost_value: p.cost_value,
1891 },
1892 Some("Disguise") => DecodedProperty::Disguise {
1893 property_id: p.property_name,
1894 subtype_id: p.subtype,
1895 cost_table: p.cost_table,
1896 cost_value: p.cost_value,
1897 },
1898 _ => DecodedProperty::Unknown {
1915 property_id: p.property_name,
1916 property_label: label,
1917 subtype: p.subtype,
1918 cost_table: p.cost_table,
1919 cost_value: p.cost_value,
1920 param1: p.param1,
1921 param1_value: p.param1_value,
1922 },
1923 }
1924}
1925
1926fn active_useable(raw: Option<bool>) -> bool {
1933 raw.unwrap_or(true)
1934}
1935
1936fn active_uses_per_day(raw: Option<u8>) -> Option<u8> {
1941 match raw {
1942 Some(0xFF) | None => None,
1943 Some(value) => Some(value),
1944 }
1945}
1946
1947#[cfg(test)]
1948mod tests {
1949 use super::*;
1950 use crate::decoded::test_tables::TestTables;
1951 use crate::uti::UtiProperty;
1952 use rakata_formats::twoda::{TwoDa, TwoDaRow};
1953
1954 fn property(property_name: u16, subtype: u16) -> UtiProperty {
1955 UtiProperty {
1956 cost_table: 1,
1957 cost_value: 5,
1958 param1: 0xFF,
1959 param1_value: 0,
1960 property_name,
1961 subtype,
1962 chance_appear: 100,
1963 useable: None,
1964 uses_per_day: None,
1965 upgrade_type: None,
1966 }
1967 }
1968
1969 fn itempropdef_with_labels(labels: &[(usize, &str)]) -> TwoDa {
1970 let max_row = labels.iter().map(|(idx, _)| *idx).max().unwrap_or(0);
1971 let mut rows = Vec::with_capacity(max_row + 1);
1972 for row_index in 0..=max_row {
1973 let label = labels
1974 .iter()
1975 .find(|(idx, _)| *idx == row_index)
1976 .map(|(_, label)| (*label).to_string())
1977 .unwrap_or_default();
1978 rows.push(TwoDaRow {
1979 label: row_index.to_string(),
1980 cells: vec![label],
1981 });
1982 }
1983 TwoDa {
1984 headers: vec!["label".to_string()],
1985 rows,
1986 }
1987 }
1988
1989 #[test]
1990 fn decodes_every_property_into_unknown_variant() {
1991 let uti = Uti {
1992 properties: vec![property(0, 1), property(7, 12), property(45, 0)],
1993 ..Uti::default()
1994 };
1995 let mut tables = TestTables::new();
1996
1997 let view = uti.resolve(&mut tables);
1998 assert_eq!(view.properties().len(), 3);
1999 assert!(view
2000 .decoded_properties
2001 .iter()
2002 .all(|p| matches!(p, DecodedProperty::Unknown { .. })));
2003 }
2004
2005 #[test]
2006 fn unknown_carries_raw_fields_through_unchanged() {
2007 let raw = property(0, 0);
2008 let mut shaped = raw.clone();
2009 shaped.property_name = 7;
2010 shaped.subtype = 12;
2011 shaped.cost_table = 3;
2012 shaped.cost_value = 99;
2013 shaped.param1 = 4;
2014 shaped.param1_value = 200;
2015 let uti = Uti {
2016 properties: vec![shaped.clone()],
2017 ..Uti::default()
2018 };
2019 let mut tables = TestTables::new();
2020
2021 let view = uti.resolve(&mut tables);
2022 let DecodedProperty::Unknown {
2023 property_id,
2024 subtype,
2025 cost_table,
2026 cost_value,
2027 param1,
2028 param1_value,
2029 ..
2030 } = &view.properties()[0]
2031 else {
2032 panic!("expected Unknown variant for unloaded itempropdef");
2033 };
2034 assert_eq!(*property_id, 7);
2035 assert_eq!(*subtype, 12);
2036 assert_eq!(*cost_table, 3);
2037 assert_eq!(*cost_value, 99);
2038 assert_eq!(*param1, 4);
2039 assert_eq!(*param1_value, 200);
2040 }
2041
2042 #[test]
2043 fn property_label_resolves_into_unknown_for_untyped_kinds() {
2044 let table = itempropdef_with_labels(&[(0, "Test_Kind_A"), (7, "Test_Kind_B")]);
2047 let mut tables = TestTables::with("itempropdef", &table);
2048
2049 let uti = Uti {
2050 properties: vec![property(7, 0), property(0, 0)],
2051 ..Uti::default()
2052 };
2053 let view = uti.resolve(&mut tables);
2054
2055 let DecodedProperty::Unknown { property_label, .. } = &view.properties()[0] else {
2056 panic!("expected Unknown variant for synthetic label");
2057 };
2058 assert_eq!(property_label.as_deref(), Some("Test_Kind_B"));
2059 let DecodedProperty::Unknown { property_label, .. } = &view.properties()[1] else {
2060 panic!("expected Unknown variant for synthetic label");
2061 };
2062 assert_eq!(property_label.as_deref(), Some("Test_Kind_A"));
2063 }
2064
2065 #[test]
2066 fn property_label_is_none_when_row_is_absent() {
2067 let table = itempropdef_with_labels(&[(0, "Test_Kind_A"), (1, "Test_Kind_B")]);
2071 let mut tables = TestTables::with("itempropdef", &table);
2072
2073 let uti = Uti {
2074 properties: vec![property(99, 0)],
2075 ..Uti::default()
2076 };
2077 let view = uti.resolve(&mut tables);
2078
2079 let DecodedProperty::Unknown { property_label, .. } = &view.properties()[0] else {
2080 panic!("expected Unknown variant for absent row");
2081 };
2082 assert!(property_label.is_none());
2083 }
2084
2085 #[test]
2086 fn property_label_is_none_when_itempropdef_is_missing() {
2087 let mut tables = TestTables::new();
2090
2091 let uti = Uti {
2092 properties: vec![property(0, 0), property(7, 0)],
2093 ..Uti::default()
2094 };
2095 let view = uti.resolve(&mut tables);
2096
2097 for prop in view.properties() {
2098 let DecodedProperty::Unknown { property_label, .. } = prop else {
2099 panic!("expected Unknown variant when itempropdef is missing");
2100 };
2101 assert!(property_label.is_none());
2102 }
2103 }
2104
2105 #[test]
2106 fn mod_added_property_label_surfaces_via_unknown() {
2107 let table = itempropdef_with_labels(&[(200, "FakeMod_GrantsCookies")]);
2111 let mut tables = TestTables::with("itempropdef", &table);
2112
2113 let uti = Uti {
2114 properties: vec![property(200, 5)],
2115 ..Uti::default()
2116 };
2117 let view = uti.resolve(&mut tables);
2118
2119 let DecodedProperty::Unknown {
2120 property_id,
2121 property_label,
2122 subtype,
2123 ..
2124 } = &view.properties()[0]
2125 else {
2126 panic!("expected Unknown variant for mod-added label");
2127 };
2128 assert_eq!(*property_id, 200);
2129 assert_eq!(property_label.as_deref(), Some("FakeMod_GrantsCookies"));
2130 assert_eq!(*subtype, 5);
2131 }
2132
2133 #[test]
2134 fn empty_property_list_decodes_to_empty_view() {
2135 let uti = Uti::default();
2136 let mut tables = TestTables::new();
2137
2138 let view = uti.resolve(&mut tables);
2139 assert!(view.properties().is_empty());
2140 }
2141
2142 #[test]
2143 fn ability_bonus_label_routes_to_typed_variant() {
2144 let table = itempropdef_with_labels(&[(0, "Ability")]);
2148 let mut tables = TestTables::with("itempropdef", &table);
2149
2150 let mut shaped = property(0, 2);
2151 shaped.cost_table = 1;
2152 shaped.cost_value = 4;
2153 let uti = Uti {
2154 properties: vec![shaped],
2155 ..Uti::default()
2156 };
2157 let view = uti.resolve(&mut tables);
2158
2159 let DecodedProperty::AbilityBonus {
2160 property_id,
2161 subtype_id,
2162 cost_table,
2163 cost_value,
2164 } = &view.properties()[0]
2165 else {
2166 panic!("expected AbilityBonus variant for `Ability` label");
2167 };
2168 assert_eq!(*property_id, 0);
2169 assert_eq!(*subtype_id, 2); assert_eq!(*cost_table, 1);
2171 assert_eq!(*cost_value, 4);
2172 }
2173
2174 #[test]
2175 fn ability_bonus_preserves_raw_subtype_for_mod_extended_rows() {
2176 let table = itempropdef_with_labels(&[(0, "Ability")]);
2180 let mut tables = TestTables::with("itempropdef", &table);
2181
2182 let uti = Uti {
2183 properties: vec![property(0, 99)],
2184 ..Uti::default()
2185 };
2186 let view = uti.resolve(&mut tables);
2187
2188 let DecodedProperty::AbilityBonus { subtype_id, .. } = &view.properties()[0] else {
2189 panic!("expected AbilityBonus variant for mod-extended subtype");
2190 };
2191 assert_eq!(*subtype_id, 99);
2192 }
2193
2194 #[test]
2195 fn ability_bonus_subtype_label_resolves_via_iprp_abilities() {
2196 let propdef = itempropdef_with_subtypes(&[(0, "Ability", "iprp_abilities")]);
2199 let abilities = subtype_2da(&[
2200 (0, "STR"),
2201 (1, "DEX"),
2202 (2, "CON"),
2203 (3, "INT"),
2204 (4, "WIS"),
2205 (5, "CHA"),
2206 ]);
2207 let mut tables = TestTables::new();
2208 add_2da_entry(&mut tables, "itempropdef", &propdef);
2209 add_2da_entry(&mut tables, "iprp_abilities", &abilities);
2210
2211 let prop = DecodedProperty::AbilityBonus {
2212 property_id: 0,
2213 subtype_id: 3,
2214 cost_table: 0,
2215 cost_value: 0,
2216 };
2217 assert_eq!(prop.subtype_label(&mut tables).as_deref(), Some("INT"));
2218 }
2219
2220 #[test]
2221 fn save_bonus_label_routes_to_typed_variant() {
2222 let table = itempropdef_with_labels(&[(26, "ImprovedSavingThrows")]);
2226 let mut tables = TestTables::with("itempropdef", &table);
2227
2228 let mut shaped = property(26, 1);
2229 shaped.cost_table = 2;
2230 shaped.cost_value = 3;
2231 let uti = Uti {
2232 properties: vec![shaped],
2233 ..Uti::default()
2234 };
2235 let view = uti.resolve(&mut tables);
2236
2237 let DecodedProperty::SaveBonus {
2238 property_id,
2239 subtype_id,
2240 cost_table,
2241 cost_value,
2242 } = &view.properties()[0]
2243 else {
2244 panic!("expected SaveBonus variant for `ImprovedSavingThrows` label");
2245 };
2246 assert_eq!(*property_id, 26);
2247 assert_eq!(*subtype_id, 1);
2248 assert_eq!(*cost_table, 2);
2249 assert_eq!(*cost_value, 3);
2250 }
2251
2252 #[test]
2253 fn save_bonus_preserves_raw_subtype_for_mod_extended_rows() {
2254 let table = itempropdef_with_labels(&[(26, "ImprovedSavingThrows")]);
2258 let mut tables = TestTables::with("itempropdef", &table);
2259
2260 let uti = Uti {
2261 properties: vec![property(26, 200)],
2262 ..Uti::default()
2263 };
2264 let view = uti.resolve(&mut tables);
2265
2266 let DecodedProperty::SaveBonus { subtype_id, .. } = &view.properties()[0] else {
2267 panic!("expected SaveBonus variant for mod-extended subtype");
2268 };
2269 assert_eq!(*subtype_id, 200);
2270 }
2271
2272 #[test]
2273 fn save_bonus_does_not_match_save_penalty_or_specific_kinds() {
2274 let table = itempropdef_with_labels(&[
2281 (26, "ImprovedSavingThrows"),
2282 (27, "ImprovedSavingThrowsSpecific"),
2283 (33, "ReducedSavingThrows"),
2284 (34, "ReducedSpecificSavingThrow"),
2285 ]);
2286 let mut tables = TestTables::with("itempropdef", &table);
2287
2288 let uti = Uti {
2289 properties: vec![
2290 property(26, 0),
2291 property(27, 0),
2292 property(33, 0),
2293 property(34, 0),
2294 ],
2295 ..Uti::default()
2296 };
2297 let view = uti.resolve(&mut tables);
2298
2299 assert!(matches!(
2300 view.properties()[0],
2301 DecodedProperty::SaveBonus { .. }
2302 ));
2303 assert!(matches!(
2304 view.properties()[1],
2305 DecodedProperty::SaveBonusSpecific { .. }
2306 ));
2307 assert!(matches!(
2308 view.properties()[2],
2309 DecodedProperty::SavePenalty { .. }
2310 ));
2311 assert!(matches!(
2312 view.properties()[3],
2313 DecodedProperty::SavePenaltySpecific { .. }
2314 ));
2315 }
2316
2317 #[test]
2318 fn save_bonus_subtype_label_resolves_via_iprp_saveelement() {
2319 let propdef =
2322 itempropdef_with_subtypes(&[(26, "ImprovedSavingThrows", "iprp_saveelement")]);
2323 let saveelement = subtype_2da(&[(0, "Universal"), (1, "Acid"), (2, "Cold")]);
2324 let mut tables = TestTables::new();
2325 add_2da_entry(&mut tables, "itempropdef", &propdef);
2326 add_2da_entry(&mut tables, "iprp_saveelement", &saveelement);
2327
2328 let prop = DecodedProperty::SaveBonus {
2329 property_id: 26,
2330 subtype_id: 2,
2331 cost_table: 0,
2332 cost_value: 0,
2333 };
2334 assert_eq!(prop.subtype_label(&mut tables).as_deref(), Some("Cold"));
2335 }
2336
2337 #[test]
2338 fn save_throw_family_subtype_labels_resolve_via_their_2das() {
2339 let propdef = itempropdef_with_subtypes(&[
2345 (26, "ImprovedSavingThrows", "iprp_saveelement"),
2346 (27, "ImprovedSavingThrowsSpecific", "iprp_savingthrow"),
2347 (33, "ReducedSavingThrows", "iprp_saveelement"),
2348 (34, "ReducedSpecificSavingThrow", "iprp_savingthrow"),
2349 ]);
2350 let saveelement = subtype_2da(&[(0, "Universal"), (1, "Acid"), (2, "Cold")]);
2351 let savingthrow = subtype_2da(&[(0, "Fortitude"), (1, "Reflex"), (2, "Will")]);
2352
2353 let mut tables = TestTables::new();
2354 add_2da_entry(&mut tables, "itempropdef", &propdef);
2355 add_2da_entry(&mut tables, "iprp_saveelement", &saveelement);
2356 add_2da_entry(&mut tables, "iprp_savingthrow", &savingthrow);
2357
2358 let specific_bonus = DecodedProperty::SaveBonusSpecific {
2359 property_id: 27,
2360 subtype_id: 2,
2361 cost_table: 0,
2362 cost_value: 0,
2363 };
2364 assert_eq!(
2365 specific_bonus.subtype_label(&mut tables).as_deref(),
2366 Some("Will")
2367 );
2368
2369 let universal_penalty = DecodedProperty::SavePenalty {
2370 property_id: 33,
2371 subtype_id: 1,
2372 cost_table: 0,
2373 cost_value: 0,
2374 };
2375 assert_eq!(
2376 universal_penalty.subtype_label(&mut tables).as_deref(),
2377 Some("Acid")
2378 );
2379
2380 let specific_penalty = DecodedProperty::SavePenaltySpecific {
2381 property_id: 34,
2382 subtype_id: 0,
2383 cost_table: 0,
2384 cost_value: 0,
2385 };
2386 assert_eq!(
2387 specific_penalty.subtype_label(&mut tables).as_deref(),
2388 Some("Fortitude")
2389 );
2390 }
2391
2392 #[test]
2393 fn damage_bonus_label_routes_to_typed_variant() {
2394 let table = itempropdef_with_labels(&[(11, "Damage")]);
2398 let mut tables = TestTables::with("itempropdef", &table);
2399
2400 let mut shaped = property(11, 5);
2401 shaped.cost_table = 4;
2402 shaped.cost_value = 2;
2403 let uti = Uti {
2404 properties: vec![shaped],
2405 ..Uti::default()
2406 };
2407 let view = uti.resolve(&mut tables);
2408
2409 let DecodedProperty::DamageBonus {
2410 property_id,
2411 subtype_id,
2412 cost_table,
2413 cost_value,
2414 } = &view.properties()[0]
2415 else {
2416 panic!("expected DamageBonus variant for `Damage` label");
2417 };
2418 assert_eq!(*property_id, 11);
2419 assert_eq!(*subtype_id, 5);
2420 assert_eq!(*cost_table, 4);
2421 assert_eq!(*cost_value, 2);
2422 }
2423
2424 #[test]
2425 fn damage_bonus_does_not_match_vulnerability_or_unrelated_kinds() {
2426 let table = itempropdef_with_labels(&[(18, "Damage_Vulnerability")]);
2434 let mut tables = TestTables::with("itempropdef", &table);
2435
2436 let uti = Uti {
2437 properties: vec![property(18, 0)],
2438 ..Uti::default()
2439 };
2440 let view = uti.resolve(&mut tables);
2441
2442 let DecodedProperty::Unknown { property_label, .. } = &view.properties()[0] else {
2443 panic!("expected Unknown variant for `Damage_Vulnerability`");
2444 };
2445 assert_eq!(property_label.as_deref(), Some("Damage_Vulnerability"));
2446 }
2447
2448 #[test]
2449 fn damage_immunity_label_routes_to_typed_variant() {
2450 let table = itempropdef_with_labels(&[(14, "DamageImmunity")]);
2451 let mut tables = TestTables::with("itempropdef", &table);
2452
2453 let uti = Uti {
2454 properties: vec![property(14, 7)],
2455 ..Uti::default()
2456 };
2457 let view = uti.resolve(&mut tables);
2458
2459 let DecodedProperty::DamageImmunity {
2460 property_id,
2461 subtype_id,
2462 ..
2463 } = &view.properties()[0]
2464 else {
2465 panic!("expected DamageImmunity variant for `DamageImmunity` label");
2466 };
2467 assert_eq!(*property_id, 14);
2468 assert_eq!(*subtype_id, 7);
2469 }
2470
2471 #[test]
2472 fn damage_resistance_label_routes_to_typed_variant() {
2473 let table = itempropdef_with_labels(&[(17, "DamageResist")]);
2476 let mut tables = TestTables::with("itempropdef", &table);
2477
2478 let uti = Uti {
2479 properties: vec![property(17, 3)],
2480 ..Uti::default()
2481 };
2482 let view = uti.resolve(&mut tables);
2483
2484 let DecodedProperty::DamageResistance {
2485 property_id,
2486 subtype_id,
2487 ..
2488 } = &view.properties()[0]
2489 else {
2490 panic!("expected DamageResistance variant for `DamageResist` label");
2491 };
2492 assert_eq!(*property_id, 17);
2493 assert_eq!(*subtype_id, 3);
2494 }
2495
2496 #[test]
2497 fn damage_resistance_does_not_match_damage_reduced() {
2498 let table = itempropdef_with_labels(&[(16, "DamageReduced")]);
2503 let mut tables = TestTables::with("itempropdef", &table);
2504
2505 let uti = Uti {
2506 properties: vec![property(16, 0)],
2507 ..Uti::default()
2508 };
2509 let view = uti.resolve(&mut tables);
2510
2511 let DecodedProperty::Unknown { property_label, .. } = &view.properties()[0] else {
2512 panic!("expected Unknown variant for `DamageReduced`");
2513 };
2514 assert_eq!(property_label.as_deref(), Some("DamageReduced"));
2515 }
2516
2517 #[test]
2518 fn damage_family_subtype_labels_resolve_via_iprp_damagetype() {
2519 let propdef = itempropdef_with_subtypes(&[
2523 (11, "Damage", "iprp_damagetype"),
2524 (14, "DamageImmunity", "iprp_damagetype"),
2525 (17, "DamageResist", "iprp_damagetype"),
2526 ]);
2527 let damagetype = subtype_2da(&[
2528 (0, "Bludgeoning"),
2529 (1, "Slashing"),
2530 (2, "Piercing"),
2531 (5, "Acid"),
2532 (6, "Cold"),
2533 ]);
2534 let mut tables = TestTables::new();
2535 add_2da_entry(&mut tables, "itempropdef", &propdef);
2536 add_2da_entry(&mut tables, "iprp_damagetype", &damagetype);
2537
2538 let bonus = DecodedProperty::DamageBonus {
2539 property_id: 11,
2540 subtype_id: 5,
2541 cost_table: 0,
2542 cost_value: 0,
2543 };
2544 assert_eq!(bonus.subtype_label(&mut tables).as_deref(), Some("Acid"));
2545
2546 let immunity = DecodedProperty::DamageImmunity {
2547 property_id: 14,
2548 subtype_id: 6,
2549 cost_table: 0,
2550 cost_value: 0,
2551 };
2552 assert_eq!(immunity.subtype_label(&mut tables).as_deref(), Some("Cold"));
2553
2554 let resistance = DecodedProperty::DamageResistance {
2555 property_id: 17,
2556 subtype_id: 1,
2557 cost_table: 0,
2558 cost_value: 0,
2559 };
2560 assert_eq!(
2561 resistance.subtype_label(&mut tables).as_deref(),
2562 Some("Slashing")
2563 );
2564 }
2565
2566 #[test]
2567 fn ac_bonus_label_routes_to_typed_variant() {
2568 let table = itempropdef_with_labels(&[(1, "Armor")]);
2573 let mut tables = TestTables::with("itempropdef", &table);
2574
2575 let mut shaped = property(1, 0);
2576 shaped.cost_table = 2;
2577 shaped.cost_value = 5;
2578 let uti = Uti {
2579 properties: vec![shaped],
2580 ..Uti::default()
2581 };
2582 let view = uti.resolve(&mut tables);
2583
2584 let DecodedProperty::AcBonus {
2585 property_id,
2586 subtype_id,
2587 cost_table,
2588 cost_value,
2589 } = &view.properties()[0]
2590 else {
2591 panic!("expected AcBonus variant for `Armor` label");
2592 };
2593 assert_eq!(*property_id, 1);
2594 assert_eq!(*subtype_id, 0);
2595 assert_eq!(*cost_table, 2);
2596 assert_eq!(*cost_value, 5);
2597 }
2598
2599 #[test]
2600 fn ac_bonus_does_not_match_armor_conditional_kinds() {
2601 let table = itempropdef_with_labels(&[
2606 (2, "ArmorAlignmentGroup"),
2607 (3, "ArmorDamageType"),
2608 (4, "ArmorRacialGroup"),
2609 ]);
2610 let mut tables = TestTables::with("itempropdef", &table);
2611
2612 let uti = Uti {
2613 properties: vec![property(2, 0), property(3, 0), property(4, 0)],
2614 ..Uti::default()
2615 };
2616 let view = uti.resolve(&mut tables);
2617
2618 for prop in view.properties() {
2619 assert!(
2620 matches!(prop, DecodedProperty::Unknown { .. }),
2621 "expected Unknown for non-Armor label, got {prop:?}"
2622 );
2623 }
2624 }
2625
2626 #[test]
2627 fn enhancement_bonus_label_routes_to_typed_variant() {
2628 let table = itempropdef_with_labels(&[(5, "Enhancement")]);
2629 let mut tables = TestTables::with("itempropdef", &table);
2630
2631 let mut shaped = property(5, 0);
2632 shaped.cost_table = 2;
2633 shaped.cost_value = 3;
2634 let uti = Uti {
2635 properties: vec![shaped],
2636 ..Uti::default()
2637 };
2638 let view = uti.resolve(&mut tables);
2639
2640 let DecodedProperty::EnhancementBonus {
2641 property_id,
2642 cost_table,
2643 cost_value,
2644 ..
2645 } = &view.properties()[0]
2646 else {
2647 panic!("expected EnhancementBonus variant for `Enhancement` label");
2648 };
2649 assert_eq!(*property_id, 5);
2650 assert_eq!(*cost_table, 2);
2651 assert_eq!(*cost_value, 3);
2652 }
2653
2654 #[test]
2655 fn enhancement_bonus_does_not_match_its_conditional_siblings() {
2656 let table = itempropdef_with_labels(&[
2661 (6, "EnhancementAlignmentGroup"),
2662 (7, "EnhancementRacialGroup"),
2663 ]);
2664 let mut tables = TestTables::with("itempropdef", &table);
2665
2666 let uti = Uti {
2667 properties: vec![property(6, 0), property(7, 0)],
2668 ..Uti::default()
2669 };
2670 let view = uti.resolve(&mut tables);
2671
2672 assert!(matches!(
2673 view.properties()[0],
2674 DecodedProperty::EnhancementAlignmentGroup { .. }
2675 ));
2676 assert!(matches!(
2677 view.properties()[1],
2678 DecodedProperty::EnhancementRacialGroup { .. }
2679 ));
2680 }
2681
2682 #[test]
2683 fn on_hit_label_routes_to_typed_variant_with_param_fields() {
2684 let table = itempropdef_with_labels(&[(32, "OnHit")]);
2689 let mut tables = TestTables::with("itempropdef", &table);
2690
2691 let mut shaped = property(32, 4);
2692 shaped.cost_table = 25;
2693 shaped.cost_value = 1;
2694 shaped.param1 = 1;
2695 shaped.param1_value = 7;
2696 let uti = Uti {
2697 properties: vec![shaped],
2698 ..Uti::default()
2699 };
2700 let view = uti.resolve(&mut tables);
2701
2702 let DecodedProperty::OnHit {
2703 property_id,
2704 subtype_id,
2705 cost_table,
2706 cost_value,
2707 param1,
2708 param1_value,
2709 } = &view.properties()[0]
2710 else {
2711 panic!("expected OnHit variant for `OnHit` label");
2712 };
2713 assert_eq!(*property_id, 32);
2714 assert_eq!(*subtype_id, 4);
2715 assert_eq!(*cost_table, 25);
2716 assert_eq!(*cost_value, 1);
2717 assert_eq!(*param1, 1);
2718 assert_eq!(*param1_value, 7);
2719 }
2720
2721 #[test]
2722 fn on_hit_does_not_match_on_monster_hit() {
2723 let table = itempropdef_with_labels(&[(48, "OnMonsterHit")]);
2726 let mut tables = TestTables::with("itempropdef", &table);
2727
2728 let uti = Uti {
2729 properties: vec![property(48, 0)],
2730 ..Uti::default()
2731 };
2732 let view = uti.resolve(&mut tables);
2733
2734 let DecodedProperty::Unknown { property_label, .. } = &view.properties()[0] else {
2735 panic!("expected Unknown variant for `OnMonsterHit`");
2736 };
2737 assert_eq!(property_label.as_deref(), Some("OnMonsterHit"));
2738 }
2739
2740 #[test]
2741 fn on_hit_subtype_label_resolves_via_iprp_onhit() {
2742 let propdef = itempropdef_with_subtypes(&[(32, "OnHit", "iprp_onhit")]);
2743 let onhit = subtype_2da(&[(0, "Sleep"), (1, "Daze"), (4, "Stun")]);
2744 let mut tables = TestTables::new();
2745 add_2da_entry(&mut tables, "itempropdef", &propdef);
2746 add_2da_entry(&mut tables, "iprp_onhit", &onhit);
2747
2748 let prop = DecodedProperty::OnHit {
2749 property_id: 32,
2750 subtype_id: 4,
2751 cost_table: 0,
2752 cost_value: 0,
2753 param1: 0,
2754 param1_value: 0,
2755 };
2756 assert_eq!(prop.subtype_label(&mut tables).as_deref(), Some("Stun"));
2757 }
2758
2759 #[test]
2760 fn ac_bonus_subtype_label_returns_none_for_subtypeless_property() {
2761 let propdef = itempropdef_with_subtypes(&[(1, "Armor", "")]);
2765 let mut tables = TestTables::with("itempropdef", &propdef);
2766
2767 let prop = DecodedProperty::AcBonus {
2768 property_id: 1,
2769 subtype_id: 0,
2770 cost_table: 2,
2771 cost_value: 5,
2772 };
2773 assert!(prop.subtype_label(&mut tables).is_none());
2774 }
2775
2776 fn active_property(property_name: u16, subtype: u16) -> UtiProperty {
2777 let mut p = property(property_name, subtype);
2781 p.useable = None;
2782 p.uses_per_day = None;
2783 p
2784 }
2785
2786 #[test]
2787 fn cast_spell_label_routes_to_typed_variant_with_active_defaults() {
2788 let table = itempropdef_with_labels(&[(10, "CastSpell")]);
2793 let mut tables = TestTables::with("itempropdef", &table);
2794
2795 let mut shaped = active_property(10, 42);
2796 shaped.cost_table = 3;
2797 shaped.cost_value = 7;
2798 let uti = Uti {
2799 properties: vec![shaped],
2800 ..Uti::default()
2801 };
2802 let view = uti.resolve(&mut tables);
2803
2804 let DecodedProperty::CastSpell {
2805 property_id,
2806 subtype_id,
2807 cost_table,
2808 cost_value,
2809 useable,
2810 uses_per_day,
2811 } = &view.properties()[0]
2812 else {
2813 panic!("expected CastSpell variant for `CastSpell` label");
2814 };
2815 assert_eq!(*property_id, 10);
2816 assert_eq!(*subtype_id, 42);
2817 assert_eq!(*cost_table, 3);
2818 assert_eq!(*cost_value, 7);
2819 assert!(*useable);
2820 assert!(uses_per_day.is_none());
2821 }
2822
2823 #[test]
2824 fn cast_spell_preserves_explicit_useable_false() {
2825 let table = itempropdef_with_labels(&[(10, "CastSpell")]);
2829 let mut tables = TestTables::with("itempropdef", &table);
2830
2831 let mut shaped = active_property(10, 0);
2832 shaped.useable = Some(false);
2833 let uti = Uti {
2834 properties: vec![shaped],
2835 ..Uti::default()
2836 };
2837 let view = uti.resolve(&mut tables);
2838
2839 let DecodedProperty::CastSpell { useable, .. } = &view.properties()[0] else {
2840 panic!("expected CastSpell variant");
2841 };
2842 assert!(!*useable);
2843 }
2844
2845 #[test]
2846 fn cast_spell_coalesces_uses_per_day_sentinel_into_none() {
2847 let table = itempropdef_with_labels(&[(10, "CastSpell")]);
2851 let mut tables = TestTables::with("itempropdef", &table);
2852
2853 let mut sentinel = active_property(10, 0);
2854 sentinel.uses_per_day = Some(0xFF);
2855 let mut explicit = active_property(10, 0);
2856 explicit.uses_per_day = Some(3);
2857 let uti = Uti {
2858 properties: vec![sentinel, explicit],
2859 ..Uti::default()
2860 };
2861 let view = uti.resolve(&mut tables);
2862
2863 let DecodedProperty::CastSpell { uses_per_day, .. } = &view.properties()[0] else {
2864 panic!("expected CastSpell variant for sentinel");
2865 };
2866 assert!(uses_per_day.is_none());
2867
2868 let DecodedProperty::CastSpell { uses_per_day, .. } = &view.properties()[1] else {
2869 panic!("expected CastSpell variant for explicit cap");
2870 };
2871 assert_eq!(*uses_per_day, Some(3));
2872 }
2873
2874 #[test]
2875 fn cast_spell_subtype_label_resolves_via_spells_2da() {
2876 let propdef = itempropdef_with_subtypes(&[(10, "CastSpell", "spells")]);
2877 let spells = subtype_2da(&[(0, "Cure_Wounds"), (5, "Force_Push")]);
2878 let mut tables = TestTables::new();
2879 add_2da_entry(&mut tables, "itempropdef", &propdef);
2880 add_2da_entry(&mut tables, "spells", &spells);
2881
2882 let prop = DecodedProperty::CastSpell {
2883 property_id: 10,
2884 subtype_id: 5,
2885 cost_table: 0,
2886 cost_value: 0,
2887 useable: true,
2888 uses_per_day: None,
2889 };
2890 assert_eq!(
2891 prop.subtype_label(&mut tables).as_deref(),
2892 Some("Force_Push")
2893 );
2894 }
2895
2896 #[test]
2897 fn trap_label_routes_to_typed_variant() {
2898 let table = itempropdef_with_labels(&[(46, "Trap")]);
2899 let mut tables = TestTables::with("itempropdef", &table);
2900
2901 let mut shaped = active_property(46, 2);
2902 shaped.uses_per_day = Some(1);
2903 let uti = Uti {
2904 properties: vec![shaped],
2905 ..Uti::default()
2906 };
2907 let view = uti.resolve(&mut tables);
2908
2909 let DecodedProperty::Trap {
2910 property_id,
2911 subtype_id,
2912 useable,
2913 uses_per_day,
2914 ..
2915 } = &view.properties()[0]
2916 else {
2917 panic!("expected Trap variant for `Trap` label");
2918 };
2919 assert_eq!(*property_id, 46);
2920 assert_eq!(*subtype_id, 2);
2921 assert!(*useable);
2922 assert_eq!(*uses_per_day, Some(1));
2923 }
2924
2925 #[test]
2926 fn trap_subtype_label_resolves_via_traps_2da() {
2927 let propdef = itempropdef_with_subtypes(&[(46, "Trap", "traps")]);
2928 let traps = subtype_2da(&[(0, "Minor_Frag"), (3, "Deadly_Plasma")]);
2929 let mut tables = TestTables::new();
2930 add_2da_entry(&mut tables, "itempropdef", &propdef);
2931 add_2da_entry(&mut tables, "traps", &traps);
2932
2933 let prop = DecodedProperty::Trap {
2934 property_id: 46,
2935 subtype_id: 3,
2936 cost_table: 0,
2937 cost_value: 0,
2938 useable: true,
2939 uses_per_day: Some(1),
2940 };
2941 assert_eq!(
2942 prop.subtype_label(&mut tables).as_deref(),
2943 Some("Deadly_Plasma")
2944 );
2945 }
2946
2947 #[test]
2948 fn thieves_tools_label_routes_to_typed_variant_with_active_defaults() {
2949 let table = itempropdef_with_labels(&[(37, "ThievesTools")]);
2954 let mut tables = TestTables::with("itempropdef", &table);
2955
2956 let mut shaped = active_property(37, 0);
2957 shaped.cost_table = 1;
2958 shaped.cost_value = 4;
2959 shaped.uses_per_day = Some(0xFF);
2960 let uti = Uti {
2961 properties: vec![shaped],
2962 ..Uti::default()
2963 };
2964 let view = uti.resolve(&mut tables);
2965
2966 let DecodedProperty::ThievesTools {
2967 property_id,
2968 cost_table,
2969 cost_value,
2970 useable,
2971 uses_per_day,
2972 ..
2973 } = &view.properties()[0]
2974 else {
2975 panic!("expected ThievesTools variant for `ThievesTools` label");
2976 };
2977 assert_eq!(*property_id, 37);
2978 assert_eq!(*cost_table, 1);
2979 assert_eq!(*cost_value, 4);
2980 assert!(*useable);
2981 assert!(uses_per_day.is_none());
2982 }
2983
2984 #[test]
2985 fn thieves_tools_subtype_label_returns_none_for_subtypeless_property() {
2986 let propdef = itempropdef_with_subtypes(&[(37, "ThievesTools", "")]);
2990 let mut tables = TestTables::with("itempropdef", &propdef);
2991
2992 let prop = DecodedProperty::ThievesTools {
2993 property_id: 37,
2994 subtype_id: 0,
2995 cost_table: 1,
2996 cost_value: 4,
2997 useable: true,
2998 uses_per_day: None,
2999 };
3000 assert!(prop.subtype_label(&mut tables).is_none());
3001 }
3002
3003 #[test]
3004 fn computer_spike_label_routes_to_typed_variant() {
3005 let table = itempropdef_with_labels(&[(53, "Computer_Spike")]);
3011 let mut tables = TestTables::with("itempropdef", &table);
3012
3013 let mut shaped = active_property(53, 0);
3014 shaped.cost_table = 1;
3015 shaped.cost_value = 5;
3016 shaped.useable = Some(true);
3017 shaped.uses_per_day = Some(2);
3018 let uti = Uti {
3019 properties: vec![shaped],
3020 ..Uti::default()
3021 };
3022 let view = uti.resolve(&mut tables);
3023
3024 let DecodedProperty::ComputerSpike {
3025 property_id,
3026 cost_table,
3027 cost_value,
3028 useable,
3029 uses_per_day,
3030 ..
3031 } = &view.properties()[0]
3032 else {
3033 panic!("expected ComputerSpike variant for `Computer_Spike` label");
3034 };
3035 assert_eq!(*property_id, 53);
3036 assert_eq!(*cost_table, 1);
3037 assert_eq!(*cost_value, 5);
3038 assert!(*useable);
3039 assert_eq!(*uses_per_day, Some(2));
3040 }
3041
3042 #[test]
3043 fn computer_spike_subtype_label_returns_none_for_subtypeless_property() {
3044 let propdef = itempropdef_with_subtypes(&[(53, "Computer_Spike", "")]);
3045 let mut tables = TestTables::with("itempropdef", &propdef);
3046
3047 let prop = DecodedProperty::ComputerSpike {
3048 property_id: 53,
3049 subtype_id: 0,
3050 cost_table: 1,
3051 cost_value: 5,
3052 useable: true,
3053 uses_per_day: Some(2),
3054 };
3055 assert!(prop.subtype_label(&mut tables).is_none());
3056 }
3057
3058 #[test]
3059 fn attack_bonus_label_routes_to_typed_variant() {
3060 let table = itempropdef_with_labels(&[(38, "AttackBonus")]);
3063 let mut tables = TestTables::with("itempropdef", &table);
3064
3065 let mut shaped = property(38, 0);
3066 shaped.cost_table = 2;
3067 shaped.cost_value = 3;
3068 let uti = Uti {
3069 properties: vec![shaped],
3070 ..Uti::default()
3071 };
3072 let view = uti.resolve(&mut tables);
3073
3074 let DecodedProperty::AttackBonus {
3075 property_id,
3076 cost_table,
3077 cost_value,
3078 ..
3079 } = &view.properties()[0]
3080 else {
3081 panic!("expected AttackBonus variant for `AttackBonus` label");
3082 };
3083 assert_eq!(*property_id, 38);
3084 assert_eq!(*cost_table, 2);
3085 assert_eq!(*cost_value, 3);
3086 }
3087
3088 #[test]
3089 fn attack_bonus_does_not_match_its_conditional_siblings() {
3090 let table = itempropdef_with_labels(&[
3097 (39, "AttackBonusAlignmentGroup"),
3098 (40, "AttackBonusRacialGroup"),
3099 ]);
3100 let mut tables = TestTables::with("itempropdef", &table);
3101
3102 let uti = Uti {
3103 properties: vec![property(39, 0), property(40, 0)],
3104 ..Uti::default()
3105 };
3106 let view = uti.resolve(&mut tables);
3107
3108 assert!(matches!(
3109 view.properties()[0],
3110 DecodedProperty::AttackBonusAlignmentGroup { .. }
3111 ));
3112 let DecodedProperty::Unknown { property_label, .. } = &view.properties()[1] else {
3113 panic!("expected Unknown for `AttackBonusRacialGroup`");
3114 };
3115 assert_eq!(property_label.as_deref(), Some("AttackBonusRacialGroup"));
3116 }
3117
3118 #[test]
3119 fn keen_label_routes_to_typed_variant() {
3120 let table = itempropdef_with_labels(&[(28, "Keen")]);
3121 let mut tables = TestTables::with("itempropdef", &table);
3122
3123 let uti = Uti {
3124 properties: vec![property(28, 0)],
3125 ..Uti::default()
3126 };
3127 let view = uti.resolve(&mut tables);
3128
3129 let DecodedProperty::Keen { property_id, .. } = &view.properties()[0] else {
3130 panic!("expected Keen variant for `Keen` label");
3131 };
3132 assert_eq!(*property_id, 28);
3133 }
3134
3135 #[test]
3136 fn massive_criticals_label_routes_to_typed_variant() {
3137 let table = itempropdef_with_labels(&[(49, "Massive_Criticals")]);
3141 let mut tables = TestTables::with("itempropdef", &table);
3142
3143 let uti = Uti {
3144 properties: vec![property(49, 0)],
3145 ..Uti::default()
3146 };
3147 let view = uti.resolve(&mut tables);
3148
3149 let DecodedProperty::MassiveCriticals { property_id, .. } = &view.properties()[0] else {
3150 panic!("expected MassiveCriticals variant for `Massive_Criticals` label");
3151 };
3152 assert_eq!(*property_id, 49);
3153 }
3154
3155 #[test]
3156 fn blaster_bolt_deflect_increase_label_routes_to_typed_variant() {
3157 let table = itempropdef_with_labels(&[(55, "Blaster_Bolt_Deflect_Increase")]);
3158 let mut tables = TestTables::with("itempropdef", &table);
3159
3160 let uti = Uti {
3161 properties: vec![property(55, 0)],
3162 ..Uti::default()
3163 };
3164 let view = uti.resolve(&mut tables);
3165
3166 let DecodedProperty::BlasterBoltDeflectIncrease { property_id, .. } = &view.properties()[0]
3167 else {
3168 panic!("expected BlasterBoltDeflectIncrease variant for the vanilla label");
3169 };
3170 assert_eq!(*property_id, 55);
3171 }
3172
3173 #[test]
3174 fn blaster_bolt_deflect_does_not_absorb_vanilla_typo_sibling() {
3175 let table = itempropdef_with_labels(&[(56, "Blaster_Bolt_Defect_Decrease")]);
3182 let mut tables = TestTables::with("itempropdef", &table);
3183
3184 let uti = Uti {
3185 properties: vec![property(56, 0)],
3186 ..Uti::default()
3187 };
3188 let view = uti.resolve(&mut tables);
3189
3190 let DecodedProperty::Unknown { property_label, .. } = &view.properties()[0] else {
3191 panic!("expected Unknown for the vanilla-typo sibling");
3192 };
3193 assert_eq!(
3194 property_label.as_deref(),
3195 Some("Blaster_Bolt_Defect_Decrease")
3196 );
3197 }
3198
3199 #[test]
3200 fn monster_damage_label_routes_to_typed_variant() {
3201 let table = itempropdef_with_labels(&[(51, "Monster_damage")]);
3204 let mut tables = TestTables::with("itempropdef", &table);
3205
3206 let uti = Uti {
3207 properties: vec![property(51, 0)],
3208 ..Uti::default()
3209 };
3210 let view = uti.resolve(&mut tables);
3211
3212 let DecodedProperty::MonsterDamage { property_id, .. } = &view.properties()[0] else {
3213 panic!("expected MonsterDamage variant for `Monster_damage` label");
3214 };
3215 assert_eq!(*property_id, 51);
3216 }
3217
3218 #[test]
3219 fn subtypeless_single_magnitude_variants_short_circuit_subtype_label() {
3220 let propdef = itempropdef_with_subtypes(&[
3224 (28, "Keen", ""),
3225 (38, "AttackBonus", ""),
3226 (49, "Massive_Criticals", ""),
3227 (51, "Monster_damage", ""),
3228 (55, "Blaster_Bolt_Deflect_Increase", ""),
3229 ]);
3230 let mut tables = TestTables::with("itempropdef", &propdef);
3231
3232 for prop in [
3233 DecodedProperty::Keen {
3234 property_id: 28,
3235 subtype_id: 0,
3236 cost_table: 0,
3237 cost_value: 0,
3238 },
3239 DecodedProperty::AttackBonus {
3240 property_id: 38,
3241 subtype_id: 0,
3242 cost_table: 0,
3243 cost_value: 0,
3244 },
3245 DecodedProperty::MassiveCriticals {
3246 property_id: 49,
3247 subtype_id: 0,
3248 cost_table: 0,
3249 cost_value: 0,
3250 },
3251 DecodedProperty::MonsterDamage {
3252 property_id: 51,
3253 subtype_id: 0,
3254 cost_table: 0,
3255 cost_value: 0,
3256 },
3257 DecodedProperty::BlasterBoltDeflectIncrease {
3258 property_id: 55,
3259 subtype_id: 0,
3260 cost_table: 0,
3261 cost_value: 0,
3262 },
3263 ] {
3264 assert!(
3265 prop.subtype_label(&mut tables).is_none(),
3266 "expected None subtype_label for subtypeless variant, got {prop:?}"
3267 );
3268 }
3269 }
3270
3271 #[test]
3272 fn bonus_feats_label_routes_to_typed_variant() {
3273 let table = itempropdef_with_labels(&[(9, "BonusFeats")]);
3274 let mut tables = TestTables::with("itempropdef", &table);
3275
3276 let uti = Uti {
3277 properties: vec![property(9, 12)],
3278 ..Uti::default()
3279 };
3280 let view = uti.resolve(&mut tables);
3281
3282 let DecodedProperty::BonusFeats {
3283 property_id,
3284 subtype_id,
3285 ..
3286 } = &view.properties()[0]
3287 else {
3288 panic!("expected BonusFeats variant for `BonusFeats` label");
3289 };
3290 assert_eq!(*property_id, 9);
3291 assert_eq!(*subtype_id, 12);
3292 }
3293
3294 #[test]
3295 fn immunity_label_routes_to_typed_variant() {
3296 let table = itempropdef_with_labels(&[(24, "Immunity")]);
3297 let mut tables = TestTables::with("itempropdef", &table);
3298
3299 let uti = Uti {
3300 properties: vec![property(24, 5)],
3301 ..Uti::default()
3302 };
3303 let view = uti.resolve(&mut tables);
3304
3305 let DecodedProperty::Immunity {
3306 property_id,
3307 subtype_id,
3308 ..
3309 } = &view.properties()[0]
3310 else {
3311 panic!("expected Immunity variant for `Immunity` label");
3312 };
3313 assert_eq!(*property_id, 24);
3314 assert_eq!(*subtype_id, 5);
3315 }
3316
3317 #[test]
3318 fn skill_label_routes_to_typed_variant() {
3319 let table = itempropdef_with_labels(&[(36, "Skill")]);
3320 let mut tables = TestTables::with("itempropdef", &table);
3321
3322 let uti = Uti {
3323 properties: vec![property(36, 2)],
3324 ..Uti::default()
3325 };
3326 let view = uti.resolve(&mut tables);
3327
3328 let DecodedProperty::Skill {
3329 property_id,
3330 subtype_id,
3331 ..
3332 } = &view.properties()[0]
3333 else {
3334 panic!("expected Skill variant for `Skill` label");
3335 };
3336 assert_eq!(*property_id, 36);
3337 assert_eq!(*subtype_id, 2);
3338 }
3339
3340 #[test]
3341 fn skill_does_not_match_decreased_skill_sibling() {
3342 let table = itempropdef_with_labels(&[(21, "DecreasedSkill")]);
3347 let mut tables = TestTables::with("itempropdef", &table);
3348
3349 let uti = Uti {
3350 properties: vec![property(21, 0)],
3351 ..Uti::default()
3352 };
3353 let view = uti.resolve(&mut tables);
3354
3355 let DecodedProperty::Unknown { property_label, .. } = &view.properties()[0] else {
3356 panic!("expected Unknown for `DecreasedSkill`");
3357 };
3358 assert_eq!(property_label.as_deref(), Some("DecreasedSkill"));
3359 }
3360
3361 #[test]
3362 fn misc_2da_backed_singleton_subtype_labels_resolve_via_their_2das() {
3363 let propdef = itempropdef_with_subtypes(&[
3367 (9, "BonusFeats", "feat"),
3368 (24, "Immunity", "iprp_immunity"),
3369 (36, "Skill", "skills"),
3370 ]);
3371 let feats = subtype_2da(&[(0, "Toughness"), (12, "Force_Sensitive")]);
3372 let immunities = subtype_2da(&[(0, "Mind_Affecting"), (5, "Paralysis")]);
3373 let skills = subtype_2da(&[(0, "Computer_Use"), (2, "Persuade")]);
3374
3375 let mut tables = TestTables::new();
3376 add_2da_entry(&mut tables, "itempropdef", &propdef);
3377 add_2da_entry(&mut tables, "feat", &feats);
3378 add_2da_entry(&mut tables, "iprp_immunity", &immunities);
3379 add_2da_entry(&mut tables, "skills", &skills);
3380
3381 let feat = DecodedProperty::BonusFeats {
3382 property_id: 9,
3383 subtype_id: 12,
3384 cost_table: 0,
3385 cost_value: 0,
3386 };
3387 assert_eq!(
3388 feat.subtype_label(&mut tables).as_deref(),
3389 Some("Force_Sensitive")
3390 );
3391
3392 let immunity = DecodedProperty::Immunity {
3393 property_id: 24,
3394 subtype_id: 5,
3395 cost_table: 0,
3396 cost_value: 0,
3397 };
3398 assert_eq!(
3399 immunity.subtype_label(&mut tables).as_deref(),
3400 Some("Paralysis")
3401 );
3402
3403 let skill = DecodedProperty::Skill {
3404 property_id: 36,
3405 subtype_id: 2,
3406 cost_table: 0,
3407 cost_value: 0,
3408 };
3409 assert_eq!(
3410 skill.subtype_label(&mut tables).as_deref(),
3411 Some("Persuade")
3412 );
3413 }
3414
3415 #[test]
3416 fn use_limitation_feat_label_routes_to_typed_variant() {
3417 let table = itempropdef_with_labels(&[(57, "Use_Limitation_Feat")]);
3422 let mut tables = TestTables::with("itempropdef", &table);
3423
3424 let uti = Uti {
3425 properties: vec![property(57, 42)],
3426 ..Uti::default()
3427 };
3428 let view = uti.resolve(&mut tables);
3429
3430 let DecodedProperty::UseLimitationFeat {
3431 property_id,
3432 subtype_id,
3433 ..
3434 } = &view.properties()[0]
3435 else {
3436 panic!("expected UseLimitationFeat variant for `Use_Limitation_Feat` label");
3437 };
3438 assert_eq!(*property_id, 57);
3439 assert_eq!(*subtype_id, 42);
3440 }
3441
3442 #[test]
3443 fn use_limitation_racial_label_routes_to_typed_variant() {
3444 let table = itempropdef_with_labels(&[(45, "UseLimitationRacial")]);
3445 let mut tables = TestTables::with("itempropdef", &table);
3446
3447 let uti = Uti {
3448 properties: vec![property(45, 3)],
3449 ..Uti::default()
3450 };
3451 let view = uti.resolve(&mut tables);
3452
3453 let DecodedProperty::UseLimitationRacial {
3454 property_id,
3455 subtype_id,
3456 ..
3457 } = &view.properties()[0]
3458 else {
3459 panic!("expected UseLimitationRacial variant for `UseLimitationRacial` label");
3460 };
3461 assert_eq!(*property_id, 45);
3462 assert_eq!(*subtype_id, 3);
3463 }
3464
3465 #[test]
3466 fn use_limitation_alignment_group_label_routes_to_typed_variant() {
3467 let table = itempropdef_with_labels(&[(43, "UseLimitationAlignmentGroup")]);
3468 let mut tables = TestTables::with("itempropdef", &table);
3469
3470 let uti = Uti {
3471 properties: vec![property(43, 2)],
3472 ..Uti::default()
3473 };
3474 let view = uti.resolve(&mut tables);
3475
3476 let DecodedProperty::UseLimitationAlignmentGroup {
3477 property_id,
3478 subtype_id,
3479 ..
3480 } = &view.properties()[0]
3481 else {
3482 panic!(
3483 "expected UseLimitationAlignmentGroup variant for \
3484 `UseLimitationAlignmentGroup` label"
3485 );
3486 };
3487 assert_eq!(*property_id, 43);
3488 assert_eq!(*subtype_id, 2);
3489 }
3490
3491 #[test]
3492 fn use_limitation_does_not_match_unused_class_sibling() {
3493 let table = itempropdef_with_labels(&[(44, "UseLimitationClass")]);
3498 let mut tables = TestTables::with("itempropdef", &table);
3499
3500 let uti = Uti {
3501 properties: vec![property(44, 0)],
3502 ..Uti::default()
3503 };
3504 let view = uti.resolve(&mut tables);
3505
3506 let DecodedProperty::Unknown { property_label, .. } = &view.properties()[0] else {
3507 panic!("expected Unknown variant for `UseLimitationClass`");
3508 };
3509 assert_eq!(property_label.as_deref(), Some("UseLimitationClass"));
3510 }
3511
3512 #[test]
3513 fn damage_racial_group_label_routes_to_typed_variant() {
3514 let table = itempropdef_with_labels(&[(13, "DamageRacialGroup")]);
3515 let mut tables = TestTables::with("itempropdef", &table);
3516
3517 let mut shaped = property(13, 3);
3518 shaped.cost_table = 4;
3519 shaped.cost_value = 2;
3520 let uti = Uti {
3521 properties: vec![shaped],
3522 ..Uti::default()
3523 };
3524 let view = uti.resolve(&mut tables);
3525
3526 let DecodedProperty::DamageRacialGroup {
3527 property_id,
3528 subtype_id,
3529 cost_table,
3530 cost_value,
3531 } = &view.properties()[0]
3532 else {
3533 panic!("expected DamageRacialGroup variant for `DamageRacialGroup` label");
3534 };
3535 assert_eq!(*property_id, 13);
3536 assert_eq!(*subtype_id, 3);
3537 assert_eq!(*cost_table, 4);
3538 assert_eq!(*cost_value, 2);
3539 }
3540
3541 #[test]
3542 fn damage_alignment_group_label_routes_to_typed_variant() {
3543 let table = itempropdef_with_labels(&[(12, "DamageAlignmentGroup")]);
3544 let mut tables = TestTables::with("itempropdef", &table);
3545
3546 let uti = Uti {
3547 properties: vec![property(12, 4)],
3548 ..Uti::default()
3549 };
3550 let view = uti.resolve(&mut tables);
3551
3552 let DecodedProperty::DamageAlignmentGroup {
3553 property_id,
3554 subtype_id,
3555 ..
3556 } = &view.properties()[0]
3557 else {
3558 panic!("expected DamageAlignmentGroup variant for `DamageAlignmentGroup` label");
3559 };
3560 assert_eq!(*property_id, 12);
3561 assert_eq!(*subtype_id, 4);
3562 }
3563
3564 #[test]
3565 fn enhancement_racial_group_label_routes_to_typed_variant() {
3566 let table = itempropdef_with_labels(&[(7, "EnhancementRacialGroup")]);
3567 let mut tables = TestTables::with("itempropdef", &table);
3568
3569 let uti = Uti {
3570 properties: vec![property(7, 3)],
3571 ..Uti::default()
3572 };
3573 let view = uti.resolve(&mut tables);
3574
3575 let DecodedProperty::EnhancementRacialGroup {
3576 property_id,
3577 subtype_id,
3578 ..
3579 } = &view.properties()[0]
3580 else {
3581 panic!("expected EnhancementRacialGroup variant for `EnhancementRacialGroup` label");
3582 };
3583 assert_eq!(*property_id, 7);
3584 assert_eq!(*subtype_id, 3);
3585 }
3586
3587 #[test]
3588 fn conditional_bonus_family_subtype_labels_resolve_via_their_2das() {
3589 let propdef = itempropdef_with_subtypes(&[
3594 (7, "EnhancementRacialGroup", "racialtypes"),
3595 (12, "DamageAlignmentGroup", "iprp_aligngrp"),
3596 (13, "DamageRacialGroup", "racialtypes"),
3597 ]);
3598 let racial = subtype_2da(&[(0, "Human"), (3, "Wookiee")]);
3599 let aligngrp = subtype_2da(&[(0, "Lawful_Good"), (4, "Chaotic_Evil")]);
3600
3601 let mut tables = TestTables::new();
3602 add_2da_entry(&mut tables, "itempropdef", &propdef);
3603 add_2da_entry(&mut tables, "racialtypes", &racial);
3604 add_2da_entry(&mut tables, "iprp_aligngrp", &aligngrp);
3605
3606 let damage_racial = DecodedProperty::DamageRacialGroup {
3607 property_id: 13,
3608 subtype_id: 3,
3609 cost_table: 0,
3610 cost_value: 0,
3611 };
3612 assert_eq!(
3613 damage_racial.subtype_label(&mut tables).as_deref(),
3614 Some("Wookiee")
3615 );
3616
3617 let damage_alignment = DecodedProperty::DamageAlignmentGroup {
3618 property_id: 12,
3619 subtype_id: 4,
3620 cost_table: 0,
3621 cost_value: 0,
3622 };
3623 assert_eq!(
3624 damage_alignment.subtype_label(&mut tables).as_deref(),
3625 Some("Chaotic_Evil")
3626 );
3627
3628 let enhancement_racial = DecodedProperty::EnhancementRacialGroup {
3629 property_id: 7,
3630 subtype_id: 3,
3631 cost_table: 0,
3632 cost_value: 0,
3633 };
3634 assert_eq!(
3635 enhancement_racial.subtype_label(&mut tables).as_deref(),
3636 Some("Wookiee")
3637 );
3638 }
3639
3640 #[test]
3641 fn use_limitation_family_subtype_labels_resolve_via_their_2das() {
3642 let propdef = itempropdef_with_subtypes(&[
3646 (43, "UseLimitationAlignmentGroup", "iprp_aligngrp"),
3647 (45, "UseLimitationRacial", "racialtypes"),
3648 (57, "Use_Limitation_Feat", "feat"),
3649 ]);
3650 let aligngrp = subtype_2da(&[(0, "Lawful_Good"), (4, "Chaotic_Evil")]);
3651 let racial = subtype_2da(&[(0, "Human"), (3, "Wookiee")]);
3652 let feats = subtype_2da(&[(0, "Toughness"), (42, "Force_Sensitive")]);
3653
3654 let mut tables = TestTables::new();
3655 add_2da_entry(&mut tables, "itempropdef", &propdef);
3656 add_2da_entry(&mut tables, "iprp_aligngrp", &aligngrp);
3657 add_2da_entry(&mut tables, "racialtypes", &racial);
3658 add_2da_entry(&mut tables, "feat", &feats);
3659
3660 let alignment = DecodedProperty::UseLimitationAlignmentGroup {
3661 property_id: 43,
3662 subtype_id: 4,
3663 cost_table: 0,
3664 cost_value: 0,
3665 };
3666 assert_eq!(
3667 alignment.subtype_label(&mut tables).as_deref(),
3668 Some("Chaotic_Evil")
3669 );
3670
3671 let racial = DecodedProperty::UseLimitationRacial {
3672 property_id: 45,
3673 subtype_id: 3,
3674 cost_table: 0,
3675 cost_value: 0,
3676 };
3677 assert_eq!(
3678 racial.subtype_label(&mut tables).as_deref(),
3679 Some("Wookiee")
3680 );
3681
3682 let feat = DecodedProperty::UseLimitationFeat {
3683 property_id: 57,
3684 subtype_id: 42,
3685 cost_table: 0,
3686 cost_value: 0,
3687 };
3688 assert_eq!(
3689 feat.subtype_label(&mut tables).as_deref(),
3690 Some("Force_Sensitive")
3691 );
3692 }
3693
3694 #[test]
3695 fn low_volume_catchall_variants_route_to_typed_variants() {
3696 let table = itempropdef_with_labels(&[
3702 (8, "AttackPenalty"),
3703 (15, "DamagePenalty"),
3704 (25, "ImprovedMagicResist"),
3705 (31, "DamageNone"),
3706 (35, "Regeneration"),
3707 (54, "Regeneration_Force_Points"),
3708 (59, "Disguise"),
3709 ]);
3710 let mut tables = TestTables::with("itempropdef", &table);
3711
3712 let uti = Uti {
3713 properties: vec![
3714 property(8, 0),
3715 property(15, 0),
3716 property(25, 0),
3717 property(31, 0),
3718 property(35, 0),
3719 property(54, 0),
3720 property(59, 7),
3721 ],
3722 ..Uti::default()
3723 };
3724 let view = uti.resolve(&mut tables);
3725
3726 assert!(matches!(
3727 view.properties()[0],
3728 DecodedProperty::AttackPenalty { .. }
3729 ));
3730 assert!(matches!(
3731 view.properties()[1],
3732 DecodedProperty::DamagePenalty { .. }
3733 ));
3734 assert!(matches!(
3735 view.properties()[2],
3736 DecodedProperty::MagicResistBonus { .. }
3737 ));
3738 assert!(matches!(
3739 view.properties()[3],
3740 DecodedProperty::DamageNone { .. }
3741 ));
3742 assert!(matches!(
3743 view.properties()[4],
3744 DecodedProperty::Regeneration { .. }
3745 ));
3746 assert!(matches!(
3747 view.properties()[5],
3748 DecodedProperty::RegenerationForcePoints { .. }
3749 ));
3750 let DecodedProperty::Disguise { subtype_id, .. } = &view.properties()[6] else {
3751 panic!("expected Disguise variant for `Disguise` label");
3752 };
3753 assert_eq!(*subtype_id, 7);
3754 }
3755
3756 #[test]
3757 fn low_volume_subtypeless_variants_short_circuit_subtype_label() {
3758 let propdef = itempropdef_with_subtypes(&[
3761 (8, "AttackPenalty", ""),
3762 (15, "DamagePenalty", ""),
3763 (25, "ImprovedMagicResist", ""),
3764 (31, "DamageNone", ""),
3765 (35, "Regeneration", ""),
3766 (54, "Regeneration_Force_Points", ""),
3767 ]);
3768 let mut tables = TestTables::with("itempropdef", &propdef);
3769
3770 for prop in [
3771 DecodedProperty::AttackPenalty {
3772 property_id: 8,
3773 subtype_id: 0,
3774 cost_table: 0,
3775 cost_value: 0,
3776 },
3777 DecodedProperty::DamagePenalty {
3778 property_id: 15,
3779 subtype_id: 0,
3780 cost_table: 0,
3781 cost_value: 0,
3782 },
3783 DecodedProperty::MagicResistBonus {
3784 property_id: 25,
3785 subtype_id: 0,
3786 cost_table: 0,
3787 cost_value: 0,
3788 },
3789 DecodedProperty::DamageNone {
3790 property_id: 31,
3791 subtype_id: 0,
3792 cost_table: 0,
3793 cost_value: 0,
3794 },
3795 DecodedProperty::Regeneration {
3796 property_id: 35,
3797 subtype_id: 0,
3798 cost_table: 0,
3799 cost_value: 0,
3800 },
3801 DecodedProperty::RegenerationForcePoints {
3802 property_id: 54,
3803 subtype_id: 0,
3804 cost_table: 0,
3805 cost_value: 0,
3806 },
3807 ] {
3808 assert!(
3809 prop.subtype_label(&mut tables).is_none(),
3810 "expected None subtype_label for subtypeless variant, got {prop:?}"
3811 );
3812 }
3813 }
3814
3815 #[test]
3816 fn disguise_subtype_label_resolves_via_appearance_2da() {
3817 let propdef = itempropdef_with_subtypes(&[(59, "Disguise", "appearance")]);
3818 let appearance = subtype_2da(&[(0, "Human"), (7, "Tusken_Raider")]);
3819 let mut tables = TestTables::new();
3820 add_2da_entry(&mut tables, "itempropdef", &propdef);
3821 add_2da_entry(&mut tables, "appearance", &appearance);
3822
3823 let prop = DecodedProperty::Disguise {
3824 property_id: 59,
3825 subtype_id: 7,
3826 cost_table: 0,
3827 cost_value: 0,
3828 };
3829 assert_eq!(
3830 prop.subtype_label(&mut tables).as_deref(),
3831 Some("Tusken_Raider")
3832 );
3833 }
3834
3835 #[test]
3836 fn newly_visible_alignment_and_special_variants_route_to_typed() {
3837 let table = itempropdef_with_labels(&[
3845 (6, "EnhancementAlignmentGroup"),
3846 (29, "Light"),
3847 (39, "AttackBonusAlignmentGroup"),
3848 (47, "True_Seeing"),
3849 ]);
3850 let mut tables = TestTables::with("itempropdef", &table);
3851
3852 let mut light_prop = property(29, 0);
3853 light_prop.param1 = 9;
3854 light_prop.param1_value = 4;
3855 let uti = Uti {
3856 properties: vec![property(6, 2), light_prop, property(39, 3), property(47, 0)],
3857 ..Uti::default()
3858 };
3859 let view = uti.resolve(&mut tables);
3860
3861 let DecodedProperty::EnhancementAlignmentGroup {
3862 subtype_id: ealignment_subtype,
3863 ..
3864 } = &view.properties()[0]
3865 else {
3866 panic!("expected EnhancementAlignmentGroup for row 6");
3867 };
3868 assert_eq!(*ealignment_subtype, 2);
3869
3870 let DecodedProperty::Light {
3871 param1,
3872 param1_value,
3873 ..
3874 } = &view.properties()[1]
3875 else {
3876 panic!("expected Light for row 29");
3877 };
3878 assert_eq!(*param1, 9);
3879 assert_eq!(*param1_value, 4);
3880
3881 let DecodedProperty::AttackBonusAlignmentGroup {
3882 subtype_id: ab_alignment_subtype,
3883 ..
3884 } = &view.properties()[2]
3885 else {
3886 panic!("expected AttackBonusAlignmentGroup for row 39");
3887 };
3888 assert_eq!(*ab_alignment_subtype, 3);
3889
3890 assert!(matches!(
3891 view.properties()[3],
3892 DecodedProperty::TrueSeeing { .. }
3893 ));
3894 }
3895
3896 #[test]
3897 fn alignment_group_variants_subtype_labels_resolve_via_iprp_aligngrp() {
3898 let propdef = itempropdef_with_subtypes(&[
3904 (6, "EnhancementAlignmentGroup", "iprp_aligngrp"),
3905 (39, "AttackBonusAlignmentGroup", "iprp_aligngrp"),
3906 ]);
3907 let aligngrp = subtype_2da(&[(0, "Lawful_Good"), (2, "Neutral"), (4, "Chaotic_Evil")]);
3908
3909 let mut tables = TestTables::new();
3910 add_2da_entry(&mut tables, "itempropdef", &propdef);
3911 add_2da_entry(&mut tables, "iprp_aligngrp", &aligngrp);
3912
3913 let enhancement = DecodedProperty::EnhancementAlignmentGroup {
3914 property_id: 6,
3915 subtype_id: 4,
3916 cost_table: 0,
3917 cost_value: 0,
3918 };
3919 assert_eq!(
3920 enhancement.subtype_label(&mut tables).as_deref(),
3921 Some("Chaotic_Evil")
3922 );
3923
3924 let attack = DecodedProperty::AttackBonusAlignmentGroup {
3925 property_id: 39,
3926 subtype_id: 0,
3927 cost_table: 0,
3928 cost_value: 0,
3929 };
3930 assert_eq!(
3931 attack.subtype_label(&mut tables).as_deref(),
3932 Some("Lawful_Good")
3933 );
3934 }
3935
3936 #[test]
3937 fn light_and_true_seeing_short_circuit_subtype_label() {
3938 let propdef = itempropdef_with_subtypes(&[(29, "Light", ""), (47, "True_Seeing", "")]);
3941 let mut tables = TestTables::with("itempropdef", &propdef);
3942
3943 let light = DecodedProperty::Light {
3944 property_id: 29,
3945 subtype_id: 0,
3946 cost_table: 0,
3947 cost_value: 0,
3948 param1: 9,
3949 param1_value: 4,
3950 };
3951 assert!(light.subtype_label(&mut tables).is_none());
3952
3953 let true_seeing = DecodedProperty::TrueSeeing {
3954 property_id: 47,
3955 subtype_id: 0,
3956 cost_table: 0,
3957 cost_value: 0,
3958 };
3959 assert!(true_seeing.subtype_label(&mut tables).is_none());
3960 }
3961
3962 #[test]
3963 fn deferred_zero_use_rows_stay_in_unknown_by_design() {
3964 let table = itempropdef_with_labels(&[
3972 (2, "ArmorAlignmentGroup"),
3973 (16, "DamageReduced"),
3974 (19, "DecreaseAbilityScore"),
3975 (22, "DamageMelee"),
3976 (40, "AttackBonusRacialGroup"),
3977 (44, "UseLimitationClass"),
3978 (48, "OnMonsterHit"),
3979 (56, "Blaster_Bolt_Defect_Decrease"),
3980 ]);
3981 let mut tables = TestTables::with("itempropdef", &table);
3982
3983 let uti = Uti {
3984 properties: vec![
3985 property(2, 0),
3986 property(16, 0),
3987 property(19, 0),
3988 property(22, 0),
3989 property(40, 0),
3990 property(44, 0),
3991 property(48, 0),
3992 property(56, 0),
3993 ],
3994 ..Uti::default()
3995 };
3996 let view = uti.resolve(&mut tables);
3997
3998 for prop in view.properties() {
3999 assert!(
4000 matches!(prop, DecodedProperty::Unknown { .. }),
4001 "expected Unknown for deferred row, got {prop:?}"
4002 );
4003 }
4004 }
4005
4006 #[test]
4007 fn properties_accessor_preserves_source_order() {
4008 let uti = Uti {
4009 properties: vec![property(7, 0), property(0, 0), property(45, 0)],
4010 ..Uti::default()
4011 };
4012 let mut tables = TestTables::new();
4013
4014 let view = uti.resolve(&mut tables);
4015 let ids: Vec<u16> = view
4016 .properties()
4017 .iter()
4018 .map(|prop| match prop {
4019 DecodedProperty::AbilityBonus { property_id, .. }
4020 | DecodedProperty::SaveBonus { property_id, .. }
4021 | DecodedProperty::SaveBonusSpecific { property_id, .. }
4022 | DecodedProperty::SavePenalty { property_id, .. }
4023 | DecodedProperty::SavePenaltySpecific { property_id, .. }
4024 | DecodedProperty::DamageBonus { property_id, .. }
4025 | DecodedProperty::DamageImmunity { property_id, .. }
4026 | DecodedProperty::DamageResistance { property_id, .. }
4027 | DecodedProperty::AcBonus { property_id, .. }
4028 | DecodedProperty::EnhancementBonus { property_id, .. }
4029 | DecodedProperty::OnHit { property_id, .. }
4030 | DecodedProperty::CastSpell { property_id, .. }
4031 | DecodedProperty::Trap { property_id, .. }
4032 | DecodedProperty::ThievesTools { property_id, .. }
4033 | DecodedProperty::ComputerSpike { property_id, .. }
4034 | DecodedProperty::UseLimitationFeat { property_id, .. }
4035 | DecodedProperty::UseLimitationRacial { property_id, .. }
4036 | DecodedProperty::UseLimitationAlignmentGroup { property_id, .. }
4037 | DecodedProperty::DamageRacialGroup { property_id, .. }
4038 | DecodedProperty::DamageAlignmentGroup { property_id, .. }
4039 | DecodedProperty::EnhancementRacialGroup { property_id, .. }
4040 | DecodedProperty::EnhancementAlignmentGroup { property_id, .. }
4041 | DecodedProperty::AttackBonusAlignmentGroup { property_id, .. }
4042 | DecodedProperty::TrueSeeing { property_id, .. }
4043 | DecodedProperty::Light { property_id, .. }
4044 | DecodedProperty::AttackBonus { property_id, .. }
4045 | DecodedProperty::Keen { property_id, .. }
4046 | DecodedProperty::MassiveCriticals { property_id, .. }
4047 | DecodedProperty::BlasterBoltDeflectIncrease { property_id, .. }
4048 | DecodedProperty::MonsterDamage { property_id, .. }
4049 | DecodedProperty::BonusFeats { property_id, .. }
4050 | DecodedProperty::Immunity { property_id, .. }
4051 | DecodedProperty::Skill { property_id, .. }
4052 | DecodedProperty::AttackPenalty { property_id, .. }
4053 | DecodedProperty::DamagePenalty { property_id, .. }
4054 | DecodedProperty::MagicResistBonus { property_id, .. }
4055 | DecodedProperty::DamageNone { property_id, .. }
4056 | DecodedProperty::Regeneration { property_id, .. }
4057 | DecodedProperty::RegenerationForcePoints { property_id, .. }
4058 | DecodedProperty::Disguise { property_id, .. }
4059 | DecodedProperty::Unknown { property_id, .. } => *property_id,
4060 })
4061 .collect();
4062 assert_eq!(ids, vec![7, 0, 45]);
4063 }
4064
4065 #[test]
4066 fn is_armor_delegates_to_source_uti_for_armor_base_item() {
4067 let uti = Uti {
4069 base_item: 35,
4070 ..Uti::default()
4071 };
4072 let mut tables = TestTables::new();
4073
4074 let view = uti.resolve(&mut tables);
4075 assert!(view.is_armor());
4076 assert_eq!(view.is_armor(), uti.is_armor());
4078 }
4079
4080 #[test]
4081 fn is_armor_returns_false_for_non_armor_base_item() {
4082 let uti = Uti {
4084 base_item: 0,
4085 ..Uti::default()
4086 };
4087 let mut tables = TestTables::new();
4088
4089 let view = uti.resolve(&mut tables);
4090 assert!(!view.is_armor());
4091 }
4092
4093 fn baseitems_with_rows(rows: &[(usize, &str, &str, &str, &str)]) -> TwoDa {
4098 let max_row = rows.iter().map(|(idx, ..)| *idx).max().unwrap_or(0);
4099 let mut table_rows = Vec::with_capacity(max_row + 1);
4100 for row_index in 0..=max_row {
4101 let cells = rows
4102 .iter()
4103 .find(|(idx, ..)| *idx == row_index)
4104 .map(|(_, ww, st, eq, mt)| {
4105 vec![
4106 (*ww).to_string(),
4107 (*st).to_string(),
4108 (*eq).to_string(),
4109 (*mt).to_string(),
4110 ]
4111 })
4112 .unwrap_or_else(|| vec![String::new(); 4]);
4113 table_rows.push(TwoDaRow {
4114 label: row_index.to_string(),
4115 cells,
4116 });
4117 }
4118 TwoDa {
4119 headers: vec![
4120 "weaponwield".to_string(),
4121 "stacking".to_string(),
4122 "equipableslots".to_string(),
4123 "modeltype".to_string(),
4124 ],
4125 rows: table_rows,
4126 }
4127 }
4128
4129 #[test]
4130 fn combat_equip_queries_default_to_safe_values_without_baseitems() {
4131 let uti = Uti {
4134 base_item: 2,
4135 ..Uti::default()
4136 };
4137 let mut tables = TestTables::new();
4138
4139 let view = uti.resolve(&mut tables);
4140 assert!(!view.is_weapon());
4141 assert!(!view.is_consumable());
4142 assert!(view.equip_slot_mask().is_none());
4143 assert!(view.model_type().is_none());
4144 }
4145
4146 #[test]
4147 fn combat_equip_queries_default_when_base_item_row_is_absent() {
4148 let table = baseitems_with_rows(&[(2, "2", "1", "0x00030", "0")]);
4151 let mut tables = TestTables::with("baseitems", &table);
4152
4153 let uti = Uti {
4154 base_item: 99,
4155 ..Uti::default()
4156 };
4157 let view = uti.resolve(&mut tables);
4158 assert!(!view.is_weapon());
4159 assert!(!view.is_consumable());
4160 assert!(view.equip_slot_mask().is_none());
4161 assert!(view.model_type().is_none());
4162 }
4163
4164 #[test]
4165 fn is_weapon_reads_weaponwield_column() {
4166 let table =
4169 baseitems_with_rows(&[(2, "2", "1", "0x00030", "0"), (35, "", "1", "0x00018", "0")]);
4170 let mut tables = TestTables::with("baseitems", &table);
4171
4172 let weapon = Uti {
4173 base_item: 2,
4174 ..Uti::default()
4175 };
4176 assert!(weapon.resolve(&mut tables).is_weapon());
4177
4178 let armor = Uti {
4179 base_item: 35,
4180 ..Uti::default()
4181 };
4182 assert!(!armor.resolve(&mut tables).is_weapon());
4183 }
4184
4185 #[test]
4186 fn is_consumable_reads_stacking_column() {
4187 let table = baseitems_with_rows(&[
4191 (60, "0", "99", "0x40000", "0"),
4192 (35, "0", "1", "0x00018", "0"),
4193 ]);
4194 let mut tables = TestTables::with("baseitems", &table);
4195
4196 let stim = Uti {
4197 base_item: 60,
4198 ..Uti::default()
4199 };
4200 assert!(stim.resolve(&mut tables).is_consumable());
4201
4202 let armor = Uti {
4203 base_item: 35,
4204 ..Uti::default()
4205 };
4206 assert!(!armor.resolve(&mut tables).is_consumable());
4207 }
4208
4209 #[test]
4210 fn equip_slot_mask_parses_hex_string_with_or_without_prefix() {
4211 let table = baseitems_with_rows(&[
4215 (2, "2", "1", "0x00030", "0"),
4216 (3, "2", "1", "0X00030", "0"),
4217 (4, "2", "1", "00030", "0"),
4218 ]);
4219 let mut tables = TestTables::with("baseitems", &table);
4220
4221 for base in [2, 3, 4] {
4222 let uti = Uti {
4223 base_item: base,
4224 ..Uti::default()
4225 };
4226 assert_eq!(uti.resolve(&mut tables).equip_slot_mask(), Some(0x0030));
4227 }
4228 }
4229
4230 #[test]
4231 fn model_type_reads_numeric_column() {
4232 let table = baseitems_with_rows(&[
4233 (2, "2", "1", "0x00030", "0"),
4234 (38, "0", "1", "0x00018", "1"),
4235 (75, "0", "1", "0x00400", "2"),
4236 ]);
4237 let mut tables = TestTables::with("baseitems", &table);
4238
4239 for (base, expected) in [(2, 0_u8), (38, 1), (75, 2)] {
4240 let uti = Uti {
4241 base_item: base,
4242 ..Uti::default()
4243 };
4244 assert_eq!(uti.resolve(&mut tables).model_type(), Some(expected));
4245 }
4246 }
4247
4248 #[test]
4249 fn has_property_kind_matches_each_family() {
4250 let table = itempropdef_with_labels(&[
4253 (0, "Ability"),
4254 (10, "CastSpell"),
4255 (11, "Damage"),
4256 (26, "ImprovedSavingThrows"),
4257 (38, "AttackBonus"),
4258 (5, "Enhancement"),
4259 (57, "Use_Limitation_Feat"),
4260 ]);
4261 let mut tables = TestTables::with("itempropdef", &table);
4262
4263 let uti = Uti {
4264 properties: vec![
4265 property(0, 0),
4266 property(10, 0),
4267 property(11, 0),
4268 property(26, 0),
4269 property(38, 0),
4270 property(5, 0),
4271 property(57, 0),
4272 ],
4273 ..Uti::default()
4274 };
4275 let view = uti.resolve(&mut tables);
4276
4277 assert!(view.has_property_kind(PropertyKindFilter::Ability));
4278 assert!(view.has_property_kind(PropertyKindFilter::Active));
4279 assert!(view.has_property_kind(PropertyKindFilter::Damage));
4280 assert!(view.has_property_kind(PropertyKindFilter::Save));
4281 assert!(view.has_property_kind(PropertyKindFilter::Attack));
4282 assert!(view.has_property_kind(PropertyKindFilter::Enhancement));
4283 assert!(view.has_property_kind(PropertyKindFilter::UseLimitation));
4284 }
4285
4286 #[test]
4287 fn has_property_kind_returns_false_for_unrepresented_families() {
4288 let table = itempropdef_with_labels(&[(0, "Ability")]);
4291 let mut tables = TestTables::with("itempropdef", &table);
4292
4293 let uti = Uti {
4294 properties: vec![property(0, 0)],
4295 ..Uti::default()
4296 };
4297 let view = uti.resolve(&mut tables);
4298
4299 assert!(view.has_property_kind(PropertyKindFilter::Ability));
4300 for filter in [
4301 PropertyKindFilter::Damage,
4302 PropertyKindFilter::Save,
4303 PropertyKindFilter::Attack,
4304 PropertyKindFilter::Enhancement,
4305 PropertyKindFilter::UseLimitation,
4306 PropertyKindFilter::Active,
4307 ] {
4308 assert!(
4309 !view.has_property_kind(filter),
4310 "unexpected match for {filter:?}"
4311 );
4312 }
4313 }
4314
4315 #[test]
4316 fn has_property_kind_damage_covers_full_damage_family() {
4317 let table = itempropdef_with_labels(&[
4322 (11, "Damage"),
4323 (12, "DamageAlignmentGroup"),
4324 (13, "DamageRacialGroup"),
4325 (14, "DamageImmunity"),
4326 (15, "DamagePenalty"),
4327 (17, "DamageResist"),
4328 ]);
4329 let mut tables = TestTables::with("itempropdef", &table);
4330
4331 for row in [11_u16, 12, 13, 14, 15, 17] {
4332 let uti = Uti {
4333 properties: vec![property(row, 0)],
4334 ..Uti::default()
4335 };
4336 let view = uti.resolve(&mut tables);
4337 assert!(
4338 view.has_property_kind(PropertyKindFilter::Damage),
4339 "expected Damage filter to match row {row}"
4340 );
4341 }
4342 }
4343
4344 #[test]
4345 fn has_property_kind_does_not_match_unknown_variants() {
4346 let table = itempropdef_with_labels(&[(200, "FakeMod_GrantsCookies")]);
4349 let mut tables = TestTables::with("itempropdef", &table);
4350
4351 let uti = Uti {
4352 properties: vec![property(200, 0)],
4353 ..Uti::default()
4354 };
4355 let view = uti.resolve(&mut tables);
4356
4357 for filter in [
4358 PropertyKindFilter::Damage,
4359 PropertyKindFilter::Ability,
4360 PropertyKindFilter::Save,
4361 PropertyKindFilter::Attack,
4362 PropertyKindFilter::Enhancement,
4363 PropertyKindFilter::UseLimitation,
4364 PropertyKindFilter::Active,
4365 ] {
4366 assert!(
4367 !view.has_property_kind(filter),
4368 "Unknown variant should not match {filter:?}"
4369 );
4370 }
4371 }
4372
4373 #[test]
4374 fn has_property_kind_returns_false_for_empty_property_list() {
4375 let uti = Uti::default();
4376 let mut tables = TestTables::new();
4377 let view = uti.resolve(&mut tables);
4378
4379 for filter in [
4380 PropertyKindFilter::Damage,
4381 PropertyKindFilter::Ability,
4382 PropertyKindFilter::Save,
4383 PropertyKindFilter::Attack,
4384 PropertyKindFilter::Enhancement,
4385 PropertyKindFilter::UseLimitation,
4386 PropertyKindFilter::Active,
4387 ] {
4388 assert!(!view.has_property_kind(filter));
4389 }
4390 }
4391
4392 #[test]
4393 fn combat_equip_queries_return_field_defaults_for_unparseable_cells() {
4394 let table = baseitems_with_rows(&[(2, "junk", "stuff", "not-hex", "wat")]);
4398 let mut tables = TestTables::with("baseitems", &table);
4399
4400 let uti = Uti {
4401 base_item: 2,
4402 ..Uti::default()
4403 };
4404 let view = uti.resolve(&mut tables);
4405 assert!(!view.is_weapon());
4406 assert!(!view.is_consumable());
4407 assert!(view.equip_slot_mask().is_none());
4408 assert!(view.model_type().is_none());
4409 }
4410
4411 fn itempropdef_with_subtypes(rows: &[(usize, &str, &str)]) -> TwoDa {
4416 let max_row = rows.iter().map(|(idx, _, _)| *idx).max().unwrap_or(0);
4417 let mut table_rows = Vec::with_capacity(max_row + 1);
4418 for row_index in 0..=max_row {
4419 let (label, subtype_resref) = rows
4420 .iter()
4421 .find(|(idx, _, _)| *idx == row_index)
4422 .map(|(_, label, sr)| ((*label).to_string(), (*sr).to_string()))
4423 .unwrap_or_default();
4424 table_rows.push(TwoDaRow {
4425 label: row_index.to_string(),
4426 cells: vec![label, subtype_resref],
4427 });
4428 }
4429 TwoDa {
4430 headers: vec!["label".to_string(), "SubTypeResRef".to_string()],
4431 rows: table_rows,
4432 }
4433 }
4434
4435 fn subtype_2da(labels: &[(usize, &str)]) -> TwoDa {
4438 itempropdef_with_labels(labels)
4442 }
4443
4444 fn add_2da_entry(tables: &mut TestTables, name: &str, table: &TwoDa) {
4445 tables.insert(name, table);
4446 }
4447
4448 #[test]
4449 fn subtype_label_resolves_full_chain_for_known_property() {
4450 let propdef = itempropdef_with_subtypes(&[
4453 (0, "Ability", "iprp_abilities"),
4454 (7, "Damage", "iprp_damagecost"),
4455 ]);
4456 let damagecost = subtype_2da(&[(0, "Bludgeoning"), (5, "Acid")]);
4457
4458 let mut tables = TestTables::new();
4459 add_2da_entry(&mut tables, "itempropdef", &propdef);
4460 add_2da_entry(&mut tables, "iprp_damagecost", &damagecost);
4461
4462 let prop = DecodedProperty::Unknown {
4463 property_id: 7,
4464 property_label: Some("Damage".to_string()),
4465 subtype: 5,
4466 cost_table: 0,
4467 cost_value: 0,
4468 param1: 0,
4469 param1_value: 0,
4470 };
4471 assert_eq!(prop.subtype_label(&mut tables).as_deref(), Some("Acid"));
4472 }
4473
4474 #[test]
4475 fn subtype_label_returns_none_for_property_with_empty_subtype_resref() {
4476 let propdef = itempropdef_with_subtypes(&[(7, "Light", "")]);
4479 let mut tables = TestTables::new();
4480 add_2da_entry(&mut tables, "itempropdef", &propdef);
4481
4482 let prop = DecodedProperty::Unknown {
4483 property_id: 7,
4484 property_label: Some("Light".to_string()),
4485 subtype: 0,
4486 cost_table: 0,
4487 cost_value: 0,
4488 param1: 0,
4489 param1_value: 0,
4490 };
4491 assert!(prop.subtype_label(&mut tables).is_none());
4492 }
4493
4494 #[test]
4495 fn subtype_label_returns_none_when_subtype_table_resref_is_missing() {
4496 let propdef = itempropdef_with_subtypes(&[(0, "Ability", "iprp_abilities")]);
4499 let mut tables = TestTables::new();
4500 add_2da_entry(&mut tables, "itempropdef", &propdef);
4501
4502 let prop = DecodedProperty::Unknown {
4503 property_id: 99,
4504 property_label: None,
4505 subtype: 0,
4506 cost_table: 0,
4507 cost_value: 0,
4508 param1: 0,
4509 param1_value: 0,
4510 };
4511 assert!(prop.subtype_label(&mut tables).is_none());
4512 }
4513
4514 #[test]
4515 fn subtype_label_returns_none_when_subtype_table_cannot_be_loaded() {
4516 let propdef = itempropdef_with_subtypes(&[(7, "Damage", "iprp_damagecost")]);
4519 let mut tables = TestTables::new();
4520 add_2da_entry(&mut tables, "itempropdef", &propdef);
4521
4522 let prop = DecodedProperty::Unknown {
4523 property_id: 7,
4524 property_label: Some("Damage".to_string()),
4525 subtype: 0,
4526 cost_table: 0,
4527 cost_value: 0,
4528 param1: 0,
4529 param1_value: 0,
4530 };
4531 assert!(prop.subtype_label(&mut tables).is_none());
4532 }
4533
4534 #[test]
4535 fn subtype_label_returns_none_when_subtype_row_is_out_of_bounds() {
4536 let propdef = itempropdef_with_subtypes(&[(7, "Damage", "iprp_damagecost")]);
4538 let damagecost = subtype_2da(&[(0, "Bludgeoning"), (1, "Slashing")]);
4539 let mut tables = TestTables::new();
4540 add_2da_entry(&mut tables, "itempropdef", &propdef);
4541 add_2da_entry(&mut tables, "iprp_damagecost", &damagecost);
4542
4543 let prop = DecodedProperty::Unknown {
4544 property_id: 7,
4545 property_label: Some("Damage".to_string()),
4546 subtype: 99,
4547 cost_table: 0,
4548 cost_value: 0,
4549 param1: 0,
4550 param1_value: 0,
4551 };
4552 assert!(prop.subtype_label(&mut tables).is_none());
4553 }
4554
4555 #[test]
4556 fn subtype_label_returns_none_when_itempropdef_is_missing() {
4557 let mut tables = TestTables::new();
4559
4560 let prop = DecodedProperty::Unknown {
4561 property_id: 7,
4562 property_label: None,
4563 subtype: 5,
4564 cost_table: 0,
4565 cost_value: 0,
4566 param1: 0,
4567 param1_value: 0,
4568 };
4569 assert!(prop.subtype_label(&mut tables).is_none());
4570 }
4571
4572 #[test]
4573 fn subtype_label_resolves_mod_extended_subtype_row() {
4574 let propdef = itempropdef_with_subtypes(&[(7, "Damage", "iprp_damagecost")]);
4578 let damagecost = subtype_2da(&[(200, "FakeMod_PsionicBurn")]);
4579 let mut tables = TestTables::new();
4580 add_2da_entry(&mut tables, "itempropdef", &propdef);
4581 add_2da_entry(&mut tables, "iprp_damagecost", &damagecost);
4582
4583 let prop = DecodedProperty::Unknown {
4584 property_id: 7,
4585 property_label: Some("Damage".to_string()),
4586 subtype: 200,
4587 cost_table: 0,
4588 cost_value: 0,
4589 param1: 0,
4590 param1_value: 0,
4591 };
4592 assert_eq!(
4593 prop.subtype_label(&mut tables).as_deref(),
4594 Some("FakeMod_PsionicBurn")
4595 );
4596 }
4597
4598 #[test]
4601 fn project_with_itempropdef_dispatches_typed_variants() {
4602 let uti = Uti {
4605 properties: vec![property(0, 2)],
4606 ..Uti::default()
4607 };
4608 let propdef = itempropdef_with_labels(&[(0, "Ability")]);
4609
4610 let projection = uti.project(Some(&propdef));
4611 assert!(matches!(
4612 projection.properties()[0],
4613 DecodedProperty::AbilityBonus { subtype_id: 2, .. }
4614 ));
4615 }
4616
4617 #[test]
4618 fn project_without_itempropdef_falls_back_to_unknown_no_label() {
4619 let uti = Uti {
4624 properties: vec![property(0, 2), property(11, 4)],
4625 ..Uti::default()
4626 };
4627
4628 let projection = uti.project(None);
4629 let labels: Vec<Option<&str>> = projection
4630 .properties()
4631 .iter()
4632 .map(|prop| match prop {
4633 DecodedProperty::Unknown { property_label, .. } => property_label.as_deref(),
4634 _ => panic!("expected every property to land in Unknown without itempropdef"),
4635 })
4636 .collect();
4637 assert_eq!(labels, vec![None, None]);
4638 }
4639
4640 #[test]
4641 fn projection_resolve_loads_baseitems_for_queries() {
4642 let uti = Uti {
4646 base_item: 0,
4647 ..Uti::default()
4648 };
4649 let baseitems = baseitems_with_rows(&[(0, "5", "0", "0x0010", "3")]);
4650 let mut tables = TestTables::with("baseitems", &baseitems);
4651
4652 let projection = uti.project(None);
4653 let resolved = projection.resolve(&mut tables);
4654
4655 assert!(resolved.is_weapon());
4656 assert_eq!(resolved.equip_slot_mask(), Some(0x0010));
4657 assert_eq!(resolved.model_type(), Some(3));
4658 }
4659
4660 #[test]
4661 fn one_projection_feeds_independent_resolutions_per_scope() {
4662 let uti = Uti {
4668 base_item: 0,
4669 ..Uti::default()
4670 };
4671
4672 let vanilla_baseitems = baseitems_with_rows(&[(0, "5", "0", "0x0010", "3")]);
4673 let mut vanilla_tables = TestTables::with("baseitems", &vanilla_baseitems);
4674
4675 let mod_baseitems = baseitems_with_rows(&[(0, "0", "10", "0x0000", "0")]);
4678 let mut mod_tables = TestTables::with("baseitems", &mod_baseitems);
4679
4680 let projection = uti.project(None);
4681 let vanilla = projection.resolve(&mut vanilla_tables);
4682 let modded = projection.resolve(&mut mod_tables);
4683
4684 assert!(vanilla.is_weapon());
4685 assert!(!modded.is_weapon());
4686 assert!(modded.is_consumable(), "mod row marks the item stackable");
4687 assert!(!vanilla.is_consumable());
4688 }
4689
4690 #[test]
4691 fn resolve_sugar_matches_project_then_resolve() {
4692 let uti = Uti {
4697 base_item: 0,
4698 properties: vec![property(0, 2)],
4699 ..Uti::default()
4700 };
4701 let propdef = itempropdef_with_labels(&[(0, "Ability")]);
4702 let baseitems = baseitems_with_rows(&[(0, "5", "0", "0x0010", "3")]);
4703 let mut sugar_tables = TestTables::new();
4704 add_2da_entry(&mut sugar_tables, "itempropdef", &propdef);
4705 add_2da_entry(&mut sugar_tables, "baseitems", &baseitems);
4706 let mut explicit_tables = TestTables::new();
4707 add_2da_entry(&mut explicit_tables, "itempropdef", &propdef);
4708 add_2da_entry(&mut explicit_tables, "baseitems", &baseitems);
4709
4710 let sugar = uti.resolve(&mut sugar_tables);
4711
4712 let projection = {
4713 let propdef_ref = explicit_tables.twoda(tables::ITEMPROPDEF);
4714 uti.project(propdef_ref)
4715 };
4716 let explicit = projection.resolve(&mut explicit_tables);
4717
4718 assert_eq!(sugar.properties(), explicit.properties());
4719 assert_eq!(sugar.is_weapon(), explicit.is_weapon());
4720 assert_eq!(sugar.is_consumable(), explicit.is_consumable());
4721 assert_eq!(sugar.equip_slot_mask(), explicit.equip_slot_mask());
4722 assert_eq!(sugar.model_type(), explicit.model_type());
4723 }
4724
4725 fn iprp_costtable_with_entries(rows: &[(usize, &str)]) -> TwoDa {
4731 let max_row = rows.iter().map(|(idx, _)| *idx).max().unwrap_or(0);
4732 let mut table_rows = Vec::with_capacity(max_row + 1);
4733 for row_index in 0..=max_row {
4734 let name = rows
4735 .iter()
4736 .find(|(idx, _)| *idx == row_index)
4737 .map(|(_, name)| (*name).to_string())
4738 .unwrap_or_default();
4739 table_rows.push(TwoDaRow {
4740 label: row_index.to_string(),
4741 cells: vec![name],
4742 });
4743 }
4744 TwoDa {
4745 headers: vec!["Name".to_string()],
4746 rows: table_rows,
4747 }
4748 }
4749
4750 fn cost_value_2da(rows: &[(usize, i32)]) -> TwoDa {
4755 let max_row = rows.iter().map(|(idx, _)| *idx).max().unwrap_or(0);
4756 let mut table_rows = Vec::with_capacity(max_row + 1);
4757 for row_index in 0..=max_row {
4758 let value = rows
4759 .iter()
4760 .find(|(idx, _)| *idx == row_index)
4761 .map(|(_, value)| value.to_string())
4762 .unwrap_or_default();
4763 table_rows.push(TwoDaRow {
4764 label: row_index.to_string(),
4765 cells: vec![value],
4766 });
4767 }
4768 TwoDa {
4769 headers: vec!["Value".to_string()],
4770 rows: table_rows,
4771 }
4772 }
4773
4774 fn property_with_cost(
4779 property_name: u16,
4780 subtype: u16,
4781 cost_table: u8,
4782 cost_value: u16,
4783 ) -> UtiProperty {
4784 UtiProperty {
4785 cost_table,
4786 cost_value,
4787 param1: 0xFF,
4788 param1_value: 0,
4789 property_name,
4790 subtype,
4791 chance_appear: 100,
4792 useable: None,
4793 uses_per_day: None,
4794 upgrade_type: None,
4795 }
4796 }
4797
4798 #[test]
4799 fn damage_bonuses_yields_cost_value_as_magnitude() {
4800 let uti = Uti {
4804 properties: vec![
4805 property_with_cost(11, 3, 4, 7),
4806 property_with_cost(11, 5, 4, 12),
4807 ],
4808 ..Uti::default()
4809 };
4810 let propdef = itempropdef_with_labels(&[(11, "Damage")]);
4811 let mut tables = TestTables::with("itempropdef", &propdef);
4812
4813 let resolved = uti.resolve(&mut tables);
4814 let yielded: Vec<(u16, i32)> = resolved.damage_bonuses().collect();
4815 assert_eq!(yielded, vec![(3, 7), (5, 12)]);
4816 }
4817
4818 #[test]
4819 fn ability_bonuses_resolve_magnitude_from_iprp_bonuscost_value() {
4820 let uti = Uti {
4825 properties: vec![
4826 property_with_cost(0, 2, 99, 4), property_with_cost(0, 4, 99, 7), ],
4829 ..Uti::default()
4830 };
4831 let propdef = itempropdef_with_labels(&[(0, "Ability")]);
4832 let bonuscost = cost_value_2da(&[(4, 2), (7, 5)]);
4833 let mut tables = TestTables::new();
4834 add_2da_entry(&mut tables, "itempropdef", &propdef);
4835 add_2da_entry(&mut tables, "iprp_bonuscost", &bonuscost);
4836
4837 let resolved = uti.resolve(&mut tables);
4838 let yielded: Vec<(u16, i32)> = resolved.ability_bonuses().collect();
4839 assert_eq!(yielded, vec![(2, 2), (4, 5)]);
4840 }
4841
4842 #[test]
4843 fn ability_bonuses_yield_nothing_when_iprp_bonuscost_missing() {
4844 let uti = Uti {
4849 properties: vec![property_with_cost(0, 2, 1, 4)],
4850 ..Uti::default()
4851 };
4852 let propdef = itempropdef_with_labels(&[(0, "Ability")]);
4853 let mut tables = TestTables::with("itempropdef", &propdef);
4854
4855 let resolved = uti.resolve(&mut tables);
4856 assert_eq!(resolved.ability_bonuses().count(), 0);
4857 }
4858
4859 #[test]
4860 fn damage_immunities_walk_dynamic_cost_table_dispatch() {
4861 let uti = Uti {
4866 properties: vec![property_with_cost(14, 8, 5, 3)],
4867 ..Uti::default()
4868 };
4869 let propdef = itempropdef_with_labels(&[(14, "DamageImmunity")]);
4870 let costtable = iprp_costtable_with_entries(&[(5, "IPRP_IMMUNCOST")]);
4872 let immuncost = cost_value_2da(&[(3, 50)]);
4873 let mut tables = TestTables::new();
4874 add_2da_entry(&mut tables, "itempropdef", &propdef);
4875 add_2da_entry(&mut tables, "iprp_costtable", &costtable);
4876 add_2da_entry(&mut tables, "iprp_immuncost", &immuncost);
4877
4878 let resolved = uti.resolve(&mut tables);
4879 let yielded: Vec<(u16, i32)> = resolved.damage_immunities().collect();
4880 assert_eq!(yielded, vec![(8, 50)]);
4881 }
4882
4883 #[test]
4884 fn damage_immunities_follow_mod_extended_cost_table_index() {
4885 let uti = Uti {
4890 properties: vec![property_with_cost(14, 8, 26, 1)],
4891 ..Uti::default()
4892 };
4893 let propdef = itempropdef_with_labels(&[(14, "DamageImmunity")]);
4894 let costtable = iprp_costtable_with_entries(&[(26, "mod_immuncost")]);
4895 let modded_cost = cost_value_2da(&[(1, 75)]);
4896 let mut tables = TestTables::new();
4897 add_2da_entry(&mut tables, "itempropdef", &propdef);
4898 add_2da_entry(&mut tables, "iprp_costtable", &costtable);
4899 add_2da_entry(&mut tables, "mod_immuncost", &modded_cost);
4900
4901 let resolved = uti.resolve(&mut tables);
4902 let yielded: Vec<(u16, i32)> = resolved.damage_immunities().collect();
4903 assert_eq!(yielded, vec![(8, 75)]);
4904 }
4905
4906 #[test]
4907 fn iterators_only_yield_matching_kind() {
4908 let uti = Uti {
4911 properties: vec![
4912 property_with_cost(0, 2, 1, 4), property_with_cost(11, 3, 4, 7), property_with_cost(14, 8, 5, 3), ],
4916 ..Uti::default()
4917 };
4918 let propdef =
4919 itempropdef_with_labels(&[(0, "Ability"), (11, "Damage"), (14, "DamageImmunity")]);
4920 let bonuscost = cost_value_2da(&[(4, 2)]);
4921 let costtable = iprp_costtable_with_entries(&[(5, "iprp_immuncost")]);
4922 let immuncost = cost_value_2da(&[(3, 50)]);
4923 let mut tables = TestTables::new();
4924 add_2da_entry(&mut tables, "itempropdef", &propdef);
4925 add_2da_entry(&mut tables, "iprp_bonuscost", &bonuscost);
4926 add_2da_entry(&mut tables, "iprp_costtable", &costtable);
4927 add_2da_entry(&mut tables, "iprp_immuncost", &immuncost);
4928
4929 let resolved = uti.resolve(&mut tables);
4930 assert_eq!(resolved.ability_bonuses().collect::<Vec<_>>(), vec![(2, 2)]);
4931 assert_eq!(resolved.damage_bonuses().collect::<Vec<_>>(), vec![(3, 7)]);
4932 assert_eq!(
4933 resolved.damage_immunities().collect::<Vec<_>>(),
4934 vec![(8, 50)]
4935 );
4936 }
4937
4938 #[test]
4939 fn one_projection_yields_diverging_magnitudes_per_scope() {
4940 let uti = Uti {
4945 properties: vec![property_with_cost(0, 2, 1, 4)],
4946 ..Uti::default()
4947 };
4948 let propdef = itempropdef_with_labels(&[(0, "Ability")]);
4949
4950 let vanilla_bonus = cost_value_2da(&[(4, 2)]);
4951 let mut vanilla_tables = TestTables::new();
4952 add_2da_entry(&mut vanilla_tables, "itempropdef", &propdef);
4953 add_2da_entry(&mut vanilla_tables, "iprp_bonuscost", &vanilla_bonus);
4954
4955 let mod_bonus = cost_value_2da(&[(4, 6)]); let mut mod_tables = TestTables::new();
4957 add_2da_entry(&mut mod_tables, "itempropdef", &propdef);
4958 add_2da_entry(&mut mod_tables, "iprp_bonuscost", &mod_bonus);
4959
4960 let projection = {
4961 let propdef_ref = vanilla_tables.twoda(tables::ITEMPROPDEF);
4962 uti.project(propdef_ref)
4963 };
4964 let vanilla = projection.resolve(&mut vanilla_tables);
4965 let modded = projection.resolve(&mut mod_tables);
4966
4967 assert_eq!(vanilla.ability_bonuses().collect::<Vec<_>>(), vec![(2, 2)]);
4968 assert_eq!(modded.ability_bonuses().collect::<Vec<_>>(), vec![(2, 6)]);
4969 }
4970}