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