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)]
202 pub delay: u32,
203 #[gff(FadeType, stamped)]
205 pub fade_type: u8,
206 #[gff(FadeColor, stamped = [0.0, 0.0, 0.0])]
208 pub fade_color: [f32; 3],
209 #[gff(FadeDelay, stamped)]
211 pub fade_delay: f32,
212 #[gff(FadeLength, stamped)]
214 pub fade_length: f32,
215 #[gff(Sound, unexamined)]
217 pub sound: ResRef,
218 #[gff(VO_ResRef, stamped)]
220 pub vo_resref: ResRef,
221 #[gff(SoundExists, stamped = 128)]
228 pub sound_exists: u8,
229 #[gff(Listener, stamped)]
231 pub listener: String,
232 #[gff(CameraAngle, stamped)]
234 pub camera_angle: u32,
235 #[gff(CameraID, stamped)]
237 pub camera_id: i32,
238 #[gff(CamHeightOffset, stamped)]
240 pub cam_height_offset: f32,
241 #[gff(TarHeightOffset, stamped)]
243 pub tar_height_offset: f32,
244 #[gff(CameraAnimation, stamped)]
246 pub camera_animation: u16,
247 #[gff(CamVidEffect, stamped = -1)]
249 pub cam_vid_effect: i32,
250 #[gff(CamFieldOfView, stamped = -1.0)]
252 pub cam_field_of_view: f32,
253
254 pub links: Vec<DlgLink>,
262}
263
264impl DlgNode {
265 fn read_node(structure: &GffStruct, link_label: &str) -> Self {
272 let links = match structure.field(link_label) {
273 Some(GffValue::List(elements)) => elements.iter().map(DlgLink::read_link).collect(),
274 _ => Vec::new(),
275 };
276 Self {
277 links,
278 ..Self::read_declared(structure)
279 }
280 }
281
282 fn write_node(
284 &self,
285 structure: &mut GffStruct,
286 link_label: GffLabel,
287 links_carry_display_inactive: bool,
288 ) {
289 self.write_declared(structure);
290 let elements: Vec<GffStruct> = self
291 .links
292 .iter()
293 .enumerate()
294 .map(|(index, link)| {
295 let mut element = GffStruct::new(i32::try_from(index).unwrap_or(0));
296 link.write_link(&mut element, links_carry_display_inactive);
297 element
298 })
299 .collect();
300 upsert_field(structure, link_label, GffValue::List(elements));
301 }
302}
303
304pub const SOUND_EXISTS_ABSENT: u8 = 0x80;
311
312#[derive(Debug, Clone, PartialEq, GffModel)]
314#[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)]
315#[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)]
316pub struct Dlg {
317 #[gff(CameraModel, stamped)]
319 pub camera_model: ResRef,
320 #[gff(DelayEntry, stamped)]
322 pub delay_entry: u32,
323 #[gff(DelayReply, stamped)]
325 pub delay_reply: u32,
326 #[gff(EndConversation, stamped)]
328 pub end_conversation: ResRef,
329 #[gff(EndConverAbort, stamped)]
331 pub end_conver_abort: ResRef,
332 #[gff(Skippable, stamped = true)]
334 pub skippable: bool,
335 #[gff(ConversationType, stamped)]
337 pub conversation_type: i32,
338 #[gff(ComputerType, stamped)]
340 pub computer_type: u8,
341 #[gff(AmbientTrack, stamped)]
343 pub ambient_track: ResRef,
344 #[gff(UnequipItems, stamped)]
346 pub unequip_items: bool,
347 #[gff(UnequipHItem, stamped)]
349 pub unequip_h_item: bool,
350 #[gff(AnimatedCut, stamped)]
352 pub animated_cut: bool,
353 #[gff(OldHitCheck, stamped)]
355 pub old_hit_check: bool,
356 #[gff(
358 EntryList,
359 unexamined,
360 list = DlgNode,
361 element_extra = DlgNodeRepliesTail,
362 element_id = positional,
363 manual_read,
364 manual_write
365 )]
366 pub entries: Vec<DlgNode>,
367 #[gff(
369 ReplyList,
370 unexamined,
371 list = DlgNode,
372 element_extra = DlgNodeEntriesTail,
373 element_id = positional,
374 manual_read,
375 manual_write
376 )]
377 pub replies: Vec<DlgNode>,
378 #[gff(
380 StartingList,
381 unexamined,
382 list = DlgLink,
383 element_id = positional,
384 manual_read,
385 manual_write
386 )]
387 pub starting_list: Vec<DlgLink>,
388 #[gff(StuntList, unexamined, list = DlgStunt, element_id = 0)]
390 pub stunt_list: Vec<DlgStunt>,
391}
392
393impl Dlg {
394 pub fn new() -> Self {
396 Self::default()
397 }
398
399 pub fn from_gff(gff: &Gff) -> Result<Self, DlgError> {
406 if gff.file_type != <Dlg as FromGff>::MAGIC && gff.file_type != GENERIC_FILE_TYPE {
407 return Err(DlgError::UnsupportedFileType(gff.file_type));
408 }
409 let root = &gff.root;
410 let nodes = |label: &str, link_label: &'static str| match root.field(label) {
411 Some(GffValue::List(elements)) => elements
412 .iter()
413 .map(|element| DlgNode::read_node(element, link_label))
414 .collect(),
415 _ => Vec::new(),
416 };
417 Ok(Self {
418 starting_list: match root.field("StartingList") {
419 Some(GffValue::List(elements)) => elements.iter().map(DlgLink::read_link).collect(),
420 _ => Vec::new(),
421 },
422 entries: nodes("EntryList", "RepliesList"),
423 replies: nodes("ReplyList", "EntriesList"),
424 ..Self::read_declared(root)
425 })
426 }
427
428 pub fn to_gff(&self) -> Gff {
430 let mut root = GffStruct::new(-1);
431 self.write_declared(&mut root);
432
433 let links: Vec<GffStruct> = self
437 .starting_list
438 .iter()
439 .enumerate()
440 .map(|(index, link)| {
441 let mut element = GffStruct::new(i32::try_from(index).unwrap_or(0));
442 link.write_link(&mut element, false);
443 element
444 })
445 .collect();
446 upsert_field(&mut root, gff_label!("StartingList"), GffValue::List(links));
447
448 for (label, link_label, nodes, display_inactive) in [
449 (
450 gff_label!("EntryList"),
451 gff_label!("RepliesList"),
452 &self.entries,
453 true,
454 ),
455 (
456 gff_label!("ReplyList"),
457 gff_label!("EntriesList"),
458 &self.replies,
459 false,
460 ),
461 ] {
462 let elements: Vec<GffStruct> = nodes
463 .iter()
464 .enumerate()
465 .map(|(index, node)| {
466 let mut element = GffStruct::new(i32::try_from(index).unwrap_or(0));
467 node.write_node(&mut element, link_label, display_inactive);
468 element
469 })
470 .collect();
471 upsert_field(&mut root, label, GffValue::List(elements));
472 }
473
474 Gff::new(*b"DLG ", root)
475 }
476}
477
478#[derive(Debug, Error)]
480pub enum DlgError {
481 #[error("unsupported DLG file type: {0:?}")]
483 UnsupportedFileType([u8; 4]),
484 #[error(transparent)]
486 Gff(#[from] GffBinaryError),
487}
488
489#[cfg_attr(
497 feature = "tracing",
498 tracing::instrument(level = "debug", skip(reader))
499)]
500pub fn read_dlg<R: Read>(reader: &mut R) -> Result<Dlg, DlgError> {
501 let gff = read_gff(reader)?;
502 Dlg::from_gff(&gff)
503}
504
505#[cfg_attr(
513 feature = "tracing",
514 tracing::instrument(level = "debug", skip(bytes), fields(bytes_len = bytes.len()))
515)]
516pub fn read_dlg_from_bytes(bytes: &[u8]) -> Result<Dlg, DlgError> {
517 let gff = read_gff_from_bytes(bytes)?;
518 Dlg::from_gff(&gff)
519}
520
521#[cfg_attr(
529 feature = "tracing",
530 tracing::instrument(level = "debug", skip(writer, dlg))
531)]
532pub fn author_dlg<W: Write>(writer: &mut W, dlg: &Dlg) -> Result<(), DlgError> {
533 let gff = dlg.to_gff();
534 write_gff(writer, &gff)?;
535 Ok(())
536}
537
538#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(dlg)))]
545pub fn author_dlg_to_vec(dlg: &Dlg) -> Result<Vec<u8>, DlgError> {
546 let mut cursor = Cursor::new(Vec::new());
547 author_dlg(&mut cursor, dlg)?;
548 Ok(cursor.into_inner())
549}
550
551#[cfg(test)]
560mod tests {
561 use rakata_formats::schema::{HasSchema, Shape};
562
563 macro_rules! written_link {
566 ($value:expr, $carries:expr) => {{
567 let mut structure = GffStruct::new(0);
568 $value.write_link(&mut structure, $carries);
569 structure
570 }};
571 }
572
573 use super::*;
574
575 #[test]
576 fn display_inactive_is_omitted_at_its_default_and_written_otherwise() {
577 let cleared = DlgLink::default();
583 assert!(!cleared.display_inactive);
584 assert!(written_link!(cleared, true)
585 .field("DisplayInactive")
586 .is_none());
587
588 let set = DlgLink {
589 display_inactive: true,
590 ..DlgLink::default()
591 };
592 let written = written_link!(set, true);
593 assert_eq!(written.field("DisplayInactive"), Some(&GffValue::UInt8(1)));
594 assert!(DlgLink::read_link(&written).display_inactive);
595 }
596
597 fn make_test_dlg_gff() -> Gff {
599 let mut root = GffStruct::new(-1);
600 root.push_field(gff_label!("CameraModel"), GffValue::resref_lit(""));
601 root.push_field(gff_label!("DelayEntry"), GffValue::UInt32(0));
602 root.push_field(gff_label!("DelayReply"), GffValue::UInt32(0));
603 root.push_field(
604 gff_label!("EndConversation"),
605 GffValue::resref_lit("k_end_conv"),
606 );
607 root.push_field(
608 gff_label!("EndConverAbort"),
609 GffValue::resref_lit("k_end_abort"),
610 );
611 root.push_field(gff_label!("Skippable"), GffValue::UInt8(1));
612 root.push_field(gff_label!("ConversationType"), GffValue::Int32(0));
613 root.push_field(gff_label!("ComputerType"), GffValue::UInt8(0));
614 root.push_field(gff_label!("AmbientTrack"), GffValue::resref_lit(""));
615 root.push_field(gff_label!("UnequipItems"), GffValue::UInt8(0));
616 root.push_field(gff_label!("UnequipHItem"), GffValue::UInt8(0));
617 root.push_field(gff_label!("AnimatedCut"), GffValue::UInt8(1));
618 root.push_field(gff_label!("OldHitCheck"), GffValue::UInt8(0));
619
620 let mut start_link = GffStruct::new(0);
622 start_link.push_field(gff_label!("Active"), GffValue::resref_lit("k_cond_start"));
623 start_link.push_field(gff_label!("Index"), GffValue::UInt32(0));
624 root.push_field(gff_label!("StartingList"), GffValue::List(vec![start_link]));
625
626 let mut entry0 = GffStruct::new(0);
628 entry0.push_field(
629 gff_label!("Text"),
630 GffValue::LocalizedString(GffLocalizedString::new(rakata_core::StrRef::from_raw(
631 50000,
632 ))),
633 );
634 entry0.push_field(gff_label!("Script"), GffValue::resref_lit("k_entry_fire"));
635 entry0.push_field(gff_label!("Speaker"), GffValue::String("Bastila".into()));
636 entry0.push_field(gff_label!("WaitFlags"), GffValue::UInt32(0));
637 entry0.push_field(gff_label!("Quest"), GffValue::String("".into()));
638 entry0.push_field(gff_label!("QuestEntry"), GffValue::UInt32(0));
639 entry0.push_field(gff_label!("PlotIndex"), GffValue::Int32(-1));
640 entry0.push_field(gff_label!("PlotXPPercentage"), GffValue::Single(1.0));
641 entry0.push_field(gff_label!("Delay"), GffValue::UInt32(u32::MAX));
642 entry0.push_field(gff_label!("FadeType"), GffValue::UInt8(0));
643 entry0.push_field(gff_label!("FadeColor"), GffValue::Vector3([0.0, 0.0, 0.0]));
644 entry0.push_field(gff_label!("FadeDelay"), GffValue::Single(0.0));
645 entry0.push_field(gff_label!("FadeLength"), GffValue::Single(0.0));
646 entry0.push_field(gff_label!("Sound"), GffValue::resref_lit(""));
647 entry0.push_field(
648 gff_label!("VO_ResRef"),
649 GffValue::resref_lit("n_bastila_hi"),
650 );
651 entry0.push_field(gff_label!("SoundExists"), GffValue::UInt8(1));
652 entry0.push_field(gff_label!("Listener"), GffValue::String("".into()));
653 entry0.push_field(gff_label!("CameraAngle"), GffValue::UInt32(4));
654 entry0.push_field(gff_label!("CameraID"), GffValue::Int32(-1));
655 entry0.push_field(gff_label!("CamHeightOffset"), GffValue::Single(0.0));
656 entry0.push_field(gff_label!("TarHeightOffset"), GffValue::Single(0.0));
657 entry0.push_field(gff_label!("CameraAnimation"), GffValue::UInt16(0));
658 entry0.push_field(gff_label!("CamVidEffect"), GffValue::Int32(-1));
659 entry0.push_field(gff_label!("CamFieldOfView"), GffValue::Single(-1.0));
660
661 let mut anim = GffStruct::new(0);
663 anim.push_field(
664 gff_label!("Participant"),
665 GffValue::String("Bastila".into()),
666 );
667 anim.push_field(gff_label!("Animation"), GffValue::UInt16(28));
668 entry0.push_field(gff_label!("AnimList"), GffValue::List(vec![anim]));
669
670 let mut reply_link = GffStruct::new(0);
672 reply_link.push_field(gff_label!("Active"), GffValue::resref_lit(""));
673 reply_link.push_field(gff_label!("Index"), GffValue::UInt32(0));
674 reply_link.push_field(gff_label!("DisplayInactive"), GffValue::UInt8(1));
675 entry0.push_field(gff_label!("RepliesList"), GffValue::List(vec![reply_link]));
676
677 let mut reply0 = GffStruct::new(1);
679 reply0.push_field(
680 gff_label!("Text"),
681 GffValue::LocalizedString(GffLocalizedString::new(rakata_core::StrRef::from_raw(
682 50001,
683 ))),
684 );
685 reply0.push_field(gff_label!("Script"), GffValue::resref_lit("k_reply_fire"));
686 reply0.push_field(gff_label!("Speaker"), GffValue::String("".into()));
687 reply0.push_field(gff_label!("WaitFlags"), GffValue::UInt32(0));
688 reply0.push_field(gff_label!("Quest"), GffValue::String("".into()));
689 reply0.push_field(gff_label!("QuestEntry"), GffValue::UInt32(0));
690 reply0.push_field(gff_label!("PlotIndex"), GffValue::Int32(-1));
691 reply0.push_field(gff_label!("PlotXPPercentage"), GffValue::Single(1.0));
692 reply0.push_field(gff_label!("Delay"), GffValue::UInt32(u32::MAX));
693 reply0.push_field(gff_label!("FadeType"), GffValue::UInt8(0));
694 reply0.push_field(gff_label!("FadeColor"), GffValue::Vector3([0.0, 0.0, 0.0]));
695 reply0.push_field(gff_label!("FadeDelay"), GffValue::Single(0.0));
696 reply0.push_field(gff_label!("FadeLength"), GffValue::Single(0.0));
697 reply0.push_field(gff_label!("Sound"), GffValue::resref_lit(""));
698 reply0.push_field(gff_label!("VO_ResRef"), GffValue::resref_lit(""));
699 reply0.push_field(gff_label!("SoundExists"), GffValue::UInt8(0));
700 reply0.push_field(gff_label!("AnimList"), GffValue::List(vec![]));
701 reply0.push_field(gff_label!("Listener"), GffValue::String("".into()));
702 reply0.push_field(gff_label!("CameraAngle"), GffValue::UInt32(0));
703 reply0.push_field(gff_label!("CameraID"), GffValue::Int32(-1));
704 reply0.push_field(gff_label!("CamHeightOffset"), GffValue::Single(0.0));
705 reply0.push_field(gff_label!("TarHeightOffset"), GffValue::Single(0.0));
706 reply0.push_field(gff_label!("CameraAnimation"), GffValue::UInt16(0));
707 reply0.push_field(gff_label!("CamVidEffect"), GffValue::Int32(-1));
708 reply0.push_field(gff_label!("CamFieldOfView"), GffValue::Single(-1.0));
709
710 let mut entry_link = GffStruct::new(0);
711 entry_link.push_field(gff_label!("Active"), GffValue::resref_lit(""));
712 entry_link.push_field(gff_label!("Index"), GffValue::UInt32(0));
713 reply0.push_field(gff_label!("EntriesList"), GffValue::List(vec![entry_link]));
714
715 root.push_field(gff_label!("EntryList"), GffValue::List(vec![entry0]));
716 root.push_field(gff_label!("ReplyList"), GffValue::List(vec![reply0]));
717
718 let mut stunt = GffStruct::new(0);
720 stunt.push_field(
721 gff_label!("Participant"),
722 GffValue::String("Bastila".into()),
723 );
724 stunt.push_field(
725 gff_label!("StuntModel"),
726 GffValue::resref_lit("p_bastilabb"),
727 );
728 root.push_field(gff_label!("StuntList"), GffValue::List(vec![stunt]));
729
730 Gff::new(*b"DLG ", root)
731 }
732
733 #[test]
734 fn reads_conversation_config() {
735 let gff = make_test_dlg_gff();
736 let dlg = Dlg::from_gff(&gff).expect("test GFF is well-formed");
737
738 assert_eq!(dlg.end_conversation, "k_end_conv");
739 assert_eq!(dlg.end_conver_abort, "k_end_abort");
740 assert!(dlg.skippable);
741 assert_eq!(dlg.conversation_type, 0);
742 assert!(dlg.animated_cut);
743 assert!(!dlg.old_hit_check);
744 }
745
746 #[test]
747 fn reads_starting_list() {
748 let gff = make_test_dlg_gff();
749 let dlg = Dlg::from_gff(&gff).expect("test GFF is well-formed");
750
751 assert_eq!(dlg.starting_list.len(), 1);
752 assert_eq!(dlg.starting_list[0].active, "k_cond_start");
753 assert_eq!(dlg.starting_list[0].index, 0);
754 }
755
756 #[test]
757 fn reads_entry_node() {
758 let gff = make_test_dlg_gff();
759 let dlg = Dlg::from_gff(&gff).expect("test GFF is well-formed");
760
761 assert_eq!(dlg.entries.len(), 1);
762 let entry = &dlg.entries[0];
763 assert_eq!(entry.text.string_ref.raw(), 50000);
764 assert_eq!(entry.script, "k_entry_fire");
765 assert_eq!(entry.speaker, "Bastila");
766 assert_eq!(entry.vo_resref, "n_bastila_hi");
767 assert_eq!(entry.sound_exists, 1);
768 assert_eq!(entry.camera_angle, 4);
769 assert_eq!(entry.animations.len(), 1);
770 assert_eq!(entry.animations[0].participant, "Bastila");
771 assert_eq!(entry.animations[0].animation, 28);
772 assert_eq!(entry.links.len(), 1);
773 assert_eq!(entry.links[0].index, 0);
774 assert!(entry.links[0].display_inactive);
775 }
776
777 #[test]
778 fn reads_reply_node() {
779 let gff = make_test_dlg_gff();
780 let dlg = Dlg::from_gff(&gff).expect("test GFF is well-formed");
781
782 assert_eq!(dlg.replies.len(), 1);
783 let reply = &dlg.replies[0];
784 assert_eq!(reply.text.string_ref.raw(), 50001);
785 assert_eq!(reply.script, "k_reply_fire");
786 assert_eq!(reply.sound_exists, 0);
787 assert_eq!(reply.links.len(), 1);
788 assert_eq!(reply.links[0].index, 0);
789 }
790
791 #[test]
792 fn reads_stunt_list() {
793 let gff = make_test_dlg_gff();
794 let dlg = Dlg::from_gff(&gff).expect("test GFF is well-formed");
795
796 assert_eq!(dlg.stunt_list.len(), 1);
797 assert_eq!(dlg.stunt_list[0].participant, "Bastila");
798 assert_eq!(dlg.stunt_list[0].stunt_model, "p_bastilabb");
799 }
800
801 #[test]
802 fn all_fields_survive_typed_roundtrip() {
803 let gff = make_test_dlg_gff();
804 let dlg = Dlg::from_gff(&gff).expect("test GFF is well-formed");
805 let bytes = author_dlg_to_vec(&dlg).expect("write succeeds");
806 let reparsed = read_dlg_from_bytes(&bytes).expect("reparse succeeds");
807
808 assert_eq!(dlg, reparsed);
809 }
810
811 #[test]
812 fn fade_color_roundtrips_as_vector3() {
813 let mut gff = make_test_dlg_gff();
814 if let Some(GffValue::List(ref mut entries)) = gff
816 .root
817 .fields
818 .iter_mut()
819 .find(|f| f.label == "EntryList")
820 .map(|f| &mut f.value)
821 {
822 if let Some(field) = entries[0]
824 .fields
825 .iter_mut()
826 .find(|f| f.label == "FadeColor")
827 {
828 field.value = GffValue::Vector3([1.0, 0.0, 0.0]);
829 }
830 }
831
832 let dlg = Dlg::from_gff(&gff).expect("test GFF is well-formed");
833 assert_eq!(dlg.entries[0].fade_color, [1.0, 0.0, 0.0]);
834
835 let bytes = author_dlg_to_vec(&dlg).expect("write succeeds");
836 let reparsed = read_dlg_from_bytes(&bytes).expect("reparse succeeds");
837 assert_eq!(reparsed.entries[0].fade_color, [1.0, 0.0, 0.0]);
838
839 let rebuilt = dlg.to_gff();
841 if let Some(GffValue::List(entries)) = rebuilt.root.field("EntryList") {
842 let fade = entries[0]
843 .field("FadeColor")
844 .expect("FadeColor must be present");
845 assert!(
846 matches!(fade, GffValue::Vector3([1.0, 0.0, 0.0])),
847 "FadeColor must be GffValue::Vector3, got {fade:?}"
848 );
849 } else {
850 panic!("EntryList missing from rebuilt GFF");
851 }
852 }
853
854 #[test]
855 fn typed_edits_roundtrip_through_gff_writer() {
856 let gff = make_test_dlg_gff();
857 let mut dlg = Dlg::from_gff(&gff).expect("test GFF is well-formed");
858 dlg.end_conversation = ResRef::new("k_end_new").expect("valid test resref");
859 dlg.entries[0].speaker = "Carth".into();
860 dlg.entries[0].links[0].display_inactive = false;
861
862 let bytes = author_dlg_to_vec(&dlg).expect("write succeeds");
863 let reparsed = read_dlg_from_bytes(&bytes).expect("reparse succeeds");
864
865 assert_eq!(reparsed.end_conversation, "k_end_new");
866 assert_eq!(reparsed.entries[0].speaker, "Carth");
867 assert!(!reparsed.entries[0].links[0].display_inactive);
868 }
869
870 #[test]
871 fn read_dlg_from_reader_matches_bytes_path() {
872 let gff = make_test_dlg_gff();
873 let bytes = {
874 let mut c = Cursor::new(Vec::new());
875 write_gff(&mut c, &gff).expect("test GFF serializes cleanly");
876 c.into_inner()
877 };
878
879 let mut cursor = Cursor::new(&bytes);
880 let via_reader = read_dlg(&mut cursor).expect("reader parse succeeds");
881 let via_bytes = read_dlg_from_bytes(&bytes).expect("bytes parse succeeds");
882
883 assert_eq!(via_reader, via_bytes);
884 }
885
886 #[test]
887 fn rejects_non_dlg_file_type() {
888 let mut gff = make_test_dlg_gff();
889 gff.file_type = *b"UTT ";
890
891 let err = Dlg::from_gff(&gff).expect_err("UTT must be rejected as DLG input");
892 assert!(matches!(
893 err,
894 DlgError::UnsupportedFileType(file_type) if file_type == *b"UTT "
895 ));
896 }
897
898 #[test]
899 fn write_dlg_matches_direct_gff_writer() {
900 let gff = make_test_dlg_gff();
901 let dlg = Dlg::from_gff(&gff).expect("test GFF is well-formed");
902
903 let via_typed = author_dlg_to_vec(&dlg).expect("typed write succeeds");
904
905 let mut direct = Cursor::new(Vec::new());
906 write_gff(&mut direct, &dlg.to_gff()).expect("direct write succeeds");
907
908 assert_eq!(via_typed, direct.into_inner());
909 }
910
911 #[test]
912 fn schema_field_count() {
913 assert_eq!(Dlg::schema().len(), 19);
914 }
915
916 #[test]
917 fn schema_no_duplicate_labels() {
918 let mut labels: Vec<&str> = Dlg::schema().iter().map(|f| f.label.as_str()).collect();
919 labels.sort_unstable();
920 let before = labels.len();
921 labels.dedup();
922 assert_eq!(before, labels.len(), "duplicate labels in DLG schema");
923 }
924
925 #[test]
929 fn the_two_node_lists_share_one_element_array() {
930 let element_of = |label: &str| {
931 let Shape::List { element, .. } = Dlg::schema()
932 .iter()
933 .find(|f| f.label.as_str() == label)
934 .expect("declared")
935 .shape
936 else {
937 panic!("{label} is a list");
938 };
939 element
940 };
941 let entries = element_of("EntryList");
942 let replies = element_of("ReplyList");
943 assert_eq!(entries.len(), 2, "the node's own part, then its list label");
944 assert!(
945 std::ptr::eq(entries[0], replies[0]),
946 "the twenty-six are one array reached twice, not two copies"
947 );
948 assert_eq!(entries[0].len(), 26);
949 assert_eq!(
950 entries[1]
951 .iter()
952 .map(|f| f.label.as_str())
953 .collect::<Vec<_>>(),
954 ["RepliesList"]
955 );
956 assert_eq!(
957 replies[1]
958 .iter()
959 .map(|f| f.label.as_str())
960 .collect::<Vec<_>>(),
961 ["EntriesList"]
962 );
963 }
964
965 #[test]
969 fn only_the_replies_list_declares_display_inactive() {
970 let labels = |parts: &[&'static [rakata_formats::schema::Field]]| {
971 parts
972 .iter()
973 .flat_map(|part| part.iter())
974 .map(|f| f.label.as_str())
975 .collect::<Vec<_>>()
976 };
977 assert!(!labels(DlgLink::PARTS).contains(&"DisplayInactive"));
978
979 let Shape::List { element, .. } = DlgNodeRepliesTail::SCHEMA[0].shape else {
980 panic!("RepliesList is a list");
981 };
982 assert!(labels(element).contains(&"DisplayInactive"));
983
984 let Shape::List { element, .. } = DlgNodeEntriesTail::SCHEMA[0].shape else {
985 panic!("EntriesList is a list");
986 };
987 assert!(!labels(element).contains(&"DisplayInactive"));
988 }
989}