1pub mod minigame;
44
45use std::io::{Cursor, Read, Write};
46
47use rakata_core::ResRef;
48use rakata_formats::gff::upsert_field;
49use rakata_formats::gff_label;
50use rakata_formats::schema::FromGff;
51use rakata_formats::schema::GffScalar;
52use rakata_formats::GENERIC_FILE_TYPE;
53use rakata_formats::{
54 read_gff, read_gff_from_bytes, write_gff, Gff, GffBinaryError, GffLocalizedString, GffModel,
55 GffStruct, GffValue,
56};
57use thiserror::Error;
58
59pub use minigame::{
60 AreMiniGame, AreMiniGameBullet, AreMiniGameEnemy, AreMiniGameGunBank, AreMiniGameModel,
61 AreMiniGameMouse, AreMiniGameObjectScripts, AreMiniGameObstacle, AreMiniGamePlayer,
62 AreMiniGameSounds, AreMiniGameTargeting, AreMiniGameVehicle, AreMiniGameVehicleScripts,
63 BANK_ID_NONE,
64};
65
66#[derive(Debug, Clone, PartialEq, GffModel)]
68pub struct Are {
69 #[gff(ID, stamped)]
71 pub unused_id: i32,
72 #[gff(Creator_ID, stamped)]
74 pub creator_id: i32,
75 #[gff(Version, stamped)]
77 pub version: u32,
78 #[gff(Comments, stamped)]
80 pub comment: String,
81 #[gff(Name, stamped)]
83 pub name: GffLocalizedString,
84 #[gff(Tag, stamped)]
86 pub tag: String,
87 #[gff(OnHeartbeat, unexamined)]
89 pub on_heartbeat: ResRef,
90 #[gff(OnUserDefined, unexamined)]
92 pub on_user_defined: ResRef,
93 #[gff(OnEnter, unexamined)]
95 pub on_enter: ResRef,
96 #[gff(OnExit, unexamined)]
98 pub on_exit: ResRef,
99 #[gff(Flags, stamped)]
101 pub flags: u32,
102 #[gff(CameraStyle, stamped)]
104 pub camera_style: i32,
105 #[gff(DefaultEnvMap, stamped)]
107 pub default_envmap: ResRef,
108 #[gff(Unescapable, constructed)]
110 pub unescapable: bool,
111 #[gff(RestrictMode, constructed, omit = audited_constant(1057))]
113 pub restrict_mode: u8,
114 #[gff(ChanceRain, stamped, range_int = (0, 100))]
116 pub chance_rain: i32,
117 #[gff(ChanceSnow, stamped, range_int = (0, 100))]
119 pub chance_snow: i32,
120 #[gff(ChanceLightning, stamped, range_int = (0, 100))]
122 pub chance_lightning: i32,
123 #[gff(WindPower, stamped, range_int = (0, 2))]
125 pub wind_power: i32,
126 #[gff(ChanceFog, stamped, omit = audited_constant(1057))]
128 pub chance_fog: i32,
129 #[gff(MoonAmbientColor, stamped)]
131 pub moon_ambient_color: u32,
132 #[gff(MoonDiffuseColor, stamped)]
134 pub moon_diffuse_color: u32,
135 #[gff(MoonFogColor, stamped)]
137 pub moon_fog_color: u32,
138 #[gff(SunAmbientColor, stamped)]
140 pub sun_ambient_color: u32,
141 #[gff(SunDiffuseColor, stamped)]
143 pub sun_diffuse_color: u32,
144 #[gff(SunFogColor, stamped)]
146 pub sun_fog_color: u32,
147 #[gff(DynAmbientColor, stamped)]
149 pub dynamic_ambient_color: u32,
150 #[gff(MoonFogNear, stamped = 10000.0)]
156 pub moon_fog_near: f32,
157 #[gff(MoonFogFar, stamped = 10000.0)]
159 pub moon_fog_far: f32,
160 #[gff(SunFogNear, stamped = 10000.0)]
162 pub fog_near: f32,
163 #[gff(SunFogFar, stamped = 10000.0)]
165 pub fog_far: f32,
166 #[gff(MoonFogOn, stamped)]
168 pub moon_fog_enabled: bool,
169 #[gff(SunFogOn, stamped)]
171 pub fog_enabled: bool,
172 #[gff(MoonShadows, stamped)]
174 pub moon_shadows: bool,
175 #[gff(SunShadows, stamped)]
177 pub shadows: bool,
178 #[gff(DayNightCycle, stamped)]
180 pub day_night_cycle: u8,
181 #[gff(IsNight, stamped)]
183 pub is_night: bool,
184 #[gff(ShadowOpacity, stamped)]
186 pub shadow_opacity: u8,
187 #[gff(LightingScheme, stamped)]
189 pub lighting_scheme: u8,
190 #[gff(NoRest, constructed)]
192 pub no_rest: bool,
193 #[gff(ModSpotCheck, stamped)]
195 pub mod_spot_check: i32,
196 #[gff(ModListenCheck, stamped)]
198 pub mod_listen_check: i32,
199 #[gff(Grass_Diffuse, stamped)]
201 pub grass_diffuse_color: u32,
202 #[gff(Grass_Ambient, stamped)]
204 pub grass_ambient_color: u32,
205 #[gff(Grass_Density, stamped)]
207 pub grass_density: f32,
208 #[gff(Grass_QuadSize, stamped)]
210 pub grass_size: f32,
211 #[gff(Grass_TexName, unexamined)]
213 pub grass_texture: ResRef,
214 #[gff(Grass_Prob_LL, stamped)]
216 pub grass_prob_ll: f32,
217 #[gff(Grass_Prob_LR, stamped)]
219 pub grass_prob_lr: f32,
220 #[gff(Grass_Prob_UL, stamped)]
222 pub grass_prob_ul: f32,
223 #[gff(Grass_Prob_UR, stamped)]
225 pub grass_prob_ur: f32,
226 #[gff(AlphaTest, stamped = 0.2)]
228 pub alpha_test: f32,
229 #[gff(StealthXPMax, unexamined)]
231 pub stealth_xp_max: u32,
232 #[gff(
243 StealthXPCurrent,
244 not_a_constant,
245 from_siblings,
246 manual_read,
247 manual_write,
248 omit = matches("StealthXPMax", 1057)
249 )]
250 pub stealth_xp_current: u32,
251 #[gff(StealthXPLoss, constructed)]
253 pub stealth_xp_loss: u32,
254 #[gff(StealthXPEnabled, constructed)]
256 pub stealth_xp: bool,
257 #[gff(TransPending, constructed, omit = audited_constant(1057))]
259 pub trans_pending: u8,
260 #[gff(TransPendNextID, constructed, omit = audited_constant(1057))]
262 pub trans_pend_next_id: u8,
263 #[gff(TransPendCurrID, constructed, omit = audited_constant(1057))]
265 pub trans_pend_curr_id: u8,
266 #[gff(LoadScreenID, stamped)]
268 pub loadscreen_id: u16,
269 #[gff(Rooms, unexamined, list = AreRoom, element_id = 0)]
271 pub rooms: Vec<AreRoom>,
272 #[gff(Expansion_List, unexamined, list = AreExpansionEntry, element_id = 0)]
274 pub expansion_list: Vec<AreExpansionEntry>,
275 #[gff(Map, unexamined, nested = AreMap)]
277 pub map: AreMap,
278 #[gff(MiniGame, not_a_constant, nested = AreMiniGame, optional)]
280 pub mini_game: Option<AreMiniGame>,
281 #[gff(
283 DisableTransit,
284 read_only_dead = "ARE-005 records DisableTransit, NoHangBack and PlayerVsPlayer as toolset fields the K1 engine never reads",
285 not_a_constant
286 )]
287 pub disable_transit: bool,
288 #[gff(
290 NoHangBack,
291 read_only_dead = "ARE-005 records DisableTransit, NoHangBack and PlayerVsPlayer as toolset fields the K1 engine never reads",
292 not_a_constant
293 )]
294 pub no_hang_back: bool,
295 #[gff(PlayerOnly, unexamined)]
297 pub player_only: bool,
298 #[gff(
300 PlayerVsPlayer,
301 read_only_dead = "ARE-005 records DisableTransit, NoHangBack and PlayerVsPlayer as toolset fields the K1 engine never reads",
302 not_a_constant
303 )]
304 pub player_vs_player: u8,
305 #[gff(
307 Grass_Emissive,
308 read_only_dead = "Grass_Emissive and the entire Dirty* overlay set are confirmed dead in this K1 build rather than merely unread; none of their field-name strings exist",
309 not_a_constant
310 )]
311 pub grass_emissive_color: u32,
312 #[gff(
314 DirtyARGBOne,
315 read_only_dead = "Grass_Emissive and the entire Dirty* overlay set are confirmed dead in this K1 build rather than merely unread; none of their field-name strings exist",
316 not_a_constant
317 )]
318 pub dirty_argb_one: i32,
319 #[gff(
321 DirtySizeOne,
322 read_only_dead = "Grass_Emissive and the entire Dirty* overlay set are confirmed dead in this K1 build rather than merely unread; none of their field-name strings exist",
323 not_a_constant
324 )]
325 pub dirty_size_one: i32,
326 #[gff(
328 DirtyFormulaOne,
329 read_only_dead = "Grass_Emissive and the entire Dirty* overlay set are confirmed dead in this K1 build rather than merely unread; none of their field-name strings exist",
330 not_a_constant
331 )]
332 pub dirty_formula_one: i32,
333 #[gff(
335 DirtyFuncOne,
336 read_only_dead = "Grass_Emissive and the entire Dirty* overlay set are confirmed dead in this K1 build rather than merely unread; none of their field-name strings exist",
337 not_a_constant
338 )]
339 pub dirty_func_one: i32,
340 #[gff(
342 DirtyARGBTwo,
343 read_only_dead = "Grass_Emissive and the entire Dirty* overlay set are confirmed dead in this K1 build rather than merely unread; none of their field-name strings exist",
344 not_a_constant
345 )]
346 pub dirty_argb_two: i32,
347 #[gff(
349 DirtySizeTwo,
350 read_only_dead = "Grass_Emissive and the entire Dirty* overlay set are confirmed dead in this K1 build rather than merely unread; none of their field-name strings exist",
351 not_a_constant
352 )]
353 pub dirty_size_two: i32,
354 #[gff(
356 DirtyFormulaTwo,
357 read_only_dead = "Grass_Emissive and the entire Dirty* overlay set are confirmed dead in this K1 build rather than merely unread; none of their field-name strings exist",
358 not_a_constant
359 )]
360 pub dirty_formula_two: i32,
361 #[gff(
363 DirtyFuncTwo,
364 read_only_dead = "Grass_Emissive and the entire Dirty* overlay set are confirmed dead in this K1 build rather than merely unread; none of their field-name strings exist",
365 not_a_constant
366 )]
367 pub dirty_func_two: i32,
368 #[gff(
370 DirtyARGBThree,
371 read_only_dead = "Grass_Emissive and the entire Dirty* overlay set are confirmed dead in this K1 build rather than merely unread; none of their field-name strings exist",
372 not_a_constant
373 )]
374 pub dirty_argb_three: i32,
375 #[gff(
377 DirtySizeThree,
378 read_only_dead = "Grass_Emissive and the entire Dirty* overlay set are confirmed dead in this K1 build rather than merely unread; none of their field-name strings exist",
379 not_a_constant
380 )]
381 pub dirty_size_three: i32,
382 #[gff(
384 DirtyFormulaThre,
385 read_only_dead = "Grass_Emissive and the entire Dirty* overlay set are confirmed dead in this K1 build rather than merely unread; none of their field-name strings exist",
386 not_a_constant
387 )]
388 pub dirty_formula_three: i32,
389 #[gff(
391 DirtyFuncThree,
392 read_only_dead = "Grass_Emissive and the entire Dirty* overlay set are confirmed dead in this K1 build rather than merely unread; none of their field-name strings exist",
393 not_a_constant
394 )]
395 pub dirty_func_three: i32,
396}
397
398impl Are {
399 pub fn new() -> Self {
401 Self::default()
402 }
403
404 pub fn from_gff(gff: &Gff) -> Result<Self, AreError> {
411 if gff.file_type != <Are as FromGff>::MAGIC && gff.file_type != GENERIC_FILE_TYPE {
412 return Err(AreError::UnsupportedFileType(gff.file_type));
413 }
414 let mut are = Self::read_declared(&gff.root);
415 are.stealth_xp_current = gff
416 .root
417 .field("StealthXPCurrent")
418 .and_then(<u32 as GffScalar>::from_gff_value)
419 .unwrap_or(are.stealth_xp_max);
420 Ok(are)
421 }
422
423 pub fn to_gff(&self) -> Gff {
425 let mut root = GffStruct::new(-1);
426 self.write_declared(&mut root);
427
428 if self.stealth_xp_current != self.stealth_xp_max {
434 upsert_field(
435 &mut root,
436 gff_label!("StealthXPCurrent"),
437 GffValue::UInt32(self.stealth_xp_current),
438 );
439 }
440 Gff::new(*b"ARE ", root)
441 }
442}
443
444#[derive(Debug, Clone, PartialEq, GffModel)]
446pub struct AreMap {
447 #[gff(MapResX, stamped)]
449 pub map_res_x: i32,
450 #[gff(NorthAxis, stamped)]
452 pub north_axis: i32,
453 #[gff(MapZoom, stamped = 1)]
455 pub map_zoom: i32,
456 #[gff(MapPt1X, stamped = 0.0, manual_read)]
458 pub map_point_1_x: f32,
459 #[gff(MapPt1Y, stamped = 0.0, manual_read)]
461 pub map_point_1_y: f32,
462 #[gff(MapPt2X, stamped = 0.0, manual_read)]
464 pub map_point_2_x: f32,
465 #[gff(MapPt2Y, stamped = 0.0, manual_read)]
467 pub map_point_2_y: f32,
468 #[gff(WorldPt1X, stamped = 0.0)]
470 pub world_point_1_x: f32,
471 #[gff(WorldPt1Y, stamped = 0.0)]
473 pub world_point_1_y: f32,
474 #[gff(WorldPt2X, stamped = 0.0)]
476 pub world_point_2_x: f32,
477 #[gff(WorldPt2Y, stamped = 0.0)]
479 pub world_point_2_y: f32,
480}
481
482impl AreMap {
483 fn read_element(structure: &GffStruct) -> Self {
492 let mut map = Self::read_declared(structure);
493 map.map_point_1_x = dual_path(structure, "MapPt1X");
494 map.map_point_1_y = dual_path(structure, "MapPt1Y");
495 map.map_point_2_x = dual_path(structure, "MapPt2X");
496 map.map_point_2_y = dual_path(structure, "MapPt2Y");
497 map
498 }
499}
500
501fn dual_path(structure: &GffStruct, label: &str) -> f32 {
507 #[allow(clippy::cast_precision_loss, clippy::as_conversions)]
508 match structure.field(label) {
509 Some(GffValue::Int32(value)) => *value as f32,
510 Some(GffValue::UInt32(value)) => *value as f32,
511 Some(GffValue::Int16(value)) => f32::from(*value),
512 Some(GffValue::UInt16(value)) => f32::from(*value),
513 Some(GffValue::Int8(value)) => f32::from(*value),
514 Some(GffValue::UInt8(value)) => f32::from(*value),
515 other => other
516 .and_then(<f32 as GffScalar>::from_gff_value)
517 .unwrap_or(0.0),
518 }
519}
520
521#[derive(Debug, Clone, PartialEq, GffModel)]
530#[gff_entry(
531 ForceRating,
532 wire = i32,
533 read_only_dead = "neither field-name string exists anywhere in swkotor.exe, confirmed by a binary-wide search; toolset-only in this build",
534 not_a_constant
535)]
536#[gff_entry(
537 DisableWeather,
538 wire = u8,
539 read_only_dead = "neither field-name string exists anywhere in swkotor.exe, confirmed by a binary-wide search; toolset-only in this build",
540 not_a_constant
541)]
542pub struct AreRoom {
543 #[gff(PartSounds, not_a_constant, list = ArePartSound, element_id = 0)]
545 pub part_sounds: Vec<ArePartSound>,
546 #[gff(RoomName, stamped)]
548 pub room_name: String,
549 #[gff(EnvAudio, stamped)]
551 pub env_audio: i32,
552 #[gff(AmbientScale, stamped)]
554 pub ambient_scale: f32,
555}
556
557#[derive(Debug, Clone, PartialEq, GffModel)]
559pub struct AreExpansionEntry {
560 #[gff(Expansion_Name, stamped)]
562 pub expansion_name: GffLocalizedString,
563 #[gff(Expansion_ID, stamped)]
565 pub expansion_id: i32,
566}
567
568#[derive(Debug, Clone, PartialEq, GffModel)]
570pub struct ArePartSound {
571 #[gff(Looping, unexamined)]
573 pub looping: bool,
574 #[gff(ModelPart, unexamined)]
576 pub model_part: String,
577 #[gff(OmenEvent, unexamined)]
579 pub omen_event: String,
580 #[gff(Sound, unexamined)]
582 pub sound: ResRef,
583}
584
585#[derive(Debug, Error)]
587pub enum AreError {
588 #[error("unsupported ARE file type: {0:?}")]
590 UnsupportedFileType([u8; 4]),
591 #[error(transparent)]
593 Gff(#[from] GffBinaryError),
594}
595
596#[cfg_attr(
604 feature = "tracing",
605 tracing::instrument(level = "debug", skip(reader))
606)]
607pub fn read_are<R: Read>(reader: &mut R) -> Result<Are, AreError> {
608 let gff = read_gff(reader)?;
609 Are::from_gff(&gff)
610}
611
612#[cfg_attr(
620 feature = "tracing",
621 tracing::instrument(level = "debug", skip(bytes), fields(bytes_len = bytes.len()))
622)]
623pub fn read_are_from_bytes(bytes: &[u8]) -> Result<Are, AreError> {
624 let gff = read_gff_from_bytes(bytes)?;
625 Are::from_gff(&gff)
626}
627
628#[cfg_attr(
636 feature = "tracing",
637 tracing::instrument(level = "debug", skip(writer, are))
638)]
639pub fn author_are<W: Write>(writer: &mut W, are: &Are) -> Result<(), AreError> {
640 let gff = are.to_gff();
641 write_gff(writer, &gff)?;
642 Ok(())
643}
644
645#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(are)))]
652pub fn author_are_to_vec(are: &Are) -> Result<Vec<u8>, AreError> {
653 let mut cursor = Cursor::new(Vec::new());
654 author_are(&mut cursor, are)?;
655 Ok(cursor.into_inner())
656}
657
658#[cfg(test)]
659mod tests {
660 use rakata_formats::gff::upsert_field;
661 use rakata_formats::gff_label;
662 use rakata_formats::schema::{HasSchema, Shape};
663
664 use super::*;
665
666 #[test]
667 fn stealth_xp_current_resolves_across_every_presence_combination() {
668 let build = |current: Option<u32>, max: Option<u32>| {
672 let mut gff = Gff::new(*b"ARE ", GffStruct::new(0));
673 if let Some(v) = current {
674 upsert_field(
675 &mut gff.root,
676 gff_label!("StealthXPCurrent"),
677 GffValue::UInt32(v),
678 );
679 }
680 if let Some(v) = max {
681 upsert_field(
682 &mut gff.root,
683 gff_label!("StealthXPMax"),
684 GffValue::UInt32(v),
685 );
686 }
687 Are::from_gff(&gff).expect("an ARE with no required fields parses")
688 };
689
690 assert_eq!(build(Some(120), Some(500)).stealth_xp_current, 120);
692 assert_eq!(build(None, Some(500)).stealth_xp_current, 500);
695 assert_eq!(build(None, None).stealth_xp_current, 0);
698 assert_eq!(build(Some(900), Some(500)).stealth_xp_current, 900);
701 assert_eq!(build(None, Some(500)).stealth_xp_max, 500);
703 }
704
705 const TEST_ARE: &[u8] = include_bytes!(concat!(
706 env!("CARGO_MANIFEST_DIR"),
707 "/../../fixtures/test.are"
708 ));
709
710 #[test]
711 fn reads_core_are_fields_from_fixture() {
712 let are = read_are_from_bytes(TEST_ARE).expect("fixture must parse");
713
714 assert_eq!(are.unused_id, 0);
715 assert_eq!(are.creator_id, 0);
716 assert_eq!(are.tag, "Untitled");
717 assert_eq!(are.name.string_ref.raw(), 75_101);
718 assert_eq!(are.comment, "comments");
719 assert_eq!(are.version, 88);
720 assert_eq!(are.flags, 0);
721 assert_eq!(are.mod_spot_check, 0);
722 assert_eq!(are.mod_listen_check, 0);
723 assert_eq!(are.camera_style, 1);
724 assert_eq!(are.default_envmap, "defaultenvmap");
725 assert_eq!(are.grass_texture, "grasstexture");
726 assert!((are.grass_density - 1.0).abs() < f32::EPSILON);
727 assert!((are.grass_size - 1.0).abs() < f32::EPSILON);
728 assert!((are.grass_prob_ll - 0.25).abs() < f32::EPSILON);
729 assert!((are.grass_prob_lr - 0.25).abs() < f32::EPSILON);
730 assert!((are.grass_prob_ul - 0.25).abs() < f32::EPSILON);
731 assert!((are.grass_prob_ur - 0.25).abs() < f32::EPSILON);
732 assert_eq!(are.sun_ambient_color, 16_777_215);
733 assert_eq!(are.sun_diffuse_color, 16_777_215);
734 assert_eq!(are.dynamic_ambient_color, 16_777_215);
735 assert_eq!(are.sun_fog_color, 16_777_215);
736 assert_eq!(are.grass_ambient_color, 16_777_215);
737 assert_eq!(are.grass_diffuse_color, 16_777_215);
738 assert_eq!(are.grass_emissive_color, 16_777_215);
739 assert!(are.fog_enabled);
740 assert!((are.fog_near - 99.0).abs() < f32::EPSILON);
741 assert!((are.fog_far - 100.0).abs() < f32::EPSILON);
742 assert!(are.shadows);
743 assert_eq!(are.shadow_opacity, 205);
744 assert_eq!(are.wind_power, 1);
745 assert!(are.unescapable);
746 assert!(are.disable_transit);
747 assert!(are.stealth_xp);
748 assert_eq!(are.stealth_xp_loss, 25);
749 assert_eq!(are.stealth_xp_max, 25);
750 assert_eq!(are.on_enter, "k_on_enter");
751 assert_eq!(are.on_exit, "onexit");
752 assert_eq!(are.on_heartbeat, "onheartbeat");
753 assert_eq!(are.on_user_defined, "onuserdefined");
754 assert!((are.alpha_test - 0.2).abs() < f32::EPSILON);
755 assert_eq!(are.chance_rain, 99);
756 assert_eq!(are.chance_snow, 99);
757 assert_eq!(are.chance_lightning, 99);
758 assert_eq!(are.moon_ambient_color, 0);
759 assert_eq!(are.moon_diffuse_color, 0);
760 assert!(!are.moon_fog_enabled);
761 assert!((are.moon_fog_near - 99.0).abs() < f32::EPSILON);
762 assert!((are.moon_fog_far - 100.0).abs() < f32::EPSILON);
763 assert_eq!(are.moon_fog_color, 0);
764 assert!(!are.moon_shadows);
765 assert_eq!(are.dirty_argb_one, 123);
766 assert_eq!(are.dirty_size_one, 1);
767 assert_eq!(are.dirty_formula_one, 1);
768 assert_eq!(are.dirty_func_one, 1);
769 assert_eq!(are.dirty_argb_two, 1234);
770 assert_eq!(are.dirty_size_two, 1);
771 assert_eq!(are.dirty_formula_two, 1);
772 assert_eq!(are.dirty_func_two, 1);
773 assert_eq!(are.dirty_argb_three, 12_345);
774 assert_eq!(are.dirty_size_three, 1);
775 assert_eq!(are.dirty_formula_three, 1);
776 assert_eq!(are.dirty_func_three, 1);
777 assert!(!are.is_night);
778 assert_eq!(are.lighting_scheme, 0);
779 assert_eq!(are.day_night_cycle, 0);
780 assert!(!are.no_rest);
781 assert!(!are.no_hang_back);
782 assert!(!are.player_only);
783 assert_eq!(are.player_vs_player, 3);
784 assert_eq!(are.map.map_zoom, 1);
785 assert_eq!(are.map.map_res_x, 18);
786 assert_eq!(are.rooms.len(), 2);
787 assert!(are.expansion_list.is_empty());
788 assert_eq!(are.rooms[0].room_name, "002ebo");
789 assert!(are.rooms[0].part_sounds.is_empty());
790 }
791
792 #[test]
797 fn a_dead_room_label_is_on_the_tree_and_not_in_the_view() {
798 let gff = read_gff_from_bytes(TEST_ARE).expect("fixture parses");
799 let Some(GffValue::List(rooms)) = gff.root.field("Rooms") else {
800 panic!("the fixture carries rooms");
801 };
802 assert_eq!(
803 rooms[0].field("DisableWeather"),
804 Some(&GffValue::UInt8(1)),
805 "the fixture is the one that carries it; if it stopped, this test \
806 no longer covers a label the view drops"
807 );
808
809 let written = read_are_from_bytes(TEST_ARE)
810 .expect("fixture parses")
811 .to_gff();
812 let Some(GffValue::List(written_rooms)) = written.root.field("Rooms") else {
813 panic!("rooms are written back");
814 };
815 assert_eq!(
816 written_rooms[0].field("RoomName"),
817 Some(&GffValue::String("002ebo".to_owned())),
818 "a modelled label does survive, so the two below are dropped for \
819 being dead rather than for the write producing nothing"
820 );
821 for label in ["DisableWeather", "ForceRating"] {
822 assert!(
823 written_rooms[0].field(label).is_none(),
824 "`{label}` is read by nothing, so it is not this view's to write"
825 );
826 }
827 }
828
829 #[test]
830 fn all_fields_survive_typed_roundtrip() {
831 let are = read_are_from_bytes(TEST_ARE).expect("fixture must parse");
832 let encoded = author_are_to_vec(&are).expect("encode must succeed");
833 let reparsed = read_are_from_bytes(&encoded).expect("decode must succeed");
834 assert_eq!(are, reparsed);
835 }
836
837 #[test]
838 fn typed_edits_roundtrip_through_gff_writer() {
839 let mut are = read_are_from_bytes(TEST_ARE).expect("fixture must parse");
840 are.tag = "m01aa".into();
841 are.on_enter = ResRef::new("k_on_newenter").expect("valid test resref");
842 are.grass_texture = ResRef::new("new_grass").expect("valid test resref");
843 are.fog_enabled = false;
844 are.fog_near = 50.0;
845 are.shadow_opacity = 180;
846 are.stealth_xp_loss = 33;
847 are.dirty_formula_three = 9;
848 are.player_vs_player = 2;
849 are.restrict_mode = 1;
850 are.chance_fog = 42;
851 are.stealth_xp_current = 10;
852 are.trans_pending = 1;
853 are.trans_pend_next_id = 3;
854 are.trans_pend_curr_id = 2;
855 are.map.map_zoom = 7;
856 are.rooms[0].ambient_scale = 0.5;
857 are.expansion_list.push(AreExpansionEntry {
858 expansion_name: GffLocalizedString::new(rakata_core::StrRef::from_raw(321)),
859 expansion_id: 42,
860 });
861 are.rooms[0].part_sounds.push(ArePartSound {
862 looping: true,
863 model_part: "ROOM_A".into(),
864 omen_event: "ON_ENTER".into(),
865 sound: ResRef::new("amb_rooma").expect("valid test resref"),
866 });
867
868 let encoded = author_are_to_vec(&are).expect("encode");
869 let reparsed = read_are_from_bytes(&encoded).expect("decode");
870
871 assert_eq!(reparsed.tag, "m01aa");
872 assert_eq!(reparsed.on_enter, "k_on_newenter");
873 assert_eq!(reparsed.grass_texture, "new_grass");
874 assert!(!reparsed.fog_enabled);
875 assert!((reparsed.fog_near - 50.0).abs() < f32::EPSILON);
876 assert_eq!(reparsed.shadow_opacity, 180);
877 assert_eq!(reparsed.stealth_xp_loss, 33);
878 assert_eq!(reparsed.dirty_formula_three, 9);
879 assert_eq!(reparsed.player_vs_player, 2);
880 assert_eq!(reparsed.restrict_mode, 1);
881 assert_eq!(reparsed.chance_fog, 42);
882 assert_eq!(reparsed.stealth_xp_current, 10);
883 assert_eq!(reparsed.trans_pending, 1);
884 assert_eq!(reparsed.trans_pend_next_id, 3);
885 assert_eq!(reparsed.trans_pend_curr_id, 2);
886 assert_eq!(reparsed.map.map_zoom, 7);
887 assert_eq!(reparsed.rooms[0].ambient_scale, 0.5);
888 assert_eq!(reparsed.expansion_list.len(), 1);
889 assert_eq!(reparsed.expansion_list[0].expansion_id, 42);
890 assert_eq!(
891 reparsed.expansion_list[0].expansion_name.string_ref.raw(),
892 321
893 );
894 assert_eq!(reparsed.rooms[0].part_sounds.len(), 1);
895 assert!(reparsed.rooms[0].part_sounds[0].looping);
896 assert_eq!(reparsed.rooms[0].part_sounds[0].model_part, "ROOM_A");
897 assert_eq!(reparsed.rooms[0].part_sounds[0].omen_event, "ON_ENTER");
898 assert_eq!(reparsed.rooms[0].part_sounds[0].sound, "amb_rooma");
899 assert_eq!(reparsed.rooms.len(), 2);
900 }
901
902 #[test]
903 fn rejects_non_are_file_type() {
904 let gff = Gff::new(*b"DLG ", GffStruct::new(-1));
905 let err = Are::from_gff(&gff).expect_err("must fail");
906 assert!(matches!(err, AreError::UnsupportedFileType(file_type) if file_type == *b"DLG "));
907 }
908
909 #[test]
910 fn read_are_from_reader_matches_bytes_path() {
911 let mut cursor = Cursor::new(TEST_ARE);
912 let via_reader = read_are(&mut cursor).expect("reader parse");
913 let via_bytes = read_are_from_bytes(TEST_ARE).expect("bytes parse");
914 assert_eq!(via_reader.tag, via_bytes.tag);
915 assert_eq!(via_reader.rooms.len(), via_bytes.rooms.len());
916 }
917
918 #[test]
919 fn a_mistyped_rooms_list_reads_as_empty() {
920 let mut root = GffStruct::new(-1);
921 root.push_field(gff_label!("Rooms"), GffValue::UInt32(7));
922 let gff = Gff::new(*b"ARE ", root);
923
924 let are = Are::from_gff(&gff).expect("a mistyped list is not a read failure");
925
926 assert!(are.rooms.is_empty());
927 }
928
929 #[test]
930 fn map_points_accept_int_encoding_for_k1_parity() {
931 let mut root = GffStruct::new(-1);
932 let mut map = GffStruct::new(0);
933 map.push_field(gff_label!("MapPt1X"), GffValue::Int32(10));
934 map.push_field(gff_label!("MapPt1Y"), GffValue::Int32(20));
935 map.push_field(gff_label!("MapPt2X"), GffValue::UInt16(30));
936 map.push_field(gff_label!("MapPt2Y"), GffValue::UInt8(40));
937 root.push_field(gff_label!("Map"), GffValue::Struct(Box::new(map)));
938 let gff = Gff::new(*b"ARE ", root);
939
940 let are = Are::from_gff(&gff).expect("must parse");
941 assert_eq!((are.map.map_point_1_x, are.map.map_point_1_y), (10.0, 20.0));
942 assert_eq!((are.map.map_point_2_x, are.map.map_point_2_y), (30.0, 40.0));
943 }
944
945 #[test]
946 fn a_mistyped_expansion_list_reads_as_empty() {
947 let mut root = GffStruct::new(-1);
948 root.push_field(gff_label!("Expansion_List"), GffValue::UInt32(7));
949 let gff = Gff::new(*b"ARE ", root);
950
951 let are = Are::from_gff(&gff).expect("a mistyped list is not a read failure");
952
953 assert!(are.expansion_list.is_empty());
954 }
955
956 #[test]
957 fn a_mistyped_part_sounds_list_reads_as_empty() {
958 let mut root = GffStruct::new(-1);
959 let mut room = GffStruct::new(0);
960 room.push_field(gff_label!("PartSounds"), GffValue::UInt32(7));
961 root.push_field(gff_label!("Rooms"), GffValue::List(vec![room]));
962 let gff = Gff::new(*b"ARE ", root);
963
964 let are = Are::from_gff(&gff).expect("a mistyped list is not a read failure");
965
966 assert!(are.rooms[0].part_sounds.is_empty());
967 }
968
969 #[test]
970 fn a_mistyped_part_sound_field_reads_as_absent() {
971 let mut root = GffStruct::new(-1);
975 let mut room = GffStruct::new(0);
976 let mut part_sound = GffStruct::new(0);
977 part_sound.push_field(gff_label!("Looping"), GffValue::String("yes".into()));
978 room.push_field(gff_label!("PartSounds"), GffValue::List(vec![part_sound]));
979 root.push_field(gff_label!("Rooms"), GffValue::List(vec![room]));
980 let gff = Gff::new(*b"ARE ", root);
981
982 let are = Are::from_gff(&gff).expect("a mistyped field is not a read failure");
983
984 assert!(!are.rooms[0].part_sounds[0].looping);
985 }
986
987 #[test]
988 fn write_are_matches_direct_gff_writer() {
989 let are = read_are_from_bytes(TEST_ARE).expect("fixture parse");
990 let from_are = author_are_to_vec(&are).expect("are encode");
991
992 let gff = are.to_gff();
993 let from_gff = rakata_formats::write_gff_to_vec(&gff).expect("gff encode");
994 assert_eq!(from_are, from_gff);
995 }
996
997 #[test]
998 fn schema_field_count() {
999 assert_eq!(Are::schema().len(), 81);
1000 }
1001
1002 #[test]
1003 fn schema_no_duplicate_labels() {
1004 let mut labels: Vec<&str> = Are::schema().iter().map(|f| f.label.as_str()).collect();
1005 labels.sort_unstable();
1006 let before = labels.len();
1007 labels.dedup();
1008 assert_eq!(before, labels.len(), "duplicate labels in ARE schema");
1009 }
1010
1011 fn child_count(label: &str) -> usize {
1015 let field = Are::schema()
1016 .iter()
1017 .find(|f| f.label.as_str() == label)
1018 .unwrap_or_else(|| panic!("{label} must exist in the schema"));
1019 match field.shape {
1020 Shape::List { element, .. } => element.iter().map(|p| p.len()).sum(),
1021 Shape::Struct { fields } => fields.iter().map(|p| p.len()).sum(),
1022 other => panic!("{label} is a container, got {other:?}"),
1023 }
1024 }
1025
1026 #[test]
1027 fn schema_containers_carry_their_children() {
1028 assert_eq!(child_count("Rooms"), 6);
1029 assert_eq!(child_count("Map"), 11);
1030 assert_eq!(child_count("Expansion_List"), 2);
1031 assert_eq!(child_count("MiniGame"), 15);
1032 }
1033
1034 #[test]
1037 fn a_part_sound_declares_what_the_view_reads() {
1038 let Shape::List { element, .. } = Are::schema()
1039 .iter()
1040 .find(|f| f.label.as_str() == "Rooms")
1041 .expect("declared")
1042 .shape
1043 else {
1044 panic!("Rooms is a list");
1045 };
1046 let part_sounds = element
1047 .iter()
1048 .flat_map(|part| part.iter())
1049 .find(|f| f.label.as_str() == "PartSounds")
1050 .expect("a room declares its part sounds");
1051 let Shape::List { element, .. } = part_sounds.shape else {
1052 panic!("PartSounds is a list");
1053 };
1054 let labels: Vec<&str> = element
1055 .iter()
1056 .flat_map(|part| part.iter())
1057 .map(|f| f.label.as_str())
1058 .collect();
1059 assert_eq!(labels, ["Looping", "ModelPart", "OmenEvent", "Sound"]);
1060 }
1061}