rakata_generics/
utc.rs

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