1use std::io::{Cursor, Read, Write};
39
40use crate::gff_helpers::{
41 get_bool, get_f32, get_i32, get_locstring, get_resref, get_string, get_u32, get_u8,
42 upsert_field,
43};
44use rakata_core::{ResRef, StrRef};
45use rakata_formats::{
46 gff_schema::{AbsentDefault, FieldLife, FieldSchema, GffSchema, GffType},
47 read_gff, read_gff_from_bytes, write_gff, Gff, GffBinaryError, GffLocalizedString, GffStruct,
48 GffValue,
49};
50use thiserror::Error;
51
52#[derive(Debug, Clone, PartialEq)]
58pub struct UteGeometryVertex {
59 pub x: f32,
61 pub y: f32,
63 pub z: f32,
65}
66
67impl UteGeometryVertex {
68 fn from_gff_struct(s: &GffStruct) -> Self {
69 Self {
70 x: get_f32(s, "X").unwrap_or(0.0),
71 y: get_f32(s, "Y").unwrap_or(0.0),
72 z: get_f32(s, "Z").unwrap_or(0.0),
73 }
74 }
75
76 fn to_gff_struct(&self) -> GffStruct {
77 let mut s = GffStruct::new(0);
78 s.push_field("X", GffValue::Single(self.x));
79 s.push_field("Y", GffValue::Single(self.y));
80 s.push_field("Z", GffValue::Single(self.z));
81 s
82 }
83}
84
85#[derive(Debug, Clone, PartialEq)]
87pub struct UteSpawnPoint {
88 pub x: f32,
90 pub y: f32,
92 pub z: f32,
94 pub orientation: f32,
96}
97
98impl UteSpawnPoint {
99 fn from_gff_struct(s: &GffStruct) -> Self {
100 Self {
101 x: get_f32(s, "X").unwrap_or(0.0),
102 y: get_f32(s, "Y").unwrap_or(0.0),
103 z: get_f32(s, "Z").unwrap_or(0.0),
104 orientation: get_f32(s, "Orientation").unwrap_or(0.0),
105 }
106 }
107
108 fn to_gff_struct(&self) -> GffStruct {
109 let mut s = GffStruct::new(0);
110 s.push_field("X", GffValue::Single(self.x));
111 s.push_field("Y", GffValue::Single(self.y));
112 s.push_field("Z", GffValue::Single(self.z));
113 s.push_field("Orientation", GffValue::Single(self.orientation));
114 s
115 }
116}
117
118#[derive(Debug, Clone, PartialEq)]
120pub struct UteAreaEntry {
121 pub area_object: u32,
123}
124
125impl UteAreaEntry {
126 fn from_gff_struct(s: &GffStruct) -> Self {
127 Self {
128 area_object: get_u32(s, "AreaObject").unwrap_or(0),
129 }
130 }
131
132 fn to_gff_struct(&self) -> GffStruct {
133 let mut s = GffStruct::new(0);
134 s.push_field("AreaObject", GffValue::UInt32(self.area_object));
135 s
136 }
137}
138
139#[derive(Debug, Clone, PartialEq)]
141pub struct UteSpawnEntry {
142 pub spawn_resref: ResRef,
144 pub spawn_cr: f32,
146}
147
148impl UteSpawnEntry {
149 fn from_gff_struct(s: &GffStruct) -> Self {
150 Self {
151 spawn_resref: get_resref(s, "SpawnResRef").unwrap_or_default(),
152 spawn_cr: get_f32(s, "SpawnCR").unwrap_or(0.0),
153 }
154 }
155
156 fn to_gff_struct(&self) -> GffStruct {
157 let mut s = GffStruct::new(0);
158 s.push_field("SpawnResRef", GffValue::ResRef(self.spawn_resref));
159 s.push_field("SpawnCR", GffValue::Single(self.spawn_cr));
160 s
161 }
162}
163
164#[derive(Debug, Clone, PartialEq)]
170pub struct UteCreature {
171 pub resref: ResRef,
173 pub cr: f32,
175 pub single_spawn: bool,
177}
178
179impl Default for UteCreature {
180 fn default() -> Self {
181 Self {
182 resref: ResRef::blank(),
183 cr: 0.0,
184 single_spawn: false,
185 }
186 }
187}
188
189impl UteCreature {
190 fn from_gff_struct(s: &GffStruct) -> Self {
191 Self {
192 resref: get_resref(s, "ResRef").unwrap_or_default(),
193 cr: get_f32(s, "CR").unwrap_or(0.0),
194 single_spawn: get_bool(s, "SingleSpawn").unwrap_or(false),
195 }
196 }
197
198 fn to_gff_struct(&self) -> GffStruct {
199 let mut s = GffStruct::new(0);
200 s.push_field("ResRef", GffValue::ResRef(self.resref));
201 s.push_field("CR", GffValue::Single(self.cr));
202 s.push_field("SingleSpawn", GffValue::UInt8(u8::from(self.single_spawn)));
203 s
204 }
205}
206
207#[derive(Debug, Clone, PartialEq)]
213pub struct Ute {
214 pub template_resref: ResRef,
217 pub tag: String,
219 pub name: GffLocalizedString,
221 pub comment: String,
223 pub palette_id: u8,
225
226 pub active: bool,
229 pub reset: bool,
231 pub reset_time: i32,
233 pub respawns: i32,
235 pub spawn_option: i32,
237 pub max_creatures: i32,
239 pub rec_creatures: i32,
241 pub player_only: bool,
243 pub faction_id: u32,
245
246 pub difficulty_index: i32,
249 pub difficulty: i32,
251
252 pub x_position: f32,
255 pub y_position: f32,
257 pub z_position: f32,
259
260 pub on_entered: ResRef,
263 pub on_exit: ResRef,
265 pub on_heartbeat: ResRef,
267 pub on_exhausted: ResRef,
269 pub on_user_defined: ResRef,
271
272 pub number_spawned: i32,
275 pub heartbeat_day: u32,
277 pub heartbeat_time: u32,
279 pub last_spawn_day: u32,
281 pub last_spawn_time: u32,
283 pub last_entered: u32,
285 pub last_left: u32,
287 pub started: bool,
289 pub exhausted: bool,
291 pub current_spawns: i32,
293 pub custom_script_id: i32,
295
296 pub area_list_max_size: i32,
299 pub spawn_pool_active: f32,
301 pub area_points: f32,
303
304 pub creatures: Vec<UteCreature>,
307
308 pub geometry: Vec<UteGeometryVertex>,
311
312 pub spawn_points: Vec<UteSpawnPoint>,
315
316 pub area_list: Vec<UteAreaEntry>,
319
320 pub spawn_list: Vec<UteSpawnEntry>,
323}
324
325impl Default for Ute {
326 fn default() -> Self {
327 Self {
328 template_resref: ResRef::blank(),
329 tag: String::new(),
330 name: GffLocalizedString::new(StrRef::invalid()),
331 comment: String::new(),
332 palette_id: 0,
333 active: true,
334 reset: false,
335 reset_time: 0,
336 respawns: 0,
337 spawn_option: 0,
338 max_creatures: 0,
339 rec_creatures: 0,
340 player_only: false,
341 faction_id: 0,
342 difficulty_index: 0,
343 difficulty: 0,
344 x_position: 0.0,
345 y_position: 0.0,
346 z_position: 0.0,
347 on_entered: ResRef::blank(),
348 on_exit: ResRef::blank(),
349 on_heartbeat: ResRef::blank(),
350 on_exhausted: ResRef::blank(),
351 on_user_defined: ResRef::blank(),
352 number_spawned: 0,
353 heartbeat_day: 0,
354 heartbeat_time: 0,
355 last_spawn_day: 0,
356 last_spawn_time: 0,
357 last_entered: 0,
358 last_left: 0,
359 started: false,
360 exhausted: false,
361 current_spawns: 0,
362 custom_script_id: 0,
363 area_list_max_size: 0,
364 spawn_pool_active: 0.0,
365 area_points: 0.0,
366 creatures: Vec::new(),
367 geometry: Vec::new(),
368 spawn_points: Vec::new(),
369 area_list: Vec::new(),
370 spawn_list: Vec::new(),
371 }
372 }
373}
374
375impl Ute {
376 pub fn new() -> Self {
378 Self::default()
379 }
380
381 pub fn from_gff(gff: &Gff) -> Result<Self, UteError> {
383 if gff.file_type != *b"UTE " && gff.file_type != *b"GFF " {
384 return Err(UteError::UnsupportedFileType(gff.file_type));
385 }
386
387 let root = &gff.root;
388
389 if matches!(root.field("LocalizedName"), Some(value) if !matches!(value, GffValue::LocalizedString(_)))
390 {
391 return Err(UteError::TypeMismatch {
392 field: "LocalizedName",
393 expected: "LocalizedString",
394 });
395 }
396
397 let creatures = match root.field("CreatureList") {
398 Some(GffValue::List(elements)) => {
399 elements.iter().map(UteCreature::from_gff_struct).collect()
400 }
401 _ => Vec::new(),
402 };
403
404 let geometry = match root.field("Geometry") {
405 Some(GffValue::List(structs)) => structs
406 .iter()
407 .map(UteGeometryVertex::from_gff_struct)
408 .collect(),
409 _ => Vec::new(),
410 };
411
412 let spawn_points = match root.field("SpawnPointList") {
413 Some(GffValue::List(structs)) => {
414 structs.iter().map(UteSpawnPoint::from_gff_struct).collect()
415 }
416 _ => Vec::new(),
417 };
418
419 let area_list = match root.field("AreaList") {
420 Some(GffValue::List(structs)) => {
421 structs.iter().map(UteAreaEntry::from_gff_struct).collect()
422 }
423 _ => Vec::new(),
424 };
425
426 let spawn_list = match root.field("SpawnList") {
427 Some(GffValue::List(structs)) => {
428 structs.iter().map(UteSpawnEntry::from_gff_struct).collect()
429 }
430 _ => Vec::new(),
431 };
432
433 Ok(Self {
434 template_resref: get_resref(root, "TemplateResRef").unwrap_or_default(),
435 tag: get_string(root, "Tag").unwrap_or_default(),
436 name: get_locstring(root, "LocalizedName")
437 .cloned()
438 .unwrap_or_else(|| GffLocalizedString::new(StrRef::invalid())),
439 comment: get_string(root, "Comment").unwrap_or_default(),
440 palette_id: get_u8(root, "PaletteID").unwrap_or(0),
441 active: get_bool(root, "Active").unwrap_or(true),
442 reset: get_bool(root, "Reset").unwrap_or(false),
443 reset_time: get_i32(root, "ResetTime").unwrap_or(0),
444 respawns: get_i32(root, "Respawns").unwrap_or(0),
445 spawn_option: get_i32(root, "SpawnOption").unwrap_or(0),
446 max_creatures: get_i32(root, "MaxCreatures").unwrap_or(0),
447 rec_creatures: get_i32(root, "RecCreatures").unwrap_or(0),
448 player_only: get_bool(root, "PlayerOnly").unwrap_or(false),
449 faction_id: get_u32(root, "Faction").unwrap_or(0),
450 difficulty_index: get_i32(root, "DifficultyIndex").unwrap_or(0),
451 difficulty: get_i32(root, "Difficulty").unwrap_or(0),
452 x_position: get_f32(root, "XPosition").unwrap_or(0.0),
453 y_position: get_f32(root, "YPosition").unwrap_or(0.0),
454 z_position: get_f32(root, "ZPosition").unwrap_or(0.0),
455 on_entered: get_resref(root, "OnEntered").unwrap_or_default(),
456 on_exit: get_resref(root, "OnExit").unwrap_or_default(),
457 on_heartbeat: get_resref(root, "OnHeartbeat").unwrap_or_default(),
458 on_exhausted: get_resref(root, "OnExhausted").unwrap_or_default(),
459 on_user_defined: get_resref(root, "OnUserDefined").unwrap_or_default(),
460 number_spawned: get_i32(root, "NumberSpawned").unwrap_or(0),
461 heartbeat_day: get_u32(root, "HeartbeatDay").unwrap_or(0),
462 heartbeat_time: get_u32(root, "HeartbeatTime").unwrap_or(0),
463 last_spawn_day: get_u32(root, "LastSpawnDay").unwrap_or(0),
464 last_spawn_time: get_u32(root, "LastSpawnTime").unwrap_or(0),
465 last_entered: get_u32(root, "LastEntered").unwrap_or(0),
466 last_left: get_u32(root, "LastLeft").unwrap_or(0),
467 started: get_bool(root, "Started").unwrap_or(false),
468 exhausted: get_bool(root, "Exhausted").unwrap_or(false),
469 current_spawns: get_i32(root, "CurrentSpawns").unwrap_or(0),
470 custom_script_id: get_i32(root, "CustomScriptId").unwrap_or(0),
471 area_list_max_size: get_i32(root, "AreaListMaxSize").unwrap_or(0),
472 spawn_pool_active: get_f32(root, "SpawnPoolActive").unwrap_or(0.0),
473 area_points: get_f32(root, "AreaPoints").unwrap_or(0.0),
474 creatures,
475 geometry,
476 spawn_points,
477 area_list,
478 spawn_list,
479 })
480 }
481
482 pub fn to_gff(&self) -> Gff {
486 let mut root = GffStruct::new(-1);
487
488 upsert_field(
489 &mut root,
490 "TemplateResRef",
491 GffValue::ResRef(self.template_resref),
492 );
493 upsert_field(&mut root, "Tag", GffValue::String(self.tag.clone()));
494 upsert_field(
495 &mut root,
496 "LocalizedName",
497 GffValue::LocalizedString(self.name.clone()),
498 );
499 upsert_field(&mut root, "Comment", GffValue::String(self.comment.clone()));
500 upsert_field(&mut root, "PaletteID", GffValue::UInt8(self.palette_id));
501
502 upsert_field(&mut root, "Active", GffValue::UInt8(u8::from(self.active)));
503 upsert_field(&mut root, "Reset", GffValue::UInt8(u8::from(self.reset)));
504 upsert_field(&mut root, "ResetTime", GffValue::Int32(self.reset_time));
505 upsert_field(&mut root, "Respawns", GffValue::Int32(self.respawns));
506 upsert_field(&mut root, "SpawnOption", GffValue::Int32(self.spawn_option));
507 upsert_field(
508 &mut root,
509 "MaxCreatures",
510 GffValue::Int32(self.max_creatures),
511 );
512 upsert_field(
513 &mut root,
514 "RecCreatures",
515 GffValue::Int32(self.rec_creatures),
516 );
517 upsert_field(
518 &mut root,
519 "PlayerOnly",
520 GffValue::UInt8(u8::from(self.player_only)),
521 );
522 upsert_field(&mut root, "Faction", GffValue::UInt32(self.faction_id));
523
524 upsert_field(
525 &mut root,
526 "DifficultyIndex",
527 GffValue::Int32(self.difficulty_index),
528 );
529 upsert_field(&mut root, "Difficulty", GffValue::Int32(self.difficulty));
530
531 upsert_field(&mut root, "XPosition", GffValue::Single(self.x_position));
532 upsert_field(&mut root, "YPosition", GffValue::Single(self.y_position));
533 upsert_field(&mut root, "ZPosition", GffValue::Single(self.z_position));
534
535 upsert_field(&mut root, "OnEntered", GffValue::ResRef(self.on_entered));
536 upsert_field(&mut root, "OnExit", GffValue::ResRef(self.on_exit));
537 upsert_field(
538 &mut root,
539 "OnHeartbeat",
540 GffValue::ResRef(self.on_heartbeat),
541 );
542 upsert_field(
543 &mut root,
544 "OnExhausted",
545 GffValue::ResRef(self.on_exhausted),
546 );
547 upsert_field(
548 &mut root,
549 "OnUserDefined",
550 GffValue::ResRef(self.on_user_defined),
551 );
552
553 upsert_field(
554 &mut root,
555 "NumberSpawned",
556 GffValue::Int32(self.number_spawned),
557 );
558 upsert_field(
559 &mut root,
560 "HeartbeatDay",
561 GffValue::UInt32(self.heartbeat_day),
562 );
563 upsert_field(
564 &mut root,
565 "HeartbeatTime",
566 GffValue::UInt32(self.heartbeat_time),
567 );
568 upsert_field(
569 &mut root,
570 "LastSpawnDay",
571 GffValue::UInt32(self.last_spawn_day),
572 );
573 upsert_field(
574 &mut root,
575 "LastSpawnTime",
576 GffValue::UInt32(self.last_spawn_time),
577 );
578 upsert_field(
579 &mut root,
580 "LastEntered",
581 GffValue::UInt32(self.last_entered),
582 );
583 upsert_field(&mut root, "LastLeft", GffValue::UInt32(self.last_left));
584 upsert_field(
585 &mut root,
586 "Started",
587 GffValue::UInt8(u8::from(self.started)),
588 );
589 upsert_field(
590 &mut root,
591 "Exhausted",
592 GffValue::UInt8(u8::from(self.exhausted)),
593 );
594 upsert_field(
595 &mut root,
596 "CurrentSpawns",
597 GffValue::Int32(self.current_spawns),
598 );
599 upsert_field(
600 &mut root,
601 "CustomScriptId",
602 GffValue::Int32(self.custom_script_id),
603 );
604
605 upsert_field(
606 &mut root,
607 "AreaListMaxSize",
608 GffValue::Int32(self.area_list_max_size),
609 );
610 upsert_field(
611 &mut root,
612 "SpawnPoolActive",
613 GffValue::Single(self.spawn_pool_active),
614 );
615 upsert_field(&mut root, "AreaPoints", GffValue::Single(self.area_points));
616
617 let creature_structs: Vec<GffStruct> =
618 self.creatures.iter().map(|c| c.to_gff_struct()).collect();
619 upsert_field(&mut root, "CreatureList", GffValue::List(creature_structs));
620
621 let geometry_structs: Vec<GffStruct> =
622 self.geometry.iter().map(|v| v.to_gff_struct()).collect();
623 upsert_field(&mut root, "Geometry", GffValue::List(geometry_structs));
624
625 let spawn_point_structs: Vec<GffStruct> = self
626 .spawn_points
627 .iter()
628 .map(|p| p.to_gff_struct())
629 .collect();
630 upsert_field(
631 &mut root,
632 "SpawnPointList",
633 GffValue::List(spawn_point_structs),
634 );
635
636 let area_list_structs: Vec<GffStruct> =
637 self.area_list.iter().map(|a| a.to_gff_struct()).collect();
638 upsert_field(&mut root, "AreaList", GffValue::List(area_list_structs));
639
640 let spawn_list_structs: Vec<GffStruct> =
641 self.spawn_list.iter().map(|e| e.to_gff_struct()).collect();
642 upsert_field(&mut root, "SpawnList", GffValue::List(spawn_list_structs));
643
644 Gff::new(*b"UTE ", root)
645 }
646}
647
648#[derive(Debug, Error)]
650pub enum UteError {
651 #[error("unsupported UTE file type: {0:?}")]
653 UnsupportedFileType([u8; 4]),
654 #[error("UTE field `{field}` has incompatible type (expected {expected})")]
656 TypeMismatch {
657 field: &'static str,
659 expected: &'static str,
661 },
662 #[error(transparent)]
664 Gff(#[from] GffBinaryError),
665}
666
667#[cfg_attr(
669 feature = "tracing",
670 tracing::instrument(level = "debug", skip(reader))
671)]
672pub fn read_ute<R: Read>(reader: &mut R) -> Result<Ute, UteError> {
673 let gff = read_gff(reader)?;
674 Ute::from_gff(&gff)
675}
676
677#[cfg_attr(
679 feature = "tracing",
680 tracing::instrument(level = "debug", skip(bytes), fields(bytes_len = bytes.len()))
681)]
682pub fn read_ute_from_bytes(bytes: &[u8]) -> Result<Ute, UteError> {
683 let gff = read_gff_from_bytes(bytes)?;
684 Ute::from_gff(&gff)
685}
686
687#[cfg_attr(
689 feature = "tracing",
690 tracing::instrument(level = "debug", skip(writer, ute))
691)]
692pub fn write_ute<W: Write>(writer: &mut W, ute: &Ute) -> Result<(), UteError> {
693 let gff = ute.to_gff();
694 write_gff(writer, &gff)?;
695 Ok(())
696}
697
698#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(ute)))]
700pub fn write_ute_to_vec(ute: &Ute) -> Result<Vec<u8>, UteError> {
701 let mut cursor = Cursor::new(Vec::new());
702 write_ute(&mut cursor, ute)?;
703 Ok(cursor.into_inner())
704}
705
706static GEOMETRY_CHILDREN: &[FieldSchema] = &[
708 FieldSchema {
709 label: "X",
710 expected_type: GffType::Single,
711 life: FieldLife::Live,
712 required: false,
713 absent: AbsentDefault::Unverified,
714 children: None,
715 constraint: None,
716 },
717 FieldSchema {
718 label: "Y",
719 expected_type: GffType::Single,
720 life: FieldLife::Live,
721 required: false,
722 absent: AbsentDefault::Unverified,
723 children: None,
724 constraint: None,
725 },
726 FieldSchema {
727 label: "Z",
728 expected_type: GffType::Single,
729 life: FieldLife::Live,
730 required: false,
731 absent: AbsentDefault::Unverified,
732 children: None,
733 constraint: None,
734 },
735];
736
737static CREATURE_LIST_CHILDREN: &[FieldSchema] = &[
739 FieldSchema {
740 label: "ResRef",
741 expected_type: GffType::ResRef,
742 life: FieldLife::Live,
743 required: false,
744 absent: AbsentDefault::Unverified,
745 children: None,
746 constraint: None,
747 },
748 FieldSchema {
749 label: "CR",
750 expected_type: GffType::Single,
751 life: FieldLife::Live,
752 required: false,
753 absent: AbsentDefault::Unverified,
754 children: None,
755 constraint: None,
756 },
757 FieldSchema {
758 label: "SingleSpawn",
759 expected_type: GffType::UInt8,
760 life: FieldLife::Live,
761 required: false,
762 absent: AbsentDefault::Unverified,
763 children: None,
764 constraint: None,
765 },
766];
767
768static SPAWN_POINT_LIST_CHILDREN: &[FieldSchema] = &[
770 FieldSchema {
771 label: "X",
772 expected_type: GffType::Single,
773 life: FieldLife::Live,
774 required: false,
775 absent: AbsentDefault::Unverified,
776 children: None,
777 constraint: None,
778 },
779 FieldSchema {
780 label: "Y",
781 expected_type: GffType::Single,
782 life: FieldLife::Live,
783 required: false,
784 absent: AbsentDefault::Unverified,
785 children: None,
786 constraint: None,
787 },
788 FieldSchema {
789 label: "Z",
790 expected_type: GffType::Single,
791 life: FieldLife::Live,
792 required: false,
793 absent: AbsentDefault::Unverified,
794 children: None,
795 constraint: None,
796 },
797 FieldSchema {
798 label: "Orientation",
799 expected_type: GffType::Single,
800 life: FieldLife::Live,
801 required: false,
802 absent: AbsentDefault::Unverified,
803 children: None,
804 constraint: None,
805 },
806];
807
808static AREA_LIST_CHILDREN: &[FieldSchema] = &[FieldSchema {
810 label: "AreaObject",
811 expected_type: GffType::UInt32,
812 life: FieldLife::Live,
813 required: false,
814 absent: AbsentDefault::Unverified,
815 children: None,
816 constraint: None,
817}];
818
819static SPAWN_LIST_CHILDREN: &[FieldSchema] = &[
821 FieldSchema {
822 label: "SpawnResRef",
823 expected_type: GffType::ResRef,
824 life: FieldLife::Live,
825 required: false,
826 absent: AbsentDefault::Unverified,
827 children: None,
828 constraint: None,
829 },
830 FieldSchema {
831 label: "SpawnCR",
832 expected_type: GffType::Single,
833 life: FieldLife::Live,
834 required: false,
835 absent: AbsentDefault::Unverified,
836 children: None,
837 constraint: None,
838 },
839];
840
841impl GffSchema for Ute {
842 fn schema() -> &'static [FieldSchema] {
843 static SCHEMA: &[FieldSchema] = &[
844 FieldSchema {
846 label: "Tag",
847 expected_type: GffType::String,
848 life: FieldLife::Live,
849 required: false,
850 absent: AbsentDefault::Unverified,
851 children: None,
852 constraint: None,
853 },
854 FieldSchema {
855 label: "LocalizedName",
856 expected_type: GffType::LocalizedString,
857 life: FieldLife::Live,
858 required: false,
859 absent: AbsentDefault::Unverified,
860 children: None,
861 constraint: None,
862 },
863 FieldSchema {
865 label: "Active",
866 expected_type: GffType::UInt8,
867 life: FieldLife::Live,
868 required: false,
869 absent: AbsentDefault::Unverified,
870 children: None,
871 constraint: None,
872 },
873 FieldSchema {
874 label: "Reset",
875 expected_type: GffType::UInt8,
876 life: FieldLife::Live,
877 required: false,
878 absent: AbsentDefault::Unverified,
879 children: None,
880 constraint: None,
881 },
882 FieldSchema {
883 label: "ResetTime",
884 expected_type: GffType::Int32,
885 life: FieldLife::Live,
886 required: false,
887 absent: AbsentDefault::Unverified,
888 children: None,
889 constraint: None,
890 },
891 FieldSchema {
892 label: "Respawns",
893 expected_type: GffType::Int32,
894 life: FieldLife::Live,
895 required: false,
896 absent: AbsentDefault::Unverified,
897 children: None,
898 constraint: None,
899 },
900 FieldSchema {
901 label: "SpawnOption",
902 expected_type: GffType::Int32,
903 life: FieldLife::Live,
904 required: false,
905 absent: AbsentDefault::Unverified,
906 children: None,
907 constraint: None,
908 },
909 FieldSchema {
910 label: "MaxCreatures",
911 expected_type: GffType::Int32,
912 life: FieldLife::Live,
913 required: false,
914 absent: AbsentDefault::Unverified,
915 children: None,
916 constraint: None,
917 },
918 FieldSchema {
919 label: "RecCreatures",
920 expected_type: GffType::Int32,
921 life: FieldLife::Live,
922 required: false,
923 absent: AbsentDefault::Unverified,
924 children: None,
925 constraint: None,
926 },
927 FieldSchema {
928 label: "PlayerOnly",
929 expected_type: GffType::UInt8,
930 life: FieldLife::Live,
931 required: false,
932 absent: AbsentDefault::Unverified,
933 children: None,
934 constraint: None,
935 },
936 FieldSchema {
937 label: "Faction",
938 expected_type: GffType::UInt32,
939 life: FieldLife::Live,
940 required: false,
941 absent: AbsentDefault::Unverified,
942 children: None,
943 constraint: None,
944 },
945 FieldSchema {
947 label: "DifficultyIndex",
948 expected_type: GffType::Int32,
949 life: FieldLife::Live,
950 required: false,
951 absent: AbsentDefault::Unverified,
952 children: None,
953 constraint: None,
954 },
955 FieldSchema {
956 label: "Difficulty",
957 expected_type: GffType::Int32,
958 life: FieldLife::Live,
959 required: false,
960 absent: AbsentDefault::Unverified,
961 children: None,
962 constraint: None,
963 },
964 FieldSchema {
966 label: "XPosition",
967 expected_type: GffType::Single,
968 life: FieldLife::Live,
969 required: false,
970 absent: AbsentDefault::Unverified,
971 children: None,
972 constraint: None,
973 },
974 FieldSchema {
975 label: "YPosition",
976 expected_type: GffType::Single,
977 life: FieldLife::Live,
978 required: false,
979 absent: AbsentDefault::Unverified,
980 children: None,
981 constraint: None,
982 },
983 FieldSchema {
984 label: "ZPosition",
985 expected_type: GffType::Single,
986 life: FieldLife::Live,
987 required: false,
988 absent: AbsentDefault::Unverified,
989 children: None,
990 constraint: None,
991 },
992 FieldSchema {
994 label: "OnEntered",
995 expected_type: GffType::ResRef,
996 life: FieldLife::Live,
997 required: false,
998 absent: AbsentDefault::Unverified,
999 children: None,
1000 constraint: None,
1001 },
1002 FieldSchema {
1003 label: "OnExit",
1004 expected_type: GffType::ResRef,
1005 life: FieldLife::Live,
1006 required: false,
1007 absent: AbsentDefault::Unverified,
1008 children: None,
1009 constraint: None,
1010 },
1011 FieldSchema {
1012 label: "OnHeartbeat",
1013 expected_type: GffType::ResRef,
1014 life: FieldLife::Live,
1015 required: false,
1016 absent: AbsentDefault::Unverified,
1017 children: None,
1018 constraint: None,
1019 },
1020 FieldSchema {
1021 label: "OnExhausted",
1022 expected_type: GffType::ResRef,
1023 life: FieldLife::Live,
1024 required: false,
1025 absent: AbsentDefault::Unverified,
1026 children: None,
1027 constraint: None,
1028 },
1029 FieldSchema {
1030 label: "OnUserDefined",
1031 expected_type: GffType::ResRef,
1032 life: FieldLife::Live,
1033 required: false,
1034 absent: AbsentDefault::Unverified,
1035 children: None,
1036 constraint: None,
1037 },
1038 FieldSchema {
1040 label: "NumberSpawned",
1041 expected_type: GffType::Int32,
1042 life: FieldLife::Live,
1043 required: false,
1044 absent: AbsentDefault::Unverified,
1045 children: None,
1046 constraint: None,
1047 },
1048 FieldSchema {
1049 label: "HeartbeatDay",
1050 expected_type: GffType::UInt32,
1051 life: FieldLife::Live,
1052 required: false,
1053 absent: AbsentDefault::Unverified,
1054 children: None,
1055 constraint: None,
1056 },
1057 FieldSchema {
1058 label: "HeartbeatTime",
1059 expected_type: GffType::UInt32,
1060 life: FieldLife::Live,
1061 required: false,
1062 absent: AbsentDefault::Unverified,
1063 children: None,
1064 constraint: None,
1065 },
1066 FieldSchema {
1067 label: "LastSpawnDay",
1068 expected_type: GffType::UInt32,
1069 life: FieldLife::Live,
1070 required: false,
1071 absent: AbsentDefault::Unverified,
1072 children: None,
1073 constraint: None,
1074 },
1075 FieldSchema {
1076 label: "LastSpawnTime",
1077 expected_type: GffType::UInt32,
1078 life: FieldLife::Live,
1079 required: false,
1080 absent: AbsentDefault::Unverified,
1081 children: None,
1082 constraint: None,
1083 },
1084 FieldSchema {
1085 label: "LastEntered",
1086 expected_type: GffType::UInt32,
1087 life: FieldLife::Live,
1088 required: false,
1089 absent: AbsentDefault::Unverified,
1090 children: None,
1091 constraint: None,
1092 },
1093 FieldSchema {
1094 label: "LastLeft",
1095 expected_type: GffType::UInt32,
1096 life: FieldLife::Live,
1097 required: false,
1098 absent: AbsentDefault::Unverified,
1099 children: None,
1100 constraint: None,
1101 },
1102 FieldSchema {
1103 label: "Started",
1104 expected_type: GffType::UInt8,
1105 life: FieldLife::Live,
1106 required: false,
1107 absent: AbsentDefault::Unverified,
1108 children: None,
1109 constraint: None,
1110 },
1111 FieldSchema {
1112 label: "Exhausted",
1113 expected_type: GffType::UInt8,
1114 life: FieldLife::Live,
1115 required: false,
1116 absent: AbsentDefault::Unverified,
1117 children: None,
1118 constraint: None,
1119 },
1120 FieldSchema {
1121 label: "CurrentSpawns",
1122 expected_type: GffType::Int32,
1123 life: FieldLife::Live,
1124 required: false,
1125 absent: AbsentDefault::Unverified,
1126 children: None,
1127 constraint: None,
1128 },
1129 FieldSchema {
1130 label: "CustomScriptId",
1131 expected_type: GffType::Int32,
1132 life: FieldLife::Live,
1133 required: false,
1134 absent: AbsentDefault::Unverified,
1135 children: None,
1136 constraint: None,
1137 },
1138 FieldSchema {
1140 label: "AreaListMaxSize",
1141 expected_type: GffType::Int32,
1142 life: FieldLife::Live,
1143 required: false,
1144 absent: AbsentDefault::Unverified,
1145 children: None,
1146 constraint: None,
1147 },
1148 FieldSchema {
1149 label: "SpawnPoolActive",
1150 expected_type: GffType::Single,
1151 life: FieldLife::Live,
1152 required: false,
1153 absent: AbsentDefault::Unverified,
1154 children: None,
1155 constraint: None,
1156 },
1157 FieldSchema {
1158 label: "AreaPoints",
1159 expected_type: GffType::Single,
1160 life: FieldLife::Live,
1161 required: false,
1162 absent: AbsentDefault::Unverified,
1163 children: None,
1164 constraint: None,
1165 },
1166 FieldSchema {
1168 label: "Geometry",
1169 expected_type: GffType::List,
1170 life: FieldLife::Live,
1171 required: false,
1172 absent: AbsentDefault::Unverified,
1173 children: Some(GEOMETRY_CHILDREN),
1174 constraint: None,
1175 },
1176 FieldSchema {
1177 label: "CreatureList",
1178 expected_type: GffType::List,
1179 life: FieldLife::Live,
1180 required: false,
1181 absent: AbsentDefault::Unverified,
1182 children: Some(CREATURE_LIST_CHILDREN),
1183 constraint: None,
1184 },
1185 FieldSchema {
1186 label: "SpawnPointList",
1187 expected_type: GffType::List,
1188 life: FieldLife::Live,
1189 required: false,
1190 absent: AbsentDefault::Unverified,
1191 children: Some(SPAWN_POINT_LIST_CHILDREN),
1192 constraint: None,
1193 },
1194 FieldSchema {
1195 label: "AreaList",
1196 expected_type: GffType::List,
1197 life: FieldLife::Live,
1198 required: false,
1199 absent: AbsentDefault::Unverified,
1200 children: Some(AREA_LIST_CHILDREN),
1201 constraint: None,
1202 },
1203 FieldSchema {
1204 label: "SpawnList",
1205 expected_type: GffType::List,
1206 life: FieldLife::Live,
1207 required: false,
1208 absent: AbsentDefault::Unverified,
1209 children: Some(SPAWN_LIST_CHILDREN),
1210 constraint: None,
1211 },
1212 FieldSchema {
1214 label: "TemplateResRef",
1215 expected_type: GffType::ResRef,
1216 life: FieldLife::Live,
1217 required: false,
1218 absent: AbsentDefault::Unverified,
1219 children: None,
1220 constraint: None,
1221 },
1222 FieldSchema {
1223 label: "Comment",
1224 expected_type: GffType::String,
1225 life: FieldLife::Live,
1226 required: false,
1227 absent: AbsentDefault::Unverified,
1228 children: None,
1229 constraint: None,
1230 },
1231 FieldSchema {
1232 label: "PaletteID",
1233 expected_type: GffType::UInt8,
1234 life: FieldLife::Live,
1235 required: false,
1236 absent: AbsentDefault::Unverified,
1237 children: None,
1238 constraint: None,
1239 },
1240 ];
1241 SCHEMA
1242 }
1243}
1244
1245#[cfg(test)]
1246mod tests {
1247 use super::*;
1248
1249 fn make_test_ute_gff() -> Gff {
1251 let mut root = GffStruct::new(-1);
1252 root.push_field("Tag", GffValue::String("TestEncounter".into()));
1253 root.push_field("TemplateResRef", GffValue::resref_lit("enc_test001"));
1254 root.push_field(
1255 "LocalizedName",
1256 GffValue::LocalizedString(GffLocalizedString::new(StrRef::from_raw(12345))),
1257 );
1258 root.push_field("Comment", GffValue::String("test comment".into()));
1259 root.push_field("PaletteID", GffValue::UInt8(3));
1260 root.push_field("Active", GffValue::UInt8(1));
1261 root.push_field("Reset", GffValue::UInt8(1));
1262 root.push_field("ResetTime", GffValue::Int32(120));
1263 root.push_field("Respawns", GffValue::Int32(-1));
1264 root.push_field("SpawnOption", GffValue::Int32(1));
1265 root.push_field("MaxCreatures", GffValue::Int32(4));
1266 root.push_field("RecCreatures", GffValue::Int32(2));
1267 root.push_field("PlayerOnly", GffValue::UInt8(0));
1268 root.push_field("Faction", GffValue::UInt32(1));
1269 root.push_field("DifficultyIndex", GffValue::Int32(3));
1270 root.push_field("Difficulty", GffValue::Int32(2));
1271 root.push_field("OnEntered", GffValue::resref_lit("k_enc_enter"));
1272 root.push_field("OnExit", GffValue::resref_lit("k_enc_exit"));
1273 root.push_field("OnHeartbeat", GffValue::resref_lit(""));
1274 root.push_field("OnExhausted", GffValue::resref_lit("k_enc_exhaust"));
1275 root.push_field("OnUserDefined", GffValue::resref_lit(""));
1276
1277 let mut c1 = GffStruct::new(0);
1279 c1.push_field("ResRef", GffValue::resref_lit("k_def_yourpaty"));
1280 c1.push_field("CR", GffValue::Single(3.0));
1281 c1.push_field("SingleSpawn", GffValue::UInt8(0));
1282
1283 let mut c2 = GffStruct::new(0);
1284 c2.push_field("ResRef", GffValue::resref_lit("k_def_darkjedi"));
1285 c2.push_field("CR", GffValue::Single(5.5));
1286 c2.push_field("SingleSpawn", GffValue::UInt8(1));
1287
1288 root.push_field("CreatureList", GffValue::List(vec![c1, c2]));
1289
1290 Gff::new(*b"UTE ", root)
1291 }
1292
1293 fn make_full_ute_gff() -> Gff {
1295 let mut root = GffStruct::new(-1);
1296
1297 root.push_field("Tag", GffValue::String("FullEncounter".into()));
1299 root.push_field("TemplateResRef", GffValue::resref_lit("enc_full001"));
1300 root.push_field(
1301 "LocalizedName",
1302 GffValue::LocalizedString(GffLocalizedString::new(StrRef::from_raw(99999))),
1303 );
1304 root.push_field("Comment", GffValue::String("full test".into()));
1305 root.push_field("PaletteID", GffValue::UInt8(7));
1306
1307 root.push_field("Active", GffValue::UInt8(1));
1309 root.push_field("Reset", GffValue::UInt8(0));
1310 root.push_field("ResetTime", GffValue::Int32(60));
1311 root.push_field("Respawns", GffValue::Int32(3));
1312 root.push_field("SpawnOption", GffValue::Int32(2));
1313 root.push_field("MaxCreatures", GffValue::Int32(6));
1314 root.push_field("RecCreatures", GffValue::Int32(3));
1315 root.push_field("PlayerOnly", GffValue::UInt8(1));
1316 root.push_field("Faction", GffValue::UInt32(42));
1317
1318 root.push_field("DifficultyIndex", GffValue::Int32(5));
1320 root.push_field("Difficulty", GffValue::Int32(4));
1321
1322 root.push_field("XPosition", GffValue::Single(10.5));
1324 root.push_field("YPosition", GffValue::Single(20.25));
1325 root.push_field("ZPosition", GffValue::Single(-3.0));
1326
1327 root.push_field("OnEntered", GffValue::resref_lit("s_enter"));
1329 root.push_field("OnExit", GffValue::resref_lit("s_exit"));
1330 root.push_field("OnHeartbeat", GffValue::resref_lit("s_hb"));
1331 root.push_field("OnExhausted", GffValue::resref_lit("s_exhaust"));
1332 root.push_field("OnUserDefined", GffValue::resref_lit("s_ud"));
1333
1334 root.push_field("NumberSpawned", GffValue::Int32(12));
1336 root.push_field("HeartbeatDay", GffValue::UInt32(100));
1337 root.push_field("HeartbeatTime", GffValue::UInt32(200));
1338 root.push_field("LastSpawnDay", GffValue::UInt32(101));
1339 root.push_field("LastSpawnTime", GffValue::UInt32(201));
1340 root.push_field("LastEntered", GffValue::UInt32(555));
1341 root.push_field("LastLeft", GffValue::UInt32(444));
1342 root.push_field("Started", GffValue::UInt8(1));
1343 root.push_field("Exhausted", GffValue::UInt8(0));
1344 root.push_field("CurrentSpawns", GffValue::Int32(3));
1345 root.push_field("CustomScriptId", GffValue::Int32(77));
1346
1347 root.push_field("AreaListMaxSize", GffValue::Int32(10));
1349 root.push_field("SpawnPoolActive", GffValue::Single(1.5));
1350 root.push_field("AreaPoints", GffValue::Single(2.75));
1351
1352 let mut c1 = GffStruct::new(0);
1354 c1.push_field("ResRef", GffValue::resref_lit("cr_sith01"));
1355 c1.push_field("CR", GffValue::Single(4.0));
1356 c1.push_field("SingleSpawn", GffValue::UInt8(1));
1357 root.push_field("CreatureList", GffValue::List(vec![c1]));
1358
1359 let mut g1 = GffStruct::new(0);
1361 g1.push_field("X", GffValue::Single(1.0));
1362 g1.push_field("Y", GffValue::Single(2.0));
1363 g1.push_field("Z", GffValue::Single(3.0));
1364 let mut g2 = GffStruct::new(0);
1365 g2.push_field("X", GffValue::Single(4.0));
1366 g2.push_field("Y", GffValue::Single(5.0));
1367 g2.push_field("Z", GffValue::Single(6.0));
1368 root.push_field("Geometry", GffValue::List(vec![g1, g2]));
1369
1370 let mut sp = GffStruct::new(0);
1372 sp.push_field("X", GffValue::Single(11.0));
1373 sp.push_field("Y", GffValue::Single(22.0));
1374 sp.push_field("Z", GffValue::Single(33.0));
1375 sp.push_field("Orientation", GffValue::Single(1.57));
1376 root.push_field("SpawnPointList", GffValue::List(vec![sp]));
1377
1378 let mut ae = GffStruct::new(0);
1380 ae.push_field("AreaObject", GffValue::UInt32(9001));
1381 root.push_field("AreaList", GffValue::List(vec![ae]));
1382
1383 let mut se = GffStruct::new(0);
1385 se.push_field("SpawnResRef", GffValue::resref_lit("sp_ref01"));
1386 se.push_field("SpawnCR", GffValue::Single(2.5));
1387 root.push_field("SpawnList", GffValue::List(vec![se]));
1388
1389 Gff::new(*b"UTE ", root)
1390 }
1391
1392 #[test]
1393 fn reads_core_ute_fields() {
1394 let gff = make_test_ute_gff();
1395 let ute = Ute::from_gff(&gff).expect("must parse");
1396
1397 assert_eq!(ute.tag, "TestEncounter");
1398 assert_eq!(ute.template_resref, "enc_test001");
1399 assert_eq!(ute.name.string_ref.raw(), 12345);
1400 assert_eq!(ute.comment, "test comment");
1401 assert_eq!(ute.palette_id, 3);
1402 assert!(ute.active);
1403 assert!(ute.reset);
1404 assert_eq!(ute.reset_time, 120);
1405 assert_eq!(ute.respawns, -1);
1406 assert_eq!(ute.spawn_option, 1);
1407 assert_eq!(ute.max_creatures, 4);
1408 assert_eq!(ute.rec_creatures, 2);
1409 assert!(!ute.player_only);
1410 assert_eq!(ute.faction_id, 1);
1411 assert_eq!(ute.difficulty_index, 3);
1412 assert_eq!(ute.difficulty, 2);
1413 assert_eq!(ute.on_entered, "k_enc_enter");
1414 assert_eq!(ute.on_exit, "k_enc_exit");
1415 assert_eq!(ute.on_heartbeat, "");
1416 assert_eq!(ute.on_exhausted, "k_enc_exhaust");
1417 assert_eq!(ute.on_user_defined, "");
1418 }
1419
1420 #[test]
1421 fn reads_creature_list() {
1422 let gff = make_test_ute_gff();
1423 let ute = Ute::from_gff(&gff).expect("must parse");
1424
1425 assert_eq!(ute.creatures.len(), 2);
1426 assert_eq!(ute.creatures[0].resref, "k_def_yourpaty");
1427 assert_eq!(ute.creatures[0].cr, 3.0);
1428 assert!(!ute.creatures[0].single_spawn);
1429 assert_eq!(ute.creatures[1].resref, "k_def_darkjedi");
1430 assert_eq!(ute.creatures[1].cr, 5.5);
1431 assert!(ute.creatures[1].single_spawn);
1432 }
1433
1434 #[test]
1435 fn all_fields_survive_typed_roundtrip() {
1436 let gff = make_full_ute_gff();
1437 let ute = Ute::from_gff(&gff).expect("must parse full GFF");
1438
1439 assert_eq!(ute.tag, "FullEncounter");
1441 assert_eq!(ute.template_resref, "enc_full001");
1442 assert_eq!(ute.name.string_ref.raw(), 99999);
1443 assert_eq!(ute.comment, "full test");
1444 assert_eq!(ute.palette_id, 7);
1445 assert!(ute.active);
1446 assert!(!ute.reset);
1447 assert_eq!(ute.reset_time, 60);
1448 assert_eq!(ute.respawns, 3);
1449 assert_eq!(ute.spawn_option, 2);
1450 assert_eq!(ute.max_creatures, 6);
1451 assert_eq!(ute.rec_creatures, 3);
1452 assert!(ute.player_only);
1453 assert_eq!(ute.faction_id, 42);
1454 assert_eq!(ute.difficulty_index, 5);
1455 assert_eq!(ute.difficulty, 4);
1456 assert_eq!(ute.x_position, 10.5);
1457 assert_eq!(ute.y_position, 20.25);
1458 assert_eq!(ute.z_position, -3.0);
1459 assert_eq!(ute.on_entered, "s_enter");
1460 assert_eq!(ute.on_exit, "s_exit");
1461 assert_eq!(ute.on_heartbeat, "s_hb");
1462 assert_eq!(ute.on_exhausted, "s_exhaust");
1463 assert_eq!(ute.on_user_defined, "s_ud");
1464 assert_eq!(ute.number_spawned, 12);
1465 assert_eq!(ute.heartbeat_day, 100);
1466 assert_eq!(ute.heartbeat_time, 200);
1467 assert_eq!(ute.last_spawn_day, 101);
1468 assert_eq!(ute.last_spawn_time, 201);
1469 assert_eq!(ute.last_entered, 555);
1470 assert_eq!(ute.last_left, 444);
1471 assert!(ute.started);
1472 assert!(!ute.exhausted);
1473 assert_eq!(ute.current_spawns, 3);
1474 assert_eq!(ute.custom_script_id, 77);
1475 assert_eq!(ute.area_list_max_size, 10);
1476 assert_eq!(ute.spawn_pool_active, 1.5);
1477 assert_eq!(ute.area_points, 2.75);
1478
1479 assert_eq!(ute.creatures.len(), 1);
1481 assert_eq!(ute.creatures[0].resref, "cr_sith01");
1482 assert_eq!(ute.creatures[0].cr, 4.0);
1483 assert!(ute.creatures[0].single_spawn);
1484
1485 assert_eq!(ute.geometry.len(), 2);
1486 assert_eq!(ute.geometry[0].x, 1.0);
1487 assert_eq!(ute.geometry[1].y, 5.0);
1488
1489 assert_eq!(ute.spawn_points.len(), 1);
1490 assert_eq!(ute.spawn_points[0].x, 11.0);
1491 assert_eq!(ute.spawn_points[0].orientation, 1.57);
1492
1493 assert_eq!(ute.area_list.len(), 1);
1494 assert_eq!(ute.area_list[0].area_object, 9001);
1495
1496 assert_eq!(ute.spawn_list.len(), 1);
1497 assert_eq!(ute.spawn_list[0].spawn_resref, "sp_ref01");
1498 assert_eq!(ute.spawn_list[0].spawn_cr, 2.5);
1499
1500 let bytes = write_ute_to_vec(&ute).expect("write succeeds");
1502 let reparsed = read_ute_from_bytes(&bytes).expect("reparse succeeds");
1503 assert_eq!(ute, reparsed);
1504 }
1505
1506 #[test]
1507 fn typed_edits_roundtrip_through_gff_writer() {
1508 let gff = make_test_ute_gff();
1509 let mut ute = Ute::from_gff(&gff).expect("must parse");
1510 ute.tag = "EditedEncounter".into();
1511 ute.max_creatures = 8;
1512 ute.on_entered = ResRef::new("rust_on_enter").expect("valid test resref");
1513 ute.creatures[0].resref = ResRef::new("k_new_creature").expect("valid test resref");
1514
1515 let bytes = write_ute_to_vec(&ute).expect("write succeeds");
1516 let reparsed = read_ute_from_bytes(&bytes).expect("reparse succeeds");
1517
1518 assert_eq!(reparsed.tag, "EditedEncounter");
1519 assert_eq!(reparsed.max_creatures, 8);
1520 assert_eq!(reparsed.on_entered, "rust_on_enter");
1521 assert_eq!(reparsed.creatures[0].resref, "k_new_creature");
1522 }
1523
1524 #[test]
1525 fn read_ute_from_reader_matches_bytes_path() {
1526 let gff = make_test_ute_gff();
1527 let bytes = {
1528 let mut c = Cursor::new(Vec::new());
1529 write_gff(&mut c, &gff).expect("test fixture must be valid");
1530 c.into_inner()
1531 };
1532
1533 let mut cursor = Cursor::new(&bytes);
1534 let via_reader = read_ute(&mut cursor).expect("reader parse succeeds");
1535 let via_bytes = read_ute_from_bytes(&bytes).expect("bytes parse succeeds");
1536
1537 assert_eq!(via_reader, via_bytes);
1538 }
1539
1540 #[test]
1541 fn rejects_non_ute_file_type() {
1542 let mut gff = make_test_ute_gff();
1543 gff.file_type = *b"UTT ";
1544
1545 let err = Ute::from_gff(&gff).expect_err("UTT must be rejected as UTE input");
1546 assert!(matches!(
1547 err,
1548 UteError::UnsupportedFileType(file_type) if file_type == *b"UTT "
1549 ));
1550 }
1551
1552 #[test]
1553 fn type_mismatch_on_localized_name_is_error() {
1554 let mut gff = make_test_ute_gff();
1555 gff.root
1556 .fields
1557 .retain(|field| field.label != "LocalizedName");
1558 gff.root.push_field("LocalizedName", GffValue::UInt32(5));
1559
1560 let err = Ute::from_gff(&gff).expect_err("type mismatch must be rejected");
1561 assert!(matches!(
1562 err,
1563 UteError::TypeMismatch {
1564 field: "LocalizedName",
1565 expected: "LocalizedString",
1566 }
1567 ));
1568 }
1569
1570 #[test]
1571 fn write_ute_matches_direct_gff_writer() {
1572 let gff = make_test_ute_gff();
1573 let ute = Ute::from_gff(&gff).expect("must parse");
1574
1575 let via_typed = write_ute_to_vec(&ute).expect("typed write succeeds");
1576
1577 let mut direct = Cursor::new(Vec::new());
1578 write_gff(&mut direct, &ute.to_gff()).expect("direct write succeeds");
1579
1580 assert_eq!(via_typed, direct.into_inner());
1581 }
1582
1583 #[test]
1584 fn empty_creature_list_ok() {
1585 let mut gff = make_test_ute_gff();
1586 gff.root.fields.retain(|f| f.label != "CreatureList");
1587
1588 let ute = Ute::from_gff(&gff).expect("must parse");
1589 assert!(ute.creatures.is_empty());
1590 }
1591
1592 #[test]
1593 fn schema_field_count() {
1594 assert_eq!(Ute::schema().len(), 43);
1595 }
1596
1597 #[test]
1598 fn schema_no_duplicate_labels() {
1599 let schema = Ute::schema();
1600 let mut labels: Vec<&str> = schema.iter().map(|f| f.label).collect();
1601 labels.sort();
1602 let before = labels.len();
1603 labels.dedup();
1604 assert_eq!(before, labels.len(), "duplicate labels in UTE schema");
1605 }
1606
1607 #[test]
1608 fn schema_lists_have_children() {
1609 let schema = Ute::schema();
1610 let geometry = schema
1611 .iter()
1612 .find(|f| f.label == "Geometry")
1613 .expect("test fixture must be valid");
1614 assert_eq!(
1615 geometry.children.expect("test fixture must be valid").len(),
1616 3
1617 );
1618 let creatures = schema
1619 .iter()
1620 .find(|f| f.label == "CreatureList")
1621 .expect("test fixture must be valid");
1622 assert_eq!(
1623 creatures
1624 .children
1625 .expect("test fixture must be valid")
1626 .len(),
1627 3
1628 );
1629 let spawn_points = schema
1630 .iter()
1631 .find(|f| f.label == "SpawnPointList")
1632 .expect("test fixture must be valid");
1633 assert_eq!(
1634 spawn_points
1635 .children
1636 .expect("test fixture must be valid")
1637 .len(),
1638 4
1639 );
1640 let area_list = schema
1641 .iter()
1642 .find(|f| f.label == "AreaList")
1643 .expect("test fixture must be valid");
1644 assert_eq!(
1645 area_list
1646 .children
1647 .expect("test fixture must be valid")
1648 .len(),
1649 1
1650 );
1651 let spawn_list = schema
1652 .iter()
1653 .find(|f| f.label == "SpawnList")
1654 .expect("test fixture must be valid");
1655 assert_eq!(
1656 spawn_list
1657 .children
1658 .expect("test fixture must be valid")
1659 .len(),
1660 2
1661 );
1662 }
1663}