Skip to main content

rakata_generics/are/
minigame.rs

1//! Typed views over the ARE `MiniGame` subsystem.
2//!
3//! One area in four thousand-odd ships a minigame, and the four that do carry
4//! a `MiniGame` struct nested in the ARE root. `Type` selects the mode: 1 is
5//! swoop racing, 2 is the turret sequence. Naming anything here for swoop
6//! alone names half the subsystem, hence the neutral module name.
7//!
8//! ## Field layout
9//! ```text
10//! MiniGame              (Struct -> AreMiniGame)
11//! +-- Type / MovementPerSec / LateralAccel / Bump_Plane
12//! +-- DoBumping / UseInertia / DOF / Music
13//! +-- Far_Clip / Near_Clip / CameraViewAngle
14//! +-- Player            (Struct -> AreMiniGamePlayer)
15//! |   +-- Models        (List<AreMiniGameModel>)
16//! |   `-- Track / Camera / CameraRotate
17//! +-- Mouse             (Struct -> AreMiniGameMouse)
18//! +-- Enemies           (List<AreMiniGameEnemy>)
19//! `-- Obstacles         (List<AreMiniGameObstacle>)
20//! ```
21//!
22//! `Mouse`, `Enemies` and `Obstacles` are siblings of `Player`, not children.
23//! This diagram and the reader both nested them under `Player` until a corpus
24//! extraction showed otherwise, which meant every enemy and obstacle in the
25//! game was silently read as absent: 95 enemies and 67 obstacles across the
26//! four minigame areas. `CSWMiniGame::Load` reads the `Enemies` and
27//! `Obstacles` lists off the `MiniGame` struct, so those two are
28//! engine-attested. `Mouse` sits alongside them in the one vanilla file that
29//! carries it, but its loader path is untraced, so that level rests on the
30//! data alone.
31
32use rakata_core::ResRef;
33use rakata_formats::{
34    gff_schema::{AbsentDefault, FieldLife, FieldSchema, GffType},
35    GffStruct, GffValue,
36};
37
38use crate::are::AreError;
39use crate::gff_helpers::{get_bool, get_f32, get_i32, get_resref, get_u32, get_u8, upsert_field};
40
41/// Typed view over the ARE `MiniGame` nested struct.
42#[derive(Debug, Clone, PartialEq)]
43pub struct AreMiniGame {
44    /// Mini-game type (`Type`): 1 = swoop, 2 = turret.
45    pub mini_game_type: u32,
46    /// Movement speed (`MovementPerSec`).
47    pub movement_per_sec: f32,
48    /// Lateral acceleration (`LateralAccel`).
49    pub lateral_accel: f32,
50    /// Bump plane index (`Bump_Plane`).
51    pub bump_plane: u32,
52    /// Bumping enabled (`DoBumping`).
53    pub do_bumping: bool,
54    /// Inertia enabled (`UseInertia`).
55    pub use_inertia: bool,
56    /// Degrees of freedom (`DOF`).
57    pub dof: u32,
58    /// Music resref (`Music`).
59    pub music: ResRef,
60    /// Far clip distance (`Far_Clip`).
61    pub far_clip: f32,
62    /// Near clip distance (`Near_Clip`).
63    pub near_clip: f32,
64    /// Camera view angle (`CameraViewAngle`).
65    pub camera_view_angle: f32,
66    /// Player sub-struct (`Player`).
67    pub player: Option<AreMiniGamePlayer>,
68    /// Mouse axis settings (`Mouse`), when the area carries the struct.
69    ///
70    /// A sibling of `Player`, not a child of it. One vanilla minigame area of
71    /// four has it.
72    ///
73    /// [`Option`] rather than a defaulted value, because presence and default
74    /// are different questions for a struct. A default answers what the
75    /// engine substitutes for an absent scalar; it says nothing about whether
76    /// a nested struct exists. Materializing an all-zero `Mouse` for the
77    /// three files without one and then writing it back would invent
78    /// structure those files never had. `MiniGame` itself is an `Option` on
79    /// [`Are`](crate::Are) for the same reason, so this matches the convention already in
80    /// place rather than adding a second one.
81    ///
82    /// What the engine does with an absent `Mouse` is untraced, which is the
83    /// argument for `Option` rather than against it: preserving the file's
84    /// actual state invents nothing under that ignorance.
85    pub mouse: Option<AreMiniGameMouse>,
86    /// Enemy list (`Enemies`), a sibling of `Player`.
87    pub enemies: Vec<AreMiniGameEnemy>,
88    /// Obstacle list (`Obstacles`), a sibling of `Player`.
89    pub obstacles: Vec<AreMiniGameObstacle>,
90}
91
92impl Default for AreMiniGame {
93    fn default() -> Self {
94        Self {
95            mini_game_type: 0,
96            movement_per_sec: 0.0,
97            lateral_accel: 60.0,
98            bump_plane: 0,
99            do_bumping: false,
100            use_inertia: false,
101            dof: 0,
102            music: ResRef::blank(),
103            far_clip: 100.0,
104            near_clip: 0.1,
105            camera_view_angle: 65.0,
106            player: None,
107            mouse: None,
108            enemies: Vec::new(),
109            obstacles: Vec::new(),
110        }
111    }
112}
113
114impl AreMiniGame {
115    pub(crate) fn from_struct(structure: &GffStruct) -> Result<Self, AreError> {
116        let player = match structure.field("Player") {
117            Some(GffValue::Struct(s)) => Some(AreMiniGamePlayer::from_struct(s)?),
118            Some(_) => {
119                return Err(AreError::TypeMismatch {
120                    field: "MiniGame.Player",
121                    expected: "Struct",
122                });
123            }
124            None => None,
125        };
126
127        // Siblings of `Player`, not children. `CSWMiniGame::Load` reads the
128        // `Enemies` and `Obstacles` lists off this struct, and every vanilla
129        // minigame area puts them here. `Mouse` sits alongside them in the
130        // one file that has it; its loader path is untraced, so the level is
131        // data-attested rather than engine-attested.
132        let mouse = match structure.field("Mouse") {
133            Some(GffValue::Struct(s)) => Some(AreMiniGameMouse::from_struct(s)),
134            Some(_) => {
135                return Err(AreError::TypeMismatch {
136                    field: "MiniGame.Mouse",
137                    expected: "Struct",
138                });
139            }
140            None => None,
141        };
142
143        let enemies = match structure.field("Enemies") {
144            Some(GffValue::List(items)) => items
145                .iter()
146                .map(AreMiniGameEnemy::from_struct)
147                .collect::<Result<Vec<_>, _>>()?,
148            Some(_) => {
149                return Err(AreError::TypeMismatch {
150                    field: "MiniGame.Enemies",
151                    expected: "List",
152                });
153            }
154            None => Vec::new(),
155        };
156
157        let obstacles = match structure.field("Obstacles") {
158            Some(GffValue::List(items)) => items
159                .iter()
160                .map(AreMiniGameObstacle::from_struct)
161                .collect::<Result<Vec<_>, _>>()?,
162            Some(_) => {
163                return Err(AreError::TypeMismatch {
164                    field: "MiniGame.Obstacles",
165                    expected: "List",
166                });
167            }
168            None => Vec::new(),
169        };
170
171        Ok(Self {
172            mini_game_type: get_u32(structure, "Type").unwrap_or(0),
173            movement_per_sec: get_f32(structure, "MovementPerSec").unwrap_or(0.0),
174            lateral_accel: get_f32(structure, "LateralAccel").unwrap_or(60.0),
175            bump_plane: get_u32(structure, "Bump_Plane").unwrap_or(0),
176            do_bumping: get_bool(structure, "DoBumping").unwrap_or(false),
177            use_inertia: get_bool(structure, "UseInertia").unwrap_or(false),
178            dof: get_u32(structure, "DOF").unwrap_or(0),
179            music: get_resref(structure, "Music").unwrap_or_default(),
180            far_clip: get_f32(structure, "Far_Clip").unwrap_or(100.0),
181            near_clip: get_f32(structure, "Near_Clip").unwrap_or(0.1),
182            camera_view_angle: get_f32(structure, "CameraViewAngle").unwrap_or(65.0),
183            player,
184            mouse,
185            enemies,
186            obstacles,
187        })
188    }
189
190    pub(crate) fn to_struct(&self) -> GffStruct {
191        let mut s = GffStruct::new(0);
192        upsert_field(&mut s, "Type", GffValue::UInt32(self.mini_game_type));
193        upsert_field(
194            &mut s,
195            "MovementPerSec",
196            GffValue::Single(self.movement_per_sec),
197        );
198        upsert_field(&mut s, "LateralAccel", GffValue::Single(self.lateral_accel));
199        upsert_field(&mut s, "Bump_Plane", GffValue::UInt32(self.bump_plane));
200        upsert_field(
201            &mut s,
202            "DoBumping",
203            GffValue::UInt8(u8::from(self.do_bumping)),
204        );
205        upsert_field(
206            &mut s,
207            "UseInertia",
208            GffValue::UInt8(u8::from(self.use_inertia)),
209        );
210        upsert_field(&mut s, "DOF", GffValue::UInt32(self.dof));
211        upsert_field(&mut s, "Music", GffValue::ResRef(self.music));
212        upsert_field(&mut s, "Far_Clip", GffValue::Single(self.far_clip));
213        upsert_field(&mut s, "Near_Clip", GffValue::Single(self.near_clip));
214        upsert_field(
215            &mut s,
216            "CameraViewAngle",
217            GffValue::Single(self.camera_view_angle),
218        );
219        if let Some(player) = &self.player {
220            upsert_field(
221                &mut s,
222                "Player",
223                GffValue::Struct(Box::new(player.to_struct())),
224            );
225        }
226        if let Some(mouse) = &self.mouse {
227            upsert_field(
228                &mut s,
229                "Mouse",
230                GffValue::Struct(Box::new(mouse.to_struct())),
231            );
232        }
233        let enemy_structs: Vec<GffStruct> = self
234            .enemies
235            .iter()
236            .map(AreMiniGameEnemy::to_struct)
237            .collect();
238        upsert_field(&mut s, "Enemies", GffValue::List(enemy_structs));
239        let obstacle_structs: Vec<GffStruct> = self
240            .obstacles
241            .iter()
242            .map(AreMiniGameObstacle::to_struct)
243            .collect();
244        upsert_field(&mut s, "Obstacles", GffValue::List(obstacle_structs));
245        s
246    }
247}
248
249/// The `Scripts` struct every minigame object carries.
250///
251/// Read by `CSWMiniGameObject::LoadScripts`, which obstacles use directly and
252/// vehicles extend (see [`AreMiniGameVehicleScripts`]). Every slot defaults to
253/// an empty resref and the engine writes that empty value into the script slot
254/// whether or not the file supplied one, so an absent field clears a prior
255/// script rather than leaving it in place.
256///
257/// `OnHeartbeat` is the fifth and last slot the base reads, after
258/// `OnAnimEvent`.
259///
260/// It was missing from the write-up of this subsystem, and from the corpus
261/// scan the write-up was checked against, because the ARE root carries a
262/// script field under the same label: a scan keyed on bare labels sees the
263/// root's and counts the nested one as already modelled.
264#[derive(Debug, Clone, PartialEq, Default)]
265pub struct AreMiniGameObjectScripts {
266    /// Fired when the object is created (`OnCreate`).
267    pub on_create: ResRef,
268    /// Fired on the object's heartbeat (`OnHeartbeat`).
269    pub on_heartbeat: ResRef,
270    /// Fired on a model animation event (`OnAnimEvent`).
271    pub on_anim_event: ResRef,
272    /// Fired when a bullet hits the object (`OnHitBullet`).
273    pub on_hit_bullet: ResRef,
274    /// Fired when a track follower hits the object (`OnHitFollower`).
275    pub on_hit_follower: ResRef,
276}
277
278impl AreMiniGameObjectScripts {
279    fn from_struct(structure: &GffStruct) -> Self {
280        Self {
281            on_create: get_resref(structure, "OnCreate").unwrap_or_default(),
282            on_heartbeat: get_resref(structure, "OnHeartbeat").unwrap_or_default(),
283            on_anim_event: get_resref(structure, "OnAnimEvent").unwrap_or_default(),
284            on_hit_bullet: get_resref(structure, "OnHitBullet").unwrap_or_default(),
285            on_hit_follower: get_resref(structure, "OnHitFollower").unwrap_or_default(),
286        }
287    }
288
289    fn write_into(&self, s: &mut GffStruct) {
290        upsert_field(s, "OnCreate", GffValue::ResRef(self.on_create));
291        upsert_field(s, "OnHeartbeat", GffValue::ResRef(self.on_heartbeat));
292        upsert_field(s, "OnAnimEvent", GffValue::ResRef(self.on_anim_event));
293        upsert_field(s, "OnHitBullet", GffValue::ResRef(self.on_hit_bullet));
294        upsert_field(s, "OnHitFollower", GffValue::ResRef(self.on_hit_follower));
295    }
296
297    fn to_struct(&self) -> GffStruct {
298        let mut s = GffStruct::new(0);
299        self.write_into(&mut s);
300        s
301    }
302}
303
304/// The `Scripts` struct on a vehicle, which is the object set plus five more.
305///
306/// `CSWTrackFollower::LoadScripts` overrides the base and reads the extra five
307/// on top of it. Obstacles never carry them, and the corpus agrees: an
308/// obstacle's `Scripts` holds exactly the base set.
309#[derive(Debug, Clone, PartialEq, Default)]
310pub struct AreMiniGameVehicleScripts {
311    /// The slots shared with obstacles.
312    pub object: AreMiniGameObjectScripts,
313    /// Fired when the vehicle takes damage (`OnDamage`).
314    pub on_damage: ResRef,
315    /// Fired when the vehicle is destroyed (`OnDeath`).
316    pub on_death: ResRef,
317    /// Fired when the vehicle fires a gun bank (`OnFire`).
318    pub on_fire: ResRef,
319    /// Fired when the vehicle hits an obstacle (`OnHitObstacle`).
320    pub on_hit_obstacle: ResRef,
321    /// Fired each time the vehicle completes a track loop (`OnTrackLoop`).
322    pub on_track_loop: ResRef,
323}
324
325impl AreMiniGameVehicleScripts {
326    fn from_struct(structure: &GffStruct) -> Self {
327        Self {
328            object: AreMiniGameObjectScripts::from_struct(structure),
329            on_damage: get_resref(structure, "OnDamage").unwrap_or_default(),
330            on_death: get_resref(structure, "OnDeath").unwrap_or_default(),
331            on_fire: get_resref(structure, "OnFire").unwrap_or_default(),
332            on_hit_obstacle: get_resref(structure, "OnHitObstacle").unwrap_or_default(),
333            on_track_loop: get_resref(structure, "OnTrackLoop").unwrap_or_default(),
334        }
335    }
336
337    fn to_struct(&self) -> GffStruct {
338        let mut s = GffStruct::new(0);
339        self.object.write_into(&mut s);
340        upsert_field(&mut s, "OnDamage", GffValue::ResRef(self.on_damage));
341        upsert_field(&mut s, "OnDeath", GffValue::ResRef(self.on_death));
342        upsert_field(&mut s, "OnFire", GffValue::ResRef(self.on_fire));
343        upsert_field(
344            &mut s,
345            "OnHitObstacle",
346            GffValue::ResRef(self.on_hit_obstacle),
347        );
348        upsert_field(&mut s, "OnTrackLoop", GffValue::ResRef(self.on_track_loop));
349        s
350    }
351}
352
353/// The `Sounds` struct on a vehicle.
354///
355/// Read by `CSWTrackFollower::LoadSounds`. Both default to empty and are
356/// written into their slots whether or not the file supplied them, the same
357/// overwrite-on-absence pattern the scripts follow. A non-empty `Engine` sound
358/// is additionally forced into looping playback.
359#[derive(Debug, Clone, PartialEq, Default)]
360pub struct AreMiniGameSounds {
361    /// Looping engine sound (`Engine`).
362    pub engine: ResRef,
363    /// One-shot destruction sound (`Death`).
364    pub death: ResRef,
365}
366
367impl AreMiniGameSounds {
368    fn from_struct(structure: &GffStruct) -> Self {
369        Self {
370            engine: get_resref(structure, "Engine").unwrap_or_default(),
371            death: get_resref(structure, "Death").unwrap_or_default(),
372        }
373    }
374
375    fn to_struct(&self) -> GffStruct {
376        let mut s = GffStruct::new(0);
377        upsert_field(&mut s, "Engine", GffValue::ResRef(self.engine));
378        upsert_field(&mut s, "Death", GffValue::ResRef(self.death));
379        s
380    }
381}
382
383/// The `BankID` value that means "no bank".
384///
385/// The engine reads `BankID` with this as its default and then gates bank
386/// creation by comparing the *resolved* value against the same literal, with
387/// no separate check for whether the file supplied one. So an absent `BankID`
388/// and one written explicitly as `0xffffffff` are the same case, which is why
389/// the view models one `u32` rather than an `Option` that would claim to tell
390/// them apart.
391pub const BANK_ID_NONE: u32 = 0xFFFF_FFFF;
392
393/// The ballistics of one gun bank (`Bullet`).
394///
395/// The first five fields are a presence-gated chain: each read reports
396/// whether the file supplied the field, and that gates whether the next one
397/// is attempted at all. If any is genuinely absent the chain truncates and
398/// the bank is never created, so there is no such thing as a partial bank
399/// built from defaults. `Bullet_Model` and `Collision_Sound` are read
400/// unconditionally once the chain completes.
401///
402/// The view models all seven as plain fields rather than making the chain
403/// visible in the type. A file missing one of the five is malformed, and
404/// saying so is a diagnostic's job: the schema marks them required and lint
405/// reports the dropped bank. Seven `Option`s would push that judgement onto
406/// every caller that only wanted to read a rate of fire.
407#[derive(Debug, Clone, PartialEq, Default)]
408pub struct AreMiniGameBullet {
409    /// Damage per hit (`Damage`).
410    pub damage: u32,
411    /// Seconds before the bullet expires (`Lifespan`).
412    pub lifespan: f32,
413    /// Seconds between shots (`Rate_Of_Fire`).
414    pub rate_of_fire: f32,
415    /// Travel speed (`Speed`).
416    pub speed: f32,
417    /// What the bank is allowed to shoot at (`Target_Type`).
418    pub target_type: u32,
419    /// Bullet model resref (`Bullet_Model`).
420    pub bullet_model: ResRef,
421    /// Sound played on impact (`Collision_Sound`).
422    pub collision_sound: ResRef,
423}
424
425impl AreMiniGameBullet {
426    fn from_struct(structure: &GffStruct) -> Self {
427        Self {
428            damage: get_u32(structure, "Damage").unwrap_or(0),
429            lifespan: get_f32(structure, "Lifespan").unwrap_or(0.0),
430            rate_of_fire: get_f32(structure, "Rate_Of_Fire").unwrap_or(0.0),
431            speed: get_f32(structure, "Speed").unwrap_or(0.0),
432            target_type: get_u32(structure, "Target_Type").unwrap_or(0),
433            bullet_model: get_resref(structure, "Bullet_Model").unwrap_or_default(),
434            collision_sound: get_resref(structure, "Collision_Sound").unwrap_or_default(),
435        }
436    }
437
438    fn to_struct(&self) -> GffStruct {
439        let mut s = GffStruct::new(0);
440        upsert_field(&mut s, "Damage", GffValue::UInt32(self.damage));
441        upsert_field(&mut s, "Lifespan", GffValue::Single(self.lifespan));
442        upsert_field(&mut s, "Rate_Of_Fire", GffValue::Single(self.rate_of_fire));
443        upsert_field(&mut s, "Speed", GffValue::Single(self.speed));
444        upsert_field(&mut s, "Target_Type", GffValue::UInt32(self.target_type));
445        upsert_field(&mut s, "Bullet_Model", GffValue::ResRef(self.bullet_model));
446        upsert_field(
447            &mut s,
448            "Collision_Sound",
449            GffValue::ResRef(self.collision_sound),
450        );
451        s
452    }
453}
454
455/// The AI aiming parameters an enemy gun bank carries.
456///
457/// Flat on the bank entry, siblings of `Bullet` rather than fields inside it,
458/// and read only for enemies: the player's own guns never carry them. Grouped
459/// here because they are one concept and because the grouping is what lets
460/// the bank say "this is an enemy's" without a separate flag.
461#[derive(Debug, Clone, PartialEq, Default)]
462pub struct AreMiniGameTargeting {
463    /// How far the bank looks for a target (`Sensing_Radius`).
464    pub sensing_radius: f32,
465    /// Horizontal firing arc (`Horiz_Spread`).
466    pub horiz_spread: f32,
467    /// Vertical firing arc (`Vert_Spread`).
468    pub vert_spread: f32,
469    /// How far shots stray from the target (`Inaccuracy`).
470    pub inaccuracy: f32,
471}
472
473impl AreMiniGameTargeting {
474    /// The four labels, which are read and written as a set.
475    const LABELS: [&'static str; 4] = [
476        "Sensing_Radius",
477        "Horiz_Spread",
478        "Vert_Spread",
479        "Inaccuracy",
480    ];
481
482    /// Reads the group, or `None` when the bank is not an enemy's.
483    ///
484    /// Presence is keyed on all four rather than any, because the engine
485    /// reads them as a presence-gated chain and aborts the whole bank if one
486    /// is missing. A bank with three of the four is a bank the engine drops,
487    /// so treating that as `Some` would report a working set where the file
488    /// describes a broken one. No vanilla bank is partial: enemy banks carry
489    /// all four and player banks carry none.
490    fn from_struct(structure: &GffStruct) -> Option<Self> {
491        if !Self::LABELS
492            .iter()
493            .all(|label| structure.field(label).is_some())
494        {
495            return None;
496        }
497        Some(Self {
498            sensing_radius: get_f32(structure, "Sensing_Radius").unwrap_or(0.0),
499            horiz_spread: get_f32(structure, "Horiz_Spread").unwrap_or(0.0),
500            vert_spread: get_f32(structure, "Vert_Spread").unwrap_or(0.0),
501            inaccuracy: get_f32(structure, "Inaccuracy").unwrap_or(0.0),
502        })
503    }
504
505    fn write_into(&self, s: &mut GffStruct) {
506        upsert_field(s, "Sensing_Radius", GffValue::Single(self.sensing_radius));
507        upsert_field(s, "Horiz_Spread", GffValue::Single(self.horiz_spread));
508        upsert_field(s, "Vert_Spread", GffValue::Single(self.vert_spread));
509        upsert_field(s, "Inaccuracy", GffValue::Single(self.inaccuracy));
510    }
511}
512
513/// One entry in a vehicle's `Gun_Banks` list.
514#[derive(Debug, Clone, PartialEq)]
515pub struct AreMiniGameGunBank {
516    /// Which hardpoint this bank occupies (`BankID`).
517    ///
518    /// [`BANK_ID_NONE`] means the engine skips the bank entirely.
519    pub bank_id: u32,
520    /// Gun model resref (`Gun_Model`).
521    ///
522    /// An invalid or absent resref aborts the bank, so an empty value here
523    /// describes a bank that will not be created.
524    pub gun_model: ResRef,
525    /// Ballistics (`Bullet`), absent when the bank carries no such struct.
526    ///
527    /// [`Option`] because absence is not the same as a zeroed struct. The
528    /// engine skips a bank with no `Bullet`; a view that materialised an
529    /// all-zero one and wrote it back would turn a bank the engine drops into
530    /// one it creates and fires at zero speed. That is a silent change of
531    /// meaning on write, which is the failure this type exists to avoid.
532    pub bullet: Option<AreMiniGameBullet>,
533    /// Sound played when the bank fires (`Fire_Sound`).
534    ///
535    /// A sibling of `Bullet`, not a field inside it, despite reading like one.
536    pub fire_sound: ResRef,
537    /// AI aiming parameters, present on enemy banks and absent on the
538    /// player's.
539    pub targeting: Option<AreMiniGameTargeting>,
540}
541
542impl Default for AreMiniGameGunBank {
543    fn default() -> Self {
544        Self {
545            bank_id: BANK_ID_NONE,
546            gun_model: ResRef::blank(),
547            bullet: None,
548            fire_sound: ResRef::blank(),
549            targeting: None,
550        }
551    }
552}
553
554impl AreMiniGameGunBank {
555    fn from_struct(structure: &GffStruct, bullet_field: &'static str) -> Result<Self, AreError> {
556        let bullet = match structure.field("Bullet") {
557            Some(GffValue::Struct(s)) => Some(AreMiniGameBullet::from_struct(s)),
558            Some(_) => {
559                return Err(AreError::TypeMismatch {
560                    field: bullet_field,
561                    expected: "Struct",
562                });
563            }
564            None => None,
565        };
566
567        Ok(Self {
568            bank_id: get_u32(structure, "BankID").unwrap_or(BANK_ID_NONE),
569            gun_model: get_resref(structure, "Gun_Model").unwrap_or_default(),
570            bullet,
571            fire_sound: get_resref(structure, "Fire_Sound").unwrap_or_default(),
572            targeting: AreMiniGameTargeting::from_struct(structure),
573        })
574    }
575
576    fn to_struct(&self) -> GffStruct {
577        let mut s = GffStruct::new(0);
578        upsert_field(&mut s, "BankID", GffValue::UInt32(self.bank_id));
579        upsert_field(&mut s, "Gun_Model", GffValue::ResRef(self.gun_model));
580        if let Some(bullet) = &self.bullet {
581            upsert_field(
582                &mut s,
583                "Bullet",
584                GffValue::Struct(Box::new(bullet.to_struct())),
585            );
586        }
587        upsert_field(&mut s, "Fire_Sound", GffValue::ResRef(self.fire_sound));
588        if let Some(targeting) = &self.targeting {
589            targeting.write_into(&mut s);
590        }
591        s
592    }
593}
594
595/// Where one vehicle's nested reads sit in the tree, for error reporting.
596///
597/// A vehicle is read at two different paths, and [`AreError::TypeMismatch`]
598/// carries a `&'static str`, so the paths cannot be built at read time. Two
599/// constants below cover both cases.
600struct VehiclePaths {
601    models: &'static str,
602    scripts: &'static str,
603    sounds: &'static str,
604    gun_banks: &'static str,
605    bullet: &'static str,
606}
607
608const PLAYER_PATHS: VehiclePaths = VehiclePaths {
609    models: "MiniGame.Player.Models",
610    scripts: "MiniGame.Player.Scripts",
611    sounds: "MiniGame.Player.Sounds",
612    gun_banks: "MiniGame.Player.Gun_Banks",
613    bullet: "MiniGame.Player.Gun_Banks[].Bullet",
614};
615
616const ENEMY_PATHS: VehiclePaths = VehiclePaths {
617    models: "MiniGame.Enemies[].Models",
618    scripts: "MiniGame.Enemies[].Scripts",
619    sounds: "MiniGame.Enemies[].Sounds",
620    gun_banks: "MiniGame.Enemies[].Gun_Banks",
621    bullet: "MiniGame.Enemies[].Gun_Banks[].Bullet",
622};
623
624/// The fields the player vehicle and every enemy vehicle both carry.
625///
626/// The engine backs both with one class, `CSWTrackFollower`, and both
627/// `CSWMiniPlayer::Load` and `CSWMiniEnemy::Load` delegate to
628/// `CSWTrackFollower::Load` on an embedded sub-object before reading anything
629/// of their own. So the sharing here is a fact about the engine's object
630/// graph, not a resemblance spotted between two field lists.
631///
632/// The distinction decides whether a shared base is worth having at all. One
633/// derived from the intersection of two field lists is a coincidence given a
634/// name, and it breaks the moment either side gains a field the other lacks.
635/// This one names a class the engine has, so a new field belongs either to
636/// that class or to one of the two subclasses, and the model has somewhere
637/// honest to put it either way.
638///
639/// Obstacles sit outside this entirely. `CSWMGObstacle::Load` reads a name and
640/// a scripts struct, with no lifecycle, weapon or track data at all.
641#[derive(Debug, Clone, PartialEq)]
642pub struct AreMiniGameVehicle {
643    /// Model list (`Models`).
644    pub models: Vec<AreMiniGameModel>,
645    /// Track resref (`Track`) the vehicle follows.
646    pub track: ResRef,
647    /// Current hit points (`Hit_Points`).
648    ///
649    /// The engine applies the file's value only when it is greater than zero.
650    /// A zero or absent value leaves whatever the object was constructed with,
651    /// so zero here means "engine decides", not "starts dead".
652    pub hit_points: u32,
653    /// Maximum hit points (`Max_HPs`), gated the same way as [`Self::hit_points`].
654    pub max_hps: u32,
655    /// Collision sphere radius (`Sphere_Radius`).
656    ///
657    /// Default `-1.0`, which the engine reads as a sentinel: it applies the
658    /// value only when it is at or above zero, so a negative radius means the
659    /// constructed default stands.
660    pub sphere_radius: f32,
661    /// Invulnerability window in seconds after a hit (`Invince_Period`).
662    pub invince_period: f32,
663    /// Damage dealt to whatever this vehicle bumps into (`Bump_Damage`).
664    pub bump_damage: i32,
665    /// Number of times the vehicle loops its track (`Num_Loops`).
666    ///
667    /// Default `-10`, a sentinel the engine passes through to a virtual setter
668    /// whose own handling of it was not traced. Vanilla writes `-1` for every
669    /// enemy, which is a different sentinel again.
670    pub num_loops: i32,
671    /// Script slots (`Scripts`).
672    ///
673    /// Not an [`Option`] despite being a nested struct, unlike
674    /// [`AreMiniGame::mouse`]. The engine's read here is traced and provably
675    /// indifferent: an absent `Scripts` and one holding nothing but empty
676    /// resrefs leave the object in the same state, because every slot is
677    /// written whether or not the file supplied it. Materializing one invents
678    /// a label but no behaviour. Once absent-value auditing can express "omit
679    /// when equal to the audited default", an all-empty `Scripts` is a
680    /// candidate to stop being written at all.
681    pub scripts: AreMiniGameVehicleScripts,
682    /// Engine and destruction sounds (`Sounds`), on the same footing as
683    /// [`Self::scripts`].
684    pub sounds: AreMiniGameSounds,
685    /// Weapon hardpoints (`Gun_Banks`).
686    pub gun_banks: Vec<AreMiniGameGunBank>,
687}
688
689impl Default for AreMiniGameVehicle {
690    fn default() -> Self {
691        Self {
692            models: Vec::new(),
693            track: ResRef::blank(),
694            hit_points: 0,
695            max_hps: 0,
696            sphere_radius: -1.0,
697            invince_period: 0.0,
698            bump_damage: 0,
699            num_loops: -10,
700            scripts: AreMiniGameVehicleScripts::default(),
701            sounds: AreMiniGameSounds::default(),
702            gun_banks: Vec::new(),
703        }
704    }
705}
706
707impl AreMiniGameVehicle {
708    /// Reads the shared fields off a `Player` struct or an `Enemies` entry.
709    fn from_struct(structure: &GffStruct, paths: &VehiclePaths) -> Result<Self, AreError> {
710        let models = match structure.field("Models") {
711            Some(GffValue::List(items)) => items
712                .iter()
713                .map(AreMiniGameModel::from_struct)
714                .collect::<Vec<_>>(),
715            Some(_) => {
716                return Err(AreError::TypeMismatch {
717                    field: paths.models,
718                    expected: "List",
719                });
720            }
721            None => Vec::new(),
722        };
723
724        let scripts = match structure.field("Scripts") {
725            Some(GffValue::Struct(s)) => AreMiniGameVehicleScripts::from_struct(s),
726            Some(_) => {
727                return Err(AreError::TypeMismatch {
728                    field: paths.scripts,
729                    expected: "Struct",
730                });
731            }
732            None => AreMiniGameVehicleScripts::default(),
733        };
734
735        let sounds = match structure.field("Sounds") {
736            Some(GffValue::Struct(s)) => AreMiniGameSounds::from_struct(s),
737            Some(_) => {
738                return Err(AreError::TypeMismatch {
739                    field: paths.sounds,
740                    expected: "Struct",
741                });
742            }
743            None => AreMiniGameSounds::default(),
744        };
745
746        let gun_banks = match structure.field("Gun_Banks") {
747            Some(GffValue::List(items)) => items
748                .iter()
749                .map(|item| AreMiniGameGunBank::from_struct(item, paths.bullet))
750                .collect::<Result<Vec<_>, _>>()?,
751            Some(_) => {
752                return Err(AreError::TypeMismatch {
753                    field: paths.gun_banks,
754                    expected: "List",
755                });
756            }
757            None => Vec::new(),
758        };
759
760        Ok(Self {
761            models,
762            track: get_resref(structure, "Track").unwrap_or_default(),
763            hit_points: get_u32(structure, "Hit_Points").unwrap_or(0),
764            max_hps: get_u32(structure, "Max_HPs").unwrap_or(0),
765            sphere_radius: get_f32(structure, "Sphere_Radius").unwrap_or(-1.0),
766            invince_period: get_f32(structure, "Invince_Period").unwrap_or(0.0),
767            bump_damage: get_i32(structure, "Bump_Damage").unwrap_or(0),
768            num_loops: get_i32(structure, "Num_Loops").unwrap_or(-10),
769            scripts,
770            sounds,
771            gun_banks,
772        })
773    }
774
775    /// Writes the shared fields into an already-created struct, so the caller
776    /// can add its own on top.
777    fn write_into(&self, s: &mut GffStruct) {
778        let model_structs: Vec<GffStruct> = self
779            .models
780            .iter()
781            .map(AreMiniGameModel::to_struct)
782            .collect();
783        upsert_field(s, "Models", GffValue::List(model_structs));
784        upsert_field(s, "Track", GffValue::ResRef(self.track));
785        upsert_field(s, "Hit_Points", GffValue::UInt32(self.hit_points));
786        upsert_field(s, "Max_HPs", GffValue::UInt32(self.max_hps));
787        upsert_field(s, "Sphere_Radius", GffValue::Single(self.sphere_radius));
788        upsert_field(s, "Invince_Period", GffValue::Single(self.invince_period));
789        upsert_field(s, "Bump_Damage", GffValue::Int32(self.bump_damage));
790        upsert_field(s, "Num_Loops", GffValue::Int32(self.num_loops));
791        upsert_field(
792            s,
793            "Scripts",
794            GffValue::Struct(Box::new(self.scripts.to_struct())),
795        );
796        upsert_field(
797            s,
798            "Sounds",
799            GffValue::Struct(Box::new(self.sounds.to_struct())),
800        );
801        let bank_structs: Vec<GffStruct> = self
802            .gun_banks
803            .iter()
804            .map(AreMiniGameGunBank::to_struct)
805            .collect();
806        upsert_field(s, "Gun_Banks", GffValue::List(bank_structs));
807    }
808}
809
810/// Typed view over the `MiniGame.Player` sub-struct.
811#[derive(Debug, Clone, PartialEq)]
812pub struct AreMiniGamePlayer {
813    /// The vehicle fields shared with every enemy.
814    pub vehicle: AreMiniGameVehicle,
815    /// Camera resref (`Camera`) - only used for turret type (type 2).
816    pub camera: ResRef,
817    /// Camera rotation flag (`CameraRotate`).
818    pub camera_rotate: bool,
819    /// Lower speed bound (`Minimum_Speed`).
820    ///
821    /// Default `-1.0`, a sentinel: the engine applies the value only when it
822    /// is at or above zero, so a negative bound leaves the constructed one.
823    pub minimum_speed: f32,
824    /// Upper speed bound (`Maximum_Speed`), default `100.0`.
825    ///
826    /// Gated the same way as [`Self::minimum_speed`], which for a default of
827    /// `100.0` means the gate never rejects it.
828    pub maximum_speed: f32,
829    /// Seconds to accelerate across the speed range (`Accel_Secs`).
830    ///
831    /// The engine keeps only the acceleration it derives from this and the
832    /// two speed bounds, never the field itself, and the derivation has three
833    /// branches: exactly `0.0` gives the full speed range as the rate, a
834    /// negative value skips the derivation, and anything else divides the
835    /// range by it. The view models the file's value, since that is what the
836    /// file holds and what a caller edits.
837    pub accel_secs: f32,
838    /// Positive-X track boundary (`TunnelXPos`).
839    pub tunnel_x_pos: f32,
840    /// Negative-X track boundary (`TunnelXNeg`).
841    pub tunnel_x_neg: f32,
842    /// Positive-Y track boundary (`TunnelYPos`).
843    pub tunnel_y_pos: f32,
844    /// Negative-Y track boundary (`TunnelYNeg`).
845    pub tunnel_y_neg: f32,
846    /// Positive-Z track boundary (`TunnelZPos`).
847    pub tunnel_z_pos: f32,
848    /// Negative-Z track boundary (`TunnelZNeg`).
849    pub tunnel_z_neg: f32,
850    /// Per-axis flags for an unbounded tunnel (`TunnelInfinite`).
851    ///
852    /// A GFF vector rather than three floats, unlike the bounds it modifies.
853    pub tunnel_infinite: [f32; 3],
854    /// Start position X offset (`Start_Offset_X`).
855    ///
856    /// The engine assembles the three into one vector and feeds it to the
857    /// vehicle's origin. They stay three fields here because that is how the
858    /// file stores them.
859    pub start_offset_x: f32,
860    /// Start position Y offset (`Start_Offset_Y`).
861    pub start_offset_y: f32,
862    /// Start position Z offset (`Start_Offset_Z`).
863    pub start_offset_z: f32,
864    /// Camera target X offset (`Target_Offset_X`).
865    ///
866    /// Three independent floats, and not assembled into a vector the way the
867    /// start offsets are.
868    pub target_offset_x: f32,
869    /// Camera target Y offset (`Target_Offset_Y`).
870    pub target_offset_y: f32,
871    /// Camera target Z offset (`Target_Offset_Z`).
872    pub target_offset_z: f32,
873}
874
875impl Default for AreMiniGamePlayer {
876    fn default() -> Self {
877        Self {
878            vehicle: AreMiniGameVehicle::default(),
879            camera: ResRef::blank(),
880            camera_rotate: false,
881            minimum_speed: -1.0,
882            maximum_speed: 100.0,
883            accel_secs: -1.0,
884            tunnel_x_pos: 0.0,
885            tunnel_x_neg: 0.0,
886            tunnel_y_pos: 0.0,
887            tunnel_y_neg: 0.0,
888            tunnel_z_pos: 0.0,
889            tunnel_z_neg: 0.0,
890            tunnel_infinite: [0.0; 3],
891            start_offset_x: 0.0,
892            start_offset_y: 0.0,
893            start_offset_z: 0.0,
894            target_offset_x: 0.0,
895            target_offset_y: 0.0,
896            target_offset_z: 0.0,
897        }
898    }
899}
900
901impl AreMiniGamePlayer {
902    fn from_struct(structure: &GffStruct) -> Result<Self, AreError> {
903        Ok(Self {
904            vehicle: AreMiniGameVehicle::from_struct(structure, &PLAYER_PATHS)?,
905            camera: get_resref(structure, "Camera").unwrap_or_default(),
906            camera_rotate: get_bool(structure, "CameraRotate").unwrap_or(false),
907            minimum_speed: get_f32(structure, "Minimum_Speed").unwrap_or(-1.0),
908            maximum_speed: get_f32(structure, "Maximum_Speed").unwrap_or(100.0),
909            accel_secs: get_f32(structure, "Accel_Secs").unwrap_or(-1.0),
910            tunnel_x_pos: get_f32(structure, "TunnelXPos").unwrap_or(0.0),
911            tunnel_x_neg: get_f32(structure, "TunnelXNeg").unwrap_or(0.0),
912            tunnel_y_pos: get_f32(structure, "TunnelYPos").unwrap_or(0.0),
913            tunnel_y_neg: get_f32(structure, "TunnelYNeg").unwrap_or(0.0),
914            tunnel_z_pos: get_f32(structure, "TunnelZPos").unwrap_or(0.0),
915            tunnel_z_neg: get_f32(structure, "TunnelZNeg").unwrap_or(0.0),
916            tunnel_infinite: match structure.field("TunnelInfinite") {
917                Some(GffValue::Vector3(v)) => *v,
918                _ => [0.0; 3],
919            },
920            start_offset_x: get_f32(structure, "Start_Offset_X").unwrap_or(0.0),
921            start_offset_y: get_f32(structure, "Start_Offset_Y").unwrap_or(0.0),
922            start_offset_z: get_f32(structure, "Start_Offset_Z").unwrap_or(0.0),
923            target_offset_x: get_f32(structure, "Target_Offset_X").unwrap_or(0.0),
924            target_offset_y: get_f32(structure, "Target_Offset_Y").unwrap_or(0.0),
925            target_offset_z: get_f32(structure, "Target_Offset_Z").unwrap_or(0.0),
926        })
927    }
928
929    fn to_struct(&self) -> GffStruct {
930        let mut s = GffStruct::new(0);
931        self.vehicle.write_into(&mut s);
932        upsert_field(&mut s, "Camera", GffValue::ResRef(self.camera));
933        upsert_field(
934            &mut s,
935            "CameraRotate",
936            GffValue::UInt8(u8::from(self.camera_rotate)),
937        );
938        upsert_field(
939            &mut s,
940            "Minimum_Speed",
941            GffValue::Single(self.minimum_speed),
942        );
943        upsert_field(
944            &mut s,
945            "Maximum_Speed",
946            GffValue::Single(self.maximum_speed),
947        );
948        upsert_field(&mut s, "Accel_Secs", GffValue::Single(self.accel_secs));
949        upsert_field(&mut s, "TunnelXPos", GffValue::Single(self.tunnel_x_pos));
950        upsert_field(&mut s, "TunnelXNeg", GffValue::Single(self.tunnel_x_neg));
951        upsert_field(&mut s, "TunnelYPos", GffValue::Single(self.tunnel_y_pos));
952        upsert_field(&mut s, "TunnelYNeg", GffValue::Single(self.tunnel_y_neg));
953        upsert_field(&mut s, "TunnelZPos", GffValue::Single(self.tunnel_z_pos));
954        upsert_field(&mut s, "TunnelZNeg", GffValue::Single(self.tunnel_z_neg));
955        upsert_field(
956            &mut s,
957            "TunnelInfinite",
958            GffValue::Vector3(self.tunnel_infinite),
959        );
960        upsert_field(
961            &mut s,
962            "Start_Offset_X",
963            GffValue::Single(self.start_offset_x),
964        );
965        upsert_field(
966            &mut s,
967            "Start_Offset_Y",
968            GffValue::Single(self.start_offset_y),
969        );
970        upsert_field(
971            &mut s,
972            "Start_Offset_Z",
973            GffValue::Single(self.start_offset_z),
974        );
975        upsert_field(
976            &mut s,
977            "Target_Offset_X",
978            GffValue::Single(self.target_offset_x),
979        );
980        upsert_field(
981            &mut s,
982            "Target_Offset_Y",
983            GffValue::Single(self.target_offset_y),
984        );
985        upsert_field(
986            &mut s,
987            "Target_Offset_Z",
988            GffValue::Single(self.target_offset_z),
989        );
990        s
991    }
992}
993
994/// Typed view over one model entry in a mini-game model list.
995#[derive(Debug, Clone, PartialEq)]
996pub struct AreMiniGameModel {
997    /// Model resref (`Model`).
998    pub model: ResRef,
999    /// Whether the model rotates (`RotatingModel`).
1000    pub rotating_model: bool,
1001}
1002
1003impl Default for AreMiniGameModel {
1004    fn default() -> Self {
1005        Self {
1006            model: ResRef::blank(),
1007            rotating_model: true,
1008        }
1009    }
1010}
1011
1012impl AreMiniGameModel {
1013    fn from_struct(structure: &GffStruct) -> Self {
1014        Self {
1015            model: get_resref(structure, "Model").unwrap_or_default(),
1016            rotating_model: get_bool(structure, "RotatingModel").unwrap_or(true),
1017        }
1018    }
1019
1020    fn to_struct(&self) -> GffStruct {
1021        let mut s = GffStruct::new(0);
1022        upsert_field(&mut s, "Model", GffValue::ResRef(self.model));
1023        upsert_field(
1024            &mut s,
1025            "RotatingModel",
1026            GffValue::UInt8(u8::from(self.rotating_model)),
1027        );
1028        s
1029    }
1030}
1031
1032/// Typed view over the `MiniGame.Player.Mouse` sub-struct.
1033#[derive(Debug, Clone, PartialEq, Default)]
1034pub struct AreMiniGameMouse {
1035    /// X axis index (`AxisX`).
1036    pub axis_x: u32,
1037    /// Y axis index (`AxisY`).
1038    pub axis_y: u32,
1039    /// Flip X axis (`FlipAxisX`).
1040    pub flip_axis_x: bool,
1041    /// Flip Y axis (`FlipAxisY`).
1042    pub flip_axis_y: bool,
1043}
1044
1045impl AreMiniGameMouse {
1046    fn from_struct(structure: &GffStruct) -> Self {
1047        Self {
1048            axis_x: get_u32(structure, "AxisX").unwrap_or(0),
1049            axis_y: get_u32(structure, "AxisY").unwrap_or(0),
1050            flip_axis_x: get_bool(structure, "FlipAxisX").unwrap_or(false),
1051            flip_axis_y: get_bool(structure, "FlipAxisY").unwrap_or(false),
1052        }
1053    }
1054
1055    fn to_struct(&self) -> GffStruct {
1056        let mut s = GffStruct::new(0);
1057        upsert_field(&mut s, "AxisX", GffValue::UInt32(self.axis_x));
1058        upsert_field(&mut s, "AxisY", GffValue::UInt32(self.axis_y));
1059        upsert_field(
1060            &mut s,
1061            "FlipAxisX",
1062            GffValue::UInt8(u8::from(self.flip_axis_x)),
1063        );
1064        upsert_field(
1065            &mut s,
1066            "FlipAxisY",
1067            GffValue::UInt8(u8::from(self.flip_axis_y)),
1068        );
1069        s
1070    }
1071}
1072
1073/// Typed view over one enemy entry in `MiniGame.Enemies`.
1074#[derive(Debug, Clone, PartialEq, Default)]
1075pub struct AreMiniGameEnemy {
1076    /// The vehicle fields shared with the player.
1077    pub vehicle: AreMiniGameVehicle,
1078    /// Enemy-only flag (`Trigger`), default `0`.
1079    ///
1080    /// The one field an enemy reads beyond the shared base. Unlike the
1081    /// carry-over fields there, the default is applied unconditionally: the
1082    /// read's presence flag is never inspected, so an absent `Trigger` lands
1083    /// as `0` exactly as if the file had written one.
1084    ///
1085    /// Kept as the byte the file holds rather than narrowed to a `bool`, even
1086    /// though vanilla only ever writes `0` or `1` and the module models its
1087    /// other byte flags as `bool`. The spec records the type and the default
1088    /// and says nothing about the value being interpreted as a flag, so
1089    /// narrowing would turn a `2` into a `1` on write on the strength of a
1090    /// guess about the name. The range vanilla happens to use is evidence
1091    /// about vanilla's content, not about the field's domain.
1092    ///
1093    /// The inconsistency with this module's six `bool` fields runs the other
1094    /// way from how it looks, and it is deliberate. `DoBumping`,
1095    /// `UseInertia`, `CameraRotate`, `RotatingModel`, `FlipAxisX` and
1096    /// `FlipAxisY` appear nowhere in the minigame or ARE specs: they predate
1097    /// the audit that covered this subsystem, so each is a `bool` on the
1098    /// strength of its name alone. `Trigger` is the one with a traced read.
1099    /// Do not "fix" it to match them.
1100    ///
1101    /// Left as is rather than widened, because a corpus round-trip reports no
1102    /// changed values across ARE, which means no vanilla file carries a value
1103    /// outside `0`/`1` in any of the six. The narrowing is lossless against
1104    /// real content and latent rather than live, so this is a note and a
1105    /// cheap future audit question, not a defect.
1106    pub trigger: u8,
1107}
1108
1109impl AreMiniGameEnemy {
1110    fn from_struct(structure: &GffStruct) -> Result<Self, AreError> {
1111        Ok(Self {
1112            vehicle: AreMiniGameVehicle::from_struct(structure, &ENEMY_PATHS)?,
1113            trigger: get_u8(structure, "Trigger").unwrap_or(0),
1114        })
1115    }
1116
1117    fn to_struct(&self) -> GffStruct {
1118        let mut s = GffStruct::new(0);
1119        self.vehicle.write_into(&mut s);
1120        upsert_field(&mut s, "Trigger", GffValue::UInt8(self.trigger));
1121        s
1122    }
1123}
1124
1125/// Typed view over one obstacle entry in `MiniGame.Obstacles`.
1126///
1127/// The lightest of the three object shapes. `CSWMGObstacle::Load` matches the
1128/// entry to an already-placed object by `Name` and reads its `Scripts` struct,
1129/// and nothing else: no lifecycle, no weapons, no track geometry.
1130#[derive(Debug, Clone, PartialEq, Default)]
1131pub struct AreMiniGameObstacle {
1132    /// Obstacle name resref (`Name`), matched against a placed object.
1133    pub name: ResRef,
1134    /// Script slots (`Scripts`), the base set with none of the vehicle
1135    /// additions.
1136    pub scripts: AreMiniGameObjectScripts,
1137}
1138
1139impl AreMiniGameObstacle {
1140    fn from_struct(structure: &GffStruct) -> Result<Self, AreError> {
1141        let scripts = match structure.field("Scripts") {
1142            Some(GffValue::Struct(s)) => AreMiniGameObjectScripts::from_struct(s),
1143            Some(_) => {
1144                return Err(AreError::TypeMismatch {
1145                    field: "MiniGame.Obstacles[].Scripts",
1146                    expected: "Struct",
1147                });
1148            }
1149            None => AreMiniGameObjectScripts::default(),
1150        };
1151
1152        Ok(Self {
1153            name: get_resref(structure, "Name").unwrap_or_default(),
1154            scripts,
1155        })
1156    }
1157
1158    fn to_struct(&self) -> GffStruct {
1159        let mut s = GffStruct::new(0);
1160        upsert_field(&mut s, "Name", GffValue::ResRef(self.name));
1161        upsert_field(
1162            &mut s,
1163            "Scripts",
1164            GffValue::Struct(Box::new(self.scripts.to_struct())),
1165        );
1166        s
1167    }
1168}
1169
1170/// One entry in a `Models` list.
1171static MODEL_CHILDREN: &[FieldSchema] = &[
1172    FieldSchema {
1173        label: "Model",
1174        expected_type: GffType::ResRef,
1175        life: FieldLife::Live,
1176        required: false,
1177        absent: AbsentDefault::Unverified,
1178        children: None,
1179        constraint: None,
1180    },
1181    FieldSchema {
1182        label: "RotatingModel",
1183        expected_type: GffType::UInt8,
1184        life: FieldLife::Live,
1185        required: false,
1186        absent: AbsentDefault::Unverified,
1187        children: None,
1188        constraint: None,
1189    },
1190];
1191
1192/// The `Scripts` struct on an obstacle: the base set and nothing else.
1193static OBJECT_SCRIPTS_CHILDREN: &[FieldSchema] = &[
1194    FieldSchema {
1195        label: "OnCreate",
1196        expected_type: GffType::ResRef,
1197        life: FieldLife::Live,
1198        required: false,
1199        absent: AbsentDefault::Unverified,
1200        children: None,
1201        constraint: None,
1202    },
1203    FieldSchema {
1204        label: "OnHeartbeat",
1205        expected_type: GffType::ResRef,
1206        life: FieldLife::Live,
1207        required: false,
1208        absent: AbsentDefault::Unverified,
1209        children: None,
1210        constraint: None,
1211    },
1212    FieldSchema {
1213        label: "OnAnimEvent",
1214        expected_type: GffType::ResRef,
1215        life: FieldLife::Live,
1216        required: false,
1217        absent: AbsentDefault::Unverified,
1218        children: None,
1219        constraint: None,
1220    },
1221    FieldSchema {
1222        label: "OnHitBullet",
1223        expected_type: GffType::ResRef,
1224        life: FieldLife::Live,
1225        required: false,
1226        absent: AbsentDefault::Unverified,
1227        children: None,
1228        constraint: None,
1229    },
1230    FieldSchema {
1231        label: "OnHitFollower",
1232        expected_type: GffType::ResRef,
1233        life: FieldLife::Live,
1234        required: false,
1235        absent: AbsentDefault::Unverified,
1236        children: None,
1237        constraint: None,
1238    },
1239];
1240
1241/// The `Scripts` struct on a vehicle: the base set plus five.
1242static VEHICLE_SCRIPTS_CHILDREN: &[FieldSchema] = &[
1243    FieldSchema {
1244        label: "OnCreate",
1245        expected_type: GffType::ResRef,
1246        life: FieldLife::Live,
1247        required: false,
1248        absent: AbsentDefault::Unverified,
1249        children: None,
1250        constraint: None,
1251    },
1252    FieldSchema {
1253        label: "OnHeartbeat",
1254        expected_type: GffType::ResRef,
1255        life: FieldLife::Live,
1256        required: false,
1257        absent: AbsentDefault::Unverified,
1258        children: None,
1259        constraint: None,
1260    },
1261    FieldSchema {
1262        label: "OnAnimEvent",
1263        expected_type: GffType::ResRef,
1264        life: FieldLife::Live,
1265        required: false,
1266        absent: AbsentDefault::Unverified,
1267        children: None,
1268        constraint: None,
1269    },
1270    FieldSchema {
1271        label: "OnHitBullet",
1272        expected_type: GffType::ResRef,
1273        life: FieldLife::Live,
1274        required: false,
1275        absent: AbsentDefault::Unverified,
1276        children: None,
1277        constraint: None,
1278    },
1279    FieldSchema {
1280        label: "OnHitFollower",
1281        expected_type: GffType::ResRef,
1282        life: FieldLife::Live,
1283        required: false,
1284        absent: AbsentDefault::Unverified,
1285        children: None,
1286        constraint: None,
1287    },
1288    FieldSchema {
1289        label: "OnDamage",
1290        expected_type: GffType::ResRef,
1291        life: FieldLife::Live,
1292        required: false,
1293        absent: AbsentDefault::Unverified,
1294        children: None,
1295        constraint: None,
1296    },
1297    FieldSchema {
1298        label: "OnDeath",
1299        expected_type: GffType::ResRef,
1300        life: FieldLife::Live,
1301        required: false,
1302        absent: AbsentDefault::Unverified,
1303        children: None,
1304        constraint: None,
1305    },
1306    FieldSchema {
1307        label: "OnFire",
1308        expected_type: GffType::ResRef,
1309        life: FieldLife::Live,
1310        required: false,
1311        absent: AbsentDefault::Unverified,
1312        children: None,
1313        constraint: None,
1314    },
1315    FieldSchema {
1316        label: "OnHitObstacle",
1317        expected_type: GffType::ResRef,
1318        life: FieldLife::Live,
1319        required: false,
1320        absent: AbsentDefault::Unverified,
1321        children: None,
1322        constraint: None,
1323    },
1324    FieldSchema {
1325        label: "OnTrackLoop",
1326        expected_type: GffType::ResRef,
1327        life: FieldLife::Live,
1328        required: false,
1329        absent: AbsentDefault::Unverified,
1330        children: None,
1331        constraint: None,
1332    },
1333];
1334
1335/// The `Sounds` struct on a vehicle.
1336static SOUNDS_CHILDREN: &[FieldSchema] = &[
1337    FieldSchema {
1338        label: "Engine",
1339        expected_type: GffType::ResRef,
1340        life: FieldLife::Live,
1341        required: false,
1342        absent: AbsentDefault::Unverified,
1343        children: None,
1344        constraint: None,
1345    },
1346    FieldSchema {
1347        label: "Death",
1348        expected_type: GffType::ResRef,
1349        life: FieldLife::Live,
1350        required: false,
1351        absent: AbsentDefault::Unverified,
1352        children: None,
1353        constraint: None,
1354    },
1355];
1356
1357/// The `Bullet` struct inside one gun bank.
1358///
1359/// The first five are `required` because absence does something worse than
1360/// default: each read reports a presence flag gating the next, so one missing
1361/// field truncates the chain and the bank is never created. A file missing
1362/// `Speed` does not get a zero-speed bank, it gets no bank.
1363static BULLET_CHILDREN: &[FieldSchema] = &[
1364    FieldSchema {
1365        label: "Damage",
1366        expected_type: GffType::UInt32,
1367        life: FieldLife::Live,
1368        required: true,
1369        absent: AbsentDefault::Unverified,
1370        children: None,
1371        constraint: None,
1372    },
1373    FieldSchema {
1374        label: "Lifespan",
1375        expected_type: GffType::Single,
1376        life: FieldLife::Live,
1377        required: true,
1378        absent: AbsentDefault::Unverified,
1379        children: None,
1380        constraint: None,
1381    },
1382    FieldSchema {
1383        label: "Rate_Of_Fire",
1384        expected_type: GffType::Single,
1385        life: FieldLife::Live,
1386        required: true,
1387        absent: AbsentDefault::Unverified,
1388        children: None,
1389        constraint: None,
1390    },
1391    FieldSchema {
1392        label: "Speed",
1393        expected_type: GffType::Single,
1394        life: FieldLife::Live,
1395        required: true,
1396        absent: AbsentDefault::Unverified,
1397        children: None,
1398        constraint: None,
1399    },
1400    FieldSchema {
1401        label: "Target_Type",
1402        expected_type: GffType::UInt32,
1403        life: FieldLife::Live,
1404        required: true,
1405        absent: AbsentDefault::Unverified,
1406        children: None,
1407        constraint: None,
1408    },
1409    FieldSchema {
1410        label: "Bullet_Model",
1411        expected_type: GffType::ResRef,
1412        life: FieldLife::Live,
1413        required: false,
1414        absent: AbsentDefault::Unverified,
1415        children: None,
1416        constraint: None,
1417    },
1418    FieldSchema {
1419        label: "Collision_Sound",
1420        expected_type: GffType::ResRef,
1421        life: FieldLife::Live,
1422        required: false,
1423        absent: AbsentDefault::Unverified,
1424        children: None,
1425        constraint: None,
1426    },
1427];
1428
1429/// One entry in a `Gun_Banks` list.
1430///
1431/// `BankID` and `Gun_Model` are `required` for the same reason as the
1432/// `Bullet` five: an absent `BankID` resolves to the skip sentinel and an
1433/// invalid `Gun_Model` aborts the bank, so in both cases the bank does not
1434/// exist rather than existing with a default.
1435///
1436/// The four targeting fields are read only for enemies. A player's bank
1437/// carries none of them, which is legitimate rather than missing, so they
1438/// are not `required`.
1439static GUN_BANK_CHILDREN: &[FieldSchema] = &[
1440    FieldSchema {
1441        label: "BankID",
1442        expected_type: GffType::UInt32,
1443        life: FieldLife::Live,
1444        required: true,
1445        absent: AbsentDefault::Unverified,
1446        children: None,
1447        constraint: None,
1448    },
1449    FieldSchema {
1450        label: "Gun_Model",
1451        expected_type: GffType::ResRef,
1452        life: FieldLife::Live,
1453        required: true,
1454        absent: AbsentDefault::Unverified,
1455        children: None,
1456        constraint: None,
1457    },
1458    FieldSchema {
1459        label: "Bullet",
1460        expected_type: GffType::Struct,
1461        life: FieldLife::Live,
1462        required: false,
1463        absent: AbsentDefault::Unverified,
1464        children: Some(BULLET_CHILDREN),
1465        constraint: None,
1466    },
1467    FieldSchema {
1468        label: "Fire_Sound",
1469        expected_type: GffType::ResRef,
1470        life: FieldLife::Live,
1471        required: false,
1472        absent: AbsentDefault::Unverified,
1473        children: None,
1474        constraint: None,
1475    },
1476    FieldSchema {
1477        label: "Sensing_Radius",
1478        expected_type: GffType::Single,
1479        life: FieldLife::Live,
1480        required: false,
1481        absent: AbsentDefault::Unverified,
1482        children: None,
1483        constraint: None,
1484    },
1485    FieldSchema {
1486        label: "Horiz_Spread",
1487        expected_type: GffType::Single,
1488        life: FieldLife::Live,
1489        required: false,
1490        absent: AbsentDefault::Unverified,
1491        children: None,
1492        constraint: None,
1493    },
1494    FieldSchema {
1495        label: "Vert_Spread",
1496        expected_type: GffType::Single,
1497        life: FieldLife::Live,
1498        required: false,
1499        absent: AbsentDefault::Unverified,
1500        children: None,
1501        constraint: None,
1502    },
1503    FieldSchema {
1504        label: "Inaccuracy",
1505        expected_type: GffType::Single,
1506        life: FieldLife::Live,
1507        required: false,
1508        absent: AbsentDefault::Unverified,
1509        children: None,
1510        constraint: None,
1511    },
1512];
1513
1514/// The `MiniGame.Player` struct: the shared vehicle set plus the camera
1515/// and the movement block only the player reads.
1516static PLAYER_CHILDREN: &[FieldSchema] = &[
1517    FieldSchema {
1518        label: "Models",
1519        expected_type: GffType::List,
1520        life: FieldLife::Live,
1521        required: false,
1522        absent: AbsentDefault::Unverified,
1523        children: Some(MODEL_CHILDREN),
1524        constraint: None,
1525    },
1526    FieldSchema {
1527        label: "Track",
1528        expected_type: GffType::ResRef,
1529        life: FieldLife::Live,
1530        required: false,
1531        absent: AbsentDefault::Unverified,
1532        children: None,
1533        constraint: None,
1534    },
1535    FieldSchema {
1536        label: "Hit_Points",
1537        expected_type: GffType::UInt32,
1538        life: FieldLife::Live,
1539        required: false,
1540        absent: AbsentDefault::Unverified,
1541        children: None,
1542        constraint: None,
1543    },
1544    FieldSchema {
1545        label: "Max_HPs",
1546        expected_type: GffType::UInt32,
1547        life: FieldLife::Live,
1548        required: false,
1549        absent: AbsentDefault::Unverified,
1550        children: None,
1551        constraint: None,
1552    },
1553    FieldSchema {
1554        label: "Sphere_Radius",
1555        expected_type: GffType::Single,
1556        life: FieldLife::Live,
1557        required: false,
1558        absent: AbsentDefault::Unverified,
1559        children: None,
1560        constraint: None,
1561    },
1562    FieldSchema {
1563        label: "Invince_Period",
1564        expected_type: GffType::Single,
1565        life: FieldLife::Live,
1566        required: false,
1567        absent: AbsentDefault::Unverified,
1568        children: None,
1569        constraint: None,
1570    },
1571    FieldSchema {
1572        label: "Bump_Damage",
1573        expected_type: GffType::Int32,
1574        life: FieldLife::Live,
1575        required: false,
1576        absent: AbsentDefault::Unverified,
1577        children: None,
1578        constraint: None,
1579    },
1580    FieldSchema {
1581        label: "Num_Loops",
1582        expected_type: GffType::Int32,
1583        life: FieldLife::Live,
1584        required: false,
1585        absent: AbsentDefault::Unverified,
1586        children: None,
1587        constraint: None,
1588    },
1589    FieldSchema {
1590        label: "Scripts",
1591        expected_type: GffType::Struct,
1592        life: FieldLife::Live,
1593        required: false,
1594        absent: AbsentDefault::Unverified,
1595        children: Some(VEHICLE_SCRIPTS_CHILDREN),
1596        constraint: None,
1597    },
1598    FieldSchema {
1599        label: "Sounds",
1600        expected_type: GffType::Struct,
1601        life: FieldLife::Live,
1602        required: false,
1603        absent: AbsentDefault::Unverified,
1604        children: Some(SOUNDS_CHILDREN),
1605        constraint: None,
1606    },
1607    FieldSchema {
1608        label: "Gun_Banks",
1609        expected_type: GffType::List,
1610        life: FieldLife::Live,
1611        required: false,
1612        absent: AbsentDefault::Unverified,
1613        children: Some(GUN_BANK_CHILDREN),
1614        constraint: None,
1615    },
1616    FieldSchema {
1617        label: "Camera",
1618        expected_type: GffType::ResRef,
1619        life: FieldLife::Live,
1620        required: false,
1621        absent: AbsentDefault::Unverified,
1622        children: None,
1623        constraint: None,
1624    },
1625    FieldSchema {
1626        label: "CameraRotate",
1627        expected_type: GffType::UInt8,
1628        life: FieldLife::Live,
1629        required: false,
1630        absent: AbsentDefault::Unverified,
1631        children: None,
1632        constraint: None,
1633    },
1634    FieldSchema {
1635        label: "Minimum_Speed",
1636        expected_type: GffType::Single,
1637        life: FieldLife::Live,
1638        required: false,
1639        absent: AbsentDefault::Unverified,
1640        children: None,
1641        constraint: None,
1642    },
1643    FieldSchema {
1644        label: "Maximum_Speed",
1645        expected_type: GffType::Single,
1646        life: FieldLife::Live,
1647        required: false,
1648        absent: AbsentDefault::Unverified,
1649        children: None,
1650        constraint: None,
1651    },
1652    FieldSchema {
1653        label: "Accel_Secs",
1654        expected_type: GffType::Single,
1655        life: FieldLife::Live,
1656        required: false,
1657        absent: AbsentDefault::Unverified,
1658        children: None,
1659        constraint: None,
1660    },
1661    FieldSchema {
1662        label: "TunnelXPos",
1663        expected_type: GffType::Single,
1664        life: FieldLife::Live,
1665        required: false,
1666        absent: AbsentDefault::Unverified,
1667        children: None,
1668        constraint: None,
1669    },
1670    FieldSchema {
1671        label: "TunnelXNeg",
1672        expected_type: GffType::Single,
1673        life: FieldLife::Live,
1674        required: false,
1675        absent: AbsentDefault::Unverified,
1676        children: None,
1677        constraint: None,
1678    },
1679    FieldSchema {
1680        label: "TunnelYPos",
1681        expected_type: GffType::Single,
1682        life: FieldLife::Live,
1683        required: false,
1684        absent: AbsentDefault::Unverified,
1685        children: None,
1686        constraint: None,
1687    },
1688    FieldSchema {
1689        label: "TunnelYNeg",
1690        expected_type: GffType::Single,
1691        life: FieldLife::Live,
1692        required: false,
1693        absent: AbsentDefault::Unverified,
1694        children: None,
1695        constraint: None,
1696    },
1697    FieldSchema {
1698        label: "TunnelZPos",
1699        expected_type: GffType::Single,
1700        life: FieldLife::Live,
1701        required: false,
1702        absent: AbsentDefault::Unverified,
1703        children: None,
1704        constraint: None,
1705    },
1706    FieldSchema {
1707        label: "TunnelZNeg",
1708        expected_type: GffType::Single,
1709        life: FieldLife::Live,
1710        required: false,
1711        absent: AbsentDefault::Unverified,
1712        children: None,
1713        constraint: None,
1714    },
1715    FieldSchema {
1716        label: "TunnelInfinite",
1717        expected_type: GffType::Vector3,
1718        life: FieldLife::Live,
1719        required: false,
1720        absent: AbsentDefault::Unverified,
1721        children: None,
1722        constraint: None,
1723    },
1724    FieldSchema {
1725        label: "Start_Offset_X",
1726        expected_type: GffType::Single,
1727        life: FieldLife::Live,
1728        required: false,
1729        absent: AbsentDefault::Unverified,
1730        children: None,
1731        constraint: None,
1732    },
1733    FieldSchema {
1734        label: "Start_Offset_Y",
1735        expected_type: GffType::Single,
1736        life: FieldLife::Live,
1737        required: false,
1738        absent: AbsentDefault::Unverified,
1739        children: None,
1740        constraint: None,
1741    },
1742    FieldSchema {
1743        label: "Start_Offset_Z",
1744        expected_type: GffType::Single,
1745        life: FieldLife::Live,
1746        required: false,
1747        absent: AbsentDefault::Unverified,
1748        children: None,
1749        constraint: None,
1750    },
1751    FieldSchema {
1752        label: "Target_Offset_X",
1753        expected_type: GffType::Single,
1754        life: FieldLife::Live,
1755        required: false,
1756        absent: AbsentDefault::Unverified,
1757        children: None,
1758        constraint: None,
1759    },
1760    FieldSchema {
1761        label: "Target_Offset_Y",
1762        expected_type: GffType::Single,
1763        life: FieldLife::Live,
1764        required: false,
1765        absent: AbsentDefault::Unverified,
1766        children: None,
1767        constraint: None,
1768    },
1769    FieldSchema {
1770        label: "Target_Offset_Z",
1771        expected_type: GffType::Single,
1772        life: FieldLife::Live,
1773        required: false,
1774        absent: AbsentDefault::Unverified,
1775        children: None,
1776        constraint: None,
1777    },
1778];
1779
1780/// One `MiniGame.Enemies` entry: the shared vehicle set plus `Trigger`.
1781static ENEMY_CHILDREN: &[FieldSchema] = &[
1782    FieldSchema {
1783        label: "Models",
1784        expected_type: GffType::List,
1785        life: FieldLife::Live,
1786        required: false,
1787        absent: AbsentDefault::Unverified,
1788        children: Some(MODEL_CHILDREN),
1789        constraint: None,
1790    },
1791    FieldSchema {
1792        label: "Track",
1793        expected_type: GffType::ResRef,
1794        life: FieldLife::Live,
1795        required: false,
1796        absent: AbsentDefault::Unverified,
1797        children: None,
1798        constraint: None,
1799    },
1800    FieldSchema {
1801        label: "Hit_Points",
1802        expected_type: GffType::UInt32,
1803        life: FieldLife::Live,
1804        required: false,
1805        absent: AbsentDefault::Unverified,
1806        children: None,
1807        constraint: None,
1808    },
1809    FieldSchema {
1810        label: "Max_HPs",
1811        expected_type: GffType::UInt32,
1812        life: FieldLife::Live,
1813        required: false,
1814        absent: AbsentDefault::Unverified,
1815        children: None,
1816        constraint: None,
1817    },
1818    FieldSchema {
1819        label: "Sphere_Radius",
1820        expected_type: GffType::Single,
1821        life: FieldLife::Live,
1822        required: false,
1823        absent: AbsentDefault::Unverified,
1824        children: None,
1825        constraint: None,
1826    },
1827    FieldSchema {
1828        label: "Invince_Period",
1829        expected_type: GffType::Single,
1830        life: FieldLife::Live,
1831        required: false,
1832        absent: AbsentDefault::Unverified,
1833        children: None,
1834        constraint: None,
1835    },
1836    FieldSchema {
1837        label: "Bump_Damage",
1838        expected_type: GffType::Int32,
1839        life: FieldLife::Live,
1840        required: false,
1841        absent: AbsentDefault::Unverified,
1842        children: None,
1843        constraint: None,
1844    },
1845    FieldSchema {
1846        label: "Num_Loops",
1847        expected_type: GffType::Int32,
1848        life: FieldLife::Live,
1849        required: false,
1850        absent: AbsentDefault::Unverified,
1851        children: None,
1852        constraint: None,
1853    },
1854    FieldSchema {
1855        label: "Scripts",
1856        expected_type: GffType::Struct,
1857        life: FieldLife::Live,
1858        required: false,
1859        absent: AbsentDefault::Unverified,
1860        children: Some(VEHICLE_SCRIPTS_CHILDREN),
1861        constraint: None,
1862    },
1863    FieldSchema {
1864        label: "Sounds",
1865        expected_type: GffType::Struct,
1866        life: FieldLife::Live,
1867        required: false,
1868        absent: AbsentDefault::Unverified,
1869        children: Some(SOUNDS_CHILDREN),
1870        constraint: None,
1871    },
1872    FieldSchema {
1873        label: "Gun_Banks",
1874        expected_type: GffType::List,
1875        life: FieldLife::Live,
1876        required: false,
1877        absent: AbsentDefault::Unverified,
1878        children: Some(GUN_BANK_CHILDREN),
1879        constraint: None,
1880    },
1881    FieldSchema {
1882        label: "Trigger",
1883        expected_type: GffType::UInt8,
1884        life: FieldLife::Live,
1885        required: false,
1886        absent: AbsentDefault::Unverified,
1887        children: None,
1888        constraint: None,
1889    },
1890];
1891
1892/// One `MiniGame.Obstacles` entry.
1893static OBSTACLE_CHILDREN: &[FieldSchema] = &[
1894    FieldSchema {
1895        label: "Name",
1896        expected_type: GffType::ResRef,
1897        life: FieldLife::Live,
1898        required: false,
1899        absent: AbsentDefault::Unverified,
1900        children: None,
1901        constraint: None,
1902    },
1903    FieldSchema {
1904        label: "Scripts",
1905        expected_type: GffType::Struct,
1906        life: FieldLife::Live,
1907        required: false,
1908        absent: AbsentDefault::Unverified,
1909        children: Some(OBJECT_SCRIPTS_CHILDREN),
1910        constraint: None,
1911    },
1912];
1913
1914/// The `MiniGame.Mouse` struct.
1915static MOUSE_CHILDREN: &[FieldSchema] = &[
1916    FieldSchema {
1917        label: "AxisX",
1918        expected_type: GffType::UInt32,
1919        life: FieldLife::Live,
1920        required: false,
1921        absent: AbsentDefault::Unverified,
1922        children: None,
1923        constraint: None,
1924    },
1925    FieldSchema {
1926        label: "AxisY",
1927        expected_type: GffType::UInt32,
1928        life: FieldLife::Live,
1929        required: false,
1930        absent: AbsentDefault::Unverified,
1931        children: None,
1932        constraint: None,
1933    },
1934    FieldSchema {
1935        label: "FlipAxisX",
1936        expected_type: GffType::UInt8,
1937        life: FieldLife::Live,
1938        required: false,
1939        absent: AbsentDefault::Unverified,
1940        children: None,
1941        constraint: None,
1942    },
1943    FieldSchema {
1944        label: "FlipAxisY",
1945        expected_type: GffType::UInt8,
1946        life: FieldLife::Live,
1947        required: false,
1948        absent: AbsentDefault::Unverified,
1949        children: None,
1950        constraint: None,
1951    },
1952];
1953
1954/// ARE `MiniGame` struct child schema.
1955pub(crate) static MINI_GAME_CHILDREN: &[FieldSchema] = &[
1956    FieldSchema {
1957        label: "Type",
1958        expected_type: GffType::UInt32,
1959        life: FieldLife::Live,
1960        required: false,
1961        absent: AbsentDefault::Unverified,
1962        children: None,
1963        constraint: None,
1964    },
1965    FieldSchema {
1966        label: "MovementPerSec",
1967        expected_type: GffType::Single,
1968        life: FieldLife::Live,
1969        required: false,
1970        absent: AbsentDefault::Unverified,
1971        children: None,
1972        constraint: None,
1973    },
1974    FieldSchema {
1975        label: "LateralAccel",
1976        expected_type: GffType::Single,
1977        life: FieldLife::Live,
1978        required: false,
1979        absent: AbsentDefault::Unverified,
1980        children: None,
1981        constraint: None,
1982    },
1983    FieldSchema {
1984        label: "Bump_Plane",
1985        expected_type: GffType::UInt32,
1986        life: FieldLife::Live,
1987        required: false,
1988        absent: AbsentDefault::Unverified,
1989        children: None,
1990        constraint: None,
1991    },
1992    FieldSchema {
1993        label: "DoBumping",
1994        expected_type: GffType::UInt8,
1995        life: FieldLife::Live,
1996        required: false,
1997        absent: AbsentDefault::Unverified,
1998        children: None,
1999        constraint: None,
2000    },
2001    FieldSchema {
2002        label: "UseInertia",
2003        expected_type: GffType::UInt8,
2004        life: FieldLife::Live,
2005        required: false,
2006        absent: AbsentDefault::Unverified,
2007        children: None,
2008        constraint: None,
2009    },
2010    FieldSchema {
2011        label: "DOF",
2012        expected_type: GffType::UInt32,
2013        life: FieldLife::Live,
2014        required: false,
2015        absent: AbsentDefault::Unverified,
2016        children: None,
2017        constraint: None,
2018    },
2019    FieldSchema {
2020        label: "Music",
2021        expected_type: GffType::ResRef,
2022        life: FieldLife::Live,
2023        required: false,
2024        absent: AbsentDefault::Unverified,
2025        children: None,
2026        constraint: None,
2027    },
2028    FieldSchema {
2029        label: "Far_Clip",
2030        expected_type: GffType::Single,
2031        life: FieldLife::Live,
2032        required: false,
2033        absent: AbsentDefault::Unverified,
2034        children: None,
2035        constraint: None,
2036    },
2037    FieldSchema {
2038        label: "Near_Clip",
2039        expected_type: GffType::Single,
2040        life: FieldLife::Live,
2041        required: false,
2042        absent: AbsentDefault::Unverified,
2043        children: None,
2044        constraint: None,
2045    },
2046    FieldSchema {
2047        label: "CameraViewAngle",
2048        expected_type: GffType::Single,
2049        life: FieldLife::Live,
2050        required: false,
2051        absent: AbsentDefault::Unverified,
2052        children: None,
2053        constraint: None,
2054    },
2055    FieldSchema {
2056        label: "Player",
2057        expected_type: GffType::Struct,
2058        life: FieldLife::Live,
2059        required: false,
2060        absent: AbsentDefault::Unverified,
2061        children: Some(PLAYER_CHILDREN),
2062        constraint: None,
2063    },
2064    FieldSchema {
2065        label: "Mouse",
2066        expected_type: GffType::Struct,
2067        life: FieldLife::Live,
2068        required: false,
2069        absent: AbsentDefault::Unverified,
2070        children: Some(MOUSE_CHILDREN),
2071        constraint: None,
2072    },
2073    FieldSchema {
2074        label: "Enemies",
2075        expected_type: GffType::List,
2076        life: FieldLife::Live,
2077        required: false,
2078        absent: AbsentDefault::Unverified,
2079        children: Some(ENEMY_CHILDREN),
2080        constraint: None,
2081    },
2082    FieldSchema {
2083        label: "Obstacles",
2084        expected_type: GffType::List,
2085        life: FieldLife::Live,
2086        required: false,
2087        absent: AbsentDefault::Unverified,
2088        children: Some(OBSTACLE_CHILDREN),
2089        constraint: None,
2090    },
2091];
2092
2093#[cfg(test)]
2094mod tests {
2095    use super::*;
2096
2097    use crate::are::{read_are_from_bytes, write_are_to_vec, Are};
2098    use rakata_formats::Gff;
2099
2100    /// Asserts every label the writer emits is declared in the schema at the
2101    /// level it lands on.
2102    ///
2103    /// Recursion only happens where a schema entry declares `children`, so a
2104    /// level with `children: None` is skipped rather than failed. That keeps
2105    /// the guard honest while the subsystem's child schemas are still being
2106    /// filled in: it covers exactly the levels that claim to be covered.
2107    fn assert_declared(structure: &GffStruct, schema: &[FieldSchema], path: &str) {
2108        for field in &structure.fields {
2109            let entry = schema
2110                .iter()
2111                .find(|f| f.label == field.label.as_str())
2112                .unwrap_or_else(|| {
2113                    panic!("{path}.{} is written but not in the schema", field.label)
2114                });
2115            let child_path = format!("{path}.{}", field.label);
2116            match (&field.value, entry.children) {
2117                (GffValue::Struct(inner), Some(children)) => {
2118                    assert_declared(inner, children, &child_path);
2119                }
2120                (GffValue::List(items), Some(children)) => {
2121                    for item in items {
2122                        assert_declared(item, children, &child_path);
2123                    }
2124                }
2125                _ => {}
2126            }
2127        }
2128    }
2129
2130    #[test]
2131    fn every_written_minigame_label_is_in_the_schema() {
2132        // Every optional struct present and every list non-empty, because a
2133        // default value leaves the lists empty and the walk then stops above
2134        // the levels this is meant to cover. The `Bullet` and targeting
2135        // levels sit four deep and are only reachable through a populated
2136        // gun bank.
2137        let vehicle = || AreMiniGameVehicle {
2138            models: vec![AreMiniGameModel::default()],
2139            gun_banks: vec![AreMiniGameGunBank {
2140                bullet: Some(AreMiniGameBullet::default()),
2141                targeting: Some(AreMiniGameTargeting::default()),
2142                ..AreMiniGameGunBank::default()
2143            }],
2144            ..AreMiniGameVehicle::default()
2145        };
2146        let mini_game = AreMiniGame {
2147            player: Some(AreMiniGamePlayer {
2148                vehicle: vehicle(),
2149                ..AreMiniGamePlayer::default()
2150            }),
2151            mouse: Some(AreMiniGameMouse::default()),
2152            enemies: vec![AreMiniGameEnemy {
2153                vehicle: vehicle(),
2154                trigger: 1,
2155            }],
2156            obstacles: vec![AreMiniGameObstacle::default()],
2157            ..AreMiniGame::default()
2158        };
2159
2160        let written = mini_game.to_struct();
2161        // Assert the walk reaches the deepest level before trusting that it
2162        // found nothing there. A guard that passes by stopping early is the
2163        // failure this whole batch keeps running into.
2164        let deepest = written
2165            .field("Player")
2166            .and_then(|v| match v {
2167                GffValue::Struct(p) => p.field("Gun_Banks"),
2168                _ => None,
2169            })
2170            .and_then(|v| match v {
2171                GffValue::List(banks) => banks.first().and_then(|b| b.field("Bullet")),
2172                _ => None,
2173            });
2174        assert!(
2175            matches!(deepest, Some(GffValue::Struct(_))),
2176            "the fixture must reach MiniGame.Player.Gun_Banks[].Bullet"
2177        );
2178
2179        assert_declared(&written, MINI_GAME_CHILDREN, "MiniGame");
2180    }
2181
2182    #[test]
2183    fn type_mismatch_on_mini_game_field_is_error() {
2184        let mut root = GffStruct::new(-1);
2185        root.push_field("MiniGame", GffValue::UInt32(7));
2186        let gff = Gff::new(*b"ARE ", root);
2187        let err = Are::from_gff(&gff).expect_err("must fail");
2188        assert!(matches!(
2189            err,
2190            AreError::TypeMismatch {
2191                field: "MiniGame",
2192                expected: "Struct"
2193            }
2194        ));
2195    }
2196
2197    #[test]
2198    fn mini_game_struct_roundtrips() {
2199        let mg = AreMiniGame {
2200            mini_game_type: 1,
2201            movement_per_sec: 25.0,
2202            lateral_accel: 45.0,
2203            bump_plane: 2,
2204            do_bumping: true,
2205            use_inertia: true,
2206            dof: 3,
2207            music: ResRef::new("mus_swoop").expect("valid test resref"),
2208            far_clip: 200.0,
2209            near_clip: 0.5,
2210            camera_view_angle: 70.0,
2211            player: Some(AreMiniGamePlayer {
2212                vehicle: AreMiniGameVehicle {
2213                    models: vec![AreMiniGameModel {
2214                        model: ResRef::new("swoopbike").expect("valid test resref"),
2215                        rotating_model: false,
2216                    }],
2217                    track: ResRef::new("trk_race01").expect("valid test resref"),
2218                    hit_points: 120,
2219                    max_hps: 150,
2220                    sphere_radius: 4.5,
2221                    invince_period: 0.75,
2222                    bump_damage: 9,
2223                    num_loops: 3,
2224                    scripts: AreMiniGameVehicleScripts {
2225                        object: AreMiniGameObjectScripts {
2226                            on_heartbeat: ResRef::new("mg_beat").expect("valid test resref"),
2227                            ..AreMiniGameObjectScripts::default()
2228                        },
2229                        on_fire: ResRef::new("mg_fire").expect("valid test resref"),
2230                        ..AreMiniGameVehicleScripts::default()
2231                    },
2232                    sounds: AreMiniGameSounds {
2233                        engine: ResRef::new("snd_engine").expect("valid test resref"),
2234                        death: ResRef::new("snd_boom").expect("valid test resref"),
2235                    },
2236                    gun_banks: vec![AreMiniGameGunBank {
2237                        bank_id: 0,
2238                        gun_model: ResRef::new("mgg_turret").expect("valid test resref"),
2239                        bullet: Some(AreMiniGameBullet {
2240                            damage: 30,
2241                            speed: 300.0,
2242                            ..AreMiniGameBullet::default()
2243                        }),
2244                        ..AreMiniGameGunBank::default()
2245                    }],
2246                },
2247                camera: ResRef::new("cam_swoop").expect("valid test resref"),
2248                camera_rotate: true,
2249                ..AreMiniGamePlayer::default()
2250            }),
2251            // Siblings of `Player`, matching where the files put them.
2252            mouse: Some(AreMiniGameMouse {
2253                axis_x: 1,
2254                axis_y: 2,
2255                flip_axis_x: true,
2256                flip_axis_y: false,
2257            }),
2258            enemies: vec![AreMiniGameEnemy {
2259                vehicle: AreMiniGameVehicle {
2260                    track: ResRef::new("trk_enemy01").expect("valid test resref"),
2261                    hit_points: 40,
2262                    max_hps: 40,
2263                    sphere_radius: 2.0,
2264                    num_loops: -1,
2265                    ..AreMiniGameVehicle::default()
2266                },
2267                trigger: 1,
2268            }],
2269            obstacles: vec![AreMiniGameObstacle {
2270                name: ResRef::new("obs_rock").expect("valid test resref"),
2271                scripts: AreMiniGameObjectScripts {
2272                    on_hit_follower: ResRef::new("mg_bump").expect("valid test resref"),
2273                    ..AreMiniGameObjectScripts::default()
2274                },
2275            }],
2276        };
2277
2278        let mut are = Are::new();
2279        are.mini_game = Some(mg);
2280
2281        let encoded = write_are_to_vec(&are).expect("encode");
2282        let reparsed = read_are_from_bytes(&encoded).expect("decode");
2283
2284        let mg_out = reparsed.mini_game.expect("MiniGame must survive roundtrip");
2285        assert_eq!(mg_out.mini_game_type, 1);
2286        assert!((mg_out.movement_per_sec - 25.0).abs() < f32::EPSILON);
2287        assert!((mg_out.lateral_accel - 45.0).abs() < f32::EPSILON);
2288        assert_eq!(mg_out.bump_plane, 2);
2289        assert!(mg_out.do_bumping);
2290        assert!(mg_out.use_inertia);
2291        assert_eq!(mg_out.dof, 3);
2292        assert_eq!(mg_out.music, "mus_swoop");
2293        assert!((mg_out.far_clip - 200.0).abs() < f32::EPSILON);
2294        assert!((mg_out.near_clip - 0.5).abs() < f32::EPSILON);
2295        assert!((mg_out.camera_view_angle - 70.0).abs() < f32::EPSILON);
2296
2297        let player = mg_out.player.expect("Player must survive roundtrip");
2298        assert_eq!(player.vehicle.models.len(), 1);
2299        assert_eq!(player.vehicle.models[0].model, "swoopbike");
2300        assert!(!player.vehicle.models[0].rotating_model);
2301        assert_eq!(player.vehicle.track, "trk_race01");
2302        assert_eq!(player.vehicle.hit_points, 120);
2303        assert_eq!(player.vehicle.max_hps, 150);
2304        assert!((player.vehicle.sphere_radius - 4.5).abs() < f32::EPSILON);
2305        assert!((player.vehicle.invince_period - 0.75).abs() < f32::EPSILON);
2306        assert_eq!(player.vehicle.bump_damage, 9);
2307        assert_eq!(player.vehicle.num_loops, 3);
2308        assert_eq!(player.vehicle.scripts.object.on_heartbeat, "mg_beat");
2309        assert_eq!(player.vehicle.scripts.on_fire, "mg_fire");
2310        assert_eq!(player.vehicle.sounds.engine, "snd_engine");
2311        assert_eq!(player.vehicle.sounds.death, "snd_boom");
2312        assert_eq!(player.camera, "cam_swoop");
2313        assert!(player.camera_rotate);
2314        let mouse = mg_out.mouse.as_ref().expect("Mouse must survive roundtrip");
2315        assert_eq!(mouse.axis_x, 1);
2316        assert_eq!(mouse.axis_y, 2);
2317        assert!(mouse.flip_axis_x);
2318        assert!(!mouse.flip_axis_y);
2319        assert_eq!(mg_out.enemies.len(), 1);
2320        assert!(mg_out.enemies[0].vehicle.models.is_empty());
2321        assert_eq!(mg_out.enemies[0].vehicle.track, "trk_enemy01");
2322        assert_eq!(mg_out.enemies[0].vehicle.hit_points, 40);
2323        assert_eq!(mg_out.enemies[0].vehicle.num_loops, -1);
2324        assert_eq!(mg_out.obstacles.len(), 1);
2325        assert_eq!(mg_out.obstacles[0].name, "obs_rock");
2326        assert_eq!(mg_out.obstacles[0].scripts.on_hit_follower, "mg_bump");
2327
2328        // Pin the level, not just the round-trip. The reader looked these up
2329        // on `Player` for as long as the type existed, which read every
2330        // vanilla minigame as having no enemies and no obstacles at all. A
2331        // round-trip could not catch it: nothing was read, so nothing was
2332        // written, and the two agreed.
2333        let encoded_gff = are.to_gff();
2334        let Some(GffValue::Struct(mini_game)) = encoded_gff.root.field("MiniGame") else {
2335            panic!("MiniGame must be a struct");
2336        };
2337        for label in ["Mouse", "Enemies", "Obstacles"] {
2338            assert!(
2339                mini_game.field(label).is_some(),
2340                "{label} belongs on MiniGame"
2341            );
2342        }
2343        let Some(GffValue::Struct(player_struct)) = mini_game.field("Player") else {
2344            panic!("Player must be a struct");
2345        };
2346        for label in ["Mouse", "Enemies", "Obstacles"] {
2347            assert!(
2348                player_struct.field(label).is_none(),
2349                "{label} must not be written under Player"
2350            );
2351        }
2352    }
2353
2354    #[test]
2355    fn a_gun_bank_without_a_bullet_stays_without_one() {
2356        // The engine skips a bank with no `Bullet`. Materialising an all-zero
2357        // one on read and writing it back would turn a bank the engine drops
2358        // into a bank it creates, firing zero-speed bullets. The round-trip
2359        // would look stable the whole time.
2360        let bank = AreMiniGameGunBank::from_struct(&GffStruct::new(0), "test.Bullet")
2361            .expect("a bank with no fields still parses");
2362        assert!(bank.bullet.is_none());
2363        assert_eq!(bank.bank_id, BANK_ID_NONE);
2364
2365        let written = bank.to_struct();
2366        assert!(
2367            written.field("Bullet").is_none(),
2368            "an absent Bullet must not be invented on write"
2369        );
2370    }
2371
2372    #[test]
2373    fn targeting_needs_all_four_labels_to_count_as_present() {
2374        // The engine reads the four as a presence-gated chain and aborts the
2375        // bank if one is missing, so three of four describes a bank that gets
2376        // dropped. Reporting that as a working targeting group would claim
2377        // the file says something it does not.
2378        let mut partial = GffStruct::new(0);
2379        partial.push_field("Sensing_Radius", GffValue::Single(200.0));
2380        partial.push_field("Horiz_Spread", GffValue::Single(70.0));
2381        partial.push_field("Vert_Spread", GffValue::Single(70.0));
2382        let bank = AreMiniGameGunBank::from_struct(&partial, "test.Bullet").expect("parses");
2383        assert!(bank.targeting.is_none(), "three of four is not a set");
2384
2385        let mut full = partial.clone();
2386        full.push_field("Inaccuracy", GffValue::Single(0.01));
2387        let bank = AreMiniGameGunBank::from_struct(&full, "test.Bullet").expect("parses");
2388        let targeting = bank.targeting.expect("all four present");
2389        assert!((targeting.sensing_radius - 200.0).abs() < f32::EPSILON);
2390        assert!((targeting.inaccuracy - 0.01).abs() < f32::EPSILON);
2391
2392        // A player bank carries none of them and must not gain four zeroes.
2393        let player_bank =
2394            AreMiniGameGunBank::from_struct(&GffStruct::new(0), "test.Bullet").expect("parses");
2395        let written = player_bank.to_struct();
2396        for label in AreMiniGameTargeting::LABELS {
2397            assert!(
2398                written.field(label).is_none(),
2399                "{label} belongs to enemy banks only"
2400            );
2401        }
2402    }
2403
2404    #[test]
2405    fn the_player_movement_block_round_trips_and_defaults_to_its_sentinels() {
2406        // Three of these default to something other than zero, and two of the
2407        // three are sentinels meaning "leave the constructed value alone". A
2408        // player struct with none of them set must come back saying that,
2409        // not saying the vehicle has a zero speed range.
2410        let bare = AreMiniGamePlayer::from_struct(&GffStruct::new(0)).expect("parses");
2411        assert!((bare.minimum_speed - -1.0).abs() < f32::EPSILON);
2412        assert!((bare.maximum_speed - 100.0).abs() < f32::EPSILON);
2413        assert!((bare.accel_secs - -1.0).abs() < f32::EPSILON);
2414        assert_eq!(bare.tunnel_infinite, [0.0; 3]);
2415
2416        let player = AreMiniGamePlayer {
2417            minimum_speed: 5.0,
2418            maximum_speed: 42.5,
2419            accel_secs: 2.5,
2420            tunnel_x_pos: 45.0,
2421            tunnel_x_neg: 2.0,
2422            tunnel_y_pos: 3.0,
2423            tunnel_y_neg: 4.0,
2424            tunnel_z_pos: 9999.0,
2425            tunnel_z_neg: -9999.0,
2426            tunnel_infinite: [0.0, 0.0, 1.0],
2427            start_offset_x: 7.0,
2428            start_offset_y: 8.0,
2429            start_offset_z: 9.0,
2430            target_offset_x: 10.0,
2431            target_offset_y: 11.0,
2432            target_offset_z: -5.0,
2433            ..AreMiniGamePlayer::default()
2434        };
2435
2436        let back = AreMiniGamePlayer::from_struct(&player.to_struct()).expect("parses");
2437        assert_eq!(back, player);
2438
2439        // `TunnelInfinite` is a GFF vector while the bounds it modifies are
2440        // separate floats, so the writer has to keep them different shapes.
2441        let written = player.to_struct();
2442        assert!(matches!(
2443            written.field("TunnelInfinite"),
2444            Some(GffValue::Vector3([0.0, 0.0, 1.0]))
2445        ));
2446        assert!(matches!(
2447            written.field("TunnelZPos"),
2448            Some(GffValue::Single(_))
2449        ));
2450    }
2451
2452    #[test]
2453    fn an_obstacle_writes_the_base_script_set_and_no_vehicle_slots() {
2454        // `CSWMGObstacle::Load` goes through the base LoadScripts, so the five
2455        // slots the track-follower override adds have no reader on an
2456        // obstacle. Writing them would put labels in the file that the engine
2457        // never looks for, and the corpus agrees: no vanilla obstacle carries
2458        // one. A round-trip cannot catch this on its own, since an invented
2459        // label reads back as whatever was invented.
2460        let mut are = Are::new();
2461        are.mini_game = Some(AreMiniGame {
2462            obstacles: vec![AreMiniGameObstacle::default()],
2463            ..AreMiniGame::default()
2464        });
2465
2466        let encoded = are.to_gff();
2467        let Some(GffValue::Struct(mini_game)) = encoded.root.field("MiniGame") else {
2468            panic!("MiniGame must be a struct");
2469        };
2470        let Some(GffValue::List(obstacles)) = mini_game.field("Obstacles") else {
2471            panic!("Obstacles must be a list");
2472        };
2473        let Some(GffValue::Struct(scripts)) = obstacles[0].field("Scripts") else {
2474            panic!("an obstacle must carry a Scripts struct");
2475        };
2476
2477        for label in [
2478            "OnCreate",
2479            "OnHeartbeat",
2480            "OnAnimEvent",
2481            "OnHitBullet",
2482            "OnHitFollower",
2483        ] {
2484            assert!(
2485                scripts.field(label).is_some(),
2486                "{label} is in the base script set"
2487            );
2488        }
2489        for label in [
2490            "OnDamage",
2491            "OnDeath",
2492            "OnFire",
2493            "OnHitObstacle",
2494            "OnTrackLoop",
2495        ] {
2496            assert!(
2497                scripts.field(label).is_none(),
2498                "{label} belongs to the vehicle override, not an obstacle"
2499            );
2500        }
2501    }
2502
2503    #[test]
2504    fn an_area_without_a_mouse_struct_does_not_gain_one() {
2505        // Three of the four vanilla minigame areas carry no `Mouse`. Writing
2506        // an all-zero one back would invent structure they never had, which a
2507        // defaulted field rather than an `Option` would do silently.
2508        let mut are = Are::new();
2509        are.mini_game = Some(AreMiniGame {
2510            mouse: None,
2511            ..AreMiniGame::default()
2512        });
2513
2514        let encoded = are.to_gff();
2515        let Some(GffValue::Struct(mini_game)) = encoded.root.field("MiniGame") else {
2516            panic!("MiniGame must be a struct");
2517        };
2518        assert!(mini_game.field("Mouse").is_none());
2519
2520        let reparsed = Are::from_gff(&encoded).expect("parses");
2521        assert!(reparsed.mini_game.expect("minigame").mouse.is_none());
2522    }
2523}