1use std::io::{Cursor, Read, Write};
16
17use crate::gff_helpers::{get_bool, get_u32, get_u8, upsert_field};
18use crate::git::creature::SavedCreature;
19use crate::git::door::SavedDoor;
20use crate::git::item::SavedItem;
21use crate::git::placeable::SavedPlaceable;
22use crate::git::sound::SavedSound;
23use crate::git::store::SavedStore;
24use crate::git::trigger::SavedTrigger;
25use crate::shared::ObjectId;
26use rakata_formats::{
27 gff_schema::{AbsentDefault, FieldLife, FieldSchema, GffSchema, GffType},
28 read_gff, read_gff_from_bytes, write_gff, Gff, GffBinaryError, GffStruct, GffValue,
29};
30use thiserror::Error;
31
32pub mod area_effect;
33pub mod area_properties;
34pub mod camera;
35pub mod creature;
36pub mod door;
37pub mod encounter;
38pub mod item;
39pub mod placeable;
40pub mod sound;
41pub mod store;
42pub mod trigger;
43pub mod waypoint;
44
45pub use area_effect::{GitAreaEffect, GitAreaEffectShape};
46pub use area_properties::GitAreaProperties;
47pub use camera::GitCamera;
48pub use creature::GitCreature;
49pub use door::GitDoor;
50pub use encounter::{GitEncounter, GitEncounterPoint, GitSpawnPoint};
51pub use item::GitItem;
52pub use placeable::GitPlaceable;
53pub use sound::GitSound;
54pub use store::GitStore;
55pub use trigger::GitTrigger;
56pub use waypoint::{GitMapNote, GitWaypoint};
57
58#[derive(Debug, Clone, PartialEq)]
84pub enum GitObjects<S, T> {
85 Static(Vec<S>),
87 Saved(Vec<T>),
89}
90
91impl<S, T> Default for GitObjects<S, T> {
92 fn default() -> Self {
95 Self::Saved(Vec::new())
96 }
97}
98
99impl<S, T> GitObjects<S, T> {
100 pub fn uses_templates(&self) -> bool {
103 matches!(self, Self::Static(_))
104 }
105
106 pub fn len(&self) -> usize {
108 match self {
109 Self::Static(entries) => entries.len(),
110 Self::Saved(entries) => entries.len(),
111 }
112 }
113
114 pub fn is_empty(&self) -> bool {
116 self.len() == 0
117 }
118
119 pub fn as_static(&self) -> Option<&[S]> {
121 match self {
122 Self::Static(entries) => Some(entries),
123 Self::Saved(_) => None,
124 }
125 }
126
127 pub fn as_saved(&self) -> Option<&[T]> {
129 match self {
130 Self::Saved(entries) => Some(entries),
131 Self::Static(_) => None,
132 }
133 }
134
135 pub fn as_static_mut(&mut self) -> Option<&mut Vec<S>> {
137 match self {
138 Self::Static(entries) => Some(entries),
139 Self::Saved(_) => None,
140 }
141 }
142
143 pub fn as_saved_mut(&mut self) -> Option<&mut Vec<T>> {
145 match self {
146 Self::Saved(entries) => Some(entries),
147 Self::Static(_) => None,
148 }
149 }
150}
151
152pub(crate) fn object_id_or_invalid(s: &GffStruct) -> ObjectId {
162 get_u32(s, "ObjectId").map_or(ObjectId::INVALID, ObjectId::new)
163}
164
165pub type GitCreatures = GitObjects<GitCreature, SavedCreature>;
167
168pub type GitDoors = GitObjects<GitDoor, SavedDoor>;
170
171pub type GitPlaceables = GitObjects<GitPlaceable, SavedPlaceable>;
173
174pub type GitItems = GitObjects<GitItem, SavedItem>;
176
177pub type GitSounds = GitObjects<GitSound, SavedSound>;
179
180pub type GitStores = GitObjects<GitStore, SavedStore>;
182
183pub type GitTriggers = GitObjects<GitTrigger, SavedTrigger>;
185
186#[derive(Debug, Clone, PartialEq, Default)]
201pub struct Git {
202 pub current_weather: u8,
205 pub weather_started: bool,
207 pub creatures: GitCreatures,
213 pub items: GitItems,
215 pub doors: GitDoors,
217 pub placeables: GitPlaceables,
219 pub waypoints: Vec<GitWaypoint>,
221 pub sounds: GitSounds,
223 pub triggers: GitTriggers,
225 pub stores: GitStores,
227 pub encounters: Vec<GitEncounter>,
239 pub area_effects: Vec<GitAreaEffect>,
241
242 pub area_properties: Option<GitAreaProperties>,
245
246 pub cameras: Vec<GitCamera>,
249}
250
251impl Git {
252 pub fn new() -> Self {
254 Self::default()
255 }
256
257 pub fn from_gff(gff: &Gff) -> Result<Self, GitError> {
259 if gff.file_type != *b"GIT " && gff.file_type != *b"GFF " {
260 return Err(GitError::UnsupportedFileType(gff.file_type));
261 }
262
263 let root = &gff.root;
264
265 fn read_list<T>(root: &GffStruct, label: &str, f: fn(&GffStruct) -> T) -> Vec<T> {
266 match root.field(label) {
267 Some(GffValue::List(elements)) => elements.iter().map(f).collect(),
268 _ => Vec::new(),
269 }
270 }
271
272 fn read_objects<S, T>(
274 root: &GffStruct,
275 label: &str,
276 use_templates: bool,
277 read_static: fn(&GffStruct) -> S,
278 read_saved: fn(&GffStruct) -> T,
279 ) -> GitObjects<S, T> {
280 if use_templates {
281 GitObjects::Static(read_list(root, label, read_static))
282 } else {
283 GitObjects::Saved(read_list(root, label, read_saved))
284 }
285 }
286
287 let use_templates = get_bool(root, "UseTemplates").unwrap_or(false);
288
289 let area_properties = match root.field("AreaProperties") {
290 Some(GffValue::Struct(s)) => Some(GitAreaProperties::from_gff_struct(s)),
291 _ => None,
292 };
293
294 Ok(Self {
295 current_weather: get_u8(root, "CurrentWeather").unwrap_or(0),
296 weather_started: get_bool(root, "WeatherStarted").unwrap_or(false),
297
298 creatures: if get_bool(root, "UseTemplates").unwrap_or(false) {
299 GitCreatures::Static(read_list(
300 root,
301 "Creature List",
302 GitCreature::from_gff_struct,
303 ))
304 } else {
305 GitCreatures::Saved(read_list(root, "Creature List", SavedCreature::from_struct))
306 },
307 items: read_objects(
308 root,
309 "List",
310 use_templates,
311 GitItem::from_gff_struct,
312 SavedItem::from_struct,
313 ),
314 doors: read_objects(
315 root,
316 "Door List",
317 use_templates,
318 GitDoor::from_gff_struct,
319 SavedDoor::from_struct,
320 ),
321 placeables: read_objects(
322 root,
323 "Placeable List",
324 use_templates,
325 GitPlaceable::from_gff_struct,
326 SavedPlaceable::from_struct,
327 ),
328 waypoints: read_list(root, "WaypointList", GitWaypoint::from_gff_struct),
329 sounds: read_objects(
330 root,
331 "SoundList",
332 use_templates,
333 GitSound::from_gff_struct,
334 SavedSound::from_struct,
335 ),
336 triggers: read_objects(
337 root,
338 "TriggerList",
339 use_templates,
340 GitTrigger::from_gff_struct,
341 SavedTrigger::from_struct,
342 ),
343 stores: read_objects(
344 root,
345 "StoreList",
346 use_templates,
347 GitStore::from_gff_struct,
348 SavedStore::from_struct,
349 ),
350 encounters: read_list(root, "Encounter List", GitEncounter::from_gff_struct),
351 area_effects: read_list(root, "AreaEffectList", GitAreaEffect::from_gff_struct),
352
353 area_properties,
354 cameras: read_list(root, "CameraList", GitCamera::from_gff_struct),
355 })
356 }
357
358 pub fn to_gff(&self) -> Gff {
360 let mut root = GffStruct::new(-1);
361
362 upsert_field(
363 &mut root,
364 "CurrentWeather",
365 GffValue::UInt8(self.current_weather),
366 );
367 upsert_field(
368 &mut root,
369 "WeatherStarted",
370 GffValue::UInt8(u8::from(self.weather_started)),
371 );
372 if self.creatures.uses_templates() {
375 upsert_field(&mut root, "UseTemplates", GffValue::UInt8(1));
376 }
377
378 fn write_list<T>(root: &mut GffStruct, label: &str, items: &[T], f: fn(&T) -> GffStruct) {
379 let structs: Vec<GffStruct> = items.iter().map(f).collect();
380 upsert_field(root, label, GffValue::List(structs));
381 }
382
383 fn write_objects<S, T>(
385 root: &mut GffStruct,
386 label: &str,
387 objects: &GitObjects<S, T>,
388 write_static: fn(&S) -> GffStruct,
389 write_saved: fn(&T, usize) -> GffStruct,
390 ) {
391 let structs: Vec<GffStruct> = match objects {
392 GitObjects::Static(entries) => entries.iter().map(write_static).collect(),
393 GitObjects::Saved(entries) => entries
394 .iter()
395 .enumerate()
396 .map(|(index, entry)| write_saved(entry, index))
397 .collect(),
398 };
399 upsert_field(root, label, GffValue::List(structs));
400 }
401
402 write_objects(
403 &mut root,
404 "Creature List",
405 &self.creatures,
406 GitCreature::to_gff_struct,
407 SavedCreature::to_struct,
408 );
409 write_objects(
410 &mut root,
411 "List",
412 &self.items,
413 GitItem::to_gff_struct,
414 SavedItem::to_struct,
415 );
416 write_objects(
417 &mut root,
418 "Door List",
419 &self.doors,
420 GitDoor::to_gff_struct,
421 SavedDoor::to_struct,
422 );
423 write_objects(
424 &mut root,
425 "Placeable List",
426 &self.placeables,
427 GitPlaceable::to_gff_struct,
428 SavedPlaceable::to_struct,
429 );
430 write_list(
431 &mut root,
432 "WaypointList",
433 &self.waypoints,
434 GitWaypoint::to_gff_struct,
435 );
436 write_objects(
437 &mut root,
438 "SoundList",
439 &self.sounds,
440 GitSound::to_gff_struct,
441 SavedSound::to_struct,
442 );
443 write_objects(
444 &mut root,
445 "TriggerList",
446 &self.triggers,
447 GitTrigger::to_gff_struct,
448 SavedTrigger::to_struct,
449 );
450 write_objects(
451 &mut root,
452 "StoreList",
453 &self.stores,
454 GitStore::to_gff_struct,
455 SavedStore::to_struct,
456 );
457 write_list(
458 &mut root,
459 "Encounter List",
460 &self.encounters,
461 GitEncounter::to_gff_struct,
462 );
463 write_list(
464 &mut root,
465 "AreaEffectList",
466 &self.area_effects,
467 GitAreaEffect::to_gff_struct,
468 );
469
470 if let Some(ref ap) = self.area_properties {
471 upsert_field(
472 &mut root,
473 "AreaProperties",
474 GffValue::Struct(Box::new(ap.to_gff_struct())),
475 );
476 }
477
478 write_list(
479 &mut root,
480 "CameraList",
481 &self.cameras,
482 GitCamera::to_gff_struct,
483 );
484
485 Gff::new(*b"GIT ", root)
486 }
487}
488
489#[derive(Debug, Error)]
491pub enum GitError {
492 #[error("unsupported GIT file type: {0:?}")]
494 UnsupportedFileType([u8; 4]),
495 #[error(transparent)]
497 Gff(#[from] GffBinaryError),
498}
499
500#[cfg_attr(
502 feature = "tracing",
503 tracing::instrument(level = "debug", skip(reader))
504)]
505pub fn read_git<R: Read>(reader: &mut R) -> Result<Git, GitError> {
506 let gff = read_gff(reader)?;
507 Git::from_gff(&gff)
508}
509
510#[cfg_attr(
512 feature = "tracing",
513 tracing::instrument(level = "debug", skip(bytes), fields(bytes_len = bytes.len()))
514)]
515pub fn read_git_from_bytes(bytes: &[u8]) -> Result<Git, GitError> {
516 let gff = read_gff_from_bytes(bytes)?;
517 Git::from_gff(&gff)
518}
519
520#[cfg_attr(
522 feature = "tracing",
523 tracing::instrument(level = "debug", skip(writer, git))
524)]
525pub fn write_git<W: Write>(writer: &mut W, git: &Git) -> Result<(), GitError> {
526 let gff = git.to_gff();
527 write_gff(writer, &gff)?;
528 Ok(())
529}
530
531#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(git)))]
533pub fn write_git_to_vec(git: &Git) -> Result<Vec<u8>, GitError> {
534 let mut cursor = Cursor::new(Vec::new());
535 write_git(&mut cursor, git)?;
536 Ok(cursor.into_inner())
537}
538
539impl GffSchema for Git {
544 fn schema() -> &'static [FieldSchema] {
545 static SCHEMA: &[FieldSchema] = &[
546 FieldSchema {
548 label: "CurrentWeather",
549 expected_type: GffType::UInt8,
550 life: FieldLife::Live,
551 required: false,
552 absent: AbsentDefault::Unverified,
553 children: None,
554 constraint: None,
555 },
556 FieldSchema {
557 label: "WeatherStarted",
558 expected_type: GffType::UInt8,
559 life: FieldLife::Live,
560 required: false,
561 absent: AbsentDefault::Unverified,
562 children: None,
563 constraint: None,
564 },
565 FieldSchema {
566 label: "UseTemplates",
567 expected_type: GffType::UInt8,
568 life: FieldLife::Live,
569 required: false,
570 absent: AbsentDefault::Unverified,
571 children: None,
572 constraint: None,
573 },
574 FieldSchema {
576 label: "Creature List",
577 expected_type: GffType::List,
578 life: FieldLife::Live,
579 required: false,
580 absent: AbsentDefault::Unverified,
581 children: Some(creature::CREATURE_LIST_CHILDREN),
582 constraint: None,
583 },
584 FieldSchema {
585 label: "List",
586 expected_type: GffType::List,
587 life: FieldLife::Live,
588 required: false,
589 absent: AbsentDefault::Unverified,
590 children: Some(item::ITEM_LIST_CHILDREN),
591 constraint: None,
592 },
593 FieldSchema {
594 label: "Door List",
595 expected_type: GffType::List,
596 life: FieldLife::Live,
597 required: false,
598 absent: AbsentDefault::Unverified,
599 children: Some(door::DOOR_LIST_CHILDREN),
600 constraint: None,
601 },
602 FieldSchema {
603 label: "Placeable List",
604 expected_type: GffType::List,
605 life: FieldLife::Live,
606 required: false,
607 absent: AbsentDefault::Unverified,
608 children: Some(placeable::PLACEABLE_LIST_CHILDREN),
609 constraint: None,
610 },
611 FieldSchema {
612 label: "WaypointList",
613 expected_type: GffType::List,
614 life: FieldLife::Live,
615 required: false,
616 absent: AbsentDefault::Unverified,
617 children: Some(waypoint::WAYPOINT_LIST_CHILDREN),
618 constraint: None,
619 },
620 FieldSchema {
621 label: "SoundList",
622 expected_type: GffType::List,
623 life: FieldLife::Live,
624 required: false,
625 absent: AbsentDefault::Unverified,
626 children: Some(sound::SOUND_LIST_CHILDREN),
627 constraint: None,
628 },
629 FieldSchema {
630 label: "TriggerList",
631 expected_type: GffType::List,
632 life: FieldLife::Live,
633 required: false,
634 absent: AbsentDefault::Unverified,
635 children: Some(trigger::TRIGGER_LIST_CHILDREN),
636 constraint: None,
637 },
638 FieldSchema {
639 label: "StoreList",
640 expected_type: GffType::List,
641 life: FieldLife::Live,
642 required: false,
643 absent: AbsentDefault::Unverified,
644 children: Some(store::STORE_LIST_CHILDREN),
645 constraint: None,
646 },
647 FieldSchema {
648 label: "Encounter List",
649 expected_type: GffType::List,
650 life: FieldLife::Live,
651 required: false,
652 absent: AbsentDefault::Unverified,
653 children: Some(encounter::ENCOUNTER_LIST_CHILDREN),
654 constraint: None,
655 },
656 FieldSchema {
657 label: "AreaEffectList",
658 expected_type: GffType::List,
659 life: FieldLife::Live,
660 required: false,
661 absent: AbsentDefault::Unverified,
662 children: Some(area_effect::AREA_EFFECT_LIST_CHILDREN),
663 constraint: None,
664 },
665 FieldSchema {
667 label: "AreaProperties",
668 expected_type: GffType::Struct,
669 life: FieldLife::Live,
670 required: false,
671 absent: AbsentDefault::Unverified,
672 children: Some(area_properties::AREA_PROPERTIES_CHILDREN),
673 constraint: None,
674 },
675 FieldSchema {
676 label: "AreaMap",
677 expected_type: GffType::Struct,
678 life: FieldLife::Live,
679 required: false,
680 absent: AbsentDefault::Unverified,
681 children: Some(area_properties::AREA_MAP_CHILDREN),
682 constraint: None,
683 },
684 FieldSchema {
686 label: "CameraList",
687 expected_type: GffType::List,
688 life: FieldLife::Live,
689 required: false,
690 absent: AbsentDefault::Unverified,
691 children: Some(camera::CAMERA_LIST_CHILDREN),
692 constraint: None,
693 },
694 FieldSchema {
696 label: "VarTable",
697 expected_type: GffType::List,
698 life: FieldLife::Live,
699 required: false,
700 absent: AbsentDefault::Unverified,
701 children: None,
702 constraint: None,
703 },
704 ];
705 SCHEMA
706 }
707}
708
709#[cfg(test)]
710mod tests {
711 use super::*;
712
713 use rakata_core::{ResRef, StrRef};
714 use rakata_formats::GffLocalizedString;
715
716 fn make_test_git_gff() -> Gff {
718 let mut root = GffStruct::new(-1);
719 root.push_field("CurrentWeather", GffValue::UInt8(0));
720 root.push_field("WeatherStarted", GffValue::UInt8(0));
721 root.push_field("UseTemplates", GffValue::UInt8(1));
722
723 let mut creature = GffStruct::new(4);
725 creature.push_field("TemplateResRef", GffValue::resref_lit("n_darthbandon"));
726 creature.push_field("XPosition", GffValue::Single(10.0));
727 creature.push_field("YPosition", GffValue::Single(20.0));
728 creature.push_field("ZPosition", GffValue::Single(0.5));
729 creature.push_field("XOrientation", GffValue::Single(0.0));
730 creature.push_field("YOrientation", GffValue::Single(1.0));
731 creature.push_field("ZOrientation", GffValue::Single(0.0));
732 creature.push_field("ObjectId", GffValue::UInt32(0));
733 root.push_field("Creature List", GffValue::List(vec![creature]));
734
735 let mut placeable = GffStruct::new(9);
737 placeable.push_field("TemplateResRef", GffValue::resref_lit("plc_footlkr"));
738 placeable.push_field("Bearing", GffValue::Single(1.57));
739 placeable.push_field("X", GffValue::Single(15.0));
740 placeable.push_field("Y", GffValue::Single(25.0));
741 placeable.push_field("Z", GffValue::Single(0.0));
742 placeable.push_field("ObjectId", GffValue::UInt32(0));
743 root.push_field("Placeable List", GffValue::List(vec![placeable]));
744
745 let mut trigger = GffStruct::new(1);
747 trigger.push_field("TemplateResRef", GffValue::resref_lit("newtransition9"));
748 trigger.push_field("LinkedToModule", GffValue::resref_lit("tar_m02aa"));
749 trigger.push_field(
750 "TransitionDestin",
751 GffValue::LocalizedString(GffLocalizedString::new(StrRef::from_raw(12345))),
752 );
753 trigger.push_field("LinkedTo", GffValue::String("wp_target".into()));
754 trigger.push_field("LinkedToFlags", GffValue::UInt8(2));
755 trigger.push_field("XPosition", GffValue::Single(5.0));
756 trigger.push_field("YPosition", GffValue::Single(5.0));
757 trigger.push_field("ZPosition", GffValue::Single(0.0));
758 let mut pt0 = GffStruct::new(0);
759 pt0.push_field("PointX", GffValue::Single(0.0));
760 pt0.push_field("PointY", GffValue::Single(0.0));
761 pt0.push_field("PointZ", GffValue::Single(0.0));
762 let mut pt1 = GffStruct::new(0);
763 pt1.push_field("PointX", GffValue::Single(5.0));
764 pt1.push_field("PointY", GffValue::Single(0.0));
765 pt1.push_field("PointZ", GffValue::Single(0.0));
766 let mut pt2 = GffStruct::new(0);
767 pt2.push_field("PointX", GffValue::Single(5.0));
768 pt2.push_field("PointY", GffValue::Single(5.0));
769 pt2.push_field("PointZ", GffValue::Single(0.0));
770 trigger.push_field("Geometry", GffValue::List(vec![pt0, pt1, pt2]));
771 trigger.push_field("ObjectId", GffValue::UInt32(0));
772 root.push_field("TriggerList", GffValue::List(vec![trigger]));
773
774 let mut props = GffStruct::new(0);
776 props.push_field("Unescapable", GffValue::UInt8(0));
777 props.push_field("StealthXPMax", GffValue::UInt32(100));
778 props.push_field("StealthXPCurrent", GffValue::UInt32(0));
779 props.push_field("StealthXPLoss", GffValue::UInt32(10));
780 props.push_field("StealthXPEnabled", GffValue::UInt8(1));
781 props.push_field("TransPending", GffValue::UInt8(0));
782 props.push_field("TransPendNextID", GffValue::UInt8(0));
783 props.push_field("TransPendCurrID", GffValue::UInt8(0));
784 props.push_field("SunFogColor", GffValue::UInt32(0x00ABCDEF));
785 props.push_field("MusicDelay", GffValue::Int32(6000));
786 props.push_field("MusicDay", GffValue::Int32(48));
787 props.push_field("MusicNight", GffValue::Int32(49));
788 props.push_field("MusicBattle", GffValue::Int32(25));
789 props.push_field("AmbientSndDay", GffValue::Int32(12));
790 props.push_field("AmbientSndNight", GffValue::Int32(13));
791 props.push_field("AmbientSndDayVol", GffValue::Int32(50));
792 props.push_field("AmbientSndNitVol", GffValue::Int32(40));
793 root.push_field("AreaProperties", GffValue::Struct(Box::new(props)));
794
795 root.push_field("List", GffValue::List(vec![]));
797 root.push_field("Door List", GffValue::List(vec![]));
798 root.push_field("WaypointList", GffValue::List(vec![]));
799 root.push_field("SoundList", GffValue::List(vec![]));
800 root.push_field("StoreList", GffValue::List(vec![]));
801 root.push_field("Encounter List", GffValue::List(vec![]));
802 root.push_field("AreaEffectList", GffValue::List(vec![]));
803 root.push_field("CameraList", GffValue::List(vec![]));
804
805 Gff::new(*b"GIT ", root)
806 }
807
808 #[test]
809 fn reads_root_scalars() {
810 let gff = make_test_git_gff();
811 let git = Git::from_gff(&gff).expect("valid GIT GFF must parse");
812
813 assert_eq!(git.current_weather, 0);
814 assert!(!git.weather_started);
815 assert!(git.creatures.uses_templates());
816 }
817
818 #[test]
819 fn reads_creature_list() {
820 let gff = make_test_git_gff();
821 let git = Git::from_gff(&gff).expect("valid GIT GFF must parse");
822
823 assert_eq!(git.creatures.len(), 1);
824 let statics = git
825 .creatures
826 .as_static()
827 .expect("the fixture sets UseTemplates = 1");
828 let c = &statics[0];
829 assert_eq!(c.template_resref, "n_darthbandon");
830 assert!((c.x_position - 10.0).abs() < f32::EPSILON);
831 assert!((c.y_position - 20.0).abs() < f32::EPSILON);
832 assert!((c.y_orientation - 1.0).abs() < f32::EPSILON);
833 }
834
835 fn sample_area_effect() -> GitAreaEffect {
836 GitAreaEffect {
837 tag: "aoe_stasis".to_string(),
838 object_id: ObjectId::INVALID,
839 area_effect_id: 12,
840 spell_id: 44,
841 spell_save_dc: 18,
842 spell_level: 3,
843 shape: GitAreaEffectShape::Circle { radius: 5.0 },
844 duration: 30_000,
845 duration_type: 2,
846 position_x: 1.5,
847 orientation_y: 1.0,
848 ..GitAreaEffect::default()
849 }
850 }
851
852 #[test]
853 fn area_effect_round_trips_through_a_list_element() {
854 let effect = sample_area_effect();
855
856 let parsed = GitAreaEffect::from_gff_struct(&effect.to_gff_struct());
857
858 assert_eq!(parsed, effect);
859 }
860
861 #[test]
862 fn an_area_effect_writes_only_the_dimensions_its_shape_uses() {
863 let circle = sample_area_effect().to_gff_struct();
867 assert!(circle.field("Radius").is_some());
868 assert!(circle.field("Length").is_none());
869 assert!(circle.field("Width").is_none());
870
871 let rectangle = GitAreaEffect {
872 shape: GitAreaEffectShape::Rectangle {
873 length: 4.0,
874 width: 2.0,
875 },
876 ..sample_area_effect()
877 }
878 .to_gff_struct();
879 assert!(rectangle.field("Radius").is_none());
880 assert!(rectangle.field("Length").is_some());
881 assert!(rectangle.field("Width").is_some());
882
883 let shapeless = GitAreaEffect {
884 shape: GitAreaEffectShape::None(7),
885 ..sample_area_effect()
886 }
887 .to_gff_struct();
888 assert!(shapeless.field("Radius").is_none());
889 assert!(shapeless.field("Length").is_none());
890 assert_eq!(shapeless.field("Shape"), Some(&GffValue::UInt8(7)));
891 }
892
893 #[test]
894 fn a_door_placement_keeps_its_blueprint_and_transition() {
895 let mut door = GffStruct::new(8);
899 door.push_field(
900 "TemplateResRef",
901 GffValue::ResRef(ResRef::new("m13aa_door01").expect("valid test resref")),
902 );
903 door.push_field("Tag", GffValue::String("EnclaveDoor".into()));
904 door.push_field("Bearing", GffValue::Single(1.57));
905 door.push_field(
906 "LinkedToModule",
907 GffValue::ResRef(ResRef::new("danm14aa").expect("valid test resref")),
908 );
909 door.push_field("LinkedTo", GffValue::String("from_courtyard".into()));
910 door.push_field("LinkedToFlags", GffValue::UInt8(2));
911 door.push_field(
912 "TransitionDestin",
913 GffValue::LocalizedString(GffLocalizedString::new(
914 StrRef::from_index(31982).expect("valid test strref"),
915 )),
916 );
917
918 let parsed = GitDoor::from_gff_struct(&door);
919 assert_eq!(parsed.template_resref, "m13aa_door01");
920 assert_eq!(parsed.linked_to_module, "danm14aa");
921 assert_eq!(parsed.linked_to, "from_courtyard");
922 assert_eq!(parsed.linked_to_flags, 2);
923 assert_eq!(
924 parsed.transition_destination.string_ref,
925 StrRef::from_index(31982).expect("valid test strref")
926 );
927
928 let written = parsed.to_gff_struct();
929 for label in [
930 "TemplateResRef",
931 "LinkedToModule",
932 "LinkedTo",
933 "LinkedToFlags",
934 "TransitionDestin",
935 ] {
936 assert_eq!(
937 written.field(label),
938 door.field(label),
939 "{label} must survive the round-trip unchanged"
940 );
941 }
942
943 assert!(
948 written.field("Tag").is_none(),
949 "the placement's Tag is dead and must not be written back"
950 );
951 }
952
953 #[test]
954 fn an_absent_object_id_is_the_invalid_placeholder_on_every_type() {
955 let empty = GffStruct::new(0);
961
962 assert_eq!(
965 GitItem::from_gff_struct(&empty).object_id,
966 ObjectId::INVALID
967 );
968 assert_eq!(
969 GitDoor::from_gff_struct(&empty).object_id,
970 ObjectId::INVALID
971 );
972 assert_eq!(
973 GitPlaceable::from_gff_struct(&empty).object_id,
974 ObjectId::INVALID
975 );
976 assert_eq!(
977 GitWaypoint::from_gff_struct(&empty).object_id,
978 ObjectId::INVALID
979 );
980 assert_eq!(
981 GitSound::from_gff_struct(&empty).object_id,
982 ObjectId::INVALID
983 );
984 assert_eq!(
985 GitTrigger::from_gff_struct(&empty).object_id,
986 ObjectId::INVALID
987 );
988 assert_eq!(
989 GitStore::from_gff_struct(&empty).object_id,
990 ObjectId::INVALID
991 );
992 assert_eq!(
993 GitEncounter::from_gff_struct(&empty).object_id,
994 ObjectId::INVALID
995 );
996 assert_eq!(
997 GitAreaEffect::from_gff_struct(&GffStruct::new(13)).object_id,
998 ObjectId::INVALID
999 );
1000
1001 assert_eq!(
1006 crate::git::creature::SavedCreature::from_struct(&empty).object_id,
1007 ObjectId::INVALID
1008 );
1009 assert_eq!(
1010 crate::git::door::SavedDoor::from_struct(&empty).object_id,
1011 ObjectId::INVALID
1012 );
1013 assert_eq!(
1014 crate::git::item::SavedItem::from_struct(&empty).object_id,
1015 ObjectId::INVALID
1016 );
1017 assert_eq!(
1018 crate::git::placeable::SavedPlaceable::from_struct(&empty).object_id,
1019 ObjectId::INVALID
1020 );
1021 assert_eq!(
1022 crate::git::sound::SavedSound::from_struct(&empty).object_id,
1023 ObjectId::INVALID
1024 );
1025 assert_eq!(
1026 crate::git::store::SavedStore::from_struct(&empty).object_id,
1027 ObjectId::INVALID
1028 );
1029 assert_eq!(
1030 crate::git::trigger::SavedTrigger::from_struct(&empty).object_id,
1031 ObjectId::INVALID
1032 );
1033 }
1034
1035 #[test]
1036 fn reads_placeable_list() {
1037 let gff = make_test_git_gff();
1038 let git = Git::from_gff(&gff).expect("valid GIT GFF must parse");
1039
1040 assert_eq!(git.placeables.len(), 1);
1041 let statics = git
1042 .placeables
1043 .as_static()
1044 .expect("the fixture sets UseTemplates = 1");
1045 let p = &statics[0];
1046 assert_eq!(p.template_resref, "plc_footlkr");
1047 assert!((p.bearing - 1.57).abs() < 0.01);
1048 assert!((p.x - 15.0).abs() < f32::EPSILON);
1049 }
1050
1051 #[test]
1052 fn reads_trigger_with_geometry() {
1053 let gff = make_test_git_gff();
1054 let git = Git::from_gff(&gff).expect("valid GIT GFF must parse");
1055
1056 assert_eq!(git.triggers.len(), 1);
1057 let statics = git
1058 .triggers
1059 .as_static()
1060 .expect("the fixture sets UseTemplates = 1");
1061 let t = &statics[0];
1062 assert_eq!(t.template_resref, "newtransition9");
1063 assert_eq!(t.linked_to_module, "tar_m02aa");
1064 assert_eq!(t.linked_to, "wp_target");
1065 assert_eq!(t.linked_to_flags, 2);
1066 assert_eq!(t.geometry.len(), 3);
1067 assert!((t.geometry[1].point_x - 5.0).abs() < f32::EPSILON);
1068 }
1069
1070 #[test]
1071 fn reads_area_properties() {
1072 let gff = make_test_git_gff();
1073 let git = Git::from_gff(&gff).expect("valid GIT GFF must parse");
1074
1075 let ap = git
1076 .area_properties
1077 .as_ref()
1078 .expect("test GFF includes AreaProperties");
1079 assert!(!ap.unescapable);
1080 assert_eq!(ap.stealth_xp_max, 100);
1081 assert!(ap.stealth_xp_enabled);
1082 assert_eq!(ap.music_day, 48);
1083 assert_eq!(ap.music_night, 49);
1084 assert_eq!(ap.music_battle, 25);
1085 assert_eq!(ap.ambient_snd_day_vol, 50);
1086 assert_eq!(ap.ambient_snd_nit_vol, 40);
1087 }
1088
1089 #[test]
1090 fn all_fields_survive_synthetic_roundtrip() {
1091 let gff = make_test_git_gff();
1092 let git = Git::from_gff(&gff).expect("valid GIT GFF must parse");
1093 let bytes = write_git_to_vec(&git).expect("write succeeds");
1094 let reparsed = read_git_from_bytes(&bytes).expect("reparse succeeds");
1095
1096 assert_eq!(reparsed, git);
1097 }
1098
1099 #[test]
1100 fn typed_edits_roundtrip_through_gff_writer() {
1101 let gff = make_test_git_gff();
1102 let mut git = Git::from_gff(&gff).expect("valid GIT GFF must parse");
1103 let statics = git
1104 .creatures
1105 .as_static_mut()
1106 .expect("the fixture sets UseTemplates = 1");
1107 statics[0].template_resref = ResRef::new("n_sithsoldier").expect("valid test resref");
1108 statics[0].x_position = 100.0;
1109 git.area_properties
1110 .as_mut()
1111 .expect("test GFF includes AreaProperties")
1112 .music_day = 99;
1113
1114 let bytes = write_git_to_vec(&git).expect("write succeeds");
1115 let reparsed = read_git_from_bytes(&bytes).expect("reparse succeeds");
1116
1117 let reparsed_statics = reparsed
1118 .creatures
1119 .as_static()
1120 .expect("round-trip keeps UseTemplates = 1");
1121 assert_eq!(reparsed_statics[0].template_resref, "n_sithsoldier");
1122 assert!((reparsed_statics[0].x_position - 100.0).abs() < f32::EPSILON);
1123 assert_eq!(
1124 reparsed
1125 .area_properties
1126 .as_ref()
1127 .expect("roundtripped GIT must have AreaProperties")
1128 .music_day,
1129 99
1130 );
1131 }
1132
1133 #[test]
1134 fn read_git_from_reader_matches_bytes_path() {
1135 let gff = make_test_git_gff();
1136 let bytes = {
1137 let mut c = Cursor::new(Vec::new());
1138 write_gff(&mut c, &gff).expect("GFF write to cursor always succeeds");
1139 c.into_inner()
1140 };
1141
1142 let mut cursor = Cursor::new(&bytes);
1143 let via_reader = read_git(&mut cursor).expect("reader parse succeeds");
1144 let via_bytes = read_git_from_bytes(&bytes).expect("bytes parse succeeds");
1145
1146 assert_eq!(via_reader, via_bytes);
1147 }
1148
1149 #[test]
1150 fn rejects_non_git_file_type() {
1151 let mut gff = make_test_git_gff();
1152 gff.file_type = *b"UTW ";
1153
1154 let err = Git::from_gff(&gff).expect_err("UTW must be rejected as GIT input");
1155 assert!(matches!(
1156 err,
1157 GitError::UnsupportedFileType(ft) if ft == *b"UTW "
1158 ));
1159 }
1160
1161 #[test]
1162 fn write_git_matches_direct_gff_writer() {
1163 let gff = make_test_git_gff();
1164 let git = Git::from_gff(&gff).expect("valid GIT GFF must parse");
1165
1166 let via_typed = write_git_to_vec(&git).expect("typed write succeeds");
1167
1168 let mut direct = Cursor::new(Vec::new());
1169 write_gff(&mut direct, &git.to_gff()).expect("direct write succeeds");
1170
1171 assert_eq!(via_typed, direct.into_inner());
1172 }
1173
1174 const TEST_GIT: &[u8] = include_bytes!(concat!(
1177 env!("CARGO_MANIFEST_DIR"),
1178 "/../../fixtures/test.git"
1179 ));
1180 const K1_GIT: &[u8] = include_bytes!(concat!(
1181 env!("CARGO_MANIFEST_DIR"),
1182 "/../../fixtures/k1_same_git_test.git"
1183 ));
1184
1185 #[test]
1186 fn parses_test_git_fixture() {
1187 let git = read_git_from_bytes(TEST_GIT).expect("test.git must parse");
1188 let total = git.creatures.len()
1190 + git.items.len()
1191 + git.doors.len()
1192 + git.placeables.len()
1193 + git.waypoints.len()
1194 + git.sounds.len()
1195 + git.triggers.len()
1196 + git.stores.len()
1197 + git.encounters.len()
1198 + git.area_effects.len();
1199 assert!(total > 0, "fixture should have some instance data");
1200 }
1201
1202 #[test]
1203 fn parses_k1_git_fixture() {
1204 let git = read_git_from_bytes(K1_GIT).expect("k1 GIT must parse");
1205 let total = git.creatures.len()
1206 + git.items.len()
1207 + git.doors.len()
1208 + git.placeables.len()
1209 + git.waypoints.len()
1210 + git.sounds.len()
1211 + git.triggers.len()
1212 + git.stores.len()
1213 + git.encounters.len()
1214 + git.area_effects.len();
1215 assert!(total > 0, "K1 fixture should have some instance data");
1216 }
1217
1218 #[test]
1219 fn all_fields_survive_typed_roundtrip() {
1220 let git = read_git_from_bytes(TEST_GIT).expect("test.git must parse");
1221 let bytes = write_git_to_vec(&git).expect("write succeeds");
1222 let reparsed = read_git_from_bytes(&bytes).expect("reparse succeeds");
1223
1224 assert_eq!(reparsed, git);
1225 }
1226
1227 #[test]
1228 fn all_fields_survive_typed_roundtrip_k1() {
1229 let git = read_git_from_bytes(K1_GIT).expect("k1 GIT must parse");
1230 let bytes = write_git_to_vec(&git).expect("write succeeds");
1231 let reparsed = read_git_from_bytes(&bytes).expect("reparse succeeds");
1232
1233 assert_eq!(reparsed, git);
1234 }
1235
1236 #[test]
1239 fn schema_field_count() {
1240 assert_eq!(Git::schema().len(), 17);
1241 }
1242
1243 #[test]
1244 fn schema_no_duplicate_labels() {
1245 let schema = Git::schema();
1246 let mut labels: Vec<&str> = schema.iter().map(|f| f.label).collect();
1247 labels.sort();
1248 let before = labels.len();
1249 labels.dedup();
1250 assert_eq!(before, labels.len(), "duplicate labels in GIT schema");
1251 }
1252
1253 #[test]
1254 fn schema_sub_schema_field_counts() {
1255 let schema = Git::schema();
1256
1257 let creature = schema
1258 .iter()
1259 .find(|f| f.label == "Creature List")
1260 .expect("schema must contain Creature List");
1261 assert_eq!(
1262 creature.children.expect("Creature List has children").len(),
1263 8
1264 );
1265
1266 let item = schema
1267 .iter()
1268 .find(|f| f.label == "List")
1269 .expect("schema must contain List");
1270 assert_eq!(item.children.expect("List has children").len(), 8);
1271
1272 let door = schema
1273 .iter()
1274 .find(|f| f.label == "Door List")
1275 .expect("schema must contain Door List");
1276 assert_eq!(door.children.expect("Door List has children").len(), 12);
1277
1278 let placeable = schema
1279 .iter()
1280 .find(|f| f.label == "Placeable List")
1281 .expect("schema must contain Placeable List");
1282 assert_eq!(
1283 placeable
1284 .children
1285 .expect("Placeable List has children")
1286 .len(),
1287 7
1288 );
1289
1290 let waypoint = schema
1291 .iter()
1292 .find(|f| f.label == "WaypointList")
1293 .expect("schema must contain WaypointList");
1294 assert_eq!(
1295 waypoint.children.expect("WaypointList has children").len(),
1296 17
1297 );
1298
1299 let sound = schema
1300 .iter()
1301 .find(|f| f.label == "SoundList")
1302 .expect("schema must contain SoundList");
1303 assert_eq!(sound.children.expect("SoundList has children").len(), 6);
1304
1305 let trigger = schema
1306 .iter()
1307 .find(|f| f.label == "TriggerList")
1308 .expect("schema must contain TriggerList");
1309 assert_eq!(
1310 trigger.children.expect("TriggerList has children").len(),
1311 14
1312 );
1313
1314 let store = schema
1315 .iter()
1316 .find(|f| f.label == "StoreList")
1317 .expect("schema must contain StoreList");
1318 assert_eq!(store.children.expect("StoreList has children").len(), 8);
1319
1320 let encounter = schema
1321 .iter()
1322 .find(|f| f.label == "Encounter List")
1323 .expect("schema must contain Encounter List");
1324 assert_eq!(
1325 encounter
1326 .children
1327 .expect("Encounter List has children")
1328 .len(),
1329 7
1330 );
1331
1332 let area_effect = schema
1333 .iter()
1334 .find(|f| f.label == "AreaEffectList")
1335 .expect("schema must contain AreaEffectList");
1336 assert_eq!(
1337 area_effect
1338 .children
1339 .expect("AreaEffectList has children")
1340 .len(),
1341 29
1342 );
1343
1344 let area_props = schema
1345 .iter()
1346 .find(|f| f.label == "AreaProperties")
1347 .expect("schema must contain AreaProperties");
1348 assert_eq!(
1349 area_props
1350 .children
1351 .expect("AreaProperties has children")
1352 .len(),
1353 18
1354 );
1355
1356 let area_map = schema
1357 .iter()
1358 .find(|f| f.label == "AreaMap")
1359 .expect("schema must contain AreaMap");
1360 assert_eq!(area_map.children.expect("AreaMap has children").len(), 4);
1361
1362 let camera = schema
1363 .iter()
1364 .find(|f| f.label == "CameraList")
1365 .expect("schema must contain CameraList");
1366 assert_eq!(camera.children.expect("CameraList has children").len(), 7);
1367 }
1368
1369 #[test]
1370 fn schema_trigger_geometry_has_children() {
1371 let schema = Git::schema();
1372 let trigger = schema
1373 .iter()
1374 .find(|f| f.label == "TriggerList")
1375 .expect("schema must contain TriggerList");
1376 let trigger_children = trigger.children.expect("TriggerList has children");
1377 let geometry = trigger_children
1378 .iter()
1379 .find(|f| f.label == "Geometry")
1380 .expect("TriggerList must contain Geometry");
1381 assert_eq!(geometry.children.expect("Geometry has children").len(), 3);
1382 }
1383
1384 #[test]
1385 fn schema_encounter_nested_lists_have_children() {
1386 let schema = Git::schema();
1387 let encounter = schema
1388 .iter()
1389 .find(|f| f.label == "Encounter List")
1390 .expect("schema must contain Encounter List");
1391 let encounter_children = encounter.children.expect("Encounter List has children");
1392
1393 let geometry = encounter_children
1394 .iter()
1395 .find(|f| f.label == "Geometry")
1396 .expect("Encounter List must contain Geometry");
1397 assert_eq!(geometry.children.expect("Geometry has children").len(), 3);
1398
1399 let spawn_points = encounter_children
1400 .iter()
1401 .find(|f| f.label == "SpawnPointList")
1402 .expect("Encounter List must contain SpawnPointList");
1403 assert_eq!(
1404 spawn_points
1405 .children
1406 .expect("SpawnPointList has children")
1407 .len(),
1408 4
1409 );
1410 }
1411
1412 #[test]
1413 fn schema_var_table_has_no_children() {
1414 let schema = Git::schema();
1415 let var_table = schema
1416 .iter()
1417 .find(|f| f.label == "VarTable")
1418 .expect("schema must contain VarTable");
1419 assert!(var_table.children.is_none());
1420 }
1421}