1use std::io::{Cursor, Read, Write};
20
21use crate::gff_helpers::{
22 get_bool, get_i16, get_i32, get_i8, get_locstring, get_resref, get_string, get_u16, get_u32,
23 get_u8, upsert_field,
24};
25use crate::shared::{
26 CommonTrapScripts, InventoryGridPosition, TrapDefaults, TrapSettings, TRAP_TYPE_ABSENT,
27};
28use rakata_core::{ResRef, StrRef};
29use rakata_formats::{
30 gff_schema::{
31 AbsentDefault, DefaultValue, FieldConstraint, FieldLife, FieldSchema, GffSchema, GffType,
32 },
33 read_gff, read_gff_from_bytes, write_gff, Gff, GffBinaryError, GffLocalizedString, GffStruct,
34 GffValue,
35};
36use thiserror::Error;
37
38#[derive(Debug, Clone, PartialEq)]
40pub struct Utp {
41 pub template_resref: ResRef,
43 pub tag: String,
45 pub name: GffLocalizedString,
47 pub description: GffLocalizedString,
49 pub comment: String,
51 pub conversation: ResRef,
53 pub faction_id: u32,
55 pub appearance_id: u32,
57 pub animation_state: u8,
59 pub animation: i32,
61 pub open: bool,
63 pub auto_remove_key: bool,
65 pub key_name: String,
67 pub key_required: bool,
69 pub lockable: bool,
71 pub locked: bool,
73 pub open_lock_dc: u8,
75 pub close_lock_dc: u8,
77 pub open_lock_diff: u8,
79 pub open_lock_diff_mod: i8,
81 pub current_hp: i16,
83 pub maximum_hp: i16,
85 pub hardness: u8,
87 pub fortitude: u8,
89 pub reflex: u8,
91 pub will: u8,
93 pub plot: bool,
95 pub invulnerable: bool,
97 pub min1_hp: bool,
99 pub not_blastable: bool,
101 pub is_static: bool,
103 pub useable: bool,
105 pub party_interact: bool,
107 pub has_inventory: bool,
109 pub die_when_empty: bool,
111 pub ground_pile: bool,
113 pub light_state: bool,
115 pub interruptable: bool,
117 pub portrait_id: u16,
119 pub portrait: ResRef,
121 pub palette_id: u8,
123 pub bodybag_id: u8,
125 pub type_id: u8,
127 pub is_body_bag: bool,
129 pub is_corpse: bool,
131 pub trap_detectable: bool,
133 pub trap_detect_dc: u8,
135 pub trap_disarmable: bool,
137 pub trap_disarm_dc: u8,
139 pub trap_flag: u8,
141 pub trap_one_shot: bool,
143 pub trap_type: u8,
145 pub on_closed: ResRef,
147 pub on_damaged: ResRef,
149 pub on_death: ResRef,
151 pub on_disarm: ResRef,
153 pub on_heartbeat: ResRef,
155 pub on_inventory: ResRef,
157 pub on_lock: ResRef,
159 pub on_melee_attacked: ResRef,
161 pub on_open: ResRef,
163 pub on_spell_cast_at: ResRef,
165 pub on_unlock: ResRef,
167 pub on_used: ResRef,
169 pub on_user_defined: ResRef,
171 pub on_dialog: ResRef,
173 pub on_end_dialogue: ResRef,
175 pub on_trap_triggered: ResRef,
177 pub inventory: Vec<UtpInventoryItem>,
179}
180
181impl Default for Utp {
182 fn default() -> Self {
183 Self {
184 template_resref: ResRef::blank(),
185 tag: String::new(),
186 name: GffLocalizedString::new(StrRef::invalid()),
187 description: GffLocalizedString::new(StrRef::invalid()),
188 comment: String::new(),
189 conversation: ResRef::blank(),
190 faction_id: 0,
191 appearance_id: 0,
192 animation_state: 0,
193 animation: 0,
194 open: false,
195 auto_remove_key: false,
196 key_name: String::new(),
197 key_required: false,
198 lockable: false,
199 locked: false,
200 open_lock_dc: 0,
201 close_lock_dc: 0,
202 open_lock_diff: 0,
203 open_lock_diff_mod: 0,
204 current_hp: 0,
205 maximum_hp: 0,
206 hardness: 0,
207 fortitude: 0,
208 reflex: 0,
209 will: 0,
210 plot: false,
211 invulnerable: false,
212 min1_hp: false,
213 not_blastable: false,
214 is_static: false,
215 useable: false,
216 party_interact: false,
217 has_inventory: false,
218 die_when_empty: false,
219 ground_pile: true,
220 light_state: false,
221 interruptable: false,
222 portrait_id: 0xffff,
223 portrait: ResRef::blank(),
224 palette_id: 0,
225 bodybag_id: 0,
226 type_id: 0,
227 is_body_bag: false,
228 is_corpse: false,
229 trap_detectable: false,
230 trap_detect_dc: 0,
231 trap_disarmable: false,
232 trap_disarm_dc: 0,
233 trap_flag: false.into(),
234 trap_one_shot: false,
235 trap_type: TRAP_TYPE_ABSENT,
236 on_closed: ResRef::blank(),
237 on_damaged: ResRef::blank(),
238 on_death: ResRef::blank(),
239 on_disarm: ResRef::blank(),
240 on_heartbeat: ResRef::blank(),
241 on_inventory: ResRef::blank(),
242 on_lock: ResRef::blank(),
243 on_melee_attacked: ResRef::blank(),
244 on_open: ResRef::blank(),
245 on_spell_cast_at: ResRef::blank(),
246 on_unlock: ResRef::blank(),
247 on_used: ResRef::blank(),
248 on_user_defined: ResRef::blank(),
249 on_dialog: ResRef::blank(),
250 on_end_dialogue: ResRef::blank(),
251 on_trap_triggered: ResRef::blank(),
252 inventory: Vec::new(),
253 }
254 }
255}
256
257impl Utp {
258 pub fn new() -> Self {
260 Self::default()
261 }
262
263 pub fn trap_settings(&self) -> TrapSettings {
265 TrapSettings {
266 detectable: self.trap_detectable,
267 detect_dc: self.trap_detect_dc,
268 disarmable: self.trap_disarmable,
269 disarm_dc: self.trap_disarm_dc,
270 flag: self.trap_flag,
271 one_shot: self.trap_one_shot,
272 trap_type: self.trap_type,
273 }
274 }
275
276 pub fn set_trap_settings(&mut self, trap: TrapSettings) {
278 self.trap_detectable = trap.detectable;
279 self.trap_detect_dc = trap.detect_dc;
280 self.trap_disarmable = trap.disarmable;
281 self.trap_disarm_dc = trap.disarm_dc;
282 self.trap_flag = trap.flag;
283 self.trap_one_shot = trap.one_shot;
284 self.trap_type = trap.trap_type;
285 }
286
287 pub fn common_trap_scripts(&self) -> CommonTrapScripts {
289 CommonTrapScripts {
290 on_closed: self.on_closed,
291 on_damaged: self.on_damaged,
292 on_death: self.on_death,
293 on_disarm: self.on_disarm,
294 on_heartbeat: self.on_heartbeat,
295 on_lock: self.on_lock,
296 on_melee_attacked: self.on_melee_attacked,
297 on_open: self.on_open,
298 on_spell_cast_at: self.on_spell_cast_at,
299 on_trap_triggered: self.on_trap_triggered,
300 on_unlock: self.on_unlock,
301 on_user_defined: self.on_user_defined,
302 }
303 }
304
305 pub fn set_common_trap_scripts(&mut self, scripts: CommonTrapScripts) {
307 self.on_closed = scripts.on_closed;
308 self.on_damaged = scripts.on_damaged;
309 self.on_death = scripts.on_death;
310 self.on_disarm = scripts.on_disarm;
311 self.on_heartbeat = scripts.on_heartbeat;
312 self.on_lock = scripts.on_lock;
313 self.on_melee_attacked = scripts.on_melee_attacked;
314 self.on_open = scripts.on_open;
315 self.on_spell_cast_at = scripts.on_spell_cast_at;
316 self.on_trap_triggered = scripts.on_trap_triggered;
317 self.on_unlock = scripts.on_unlock;
318 self.on_user_defined = scripts.on_user_defined;
319 }
320
321 pub fn from_gff(gff: &Gff) -> Result<Self, UtpError> {
323 if gff.file_type != *b"UTP " && gff.file_type != *b"GFF " {
324 return Err(UtpError::UnsupportedFileType(gff.file_type));
325 }
326
327 let root = &gff.root;
328
329 let inventory = match root.field("ItemList") {
330 Some(GffValue::List(item_structs)) => item_structs
331 .iter()
332 .map(UtpInventoryItem::from_struct)
333 .collect::<Vec<_>>(),
334 Some(_) => {
335 return Err(UtpError::TypeMismatch {
336 field: "ItemList",
337 expected: "List",
338 });
339 }
340 None => Vec::new(),
341 };
342
343 let useable = get_bool(root, "Useable").unwrap_or(false);
344 let plot = get_bool(root, "Plot").unwrap_or(false);
345
346 let is_static = match root.field("Static") {
349 Some(_) => get_bool(root, "Static").unwrap_or(false),
350 None => !useable,
351 };
352
353 let invulnerable = get_bool(root, "Invulnerable").unwrap_or(false);
361
362 let trap = TrapSettings::read(
366 TrapDefaults::PLACEABLE,
367 |label| get_bool(root, label),
368 |label| get_u8(root, label),
369 );
370 let common_scripts =
371 CommonTrapScripts::read(ResRef::blank(), |label| get_resref(root, label));
372
373 Ok(Self {
374 template_resref: get_resref(root, "TemplateResRef").unwrap_or_default(),
375 tag: get_string(root, "Tag").unwrap_or_default(),
376 name: get_locstring(root, "LocName")
377 .cloned()
378 .unwrap_or_else(|| GffLocalizedString::new(StrRef::invalid())),
379 description: get_locstring(root, "Description")
380 .cloned()
381 .unwrap_or_else(|| GffLocalizedString::new(StrRef::invalid())),
382 comment: get_string(root, "Comment").unwrap_or_default(),
383 conversation: get_resref(root, "Conversation").unwrap_or_default(),
384 faction_id: get_u32(root, "Faction").unwrap_or(0),
385 appearance_id: get_u32(root, "Appearance").unwrap_or(0),
386 animation_state: get_u8(root, "AnimationState").unwrap_or(0),
387 animation: get_i32(root, "Animation").unwrap_or(0),
388 open: get_bool(root, "Open").unwrap_or(false),
389 auto_remove_key: get_bool(root, "AutoRemoveKey").unwrap_or(false),
390 key_name: get_string(root, "KeyName").unwrap_or_default(),
391 key_required: get_bool(root, "KeyRequired").unwrap_or(false),
392 lockable: get_bool(root, "Lockable").unwrap_or(false),
393 locked: get_bool(root, "Locked").unwrap_or(false),
394 open_lock_dc: get_u8(root, "OpenLockDC").unwrap_or(0),
395 close_lock_dc: get_u8(root, "CloseLockDC").unwrap_or(0),
396 open_lock_diff: get_u8(root, "OpenLockDiff").unwrap_or(0),
397 open_lock_diff_mod: get_i8(root, "OpenLockDiffMod").unwrap_or(0),
398 current_hp: get_i16(root, "CurrentHP").unwrap_or(0),
399 maximum_hp: get_i16(root, "HP").unwrap_or(0),
400 hardness: get_u8(root, "Hardness").unwrap_or(0),
401 fortitude: get_u8(root, "Fort").unwrap_or(0),
402 reflex: get_u8(root, "Ref").unwrap_or(0),
403 will: get_u8(root, "Will").unwrap_or(0),
404 plot,
405 invulnerable,
406 min1_hp: get_bool(root, "Min1HP").unwrap_or(false),
407 not_blastable: get_bool(root, "NotBlastable").unwrap_or(false),
408 is_static,
409 useable,
410 party_interact: get_bool(root, "PartyInteract").unwrap_or(false),
411 has_inventory: get_bool(root, "HasInventory").unwrap_or(false),
412 die_when_empty: get_bool(root, "DieWhenEmpty").unwrap_or(false),
413 ground_pile: get_bool(root, "GroundPile").unwrap_or(true),
414 light_state: get_bool(root, "LightState").unwrap_or(false),
415 interruptable: get_bool(root, "Interruptable").unwrap_or(false),
416 portrait_id: get_u16(root, "PortraitId").unwrap_or(0xffff),
417 portrait: get_resref(root, "Portrait").unwrap_or_default(),
418 palette_id: get_u8(root, "PaletteID").unwrap_or(0),
419 bodybag_id: get_u8(root, "BodyBag").unwrap_or(0),
420 type_id: get_u8(root, "Type").unwrap_or(0),
421 is_body_bag: get_bool(root, "IsBodyBag").unwrap_or(false),
422 is_corpse: get_bool(root, "IsCorpse").unwrap_or(false),
423 trap_detectable: trap.detectable,
424 trap_detect_dc: trap.detect_dc,
425 trap_disarmable: trap.disarmable,
426 trap_disarm_dc: trap.disarm_dc,
427 trap_flag: trap.flag,
428 trap_one_shot: trap.one_shot,
429 trap_type: trap.trap_type,
430 on_closed: common_scripts.on_closed,
431 on_damaged: common_scripts.on_damaged,
432 on_death: common_scripts.on_death,
433 on_disarm: common_scripts.on_disarm,
434 on_heartbeat: common_scripts.on_heartbeat,
435 on_inventory: get_resref(root, "OnInvDisturbed").unwrap_or_default(),
436 on_lock: common_scripts.on_lock,
437 on_melee_attacked: common_scripts.on_melee_attacked,
438 on_open: common_scripts.on_open,
439 on_spell_cast_at: common_scripts.on_spell_cast_at,
440 on_unlock: common_scripts.on_unlock,
441 on_used: get_resref(root, "OnUsed").unwrap_or_default(),
442 on_user_defined: common_scripts.on_user_defined,
443 on_dialog: get_resref(root, "OnDialog").unwrap_or_default(),
444 on_end_dialogue: get_resref(root, "OnEndDialogue").unwrap_or_default(),
445 on_trap_triggered: common_scripts.on_trap_triggered,
446 inventory,
447 })
448 }
449
450 pub fn to_gff(&self) -> Gff {
452 let mut root = GffStruct::new(-1);
453
454 upsert_field(
455 &mut root,
456 "TemplateResRef",
457 GffValue::ResRef(self.template_resref),
458 );
459 upsert_field(&mut root, "Tag", GffValue::String(self.tag.clone()));
460 upsert_field(
461 &mut root,
462 "LocName",
463 GffValue::LocalizedString(self.name.clone()),
464 );
465 upsert_field(
466 &mut root,
467 "Description",
468 GffValue::LocalizedString(self.description.clone()),
469 );
470 upsert_field(&mut root, "Comment", GffValue::String(self.comment.clone()));
471 upsert_field(
472 &mut root,
473 "Conversation",
474 GffValue::ResRef(self.conversation),
475 );
476
477 upsert_field(&mut root, "Faction", GffValue::UInt32(self.faction_id));
478 upsert_field(
479 &mut root,
480 "Appearance",
481 GffValue::UInt32(self.appearance_id),
482 );
483 upsert_field(
484 &mut root,
485 "AnimationState",
486 GffValue::UInt8(self.animation_state),
487 );
488 upsert_field(&mut root, "Animation", GffValue::Int32(self.animation));
489 upsert_field(&mut root, "Open", GffValue::UInt8(u8::from(self.open)));
490
491 upsert_field(
492 &mut root,
493 "AutoRemoveKey",
494 GffValue::UInt8(u8::from(self.auto_remove_key)),
495 );
496 upsert_field(
497 &mut root,
498 "KeyName",
499 GffValue::String(self.key_name.clone()),
500 );
501 upsert_field(
502 &mut root,
503 "KeyRequired",
504 GffValue::UInt8(u8::from(self.key_required)),
505 );
506 upsert_field(
507 &mut root,
508 "Lockable",
509 GffValue::UInt8(u8::from(self.lockable)),
510 );
511 upsert_field(&mut root, "Locked", GffValue::UInt8(u8::from(self.locked)));
512 upsert_field(&mut root, "OpenLockDC", GffValue::UInt8(self.open_lock_dc));
513 upsert_field(
514 &mut root,
515 "CloseLockDC",
516 GffValue::UInt8(self.close_lock_dc),
517 );
518 upsert_field(
519 &mut root,
520 "OpenLockDiff",
521 GffValue::UInt8(self.open_lock_diff),
522 );
523 upsert_field(
524 &mut root,
525 "OpenLockDiffMod",
526 GffValue::Int8(self.open_lock_diff_mod),
527 );
528
529 upsert_field(&mut root, "CurrentHP", GffValue::Int16(self.current_hp));
530 upsert_field(&mut root, "HP", GffValue::Int16(self.maximum_hp));
531 upsert_field(&mut root, "Hardness", GffValue::UInt8(self.hardness));
532 upsert_field(&mut root, "Fort", GffValue::UInt8(self.fortitude));
533 upsert_field(&mut root, "Ref", GffValue::UInt8(self.reflex));
534 upsert_field(&mut root, "Will", GffValue::UInt8(self.will));
535
536 upsert_field(&mut root, "Plot", GffValue::UInt8(u8::from(self.plot)));
537 upsert_field(
538 &mut root,
539 "Invulnerable",
540 GffValue::UInt8(u8::from(self.invulnerable)),
541 );
542 upsert_field(&mut root, "Min1HP", GffValue::UInt8(u8::from(self.min1_hp)));
543 upsert_field(
544 &mut root,
545 "NotBlastable",
546 GffValue::UInt8(u8::from(self.not_blastable)),
547 );
548 upsert_field(
549 &mut root,
550 "Static",
551 GffValue::UInt8(u8::from(self.is_static)),
552 );
553 upsert_field(
554 &mut root,
555 "Useable",
556 GffValue::UInt8(u8::from(self.useable)),
557 );
558 upsert_field(
559 &mut root,
560 "PartyInteract",
561 GffValue::UInt8(u8::from(self.party_interact)),
562 );
563 upsert_field(
564 &mut root,
565 "HasInventory",
566 GffValue::UInt8(u8::from(self.has_inventory)),
567 );
568 upsert_field(
569 &mut root,
570 "DieWhenEmpty",
571 GffValue::UInt8(u8::from(self.die_when_empty)),
572 );
573 upsert_field(
574 &mut root,
575 "GroundPile",
576 GffValue::UInt8(u8::from(self.ground_pile)),
577 );
578 upsert_field(
579 &mut root,
580 "LightState",
581 GffValue::UInt8(u8::from(self.light_state)),
582 );
583 upsert_field(
584 &mut root,
585 "Interruptable",
586 GffValue::UInt8(u8::from(self.interruptable)),
587 );
588
589 upsert_field(&mut root, "PortraitId", GffValue::UInt16(self.portrait_id));
590 upsert_field(&mut root, "Portrait", GffValue::ResRef(self.portrait));
591 upsert_field(&mut root, "PaletteID", GffValue::UInt8(self.palette_id));
592 upsert_field(&mut root, "BodyBag", GffValue::UInt8(self.bodybag_id));
593 upsert_field(&mut root, "Type", GffValue::UInt8(self.type_id));
594 upsert_field(
595 &mut root,
596 "IsBodyBag",
597 GffValue::UInt8(u8::from(self.is_body_bag)),
598 );
599 upsert_field(
600 &mut root,
601 "IsCorpse",
602 GffValue::UInt8(u8::from(self.is_corpse)),
603 );
604
605 self.trap_settings()
606 .write(|label, value| upsert_field(&mut root, label, value));
607 self.common_trap_scripts()
608 .write(|label, value| upsert_field(&mut root, label, value));
609 upsert_field(
610 &mut root,
611 "OnInvDisturbed",
612 GffValue::ResRef(self.on_inventory),
613 );
614 upsert_field(&mut root, "OnUsed", GffValue::ResRef(self.on_used));
615 upsert_field(&mut root, "OnDialog", GffValue::ResRef(self.on_dialog));
616 upsert_field(
617 &mut root,
618 "OnEndDialogue",
619 GffValue::ResRef(self.on_end_dialogue),
620 );
621 let item_structs = self
622 .inventory
623 .iter()
624 .map(UtpInventoryItem::to_struct)
625 .collect::<Vec<GffStruct>>();
626 upsert_field(&mut root, "ItemList", GffValue::List(item_structs));
627
628 Gff::new(*b"UTP ", root)
629 }
630}
631
632#[derive(Debug, Clone, PartialEq)]
634pub struct UtpInventoryItem {
635 pub inventory_res: ResRef,
637 pub droppable: bool,
639 pub repos_pos_x: u16,
641 pub repos_pos_y: u16,
643}
644
645impl UtpInventoryItem {
646 fn from_struct(structure: &GffStruct) -> Self {
647 Self {
648 inventory_res: get_resref(structure, "InventoryRes").unwrap_or_default(),
649 droppable: get_bool(structure, "Dropable").unwrap_or(false),
650 repos_pos_x: get_u16(structure, "Repos_PosX").unwrap_or(0),
651 repos_pos_y: get_u16(structure, "Repos_PosY")
652 .or_else(|| get_u16(structure, "Repos_Posy"))
653 .unwrap_or(0),
654 }
655 }
656
657 fn to_struct(&self) -> GffStruct {
658 let mut structure = GffStruct::new(0);
659
660 upsert_field(
661 &mut structure,
662 "InventoryRes",
663 GffValue::ResRef(self.inventory_res),
664 );
665 upsert_field(
666 &mut structure,
667 "Dropable",
668 GffValue::UInt8(u8::from(self.droppable)),
669 );
670 upsert_field(
671 &mut structure,
672 "Repos_PosX",
673 GffValue::UInt16(self.repos_pos_x),
674 );
675 upsert_field(
676 &mut structure,
677 "Repos_PosY",
678 GffValue::UInt16(self.repos_pos_y),
679 );
680
681 structure
682 }
683
684 pub fn position(&self) -> InventoryGridPosition {
686 InventoryGridPosition {
687 x: self.repos_pos_x,
688 y: self.repos_pos_y,
689 }
690 }
691
692 pub fn set_position(&mut self, position: InventoryGridPosition) {
694 self.repos_pos_x = position.x;
695 self.repos_pos_y = position.y;
696 }
697}
698
699#[derive(Debug, Error)]
701pub enum UtpError {
702 #[error("unsupported UTP file type: {0:?}")]
704 UnsupportedFileType([u8; 4]),
705 #[error("UTP field `{field}` has incompatible type (expected {expected})")]
707 TypeMismatch {
708 field: &'static str,
710 expected: &'static str,
712 },
713 #[error(transparent)]
715 Gff(#[from] GffBinaryError),
716}
717
718#[cfg_attr(
720 feature = "tracing",
721 tracing::instrument(level = "debug", skip(reader))
722)]
723pub fn read_utp<R: Read>(reader: &mut R) -> Result<Utp, UtpError> {
724 let gff = read_gff(reader)?;
725 Utp::from_gff(&gff)
726}
727
728#[cfg_attr(
730 feature = "tracing",
731 tracing::instrument(level = "debug", skip(bytes), fields(bytes_len = bytes.len()))
732)]
733pub fn read_utp_from_bytes(bytes: &[u8]) -> Result<Utp, UtpError> {
734 let gff = read_gff_from_bytes(bytes)?;
735 Utp::from_gff(&gff)
736}
737
738#[cfg_attr(
740 feature = "tracing",
741 tracing::instrument(level = "debug", skip(writer, utp))
742)]
743pub fn write_utp<W: Write>(writer: &mut W, utp: &Utp) -> Result<(), UtpError> {
744 let gff = utp.to_gff();
745 write_gff(writer, &gff)?;
746 Ok(())
747}
748
749#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(utp)))]
751pub fn write_utp_to_vec(utp: &Utp) -> Result<Vec<u8>, UtpError> {
752 let mut cursor = Cursor::new(Vec::new());
753 write_utp(&mut cursor, utp)?;
754 Ok(cursor.into_inner())
755}
756
757static ITEM_LIST_CHILDREN: &[FieldSchema] = &[
759 FieldSchema {
760 label: "InventoryRes",
761 expected_type: GffType::ResRef,
762 life: FieldLife::Live,
763 required: false,
764 absent: AbsentDefault::Unverified,
765 children: None,
766 constraint: None,
767 },
768 FieldSchema {
769 label: "Infinite",
770 expected_type: GffType::UInt8,
771 life: FieldLife::Live,
772 required: false,
773 absent: AbsentDefault::Unverified,
774 children: None,
775 constraint: None,
776 },
777 FieldSchema {
778 label: "ObjectId",
779 expected_type: GffType::UInt32,
780 life: FieldLife::Live,
781 required: false,
782 absent: AbsentDefault::Unverified,
783 children: None,
784 constraint: None,
785 },
786 FieldSchema {
787 label: "Dropable",
788 expected_type: GffType::UInt8,
789 life: FieldLife::Live,
790 required: false,
791 absent: AbsentDefault::Unverified,
792 children: None,
793 constraint: None,
794 },
795 FieldSchema {
796 label: "Repos_PosX",
797 expected_type: GffType::UInt16,
798 life: FieldLife::Live,
799 required: false,
800 absent: AbsentDefault::Unverified,
801 children: None,
802 constraint: None,
803 },
804 FieldSchema {
805 label: "Repos_PosY",
806 expected_type: GffType::UInt16,
807 life: FieldLife::Live,
808 required: false,
809 absent: AbsentDefault::Unverified,
810 children: None,
811 constraint: None,
812 },
813 FieldSchema {
814 label: "Repos_Posy",
815 expected_type: GffType::UInt16,
816 life: FieldLife::Live,
817 required: false,
818 absent: AbsentDefault::Unverified,
819 children: None,
820 constraint: None,
821 },
822];
823
824impl GffSchema for Utp {
825 fn schema() -> &'static [FieldSchema] {
826 static SCHEMA: &[FieldSchema] = &[
827 FieldSchema {
828 label: "Animation",
829 expected_type: GffType::Int32,
830 life: FieldLife::Live,
831 required: false,
832 absent: AbsentDefault::Stamped(DefaultValue::Int32(0), "utp.md: Open, Animation and AnimationState are each ordinary unconditional literal reads; the gating between them is a chain, not a default"),
833 children: None,
834 constraint: None,
835 },
836 FieldSchema {
838 label: "OnFailToOpen",
839 expected_type: GffType::ResRef,
840 life: FieldLife::ReadOnlyDead(
841 "this hook belongs to doors; a placeable never reads it",
842 ),
843 required: false,
844 absent: AbsentDefault::Unverified,
845 children: None,
846 constraint: None,
847 },
848 FieldSchema {
850 label: "Tag",
851 expected_type: GffType::String,
852 life: FieldLife::Live,
853 required: false,
854 absent: AbsentDefault::Stamped(DefaultValue::Text(""), "utp.md: the rest are unremarkable unconditional literals"),
855 children: None,
856 constraint: None,
857 },
858 FieldSchema {
859 label: "LocName",
860 expected_type: GffType::LocalizedString,
861 life: FieldLife::Live,
862 required: false,
863 absent: AbsentDefault::Stamped(DefaultValue::EmptyLocalizedString, "utp.md: the rest are unremarkable unconditional literals"),
864 children: None,
865 constraint: None,
866 },
867 FieldSchema {
868 label: "Description",
869 expected_type: GffType::LocalizedString,
870 life: FieldLife::Live,
871 required: false,
872 absent: AbsentDefault::Stamped(DefaultValue::EmptyLocalizedString, "utp.md: the rest are unremarkable unconditional literals"),
873 children: None,
874 constraint: None,
875 },
876 FieldSchema {
877 label: "Conversation",
878 expected_type: GffType::ResRef,
879 life: FieldLife::Live,
880 required: false,
881 absent: AbsentDefault::Stamped(DefaultValue::Text(""), "utp.md: the rest are unremarkable unconditional literals"),
882 children: None,
883 constraint: None,
884 },
885 FieldSchema {
886 label: "Faction",
887 expected_type: GffType::UInt32,
888 life: FieldLife::Live,
889 required: false,
890 absent: AbsentDefault::Stamped(DefaultValue::UInt32(0), "utp.md: the rest are unremarkable unconditional literals"),
891 children: None,
892 constraint: None,
893 },
894 FieldSchema {
896 label: "Appearance",
897 expected_type: GffType::UInt32,
898 life: FieldLife::Live,
899 required: false,
900 absent: AbsentDefault::Unverified,
901 children: None,
902 constraint: Some(FieldConstraint::RangeInt(0, 255)),
903 },
904 FieldSchema {
905 label: "AnimationState",
906 expected_type: GffType::UInt8,
907 life: FieldLife::Live,
908 required: false,
909 absent: AbsentDefault::Stamped(DefaultValue::UInt8(0), "utp.md: Open, Animation and AnimationState are each ordinary unconditional literal reads; the gating between them is a chain, not a default"),
910 children: None,
911 constraint: None,
912 },
913 FieldSchema {
914 label: "Open",
915 expected_type: GffType::UInt8,
916 life: FieldLife::Live,
917 required: false,
918 absent: AbsentDefault::Stamped(DefaultValue::UInt8(0), "utp.md: Open, Animation and AnimationState are each ordinary unconditional literal reads; the gating between them is a chain, not a default"),
919 children: None,
920 constraint: None,
921 },
922 FieldSchema {
924 label: "HP",
925 expected_type: GffType::Int16,
926 life: FieldLife::Live,
927 required: false,
928 absent: AbsentDefault::Stamped(DefaultValue::Int16(0), "utp.md: the rest are unremarkable unconditional literals"),
929 children: None,
930 constraint: None,
931 },
932 FieldSchema {
933 label: "CurrentHP",
934 expected_type: GffType::Int16,
935 life: FieldLife::Live,
936 required: false,
937 absent: AbsentDefault::Stamped(DefaultValue::Int16(0), "utp.md: the constructor arms HP, CurrentHP and all three trap flags, and each read stamps a hardcoded 0 over it"),
938 children: None,
939 constraint: None,
940 },
941 FieldSchema {
942 label: "Hardness",
943 expected_type: GffType::UInt8,
944 life: FieldLife::Live,
945 required: false,
946 absent: AbsentDefault::Stamped(DefaultValue::UInt8(0), "utp.md: the rest are unremarkable unconditional literals"),
947 children: None,
948 constraint: None,
949 },
950 FieldSchema {
951 label: "Fort",
952 expected_type: GffType::UInt8,
953 life: FieldLife::Live,
954 required: false,
955 absent: AbsentDefault::Stamped(DefaultValue::UInt8(0), "utp.md: the rest are unremarkable unconditional literals"),
956 children: None,
957 constraint: None,
958 },
959 FieldSchema {
960 label: "Ref",
961 expected_type: GffType::UInt8,
962 life: FieldLife::Live,
963 required: false,
964 absent: AbsentDefault::Stamped(DefaultValue::UInt8(0), "utp.md: the rest are unremarkable unconditional literals"),
965 children: None,
966 constraint: None,
967 },
968 FieldSchema {
969 label: "Will",
970 expected_type: GffType::UInt8,
971 life: FieldLife::Live,
972 required: false,
973 absent: AbsentDefault::Stamped(DefaultValue::UInt8(0), "utp.md: the rest are unremarkable unconditional literals"),
974 children: None,
975 constraint: None,
976 },
977 FieldSchema {
979 label: "Plot",
980 expected_type: GffType::UInt8,
981 life: FieldLife::Live,
982 required: false,
983 absent: AbsentDefault::Constructed(DefaultValue::UInt8(0), "utp.md: Invulnerable and Plot feed one member and the loader tries Invulnerable first, both carrying over the constructed 0"),
984 children: None,
985 constraint: None,
986 },
987 FieldSchema {
988 label: "Useable",
989 expected_type: GffType::UInt8,
990 life: FieldLife::Live,
991 required: false,
992 absent: AbsentDefault::Stamped(DefaultValue::UInt8(0), "utp.md: the rest are unremarkable unconditional literals"),
993 children: None,
994 constraint: None,
995 },
996 FieldSchema {
997 label: "Static",
998 expected_type: GffType::UInt8,
999 life: FieldLife::Live,
1000 required: false,
1001 absent: AbsentDefault::NotAConstant("utp.md: an absent Static is derived from the placeable being usable rather than from a constant"),
1002 children: None,
1003 constraint: None,
1004 },
1005 FieldSchema {
1006 label: "Invulnerable",
1007 expected_type: GffType::UInt8,
1008 life: FieldLife::Live,
1009 required: false,
1010 absent: AbsentDefault::Constructed(DefaultValue::UInt8(0), "utp.md: Invulnerable and Plot feed one member and the loader tries Invulnerable first, both carrying over the constructed 0"),
1011 children: None,
1012 constraint: None,
1013 },
1014 FieldSchema {
1015 label: "Min1HP",
1016 expected_type: GffType::UInt8,
1017 life: FieldLife::Live,
1018 required: false,
1019 absent: AbsentDefault::Constructed(DefaultValue::UInt8(0), "utp.md: the rest are unremarkable unconditional literals"),
1020 children: None,
1021 constraint: None,
1022 },
1023 FieldSchema {
1024 label: "PartyInteract",
1025 expected_type: GffType::UInt8,
1026 life: FieldLife::Live,
1027 required: false,
1028 absent: AbsentDefault::Stamped(DefaultValue::UInt8(0), "utp.md: the rest are unremarkable unconditional literals"),
1029 children: None,
1030 constraint: None,
1031 },
1032 FieldSchema {
1033 label: "HasInventory",
1034 expected_type: GffType::UInt8,
1035 life: FieldLife::Live,
1036 required: false,
1037 absent: AbsentDefault::Stamped(DefaultValue::UInt8(0), "utp.md: HasInventory and DieWhenEmpty are both ordinary unconditional byte reads with a literal 0 default; the surprise is the label-to-member wiring, not the default"),
1038 children: None,
1039 constraint: None,
1040 },
1041 FieldSchema {
1042 label: "DieWhenEmpty",
1043 expected_type: GffType::UInt8,
1044 life: FieldLife::Live,
1045 required: false,
1046 absent: AbsentDefault::Stamped(DefaultValue::UInt8(0), "utp.md: HasInventory and DieWhenEmpty are both ordinary unconditional byte reads with a literal 0 default; the surprise is the label-to-member wiring, not the default"),
1047 children: None,
1048 constraint: None,
1049 },
1050 FieldSchema {
1051 label: "GroundPile",
1052 expected_type: GffType::UInt8,
1053 life: FieldLife::Live,
1054 required: false,
1055 absent: AbsentDefault::Unverified,
1056 children: None,
1057 constraint: None,
1058 },
1059 FieldSchema {
1060 label: "BodyBag",
1061 expected_type: GffType::UInt8,
1062 life: FieldLife::Live,
1063 required: false,
1064 absent: AbsentDefault::Constructed(DefaultValue::UInt8(0), "utp.md: the rest are unremarkable unconditional literals"),
1065 children: None,
1066 constraint: None,
1067 },
1068 FieldSchema {
1069 label: "LightState",
1070 expected_type: GffType::UInt8,
1071 life: FieldLife::Live,
1072 required: false,
1073 absent: AbsentDefault::NotAConstant("utp.md: LightState defaults to a placeables.2da LightColor lookup keyed by the already-resolved Appearance, so the value comes from a sibling through table data"),
1074 children: None,
1075 constraint: None,
1076 },
1077 FieldSchema {
1079 label: "Locked",
1080 expected_type: GffType::UInt8,
1081 life: FieldLife::Live,
1082 required: false,
1083 absent: AbsentDefault::Stamped(DefaultValue::UInt8(0), "utp.md: the rest are unremarkable unconditional literals"),
1084 children: None,
1085 constraint: None,
1086 },
1087 FieldSchema {
1088 label: "Lockable",
1089 expected_type: GffType::UInt8,
1090 life: FieldLife::Live,
1091 required: false,
1092 absent: AbsentDefault::Stamped(DefaultValue::UInt8(0), "utp.md: the rest are unremarkable unconditional literals"),
1093 children: None,
1094 constraint: None,
1095 },
1096 FieldSchema {
1097 label: "OpenLockDC",
1098 expected_type: GffType::UInt8,
1099 life: FieldLife::Live,
1100 required: false,
1101 absent: AbsentDefault::Stamped(DefaultValue::UInt8(0), "utp.md: the rest are unremarkable unconditional literals"),
1102 children: None,
1103 constraint: None,
1104 },
1105 FieldSchema {
1106 label: "CloseLockDC",
1107 expected_type: GffType::UInt8,
1108 life: FieldLife::Live,
1109 required: false,
1110 absent: AbsentDefault::Stamped(DefaultValue::UInt8(0), "utp.md: the rest are unremarkable unconditional literals"),
1111 children: None,
1112 constraint: None,
1113 },
1114 FieldSchema {
1115 label: "KeyName",
1116 expected_type: GffType::String,
1117 life: FieldLife::Live,
1118 required: false,
1119 absent: AbsentDefault::Stamped(DefaultValue::Text(""), "utp.md: the rest are unremarkable unconditional literals"),
1120 children: None,
1121 constraint: None,
1122 },
1123 FieldSchema {
1124 label: "KeyRequired",
1125 expected_type: GffType::UInt8,
1126 life: FieldLife::Live,
1127 required: false,
1128 absent: AbsentDefault::Stamped(DefaultValue::UInt8(0), "utp.md: the rest are unremarkable unconditional literals"),
1129 children: None,
1130 constraint: None,
1131 },
1132 FieldSchema {
1133 label: "AutoRemoveKey",
1134 expected_type: GffType::UInt8,
1135 life: FieldLife::Live,
1136 required: false,
1137 absent: AbsentDefault::Stamped(DefaultValue::UInt8(0), "utp.md: the rest are unremarkable unconditional literals"),
1138 children: None,
1139 constraint: None,
1140 },
1141 FieldSchema {
1143 label: "PortraitId",
1144 expected_type: GffType::UInt16,
1145 life: FieldLife::Live,
1146 required: false,
1147 absent: AbsentDefault::Unverified,
1148 children: None,
1149 constraint: None,
1150 },
1151 FieldSchema {
1152 label: "Portrait",
1153 expected_type: GffType::ResRef,
1154 life: FieldLife::Live,
1155 required: false,
1156 absent: AbsentDefault::Unverified,
1157 children: None,
1158 constraint: None,
1159 },
1160 FieldSchema {
1162 label: "LoadScreenID",
1163 expected_type: GffType::UInt16,
1164 life: FieldLife::Live,
1165 required: false,
1166 absent: AbsentDefault::Unverified,
1167 children: None,
1168 constraint: None,
1169 },
1170 FieldSchema {
1172 label: "TrapType",
1173 expected_type: GffType::UInt8,
1174 life: FieldLife::Live,
1175 required: false,
1176 absent: AbsentDefault::Constructed(DefaultValue::UInt8(255), "utp.md: the rest are unremarkable unconditional literals"),
1177 children: None,
1178 constraint: None,
1179 },
1180 FieldSchema {
1181 label: "TrapDisarmable",
1182 expected_type: GffType::UInt8,
1183 life: FieldLife::Live,
1184 required: false,
1185 absent: AbsentDefault::Stamped(DefaultValue::UInt8(0), "utp.md: the constructor arms HP, CurrentHP and all three trap flags, and each read stamps a hardcoded 0 over it"),
1186 children: None,
1187 constraint: None,
1188 },
1189 FieldSchema {
1190 label: "TrapDetectable",
1191 expected_type: GffType::UInt8,
1192 life: FieldLife::Live,
1193 required: false,
1194 absent: AbsentDefault::Stamped(DefaultValue::UInt8(0), "utp.md: the constructor arms HP, CurrentHP and all three trap flags, and each read stamps a hardcoded 0 over it"),
1195 children: None,
1196 constraint: None,
1197 },
1198 FieldSchema {
1199 label: "DisarmDC",
1200 expected_type: GffType::UInt8,
1201 life: FieldLife::Live,
1202 required: false,
1203 absent: AbsentDefault::Stamped(DefaultValue::UInt8(0), "utp.md: the rest are unremarkable unconditional literals"),
1204 children: None,
1205 constraint: None,
1206 },
1207 FieldSchema {
1208 label: "TrapDetectDC",
1209 expected_type: GffType::UInt8,
1210 life: FieldLife::Live,
1211 required: false,
1212 absent: AbsentDefault::Stamped(DefaultValue::UInt8(0), "utp.md: the rest are unremarkable unconditional literals"),
1213 children: None,
1214 constraint: None,
1215 },
1216 FieldSchema {
1217 label: "TrapFlag",
1218 expected_type: GffType::UInt8,
1219 life: FieldLife::Live,
1220 required: false,
1221 absent: AbsentDefault::Stamped(DefaultValue::UInt8(0), "utp.md: the rest are unremarkable unconditional literals"),
1222 children: None,
1223 constraint: None,
1224 },
1225 FieldSchema {
1226 label: "TrapOneShot",
1227 expected_type: GffType::UInt8,
1228 life: FieldLife::Live,
1229 required: false,
1230 absent: AbsentDefault::Stamped(DefaultValue::UInt8(0), "utp.md: the constructor arms HP, CurrentHP and all three trap flags, and each read stamps a hardcoded 0 over it"),
1231 children: None,
1232 constraint: None,
1233 },
1234 FieldSchema {
1236 label: "OnClosed",
1237 expected_type: GffType::ResRef,
1238 life: FieldLife::Live,
1239 required: false,
1240 absent: AbsentDefault::Stamped(DefaultValue::Text(""), "utp.md: a placeable constructor never pre-arms its script slots, so an absent hook is genuinely empty"),
1241 children: None,
1242 constraint: None,
1243 },
1244 FieldSchema {
1245 label: "OnDamaged",
1246 expected_type: GffType::ResRef,
1247 life: FieldLife::Live,
1248 required: false,
1249 absent: AbsentDefault::Stamped(DefaultValue::Text(""), "utp.md: a placeable constructor never pre-arms its script slots, so an absent hook is genuinely empty"),
1250 children: None,
1251 constraint: None,
1252 },
1253 FieldSchema {
1254 label: "OnDeath",
1255 expected_type: GffType::ResRef,
1256 life: FieldLife::Live,
1257 required: false,
1258 absent: AbsentDefault::Stamped(DefaultValue::Text(""), "utp.md: a placeable constructor never pre-arms its script slots, so an absent hook is genuinely empty"),
1259 children: None,
1260 constraint: None,
1261 },
1262 FieldSchema {
1263 label: "OnDisarm",
1264 expected_type: GffType::ResRef,
1265 life: FieldLife::Live,
1266 required: false,
1267 absent: AbsentDefault::Stamped(DefaultValue::Text(""), "utp.md: a placeable constructor never pre-arms its script slots, so an absent hook is genuinely empty"),
1268 children: None,
1269 constraint: None,
1270 },
1271 FieldSchema {
1272 label: "OnHeartbeat",
1273 expected_type: GffType::ResRef,
1274 life: FieldLife::Live,
1275 required: false,
1276 absent: AbsentDefault::Stamped(DefaultValue::Text(""), "utp.md: a placeable constructor never pre-arms its script slots, so an absent hook is genuinely empty"),
1277 children: None,
1278 constraint: None,
1279 },
1280 FieldSchema {
1281 label: "OnInvDisturbed",
1282 expected_type: GffType::ResRef,
1283 life: FieldLife::Live,
1284 required: false,
1285 absent: AbsentDefault::Stamped(DefaultValue::Text(""), "utp.md: a placeable constructor never pre-arms its script slots, so an absent hook is genuinely empty"),
1286 children: None,
1287 constraint: None,
1288 },
1289 FieldSchema {
1290 label: "OnLock",
1291 expected_type: GffType::ResRef,
1292 life: FieldLife::Live,
1293 required: false,
1294 absent: AbsentDefault::Stamped(DefaultValue::Text(""), "utp.md: a placeable constructor never pre-arms its script slots, so an absent hook is genuinely empty"),
1295 children: None,
1296 constraint: None,
1297 },
1298 FieldSchema {
1299 label: "OnMeleeAttacked",
1300 expected_type: GffType::ResRef,
1301 life: FieldLife::Live,
1302 required: false,
1303 absent: AbsentDefault::Stamped(DefaultValue::Text(""), "utp.md: a placeable constructor never pre-arms its script slots, so an absent hook is genuinely empty"),
1304 children: None,
1305 constraint: None,
1306 },
1307 FieldSchema {
1308 label: "OnOpen",
1309 expected_type: GffType::ResRef,
1310 life: FieldLife::Live,
1311 required: false,
1312 absent: AbsentDefault::Stamped(DefaultValue::Text(""), "utp.md: a placeable constructor never pre-arms its script slots, so an absent hook is genuinely empty"),
1313 children: None,
1314 constraint: None,
1315 },
1316 FieldSchema {
1317 label: "OnSpellCastAt",
1318 expected_type: GffType::ResRef,
1319 life: FieldLife::Live,
1320 required: false,
1321 absent: AbsentDefault::Stamped(DefaultValue::Text(""), "utp.md: a placeable constructor never pre-arms its script slots, so an absent hook is genuinely empty"),
1322 children: None,
1323 constraint: None,
1324 },
1325 FieldSchema {
1326 label: "OnTrapTriggered",
1327 expected_type: GffType::ResRef,
1328 life: FieldLife::Live,
1329 required: false,
1330 absent: AbsentDefault::Stamped(DefaultValue::Text(""), "utp.md: a placeable constructor never pre-arms its script slots, so an absent hook is genuinely empty"),
1331 children: None,
1332 constraint: None,
1333 },
1334 FieldSchema {
1335 label: "OnUnlock",
1336 expected_type: GffType::ResRef,
1337 life: FieldLife::Live,
1338 required: false,
1339 absent: AbsentDefault::Stamped(DefaultValue::Text(""), "utp.md: the rest are unremarkable unconditional literals"),
1340 children: None,
1341 constraint: None,
1342 },
1343 FieldSchema {
1344 label: "OnUsed",
1345 expected_type: GffType::ResRef,
1346 life: FieldLife::Live,
1347 required: false,
1348 absent: AbsentDefault::Stamped(DefaultValue::Text(""), "utp.md: a placeable constructor never pre-arms its script slots, so an absent hook is genuinely empty"),
1349 children: None,
1350 constraint: None,
1351 },
1352 FieldSchema {
1353 label: "OnUserDefined",
1354 expected_type: GffType::ResRef,
1355 life: FieldLife::Live,
1356 required: false,
1357 absent: AbsentDefault::Stamped(DefaultValue::Text(""), "utp.md: a placeable constructor never pre-arms its script slots, so an absent hook is genuinely empty"),
1358 children: None,
1359 constraint: None,
1360 },
1361 FieldSchema {
1362 label: "OnDialog",
1363 expected_type: GffType::ResRef,
1364 life: FieldLife::Live,
1365 required: false,
1366 absent: AbsentDefault::Stamped(DefaultValue::Text(""), "utp.md: a placeable constructor never pre-arms its script slots, so an absent hook is genuinely empty"),
1367 children: None,
1368 constraint: None,
1369 },
1370 FieldSchema {
1371 label: "OnEndDialogue",
1372 expected_type: GffType::ResRef,
1373 life: FieldLife::Live,
1374 required: false,
1375 absent: AbsentDefault::Stamped(DefaultValue::Text(""), "utp.md: a placeable constructor never pre-arms its script slots, so an absent hook is genuinely empty"),
1376 children: None,
1377 constraint: None,
1378 },
1379 FieldSchema {
1381 label: "ItemList",
1382 expected_type: GffType::List,
1383 life: FieldLife::Live,
1384 required: false,
1385 absent: AbsentDefault::Unverified,
1386 children: Some(ITEM_LIST_CHILDREN),
1387 constraint: None,
1388 },
1389 FieldSchema {
1391 label: "TemplateResRef",
1392 expected_type: GffType::ResRef,
1393 life: FieldLife::Live,
1394 required: false,
1395 absent: AbsentDefault::Unverified,
1396 children: None,
1397 constraint: None,
1398 },
1399 FieldSchema {
1400 label: "Comment",
1401 expected_type: GffType::String,
1402 life: FieldLife::Live,
1403 required: false,
1404 absent: AbsentDefault::Unverified,
1405 children: None,
1406 constraint: None,
1407 },
1408 FieldSchema {
1409 label: "PaletteID",
1410 expected_type: GffType::UInt8,
1411 life: FieldLife::Live,
1412 required: false,
1413 absent: AbsentDefault::Unverified,
1414 children: None,
1415 constraint: None,
1416 },
1417 FieldSchema {
1418 label: "OpenLockDiff",
1419 expected_type: GffType::UInt8,
1420 life: FieldLife::Live,
1421 required: false,
1422 absent: AbsentDefault::Unverified,
1423 children: None,
1424 constraint: None,
1425 },
1426 FieldSchema {
1427 label: "OpenLockDiffMod",
1428 expected_type: GffType::Int8,
1429 life: FieldLife::Live,
1430 required: false,
1431 absent: AbsentDefault::Unverified,
1432 children: None,
1433 constraint: None,
1434 },
1435 FieldSchema {
1436 label: "NotBlastable",
1437 expected_type: GffType::UInt8,
1438 life: FieldLife::Live,
1439 required: false,
1440 absent: AbsentDefault::Unverified,
1441 children: None,
1442 constraint: None,
1443 },
1444 FieldSchema {
1445 label: "Interruptable",
1446 expected_type: GffType::UInt8,
1447 life: FieldLife::Live,
1448 required: false,
1449 absent: AbsentDefault::Unverified,
1450 children: None,
1451 constraint: None,
1452 },
1453 FieldSchema {
1454 label: "Type",
1455 expected_type: GffType::UInt8,
1456 life: FieldLife::Live,
1457 required: false,
1458 absent: AbsentDefault::Unverified,
1459 children: None,
1460 constraint: None,
1461 },
1462 FieldSchema {
1463 label: "IsBodyBag",
1464 expected_type: GffType::UInt8,
1465 life: FieldLife::Live,
1466 required: false,
1467 absent: AbsentDefault::Constructed(DefaultValue::UInt8(0), "utp.md: the rest are unremarkable unconditional literals"),
1468 children: None,
1469 constraint: None,
1470 },
1471 FieldSchema {
1472 label: "IsCorpse",
1473 expected_type: GffType::UInt8,
1474 life: FieldLife::Live,
1475 required: false,
1476 absent: AbsentDefault::Constructed(DefaultValue::UInt8(0), "utp.md: Min1HP, BodyBag, IsBodyBag and IsCorpse carry over the object's constructed value"),
1477 children: None,
1478 constraint: None,
1479 },
1480 ];
1481 SCHEMA
1482 }
1483}
1484
1485#[cfg(test)]
1486mod tests {
1487 use super::*;
1488
1489 const TEST_UTP: &[u8] = include_bytes!(concat!(
1490 env!("CARGO_MANIFEST_DIR"),
1491 "/../../fixtures/test.utp"
1492 ));
1493
1494 #[test]
1495 fn reads_core_utp_fields_from_fixture() {
1496 let utp = read_utp_from_bytes(TEST_UTP).expect("fixture must parse");
1497
1498 assert_eq!(utp.tag, "SecLoc");
1499 assert_eq!(utp.template_resref, "lockerlg002");
1500 assert_eq!(utp.name.string_ref.raw(), 74_450);
1501 assert_eq!(utp.description.string_ref.raw(), -1);
1502 assert!(utp.auto_remove_key);
1503 assert_eq!(utp.close_lock_dc, 13);
1504 assert_eq!(utp.conversation, "conversation");
1505 assert_eq!(utp.faction_id, 1);
1506 assert!(utp.plot);
1507 assert!(utp.not_blastable);
1508 assert!(utp.min1_hp);
1509 assert!(utp.key_required);
1510 assert!(!utp.lockable);
1511 assert!(utp.locked);
1512 assert_eq!(utp.open_lock_dc, 28);
1513 assert_eq!(utp.open_lock_diff, 1);
1514 assert_eq!(utp.open_lock_diff_mod, 1);
1515 assert_eq!(utp.key_name, "somekey");
1516 assert_eq!(utp.animation_state, 2);
1517 assert_eq!(utp.appearance_id, 67);
1518 assert_eq!(utp.maximum_hp, 15);
1519 assert_eq!(utp.current_hp, 15);
1520 assert_eq!(utp.hardness, 5);
1521 assert_eq!(utp.fortitude, 16);
1522 assert_eq!(utp.reflex, 0);
1523 assert_eq!(utp.will, 0);
1524 assert_eq!(utp.on_closed, "onclosed");
1525 assert_eq!(utp.on_damaged, "ondamaged");
1526 assert_eq!(utp.on_death, "ondeath");
1527 assert_eq!(utp.on_disarm, "ondisarm");
1528 assert_eq!(utp.on_heartbeat, "onheartbeat");
1529 assert_eq!(utp.on_inventory, "oninvdisturbed");
1530 assert_eq!(utp.on_lock, "onlock");
1531 assert_eq!(utp.on_melee_attacked, "onmeleeattacked");
1532 assert_eq!(utp.on_open, "onopen");
1533 assert_eq!(utp.on_spell_cast_at, "onspellcastat");
1534 assert_eq!(utp.on_unlock, "onunlock");
1535 assert_eq!(utp.on_used, "onused");
1536 assert_eq!(utp.on_user_defined, "onuserdefined");
1537 assert_eq!(utp.on_end_dialogue, "onenddialogue");
1538 assert!(utp.has_inventory);
1539 assert!(utp.party_interact);
1540 assert!(utp.is_static);
1541 assert!(utp.useable);
1542 assert_eq!(utp.comment, "Large standup locker");
1543 assert!(utp.interruptable);
1544 assert_eq!(utp.portrait_id, 0);
1545 assert!(utp.trap_detectable);
1546 assert_eq!(utp.trap_detect_dc, 0);
1547 assert!(utp.trap_disarmable);
1548 assert_eq!(utp.trap_disarm_dc, 15);
1549 assert_eq!(utp.trap_flag, 0);
1550 assert!(utp.trap_one_shot);
1551 assert_eq!(utp.trap_type, 0);
1552 assert_eq!(utp.bodybag_id, 0);
1553 assert_eq!(utp.type_id, 0);
1554 assert_eq!(utp.palette_id, 6);
1555
1556 assert_eq!(utp.inventory.len(), 2);
1557 assert_eq!(utp.inventory[0].inventory_res, "g_w_iongren01");
1558 assert!(!utp.inventory[0].droppable);
1559 assert_eq!(utp.inventory[1].inventory_res, "g_w_iongren02");
1560 assert!(utp.inventory[1].droppable);
1561 }
1562
1563 #[test]
1564 fn all_fields_survive_typed_roundtrip() {
1565 let utp = read_utp_from_bytes(TEST_UTP).expect("fixture must parse");
1566 let bytes = write_utp_to_vec(&utp).expect("write succeeds");
1567 let reparsed = read_utp_from_bytes(&bytes).expect("reparse succeeds");
1568 assert_eq!(reparsed, utp);
1569 }
1570
1571 #[test]
1572 fn typed_edits_roundtrip_through_gff_writer() {
1573 let mut utp = read_utp_from_bytes(TEST_UTP).expect("fixture must parse");
1574 utp.tag = "SecLocRust".into();
1575 utp.open_lock_dc = 33;
1576 utp.locked = false;
1577 utp.inventory[0].droppable = true;
1578 utp.comment = "Rust comment".into();
1579 utp.on_open = ResRef::new("k_on_open_new").expect("valid test resref");
1580
1581 let encoded = write_utp_to_vec(&utp).expect("encode");
1582 let reparsed = read_utp_from_bytes(&encoded).expect("decode");
1583
1584 assert_eq!(reparsed.tag, "SecLocRust");
1585 assert_eq!(reparsed.open_lock_dc, 33);
1586 assert!(!reparsed.locked);
1587 assert!(reparsed.inventory[0].droppable);
1588 assert_eq!(reparsed.comment, "Rust comment");
1589 assert_eq!(reparsed.on_open, "k_on_open_new");
1590 }
1591
1592 #[test]
1593 fn static_defaults_to_inverse_of_useable_when_static_missing() {
1594 let mut root = GffStruct::new(-1);
1595 root.push_field("Useable", GffValue::UInt8(1));
1596 root.push_field("ItemList", GffValue::List(Vec::new()));
1597 let utp = Utp::from_gff(&Gff::new(*b"UTP ", root)).expect("must parse");
1598 assert!(!utp.is_static);
1599
1600 let mut root2 = GffStruct::new(-1);
1601 root2.push_field("Useable", GffValue::UInt8(0));
1602 root2.push_field("ItemList", GffValue::List(Vec::new()));
1603 let utp2 = Utp::from_gff(&Gff::new(*b"UTP ", root2)).expect("must parse");
1604 assert!(utp2.is_static);
1605 }
1606
1607 #[test]
1608 fn rejects_non_utp_file_type() {
1609 let gff = Gff::new(*b"UTC ", GffStruct::new(-1));
1610 let err = Utp::from_gff(&gff).expect_err("must fail");
1611 assert!(matches!(err, UtpError::UnsupportedFileType(file_type) if file_type == *b"UTC "));
1612 }
1613
1614 #[test]
1615 fn read_utp_from_reader_matches_bytes_path() {
1616 let mut cursor = Cursor::new(TEST_UTP);
1617 let via_reader = read_utp(&mut cursor).expect("reader parse");
1618 let via_bytes = read_utp_from_bytes(TEST_UTP).expect("bytes parse");
1619 assert_eq!(via_reader.tag, via_bytes.tag);
1620 assert_eq!(via_reader.inventory.len(), via_bytes.inventory.len());
1621 }
1622
1623 #[test]
1624 fn type_mismatch_on_item_list_is_error() {
1625 let mut root = GffStruct::new(-1);
1626 root.push_field("ItemList", GffValue::UInt32(7));
1627 let gff = Gff::new(*b"UTP ", root);
1628 let err = Utp::from_gff(&gff).expect_err("must fail");
1629 assert!(matches!(
1630 err,
1631 UtpError::TypeMismatch {
1632 field: "ItemList",
1633 expected: "List"
1634 }
1635 ));
1636 }
1637
1638 #[test]
1639 fn write_utp_matches_direct_gff_writer() {
1640 let utp = read_utp_from_bytes(TEST_UTP).expect("fixture parse");
1641 let from_utp = write_utp_to_vec(&utp).expect("utp encode");
1642
1643 let gff = utp.to_gff();
1644 let from_gff = rakata_formats::write_gff_to_vec(&gff).expect("gff encode");
1645 assert_eq!(from_utp, from_gff);
1646 }
1647
1648 #[test]
1649 fn schema_field_count() {
1650 assert_eq!(Utp::schema().len(), 71);
1651 }
1652
1653 #[test]
1654 fn schema_no_duplicate_labels() {
1655 let schema = Utp::schema();
1656 let mut labels: Vec<&str> = schema.iter().map(|f| f.label).collect();
1657 labels.sort();
1658 let before = labels.len();
1659 labels.dedup();
1660 assert_eq!(before, labels.len(), "duplicate labels in UTP schema");
1661 }
1662
1663 #[test]
1664 fn schema_item_list_has_children() {
1665 let item_list = Utp::schema()
1666 .iter()
1667 .find(|f| f.label == "ItemList")
1668 .expect("test fixture must be valid");
1669 assert!(item_list.children.is_some());
1670 assert_eq!(
1671 item_list
1672 .children
1673 .expect("test fixture must be valid")
1674 .len(),
1675 7
1676 );
1677 }
1678}