1use rakata_core::ResRef;
4use rakata_formats::{
5 gff_schema::{AbsentDefault, FieldLife, FieldSchema, GffType},
6 GffLocalizedString, GffStruct, GffValue,
7};
8
9use super::item::SavedItem;
10use super::object_id_or_invalid;
11use crate::gff_helpers::{
12 get_f32, get_i16, get_i32, get_i8, get_locstring, get_resref, get_string, get_u16, get_u32,
13 get_u8, upsert_field,
14};
15use crate::shared::ObjectId;
16use crate::utc::UtcSkills;
17
18#[derive(Debug, Clone, PartialEq, Default)]
20pub struct GitCreature {
21 pub template_resref: ResRef,
23 pub x_position: f32,
25 pub y_position: f32,
27 pub z_position: f32,
29 pub x_orientation: f32,
31 pub y_orientation: f32,
33 pub z_orientation: f32,
35}
36
37impl GitCreature {
55 pub(crate) fn from_gff_struct(s: &GffStruct) -> Self {
56 Self {
57 template_resref: get_resref(s, "TemplateResRef").unwrap_or_default(),
58 x_position: get_f32(s, "XPosition").unwrap_or(0.0),
59 y_position: get_f32(s, "YPosition").unwrap_or(0.0),
60 z_position: get_f32(s, "ZPosition").unwrap_or(0.0),
61 x_orientation: get_f32(s, "XOrientation").unwrap_or(0.0),
62 y_orientation: get_f32(s, "YOrientation").unwrap_or(0.0),
63 z_orientation: get_f32(s, "ZOrientation").unwrap_or(0.0),
64 }
65 }
66
67 pub(crate) fn to_gff_struct(&self) -> GffStruct {
68 let mut s = GffStruct::new(4);
69 upsert_field(
70 &mut s,
71 "TemplateResRef",
72 GffValue::ResRef(self.template_resref),
73 );
74 upsert_field(&mut s, "XPosition", GffValue::Single(self.x_position));
75 upsert_field(&mut s, "YPosition", GffValue::Single(self.y_position));
76 upsert_field(&mut s, "ZPosition", GffValue::Single(self.z_position));
77 upsert_field(&mut s, "XOrientation", GffValue::Single(self.x_orientation));
78 upsert_field(&mut s, "YOrientation", GffValue::Single(self.y_orientation));
79 upsert_field(&mut s, "ZOrientation", GffValue::Single(self.z_orientation));
80 s
81 }
82}
83
84pub(crate) static CREATURE_LIST_CHILDREN: &[FieldSchema] = &[
86 FieldSchema {
87 label: "ObjectId",
88 expected_type: GffType::UInt32,
89 life: FieldLife::Live,
90 required: false,
91 absent: AbsentDefault::Unverified,
92 children: None,
93 constraint: None,
94 },
95 FieldSchema {
96 label: "TemplateResRef",
97 expected_type: GffType::ResRef,
98 life: FieldLife::Live,
99 required: false,
100 absent: AbsentDefault::Unverified,
101 children: None,
102 constraint: None,
103 },
104 FieldSchema {
105 label: "XPosition",
106 expected_type: GffType::Single,
107 life: FieldLife::Live,
108 required: false,
109 absent: AbsentDefault::Unverified,
110 children: None,
111 constraint: None,
112 },
113 FieldSchema {
114 label: "YPosition",
115 expected_type: GffType::Single,
116 life: FieldLife::Live,
117 required: false,
118 absent: AbsentDefault::Unverified,
119 children: None,
120 constraint: None,
121 },
122 FieldSchema {
123 label: "ZPosition",
124 expected_type: GffType::Single,
125 life: FieldLife::Live,
126 required: false,
127 absent: AbsentDefault::Unverified,
128 children: None,
129 constraint: None,
130 },
131 FieldSchema {
132 label: "XOrientation",
133 expected_type: GffType::Single,
134 life: FieldLife::Live,
135 required: false,
136 absent: AbsentDefault::Unverified,
137 children: None,
138 constraint: None,
139 },
140 FieldSchema {
141 label: "YOrientation",
142 expected_type: GffType::Single,
143 life: FieldLife::Live,
144 required: false,
145 absent: AbsentDefault::Unverified,
146 children: None,
147 constraint: None,
148 },
149 FieldSchema {
150 label: "ZOrientation",
151 expected_type: GffType::Single,
152 life: FieldLife::Live,
153 required: false,
154 absent: AbsentDefault::Unverified,
155 children: None,
156 constraint: None,
157 },
158];
159
160#[derive(Debug, Clone, PartialEq, Default)]
216pub struct SavedCreature {
217 pub tag: String,
220 pub first_name: GffLocalizedString,
222 pub last_name: GffLocalizedString,
224 pub description: GffLocalizedString,
226 pub object_id: ObjectId,
228 pub area_id: u32,
230 pub conversation: ResRef,
232
233 pub x_position: f32,
236 pub y_position: f32,
238 pub z_position: f32,
240 pub x_orientation: f32,
242 pub y_orientation: f32,
244 pub z_orientation: f32,
246
247 pub appearance_type: u16,
250 pub appearance_head: u8,
252 pub portrait_id: u16,
255 pub phenotype: i32,
257 pub color_skin: u8,
259 pub color_hair: u8,
261 pub color_tattoo1: u8,
263 pub color_tattoo2: u8,
265 pub duplicating_head: u8,
267 pub use_backup_head: u8,
269 pub is_disguised: u8,
271 pub body_bag: u8,
273
274 pub strength: u8,
277 pub dexterity: u8,
279 pub constitution: u8,
281 pub intelligence: u8,
283 pub wisdom: u8,
285 pub charisma: u8,
287
288 pub current_hit_points: i16,
291 pub hit_points: i16,
293 pub max_hit_points: i16,
297 pub pregame_current: i16,
300 pub force_points: i16,
302 pub current_force: i16,
304 pub max_force_points: i16,
306
307 pub armor_class: i16,
311 pub ref_save_throw: i8,
313 pub will_save_throw: i8,
315 pub fort_save_throw: i8,
317 pub ref_bonus: i16,
320 pub will_bonus: i16,
322 pub fort_bonus: i16,
324 pub natural_ac: u8,
326
327 pub experience: u32,
330 pub gold: u32,
333 pub skill_points: u16,
335 pub multiclass_level_up_in: u8,
338 pub joining_xp: i32,
341 pub starting_package: u8,
343 pub challenge_rating: f32,
345 pub age: i32,
347
348 pub race: u8,
351 pub subrace: String,
353 pub subrace_index: u8,
355 pub deity: String,
357 pub gender: u8,
359 pub good_evil: u8,
362 pub faction_id: u16,
364 pub sound_set_file: u16,
366
367 pub ai_state: i32,
370 pub not_reorienting: u8,
372 pub detect_mode: u8,
375 pub stealth_mode: u8,
377 pub commandable: u8,
379 pub listening: u8,
381 pub party_interact: u8,
383 pub plot: u8,
385 pub min1_hp: u8,
387 pub interruptable: u8,
389 pub disarmable: u8,
391 pub is_destroyable: u8,
393 pub is_raiseable: u8,
395 pub dead_selectable: u8,
397 pub is_pc: u8,
399 pub creation_script_fired: u8,
401 pub ambient_anim_state: u8,
403 pub animation: i32,
405 pub creature_size: i32,
407 pub movement_rate: u8,
409
410 pub classes: Vec<SavedClass>,
413 pub feats: Vec<u16>,
415 pub skills: UtcSkills,
417 pub inventory: Vec<SavedItem>,
419 pub equipment: Vec<SavedItem>,
421
422 pub tail: u8,
425 pub wings: u8,
427
428 pub on_attacked: ResRef,
431 pub on_damaged: ResRef,
433 pub on_death: ResRef,
435 pub on_dialog: ResRef,
437 pub on_disturbed: ResRef,
439 pub on_end_dialog: ResRef,
441 pub on_end_round: ResRef,
443 pub on_heartbeat: ResRef,
445 pub on_blocked: ResRef,
447 pub on_notice: ResRef,
449 pub on_rested: ResRef,
451 pub on_spawn: ResRef,
453 pub on_spell: ResRef,
455 pub on_user_defined: ResRef,
457}
458
459impl SavedCreature {
460 pub fn new() -> Self {
462 Self::default()
463 }
464
465 pub fn from_struct(structure: &GffStruct) -> Self {
471 Self {
472 tag: get_string(structure, "Tag").unwrap_or_default(),
473 first_name: get_locstring(structure, "FirstName")
474 .cloned()
475 .unwrap_or_default(),
476 last_name: get_locstring(structure, "LastName")
477 .cloned()
478 .unwrap_or_default(),
479 description: get_locstring(structure, "Description")
480 .cloned()
481 .unwrap_or_default(),
482 object_id: object_id_or_invalid(structure),
483 area_id: get_u32(structure, "AreaId").unwrap_or(0),
484 conversation: get_resref(structure, "Conversation").unwrap_or_default(),
485
486 x_position: get_f32(structure, "XPosition").unwrap_or(0.0),
487 y_position: get_f32(structure, "YPosition").unwrap_or(0.0),
488 z_position: get_f32(structure, "ZPosition").unwrap_or(0.0),
489 x_orientation: get_f32(structure, "XOrientation").unwrap_or(0.0),
490 y_orientation: get_f32(structure, "YOrientation").unwrap_or(0.0),
491 z_orientation: get_f32(structure, "ZOrientation").unwrap_or(0.0),
492
493 appearance_type: get_u16(structure, "Appearance_Type").unwrap_or(0),
494 appearance_head: get_u8(structure, "Appearance_Head").unwrap_or(0),
495 portrait_id: get_u16(structure, "PortraitId").unwrap_or(0),
496 phenotype: get_i32(structure, "Phenotype").unwrap_or(0),
497 color_skin: get_u8(structure, "Color_Skin").unwrap_or(0),
498 color_hair: get_u8(structure, "Color_Hair").unwrap_or(0),
499 color_tattoo1: get_u8(structure, "Color_Tattoo1").unwrap_or(0),
500 color_tattoo2: get_u8(structure, "Color_Tattoo2").unwrap_or(0),
501 duplicating_head: get_u8(structure, "DuplicatingHead").unwrap_or(0),
502 use_backup_head: get_u8(structure, "UseBackupHead").unwrap_or(0),
503 is_disguised: get_u8(structure, "PM_IsDisguised").unwrap_or(0),
504 body_bag: get_u8(structure, "BodyBag").unwrap_or(0),
505
506 strength: get_u8(structure, "Str").unwrap_or(0),
507 dexterity: get_u8(structure, "Dex").unwrap_or(0),
508 constitution: get_u8(structure, "Con").unwrap_or(0),
509 intelligence: get_u8(structure, "Int").unwrap_or(0),
510 wisdom: get_u8(structure, "Wis").unwrap_or(0),
511 charisma: get_u8(structure, "Cha").unwrap_or(0),
512
513 current_hit_points: get_i16(structure, "CurrentHitPoints").unwrap_or(0),
514 hit_points: get_i16(structure, "HitPoints").unwrap_or(0),
515 max_hit_points: get_i16(structure, "MaxHitPoints").unwrap_or(0),
516 pregame_current: get_i16(structure, "PregameCurrent").unwrap_or(0),
517 force_points: get_i16(structure, "ForcePoints").unwrap_or(0),
518 current_force: get_i16(structure, "CurrentForce").unwrap_or(0),
519 max_force_points: get_i16(structure, "MaxForcePoints").unwrap_or(0),
520
521 armor_class: get_i16(structure, "ArmorClass").unwrap_or(0),
522 ref_save_throw: get_i8(structure, "RefSaveThrow").unwrap_or(0),
523 will_save_throw: get_i8(structure, "WillSaveThrow").unwrap_or(0),
524 fort_save_throw: get_i8(structure, "FortSaveThrow").unwrap_or(0),
525 ref_bonus: get_i16(structure, "refbonus").unwrap_or(0),
526 will_bonus: get_i16(structure, "willbonus").unwrap_or(0),
527 fort_bonus: get_i16(structure, "fortbonus").unwrap_or(0),
528 natural_ac: get_u8(structure, "NaturalAC").unwrap_or(0),
529
530 experience: get_u32(structure, "Experience").unwrap_or(0),
531 gold: get_u32(structure, "Gold").unwrap_or(0),
532 skill_points: get_u16(structure, "SkillPoints").unwrap_or(0),
533 multiclass_level_up_in: get_u8(structure, "MClassLevUpIn").unwrap_or(0),
534 joining_xp: get_i32(structure, "JoiningXP").unwrap_or(0),
535 starting_package: get_u8(structure, "StartingPackage").unwrap_or(0),
536 challenge_rating: get_f32(structure, "ChallengeRating").unwrap_or(0.0),
537 age: get_i32(structure, "Age").unwrap_or(0),
538
539 race: get_u8(structure, "Race").unwrap_or(0),
540 subrace: get_string(structure, "Subrace").unwrap_or_default(),
541 subrace_index: get_u8(structure, "SubraceIndex").unwrap_or(0),
542 deity: get_string(structure, "Deity").unwrap_or_default(),
543 gender: get_u8(structure, "Gender").unwrap_or(0),
544 good_evil: get_u8(structure, "GoodEvil").unwrap_or(0),
545 faction_id: get_u16(structure, "FactionID").unwrap_or(0),
546 sound_set_file: get_u16(structure, "SoundSetFile").unwrap_or(0),
547
548 ai_state: get_i32(structure, "AIState").unwrap_or(0),
549 not_reorienting: get_u8(structure, "NotReorienting").unwrap_or(0),
550 detect_mode: get_u8(structure, "DetectMode").unwrap_or(0),
551 stealth_mode: get_u8(structure, "StealthMode").unwrap_or(0),
552 commandable: get_u8(structure, "Commandable").unwrap_or(0),
553 listening: get_u8(structure, "Listening").unwrap_or(0),
554 party_interact: get_u8(structure, "PartyInteract").unwrap_or(0),
555 plot: get_u8(structure, "Plot").unwrap_or(0),
556 min1_hp: get_u8(structure, "Min1HP").unwrap_or(0),
557 interruptable: get_u8(structure, "Interruptable").unwrap_or(0),
558 disarmable: get_u8(structure, "Disarmable").unwrap_or(0),
559 is_destroyable: get_u8(structure, "IsDestroyable").unwrap_or(0),
560 is_raiseable: get_u8(structure, "IsRaiseable").unwrap_or(0),
561 dead_selectable: get_u8(structure, "DeadSelectable").unwrap_or(0),
562 is_pc: get_u8(structure, "IsPC").unwrap_or(0),
563 creation_script_fired: get_u8(structure, "CreatnScrptFird").unwrap_or(0),
564 ambient_anim_state: get_u8(structure, "AmbientAnimState").unwrap_or(0),
565 animation: get_i32(structure, "Animation").unwrap_or(0),
566 creature_size: get_i32(structure, "CreatureSize").unwrap_or(0),
567 movement_rate: get_u8(structure, "MovementRate").unwrap_or(0),
568
569 classes: read_list(structure, "ClassList", SavedClass::from_struct),
570 feats: read_list(structure, "FeatList", |e| get_u16(e, "Feat").unwrap_or(0)),
571 skills: read_skills(structure),
572 inventory: read_list(structure, "ItemList", SavedItem::from_struct),
573 equipment: read_list(structure, "Equip_ItemList", SavedItem::from_struct),
574
575 tail: get_u8(structure, "Tail").unwrap_or(0),
576 wings: get_u8(structure, "Wings").unwrap_or(0),
577
578 on_attacked: get_resref(structure, "ScriptAttacked").unwrap_or_default(),
579 on_damaged: get_resref(structure, "ScriptDamaged").unwrap_or_default(),
580 on_death: get_resref(structure, "ScriptDeath").unwrap_or_default(),
581 on_dialog: get_resref(structure, "ScriptDialogue").unwrap_or_default(),
582 on_disturbed: get_resref(structure, "ScriptDisturbed").unwrap_or_default(),
583 on_end_dialog: get_resref(structure, "ScriptEndDialogu").unwrap_or_default(),
584 on_end_round: get_resref(structure, "ScriptEndRound").unwrap_or_default(),
585 on_heartbeat: get_resref(structure, "ScriptHeartbeat").unwrap_or_default(),
586 on_blocked: get_resref(structure, "ScriptOnBlocked").unwrap_or_default(),
587 on_notice: get_resref(structure, "ScriptOnNotice").unwrap_or_default(),
588 on_rested: get_resref(structure, "ScriptRested").unwrap_or_default(),
589 on_spawn: get_resref(structure, "ScriptSpawn").unwrap_or_default(),
590 on_spell: get_resref(structure, "ScriptSpellAt").unwrap_or_default(),
591 on_user_defined: get_resref(structure, "ScriptUserDefine").unwrap_or_default(),
592 }
593 }
594
595 pub fn to_struct(&self, index: usize) -> GffStruct {
601 let mut s = GffStruct::new(i32::try_from(index).expect("creature index fits i32"));
602
603 upsert_field(&mut s, "Tag", GffValue::String(self.tag.clone()));
604 upsert_field(
605 &mut s,
606 "FirstName",
607 GffValue::LocalizedString(self.first_name.clone()),
608 );
609 upsert_field(
610 &mut s,
611 "LastName",
612 GffValue::LocalizedString(self.last_name.clone()),
613 );
614 upsert_field(
615 &mut s,
616 "Description",
617 GffValue::LocalizedString(self.description.clone()),
618 );
619 upsert_field(&mut s, "ObjectId", GffValue::UInt32(self.object_id.get()));
620 upsert_field(&mut s, "AreaId", GffValue::UInt32(self.area_id));
621 upsert_field(&mut s, "Conversation", GffValue::ResRef(self.conversation));
622
623 upsert_field(&mut s, "XPosition", GffValue::Single(self.x_position));
624 upsert_field(&mut s, "YPosition", GffValue::Single(self.y_position));
625 upsert_field(&mut s, "ZPosition", GffValue::Single(self.z_position));
626 upsert_field(&mut s, "XOrientation", GffValue::Single(self.x_orientation));
627 upsert_field(&mut s, "YOrientation", GffValue::Single(self.y_orientation));
628 upsert_field(&mut s, "ZOrientation", GffValue::Single(self.z_orientation));
629
630 upsert_field(
631 &mut s,
632 "Appearance_Type",
633 GffValue::UInt16(self.appearance_type),
634 );
635 upsert_field(
636 &mut s,
637 "Appearance_Head",
638 GffValue::UInt8(self.appearance_head),
639 );
640 upsert_field(&mut s, "PortraitId", GffValue::UInt16(self.portrait_id));
641 upsert_field(&mut s, "Phenotype", GffValue::Int32(self.phenotype));
642 upsert_field(&mut s, "Color_Skin", GffValue::UInt8(self.color_skin));
643 upsert_field(&mut s, "Color_Hair", GffValue::UInt8(self.color_hair));
644 upsert_field(&mut s, "Color_Tattoo1", GffValue::UInt8(self.color_tattoo1));
645 upsert_field(&mut s, "Color_Tattoo2", GffValue::UInt8(self.color_tattoo2));
646 upsert_field(
647 &mut s,
648 "DuplicatingHead",
649 GffValue::UInt8(self.duplicating_head),
650 );
651 upsert_field(
652 &mut s,
653 "UseBackupHead",
654 GffValue::UInt8(self.use_backup_head),
655 );
656 upsert_field(&mut s, "PM_IsDisguised", GffValue::UInt8(self.is_disguised));
657 upsert_field(&mut s, "BodyBag", GffValue::UInt8(self.body_bag));
658
659 upsert_field(&mut s, "Str", GffValue::UInt8(self.strength));
660 upsert_field(&mut s, "Dex", GffValue::UInt8(self.dexterity));
661 upsert_field(&mut s, "Con", GffValue::UInt8(self.constitution));
662 upsert_field(&mut s, "Int", GffValue::UInt8(self.intelligence));
663 upsert_field(&mut s, "Wis", GffValue::UInt8(self.wisdom));
664 upsert_field(&mut s, "Cha", GffValue::UInt8(self.charisma));
665
666 upsert_field(
667 &mut s,
668 "CurrentHitPoints",
669 GffValue::Int16(self.current_hit_points),
670 );
671 upsert_field(&mut s, "HitPoints", GffValue::Int16(self.hit_points));
672 upsert_field(&mut s, "MaxHitPoints", GffValue::Int16(self.max_hit_points));
673 upsert_field(
674 &mut s,
675 "PregameCurrent",
676 GffValue::Int16(self.pregame_current),
677 );
678 upsert_field(&mut s, "ForcePoints", GffValue::Int16(self.force_points));
679 upsert_field(&mut s, "CurrentForce", GffValue::Int16(self.current_force));
680 upsert_field(
681 &mut s,
682 "MaxForcePoints",
683 GffValue::Int16(self.max_force_points),
684 );
685
686 upsert_field(&mut s, "ArmorClass", GffValue::Int16(self.armor_class));
687 upsert_field(&mut s, "RefSaveThrow", GffValue::Int8(self.ref_save_throw));
688 upsert_field(
689 &mut s,
690 "WillSaveThrow",
691 GffValue::Int8(self.will_save_throw),
692 );
693 upsert_field(
694 &mut s,
695 "FortSaveThrow",
696 GffValue::Int8(self.fort_save_throw),
697 );
698 upsert_field(&mut s, "refbonus", GffValue::Int16(self.ref_bonus));
699 upsert_field(&mut s, "willbonus", GffValue::Int16(self.will_bonus));
700 upsert_field(&mut s, "fortbonus", GffValue::Int16(self.fort_bonus));
701 upsert_field(&mut s, "NaturalAC", GffValue::UInt8(self.natural_ac));
702
703 upsert_field(&mut s, "Experience", GffValue::UInt32(self.experience));
704 upsert_field(&mut s, "Gold", GffValue::UInt32(self.gold));
705 upsert_field(&mut s, "SkillPoints", GffValue::UInt16(self.skill_points));
706 upsert_field(
707 &mut s,
708 "MClassLevUpIn",
709 GffValue::UInt8(self.multiclass_level_up_in),
710 );
711 upsert_field(&mut s, "JoiningXP", GffValue::Int32(self.joining_xp));
712 upsert_field(
713 &mut s,
714 "StartingPackage",
715 GffValue::UInt8(self.starting_package),
716 );
717 upsert_field(
718 &mut s,
719 "ChallengeRating",
720 GffValue::Single(self.challenge_rating),
721 );
722 upsert_field(&mut s, "Age", GffValue::Int32(self.age));
723
724 upsert_field(&mut s, "Race", GffValue::UInt8(self.race));
725 upsert_field(&mut s, "Subrace", GffValue::String(self.subrace.clone()));
726 upsert_field(&mut s, "SubraceIndex", GffValue::UInt8(self.subrace_index));
727 upsert_field(&mut s, "Deity", GffValue::String(self.deity.clone()));
728 upsert_field(&mut s, "Gender", GffValue::UInt8(self.gender));
729 upsert_field(&mut s, "GoodEvil", GffValue::UInt8(self.good_evil));
730 upsert_field(&mut s, "FactionID", GffValue::UInt16(self.faction_id));
731 upsert_field(
732 &mut s,
733 "SoundSetFile",
734 GffValue::UInt16(self.sound_set_file),
735 );
736
737 upsert_field(&mut s, "AIState", GffValue::Int32(self.ai_state));
738 upsert_field(
739 &mut s,
740 "NotReorienting",
741 GffValue::UInt8(self.not_reorienting),
742 );
743 upsert_field(&mut s, "DetectMode", GffValue::UInt8(self.detect_mode));
744 upsert_field(&mut s, "StealthMode", GffValue::UInt8(self.stealth_mode));
745 upsert_field(&mut s, "Commandable", GffValue::UInt8(self.commandable));
746 upsert_field(&mut s, "Listening", GffValue::UInt8(self.listening));
747 upsert_field(
748 &mut s,
749 "PartyInteract",
750 GffValue::UInt8(self.party_interact),
751 );
752 upsert_field(&mut s, "Plot", GffValue::UInt8(self.plot));
753 upsert_field(&mut s, "Min1HP", GffValue::UInt8(self.min1_hp));
754 upsert_field(&mut s, "Interruptable", GffValue::UInt8(self.interruptable));
755 upsert_field(&mut s, "Disarmable", GffValue::UInt8(self.disarmable));
756 upsert_field(
757 &mut s,
758 "IsDestroyable",
759 GffValue::UInt8(self.is_destroyable),
760 );
761 upsert_field(&mut s, "IsRaiseable", GffValue::UInt8(self.is_raiseable));
762 upsert_field(
763 &mut s,
764 "DeadSelectable",
765 GffValue::UInt8(self.dead_selectable),
766 );
767 upsert_field(&mut s, "IsPC", GffValue::UInt8(self.is_pc));
768 upsert_field(
769 &mut s,
770 "CreatnScrptFird",
771 GffValue::UInt8(self.creation_script_fired),
772 );
773 upsert_field(
774 &mut s,
775 "AmbientAnimState",
776 GffValue::UInt8(self.ambient_anim_state),
777 );
778 upsert_field(&mut s, "Animation", GffValue::Int32(self.animation));
779 upsert_field(&mut s, "CreatureSize", GffValue::Int32(self.creature_size));
780 upsert_field(&mut s, "MovementRate", GffValue::UInt8(self.movement_rate));
781
782 upsert_field(
783 &mut s,
784 "ClassList",
785 GffValue::List(
786 self.classes
787 .iter()
788 .enumerate()
789 .map(|(i, c)| c.to_struct(i))
790 .collect(),
791 ),
792 );
793 upsert_field(
794 &mut s,
795 "FeatList",
796 GffValue::List(
797 self.feats
798 .iter()
799 .enumerate()
800 .map(|(i, feat)| {
801 let mut e = GffStruct::new(i32::try_from(i).expect("feat index fits i32"));
802 upsert_field(&mut e, "Feat", GffValue::UInt16(*feat));
803 e
804 })
805 .collect(),
806 ),
807 );
808 upsert_field(
809 &mut s,
810 "SkillList",
811 GffValue::List(write_skills(&self.skills)),
812 );
813 upsert_field(
814 &mut s,
815 "ItemList",
816 GffValue::List(
817 self.inventory
818 .iter()
819 .enumerate()
820 .map(|(index, item)| item.to_struct(index))
821 .collect(),
822 ),
823 );
824 upsert_field(
825 &mut s,
826 "Equip_ItemList",
827 GffValue::List(
828 self.equipment
829 .iter()
830 .enumerate()
831 .map(|(index, item)| item.to_struct(index))
832 .collect(),
833 ),
834 );
835
836 upsert_field(&mut s, "Tail", GffValue::UInt8(self.tail));
837 upsert_field(&mut s, "Wings", GffValue::UInt8(self.wings));
838
839 upsert_field(&mut s, "ScriptAttacked", GffValue::ResRef(self.on_attacked));
840 upsert_field(&mut s, "ScriptDamaged", GffValue::ResRef(self.on_damaged));
841 upsert_field(&mut s, "ScriptDeath", GffValue::ResRef(self.on_death));
842 upsert_field(&mut s, "ScriptDialogue", GffValue::ResRef(self.on_dialog));
843 upsert_field(
844 &mut s,
845 "ScriptDisturbed",
846 GffValue::ResRef(self.on_disturbed),
847 );
848 upsert_field(
849 &mut s,
850 "ScriptEndDialogu",
851 GffValue::ResRef(self.on_end_dialog),
852 );
853 upsert_field(
854 &mut s,
855 "ScriptEndRound",
856 GffValue::ResRef(self.on_end_round),
857 );
858 upsert_field(
859 &mut s,
860 "ScriptHeartbeat",
861 GffValue::ResRef(self.on_heartbeat),
862 );
863 upsert_field(&mut s, "ScriptOnBlocked", GffValue::ResRef(self.on_blocked));
864 upsert_field(&mut s, "ScriptOnNotice", GffValue::ResRef(self.on_notice));
865 upsert_field(&mut s, "ScriptRested", GffValue::ResRef(self.on_rested));
866 upsert_field(&mut s, "ScriptSpawn", GffValue::ResRef(self.on_spawn));
867 upsert_field(&mut s, "ScriptSpellAt", GffValue::ResRef(self.on_spell));
868 upsert_field(
869 &mut s,
870 "ScriptUserDefine",
871 GffValue::ResRef(self.on_user_defined),
872 );
873
874 s
875 }
876}
877
878#[derive(Debug, Clone, PartialEq, Eq, Default)]
883pub struct SavedClass {
884 pub class_id: i32,
886 pub class_level: i16,
888 pub powers: Vec<u16>,
890 pub spells_per_day: Vec<u8>,
892}
893
894impl SavedClass {
895 fn from_struct(structure: &GffStruct) -> Self {
896 Self {
897 class_id: get_i32(structure, "Class").unwrap_or(0),
898 class_level: get_i16(structure, "ClassLevel").unwrap_or(0),
899 powers: read_list(structure, "KnownList0", |e| {
900 get_u16(e, "Spell").unwrap_or(0)
901 }),
902 spells_per_day: read_list(structure, "SpellsPerDayList", |e| {
903 get_u8(e, "NumSpellsLeft").unwrap_or(0)
904 }),
905 }
906 }
907
908 fn to_struct(&self, index: usize) -> GffStruct {
909 let mut s = GffStruct::new(i32::try_from(index).expect("class index fits i32"));
910 upsert_field(&mut s, "Class", GffValue::Int32(self.class_id));
911 upsert_field(&mut s, "ClassLevel", GffValue::Int16(self.class_level));
912 upsert_field(
913 &mut s,
914 "KnownList0",
915 GffValue::List(
916 self.powers
917 .iter()
918 .enumerate()
919 .map(|(i, spell)| {
920 let mut e = GffStruct::new(i32::try_from(i).expect("power index fits i32"));
921 upsert_field(&mut e, "Spell", GffValue::UInt16(*spell));
922 e
923 })
924 .collect(),
925 ),
926 );
927 upsert_field(
928 &mut s,
929 "SpellsPerDayList",
930 GffValue::List(
931 self.spells_per_day
932 .iter()
933 .enumerate()
934 .map(|(i, left)| {
935 let mut e = GffStruct::new(i32::try_from(i).expect("spell level fits i32"));
936 upsert_field(&mut e, "NumSpellsLeft", GffValue::UInt8(*left));
937 e
938 })
939 .collect(),
940 ),
941 );
942 s
943 }
944}
945
946fn read_list<T>(structure: &GffStruct, label: &str, build: impl Fn(&GffStruct) -> T) -> Vec<T> {
949 match structure.field(label) {
950 Some(GffValue::List(entries)) => entries.iter().map(build).collect(),
951 _ => Vec::new(),
952 }
953}
954
955fn read_skills(structure: &GffStruct) -> UtcSkills {
958 let ranks = read_list(structure, "SkillList", |e| get_u8(e, "Rank").unwrap_or(0));
959 let at = |index: usize| ranks.get(index).copied().unwrap_or(0);
960 UtcSkills {
961 computer_use: at(0),
962 demolitions: at(1),
963 stealth: at(2),
964 awareness: at(3),
965 persuade: at(4),
966 repair: at(5),
967 security: at(6),
968 treat_injury: at(7),
969 }
970}
971
972fn write_skills(skills: &UtcSkills) -> Vec<GffStruct> {
974 [
975 skills.computer_use,
976 skills.demolitions,
977 skills.stealth,
978 skills.awareness,
979 skills.persuade,
980 skills.repair,
981 skills.security,
982 skills.treat_injury,
983 ]
984 .iter()
985 .enumerate()
986 .map(|(index, rank)| {
987 let mut e = GffStruct::new(i32::try_from(index).expect("skill index fits i32"));
988 upsert_field(&mut e, "Rank", GffValue::UInt8(*rank));
989 e
990 })
991 .collect()
992}
993
994#[cfg(test)]
995mod tests {
996 use super::*;
997
998 #[test]
999 fn round_trips_through_a_gff_struct() {
1000 let mut creature = SavedCreature::new();
1001 creature.tag = "bastila".to_string();
1002 creature.current_hit_points = 42;
1003 creature.gold = 1250;
1004 creature.good_evil = 75;
1005 creature.ref_bonus = -3;
1006 creature.on_death = ResRef::new("k_hen_death").expect("valid resref");
1007 creature.conversation = ResRef::new("k_hen_dlg").expect("valid resref");
1008
1009 let parsed = SavedCreature::from_struct(&creature.to_struct(0));
1010
1011 assert_eq!(parsed, creature);
1012 }
1013
1014 #[test]
1015 fn an_absent_field_falls_back_to_the_engine_default() {
1016 let empty = SavedCreature::from_struct(&GffStruct::new(0));
1019
1020 assert_eq!(empty.current_hit_points, 0);
1021 assert_eq!(empty.gold, 0);
1022 assert!(empty.tag.is_empty());
1023 }
1024
1025 #[test]
1026 fn progression_lists_round_trip() {
1027 let mut creature = SavedCreature::new();
1028 creature.classes = vec![SavedClass {
1029 class_id: 3,
1030 class_level: 7,
1031 powers: vec![10, 22, 41],
1032 spells_per_day: vec![0, 2, 1],
1033 }];
1034 creature.feats = vec![1, 5, 9, 44];
1035 creature.skills.stealth = 6;
1036 creature.skills.treat_injury = 12;
1037
1038 let parsed = SavedCreature::from_struct(&creature.to_struct(0));
1039
1040 assert_eq!(parsed.classes, creature.classes);
1041 assert_eq!(parsed.feats, creature.feats);
1042 assert_eq!(parsed.skills, creature.skills);
1043 }
1044
1045 #[test]
1046 fn skill_ranks_keep_their_engine_order() {
1047 let mut creature = SavedCreature::new();
1049 creature.skills.computer_use = 1;
1050 creature.skills.persuade = 4;
1051
1052 let written = creature.to_struct(0);
1053 let GffValue::List(entries) = written.field("SkillList").expect("written") else {
1054 panic!("SkillList should be a list");
1055 };
1056
1057 assert_eq!(entries.len(), 8);
1058 assert_eq!(get_u8(&entries[0], "Rank"), Some(1));
1059 assert_eq!(get_u8(&entries[4], "Rank"), Some(4));
1060 }
1061
1062 #[test]
1063 fn a_class_without_powers_still_round_trips() {
1064 let mut creature = SavedCreature::new();
1065 creature.classes = vec![SavedClass {
1066 class_id: 1,
1067 class_level: 2,
1068 ..SavedClass::default()
1069 }];
1070
1071 let parsed = SavedCreature::from_struct(&creature.to_struct(0));
1072
1073 assert_eq!(parsed.classes[0].class_id, 1);
1074 assert!(parsed.classes[0].powers.is_empty());
1075 assert!(parsed.classes[0].spells_per_day.is_empty());
1076 }
1077
1078 #[test]
1079 fn a_default_snapshot_writes_every_script_hook() {
1080 let written = SavedCreature::default().to_struct(0);
1083
1084 for label in [
1085 "ScriptAttacked",
1086 "ScriptDamaged",
1087 "ScriptDeath",
1088 "ScriptDialogue",
1089 "ScriptDisturbed",
1090 "ScriptEndDialogu",
1091 "ScriptEndRound",
1092 "ScriptHeartbeat",
1093 "ScriptOnBlocked",
1094 "ScriptOnNotice",
1095 "ScriptRested",
1096 "ScriptSpawn",
1097 "ScriptSpellAt",
1098 "ScriptUserDefine",
1099 "Conversation",
1100 ] {
1101 assert!(written.field(label).is_some(), "{label} was not written");
1102 }
1103 }
1104
1105 #[test]
1106 fn negative_save_bonuses_survive() {
1107 let mut creature = SavedCreature::new();
1110 creature.ref_bonus = -5;
1111 creature.will_bonus = -1;
1112 creature.fort_save_throw = -2;
1113
1114 let parsed = SavedCreature::from_struct(&creature.to_struct(0));
1115
1116 assert_eq!(parsed.ref_bonus, -5);
1117 assert_eq!(parsed.will_bonus, -1);
1118 assert_eq!(parsed.fort_save_throw, -2);
1119 }
1120}