Skip to main content

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