rakata_generics/
are.rs

1//! ARE (`.are`) typed generic wrapper.
2//!
3//! ARE resources are GFF-backed area metadata containers.
4//!
5//! ## Scope
6//! - Typed access for all engine-read area fields (identity, scripts, map, core flags,
7//!   weather, lighting, grass, stealth/transition save-state, and MiniGame struct).
8//! - Deterministic lossless conversion back to [`Gff`].
9//!
10//! ## Field Layout (simplified)
11//! ```text
12//! ARE root struct
13//! +-- Tag              (CExoString)
14//! +-- Name             (CExoLocString)
15//! +-- Comments         (CExoString)
16//! +-- AlphaTest        (FLOAT)
17//! +-- CameraStyle      (INT)
18//! +-- DefaultEnvMap    (CResRef)
19//! +-- RestrictMode     (BYTE)
20//! +-- OnEnter/Exit/... (CResRef)
21//! +-- Flags            (DWORD)
22//! +-- Version          (DWORD)
23//! +-- LoadScreenID     (WORD)
24//! +-- ChanceRain/Snow/Lightning/Fog (INT)
25//! +-- StealthXPCurrent (DWORD)
26//! +-- TransPending/TransPendNextID/TransPendCurrID (BYTE)
27//! +-- Expansion_List   (List<Struct>)
28//! |   +-- Expansion_Name (CExoLocString)
29//! |   `-- Expansion_ID   (INT)
30//! +-- MiniGame         (Struct -> AreMiniGame)
31//! |   +-- Type / MovementPerSec / LateralAccel / Bump_Plane
32//! |   +-- DoBumping / UseInertia / DOF / Music
33//! |   +-- Far_Clip / Near_Clip / CameraViewAngle
34//! |   `-- Player        (Struct -> AreMiniGamePlayer)
35//! |       +-- Models    (List<AreMiniGameModel>)
36//! |       +-- Track / Camera / CameraRotate
37//! |       +-- Mouse     (Struct -> AreMiniGameMouse)
38//! |       +-- Enemies   (List<AreMiniGameEnemy>)
39//! |       `-- Obstacles (List<AreMiniGameObstacle>)
40//! +-- Map              (Struct)
41//! |   +-- NorthAxis / MapZoom / MapResX
42//! |   +-- MapPt{1,2}{X,Y} / WorldPt{1,2}{X,Y}
43//! +-- Rooms            (List<Struct>)
44//!     +-- RoomName / AmbientScale / EnvAudio / ForceRating / DisableWeather
45//!     `-- PartSounds   (List<Struct>)
46//!         +-- Looping / ModelPart / OmenEvent / Sound
47//! ```
48
49use std::io::{Cursor, Read, Write};
50
51use crate::gff_helpers::{
52    get_bool, get_f32, get_i32, get_locstring, get_resref, get_string, get_u16, get_u32, get_u8,
53    upsert_field,
54};
55use rakata_core::{ResRef, StrRef};
56use rakata_formats::{
57    gff_schema::{FieldConstraint, FieldSchema, GffSchema, GffType},
58    read_gff, read_gff_from_bytes, write_gff, Gff, GffBinaryError, GffLocalizedString, GffStruct,
59    GffValue,
60};
61use thiserror::Error;
62
63/// Typed ARE model built from/to [`Gff`] data.
64#[derive(Debug, Clone, PartialEq)]
65pub struct Are {
66    /// Legacy area id (`ID`).
67    pub unused_id: i32,
68    /// Legacy creator id (`Creator_ID`).
69    pub creator_id: i32,
70    /// Area format version (`Version`).
71    pub version: u32,
72    /// Area tag (`Tag`).
73    pub tag: String,
74    /// Localized area name (`Name`).
75    pub name: GffLocalizedString,
76    /// Toolset/comment field (`Comments`).
77    pub comment: String,
78    /// Alpha test threshold (`AlphaTest`).
79    pub alpha_test: f32,
80    /// Camera style ID (`CameraStyle`).
81    pub camera_style: i32,
82    /// Default environment map resref (`DefaultEnvMap`).
83    pub default_envmap: ResRef,
84    /// Restrict mode (`RestrictMode`) - triggers `UnstealthParty` on non-zero change.
85    pub restrict_mode: u8,
86    /// Grass texture resref (`Grass_TexName`).
87    pub grass_texture: ResRef,
88    /// Grass density (`Grass_Density`).
89    pub grass_density: f32,
90    /// Grass quad size (`Grass_QuadSize`).
91    pub grass_size: f32,
92    /// Grass probability lower-left (`Grass_Prob_LL`).
93    pub grass_prob_ll: f32,
94    /// Grass probability lower-right (`Grass_Prob_LR`).
95    pub grass_prob_lr: f32,
96    /// Grass probability upper-left (`Grass_Prob_UL`).
97    pub grass_prob_ul: f32,
98    /// Grass probability upper-right (`Grass_Prob_UR`).
99    pub grass_prob_ur: f32,
100    /// Sun fog enabled (`SunFogOn`).
101    pub fog_enabled: bool,
102    /// Sun fog near distance (`SunFogNear`).
103    pub fog_near: f32,
104    /// Sun fog far distance (`SunFogFar`).
105    pub fog_far: f32,
106    /// Sun shadows enabled (`SunShadows`).
107    pub shadows: bool,
108    /// Shadow opacity (`ShadowOpacity`).
109    pub shadow_opacity: u8,
110    /// Wind power (`WindPower`).
111    pub wind_power: i32,
112    /// Unescapable area flag (`Unescapable`).
113    pub unescapable: bool,
114    /// Disable transit flag (`DisableTransit`).
115    pub disable_transit: bool,
116    /// Stealth XP enabled (`StealthXPEnabled`).
117    pub stealth_xp: bool,
118    /// Stealth XP loss (`StealthXPLoss`).
119    pub stealth_xp_loss: u32,
120    /// Stealth XP max (`StealthXPMax`).
121    pub stealth_xp_max: u32,
122    /// Current stealth XP counter (`StealthXPCurrent`, save-state).
123    pub stealth_xp_current: u32,
124    /// Enter script resref (`OnEnter`).
125    pub on_enter: ResRef,
126    /// Exit script resref (`OnExit`).
127    pub on_exit: ResRef,
128    /// Heartbeat script resref (`OnHeartbeat`).
129    pub on_heartbeat: ResRef,
130    /// User-defined event script resref (`OnUserDefined`).
131    pub on_user_defined: ResRef,
132    /// Area flags (`Flags`).
133    pub flags: u32,
134    /// Load screen ID (`LoadScreenID`).
135    pub loadscreen_id: u16,
136    /// Chance of rain (`ChanceRain`, K2).
137    pub chance_rain: i32,
138    /// Chance of snow (`ChanceSnow`, K2).
139    pub chance_snow: i32,
140    /// Chance of lightning (`ChanceLightning`, K2).
141    pub chance_lightning: i32,
142    /// Chance of fog (`ChanceFog`).
143    pub chance_fog: i32,
144    /// Mod spot-check modifier (`ModSpotCheck`).
145    pub mod_spot_check: i32,
146    /// Mod listen-check modifier (`ModListenCheck`).
147    pub mod_listen_check: i32,
148    /// Moon ambient color (`MoonAmbientColor`).
149    pub moon_ambient_color: u32,
150    /// Moon diffuse color (`MoonDiffuseColor`).
151    pub moon_diffuse_color: u32,
152    /// Moon fog enabled (`MoonFogOn`).
153    pub moon_fog_enabled: bool,
154    /// Moon fog near distance (`MoonFogNear`).
155    pub moon_fog_near: f32,
156    /// Moon fog far distance (`MoonFogFar`).
157    pub moon_fog_far: f32,
158    /// Moon fog color (`MoonFogColor`).
159    pub moon_fog_color: u32,
160    /// Moon shadows enabled (`MoonShadows`).
161    pub moon_shadows: bool,
162    /// Sun ambient color (`SunAmbientColor`).
163    pub sun_ambient_color: u32,
164    /// Sun diffuse color (`SunDiffuseColor`).
165    pub sun_diffuse_color: u32,
166    /// Dynamic ambient color (`DynAmbientColor`).
167    pub dynamic_ambient_color: u32,
168    /// Sun fog color (`SunFogColor`).
169    pub sun_fog_color: u32,
170    /// Grass ambient color (`Grass_Ambient`).
171    pub grass_ambient_color: u32,
172    /// Grass diffuse color (`Grass_Diffuse`).
173    pub grass_diffuse_color: u32,
174    /// Grass emissive color (`Grass_Emissive`).
175    pub grass_emissive_color: u32,
176    /// Dirty overlay color one (`DirtyARGBOne`).
177    pub dirty_argb_one: i32,
178    /// Dirty overlay size one (`DirtySizeOne`).
179    pub dirty_size_one: i32,
180    /// Dirty overlay formula one (`DirtyFormulaOne`).
181    pub dirty_formula_one: i32,
182    /// Dirty overlay func one (`DirtyFuncOne`).
183    pub dirty_func_one: i32,
184    /// Dirty overlay color two (`DirtyARGBTwo`).
185    pub dirty_argb_two: i32,
186    /// Dirty overlay size two (`DirtySizeTwo`).
187    pub dirty_size_two: i32,
188    /// Dirty overlay formula two (`DirtyFormulaTwo`).
189    pub dirty_formula_two: i32,
190    /// Dirty overlay func two (`DirtyFuncTwo`).
191    pub dirty_func_two: i32,
192    /// Dirty overlay color three (`DirtyARGBThree`).
193    pub dirty_argb_three: i32,
194    /// Dirty overlay size three (`DirtySizeThree`).
195    pub dirty_size_three: i32,
196    /// Dirty overlay formula three (`DirtyFormulaThre`).
197    pub dirty_formula_three: i32,
198    /// Dirty overlay func three (`DirtyFuncThree`).
199    pub dirty_func_three: i32,
200    /// Is-night flag (`IsNight`).
201    pub is_night: bool,
202    /// Lighting scheme (`LightingScheme`).
203    pub lighting_scheme: u8,
204    /// Day/night cycle flag (`DayNightCycle`).
205    pub day_night_cycle: u8,
206    /// No-rest flag (`NoRest`).
207    pub no_rest: bool,
208    /// No-hang-back flag (`NoHangBack`).
209    pub no_hang_back: bool,
210    /// Player-only flag (`PlayerOnly`).
211    pub player_only: bool,
212    /// Player-vs-player mode (`PlayerVsPlayer`).
213    pub player_vs_player: u8,
214    /// Transition pending flag (`TransPending`, save-state).
215    pub trans_pending: u8,
216    /// Transition pending next ID (`TransPendNextID`, save-state).
217    pub trans_pend_next_id: u8,
218    /// Transition pending current ID (`TransPendCurrID`, save-state).
219    pub trans_pend_curr_id: u8,
220    /// Embedded map metadata (`Map` struct).
221    pub map: AreMap,
222    /// Embedded room metadata (`Rooms` list).
223    pub rooms: Vec<AreRoom>,
224    /// Area expansion entries (`Expansion_List` list).
225    pub expansion_list: Vec<AreExpansionEntry>,
226    /// MiniGame struct (`MiniGame`).
227    pub mini_game: Option<AreMiniGame>,
228}
229
230/// Typed view over the ARE `MiniGame` nested struct.
231#[derive(Debug, Clone, PartialEq)]
232pub struct AreMiniGame {
233    /// Mini-game type (`Type`): 1 = swoop, 2 = turret.
234    pub mini_game_type: u32,
235    /// Movement speed (`MovementPerSec`).
236    pub movement_per_sec: f32,
237    /// Lateral acceleration (`LateralAccel`).
238    pub lateral_accel: f32,
239    /// Bump plane index (`Bump_Plane`).
240    pub bump_plane: u32,
241    /// Bumping enabled (`DoBumping`).
242    pub do_bumping: bool,
243    /// Inertia enabled (`UseInertia`).
244    pub use_inertia: bool,
245    /// Degrees of freedom (`DOF`).
246    pub dof: u32,
247    /// Music resref (`Music`).
248    pub music: ResRef,
249    /// Far clip distance (`Far_Clip`).
250    pub far_clip: f32,
251    /// Near clip distance (`Near_Clip`).
252    pub near_clip: f32,
253    /// Camera view angle (`CameraViewAngle`).
254    pub camera_view_angle: f32,
255    /// Player sub-struct (`Player`).
256    pub player: Option<AreMiniGamePlayer>,
257}
258
259impl Default for AreMiniGame {
260    fn default() -> Self {
261        Self {
262            mini_game_type: 0,
263            movement_per_sec: 0.0,
264            lateral_accel: 60.0,
265            bump_plane: 0,
266            do_bumping: false,
267            use_inertia: false,
268            dof: 0,
269            music: ResRef::blank(),
270            far_clip: 100.0,
271            near_clip: 0.1,
272            camera_view_angle: 65.0,
273            player: None,
274        }
275    }
276}
277
278impl AreMiniGame {
279    fn from_struct(structure: &GffStruct) -> Result<Self, AreError> {
280        let player = match structure.field("Player") {
281            Some(GffValue::Struct(s)) => Some(AreMiniGamePlayer::from_struct(s)?),
282            Some(_) => {
283                return Err(AreError::TypeMismatch {
284                    field: "MiniGame.Player",
285                    expected: "Struct",
286                });
287            }
288            None => None,
289        };
290
291        Ok(Self {
292            mini_game_type: get_u32(structure, "Type").unwrap_or(0),
293            movement_per_sec: get_f32(structure, "MovementPerSec").unwrap_or(0.0),
294            lateral_accel: get_f32(structure, "LateralAccel").unwrap_or(60.0),
295            bump_plane: get_u32(structure, "Bump_Plane").unwrap_or(0),
296            do_bumping: get_bool(structure, "DoBumping").unwrap_or(false),
297            use_inertia: get_bool(structure, "UseInertia").unwrap_or(false),
298            dof: get_u32(structure, "DOF").unwrap_or(0),
299            music: get_resref(structure, "Music").unwrap_or_default(),
300            far_clip: get_f32(structure, "Far_Clip").unwrap_or(100.0),
301            near_clip: get_f32(structure, "Near_Clip").unwrap_or(0.1),
302            camera_view_angle: get_f32(structure, "CameraViewAngle").unwrap_or(65.0),
303            player,
304        })
305    }
306
307    fn to_struct(&self) -> GffStruct {
308        let mut s = GffStruct::new(0);
309        upsert_field(&mut s, "Type", GffValue::UInt32(self.mini_game_type));
310        upsert_field(
311            &mut s,
312            "MovementPerSec",
313            GffValue::Single(self.movement_per_sec),
314        );
315        upsert_field(&mut s, "LateralAccel", GffValue::Single(self.lateral_accel));
316        upsert_field(&mut s, "Bump_Plane", GffValue::UInt32(self.bump_plane));
317        upsert_field(
318            &mut s,
319            "DoBumping",
320            GffValue::UInt8(u8::from(self.do_bumping)),
321        );
322        upsert_field(
323            &mut s,
324            "UseInertia",
325            GffValue::UInt8(u8::from(self.use_inertia)),
326        );
327        upsert_field(&mut s, "DOF", GffValue::UInt32(self.dof));
328        upsert_field(&mut s, "Music", GffValue::ResRef(self.music));
329        upsert_field(&mut s, "Far_Clip", GffValue::Single(self.far_clip));
330        upsert_field(&mut s, "Near_Clip", GffValue::Single(self.near_clip));
331        upsert_field(
332            &mut s,
333            "CameraViewAngle",
334            GffValue::Single(self.camera_view_angle),
335        );
336        if let Some(player) = &self.player {
337            upsert_field(
338                &mut s,
339                "Player",
340                GffValue::Struct(Box::new(player.to_struct())),
341            );
342        }
343        s
344    }
345}
346
347/// Typed view over the `MiniGame.Player` sub-struct.
348#[derive(Debug, Clone, PartialEq, Default)]
349pub struct AreMiniGamePlayer {
350    /// Player model list (`Models`).
351    pub models: Vec<AreMiniGameModel>,
352    /// Track resref (`Track`).
353    pub track: ResRef,
354    /// Camera resref (`Camera`) - only used for turret type (type 2).
355    pub camera: ResRef,
356    /// Camera rotation flag (`CameraRotate`).
357    pub camera_rotate: bool,
358    /// Mouse axis settings (`Mouse`).
359    pub mouse: AreMiniGameMouse,
360    /// Enemy list (`Enemies`).
361    pub enemies: Vec<AreMiniGameEnemy>,
362    /// Obstacle list (`Obstacles`).
363    pub obstacles: Vec<AreMiniGameObstacle>,
364}
365
366impl AreMiniGamePlayer {
367    fn from_struct(structure: &GffStruct) -> Result<Self, AreError> {
368        let models = match structure.field("Models") {
369            Some(GffValue::List(items)) => items
370                .iter()
371                .map(AreMiniGameModel::from_struct)
372                .collect::<Vec<_>>(),
373            Some(_) => {
374                return Err(AreError::TypeMismatch {
375                    field: "MiniGame.Player.Models",
376                    expected: "List",
377                });
378            }
379            None => Vec::new(),
380        };
381
382        let mouse = match structure.field("Mouse") {
383            Some(GffValue::Struct(s)) => AreMiniGameMouse::from_struct(s),
384            Some(_) => {
385                return Err(AreError::TypeMismatch {
386                    field: "MiniGame.Player.Mouse",
387                    expected: "Struct",
388                });
389            }
390            None => AreMiniGameMouse::default(),
391        };
392
393        let enemies = match structure.field("Enemies") {
394            Some(GffValue::List(items)) => items
395                .iter()
396                .map(AreMiniGameEnemy::from_struct)
397                .collect::<Result<Vec<_>, _>>()?,
398            Some(_) => {
399                return Err(AreError::TypeMismatch {
400                    field: "MiniGame.Player.Enemies",
401                    expected: "List",
402                });
403            }
404            None => Vec::new(),
405        };
406
407        let obstacles = match structure.field("Obstacles") {
408            Some(GffValue::List(items)) => items
409                .iter()
410                .map(AreMiniGameObstacle::from_struct)
411                .collect::<Vec<_>>(),
412            Some(_) => {
413                return Err(AreError::TypeMismatch {
414                    field: "MiniGame.Player.Obstacles",
415                    expected: "List",
416                });
417            }
418            None => Vec::new(),
419        };
420
421        Ok(Self {
422            models,
423            track: get_resref(structure, "Track").unwrap_or_default(),
424            camera: get_resref(structure, "Camera").unwrap_or_default(),
425            camera_rotate: get_bool(structure, "CameraRotate").unwrap_or(false),
426            mouse,
427            enemies,
428            obstacles,
429        })
430    }
431
432    fn to_struct(&self) -> GffStruct {
433        let mut s = GffStruct::new(0);
434        let model_structs: Vec<GffStruct> = self
435            .models
436            .iter()
437            .map(AreMiniGameModel::to_struct)
438            .collect();
439        upsert_field(&mut s, "Models", GffValue::List(model_structs));
440        upsert_field(&mut s, "Track", GffValue::ResRef(self.track));
441        upsert_field(&mut s, "Camera", GffValue::ResRef(self.camera));
442        upsert_field(
443            &mut s,
444            "CameraRotate",
445            GffValue::UInt8(u8::from(self.camera_rotate)),
446        );
447        upsert_field(
448            &mut s,
449            "Mouse",
450            GffValue::Struct(Box::new(self.mouse.to_struct())),
451        );
452        let enemy_structs: Vec<GffStruct> = self
453            .enemies
454            .iter()
455            .map(AreMiniGameEnemy::to_struct)
456            .collect();
457        upsert_field(&mut s, "Enemies", GffValue::List(enemy_structs));
458        let obstacle_structs: Vec<GffStruct> = self
459            .obstacles
460            .iter()
461            .map(AreMiniGameObstacle::to_struct)
462            .collect();
463        upsert_field(&mut s, "Obstacles", GffValue::List(obstacle_structs));
464        s
465    }
466}
467
468/// Typed view over one model entry in a mini-game model list.
469#[derive(Debug, Clone, PartialEq)]
470pub struct AreMiniGameModel {
471    /// Model resref (`Model`).
472    pub model: ResRef,
473    /// Whether the model rotates (`RotatingModel`).
474    pub rotating_model: bool,
475}
476
477impl Default for AreMiniGameModel {
478    fn default() -> Self {
479        Self {
480            model: ResRef::blank(),
481            rotating_model: true,
482        }
483    }
484}
485
486impl AreMiniGameModel {
487    fn from_struct(structure: &GffStruct) -> Self {
488        Self {
489            model: get_resref(structure, "Model").unwrap_or_default(),
490            rotating_model: get_bool(structure, "RotatingModel").unwrap_or(true),
491        }
492    }
493
494    fn to_struct(&self) -> GffStruct {
495        let mut s = GffStruct::new(0);
496        upsert_field(&mut s, "Model", GffValue::ResRef(self.model));
497        upsert_field(
498            &mut s,
499            "RotatingModel",
500            GffValue::UInt8(u8::from(self.rotating_model)),
501        );
502        s
503    }
504}
505
506/// Typed view over the `MiniGame.Player.Mouse` sub-struct.
507#[derive(Debug, Clone, PartialEq, Default)]
508pub struct AreMiniGameMouse {
509    /// X axis index (`AxisX`).
510    pub axis_x: u32,
511    /// Y axis index (`AxisY`).
512    pub axis_y: u32,
513    /// Flip X axis (`FlipAxisX`).
514    pub flip_axis_x: bool,
515    /// Flip Y axis (`FlipAxisY`).
516    pub flip_axis_y: bool,
517}
518
519impl AreMiniGameMouse {
520    fn from_struct(structure: &GffStruct) -> Self {
521        Self {
522            axis_x: get_u32(structure, "AxisX").unwrap_or(0),
523            axis_y: get_u32(structure, "AxisY").unwrap_or(0),
524            flip_axis_x: get_bool(structure, "FlipAxisX").unwrap_or(false),
525            flip_axis_y: get_bool(structure, "FlipAxisY").unwrap_or(false),
526        }
527    }
528
529    fn to_struct(&self) -> GffStruct {
530        let mut s = GffStruct::new(0);
531        upsert_field(&mut s, "AxisX", GffValue::UInt32(self.axis_x));
532        upsert_field(&mut s, "AxisY", GffValue::UInt32(self.axis_y));
533        upsert_field(
534            &mut s,
535            "FlipAxisX",
536            GffValue::UInt8(u8::from(self.flip_axis_x)),
537        );
538        upsert_field(
539            &mut s,
540            "FlipAxisY",
541            GffValue::UInt8(u8::from(self.flip_axis_y)),
542        );
543        s
544    }
545}
546
547/// Typed view over one enemy entry in `MiniGame.Player.Enemies`.
548#[derive(Debug, Clone, PartialEq, Default)]
549pub struct AreMiniGameEnemy {
550    /// Enemy model list (`Models`).
551    pub models: Vec<AreMiniGameModel>,
552    /// Enemy track resref (`Track`).
553    pub track: ResRef,
554}
555
556impl AreMiniGameEnemy {
557    fn from_struct(structure: &GffStruct) -> Result<Self, AreError> {
558        let models = match structure.field("Models") {
559            Some(GffValue::List(items)) => items
560                .iter()
561                .map(AreMiniGameModel::from_struct)
562                .collect::<Vec<_>>(),
563            Some(_) => {
564                return Err(AreError::TypeMismatch {
565                    field: "MiniGame.Player.Enemies[].Models",
566                    expected: "List",
567                });
568            }
569            None => Vec::new(),
570        };
571
572        Ok(Self {
573            models,
574            track: get_resref(structure, "Track").unwrap_or_default(),
575        })
576    }
577
578    fn to_struct(&self) -> GffStruct {
579        let mut s = GffStruct::new(0);
580        let model_structs: Vec<GffStruct> = self
581            .models
582            .iter()
583            .map(AreMiniGameModel::to_struct)
584            .collect();
585        upsert_field(&mut s, "Models", GffValue::List(model_structs));
586        upsert_field(&mut s, "Track", GffValue::ResRef(self.track));
587        s
588    }
589}
590
591/// Typed view over one obstacle entry in `MiniGame.Player.Obstacles`.
592#[derive(Debug, Clone, PartialEq, Default)]
593pub struct AreMiniGameObstacle {
594    /// Obstacle name resref (`Name`).
595    pub name: ResRef,
596}
597
598impl AreMiniGameObstacle {
599    fn from_struct(structure: &GffStruct) -> Self {
600        Self {
601            name: get_resref(structure, "Name").unwrap_or_default(),
602        }
603    }
604
605    fn to_struct(&self) -> GffStruct {
606        let mut s = GffStruct::new(0);
607        upsert_field(&mut s, "Name", GffValue::ResRef(self.name));
608        s
609    }
610}
611
612impl Default for Are {
613    fn default() -> Self {
614        Self {
615            unused_id: 0,
616            creator_id: 0,
617            version: 0,
618            tag: String::new(),
619            name: GffLocalizedString::new(StrRef::invalid()),
620            comment: String::new(),
621            alpha_test: 0.0,
622            camera_style: 0,
623            default_envmap: ResRef::blank(),
624            restrict_mode: 0,
625            grass_texture: ResRef::blank(),
626            grass_density: 0.0,
627            grass_size: 0.0,
628            grass_prob_ll: 0.0,
629            grass_prob_lr: 0.0,
630            grass_prob_ul: 0.0,
631            grass_prob_ur: 0.0,
632            fog_enabled: false,
633            fog_near: 0.0,
634            fog_far: 0.0,
635            shadows: false,
636            shadow_opacity: 0,
637            wind_power: 0,
638            unescapable: false,
639            disable_transit: false,
640            stealth_xp: false,
641            stealth_xp_loss: 0,
642            stealth_xp_max: 0,
643            stealth_xp_current: 0,
644            on_enter: ResRef::blank(),
645            on_exit: ResRef::blank(),
646            on_heartbeat: ResRef::blank(),
647            on_user_defined: ResRef::blank(),
648            flags: 0,
649            loadscreen_id: 0,
650            chance_rain: 0,
651            chance_snow: 0,
652            chance_lightning: 0,
653            chance_fog: 0,
654            mod_spot_check: 0,
655            mod_listen_check: 0,
656            moon_ambient_color: 0,
657            moon_diffuse_color: 0,
658            moon_fog_enabled: false,
659            moon_fog_near: 0.0,
660            moon_fog_far: 0.0,
661            moon_fog_color: 0,
662            moon_shadows: false,
663            sun_ambient_color: 0,
664            sun_diffuse_color: 0,
665            dynamic_ambient_color: 0,
666            sun_fog_color: 0,
667            grass_ambient_color: 0,
668            grass_diffuse_color: 0,
669            grass_emissive_color: 0,
670            dirty_argb_one: 0,
671            dirty_size_one: 0,
672            dirty_formula_one: 0,
673            dirty_func_one: 0,
674            dirty_argb_two: 0,
675            dirty_size_two: 0,
676            dirty_formula_two: 0,
677            dirty_func_two: 0,
678            dirty_argb_three: 0,
679            dirty_size_three: 0,
680            dirty_formula_three: 0,
681            dirty_func_three: 0,
682            is_night: false,
683            lighting_scheme: 0,
684            day_night_cycle: 0,
685            no_rest: false,
686            no_hang_back: false,
687            player_only: false,
688            player_vs_player: 0,
689            trans_pending: 0,
690            trans_pend_next_id: 0,
691            trans_pend_curr_id: 0,
692            map: AreMap::default(),
693            rooms: Vec::new(),
694            expansion_list: Vec::new(),
695            mini_game: None,
696        }
697    }
698}
699
700impl Are {
701    /// Creates an empty ARE value.
702    pub fn new() -> Self {
703        Self::default()
704    }
705
706    /// Builds typed ARE data from a parsed GFF container.
707    pub fn from_gff(gff: &Gff) -> Result<Self, AreError> {
708        if gff.file_type != *b"ARE " && gff.file_type != *b"GFF " {
709            return Err(AreError::UnsupportedFileType(gff.file_type));
710        }
711
712        let root = &gff.root;
713
714        let map = match root.field("Map") {
715            Some(GffValue::Struct(map_struct)) => AreMap::from_struct(map_struct),
716            Some(_) => {
717                return Err(AreError::TypeMismatch {
718                    field: "Map",
719                    expected: "Struct",
720                });
721            }
722            None => AreMap::default(),
723        };
724
725        let rooms = match root.field("Rooms") {
726            Some(GffValue::List(room_structs)) => room_structs
727                .iter()
728                .map(AreRoom::from_struct)
729                .collect::<Result<Vec<_>, _>>()?,
730            Some(_) => {
731                return Err(AreError::TypeMismatch {
732                    field: "Rooms",
733                    expected: "List",
734                });
735            }
736            None => Vec::new(),
737        };
738
739        let expansion_list = match root.field("Expansion_List") {
740            Some(GffValue::List(expansion_structs)) => expansion_structs
741                .iter()
742                .map(AreExpansionEntry::from_struct)
743                .collect::<Result<Vec<_>, _>>()?,
744            Some(_) => {
745                return Err(AreError::TypeMismatch {
746                    field: "Expansion_List",
747                    expected: "List",
748                });
749            }
750            None => Vec::new(),
751        };
752
753        let mini_game = match root.field("MiniGame") {
754            Some(GffValue::Struct(s)) => Some(AreMiniGame::from_struct(s)?),
755            Some(_) => {
756                return Err(AreError::TypeMismatch {
757                    field: "MiniGame",
758                    expected: "Struct",
759                });
760            }
761            None => None,
762        };
763
764        Ok(Self {
765            unused_id: get_i32(root, "ID").unwrap_or(0),
766            creator_id: get_i32(root, "Creator_ID").unwrap_or(0),
767            version: get_u32(root, "Version").unwrap_or(0),
768            tag: get_string(root, "Tag").unwrap_or_default(),
769            name: get_locstring(root, "Name")
770                .cloned()
771                .unwrap_or_else(|| GffLocalizedString::new(StrRef::invalid())),
772            comment: get_string(root, "Comments").unwrap_or_default(),
773            alpha_test: get_f32(root, "AlphaTest").unwrap_or(0.0),
774            camera_style: get_i32(root, "CameraStyle").unwrap_or(0),
775            default_envmap: get_resref(root, "DefaultEnvMap").unwrap_or_default(),
776            restrict_mode: get_u8(root, "RestrictMode").unwrap_or(0),
777            grass_texture: get_resref(root, "Grass_TexName").unwrap_or_default(),
778            grass_density: get_f32(root, "Grass_Density").unwrap_or(0.0),
779            grass_size: get_f32(root, "Grass_QuadSize").unwrap_or(0.0),
780            grass_prob_ll: get_f32(root, "Grass_Prob_LL").unwrap_or(0.0),
781            grass_prob_lr: get_f32(root, "Grass_Prob_LR").unwrap_or(0.0),
782            grass_prob_ul: get_f32(root, "Grass_Prob_UL").unwrap_or(0.0),
783            grass_prob_ur: get_f32(root, "Grass_Prob_UR").unwrap_or(0.0),
784            fog_enabled: get_bool(root, "SunFogOn").unwrap_or(false),
785            fog_near: get_f32(root, "SunFogNear").unwrap_or(0.0),
786            fog_far: get_f32(root, "SunFogFar").unwrap_or(0.0),
787            shadows: get_bool(root, "SunShadows").unwrap_or(false),
788            shadow_opacity: get_u8(root, "ShadowOpacity").unwrap_or(0),
789            wind_power: get_i32(root, "WindPower").unwrap_or(0),
790            unescapable: get_bool(root, "Unescapable").unwrap_or(false),
791            disable_transit: get_bool(root, "DisableTransit").unwrap_or(false),
792            stealth_xp: get_bool(root, "StealthXPEnabled").unwrap_or(false),
793            stealth_xp_loss: get_u32(root, "StealthXPLoss").unwrap_or(0),
794            stealth_xp_max: get_u32(root, "StealthXPMax").unwrap_or(0),
795            stealth_xp_current: get_u32(root, "StealthXPCurrent").unwrap_or(0),
796            on_enter: get_resref(root, "OnEnter").unwrap_or_default(),
797            on_exit: get_resref(root, "OnExit").unwrap_or_default(),
798            on_heartbeat: get_resref(root, "OnHeartbeat").unwrap_or_default(),
799            on_user_defined: get_resref(root, "OnUserDefined").unwrap_or_default(),
800            flags: get_u32(root, "Flags").unwrap_or(0),
801            loadscreen_id: get_u16(root, "LoadScreenID").unwrap_or(0),
802            chance_rain: get_i32(root, "ChanceRain").unwrap_or(0),
803            chance_snow: get_i32(root, "ChanceSnow").unwrap_or(0),
804            chance_lightning: get_i32(root, "ChanceLightning").unwrap_or(0),
805            chance_fog: get_i32(root, "ChanceFog").unwrap_or(0),
806            mod_spot_check: get_i32(root, "ModSpotCheck").unwrap_or(0),
807            mod_listen_check: get_i32(root, "ModListenCheck").unwrap_or(0),
808            moon_ambient_color: get_u32(root, "MoonAmbientColor").unwrap_or(0),
809            moon_diffuse_color: get_u32(root, "MoonDiffuseColor").unwrap_or(0),
810            moon_fog_enabled: get_bool(root, "MoonFogOn").unwrap_or(false),
811            moon_fog_near: get_f32(root, "MoonFogNear").unwrap_or(0.0),
812            moon_fog_far: get_f32(root, "MoonFogFar").unwrap_or(0.0),
813            moon_fog_color: get_u32(root, "MoonFogColor").unwrap_or(0),
814            moon_shadows: get_bool(root, "MoonShadows").unwrap_or(false),
815            sun_ambient_color: get_u32(root, "SunAmbientColor").unwrap_or(0),
816            sun_diffuse_color: get_u32(root, "SunDiffuseColor").unwrap_or(0),
817            dynamic_ambient_color: get_u32(root, "DynAmbientColor").unwrap_or(0),
818            sun_fog_color: get_u32(root, "SunFogColor").unwrap_or(0),
819            grass_ambient_color: get_u32(root, "Grass_Ambient").unwrap_or(0),
820            grass_diffuse_color: get_u32(root, "Grass_Diffuse").unwrap_or(0),
821            grass_emissive_color: get_u32(root, "Grass_Emissive").unwrap_or(0),
822            dirty_argb_one: get_i32(root, "DirtyARGBOne").unwrap_or(0),
823            dirty_size_one: get_i32(root, "DirtySizeOne").unwrap_or(0),
824            dirty_formula_one: get_i32(root, "DirtyFormulaOne").unwrap_or(0),
825            dirty_func_one: get_i32(root, "DirtyFuncOne").unwrap_or(0),
826            dirty_argb_two: get_i32(root, "DirtyARGBTwo").unwrap_or(0),
827            dirty_size_two: get_i32(root, "DirtySizeTwo").unwrap_or(0),
828            dirty_formula_two: get_i32(root, "DirtyFormulaTwo").unwrap_or(0),
829            dirty_func_two: get_i32(root, "DirtyFuncTwo").unwrap_or(0),
830            dirty_argb_three: get_i32(root, "DirtyARGBThree").unwrap_or(0),
831            dirty_size_three: get_i32(root, "DirtySizeThree").unwrap_or(0),
832            dirty_formula_three: get_i32(root, "DirtyFormulaThre").unwrap_or(0),
833            dirty_func_three: get_i32(root, "DirtyFuncThree").unwrap_or(0),
834            is_night: get_bool(root, "IsNight").unwrap_or(false),
835            lighting_scheme: get_u8(root, "LightingScheme").unwrap_or(0),
836            day_night_cycle: get_u8(root, "DayNightCycle").unwrap_or(0),
837            no_rest: get_bool(root, "NoRest").unwrap_or(false),
838            no_hang_back: get_bool(root, "NoHangBack").unwrap_or(false),
839            player_only: get_bool(root, "PlayerOnly").unwrap_or(false),
840            player_vs_player: get_u8(root, "PlayerVsPlayer").unwrap_or(0),
841            trans_pending: get_u8(root, "TransPending").unwrap_or(0),
842            trans_pend_next_id: get_u8(root, "TransPendNextID").unwrap_or(0),
843            trans_pend_curr_id: get_u8(root, "TransPendCurrID").unwrap_or(0),
844            map,
845            rooms,
846            expansion_list,
847            mini_game,
848        })
849    }
850
851    /// Converts this typed ARE value into a GFF container.
852    pub fn to_gff(&self) -> Gff {
853        let mut root = GffStruct::new(-1);
854
855        upsert_field(&mut root, "ID", GffValue::Int32(self.unused_id));
856        upsert_field(&mut root, "Creator_ID", GffValue::Int32(self.creator_id));
857        upsert_field(&mut root, "Version", GffValue::UInt32(self.version));
858        upsert_field(&mut root, "Tag", GffValue::String(self.tag.clone()));
859        upsert_field(
860            &mut root,
861            "Name",
862            GffValue::LocalizedString(self.name.clone()),
863        );
864        upsert_field(
865            &mut root,
866            "Comments",
867            GffValue::String(self.comment.clone()),
868        );
869        upsert_field(&mut root, "AlphaTest", GffValue::Single(self.alpha_test));
870        upsert_field(&mut root, "CameraStyle", GffValue::Int32(self.camera_style));
871        upsert_field(
872            &mut root,
873            "DefaultEnvMap",
874            GffValue::ResRef(self.default_envmap),
875        );
876        upsert_field(
877            &mut root,
878            "RestrictMode",
879            GffValue::UInt8(self.restrict_mode),
880        );
881        upsert_field(
882            &mut root,
883            "Grass_TexName",
884            GffValue::ResRef(self.grass_texture),
885        );
886        upsert_field(
887            &mut root,
888            "Grass_Density",
889            GffValue::Single(self.grass_density),
890        );
891        upsert_field(
892            &mut root,
893            "Grass_QuadSize",
894            GffValue::Single(self.grass_size),
895        );
896        upsert_field(
897            &mut root,
898            "Grass_Prob_LL",
899            GffValue::Single(self.grass_prob_ll),
900        );
901        upsert_field(
902            &mut root,
903            "Grass_Prob_LR",
904            GffValue::Single(self.grass_prob_lr),
905        );
906        upsert_field(
907            &mut root,
908            "Grass_Prob_UL",
909            GffValue::Single(self.grass_prob_ul),
910        );
911        upsert_field(
912            &mut root,
913            "Grass_Prob_UR",
914            GffValue::Single(self.grass_prob_ur),
915        );
916        upsert_field(
917            &mut root,
918            "SunFogOn",
919            GffValue::UInt8(u8::from(self.fog_enabled)),
920        );
921        upsert_field(&mut root, "SunFogNear", GffValue::Single(self.fog_near));
922        upsert_field(&mut root, "SunFogFar", GffValue::Single(self.fog_far));
923        upsert_field(
924            &mut root,
925            "SunShadows",
926            GffValue::UInt8(u8::from(self.shadows)),
927        );
928        upsert_field(
929            &mut root,
930            "ShadowOpacity",
931            GffValue::UInt8(self.shadow_opacity),
932        );
933        upsert_field(&mut root, "WindPower", GffValue::Int32(self.wind_power));
934        upsert_field(
935            &mut root,
936            "Unescapable",
937            GffValue::UInt8(u8::from(self.unescapable)),
938        );
939        upsert_field(
940            &mut root,
941            "DisableTransit",
942            GffValue::UInt8(u8::from(self.disable_transit)),
943        );
944        upsert_field(
945            &mut root,
946            "StealthXPEnabled",
947            GffValue::UInt8(u8::from(self.stealth_xp)),
948        );
949        upsert_field(
950            &mut root,
951            "StealthXPLoss",
952            GffValue::UInt32(self.stealth_xp_loss),
953        );
954        upsert_field(
955            &mut root,
956            "StealthXPMax",
957            GffValue::UInt32(self.stealth_xp_max),
958        );
959        upsert_field(
960            &mut root,
961            "StealthXPCurrent",
962            GffValue::UInt32(self.stealth_xp_current),
963        );
964        upsert_field(&mut root, "OnEnter", GffValue::ResRef(self.on_enter));
965        upsert_field(&mut root, "OnExit", GffValue::ResRef(self.on_exit));
966        upsert_field(
967            &mut root,
968            "OnHeartbeat",
969            GffValue::ResRef(self.on_heartbeat),
970        );
971        upsert_field(
972            &mut root,
973            "OnUserDefined",
974            GffValue::ResRef(self.on_user_defined),
975        );
976        upsert_field(&mut root, "Flags", GffValue::UInt32(self.flags));
977        upsert_field(
978            &mut root,
979            "LoadScreenID",
980            GffValue::UInt16(self.loadscreen_id),
981        );
982        upsert_field(&mut root, "ChanceRain", GffValue::Int32(self.chance_rain));
983        upsert_field(&mut root, "ChanceSnow", GffValue::Int32(self.chance_snow));
984        upsert_field(
985            &mut root,
986            "ChanceLightning",
987            GffValue::Int32(self.chance_lightning),
988        );
989        upsert_field(&mut root, "ChanceFog", GffValue::Int32(self.chance_fog));
990        upsert_field(
991            &mut root,
992            "ModSpotCheck",
993            GffValue::Int32(self.mod_spot_check),
994        );
995        upsert_field(
996            &mut root,
997            "ModListenCheck",
998            GffValue::Int32(self.mod_listen_check),
999        );
1000        upsert_field(
1001            &mut root,
1002            "MoonAmbientColor",
1003            GffValue::UInt32(self.moon_ambient_color),
1004        );
1005        upsert_field(
1006            &mut root,
1007            "MoonDiffuseColor",
1008            GffValue::UInt32(self.moon_diffuse_color),
1009        );
1010        upsert_field(
1011            &mut root,
1012            "MoonFogOn",
1013            GffValue::UInt8(u8::from(self.moon_fog_enabled)),
1014        );
1015        upsert_field(
1016            &mut root,
1017            "MoonFogNear",
1018            GffValue::Single(self.moon_fog_near),
1019        );
1020        upsert_field(&mut root, "MoonFogFar", GffValue::Single(self.moon_fog_far));
1021        upsert_field(
1022            &mut root,
1023            "MoonFogColor",
1024            GffValue::UInt32(self.moon_fog_color),
1025        );
1026        upsert_field(
1027            &mut root,
1028            "MoonShadows",
1029            GffValue::UInt8(u8::from(self.moon_shadows)),
1030        );
1031        upsert_field(
1032            &mut root,
1033            "SunAmbientColor",
1034            GffValue::UInt32(self.sun_ambient_color),
1035        );
1036        upsert_field(
1037            &mut root,
1038            "SunDiffuseColor",
1039            GffValue::UInt32(self.sun_diffuse_color),
1040        );
1041        upsert_field(
1042            &mut root,
1043            "DynAmbientColor",
1044            GffValue::UInt32(self.dynamic_ambient_color),
1045        );
1046        upsert_field(
1047            &mut root,
1048            "SunFogColor",
1049            GffValue::UInt32(self.sun_fog_color),
1050        );
1051        upsert_field(
1052            &mut root,
1053            "Grass_Ambient",
1054            GffValue::UInt32(self.grass_ambient_color),
1055        );
1056        upsert_field(
1057            &mut root,
1058            "Grass_Diffuse",
1059            GffValue::UInt32(self.grass_diffuse_color),
1060        );
1061        upsert_field(
1062            &mut root,
1063            "Grass_Emissive",
1064            GffValue::UInt32(self.grass_emissive_color),
1065        );
1066        upsert_field(
1067            &mut root,
1068            "DirtyARGBOne",
1069            GffValue::Int32(self.dirty_argb_one),
1070        );
1071        upsert_field(
1072            &mut root,
1073            "DirtySizeOne",
1074            GffValue::Int32(self.dirty_size_one),
1075        );
1076        upsert_field(
1077            &mut root,
1078            "DirtyFormulaOne",
1079            GffValue::Int32(self.dirty_formula_one),
1080        );
1081        upsert_field(
1082            &mut root,
1083            "DirtyFuncOne",
1084            GffValue::Int32(self.dirty_func_one),
1085        );
1086        upsert_field(
1087            &mut root,
1088            "DirtyARGBTwo",
1089            GffValue::Int32(self.dirty_argb_two),
1090        );
1091        upsert_field(
1092            &mut root,
1093            "DirtySizeTwo",
1094            GffValue::Int32(self.dirty_size_two),
1095        );
1096        upsert_field(
1097            &mut root,
1098            "DirtyFormulaTwo",
1099            GffValue::Int32(self.dirty_formula_two),
1100        );
1101        upsert_field(
1102            &mut root,
1103            "DirtyFuncTwo",
1104            GffValue::Int32(self.dirty_func_two),
1105        );
1106        upsert_field(
1107            &mut root,
1108            "DirtyARGBThree",
1109            GffValue::Int32(self.dirty_argb_three),
1110        );
1111        upsert_field(
1112            &mut root,
1113            "DirtySizeThree",
1114            GffValue::Int32(self.dirty_size_three),
1115        );
1116        upsert_field(
1117            &mut root,
1118            "DirtyFormulaThre",
1119            GffValue::Int32(self.dirty_formula_three),
1120        );
1121        upsert_field(
1122            &mut root,
1123            "DirtyFuncThree",
1124            GffValue::Int32(self.dirty_func_three),
1125        );
1126        upsert_field(
1127            &mut root,
1128            "IsNight",
1129            GffValue::UInt8(u8::from(self.is_night)),
1130        );
1131        upsert_field(
1132            &mut root,
1133            "LightingScheme",
1134            GffValue::UInt8(self.lighting_scheme),
1135        );
1136        upsert_field(
1137            &mut root,
1138            "DayNightCycle",
1139            GffValue::UInt8(self.day_night_cycle),
1140        );
1141        upsert_field(&mut root, "NoRest", GffValue::UInt8(u8::from(self.no_rest)));
1142        upsert_field(
1143            &mut root,
1144            "NoHangBack",
1145            GffValue::UInt8(u8::from(self.no_hang_back)),
1146        );
1147        upsert_field(
1148            &mut root,
1149            "PlayerOnly",
1150            GffValue::UInt8(u8::from(self.player_only)),
1151        );
1152        upsert_field(
1153            &mut root,
1154            "PlayerVsPlayer",
1155            GffValue::UInt8(self.player_vs_player),
1156        );
1157        upsert_field(
1158            &mut root,
1159            "TransPending",
1160            GffValue::UInt8(self.trans_pending),
1161        );
1162        upsert_field(
1163            &mut root,
1164            "TransPendNextID",
1165            GffValue::UInt8(self.trans_pend_next_id),
1166        );
1167        upsert_field(
1168            &mut root,
1169            "TransPendCurrID",
1170            GffValue::UInt8(self.trans_pend_curr_id),
1171        );
1172
1173        let map_struct = self.map.to_struct();
1174        upsert_field(&mut root, "Map", GffValue::Struct(Box::new(map_struct)));
1175
1176        let room_structs = self
1177            .rooms
1178            .iter()
1179            .map(AreRoom::to_struct)
1180            .collect::<Vec<GffStruct>>();
1181        upsert_field(&mut root, "Rooms", GffValue::List(room_structs));
1182        let expansion_structs = self
1183            .expansion_list
1184            .iter()
1185            .map(AreExpansionEntry::to_struct)
1186            .collect::<Vec<GffStruct>>();
1187        upsert_field(
1188            &mut root,
1189            "Expansion_List",
1190            GffValue::List(expansion_structs),
1191        );
1192
1193        if let Some(mg) = &self.mini_game {
1194            upsert_field(
1195                &mut root,
1196                "MiniGame",
1197                GffValue::Struct(Box::new(mg.to_struct())),
1198            );
1199        }
1200
1201        Gff::new(*b"ARE ", root)
1202    }
1203}
1204
1205/// Typed view over the ARE `Map` nested struct.
1206#[derive(Debug, Clone, PartialEq)]
1207pub struct AreMap {
1208    /// Map north-axis selection (`NorthAxis`).
1209    pub north_axis: i32,
1210    /// Map zoom level (`MapZoom`).
1211    pub map_zoom: i32,
1212    /// Horizontal map resolution (`MapResX`).
1213    pub map_res_x: i32,
1214    /// UI map point 1 (`MapPt1X`, `MapPt1Y`).
1215    pub map_point_1: [f32; 2],
1216    /// UI map point 2 (`MapPt2X`, `MapPt2Y`).
1217    pub map_point_2: [f32; 2],
1218    /// World point 1 (`WorldPt1X`, `WorldPt1Y`).
1219    pub world_point_1: [f32; 2],
1220    /// World point 2 (`WorldPt2X`, `WorldPt2Y`).
1221    pub world_point_2: [f32; 2],
1222}
1223
1224impl Default for AreMap {
1225    fn default() -> Self {
1226        Self {
1227            north_axis: 0,
1228            map_zoom: 0,
1229            map_res_x: 0,
1230            map_point_1: [0.0, 0.0],
1231            map_point_2: [0.0, 0.0],
1232            world_point_1: [0.0, 0.0],
1233            world_point_2: [0.0, 0.0],
1234        }
1235    }
1236}
1237
1238impl AreMap {
1239    fn from_struct(structure: &GffStruct) -> Self {
1240        Self {
1241            north_axis: get_i32(structure, "NorthAxis").unwrap_or(0),
1242            map_zoom: get_i32(structure, "MapZoom").unwrap_or(0),
1243            map_res_x: get_i32(structure, "MapResX").unwrap_or(0),
1244            map_point_1: [
1245                get_map_point_f32(structure, "MapPt1X").unwrap_or(0.0),
1246                get_map_point_f32(structure, "MapPt1Y").unwrap_or(0.0),
1247            ],
1248            map_point_2: [
1249                get_map_point_f32(structure, "MapPt2X").unwrap_or(0.0),
1250                get_map_point_f32(structure, "MapPt2Y").unwrap_or(0.0),
1251            ],
1252            world_point_1: [
1253                get_f32(structure, "WorldPt1X").unwrap_or(0.0),
1254                get_f32(structure, "WorldPt1Y").unwrap_or(0.0),
1255            ],
1256            world_point_2: [
1257                get_f32(structure, "WorldPt2X").unwrap_or(0.0),
1258                get_f32(structure, "WorldPt2Y").unwrap_or(0.0),
1259            ],
1260        }
1261    }
1262
1263    fn to_struct(&self) -> GffStruct {
1264        let mut structure = GffStruct::new(0);
1265        upsert_field(
1266            &mut structure,
1267            "NorthAxis",
1268            GffValue::Int32(self.north_axis),
1269        );
1270        upsert_field(&mut structure, "MapZoom", GffValue::Int32(self.map_zoom));
1271        upsert_field(&mut structure, "MapResX", GffValue::Int32(self.map_res_x));
1272        upsert_field(
1273            &mut structure,
1274            "MapPt1X",
1275            GffValue::Single(self.map_point_1[0]),
1276        );
1277        upsert_field(
1278            &mut structure,
1279            "MapPt1Y",
1280            GffValue::Single(self.map_point_1[1]),
1281        );
1282        upsert_field(
1283            &mut structure,
1284            "MapPt2X",
1285            GffValue::Single(self.map_point_2[0]),
1286        );
1287        upsert_field(
1288            &mut structure,
1289            "MapPt2Y",
1290            GffValue::Single(self.map_point_2[1]),
1291        );
1292        upsert_field(
1293            &mut structure,
1294            "WorldPt1X",
1295            GffValue::Single(self.world_point_1[0]),
1296        );
1297        upsert_field(
1298            &mut structure,
1299            "WorldPt1Y",
1300            GffValue::Single(self.world_point_1[1]),
1301        );
1302        upsert_field(
1303            &mut structure,
1304            "WorldPt2X",
1305            GffValue::Single(self.world_point_2[0]),
1306        );
1307        upsert_field(
1308            &mut structure,
1309            "WorldPt2Y",
1310            GffValue::Single(self.world_point_2[1]),
1311        );
1312        structure
1313    }
1314}
1315
1316/// Typed view over one ARE room entry in the `Rooms` list.
1317#[derive(Debug, Clone, PartialEq)]
1318pub struct AreRoom {
1319    /// Room name (`RoomName`).
1320    pub room_name: String,
1321    /// Ambient scale (`AmbientScale`).
1322    pub ambient_scale: f32,
1323    /// Environment audio ID (`EnvAudio`).
1324    pub env_audio: i32,
1325    /// Force-rating value (`ForceRating`, K2).
1326    pub force_rating: i32,
1327    /// Weather-disable flag (`DisableWeather`, K2).
1328    pub disable_weather: bool,
1329    /// Per-room part sound definitions (`PartSounds` list).
1330    pub part_sounds: Vec<ArePartSound>,
1331}
1332
1333impl AreRoom {
1334    fn from_struct(structure: &GffStruct) -> Result<Self, AreError> {
1335        let part_sounds = match structure.field("PartSounds") {
1336            Some(GffValue::List(part_sound_structs)) => part_sound_structs
1337                .iter()
1338                .map(ArePartSound::from_struct)
1339                .collect::<Result<Vec<_>, _>>()?,
1340            Some(_) => {
1341                return Err(AreError::TypeMismatch {
1342                    field: "Rooms[].PartSounds",
1343                    expected: "List",
1344                });
1345            }
1346            None => Vec::new(),
1347        };
1348
1349        Ok(Self {
1350            room_name: get_string(structure, "RoomName").unwrap_or_default(),
1351            ambient_scale: get_f32(structure, "AmbientScale").unwrap_or(0.0),
1352            env_audio: get_i32(structure, "EnvAudio").unwrap_or(0),
1353            force_rating: get_i32(structure, "ForceRating").unwrap_or(0),
1354            disable_weather: get_bool(structure, "DisableWeather").unwrap_or(false),
1355            part_sounds,
1356        })
1357    }
1358
1359    fn to_struct(&self) -> GffStruct {
1360        let mut structure = GffStruct::new(0);
1361        upsert_field(
1362            &mut structure,
1363            "RoomName",
1364            GffValue::String(self.room_name.clone()),
1365        );
1366        upsert_field(
1367            &mut structure,
1368            "AmbientScale",
1369            GffValue::Single(self.ambient_scale),
1370        );
1371        upsert_field(&mut structure, "EnvAudio", GffValue::Int32(self.env_audio));
1372        upsert_field(
1373            &mut structure,
1374            "ForceRating",
1375            GffValue::Int32(self.force_rating),
1376        );
1377        upsert_field(
1378            &mut structure,
1379            "DisableWeather",
1380            GffValue::UInt8(u8::from(self.disable_weather)),
1381        );
1382        let part_sound_structs = self
1383            .part_sounds
1384            .iter()
1385            .map(ArePartSound::to_struct)
1386            .collect::<Vec<GffStruct>>();
1387        upsert_field(
1388            &mut structure,
1389            "PartSounds",
1390            GffValue::List(part_sound_structs),
1391        );
1392        structure
1393    }
1394}
1395
1396/// Typed view over one ARE expansion-list entry.
1397#[derive(Debug, Clone, PartialEq)]
1398pub struct AreExpansionEntry {
1399    /// Localized expansion name (`Expansion_Name`).
1400    pub expansion_name: GffLocalizedString,
1401    /// Expansion identifier (`Expansion_ID`).
1402    pub expansion_id: i32,
1403}
1404
1405impl AreExpansionEntry {
1406    fn from_struct(structure: &GffStruct) -> Result<Self, AreError> {
1407        let expansion_name = match structure.field("Expansion_Name") {
1408            Some(GffValue::LocalizedString(value)) => value.clone(),
1409            Some(_) => {
1410                return Err(AreError::TypeMismatch {
1411                    field: "Expansion_List[].Expansion_Name",
1412                    expected: "LocalizedString",
1413                });
1414            }
1415            None => GffLocalizedString::new(StrRef::invalid()),
1416        };
1417
1418        Ok(Self {
1419            expansion_name,
1420            expansion_id: get_i32(structure, "Expansion_ID").unwrap_or(0),
1421        })
1422    }
1423
1424    fn to_struct(&self) -> GffStruct {
1425        let mut structure = GffStruct::new(0);
1426        upsert_field(
1427            &mut structure,
1428            "Expansion_Name",
1429            GffValue::LocalizedString(self.expansion_name.clone()),
1430        );
1431        upsert_field(
1432            &mut structure,
1433            "Expansion_ID",
1434            GffValue::Int32(self.expansion_id),
1435        );
1436        structure
1437    }
1438}
1439
1440/// Typed view over one room part-sound entry (`Rooms[].PartSounds[]`).
1441#[derive(Debug, Clone, PartialEq)]
1442pub struct ArePartSound {
1443    /// Looping flag (`Looping`).
1444    pub looping: bool,
1445    /// Model part identifier (`ModelPart`).
1446    pub model_part: String,
1447    /// Optional omen event token (`OmenEvent`).
1448    pub omen_event: String,
1449    /// Sound resref (`Sound`).
1450    pub sound: ResRef,
1451}
1452
1453impl ArePartSound {
1454    fn from_struct(structure: &GffStruct) -> Result<Self, AreError> {
1455        let looping = match structure.field("Looping") {
1456            Some(GffValue::UInt8(value)) => *value != 0,
1457            Some(GffValue::Int8(value)) => *value != 0,
1458            Some(GffValue::UInt16(value)) => *value != 0,
1459            Some(GffValue::Int16(value)) => *value != 0,
1460            Some(GffValue::UInt32(value)) => *value != 0,
1461            Some(GffValue::Int32(value)) => *value != 0,
1462            Some(_) => {
1463                return Err(AreError::TypeMismatch {
1464                    field: "Rooms[].PartSounds[].Looping",
1465                    expected: "numeric bool",
1466                });
1467            }
1468            None => false,
1469        };
1470
1471        Ok(Self {
1472            looping,
1473            model_part: get_string(structure, "ModelPart").unwrap_or_default(),
1474            omen_event: get_string(structure, "OmenEvent").unwrap_or_default(),
1475            sound: get_resref(structure, "Sound").unwrap_or_default(),
1476        })
1477    }
1478
1479    fn to_struct(&self) -> GffStruct {
1480        let mut structure = GffStruct::new(0);
1481        upsert_field(
1482            &mut structure,
1483            "Looping",
1484            GffValue::UInt8(u8::from(self.looping)),
1485        );
1486        upsert_field(
1487            &mut structure,
1488            "ModelPart",
1489            GffValue::String(self.model_part.clone()),
1490        );
1491        upsert_field(
1492            &mut structure,
1493            "OmenEvent",
1494            GffValue::String(self.omen_event.clone()),
1495        );
1496        upsert_field(&mut structure, "Sound", GffValue::ResRef(self.sound));
1497        structure
1498    }
1499}
1500
1501/// Errors produced while reading or writing typed ARE data.
1502#[derive(Debug, Error)]
1503pub enum AreError {
1504    /// Source file type is not supported by this parser.
1505    #[error("unsupported ARE file type: {0:?}")]
1506    UnsupportedFileType([u8; 4]),
1507    /// A required container field had an unexpected runtime type.
1508    #[error("ARE field `{field}` has incompatible type (expected {expected})")]
1509    TypeMismatch {
1510        /// Field label where mismatch occurred.
1511        field: &'static str,
1512        /// Expected runtime value kind.
1513        expected: &'static str,
1514    },
1515    /// Underlying GFF parser/writer error.
1516    #[error(transparent)]
1517    Gff(#[from] GffBinaryError),
1518}
1519
1520/// Reads typed ARE data from a reader at the current stream position.
1521#[cfg_attr(
1522    feature = "tracing",
1523    tracing::instrument(level = "debug", skip(reader))
1524)]
1525pub fn read_are<R: Read>(reader: &mut R) -> Result<Are, AreError> {
1526    let gff = read_gff(reader)?;
1527    Are::from_gff(&gff)
1528}
1529
1530/// Reads typed ARE data directly from bytes.
1531#[cfg_attr(
1532    feature = "tracing",
1533    tracing::instrument(level = "debug", skip(bytes), fields(bytes_len = bytes.len()))
1534)]
1535pub fn read_are_from_bytes(bytes: &[u8]) -> Result<Are, AreError> {
1536    let gff = read_gff_from_bytes(bytes)?;
1537    Are::from_gff(&gff)
1538}
1539
1540/// Writes typed ARE data to an output writer.
1541#[cfg_attr(
1542    feature = "tracing",
1543    tracing::instrument(level = "debug", skip(writer, are))
1544)]
1545pub fn write_are<W: Write>(writer: &mut W, are: &Are) -> Result<(), AreError> {
1546    let gff = are.to_gff();
1547    write_gff(writer, &gff)?;
1548    Ok(())
1549}
1550
1551/// Serializes typed ARE data into a byte vector.
1552#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(are)))]
1553pub fn write_are_to_vec(are: &Are) -> Result<Vec<u8>, AreError> {
1554    let mut cursor = Cursor::new(Vec::new());
1555    write_are(&mut cursor, are)?;
1556    Ok(cursor.into_inner())
1557}
1558
1559fn get_map_point_f32(structure: &GffStruct, label: &str) -> Option<f32> {
1560    match structure.field(label) {
1561        Some(GffValue::Single(value)) => Some(*value),
1562        // Intentional precision loss: GFF fields may store coordinates as
1563        // Double/Int32/UInt32 but map points are always f32 in practice.
1564        #[allow(clippy::cast_possible_truncation, clippy::as_conversions)]
1565        Some(GffValue::Double(value)) => Some(*value as f32),
1566        #[allow(clippy::cast_precision_loss, clippy::as_conversions)]
1567        Some(GffValue::Int32(value)) => Some(*value as f32),
1568        #[allow(clippy::cast_precision_loss, clippy::as_conversions)]
1569        Some(GffValue::UInt32(value)) => Some(*value as f32),
1570        Some(GffValue::Int16(value)) => Some(f32::from(*value)),
1571        Some(GffValue::UInt16(value)) => Some(f32::from(*value)),
1572        Some(GffValue::Int8(value)) => Some(f32::from(*value)),
1573        Some(GffValue::UInt8(value)) => Some(f32::from(*value)),
1574        _ => None,
1575    }
1576}
1577
1578/// ARE `Rooms` list entry child schema.
1579static ROOMS_CHILDREN: &[FieldSchema] = &[
1580    FieldSchema {
1581        label: "RoomName",
1582        expected_type: GffType::String,
1583        required: false,
1584        children: None,
1585        constraint: None,
1586    },
1587    FieldSchema {
1588        label: "EnvAudio",
1589        expected_type: GffType::Int32,
1590        required: false,
1591        children: None,
1592        constraint: None,
1593    },
1594    FieldSchema {
1595        label: "AmbientScale",
1596        expected_type: GffType::Single,
1597        required: false,
1598        children: None,
1599        constraint: None,
1600    },
1601    FieldSchema {
1602        label: "ForceRating",
1603        expected_type: GffType::Int32,
1604        required: false,
1605        children: None,
1606        constraint: None,
1607    },
1608    FieldSchema {
1609        label: "DisableWeather",
1610        expected_type: GffType::UInt8,
1611        required: false,
1612        children: None,
1613        constraint: None,
1614    },
1615];
1616
1617/// ARE `Map` struct child schema.
1618static MAP_CHILDREN: &[FieldSchema] = &[
1619    FieldSchema {
1620        label: "MapResX",
1621        expected_type: GffType::Int32,
1622        required: false,
1623        children: None,
1624        constraint: None,
1625    },
1626    FieldSchema {
1627        label: "NorthAxis",
1628        expected_type: GffType::Int32,
1629        required: false,
1630        children: None,
1631        constraint: None,
1632    },
1633    FieldSchema {
1634        label: "MapZoom",
1635        expected_type: GffType::Int32,
1636        required: false,
1637        children: None,
1638        constraint: None,
1639    },
1640    FieldSchema {
1641        label: "MapPt1X",
1642        expected_type: GffType::Single,
1643        required: false,
1644        children: None,
1645        constraint: None,
1646    },
1647    FieldSchema {
1648        label: "MapPt1Y",
1649        expected_type: GffType::Single,
1650        required: false,
1651        children: None,
1652        constraint: None,
1653    },
1654    FieldSchema {
1655        label: "MapPt2X",
1656        expected_type: GffType::Single,
1657        required: false,
1658        children: None,
1659        constraint: None,
1660    },
1661    FieldSchema {
1662        label: "MapPt2Y",
1663        expected_type: GffType::Single,
1664        required: false,
1665        children: None,
1666        constraint: None,
1667    },
1668    FieldSchema {
1669        label: "WorldPt1X",
1670        expected_type: GffType::Single,
1671        required: false,
1672        children: None,
1673        constraint: None,
1674    },
1675    FieldSchema {
1676        label: "WorldPt1Y",
1677        expected_type: GffType::Single,
1678        required: false,
1679        children: None,
1680        constraint: None,
1681    },
1682    FieldSchema {
1683        label: "WorldPt2X",
1684        expected_type: GffType::Single,
1685        required: false,
1686        children: None,
1687        constraint: None,
1688    },
1689    FieldSchema {
1690        label: "WorldPt2Y",
1691        expected_type: GffType::Single,
1692        required: false,
1693        children: None,
1694        constraint: None,
1695    },
1696];
1697
1698/// ARE `Expansion_List` entry child schema.
1699static EXPANSION_LIST_CHILDREN: &[FieldSchema] = &[
1700    FieldSchema {
1701        label: "Expansion_Name",
1702        expected_type: GffType::LocalizedString,
1703        required: false,
1704        children: None,
1705        constraint: None,
1706    },
1707    FieldSchema {
1708        label: "Expansion_ID",
1709        expected_type: GffType::Int32,
1710        required: false,
1711        children: None,
1712        constraint: None,
1713    },
1714];
1715
1716/// ARE `MiniGame` struct child schema.
1717static MINI_GAME_CHILDREN: &[FieldSchema] = &[
1718    FieldSchema {
1719        label: "Type",
1720        expected_type: GffType::UInt32,
1721        required: false,
1722        children: None,
1723        constraint: None,
1724    },
1725    FieldSchema {
1726        label: "MovementPerSec",
1727        expected_type: GffType::Single,
1728        required: false,
1729        children: None,
1730        constraint: None,
1731    },
1732    FieldSchema {
1733        label: "LateralAccel",
1734        expected_type: GffType::Single,
1735        required: false,
1736        children: None,
1737        constraint: None,
1738    },
1739    FieldSchema {
1740        label: "Bump_Plane",
1741        expected_type: GffType::UInt32,
1742        required: false,
1743        children: None,
1744        constraint: None,
1745    },
1746    FieldSchema {
1747        label: "DoBumping",
1748        expected_type: GffType::UInt8,
1749        required: false,
1750        children: None,
1751        constraint: None,
1752    },
1753    FieldSchema {
1754        label: "UseInertia",
1755        expected_type: GffType::UInt8,
1756        required: false,
1757        children: None,
1758        constraint: None,
1759    },
1760    FieldSchema {
1761        label: "DOF",
1762        expected_type: GffType::UInt32,
1763        required: false,
1764        children: None,
1765        constraint: None,
1766    },
1767    FieldSchema {
1768        label: "Music",
1769        expected_type: GffType::ResRef,
1770        required: false,
1771        children: None,
1772        constraint: None,
1773    },
1774    FieldSchema {
1775        label: "Far_Clip",
1776        expected_type: GffType::Single,
1777        required: false,
1778        children: None,
1779        constraint: None,
1780    },
1781    FieldSchema {
1782        label: "Near_Clip",
1783        expected_type: GffType::Single,
1784        required: false,
1785        children: None,
1786        constraint: None,
1787    },
1788    FieldSchema {
1789        label: "CameraViewAngle",
1790        expected_type: GffType::Single,
1791        required: false,
1792        children: None,
1793        constraint: None,
1794    },
1795    FieldSchema {
1796        label: "Player",
1797        expected_type: GffType::Struct,
1798        required: false,
1799        children: None,
1800        constraint: None,
1801    },
1802];
1803
1804impl GffSchema for Are {
1805    fn schema() -> &'static [FieldSchema] {
1806        static SCHEMA: &[FieldSchema] = &[
1807            // ===== Identity =====
1808            FieldSchema {
1809                label: "ID",
1810                expected_type: GffType::Int32,
1811                required: false,
1812                children: None,
1813                constraint: None,
1814            },
1815            FieldSchema {
1816                label: "Creator_ID",
1817                expected_type: GffType::Int32,
1818                required: false,
1819                children: None,
1820                constraint: None,
1821            },
1822            FieldSchema {
1823                label: "Version",
1824                expected_type: GffType::UInt32,
1825                required: false,
1826                children: None,
1827                constraint: None,
1828            },
1829            FieldSchema {
1830                label: "Comments",
1831                expected_type: GffType::String,
1832                required: false,
1833                children: None,
1834                constraint: None,
1835            },
1836            FieldSchema {
1837                label: "Name",
1838                expected_type: GffType::LocalizedString,
1839                required: false,
1840                children: None,
1841                constraint: None,
1842            },
1843            FieldSchema {
1844                label: "Tag",
1845                expected_type: GffType::String,
1846                required: false,
1847                children: None,
1848                constraint: None,
1849            },
1850            // ===== Scripts (4) =====
1851            FieldSchema {
1852                label: "OnHeartbeat",
1853                expected_type: GffType::ResRef,
1854                required: false,
1855                children: None,
1856                constraint: None,
1857            },
1858            FieldSchema {
1859                label: "OnUserDefined",
1860                expected_type: GffType::ResRef,
1861                required: false,
1862                children: None,
1863                constraint: None,
1864            },
1865            FieldSchema {
1866                label: "OnEnter",
1867                expected_type: GffType::ResRef,
1868                required: false,
1869                children: None,
1870                constraint: None,
1871            },
1872            FieldSchema {
1873                label: "OnExit",
1874                expected_type: GffType::ResRef,
1875                required: false,
1876                children: None,
1877                constraint: None,
1878            },
1879            // ===== Flags & mode =====
1880            FieldSchema {
1881                label: "Flags",
1882                expected_type: GffType::UInt32,
1883                required: false,
1884                children: None,
1885                constraint: None,
1886            },
1887            FieldSchema {
1888                label: "CameraStyle",
1889                expected_type: GffType::Int32,
1890                required: false,
1891                children: None,
1892                constraint: None,
1893            },
1894            FieldSchema {
1895                label: "DefaultEnvMap",
1896                expected_type: GffType::ResRef,
1897                required: false,
1898                children: None,
1899                constraint: None,
1900            },
1901            FieldSchema {
1902                label: "Unescapable",
1903                expected_type: GffType::UInt8,
1904                required: false,
1905                children: None,
1906                constraint: None,
1907            },
1908            FieldSchema {
1909                label: "RestrictMode",
1910                expected_type: GffType::UInt8,
1911                required: false,
1912                children: None,
1913                constraint: None,
1914            },
1915            // ===== Weather (5) =====
1916            FieldSchema {
1917                label: "ChanceRain",
1918                expected_type: GffType::Int32,
1919                required: false,
1920                children: None,
1921                constraint: Some(FieldConstraint::RangeInt(0, 100)),
1922            },
1923            FieldSchema {
1924                label: "ChanceSnow",
1925                expected_type: GffType::Int32,
1926                required: false,
1927                children: None,
1928                constraint: Some(FieldConstraint::RangeInt(0, 100)),
1929            },
1930            FieldSchema {
1931                label: "ChanceLightning",
1932                expected_type: GffType::Int32,
1933                required: false,
1934                children: None,
1935                constraint: Some(FieldConstraint::RangeInt(0, 100)),
1936            },
1937            FieldSchema {
1938                label: "WindPower",
1939                expected_type: GffType::Int32,
1940                required: false,
1941                children: None,
1942                constraint: Some(FieldConstraint::RangeInt(0, 2)),
1943            },
1944            FieldSchema {
1945                label: "ChanceFog",
1946                expected_type: GffType::Int32,
1947                required: false,
1948                children: None,
1949                constraint: None,
1950            },
1951            // ===== Lighting (20) =====
1952            FieldSchema {
1953                label: "MoonAmbientColor",
1954                expected_type: GffType::UInt32,
1955                required: false,
1956                children: None,
1957                constraint: None,
1958            },
1959            FieldSchema {
1960                label: "MoonDiffuseColor",
1961                expected_type: GffType::UInt32,
1962                required: false,
1963                children: None,
1964                constraint: None,
1965            },
1966            FieldSchema {
1967                label: "MoonFogColor",
1968                expected_type: GffType::UInt32,
1969                required: false,
1970                children: None,
1971                constraint: None,
1972            },
1973            FieldSchema {
1974                label: "SunAmbientColor",
1975                expected_type: GffType::UInt32,
1976                required: false,
1977                children: None,
1978                constraint: None,
1979            },
1980            FieldSchema {
1981                label: "SunDiffuseColor",
1982                expected_type: GffType::UInt32,
1983                required: false,
1984                children: None,
1985                constraint: None,
1986            },
1987            FieldSchema {
1988                label: "SunFogColor",
1989                expected_type: GffType::UInt32,
1990                required: false,
1991                children: None,
1992                constraint: None,
1993            },
1994            FieldSchema {
1995                label: "DynAmbientColor",
1996                expected_type: GffType::UInt32,
1997                required: false,
1998                children: None,
1999                constraint: None,
2000            },
2001            FieldSchema {
2002                label: "MoonFogNear",
2003                expected_type: GffType::Single,
2004                required: false,
2005                children: None,
2006                constraint: None,
2007            },
2008            FieldSchema {
2009                label: "MoonFogFar",
2010                expected_type: GffType::Single,
2011                required: false,
2012                children: None,
2013                constraint: None,
2014            },
2015            FieldSchema {
2016                label: "SunFogNear",
2017                expected_type: GffType::Single,
2018                required: false,
2019                children: None,
2020                constraint: None,
2021            },
2022            FieldSchema {
2023                label: "SunFogFar",
2024                expected_type: GffType::Single,
2025                required: false,
2026                children: None,
2027                constraint: None,
2028            },
2029            FieldSchema {
2030                label: "MoonFogOn",
2031                expected_type: GffType::UInt8,
2032                required: false,
2033                children: None,
2034                constraint: None,
2035            },
2036            FieldSchema {
2037                label: "SunFogOn",
2038                expected_type: GffType::UInt8,
2039                required: false,
2040                children: None,
2041                constraint: None,
2042            },
2043            FieldSchema {
2044                label: "MoonShadows",
2045                expected_type: GffType::UInt8,
2046                required: false,
2047                children: None,
2048                constraint: None,
2049            },
2050            FieldSchema {
2051                label: "SunShadows",
2052                expected_type: GffType::UInt8,
2053                required: false,
2054                children: None,
2055                constraint: None,
2056            },
2057            FieldSchema {
2058                label: "DayNightCycle",
2059                expected_type: GffType::UInt8,
2060                required: false,
2061                children: None,
2062                constraint: None,
2063            },
2064            FieldSchema {
2065                label: "IsNight",
2066                expected_type: GffType::UInt8,
2067                required: false,
2068                children: None,
2069                constraint: None,
2070            },
2071            FieldSchema {
2072                label: "ShadowOpacity",
2073                expected_type: GffType::UInt8,
2074                required: false,
2075                children: None,
2076                constraint: None,
2077            },
2078            FieldSchema {
2079                label: "LightingScheme",
2080                expected_type: GffType::UInt8,
2081                required: false,
2082                children: None,
2083                constraint: None,
2084            },
2085            FieldSchema {
2086                label: "NoRest",
2087                expected_type: GffType::UInt8,
2088                required: false,
2089                children: None,
2090                constraint: None,
2091            },
2092            // ===== Skill modifiers (2) =====
2093            FieldSchema {
2094                label: "ModSpotCheck",
2095                expected_type: GffType::Int32,
2096                required: false,
2097                children: None,
2098                constraint: None,
2099            },
2100            FieldSchema {
2101                label: "ModListenCheck",
2102                expected_type: GffType::Int32,
2103                required: false,
2104                children: None,
2105                constraint: None,
2106            },
2107            // ===== Grass (10) =====
2108            FieldSchema {
2109                label: "Grass_Diffuse",
2110                expected_type: GffType::UInt32,
2111                required: false,
2112                children: None,
2113                constraint: None,
2114            },
2115            FieldSchema {
2116                label: "Grass_Ambient",
2117                expected_type: GffType::UInt32,
2118                required: false,
2119                children: None,
2120                constraint: None,
2121            },
2122            FieldSchema {
2123                label: "Grass_Density",
2124                expected_type: GffType::Single,
2125                required: false,
2126                children: None,
2127                constraint: None,
2128            },
2129            FieldSchema {
2130                label: "Grass_QuadSize",
2131                expected_type: GffType::Single,
2132                required: false,
2133                children: None,
2134                constraint: None,
2135            },
2136            FieldSchema {
2137                label: "Grass_TexName",
2138                expected_type: GffType::ResRef,
2139                required: false,
2140                children: None,
2141                constraint: None,
2142            },
2143            FieldSchema {
2144                label: "Grass_Prob_LL",
2145                expected_type: GffType::Single,
2146                required: false,
2147                children: None,
2148                constraint: None,
2149            },
2150            FieldSchema {
2151                label: "Grass_Prob_LR",
2152                expected_type: GffType::Single,
2153                required: false,
2154                children: None,
2155                constraint: None,
2156            },
2157            FieldSchema {
2158                label: "Grass_Prob_UL",
2159                expected_type: GffType::Single,
2160                required: false,
2161                children: None,
2162                constraint: None,
2163            },
2164            FieldSchema {
2165                label: "Grass_Prob_UR",
2166                expected_type: GffType::Single,
2167                required: false,
2168                children: None,
2169                constraint: None,
2170            },
2171            FieldSchema {
2172                label: "AlphaTest",
2173                expected_type: GffType::Single,
2174                required: false,
2175                children: None,
2176                constraint: None,
2177            },
2178            // ===== Stealth / transition (save-state) (7) =====
2179            FieldSchema {
2180                label: "StealthXPMax",
2181                expected_type: GffType::UInt32,
2182                required: false,
2183                children: None,
2184                constraint: None,
2185            },
2186            FieldSchema {
2187                label: "StealthXPCurrent",
2188                expected_type: GffType::UInt32,
2189                required: false,
2190                children: None,
2191                constraint: None,
2192            },
2193            FieldSchema {
2194                label: "StealthXPLoss",
2195                expected_type: GffType::UInt32,
2196                required: false,
2197                children: None,
2198                constraint: None,
2199            },
2200            FieldSchema {
2201                label: "StealthXPEnabled",
2202                expected_type: GffType::UInt8,
2203                required: false,
2204                children: None,
2205                constraint: None,
2206            },
2207            FieldSchema {
2208                label: "TransPending",
2209                expected_type: GffType::UInt8,
2210                required: false,
2211                children: None,
2212                constraint: None,
2213            },
2214            FieldSchema {
2215                label: "TransPendNextID",
2216                expected_type: GffType::UInt8,
2217                required: false,
2218                children: None,
2219                constraint: None,
2220            },
2221            FieldSchema {
2222                label: "TransPendCurrID",
2223                expected_type: GffType::UInt8,
2224                required: false,
2225                children: None,
2226                constraint: None,
2227            },
2228            // ===== Other =====
2229            FieldSchema {
2230                label: "LoadScreenID",
2231                expected_type: GffType::UInt16,
2232                required: false,
2233                children: None,
2234                constraint: None,
2235            },
2236            // ===== Lists (2) + Structs (2) =====
2237            FieldSchema {
2238                label: "Rooms",
2239                expected_type: GffType::List,
2240                required: false,
2241                children: Some(ROOMS_CHILDREN),
2242                constraint: None,
2243            },
2244            FieldSchema {
2245                label: "Expansion_List",
2246                expected_type: GffType::List,
2247                required: false,
2248                children: Some(EXPANSION_LIST_CHILDREN),
2249                constraint: None,
2250            },
2251            FieldSchema {
2252                label: "Map",
2253                expected_type: GffType::Struct,
2254                required: false,
2255                children: Some(MAP_CHILDREN),
2256                constraint: None,
2257            },
2258            FieldSchema {
2259                label: "MiniGame",
2260                expected_type: GffType::Struct,
2261                required: false,
2262                children: Some(MINI_GAME_CHILDREN),
2263                constraint: None,
2264            },
2265            // ===== Toolset / K2 / NWN fields =====
2266            FieldSchema {
2267                label: "DisableTransit",
2268                expected_type: GffType::UInt8,
2269                required: false,
2270                children: None,
2271                constraint: None,
2272            },
2273            FieldSchema {
2274                label: "NoHangBack",
2275                expected_type: GffType::UInt8,
2276                required: false,
2277                children: None,
2278                constraint: None,
2279            },
2280            FieldSchema {
2281                label: "PlayerOnly",
2282                expected_type: GffType::UInt8,
2283                required: false,
2284                children: None,
2285                constraint: None,
2286            },
2287            FieldSchema {
2288                label: "PlayerVsPlayer",
2289                expected_type: GffType::UInt8,
2290                required: false,
2291                children: None,
2292                constraint: None,
2293            },
2294            FieldSchema {
2295                label: "Grass_Emissive",
2296                expected_type: GffType::UInt32,
2297                required: false,
2298                children: None,
2299                constraint: None,
2300            },
2301            // ===== Dirty overlays (12) =====
2302            FieldSchema {
2303                label: "DirtyARGBOne",
2304                expected_type: GffType::Int32,
2305                required: false,
2306                children: None,
2307                constraint: None,
2308            },
2309            FieldSchema {
2310                label: "DirtySizeOne",
2311                expected_type: GffType::Int32,
2312                required: false,
2313                children: None,
2314                constraint: None,
2315            },
2316            FieldSchema {
2317                label: "DirtyFormulaOne",
2318                expected_type: GffType::Int32,
2319                required: false,
2320                children: None,
2321                constraint: None,
2322            },
2323            FieldSchema {
2324                label: "DirtyFuncOne",
2325                expected_type: GffType::Int32,
2326                required: false,
2327                children: None,
2328                constraint: None,
2329            },
2330            FieldSchema {
2331                label: "DirtyARGBTwo",
2332                expected_type: GffType::Int32,
2333                required: false,
2334                children: None,
2335                constraint: None,
2336            },
2337            FieldSchema {
2338                label: "DirtySizeTwo",
2339                expected_type: GffType::Int32,
2340                required: false,
2341                children: None,
2342                constraint: None,
2343            },
2344            FieldSchema {
2345                label: "DirtyFormulaTwo",
2346                expected_type: GffType::Int32,
2347                required: false,
2348                children: None,
2349                constraint: None,
2350            },
2351            FieldSchema {
2352                label: "DirtyFuncTwo",
2353                expected_type: GffType::Int32,
2354                required: false,
2355                children: None,
2356                constraint: None,
2357            },
2358            FieldSchema {
2359                label: "DirtyARGBThree",
2360                expected_type: GffType::Int32,
2361                required: false,
2362                children: None,
2363                constraint: None,
2364            },
2365            FieldSchema {
2366                label: "DirtySizeThree",
2367                expected_type: GffType::Int32,
2368                required: false,
2369                children: None,
2370                constraint: None,
2371            },
2372            FieldSchema {
2373                label: "DirtyFormulaThre",
2374                expected_type: GffType::Int32,
2375                required: false,
2376                children: None,
2377                constraint: None,
2378            },
2379            FieldSchema {
2380                label: "DirtyFuncThree",
2381                expected_type: GffType::Int32,
2382                required: false,
2383                children: None,
2384                constraint: None,
2385            },
2386        ];
2387        SCHEMA
2388    }
2389}
2390
2391#[cfg(test)]
2392mod tests {
2393    use super::*;
2394
2395    const TEST_ARE: &[u8] = include_bytes!(concat!(
2396        env!("CARGO_MANIFEST_DIR"),
2397        "/../../fixtures/test.are"
2398    ));
2399
2400    #[test]
2401    fn reads_core_are_fields_from_fixture() {
2402        let are = read_are_from_bytes(TEST_ARE).expect("fixture must parse");
2403
2404        assert_eq!(are.unused_id, 0);
2405        assert_eq!(are.creator_id, 0);
2406        assert_eq!(are.tag, "Untitled");
2407        assert_eq!(are.name.string_ref.raw(), 75_101);
2408        assert_eq!(are.comment, "comments");
2409        assert_eq!(are.version, 88);
2410        assert_eq!(are.flags, 0);
2411        assert_eq!(are.mod_spot_check, 0);
2412        assert_eq!(are.mod_listen_check, 0);
2413        assert_eq!(are.camera_style, 1);
2414        assert_eq!(are.default_envmap, "defaultenvmap");
2415        assert_eq!(are.grass_texture, "grasstexture");
2416        assert!((are.grass_density - 1.0).abs() < f32::EPSILON);
2417        assert!((are.grass_size - 1.0).abs() < f32::EPSILON);
2418        assert!((are.grass_prob_ll - 0.25).abs() < f32::EPSILON);
2419        assert!((are.grass_prob_lr - 0.25).abs() < f32::EPSILON);
2420        assert!((are.grass_prob_ul - 0.25).abs() < f32::EPSILON);
2421        assert!((are.grass_prob_ur - 0.25).abs() < f32::EPSILON);
2422        assert_eq!(are.sun_ambient_color, 16_777_215);
2423        assert_eq!(are.sun_diffuse_color, 16_777_215);
2424        assert_eq!(are.dynamic_ambient_color, 16_777_215);
2425        assert_eq!(are.sun_fog_color, 16_777_215);
2426        assert_eq!(are.grass_ambient_color, 16_777_215);
2427        assert_eq!(are.grass_diffuse_color, 16_777_215);
2428        assert_eq!(are.grass_emissive_color, 16_777_215);
2429        assert!(are.fog_enabled);
2430        assert!((are.fog_near - 99.0).abs() < f32::EPSILON);
2431        assert!((are.fog_far - 100.0).abs() < f32::EPSILON);
2432        assert!(are.shadows);
2433        assert_eq!(are.shadow_opacity, 205);
2434        assert_eq!(are.wind_power, 1);
2435        assert!(are.unescapable);
2436        assert!(are.disable_transit);
2437        assert!(are.stealth_xp);
2438        assert_eq!(are.stealth_xp_loss, 25);
2439        assert_eq!(are.stealth_xp_max, 25);
2440        assert_eq!(are.on_enter, "k_on_enter");
2441        assert_eq!(are.on_exit, "onexit");
2442        assert_eq!(are.on_heartbeat, "onheartbeat");
2443        assert_eq!(are.on_user_defined, "onuserdefined");
2444        assert!((are.alpha_test - 0.2).abs() < f32::EPSILON);
2445        assert_eq!(are.chance_rain, 99);
2446        assert_eq!(are.chance_snow, 99);
2447        assert_eq!(are.chance_lightning, 99);
2448        assert_eq!(are.moon_ambient_color, 0);
2449        assert_eq!(are.moon_diffuse_color, 0);
2450        assert!(!are.moon_fog_enabled);
2451        assert!((are.moon_fog_near - 99.0).abs() < f32::EPSILON);
2452        assert!((are.moon_fog_far - 100.0).abs() < f32::EPSILON);
2453        assert_eq!(are.moon_fog_color, 0);
2454        assert!(!are.moon_shadows);
2455        assert_eq!(are.dirty_argb_one, 123);
2456        assert_eq!(are.dirty_size_one, 1);
2457        assert_eq!(are.dirty_formula_one, 1);
2458        assert_eq!(are.dirty_func_one, 1);
2459        assert_eq!(are.dirty_argb_two, 1234);
2460        assert_eq!(are.dirty_size_two, 1);
2461        assert_eq!(are.dirty_formula_two, 1);
2462        assert_eq!(are.dirty_func_two, 1);
2463        assert_eq!(are.dirty_argb_three, 12_345);
2464        assert_eq!(are.dirty_size_three, 1);
2465        assert_eq!(are.dirty_formula_three, 1);
2466        assert_eq!(are.dirty_func_three, 1);
2467        assert!(!are.is_night);
2468        assert_eq!(are.lighting_scheme, 0);
2469        assert_eq!(are.day_night_cycle, 0);
2470        assert!(!are.no_rest);
2471        assert!(!are.no_hang_back);
2472        assert!(!are.player_only);
2473        assert_eq!(are.player_vs_player, 3);
2474        assert_eq!(are.map.map_zoom, 1);
2475        assert_eq!(are.map.map_res_x, 18);
2476        assert_eq!(are.rooms.len(), 2);
2477        assert!(are.expansion_list.is_empty());
2478        assert_eq!(are.rooms[0].room_name, "002ebo");
2479        assert!(are.rooms[0].disable_weather);
2480        assert!(are.rooms[0].part_sounds.is_empty());
2481    }
2482
2483    #[test]
2484    fn all_fields_survive_typed_roundtrip() {
2485        let are = read_are_from_bytes(TEST_ARE).expect("fixture must parse");
2486        let encoded = write_are_to_vec(&are).expect("encode must succeed");
2487        let reparsed = read_are_from_bytes(&encoded).expect("decode must succeed");
2488        assert_eq!(are, reparsed);
2489    }
2490
2491    #[test]
2492    fn typed_edits_roundtrip_through_gff_writer() {
2493        let mut are = read_are_from_bytes(TEST_ARE).expect("fixture must parse");
2494        are.tag = "m01aa".into();
2495        are.on_enter = ResRef::new("k_on_newenter").expect("valid test resref");
2496        are.grass_texture = ResRef::new("new_grass").expect("valid test resref");
2497        are.fog_enabled = false;
2498        are.fog_near = 50.0;
2499        are.shadow_opacity = 180;
2500        are.stealth_xp_loss = 33;
2501        are.dirty_formula_three = 9;
2502        are.player_vs_player = 2;
2503        are.restrict_mode = 1;
2504        are.chance_fog = 42;
2505        are.stealth_xp_current = 10;
2506        are.trans_pending = 1;
2507        are.trans_pend_next_id = 3;
2508        are.trans_pend_curr_id = 2;
2509        are.map.map_zoom = 7;
2510        are.rooms[0].ambient_scale = 0.5;
2511        are.expansion_list.push(AreExpansionEntry {
2512            expansion_name: GffLocalizedString::new(StrRef::from_raw(321)),
2513            expansion_id: 42,
2514        });
2515        are.rooms[0].part_sounds.push(ArePartSound {
2516            looping: true,
2517            model_part: "ROOM_A".into(),
2518            omen_event: "ON_ENTER".into(),
2519            sound: ResRef::new("amb_rooma").expect("valid test resref"),
2520        });
2521
2522        let encoded = write_are_to_vec(&are).expect("encode");
2523        let reparsed = read_are_from_bytes(&encoded).expect("decode");
2524
2525        assert_eq!(reparsed.tag, "m01aa");
2526        assert_eq!(reparsed.on_enter, "k_on_newenter");
2527        assert_eq!(reparsed.grass_texture, "new_grass");
2528        assert!(!reparsed.fog_enabled);
2529        assert!((reparsed.fog_near - 50.0).abs() < f32::EPSILON);
2530        assert_eq!(reparsed.shadow_opacity, 180);
2531        assert_eq!(reparsed.stealth_xp_loss, 33);
2532        assert_eq!(reparsed.dirty_formula_three, 9);
2533        assert_eq!(reparsed.player_vs_player, 2);
2534        assert_eq!(reparsed.restrict_mode, 1);
2535        assert_eq!(reparsed.chance_fog, 42);
2536        assert_eq!(reparsed.stealth_xp_current, 10);
2537        assert_eq!(reparsed.trans_pending, 1);
2538        assert_eq!(reparsed.trans_pend_next_id, 3);
2539        assert_eq!(reparsed.trans_pend_curr_id, 2);
2540        assert_eq!(reparsed.map.map_zoom, 7);
2541        assert_eq!(reparsed.rooms[0].ambient_scale, 0.5);
2542        assert_eq!(reparsed.expansion_list.len(), 1);
2543        assert_eq!(reparsed.expansion_list[0].expansion_id, 42);
2544        assert_eq!(
2545            reparsed.expansion_list[0].expansion_name.string_ref.raw(),
2546            321
2547        );
2548        assert_eq!(reparsed.rooms[0].part_sounds.len(), 1);
2549        assert!(reparsed.rooms[0].part_sounds[0].looping);
2550        assert_eq!(reparsed.rooms[0].part_sounds[0].model_part, "ROOM_A");
2551        assert_eq!(reparsed.rooms[0].part_sounds[0].omen_event, "ON_ENTER");
2552        assert_eq!(reparsed.rooms[0].part_sounds[0].sound, "amb_rooma");
2553        assert_eq!(reparsed.rooms.len(), 2);
2554    }
2555
2556    #[test]
2557    fn rejects_non_are_file_type() {
2558        let gff = Gff::new(*b"DLG ", GffStruct::new(-1));
2559        let err = Are::from_gff(&gff).expect_err("must fail");
2560        assert!(matches!(err, AreError::UnsupportedFileType(file_type) if file_type == *b"DLG "));
2561    }
2562
2563    #[test]
2564    fn read_are_from_reader_matches_bytes_path() {
2565        let mut cursor = Cursor::new(TEST_ARE);
2566        let via_reader = read_are(&mut cursor).expect("reader parse");
2567        let via_bytes = read_are_from_bytes(TEST_ARE).expect("bytes parse");
2568        assert_eq!(via_reader.tag, via_bytes.tag);
2569        assert_eq!(via_reader.rooms.len(), via_bytes.rooms.len());
2570    }
2571
2572    #[test]
2573    fn type_mismatch_on_rooms_field_is_error() {
2574        let mut root = GffStruct::new(-1);
2575        root.push_field("Rooms", GffValue::UInt32(7));
2576        let gff = Gff::new(*b"ARE ", root);
2577        let err = Are::from_gff(&gff).expect_err("must fail");
2578        assert!(matches!(
2579            err,
2580            AreError::TypeMismatch {
2581                field: "Rooms",
2582                expected: "List"
2583            }
2584        ));
2585    }
2586
2587    #[test]
2588    fn map_points_accept_int_encoding_for_k1_parity() {
2589        let mut root = GffStruct::new(-1);
2590        let mut map = GffStruct::new(0);
2591        map.push_field("MapPt1X", GffValue::Int32(10));
2592        map.push_field("MapPt1Y", GffValue::Int32(20));
2593        map.push_field("MapPt2X", GffValue::UInt16(30));
2594        map.push_field("MapPt2Y", GffValue::UInt8(40));
2595        root.push_field("Map", GffValue::Struct(Box::new(map)));
2596        let gff = Gff::new(*b"ARE ", root);
2597
2598        let are = Are::from_gff(&gff).expect("must parse");
2599        assert_eq!(are.map.map_point_1, [10.0, 20.0]);
2600        assert_eq!(are.map.map_point_2, [30.0, 40.0]);
2601    }
2602
2603    #[test]
2604    fn type_mismatch_on_expansion_list_field_is_error() {
2605        let mut root = GffStruct::new(-1);
2606        root.push_field("Expansion_List", GffValue::UInt32(7));
2607        let gff = Gff::new(*b"ARE ", root);
2608        let err = Are::from_gff(&gff).expect_err("must fail");
2609        assert!(matches!(
2610            err,
2611            AreError::TypeMismatch {
2612                field: "Expansion_List",
2613                expected: "List"
2614            }
2615        ));
2616    }
2617
2618    #[test]
2619    fn type_mismatch_on_room_part_sounds_field_is_error() {
2620        let mut root = GffStruct::new(-1);
2621        let mut room = GffStruct::new(0);
2622        room.push_field("PartSounds", GffValue::UInt32(7));
2623        root.push_field("Rooms", GffValue::List(vec![room]));
2624        let gff = Gff::new(*b"ARE ", root);
2625        let err = Are::from_gff(&gff).expect_err("must fail");
2626        assert!(matches!(
2627            err,
2628            AreError::TypeMismatch {
2629                field: "Rooms[].PartSounds",
2630                expected: "List"
2631            }
2632        ));
2633    }
2634
2635    #[test]
2636    fn type_mismatch_on_part_sound_looping_field_is_error() {
2637        let mut root = GffStruct::new(-1);
2638        let mut room = GffStruct::new(0);
2639        let mut part_sound = GffStruct::new(0);
2640        part_sound.push_field("Looping", GffValue::String("yes".into()));
2641        room.push_field("PartSounds", GffValue::List(vec![part_sound]));
2642        root.push_field("Rooms", GffValue::List(vec![room]));
2643        let gff = Gff::new(*b"ARE ", root);
2644        let err = Are::from_gff(&gff).expect_err("must fail");
2645        assert!(matches!(
2646            err,
2647            AreError::TypeMismatch {
2648                field: "Rooms[].PartSounds[].Looping",
2649                expected: "numeric bool"
2650            }
2651        ));
2652    }
2653
2654    #[test]
2655    fn write_are_matches_direct_gff_writer() {
2656        let are = read_are_from_bytes(TEST_ARE).expect("fixture parse");
2657        let from_are = write_are_to_vec(&are).expect("are encode");
2658
2659        let gff = are.to_gff();
2660        let from_gff = rakata_formats::write_gff_to_vec(&gff).expect("gff encode");
2661        assert_eq!(from_are, from_gff);
2662    }
2663
2664    #[test]
2665    fn schema_field_count() {
2666        assert_eq!(Are::schema().len(), 81);
2667    }
2668
2669    #[test]
2670    fn schema_no_duplicate_labels() {
2671        let schema = Are::schema();
2672        let mut labels: Vec<&str> = schema.iter().map(|f| f.label).collect();
2673        labels.sort();
2674        let before = labels.len();
2675        labels.dedup();
2676        assert_eq!(before, labels.len(), "duplicate labels in ARE schema");
2677    }
2678
2679    #[test]
2680    fn schema_rooms_has_children() {
2681        let rooms = Are::schema()
2682            .iter()
2683            .find(|f| f.label == "Rooms")
2684            .expect("Rooms field must exist in schema");
2685        assert!(rooms.children.is_some());
2686        assert_eq!(
2687            rooms
2688                .children
2689                .expect("Rooms children must be present")
2690                .len(),
2691            5
2692        );
2693    }
2694
2695    #[test]
2696    fn schema_map_has_children() {
2697        let map = Are::schema()
2698            .iter()
2699            .find(|f| f.label == "Map")
2700            .expect("Map field must exist in schema");
2701        assert!(map.children.is_some());
2702        assert_eq!(
2703            map.children.expect("Map children must be present").len(),
2704            11
2705        );
2706    }
2707
2708    #[test]
2709    fn type_mismatch_on_mini_game_field_is_error() {
2710        let mut root = GffStruct::new(-1);
2711        root.push_field("MiniGame", GffValue::UInt32(7));
2712        let gff = Gff::new(*b"ARE ", root);
2713        let err = Are::from_gff(&gff).expect_err("must fail");
2714        assert!(matches!(
2715            err,
2716            AreError::TypeMismatch {
2717                field: "MiniGame",
2718                expected: "Struct"
2719            }
2720        ));
2721    }
2722
2723    #[test]
2724    fn mini_game_struct_roundtrips() {
2725        let mg = AreMiniGame {
2726            mini_game_type: 1,
2727            movement_per_sec: 25.0,
2728            lateral_accel: 45.0,
2729            bump_plane: 2,
2730            do_bumping: true,
2731            use_inertia: true,
2732            dof: 3,
2733            music: ResRef::new("mus_swoop").expect("valid test resref"),
2734            far_clip: 200.0,
2735            near_clip: 0.5,
2736            camera_view_angle: 70.0,
2737            player: Some(AreMiniGamePlayer {
2738                models: vec![AreMiniGameModel {
2739                    model: ResRef::new("swoopbike").expect("valid test resref"),
2740                    rotating_model: false,
2741                }],
2742                track: ResRef::new("trk_race01").expect("valid test resref"),
2743                camera: ResRef::new("cam_swoop").expect("valid test resref"),
2744                camera_rotate: true,
2745                mouse: AreMiniGameMouse {
2746                    axis_x: 1,
2747                    axis_y: 2,
2748                    flip_axis_x: true,
2749                    flip_axis_y: false,
2750                },
2751                enemies: vec![AreMiniGameEnemy {
2752                    models: Vec::new(),
2753                    track: ResRef::new("trk_enemy01").expect("valid test resref"),
2754                }],
2755                obstacles: vec![AreMiniGameObstacle {
2756                    name: ResRef::new("obs_rock").expect("valid test resref"),
2757                }],
2758            }),
2759        };
2760
2761        let mut are = Are::new();
2762        are.mini_game = Some(mg);
2763
2764        let encoded = write_are_to_vec(&are).expect("encode");
2765        let reparsed = read_are_from_bytes(&encoded).expect("decode");
2766
2767        let mg_out = reparsed.mini_game.expect("MiniGame must survive roundtrip");
2768        assert_eq!(mg_out.mini_game_type, 1);
2769        assert!((mg_out.movement_per_sec - 25.0).abs() < f32::EPSILON);
2770        assert!((mg_out.lateral_accel - 45.0).abs() < f32::EPSILON);
2771        assert_eq!(mg_out.bump_plane, 2);
2772        assert!(mg_out.do_bumping);
2773        assert!(mg_out.use_inertia);
2774        assert_eq!(mg_out.dof, 3);
2775        assert_eq!(mg_out.music, "mus_swoop");
2776        assert!((mg_out.far_clip - 200.0).abs() < f32::EPSILON);
2777        assert!((mg_out.near_clip - 0.5).abs() < f32::EPSILON);
2778        assert!((mg_out.camera_view_angle - 70.0).abs() < f32::EPSILON);
2779
2780        let player = mg_out.player.expect("Player must survive roundtrip");
2781        assert_eq!(player.models.len(), 1);
2782        assert_eq!(player.models[0].model, "swoopbike");
2783        assert!(!player.models[0].rotating_model);
2784        assert_eq!(player.track, "trk_race01");
2785        assert_eq!(player.camera, "cam_swoop");
2786        assert!(player.camera_rotate);
2787        assert_eq!(player.mouse.axis_x, 1);
2788        assert_eq!(player.mouse.axis_y, 2);
2789        assert!(player.mouse.flip_axis_x);
2790        assert!(!player.mouse.flip_axis_y);
2791        assert_eq!(player.enemies.len(), 1);
2792        assert!(player.enemies[0].models.is_empty());
2793        assert_eq!(player.enemies[0].track, "trk_enemy01");
2794        assert_eq!(player.obstacles.len(), 1);
2795        assert_eq!(player.obstacles[0].name, "obs_rock");
2796    }
2797}