1use std::io::{Cursor, Read, Write};
29
30use crate::gff_helpers::{
31 get_bool, get_i32, get_locstring, get_resref, get_string, get_u16, get_u32, get_u8,
32 upsert_field,
33};
34use rakata_core::{ResRef, StrRef};
35use rakata_formats::{
36 gff_schema::{FieldSchema, GffSchema, GffType},
37 read_gff, read_gff_from_bytes, write_gff, Gff, GffBinaryError, GffLocalizedString, GffStruct,
38 GffValue,
39};
40use thiserror::Error;
41
42pub const fn is_armor_base_item(base_item: i32) -> bool {
50 match base_item {
51 35..=43 => true,
53 53 | 58 | 63..=65 | 69 | 71 | 85 | 89 | 98 | 100 | 102 | 103 => true,
55 _ => false,
56 }
57}
58
59#[derive(Debug, Clone, PartialEq)]
61pub struct Uti {
62 pub template_resref: ResRef,
64 pub base_item: i32,
66 pub name: GffLocalizedString,
68 pub description_unidentified: GffLocalizedString,
70 pub description_identified: GffLocalizedString,
72 pub tag: String,
74 pub charges: u8,
76 pub max_charges: u8,
78 pub cost: u32,
80 pub stack_size: u16,
82 pub plot: bool,
84 pub add_cost: u32,
86 pub palette_id: u8,
88 pub comment: String,
90 pub model_variation: u8,
92 pub body_variation: u8,
94 pub texture_variation: u8,
96 pub upgrade_level: u8,
98 pub stolen: bool,
100 pub identified: bool,
102 pub droppable: bool,
104 pub pickpocketable: bool,
106 pub non_equippable: bool,
108 pub new_item: bool,
110 pub deleting: bool,
112 pub upgrades: u32,
114 pub properties: Vec<UtiProperty>,
116}
117
118impl Default for Uti {
119 fn default() -> Self {
120 Self {
121 template_resref: ResRef::blank(),
122 base_item: 0,
123 name: GffLocalizedString::new(StrRef::invalid()),
124 description_unidentified: GffLocalizedString::new(StrRef::invalid()),
125 description_identified: GffLocalizedString::new(StrRef::invalid()),
126 tag: String::new(),
127 charges: 0,
128 max_charges: 0,
129 cost: 0,
130 stack_size: 0,
131 plot: false,
132 add_cost: 0,
133 palette_id: 0,
134 comment: String::new(),
135 model_variation: 0,
136 body_variation: 0,
137 texture_variation: 0,
138 upgrade_level: 0,
139 stolen: false,
140 identified: false,
141 droppable: false,
142 pickpocketable: false,
143 non_equippable: false,
144 new_item: false,
145 deleting: false,
146 upgrades: 0,
147 properties: Vec::new(),
148 }
149 }
150}
151
152impl Uti {
153 pub fn new() -> Self {
155 Self::default()
156 }
157
158 pub fn is_armor(&self) -> bool {
160 is_armor_base_item(self.base_item)
161 }
162
163 pub fn from_gff(gff: &Gff) -> Result<Self, UtiError> {
165 if gff.file_type != *b"UTI " && gff.file_type != *b"GFF " {
166 return Err(UtiError::UnsupportedFileType(gff.file_type));
167 }
168
169 let root = &gff.root;
170
171 let properties = match root.field("PropertiesList") {
172 Some(GffValue::List(property_structs)) => property_structs
173 .iter()
174 .map(UtiProperty::from_struct)
175 .collect::<Vec<_>>(),
176 Some(_) => {
177 return Err(UtiError::TypeMismatch {
178 field: "PropertiesList",
179 expected: "List",
180 });
181 }
182 None => Vec::new(),
183 };
184
185 let charges = get_u8(root, "Charges").unwrap_or(50);
190 let max_charges = get_u8(root, "MaxCharges").unwrap_or(charges);
191 Ok(Self {
192 template_resref: get_resref(root, "TemplateResRef").unwrap_or_default(),
193 base_item: get_i32(root, "BaseItem").unwrap_or(0),
194 name: get_locstring(root, "LocalizedName")
195 .cloned()
196 .unwrap_or_else(|| GffLocalizedString::new(StrRef::invalid())),
197 description_unidentified: get_locstring(root, "Description")
198 .cloned()
199 .unwrap_or_else(|| GffLocalizedString::new(StrRef::invalid())),
200 description_identified: get_locstring(root, "DescIdentified")
201 .cloned()
202 .unwrap_or_else(|| GffLocalizedString::new(StrRef::invalid())),
203 tag: get_string(root, "Tag").unwrap_or_default(),
204 charges,
205 max_charges,
206 cost: get_u32(root, "Cost").unwrap_or(0),
207 stack_size: get_u16(root, "StackSize").unwrap_or(0),
208 plot: get_bool(root, "Plot").unwrap_or(false),
209 add_cost: get_u32(root, "AddCost").unwrap_or(0),
210 palette_id: get_u8(root, "PaletteID").unwrap_or(0),
211 comment: get_string(root, "Comment").unwrap_or_default(),
212 model_variation: get_u8(root, "ModelVariation").unwrap_or(0),
213 body_variation: get_u8(root, "BodyVariation").unwrap_or(0),
214 texture_variation: get_u8(root, "TextureVar").unwrap_or(0),
215 upgrade_level: get_u8(root, "UpgradeLevel").unwrap_or(0),
216 stolen: get_bool(root, "Stolen").unwrap_or(false),
217 identified: get_bool(root, "Identified").unwrap_or(true),
218 droppable: get_bool(root, "Dropable").unwrap_or(false),
219 pickpocketable: get_bool(root, "Pickpocketable").unwrap_or(false),
220 non_equippable: get_bool(root, "NonEquippable").unwrap_or(false),
221 new_item: get_bool(root, "NewItem").unwrap_or(false),
222 deleting: get_bool(root, "DELETING").unwrap_or(false),
223 upgrades: get_u32(root, "Upgrades").unwrap_or(0),
224 properties,
225 })
226 }
227
228 pub fn to_gff(&self) -> Gff {
230 let mut root = GffStruct::new(-1);
231
232 upsert_field(
233 &mut root,
234 "TemplateResRef",
235 GffValue::ResRef(self.template_resref),
236 );
237 upsert_field(&mut root, "BaseItem", GffValue::Int32(self.base_item));
238 upsert_field(
239 &mut root,
240 "LocalizedName",
241 GffValue::LocalizedString(self.name.clone()),
242 );
243 upsert_field(
244 &mut root,
245 "Description",
246 GffValue::LocalizedString(self.description_unidentified.clone()),
247 );
248 upsert_field(
249 &mut root,
250 "DescIdentified",
251 GffValue::LocalizedString(self.description_identified.clone()),
252 );
253 upsert_field(&mut root, "Tag", GffValue::String(self.tag.clone()));
254 upsert_field(&mut root, "Charges", GffValue::UInt8(self.charges));
255 upsert_field(&mut root, "MaxCharges", GffValue::UInt8(self.max_charges));
256 upsert_field(&mut root, "Cost", GffValue::UInt32(self.cost));
257 upsert_field(&mut root, "StackSize", GffValue::UInt16(self.stack_size));
258 upsert_field(&mut root, "Plot", GffValue::UInt8(u8::from(self.plot)));
259 upsert_field(&mut root, "AddCost", GffValue::UInt32(self.add_cost));
260 upsert_field(&mut root, "PaletteID", GffValue::UInt8(self.palette_id));
261 upsert_field(&mut root, "Comment", GffValue::String(self.comment.clone()));
262 upsert_field(
263 &mut root,
264 "ModelVariation",
265 GffValue::UInt8(self.model_variation),
266 );
267 upsert_field(
268 &mut root,
269 "BodyVariation",
270 GffValue::UInt8(self.body_variation),
271 );
272 upsert_field(
273 &mut root,
274 "TextureVar",
275 GffValue::UInt8(self.texture_variation),
276 );
277 upsert_field(
278 &mut root,
279 "UpgradeLevel",
280 GffValue::UInt8(self.upgrade_level),
281 );
282 upsert_field(&mut root, "Stolen", GffValue::UInt8(u8::from(self.stolen)));
283 upsert_field(
284 &mut root,
285 "Identified",
286 GffValue::UInt8(u8::from(self.identified)),
287 );
288 upsert_field(
289 &mut root,
290 "Dropable",
291 GffValue::UInt8(u8::from(self.droppable)),
292 );
293 upsert_field(
294 &mut root,
295 "Pickpocketable",
296 GffValue::UInt8(u8::from(self.pickpocketable)),
297 );
298 upsert_field(
299 &mut root,
300 "NonEquippable",
301 GffValue::UInt8(u8::from(self.non_equippable)),
302 );
303 upsert_field(
304 &mut root,
305 "NewItem",
306 GffValue::UInt8(u8::from(self.new_item)),
307 );
308 upsert_field(
309 &mut root,
310 "DELETING",
311 GffValue::UInt8(u8::from(self.deleting)),
312 );
313 upsert_field(&mut root, "Upgrades", GffValue::UInt32(self.upgrades));
314
315 let property_structs = self
316 .properties
317 .iter()
318 .map(UtiProperty::to_struct)
319 .collect::<Vec<GffStruct>>();
320 upsert_field(
321 &mut root,
322 "PropertiesList",
323 GffValue::List(property_structs),
324 );
325
326 Gff::new(*b"UTI ", root)
327 }
328}
329
330#[derive(Debug, Clone, PartialEq)]
332pub struct UtiProperty {
333 pub cost_table: u8,
335 pub cost_value: u16,
337 pub param1: u8,
339 pub param1_value: u8,
341 pub property_name: u16,
343 pub subtype: u16,
345 pub chance_appear: u8,
347 pub useable: Option<bool>,
349 pub uses_per_day: Option<u8>,
351 pub upgrade_type: Option<u8>,
353}
354
355impl UtiProperty {
356 fn from_struct(structure: &GffStruct) -> Self {
357 Self {
358 cost_table: get_u8(structure, "CostTable").unwrap_or(0),
359 cost_value: get_u16(structure, "CostValue").unwrap_or(0),
360 param1: get_u8(structure, "Param1").unwrap_or(0),
361 param1_value: get_u8(structure, "Param1Value").unwrap_or(0),
362 property_name: get_u16(structure, "PropertyName").unwrap_or(0),
363 subtype: get_u16(structure, "Subtype").unwrap_or(0),
364 chance_appear: get_u8(structure, "ChanceAppear").unwrap_or(100),
365 useable: get_bool(structure, "Useable"),
366 uses_per_day: get_u8(structure, "UsesPerDay"),
367 upgrade_type: get_u8(structure, "UpgradeType"),
368 }
369 }
370
371 fn to_struct(&self) -> GffStruct {
372 let mut structure = GffStruct::new(0);
373
374 upsert_field(
375 &mut structure,
376 "CostTable",
377 GffValue::UInt8(self.cost_table),
378 );
379 upsert_field(
380 &mut structure,
381 "CostValue",
382 GffValue::UInt16(self.cost_value),
383 );
384 upsert_field(&mut structure, "Param1", GffValue::UInt8(self.param1));
385 upsert_field(
386 &mut structure,
387 "Param1Value",
388 GffValue::UInt8(self.param1_value),
389 );
390 upsert_field(
391 &mut structure,
392 "PropertyName",
393 GffValue::UInt16(self.property_name),
394 );
395 upsert_field(&mut structure, "Subtype", GffValue::UInt16(self.subtype));
396 upsert_field(
397 &mut structure,
398 "ChanceAppear",
399 GffValue::UInt8(self.chance_appear),
400 );
401 if let Some(value) = self.useable {
402 upsert_field(&mut structure, "Useable", GffValue::UInt8(u8::from(value)));
403 }
404 if let Some(value) = self.uses_per_day {
405 upsert_field(&mut structure, "UsesPerDay", GffValue::UInt8(value));
406 }
407 if let Some(value) = self.upgrade_type {
408 upsert_field(&mut structure, "UpgradeType", GffValue::UInt8(value));
409 }
410
411 structure
412 }
413}
414
415#[derive(Debug, Error)]
417pub enum UtiError {
418 #[error("unsupported UTI file type: {0:?}")]
420 UnsupportedFileType([u8; 4]),
421 #[error("UTI field `{field}` has incompatible type (expected {expected})")]
423 TypeMismatch {
424 field: &'static str,
426 expected: &'static str,
428 },
429 #[error(transparent)]
431 Gff(#[from] GffBinaryError),
432}
433
434#[cfg_attr(
436 feature = "tracing",
437 tracing::instrument(level = "debug", skip(reader))
438)]
439pub fn read_uti<R: Read>(reader: &mut R) -> Result<Uti, UtiError> {
440 let gff = read_gff(reader)?;
441 Uti::from_gff(&gff)
442}
443
444#[cfg_attr(
446 feature = "tracing",
447 tracing::instrument(level = "debug", skip(bytes), fields(bytes_len = bytes.len()))
448)]
449pub fn read_uti_from_bytes(bytes: &[u8]) -> Result<Uti, UtiError> {
450 let gff = read_gff_from_bytes(bytes)?;
451 Uti::from_gff(&gff)
452}
453
454#[cfg_attr(
456 feature = "tracing",
457 tracing::instrument(level = "debug", skip(writer, uti))
458)]
459pub fn write_uti<W: Write>(writer: &mut W, uti: &Uti) -> Result<(), UtiError> {
460 let gff = uti.to_gff();
461 write_gff(writer, &gff)?;
462 Ok(())
463}
464
465#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(uti)))]
467pub fn write_uti_to_vec(uti: &Uti) -> Result<Vec<u8>, UtiError> {
468 let mut cursor = Cursor::new(Vec::new());
469 write_uti(&mut cursor, uti)?;
470 Ok(cursor.into_inner())
471}
472
473static PROPERTIES_LIST_CHILDREN: &[FieldSchema] = &[
475 FieldSchema {
476 label: "PropertyName",
477 expected_type: GffType::UInt16,
478 required: false,
479 children: None,
480 constraint: None,
481 },
482 FieldSchema {
483 label: "Subtype",
484 expected_type: GffType::UInt16,
485 required: false,
486 children: None,
487 constraint: None,
488 },
489 FieldSchema {
490 label: "CostTable",
491 expected_type: GffType::UInt8,
492 required: false,
493 children: None,
494 constraint: None,
495 },
496 FieldSchema {
497 label: "CostValue",
498 expected_type: GffType::UInt16,
499 required: false,
500 children: None,
501 constraint: None,
502 },
503 FieldSchema {
504 label: "Param1",
505 expected_type: GffType::UInt8,
506 required: false,
507 children: None,
508 constraint: None,
509 },
510 FieldSchema {
511 label: "Param1Value",
512 expected_type: GffType::UInt8,
513 required: false,
514 children: None,
515 constraint: None,
516 },
517 FieldSchema {
518 label: "ChanceAppear",
519 expected_type: GffType::UInt8,
520 required: false,
521 children: None,
522 constraint: None,
523 },
524 FieldSchema {
525 label: "Useable",
526 expected_type: GffType::UInt8,
527 required: false,
528 children: None,
529 constraint: None,
530 },
531 FieldSchema {
532 label: "UsesPerDay",
533 expected_type: GffType::UInt8,
534 required: false,
535 children: None,
536 constraint: None,
537 },
538 FieldSchema {
539 label: "UpgradeType",
540 expected_type: GffType::UInt8,
541 required: false,
542 children: None,
543 constraint: None,
544 },
545];
546
547impl GffSchema for Uti {
548 fn schema() -> &'static [FieldSchema] {
549 static SCHEMA: &[FieldSchema] = &[
550 FieldSchema {
552 label: "BaseItem",
553 expected_type: GffType::Int32,
554 required: false,
555 children: None,
556 constraint: None,
557 },
558 FieldSchema {
559 label: "Tag",
560 expected_type: GffType::String,
561 required: false,
562 children: None,
563 constraint: None,
564 },
565 FieldSchema {
566 label: "Identified",
567 expected_type: GffType::UInt8,
568 required: false,
569 children: None,
570 constraint: None,
571 },
572 FieldSchema {
573 label: "Description",
574 expected_type: GffType::LocalizedString,
575 required: false,
576 children: None,
577 constraint: None,
578 },
579 FieldSchema {
580 label: "DescIdentified",
581 expected_type: GffType::LocalizedString,
582 required: false,
583 children: None,
584 constraint: None,
585 },
586 FieldSchema {
587 label: "LocalizedName",
588 expected_type: GffType::LocalizedString,
589 required: false,
590 children: None,
591 constraint: None,
592 },
593 FieldSchema {
594 label: "StackSize",
595 expected_type: GffType::UInt16,
596 required: false,
597 children: None,
598 constraint: None,
599 },
600 FieldSchema {
601 label: "Stolen",
602 expected_type: GffType::UInt8,
603 required: false,
604 children: None,
605 constraint: None,
606 },
607 FieldSchema {
608 label: "Upgrades",
609 expected_type: GffType::UInt32,
610 required: false,
611 children: None,
612 constraint: None,
613 },
614 FieldSchema {
615 label: "Dropable",
616 expected_type: GffType::UInt8,
617 required: false,
618 children: None,
619 constraint: None,
620 },
621 FieldSchema {
622 label: "Pickpocketable",
623 expected_type: GffType::UInt8,
624 required: false,
625 children: None,
626 constraint: None,
627 },
628 FieldSchema {
629 label: "NonEquippable",
630 expected_type: GffType::UInt8,
631 required: false,
632 children: None,
633 constraint: None,
634 },
635 FieldSchema {
636 label: "ModelVariation",
637 expected_type: GffType::UInt8,
638 required: false,
639 children: None,
640 constraint: None,
641 },
642 FieldSchema {
643 label: "TextureVar",
644 expected_type: GffType::UInt8,
645 required: false,
646 children: None,
647 constraint: None,
648 },
649 FieldSchema {
650 label: "Charges",
651 expected_type: GffType::UInt8,
652 required: false,
653 children: None,
654 constraint: None,
655 },
656 FieldSchema {
657 label: "MaxCharges",
658 expected_type: GffType::UInt8,
659 required: false,
660 children: None,
661 constraint: None,
662 },
663 FieldSchema {
664 label: "NewItem",
665 expected_type: GffType::UInt8,
666 required: false,
667 children: None,
668 constraint: None,
669 },
670 FieldSchema {
671 label: "DELETING",
672 expected_type: GffType::UInt8,
673 required: false,
674 children: None,
675 constraint: None,
676 },
677 FieldSchema {
678 label: "AddCost",
679 expected_type: GffType::UInt32,
680 required: false,
681 children: None,
682 constraint: None,
683 },
684 FieldSchema {
685 label: "Plot",
686 expected_type: GffType::UInt8,
687 required: false,
688 children: None,
689 constraint: None,
690 },
691 FieldSchema {
693 label: "PropertiesList",
694 expected_type: GffType::List,
695 required: false,
696 children: Some(PROPERTIES_LIST_CHILDREN),
697 constraint: None,
698 },
699 FieldSchema {
701 label: "TemplateResRef",
702 expected_type: GffType::ResRef,
703 required: false,
704 children: None,
705 constraint: None,
706 },
707 FieldSchema {
708 label: "Comment",
709 expected_type: GffType::String,
710 required: false,
711 children: None,
712 constraint: None,
713 },
714 FieldSchema {
715 label: "PaletteID",
716 expected_type: GffType::UInt8,
717 required: false,
718 children: None,
719 constraint: None,
720 },
721 FieldSchema {
722 label: "Cost",
723 expected_type: GffType::UInt32,
724 required: false,
725 children: None,
726 constraint: None,
727 },
728 FieldSchema {
729 label: "BodyVariation",
730 expected_type: GffType::UInt8,
731 required: false,
732 children: None,
733 constraint: None,
734 },
735 FieldSchema {
736 label: "UpgradeLevel",
737 expected_type: GffType::UInt8,
738 required: false,
739 children: None,
740 constraint: None,
741 },
742 FieldSchema {
743 label: "ModelPart1",
744 expected_type: GffType::UInt8,
745 required: false,
746 children: None,
747 constraint: None,
748 },
749 ];
750 SCHEMA
751 }
752}
753
754#[cfg(test)]
755mod tests {
756 use super::*;
757
758 const TEST_UTI: &[u8] = include_bytes!(concat!(
759 env!("CARGO_MANIFEST_DIR"),
760 "/../../fixtures/test.uti"
761 ));
762
763 #[test]
764 fn reads_core_uti_fields_from_fixture() {
765 let uti = read_uti_from_bytes(TEST_UTI).expect("fixture must parse");
766
767 assert_eq!(uti.template_resref, "g_a_class4001");
768 assert_eq!(uti.base_item, 38);
769 assert_eq!(uti.name.string_ref.raw(), 5632);
770 assert_eq!(uti.description_unidentified.string_ref.raw(), 456);
771 assert_eq!(uti.description_identified.string_ref.raw(), 5633);
772 assert_eq!(uti.tag, "G_A_CLASS4001");
773 assert_eq!(uti.charges, 13);
774 assert_eq!(uti.max_charges, 13);
775 assert_eq!(uti.cost, 50);
776 assert_eq!(uti.stack_size, 1);
777 assert!(uti.plot);
778 assert_eq!(uti.add_cost, 50);
779 assert_eq!(uti.palette_id, 1);
780 assert_eq!(uti.comment, "itemo");
781 assert_eq!(uti.model_variation, 2);
782 assert_eq!(uti.body_variation, 3);
783 assert_eq!(uti.texture_variation, 1);
784 assert_eq!(uti.upgrade_level, 0);
785 assert!(uti.is_armor());
786 assert!(uti.stolen);
787 assert!(uti.identified);
788 assert!(!uti.droppable);
789 assert!(!uti.pickpocketable);
790 assert!(!uti.non_equippable);
791 assert!(!uti.new_item);
792 assert!(!uti.deleting);
793 assert_eq!(uti.upgrades, 0);
794
795 assert_eq!(uti.properties.len(), 2);
796 assert_eq!(uti.properties[0].property_name, 45);
797 assert_eq!(uti.properties[0].subtype, 6);
798 assert_eq!(uti.properties[0].cost_table, 1);
799 assert_eq!(uti.properties[0].cost_value, 1);
800 assert_eq!(uti.properties[0].param1, 255);
801 assert_eq!(uti.properties[0].param1_value, 1);
802 assert_eq!(uti.properties[0].chance_appear, 100);
803 assert_eq!(uti.properties[0].useable, None);
804 assert_eq!(uti.properties[0].uses_per_day, None);
805 assert_eq!(uti.properties[0].upgrade_type, None);
806 assert_eq!(uti.properties[1].upgrade_type, Some(24));
807 }
808
809 #[test]
810 fn all_fields_survive_typed_roundtrip() {
811 let uti = read_uti_from_bytes(TEST_UTI).expect("fixture must parse");
812 let bytes = write_uti_to_vec(&uti).expect("write succeeds");
813 let reparsed = read_uti_from_bytes(&bytes).expect("reparse succeeds");
814 assert_eq!(reparsed, uti);
815 }
816
817 #[test]
818 fn typed_edits_roundtrip_through_gff_writer() {
819 let mut uti = read_uti_from_bytes(TEST_UTI).expect("fixture must parse");
820 uti.tag = "g_a_class4001_mod".into();
821 uti.cost = 777;
822 uti.plot = false;
823 uti.max_charges = 55;
824 uti.droppable = true;
825 uti.pickpocketable = true;
826 uti.non_equippable = true;
827 uti.new_item = true;
828 uti.deleting = true;
829 uti.upgrades = 0xABCD;
830 uti.properties[0].chance_appear = 25;
831 uti.properties[0].useable = Some(true);
832 uti.properties[0].uses_per_day = Some(5);
833 uti.properties[0].upgrade_type = Some(5);
834
835 let encoded = write_uti_to_vec(&uti).expect("encode");
836 let reparsed = read_uti_from_bytes(&encoded).expect("decode");
837
838 assert_eq!(reparsed.tag, "g_a_class4001_mod");
839 assert_eq!(reparsed.cost, 777);
840 assert!(!reparsed.plot);
841 assert_eq!(reparsed.max_charges, 55);
842 assert!(reparsed.droppable);
843 assert!(reparsed.pickpocketable);
844 assert!(reparsed.non_equippable);
845 assert!(reparsed.new_item);
846 assert!(reparsed.deleting);
847 assert_eq!(reparsed.upgrades, 0xABCD);
848 assert_eq!(reparsed.properties[0].chance_appear, 25);
849 assert_eq!(reparsed.properties[0].useable, Some(true));
850 assert_eq!(reparsed.properties[0].uses_per_day, Some(5));
851 assert_eq!(reparsed.properties[0].upgrade_type, Some(5));
852 }
853
854 #[test]
855 fn applies_runtime_defaults_for_missing_charge_and_flag_fields() {
856 let mut root = GffStruct::new(-1);
857 root.push_field("TemplateResRef", GffValue::resref_lit("g_i_test"));
858 root.push_field("BaseItem", GffValue::Int32(1));
859 root.push_field(
860 "LocalizedName",
861 GffValue::LocalizedString(GffLocalizedString::new(1)),
862 );
863 root.push_field(
864 "Description",
865 GffValue::LocalizedString(GffLocalizedString::new(2)),
866 );
867 root.push_field(
868 "DescIdentified",
869 GffValue::LocalizedString(GffLocalizedString::new(3)),
870 );
871 root.push_field("PropertiesList", GffValue::List(Vec::new()));
872 let gff = Gff::new(*b"UTI ", root);
873
874 let uti = Uti::from_gff(&gff).expect("must parse");
875 assert_eq!(uti.charges, 50);
876 assert_eq!(uti.max_charges, 50);
877 assert!(uti.identified);
878 assert!(!uti.droppable);
879 assert!(!uti.pickpocketable);
880 assert!(!uti.non_equippable);
881 assert!(!uti.new_item);
882 assert!(!uti.deleting);
883 assert_eq!(uti.upgrades, 0);
884 }
885
886 #[test]
887 fn reads_runtime_state_fields_from_gff() {
888 let mut root = GffStruct::new(-1);
889 root.push_field("TemplateResRef", GffValue::resref_lit("g_i_test"));
890 root.push_field("BaseItem", GffValue::Int32(1));
891 root.push_field(
892 "LocalizedName",
893 GffValue::LocalizedString(GffLocalizedString::new(1)),
894 );
895 root.push_field(
896 "Description",
897 GffValue::LocalizedString(GffLocalizedString::new(2)),
898 );
899 root.push_field(
900 "DescIdentified",
901 GffValue::LocalizedString(GffLocalizedString::new(3)),
902 );
903 root.push_field("Charges", GffValue::UInt8(9));
904 root.push_field("MaxCharges", GffValue::UInt8(12));
905 root.push_field("Identified", GffValue::UInt8(0));
906 root.push_field("Dropable", GffValue::UInt8(1));
907 root.push_field("Pickpocketable", GffValue::UInt8(1));
908 root.push_field("NonEquippable", GffValue::UInt8(1));
909 root.push_field("NewItem", GffValue::UInt8(1));
910 root.push_field("DELETING", GffValue::UInt8(1));
911 root.push_field("Upgrades", GffValue::UInt32(0x1234_5678));
912 root.push_field("PropertiesList", GffValue::List(Vec::new()));
913 let gff = Gff::new(*b"UTI ", root);
914
915 let uti = Uti::from_gff(&gff).expect("must parse");
916 assert_eq!(uti.charges, 9);
917 assert_eq!(uti.max_charges, 12);
918 assert!(!uti.identified);
919 assert!(uti.droppable);
920 assert!(uti.pickpocketable);
921 assert!(uti.non_equippable);
922 assert!(uti.new_item);
923 assert!(uti.deleting);
924 assert_eq!(uti.upgrades, 0x1234_5678);
925 }
926
927 #[test]
928 fn rejects_non_uti_file_type() {
929 let gff = Gff::new(*b"UTC ", GffStruct::new(-1));
930 let err = Uti::from_gff(&gff).expect_err("must fail");
931 assert!(matches!(err, UtiError::UnsupportedFileType(file_type) if file_type == *b"UTC "));
932 }
933
934 #[test]
935 fn read_uti_from_reader_matches_bytes_path() {
936 let mut cursor = Cursor::new(TEST_UTI);
937 let via_reader = read_uti(&mut cursor).expect("reader parse");
938 let via_bytes = read_uti_from_bytes(TEST_UTI).expect("bytes parse");
939 assert_eq!(via_reader.template_resref, via_bytes.template_resref);
940 assert_eq!(via_reader.properties.len(), via_bytes.properties.len());
941 }
942
943 #[test]
944 fn type_mismatch_on_properties_list_is_error() {
945 let mut root = GffStruct::new(-1);
946 root.push_field("PropertiesList", GffValue::UInt32(7));
947 let gff = Gff::new(*b"UTI ", root);
948 let err = Uti::from_gff(&gff).expect_err("must fail");
949 assert!(matches!(
950 err,
951 UtiError::TypeMismatch {
952 field: "PropertiesList",
953 expected: "List"
954 }
955 ));
956 }
957
958 #[test]
959 fn write_uti_matches_direct_gff_writer() {
960 let uti = read_uti_from_bytes(TEST_UTI).expect("fixture parse");
961 let from_uti = write_uti_to_vec(&uti).expect("uti encode");
962
963 let gff = uti.to_gff();
964 let from_gff = rakata_formats::write_gff_to_vec(&gff).expect("gff encode");
965 assert_eq!(from_uti, from_gff);
966 }
967
968 #[test]
969 fn armor_base_item_helper_matches_known_values() {
970 assert!(is_armor_base_item(38));
971 assert!(is_armor_base_item(103));
972 assert!(!is_armor_base_item(1));
973 assert!(!is_armor_base_item(-1));
974 }
975
976 #[test]
977 fn schema_field_count() {
978 assert_eq!(Uti::schema().len(), 28); }
980
981 #[test]
982 fn schema_no_duplicate_labels() {
983 let schema = Uti::schema();
984 let mut labels: Vec<&str> = schema.iter().map(|f| f.label).collect();
985 labels.sort();
986 let before = labels.len();
987 labels.dedup();
988 assert_eq!(before, labels.len(), "duplicate labels in UTI schema");
989 }
990
991 #[test]
992 fn schema_properties_list_has_children() {
993 let props = Uti::schema()
994 .iter()
995 .find(|f| f.label == "PropertiesList")
996 .expect("test fixture must be valid");
997 assert!(props.children.is_some());
998 assert_eq!(
999 props.children.expect("test fixture must be valid").len(),
1000 10
1001 );
1002 }
1003}