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 crate::shared::{PORTRAIT_ID_USE_RESREF, SCRIPT_SLOT_SEED};
41use rakata_core::{ResRef, StrRef};
42use rakata_formats::{
43 gff_schema::{
44 AbsentDefault, DefaultValue, FieldConstraint, FieldLife, FieldSchema, GffSchema, GffType,
45 },
46 read_gff, read_gff_from_bytes, write_gff, Gff, GffBinaryError, GffLocalizedString, GffStruct,
47 GffValue,
48};
49use thiserror::Error;
50
51#[derive(Debug, Clone, PartialEq)]
53pub struct Utc {
54 pub template_resref: ResRef,
56 pub tag: String,
58 pub comment: String,
60 pub conversation: ResRef,
62 pub first_name: GffLocalizedString,
64 pub last_name: GffLocalizedString,
66 pub age: i32,
68 pub starting_package: u8,
70 pub gold: u32,
72 pub invulnerable: bool,
74 pub experience: u32,
76 pub subrace_id: u8,
78 pub perception_id: u8,
80 pub race_id: u8,
82 pub color_skin: u8,
84 pub color_hair: u8,
86 pub color_tattoo1: u8,
88 pub color_tattoo2: u8,
90 pub appearance_head: u8,
92 pub duplicating_head: u8,
94 pub use_backup_head: u8,
96 pub appearance_id: u16,
98 pub gender_id: u8,
100 pub faction_id: u16,
102 pub walkrate_id: i32,
104 pub ai_state: u16,
106 pub skill_points: u16,
108 pub movement_rate: u8,
110 pub soundset_id: u16,
112 pub portrait_id: u16,
114 pub portrait_resref: ResRef,
116 pub save_will: u8,
118 pub save_fortitude: u8,
120 pub morale: u8,
122 pub morale_recovery: u8,
124 pub morale_breakpoint: u8,
126 pub palette_id: u8,
128 pub bodybag_id: u8,
130 pub body_variation: u8,
132 pub texture_variation: u8,
134 pub alignment: u8,
136 pub challenge_rating: f32,
138 pub blindspot: f32,
140 pub multiplier_set: u8,
142 pub natural_ac: u8,
144 pub reflex_bonus: i16,
146 pub willpower_bonus: i16,
148 pub fortitude_bonus: i16,
150 pub strength: u8,
152 pub dexterity: u8,
154 pub constitution: u8,
156 pub intelligence: u8,
158 pub wisdom: u8,
160 pub charisma: u8,
162 pub current_hp: i16,
164 pub max_hp: i16,
166 pub hp: i16,
168 pub fp: i16,
170 pub max_fp: i16,
172 pub not_reorienting: bool,
174 pub party_interact: bool,
176 pub no_perm_death: bool,
178 pub min1_hp: bool,
180 pub plot: bool,
182 pub interruptable: bool,
184 pub is_pc: bool,
186 pub disarmable: bool,
188 pub ignore_cre_path: bool,
190 pub hologram: bool,
192 pub will_not_render: bool,
194 pub deity: String,
196 pub description: GffLocalizedString,
198 pub lawfulness: u8,
200 pub phenotype_id: i32,
202 pub subrace_name: String,
204 pub on_end_dialog: ResRef,
206 pub on_blocked: ResRef,
208 pub on_heartbeat: ResRef,
210 pub on_notice: ResRef,
212 pub on_spell: ResRef,
214 pub on_attacked: ResRef,
216 pub on_damaged: ResRef,
218 pub on_disturbed: ResRef,
220 pub on_end_round: ResRef,
222 pub on_dialog: ResRef,
224 pub on_spawn: ResRef,
226 pub on_rested: ResRef,
228 pub on_death: ResRef,
230 pub on_user_defined: ResRef,
232 pub skills: UtcSkills,
234 pub classes: Vec<UtcClass>,
236 pub special_abilities: Vec<UtcSpecialAbility>,
238 pub feats: Vec<u16>,
240 pub equipment: Vec<UtcEquipmentItem>,
242 pub inventory: Vec<UtcInventoryItem>,
244}
245
246impl Default for Utc {
247 fn default() -> Self {
248 Self {
249 template_resref: ResRef::blank(),
250 tag: String::new(),
251 comment: String::new(),
252 conversation: ResRef::blank(),
253 first_name: GffLocalizedString::new(StrRef::invalid()),
254 last_name: GffLocalizedString::new(StrRef::invalid()),
255 age: 0,
256 starting_package: 0,
257 gold: 0,
258 invulnerable: false,
259 experience: 0,
260 subrace_id: 0,
261 perception_id: 0,
262 race_id: 0,
263 color_skin: 0,
264 color_hair: 0,
265 color_tattoo1: 0,
266 color_tattoo2: 0,
267 appearance_head: 0,
268 duplicating_head: 0,
269 use_backup_head: 0,
270 appearance_id: 0,
271 gender_id: 0,
272 faction_id: 0,
273 walkrate_id: 0,
274 ai_state: 0,
275 skill_points: 0,
276 movement_rate: 0,
277 soundset_id: 0,
278 portrait_id: PORTRAIT_ID_USE_RESREF,
279 portrait_resref: ResRef::blank(),
280 save_will: 0,
281 save_fortitude: 0,
282 morale: 0,
283 morale_recovery: 0,
284 morale_breakpoint: 0,
285 palette_id: 0,
286 bodybag_id: 0,
287 body_variation: 0,
288 texture_variation: 0,
289 alignment: 0,
290 challenge_rating: 0.0,
291 blindspot: 0.0,
292 multiplier_set: 0,
293 natural_ac: 0,
294 reflex_bonus: 0,
295 willpower_bonus: 0,
296 fortitude_bonus: 0,
297 strength: 0,
298 dexterity: 0,
299 constitution: 0,
300 intelligence: 0,
301 wisdom: 0,
302 charisma: 0,
303 current_hp: 0,
304 max_hp: 0,
305 hp: 0,
306 fp: 0,
307 max_fp: 0,
308 not_reorienting: false,
309 party_interact: false,
310 no_perm_death: false,
311 min1_hp: false,
312 plot: false,
313 interruptable: false,
314 is_pc: false,
315 disarmable: false,
316 ignore_cre_path: false,
317 hologram: false,
318 will_not_render: false,
319 deity: String::new(),
320 description: GffLocalizedString::new(StrRef::invalid()),
321 lawfulness: 0,
322 phenotype_id: 0,
323 subrace_name: String::new(),
324 on_end_dialog: SCRIPT_SLOT_SEED,
325 on_blocked: SCRIPT_SLOT_SEED,
326 on_heartbeat: SCRIPT_SLOT_SEED,
327 on_notice: SCRIPT_SLOT_SEED,
328 on_spell: SCRIPT_SLOT_SEED,
329 on_attacked: SCRIPT_SLOT_SEED,
330 on_damaged: SCRIPT_SLOT_SEED,
331 on_disturbed: SCRIPT_SLOT_SEED,
332 on_end_round: SCRIPT_SLOT_SEED,
333 on_dialog: SCRIPT_SLOT_SEED,
334 on_spawn: SCRIPT_SLOT_SEED,
335 on_rested: SCRIPT_SLOT_SEED,
336 on_death: SCRIPT_SLOT_SEED,
337 on_user_defined: SCRIPT_SLOT_SEED,
338 skills: UtcSkills::default(),
339 classes: Vec::new(),
340 special_abilities: Vec::new(),
341 feats: Vec::new(),
342 equipment: Vec::new(),
343 inventory: Vec::new(),
344 }
345 }
346}
347
348impl Utc {
349 pub fn new() -> Self {
351 Self::default()
352 }
353
354 pub fn from_gff(gff: &Gff) -> Result<Self, UtcError> {
356 if gff.file_type != *b"UTC " && gff.file_type != *b"GFF " {
357 return Err(UtcError::UnsupportedFileType(gff.file_type));
358 }
359
360 let root = &gff.root;
361
362 let skills = match root.field("SkillList") {
363 Some(GffValue::List(skill_structs)) => UtcSkills::from_list(skill_structs),
364 Some(_) => {
365 return Err(UtcError::TypeMismatch {
366 field: "SkillList",
367 expected: "List",
368 });
369 }
370 None => UtcSkills::default(),
371 };
372
373 let classes = match root.field("ClassList") {
374 Some(GffValue::List(class_structs)) => class_structs
375 .iter()
376 .map(UtcClass::from_struct)
377 .collect::<Result<Vec<_>, _>>()?,
378 Some(_) => {
379 return Err(UtcError::TypeMismatch {
380 field: "ClassList",
381 expected: "List",
382 });
383 }
384 None => Vec::new(),
385 };
386
387 let special_abilities = match root.field("SpecAbilityList") {
388 Some(GffValue::List(ability_structs)) => ability_structs
389 .iter()
390 .map(UtcSpecialAbility::from_struct)
391 .collect::<Vec<_>>(),
392 Some(_) => {
393 return Err(UtcError::TypeMismatch {
394 field: "SpecAbilityList",
395 expected: "List",
396 });
397 }
398 None => Vec::new(),
399 };
400
401 let feats = match root.field("FeatList") {
402 Some(GffValue::List(feat_structs)) => feat_structs
403 .iter()
404 .map(|feat_struct| get_u16(feat_struct, "Feat").unwrap_or(0))
405 .collect::<Vec<_>>(),
406 Some(_) => {
407 return Err(UtcError::TypeMismatch {
408 field: "FeatList",
409 expected: "List",
410 });
411 }
412 None => Vec::new(),
413 };
414
415 let equipment = match root.field("Equip_ItemList") {
416 Some(GffValue::List(equipment_structs)) => equipment_structs
417 .iter()
418 .map(UtcEquipmentItem::from_struct)
419 .collect::<Vec<_>>(),
420 Some(_) => {
421 return Err(UtcError::TypeMismatch {
422 field: "Equip_ItemList",
423 expected: "List",
424 });
425 }
426 None => Vec::new(),
427 };
428
429 let inventory = match root.field("ItemList") {
430 Some(GffValue::List(inventory_structs)) => inventory_structs
431 .iter()
432 .map(UtcInventoryItem::from_struct)
433 .collect::<Vec<_>>(),
434 Some(_) => {
435 return Err(UtcError::TypeMismatch {
436 field: "ItemList",
437 expected: "List",
438 });
439 }
440 None => Vec::new(),
441 };
442
443 Ok(Self {
444 template_resref: get_resref(root, "TemplateResRef").unwrap_or_default(),
445 tag: get_string(root, "Tag").unwrap_or_default(),
446 comment: get_string(root, "Comment").unwrap_or_default(),
447 conversation: get_resref(root, "Conversation").unwrap_or_default(),
448 first_name: get_locstring(root, "FirstName")
449 .cloned()
450 .unwrap_or_else(|| GffLocalizedString::new(StrRef::invalid())),
451 last_name: get_locstring(root, "LastName")
452 .cloned()
453 .unwrap_or_else(|| GffLocalizedString::new(StrRef::invalid())),
454 age: get_i32(root, "Age").unwrap_or(0),
455 starting_package: get_u8(root, "StartingPackage").unwrap_or(0),
456 gold: get_u32(root, "Gold").unwrap_or(0),
457 invulnerable: get_bool(root, "Invulnerable").unwrap_or(false),
458 experience: get_u32(root, "Experience").unwrap_or(0),
459 subrace_id: get_u8(root, "SubraceIndex").unwrap_or(0),
460 perception_id: get_u8(root, "PerceptionRange").unwrap_or(0),
461 race_id: get_u8(root, "Race").unwrap_or(0),
462 color_skin: get_u8(root, "Color_Skin").unwrap_or(0),
463 color_hair: get_u8(root, "Color_Hair").unwrap_or(0),
464 color_tattoo1: get_u8(root, "Color_Tattoo1").unwrap_or(0),
465 color_tattoo2: get_u8(root, "Color_Tattoo2").unwrap_or(0),
466 appearance_head: get_u8(root, "Appearance_Head").unwrap_or(0),
467 duplicating_head: get_u8(root, "DuplicatingHead").unwrap_or(0),
468 use_backup_head: get_u8(root, "UseBackupHead").unwrap_or(0),
469 appearance_id: get_u16(root, "Appearance_Type").unwrap_or(0),
470 gender_id: get_u8(root, "Gender").unwrap_or(0),
471 faction_id: get_u16(root, "FactionID").unwrap_or(0),
472 walkrate_id: get_i32(root, "WalkRate").unwrap_or(0),
473 ai_state: get_u16(root, "AIState")
474 .or_else(|| get_i32(root, "AIState").and_then(|v| u16::try_from(v).ok()))
475 .unwrap_or(0),
476 skill_points: get_u16(root, "SkillPoints").unwrap_or(0),
477 movement_rate: get_u8(root, "MovementRate")
483 .or_else(|| get_i32(root, "WalkRate").and_then(|v| u8::try_from(v).ok()))
484 .unwrap_or(0),
485 soundset_id: get_u16(root, "SoundSetFile").unwrap_or(0),
486 portrait_id: get_u16(root, "PortraitId").unwrap_or(PORTRAIT_ID_USE_RESREF),
487 portrait_resref: get_resref(root, "Portrait").unwrap_or_default(),
488 save_will: get_u8(root, "SaveWill").unwrap_or(0),
489 save_fortitude: get_u8(root, "SaveFortitude").unwrap_or(0),
490 morale: get_u8(root, "Morale").unwrap_or(0),
491 morale_recovery: get_u8(root, "MoraleRecovery").unwrap_or(0),
492 morale_breakpoint: get_u8(root, "MoraleBreakpoint").unwrap_or(0),
493 palette_id: get_u8(root, "PaletteID").unwrap_or(0),
494 bodybag_id: get_u8(root, "BodyBag").unwrap_or(0),
495 body_variation: get_u8(root, "BodyVariation").unwrap_or(0),
496 texture_variation: get_u8(root, "TextureVar").unwrap_or(0),
497 alignment: get_u8(root, "GoodEvil").unwrap_or(0),
498 challenge_rating: get_f32(root, "ChallengeRating").unwrap_or(0.0),
499 blindspot: get_f32(root, "BlindSpot").unwrap_or(0.0),
500 multiplier_set: get_u8(root, "MultiplierSet").unwrap_or(0),
501 natural_ac: get_u8(root, "NaturalAC").unwrap_or(0),
502 reflex_bonus: get_i16(root, "refbonus").unwrap_or(0),
503 willpower_bonus: get_i16(root, "willbonus").unwrap_or(0),
504 fortitude_bonus: get_i16(root, "fortbonus").unwrap_or(0),
505 strength: get_u8(root, "Str").unwrap_or(0),
506 dexterity: get_u8(root, "Dex").unwrap_or(0),
507 constitution: get_u8(root, "Con").unwrap_or(0),
508 intelligence: get_u8(root, "Int").unwrap_or(0),
509 wisdom: get_u8(root, "Wis").unwrap_or(0),
510 charisma: get_u8(root, "Cha").unwrap_or(0),
511 current_hp: get_i16(root, "CurrentHitPoints").unwrap_or(0),
512 max_hp: get_i16(root, "MaxHitPoints").unwrap_or(0),
513 hp: get_i16(root, "HitPoints").unwrap_or(0),
514 fp: get_i16(root, "CurrentForce").unwrap_or(0),
515 max_fp: get_i16(root, "ForcePoints").unwrap_or(0),
516 not_reorienting: get_bool(root, "NotReorienting").unwrap_or(false),
517 party_interact: get_bool(root, "PartyInteract").unwrap_or(false),
518 no_perm_death: get_bool(root, "NoPermDeath").unwrap_or(false),
519 min1_hp: get_bool(root, "Min1HP").unwrap_or(false),
520 plot: get_bool(root, "Plot").unwrap_or(false),
521 interruptable: get_bool(root, "Interruptable").unwrap_or(false),
522 is_pc: get_bool(root, "IsPC").unwrap_or(false),
523 disarmable: get_bool(root, "Disarmable").unwrap_or(false),
524 ignore_cre_path: get_bool(root, "IgnoreCrePath").unwrap_or(false),
525 hologram: get_bool(root, "Hologram").unwrap_or(false),
526 will_not_render: get_bool(root, "WillNotRender").unwrap_or(false),
527 deity: get_string(root, "Deity").unwrap_or_default(),
528 description: get_locstring(root, "Description")
529 .cloned()
530 .unwrap_or_else(|| GffLocalizedString::new(StrRef::invalid())),
531 lawfulness: get_u8(root, "LawfulChaotic").unwrap_or(0),
532 phenotype_id: get_i32(root, "Phenotype").unwrap_or(0),
533 subrace_name: get_string(root, "Subrace").unwrap_or_default(),
534 on_end_dialog: get_resref(root, "ScriptEndDialogu").unwrap_or(SCRIPT_SLOT_SEED),
535 on_blocked: get_resref(root, "ScriptOnBlocked").unwrap_or(SCRIPT_SLOT_SEED),
536 on_heartbeat: get_resref(root, "ScriptHeartbeat").unwrap_or(SCRIPT_SLOT_SEED),
537 on_notice: get_resref(root, "ScriptOnNotice").unwrap_or(SCRIPT_SLOT_SEED),
538 on_spell: get_resref(root, "ScriptSpellAt").unwrap_or(SCRIPT_SLOT_SEED),
539 on_attacked: get_resref(root, "ScriptAttacked").unwrap_or(SCRIPT_SLOT_SEED),
540 on_damaged: get_resref(root, "ScriptDamaged").unwrap_or(SCRIPT_SLOT_SEED),
541 on_disturbed: get_resref(root, "ScriptDisturbed").unwrap_or(SCRIPT_SLOT_SEED),
542 on_end_round: get_resref(root, "ScriptEndRound").unwrap_or(SCRIPT_SLOT_SEED),
543 on_dialog: get_resref(root, "ScriptDialogue").unwrap_or(SCRIPT_SLOT_SEED),
544 on_spawn: get_resref(root, "ScriptSpawn").unwrap_or(SCRIPT_SLOT_SEED),
545 on_rested: get_resref(root, "ScriptRested").unwrap_or(SCRIPT_SLOT_SEED),
546 on_death: get_resref(root, "ScriptDeath").unwrap_or(SCRIPT_SLOT_SEED),
547 on_user_defined: get_resref(root, "ScriptUserDefine").unwrap_or(SCRIPT_SLOT_SEED),
548 skills,
549 classes,
550 special_abilities,
551 feats,
552 equipment,
553 inventory,
554 })
555 }
556
557 pub fn to_gff(&self) -> Gff {
559 let mut root = GffStruct::new(-1);
560
561 upsert_field(
562 &mut root,
563 "TemplateResRef",
564 GffValue::ResRef(self.template_resref),
565 );
566 upsert_field(&mut root, "Tag", GffValue::String(self.tag.clone()));
567 upsert_field(&mut root, "Comment", GffValue::String(self.comment.clone()));
568 upsert_field(
569 &mut root,
570 "Conversation",
571 GffValue::ResRef(self.conversation),
572 );
573 upsert_field(
574 &mut root,
575 "FirstName",
576 GffValue::LocalizedString(self.first_name.clone()),
577 );
578 upsert_field(
579 &mut root,
580 "LastName",
581 GffValue::LocalizedString(self.last_name.clone()),
582 );
583
584 upsert_field(&mut root, "Age", GffValue::Int32(self.age));
585 upsert_field(
586 &mut root,
587 "StartingPackage",
588 GffValue::UInt8(self.starting_package),
589 );
590 upsert_field(&mut root, "Gold", GffValue::UInt32(self.gold));
591 upsert_field(
592 &mut root,
593 "Invulnerable",
594 GffValue::UInt8(u8::from(self.invulnerable)),
595 );
596 upsert_field(&mut root, "Experience", GffValue::UInt32(self.experience));
597
598 upsert_field(&mut root, "SubraceIndex", GffValue::UInt8(self.subrace_id));
599 upsert_field(
600 &mut root,
601 "PerceptionRange",
602 GffValue::UInt8(self.perception_id),
603 );
604 upsert_field(&mut root, "Race", GffValue::UInt8(self.race_id));
605 upsert_field(&mut root, "Color_Skin", GffValue::UInt8(self.color_skin));
606 upsert_field(&mut root, "Color_Hair", GffValue::UInt8(self.color_hair));
607 upsert_field(
608 &mut root,
609 "Color_Tattoo1",
610 GffValue::UInt8(self.color_tattoo1),
611 );
612 upsert_field(
613 &mut root,
614 "Color_Tattoo2",
615 GffValue::UInt8(self.color_tattoo2),
616 );
617 upsert_field(
618 &mut root,
619 "Appearance_Head",
620 GffValue::UInt8(self.appearance_head),
621 );
622 upsert_field(
623 &mut root,
624 "DuplicatingHead",
625 GffValue::UInt8(self.duplicating_head),
626 );
627 upsert_field(
628 &mut root,
629 "UseBackupHead",
630 GffValue::UInt8(self.use_backup_head),
631 );
632 upsert_field(
633 &mut root,
634 "Appearance_Type",
635 GffValue::UInt16(self.appearance_id),
636 );
637 upsert_field(&mut root, "Gender", GffValue::UInt8(self.gender_id));
638 upsert_field(&mut root, "FactionID", GffValue::UInt16(self.faction_id));
639 upsert_field(&mut root, "WalkRate", GffValue::Int32(self.walkrate_id));
640 upsert_field(
642 &mut root,
643 "AIState",
644 GffValue::Int32(i32::from(self.ai_state)),
645 );
646 upsert_field(
647 &mut root,
648 "SkillPoints",
649 GffValue::UInt16(self.skill_points),
650 );
651 upsert_field(
652 &mut root,
653 "MovementRate",
654 GffValue::UInt8(self.movement_rate),
655 );
656 upsert_field(
657 &mut root,
658 "SoundSetFile",
659 GffValue::UInt16(self.soundset_id),
660 );
661 upsert_field(&mut root, "PortraitId", GffValue::UInt16(self.portrait_id));
662 upsert_field(
663 &mut root,
664 "Portrait",
665 GffValue::ResRef(self.portrait_resref),
666 );
667 upsert_field(&mut root, "SaveWill", GffValue::UInt8(self.save_will));
668 upsert_field(
669 &mut root,
670 "SaveFortitude",
671 GffValue::UInt8(self.save_fortitude),
672 );
673 upsert_field(&mut root, "Morale", GffValue::UInt8(self.morale));
674 upsert_field(
675 &mut root,
676 "MoraleRecovery",
677 GffValue::UInt8(self.morale_recovery),
678 );
679 upsert_field(
680 &mut root,
681 "MoraleBreakpoint",
682 GffValue::UInt8(self.morale_breakpoint),
683 );
684 upsert_field(&mut root, "PaletteID", GffValue::UInt8(self.palette_id));
685 upsert_field(&mut root, "BodyBag", GffValue::UInt8(self.bodybag_id));
686 upsert_field(
687 &mut root,
688 "BodyVariation",
689 GffValue::UInt8(self.body_variation),
690 );
691 upsert_field(
692 &mut root,
693 "TextureVar",
694 GffValue::UInt8(self.texture_variation),
695 );
696
697 upsert_field(&mut root, "GoodEvil", GffValue::UInt8(self.alignment));
698 upsert_field(
699 &mut root,
700 "ChallengeRating",
701 GffValue::Single(self.challenge_rating),
702 );
703 upsert_field(&mut root, "BlindSpot", GffValue::Single(self.blindspot));
704 upsert_field(
705 &mut root,
706 "MultiplierSet",
707 GffValue::UInt8(self.multiplier_set),
708 );
709 upsert_field(&mut root, "NaturalAC", GffValue::UInt8(self.natural_ac));
710 upsert_field(&mut root, "refbonus", GffValue::Int16(self.reflex_bonus));
711 upsert_field(
712 &mut root,
713 "willbonus",
714 GffValue::Int16(self.willpower_bonus),
715 );
716 upsert_field(
717 &mut root,
718 "fortbonus",
719 GffValue::Int16(self.fortitude_bonus),
720 );
721
722 upsert_field(&mut root, "Str", GffValue::UInt8(self.strength));
723 upsert_field(&mut root, "Dex", GffValue::UInt8(self.dexterity));
724 upsert_field(&mut root, "Con", GffValue::UInt8(self.constitution));
725 upsert_field(&mut root, "Int", GffValue::UInt8(self.intelligence));
726 upsert_field(&mut root, "Wis", GffValue::UInt8(self.wisdom));
727 upsert_field(&mut root, "Cha", GffValue::UInt8(self.charisma));
728
729 upsert_field(
730 &mut root,
731 "CurrentHitPoints",
732 GffValue::Int16(self.current_hp),
733 );
734 upsert_field(&mut root, "MaxHitPoints", GffValue::Int16(self.max_hp));
735 upsert_field(&mut root, "HitPoints", GffValue::Int16(self.hp));
736 upsert_field(&mut root, "CurrentForce", GffValue::Int16(self.fp));
737 upsert_field(&mut root, "ForcePoints", GffValue::Int16(self.max_fp));
738
739 upsert_field(
740 &mut root,
741 "NotReorienting",
742 GffValue::UInt8(u8::from(self.not_reorienting)),
743 );
744 upsert_field(
745 &mut root,
746 "PartyInteract",
747 GffValue::UInt8(u8::from(self.party_interact)),
748 );
749 upsert_field(
750 &mut root,
751 "NoPermDeath",
752 GffValue::UInt8(u8::from(self.no_perm_death)),
753 );
754 upsert_field(&mut root, "Min1HP", GffValue::UInt8(u8::from(self.min1_hp)));
755 upsert_field(&mut root, "Plot", GffValue::UInt8(u8::from(self.plot)));
756 upsert_field(
757 &mut root,
758 "Interruptable",
759 GffValue::UInt8(u8::from(self.interruptable)),
760 );
761 upsert_field(&mut root, "IsPC", GffValue::UInt8(u8::from(self.is_pc)));
762 upsert_field(
763 &mut root,
764 "Disarmable",
765 GffValue::UInt8(u8::from(self.disarmable)),
766 );
767 upsert_field(
768 &mut root,
769 "IgnoreCrePath",
770 GffValue::UInt8(u8::from(self.ignore_cre_path)),
771 );
772 upsert_field(
773 &mut root,
774 "Hologram",
775 GffValue::UInt8(u8::from(self.hologram)),
776 );
777 upsert_field(
778 &mut root,
779 "WillNotRender",
780 GffValue::UInt8(u8::from(self.will_not_render)),
781 );
782 upsert_field(&mut root, "Deity", GffValue::String(self.deity.clone()));
783 upsert_field(
784 &mut root,
785 "Description",
786 GffValue::LocalizedString(self.description.clone()),
787 );
788 upsert_field(&mut root, "LawfulChaotic", GffValue::UInt8(self.lawfulness));
789 upsert_field(&mut root, "Phenotype", GffValue::Int32(self.phenotype_id));
790 upsert_field(
791 &mut root,
792 "Subrace",
793 GffValue::String(self.subrace_name.clone()),
794 );
795
796 upsert_field(
797 &mut root,
798 "ScriptEndDialogu",
799 GffValue::ResRef(self.on_end_dialog),
800 );
801 upsert_field(
802 &mut root,
803 "ScriptOnBlocked",
804 GffValue::ResRef(self.on_blocked),
805 );
806 upsert_field(
807 &mut root,
808 "ScriptHeartbeat",
809 GffValue::ResRef(self.on_heartbeat),
810 );
811 upsert_field(
812 &mut root,
813 "ScriptOnNotice",
814 GffValue::ResRef(self.on_notice),
815 );
816 upsert_field(&mut root, "ScriptSpellAt", GffValue::ResRef(self.on_spell));
817 upsert_field(
818 &mut root,
819 "ScriptAttacked",
820 GffValue::ResRef(self.on_attacked),
821 );
822 upsert_field(
823 &mut root,
824 "ScriptDamaged",
825 GffValue::ResRef(self.on_damaged),
826 );
827 upsert_field(
828 &mut root,
829 "ScriptDisturbed",
830 GffValue::ResRef(self.on_disturbed),
831 );
832 upsert_field(
833 &mut root,
834 "ScriptEndRound",
835 GffValue::ResRef(self.on_end_round),
836 );
837 upsert_field(
838 &mut root,
839 "ScriptDialogue",
840 GffValue::ResRef(self.on_dialog),
841 );
842 upsert_field(&mut root, "ScriptSpawn", GffValue::ResRef(self.on_spawn));
843 upsert_field(&mut root, "ScriptRested", GffValue::ResRef(self.on_rested));
844 upsert_field(&mut root, "ScriptDeath", GffValue::ResRef(self.on_death));
845 upsert_field(
846 &mut root,
847 "ScriptUserDefine",
848 GffValue::ResRef(self.on_user_defined),
849 );
850
851 upsert_field(
852 &mut root,
853 "SkillList",
854 GffValue::List(self.skills.to_list()),
855 );
856
857 let class_structs = self
858 .classes
859 .iter()
860 .map(UtcClass::to_struct)
861 .collect::<Vec<GffStruct>>();
862 upsert_field(&mut root, "ClassList", GffValue::List(class_structs));
863
864 let special_ability_structs = self
865 .special_abilities
866 .iter()
867 .map(UtcSpecialAbility::to_struct)
868 .collect::<Vec<GffStruct>>();
869 upsert_field(
870 &mut root,
871 "SpecAbilityList",
872 GffValue::List(special_ability_structs),
873 );
874
875 upsert_field(
876 &mut root,
877 "FeatList",
878 GffValue::List(write_feat_structs(&self.feats, None)),
879 );
880
881 let equipment_structs = self
882 .equipment
883 .iter()
884 .map(UtcEquipmentItem::to_struct)
885 .collect::<Vec<GffStruct>>();
886 upsert_field(
887 &mut root,
888 "Equip_ItemList",
889 GffValue::List(equipment_structs),
890 );
891
892 let inventory_structs = self
893 .inventory
894 .iter()
895 .map(UtcInventoryItem::to_struct)
896 .collect::<Vec<GffStruct>>();
897 upsert_field(&mut root, "ItemList", GffValue::List(inventory_structs));
898
899 Gff::new(*b"UTC ", root)
900 }
901}
902
903#[derive(Debug, Clone, PartialEq, Default)]
905pub struct UtcSkills {
906 pub computer_use: u8,
908 pub demolitions: u8,
910 pub stealth: u8,
912 pub awareness: u8,
914 pub persuade: u8,
916 pub repair: u8,
918 pub security: u8,
920 pub treat_injury: u8,
922}
923
924impl UtcSkills {
925 fn from_list(list: &[GffStruct]) -> Self {
926 Self {
927 computer_use: read_skill_rank(list, 0),
928 demolitions: read_skill_rank(list, 1),
929 stealth: read_skill_rank(list, 2),
930 awareness: read_skill_rank(list, 3),
931 persuade: read_skill_rank(list, 4),
932 repair: read_skill_rank(list, 5),
933 security: read_skill_rank(list, 6),
934 treat_injury: read_skill_rank(list, 7),
935 }
936 }
937
938 fn to_list(&self) -> Vec<GffStruct> {
939 let mut list = default_skill_structs();
940
941 write_skill_rank(&mut list[0], self.computer_use);
942 write_skill_rank(&mut list[1], self.demolitions);
943 write_skill_rank(&mut list[2], self.stealth);
944 write_skill_rank(&mut list[3], self.awareness);
945 write_skill_rank(&mut list[4], self.persuade);
946 write_skill_rank(&mut list[5], self.repair);
947 write_skill_rank(&mut list[6], self.security);
948 write_skill_rank(&mut list[7], self.treat_injury);
949
950 list
951 }
952}
953
954#[derive(Debug, Clone, PartialEq)]
956pub struct UtcClass {
957 pub class_id: i32,
959 pub class_level: i16,
961 pub powers: Vec<u16>,
963}
964
965impl UtcClass {
966 fn from_struct(structure: &GffStruct) -> Result<Self, UtcError> {
967 let powers = parse_known_list(structure, "KnownList0")?;
968
969 Ok(Self {
970 class_id: get_i32(structure, "Class").unwrap_or(0),
971 class_level: get_i16(structure, "ClassLevel").unwrap_or(0),
972 powers,
973 })
974 }
975
976 fn to_struct(&self) -> GffStruct {
977 let mut structure = GffStruct::new(2);
978
979 upsert_field(&mut structure, "Class", GffValue::Int32(self.class_id));
980 upsert_field(
981 &mut structure,
982 "ClassLevel",
983 GffValue::Int16(self.class_level),
984 );
985 write_known_list(&mut structure, "KnownList0", &self.powers);
986 structure
987 }
988}
989
990#[derive(Debug, Clone, PartialEq)]
992pub struct UtcSpecialAbility {
993 pub spell_id: u16,
995 pub spell_flags: u8,
997 pub spell_caster_level: u8,
999}
1000
1001impl UtcSpecialAbility {
1002 fn from_struct(structure: &GffStruct) -> Self {
1003 Self {
1004 spell_id: get_u16(structure, "Spell").unwrap_or(0),
1005 spell_flags: get_u8(structure, "SpellFlags").unwrap_or(0),
1006 spell_caster_level: get_u8(structure, "SpellCasterLevel").unwrap_or(0),
1007 }
1008 }
1009
1010 fn to_struct(&self) -> GffStruct {
1011 let mut structure = GffStruct::new(4);
1012
1013 upsert_field(&mut structure, "Spell", GffValue::UInt16(self.spell_id));
1014 upsert_field(
1015 &mut structure,
1016 "SpellFlags",
1017 GffValue::UInt8(self.spell_flags),
1018 );
1019 upsert_field(
1020 &mut structure,
1021 "SpellCasterLevel",
1022 GffValue::UInt8(self.spell_caster_level),
1023 );
1024 structure
1025 }
1026}
1027
1028fn parse_known_list(structure: &GffStruct, label: &'static str) -> Result<Vec<u16>, UtcError> {
1029 match structure.field(label) {
1030 Some(GffValue::List(power_structs)) => Ok(power_structs
1031 .iter()
1032 .map(|power_struct| get_u16(power_struct, "Spell").unwrap_or(0))
1033 .collect::<Vec<_>>()),
1034 Some(_) => Err(UtcError::TypeMismatch {
1035 field: label,
1036 expected: "List",
1037 }),
1038 None => Ok(Vec::new()),
1039 }
1040}
1041
1042fn write_known_list(structure: &mut GffStruct, label: &'static str, powers: &[u16]) {
1043 let power_structs = powers
1044 .iter()
1045 .copied()
1046 .map(|spell| {
1047 let mut s = GffStruct::new(3);
1048 upsert_field(&mut s, "Spell", GffValue::UInt16(spell));
1049 upsert_field(&mut s, "SpellFlags", GffValue::UInt8(1));
1050 upsert_field(&mut s, "SpellMetaMagic", GffValue::UInt8(0));
1051 s
1052 })
1053 .collect::<Vec<_>>();
1054
1055 upsert_field(structure, label, GffValue::List(power_structs));
1056}
1057
1058#[derive(Debug, Clone, PartialEq)]
1060pub struct UtcEquipmentItem {
1061 pub slot_id: i32,
1063 pub resref: ResRef,
1065 pub droppable: bool,
1067}
1068
1069impl UtcEquipmentItem {
1070 pub fn new(slot_id: i32, resref: ResRef) -> Self {
1072 Self {
1073 slot_id,
1074 resref,
1075 droppable: false,
1076 }
1077 }
1078
1079 fn from_struct(structure: &GffStruct) -> Self {
1080 Self {
1081 slot_id: structure.struct_id,
1082 resref: get_resref(structure, "EquippedRes").unwrap_or_default(),
1083 droppable: get_bool(structure, "Dropable").unwrap_or(false),
1084 }
1085 }
1086
1087 fn to_struct(&self) -> GffStruct {
1088 let mut structure = GffStruct::new(self.slot_id);
1089
1090 upsert_field(&mut structure, "EquippedRes", GffValue::ResRef(self.resref));
1091 if self.droppable {
1092 upsert_field(&mut structure, "Dropable", GffValue::UInt8(1));
1093 }
1094
1095 structure
1096 }
1097}
1098
1099#[derive(Debug, Clone, PartialEq)]
1101pub struct UtcInventoryItem {
1102 pub entry_id: i32,
1104 pub resref: ResRef,
1106 pub droppable: bool,
1108 pub repos_pos_x: Option<u16>,
1110 pub repos_pos_y: Option<u16>,
1112}
1113
1114impl UtcInventoryItem {
1115 pub fn new(entry_id: i32, resref: ResRef) -> Self {
1117 Self {
1118 entry_id,
1119 resref,
1120 droppable: false,
1121 repos_pos_x: None,
1122 repos_pos_y: None,
1123 }
1124 }
1125
1126 fn from_struct(structure: &GffStruct) -> Self {
1127 Self {
1128 entry_id: structure.struct_id,
1129 resref: get_resref(structure, "InventoryRes").unwrap_or_default(),
1130 droppable: get_bool(structure, "Dropable").unwrap_or(false),
1131 repos_pos_x: get_u16(structure, "Repos_PosX"),
1132 repos_pos_y: get_u16(structure, "Repos_Posy"),
1133 }
1134 }
1135
1136 fn to_struct(&self) -> GffStruct {
1137 let mut structure = GffStruct::new(self.entry_id);
1138
1139 upsert_field(
1140 &mut structure,
1141 "InventoryRes",
1142 GffValue::ResRef(self.resref),
1143 );
1144 if self.droppable {
1145 upsert_field(&mut structure, "Dropable", GffValue::UInt8(1));
1146 }
1147
1148 if let Some(value) = self.repos_pos_x {
1149 upsert_field(&mut structure, "Repos_PosX", GffValue::UInt16(value));
1150 }
1151 if let Some(value) = self.repos_pos_y {
1152 upsert_field(&mut structure, "Repos_Posy", GffValue::UInt16(value));
1153 }
1154
1155 structure
1156 }
1157}
1158
1159#[derive(Debug, Error)]
1161pub enum UtcError {
1162 #[error("unsupported UTC file type: {0:?}")]
1164 UnsupportedFileType([u8; 4]),
1165 #[error("UTC field `{field}` has incompatible type (expected {expected})")]
1167 TypeMismatch {
1168 field: &'static str,
1170 expected: &'static str,
1172 },
1173 #[error(transparent)]
1175 Gff(#[from] GffBinaryError),
1176}
1177
1178#[cfg_attr(
1180 feature = "tracing",
1181 tracing::instrument(level = "debug", skip(reader))
1182)]
1183pub fn read_utc<R: Read>(reader: &mut R) -> Result<Utc, UtcError> {
1184 let gff = read_gff(reader)?;
1185 Utc::from_gff(&gff)
1186}
1187
1188#[cfg_attr(
1190 feature = "tracing",
1191 tracing::instrument(level = "debug", skip(bytes), fields(bytes_len = bytes.len()))
1192)]
1193pub fn read_utc_from_bytes(bytes: &[u8]) -> Result<Utc, UtcError> {
1194 let gff = read_gff_from_bytes(bytes)?;
1195 Utc::from_gff(&gff)
1196}
1197
1198#[cfg_attr(
1200 feature = "tracing",
1201 tracing::instrument(level = "debug", skip(writer, utc))
1202)]
1203pub fn write_utc<W: Write>(writer: &mut W, utc: &Utc) -> Result<(), UtcError> {
1204 let gff = utc.to_gff();
1205 write_gff(writer, &gff)?;
1206 Ok(())
1207}
1208
1209#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(utc)))]
1211pub fn write_utc_to_vec(utc: &Utc) -> Result<Vec<u8>, UtcError> {
1212 let mut cursor = Cursor::new(Vec::new());
1213 write_utc(&mut cursor, utc)?;
1214 Ok(cursor.into_inner())
1215}
1216
1217fn write_feat_structs(feats: &[u16], existing: Option<&[GffStruct]>) -> Vec<GffStruct> {
1218 let mut feat_structs = existing.map_or_else(Vec::new, <[GffStruct]>::to_vec);
1219
1220 while feat_structs.len() < feats.len() {
1221 feat_structs.push(GffStruct::new(1));
1222 }
1223 if feat_structs.len() > feats.len() {
1224 feat_structs.truncate(feats.len());
1225 }
1226
1227 for (idx, feat_id) in feats.iter().copied().enumerate() {
1228 let feat_struct = &mut feat_structs[idx];
1229 upsert_field(feat_struct, "Feat", GffValue::UInt16(feat_id));
1230 }
1231
1232 feat_structs
1233}
1234
1235fn default_skill_structs() -> Vec<GffStruct> {
1236 let mut list = Vec::with_capacity(8);
1237 for _ in 0..8 {
1238 let mut skill = GffStruct::new(0);
1239 upsert_field(&mut skill, "Rank", GffValue::UInt8(0));
1240 list.push(skill);
1241 }
1242 list
1243}
1244
1245fn read_skill_rank(list: &[GffStruct], index: usize) -> u8 {
1246 list.get(index)
1247 .and_then(|skill| get_u8(skill, "Rank"))
1248 .unwrap_or(0)
1249}
1250
1251fn write_skill_rank(skill: &mut GffStruct, rank: u8) {
1252 upsert_field(skill, "Rank", GffValue::UInt8(rank));
1253}
1254
1255static CLASS_LIST_CHILDREN: &[FieldSchema] = &[
1257 FieldSchema {
1258 label: "KnownList0",
1259 expected_type: GffType::List,
1260 life: FieldLife::Live,
1261 required: false,
1262 absent: AbsentDefault::Unverified,
1263 children: None,
1264 constraint: None,
1265 },
1266 FieldSchema {
1267 label: "Class",
1268 expected_type: GffType::Int32,
1269 life: FieldLife::Live,
1270 required: false,
1271 absent: AbsentDefault::Unverified,
1272 children: None,
1273 constraint: None,
1274 },
1275 FieldSchema {
1276 label: "ClassLevel",
1277 expected_type: GffType::Int16,
1278 life: FieldLife::Live,
1279 required: false,
1280 absent: AbsentDefault::Unverified,
1281 children: None,
1282 constraint: None,
1283 },
1284 FieldSchema {
1285 label: "SpellsPerDayList",
1286 expected_type: GffType::List,
1287 life: FieldLife::Live,
1288 required: false,
1289 absent: AbsentDefault::Unverified,
1290 children: None,
1291 constraint: None,
1292 },
1293];
1294
1295static FEAT_LIST_CHILDREN: &[FieldSchema] = &[FieldSchema {
1297 label: "Feat",
1298 expected_type: GffType::UInt16,
1299 life: FieldLife::Live,
1300 required: false,
1301 absent: AbsentDefault::Unverified,
1302 children: None,
1303 constraint: None,
1304}];
1305
1306static SKILL_LIST_CHILDREN: &[FieldSchema] = &[FieldSchema {
1308 label: "Rank",
1309 expected_type: GffType::UInt8,
1310 life: FieldLife::Live,
1311 required: false,
1312 absent: AbsentDefault::Unverified,
1313 children: None,
1314 constraint: None,
1315}];
1316
1317static SPEC_ABILITY_LIST_CHILDREN: &[FieldSchema] = &[
1319 FieldSchema {
1320 label: "Spell",
1321 expected_type: GffType::UInt16,
1322 life: FieldLife::Live,
1323 required: false,
1324 absent: AbsentDefault::Unverified,
1325 children: None,
1326 constraint: None,
1327 },
1328 FieldSchema {
1329 label: "SpellFlags",
1330 expected_type: GffType::UInt8,
1331 life: FieldLife::Live,
1332 required: false,
1333 absent: AbsentDefault::Unverified,
1334 children: None,
1335 constraint: None,
1336 },
1337 FieldSchema {
1338 label: "SpellCasterLevel",
1339 expected_type: GffType::UInt8,
1340 life: FieldLife::Live,
1341 required: false,
1342 absent: AbsentDefault::Unverified,
1343 children: None,
1344 constraint: None,
1345 },
1346];
1347
1348static ITEM_LIST_CHILDREN: &[FieldSchema] = &[
1350 FieldSchema {
1351 label: "InventoryRes",
1352 expected_type: GffType::ResRef,
1353 life: FieldLife::Live,
1354 required: false,
1355 absent: AbsentDefault::Unverified,
1356 children: None,
1357 constraint: None,
1358 },
1359 FieldSchema {
1360 label: "Infinite",
1361 expected_type: GffType::UInt8,
1362 life: FieldLife::Live,
1363 required: false,
1364 absent: AbsentDefault::Unverified,
1365 children: None,
1366 constraint: None,
1367 },
1368 FieldSchema {
1369 label: "ObjectId",
1370 expected_type: GffType::UInt32,
1371 life: FieldLife::Live,
1372 required: false,
1373 absent: AbsentDefault::Unverified,
1374 children: None,
1375 constraint: None,
1376 },
1377 FieldSchema {
1378 label: "Dropable",
1379 expected_type: GffType::UInt8,
1380 life: FieldLife::Live,
1381 required: false,
1382 absent: AbsentDefault::Stamped(DefaultValue::UInt8(0), "utc.md: Dropable is unconditional on both item lists, a literal 0 stamped regardless of presence"),
1383 children: None,
1384 constraint: None,
1385 },
1386 FieldSchema {
1387 label: "Repos_PosX",
1388 expected_type: GffType::UInt16,
1389 life: FieldLife::Live,
1390 required: false,
1391 absent: AbsentDefault::Unverified,
1392 children: None,
1393 constraint: None,
1394 },
1395 FieldSchema {
1396 label: "Repos_PosY",
1397 expected_type: GffType::UInt16,
1398 life: FieldLife::Live,
1399 required: false,
1400 absent: AbsentDefault::Unverified,
1401 children: None,
1402 constraint: None,
1403 },
1404 FieldSchema {
1405 label: "Repos_Posy",
1406 expected_type: GffType::UInt16,
1407 life: FieldLife::Live,
1408 required: false,
1409 absent: AbsentDefault::Unverified,
1410 children: None,
1411 constraint: None,
1412 },
1413];
1414
1415static EQUIP_ITEM_LIST_CHILDREN: &[FieldSchema] = &[
1417 FieldSchema {
1418 label: "EquippedRes",
1419 expected_type: GffType::ResRef,
1420 life: FieldLife::Live,
1421 required: false,
1422 absent: AbsentDefault::Unverified,
1423 children: None,
1424 constraint: None,
1425 },
1426 FieldSchema {
1427 label: "ObjectId",
1428 expected_type: GffType::UInt32,
1429 life: FieldLife::Live,
1430 required: false,
1431 absent: AbsentDefault::Unverified,
1432 children: None,
1433 constraint: None,
1434 },
1435 FieldSchema {
1436 label: "Dropable",
1437 expected_type: GffType::UInt8,
1438 life: FieldLife::Live,
1439 required: false,
1440 absent: AbsentDefault::Unverified,
1441 children: None,
1442 constraint: None,
1443 },
1444];
1445
1446impl GffSchema for Utc {
1447 fn schema() -> &'static [FieldSchema] {
1448 static SCHEMA: &[FieldSchema] = &[
1449 FieldSchema {
1450 label: "MaxHitPoints",
1451 expected_type: GffType::Int16,
1452 life: FieldLife::Live,
1453 required: false,
1454 absent: AbsentDefault::Unverified,
1455 children: None,
1456 constraint: None,
1457 },
1458 FieldSchema {
1461 label: "FirstName",
1462 expected_type: GffType::LocalizedString,
1463 life: FieldLife::Live,
1464 required: false,
1465 absent: AbsentDefault::Stamped(DefaultValue::EmptyLocalizedString, "utc.md: FirstName and LastName are read against a freshly default-constructed empty localized string, so an absent one overwrites rather than carrying over"),
1466 children: None,
1467 constraint: None,
1468 },
1469 FieldSchema {
1470 label: "LastName",
1471 expected_type: GffType::LocalizedString,
1472 life: FieldLife::Live,
1473 required: false,
1474 absent: AbsentDefault::Stamped(DefaultValue::EmptyLocalizedString, "utc.md: FirstName and LastName are read against a freshly default-constructed empty localized string, so an absent one overwrites rather than carrying over"),
1475 children: None,
1476 constraint: None,
1477 },
1478 FieldSchema {
1479 label: "Description",
1480 expected_type: GffType::LocalizedString,
1481 life: FieldLife::Live,
1482 required: false,
1483 absent: AbsentDefault::Constructed(DefaultValue::EmptyLocalizedString, "utc.md: the read fallback is the object's own current value, stamped back unconditionally, so an absent field is a no-op on a freshly constructed load"),
1484 children: None,
1485 constraint: None,
1486 },
1487 FieldSchema {
1488 label: "IsPC",
1489 expected_type: GffType::UInt8,
1490 life: FieldLife::Live,
1491 required: false,
1492 absent: AbsentDefault::Unverified,
1493 children: None,
1494 constraint: None,
1495 },
1496 FieldSchema {
1497 label: "Tag",
1498 expected_type: GffType::String,
1499 life: FieldLife::Live,
1500 required: false,
1501 absent: AbsentDefault::Constructed(DefaultValue::Text(""), "utc.md: the read fallback is the object's own current value, stamped back unconditionally, so an absent field is a no-op on a freshly constructed load"),
1502 children: None,
1503 constraint: None,
1504 },
1505 FieldSchema {
1506 label: "Conversation",
1507 expected_type: GffType::ResRef,
1508 life: FieldLife::Live,
1509 required: false,
1510 absent: AbsentDefault::Constructed(DefaultValue::Text(""), "utc.md: the read fallback is the object's own current value, stamped back unconditionally, so an absent field is a no-op on a freshly constructed load"),
1511 children: None,
1512 constraint: None,
1513 },
1514 FieldSchema {
1515 label: "Interruptable",
1516 expected_type: GffType::UInt8,
1517 life: FieldLife::Live,
1518 required: false,
1519 absent: AbsentDefault::Constructed(DefaultValue::UInt8(0), "utc.md: BodyBag and Interruptable are read live via an ordinary carry-over byte read, unlike the identically-named dead field on UTD and UTP"),
1520 children: None,
1521 constraint: None,
1522 },
1523 FieldSchema {
1525 label: "Age",
1526 expected_type: GffType::Int32,
1527 life: FieldLife::Live,
1528 required: false,
1529 absent: AbsentDefault::Constructed(DefaultValue::Int32(0), "utc.md: the read fallback is the object's own current value, stamped back unconditionally, so an absent field is a no-op on a freshly constructed load"),
1530 children: None,
1531 constraint: None,
1532 },
1533 FieldSchema {
1534 label: "Gender",
1535 expected_type: GffType::UInt8,
1536 life: FieldLife::Live,
1537 required: false,
1538 absent: AbsentDefault::Constructed(DefaultValue::UInt8(0), "utc.md: the read fallback is the object's own current value, stamped back unconditionally, so an absent field is a no-op on a freshly constructed load"),
1539 children: None,
1540 constraint: Some(FieldConstraint::RangeInt(0, 4)),
1541 },
1542 FieldSchema {
1543 label: "StartingPackage",
1544 expected_type: GffType::UInt8,
1545 life: FieldLife::Live,
1546 required: false,
1547 absent: AbsentDefault::Constructed(DefaultValue::UInt8(0), "utc.md: the read fallback is the object's own current value, stamped back unconditionally, so an absent field is a no-op on a freshly constructed load"),
1548 children: None,
1549 constraint: None,
1550 },
1551 FieldSchema {
1552 label: "Race",
1553 expected_type: GffType::UInt8,
1554 life: FieldLife::Live,
1555 required: false,
1556 absent: AbsentDefault::Unverified,
1557 children: None,
1558 constraint: None,
1559 },
1560 FieldSchema {
1561 label: "Subrace",
1562 expected_type: GffType::String,
1563 life: FieldLife::Live,
1564 required: false,
1565 absent: AbsentDefault::Constructed(DefaultValue::Text(""), "utc.md: the read fallback is the object's own current value, stamped back unconditionally, so an absent field is a no-op on a freshly constructed load"),
1566 children: None,
1567 constraint: None,
1568 },
1569 FieldSchema {
1570 label: "SubraceIndex",
1571 expected_type: GffType::UInt8,
1572 life: FieldLife::Live,
1573 required: false,
1574 absent: AbsentDefault::Constructed(DefaultValue::UInt8(0), "utc.md: the read fallback is the object's own current value, stamped back unconditionally, so an absent field is a no-op on a freshly constructed load"),
1575 children: None,
1576 constraint: None,
1577 },
1578 FieldSchema {
1579 label: "Deity",
1580 expected_type: GffType::String,
1581 life: FieldLife::Live,
1582 required: false,
1583 absent: AbsentDefault::Constructed(DefaultValue::Text(""), "utc.md: the read fallback is the object's own current value, stamped back unconditionally, so an absent field is a no-op on a freshly constructed load"),
1584 children: None,
1585 constraint: None,
1586 },
1587 FieldSchema {
1589 label: "Str",
1590 expected_type: GffType::UInt8,
1591 life: FieldLife::Live,
1592 required: false,
1593 absent: AbsentDefault::Constructed(DefaultValue::UInt8(0), "utc.md: the constructor initializes all six ability scores to a literal 0 before any read, so an absent score resolves to 0 rather than a tabletop 10"),
1594 children: None,
1595 constraint: None,
1596 },
1597 FieldSchema {
1598 label: "Dex",
1599 expected_type: GffType::UInt8,
1600 life: FieldLife::Live,
1601 required: false,
1602 absent: AbsentDefault::Constructed(DefaultValue::UInt8(0), "utc.md: the constructor initializes all six ability scores to a literal 0 before any read, so an absent score resolves to 0 rather than a tabletop 10"),
1603 children: None,
1604 constraint: None,
1605 },
1606 FieldSchema {
1607 label: "Int",
1608 expected_type: GffType::UInt8,
1609 life: FieldLife::Live,
1610 required: false,
1611 absent: AbsentDefault::Constructed(DefaultValue::UInt8(0), "utc.md: the constructor initializes all six ability scores to a literal 0 before any read, so an absent score resolves to 0 rather than a tabletop 10"),
1612 children: None,
1613 constraint: None,
1614 },
1615 FieldSchema {
1616 label: "Wis",
1617 expected_type: GffType::UInt8,
1618 life: FieldLife::Live,
1619 required: false,
1620 absent: AbsentDefault::Constructed(DefaultValue::UInt8(0), "utc.md: the constructor initializes all six ability scores to a literal 0 before any read, so an absent score resolves to 0 rather than a tabletop 10"),
1621 children: None,
1622 constraint: None,
1623 },
1624 FieldSchema {
1625 label: "Con",
1626 expected_type: GffType::UInt8,
1627 life: FieldLife::Live,
1628 required: false,
1629 absent: AbsentDefault::Constructed(DefaultValue::UInt8(0), "utc.md: the constructor initializes all six ability scores to a literal 0 before any read, so an absent score resolves to 0 rather than a tabletop 10"),
1630 children: None,
1631 constraint: None,
1632 },
1633 FieldSchema {
1634 label: "Cha",
1635 expected_type: GffType::UInt8,
1636 life: FieldLife::Live,
1637 required: false,
1638 absent: AbsentDefault::Constructed(DefaultValue::UInt8(0), "utc.md: the constructor initializes all six ability scores to a literal 0 before any read, so an absent score resolves to 0 rather than a tabletop 10"),
1639 children: None,
1640 constraint: None,
1641 },
1642 FieldSchema {
1643 label: "NaturalAC",
1644 expected_type: GffType::UInt8,
1645 life: FieldLife::Live,
1646 required: false,
1647 absent: AbsentDefault::Constructed(DefaultValue::UInt8(0), "utc.md: the read fallback is the object's own current value, stamped back unconditionally, so an absent field is a no-op on a freshly constructed load"),
1648 children: None,
1649 constraint: None,
1650 },
1651 FieldSchema {
1653 label: "SoundSetFile",
1654 expected_type: GffType::UInt16,
1655 life: FieldLife::Live,
1656 required: false,
1657 absent: AbsentDefault::Unverified,
1658 children: None,
1659 constraint: None,
1660 },
1661 FieldSchema {
1662 label: "Gold",
1663 expected_type: GffType::UInt32,
1664 life: FieldLife::Live,
1665 required: false,
1666 absent: AbsentDefault::Unverified,
1667 children: None,
1668 constraint: None,
1669 },
1670 FieldSchema {
1672 label: "Invulnerable",
1673 expected_type: GffType::UInt8,
1674 life: FieldLife::Live,
1675 required: false,
1676 absent: AbsentDefault::Constructed(DefaultValue::UInt8(0), "utc.md: the Invulnerable and Plot labels both land in one member, with a final fallback of the object's own prior plot value"),
1677 children: None,
1678 constraint: None,
1679 },
1680 FieldSchema {
1681 label: "Plot",
1682 expected_type: GffType::UInt8,
1683 life: FieldLife::Live,
1684 required: false,
1685 absent: AbsentDefault::Constructed(DefaultValue::UInt8(0), "utc.md: the Invulnerable and Plot labels both land in one member, with a final fallback of the object's own prior plot value"),
1686 children: None,
1687 constraint: None,
1688 },
1689 FieldSchema {
1690 label: "Min1HP",
1691 expected_type: GffType::UInt8,
1692 life: FieldLife::Live,
1693 required: false,
1694 absent: AbsentDefault::Constructed(DefaultValue::UInt8(0), "utc.md: the read fallback is the object's own current value, stamped back unconditionally, so an absent field is a no-op on a freshly constructed load"),
1695 children: None,
1696 constraint: None,
1697 },
1698 FieldSchema {
1699 label: "PartyInteract",
1700 expected_type: GffType::UInt8,
1701 life: FieldLife::Live,
1702 required: false,
1703 absent: AbsentDefault::Constructed(DefaultValue::UInt8(0), "utc.md: the read fallback is the object's own current value, stamped back unconditionally, so an absent field is a no-op on a freshly constructed load"),
1704 children: None,
1705 constraint: None,
1706 },
1707 FieldSchema {
1708 label: "NotReorienting",
1709 expected_type: GffType::UInt8,
1710 life: FieldLife::Live,
1711 required: false,
1712 absent: AbsentDefault::Unverified,
1713 children: None,
1714 constraint: None,
1715 },
1716 FieldSchema {
1717 label: "Disarmable",
1718 expected_type: GffType::UInt8,
1719 life: FieldLife::Live,
1720 required: false,
1721 absent: AbsentDefault::Constructed(DefaultValue::UInt8(0), "utc.md: the read fallback is the object's own current value, stamped back unconditionally, so an absent field is a no-op on a freshly constructed load"),
1722 children: None,
1723 constraint: None,
1724 },
1725 FieldSchema {
1727 label: "Experience",
1728 expected_type: GffType::UInt32,
1729 life: FieldLife::Live,
1730 required: false,
1731 absent: AbsentDefault::Constructed(DefaultValue::UInt32(0), "utc.md: Experience is absent from every vanilla file; the constructor holds 0 and SetExperience refuses to lower XP, so both sides agree"),
1732 children: None,
1733 constraint: None,
1734 },
1735 FieldSchema {
1737 label: "PortraitId",
1738 expected_type: GffType::UInt16,
1739 life: FieldLife::Live,
1740 required: false,
1741 absent: AbsentDefault::Stamped(DefaultValue::UInt16(65535), "utc.md: PortraitId uses a fresh literal sentinel 0xFFFF, which fails the same < 0xFFFE check and routes to the Portrait resref path"),
1742 children: None,
1743 constraint: None,
1744 },
1745 FieldSchema {
1746 label: "Portrait",
1747 expected_type: GffType::ResRef,
1748 life: FieldLife::Live,
1749 required: false,
1750 absent: AbsentDefault::Constructed(DefaultValue::Text(""), "utc.md: the read fallback is the object's own current value, stamped back unconditionally, so an absent field is a no-op on a freshly constructed load"),
1751 children: None,
1752 constraint: None,
1753 },
1754 FieldSchema {
1756 label: "GoodEvil",
1757 expected_type: GffType::UInt8,
1758 life: FieldLife::Live,
1759 required: false,
1760 absent: AbsentDefault::Constructed(DefaultValue::UInt8(0), "utc.md: the read fallback is the object's own current value, stamped back unconditionally, so an absent field is a no-op on a freshly constructed load"),
1761 children: None,
1762 constraint: Some(FieldConstraint::RangeInt(0, 100)),
1763 },
1764 FieldSchema {
1766 label: "Color_Skin",
1767 expected_type: GffType::UInt8,
1768 life: FieldLife::Live,
1769 required: false,
1770 absent: AbsentDefault::Constructed(DefaultValue::UInt8(0), "utc.md: the read fallback is the object's own current value, stamped back unconditionally, so an absent field is a no-op on a freshly constructed load"),
1771 children: None,
1772 constraint: None,
1773 },
1774 FieldSchema {
1775 label: "Color_Hair",
1776 expected_type: GffType::UInt8,
1777 life: FieldLife::Live,
1778 required: false,
1779 absent: AbsentDefault::Constructed(DefaultValue::UInt8(0), "utc.md: the read fallback is the object's own current value, stamped back unconditionally, so an absent field is a no-op on a freshly constructed load"),
1780 children: None,
1781 constraint: None,
1782 },
1783 FieldSchema {
1784 label: "Color_Tattoo1",
1785 expected_type: GffType::UInt8,
1786 life: FieldLife::Live,
1787 required: false,
1788 absent: AbsentDefault::Constructed(DefaultValue::UInt8(0), "utc.md: the read fallback is the object's own current value, stamped back unconditionally, so an absent field is a no-op on a freshly constructed load"),
1789 children: None,
1790 constraint: None,
1791 },
1792 FieldSchema {
1793 label: "Color_Tattoo2",
1794 expected_type: GffType::UInt8,
1795 life: FieldLife::Live,
1796 required: false,
1797 absent: AbsentDefault::Constructed(DefaultValue::UInt8(0), "utc.md: the read fallback is the object's own current value, stamped back unconditionally, so an absent field is a no-op on a freshly constructed load"),
1798 children: None,
1799 constraint: None,
1800 },
1801 FieldSchema {
1802 label: "Phenotype",
1803 expected_type: GffType::Int32,
1804 life: FieldLife::Live,
1805 required: false,
1806 absent: AbsentDefault::Constructed(DefaultValue::Int32(0), "utc.md: the read fallback is the object's own current value, stamped back unconditionally, so an absent field is a no-op on a freshly constructed load"),
1807 children: None,
1808 constraint: None,
1809 },
1810 FieldSchema {
1811 label: "Appearance_Type",
1812 expected_type: GffType::UInt16,
1813 life: FieldLife::Live,
1814 required: false,
1815 absent: AbsentDefault::Constructed(DefaultValue::UInt16(0), "utc.md: the read fallback is the object's own current value, stamped back unconditionally, so an absent field is a no-op on a freshly constructed load"),
1816 children: None,
1817 constraint: None,
1818 },
1819 FieldSchema {
1820 label: "Appearance_Head",
1821 expected_type: GffType::UInt8,
1822 life: FieldLife::Live,
1823 required: false,
1824 absent: AbsentDefault::Constructed(DefaultValue::UInt8(0), "utc.md: the head constructor default is 0; the engine override of 0 to 1 checks the resolved value rather than presence, so it is resolution and not a read fallback"),
1825 children: None,
1826 constraint: None,
1827 },
1828 FieldSchema {
1829 label: "DuplicatingHead",
1830 expected_type: GffType::UInt8,
1831 life: FieldLife::Live,
1832 required: false,
1833 absent: AbsentDefault::Constructed(DefaultValue::UInt8(0), "utc.md: the read fallback is the object's own current value, stamped back unconditionally, so an absent field is a no-op on a freshly constructed load"),
1834 children: None,
1835 constraint: None,
1836 },
1837 FieldSchema {
1838 label: "UseBackupHead",
1839 expected_type: GffType::UInt8,
1840 life: FieldLife::Live,
1841 required: false,
1842 absent: AbsentDefault::Constructed(DefaultValue::UInt8(0), "utc.md: the read fallback is the object's own current value, stamped back unconditionally, so an absent field is a no-op on a freshly constructed load"),
1843 children: None,
1844 constraint: None,
1845 },
1846 FieldSchema {
1848 label: "FactionID",
1849 expected_type: GffType::UInt16,
1850 life: FieldLife::Live,
1851 required: false,
1852 absent: AbsentDefault::Constructed(DefaultValue::UInt16(0), "utc.md: the read fallback is the object's own current value, stamped back unconditionally, so an absent field is a no-op on a freshly constructed load"),
1853 children: None,
1854 constraint: None,
1855 },
1856 FieldSchema {
1858 label: "ChallengeRating",
1859 expected_type: GffType::Single,
1860 life: FieldLife::Live,
1861 required: false,
1862 absent: AbsentDefault::Constructed(DefaultValue::Single(0.0), "utc.md: the read fallback is the object's own current value, stamped back unconditionally, so an absent field is a no-op on a freshly constructed load"),
1863 children: None,
1864 constraint: None,
1865 },
1866 FieldSchema {
1867 label: "AIState",
1868 expected_type: GffType::Int32,
1869 life: FieldLife::Live,
1870 required: false,
1871 absent: AbsentDefault::Constructed(DefaultValue::Int32(0), "utc.md: the read fallback is the object's own current value, stamped back unconditionally, so an absent field is a no-op on a freshly constructed load"),
1872 children: None,
1873 constraint: None,
1874 },
1875 FieldSchema {
1876 label: "BodyBag",
1877 expected_type: GffType::UInt8,
1878 life: FieldLife::Live,
1879 required: false,
1880 absent: AbsentDefault::Constructed(DefaultValue::UInt8(0), "utc.md: BodyBag and Interruptable are read live via an ordinary carry-over byte read, unlike the identically-named dead field on UTD and UTP"),
1881 children: None,
1882 constraint: None,
1883 },
1884 FieldSchema {
1885 label: "PerceptionRange",
1886 expected_type: GffType::UInt8,
1887 life: FieldLife::Live,
1888 required: false,
1889 absent: AbsentDefault::Unverified,
1890 children: None,
1891 constraint: None,
1892 },
1893 FieldSchema {
1895 label: "willbonus",
1896 expected_type: GffType::Int16,
1897 life: FieldLife::Live,
1898 required: false,
1899 absent: AbsentDefault::Unverified,
1900 children: None,
1901 constraint: None,
1902 },
1903 FieldSchema {
1904 label: "fortbonus",
1905 expected_type: GffType::Int16,
1906 life: FieldLife::Live,
1907 required: false,
1908 absent: AbsentDefault::Unverified,
1909 children: None,
1910 constraint: None,
1911 },
1912 FieldSchema {
1913 label: "refbonus",
1914 expected_type: GffType::Int16,
1915 life: FieldLife::Live,
1916 required: false,
1917 absent: AbsentDefault::Unverified,
1918 children: None,
1919 constraint: None,
1920 },
1921 FieldSchema {
1923 label: "HitPoints",
1924 expected_type: GffType::Int16,
1925 life: FieldLife::Live,
1926 required: false,
1927 absent: AbsentDefault::Unverified,
1928 children: None,
1929 constraint: None,
1930 },
1931 FieldSchema {
1932 label: "ForcePoints",
1933 expected_type: GffType::Int16,
1934 life: FieldLife::Live,
1935 required: false,
1936 absent: AbsentDefault::Unverified,
1937 children: None,
1938 constraint: None,
1939 },
1940 FieldSchema {
1941 label: "CurrentHitPoints",
1942 expected_type: GffType::Int16,
1943 life: FieldLife::Live,
1944 required: false,
1945 absent: AbsentDefault::Unverified,
1946 children: None,
1947 constraint: None,
1948 },
1949 FieldSchema {
1950 label: "CurrentForce",
1951 expected_type: GffType::Int16,
1952 life: FieldLife::Live,
1953 required: false,
1954 absent: AbsentDefault::Unverified,
1955 children: None,
1956 constraint: None,
1957 },
1958 FieldSchema {
1960 label: "SkillPoints",
1961 expected_type: GffType::UInt16,
1962 life: FieldLife::Live,
1963 required: false,
1964 absent: AbsentDefault::Unverified,
1965 children: None,
1966 constraint: None,
1967 },
1968 FieldSchema {
1969 label: "MovementRate",
1970 expected_type: GffType::UInt8,
1971 life: FieldLife::Live,
1972 required: false,
1973 absent: AbsentDefault::NotAConstant("utc.md: MovementRate falls back to the WalkRate label; what the pair resolves to when neither is present is not documented"),
1974 children: None,
1975 constraint: None,
1976 },
1977 FieldSchema {
1978 label: "WalkRate",
1979 expected_type: GffType::Int32,
1980 life: FieldLife::Live,
1981 required: false,
1982 absent: AbsentDefault::NotAConstant("utc.md: WalkRate falls back to the object's current movement_rate, the mirror of the MovementRate-falls-back-to-WalkRate rule"),
1983 children: None,
1984 constraint: None,
1985 },
1986 FieldSchema {
1988 label: "CreatureSize",
1989 expected_type: GffType::Int32,
1990 life: FieldLife::Live,
1991 required: false,
1992 absent: AbsentDefault::Unverified,
1993 children: None,
1994 constraint: None,
1995 },
1996 FieldSchema {
1997 label: "IsDestroyable",
1998 expected_type: GffType::UInt8,
1999 life: FieldLife::Live,
2000 required: false,
2001 absent: AbsentDefault::Unverified,
2002 children: None,
2003 constraint: None,
2004 },
2005 FieldSchema {
2006 label: "IsRaiseable",
2007 expected_type: GffType::UInt8,
2008 life: FieldLife::Live,
2009 required: false,
2010 absent: AbsentDefault::Unverified,
2011 children: None,
2012 constraint: None,
2013 },
2014 FieldSchema {
2015 label: "DeadSelectable",
2016 expected_type: GffType::UInt8,
2017 life: FieldLife::Live,
2018 required: false,
2019 absent: AbsentDefault::Unverified,
2020 children: None,
2021 constraint: None,
2022 },
2023 FieldSchema {
2024 label: "AmbientAnimState",
2025 expected_type: GffType::UInt8,
2026 life: FieldLife::Live,
2027 required: false,
2028 absent: AbsentDefault::Unverified,
2029 children: None,
2030 constraint: None,
2031 },
2032 FieldSchema {
2033 label: "Animation",
2034 expected_type: GffType::Int32,
2035 life: FieldLife::Live,
2036 required: false,
2037 absent: AbsentDefault::Unverified,
2038 children: None,
2039 constraint: None,
2040 },
2041 FieldSchema {
2042 label: "CreatnScrptFird",
2043 expected_type: GffType::UInt8,
2044 life: FieldLife::Live,
2045 required: false,
2046 absent: AbsentDefault::Unverified,
2047 children: None,
2048 constraint: None,
2049 },
2050 FieldSchema {
2051 label: "PM_IsDisguised",
2052 expected_type: GffType::UInt8,
2053 life: FieldLife::Live,
2054 required: false,
2055 absent: AbsentDefault::Unverified,
2056 children: None,
2057 constraint: None,
2058 },
2059 FieldSchema {
2060 label: "PM_Appearance",
2061 expected_type: GffType::UInt16,
2062 life: FieldLife::Live,
2063 required: false,
2064 absent: AbsentDefault::Unverified,
2065 children: None,
2066 constraint: None,
2067 },
2068 FieldSchema {
2069 label: "Listening",
2070 expected_type: GffType::UInt8,
2071 life: FieldLife::Live,
2072 required: false,
2073 absent: AbsentDefault::Unverified,
2074 children: None,
2075 constraint: None,
2076 },
2077 FieldSchema {
2078 label: "AreaId",
2079 expected_type: GffType::UInt32,
2080 life: FieldLife::Live,
2081 required: false,
2082 absent: AbsentDefault::Unverified,
2083 children: None,
2084 constraint: None,
2085 },
2086 FieldSchema {
2087 label: "DetectMode",
2088 expected_type: GffType::UInt8,
2089 life: FieldLife::Live,
2090 required: false,
2091 absent: AbsentDefault::Unverified,
2092 children: None,
2093 constraint: None,
2094 },
2095 FieldSchema {
2096 label: "StealthMode",
2097 expected_type: GffType::UInt8,
2098 life: FieldLife::Live,
2099 required: false,
2100 absent: AbsentDefault::Unverified,
2101 children: None,
2102 constraint: None,
2103 },
2104 FieldSchema {
2106 label: "ScriptHeartbeat",
2107 expected_type: GffType::ResRef,
2108 life: FieldLife::Live,
2109 required: false,
2110 absent: AbsentDefault::Constructed(DefaultValue::Text("default"), "utc.md: the creature constructor pre-arms all fourteen script slots with the literal `default` before any GFF read happens"),
2111 children: None,
2112 constraint: None,
2113 },
2114 FieldSchema {
2115 label: "ScriptOnNotice",
2116 expected_type: GffType::ResRef,
2117 life: FieldLife::Live,
2118 required: false,
2119 absent: AbsentDefault::Constructed(DefaultValue::Text("default"), "utc.md: the creature constructor pre-arms all fourteen script slots with the literal `default` before any GFF read happens"),
2120 children: None,
2121 constraint: None,
2122 },
2123 FieldSchema {
2124 label: "ScriptSpellAt",
2125 expected_type: GffType::ResRef,
2126 life: FieldLife::Live,
2127 required: false,
2128 absent: AbsentDefault::Constructed(DefaultValue::Text("default"), "utc.md: the creature constructor pre-arms all fourteen script slots with the literal `default` before any GFF read happens"),
2129 children: None,
2130 constraint: None,
2131 },
2132 FieldSchema {
2133 label: "ScriptAttacked",
2134 expected_type: GffType::ResRef,
2135 life: FieldLife::Live,
2136 required: false,
2137 absent: AbsentDefault::Constructed(DefaultValue::Text("default"), "utc.md: the creature constructor pre-arms all fourteen script slots with the literal `default` before any GFF read happens"),
2138 children: None,
2139 constraint: None,
2140 },
2141 FieldSchema {
2142 label: "ScriptDamaged",
2143 expected_type: GffType::ResRef,
2144 life: FieldLife::Live,
2145 required: false,
2146 absent: AbsentDefault::Constructed(DefaultValue::Text("default"), "utc.md: the creature constructor pre-arms all fourteen script slots with the literal `default` before any GFF read happens"),
2147 children: None,
2148 constraint: None,
2149 },
2150 FieldSchema {
2151 label: "ScriptDisturbed",
2152 expected_type: GffType::ResRef,
2153 life: FieldLife::Live,
2154 required: false,
2155 absent: AbsentDefault::Constructed(DefaultValue::Text("default"), "utc.md: the creature constructor pre-arms all fourteen script slots with the literal `default` before any GFF read happens"),
2156 children: None,
2157 constraint: None,
2158 },
2159 FieldSchema {
2160 label: "ScriptEndRound",
2161 expected_type: GffType::ResRef,
2162 life: FieldLife::Live,
2163 required: false,
2164 absent: AbsentDefault::Constructed(DefaultValue::Text("default"), "utc.md: the creature constructor pre-arms all fourteen script slots with the literal `default` before any GFF read happens"),
2165 children: None,
2166 constraint: None,
2167 },
2168 FieldSchema {
2169 label: "ScriptDialogue",
2170 expected_type: GffType::ResRef,
2171 life: FieldLife::Live,
2172 required: false,
2173 absent: AbsentDefault::Constructed(DefaultValue::Text("default"), "utc.md: the creature constructor pre-arms all fourteen script slots with the literal `default` before any GFF read happens"),
2174 children: None,
2175 constraint: None,
2176 },
2177 FieldSchema {
2178 label: "ScriptSpawn",
2179 expected_type: GffType::ResRef,
2180 life: FieldLife::Live,
2181 required: false,
2182 absent: AbsentDefault::Constructed(DefaultValue::Text("default"), "utc.md: the creature constructor pre-arms all fourteen script slots with the literal `default` before any GFF read happens"),
2183 children: None,
2184 constraint: None,
2185 },
2186 FieldSchema {
2187 label: "ScriptRested",
2188 expected_type: GffType::ResRef,
2189 life: FieldLife::Live,
2190 required: false,
2191 absent: AbsentDefault::Constructed(DefaultValue::Text("default"), "utc.md: the creature constructor pre-arms all fourteen script slots with the literal `default` before any GFF read happens"),
2192 children: None,
2193 constraint: None,
2194 },
2195 FieldSchema {
2196 label: "ScriptDeath",
2197 expected_type: GffType::ResRef,
2198 life: FieldLife::Live,
2199 required: false,
2200 absent: AbsentDefault::Constructed(DefaultValue::Text("default"), "utc.md: the creature constructor pre-arms all fourteen script slots with the literal `default` before any GFF read happens"),
2201 children: None,
2202 constraint: None,
2203 },
2204 FieldSchema {
2205 label: "ScriptUserDefine",
2206 expected_type: GffType::ResRef,
2207 life: FieldLife::Live,
2208 required: false,
2209 absent: AbsentDefault::Constructed(DefaultValue::Text("default"), "utc.md: the creature constructor pre-arms all fourteen script slots with the literal `default` before any GFF read happens"),
2210 children: None,
2211 constraint: None,
2212 },
2213 FieldSchema {
2214 label: "ScriptOnBlocked",
2215 expected_type: GffType::ResRef,
2216 life: FieldLife::Live,
2217 required: false,
2218 absent: AbsentDefault::Constructed(DefaultValue::Text("default"), "utc.md: the creature constructor pre-arms all fourteen script slots with the literal `default` before any GFF read happens"),
2219 children: None,
2220 constraint: None,
2221 },
2222 FieldSchema {
2223 label: "ScriptEndDialogu",
2224 expected_type: GffType::ResRef,
2225 life: FieldLife::Live,
2226 required: false,
2227 absent: AbsentDefault::Constructed(DefaultValue::Text("default"), "utc.md: the creature constructor pre-arms all fourteen script slots with the literal `default` before any GFF read happens"),
2228 children: None,
2229 constraint: None,
2230 },
2231 FieldSchema {
2233 label: "ClassList",
2234 expected_type: GffType::List,
2235 life: FieldLife::Live,
2236 required: false,
2237 absent: AbsentDefault::Unverified,
2238 children: Some(CLASS_LIST_CHILDREN),
2239 constraint: None,
2240 },
2241 FieldSchema {
2242 label: "FeatList",
2243 expected_type: GffType::List,
2244 life: FieldLife::Live,
2245 required: false,
2246 absent: AbsentDefault::Unverified,
2247 children: Some(FEAT_LIST_CHILDREN),
2248 constraint: None,
2249 },
2250 FieldSchema {
2251 label: "SkillList",
2252 expected_type: GffType::List,
2253 life: FieldLife::Live,
2254 required: false,
2255 absent: AbsentDefault::NotAConstant(
2256 "utc.md: an absent SkillList skips the block and leaves the eight skill ranks at whatever they held, eight zeroes on a fresh load, which is a structured value rather than one constant",
2257 ),
2258 children: Some(SKILL_LIST_CHILDREN),
2259 constraint: None,
2260 },
2261 FieldSchema {
2262 label: "Equip_ItemList",
2263 expected_type: GffType::List,
2264 life: FieldLife::Live,
2265 required: false,
2266 absent: AbsentDefault::Unverified,
2267 children: Some(EQUIP_ITEM_LIST_CHILDREN),
2268 constraint: None,
2269 },
2270 FieldSchema {
2271 label: "ItemList",
2272 expected_type: GffType::List,
2273 life: FieldLife::Live,
2274 required: false,
2275 absent: AbsentDefault::Unverified,
2276 children: Some(ITEM_LIST_CHILDREN),
2277 constraint: None,
2278 },
2279 FieldSchema {
2280 label: "SpecAbilityList",
2281 expected_type: GffType::List,
2282 life: FieldLife::Live,
2283 required: false,
2284 absent: AbsentDefault::Unverified,
2285 children: Some(SPEC_ABILITY_LIST_CHILDREN),
2286 constraint: None,
2287 },
2288 FieldSchema {
2289 label: "LvlStatList",
2290 expected_type: GffType::List,
2291 life: FieldLife::Live,
2292 required: false,
2293 absent: AbsentDefault::Unverified,
2294 children: None,
2295 constraint: None,
2296 },
2297 FieldSchema {
2299 label: "TemplateResRef",
2300 expected_type: GffType::ResRef,
2301 life: FieldLife::Live,
2302 required: false,
2303 absent: AbsentDefault::Unverified,
2304 children: None,
2305 constraint: None,
2306 },
2307 FieldSchema {
2308 label: "Comment",
2309 expected_type: GffType::String,
2310 life: FieldLife::Live,
2311 required: false,
2312 absent: AbsentDefault::Unverified,
2313 children: None,
2314 constraint: None,
2315 },
2316 FieldSchema {
2317 label: "PaletteID",
2318 expected_type: GffType::UInt8,
2319 life: FieldLife::Live,
2320 required: false,
2321 absent: AbsentDefault::Unverified,
2322 children: None,
2323 constraint: None,
2324 },
2325 FieldSchema {
2326 label: "SaveWill",
2327 expected_type: GffType::UInt8,
2328 life: FieldLife::Live,
2329 required: false,
2330 absent: AbsentDefault::Unverified,
2331 children: None,
2332 constraint: None,
2333 },
2334 FieldSchema {
2335 label: "SaveFortitude",
2336 expected_type: GffType::UInt8,
2337 life: FieldLife::Live,
2338 required: false,
2339 absent: AbsentDefault::Unverified,
2340 children: None,
2341 constraint: None,
2342 },
2343 FieldSchema {
2344 label: "BodyVariation",
2345 expected_type: GffType::UInt8,
2346 life: FieldLife::Live,
2347 required: false,
2348 absent: AbsentDefault::Unverified,
2349 children: None,
2350 constraint: None,
2351 },
2352 FieldSchema {
2353 label: "TextureVar",
2354 expected_type: GffType::UInt8,
2355 life: FieldLife::Live,
2356 required: false,
2357 absent: AbsentDefault::Unverified,
2358 children: None,
2359 constraint: None,
2360 },
2361 FieldSchema {
2362 label: "Morale",
2363 expected_type: GffType::UInt8,
2364 life: FieldLife::Live,
2365 required: false,
2366 absent: AbsentDefault::Unverified,
2367 children: None,
2368 constraint: None,
2369 },
2370 FieldSchema {
2371 label: "MoraleRecovery",
2372 expected_type: GffType::UInt8,
2373 life: FieldLife::Live,
2374 required: false,
2375 absent: AbsentDefault::Unverified,
2376 children: None,
2377 constraint: None,
2378 },
2379 FieldSchema {
2380 label: "MoraleBreakpoint",
2381 expected_type: GffType::UInt8,
2382 life: FieldLife::Live,
2383 required: false,
2384 absent: AbsentDefault::Unverified,
2385 children: None,
2386 constraint: None,
2387 },
2388 FieldSchema {
2389 label: "BlindSpot",
2390 expected_type: GffType::Single,
2391 life: FieldLife::Live,
2392 required: false,
2393 absent: AbsentDefault::Unverified,
2394 children: None,
2395 constraint: None,
2396 },
2397 FieldSchema {
2398 label: "MultiplierSet",
2399 expected_type: GffType::UInt8,
2400 life: FieldLife::Live,
2401 required: false,
2402 absent: AbsentDefault::Unverified,
2403 children: None,
2404 constraint: None,
2405 },
2406 FieldSchema {
2407 label: "NoPermDeath",
2408 expected_type: GffType::UInt8,
2409 life: FieldLife::Live,
2410 required: false,
2411 absent: AbsentDefault::Unverified,
2412 children: None,
2413 constraint: None,
2414 },
2415 FieldSchema {
2416 label: "IgnoreCrePath",
2417 expected_type: GffType::UInt8,
2418 life: FieldLife::Live,
2419 required: false,
2420 absent: AbsentDefault::Unverified,
2421 children: None,
2422 constraint: None,
2423 },
2424 FieldSchema {
2425 label: "Hologram",
2426 expected_type: GffType::UInt8,
2427 life: FieldLife::Live,
2428 required: false,
2429 absent: AbsentDefault::Unverified,
2430 children: None,
2431 constraint: None,
2432 },
2433 FieldSchema {
2434 label: "WillNotRender",
2435 expected_type: GffType::UInt8,
2436 life: FieldLife::Live,
2437 required: false,
2438 absent: AbsentDefault::Unverified,
2439 children: None,
2440 constraint: None,
2441 },
2442 FieldSchema {
2443 label: "LawfulChaotic",
2444 expected_type: GffType::UInt8,
2445 life: FieldLife::Live,
2446 required: false,
2447 absent: AbsentDefault::Unverified,
2448 children: None,
2449 constraint: None,
2450 },
2451 ];
2452 SCHEMA
2453 }
2454}
2455
2456#[cfg(test)]
2457mod tests {
2458 use super::*;
2459
2460 const TEST_UTC: &[u8] = include_bytes!(concat!(
2461 env!("CARGO_MANIFEST_DIR"),
2462 "/../../fixtures/test.utc"
2463 ));
2464
2465 #[test]
2466 fn reads_core_utc_fields_from_fixture() {
2467 let utc = read_utc_from_bytes(TEST_UTC).expect("fixture must parse");
2468
2469 assert_eq!(utc.template_resref, "n_minecoorta");
2470 assert_eq!(utc.tag, "Coorta");
2471 assert_eq!(utc.comment, "comment");
2472 assert_eq!(utc.conversation, "coorta");
2473
2474 assert_eq!(utc.first_name.string_ref.raw(), 76_046);
2475 assert_eq!(utc.last_name.string_ref.raw(), 123);
2476
2477 assert_eq!(utc.age, 25);
2478 assert_eq!(utc.starting_package, 3);
2479 assert_eq!(utc.gold, 500);
2480 assert!(utc.invulnerable);
2481 assert_eq!(utc.experience, 1200);
2482 assert_eq!(utc.color_skin, 2);
2483 assert_eq!(utc.color_hair, 4);
2484 assert_eq!(utc.color_tattoo1, 1);
2485 assert_eq!(utc.color_tattoo2, 3);
2486 assert_eq!(utc.appearance_head, 5);
2487 assert_eq!(utc.duplicating_head, 0);
2488 assert_eq!(utc.use_backup_head, 0);
2489 assert_eq!(utc.ai_state, 100);
2490 assert_eq!(utc.skill_points, 8);
2491 assert_eq!(utc.movement_rate, 7);
2492
2493 assert_eq!(utc.appearance_id, 636);
2494 assert_eq!(utc.gender_id, 2);
2495 assert_eq!(utc.race_id, 6);
2496 assert_eq!(utc.faction_id, 5);
2497 assert_eq!(utc.perception_id, 11);
2498 assert_eq!(utc.walkrate_id, 7);
2499 assert_eq!(utc.soundset_id, 46);
2500 assert_eq!(utc.portrait_id, 1);
2501 assert!(utc.portrait_resref.is_empty());
2502 assert_eq!(utc.save_will, 0);
2503 assert_eq!(utc.save_fortitude, 0);
2504 assert_eq!(utc.morale, 0);
2505 assert_eq!(utc.morale_recovery, 0);
2506 assert_eq!(utc.morale_breakpoint, 0);
2507 assert_eq!(utc.description.string_ref.raw(), 123);
2508 assert_eq!(utc.lawfulness, 0);
2509 assert_eq!(utc.phenotype_id, 0);
2510 assert!(utc.deity.is_empty());
2511 assert!(utc.subrace_name.is_empty());
2512
2513 assert_eq!(utc.alignment, 50);
2514 assert!((utc.challenge_rating - 1.0).abs() < f32::EPSILON);
2515 assert!((utc.blindspot - 120.0).abs() < f32::EPSILON);
2516 assert_eq!(utc.natural_ac, 1);
2517 assert_eq!(utc.reflex_bonus, 1);
2518 assert_eq!(utc.willpower_bonus, 1);
2519 assert_eq!(utc.fortitude_bonus, 1);
2520
2521 assert_eq!(utc.strength, 10);
2522 assert_eq!(utc.dexterity, 10);
2523 assert_eq!(utc.constitution, 10);
2524 assert_eq!(utc.intelligence, 10);
2525 assert_eq!(utc.wisdom, 10);
2526 assert_eq!(utc.charisma, 10);
2527
2528 assert_eq!(utc.current_hp, 8);
2529 assert_eq!(utc.max_hp, 8);
2530 assert_eq!(utc.hp, 8);
2531 assert_eq!(utc.fp, 1);
2532 assert_eq!(utc.max_fp, 1);
2533
2534 assert!(utc.not_reorienting);
2535 assert!(utc.party_interact);
2536 assert!(utc.no_perm_death);
2537 assert!(utc.min1_hp);
2538 assert!(utc.plot);
2539 assert!(utc.interruptable);
2540 assert!(utc.is_pc);
2541 assert!(utc.disarmable);
2542 assert!(utc.ignore_cre_path);
2543 assert!(utc.hologram);
2544
2545 assert_eq!(utc.on_attacked, "k_def_attacked01");
2546 assert_eq!(utc.on_damaged, "k_def_damage01");
2547 assert_eq!(utc.on_death, "k_def_death01");
2548 assert_eq!(utc.on_dialog, "k_def_dialogue01");
2549 assert_eq!(utc.on_disturbed, "k_def_disturb01");
2550 assert_eq!(utc.on_end_dialog, "k_def_endconv");
2551 assert_eq!(utc.on_end_round, "k_def_combend01");
2552 assert_eq!(utc.on_heartbeat, "k_def_heartbt01");
2553 assert_eq!(utc.on_blocked, "k_def_blocked01");
2554 assert_eq!(utc.on_notice, "k_def_percept01");
2555 assert_eq!(utc.on_spawn, "k_def_spawn01");
2556 assert_eq!(utc.on_spell, "k_def_spellat01");
2557 assert_eq!(utc.on_user_defined, "k_def_userdef01");
2558
2559 assert_eq!(utc.skills.computer_use, 1);
2560 assert_eq!(utc.skills.demolitions, 2);
2561 assert_eq!(utc.skills.stealth, 3);
2562 assert_eq!(utc.skills.awareness, 4);
2563 assert_eq!(utc.skills.persuade, 5);
2564 assert_eq!(utc.skills.repair, 6);
2565 assert_eq!(utc.skills.security, 7);
2566 assert_eq!(utc.skills.treat_injury, 8);
2567
2568 assert_eq!(utc.classes.len(), 2);
2569 let scout_class = utc
2570 .classes
2571 .iter()
2572 .find(|class| class.class_id == 1 && class.class_level == 3)
2573 .expect("fixture should contain class_id=1 level=3");
2574 assert_eq!(scout_class.powers, vec![9, 11]);
2575 assert!(utc.special_abilities.is_empty());
2576
2577 assert_eq!(utc.feats, vec![93, 94]);
2578
2579 assert_eq!(utc.equipment.len(), 2);
2580 assert_eq!(utc.equipment[0].slot_id, 2);
2581 assert_eq!(utc.equipment[0].resref, "mineruniform");
2582 assert!(utc.equipment[0].droppable);
2583 assert_eq!(utc.equipment[1].slot_id, 131_072);
2584 assert_eq!(utc.equipment[1].resref, "g_i_crhide008");
2585 assert!(!utc.equipment[1].droppable);
2586
2587 assert_eq!(utc.inventory.len(), 4);
2588 assert_eq!(utc.inventory[0].entry_id, 0);
2589 assert_eq!(utc.inventory[0].resref, "g_w_thermldet01");
2590 assert!(utc.inventory[0].droppable);
2591 assert_eq!(utc.inventory[1].entry_id, 1);
2592 assert_eq!(utc.inventory[1].resref, "g_w_thermldet01");
2593 assert!(!utc.inventory[1].droppable);
2594 assert_eq!(utc.inventory[3].resref, "g_w_thermldet02");
2595 assert_eq!(utc.inventory[3].repos_pos_x, Some(3));
2596 assert_eq!(utc.inventory[3].repos_pos_y, Some(0));
2597 }
2598
2599 #[test]
2600 fn all_fields_survive_typed_roundtrip() {
2601 let utc = read_utc_from_bytes(TEST_UTC).expect("fixture must parse");
2602 let encoded = write_utc_to_vec(&utc).expect("encode must succeed");
2603 let reparsed = read_utc_from_bytes(&encoded).expect("decode must succeed");
2604 assert_eq!(utc, reparsed);
2605 }
2606
2607 #[test]
2608 fn typed_edits_roundtrip_through_gff_writer() {
2609 let mut utc = read_utc_from_bytes(TEST_UTC).expect("fixture must parse");
2610 utc.tag = "Coorta_Mod".into();
2611 utc.on_spawn = ResRef::new("k_new_spawn").expect("valid test resref");
2612 utc.skills.persuade = 12;
2613 utc.portrait_resref = ResRef::new("po_pfha01").expect("valid test resref");
2614 utc.save_will = 11;
2615 utc.save_fortitude = 9;
2616 utc.morale = 8;
2617 utc.morale_recovery = 7;
2618 utc.morale_breakpoint = 6;
2619 utc.description = GffLocalizedString::new(StrRef::from_raw(42_424));
2620 utc.lawfulness = 35;
2621 utc.phenotype_id = 2;
2622 utc.deity = "The Force".into();
2623 utc.subrace_name = "Miner".into();
2624 utc.feats = vec![94, 93, 120];
2625 utc.classes[0].powers = vec![9, 12, 15];
2626 utc.special_abilities = vec![UtcSpecialAbility {
2627 spell_id: 321,
2628 spell_flags: 0b11,
2629 spell_caster_level: 7,
2630 }];
2631 utc.equipment[0].resref = ResRef::new("g_a_class4001").expect("valid test resref");
2632 utc.equipment[0].droppable = false;
2633 utc.inventory[0].resref = ResRef::new("g_w_blstrrfl01").expect("valid test resref");
2634 utc.inventory.push(UtcInventoryItem {
2635 entry_id: 4,
2636 resref: ResRef::new("g_i_progspike01").expect("valid test resref"),
2637 droppable: true,
2638 repos_pos_x: Some(4),
2639 repos_pos_y: Some(0),
2640 });
2641
2642 let encoded = write_utc_to_vec(&utc).expect("encode");
2643 let reparsed = read_utc_from_bytes(&encoded).expect("decode");
2644
2645 assert_eq!(reparsed.tag, "Coorta_Mod");
2646 assert_eq!(reparsed.on_spawn, "k_new_spawn");
2647 assert_eq!(reparsed.skills.persuade, 12);
2648 assert_eq!(reparsed.portrait_resref, "po_pfha01");
2649 assert_eq!(reparsed.save_will, 11);
2650 assert_eq!(reparsed.save_fortitude, 9);
2651 assert_eq!(reparsed.morale, 8);
2652 assert_eq!(reparsed.morale_recovery, 7);
2653 assert_eq!(reparsed.morale_breakpoint, 6);
2654 assert_eq!(reparsed.description.string_ref.raw(), 42_424);
2655 assert_eq!(reparsed.lawfulness, 35);
2656 assert_eq!(reparsed.phenotype_id, 2);
2657 assert_eq!(reparsed.deity, "The Force");
2658 assert_eq!(reparsed.subrace_name, "Miner");
2659 assert_eq!(reparsed.feats, vec![94, 93, 120]);
2660 assert_eq!(reparsed.classes[0].powers, vec![9, 12, 15]);
2661 assert_eq!(reparsed.special_abilities.len(), 1);
2662 assert_eq!(reparsed.special_abilities[0].spell_id, 321);
2663 assert_eq!(reparsed.special_abilities[0].spell_flags, 0b11);
2664 assert_eq!(reparsed.special_abilities[0].spell_caster_level, 7);
2665 assert_eq!(reparsed.equipment[0].resref, "g_a_class4001");
2666 assert!(!reparsed.equipment[0].droppable);
2667 assert_eq!(reparsed.inventory[0].resref, "g_w_blstrrfl01");
2668 assert_eq!(reparsed.inventory.len(), 5);
2669 assert_eq!(reparsed.inventory[4].entry_id, 4);
2670 assert_eq!(reparsed.inventory[4].resref, "g_i_progspike01");
2671 assert!(reparsed.inventory[4].droppable);
2672 }
2673
2674 #[test]
2675 fn no_op_rebuild_preserves_list_order_and_struct_ids() {
2676 let utc = read_utc_from_bytes(TEST_UTC).expect("fixture must parse");
2677 let rebuilt = utc.to_gff();
2678
2679 assert_eq!(
2680 list_struct_ids(find_list(&rebuilt.root, "FeatList")),
2681 vec![1, 1]
2682 );
2683 assert_eq!(
2684 list_struct_ids(find_list(&rebuilt.root, "Equip_ItemList")),
2685 vec![2, 131_072]
2686 );
2687 assert_eq!(
2688 list_struct_ids(find_list(&rebuilt.root, "ItemList")),
2689 vec![0, 1, 2, 3]
2690 );
2691 assert_eq!(
2692 list_u16_field(find_list(&rebuilt.root, "FeatList"), "Feat"),
2693 vec![93, 94]
2694 );
2695 }
2696
2697 #[test]
2698 fn rejects_non_utc_file_type() {
2699 let gff = Gff::new(*b"UTI ", GffStruct::new(-1));
2700 let err = Utc::from_gff(&gff).expect_err("must fail");
2701 assert!(matches!(err, UtcError::UnsupportedFileType(file_type) if file_type == *b"UTI "));
2702 }
2703
2704 #[test]
2705 fn read_utc_from_reader_matches_bytes_path() {
2706 let mut cursor = Cursor::new(TEST_UTC);
2707 let via_reader = read_utc(&mut cursor).expect("reader parse");
2708 let via_bytes = read_utc_from_bytes(TEST_UTC).expect("bytes parse");
2709 assert_eq!(via_reader.template_resref, via_bytes.template_resref);
2710 assert_eq!(via_reader.classes.len(), via_bytes.classes.len());
2711 }
2712
2713 #[test]
2714 fn type_mismatch_on_class_list_is_error() {
2715 let mut root = GffStruct::new(-1);
2716 root.push_field("ClassList", GffValue::UInt32(7));
2717 let gff = Gff::new(*b"UTC ", root);
2718 let err = Utc::from_gff(&gff).expect_err("must fail");
2719 assert!(matches!(
2720 err,
2721 UtcError::TypeMismatch {
2722 field: "ClassList",
2723 expected: "List"
2724 }
2725 ));
2726 }
2727
2728 #[test]
2729 fn type_mismatch_on_spec_ability_list_is_error() {
2730 let mut root = GffStruct::new(-1);
2731 root.push_field("SpecAbilityList", GffValue::UInt32(7));
2732 let gff = Gff::new(*b"UTC ", root);
2733 let err = Utc::from_gff(&gff).expect_err("must fail");
2734 assert!(matches!(
2735 err,
2736 UtcError::TypeMismatch {
2737 field: "SpecAbilityList",
2738 expected: "List"
2739 }
2740 ));
2741 }
2742
2743 #[test]
2744 fn write_utc_matches_direct_gff_writer() {
2745 let utc = read_utc_from_bytes(TEST_UTC).expect("fixture parse");
2746 let from_utc = write_utc_to_vec(&utc).expect("utc encode");
2747
2748 let gff = utc.to_gff();
2749 let from_gff = rakata_formats::write_gff_to_vec(&gff).expect("gff encode");
2750 assert_eq!(from_utc, from_gff);
2751 }
2752
2753 fn find_list<'a>(structure: &'a GffStruct, label: &str) -> &'a [GffStruct] {
2754 match structure.field(label) {
2755 Some(GffValue::List(values)) => values.as_slice(),
2756 Some(other) => panic!("field {label} is not a list: {other:?}"),
2757 None => panic!("missing list field {label}"),
2758 }
2759 }
2760
2761 fn list_struct_ids(list: &[GffStruct]) -> Vec<i32> {
2762 list.iter().map(|entry| entry.struct_id).collect::<Vec<_>>()
2763 }
2764
2765 fn list_u16_field(list: &[GffStruct], label: &str) -> Vec<u16> {
2766 list.iter()
2767 .map(|entry| match entry.field(label) {
2768 Some(GffValue::UInt16(value)) => *value,
2769 Some(other) => panic!("field {label} is not UInt16: {other:?}"),
2770 None => panic!("missing field {label}"),
2771 })
2772 .collect::<Vec<_>>()
2773 }
2774
2775 #[test]
2776 fn schema_field_count() {
2777 assert_eq!(Utc::schema().len(), 109);
2778 }
2779
2780 #[test]
2781 fn schema_no_duplicate_labels() {
2782 let schema = Utc::schema();
2783 let mut labels: Vec<&str> = schema.iter().map(|f| f.label).collect();
2784 labels.sort();
2785 let before = labels.len();
2786 labels.dedup();
2787 assert_eq!(before, labels.len(), "duplicate labels in UTC schema");
2788 }
2789
2790 #[test]
2791 fn schema_has_expected_list_children() {
2792 let schema = Utc::schema();
2793 let expected: &[(&str, usize)] = &[
2794 ("ClassList", 4),
2795 ("FeatList", 1),
2796 ("SkillList", 1),
2797 ("SpecAbilityList", 3),
2798 ("ItemList", 7),
2799 ("Equip_ItemList", 3),
2800 ];
2801 for (label, child_count) in expected {
2802 let field = schema
2803 .iter()
2804 .find(|f| f.label == *label)
2805 .unwrap_or_else(|| panic!("missing list field {label}"));
2806 let children = field
2807 .children
2808 .unwrap_or_else(|| panic!("{label} should have children"));
2809 assert_eq!(
2810 children.len(),
2811 *child_count,
2812 "{label} children count mismatch"
2813 );
2814 }
2815 }
2816}
2817
2818#[cfg(test)]
2819mod write_coverage_tests {
2820 use super::*;
2821 use rakata_formats::gff_schema::GffType;
2822
2823 fn distinctive(kind: GffType) -> Option<GffValue> {
2826 Some(match kind {
2827 GffType::UInt8 => GffValue::UInt8(7),
2828 GffType::Int8 => GffValue::Int8(7),
2829 GffType::UInt16 => GffValue::UInt16(7),
2830 GffType::Int16 => GffValue::Int16(7),
2831 GffType::UInt32 => GffValue::UInt32(7),
2832 GffType::Int32 => GffValue::Int32(7),
2833 GffType::Single => GffValue::Single(7.5),
2834 GffType::String => GffValue::String("seven".to_string()),
2835 GffType::ResRef => GffValue::ResRef(ResRef::new("seven").expect("valid test resref")),
2836 GffType::LocalizedString => {
2837 GffValue::LocalizedString(GffLocalizedString::new(StrRef::from_raw(7)))
2838 }
2839 _ => return None,
2842 })
2843 }
2844
2845 #[test]
2846 fn every_schema_label_is_a_valid_gff_label() {
2847 let invalid: Vec<&str> = Utc::schema()
2852 .iter()
2853 .filter(|field| rakata_formats::gff::GffLabel::new(field.label).is_err())
2854 .map(|field| field.label)
2855 .collect();
2856
2857 assert!(
2858 invalid.is_empty(),
2859 "schema labels no GFF can hold: {invalid:?}"
2860 );
2861 }
2862
2863 #[test]
2864 fn a_field_that_is_read_is_also_written() {
2865 let mut root = GffStruct::new(-1);
2873 for field in Utc::schema() {
2874 if let Some(value) = distinctive(field.expected_type) {
2875 upsert_field(&mut root, field.label, value);
2876 }
2877 }
2878
2879 let first = Utc::from_gff(&Gff::new(*b"UTC ", root)).expect("populated UTC parses");
2880 let second = Utc::from_gff(&first.to_gff()).expect("rewritten UTC parses");
2881
2882 assert_eq!(
2883 first, second,
2884 "a field survived the read but was lost on write"
2885 );
2886 }
2887}