1use std::io::{Cursor, Read, Write};
21
22use crate::gff_helpers::{
23 get_bool, get_f32, get_i16, get_i8, get_locstring, get_resref, get_string, get_u16, get_u32,
24 get_u8, upsert_field,
25};
26use crate::shared::{CommonTrapScripts, TrapSettings};
27use rakata_core::{ResRef, StrRef};
28use rakata_formats::{
29 gff_schema::{FieldConstraint, FieldSchema, GffSchema, GffType},
30 read_gff, read_gff_from_bytes, write_gff, Gff, GffBinaryError, GffLocalizedString, GffStruct,
31 GffValue,
32};
33use thiserror::Error;
34
35#[derive(Debug, Clone, PartialEq)]
37pub struct Utd {
38 pub template_resref: ResRef,
40 pub tag: String,
42 pub name: GffLocalizedString,
44 pub description: GffLocalizedString,
46 pub comment: String,
48 pub conversation: ResRef,
50 pub faction_id: u32,
52 pub appearance_id: u8,
54 pub unused_appearance_id: u32,
56 pub open_state: u8,
58 pub animation_state: u8,
60 pub auto_remove_key: bool,
62 pub bearing: f32,
64 pub key_name: String,
66 pub key_required: bool,
68 pub lockable: bool,
70 pub locked: bool,
72 pub open_lock_dc: u8,
74 pub close_lock_dc: u8,
76 pub secret_door_dc: u8,
78 pub open_lock_diff: u8,
80 pub open_lock_diff_mod: i8,
82 pub current_hp: i16,
84 pub maximum_hp: i16,
86 pub hardness: u8,
88 pub fortitude: u8,
90 pub reflex: u8,
92 pub will: u8,
94 pub plot: bool,
96 pub invulnerable: bool,
98 pub min1_hp: bool,
100 pub is_static: bool,
102 pub not_blastable: bool,
104 pub interruptable: bool,
106 pub portrait_id: u16,
108 pub portrait: ResRef,
110 pub palette_id: u8,
112 pub trap_detectable: bool,
114 pub trap_detect_dc: u8,
116 pub trap_disarmable: bool,
118 pub trap_disarm_dc: u8,
120 pub trap_flag: u8,
122 pub trap_one_shot: bool,
124 pub trap_type: u8,
126 pub on_closed: ResRef,
128 pub on_damaged: ResRef,
130 pub on_death: ResRef,
132 pub on_disarm: ResRef,
134 pub on_heartbeat: ResRef,
136 pub on_lock: ResRef,
138 pub on_melee_attacked: ResRef,
140 pub on_open: ResRef,
142 pub on_spell_cast_at: ResRef,
144 pub on_trap_triggered: ResRef,
146 pub on_unlock: ResRef,
148 pub on_user_defined: ResRef,
150 pub on_click: ResRef,
152 pub on_fail_to_open: ResRef,
154 pub on_dialog: ResRef,
156 pub linked_to_flags: u8,
158 pub linked_to: String,
160 pub linked_to_module: ResRef,
162 pub transition_destination: GffLocalizedString,
164 pub loadscreen_id: u16,
166}
167
168impl Default for Utd {
169 fn default() -> Self {
170 Self {
171 template_resref: ResRef::blank(),
172 tag: String::new(),
173 name: GffLocalizedString::new(StrRef::invalid()),
174 description: GffLocalizedString::new(StrRef::invalid()),
175 comment: String::new(),
176 conversation: ResRef::blank(),
177 faction_id: 0,
178 appearance_id: 0,
179 unused_appearance_id: 0,
180 open_state: 0,
181 animation_state: 0,
182 auto_remove_key: false,
183 bearing: 0.0,
184 key_name: String::new(),
185 key_required: false,
186 lockable: false,
187 locked: false,
188 open_lock_dc: 0,
189 close_lock_dc: 0,
190 secret_door_dc: 0,
191 open_lock_diff: 0,
192 open_lock_diff_mod: 0,
193 current_hp: 0,
194 maximum_hp: 0,
195 hardness: 0,
196 fortitude: 0,
197 reflex: 0,
198 will: 0,
199 plot: false,
200 invulnerable: false,
201 min1_hp: false,
202 is_static: false,
203 not_blastable: false,
204 interruptable: false,
205 portrait_id: 0,
206 portrait: ResRef::blank(),
207 palette_id: 0,
208 trap_detectable: false,
209 trap_detect_dc: 0,
210 trap_disarmable: false,
211 trap_disarm_dc: 0,
212 trap_flag: 0,
213 trap_one_shot: false,
214 trap_type: 0,
215 on_closed: ResRef::blank(),
216 on_damaged: ResRef::blank(),
217 on_death: ResRef::blank(),
218 on_disarm: ResRef::blank(),
219 on_heartbeat: ResRef::blank(),
220 on_lock: ResRef::blank(),
221 on_melee_attacked: ResRef::blank(),
222 on_open: ResRef::blank(),
223 on_spell_cast_at: ResRef::blank(),
224 on_trap_triggered: ResRef::blank(),
225 on_unlock: ResRef::blank(),
226 on_user_defined: ResRef::blank(),
227 on_click: ResRef::blank(),
228 on_fail_to_open: ResRef::blank(),
229 on_dialog: ResRef::blank(),
230 linked_to_flags: 0,
231 linked_to: String::new(),
232 linked_to_module: ResRef::blank(),
233 transition_destination: GffLocalizedString::new(StrRef::invalid()),
234 loadscreen_id: 0,
235 }
236 }
237}
238
239impl Utd {
240 pub fn new() -> Self {
242 Self::default()
243 }
244
245 pub fn trap_settings(&self) -> TrapSettings {
247 TrapSettings {
248 detectable: self.trap_detectable,
249 detect_dc: self.trap_detect_dc,
250 disarmable: self.trap_disarmable,
251 disarm_dc: self.trap_disarm_dc,
252 flag: self.trap_flag,
253 one_shot: self.trap_one_shot,
254 trap_type: self.trap_type,
255 }
256 }
257
258 pub fn set_trap_settings(&mut self, trap: TrapSettings) {
260 self.trap_detectable = trap.detectable;
261 self.trap_detect_dc = trap.detect_dc;
262 self.trap_disarmable = trap.disarmable;
263 self.trap_disarm_dc = trap.disarm_dc;
264 self.trap_flag = trap.flag;
265 self.trap_one_shot = trap.one_shot;
266 self.trap_type = trap.trap_type;
267 }
268
269 pub fn common_trap_scripts(&self) -> CommonTrapScripts {
271 CommonTrapScripts {
272 on_closed: self.on_closed,
273 on_damaged: self.on_damaged,
274 on_death: self.on_death,
275 on_disarm: self.on_disarm,
276 on_heartbeat: self.on_heartbeat,
277 on_lock: self.on_lock,
278 on_melee_attacked: self.on_melee_attacked,
279 on_open: self.on_open,
280 on_spell_cast_at: self.on_spell_cast_at,
281 on_trap_triggered: self.on_trap_triggered,
282 on_unlock: self.on_unlock,
283 on_user_defined: self.on_user_defined,
284 on_fail_to_open: self.on_fail_to_open,
285 }
286 }
287
288 pub fn set_common_trap_scripts(&mut self, scripts: CommonTrapScripts) {
290 self.on_closed = scripts.on_closed;
291 self.on_damaged = scripts.on_damaged;
292 self.on_death = scripts.on_death;
293 self.on_disarm = scripts.on_disarm;
294 self.on_heartbeat = scripts.on_heartbeat;
295 self.on_lock = scripts.on_lock;
296 self.on_melee_attacked = scripts.on_melee_attacked;
297 self.on_open = scripts.on_open;
298 self.on_spell_cast_at = scripts.on_spell_cast_at;
299 self.on_trap_triggered = scripts.on_trap_triggered;
300 self.on_unlock = scripts.on_unlock;
301 self.on_user_defined = scripts.on_user_defined;
302 self.on_fail_to_open = scripts.on_fail_to_open;
303 }
304
305 pub fn from_gff(gff: &Gff) -> Result<Self, UtdError> {
307 if gff.file_type != *b"UTD " && gff.file_type != *b"GFF " {
308 return Err(UtdError::UnsupportedFileType(gff.file_type));
309 }
310
311 let root = &gff.root;
312
313 if matches!(root.field("TransitionDestination"), Some(value) if !matches!(value, GffValue::LocalizedString(_)))
314 {
315 return Err(UtdError::TypeMismatch {
316 field: "TransitionDestination",
317 expected: "LocalizedString",
318 });
319 }
320 if matches!(root.field("TransDest"), Some(value) if !matches!(value, GffValue::LocalizedString(_)))
321 {
322 return Err(UtdError::TypeMismatch {
323 field: "TransDest",
324 expected: "LocalizedString",
325 });
326 }
327
328 let plot = get_bool(root, "Plot").unwrap_or(false);
329 let invulnerable = get_bool(root, "Invulnerable").unwrap_or(plot);
331
332 let trap = TrapSettings::read(|label| get_bool(root, label), |label| get_u8(root, label));
333 let common_scripts = CommonTrapScripts::read(|label| get_resref(root, label));
334
335 Ok(Self {
336 template_resref: get_resref(root, "TemplateResRef").unwrap_or_default(),
337 tag: get_string(root, "Tag").unwrap_or_default(),
338 name: get_locstring(root, "LocName")
339 .cloned()
340 .unwrap_or_else(|| GffLocalizedString::new(StrRef::invalid())),
341 description: get_locstring(root, "Description")
342 .cloned()
343 .unwrap_or_else(|| GffLocalizedString::new(StrRef::invalid())),
344 comment: get_string(root, "Comment").unwrap_or_default(),
345 conversation: get_resref(root, "Conversation").unwrap_or_default(),
346 faction_id: get_u32(root, "Faction").unwrap_or(0),
347 appearance_id: get_u8(root, "GenericType").unwrap_or(0),
348 unused_appearance_id: get_u32(root, "Appearance").unwrap_or(0),
349 open_state: get_u8(root, "OpenState").unwrap_or(0),
350 animation_state: get_u8(root, "AnimationState").unwrap_or(0),
351 auto_remove_key: get_bool(root, "AutoRemoveKey").unwrap_or(false),
352 bearing: get_f32(root, "Bearing").unwrap_or(0.0),
353 key_name: get_string(root, "KeyName").unwrap_or_default(),
354 key_required: get_bool(root, "KeyRequired").unwrap_or(false),
355 lockable: get_bool(root, "Lockable").unwrap_or(false),
356 locked: get_bool(root, "Locked").unwrap_or(false),
357 open_lock_dc: get_u8(root, "OpenLockDC").unwrap_or(0),
358 close_lock_dc: get_u8(root, "CloseLockDC").unwrap_or(0),
359 secret_door_dc: get_u8(root, "SecretDoorDC").unwrap_or(0),
360 open_lock_diff: get_u8(root, "OpenLockDiff").unwrap_or(0),
361 open_lock_diff_mod: get_i8(root, "OpenLockDiffMod").unwrap_or(0),
362 current_hp: get_i16(root, "CurrentHP").unwrap_or(0),
363 maximum_hp: get_i16(root, "HP").unwrap_or(0),
364 hardness: get_u8(root, "Hardness").unwrap_or(0),
365 fortitude: get_u8(root, "Fort").unwrap_or(0),
366 reflex: get_u8(root, "Ref").unwrap_or(0),
367 will: get_u8(root, "Will").unwrap_or(0),
368 plot,
369 invulnerable,
370 min1_hp: get_bool(root, "Min1HP").unwrap_or(false),
371 is_static: get_bool(root, "Static").unwrap_or(false),
372 not_blastable: get_bool(root, "NotBlastable").unwrap_or(false),
373 interruptable: get_bool(root, "Interruptable").unwrap_or(false),
374 portrait_id: get_u16(root, "PortraitId").unwrap_or(0),
375 portrait: get_resref(root, "Portrait").unwrap_or_default(),
376 palette_id: get_u8(root, "PaletteID").unwrap_or(0),
377 trap_detectable: trap.detectable,
378 trap_detect_dc: trap.detect_dc,
379 trap_disarmable: trap.disarmable,
380 trap_disarm_dc: trap.disarm_dc,
381 trap_flag: trap.flag,
382 trap_one_shot: trap.one_shot,
383 trap_type: trap.trap_type,
384 on_closed: common_scripts.on_closed,
385 on_damaged: common_scripts.on_damaged,
386 on_death: common_scripts.on_death,
387 on_disarm: common_scripts.on_disarm,
388 on_heartbeat: common_scripts.on_heartbeat,
389 on_lock: common_scripts.on_lock,
390 on_melee_attacked: common_scripts.on_melee_attacked,
391 on_open: common_scripts.on_open,
392 on_spell_cast_at: common_scripts.on_spell_cast_at,
393 on_trap_triggered: common_scripts.on_trap_triggered,
394 on_unlock: common_scripts.on_unlock,
395 on_user_defined: common_scripts.on_user_defined,
396 on_click: get_resref(root, "OnClick").unwrap_or_default(),
397 on_fail_to_open: common_scripts.on_fail_to_open,
398 on_dialog: get_resref(root, "OnDialog").unwrap_or_default(),
399 linked_to_flags: get_u8(root, "LinkedToFlags").unwrap_or(0),
400 linked_to: get_string(root, "LinkedTo").unwrap_or_default(),
401 linked_to_module: get_resref(root, "LinkedToModule").unwrap_or_default(),
402 transition_destination: get_locstring(root, "TransitionDestination")
403 .or_else(|| get_locstring(root, "TransDest"))
404 .cloned()
405 .unwrap_or_else(|| GffLocalizedString::new(StrRef::invalid())),
406 loadscreen_id: get_u16(root, "LoadScreenID").unwrap_or(0),
407 })
408 }
409
410 pub fn to_gff(&self) -> Gff {
412 let mut root = GffStruct::new(-1);
413
414 upsert_field(
415 &mut root,
416 "TemplateResRef",
417 GffValue::ResRef(self.template_resref),
418 );
419 upsert_field(&mut root, "Tag", GffValue::String(self.tag.clone()));
420 upsert_field(
421 &mut root,
422 "LocName",
423 GffValue::LocalizedString(self.name.clone()),
424 );
425 upsert_field(
426 &mut root,
427 "Description",
428 GffValue::LocalizedString(self.description.clone()),
429 );
430 upsert_field(&mut root, "Comment", GffValue::String(self.comment.clone()));
431 upsert_field(
432 &mut root,
433 "Conversation",
434 GffValue::ResRef(self.conversation),
435 );
436
437 upsert_field(&mut root, "Faction", GffValue::UInt32(self.faction_id));
438 upsert_field(
439 &mut root,
440 "GenericType",
441 GffValue::UInt8(self.appearance_id),
442 );
443 upsert_field(
444 &mut root,
445 "Appearance",
446 GffValue::UInt32(self.unused_appearance_id),
447 );
448 upsert_field(&mut root, "OpenState", GffValue::UInt8(self.open_state));
449 upsert_field(
450 &mut root,
451 "AnimationState",
452 GffValue::UInt8(self.animation_state),
453 );
454 upsert_field(
455 &mut root,
456 "AutoRemoveKey",
457 GffValue::UInt8(u8::from(self.auto_remove_key)),
458 );
459 upsert_field(&mut root, "Bearing", GffValue::Single(self.bearing));
460
461 upsert_field(
462 &mut root,
463 "KeyName",
464 GffValue::String(self.key_name.clone()),
465 );
466 upsert_field(
467 &mut root,
468 "KeyRequired",
469 GffValue::UInt8(u8::from(self.key_required)),
470 );
471 upsert_field(
472 &mut root,
473 "Lockable",
474 GffValue::UInt8(u8::from(self.lockable)),
475 );
476 upsert_field(&mut root, "Locked", GffValue::UInt8(u8::from(self.locked)));
477 upsert_field(&mut root, "OpenLockDC", GffValue::UInt8(self.open_lock_dc));
478 upsert_field(
479 &mut root,
480 "CloseLockDC",
481 GffValue::UInt8(self.close_lock_dc),
482 );
483 upsert_field(
484 &mut root,
485 "SecretDoorDC",
486 GffValue::UInt8(self.secret_door_dc),
487 );
488 upsert_field(
489 &mut root,
490 "OpenLockDiff",
491 GffValue::UInt8(self.open_lock_diff),
492 );
493 upsert_field(
494 &mut root,
495 "OpenLockDiffMod",
496 GffValue::Int8(self.open_lock_diff_mod),
497 );
498
499 upsert_field(&mut root, "CurrentHP", GffValue::Int16(self.current_hp));
500 upsert_field(&mut root, "HP", GffValue::Int16(self.maximum_hp));
501 upsert_field(&mut root, "Hardness", GffValue::UInt8(self.hardness));
502 upsert_field(&mut root, "Fort", GffValue::UInt8(self.fortitude));
503 upsert_field(&mut root, "Ref", GffValue::UInt8(self.reflex));
504 upsert_field(&mut root, "Will", GffValue::UInt8(self.will));
505
506 upsert_field(&mut root, "Plot", GffValue::UInt8(u8::from(self.plot)));
507 upsert_field(
508 &mut root,
509 "Invulnerable",
510 GffValue::UInt8(u8::from(self.invulnerable)),
511 );
512 upsert_field(&mut root, "Min1HP", GffValue::UInt8(u8::from(self.min1_hp)));
513 upsert_field(
514 &mut root,
515 "Static",
516 GffValue::UInt8(u8::from(self.is_static)),
517 );
518 upsert_field(
519 &mut root,
520 "NotBlastable",
521 GffValue::UInt8(u8::from(self.not_blastable)),
522 );
523 upsert_field(
524 &mut root,
525 "Interruptable",
526 GffValue::UInt8(u8::from(self.interruptable)),
527 );
528 upsert_field(&mut root, "PortraitId", GffValue::UInt16(self.portrait_id));
529 upsert_field(&mut root, "Portrait", GffValue::ResRef(self.portrait));
530 upsert_field(&mut root, "PaletteID", GffValue::UInt8(self.palette_id));
531
532 self.trap_settings()
533 .write(|label, value| upsert_field(&mut root, label, value));
534 self.common_trap_scripts()
535 .write(|label, value| upsert_field(&mut root, label, value));
536 upsert_field(&mut root, "OnClick", GffValue::ResRef(self.on_click));
537 upsert_field(
538 &mut root,
539 "OnFailToOpen",
540 GffValue::ResRef(self.on_fail_to_open),
541 );
542 upsert_field(&mut root, "OnDialog", GffValue::ResRef(self.on_dialog));
543
544 upsert_field(
545 &mut root,
546 "LinkedToFlags",
547 GffValue::UInt8(self.linked_to_flags),
548 );
549 upsert_field(
550 &mut root,
551 "LinkedTo",
552 GffValue::String(self.linked_to.clone()),
553 );
554 upsert_field(
555 &mut root,
556 "LinkedToModule",
557 GffValue::ResRef(self.linked_to_module),
558 );
559 if root.field("TransitionDestination").is_some() {
563 upsert_field(
564 &mut root,
565 "TransitionDestination",
566 GffValue::LocalizedString(self.transition_destination.clone()),
567 );
568 } else if root.field("TransDest").is_some() {
569 upsert_field(
570 &mut root,
571 "TransDest",
572 GffValue::LocalizedString(self.transition_destination.clone()),
573 );
574 }
575 upsert_field(
576 &mut root,
577 "LoadScreenID",
578 GffValue::UInt16(self.loadscreen_id),
579 );
580
581 Gff::new(*b"UTD ", root)
582 }
583}
584
585#[derive(Debug, Error)]
587pub enum UtdError {
588 #[error("unsupported UTD file type: {0:?}")]
590 UnsupportedFileType([u8; 4]),
591 #[error("UTD field `{field}` has incompatible type (expected {expected})")]
593 TypeMismatch {
594 field: &'static str,
596 expected: &'static str,
598 },
599 #[error(transparent)]
601 Gff(#[from] GffBinaryError),
602}
603
604#[cfg_attr(
606 feature = "tracing",
607 tracing::instrument(level = "debug", skip(reader))
608)]
609pub fn read_utd<R: Read>(reader: &mut R) -> Result<Utd, UtdError> {
610 let gff = read_gff(reader)?;
611 Utd::from_gff(&gff)
612}
613
614#[cfg_attr(
616 feature = "tracing",
617 tracing::instrument(level = "debug", skip(bytes), fields(bytes_len = bytes.len()))
618)]
619pub fn read_utd_from_bytes(bytes: &[u8]) -> Result<Utd, UtdError> {
620 let gff = read_gff_from_bytes(bytes)?;
621 Utd::from_gff(&gff)
622}
623
624#[cfg_attr(
626 feature = "tracing",
627 tracing::instrument(level = "debug", skip(writer, utd))
628)]
629pub fn write_utd<W: Write>(writer: &mut W, utd: &Utd) -> Result<(), UtdError> {
630 let gff = utd.to_gff();
631 write_gff(writer, &gff)?;
632 Ok(())
633}
634
635#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(utd)))]
637pub fn write_utd_to_vec(utd: &Utd) -> Result<Vec<u8>, UtdError> {
638 let mut cursor = Cursor::new(Vec::new());
639 write_utd(&mut cursor, utd)?;
640 Ok(cursor.into_inner())
641}
642
643impl GffSchema for Utd {
644 fn schema() -> &'static [FieldSchema] {
645 static SCHEMA: &[FieldSchema] = &[
646 FieldSchema {
648 label: "Tag",
649 expected_type: GffType::String,
650 required: false,
651 children: None,
652 constraint: None,
653 },
654 FieldSchema {
655 label: "LocName",
656 expected_type: GffType::LocalizedString,
657 required: false,
658 children: None,
659 constraint: None,
660 },
661 FieldSchema {
662 label: "Description",
663 expected_type: GffType::LocalizedString,
664 required: false,
665 children: None,
666 constraint: None,
667 },
668 FieldSchema {
669 label: "Conversation",
670 expected_type: GffType::ResRef,
671 required: false,
672 children: None,
673 constraint: None,
674 },
675 FieldSchema {
676 label: "Faction",
677 expected_type: GffType::UInt32,
678 required: false,
679 children: None,
680 constraint: None,
681 },
682 FieldSchema {
684 label: "Appearance",
685 expected_type: GffType::UInt32,
686 required: false,
687 children: None,
688 constraint: Some(FieldConstraint::RangeInt(0, 255)),
689 },
690 FieldSchema {
691 label: "GenericType",
692 expected_type: GffType::UInt8,
693 required: false,
694 children: None,
695 constraint: None,
696 },
697 FieldSchema {
698 label: "OpenState",
699 expected_type: GffType::UInt8,
700 required: false,
701 children: None,
702 constraint: None,
703 },
704 FieldSchema {
706 label: "HP",
707 expected_type: GffType::Int16,
708 required: false,
709 children: None,
710 constraint: None,
711 },
712 FieldSchema {
713 label: "CurrentHP",
714 expected_type: GffType::Int16,
715 required: false,
716 children: None,
717 constraint: None,
718 },
719 FieldSchema {
720 label: "Hardness",
721 expected_type: GffType::UInt8,
722 required: false,
723 children: None,
724 constraint: None,
725 },
726 FieldSchema {
727 label: "Fort",
728 expected_type: GffType::UInt8,
729 required: false,
730 children: None,
731 constraint: None,
732 },
733 FieldSchema {
734 label: "Ref",
735 expected_type: GffType::UInt8,
736 required: false,
737 children: None,
738 constraint: None,
739 },
740 FieldSchema {
741 label: "Will",
742 expected_type: GffType::UInt8,
743 required: false,
744 children: None,
745 constraint: None,
746 },
747 FieldSchema {
749 label: "Plot",
750 expected_type: GffType::UInt8,
751 required: false,
752 children: None,
753 constraint: None,
754 },
755 FieldSchema {
756 label: "Static",
757 expected_type: GffType::UInt8,
758 required: false,
759 children: None,
760 constraint: None,
761 },
762 FieldSchema {
763 label: "Invulnerable",
764 expected_type: GffType::UInt8,
765 required: false,
766 children: None,
767 constraint: None,
768 },
769 FieldSchema {
770 label: "Min1HP",
771 expected_type: GffType::UInt8,
772 required: false,
773 children: None,
774 constraint: None,
775 },
776 FieldSchema {
778 label: "Locked",
779 expected_type: GffType::UInt8,
780 required: false,
781 children: None,
782 constraint: None,
783 },
784 FieldSchema {
785 label: "Lockable",
786 expected_type: GffType::UInt8,
787 required: false,
788 children: None,
789 constraint: None,
790 },
791 FieldSchema {
792 label: "OpenLockDC",
793 expected_type: GffType::UInt8,
794 required: false,
795 children: None,
796 constraint: None,
797 },
798 FieldSchema {
799 label: "CloseLockDC",
800 expected_type: GffType::UInt8,
801 required: false,
802 children: None,
803 constraint: None,
804 },
805 FieldSchema {
806 label: "SecretDoorDC",
807 expected_type: GffType::UInt8,
808 required: false,
809 children: None,
810 constraint: None,
811 },
812 FieldSchema {
813 label: "KeyName",
814 expected_type: GffType::String,
815 required: false,
816 children: None,
817 constraint: None,
818 },
819 FieldSchema {
820 label: "KeyRequired",
821 expected_type: GffType::UInt8,
822 required: false,
823 children: None,
824 constraint: None,
825 },
826 FieldSchema {
827 label: "AutoRemoveKey",
828 expected_type: GffType::UInt8,
829 required: false,
830 children: None,
831 constraint: None,
832 },
833 FieldSchema {
835 label: "Bearing",
836 expected_type: GffType::Single,
837 required: false,
838 children: None,
839 constraint: None,
840 },
841 FieldSchema {
843 label: "PortraitId",
844 expected_type: GffType::UInt16,
845 required: false,
846 children: None,
847 constraint: None,
848 },
849 FieldSchema {
850 label: "Portrait",
851 expected_type: GffType::ResRef,
852 required: false,
853 children: None,
854 constraint: None,
855 },
856 FieldSchema {
858 label: "LinkedToFlags",
859 expected_type: GffType::UInt8,
860 required: false,
861 children: None,
862 constraint: None,
863 },
864 FieldSchema {
865 label: "LinkedTo",
866 expected_type: GffType::String,
867 required: false,
868 children: None,
869 constraint: None,
870 },
871 FieldSchema {
872 label: "LinkedToModule",
873 expected_type: GffType::ResRef,
874 required: false,
875 children: None,
876 constraint: None,
877 },
878 FieldSchema {
879 label: "TransitionDestination",
880 expected_type: GffType::LocalizedString,
881 required: false,
882 children: None,
883 constraint: None,
884 },
885 FieldSchema {
886 label: "LoadScreenID",
887 expected_type: GffType::UInt16,
888 required: false,
889 children: None,
890 constraint: None,
891 },
892 FieldSchema {
894 label: "TrapType",
895 expected_type: GffType::UInt8,
896 required: false,
897 children: None,
898 constraint: None,
899 },
900 FieldSchema {
901 label: "TrapDisarmable",
902 expected_type: GffType::UInt8,
903 required: false,
904 children: None,
905 constraint: None,
906 },
907 FieldSchema {
908 label: "TrapDetectable",
909 expected_type: GffType::UInt8,
910 required: false,
911 children: None,
912 constraint: None,
913 },
914 FieldSchema {
915 label: "DisarmDC",
916 expected_type: GffType::UInt8,
917 required: false,
918 children: None,
919 constraint: None,
920 },
921 FieldSchema {
922 label: "TrapDetectDC",
923 expected_type: GffType::UInt8,
924 required: false,
925 children: None,
926 constraint: None,
927 },
928 FieldSchema {
929 label: "TrapFlag",
930 expected_type: GffType::UInt8,
931 required: false,
932 children: None,
933 constraint: None,
934 },
935 FieldSchema {
936 label: "TrapOneShot",
937 expected_type: GffType::UInt8,
938 required: false,
939 children: None,
940 constraint: None,
941 },
942 FieldSchema {
944 label: "OnClosed",
945 expected_type: GffType::ResRef,
946 required: false,
947 children: None,
948 constraint: None,
949 },
950 FieldSchema {
951 label: "OnDamaged",
952 expected_type: GffType::ResRef,
953 required: false,
954 children: None,
955 constraint: None,
956 },
957 FieldSchema {
958 label: "OnDeath",
959 expected_type: GffType::ResRef,
960 required: false,
961 children: None,
962 constraint: None,
963 },
964 FieldSchema {
965 label: "OnDisarm",
966 expected_type: GffType::ResRef,
967 required: false,
968 children: None,
969 constraint: None,
970 },
971 FieldSchema {
972 label: "OnHeartbeat",
973 expected_type: GffType::ResRef,
974 required: false,
975 children: None,
976 constraint: None,
977 },
978 FieldSchema {
979 label: "OnLock",
980 expected_type: GffType::ResRef,
981 required: false,
982 children: None,
983 constraint: None,
984 },
985 FieldSchema {
986 label: "OnMeleeAttacked",
987 expected_type: GffType::ResRef,
988 required: false,
989 children: None,
990 constraint: None,
991 },
992 FieldSchema {
993 label: "OnOpen",
994 expected_type: GffType::ResRef,
995 required: false,
996 children: None,
997 constraint: None,
998 },
999 FieldSchema {
1000 label: "OnSpellCastAt",
1001 expected_type: GffType::ResRef,
1002 required: false,
1003 children: None,
1004 constraint: None,
1005 },
1006 FieldSchema {
1007 label: "OnTrapTriggered",
1008 expected_type: GffType::ResRef,
1009 required: false,
1010 children: None,
1011 constraint: None,
1012 },
1013 FieldSchema {
1014 label: "OnUnlock",
1015 expected_type: GffType::ResRef,
1016 required: false,
1017 children: None,
1018 constraint: None,
1019 },
1020 FieldSchema {
1021 label: "OnUserDefined",
1022 expected_type: GffType::ResRef,
1023 required: false,
1024 children: None,
1025 constraint: None,
1026 },
1027 FieldSchema {
1028 label: "OnClick",
1029 expected_type: GffType::ResRef,
1030 required: false,
1031 children: None,
1032 constraint: None,
1033 },
1034 FieldSchema {
1035 label: "OnFailToOpen",
1036 expected_type: GffType::ResRef,
1037 required: false,
1038 children: None,
1039 constraint: None,
1040 },
1041 FieldSchema {
1042 label: "OnDialog",
1043 expected_type: GffType::ResRef,
1044 required: false,
1045 children: None,
1046 constraint: None,
1047 },
1048 FieldSchema {
1050 label: "TemplateResRef",
1051 expected_type: GffType::ResRef,
1052 required: false,
1053 children: None,
1054 constraint: None,
1055 },
1056 FieldSchema {
1057 label: "Comment",
1058 expected_type: GffType::String,
1059 required: false,
1060 children: None,
1061 constraint: None,
1062 },
1063 FieldSchema {
1064 label: "PaletteID",
1065 expected_type: GffType::UInt8,
1066 required: false,
1067 children: None,
1068 constraint: None,
1069 },
1070 FieldSchema {
1071 label: "AnimationState",
1072 expected_type: GffType::UInt8,
1073 required: false,
1074 children: None,
1075 constraint: None,
1076 },
1077 FieldSchema {
1078 label: "OpenLockDiff",
1079 expected_type: GffType::UInt8,
1080 required: false,
1081 children: None,
1082 constraint: None,
1083 },
1084 FieldSchema {
1085 label: "OpenLockDiffMod",
1086 expected_type: GffType::Int8,
1087 required: false,
1088 children: None,
1089 constraint: None,
1090 },
1091 FieldSchema {
1092 label: "NotBlastable",
1093 expected_type: GffType::UInt8,
1094 required: false,
1095 children: None,
1096 constraint: None,
1097 },
1098 FieldSchema {
1099 label: "Interruptable",
1100 expected_type: GffType::UInt8,
1101 required: false,
1102 children: None,
1103 constraint: None,
1104 },
1105 ];
1106 SCHEMA
1107 }
1108}
1109
1110#[cfg(test)]
1111mod tests {
1112 use super::*;
1113
1114 const TEST_UTD: &[u8] = include_bytes!(concat!(
1115 env!("CARGO_MANIFEST_DIR"),
1116 "/../../fixtures/test.utd"
1117 ));
1118
1119 #[test]
1120 fn reads_core_utd_fields_from_fixture() {
1121 let utd = read_utd_from_bytes(TEST_UTD).expect("fixture must parse");
1122
1123 assert_eq!(utd.tag, "TelosDoor13");
1124 assert_eq!(utd.template_resref, "door_tel014");
1125 assert_eq!(utd.name.string_ref.raw(), 123_731);
1126 assert_eq!(utd.description.string_ref.raw(), -1);
1127 assert!(utd.auto_remove_key);
1128 assert_eq!(utd.close_lock_dc, 0);
1129 assert_eq!(utd.conversation, "convoresref");
1130 assert!(utd.interruptable);
1131 assert_eq!(utd.faction_id, 1);
1132 assert!(utd.plot);
1133 assert!(utd.not_blastable);
1134 assert!(utd.min1_hp);
1135 assert!(utd.key_required);
1136 assert!(utd.lockable);
1137 assert!(utd.locked);
1138 assert_eq!(utd.open_lock_dc, 28);
1139 assert_eq!(utd.open_lock_diff, 1);
1140 assert_eq!(utd.open_lock_diff_mod, 1);
1141 assert_eq!(utd.portrait_id, 0);
1142 assert!(utd.trap_detectable);
1143 assert_eq!(utd.trap_detect_dc, 0);
1144 assert!(utd.trap_disarmable);
1145 assert_eq!(utd.trap_disarm_dc, 28);
1146 assert_eq!(utd.trap_flag, 0);
1147 assert!(utd.trap_one_shot);
1148 assert_eq!(utd.trap_type, 2);
1149 assert_eq!(utd.key_name, "keyname");
1150 assert_eq!(utd.animation_state, 1);
1151 assert_eq!(utd.unused_appearance_id, 1);
1152 assert_eq!(utd.maximum_hp, 20);
1153 assert_eq!(utd.current_hp, 60);
1154 assert_eq!(utd.hardness, 5);
1155 assert_eq!(utd.fortitude, 28);
1156 assert_eq!(utd.reflex, 0);
1157 assert_eq!(utd.will, 0);
1158 assert_eq!(utd.on_closed, "onclosed");
1159 assert_eq!(utd.on_damaged, "ondamaged");
1160 assert_eq!(utd.on_death, "ondeath");
1161 assert_eq!(utd.on_disarm, "ondisarm");
1162 assert_eq!(utd.on_heartbeat, "onheartbeat");
1163 assert_eq!(utd.on_lock, "onlock");
1164 assert_eq!(utd.on_melee_attacked, "onmeleeattacked");
1165 assert_eq!(utd.on_open, "onopen");
1166 assert_eq!(utd.on_spell_cast_at, "onspellcastat");
1167 assert_eq!(utd.on_trap_triggered, "ontraptriggered");
1168 assert_eq!(utd.on_unlock, "onunlock");
1169 assert_eq!(utd.on_user_defined, "onuserdefined");
1170 assert_eq!(utd.loadscreen_id, 0);
1171 assert_eq!(utd.appearance_id, 110);
1172 assert!(utd.is_static);
1173 assert_eq!(utd.open_state, 1);
1174 assert_eq!(utd.on_click, "onclick");
1175 assert_eq!(utd.on_fail_to_open, "onfailtoopen");
1176 assert_eq!(utd.comment, "abcdefg");
1177 assert_eq!(utd.palette_id, 1);
1178 }
1179
1180 #[test]
1181 fn all_fields_survive_typed_roundtrip() {
1182 let utd = read_utd_from_bytes(TEST_UTD).expect("fixture must parse");
1183 let bytes = write_utd_to_vec(&utd).expect("write succeeds");
1184 let reparsed = read_utd_from_bytes(&bytes).expect("reparse succeeds");
1185 assert_eq!(reparsed, utd);
1186 }
1187
1188 #[test]
1189 fn typed_edits_roundtrip_through_gff_writer() {
1190 let mut utd = read_utd_from_bytes(TEST_UTD).expect("fixture must parse");
1191 utd.tag = "TelosDoor13_Rust".into();
1192 utd.open_state = 2;
1193 utd.on_open = ResRef::new("rust_on_open").expect("valid resref literal");
1194
1195 let bytes = write_utd_to_vec(&utd).expect("write succeeds");
1196 let reparsed = read_utd_from_bytes(&bytes).expect("reparse succeeds");
1197
1198 assert_eq!(reparsed.tag, "TelosDoor13_Rust");
1199 assert_eq!(reparsed.open_state, 2);
1200 assert_eq!(reparsed.on_open, "rust_on_open");
1201 }
1202
1203 #[test]
1204 fn read_utd_from_reader_matches_bytes_path() {
1205 let mut cursor = Cursor::new(TEST_UTD);
1206 let from_reader = read_utd(&mut cursor).expect("reader parse succeeds");
1207 let from_bytes = read_utd_from_bytes(TEST_UTD).expect("bytes parse succeeds");
1208
1209 assert_eq!(from_reader, from_bytes);
1210 }
1211
1212 #[test]
1213 fn rejects_non_utd_file_type() {
1214 let mut gff = read_gff_from_bytes(TEST_UTD).expect("fixture must parse");
1215 gff.file_type = *b"UTC ";
1216
1217 let err = Utd::from_gff(&gff).expect_err("UTC must be rejected as UTD input");
1218 assert!(matches!(
1219 err,
1220 UtdError::UnsupportedFileType(file_type) if file_type == *b"UTC "
1221 ));
1222 }
1223
1224 #[test]
1225 fn type_mismatch_on_transition_destination_is_error() {
1226 let mut gff = read_gff_from_bytes(TEST_UTD).expect("fixture must parse");
1227 gff.root.push_field("TransDest", GffValue::UInt32(99));
1228
1229 let err = Utd::from_gff(&gff).expect_err("type mismatch must be rejected");
1230 assert!(matches!(
1231 err,
1232 UtdError::TypeMismatch {
1233 field: "TransDest",
1234 expected: "LocalizedString",
1235 }
1236 ));
1237 }
1238
1239 #[test]
1240 fn write_utd_matches_direct_gff_writer() {
1241 let utd = read_utd_from_bytes(TEST_UTD).expect("fixture must parse");
1242
1243 let via_typed = write_utd_to_vec(&utd).expect("typed write succeeds");
1244
1245 let mut direct = Cursor::new(Vec::new());
1246 write_gff(&mut direct, &utd.to_gff()).expect("direct write succeeds");
1247
1248 assert_eq!(via_typed, direct.into_inner());
1249 }
1250
1251 #[test]
1252 fn schema_field_count() {
1253 assert_eq!(Utd::schema().len(), 64); }
1255
1256 #[test]
1257 fn schema_no_duplicate_labels() {
1258 let schema = Utd::schema();
1259 let mut labels: Vec<&str> = schema.iter().map(|f| f.label).collect();
1260 labels.sort();
1261 let before = labels.len();
1262 labels.dedup();
1263 assert_eq!(before, labels.len(), "duplicate labels in UTD schema");
1264 }
1265}