1use std::io::{Cursor, Read, Write};
39
40use rakata_core::ResRef;
41use rakata_formats::gff::upsert_field;
42use rakata_formats::gff_label;
43use rakata_formats::schema::FromGff;
44use rakata_formats::GENERIC_FILE_TYPE;
45use rakata_formats::{
46 read_gff, read_gff_from_bytes, write_gff, Gff, GffBinaryError, GffLocalizedString, GffModel,
47 GffStruct, GffValue,
48};
49use thiserror::Error;
50
51#[derive(Debug, Clone, PartialEq, GffModel)]
53pub struct Ute {
54 #[gff(TemplateResRef, unexamined)]
56 pub template_resref: ResRef,
57 #[gff(Tag, unanswered)]
59 pub tag: String,
60 #[gff(LocalizedName, constructed)]
62 pub name: GffLocalizedString,
63 #[gff(Comment, unexamined)]
65 pub comment: String,
66 #[gff(PaletteID, unexamined)]
68 pub palette_id: u8,
69 #[gff(Active, constructed = true)]
71 pub active: bool,
72 #[gff(Reset, constructed)]
74 pub reset: bool,
75 #[gff(ResetTime, constructed = 60)]
77 pub reset_time: i32,
78 #[gff(Respawns, constructed)]
80 pub respawns: i32,
81 #[gff(SpawnOption, constructed)]
83 pub spawn_option: i32,
84 #[gff(MaxCreatures, constructed = 8)]
86 pub max_creatures: i32,
87 #[gff(RecCreatures, constructed = 2)]
89 pub rec_creatures: i32,
90 #[gff(PlayerOnly, constructed)]
92 pub player_only: bool,
93 #[gff(Faction, constructed = 1)]
95 pub faction_id: u32,
96 #[gff(DifficultyIndex, unanswered)]
98 pub difficulty_index: i32,
99 #[gff(Difficulty, unanswered)]
101 pub difficulty: i32,
102 #[gff(XPosition, constructed, omit = audited_constant(133))]
104 pub x_position: f32,
105 #[gff(YPosition, constructed, omit = audited_constant(133))]
107 pub y_position: f32,
108 #[gff(ZPosition, constructed, omit = audited_constant(133))]
110 pub z_position: f32,
111 #[gff(OnEntered, constructed)]
113 pub on_entered: ResRef,
114 #[gff(OnExit, constructed)]
116 pub on_exit: ResRef,
117 #[gff(OnHeartbeat, constructed)]
119 pub on_heartbeat: ResRef,
120 #[gff(OnExhausted, constructed)]
122 pub on_exhausted: ResRef,
123 #[gff(OnUserDefined, constructed)]
125 pub on_user_defined: ResRef,
126 #[gff(NumberSpawned, constructed, omit = audited_constant(133))]
128 pub number_spawned: i32,
129 #[gff(HeartbeatDay, constructed, omit = audited_constant(133))]
131 pub heartbeat_day: u32,
132 #[gff(HeartbeatTime, constructed, omit = audited_constant(133))]
134 pub heartbeat_time: u32,
135 #[gff(LastSpawnDay, constructed, omit = audited_constant(133))]
137 pub last_spawn_day: u32,
138 #[gff(LastSpawnTime, constructed, omit = audited_constant(133))]
140 pub last_spawn_time: u32,
141 #[gff(LastEntered, constructed = 2130706432, omit = audited_constant(133))]
143 pub last_entered: u32,
144 #[gff(LastLeft, constructed = 2130706432, omit = audited_constant(133))]
146 pub last_left: u32,
147 #[gff(Started, constructed, omit = audited_constant(133))]
149 pub started: bool,
150 #[gff(Exhausted, constructed, omit = audited_constant(133))]
152 pub exhausted: bool,
153 #[gff(CurrentSpawns, constructed, omit = audited_constant(133))]
155 pub current_spawns: i32,
156 #[gff(CustomScriptId, undefined)]
158 pub custom_script_id: i32,
159 #[gff(AreaListMaxSize, constructed = 16, omit = audited_constant(133))]
161 pub area_list_max_size: i32,
162 #[gff(SpawnPoolActive, constructed, omit = audited_constant(133))]
164 pub spawn_pool_active: f32,
165 #[gff(AreaPoints, undefined)]
167 pub area_points: f32,
168 #[gff(CreatureList, not_a_constant, list = UteCreature, element_id = 0)]
170 pub creatures: Vec<UteCreature>,
171 #[gff(Geometry, manual_write, not_a_constant, list = UteGeometryVertex, element_id = 0)]
173 pub geometry: Vec<UteGeometryVertex>,
174 #[gff(SpawnPointList, not_a_constant, omit = empty(133), list = UteSpawnPoint, element_id = 0)]
176 pub spawn_points: Vec<UteSpawnPoint>,
177 #[gff(AreaList, not_a_constant, omit = empty(133), list = UteAreaEntry, element_id = 0)]
179 pub area_list: Vec<UteAreaEntry>,
180 #[gff(SpawnList, not_a_constant, omit = empty(133), list = UteSpawnEntry, element_id = 0)]
182 pub spawn_list: Vec<UteSpawnEntry>,
183}
184
185impl Ute {
186 pub fn new() -> Self {
188 Self::default()
189 }
190
191 pub fn from_gff(gff: &Gff) -> Result<Self, UteError> {
198 if gff.file_type != <Ute as FromGff>::MAGIC && gff.file_type != GENERIC_FILE_TYPE {
199 return Err(UteError::UnsupportedFileType(gff.file_type));
200 }
201
202 Ok(Self::read_declared(&gff.root))
203 }
204
205 pub fn to_gff(&self) -> Gff {
207 let mut root = GffStruct::new(-1);
208 self.write_declared(&mut root);
209
210 if !self.geometry.is_empty() {
215 upsert_field(
216 &mut root,
217 gff_label!("Geometry"),
218 GffValue::List(
219 self.geometry
220 .iter()
221 .map(|vertex| {
222 let mut element = GffStruct::new(0);
223 vertex.write_element(&mut element);
224 element
225 })
226 .collect(),
227 ),
228 );
229 }
230
231 Gff::new(*b"UTE ", root)
232 }
233}
234
235#[derive(Debug, Clone, PartialEq, GffModel)]
237pub struct UteCreature {
238 #[gff(ResRef, unexamined)]
240 pub resref: ResRef,
241 #[gff(CR, unexamined)]
243 pub cr: f32,
244 #[gff(SingleSpawn, unexamined)]
246 pub single_spawn: bool,
247}
248
249#[derive(Debug, Clone, PartialEq, GffModel)]
251pub struct UteGeometryVertex {
252 #[gff(X, unexamined)]
254 pub x: f32,
255 #[gff(Y, unexamined)]
257 pub y: f32,
258 #[gff(Z, unexamined)]
260 pub z: f32,
261}
262
263#[derive(Debug, Clone, PartialEq, GffModel)]
265pub struct UteSpawnPoint {
266 #[gff(X, unexamined)]
268 pub x: f32,
269 #[gff(Y, unexamined)]
271 pub y: f32,
272 #[gff(Z, unexamined)]
274 pub z: f32,
275 #[gff(Orientation, unexamined)]
277 pub orientation: f32,
278}
279
280#[derive(Debug, Clone, PartialEq, GffModel)]
282pub struct UteAreaEntry {
283 #[gff(AreaObject, unexamined)]
285 pub area_object: u32,
286}
287
288#[derive(Debug, Clone, PartialEq, GffModel)]
290pub struct UteSpawnEntry {
291 #[gff(SpawnResRef, unexamined)]
293 pub spawn_resref: ResRef,
294 #[gff(SpawnCR, unexamined)]
296 pub spawn_cr: f32,
297}
298
299#[derive(Debug, Error)]
301pub enum UteError {
302 #[error("unsupported UTE file type: {0:?}")]
304 UnsupportedFileType([u8; 4]),
305 #[error(transparent)]
307 Gff(#[from] GffBinaryError),
308}
309
310#[cfg_attr(
318 feature = "tracing",
319 tracing::instrument(level = "debug", skip(reader))
320)]
321pub fn read_ute<R: Read>(reader: &mut R) -> Result<Ute, UteError> {
322 let gff = read_gff(reader)?;
323 Ute::from_gff(&gff)
324}
325
326#[cfg_attr(
334 feature = "tracing",
335 tracing::instrument(level = "debug", skip(bytes), fields(bytes_len = bytes.len()))
336)]
337pub fn read_ute_from_bytes(bytes: &[u8]) -> Result<Ute, UteError> {
338 let gff = read_gff_from_bytes(bytes)?;
339 Ute::from_gff(&gff)
340}
341
342#[cfg_attr(
350 feature = "tracing",
351 tracing::instrument(level = "debug", skip(writer, ute))
352)]
353pub fn author_ute<W: Write>(writer: &mut W, ute: &Ute) -> Result<(), UteError> {
354 let gff = ute.to_gff();
355 write_gff(writer, &gff)?;
356 Ok(())
357}
358
359#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(ute)))]
366pub fn author_ute_to_vec(ute: &Ute) -> Result<Vec<u8>, UteError> {
367 let mut cursor = Cursor::new(Vec::new());
368 author_ute(&mut cursor, ute)?;
369 Ok(cursor.into_inner())
370}
371
372#[cfg(test)]
373mod tests {
374 use super::*;
375 use rakata_core::StrRef;
376 use rakata_formats::schema::{HasSchema, Shape};
377
378 #[test]
379 fn empty_geometry_is_omitted_and_a_real_boundary_is_written() {
380 let empty = Ute::default();
389 assert!(empty.geometry.is_empty());
390 assert!(empty.to_gff().root.field("Geometry").is_none());
391
392 let bounded = Ute {
393 geometry: vec![UteGeometryVertex {
394 x: 1.0,
395 y: 2.0,
396 z: 3.0,
397 }],
398 ..Ute::default()
399 };
400 let written = bounded.to_gff();
401 let Some(GffValue::List(vertices)) = written.root.field("Geometry") else {
402 panic!("a non-empty boundary must be written");
403 };
404 assert_eq!(vertices.len(), 1);
405 }
406
407 fn make_test_ute_gff() -> Gff {
409 let mut root = GffStruct::new(-1);
410 root.push_field(gff_label!("Tag"), GffValue::String("TestEncounter".into()));
411 root.push_field(
412 gff_label!("TemplateResRef"),
413 GffValue::resref_lit("enc_test001"),
414 );
415 root.push_field(
416 gff_label!("LocalizedName"),
417 GffValue::LocalizedString(GffLocalizedString::new(StrRef::from_raw(12345))),
418 );
419 root.push_field(
420 gff_label!("Comment"),
421 GffValue::String("test comment".into()),
422 );
423 root.push_field(gff_label!("PaletteID"), GffValue::UInt8(3));
424 root.push_field(gff_label!("Active"), GffValue::UInt8(1));
425 root.push_field(gff_label!("Reset"), GffValue::UInt8(1));
426 root.push_field(gff_label!("ResetTime"), GffValue::Int32(120));
427 root.push_field(gff_label!("Respawns"), GffValue::Int32(-1));
428 root.push_field(gff_label!("SpawnOption"), GffValue::Int32(1));
429 root.push_field(gff_label!("MaxCreatures"), GffValue::Int32(4));
430 root.push_field(gff_label!("RecCreatures"), GffValue::Int32(2));
431 root.push_field(gff_label!("PlayerOnly"), GffValue::UInt8(0));
432 root.push_field(gff_label!("Faction"), GffValue::UInt32(1));
433 root.push_field(gff_label!("DifficultyIndex"), GffValue::Int32(3));
434 root.push_field(gff_label!("Difficulty"), GffValue::Int32(2));
435 root.push_field(gff_label!("OnEntered"), GffValue::resref_lit("k_enc_enter"));
436 root.push_field(gff_label!("OnExit"), GffValue::resref_lit("k_enc_exit"));
437 root.push_field(gff_label!("OnHeartbeat"), GffValue::resref_lit(""));
438 root.push_field(
439 gff_label!("OnExhausted"),
440 GffValue::resref_lit("k_enc_exhaust"),
441 );
442 root.push_field(gff_label!("OnUserDefined"), GffValue::resref_lit(""));
443
444 let mut c1 = GffStruct::new(0);
446 c1.push_field(gff_label!("ResRef"), GffValue::resref_lit("k_def_yourpaty"));
447 c1.push_field(gff_label!("CR"), GffValue::Single(3.0));
448 c1.push_field(gff_label!("SingleSpawn"), GffValue::UInt8(0));
449
450 let mut c2 = GffStruct::new(0);
451 c2.push_field(gff_label!("ResRef"), GffValue::resref_lit("k_def_darkjedi"));
452 c2.push_field(gff_label!("CR"), GffValue::Single(5.5));
453 c2.push_field(gff_label!("SingleSpawn"), GffValue::UInt8(1));
454
455 root.push_field(gff_label!("CreatureList"), GffValue::List(vec![c1, c2]));
456
457 Gff::new(*b"UTE ", root)
458 }
459
460 fn make_full_ute_gff() -> Gff {
462 let mut root = GffStruct::new(-1);
463
464 root.push_field(gff_label!("Tag"), GffValue::String("FullEncounter".into()));
466 root.push_field(
467 gff_label!("TemplateResRef"),
468 GffValue::resref_lit("enc_full001"),
469 );
470 root.push_field(
471 gff_label!("LocalizedName"),
472 GffValue::LocalizedString(GffLocalizedString::new(StrRef::from_raw(99999))),
473 );
474 root.push_field(gff_label!("Comment"), GffValue::String("full test".into()));
475 root.push_field(gff_label!("PaletteID"), GffValue::UInt8(7));
476
477 root.push_field(gff_label!("Active"), GffValue::UInt8(1));
479 root.push_field(gff_label!("Reset"), GffValue::UInt8(0));
480 root.push_field(gff_label!("ResetTime"), GffValue::Int32(60));
481 root.push_field(gff_label!("Respawns"), GffValue::Int32(3));
482 root.push_field(gff_label!("SpawnOption"), GffValue::Int32(2));
483 root.push_field(gff_label!("MaxCreatures"), GffValue::Int32(6));
484 root.push_field(gff_label!("RecCreatures"), GffValue::Int32(3));
485 root.push_field(gff_label!("PlayerOnly"), GffValue::UInt8(1));
486 root.push_field(gff_label!("Faction"), GffValue::UInt32(42));
487
488 root.push_field(gff_label!("DifficultyIndex"), GffValue::Int32(5));
490 root.push_field(gff_label!("Difficulty"), GffValue::Int32(4));
491
492 root.push_field(gff_label!("XPosition"), GffValue::Single(10.5));
494 root.push_field(gff_label!("YPosition"), GffValue::Single(20.25));
495 root.push_field(gff_label!("ZPosition"), GffValue::Single(-3.0));
496
497 root.push_field(gff_label!("OnEntered"), GffValue::resref_lit("s_enter"));
499 root.push_field(gff_label!("OnExit"), GffValue::resref_lit("s_exit"));
500 root.push_field(gff_label!("OnHeartbeat"), GffValue::resref_lit("s_hb"));
501 root.push_field(gff_label!("OnExhausted"), GffValue::resref_lit("s_exhaust"));
502 root.push_field(gff_label!("OnUserDefined"), GffValue::resref_lit("s_ud"));
503
504 root.push_field(gff_label!("NumberSpawned"), GffValue::Int32(12));
506 root.push_field(gff_label!("HeartbeatDay"), GffValue::UInt32(100));
507 root.push_field(gff_label!("HeartbeatTime"), GffValue::UInt32(200));
508 root.push_field(gff_label!("LastSpawnDay"), GffValue::UInt32(101));
509 root.push_field(gff_label!("LastSpawnTime"), GffValue::UInt32(201));
510 root.push_field(gff_label!("LastEntered"), GffValue::UInt32(555));
511 root.push_field(gff_label!("LastLeft"), GffValue::UInt32(444));
512 root.push_field(gff_label!("Started"), GffValue::UInt8(1));
513 root.push_field(gff_label!("Exhausted"), GffValue::UInt8(0));
514 root.push_field(gff_label!("CurrentSpawns"), GffValue::Int32(3));
515 root.push_field(gff_label!("CustomScriptId"), GffValue::Int32(77));
516
517 root.push_field(gff_label!("AreaListMaxSize"), GffValue::Int32(10));
519 root.push_field(gff_label!("SpawnPoolActive"), GffValue::Single(1.5));
520 root.push_field(gff_label!("AreaPoints"), GffValue::Single(2.75));
521
522 let mut c1 = GffStruct::new(0);
524 c1.push_field(gff_label!("ResRef"), GffValue::resref_lit("cr_sith01"));
525 c1.push_field(gff_label!("CR"), GffValue::Single(4.0));
526 c1.push_field(gff_label!("SingleSpawn"), GffValue::UInt8(1));
527 root.push_field(gff_label!("CreatureList"), GffValue::List(vec![c1]));
528
529 let mut g1 = GffStruct::new(0);
531 g1.push_field(gff_label!("X"), GffValue::Single(1.0));
532 g1.push_field(gff_label!("Y"), GffValue::Single(2.0));
533 g1.push_field(gff_label!("Z"), GffValue::Single(3.0));
534 let mut g2 = GffStruct::new(0);
535 g2.push_field(gff_label!("X"), GffValue::Single(4.0));
536 g2.push_field(gff_label!("Y"), GffValue::Single(5.0));
537 g2.push_field(gff_label!("Z"), GffValue::Single(6.0));
538 root.push_field(gff_label!("Geometry"), GffValue::List(vec![g1, g2]));
539
540 let mut sp = GffStruct::new(0);
542 sp.push_field(gff_label!("X"), GffValue::Single(11.0));
543 sp.push_field(gff_label!("Y"), GffValue::Single(22.0));
544 sp.push_field(gff_label!("Z"), GffValue::Single(33.0));
545 sp.push_field(gff_label!("Orientation"), GffValue::Single(1.57));
546 root.push_field(gff_label!("SpawnPointList"), GffValue::List(vec![sp]));
547
548 let mut ae = GffStruct::new(0);
550 ae.push_field(gff_label!("AreaObject"), GffValue::UInt32(9001));
551 root.push_field(gff_label!("AreaList"), GffValue::List(vec![ae]));
552
553 let mut se = GffStruct::new(0);
555 se.push_field(gff_label!("SpawnResRef"), GffValue::resref_lit("sp_ref01"));
556 se.push_field(gff_label!("SpawnCR"), GffValue::Single(2.5));
557 root.push_field(gff_label!("SpawnList"), GffValue::List(vec![se]));
558
559 Gff::new(*b"UTE ", root)
560 }
561
562 #[test]
563 fn reads_core_ute_fields() {
564 let gff = make_test_ute_gff();
565 let ute = Ute::from_gff(&gff).expect("must parse");
566
567 assert_eq!(ute.tag, "TestEncounter");
568 assert_eq!(ute.template_resref, "enc_test001");
569 assert_eq!(ute.name.string_ref.raw(), 12345);
570 assert_eq!(ute.comment, "test comment");
571 assert_eq!(ute.palette_id, 3);
572 assert!(ute.active);
573 assert!(ute.reset);
574 assert_eq!(ute.reset_time, 120);
575 assert_eq!(ute.respawns, -1);
576 assert_eq!(ute.spawn_option, 1);
577 assert_eq!(ute.max_creatures, 4);
578 assert_eq!(ute.rec_creatures, 2);
579 assert!(!ute.player_only);
580 assert_eq!(ute.faction_id, 1);
581 assert_eq!(ute.difficulty_index, 3);
582 assert_eq!(ute.difficulty, 2);
583 assert_eq!(ute.on_entered, "k_enc_enter");
584 assert_eq!(ute.on_exit, "k_enc_exit");
585 assert_eq!(ute.on_heartbeat, "");
586 assert_eq!(ute.on_exhausted, "k_enc_exhaust");
587 assert_eq!(ute.on_user_defined, "");
588 }
589
590 #[test]
591 fn reads_creature_list() {
592 let gff = make_test_ute_gff();
593 let ute = Ute::from_gff(&gff).expect("must parse");
594
595 assert_eq!(ute.creatures.len(), 2);
596 assert_eq!(ute.creatures[0].resref, "k_def_yourpaty");
597 assert_eq!(ute.creatures[0].cr, 3.0);
598 assert!(!ute.creatures[0].single_spawn);
599 assert_eq!(ute.creatures[1].resref, "k_def_darkjedi");
600 assert_eq!(ute.creatures[1].cr, 5.5);
601 assert!(ute.creatures[1].single_spawn);
602 }
603
604 #[test]
605 fn all_fields_survive_typed_roundtrip() {
606 let gff = make_full_ute_gff();
607 let ute = Ute::from_gff(&gff).expect("must parse full GFF");
608
609 assert_eq!(ute.tag, "FullEncounter");
611 assert_eq!(ute.template_resref, "enc_full001");
612 assert_eq!(ute.name.string_ref.raw(), 99999);
613 assert_eq!(ute.comment, "full test");
614 assert_eq!(ute.palette_id, 7);
615 assert!(ute.active);
616 assert!(!ute.reset);
617 assert_eq!(ute.reset_time, 60);
618 assert_eq!(ute.respawns, 3);
619 assert_eq!(ute.spawn_option, 2);
620 assert_eq!(ute.max_creatures, 6);
621 assert_eq!(ute.rec_creatures, 3);
622 assert!(ute.player_only);
623 assert_eq!(ute.faction_id, 42);
624 assert_eq!(ute.difficulty_index, 5);
625 assert_eq!(ute.difficulty, 4);
626 assert_eq!(ute.x_position, 10.5);
627 assert_eq!(ute.y_position, 20.25);
628 assert_eq!(ute.z_position, -3.0);
629 assert_eq!(ute.on_entered, "s_enter");
630 assert_eq!(ute.on_exit, "s_exit");
631 assert_eq!(ute.on_heartbeat, "s_hb");
632 assert_eq!(ute.on_exhausted, "s_exhaust");
633 assert_eq!(ute.on_user_defined, "s_ud");
634 assert_eq!(ute.number_spawned, 12);
635 assert_eq!(ute.heartbeat_day, 100);
636 assert_eq!(ute.heartbeat_time, 200);
637 assert_eq!(ute.last_spawn_day, 101);
638 assert_eq!(ute.last_spawn_time, 201);
639 assert_eq!(ute.last_entered, 555);
640 assert_eq!(ute.last_left, 444);
641 assert!(ute.started);
642 assert!(!ute.exhausted);
643 assert_eq!(ute.current_spawns, 3);
644 assert_eq!(ute.custom_script_id, 77);
645 assert_eq!(ute.area_list_max_size, 10);
646 assert_eq!(ute.spawn_pool_active, 1.5);
647 assert_eq!(ute.area_points, 2.75);
648
649 assert_eq!(ute.creatures.len(), 1);
651 assert_eq!(ute.creatures[0].resref, "cr_sith01");
652 assert_eq!(ute.creatures[0].cr, 4.0);
653 assert!(ute.creatures[0].single_spawn);
654
655 assert_eq!(ute.geometry.len(), 2);
656 assert_eq!(ute.geometry[0].x, 1.0);
657 assert_eq!(ute.geometry[1].y, 5.0);
658
659 assert_eq!(ute.spawn_points.len(), 1);
660 assert_eq!(ute.spawn_points[0].x, 11.0);
661 assert_eq!(ute.spawn_points[0].orientation, 1.57);
662
663 assert_eq!(ute.area_list.len(), 1);
664 assert_eq!(ute.area_list[0].area_object, 9001);
665
666 assert_eq!(ute.spawn_list.len(), 1);
667 assert_eq!(ute.spawn_list[0].spawn_resref, "sp_ref01");
668 assert_eq!(ute.spawn_list[0].spawn_cr, 2.5);
669
670 let bytes = author_ute_to_vec(&ute).expect("write succeeds");
672 let reparsed = read_ute_from_bytes(&bytes).expect("reparse succeeds");
673 assert_eq!(ute, reparsed);
674 }
675
676 #[test]
677 fn typed_edits_roundtrip_through_gff_writer() {
678 let gff = make_test_ute_gff();
679 let mut ute = Ute::from_gff(&gff).expect("must parse");
680 ute.tag = "EditedEncounter".into();
681 ute.max_creatures = 8;
682 ute.on_entered = ResRef::new("rust_on_enter").expect("valid test resref");
683 ute.creatures[0].resref = ResRef::new("k_new_creature").expect("valid test resref");
684
685 let bytes = author_ute_to_vec(&ute).expect("write succeeds");
686 let reparsed = read_ute_from_bytes(&bytes).expect("reparse succeeds");
687
688 assert_eq!(reparsed.tag, "EditedEncounter");
689 assert_eq!(reparsed.max_creatures, 8);
690 assert_eq!(reparsed.on_entered, "rust_on_enter");
691 assert_eq!(reparsed.creatures[0].resref, "k_new_creature");
692 }
693
694 #[test]
695 fn read_ute_from_reader_matches_bytes_path() {
696 let gff = make_test_ute_gff();
697 let bytes = {
698 let mut c = Cursor::new(Vec::new());
699 write_gff(&mut c, &gff).expect("test fixture must be valid");
700 c.into_inner()
701 };
702
703 let mut cursor = Cursor::new(&bytes);
704 let via_reader = read_ute(&mut cursor).expect("reader parse succeeds");
705 let via_bytes = read_ute_from_bytes(&bytes).expect("bytes parse succeeds");
706
707 assert_eq!(via_reader, via_bytes);
708 }
709
710 #[test]
711 fn rejects_non_ute_file_type() {
712 let mut gff = make_test_ute_gff();
713 gff.file_type = *b"UTT ";
714
715 let err = Ute::from_gff(&gff).expect_err("UTT must be rejected as UTE input");
716 assert!(matches!(
717 err,
718 UteError::UnsupportedFileType(file_type) if file_type == *b"UTT "
719 ));
720 }
721
722 #[test]
723 fn a_mistyped_localized_name_reads_as_absent() {
724 let mut gff = make_test_ute_gff();
725 gff.root
726 .fields
727 .retain(|field| field.label != "LocalizedName");
728 gff.root
729 .push_field(gff_label!("LocalizedName"), GffValue::UInt32(5));
730
731 let ute = Ute::from_gff(&gff).expect("a mistyped field is not a read failure");
732
733 assert_eq!(ute.name, GffLocalizedString::default());
734 }
735
736 #[test]
737 fn write_ute_matches_direct_gff_writer() {
738 let gff = make_test_ute_gff();
739 let ute = Ute::from_gff(&gff).expect("must parse");
740
741 let via_typed = author_ute_to_vec(&ute).expect("typed write succeeds");
742
743 let mut direct = Cursor::new(Vec::new());
744 write_gff(&mut direct, &ute.to_gff()).expect("direct write succeeds");
745
746 assert_eq!(via_typed, direct.into_inner());
747 }
748
749 #[test]
750 fn empty_creature_list_ok() {
751 let mut gff = make_test_ute_gff();
752 gff.root.fields.retain(|f| f.label != "CreatureList");
753
754 let ute = Ute::from_gff(&gff).expect("must parse");
755 assert!(ute.creatures.is_empty());
756 }
757
758 #[test]
759 fn schema_field_count() {
760 assert_eq!(Ute::schema().len(), 43);
761 }
762
763 #[test]
764 fn schema_no_duplicate_labels() {
765 let mut labels: Vec<&str> = Ute::schema().iter().map(|f| f.label.as_str()).collect();
766 labels.sort_unstable();
767 let before = labels.len();
768 labels.dedup();
769 assert_eq!(before, labels.len(), "duplicate labels in UTE schema");
770 }
771
772 #[test]
773 fn schema_lists_carry_their_elements() {
774 let element_count = |label: &str| {
775 let field = Ute::schema()
776 .iter()
777 .find(|f| f.label.as_str() == label)
778 .expect("declared");
779 match field.shape {
780 Shape::List { element, .. } => element.iter().map(|p| p.len()).sum::<usize>(),
783 other => panic!("{label} is a list, got {other:?}"),
784 }
785 };
786 assert_eq!(element_count("CreatureList"), 3);
787 assert_eq!(element_count("Geometry"), 3);
788 assert_eq!(element_count("SpawnPointList"), 4);
789 assert_eq!(element_count("AreaList"), 1);
790 assert_eq!(element_count("SpawnList"), 2);
791 }
792}