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