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::{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 required: false,
712 children: None,
713 constraint: None,
714 },
715 FieldSchema {
716 label: "Y",
717 expected_type: GffType::Single,
718 required: false,
719 children: None,
720 constraint: None,
721 },
722 FieldSchema {
723 label: "Z",
724 expected_type: GffType::Single,
725 required: false,
726 children: None,
727 constraint: None,
728 },
729];
730
731static CREATURE_LIST_CHILDREN: &[FieldSchema] = &[
733 FieldSchema {
734 label: "ResRef",
735 expected_type: GffType::ResRef,
736 required: false,
737 children: None,
738 constraint: None,
739 },
740 FieldSchema {
741 label: "CR",
742 expected_type: GffType::Single,
743 required: false,
744 children: None,
745 constraint: None,
746 },
747 FieldSchema {
748 label: "SingleSpawn",
749 expected_type: GffType::UInt8,
750 required: false,
751 children: None,
752 constraint: None,
753 },
754];
755
756static SPAWN_POINT_LIST_CHILDREN: &[FieldSchema] = &[
758 FieldSchema {
759 label: "X",
760 expected_type: GffType::Single,
761 required: false,
762 children: None,
763 constraint: None,
764 },
765 FieldSchema {
766 label: "Y",
767 expected_type: GffType::Single,
768 required: false,
769 children: None,
770 constraint: None,
771 },
772 FieldSchema {
773 label: "Z",
774 expected_type: GffType::Single,
775 required: false,
776 children: None,
777 constraint: None,
778 },
779 FieldSchema {
780 label: "Orientation",
781 expected_type: GffType::Single,
782 required: false,
783 children: None,
784 constraint: None,
785 },
786];
787
788static AREA_LIST_CHILDREN: &[FieldSchema] = &[FieldSchema {
790 label: "AreaObject",
791 expected_type: GffType::UInt32,
792 required: false,
793 children: None,
794 constraint: None,
795}];
796
797static SPAWN_LIST_CHILDREN: &[FieldSchema] = &[
799 FieldSchema {
800 label: "SpawnResRef",
801 expected_type: GffType::ResRef,
802 required: false,
803 children: None,
804 constraint: None,
805 },
806 FieldSchema {
807 label: "SpawnCR",
808 expected_type: GffType::Single,
809 required: false,
810 children: None,
811 constraint: None,
812 },
813];
814
815impl GffSchema for Ute {
816 fn schema() -> &'static [FieldSchema] {
817 static SCHEMA: &[FieldSchema] = &[
818 FieldSchema {
820 label: "Tag",
821 expected_type: GffType::String,
822 required: false,
823 children: None,
824 constraint: None,
825 },
826 FieldSchema {
827 label: "LocalizedName",
828 expected_type: GffType::LocalizedString,
829 required: false,
830 children: None,
831 constraint: None,
832 },
833 FieldSchema {
835 label: "Active",
836 expected_type: GffType::UInt8,
837 required: false,
838 children: None,
839 constraint: None,
840 },
841 FieldSchema {
842 label: "Reset",
843 expected_type: GffType::UInt8,
844 required: false,
845 children: None,
846 constraint: None,
847 },
848 FieldSchema {
849 label: "ResetTime",
850 expected_type: GffType::Int32,
851 required: false,
852 children: None,
853 constraint: None,
854 },
855 FieldSchema {
856 label: "Respawns",
857 expected_type: GffType::Int32,
858 required: false,
859 children: None,
860 constraint: None,
861 },
862 FieldSchema {
863 label: "SpawnOption",
864 expected_type: GffType::Int32,
865 required: false,
866 children: None,
867 constraint: None,
868 },
869 FieldSchema {
870 label: "MaxCreatures",
871 expected_type: GffType::Int32,
872 required: false,
873 children: None,
874 constraint: None,
875 },
876 FieldSchema {
877 label: "RecCreatures",
878 expected_type: GffType::Int32,
879 required: false,
880 children: None,
881 constraint: None,
882 },
883 FieldSchema {
884 label: "PlayerOnly",
885 expected_type: GffType::UInt8,
886 required: false,
887 children: None,
888 constraint: None,
889 },
890 FieldSchema {
891 label: "Faction",
892 expected_type: GffType::UInt32,
893 required: false,
894 children: None,
895 constraint: None,
896 },
897 FieldSchema {
899 label: "DifficultyIndex",
900 expected_type: GffType::Int32,
901 required: false,
902 children: None,
903 constraint: None,
904 },
905 FieldSchema {
906 label: "Difficulty",
907 expected_type: GffType::Int32,
908 required: false,
909 children: None,
910 constraint: None,
911 },
912 FieldSchema {
914 label: "XPosition",
915 expected_type: GffType::Single,
916 required: false,
917 children: None,
918 constraint: None,
919 },
920 FieldSchema {
921 label: "YPosition",
922 expected_type: GffType::Single,
923 required: false,
924 children: None,
925 constraint: None,
926 },
927 FieldSchema {
928 label: "ZPosition",
929 expected_type: GffType::Single,
930 required: false,
931 children: None,
932 constraint: None,
933 },
934 FieldSchema {
936 label: "OnEntered",
937 expected_type: GffType::ResRef,
938 required: false,
939 children: None,
940 constraint: None,
941 },
942 FieldSchema {
943 label: "OnExit",
944 expected_type: GffType::ResRef,
945 required: false,
946 children: None,
947 constraint: None,
948 },
949 FieldSchema {
950 label: "OnHeartbeat",
951 expected_type: GffType::ResRef,
952 required: false,
953 children: None,
954 constraint: None,
955 },
956 FieldSchema {
957 label: "OnExhausted",
958 expected_type: GffType::ResRef,
959 required: false,
960 children: None,
961 constraint: None,
962 },
963 FieldSchema {
964 label: "OnUserDefined",
965 expected_type: GffType::ResRef,
966 required: false,
967 children: None,
968 constraint: None,
969 },
970 FieldSchema {
972 label: "NumberSpawned",
973 expected_type: GffType::Int32,
974 required: false,
975 children: None,
976 constraint: None,
977 },
978 FieldSchema {
979 label: "HeartbeatDay",
980 expected_type: GffType::UInt32,
981 required: false,
982 children: None,
983 constraint: None,
984 },
985 FieldSchema {
986 label: "HeartbeatTime",
987 expected_type: GffType::UInt32,
988 required: false,
989 children: None,
990 constraint: None,
991 },
992 FieldSchema {
993 label: "LastSpawnDay",
994 expected_type: GffType::UInt32,
995 required: false,
996 children: None,
997 constraint: None,
998 },
999 FieldSchema {
1000 label: "LastSpawnTime",
1001 expected_type: GffType::UInt32,
1002 required: false,
1003 children: None,
1004 constraint: None,
1005 },
1006 FieldSchema {
1007 label: "LastEntered",
1008 expected_type: GffType::UInt32,
1009 required: false,
1010 children: None,
1011 constraint: None,
1012 },
1013 FieldSchema {
1014 label: "LastLeft",
1015 expected_type: GffType::UInt32,
1016 required: false,
1017 children: None,
1018 constraint: None,
1019 },
1020 FieldSchema {
1021 label: "Started",
1022 expected_type: GffType::UInt8,
1023 required: false,
1024 children: None,
1025 constraint: None,
1026 },
1027 FieldSchema {
1028 label: "Exhausted",
1029 expected_type: GffType::UInt8,
1030 required: false,
1031 children: None,
1032 constraint: None,
1033 },
1034 FieldSchema {
1035 label: "CurrentSpawns",
1036 expected_type: GffType::Int32,
1037 required: false,
1038 children: None,
1039 constraint: None,
1040 },
1041 FieldSchema {
1042 label: "CustomScriptId",
1043 expected_type: GffType::Int32,
1044 required: false,
1045 children: None,
1046 constraint: None,
1047 },
1048 FieldSchema {
1050 label: "AreaListMaxSize",
1051 expected_type: GffType::Int32,
1052 required: false,
1053 children: None,
1054 constraint: None,
1055 },
1056 FieldSchema {
1057 label: "SpawnPoolActive",
1058 expected_type: GffType::Single,
1059 required: false,
1060 children: None,
1061 constraint: None,
1062 },
1063 FieldSchema {
1064 label: "AreaPoints",
1065 expected_type: GffType::Single,
1066 required: false,
1067 children: None,
1068 constraint: None,
1069 },
1070 FieldSchema {
1072 label: "Geometry",
1073 expected_type: GffType::List,
1074 required: false,
1075 children: Some(GEOMETRY_CHILDREN),
1076 constraint: None,
1077 },
1078 FieldSchema {
1079 label: "CreatureList",
1080 expected_type: GffType::List,
1081 required: false,
1082 children: Some(CREATURE_LIST_CHILDREN),
1083 constraint: None,
1084 },
1085 FieldSchema {
1086 label: "SpawnPointList",
1087 expected_type: GffType::List,
1088 required: false,
1089 children: Some(SPAWN_POINT_LIST_CHILDREN),
1090 constraint: None,
1091 },
1092 FieldSchema {
1093 label: "AreaList",
1094 expected_type: GffType::List,
1095 required: false,
1096 children: Some(AREA_LIST_CHILDREN),
1097 constraint: None,
1098 },
1099 FieldSchema {
1100 label: "SpawnList",
1101 expected_type: GffType::List,
1102 required: false,
1103 children: Some(SPAWN_LIST_CHILDREN),
1104 constraint: None,
1105 },
1106 FieldSchema {
1108 label: "TemplateResRef",
1109 expected_type: GffType::ResRef,
1110 required: false,
1111 children: None,
1112 constraint: None,
1113 },
1114 FieldSchema {
1115 label: "Comment",
1116 expected_type: GffType::String,
1117 required: false,
1118 children: None,
1119 constraint: None,
1120 },
1121 FieldSchema {
1122 label: "PaletteID",
1123 expected_type: GffType::UInt8,
1124 required: false,
1125 children: None,
1126 constraint: None,
1127 },
1128 ];
1129 SCHEMA
1130 }
1131}
1132
1133#[cfg(test)]
1134mod tests {
1135 use super::*;
1136
1137 fn make_test_ute_gff() -> Gff {
1139 let mut root = GffStruct::new(-1);
1140 root.push_field("Tag", GffValue::String("TestEncounter".into()));
1141 root.push_field("TemplateResRef", GffValue::resref_lit("enc_test001"));
1142 root.push_field(
1143 "LocalizedName",
1144 GffValue::LocalizedString(GffLocalizedString::new(StrRef::from_raw(12345))),
1145 );
1146 root.push_field("Comment", GffValue::String("test comment".into()));
1147 root.push_field("PaletteID", GffValue::UInt8(3));
1148 root.push_field("Active", GffValue::UInt8(1));
1149 root.push_field("Reset", GffValue::UInt8(1));
1150 root.push_field("ResetTime", GffValue::Int32(120));
1151 root.push_field("Respawns", GffValue::Int32(-1));
1152 root.push_field("SpawnOption", GffValue::Int32(1));
1153 root.push_field("MaxCreatures", GffValue::Int32(4));
1154 root.push_field("RecCreatures", GffValue::Int32(2));
1155 root.push_field("PlayerOnly", GffValue::UInt8(0));
1156 root.push_field("Faction", GffValue::UInt32(1));
1157 root.push_field("DifficultyIndex", GffValue::Int32(3));
1158 root.push_field("Difficulty", GffValue::Int32(2));
1159 root.push_field("OnEntered", GffValue::resref_lit("k_enc_enter"));
1160 root.push_field("OnExit", GffValue::resref_lit("k_enc_exit"));
1161 root.push_field("OnHeartbeat", GffValue::resref_lit(""));
1162 root.push_field("OnExhausted", GffValue::resref_lit("k_enc_exhaust"));
1163 root.push_field("OnUserDefined", GffValue::resref_lit(""));
1164
1165 let mut c1 = GffStruct::new(0);
1167 c1.push_field("ResRef", GffValue::resref_lit("k_def_yourpaty"));
1168 c1.push_field("CR", GffValue::Single(3.0));
1169 c1.push_field("SingleSpawn", GffValue::UInt8(0));
1170
1171 let mut c2 = GffStruct::new(0);
1172 c2.push_field("ResRef", GffValue::resref_lit("k_def_darkjedi"));
1173 c2.push_field("CR", GffValue::Single(5.5));
1174 c2.push_field("SingleSpawn", GffValue::UInt8(1));
1175
1176 root.push_field("CreatureList", GffValue::List(vec![c1, c2]));
1177
1178 Gff::new(*b"UTE ", root)
1179 }
1180
1181 fn make_full_ute_gff() -> Gff {
1183 let mut root = GffStruct::new(-1);
1184
1185 root.push_field("Tag", GffValue::String("FullEncounter".into()));
1187 root.push_field("TemplateResRef", GffValue::resref_lit("enc_full001"));
1188 root.push_field(
1189 "LocalizedName",
1190 GffValue::LocalizedString(GffLocalizedString::new(StrRef::from_raw(99999))),
1191 );
1192 root.push_field("Comment", GffValue::String("full test".into()));
1193 root.push_field("PaletteID", GffValue::UInt8(7));
1194
1195 root.push_field("Active", GffValue::UInt8(1));
1197 root.push_field("Reset", GffValue::UInt8(0));
1198 root.push_field("ResetTime", GffValue::Int32(60));
1199 root.push_field("Respawns", GffValue::Int32(3));
1200 root.push_field("SpawnOption", GffValue::Int32(2));
1201 root.push_field("MaxCreatures", GffValue::Int32(6));
1202 root.push_field("RecCreatures", GffValue::Int32(3));
1203 root.push_field("PlayerOnly", GffValue::UInt8(1));
1204 root.push_field("Faction", GffValue::UInt32(42));
1205
1206 root.push_field("DifficultyIndex", GffValue::Int32(5));
1208 root.push_field("Difficulty", GffValue::Int32(4));
1209
1210 root.push_field("XPosition", GffValue::Single(10.5));
1212 root.push_field("YPosition", GffValue::Single(20.25));
1213 root.push_field("ZPosition", GffValue::Single(-3.0));
1214
1215 root.push_field("OnEntered", GffValue::resref_lit("s_enter"));
1217 root.push_field("OnExit", GffValue::resref_lit("s_exit"));
1218 root.push_field("OnHeartbeat", GffValue::resref_lit("s_hb"));
1219 root.push_field("OnExhausted", GffValue::resref_lit("s_exhaust"));
1220 root.push_field("OnUserDefined", GffValue::resref_lit("s_ud"));
1221
1222 root.push_field("NumberSpawned", GffValue::Int32(12));
1224 root.push_field("HeartbeatDay", GffValue::UInt32(100));
1225 root.push_field("HeartbeatTime", GffValue::UInt32(200));
1226 root.push_field("LastSpawnDay", GffValue::UInt32(101));
1227 root.push_field("LastSpawnTime", GffValue::UInt32(201));
1228 root.push_field("LastEntered", GffValue::UInt32(555));
1229 root.push_field("LastLeft", GffValue::UInt32(444));
1230 root.push_field("Started", GffValue::UInt8(1));
1231 root.push_field("Exhausted", GffValue::UInt8(0));
1232 root.push_field("CurrentSpawns", GffValue::Int32(3));
1233 root.push_field("CustomScriptId", GffValue::Int32(77));
1234
1235 root.push_field("AreaListMaxSize", GffValue::Int32(10));
1237 root.push_field("SpawnPoolActive", GffValue::Single(1.5));
1238 root.push_field("AreaPoints", GffValue::Single(2.75));
1239
1240 let mut c1 = GffStruct::new(0);
1242 c1.push_field("ResRef", GffValue::resref_lit("cr_sith01"));
1243 c1.push_field("CR", GffValue::Single(4.0));
1244 c1.push_field("SingleSpawn", GffValue::UInt8(1));
1245 root.push_field("CreatureList", GffValue::List(vec![c1]));
1246
1247 let mut g1 = GffStruct::new(0);
1249 g1.push_field("X", GffValue::Single(1.0));
1250 g1.push_field("Y", GffValue::Single(2.0));
1251 g1.push_field("Z", GffValue::Single(3.0));
1252 let mut g2 = GffStruct::new(0);
1253 g2.push_field("X", GffValue::Single(4.0));
1254 g2.push_field("Y", GffValue::Single(5.0));
1255 g2.push_field("Z", GffValue::Single(6.0));
1256 root.push_field("Geometry", GffValue::List(vec![g1, g2]));
1257
1258 let mut sp = GffStruct::new(0);
1260 sp.push_field("X", GffValue::Single(11.0));
1261 sp.push_field("Y", GffValue::Single(22.0));
1262 sp.push_field("Z", GffValue::Single(33.0));
1263 sp.push_field("Orientation", GffValue::Single(1.57));
1264 root.push_field("SpawnPointList", GffValue::List(vec![sp]));
1265
1266 let mut ae = GffStruct::new(0);
1268 ae.push_field("AreaObject", GffValue::UInt32(9001));
1269 root.push_field("AreaList", GffValue::List(vec![ae]));
1270
1271 let mut se = GffStruct::new(0);
1273 se.push_field("SpawnResRef", GffValue::resref_lit("sp_ref01"));
1274 se.push_field("SpawnCR", GffValue::Single(2.5));
1275 root.push_field("SpawnList", GffValue::List(vec![se]));
1276
1277 Gff::new(*b"UTE ", root)
1278 }
1279
1280 #[test]
1281 fn reads_core_ute_fields() {
1282 let gff = make_test_ute_gff();
1283 let ute = Ute::from_gff(&gff).expect("must parse");
1284
1285 assert_eq!(ute.tag, "TestEncounter");
1286 assert_eq!(ute.template_resref, "enc_test001");
1287 assert_eq!(ute.name.string_ref.raw(), 12345);
1288 assert_eq!(ute.comment, "test comment");
1289 assert_eq!(ute.palette_id, 3);
1290 assert!(ute.active);
1291 assert!(ute.reset);
1292 assert_eq!(ute.reset_time, 120);
1293 assert_eq!(ute.respawns, -1);
1294 assert_eq!(ute.spawn_option, 1);
1295 assert_eq!(ute.max_creatures, 4);
1296 assert_eq!(ute.rec_creatures, 2);
1297 assert!(!ute.player_only);
1298 assert_eq!(ute.faction_id, 1);
1299 assert_eq!(ute.difficulty_index, 3);
1300 assert_eq!(ute.difficulty, 2);
1301 assert_eq!(ute.on_entered, "k_enc_enter");
1302 assert_eq!(ute.on_exit, "k_enc_exit");
1303 assert_eq!(ute.on_heartbeat, "");
1304 assert_eq!(ute.on_exhausted, "k_enc_exhaust");
1305 assert_eq!(ute.on_user_defined, "");
1306 }
1307
1308 #[test]
1309 fn reads_creature_list() {
1310 let gff = make_test_ute_gff();
1311 let ute = Ute::from_gff(&gff).expect("must parse");
1312
1313 assert_eq!(ute.creatures.len(), 2);
1314 assert_eq!(ute.creatures[0].resref, "k_def_yourpaty");
1315 assert_eq!(ute.creatures[0].cr, 3.0);
1316 assert!(!ute.creatures[0].single_spawn);
1317 assert_eq!(ute.creatures[1].resref, "k_def_darkjedi");
1318 assert_eq!(ute.creatures[1].cr, 5.5);
1319 assert!(ute.creatures[1].single_spawn);
1320 }
1321
1322 #[test]
1323 fn all_fields_survive_typed_roundtrip() {
1324 let gff = make_full_ute_gff();
1325 let ute = Ute::from_gff(&gff).expect("must parse full GFF");
1326
1327 assert_eq!(ute.tag, "FullEncounter");
1329 assert_eq!(ute.template_resref, "enc_full001");
1330 assert_eq!(ute.name.string_ref.raw(), 99999);
1331 assert_eq!(ute.comment, "full test");
1332 assert_eq!(ute.palette_id, 7);
1333 assert!(ute.active);
1334 assert!(!ute.reset);
1335 assert_eq!(ute.reset_time, 60);
1336 assert_eq!(ute.respawns, 3);
1337 assert_eq!(ute.spawn_option, 2);
1338 assert_eq!(ute.max_creatures, 6);
1339 assert_eq!(ute.rec_creatures, 3);
1340 assert!(ute.player_only);
1341 assert_eq!(ute.faction_id, 42);
1342 assert_eq!(ute.difficulty_index, 5);
1343 assert_eq!(ute.difficulty, 4);
1344 assert_eq!(ute.x_position, 10.5);
1345 assert_eq!(ute.y_position, 20.25);
1346 assert_eq!(ute.z_position, -3.0);
1347 assert_eq!(ute.on_entered, "s_enter");
1348 assert_eq!(ute.on_exit, "s_exit");
1349 assert_eq!(ute.on_heartbeat, "s_hb");
1350 assert_eq!(ute.on_exhausted, "s_exhaust");
1351 assert_eq!(ute.on_user_defined, "s_ud");
1352 assert_eq!(ute.number_spawned, 12);
1353 assert_eq!(ute.heartbeat_day, 100);
1354 assert_eq!(ute.heartbeat_time, 200);
1355 assert_eq!(ute.last_spawn_day, 101);
1356 assert_eq!(ute.last_spawn_time, 201);
1357 assert_eq!(ute.last_entered, 555);
1358 assert_eq!(ute.last_left, 444);
1359 assert!(ute.started);
1360 assert!(!ute.exhausted);
1361 assert_eq!(ute.current_spawns, 3);
1362 assert_eq!(ute.custom_script_id, 77);
1363 assert_eq!(ute.area_list_max_size, 10);
1364 assert_eq!(ute.spawn_pool_active, 1.5);
1365 assert_eq!(ute.area_points, 2.75);
1366
1367 assert_eq!(ute.creatures.len(), 1);
1369 assert_eq!(ute.creatures[0].resref, "cr_sith01");
1370 assert_eq!(ute.creatures[0].cr, 4.0);
1371 assert!(ute.creatures[0].single_spawn);
1372
1373 assert_eq!(ute.geometry.len(), 2);
1374 assert_eq!(ute.geometry[0].x, 1.0);
1375 assert_eq!(ute.geometry[1].y, 5.0);
1376
1377 assert_eq!(ute.spawn_points.len(), 1);
1378 assert_eq!(ute.spawn_points[0].x, 11.0);
1379 assert_eq!(ute.spawn_points[0].orientation, 1.57);
1380
1381 assert_eq!(ute.area_list.len(), 1);
1382 assert_eq!(ute.area_list[0].area_object, 9001);
1383
1384 assert_eq!(ute.spawn_list.len(), 1);
1385 assert_eq!(ute.spawn_list[0].spawn_resref, "sp_ref01");
1386 assert_eq!(ute.spawn_list[0].spawn_cr, 2.5);
1387
1388 let bytes = write_ute_to_vec(&ute).expect("write succeeds");
1390 let reparsed = read_ute_from_bytes(&bytes).expect("reparse succeeds");
1391 assert_eq!(ute, reparsed);
1392 }
1393
1394 #[test]
1395 fn typed_edits_roundtrip_through_gff_writer() {
1396 let gff = make_test_ute_gff();
1397 let mut ute = Ute::from_gff(&gff).expect("must parse");
1398 ute.tag = "EditedEncounter".into();
1399 ute.max_creatures = 8;
1400 ute.on_entered = ResRef::new("rust_on_enter").expect("valid test resref");
1401 ute.creatures[0].resref = ResRef::new("k_new_creature").expect("valid test resref");
1402
1403 let bytes = write_ute_to_vec(&ute).expect("write succeeds");
1404 let reparsed = read_ute_from_bytes(&bytes).expect("reparse succeeds");
1405
1406 assert_eq!(reparsed.tag, "EditedEncounter");
1407 assert_eq!(reparsed.max_creatures, 8);
1408 assert_eq!(reparsed.on_entered, "rust_on_enter");
1409 assert_eq!(reparsed.creatures[0].resref, "k_new_creature");
1410 }
1411
1412 #[test]
1413 fn read_ute_from_reader_matches_bytes_path() {
1414 let gff = make_test_ute_gff();
1415 let bytes = {
1416 let mut c = Cursor::new(Vec::new());
1417 write_gff(&mut c, &gff).expect("test fixture must be valid");
1418 c.into_inner()
1419 };
1420
1421 let mut cursor = Cursor::new(&bytes);
1422 let via_reader = read_ute(&mut cursor).expect("reader parse succeeds");
1423 let via_bytes = read_ute_from_bytes(&bytes).expect("bytes parse succeeds");
1424
1425 assert_eq!(via_reader, via_bytes);
1426 }
1427
1428 #[test]
1429 fn rejects_non_ute_file_type() {
1430 let mut gff = make_test_ute_gff();
1431 gff.file_type = *b"UTT ";
1432
1433 let err = Ute::from_gff(&gff).expect_err("UTT must be rejected as UTE input");
1434 assert!(matches!(
1435 err,
1436 UteError::UnsupportedFileType(file_type) if file_type == *b"UTT "
1437 ));
1438 }
1439
1440 #[test]
1441 fn type_mismatch_on_localized_name_is_error() {
1442 let mut gff = make_test_ute_gff();
1443 gff.root
1444 .fields
1445 .retain(|field| field.label != "LocalizedName");
1446 gff.root.push_field("LocalizedName", GffValue::UInt32(5));
1447
1448 let err = Ute::from_gff(&gff).expect_err("type mismatch must be rejected");
1449 assert!(matches!(
1450 err,
1451 UteError::TypeMismatch {
1452 field: "LocalizedName",
1453 expected: "LocalizedString",
1454 }
1455 ));
1456 }
1457
1458 #[test]
1459 fn write_ute_matches_direct_gff_writer() {
1460 let gff = make_test_ute_gff();
1461 let ute = Ute::from_gff(&gff).expect("must parse");
1462
1463 let via_typed = write_ute_to_vec(&ute).expect("typed write succeeds");
1464
1465 let mut direct = Cursor::new(Vec::new());
1466 write_gff(&mut direct, &ute.to_gff()).expect("direct write succeeds");
1467
1468 assert_eq!(via_typed, direct.into_inner());
1469 }
1470
1471 #[test]
1472 fn empty_creature_list_ok() {
1473 let mut gff = make_test_ute_gff();
1474 gff.root.fields.retain(|f| f.label != "CreatureList");
1475
1476 let ute = Ute::from_gff(&gff).expect("must parse");
1477 assert!(ute.creatures.is_empty());
1478 }
1479
1480 #[test]
1481 fn schema_field_count() {
1482 assert_eq!(Ute::schema().len(), 43);
1483 }
1484
1485 #[test]
1486 fn schema_no_duplicate_labels() {
1487 let schema = Ute::schema();
1488 let mut labels: Vec<&str> = schema.iter().map(|f| f.label).collect();
1489 labels.sort();
1490 let before = labels.len();
1491 labels.dedup();
1492 assert_eq!(before, labels.len(), "duplicate labels in UTE schema");
1493 }
1494
1495 #[test]
1496 fn schema_lists_have_children() {
1497 let schema = Ute::schema();
1498 let geometry = schema
1499 .iter()
1500 .find(|f| f.label == "Geometry")
1501 .expect("test fixture must be valid");
1502 assert_eq!(
1503 geometry.children.expect("test fixture must be valid").len(),
1504 3
1505 );
1506 let creatures = schema
1507 .iter()
1508 .find(|f| f.label == "CreatureList")
1509 .expect("test fixture must be valid");
1510 assert_eq!(
1511 creatures
1512 .children
1513 .expect("test fixture must be valid")
1514 .len(),
1515 3
1516 );
1517 let spawn_points = schema
1518 .iter()
1519 .find(|f| f.label == "SpawnPointList")
1520 .expect("test fixture must be valid");
1521 assert_eq!(
1522 spawn_points
1523 .children
1524 .expect("test fixture must be valid")
1525 .len(),
1526 4
1527 );
1528 let area_list = schema
1529 .iter()
1530 .find(|f| f.label == "AreaList")
1531 .expect("test fixture must be valid");
1532 assert_eq!(
1533 area_list
1534 .children
1535 .expect("test fixture must be valid")
1536 .len(),
1537 1
1538 );
1539 let spawn_list = schema
1540 .iter()
1541 .find(|f| f.label == "SpawnList")
1542 .expect("test fixture must be valid");
1543 assert_eq!(
1544 spawn_list
1545 .children
1546 .expect("test fixture must be valid")
1547 .len(),
1548 2
1549 );
1550 }
1551}