1use std::io::{Cursor, Read, Write};
35
36use crate::gff_helpers::{
37 get_bool, get_f32, get_i16, get_i32, get_locstring, get_resref, get_string, get_u16, get_u32,
38 get_u8, upsert_field,
39};
40use rakata_core::{ResRef, StrRef};
41use rakata_formats::{
42 gff_schema::{FieldConstraint, FieldSchema, GffSchema, GffType},
43 read_gff, read_gff_from_bytes, write_gff, Gff, GffBinaryError, GffLocalizedString, GffStruct,
44 GffValue,
45};
46use thiserror::Error;
47
48#[derive(Debug, Clone, PartialEq)]
50pub struct Utc {
51 pub template_resref: ResRef,
53 pub tag: String,
55 pub comment: String,
57 pub conversation: ResRef,
59 pub first_name: GffLocalizedString,
61 pub last_name: GffLocalizedString,
63 pub age: i32,
65 pub starting_package: u8,
67 pub gold: u32,
69 pub invulnerable: bool,
71 pub experience: u32,
73 pub subrace_id: u8,
75 pub perception_id: u8,
77 pub race_id: u8,
79 pub color_skin: u8,
81 pub color_hair: u8,
83 pub color_tattoo1: u8,
85 pub color_tattoo2: u8,
87 pub appearance_head: u8,
89 pub duplicating_head: u8,
91 pub use_backup_head: u8,
93 pub appearance_id: u16,
95 pub gender_id: u8,
97 pub faction_id: u16,
99 pub walkrate_id: i32,
101 pub ai_state: u16,
103 pub skill_points: u16,
105 pub movement_rate: u8,
107 pub soundset_id: u16,
109 pub portrait_id: u16,
111 pub portrait_resref: ResRef,
113 pub save_will: u8,
115 pub save_fortitude: u8,
117 pub morale: u8,
119 pub morale_recovery: u8,
121 pub morale_breakpoint: u8,
123 pub palette_id: u8,
125 pub bodybag_id: u8,
127 pub body_variation: u8,
129 pub texture_variation: u8,
131 pub alignment: u8,
133 pub challenge_rating: f32,
135 pub blindspot: f32,
137 pub multiplier_set: u8,
139 pub natural_ac: u8,
141 pub reflex_bonus: i16,
143 pub willpower_bonus: i16,
145 pub fortitude_bonus: i16,
147 pub strength: u8,
149 pub dexterity: u8,
151 pub constitution: u8,
153 pub intelligence: u8,
155 pub wisdom: u8,
157 pub charisma: u8,
159 pub current_hp: i16,
161 pub max_hp: i16,
163 pub hp: i16,
165 pub fp: i16,
167 pub max_fp: i16,
169 pub not_reorienting: bool,
171 pub party_interact: bool,
173 pub no_perm_death: bool,
175 pub min1_hp: bool,
177 pub plot: bool,
179 pub interruptable: bool,
181 pub is_pc: bool,
183 pub disarmable: bool,
185 pub ignore_cre_path: bool,
187 pub hologram: bool,
189 pub will_not_render: bool,
191 pub deity: String,
193 pub description: GffLocalizedString,
195 pub lawfulness: u8,
197 pub phenotype_id: i32,
199 pub subrace_name: String,
201 pub on_end_dialog: ResRef,
203 pub on_blocked: ResRef,
205 pub on_heartbeat: ResRef,
207 pub on_notice: ResRef,
209 pub on_spell: ResRef,
211 pub on_attacked: ResRef,
213 pub on_damaged: ResRef,
215 pub on_disturbed: ResRef,
217 pub on_end_round: ResRef,
219 pub on_dialog: ResRef,
221 pub on_spawn: ResRef,
223 pub on_rested: ResRef,
225 pub on_death: ResRef,
227 pub on_user_defined: ResRef,
229 pub skills: UtcSkills,
231 pub classes: Vec<UtcClass>,
233 pub special_abilities: Vec<UtcSpecialAbility>,
235 pub feats: Vec<u16>,
237 pub equipment: Vec<UtcEquipmentItem>,
239 pub inventory: Vec<UtcInventoryItem>,
241}
242
243impl Default for Utc {
244 fn default() -> Self {
245 Self {
246 template_resref: ResRef::blank(),
247 tag: String::new(),
248 comment: String::new(),
249 conversation: ResRef::blank(),
250 first_name: GffLocalizedString::new(StrRef::invalid()),
251 last_name: GffLocalizedString::new(StrRef::invalid()),
252 age: 0,
253 starting_package: 0,
254 gold: 0,
255 invulnerable: false,
256 experience: 0,
257 subrace_id: 0,
258 perception_id: 0,
259 race_id: 0,
260 color_skin: 0,
261 color_hair: 0,
262 color_tattoo1: 0,
263 color_tattoo2: 0,
264 appearance_head: 0,
265 duplicating_head: 0,
266 use_backup_head: 0,
267 appearance_id: 0,
268 gender_id: 0,
269 faction_id: 0,
270 walkrate_id: 0,
271 ai_state: 0,
272 skill_points: 0,
273 movement_rate: 0,
274 soundset_id: 0,
275 portrait_id: 0,
276 portrait_resref: ResRef::blank(),
277 save_will: 0,
278 save_fortitude: 0,
279 morale: 0,
280 morale_recovery: 0,
281 morale_breakpoint: 0,
282 palette_id: 0,
283 bodybag_id: 0,
284 body_variation: 0,
285 texture_variation: 0,
286 alignment: 0,
287 challenge_rating: 0.0,
288 blindspot: 0.0,
289 multiplier_set: 0,
290 natural_ac: 0,
291 reflex_bonus: 0,
292 willpower_bonus: 0,
293 fortitude_bonus: 0,
294 strength: 0,
295 dexterity: 0,
296 constitution: 0,
297 intelligence: 0,
298 wisdom: 0,
299 charisma: 0,
300 current_hp: 0,
301 max_hp: 0,
302 hp: 0,
303 fp: 0,
304 max_fp: 0,
305 not_reorienting: false,
306 party_interact: false,
307 no_perm_death: false,
308 min1_hp: false,
309 plot: false,
310 interruptable: false,
311 is_pc: false,
312 disarmable: false,
313 ignore_cre_path: false,
314 hologram: false,
315 will_not_render: false,
316 deity: String::new(),
317 description: GffLocalizedString::new(StrRef::invalid()),
318 lawfulness: 0,
319 phenotype_id: 0,
320 subrace_name: String::new(),
321 on_end_dialog: ResRef::blank(),
322 on_blocked: ResRef::blank(),
323 on_heartbeat: ResRef::blank(),
324 on_notice: ResRef::blank(),
325 on_spell: ResRef::blank(),
326 on_attacked: ResRef::blank(),
327 on_damaged: ResRef::blank(),
328 on_disturbed: ResRef::blank(),
329 on_end_round: ResRef::blank(),
330 on_dialog: ResRef::blank(),
331 on_spawn: ResRef::blank(),
332 on_rested: ResRef::blank(),
333 on_death: ResRef::blank(),
334 on_user_defined: ResRef::blank(),
335 skills: UtcSkills::default(),
336 classes: Vec::new(),
337 special_abilities: Vec::new(),
338 feats: Vec::new(),
339 equipment: Vec::new(),
340 inventory: Vec::new(),
341 }
342 }
343}
344
345impl Utc {
346 pub fn new() -> Self {
348 Self::default()
349 }
350
351 pub fn from_gff(gff: &Gff) -> Result<Self, UtcError> {
353 if gff.file_type != *b"UTC " && gff.file_type != *b"GFF " {
354 return Err(UtcError::UnsupportedFileType(gff.file_type));
355 }
356
357 let root = &gff.root;
358
359 let skills = match root.field("SkillList") {
360 Some(GffValue::List(skill_structs)) => UtcSkills::from_list(skill_structs),
361 Some(_) => {
362 return Err(UtcError::TypeMismatch {
363 field: "SkillList",
364 expected: "List",
365 });
366 }
367 None => UtcSkills::default(),
368 };
369
370 let classes = match root.field("ClassList") {
371 Some(GffValue::List(class_structs)) => class_structs
372 .iter()
373 .map(UtcClass::from_struct)
374 .collect::<Result<Vec<_>, _>>()?,
375 Some(_) => {
376 return Err(UtcError::TypeMismatch {
377 field: "ClassList",
378 expected: "List",
379 });
380 }
381 None => Vec::new(),
382 };
383
384 let special_abilities = match root.field("SpecAbilityList") {
385 Some(GffValue::List(ability_structs)) => ability_structs
386 .iter()
387 .map(UtcSpecialAbility::from_struct)
388 .collect::<Vec<_>>(),
389 Some(_) => {
390 return Err(UtcError::TypeMismatch {
391 field: "SpecAbilityList",
392 expected: "List",
393 });
394 }
395 None => Vec::new(),
396 };
397
398 let feats = match root.field("FeatList") {
399 Some(GffValue::List(feat_structs)) => feat_structs
400 .iter()
401 .map(|feat_struct| get_u16(feat_struct, "Feat").unwrap_or(0))
402 .collect::<Vec<_>>(),
403 Some(_) => {
404 return Err(UtcError::TypeMismatch {
405 field: "FeatList",
406 expected: "List",
407 });
408 }
409 None => Vec::new(),
410 };
411
412 let equipment = match root.field("Equip_ItemList") {
413 Some(GffValue::List(equipment_structs)) => equipment_structs
414 .iter()
415 .map(UtcEquipmentItem::from_struct)
416 .collect::<Vec<_>>(),
417 Some(_) => {
418 return Err(UtcError::TypeMismatch {
419 field: "Equip_ItemList",
420 expected: "List",
421 });
422 }
423 None => Vec::new(),
424 };
425
426 let inventory = match root.field("ItemList") {
427 Some(GffValue::List(inventory_structs)) => inventory_structs
428 .iter()
429 .map(UtcInventoryItem::from_struct)
430 .collect::<Vec<_>>(),
431 Some(_) => {
432 return Err(UtcError::TypeMismatch {
433 field: "ItemList",
434 expected: "List",
435 });
436 }
437 None => Vec::new(),
438 };
439
440 Ok(Self {
441 template_resref: get_resref(root, "TemplateResRef").unwrap_or_default(),
442 tag: get_string(root, "Tag").unwrap_or_default(),
443 comment: get_string(root, "Comment").unwrap_or_default(),
444 conversation: get_resref(root, "Conversation").unwrap_or_default(),
445 first_name: get_locstring(root, "FirstName")
446 .cloned()
447 .unwrap_or_else(|| GffLocalizedString::new(StrRef::invalid())),
448 last_name: get_locstring(root, "LastName")
449 .cloned()
450 .unwrap_or_else(|| GffLocalizedString::new(StrRef::invalid())),
451 age: get_i32(root, "Age").unwrap_or(0),
452 starting_package: get_u8(root, "StartingPackage").unwrap_or(0),
453 gold: get_u32(root, "Gold").unwrap_or(0),
454 invulnerable: get_bool(root, "Invulnerable").unwrap_or(false),
455 experience: get_u32(root, "Experience").unwrap_or(0),
456 subrace_id: get_u8(root, "SubraceIndex").unwrap_or(0),
457 perception_id: get_u8(root, "PerceptionRange").unwrap_or(0),
458 race_id: get_u8(root, "Race").unwrap_or(0),
459 color_skin: get_u8(root, "Color_Skin").unwrap_or(0),
460 color_hair: get_u8(root, "Color_Hair").unwrap_or(0),
461 color_tattoo1: get_u8(root, "Color_Tattoo1").unwrap_or(0),
462 color_tattoo2: get_u8(root, "Color_Tattoo2").unwrap_or(0),
463 appearance_head: get_u8(root, "Appearance_Head").unwrap_or(0),
464 duplicating_head: get_u8(root, "DuplicatingHead").unwrap_or(0),
465 use_backup_head: get_u8(root, "UseBackupHead").unwrap_or(0),
466 appearance_id: get_u16(root, "Appearance_Type").unwrap_or(0),
467 gender_id: get_u8(root, "Gender").unwrap_or(0),
468 faction_id: get_u16(root, "FactionID").unwrap_or(0),
469 walkrate_id: get_i32(root, "WalkRate").unwrap_or(0),
470 ai_state: get_u16(root, "AIState")
471 .or_else(|| get_i32(root, "AIState").and_then(|v| u16::try_from(v).ok()))
472 .unwrap_or(0),
473 skill_points: get_u16(root, "SkillPoints").unwrap_or(0),
474 movement_rate: get_u8(root, "MovementRate").unwrap_or(0),
475 soundset_id: get_u16(root, "SoundSetFile").unwrap_or(0),
476 portrait_id: get_u16(root, "PortraitId").unwrap_or(0),
477 portrait_resref: get_resref(root, "Portrait").unwrap_or_default(),
478 save_will: get_u8(root, "SaveWill").unwrap_or(0),
479 save_fortitude: get_u8(root, "SaveFortitude").unwrap_or(0),
480 morale: get_u8(root, "Morale").unwrap_or(0),
481 morale_recovery: get_u8(root, "MoraleRecovery").unwrap_or(0),
482 morale_breakpoint: get_u8(root, "MoraleBreakpoint").unwrap_or(0),
483 palette_id: get_u8(root, "PaletteID").unwrap_or(0),
484 bodybag_id: get_u8(root, "BodyBag").unwrap_or(0),
485 body_variation: get_u8(root, "BodyVariation").unwrap_or(0),
486 texture_variation: get_u8(root, "TextureVar").unwrap_or(0),
487 alignment: get_u8(root, "GoodEvil").unwrap_or(0),
488 challenge_rating: get_f32(root, "ChallengeRating").unwrap_or(0.0),
489 blindspot: get_f32(root, "BlindSpot").unwrap_or(0.0),
490 multiplier_set: get_u8(root, "MultiplierSet").unwrap_or(0),
491 natural_ac: get_u8(root, "NaturalAC").unwrap_or(0),
492 reflex_bonus: get_i16(root, "refbonus").unwrap_or(0),
493 willpower_bonus: get_i16(root, "willbonus").unwrap_or(0),
494 fortitude_bonus: get_i16(root, "fortbonus").unwrap_or(0),
495 strength: get_u8(root, "Str").unwrap_or(0),
496 dexterity: get_u8(root, "Dex").unwrap_or(0),
497 constitution: get_u8(root, "Con").unwrap_or(0),
498 intelligence: get_u8(root, "Int").unwrap_or(0),
499 wisdom: get_u8(root, "Wis").unwrap_or(0),
500 charisma: get_u8(root, "Cha").unwrap_or(0),
501 current_hp: get_i16(root, "CurrentHitPoints").unwrap_or(0),
502 max_hp: get_i16(root, "MaxHitPoints").unwrap_or(0),
503 hp: get_i16(root, "HitPoints").unwrap_or(0),
504 fp: get_i16(root, "CurrentForce").unwrap_or(0),
505 max_fp: get_i16(root, "ForcePoints").unwrap_or(0),
506 not_reorienting: get_bool(root, "NotReorienting").unwrap_or(false),
507 party_interact: get_bool(root, "PartyInteract").unwrap_or(false),
508 no_perm_death: get_bool(root, "NoPermDeath").unwrap_or(false),
509 min1_hp: get_bool(root, "Min1HP").unwrap_or(false),
510 plot: get_bool(root, "Plot").unwrap_or(false),
511 interruptable: get_bool(root, "Interruptable").unwrap_or(false),
512 is_pc: get_bool(root, "IsPC").unwrap_or(false),
513 disarmable: get_bool(root, "Disarmable").unwrap_or(false),
514 ignore_cre_path: get_bool(root, "IgnoreCrePath").unwrap_or(false),
515 hologram: get_bool(root, "Hologram").unwrap_or(false),
516 will_not_render: get_bool(root, "WillNotRender").unwrap_or(false),
517 deity: get_string(root, "Deity").unwrap_or_default(),
518 description: get_locstring(root, "Description")
519 .cloned()
520 .unwrap_or_else(|| GffLocalizedString::new(StrRef::invalid())),
521 lawfulness: get_u8(root, "LawfulChaotic").unwrap_or(0),
522 phenotype_id: get_i32(root, "Phenotype").unwrap_or(0),
523 subrace_name: get_string(root, "Subrace").unwrap_or_default(),
524 on_end_dialog: get_resref(root, "ScriptEndDialogu").unwrap_or_default(),
525 on_blocked: get_resref(root, "ScriptOnBlocked").unwrap_or_default(),
526 on_heartbeat: get_resref(root, "ScriptHeartbeat").unwrap_or_default(),
527 on_notice: get_resref(root, "ScriptOnNotice").unwrap_or_default(),
528 on_spell: get_resref(root, "ScriptSpellAt").unwrap_or_default(),
529 on_attacked: get_resref(root, "ScriptAttacked").unwrap_or_default(),
530 on_damaged: get_resref(root, "ScriptDamaged").unwrap_or_default(),
531 on_disturbed: get_resref(root, "ScriptDisturbed").unwrap_or_default(),
532 on_end_round: get_resref(root, "ScriptEndRound").unwrap_or_default(),
533 on_dialog: get_resref(root, "ScriptDialogue").unwrap_or_default(),
534 on_spawn: get_resref(root, "ScriptSpawn").unwrap_or_default(),
535 on_rested: get_resref(root, "ScriptRested").unwrap_or_default(),
536 on_death: get_resref(root, "ScriptDeath").unwrap_or_default(),
537 on_user_defined: get_resref(root, "ScriptUserDefine").unwrap_or_default(),
538 skills,
539 classes,
540 special_abilities,
541 feats,
542 equipment,
543 inventory,
544 })
545 }
546
547 pub fn to_gff(&self) -> Gff {
549 let mut root = GffStruct::new(-1);
550
551 upsert_field(
552 &mut root,
553 "TemplateResRef",
554 GffValue::ResRef(self.template_resref),
555 );
556 upsert_field(&mut root, "Tag", GffValue::String(self.tag.clone()));
557 upsert_field(&mut root, "Comment", GffValue::String(self.comment.clone()));
558 upsert_field(
559 &mut root,
560 "Conversation",
561 GffValue::ResRef(self.conversation),
562 );
563 upsert_field(
564 &mut root,
565 "FirstName",
566 GffValue::LocalizedString(self.first_name.clone()),
567 );
568 upsert_field(
569 &mut root,
570 "LastName",
571 GffValue::LocalizedString(self.last_name.clone()),
572 );
573
574 upsert_field(&mut root, "Age", GffValue::Int32(self.age));
575 upsert_field(
576 &mut root,
577 "StartingPackage",
578 GffValue::UInt8(self.starting_package),
579 );
580 upsert_field(&mut root, "Gold", GffValue::UInt32(self.gold));
581 upsert_field(
582 &mut root,
583 "Invulnerable",
584 GffValue::UInt8(u8::from(self.invulnerable)),
585 );
586 upsert_field(&mut root, "Experience", GffValue::UInt32(self.experience));
587
588 upsert_field(&mut root, "SubraceIndex", GffValue::UInt8(self.subrace_id));
589 upsert_field(
590 &mut root,
591 "PerceptionRange",
592 GffValue::UInt8(self.perception_id),
593 );
594 upsert_field(&mut root, "Race", GffValue::UInt8(self.race_id));
595 upsert_field(&mut root, "Color_Skin", GffValue::UInt8(self.color_skin));
596 upsert_field(&mut root, "Color_Hair", GffValue::UInt8(self.color_hair));
597 upsert_field(
598 &mut root,
599 "Color_Tattoo1",
600 GffValue::UInt8(self.color_tattoo1),
601 );
602 upsert_field(
603 &mut root,
604 "Color_Tattoo2",
605 GffValue::UInt8(self.color_tattoo2),
606 );
607 upsert_field(
608 &mut root,
609 "Appearance_Head",
610 GffValue::UInt8(self.appearance_head),
611 );
612 upsert_field(
613 &mut root,
614 "DuplicatingHead",
615 GffValue::UInt8(self.duplicating_head),
616 );
617 upsert_field(
618 &mut root,
619 "UseBackupHead",
620 GffValue::UInt8(self.use_backup_head),
621 );
622 upsert_field(
623 &mut root,
624 "Appearance_Type",
625 GffValue::UInt16(self.appearance_id),
626 );
627 upsert_field(&mut root, "Gender", GffValue::UInt8(self.gender_id));
628 upsert_field(&mut root, "FactionID", GffValue::UInt16(self.faction_id));
629 upsert_field(&mut root, "WalkRate", GffValue::Int32(self.walkrate_id));
630 upsert_field(
632 &mut root,
633 "AIState",
634 GffValue::Int32(i32::from(self.ai_state)),
635 );
636 upsert_field(
637 &mut root,
638 "SkillPoints",
639 GffValue::UInt16(self.skill_points),
640 );
641 upsert_field(
642 &mut root,
643 "MovementRate",
644 GffValue::UInt8(self.movement_rate),
645 );
646 upsert_field(
647 &mut root,
648 "SoundSetFile",
649 GffValue::UInt16(self.soundset_id),
650 );
651 upsert_field(&mut root, "PortraitId", GffValue::UInt16(self.portrait_id));
652 upsert_field(
653 &mut root,
654 "Portrait",
655 GffValue::ResRef(self.portrait_resref),
656 );
657 upsert_field(&mut root, "SaveWill", GffValue::UInt8(self.save_will));
658 upsert_field(
659 &mut root,
660 "SaveFortitude",
661 GffValue::UInt8(self.save_fortitude),
662 );
663 upsert_field(&mut root, "Morale", GffValue::UInt8(self.morale));
664 upsert_field(
665 &mut root,
666 "MoraleRecovery",
667 GffValue::UInt8(self.morale_recovery),
668 );
669 upsert_field(
670 &mut root,
671 "MoraleBreakpoint",
672 GffValue::UInt8(self.morale_breakpoint),
673 );
674 upsert_field(&mut root, "PaletteID", GffValue::UInt8(self.palette_id));
675 upsert_field(&mut root, "BodyBag", GffValue::UInt8(self.bodybag_id));
676 upsert_field(
677 &mut root,
678 "BodyVariation",
679 GffValue::UInt8(self.body_variation),
680 );
681 upsert_field(
682 &mut root,
683 "TextureVar",
684 GffValue::UInt8(self.texture_variation),
685 );
686
687 upsert_field(&mut root, "GoodEvil", GffValue::UInt8(self.alignment));
688 upsert_field(
689 &mut root,
690 "ChallengeRating",
691 GffValue::Single(self.challenge_rating),
692 );
693 upsert_field(&mut root, "BlindSpot", GffValue::Single(self.blindspot));
694 upsert_field(
695 &mut root,
696 "MultiplierSet",
697 GffValue::UInt8(self.multiplier_set),
698 );
699 upsert_field(&mut root, "NaturalAC", GffValue::UInt8(self.natural_ac));
700 upsert_field(&mut root, "refbonus", GffValue::Int16(self.reflex_bonus));
701 upsert_field(
702 &mut root,
703 "willbonus",
704 GffValue::Int16(self.willpower_bonus),
705 );
706 upsert_field(
707 &mut root,
708 "fortbonus",
709 GffValue::Int16(self.fortitude_bonus),
710 );
711
712 upsert_field(&mut root, "Str", GffValue::UInt8(self.strength));
713 upsert_field(&mut root, "Dex", GffValue::UInt8(self.dexterity));
714 upsert_field(&mut root, "Con", GffValue::UInt8(self.constitution));
715 upsert_field(&mut root, "Int", GffValue::UInt8(self.intelligence));
716 upsert_field(&mut root, "Wis", GffValue::UInt8(self.wisdom));
717 upsert_field(&mut root, "Cha", GffValue::UInt8(self.charisma));
718
719 upsert_field(
720 &mut root,
721 "CurrentHitPoints",
722 GffValue::Int16(self.current_hp),
723 );
724 upsert_field(&mut root, "MaxHitPoints", GffValue::Int16(self.max_hp));
725 upsert_field(&mut root, "HitPoints", GffValue::Int16(self.hp));
726 upsert_field(&mut root, "CurrentForce", GffValue::Int16(self.fp));
727 upsert_field(&mut root, "ForcePoints", GffValue::Int16(self.max_fp));
728
729 upsert_field(
730 &mut root,
731 "NotReorienting",
732 GffValue::UInt8(u8::from(self.not_reorienting)),
733 );
734 upsert_field(
735 &mut root,
736 "PartyInteract",
737 GffValue::UInt8(u8::from(self.party_interact)),
738 );
739 upsert_field(
740 &mut root,
741 "NoPermDeath",
742 GffValue::UInt8(u8::from(self.no_perm_death)),
743 );
744 upsert_field(&mut root, "Min1HP", GffValue::UInt8(u8::from(self.min1_hp)));
745 upsert_field(&mut root, "Plot", GffValue::UInt8(u8::from(self.plot)));
746 upsert_field(
747 &mut root,
748 "Interruptable",
749 GffValue::UInt8(u8::from(self.interruptable)),
750 );
751 upsert_field(&mut root, "IsPC", GffValue::UInt8(u8::from(self.is_pc)));
752 upsert_field(
753 &mut root,
754 "Disarmable",
755 GffValue::UInt8(u8::from(self.disarmable)),
756 );
757 upsert_field(
758 &mut root,
759 "IgnoreCrePath",
760 GffValue::UInt8(u8::from(self.ignore_cre_path)),
761 );
762 upsert_field(
763 &mut root,
764 "Hologram",
765 GffValue::UInt8(u8::from(self.hologram)),
766 );
767 upsert_field(
768 &mut root,
769 "WillNotRender",
770 GffValue::UInt8(u8::from(self.will_not_render)),
771 );
772 upsert_field(&mut root, "Deity", GffValue::String(self.deity.clone()));
773 upsert_field(
774 &mut root,
775 "Description",
776 GffValue::LocalizedString(self.description.clone()),
777 );
778 upsert_field(&mut root, "LawfulChaotic", GffValue::UInt8(self.lawfulness));
779 upsert_field(&mut root, "Phenotype", GffValue::Int32(self.phenotype_id));
780 upsert_field(
781 &mut root,
782 "Subrace",
783 GffValue::String(self.subrace_name.clone()),
784 );
785
786 upsert_field(
787 &mut root,
788 "ScriptEndDialogu",
789 GffValue::ResRef(self.on_end_dialog),
790 );
791 upsert_field(
792 &mut root,
793 "ScriptOnBlocked",
794 GffValue::ResRef(self.on_blocked),
795 );
796 upsert_field(
797 &mut root,
798 "ScriptHeartbeat",
799 GffValue::ResRef(self.on_heartbeat),
800 );
801 upsert_field(
802 &mut root,
803 "ScriptOnNotice",
804 GffValue::ResRef(self.on_notice),
805 );
806 upsert_field(&mut root, "ScriptSpellAt", GffValue::ResRef(self.on_spell));
807 upsert_field(
808 &mut root,
809 "ScriptAttacked",
810 GffValue::ResRef(self.on_attacked),
811 );
812 upsert_field(
813 &mut root,
814 "ScriptDamaged",
815 GffValue::ResRef(self.on_damaged),
816 );
817 upsert_field(
818 &mut root,
819 "ScriptDisturbed",
820 GffValue::ResRef(self.on_disturbed),
821 );
822 upsert_field(
823 &mut root,
824 "ScriptEndRound",
825 GffValue::ResRef(self.on_end_round),
826 );
827 upsert_field(
828 &mut root,
829 "ScriptDialogue",
830 GffValue::ResRef(self.on_dialog),
831 );
832 upsert_field(&mut root, "ScriptSpawn", GffValue::ResRef(self.on_spawn));
833 upsert_field(&mut root, "ScriptRested", GffValue::ResRef(self.on_rested));
834 upsert_field(&mut root, "ScriptDeath", GffValue::ResRef(self.on_death));
835 upsert_field(
836 &mut root,
837 "ScriptUserDefine",
838 GffValue::ResRef(self.on_user_defined),
839 );
840
841 upsert_field(
842 &mut root,
843 "SkillList",
844 GffValue::List(self.skills.to_list()),
845 );
846
847 let class_structs = self
848 .classes
849 .iter()
850 .map(UtcClass::to_struct)
851 .collect::<Vec<GffStruct>>();
852 upsert_field(&mut root, "ClassList", GffValue::List(class_structs));
853
854 let special_ability_structs = self
855 .special_abilities
856 .iter()
857 .map(UtcSpecialAbility::to_struct)
858 .collect::<Vec<GffStruct>>();
859 upsert_field(
860 &mut root,
861 "SpecAbilityList",
862 GffValue::List(special_ability_structs),
863 );
864
865 upsert_field(
866 &mut root,
867 "FeatList",
868 GffValue::List(write_feat_structs(&self.feats, None)),
869 );
870
871 let equipment_structs = self
872 .equipment
873 .iter()
874 .map(UtcEquipmentItem::to_struct)
875 .collect::<Vec<GffStruct>>();
876 upsert_field(
877 &mut root,
878 "Equip_ItemList",
879 GffValue::List(equipment_structs),
880 );
881
882 let inventory_structs = self
883 .inventory
884 .iter()
885 .map(UtcInventoryItem::to_struct)
886 .collect::<Vec<GffStruct>>();
887 upsert_field(&mut root, "ItemList", GffValue::List(inventory_structs));
888
889 Gff::new(*b"UTC ", root)
890 }
891}
892
893#[derive(Debug, Clone, PartialEq, Default)]
895pub struct UtcSkills {
896 pub computer_use: u8,
898 pub demolitions: u8,
900 pub stealth: u8,
902 pub awareness: u8,
904 pub persuade: u8,
906 pub repair: u8,
908 pub security: u8,
910 pub treat_injury: u8,
912}
913
914impl UtcSkills {
915 fn from_list(list: &[GffStruct]) -> Self {
916 Self {
917 computer_use: read_skill_rank(list, 0),
918 demolitions: read_skill_rank(list, 1),
919 stealth: read_skill_rank(list, 2),
920 awareness: read_skill_rank(list, 3),
921 persuade: read_skill_rank(list, 4),
922 repair: read_skill_rank(list, 5),
923 security: read_skill_rank(list, 6),
924 treat_injury: read_skill_rank(list, 7),
925 }
926 }
927
928 fn to_list(&self) -> Vec<GffStruct> {
929 let mut list = default_skill_structs();
930
931 write_skill_rank(&mut list[0], self.computer_use);
932 write_skill_rank(&mut list[1], self.demolitions);
933 write_skill_rank(&mut list[2], self.stealth);
934 write_skill_rank(&mut list[3], self.awareness);
935 write_skill_rank(&mut list[4], self.persuade);
936 write_skill_rank(&mut list[5], self.repair);
937 write_skill_rank(&mut list[6], self.security);
938 write_skill_rank(&mut list[7], self.treat_injury);
939
940 list
941 }
942}
943
944#[derive(Debug, Clone, PartialEq)]
946pub struct UtcClass {
947 pub class_id: i32,
949 pub class_level: i16,
951 pub powers: Vec<u16>,
953}
954
955impl UtcClass {
956 fn from_struct(structure: &GffStruct) -> Result<Self, UtcError> {
957 let powers = parse_known_list(structure, "KnownList0")?;
958
959 Ok(Self {
960 class_id: get_i32(structure, "Class").unwrap_or(0),
961 class_level: get_i16(structure, "ClassLevel").unwrap_or(0),
962 powers,
963 })
964 }
965
966 fn to_struct(&self) -> GffStruct {
967 let mut structure = GffStruct::new(2);
968
969 upsert_field(&mut structure, "Class", GffValue::Int32(self.class_id));
970 upsert_field(
971 &mut structure,
972 "ClassLevel",
973 GffValue::Int16(self.class_level),
974 );
975 write_known_list(&mut structure, "KnownList0", &self.powers);
976 structure
977 }
978}
979
980#[derive(Debug, Clone, PartialEq)]
982pub struct UtcSpecialAbility {
983 pub spell_id: u16,
985 pub spell_flags: u8,
987 pub spell_caster_level: u8,
989}
990
991impl UtcSpecialAbility {
992 fn from_struct(structure: &GffStruct) -> Self {
993 Self {
994 spell_id: get_u16(structure, "Spell").unwrap_or(0),
995 spell_flags: get_u8(structure, "SpellFlags").unwrap_or(0),
996 spell_caster_level: get_u8(structure, "SpellCasterLevel").unwrap_or(0),
997 }
998 }
999
1000 fn to_struct(&self) -> GffStruct {
1001 let mut structure = GffStruct::new(4);
1002
1003 upsert_field(&mut structure, "Spell", GffValue::UInt16(self.spell_id));
1004 upsert_field(
1005 &mut structure,
1006 "SpellFlags",
1007 GffValue::UInt8(self.spell_flags),
1008 );
1009 upsert_field(
1010 &mut structure,
1011 "SpellCasterLevel",
1012 GffValue::UInt8(self.spell_caster_level),
1013 );
1014 structure
1015 }
1016}
1017
1018fn parse_known_list(structure: &GffStruct, label: &'static str) -> Result<Vec<u16>, UtcError> {
1019 match structure.field(label) {
1020 Some(GffValue::List(power_structs)) => Ok(power_structs
1021 .iter()
1022 .map(|power_struct| get_u16(power_struct, "Spell").unwrap_or(0))
1023 .collect::<Vec<_>>()),
1024 Some(_) => Err(UtcError::TypeMismatch {
1025 field: label,
1026 expected: "List",
1027 }),
1028 None => Ok(Vec::new()),
1029 }
1030}
1031
1032fn write_known_list(structure: &mut GffStruct, label: &'static str, powers: &[u16]) {
1033 let power_structs = powers
1034 .iter()
1035 .copied()
1036 .map(|spell| {
1037 let mut s = GffStruct::new(3);
1038 upsert_field(&mut s, "Spell", GffValue::UInt16(spell));
1039 upsert_field(&mut s, "SpellFlags", GffValue::UInt8(1));
1040 upsert_field(&mut s, "SpellMetaMagic", GffValue::UInt8(0));
1041 s
1042 })
1043 .collect::<Vec<_>>();
1044
1045 upsert_field(structure, label, GffValue::List(power_structs));
1046}
1047
1048#[derive(Debug, Clone, PartialEq)]
1050pub struct UtcEquipmentItem {
1051 pub slot_id: i32,
1053 pub resref: ResRef,
1055 pub droppable: bool,
1057}
1058
1059impl UtcEquipmentItem {
1060 pub fn new(slot_id: i32, resref: ResRef) -> Self {
1062 Self {
1063 slot_id,
1064 resref,
1065 droppable: false,
1066 }
1067 }
1068
1069 fn from_struct(structure: &GffStruct) -> Self {
1070 Self {
1071 slot_id: structure.struct_id,
1072 resref: get_resref(structure, "EquippedRes").unwrap_or_default(),
1073 droppable: get_bool(structure, "Dropable").unwrap_or(false),
1074 }
1075 }
1076
1077 fn to_struct(&self) -> GffStruct {
1078 let mut structure = GffStruct::new(self.slot_id);
1079
1080 upsert_field(&mut structure, "EquippedRes", GffValue::ResRef(self.resref));
1081 if self.droppable {
1082 upsert_field(&mut structure, "Dropable", GffValue::UInt8(1));
1083 }
1084
1085 structure
1086 }
1087}
1088
1089#[derive(Debug, Clone, PartialEq)]
1091pub struct UtcInventoryItem {
1092 pub entry_id: i32,
1094 pub resref: ResRef,
1096 pub droppable: bool,
1098 pub repos_pos_x: Option<u16>,
1100 pub repos_pos_y: Option<u16>,
1102}
1103
1104impl UtcInventoryItem {
1105 pub fn new(entry_id: i32, resref: ResRef) -> Self {
1107 Self {
1108 entry_id,
1109 resref,
1110 droppable: false,
1111 repos_pos_x: None,
1112 repos_pos_y: None,
1113 }
1114 }
1115
1116 fn from_struct(structure: &GffStruct) -> Self {
1117 Self {
1118 entry_id: structure.struct_id,
1119 resref: get_resref(structure, "InventoryRes").unwrap_or_default(),
1120 droppable: get_bool(structure, "Dropable").unwrap_or(false),
1121 repos_pos_x: get_u16(structure, "Repos_PosX"),
1122 repos_pos_y: get_u16(structure, "Repos_Posy"),
1123 }
1124 }
1125
1126 fn to_struct(&self) -> GffStruct {
1127 let mut structure = GffStruct::new(self.entry_id);
1128
1129 upsert_field(
1130 &mut structure,
1131 "InventoryRes",
1132 GffValue::ResRef(self.resref),
1133 );
1134 if self.droppable {
1135 upsert_field(&mut structure, "Dropable", GffValue::UInt8(1));
1136 }
1137
1138 if let Some(value) = self.repos_pos_x {
1139 upsert_field(&mut structure, "Repos_PosX", GffValue::UInt16(value));
1140 }
1141 if let Some(value) = self.repos_pos_y {
1142 upsert_field(&mut structure, "Repos_Posy", GffValue::UInt16(value));
1143 }
1144
1145 structure
1146 }
1147}
1148
1149#[derive(Debug, Error)]
1151pub enum UtcError {
1152 #[error("unsupported UTC file type: {0:?}")]
1154 UnsupportedFileType([u8; 4]),
1155 #[error("UTC field `{field}` has incompatible type (expected {expected})")]
1157 TypeMismatch {
1158 field: &'static str,
1160 expected: &'static str,
1162 },
1163 #[error(transparent)]
1165 Gff(#[from] GffBinaryError),
1166}
1167
1168#[cfg_attr(
1170 feature = "tracing",
1171 tracing::instrument(level = "debug", skip(reader))
1172)]
1173pub fn read_utc<R: Read>(reader: &mut R) -> Result<Utc, UtcError> {
1174 let gff = read_gff(reader)?;
1175 Utc::from_gff(&gff)
1176}
1177
1178#[cfg_attr(
1180 feature = "tracing",
1181 tracing::instrument(level = "debug", skip(bytes), fields(bytes_len = bytes.len()))
1182)]
1183pub fn read_utc_from_bytes(bytes: &[u8]) -> Result<Utc, UtcError> {
1184 let gff = read_gff_from_bytes(bytes)?;
1185 Utc::from_gff(&gff)
1186}
1187
1188#[cfg_attr(
1190 feature = "tracing",
1191 tracing::instrument(level = "debug", skip(writer, utc))
1192)]
1193pub fn write_utc<W: Write>(writer: &mut W, utc: &Utc) -> Result<(), UtcError> {
1194 let gff = utc.to_gff();
1195 write_gff(writer, &gff)?;
1196 Ok(())
1197}
1198
1199#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(utc)))]
1201pub fn write_utc_to_vec(utc: &Utc) -> Result<Vec<u8>, UtcError> {
1202 let mut cursor = Cursor::new(Vec::new());
1203 write_utc(&mut cursor, utc)?;
1204 Ok(cursor.into_inner())
1205}
1206
1207fn write_feat_structs(feats: &[u16], existing: Option<&[GffStruct]>) -> Vec<GffStruct> {
1208 let mut feat_structs = existing.map_or_else(Vec::new, <[GffStruct]>::to_vec);
1209
1210 while feat_structs.len() < feats.len() {
1211 feat_structs.push(GffStruct::new(1));
1212 }
1213 if feat_structs.len() > feats.len() {
1214 feat_structs.truncate(feats.len());
1215 }
1216
1217 for (idx, feat_id) in feats.iter().copied().enumerate() {
1218 let feat_struct = &mut feat_structs[idx];
1219 upsert_field(feat_struct, "Feat", GffValue::UInt16(feat_id));
1220 }
1221
1222 feat_structs
1223}
1224
1225fn default_skill_structs() -> Vec<GffStruct> {
1226 let mut list = Vec::with_capacity(8);
1227 for _ in 0..8 {
1228 let mut skill = GffStruct::new(0);
1229 upsert_field(&mut skill, "Rank", GffValue::UInt8(0));
1230 list.push(skill);
1231 }
1232 list
1233}
1234
1235fn read_skill_rank(list: &[GffStruct], index: usize) -> u8 {
1236 list.get(index)
1237 .and_then(|skill| get_u8(skill, "Rank"))
1238 .unwrap_or(0)
1239}
1240
1241fn write_skill_rank(skill: &mut GffStruct, rank: u8) {
1242 upsert_field(skill, "Rank", GffValue::UInt8(rank));
1243}
1244
1245static CLASS_LIST_CHILDREN: &[FieldSchema] = &[
1247 FieldSchema {
1248 label: "Class",
1249 expected_type: GffType::Int32,
1250 required: false,
1251 children: None,
1252 constraint: None,
1253 },
1254 FieldSchema {
1255 label: "ClassLevel",
1256 expected_type: GffType::Int16,
1257 required: false,
1258 children: None,
1259 constraint: None,
1260 },
1261 FieldSchema {
1262 label: "SpellsPerDayList",
1263 expected_type: GffType::List,
1264 required: false,
1265 children: None,
1266 constraint: None,
1267 },
1268];
1269
1270static FEAT_LIST_CHILDREN: &[FieldSchema] = &[FieldSchema {
1272 label: "Feat",
1273 expected_type: GffType::UInt16,
1274 required: false,
1275 children: None,
1276 constraint: None,
1277}];
1278
1279static SKILL_LIST_CHILDREN: &[FieldSchema] = &[FieldSchema {
1281 label: "Rank",
1282 expected_type: GffType::UInt8,
1283 required: false,
1284 children: None,
1285 constraint: None,
1286}];
1287
1288static SPEC_ABILITY_LIST_CHILDREN: &[FieldSchema] = &[
1290 FieldSchema {
1291 label: "Spell",
1292 expected_type: GffType::UInt16,
1293 required: false,
1294 children: None,
1295 constraint: None,
1296 },
1297 FieldSchema {
1298 label: "SpellFlags",
1299 expected_type: GffType::UInt8,
1300 required: false,
1301 children: None,
1302 constraint: None,
1303 },
1304 FieldSchema {
1305 label: "SpellCasterLevel",
1306 expected_type: GffType::UInt8,
1307 required: false,
1308 children: None,
1309 constraint: None,
1310 },
1311];
1312
1313static ITEM_LIST_CHILDREN: &[FieldSchema] = &[
1315 FieldSchema {
1316 label: "InventoryRes",
1317 expected_type: GffType::ResRef,
1318 required: false,
1319 children: None,
1320 constraint: None,
1321 },
1322 FieldSchema {
1323 label: "Infinite",
1324 expected_type: GffType::UInt8,
1325 required: false,
1326 children: None,
1327 constraint: None,
1328 },
1329 FieldSchema {
1330 label: "ObjectId",
1331 expected_type: GffType::UInt32,
1332 required: false,
1333 children: None,
1334 constraint: None,
1335 },
1336 FieldSchema {
1337 label: "Dropable",
1338 expected_type: GffType::UInt8,
1339 required: false,
1340 children: None,
1341 constraint: None,
1342 },
1343 FieldSchema {
1344 label: "Repos_PosX",
1345 expected_type: GffType::UInt16,
1346 required: false,
1347 children: None,
1348 constraint: None,
1349 },
1350 FieldSchema {
1351 label: "Repos_PosY",
1352 expected_type: GffType::UInt16,
1353 required: false,
1354 children: None,
1355 constraint: None,
1356 },
1357 FieldSchema {
1358 label: "Repos_Posy",
1359 expected_type: GffType::UInt16,
1360 required: false,
1361 children: None,
1362 constraint: None,
1363 },
1364];
1365
1366static EQUIP_ITEM_LIST_CHILDREN: &[FieldSchema] = &[
1368 FieldSchema {
1369 label: "EquippedRes",
1370 expected_type: GffType::ResRef,
1371 required: false,
1372 children: None,
1373 constraint: None,
1374 },
1375 FieldSchema {
1376 label: "ObjectId",
1377 expected_type: GffType::UInt32,
1378 required: false,
1379 children: None,
1380 constraint: None,
1381 },
1382 FieldSchema {
1383 label: "Dropable",
1384 expected_type: GffType::UInt8,
1385 required: false,
1386 children: None,
1387 constraint: None,
1388 },
1389];
1390
1391impl GffSchema for Utc {
1392 fn schema() -> &'static [FieldSchema] {
1393 static SCHEMA: &[FieldSchema] = &[
1394 FieldSchema {
1397 label: "FirstName",
1398 expected_type: GffType::LocalizedString,
1399 required: false,
1400 children: None,
1401 constraint: None,
1402 },
1403 FieldSchema {
1404 label: "LastName",
1405 expected_type: GffType::LocalizedString,
1406 required: false,
1407 children: None,
1408 constraint: None,
1409 },
1410 FieldSchema {
1411 label: "Description",
1412 expected_type: GffType::LocalizedString,
1413 required: false,
1414 children: None,
1415 constraint: None,
1416 },
1417 FieldSchema {
1418 label: "IsPC",
1419 expected_type: GffType::UInt8,
1420 required: false,
1421 children: None,
1422 constraint: None,
1423 },
1424 FieldSchema {
1425 label: "Tag",
1426 expected_type: GffType::String,
1427 required: false,
1428 children: None,
1429 constraint: None,
1430 },
1431 FieldSchema {
1432 label: "Conversation",
1433 expected_type: GffType::ResRef,
1434 required: false,
1435 children: None,
1436 constraint: None,
1437 },
1438 FieldSchema {
1439 label: "Interruptable",
1440 expected_type: GffType::UInt8,
1441 required: false,
1442 children: None,
1443 constraint: None,
1444 },
1445 FieldSchema {
1447 label: "Age",
1448 expected_type: GffType::Int32,
1449 required: false,
1450 children: None,
1451 constraint: None,
1452 },
1453 FieldSchema {
1454 label: "Gender",
1455 expected_type: GffType::UInt8,
1456 required: false,
1457 children: None,
1458 constraint: Some(FieldConstraint::RangeInt(0, 4)),
1459 },
1460 FieldSchema {
1461 label: "StartingPackage",
1462 expected_type: GffType::UInt8,
1463 required: false,
1464 children: None,
1465 constraint: None,
1466 },
1467 FieldSchema {
1468 label: "Race",
1469 expected_type: GffType::UInt8,
1470 required: false,
1471 children: None,
1472 constraint: None,
1473 },
1474 FieldSchema {
1475 label: "Subrace",
1476 expected_type: GffType::String,
1477 required: false,
1478 children: None,
1479 constraint: None,
1480 },
1481 FieldSchema {
1482 label: "SubraceIndex",
1483 expected_type: GffType::UInt8,
1484 required: false,
1485 children: None,
1486 constraint: None,
1487 },
1488 FieldSchema {
1489 label: "Deity",
1490 expected_type: GffType::String,
1491 required: false,
1492 children: None,
1493 constraint: None,
1494 },
1495 FieldSchema {
1497 label: "Str",
1498 expected_type: GffType::UInt8,
1499 required: false,
1500 children: None,
1501 constraint: None,
1502 },
1503 FieldSchema {
1504 label: "Dex",
1505 expected_type: GffType::UInt8,
1506 required: false,
1507 children: None,
1508 constraint: None,
1509 },
1510 FieldSchema {
1511 label: "Int",
1512 expected_type: GffType::UInt8,
1513 required: false,
1514 children: None,
1515 constraint: None,
1516 },
1517 FieldSchema {
1518 label: "Wis",
1519 expected_type: GffType::UInt8,
1520 required: false,
1521 children: None,
1522 constraint: None,
1523 },
1524 FieldSchema {
1525 label: "Con",
1526 expected_type: GffType::UInt8,
1527 required: false,
1528 children: None,
1529 constraint: None,
1530 },
1531 FieldSchema {
1532 label: "Cha",
1533 expected_type: GffType::UInt8,
1534 required: false,
1535 children: None,
1536 constraint: None,
1537 },
1538 FieldSchema {
1539 label: "NaturalAC",
1540 expected_type: GffType::UInt8,
1541 required: false,
1542 children: None,
1543 constraint: None,
1544 },
1545 FieldSchema {
1547 label: "SoundSetFile",
1548 expected_type: GffType::UInt16,
1549 required: false,
1550 children: None,
1551 constraint: None,
1552 },
1553 FieldSchema {
1554 label: "Gold",
1555 expected_type: GffType::UInt32,
1556 required: false,
1557 children: None,
1558 constraint: None,
1559 },
1560 FieldSchema {
1562 label: "Invulnerable",
1563 expected_type: GffType::UInt8,
1564 required: false,
1565 children: None,
1566 constraint: None,
1567 },
1568 FieldSchema {
1569 label: "Plot",
1570 expected_type: GffType::UInt8,
1571 required: false,
1572 children: None,
1573 constraint: None,
1574 },
1575 FieldSchema {
1576 label: "Min1HP",
1577 expected_type: GffType::UInt8,
1578 required: false,
1579 children: None,
1580 constraint: None,
1581 },
1582 FieldSchema {
1583 label: "PartyInteract",
1584 expected_type: GffType::UInt8,
1585 required: false,
1586 children: None,
1587 constraint: None,
1588 },
1589 FieldSchema {
1590 label: "NotReorienting",
1591 expected_type: GffType::UInt8,
1592 required: false,
1593 children: None,
1594 constraint: None,
1595 },
1596 FieldSchema {
1597 label: "Disarmable",
1598 expected_type: GffType::UInt8,
1599 required: false,
1600 children: None,
1601 constraint: None,
1602 },
1603 FieldSchema {
1605 label: "Experience",
1606 expected_type: GffType::UInt32,
1607 required: false,
1608 children: None,
1609 constraint: None,
1610 },
1611 FieldSchema {
1613 label: "PortraitId",
1614 expected_type: GffType::UInt16,
1615 required: false,
1616 children: None,
1617 constraint: None,
1618 },
1619 FieldSchema {
1620 label: "Portrait",
1621 expected_type: GffType::ResRef,
1622 required: false,
1623 children: None,
1624 constraint: None,
1625 },
1626 FieldSchema {
1628 label: "GoodEvil",
1629 expected_type: GffType::UInt8,
1630 required: false,
1631 children: None,
1632 constraint: Some(FieldConstraint::RangeInt(0, 100)),
1633 },
1634 FieldSchema {
1636 label: "Color_Skin",
1637 expected_type: GffType::UInt8,
1638 required: false,
1639 children: None,
1640 constraint: None,
1641 },
1642 FieldSchema {
1643 label: "Color_Hair",
1644 expected_type: GffType::UInt8,
1645 required: false,
1646 children: None,
1647 constraint: None,
1648 },
1649 FieldSchema {
1650 label: "Color_Tattoo1",
1651 expected_type: GffType::UInt8,
1652 required: false,
1653 children: None,
1654 constraint: None,
1655 },
1656 FieldSchema {
1657 label: "Color_Tattoo2",
1658 expected_type: GffType::UInt8,
1659 required: false,
1660 children: None,
1661 constraint: None,
1662 },
1663 FieldSchema {
1664 label: "Phenotype",
1665 expected_type: GffType::Int32,
1666 required: false,
1667 children: None,
1668 constraint: None,
1669 },
1670 FieldSchema {
1671 label: "Appearance_Type",
1672 expected_type: GffType::UInt16,
1673 required: false,
1674 children: None,
1675 constraint: None,
1676 },
1677 FieldSchema {
1678 label: "Appearance_Head",
1679 expected_type: GffType::UInt8,
1680 required: false,
1681 children: None,
1682 constraint: None,
1683 },
1684 FieldSchema {
1685 label: "DuplicatingHead",
1686 expected_type: GffType::UInt8,
1687 required: false,
1688 children: None,
1689 constraint: None,
1690 },
1691 FieldSchema {
1692 label: "UseBackupHead",
1693 expected_type: GffType::UInt8,
1694 required: false,
1695 children: None,
1696 constraint: None,
1697 },
1698 FieldSchema {
1700 label: "FactionID",
1701 expected_type: GffType::UInt16,
1702 required: false,
1703 children: None,
1704 constraint: None,
1705 },
1706 FieldSchema {
1708 label: "ChallengeRating",
1709 expected_type: GffType::Single,
1710 required: false,
1711 children: None,
1712 constraint: None,
1713 },
1714 FieldSchema {
1715 label: "AIState",
1716 expected_type: GffType::Int32,
1717 required: false,
1718 children: None,
1719 constraint: None,
1720 },
1721 FieldSchema {
1722 label: "BodyBag",
1723 expected_type: GffType::UInt8,
1724 required: false,
1725 children: None,
1726 constraint: None,
1727 },
1728 FieldSchema {
1729 label: "PerceptionRange",
1730 expected_type: GffType::UInt8,
1731 required: false,
1732 children: None,
1733 constraint: None,
1734 },
1735 FieldSchema {
1737 label: "willbonus",
1738 expected_type: GffType::Int16,
1739 required: false,
1740 children: None,
1741 constraint: None,
1742 },
1743 FieldSchema {
1744 label: "fortbonus",
1745 expected_type: GffType::Int16,
1746 required: false,
1747 children: None,
1748 constraint: None,
1749 },
1750 FieldSchema {
1751 label: "refbonus",
1752 expected_type: GffType::Int16,
1753 required: false,
1754 children: None,
1755 constraint: None,
1756 },
1757 FieldSchema {
1759 label: "HitPoints",
1760 expected_type: GffType::Int16,
1761 required: false,
1762 children: None,
1763 constraint: None,
1764 },
1765 FieldSchema {
1766 label: "ForcePoints",
1767 expected_type: GffType::Int16,
1768 required: false,
1769 children: None,
1770 constraint: None,
1771 },
1772 FieldSchema {
1773 label: "CurrentHitPoints",
1774 expected_type: GffType::Int16,
1775 required: false,
1776 children: None,
1777 constraint: None,
1778 },
1779 FieldSchema {
1780 label: "CurrentForce",
1781 expected_type: GffType::Int16,
1782 required: false,
1783 children: None,
1784 constraint: None,
1785 },
1786 FieldSchema {
1788 label: "SkillPoints",
1789 expected_type: GffType::UInt16,
1790 required: false,
1791 children: None,
1792 constraint: None,
1793 },
1794 FieldSchema {
1795 label: "MovementRate",
1796 expected_type: GffType::UInt8,
1797 required: false,
1798 children: None,
1799 constraint: None,
1800 },
1801 FieldSchema {
1802 label: "WalkRate",
1803 expected_type: GffType::Int32,
1804 required: false,
1805 children: None,
1806 constraint: None,
1807 },
1808 FieldSchema {
1810 label: "CreatureSize",
1811 expected_type: GffType::Int32,
1812 required: false,
1813 children: None,
1814 constraint: None,
1815 },
1816 FieldSchema {
1817 label: "IsDestroyable",
1818 expected_type: GffType::UInt8,
1819 required: false,
1820 children: None,
1821 constraint: None,
1822 },
1823 FieldSchema {
1824 label: "IsRaiseable",
1825 expected_type: GffType::UInt8,
1826 required: false,
1827 children: None,
1828 constraint: None,
1829 },
1830 FieldSchema {
1831 label: "DeadSelectable",
1832 expected_type: GffType::UInt8,
1833 required: false,
1834 children: None,
1835 constraint: None,
1836 },
1837 FieldSchema {
1838 label: "AmbientAnimState",
1839 expected_type: GffType::UInt8,
1840 required: false,
1841 children: None,
1842 constraint: None,
1843 },
1844 FieldSchema {
1845 label: "Animation",
1846 expected_type: GffType::Int32,
1847 required: false,
1848 children: None,
1849 constraint: None,
1850 },
1851 FieldSchema {
1852 label: "CreatnScrptFird",
1853 expected_type: GffType::UInt8,
1854 required: false,
1855 children: None,
1856 constraint: None,
1857 },
1858 FieldSchema {
1859 label: "PM_IsDisguised",
1860 expected_type: GffType::UInt8,
1861 required: false,
1862 children: None,
1863 constraint: None,
1864 },
1865 FieldSchema {
1866 label: "PM_Appearance",
1867 expected_type: GffType::UInt16,
1868 required: false,
1869 children: None,
1870 constraint: None,
1871 },
1872 FieldSchema {
1873 label: "Listening",
1874 expected_type: GffType::UInt8,
1875 required: false,
1876 children: None,
1877 constraint: None,
1878 },
1879 FieldSchema {
1880 label: "AreaId",
1881 expected_type: GffType::UInt32,
1882 required: false,
1883 children: None,
1884 constraint: None,
1885 },
1886 FieldSchema {
1887 label: "DetectMode",
1888 expected_type: GffType::UInt8,
1889 required: false,
1890 children: None,
1891 constraint: None,
1892 },
1893 FieldSchema {
1894 label: "StealthMode",
1895 expected_type: GffType::UInt8,
1896 required: false,
1897 children: None,
1898 constraint: None,
1899 },
1900 FieldSchema {
1902 label: "ScriptHeartbeat",
1903 expected_type: GffType::ResRef,
1904 required: false,
1905 children: None,
1906 constraint: None,
1907 },
1908 FieldSchema {
1909 label: "ScriptOnNotice",
1910 expected_type: GffType::ResRef,
1911 required: false,
1912 children: None,
1913 constraint: None,
1914 },
1915 FieldSchema {
1916 label: "ScriptSpellAt",
1917 expected_type: GffType::ResRef,
1918 required: false,
1919 children: None,
1920 constraint: None,
1921 },
1922 FieldSchema {
1923 label: "ScriptAttacked",
1924 expected_type: GffType::ResRef,
1925 required: false,
1926 children: None,
1927 constraint: None,
1928 },
1929 FieldSchema {
1930 label: "ScriptDamaged",
1931 expected_type: GffType::ResRef,
1932 required: false,
1933 children: None,
1934 constraint: None,
1935 },
1936 FieldSchema {
1937 label: "ScriptDisturbed",
1938 expected_type: GffType::ResRef,
1939 required: false,
1940 children: None,
1941 constraint: None,
1942 },
1943 FieldSchema {
1944 label: "ScriptEndRound",
1945 expected_type: GffType::ResRef,
1946 required: false,
1947 children: None,
1948 constraint: None,
1949 },
1950 FieldSchema {
1951 label: "ScriptDialogue",
1952 expected_type: GffType::ResRef,
1953 required: false,
1954 children: None,
1955 constraint: None,
1956 },
1957 FieldSchema {
1958 label: "ScriptSpawn",
1959 expected_type: GffType::ResRef,
1960 required: false,
1961 children: None,
1962 constraint: None,
1963 },
1964 FieldSchema {
1965 label: "ScriptRested",
1966 expected_type: GffType::ResRef,
1967 required: false,
1968 children: None,
1969 constraint: None,
1970 },
1971 FieldSchema {
1972 label: "ScriptDeath",
1973 expected_type: GffType::ResRef,
1974 required: false,
1975 children: None,
1976 constraint: None,
1977 },
1978 FieldSchema {
1979 label: "ScriptUserDefine",
1980 expected_type: GffType::ResRef,
1981 required: false,
1982 children: None,
1983 constraint: None,
1984 },
1985 FieldSchema {
1986 label: "ScriptOnBlocked",
1987 expected_type: GffType::ResRef,
1988 required: false,
1989 children: None,
1990 constraint: None,
1991 },
1992 FieldSchema {
1993 label: "ScriptEndDialogue",
1994 expected_type: GffType::ResRef,
1995 required: false,
1996 children: None,
1997 constraint: None,
1998 },
1999 FieldSchema {
2001 label: "ClassList",
2002 expected_type: GffType::List,
2003 required: false,
2004 children: Some(CLASS_LIST_CHILDREN),
2005 constraint: None,
2006 },
2007 FieldSchema {
2008 label: "FeatList",
2009 expected_type: GffType::List,
2010 required: false,
2011 children: Some(FEAT_LIST_CHILDREN),
2012 constraint: None,
2013 },
2014 FieldSchema {
2015 label: "SkillList",
2016 expected_type: GffType::List,
2017 required: false,
2018 children: Some(SKILL_LIST_CHILDREN),
2019 constraint: None,
2020 },
2021 FieldSchema {
2022 label: "Equip_ItemList",
2023 expected_type: GffType::List,
2024 required: false,
2025 children: Some(EQUIP_ITEM_LIST_CHILDREN),
2026 constraint: None,
2027 },
2028 FieldSchema {
2029 label: "ItemList",
2030 expected_type: GffType::List,
2031 required: false,
2032 children: Some(ITEM_LIST_CHILDREN),
2033 constraint: None,
2034 },
2035 FieldSchema {
2036 label: "SpecAbilityList",
2037 expected_type: GffType::List,
2038 required: false,
2039 children: Some(SPEC_ABILITY_LIST_CHILDREN),
2040 constraint: None,
2041 },
2042 FieldSchema {
2043 label: "LvlStatList",
2044 expected_type: GffType::List,
2045 required: false,
2046 children: None,
2047 constraint: None,
2048 },
2049 FieldSchema {
2051 label: "TemplateResRef",
2052 expected_type: GffType::ResRef,
2053 required: false,
2054 children: None,
2055 constraint: None,
2056 },
2057 FieldSchema {
2058 label: "Comment",
2059 expected_type: GffType::String,
2060 required: false,
2061 children: None,
2062 constraint: None,
2063 },
2064 FieldSchema {
2065 label: "PaletteID",
2066 expected_type: GffType::UInt8,
2067 required: false,
2068 children: None,
2069 constraint: None,
2070 },
2071 FieldSchema {
2072 label: "SaveWill",
2073 expected_type: GffType::UInt8,
2074 required: false,
2075 children: None,
2076 constraint: None,
2077 },
2078 FieldSchema {
2079 label: "SaveFortitude",
2080 expected_type: GffType::UInt8,
2081 required: false,
2082 children: None,
2083 constraint: None,
2084 },
2085 FieldSchema {
2086 label: "BodyVariation",
2087 expected_type: GffType::UInt8,
2088 required: false,
2089 children: None,
2090 constraint: None,
2091 },
2092 FieldSchema {
2093 label: "TextureVar",
2094 expected_type: GffType::UInt8,
2095 required: false,
2096 children: None,
2097 constraint: None,
2098 },
2099 FieldSchema {
2100 label: "Morale",
2101 expected_type: GffType::UInt8,
2102 required: false,
2103 children: None,
2104 constraint: None,
2105 },
2106 FieldSchema {
2107 label: "MoraleRecovery",
2108 expected_type: GffType::UInt8,
2109 required: false,
2110 children: None,
2111 constraint: None,
2112 },
2113 FieldSchema {
2114 label: "MoraleBreakpoint",
2115 expected_type: GffType::UInt8,
2116 required: false,
2117 children: None,
2118 constraint: None,
2119 },
2120 FieldSchema {
2121 label: "BlindSpot",
2122 expected_type: GffType::Single,
2123 required: false,
2124 children: None,
2125 constraint: None,
2126 },
2127 FieldSchema {
2128 label: "MultiplierSet",
2129 expected_type: GffType::UInt8,
2130 required: false,
2131 children: None,
2132 constraint: None,
2133 },
2134 FieldSchema {
2135 label: "NoPermDeath",
2136 expected_type: GffType::UInt8,
2137 required: false,
2138 children: None,
2139 constraint: None,
2140 },
2141 FieldSchema {
2142 label: "IgnoreCrePath",
2143 expected_type: GffType::UInt8,
2144 required: false,
2145 children: None,
2146 constraint: None,
2147 },
2148 FieldSchema {
2149 label: "Hologram",
2150 expected_type: GffType::UInt8,
2151 required: false,
2152 children: None,
2153 constraint: None,
2154 },
2155 FieldSchema {
2156 label: "WillNotRender",
2157 expected_type: GffType::UInt8,
2158 required: false,
2159 children: None,
2160 constraint: None,
2161 },
2162 FieldSchema {
2163 label: "LawfulChaotic",
2164 expected_type: GffType::UInt8,
2165 required: false,
2166 children: None,
2167 constraint: None,
2168 },
2169 ];
2170 SCHEMA
2171 }
2172}
2173
2174#[cfg(test)]
2175mod tests {
2176 use super::*;
2177
2178 const TEST_UTC: &[u8] = include_bytes!(concat!(
2179 env!("CARGO_MANIFEST_DIR"),
2180 "/../../fixtures/test.utc"
2181 ));
2182
2183 #[test]
2184 fn reads_core_utc_fields_from_fixture() {
2185 let utc = read_utc_from_bytes(TEST_UTC).expect("fixture must parse");
2186
2187 assert_eq!(utc.template_resref, "n_minecoorta");
2188 assert_eq!(utc.tag, "Coorta");
2189 assert_eq!(utc.comment, "comment");
2190 assert_eq!(utc.conversation, "coorta");
2191
2192 assert_eq!(utc.first_name.string_ref.raw(), 76_046);
2193 assert_eq!(utc.last_name.string_ref.raw(), 123);
2194
2195 assert_eq!(utc.age, 25);
2196 assert_eq!(utc.starting_package, 3);
2197 assert_eq!(utc.gold, 500);
2198 assert!(utc.invulnerable);
2199 assert_eq!(utc.experience, 1200);
2200 assert_eq!(utc.color_skin, 2);
2201 assert_eq!(utc.color_hair, 4);
2202 assert_eq!(utc.color_tattoo1, 1);
2203 assert_eq!(utc.color_tattoo2, 3);
2204 assert_eq!(utc.appearance_head, 5);
2205 assert_eq!(utc.duplicating_head, 0);
2206 assert_eq!(utc.use_backup_head, 0);
2207 assert_eq!(utc.ai_state, 100);
2208 assert_eq!(utc.skill_points, 8);
2209 assert_eq!(utc.movement_rate, 7);
2210
2211 assert_eq!(utc.appearance_id, 636);
2212 assert_eq!(utc.gender_id, 2);
2213 assert_eq!(utc.race_id, 6);
2214 assert_eq!(utc.faction_id, 5);
2215 assert_eq!(utc.perception_id, 11);
2216 assert_eq!(utc.walkrate_id, 7);
2217 assert_eq!(utc.soundset_id, 46);
2218 assert_eq!(utc.portrait_id, 1);
2219 assert!(utc.portrait_resref.is_empty());
2220 assert_eq!(utc.save_will, 0);
2221 assert_eq!(utc.save_fortitude, 0);
2222 assert_eq!(utc.morale, 0);
2223 assert_eq!(utc.morale_recovery, 0);
2224 assert_eq!(utc.morale_breakpoint, 0);
2225 assert_eq!(utc.description.string_ref.raw(), 123);
2226 assert_eq!(utc.lawfulness, 0);
2227 assert_eq!(utc.phenotype_id, 0);
2228 assert!(utc.deity.is_empty());
2229 assert!(utc.subrace_name.is_empty());
2230
2231 assert_eq!(utc.alignment, 50);
2232 assert!((utc.challenge_rating - 1.0).abs() < f32::EPSILON);
2233 assert!((utc.blindspot - 120.0).abs() < f32::EPSILON);
2234 assert_eq!(utc.natural_ac, 1);
2235 assert_eq!(utc.reflex_bonus, 1);
2236 assert_eq!(utc.willpower_bonus, 1);
2237 assert_eq!(utc.fortitude_bonus, 1);
2238
2239 assert_eq!(utc.strength, 10);
2240 assert_eq!(utc.dexterity, 10);
2241 assert_eq!(utc.constitution, 10);
2242 assert_eq!(utc.intelligence, 10);
2243 assert_eq!(utc.wisdom, 10);
2244 assert_eq!(utc.charisma, 10);
2245
2246 assert_eq!(utc.current_hp, 8);
2247 assert_eq!(utc.max_hp, 8);
2248 assert_eq!(utc.hp, 8);
2249 assert_eq!(utc.fp, 1);
2250 assert_eq!(utc.max_fp, 1);
2251
2252 assert!(utc.not_reorienting);
2253 assert!(utc.party_interact);
2254 assert!(utc.no_perm_death);
2255 assert!(utc.min1_hp);
2256 assert!(utc.plot);
2257 assert!(utc.interruptable);
2258 assert!(utc.is_pc);
2259 assert!(utc.disarmable);
2260 assert!(utc.ignore_cre_path);
2261 assert!(utc.hologram);
2262
2263 assert_eq!(utc.on_attacked, "k_def_attacked01");
2264 assert_eq!(utc.on_damaged, "k_def_damage01");
2265 assert_eq!(utc.on_death, "k_def_death01");
2266 assert_eq!(utc.on_dialog, "k_def_dialogue01");
2267 assert_eq!(utc.on_disturbed, "k_def_disturb01");
2268 assert_eq!(utc.on_end_dialog, "k_def_endconv");
2269 assert_eq!(utc.on_end_round, "k_def_combend01");
2270 assert_eq!(utc.on_heartbeat, "k_def_heartbt01");
2271 assert_eq!(utc.on_blocked, "k_def_blocked01");
2272 assert_eq!(utc.on_notice, "k_def_percept01");
2273 assert_eq!(utc.on_spawn, "k_def_spawn01");
2274 assert_eq!(utc.on_spell, "k_def_spellat01");
2275 assert_eq!(utc.on_user_defined, "k_def_userdef01");
2276
2277 assert_eq!(utc.skills.computer_use, 1);
2278 assert_eq!(utc.skills.demolitions, 2);
2279 assert_eq!(utc.skills.stealth, 3);
2280 assert_eq!(utc.skills.awareness, 4);
2281 assert_eq!(utc.skills.persuade, 5);
2282 assert_eq!(utc.skills.repair, 6);
2283 assert_eq!(utc.skills.security, 7);
2284 assert_eq!(utc.skills.treat_injury, 8);
2285
2286 assert_eq!(utc.classes.len(), 2);
2287 let scout_class = utc
2288 .classes
2289 .iter()
2290 .find(|class| class.class_id == 1 && class.class_level == 3)
2291 .expect("fixture should contain class_id=1 level=3");
2292 assert_eq!(scout_class.powers, vec![9, 11]);
2293 assert!(utc.special_abilities.is_empty());
2294
2295 assert_eq!(utc.feats, vec![93, 94]);
2296
2297 assert_eq!(utc.equipment.len(), 2);
2298 assert_eq!(utc.equipment[0].slot_id, 2);
2299 assert_eq!(utc.equipment[0].resref, "mineruniform");
2300 assert!(utc.equipment[0].droppable);
2301 assert_eq!(utc.equipment[1].slot_id, 131_072);
2302 assert_eq!(utc.equipment[1].resref, "g_i_crhide008");
2303 assert!(!utc.equipment[1].droppable);
2304
2305 assert_eq!(utc.inventory.len(), 4);
2306 assert_eq!(utc.inventory[0].entry_id, 0);
2307 assert_eq!(utc.inventory[0].resref, "g_w_thermldet01");
2308 assert!(utc.inventory[0].droppable);
2309 assert_eq!(utc.inventory[1].entry_id, 1);
2310 assert_eq!(utc.inventory[1].resref, "g_w_thermldet01");
2311 assert!(!utc.inventory[1].droppable);
2312 assert_eq!(utc.inventory[3].resref, "g_w_thermldet02");
2313 assert_eq!(utc.inventory[3].repos_pos_x, Some(3));
2314 assert_eq!(utc.inventory[3].repos_pos_y, Some(0));
2315 }
2316
2317 #[test]
2318 fn all_fields_survive_typed_roundtrip() {
2319 let utc = read_utc_from_bytes(TEST_UTC).expect("fixture must parse");
2320 let encoded = write_utc_to_vec(&utc).expect("encode must succeed");
2321 let reparsed = read_utc_from_bytes(&encoded).expect("decode must succeed");
2322 assert_eq!(utc, reparsed);
2323 }
2324
2325 #[test]
2326 fn typed_edits_roundtrip_through_gff_writer() {
2327 let mut utc = read_utc_from_bytes(TEST_UTC).expect("fixture must parse");
2328 utc.tag = "Coorta_Mod".into();
2329 utc.on_spawn = ResRef::new("k_new_spawn").expect("valid test resref");
2330 utc.skills.persuade = 12;
2331 utc.portrait_resref = ResRef::new("po_pfha01").expect("valid test resref");
2332 utc.save_will = 11;
2333 utc.save_fortitude = 9;
2334 utc.morale = 8;
2335 utc.morale_recovery = 7;
2336 utc.morale_breakpoint = 6;
2337 utc.description = GffLocalizedString::new(StrRef::from_raw(42_424));
2338 utc.lawfulness = 35;
2339 utc.phenotype_id = 2;
2340 utc.deity = "The Force".into();
2341 utc.subrace_name = "Miner".into();
2342 utc.feats = vec![94, 93, 120];
2343 utc.classes[0].powers = vec![9, 12, 15];
2344 utc.special_abilities = vec![UtcSpecialAbility {
2345 spell_id: 321,
2346 spell_flags: 0b11,
2347 spell_caster_level: 7,
2348 }];
2349 utc.equipment[0].resref = ResRef::new("g_a_class4001").expect("valid test resref");
2350 utc.equipment[0].droppable = false;
2351 utc.inventory[0].resref = ResRef::new("g_w_blstrrfl01").expect("valid test resref");
2352 utc.inventory.push(UtcInventoryItem {
2353 entry_id: 4,
2354 resref: ResRef::new("g_i_progspike01").expect("valid test resref"),
2355 droppable: true,
2356 repos_pos_x: Some(4),
2357 repos_pos_y: Some(0),
2358 });
2359
2360 let encoded = write_utc_to_vec(&utc).expect("encode");
2361 let reparsed = read_utc_from_bytes(&encoded).expect("decode");
2362
2363 assert_eq!(reparsed.tag, "Coorta_Mod");
2364 assert_eq!(reparsed.on_spawn, "k_new_spawn");
2365 assert_eq!(reparsed.skills.persuade, 12);
2366 assert_eq!(reparsed.portrait_resref, "po_pfha01");
2367 assert_eq!(reparsed.save_will, 11);
2368 assert_eq!(reparsed.save_fortitude, 9);
2369 assert_eq!(reparsed.morale, 8);
2370 assert_eq!(reparsed.morale_recovery, 7);
2371 assert_eq!(reparsed.morale_breakpoint, 6);
2372 assert_eq!(reparsed.description.string_ref.raw(), 42_424);
2373 assert_eq!(reparsed.lawfulness, 35);
2374 assert_eq!(reparsed.phenotype_id, 2);
2375 assert_eq!(reparsed.deity, "The Force");
2376 assert_eq!(reparsed.subrace_name, "Miner");
2377 assert_eq!(reparsed.feats, vec![94, 93, 120]);
2378 assert_eq!(reparsed.classes[0].powers, vec![9, 12, 15]);
2379 assert_eq!(reparsed.special_abilities.len(), 1);
2380 assert_eq!(reparsed.special_abilities[0].spell_id, 321);
2381 assert_eq!(reparsed.special_abilities[0].spell_flags, 0b11);
2382 assert_eq!(reparsed.special_abilities[0].spell_caster_level, 7);
2383 assert_eq!(reparsed.equipment[0].resref, "g_a_class4001");
2384 assert!(!reparsed.equipment[0].droppable);
2385 assert_eq!(reparsed.inventory[0].resref, "g_w_blstrrfl01");
2386 assert_eq!(reparsed.inventory.len(), 5);
2387 assert_eq!(reparsed.inventory[4].entry_id, 4);
2388 assert_eq!(reparsed.inventory[4].resref, "g_i_progspike01");
2389 assert!(reparsed.inventory[4].droppable);
2390 }
2391
2392 #[test]
2393 fn no_op_rebuild_preserves_list_order_and_struct_ids() {
2394 let utc = read_utc_from_bytes(TEST_UTC).expect("fixture must parse");
2395 let rebuilt = utc.to_gff();
2396
2397 assert_eq!(
2398 list_struct_ids(find_list(&rebuilt.root, "FeatList")),
2399 vec![1, 1]
2400 );
2401 assert_eq!(
2402 list_struct_ids(find_list(&rebuilt.root, "Equip_ItemList")),
2403 vec![2, 131_072]
2404 );
2405 assert_eq!(
2406 list_struct_ids(find_list(&rebuilt.root, "ItemList")),
2407 vec![0, 1, 2, 3]
2408 );
2409 assert_eq!(
2410 list_u16_field(find_list(&rebuilt.root, "FeatList"), "Feat"),
2411 vec![93, 94]
2412 );
2413 }
2414
2415 #[test]
2416 fn rejects_non_utc_file_type() {
2417 let gff = Gff::new(*b"UTI ", GffStruct::new(-1));
2418 let err = Utc::from_gff(&gff).expect_err("must fail");
2419 assert!(matches!(err, UtcError::UnsupportedFileType(file_type) if file_type == *b"UTI "));
2420 }
2421
2422 #[test]
2423 fn read_utc_from_reader_matches_bytes_path() {
2424 let mut cursor = Cursor::new(TEST_UTC);
2425 let via_reader = read_utc(&mut cursor).expect("reader parse");
2426 let via_bytes = read_utc_from_bytes(TEST_UTC).expect("bytes parse");
2427 assert_eq!(via_reader.template_resref, via_bytes.template_resref);
2428 assert_eq!(via_reader.classes.len(), via_bytes.classes.len());
2429 }
2430
2431 #[test]
2432 fn type_mismatch_on_class_list_is_error() {
2433 let mut root = GffStruct::new(-1);
2434 root.push_field("ClassList", GffValue::UInt32(7));
2435 let gff = Gff::new(*b"UTC ", root);
2436 let err = Utc::from_gff(&gff).expect_err("must fail");
2437 assert!(matches!(
2438 err,
2439 UtcError::TypeMismatch {
2440 field: "ClassList",
2441 expected: "List"
2442 }
2443 ));
2444 }
2445
2446 #[test]
2447 fn type_mismatch_on_spec_ability_list_is_error() {
2448 let mut root = GffStruct::new(-1);
2449 root.push_field("SpecAbilityList", GffValue::UInt32(7));
2450 let gff = Gff::new(*b"UTC ", root);
2451 let err = Utc::from_gff(&gff).expect_err("must fail");
2452 assert!(matches!(
2453 err,
2454 UtcError::TypeMismatch {
2455 field: "SpecAbilityList",
2456 expected: "List"
2457 }
2458 ));
2459 }
2460
2461 #[test]
2462 fn write_utc_matches_direct_gff_writer() {
2463 let utc = read_utc_from_bytes(TEST_UTC).expect("fixture parse");
2464 let from_utc = write_utc_to_vec(&utc).expect("utc encode");
2465
2466 let gff = utc.to_gff();
2467 let from_gff = rakata_formats::write_gff_to_vec(&gff).expect("gff encode");
2468 assert_eq!(from_utc, from_gff);
2469 }
2470
2471 fn find_list<'a>(structure: &'a GffStruct, label: &str) -> &'a [GffStruct] {
2472 match structure.field(label) {
2473 Some(GffValue::List(values)) => values.as_slice(),
2474 Some(other) => panic!("field {label} is not a list: {other:?}"),
2475 None => panic!("missing list field {label}"),
2476 }
2477 }
2478
2479 fn list_struct_ids(list: &[GffStruct]) -> Vec<i32> {
2480 list.iter().map(|entry| entry.struct_id).collect::<Vec<_>>()
2481 }
2482
2483 fn list_u16_field(list: &[GffStruct], label: &str) -> Vec<u16> {
2484 list.iter()
2485 .map(|entry| match entry.field(label) {
2486 Some(GffValue::UInt16(value)) => *value,
2487 Some(other) => panic!("field {label} is not UInt16: {other:?}"),
2488 None => panic!("missing field {label}"),
2489 })
2490 .collect::<Vec<_>>()
2491 }
2492
2493 #[test]
2494 fn schema_field_count() {
2495 assert_eq!(Utc::schema().len(), 108);
2496 }
2497
2498 #[test]
2499 fn schema_no_duplicate_labels() {
2500 let schema = Utc::schema();
2501 let mut labels: Vec<&str> = schema.iter().map(|f| f.label).collect();
2502 labels.sort();
2503 let before = labels.len();
2504 labels.dedup();
2505 assert_eq!(before, labels.len(), "duplicate labels in UTC schema");
2506 }
2507
2508 #[test]
2509 fn schema_has_expected_list_children() {
2510 let schema = Utc::schema();
2511 let expected: &[(&str, usize)] = &[
2512 ("ClassList", 3),
2513 ("FeatList", 1),
2514 ("SkillList", 1),
2515 ("SpecAbilityList", 3),
2516 ("ItemList", 7),
2517 ("Equip_ItemList", 3),
2518 ];
2519 for (label, child_count) in expected {
2520 let field = schema
2521 .iter()
2522 .find(|f| f.label == *label)
2523 .unwrap_or_else(|| panic!("missing list field {label}"));
2524 let children = field
2525 .children
2526 .unwrap_or_else(|| panic!("{label} should have children"));
2527 assert_eq!(
2528 children.len(),
2529 *child_count,
2530 "{label} children count mismatch"
2531 );
2532 }
2533 }
2534}