1use std::io::{Cursor, Read, Write};
36
37use rakata_core::ResRef;
38use rakata_formats::gff::{upsert_field, GffLabel};
39use rakata_formats::gff_label;
40use rakata_formats::schema::FromGff;
41use rakata_formats::schema::GffScalar;
42use rakata_formats::GENERIC_FILE_TYPE;
43use rakata_formats::{
44 read_gff, read_gff_from_bytes, write_gff, Gff, GffBinaryError, GffLocalizedString, GffModel,
45 GffStruct, GffValue,
46};
47use thiserror::Error;
48
49#[derive(Debug, Clone, PartialEq, GffModel)]
56#[gff_entry(DisplayInactive, wire = u8, stamped)]
57pub struct DlgLinkDisplayInactive {}
58
59#[derive(Debug, Clone, PartialEq, GffModel)]
65#[gff_entry(
66 RepliesList,
67 list = DlgLink,
68 element_extra = DlgLinkDisplayInactive,
69 element_id = positional,
70 unexamined
71)]
72pub struct DlgNodeRepliesTail {}
73
74#[derive(Debug, Clone, PartialEq, GffModel)]
76#[gff_entry(EntriesList, list = DlgLink, element_id = positional, unexamined)]
77pub struct DlgNodeEntriesTail {}
78
79#[derive(Debug, Clone, PartialEq, GffModel)]
81#[gff_entry(IsChild, wire = u8, read_only_dead = "the label string does not exist anywhere in the engine binary, so no read path can name it", unexamined)]
82#[gff_entry(LinkComment, wire = String, read_only_dead = "the label string does not exist anywhere in the engine binary, so no read path can name it", unexamined)]
83pub struct DlgLink {
84 #[gff(Active, stamped)]
86 pub active: ResRef,
87 #[gff(Index, stamped)]
89 pub index: u32,
90 pub display_inactive: bool,
101}
102
103impl DlgLink {
104 fn read_link(structure: &GffStruct) -> Self {
113 Self {
114 display_inactive: structure
115 .field("DisplayInactive")
116 .and_then(<bool as GffScalar>::from_gff_value)
117 .unwrap_or(false),
118 ..Self::read_declared(structure)
119 }
120 }
121
122 fn write_link(&self, structure: &mut GffStruct, carries_display_inactive: bool) {
129 self.write_declared(structure);
130 if carries_display_inactive && self.display_inactive {
131 upsert_field(
132 structure,
133 gff_label!("DisplayInactive"),
134 GffValue::UInt8(u8::from(self.display_inactive)),
135 );
136 }
137 }
138}
139
140#[derive(Debug, Clone, PartialEq, GffModel)]
142pub struct DlgAnimation {
143 #[gff(Participant, unexamined)]
145 pub participant: String,
146 #[gff(Animation, unexamined)]
148 pub animation: u16,
149}
150
151#[derive(Debug, Clone, PartialEq, GffModel)]
153pub struct DlgStunt {
154 #[gff(Participant, unexamined)]
156 pub participant: String,
157 #[gff(StuntModel, unexamined)]
159 pub stunt_model: ResRef,
160}
161
162#[derive(Debug, Clone, PartialEq, GffModel)]
169#[gff_entry(Comment, wire = String, read_only_dead = "the label string does not exist anywhere in the engine binary, so no read path can name it", unexamined)]
170pub struct DlgNode {
171 #[gff(AnimList, unexamined, list = DlgAnimation, element_id = 0)]
173 pub animations: Vec<DlgAnimation>,
174 #[gff(Text, unexamined)]
176 pub text: GffLocalizedString,
177 #[gff(Script, unexamined)]
179 pub script: ResRef,
180 #[gff(Speaker, stamped, omit = audited_constant(1167))]
184 pub speaker: String,
185 #[gff(WaitFlags, stamped)]
187 pub wait_flags: u32,
188 #[gff(Quest, stamped)]
190 pub quest: String,
191 #[gff(QuestEntry, stamped)]
193 pub quest_entry: u32,
194 #[gff(PlotIndex, stamped)]
196 pub plot_index: i32,
197 #[gff(PlotXPPercentage, stamped)]
199 pub plot_xp_percentage: f32,
200 #[gff(Delay, stamped)]
205 pub delay: u32,
206 #[gff(FadeType, stamped)]
208 pub fade_type: u8,
209 #[gff(FadeColor, stamped = [0.0, 0.0, 0.0])]
211 pub fade_color: [f32; 3],
212 #[gff(FadeDelay, stamped)]
214 pub fade_delay: f32,
215 #[gff(FadeLength, stamped)]
217 pub fade_length: f32,
218 #[gff(Sound, unexamined)]
220 pub sound: ResRef,
221 #[gff(VO_ResRef, stamped)]
223 pub vo_resref: ResRef,
224 #[gff(SoundExists, stamped = 128)]
231 pub sound_exists: u8,
232 #[gff(Listener, stamped)]
234 pub listener: String,
235 #[gff(CameraAngle, stamped)]
237 pub camera_angle: u32,
238 #[gff(CameraID, stamped)]
240 pub camera_id: i32,
241 #[gff(CamHeightOffset, stamped)]
243 pub cam_height_offset: f32,
244 #[gff(TarHeightOffset, stamped)]
246 pub tar_height_offset: f32,
247 #[gff(CameraAnimation, stamped)]
249 pub camera_animation: u16,
250 #[gff(CamVidEffect, stamped = -1)]
252 pub cam_vid_effect: i32,
253 #[gff(CamFieldOfView, stamped = -1.0)]
255 pub cam_field_of_view: f32,
256
257 pub links: Vec<DlgLink>,
265}
266
267impl DlgNode {
268 fn read_node(structure: &GffStruct, link_label: &str) -> Self {
275 let links = match structure.field(link_label) {
276 Some(GffValue::List(elements)) => elements.iter().map(DlgLink::read_link).collect(),
277 _ => Vec::new(),
278 };
279 Self {
280 links,
281 ..Self::read_declared(structure)
282 }
283 }
284
285 fn write_node(
287 &self,
288 structure: &mut GffStruct,
289 link_label: GffLabel,
290 links_carry_display_inactive: bool,
291 ) {
292 self.write_declared(structure);
293 let elements: Vec<GffStruct> = self
294 .links
295 .iter()
296 .enumerate()
297 .map(|(index, link)| {
298 let mut element = GffStruct::new(i32::try_from(index).unwrap_or(0));
299 link.write_link(&mut element, links_carry_display_inactive);
300 element
301 })
302 .collect();
303 upsert_field(structure, link_label, GffValue::List(elements));
304 }
305}
306
307pub const SOUND_EXISTS_ABSENT: u8 = 0x80;
314
315#[derive(Debug, Clone, PartialEq, GffModel)]
317#[gff_entry(NumWords, wire = u32, read_only_dead = "the label string does not exist anywhere in the engine binary, so no read path can name it", unexamined)]
318#[gff_entry(VO_ID, wire = String, read_only_dead = "the label string does not exist anywhere in the engine binary, so no read path can name it", unexamined)]
319pub struct Dlg {
320 #[gff(CameraModel, stamped)]
322 pub camera_model: ResRef,
323 #[gff(DelayEntry, stamped)]
326 pub delay_entry: u32,
327 #[gff(DelayReply, stamped)]
330 pub delay_reply: u32,
331 #[gff(EndConversation, stamped)]
333 pub end_conversation: ResRef,
334 #[gff(EndConverAbort, stamped)]
336 pub end_conver_abort: ResRef,
337 #[gff(Skippable, stamped = true)]
339 pub skippable: bool,
340 #[gff(ConversationType, stamped)]
342 pub conversation_type: i32,
343 #[gff(ComputerType, stamped)]
345 pub computer_type: u8,
346 #[gff(AmbientTrack, stamped)]
348 pub ambient_track: ResRef,
349 #[gff(UnequipItems, stamped)]
351 pub unequip_items: bool,
352 #[gff(UnequipHItem, stamped)]
354 pub unequip_h_item: bool,
355 #[gff(AnimatedCut, stamped)]
357 pub animated_cut: bool,
358 #[gff(OldHitCheck, stamped)]
360 pub old_hit_check: bool,
361 #[gff(
363 EntryList,
364 unexamined,
365 list = DlgNode,
366 element_extra = DlgNodeRepliesTail,
367 element_id = positional,
368 manual_read,
369 manual_write
370 )]
371 pub entries: Vec<DlgNode>,
372 #[gff(
374 ReplyList,
375 unexamined,
376 list = DlgNode,
377 element_extra = DlgNodeEntriesTail,
378 element_id = positional,
379 manual_read,
380 manual_write
381 )]
382 pub replies: Vec<DlgNode>,
383 #[gff(
385 StartingList,
386 unexamined,
387 list = DlgLink,
388 element_id = positional,
389 manual_read,
390 manual_write
391 )]
392 pub starting_list: Vec<DlgLink>,
393 #[gff(StuntList, unexamined, list = DlgStunt, element_id = 0)]
395 pub stunt_list: Vec<DlgStunt>,
396}
397
398impl Dlg {
399 pub fn new() -> Self {
401 Self::default()
402 }
403
404 pub fn from_gff(gff: &Gff) -> Result<Self, DlgError> {
411 if gff.file_type != <Dlg as FromGff>::MAGIC && gff.file_type != GENERIC_FILE_TYPE {
412 return Err(DlgError::UnsupportedFileType(gff.file_type));
413 }
414 let root = &gff.root;
415 let nodes = |label: &str, link_label: &'static str| match root.field(label) {
416 Some(GffValue::List(elements)) => elements
417 .iter()
418 .map(|element| DlgNode::read_node(element, link_label))
419 .collect(),
420 _ => Vec::new(),
421 };
422 Ok(Self {
423 starting_list: match root.field("StartingList") {
424 Some(GffValue::List(elements)) => elements.iter().map(DlgLink::read_link).collect(),
425 _ => Vec::new(),
426 },
427 entries: nodes("EntryList", "RepliesList"),
428 replies: nodes("ReplyList", "EntriesList"),
429 ..Self::read_declared(root)
430 })
431 }
432
433 pub fn to_gff(&self) -> Gff {
435 let mut root = GffStruct::new(-1);
436 self.write_declared(&mut root);
437
438 let links: Vec<GffStruct> = self
442 .starting_list
443 .iter()
444 .enumerate()
445 .map(|(index, link)| {
446 let mut element = GffStruct::new(i32::try_from(index).unwrap_or(0));
447 link.write_link(&mut element, false);
448 element
449 })
450 .collect();
451 upsert_field(&mut root, gff_label!("StartingList"), GffValue::List(links));
452
453 for (label, link_label, nodes, display_inactive) in [
454 (
455 gff_label!("EntryList"),
456 gff_label!("RepliesList"),
457 &self.entries,
458 true,
459 ),
460 (
461 gff_label!("ReplyList"),
462 gff_label!("EntriesList"),
463 &self.replies,
464 false,
465 ),
466 ] {
467 let elements: Vec<GffStruct> = nodes
468 .iter()
469 .enumerate()
470 .map(|(index, node)| {
471 let mut element = GffStruct::new(i32::try_from(index).unwrap_or(0));
472 node.write_node(&mut element, link_label, display_inactive);
473 element
474 })
475 .collect();
476 upsert_field(&mut root, label, GffValue::List(elements));
477 }
478
479 Gff::new(*b"DLG ", root)
480 }
481}
482
483#[derive(Debug, Error)]
485pub enum DlgError {
486 #[error("unsupported DLG file type: {0:?}")]
488 UnsupportedFileType([u8; 4]),
489 #[error(transparent)]
491 Gff(#[from] GffBinaryError),
492}
493
494#[cfg_attr(
502 feature = "tracing",
503 tracing::instrument(level = "debug", skip(reader))
504)]
505pub fn read_dlg<R: Read>(reader: &mut R) -> Result<Dlg, DlgError> {
506 let gff = read_gff(reader)?;
507 Dlg::from_gff(&gff)
508}
509
510#[cfg_attr(
518 feature = "tracing",
519 tracing::instrument(level = "debug", skip(bytes), fields(bytes_len = bytes.len()))
520)]
521pub fn read_dlg_from_bytes(bytes: &[u8]) -> Result<Dlg, DlgError> {
522 let gff = read_gff_from_bytes(bytes)?;
523 Dlg::from_gff(&gff)
524}
525
526#[cfg_attr(
534 feature = "tracing",
535 tracing::instrument(level = "debug", skip(writer, dlg))
536)]
537pub fn author_dlg<W: Write>(writer: &mut W, dlg: &Dlg) -> Result<(), DlgError> {
538 let gff = dlg.to_gff();
539 write_gff(writer, &gff)?;
540 Ok(())
541}
542
543#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(dlg)))]
550pub fn author_dlg_to_vec(dlg: &Dlg) -> Result<Vec<u8>, DlgError> {
551 let mut cursor = Cursor::new(Vec::new());
552 author_dlg(&mut cursor, dlg)?;
553 Ok(cursor.into_inner())
554}
555
556#[cfg(test)]
565mod tests {
566 use rakata_formats::schema::{HasSchema, Shape};
567
568 macro_rules! written_link {
571 ($value:expr, $carries:expr) => {{
572 let mut structure = GffStruct::new(0);
573 $value.write_link(&mut structure, $carries);
574 structure
575 }};
576 }
577
578 use super::*;
579
580 #[test]
581 fn display_inactive_is_omitted_at_its_default_and_written_otherwise() {
582 let cleared = DlgLink::default();
588 assert!(!cleared.display_inactive);
589 assert!(written_link!(cleared, true)
590 .field("DisplayInactive")
591 .is_none());
592
593 let set = DlgLink {
594 display_inactive: true,
595 ..DlgLink::default()
596 };
597 let written = written_link!(set, true);
598 assert_eq!(written.field("DisplayInactive"), Some(&GffValue::UInt8(1)));
599 assert!(DlgLink::read_link(&written).display_inactive);
600 }
601
602 fn make_test_dlg_gff() -> Gff {
604 let mut root = GffStruct::new(-1);
605 root.push_field(gff_label!("CameraModel"), GffValue::resref_lit(""));
606 root.push_field(gff_label!("DelayEntry"), GffValue::UInt32(0));
607 root.push_field(gff_label!("DelayReply"), GffValue::UInt32(0));
608 root.push_field(
609 gff_label!("EndConversation"),
610 GffValue::resref_lit("k_end_conv"),
611 );
612 root.push_field(
613 gff_label!("EndConverAbort"),
614 GffValue::resref_lit("k_end_abort"),
615 );
616 root.push_field(gff_label!("Skippable"), GffValue::UInt8(1));
617 root.push_field(gff_label!("ConversationType"), GffValue::Int32(0));
618 root.push_field(gff_label!("ComputerType"), GffValue::UInt8(0));
619 root.push_field(gff_label!("AmbientTrack"), GffValue::resref_lit(""));
620 root.push_field(gff_label!("UnequipItems"), GffValue::UInt8(0));
621 root.push_field(gff_label!("UnequipHItem"), GffValue::UInt8(0));
622 root.push_field(gff_label!("AnimatedCut"), GffValue::UInt8(1));
623 root.push_field(gff_label!("OldHitCheck"), GffValue::UInt8(0));
624
625 let mut start_link = GffStruct::new(0);
627 start_link.push_field(gff_label!("Active"), GffValue::resref_lit("k_cond_start"));
628 start_link.push_field(gff_label!("Index"), GffValue::UInt32(0));
629 root.push_field(gff_label!("StartingList"), GffValue::List(vec![start_link]));
630
631 let mut entry0 = GffStruct::new(0);
633 entry0.push_field(
634 gff_label!("Text"),
635 GffValue::LocalizedString(GffLocalizedString::new(rakata_core::StrRef::from_raw(
636 50000,
637 ))),
638 );
639 entry0.push_field(gff_label!("Script"), GffValue::resref_lit("k_entry_fire"));
640 entry0.push_field(gff_label!("Speaker"), GffValue::String("Bastila".into()));
641 entry0.push_field(gff_label!("WaitFlags"), GffValue::UInt32(0));
642 entry0.push_field(gff_label!("Quest"), GffValue::String("".into()));
643 entry0.push_field(gff_label!("QuestEntry"), GffValue::UInt32(0));
644 entry0.push_field(gff_label!("PlotIndex"), GffValue::Int32(-1));
645 entry0.push_field(gff_label!("PlotXPPercentage"), GffValue::Single(1.0));
646 entry0.push_field(gff_label!("Delay"), GffValue::UInt32(u32::MAX));
647 entry0.push_field(gff_label!("FadeType"), GffValue::UInt8(0));
648 entry0.push_field(gff_label!("FadeColor"), GffValue::Vector3([0.0, 0.0, 0.0]));
649 entry0.push_field(gff_label!("FadeDelay"), GffValue::Single(0.0));
650 entry0.push_field(gff_label!("FadeLength"), GffValue::Single(0.0));
651 entry0.push_field(gff_label!("Sound"), GffValue::resref_lit(""));
652 entry0.push_field(
653 gff_label!("VO_ResRef"),
654 GffValue::resref_lit("n_bastila_hi"),
655 );
656 entry0.push_field(gff_label!("SoundExists"), GffValue::UInt8(1));
657 entry0.push_field(gff_label!("Listener"), GffValue::String("".into()));
658 entry0.push_field(gff_label!("CameraAngle"), GffValue::UInt32(4));
659 entry0.push_field(gff_label!("CameraID"), GffValue::Int32(-1));
660 entry0.push_field(gff_label!("CamHeightOffset"), GffValue::Single(0.0));
661 entry0.push_field(gff_label!("TarHeightOffset"), GffValue::Single(0.0));
662 entry0.push_field(gff_label!("CameraAnimation"), GffValue::UInt16(0));
663 entry0.push_field(gff_label!("CamVidEffect"), GffValue::Int32(-1));
664 entry0.push_field(gff_label!("CamFieldOfView"), GffValue::Single(-1.0));
665
666 let mut anim = GffStruct::new(0);
668 anim.push_field(
669 gff_label!("Participant"),
670 GffValue::String("Bastila".into()),
671 );
672 anim.push_field(gff_label!("Animation"), GffValue::UInt16(28));
673 entry0.push_field(gff_label!("AnimList"), GffValue::List(vec![anim]));
674
675 let mut reply_link = GffStruct::new(0);
677 reply_link.push_field(gff_label!("Active"), GffValue::resref_lit(""));
678 reply_link.push_field(gff_label!("Index"), GffValue::UInt32(0));
679 reply_link.push_field(gff_label!("DisplayInactive"), GffValue::UInt8(1));
680 entry0.push_field(gff_label!("RepliesList"), GffValue::List(vec![reply_link]));
681
682 let mut reply0 = GffStruct::new(1);
684 reply0.push_field(
685 gff_label!("Text"),
686 GffValue::LocalizedString(GffLocalizedString::new(rakata_core::StrRef::from_raw(
687 50001,
688 ))),
689 );
690 reply0.push_field(gff_label!("Script"), GffValue::resref_lit("k_reply_fire"));
691 reply0.push_field(gff_label!("Speaker"), GffValue::String("".into()));
692 reply0.push_field(gff_label!("WaitFlags"), GffValue::UInt32(0));
693 reply0.push_field(gff_label!("Quest"), GffValue::String("".into()));
694 reply0.push_field(gff_label!("QuestEntry"), GffValue::UInt32(0));
695 reply0.push_field(gff_label!("PlotIndex"), GffValue::Int32(-1));
696 reply0.push_field(gff_label!("PlotXPPercentage"), GffValue::Single(1.0));
697 reply0.push_field(gff_label!("Delay"), GffValue::UInt32(u32::MAX));
698 reply0.push_field(gff_label!("FadeType"), GffValue::UInt8(0));
699 reply0.push_field(gff_label!("FadeColor"), GffValue::Vector3([0.0, 0.0, 0.0]));
700 reply0.push_field(gff_label!("FadeDelay"), GffValue::Single(0.0));
701 reply0.push_field(gff_label!("FadeLength"), GffValue::Single(0.0));
702 reply0.push_field(gff_label!("Sound"), GffValue::resref_lit(""));
703 reply0.push_field(gff_label!("VO_ResRef"), GffValue::resref_lit(""));
704 reply0.push_field(gff_label!("SoundExists"), GffValue::UInt8(0));
705 reply0.push_field(gff_label!("AnimList"), GffValue::List(vec![]));
706 reply0.push_field(gff_label!("Listener"), GffValue::String("".into()));
707 reply0.push_field(gff_label!("CameraAngle"), GffValue::UInt32(0));
708 reply0.push_field(gff_label!("CameraID"), GffValue::Int32(-1));
709 reply0.push_field(gff_label!("CamHeightOffset"), GffValue::Single(0.0));
710 reply0.push_field(gff_label!("TarHeightOffset"), GffValue::Single(0.0));
711 reply0.push_field(gff_label!("CameraAnimation"), GffValue::UInt16(0));
712 reply0.push_field(gff_label!("CamVidEffect"), GffValue::Int32(-1));
713 reply0.push_field(gff_label!("CamFieldOfView"), GffValue::Single(-1.0));
714
715 let mut entry_link = GffStruct::new(0);
716 entry_link.push_field(gff_label!("Active"), GffValue::resref_lit(""));
717 entry_link.push_field(gff_label!("Index"), GffValue::UInt32(0));
718 reply0.push_field(gff_label!("EntriesList"), GffValue::List(vec![entry_link]));
719
720 root.push_field(gff_label!("EntryList"), GffValue::List(vec![entry0]));
721 root.push_field(gff_label!("ReplyList"), GffValue::List(vec![reply0]));
722
723 let mut stunt = GffStruct::new(0);
725 stunt.push_field(
726 gff_label!("Participant"),
727 GffValue::String("Bastila".into()),
728 );
729 stunt.push_field(
730 gff_label!("StuntModel"),
731 GffValue::resref_lit("p_bastilabb"),
732 );
733 root.push_field(gff_label!("StuntList"), GffValue::List(vec![stunt]));
734
735 Gff::new(*b"DLG ", root)
736 }
737
738 #[test]
739 fn reads_conversation_config() {
740 let gff = make_test_dlg_gff();
741 let dlg = Dlg::from_gff(&gff).expect("test GFF is well-formed");
742
743 assert_eq!(dlg.end_conversation, "k_end_conv");
744 assert_eq!(dlg.end_conver_abort, "k_end_abort");
745 assert!(dlg.skippable);
746 assert_eq!(dlg.conversation_type, 0);
747 assert!(dlg.animated_cut);
748 assert!(!dlg.old_hit_check);
749 }
750
751 #[test]
752 fn reads_starting_list() {
753 let gff = make_test_dlg_gff();
754 let dlg = Dlg::from_gff(&gff).expect("test GFF is well-formed");
755
756 assert_eq!(dlg.starting_list.len(), 1);
757 assert_eq!(dlg.starting_list[0].active, "k_cond_start");
758 assert_eq!(dlg.starting_list[0].index, 0);
759 }
760
761 #[test]
762 fn reads_entry_node() {
763 let gff = make_test_dlg_gff();
764 let dlg = Dlg::from_gff(&gff).expect("test GFF is well-formed");
765
766 assert_eq!(dlg.entries.len(), 1);
767 let entry = &dlg.entries[0];
768 assert_eq!(entry.text.string_ref.raw(), 50000);
769 assert_eq!(entry.script, "k_entry_fire");
770 assert_eq!(entry.speaker, "Bastila");
771 assert_eq!(entry.vo_resref, "n_bastila_hi");
772 assert_eq!(entry.sound_exists, 1);
773 assert_eq!(entry.camera_angle, 4);
774 assert_eq!(entry.animations.len(), 1);
775 assert_eq!(entry.animations[0].participant, "Bastila");
776 assert_eq!(entry.animations[0].animation, 28);
777 assert_eq!(entry.links.len(), 1);
778 assert_eq!(entry.links[0].index, 0);
779 assert!(entry.links[0].display_inactive);
780 }
781
782 #[test]
783 fn reads_reply_node() {
784 let gff = make_test_dlg_gff();
785 let dlg = Dlg::from_gff(&gff).expect("test GFF is well-formed");
786
787 assert_eq!(dlg.replies.len(), 1);
788 let reply = &dlg.replies[0];
789 assert_eq!(reply.text.string_ref.raw(), 50001);
790 assert_eq!(reply.script, "k_reply_fire");
791 assert_eq!(reply.sound_exists, 0);
792 assert_eq!(reply.links.len(), 1);
793 assert_eq!(reply.links[0].index, 0);
794 }
795
796 #[test]
797 fn reads_stunt_list() {
798 let gff = make_test_dlg_gff();
799 let dlg = Dlg::from_gff(&gff).expect("test GFF is well-formed");
800
801 assert_eq!(dlg.stunt_list.len(), 1);
802 assert_eq!(dlg.stunt_list[0].participant, "Bastila");
803 assert_eq!(dlg.stunt_list[0].stunt_model, "p_bastilabb");
804 }
805
806 #[test]
807 fn all_fields_survive_typed_roundtrip() {
808 let gff = make_test_dlg_gff();
809 let dlg = Dlg::from_gff(&gff).expect("test GFF is well-formed");
810 let bytes = author_dlg_to_vec(&dlg).expect("write succeeds");
811 let reparsed = read_dlg_from_bytes(&bytes).expect("reparse succeeds");
812
813 assert_eq!(dlg, reparsed);
814 }
815
816 #[test]
817 fn fade_color_roundtrips_as_vector3() {
818 let mut gff = make_test_dlg_gff();
819 if let Some(GffValue::List(ref mut entries)) = gff
821 .root
822 .fields
823 .iter_mut()
824 .find(|f| f.label == "EntryList")
825 .map(|f| &mut f.value)
826 {
827 if let Some(field) = entries[0]
829 .fields
830 .iter_mut()
831 .find(|f| f.label == "FadeColor")
832 {
833 field.value = GffValue::Vector3([1.0, 0.0, 0.0]);
834 }
835 }
836
837 let dlg = Dlg::from_gff(&gff).expect("test GFF is well-formed");
838 assert_eq!(dlg.entries[0].fade_color, [1.0, 0.0, 0.0]);
839
840 let bytes = author_dlg_to_vec(&dlg).expect("write succeeds");
841 let reparsed = read_dlg_from_bytes(&bytes).expect("reparse succeeds");
842 assert_eq!(reparsed.entries[0].fade_color, [1.0, 0.0, 0.0]);
843
844 let rebuilt = dlg.to_gff();
846 if let Some(GffValue::List(entries)) = rebuilt.root.field("EntryList") {
847 let fade = entries[0]
848 .field("FadeColor")
849 .expect("FadeColor must be present");
850 assert!(
851 matches!(fade, GffValue::Vector3([1.0, 0.0, 0.0])),
852 "FadeColor must be GffValue::Vector3, got {fade:?}"
853 );
854 } else {
855 panic!("EntryList missing from rebuilt GFF");
856 }
857 }
858
859 #[test]
860 fn typed_edits_roundtrip_through_gff_writer() {
861 let gff = make_test_dlg_gff();
862 let mut dlg = Dlg::from_gff(&gff).expect("test GFF is well-formed");
863 dlg.end_conversation = ResRef::new("k_end_new").expect("valid test resref");
864 dlg.entries[0].speaker = "Carth".into();
865 dlg.entries[0].links[0].display_inactive = false;
866
867 let bytes = author_dlg_to_vec(&dlg).expect("write succeeds");
868 let reparsed = read_dlg_from_bytes(&bytes).expect("reparse succeeds");
869
870 assert_eq!(reparsed.end_conversation, "k_end_new");
871 assert_eq!(reparsed.entries[0].speaker, "Carth");
872 assert!(!reparsed.entries[0].links[0].display_inactive);
873 }
874
875 #[test]
876 fn read_dlg_from_reader_matches_bytes_path() {
877 let gff = make_test_dlg_gff();
878 let bytes = {
879 let mut c = Cursor::new(Vec::new());
880 write_gff(&mut c, &gff).expect("test GFF serializes cleanly");
881 c.into_inner()
882 };
883
884 let mut cursor = Cursor::new(&bytes);
885 let via_reader = read_dlg(&mut cursor).expect("reader parse succeeds");
886 let via_bytes = read_dlg_from_bytes(&bytes).expect("bytes parse succeeds");
887
888 assert_eq!(via_reader, via_bytes);
889 }
890
891 #[test]
892 fn rejects_non_dlg_file_type() {
893 let mut gff = make_test_dlg_gff();
894 gff.file_type = *b"UTT ";
895
896 let err = Dlg::from_gff(&gff).expect_err("UTT must be rejected as DLG input");
897 assert!(matches!(
898 err,
899 DlgError::UnsupportedFileType(file_type) if file_type == *b"UTT "
900 ));
901 }
902
903 #[test]
904 fn write_dlg_matches_direct_gff_writer() {
905 let gff = make_test_dlg_gff();
906 let dlg = Dlg::from_gff(&gff).expect("test GFF is well-formed");
907
908 let via_typed = author_dlg_to_vec(&dlg).expect("typed write succeeds");
909
910 let mut direct = Cursor::new(Vec::new());
911 write_gff(&mut direct, &dlg.to_gff()).expect("direct write succeeds");
912
913 assert_eq!(via_typed, direct.into_inner());
914 }
915
916 #[test]
917 fn schema_field_count() {
918 assert_eq!(Dlg::schema().len(), 19);
919 }
920
921 #[test]
922 fn schema_no_duplicate_labels() {
923 let mut labels: Vec<&str> = Dlg::schema().iter().map(|f| f.label.as_str()).collect();
924 labels.sort_unstable();
925 let before = labels.len();
926 labels.dedup();
927 assert_eq!(before, labels.len(), "duplicate labels in DLG schema");
928 }
929
930 #[test]
934 fn the_two_node_lists_share_one_element_array() {
935 let element_of = |label: &str| {
936 let Shape::List { element, .. } = Dlg::schema()
937 .iter()
938 .find(|f| f.label.as_str() == label)
939 .expect("declared")
940 .shape
941 else {
942 panic!("{label} is a list");
943 };
944 element
945 };
946 let entries = element_of("EntryList");
947 let replies = element_of("ReplyList");
948 assert_eq!(entries.len(), 2, "the node's own part, then its list label");
949 assert!(
950 std::ptr::eq(entries[0], replies[0]),
951 "the twenty-six are one array reached twice, not two copies"
952 );
953 assert_eq!(entries[0].len(), 26);
954 assert_eq!(
955 entries[1]
956 .iter()
957 .map(|f| f.label.as_str())
958 .collect::<Vec<_>>(),
959 ["RepliesList"]
960 );
961 assert_eq!(
962 replies[1]
963 .iter()
964 .map(|f| f.label.as_str())
965 .collect::<Vec<_>>(),
966 ["EntriesList"]
967 );
968 }
969
970 #[test]
974 fn only_the_replies_list_declares_display_inactive() {
975 let labels = |parts: &[&'static [rakata_formats::schema::Field]]| {
976 parts
977 .iter()
978 .flat_map(|part| part.iter())
979 .map(|f| f.label.as_str())
980 .collect::<Vec<_>>()
981 };
982 assert!(!labels(DlgLink::PARTS).contains(&"DisplayInactive"));
983
984 let Shape::List { element, .. } = DlgNodeRepliesTail::SCHEMA[0].shape else {
985 panic!("RepliesList is a list");
986 };
987 assert!(labels(element).contains(&"DisplayInactive"));
988
989 let Shape::List { element, .. } = DlgNodeEntriesTail::SCHEMA[0].shape else {
990 panic!("EntriesList is a list");
991 };
992 assert!(!labels(element).contains(&"DisplayInactive"));
993 }
994}