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