1use std::io::{Cursor, Read, Write};
28
29use crate::gff_helpers::{
30 get_bool, get_f32, get_i32, get_locstring, get_resref, get_string, get_u16, get_u32, get_u8,
31 upsert_field,
32};
33use rakata_core::{ResRef, StrRef};
34use rakata_formats::{
35 gff_schema::{FieldSchema, GffSchema, GffType},
36 read_gff, read_gff_from_bytes, write_gff, Gff, GffBinaryError, GffLocalizedString, GffStruct,
37 GffValue,
38};
39use thiserror::Error;
40
41#[derive(Debug, Clone, PartialEq, Default)]
43pub struct DlgLink {
44 pub active: ResRef,
46 pub index: u32,
48 pub display_inactive: bool,
51}
52
53impl DlgLink {
54 fn from_gff_struct(s: &GffStruct) -> Self {
55 Self {
56 active: get_resref(s, "Active").unwrap_or_default(),
57 index: get_u32(s, "Index").unwrap_or(0),
58 display_inactive: get_bool(s, "DisplayInactive").unwrap_or(false),
59 }
60 }
61
62 fn to_gff_struct(&self, include_display_inactive: bool) -> GffStruct {
63 let mut s = GffStruct::new(0);
64 upsert_field(&mut s, "Active", GffValue::ResRef(self.active));
65 upsert_field(&mut s, "Index", GffValue::UInt32(self.index));
66 if include_display_inactive {
67 upsert_field(
68 &mut s,
69 "DisplayInactive",
70 GffValue::UInt8(u8::from(self.display_inactive)),
71 );
72 }
73 s
74 }
75}
76
77#[derive(Debug, Clone, PartialEq)]
79pub struct DlgAnimation {
80 pub participant: String,
82 pub animation: u16,
84}
85
86impl DlgAnimation {
87 fn from_gff_struct(s: &GffStruct) -> Self {
88 Self {
89 participant: get_string(s, "Participant").unwrap_or_default(),
90 animation: get_u16(s, "Animation").unwrap_or(0),
91 }
92 }
93
94 fn to_gff_struct(&self) -> GffStruct {
95 let mut s = GffStruct::new(0);
96 upsert_field(
97 &mut s,
98 "Participant",
99 GffValue::String(self.participant.clone()),
100 );
101 upsert_field(&mut s, "Animation", GffValue::UInt16(self.animation));
102 s
103 }
104}
105
106#[derive(Debug, Clone, PartialEq)]
108pub struct DlgStunt {
109 pub participant: String,
111 pub stunt_model: ResRef,
113}
114
115impl DlgStunt {
116 fn from_gff_struct(s: &GffStruct) -> Self {
117 Self {
118 participant: get_string(s, "Participant").unwrap_or_default(),
119 stunt_model: get_resref(s, "StuntModel").unwrap_or_default(),
120 }
121 }
122
123 fn to_gff_struct(&self) -> GffStruct {
124 let mut s = GffStruct::new(0);
125 upsert_field(
126 &mut s,
127 "Participant",
128 GffValue::String(self.participant.clone()),
129 );
130 upsert_field(&mut s, "StuntModel", GffValue::ResRef(self.stunt_model));
131 s
132 }
133}
134
135#[derive(Debug, Clone, PartialEq)]
142pub struct DlgNode {
143 pub text: GffLocalizedString,
146 pub script: ResRef,
148 pub speaker: String,
150 pub wait_flags: u32,
152 pub quest: String,
154 pub quest_entry: u32,
156 pub plot_index: i32,
158 pub plot_xp_percentage: f32,
160 pub delay: u32,
162 pub fade_type: u8,
164 pub fade_color: [f32; 3],
166 pub fade_delay: f32,
168 pub fade_length: f32,
170 pub sound: ResRef,
172 pub vo_resref: ResRef,
174 pub sound_exists: bool,
176 pub animations: Vec<DlgAnimation>,
178
179 pub listener: String,
182 pub camera_angle: u32,
184 pub camera_id: i32,
186 pub cam_height_offset: f32,
188 pub tar_height_offset: f32,
190 pub camera_animation: u16,
192 pub cam_vid_effect: i32,
194 pub cam_field_of_view: f32,
196
197 pub links: Vec<DlgLink>,
201}
202
203impl Default for DlgNode {
204 fn default() -> Self {
205 Self {
206 text: GffLocalizedString::new(StrRef::invalid()),
207 script: ResRef::blank(),
208 speaker: String::new(),
209 wait_flags: 0,
210 quest: String::new(),
211 quest_entry: 0,
212 plot_index: -1,
213 plot_xp_percentage: 1.0,
214 delay: u32::MAX,
215 fade_type: 0,
216 fade_color: [0.0, 0.0, 0.0],
217 fade_delay: 0.0,
218 fade_length: 0.0,
219 sound: ResRef::blank(),
220 vo_resref: ResRef::blank(),
221 sound_exists: false,
222 animations: Vec::new(),
223 listener: String::new(),
224 camera_angle: 0,
225 camera_id: -1,
226 cam_height_offset: 0.0,
227 tar_height_offset: 0.0,
228 camera_animation: 0,
229 cam_vid_effect: -1,
230 cam_field_of_view: -1.0,
231 links: Vec::new(),
232 }
233 }
234}
235
236impl DlgNode {
237 fn from_gff_struct(s: &GffStruct, link_field: &str) -> Self {
238 let animations = match s.field("AnimList") {
239 Some(GffValue::List(elements)) => {
240 elements.iter().map(DlgAnimation::from_gff_struct).collect()
241 }
242 _ => Vec::new(),
243 };
244
245 let links = match s.field(link_field) {
246 Some(GffValue::List(elements)) => {
247 elements.iter().map(DlgLink::from_gff_struct).collect()
248 }
249 _ => Vec::new(),
250 };
251
252 let fade_color = match s.field("FadeColor") {
253 Some(GffValue::Vector3(v)) => *v,
254 _ => [0.0, 0.0, 0.0],
255 };
256
257 Self {
258 text: get_locstring(s, "Text")
259 .cloned()
260 .unwrap_or_else(|| GffLocalizedString::new(StrRef::invalid())),
261 script: get_resref(s, "Script").unwrap_or_default(),
262 speaker: get_string(s, "Speaker").unwrap_or_default(),
263 wait_flags: get_u32(s, "WaitFlags").unwrap_or(0),
264 quest: get_string(s, "Quest").unwrap_or_default(),
265 quest_entry: get_u32(s, "QuestEntry").unwrap_or(0),
266 plot_index: get_i32(s, "PlotIndex").unwrap_or(-1),
267 plot_xp_percentage: get_f32(s, "PlotXPPercentage").unwrap_or(1.0),
268 delay: get_u32(s, "Delay").unwrap_or(u32::MAX),
269 fade_type: get_u8(s, "FadeType").unwrap_or(0),
270 fade_color,
271 fade_delay: get_f32(s, "FadeDelay").unwrap_or(0.0),
272 fade_length: get_f32(s, "FadeLength").unwrap_or(0.0),
273 sound: get_resref(s, "Sound").unwrap_or_default(),
274 vo_resref: get_resref(s, "VO_ResRef").unwrap_or_default(),
275 sound_exists: get_bool(s, "SoundExists").unwrap_or(false),
276 animations,
277 listener: get_string(s, "Listener").unwrap_or_default(),
278 camera_angle: get_u32(s, "CameraAngle").unwrap_or(0),
279 camera_id: get_i32(s, "CameraID").unwrap_or(-1),
280 cam_height_offset: get_f32(s, "CamHeightOffset").unwrap_or(0.0),
281 tar_height_offset: get_f32(s, "TarHeightOffset").unwrap_or(0.0),
282 camera_animation: get_u16(s, "CameraAnimation").unwrap_or(0),
283 cam_vid_effect: get_i32(s, "CamVidEffect").unwrap_or(-1),
284 cam_field_of_view: get_f32(s, "CamFieldOfView").unwrap_or(-1.0),
285 links,
286 }
287 }
288
289 fn to_gff_struct(
290 &self,
291 link_field: &str,
292 include_display_inactive: bool,
293 struct_id: i32,
294 ) -> GffStruct {
295 let mut s = GffStruct::new(struct_id);
296
297 upsert_field(&mut s, "Text", GffValue::LocalizedString(self.text.clone()));
298 upsert_field(&mut s, "Script", GffValue::ResRef(self.script));
299 upsert_field(&mut s, "Speaker", GffValue::String(self.speaker.clone()));
300 upsert_field(&mut s, "WaitFlags", GffValue::UInt32(self.wait_flags));
301 upsert_field(&mut s, "Quest", GffValue::String(self.quest.clone()));
302 upsert_field(&mut s, "QuestEntry", GffValue::UInt32(self.quest_entry));
303 upsert_field(&mut s, "PlotIndex", GffValue::Int32(self.plot_index));
304 upsert_field(
305 &mut s,
306 "PlotXPPercentage",
307 GffValue::Single(self.plot_xp_percentage),
308 );
309 upsert_field(&mut s, "Delay", GffValue::UInt32(self.delay));
310 upsert_field(&mut s, "FadeType", GffValue::UInt8(self.fade_type));
311 upsert_field(&mut s, "FadeColor", GffValue::Vector3(self.fade_color));
312 upsert_field(&mut s, "FadeDelay", GffValue::Single(self.fade_delay));
313 upsert_field(&mut s, "FadeLength", GffValue::Single(self.fade_length));
314 upsert_field(&mut s, "Sound", GffValue::ResRef(self.sound));
315 upsert_field(&mut s, "VO_ResRef", GffValue::ResRef(self.vo_resref));
316 upsert_field(
317 &mut s,
318 "SoundExists",
319 GffValue::UInt8(u8::from(self.sound_exists)),
320 );
321
322 let anim_structs: Vec<GffStruct> =
323 self.animations.iter().map(|a| a.to_gff_struct()).collect();
324 upsert_field(&mut s, "AnimList", GffValue::List(anim_structs));
325
326 upsert_field(&mut s, "Listener", GffValue::String(self.listener.clone()));
327 upsert_field(&mut s, "CameraAngle", GffValue::UInt32(self.camera_angle));
328 upsert_field(&mut s, "CameraID", GffValue::Int32(self.camera_id));
329 upsert_field(
330 &mut s,
331 "CamHeightOffset",
332 GffValue::Single(self.cam_height_offset),
333 );
334 upsert_field(
335 &mut s,
336 "TarHeightOffset",
337 GffValue::Single(self.tar_height_offset),
338 );
339 upsert_field(
340 &mut s,
341 "CameraAnimation",
342 GffValue::UInt16(self.camera_animation),
343 );
344 upsert_field(&mut s, "CamVidEffect", GffValue::Int32(self.cam_vid_effect));
345 upsert_field(
346 &mut s,
347 "CamFieldOfView",
348 GffValue::Single(self.cam_field_of_view),
349 );
350
351 let link_structs: Vec<GffStruct> = self
352 .links
353 .iter()
354 .map(|l| l.to_gff_struct(include_display_inactive))
355 .collect();
356 upsert_field(&mut s, link_field, GffValue::List(link_structs));
357
358 s
359 }
360}
361
362#[derive(Debug, Clone, PartialEq)]
364pub struct Dlg {
365 pub camera_model: ResRef,
368 pub delay_entry: u32,
370 pub delay_reply: u32,
372 pub end_conversation: ResRef,
374 pub end_conver_abort: ResRef,
376 pub skippable: bool,
378 pub conversation_type: i32,
380 pub computer_type: u8,
382 pub ambient_track: ResRef,
384 pub unequip_items: bool,
386 pub unequip_h_item: bool,
388 pub animated_cut: bool,
390 pub old_hit_check: bool,
392
393 pub starting_list: Vec<DlgLink>,
396 pub entries: Vec<DlgNode>,
398 pub replies: Vec<DlgNode>,
400 pub stunt_list: Vec<DlgStunt>,
402}
403
404impl Default for Dlg {
405 fn default() -> Self {
406 Self {
407 camera_model: ResRef::blank(),
408 delay_entry: 0,
409 delay_reply: 0,
410 end_conversation: ResRef::blank(),
411 end_conver_abort: ResRef::blank(),
412 skippable: true,
413 conversation_type: 0,
414 computer_type: 0,
415 ambient_track: ResRef::blank(),
416 unequip_items: false,
417 unequip_h_item: false,
418 animated_cut: false,
419 old_hit_check: false,
420 starting_list: Vec::new(),
421 entries: Vec::new(),
422 replies: Vec::new(),
423 stunt_list: Vec::new(),
424 }
425 }
426}
427
428impl Dlg {
429 pub fn new() -> Self {
431 Self::default()
432 }
433
434 pub fn from_gff(gff: &Gff) -> Result<Self, DlgError> {
436 if gff.file_type != *b"DLG " && gff.file_type != *b"GFF " {
437 return Err(DlgError::UnsupportedFileType(gff.file_type));
438 }
439
440 let root = &gff.root;
441
442 let starting_list = match root.field("StartingList") {
443 Some(GffValue::List(elements)) => {
444 elements.iter().map(DlgLink::from_gff_struct).collect()
445 }
446 _ => Vec::new(),
447 };
448
449 let entries = match root.field("EntryList") {
450 Some(GffValue::List(elements)) => elements
451 .iter()
452 .map(|s| DlgNode::from_gff_struct(s, "RepliesList"))
453 .collect(),
454 _ => Vec::new(),
455 };
456
457 let replies = match root.field("ReplyList") {
458 Some(GffValue::List(elements)) => elements
459 .iter()
460 .map(|s| DlgNode::from_gff_struct(s, "EntriesList"))
461 .collect(),
462 _ => Vec::new(),
463 };
464
465 let stunt_list = match root.field("StuntList") {
466 Some(GffValue::List(elements)) => {
467 elements.iter().map(DlgStunt::from_gff_struct).collect()
468 }
469 _ => Vec::new(),
470 };
471
472 Ok(Self {
473 camera_model: get_resref(root, "CameraModel").unwrap_or_default(),
474 delay_entry: get_u32(root, "DelayEntry").unwrap_or(0),
475 delay_reply: get_u32(root, "DelayReply").unwrap_or(0),
476 end_conversation: get_resref(root, "EndConversation").unwrap_or_default(),
477 end_conver_abort: get_resref(root, "EndConverAbort").unwrap_or_default(),
478 skippable: get_bool(root, "Skippable").unwrap_or(true),
479 conversation_type: get_i32(root, "ConversationType").unwrap_or(0),
480 computer_type: get_u8(root, "ComputerType").unwrap_or(0),
481 ambient_track: get_resref(root, "AmbientTrack").unwrap_or_default(),
482 unequip_items: get_bool(root, "UnequipItems").unwrap_or(false),
483 unequip_h_item: get_bool(root, "UnequipHItem").unwrap_or(false),
484 animated_cut: get_bool(root, "AnimatedCut").unwrap_or(false),
485 old_hit_check: get_bool(root, "OldHitCheck").unwrap_or(false),
486 starting_list,
487 entries,
488 replies,
489 stunt_list,
490 })
491 }
492
493 pub fn to_gff(&self) -> Gff {
497 let mut root = GffStruct::new(-1);
498
499 upsert_field(
500 &mut root,
501 "CameraModel",
502 GffValue::ResRef(self.camera_model),
503 );
504 upsert_field(&mut root, "DelayEntry", GffValue::UInt32(self.delay_entry));
505 upsert_field(&mut root, "DelayReply", GffValue::UInt32(self.delay_reply));
506 upsert_field(
507 &mut root,
508 "EndConversation",
509 GffValue::ResRef(self.end_conversation),
510 );
511 upsert_field(
512 &mut root,
513 "EndConverAbort",
514 GffValue::ResRef(self.end_conver_abort),
515 );
516 upsert_field(
517 &mut root,
518 "Skippable",
519 GffValue::UInt8(u8::from(self.skippable)),
520 );
521 upsert_field(
522 &mut root,
523 "ConversationType",
524 GffValue::Int32(self.conversation_type),
525 );
526 upsert_field(
527 &mut root,
528 "ComputerType",
529 GffValue::UInt8(self.computer_type),
530 );
531 upsert_field(
532 &mut root,
533 "AmbientTrack",
534 GffValue::ResRef(self.ambient_track),
535 );
536 upsert_field(
537 &mut root,
538 "UnequipItems",
539 GffValue::UInt8(u8::from(self.unequip_items)),
540 );
541 upsert_field(
542 &mut root,
543 "UnequipHItem",
544 GffValue::UInt8(u8::from(self.unequip_h_item)),
545 );
546 upsert_field(
547 &mut root,
548 "AnimatedCut",
549 GffValue::UInt8(u8::from(self.animated_cut)),
550 );
551 upsert_field(
552 &mut root,
553 "OldHitCheck",
554 GffValue::UInt8(u8::from(self.old_hit_check)),
555 );
556
557 let start_structs: Vec<GffStruct> = self
559 .starting_list
560 .iter()
561 .map(|l| l.to_gff_struct(false))
562 .collect();
563 upsert_field(&mut root, "StartingList", GffValue::List(start_structs));
564
565 let entry_structs: Vec<GffStruct> = self
567 .entries
568 .iter()
569 .map(|n| n.to_gff_struct("RepliesList", true, 0))
570 .collect();
571 upsert_field(&mut root, "EntryList", GffValue::List(entry_structs));
572
573 let reply_structs: Vec<GffStruct> = self
575 .replies
576 .iter()
577 .map(|n| n.to_gff_struct("EntriesList", false, 1))
578 .collect();
579 upsert_field(&mut root, "ReplyList", GffValue::List(reply_structs));
580
581 let stunt_structs: Vec<GffStruct> = self
583 .stunt_list
584 .iter()
585 .map(|st| st.to_gff_struct())
586 .collect();
587 upsert_field(&mut root, "StuntList", GffValue::List(stunt_structs));
588
589 Gff::new(*b"DLG ", root)
590 }
591}
592
593#[derive(Debug, Error)]
595pub enum DlgError {
596 #[error("unsupported DLG file type: {0:?}")]
598 UnsupportedFileType([u8; 4]),
599 #[error(transparent)]
601 Gff(#[from] GffBinaryError),
602}
603
604#[cfg_attr(
606 feature = "tracing",
607 tracing::instrument(level = "debug", skip(reader))
608)]
609pub fn read_dlg<R: Read>(reader: &mut R) -> Result<Dlg, DlgError> {
610 let gff = read_gff(reader)?;
611 Dlg::from_gff(&gff)
612}
613
614#[cfg_attr(
616 feature = "tracing",
617 tracing::instrument(level = "debug", skip(bytes), fields(bytes_len = bytes.len()))
618)]
619pub fn read_dlg_from_bytes(bytes: &[u8]) -> Result<Dlg, DlgError> {
620 let gff = read_gff_from_bytes(bytes)?;
621 Dlg::from_gff(&gff)
622}
623
624#[cfg_attr(
626 feature = "tracing",
627 tracing::instrument(level = "debug", skip(writer, dlg))
628)]
629pub fn write_dlg<W: Write>(writer: &mut W, dlg: &Dlg) -> Result<(), DlgError> {
630 let gff = dlg.to_gff();
631 write_gff(writer, &gff)?;
632 Ok(())
633}
634
635#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(dlg)))]
637pub fn write_dlg_to_vec(dlg: &Dlg) -> Result<Vec<u8>, DlgError> {
638 let mut cursor = Cursor::new(Vec::new());
639 write_dlg(&mut cursor, dlg)?;
640 Ok(cursor.into_inner())
641}
642
643static ENTRY_REPLY_LINK_CHILDREN: &[FieldSchema] = &[
649 FieldSchema {
650 label: "Active",
651 expected_type: GffType::ResRef,
652 required: false,
653 children: None,
654 constraint: None,
655 },
656 FieldSchema {
657 label: "Index",
658 expected_type: GffType::UInt32,
659 required: false,
660 children: None,
661 constraint: None,
662 },
663 FieldSchema {
664 label: "DisplayInactive",
665 expected_type: GffType::UInt8,
666 required: false,
667 children: None,
668 constraint: None,
669 },
670];
671
672static REPLY_ENTRY_LINK_CHILDREN: &[FieldSchema] = &[
676 FieldSchema {
677 label: "Active",
678 expected_type: GffType::ResRef,
679 required: false,
680 children: None,
681 constraint: None,
682 },
683 FieldSchema {
684 label: "Index",
685 expected_type: GffType::UInt32,
686 required: false,
687 children: None,
688 constraint: None,
689 },
690];
691
692static ANIM_LIST_CHILDREN: &[FieldSchema] = &[
694 FieldSchema {
695 label: "Participant",
696 expected_type: GffType::String,
697 required: false,
698 children: None,
699 constraint: None,
700 },
701 FieldSchema {
702 label: "Animation",
703 expected_type: GffType::UInt16,
704 required: false,
705 children: None,
706 constraint: None,
707 },
708];
709
710static STUNT_LIST_CHILDREN: &[FieldSchema] = &[
712 FieldSchema {
713 label: "Participant",
714 expected_type: GffType::String,
715 required: false,
716 children: None,
717 constraint: None,
718 },
719 FieldSchema {
720 label: "StuntModel",
721 expected_type: GffType::ResRef,
722 required: false,
723 children: None,
724 constraint: None,
725 },
726];
727
728static ENTRY_NODE_CHILDREN: &[FieldSchema] = &[
734 FieldSchema {
736 label: "Text",
737 expected_type: GffType::LocalizedString,
738 required: false,
739 children: None,
740 constraint: None,
741 },
742 FieldSchema {
743 label: "Script",
744 expected_type: GffType::ResRef,
745 required: false,
746 children: None,
747 constraint: None,
748 },
749 FieldSchema {
750 label: "Speaker",
751 expected_type: GffType::String,
752 required: false,
753 children: None,
754 constraint: None,
755 },
756 FieldSchema {
757 label: "WaitFlags",
758 expected_type: GffType::UInt32,
759 required: false,
760 children: None,
761 constraint: None,
762 },
763 FieldSchema {
764 label: "Quest",
765 expected_type: GffType::String,
766 required: false,
767 children: None,
768 constraint: None,
769 },
770 FieldSchema {
771 label: "QuestEntry",
772 expected_type: GffType::UInt32,
773 required: false,
774 children: None,
775 constraint: None,
776 },
777 FieldSchema {
778 label: "PlotIndex",
779 expected_type: GffType::Int32,
780 required: false,
781 children: None,
782 constraint: None,
783 },
784 FieldSchema {
785 label: "PlotXPPercentage",
786 expected_type: GffType::Single,
787 required: false,
788 children: None,
789 constraint: None,
790 },
791 FieldSchema {
792 label: "Delay",
793 expected_type: GffType::UInt32,
794 required: false,
795 children: None,
796 constraint: None,
797 },
798 FieldSchema {
799 label: "FadeType",
800 expected_type: GffType::UInt8,
801 required: false,
802 children: None,
803 constraint: None,
804 },
805 FieldSchema {
806 label: "FadeColor",
807 expected_type: GffType::Vector3,
808 required: false,
809 children: None,
810 constraint: None,
811 },
812 FieldSchema {
813 label: "FadeDelay",
814 expected_type: GffType::Single,
815 required: false,
816 children: None,
817 constraint: None,
818 },
819 FieldSchema {
820 label: "FadeLength",
821 expected_type: GffType::Single,
822 required: false,
823 children: None,
824 constraint: None,
825 },
826 FieldSchema {
827 label: "Sound",
828 expected_type: GffType::ResRef,
829 required: false,
830 children: None,
831 constraint: None,
832 },
833 FieldSchema {
834 label: "VO_ResRef",
835 expected_type: GffType::ResRef,
836 required: false,
837 children: None,
838 constraint: None,
839 },
840 FieldSchema {
841 label: "SoundExists",
842 expected_type: GffType::UInt8,
843 required: false,
844 children: None,
845 constraint: None,
846 },
847 FieldSchema {
848 label: "AnimList",
849 expected_type: GffType::List,
850 required: false,
851 children: Some(ANIM_LIST_CHILDREN),
852 constraint: None,
853 },
854 FieldSchema {
856 label: "Listener",
857 expected_type: GffType::String,
858 required: false,
859 children: None,
860 constraint: None,
861 },
862 FieldSchema {
863 label: "CameraAngle",
864 expected_type: GffType::UInt32,
865 required: false,
866 children: None,
867 constraint: None,
868 },
869 FieldSchema {
870 label: "CameraID",
871 expected_type: GffType::Int32,
872 required: false,
873 children: None,
874 constraint: None,
875 },
876 FieldSchema {
877 label: "CamHeightOffset",
878 expected_type: GffType::Single,
879 required: false,
880 children: None,
881 constraint: None,
882 },
883 FieldSchema {
884 label: "TarHeightOffset",
885 expected_type: GffType::Single,
886 required: false,
887 children: None,
888 constraint: None,
889 },
890 FieldSchema {
891 label: "CameraAnimation",
892 expected_type: GffType::UInt16,
893 required: false,
894 children: None,
895 constraint: None,
896 },
897 FieldSchema {
898 label: "CamVidEffect",
899 expected_type: GffType::Int32,
900 required: false,
901 children: None,
902 constraint: None,
903 },
904 FieldSchema {
905 label: "CamFieldOfView",
906 expected_type: GffType::Single,
907 required: false,
908 children: None,
909 constraint: None,
910 },
911 FieldSchema {
913 label: "RepliesList",
914 expected_type: GffType::List,
915 required: false,
916 children: Some(ENTRY_REPLY_LINK_CHILDREN),
917 constraint: None,
918 },
919];
920
921static REPLY_NODE_CHILDREN: &[FieldSchema] = &[
923 FieldSchema {
925 label: "Text",
926 expected_type: GffType::LocalizedString,
927 required: false,
928 children: None,
929 constraint: None,
930 },
931 FieldSchema {
932 label: "Script",
933 expected_type: GffType::ResRef,
934 required: false,
935 children: None,
936 constraint: None,
937 },
938 FieldSchema {
939 label: "Speaker",
940 expected_type: GffType::String,
941 required: false,
942 children: None,
943 constraint: None,
944 },
945 FieldSchema {
946 label: "WaitFlags",
947 expected_type: GffType::UInt32,
948 required: false,
949 children: None,
950 constraint: None,
951 },
952 FieldSchema {
953 label: "Quest",
954 expected_type: GffType::String,
955 required: false,
956 children: None,
957 constraint: None,
958 },
959 FieldSchema {
960 label: "QuestEntry",
961 expected_type: GffType::UInt32,
962 required: false,
963 children: None,
964 constraint: None,
965 },
966 FieldSchema {
967 label: "PlotIndex",
968 expected_type: GffType::Int32,
969 required: false,
970 children: None,
971 constraint: None,
972 },
973 FieldSchema {
974 label: "PlotXPPercentage",
975 expected_type: GffType::Single,
976 required: false,
977 children: None,
978 constraint: None,
979 },
980 FieldSchema {
981 label: "Delay",
982 expected_type: GffType::UInt32,
983 required: false,
984 children: None,
985 constraint: None,
986 },
987 FieldSchema {
988 label: "FadeType",
989 expected_type: GffType::UInt8,
990 required: false,
991 children: None,
992 constraint: None,
993 },
994 FieldSchema {
995 label: "FadeColor",
996 expected_type: GffType::Vector3,
997 required: false,
998 children: None,
999 constraint: None,
1000 },
1001 FieldSchema {
1002 label: "FadeDelay",
1003 expected_type: GffType::Single,
1004 required: false,
1005 children: None,
1006 constraint: None,
1007 },
1008 FieldSchema {
1009 label: "FadeLength",
1010 expected_type: GffType::Single,
1011 required: false,
1012 children: None,
1013 constraint: None,
1014 },
1015 FieldSchema {
1016 label: "Sound",
1017 expected_type: GffType::ResRef,
1018 required: false,
1019 children: None,
1020 constraint: None,
1021 },
1022 FieldSchema {
1023 label: "VO_ResRef",
1024 expected_type: GffType::ResRef,
1025 required: false,
1026 children: None,
1027 constraint: None,
1028 },
1029 FieldSchema {
1030 label: "SoundExists",
1031 expected_type: GffType::UInt8,
1032 required: false,
1033 children: None,
1034 constraint: None,
1035 },
1036 FieldSchema {
1037 label: "AnimList",
1038 expected_type: GffType::List,
1039 required: false,
1040 children: Some(ANIM_LIST_CHILDREN),
1041 constraint: None,
1042 },
1043 FieldSchema {
1045 label: "Listener",
1046 expected_type: GffType::String,
1047 required: false,
1048 children: None,
1049 constraint: None,
1050 },
1051 FieldSchema {
1052 label: "CameraAngle",
1053 expected_type: GffType::UInt32,
1054 required: false,
1055 children: None,
1056 constraint: None,
1057 },
1058 FieldSchema {
1059 label: "CameraID",
1060 expected_type: GffType::Int32,
1061 required: false,
1062 children: None,
1063 constraint: None,
1064 },
1065 FieldSchema {
1066 label: "CamHeightOffset",
1067 expected_type: GffType::Single,
1068 required: false,
1069 children: None,
1070 constraint: None,
1071 },
1072 FieldSchema {
1073 label: "TarHeightOffset",
1074 expected_type: GffType::Single,
1075 required: false,
1076 children: None,
1077 constraint: None,
1078 },
1079 FieldSchema {
1080 label: "CameraAnimation",
1081 expected_type: GffType::UInt16,
1082 required: false,
1083 children: None,
1084 constraint: None,
1085 },
1086 FieldSchema {
1087 label: "CamVidEffect",
1088 expected_type: GffType::Int32,
1089 required: false,
1090 children: None,
1091 constraint: None,
1092 },
1093 FieldSchema {
1094 label: "CamFieldOfView",
1095 expected_type: GffType::Single,
1096 required: false,
1097 children: None,
1098 constraint: None,
1099 },
1100 FieldSchema {
1102 label: "EntriesList",
1103 expected_type: GffType::List,
1104 required: false,
1105 children: Some(REPLY_ENTRY_LINK_CHILDREN),
1106 constraint: None,
1107 },
1108];
1109
1110impl GffSchema for Dlg {
1111 fn schema() -> &'static [FieldSchema] {
1112 static SCHEMA: &[FieldSchema] = &[
1113 FieldSchema {
1115 label: "CameraModel",
1116 expected_type: GffType::ResRef,
1117 required: false,
1118 children: None,
1119 constraint: None,
1120 },
1121 FieldSchema {
1122 label: "DelayEntry",
1123 expected_type: GffType::UInt32,
1124 required: false,
1125 children: None,
1126 constraint: None,
1127 },
1128 FieldSchema {
1129 label: "DelayReply",
1130 expected_type: GffType::UInt32,
1131 required: false,
1132 children: None,
1133 constraint: None,
1134 },
1135 FieldSchema {
1136 label: "EndConversation",
1137 expected_type: GffType::ResRef,
1138 required: false,
1139 children: None,
1140 constraint: None,
1141 },
1142 FieldSchema {
1143 label: "EndConverAbort",
1144 expected_type: GffType::ResRef,
1145 required: false,
1146 children: None,
1147 constraint: None,
1148 },
1149 FieldSchema {
1150 label: "Skippable",
1151 expected_type: GffType::UInt8,
1152 required: false,
1153 children: None,
1154 constraint: None,
1155 },
1156 FieldSchema {
1157 label: "ConversationType",
1158 expected_type: GffType::Int32,
1159 required: false,
1160 children: None,
1161 constraint: None,
1162 },
1163 FieldSchema {
1164 label: "ComputerType",
1165 expected_type: GffType::UInt8,
1166 required: false,
1167 children: None,
1168 constraint: None,
1169 },
1170 FieldSchema {
1171 label: "AmbientTrack",
1172 expected_type: GffType::ResRef,
1173 required: false,
1174 children: None,
1175 constraint: None,
1176 },
1177 FieldSchema {
1178 label: "UnequipItems",
1179 expected_type: GffType::UInt8,
1180 required: false,
1181 children: None,
1182 constraint: None,
1183 },
1184 FieldSchema {
1185 label: "UnequipHItem",
1186 expected_type: GffType::UInt8,
1187 required: false,
1188 children: None,
1189 constraint: None,
1190 },
1191 FieldSchema {
1192 label: "AnimatedCut",
1193 expected_type: GffType::UInt8,
1194 required: false,
1195 children: None,
1196 constraint: None,
1197 },
1198 FieldSchema {
1199 label: "OldHitCheck",
1200 expected_type: GffType::UInt8,
1201 required: false,
1202 children: None,
1203 constraint: None,
1204 },
1205 FieldSchema {
1207 label: "EntryList",
1208 expected_type: GffType::List,
1209 required: false,
1210 children: Some(ENTRY_NODE_CHILDREN),
1211 constraint: None,
1212 },
1213 FieldSchema {
1214 label: "ReplyList",
1215 expected_type: GffType::List,
1216 required: false,
1217 children: Some(REPLY_NODE_CHILDREN),
1218 constraint: None,
1219 },
1220 FieldSchema {
1221 label: "StartingList",
1222 expected_type: GffType::List,
1223 required: false,
1224 children: Some(REPLY_ENTRY_LINK_CHILDREN),
1225 constraint: None,
1226 },
1227 FieldSchema {
1228 label: "StuntList",
1229 expected_type: GffType::List,
1230 required: false,
1231 children: Some(STUNT_LIST_CHILDREN),
1232 constraint: None,
1233 },
1234 ];
1235 SCHEMA
1236 }
1237}
1238
1239#[cfg(test)]
1240mod tests {
1241 use super::*;
1242
1243 fn make_test_dlg_gff() -> Gff {
1245 let mut root = GffStruct::new(-1);
1246 root.push_field("CameraModel", GffValue::resref_lit(""));
1247 root.push_field("DelayEntry", GffValue::UInt32(0));
1248 root.push_field("DelayReply", GffValue::UInt32(0));
1249 root.push_field("EndConversation", GffValue::resref_lit("k_end_conv"));
1250 root.push_field("EndConverAbort", GffValue::resref_lit("k_end_abort"));
1251 root.push_field("Skippable", GffValue::UInt8(1));
1252 root.push_field("ConversationType", GffValue::Int32(0));
1253 root.push_field("ComputerType", GffValue::UInt8(0));
1254 root.push_field("AmbientTrack", GffValue::resref_lit(""));
1255 root.push_field("UnequipItems", GffValue::UInt8(0));
1256 root.push_field("UnequipHItem", GffValue::UInt8(0));
1257 root.push_field("AnimatedCut", GffValue::UInt8(1));
1258 root.push_field("OldHitCheck", GffValue::UInt8(0));
1259
1260 let mut start_link = GffStruct::new(0);
1262 start_link.push_field("Active", GffValue::resref_lit("k_cond_start"));
1263 start_link.push_field("Index", GffValue::UInt32(0));
1264 root.push_field("StartingList", GffValue::List(vec![start_link]));
1265
1266 let mut entry0 = GffStruct::new(0);
1268 entry0.push_field(
1269 "Text",
1270 GffValue::LocalizedString(GffLocalizedString::new(StrRef::from_raw(50000))),
1271 );
1272 entry0.push_field("Script", GffValue::resref_lit("k_entry_fire"));
1273 entry0.push_field("Speaker", GffValue::String("Bastila".into()));
1274 entry0.push_field("WaitFlags", GffValue::UInt32(0));
1275 entry0.push_field("Quest", GffValue::String("".into()));
1276 entry0.push_field("QuestEntry", GffValue::UInt32(0));
1277 entry0.push_field("PlotIndex", GffValue::Int32(-1));
1278 entry0.push_field("PlotXPPercentage", GffValue::Single(1.0));
1279 entry0.push_field("Delay", GffValue::UInt32(u32::MAX));
1280 entry0.push_field("FadeType", GffValue::UInt8(0));
1281 entry0.push_field("FadeColor", GffValue::Vector3([0.0, 0.0, 0.0]));
1282 entry0.push_field("FadeDelay", GffValue::Single(0.0));
1283 entry0.push_field("FadeLength", GffValue::Single(0.0));
1284 entry0.push_field("Sound", GffValue::resref_lit(""));
1285 entry0.push_field("VO_ResRef", GffValue::resref_lit("n_bastila_hi"));
1286 entry0.push_field("SoundExists", GffValue::UInt8(1));
1287 entry0.push_field("Listener", GffValue::String("".into()));
1288 entry0.push_field("CameraAngle", GffValue::UInt32(4));
1289 entry0.push_field("CameraID", GffValue::Int32(-1));
1290 entry0.push_field("CamHeightOffset", GffValue::Single(0.0));
1291 entry0.push_field("TarHeightOffset", GffValue::Single(0.0));
1292 entry0.push_field("CameraAnimation", GffValue::UInt16(0));
1293 entry0.push_field("CamVidEffect", GffValue::Int32(-1));
1294 entry0.push_field("CamFieldOfView", GffValue::Single(-1.0));
1295
1296 let mut anim = GffStruct::new(0);
1298 anim.push_field("Participant", GffValue::String("Bastila".into()));
1299 anim.push_field("Animation", GffValue::UInt16(28));
1300 entry0.push_field("AnimList", GffValue::List(vec![anim]));
1301
1302 let mut reply_link = GffStruct::new(0);
1304 reply_link.push_field("Active", GffValue::resref_lit(""));
1305 reply_link.push_field("Index", GffValue::UInt32(0));
1306 reply_link.push_field("DisplayInactive", GffValue::UInt8(1));
1307 entry0.push_field("RepliesList", GffValue::List(vec![reply_link]));
1308
1309 let mut reply0 = GffStruct::new(1);
1311 reply0.push_field(
1312 "Text",
1313 GffValue::LocalizedString(GffLocalizedString::new(StrRef::from_raw(50001))),
1314 );
1315 reply0.push_field("Script", GffValue::resref_lit("k_reply_fire"));
1316 reply0.push_field("Speaker", GffValue::String("".into()));
1317 reply0.push_field("WaitFlags", GffValue::UInt32(0));
1318 reply0.push_field("Quest", GffValue::String("".into()));
1319 reply0.push_field("QuestEntry", GffValue::UInt32(0));
1320 reply0.push_field("PlotIndex", GffValue::Int32(-1));
1321 reply0.push_field("PlotXPPercentage", GffValue::Single(1.0));
1322 reply0.push_field("Delay", GffValue::UInt32(u32::MAX));
1323 reply0.push_field("FadeType", GffValue::UInt8(0));
1324 reply0.push_field("FadeColor", GffValue::Vector3([0.0, 0.0, 0.0]));
1325 reply0.push_field("FadeDelay", GffValue::Single(0.0));
1326 reply0.push_field("FadeLength", GffValue::Single(0.0));
1327 reply0.push_field("Sound", GffValue::resref_lit(""));
1328 reply0.push_field("VO_ResRef", GffValue::resref_lit(""));
1329 reply0.push_field("SoundExists", GffValue::UInt8(0));
1330 reply0.push_field("AnimList", GffValue::List(vec![]));
1331 reply0.push_field("Listener", GffValue::String("".into()));
1332 reply0.push_field("CameraAngle", GffValue::UInt32(0));
1333 reply0.push_field("CameraID", GffValue::Int32(-1));
1334 reply0.push_field("CamHeightOffset", GffValue::Single(0.0));
1335 reply0.push_field("TarHeightOffset", GffValue::Single(0.0));
1336 reply0.push_field("CameraAnimation", GffValue::UInt16(0));
1337 reply0.push_field("CamVidEffect", GffValue::Int32(-1));
1338 reply0.push_field("CamFieldOfView", GffValue::Single(-1.0));
1339
1340 let mut entry_link = GffStruct::new(0);
1341 entry_link.push_field("Active", GffValue::resref_lit(""));
1342 entry_link.push_field("Index", GffValue::UInt32(0));
1343 reply0.push_field("EntriesList", GffValue::List(vec![entry_link]));
1344
1345 root.push_field("EntryList", GffValue::List(vec![entry0]));
1346 root.push_field("ReplyList", GffValue::List(vec![reply0]));
1347
1348 let mut stunt = GffStruct::new(0);
1350 stunt.push_field("Participant", GffValue::String("Bastila".into()));
1351 stunt.push_field("StuntModel", GffValue::resref_lit("p_bastilabb"));
1352 root.push_field("StuntList", GffValue::List(vec![stunt]));
1353
1354 Gff::new(*b"DLG ", root)
1355 }
1356
1357 #[test]
1358 fn reads_conversation_config() {
1359 let gff = make_test_dlg_gff();
1360 let dlg = Dlg::from_gff(&gff).expect("test GFF is well-formed");
1361
1362 assert_eq!(dlg.end_conversation, "k_end_conv");
1363 assert_eq!(dlg.end_conver_abort, "k_end_abort");
1364 assert!(dlg.skippable);
1365 assert_eq!(dlg.conversation_type, 0);
1366 assert!(dlg.animated_cut);
1367 assert!(!dlg.old_hit_check);
1368 }
1369
1370 #[test]
1371 fn reads_starting_list() {
1372 let gff = make_test_dlg_gff();
1373 let dlg = Dlg::from_gff(&gff).expect("test GFF is well-formed");
1374
1375 assert_eq!(dlg.starting_list.len(), 1);
1376 assert_eq!(dlg.starting_list[0].active, "k_cond_start");
1377 assert_eq!(dlg.starting_list[0].index, 0);
1378 }
1379
1380 #[test]
1381 fn reads_entry_node() {
1382 let gff = make_test_dlg_gff();
1383 let dlg = Dlg::from_gff(&gff).expect("test GFF is well-formed");
1384
1385 assert_eq!(dlg.entries.len(), 1);
1386 let entry = &dlg.entries[0];
1387 assert_eq!(entry.text.string_ref.raw(), 50000);
1388 assert_eq!(entry.script, "k_entry_fire");
1389 assert_eq!(entry.speaker, "Bastila");
1390 assert_eq!(entry.vo_resref, "n_bastila_hi");
1391 assert!(entry.sound_exists);
1392 assert_eq!(entry.camera_angle, 4);
1393 assert_eq!(entry.animations.len(), 1);
1394 assert_eq!(entry.animations[0].participant, "Bastila");
1395 assert_eq!(entry.animations[0].animation, 28);
1396 assert_eq!(entry.links.len(), 1);
1397 assert_eq!(entry.links[0].index, 0);
1398 assert!(entry.links[0].display_inactive);
1399 }
1400
1401 #[test]
1402 fn reads_reply_node() {
1403 let gff = make_test_dlg_gff();
1404 let dlg = Dlg::from_gff(&gff).expect("test GFF is well-formed");
1405
1406 assert_eq!(dlg.replies.len(), 1);
1407 let reply = &dlg.replies[0];
1408 assert_eq!(reply.text.string_ref.raw(), 50001);
1409 assert_eq!(reply.script, "k_reply_fire");
1410 assert!(!reply.sound_exists);
1411 assert_eq!(reply.links.len(), 1);
1412 assert_eq!(reply.links[0].index, 0);
1413 }
1414
1415 #[test]
1416 fn reads_stunt_list() {
1417 let gff = make_test_dlg_gff();
1418 let dlg = Dlg::from_gff(&gff).expect("test GFF is well-formed");
1419
1420 assert_eq!(dlg.stunt_list.len(), 1);
1421 assert_eq!(dlg.stunt_list[0].participant, "Bastila");
1422 assert_eq!(dlg.stunt_list[0].stunt_model, "p_bastilabb");
1423 }
1424
1425 #[test]
1426 fn all_fields_survive_typed_roundtrip() {
1427 let gff = make_test_dlg_gff();
1428 let dlg = Dlg::from_gff(&gff).expect("test GFF is well-formed");
1429 let bytes = write_dlg_to_vec(&dlg).expect("write succeeds");
1430 let reparsed = read_dlg_from_bytes(&bytes).expect("reparse succeeds");
1431
1432 assert_eq!(dlg, reparsed);
1433 }
1434
1435 #[test]
1436 fn fade_color_roundtrips_as_vector3() {
1437 let mut gff = make_test_dlg_gff();
1438 if let Some(GffValue::List(ref mut entries)) = gff
1440 .root
1441 .fields
1442 .iter_mut()
1443 .find(|f| f.label == "EntryList")
1444 .map(|f| &mut f.value)
1445 {
1446 if let Some(field) = entries[0]
1448 .fields
1449 .iter_mut()
1450 .find(|f| f.label == "FadeColor")
1451 {
1452 field.value = GffValue::Vector3([1.0, 0.0, 0.0]);
1453 }
1454 }
1455
1456 let dlg = Dlg::from_gff(&gff).expect("test GFF is well-formed");
1457 assert_eq!(dlg.entries[0].fade_color, [1.0, 0.0, 0.0]);
1458
1459 let bytes = write_dlg_to_vec(&dlg).expect("write succeeds");
1460 let reparsed = read_dlg_from_bytes(&bytes).expect("reparse succeeds");
1461 assert_eq!(reparsed.entries[0].fade_color, [1.0, 0.0, 0.0]);
1462
1463 let rebuilt = dlg.to_gff();
1465 if let Some(GffValue::List(entries)) = rebuilt.root.field("EntryList") {
1466 let fade = entries[0]
1467 .field("FadeColor")
1468 .expect("FadeColor must be present");
1469 assert!(
1470 matches!(fade, GffValue::Vector3([1.0, 0.0, 0.0])),
1471 "FadeColor must be GffValue::Vector3, got {fade:?}"
1472 );
1473 } else {
1474 panic!("EntryList missing from rebuilt GFF");
1475 }
1476 }
1477
1478 #[test]
1479 fn typed_edits_roundtrip_through_gff_writer() {
1480 let gff = make_test_dlg_gff();
1481 let mut dlg = Dlg::from_gff(&gff).expect("test GFF is well-formed");
1482 dlg.end_conversation = ResRef::new("k_end_new").expect("valid test resref");
1483 dlg.entries[0].speaker = "Carth".into();
1484 dlg.entries[0].links[0].display_inactive = false;
1485
1486 let bytes = write_dlg_to_vec(&dlg).expect("write succeeds");
1487 let reparsed = read_dlg_from_bytes(&bytes).expect("reparse succeeds");
1488
1489 assert_eq!(reparsed.end_conversation, "k_end_new");
1490 assert_eq!(reparsed.entries[0].speaker, "Carth");
1491 assert!(!reparsed.entries[0].links[0].display_inactive);
1492 }
1493
1494 #[test]
1495 fn read_dlg_from_reader_matches_bytes_path() {
1496 let gff = make_test_dlg_gff();
1497 let bytes = {
1498 let mut c = Cursor::new(Vec::new());
1499 write_gff(&mut c, &gff).expect("test GFF serializes cleanly");
1500 c.into_inner()
1501 };
1502
1503 let mut cursor = Cursor::new(&bytes);
1504 let via_reader = read_dlg(&mut cursor).expect("reader parse succeeds");
1505 let via_bytes = read_dlg_from_bytes(&bytes).expect("bytes parse succeeds");
1506
1507 assert_eq!(via_reader, via_bytes);
1508 }
1509
1510 #[test]
1511 fn rejects_non_dlg_file_type() {
1512 let mut gff = make_test_dlg_gff();
1513 gff.file_type = *b"UTT ";
1514
1515 let err = Dlg::from_gff(&gff).expect_err("UTT must be rejected as DLG input");
1516 assert!(matches!(
1517 err,
1518 DlgError::UnsupportedFileType(file_type) if file_type == *b"UTT "
1519 ));
1520 }
1521
1522 #[test]
1523 fn write_dlg_matches_direct_gff_writer() {
1524 let gff = make_test_dlg_gff();
1525 let dlg = Dlg::from_gff(&gff).expect("test GFF is well-formed");
1526
1527 let via_typed = write_dlg_to_vec(&dlg).expect("typed write succeeds");
1528
1529 let mut direct = Cursor::new(Vec::new());
1530 write_gff(&mut direct, &dlg.to_gff()).expect("direct write succeeds");
1531
1532 assert_eq!(via_typed, direct.into_inner());
1533 }
1534
1535 #[test]
1536 fn schema_field_count() {
1537 assert_eq!(Dlg::schema().len(), 17);
1538 }
1539
1540 #[test]
1541 fn schema_no_duplicate_labels() {
1542 let schema = Dlg::schema();
1543 let mut labels: Vec<&str> = schema.iter().map(|f| f.label).collect();
1544 labels.sort();
1545 let before = labels.len();
1546 labels.dedup();
1547 assert_eq!(before, labels.len(), "duplicate labels in DLG schema");
1548 }
1549
1550 #[test]
1551 fn schema_entry_node_count() {
1552 let entry_list = Dlg::schema()
1553 .iter()
1554 .find(|f| f.label == "EntryList")
1555 .expect("EntryList must exist");
1556 let children = entry_list.children.expect("EntryList must have children");
1557 assert_eq!(children.len(), 26);
1558 }
1559
1560 #[test]
1561 fn schema_reply_node_count() {
1562 let reply_list = Dlg::schema()
1563 .iter()
1564 .find(|f| f.label == "ReplyList")
1565 .expect("ReplyList must exist");
1566 let children = reply_list.children.expect("ReplyList must have children");
1567 assert_eq!(children.len(), 26);
1568 }
1569}