rakata_generics/
git.rs

1//! GIT (`.git`) typed generic wrapper.
2//!
3//! GIT (Game Instance Template) resources are GFF-backed area instance data.
4//! Each area has one GIT that holds positioned object instances (creatures,
5//! items, doors, placeables, waypoints, sounds, triggers, stores, encounters,
6//! area effects), area-level properties (music, ambient sounds, stealth XP),
7//! and a camera list.
8//!
9//! ## Scope
10//! - Typed access for all 10 instance list types with position/orientation.
11//! - Typed `AreaProperties` with music, ambient sound, and stealth XP fields.
12//! - Typed camera list with Position (Vector3) and Orientation (Quaternion).
13//! - Full typed roundtrip for all engine-read fields.
14
15use std::io::{Cursor, Read, Write};
16
17use crate::gff_helpers::{
18    get_bool, get_f32, get_i32, get_locstring, get_resref, get_string, get_u32, get_u8,
19    upsert_field,
20};
21use crate::shared::GitTriggerPoint;
22use rakata_core::{ResRef, StrRef};
23use rakata_formats::{
24    gff_schema::{FieldSchema, GffSchema, GffType},
25    read_gff, read_gff_from_bytes, write_gff, Gff, GffBinaryError, GffLocalizedString, GffStruct,
26    GffValue,
27};
28use thiserror::Error;
29
30// =========================================================================
31// Instance sub-types
32// =========================================================================
33
34/// A creature instance placed in the area (struct type 4).
35#[derive(Debug, Clone, PartialEq, Default)]
36pub struct GitCreature {
37    /// Template resref (`TemplateResRef`).
38    pub template_resref: ResRef,
39    /// X position (`XPosition`).
40    pub x_position: f32,
41    /// Y position (`YPosition`).
42    pub y_position: f32,
43    /// Z position (`ZPosition`).
44    pub z_position: f32,
45    /// X orientation (`XOrientation`).
46    pub x_orientation: f32,
47    /// Y orientation (`YOrientation`).
48    pub y_orientation: f32,
49    /// Z orientation (`ZOrientation`).
50    pub z_orientation: f32,
51    /// Runtime object ID (`ObjectId`). Save-game only.
52    pub object_id: u32,
53}
54
55impl GitCreature {
56    fn from_gff_struct(s: &GffStruct) -> Self {
57        Self {
58            template_resref: get_resref(s, "TemplateResRef").unwrap_or_default(),
59            x_position: get_f32(s, "XPosition").unwrap_or(0.0),
60            y_position: get_f32(s, "YPosition").unwrap_or(0.0),
61            z_position: get_f32(s, "ZPosition").unwrap_or(0.0),
62            x_orientation: get_f32(s, "XOrientation").unwrap_or(0.0),
63            y_orientation: get_f32(s, "YOrientation").unwrap_or(0.0),
64            z_orientation: get_f32(s, "ZOrientation").unwrap_or(0.0),
65            object_id: get_u32(s, "ObjectId").unwrap_or(0),
66        }
67    }
68
69    fn to_gff_struct(&self) -> GffStruct {
70        let mut s = GffStruct::new(4);
71        upsert_field(
72            &mut s,
73            "TemplateResRef",
74            GffValue::ResRef(self.template_resref),
75        );
76        upsert_field(&mut s, "XPosition", GffValue::Single(self.x_position));
77        upsert_field(&mut s, "YPosition", GffValue::Single(self.y_position));
78        upsert_field(&mut s, "ZPosition", GffValue::Single(self.z_position));
79        upsert_field(&mut s, "XOrientation", GffValue::Single(self.x_orientation));
80        upsert_field(&mut s, "YOrientation", GffValue::Single(self.y_orientation));
81        upsert_field(&mut s, "ZOrientation", GffValue::Single(self.z_orientation));
82        upsert_field(&mut s, "ObjectId", GffValue::UInt32(self.object_id));
83        s
84    }
85}
86
87/// An item instance placed in the area (struct type 0).
88#[derive(Debug, Clone, PartialEq, Default)]
89pub struct GitItem {
90    /// Template resref (`TemplateResRef`).
91    pub template_resref: ResRef,
92    /// X position (`XPosition`).
93    pub x_position: f32,
94    /// Y position (`YPosition`).
95    pub y_position: f32,
96    /// Z position (`ZPosition`).
97    pub z_position: f32,
98    /// X orientation (`XOrientation`).
99    pub x_orientation: f32,
100    /// Y orientation (`YOrientation`).
101    pub y_orientation: f32,
102    /// Z orientation (`ZOrientation`).
103    pub z_orientation: f32,
104    /// Runtime object ID (`ObjectId`). Save-game only.
105    pub object_id: u32,
106}
107
108impl GitItem {
109    fn from_gff_struct(s: &GffStruct) -> Self {
110        Self {
111            template_resref: get_resref(s, "TemplateResRef").unwrap_or_default(),
112            x_position: get_f32(s, "XPosition").unwrap_or(0.0),
113            y_position: get_f32(s, "YPosition").unwrap_or(0.0),
114            z_position: get_f32(s, "ZPosition").unwrap_or(0.0),
115            x_orientation: get_f32(s, "XOrientation").unwrap_or(0.0),
116            y_orientation: get_f32(s, "YOrientation").unwrap_or(0.0),
117            z_orientation: get_f32(s, "ZOrientation").unwrap_or(0.0),
118            object_id: get_u32(s, "ObjectId").unwrap_or(0),
119        }
120    }
121
122    fn to_gff_struct(&self) -> GffStruct {
123        let mut s = GffStruct::new(0);
124        upsert_field(
125            &mut s,
126            "TemplateResRef",
127            GffValue::ResRef(self.template_resref),
128        );
129        upsert_field(&mut s, "XPosition", GffValue::Single(self.x_position));
130        upsert_field(&mut s, "YPosition", GffValue::Single(self.y_position));
131        upsert_field(&mut s, "ZPosition", GffValue::Single(self.z_position));
132        upsert_field(&mut s, "XOrientation", GffValue::Single(self.x_orientation));
133        upsert_field(&mut s, "YOrientation", GffValue::Single(self.y_orientation));
134        upsert_field(&mut s, "ZOrientation", GffValue::Single(self.z_orientation));
135        upsert_field(&mut s, "ObjectId", GffValue::UInt32(self.object_id));
136        s
137    }
138}
139
140/// A door instance placed in the area (struct type 8).
141///
142/// Doors use `Bearing` for orientation and `X`/`Y`/`Z` for position (not
143/// `XPosition`/`YPosition`/`ZPosition`). No `TemplateResRef` - doors are
144/// matched to their layout definition by the engine.
145#[derive(Debug, Clone, PartialEq, Default)]
146pub struct GitDoor {
147    /// Facing angle in radians (`Bearing`).
148    pub bearing: f32,
149    /// X position (`X`).
150    pub x: f32,
151    /// Y position (`Y`).
152    pub y: f32,
153    /// Z position (`Z`).
154    pub z: f32,
155    /// Runtime object ID (`ObjectId`). Save-game only.
156    pub object_id: u32,
157}
158
159impl GitDoor {
160    fn from_gff_struct(s: &GffStruct) -> Self {
161        Self {
162            bearing: get_f32(s, "Bearing").unwrap_or(0.0),
163            x: get_f32(s, "X").unwrap_or(0.0),
164            y: get_f32(s, "Y").unwrap_or(0.0),
165            z: get_f32(s, "Z").unwrap_or(0.0),
166            object_id: get_u32(s, "ObjectId").unwrap_or(0),
167        }
168    }
169
170    fn to_gff_struct(&self) -> GffStruct {
171        let mut s = GffStruct::new(8);
172        upsert_field(&mut s, "Bearing", GffValue::Single(self.bearing));
173        upsert_field(&mut s, "X", GffValue::Single(self.x));
174        upsert_field(&mut s, "Y", GffValue::Single(self.y));
175        upsert_field(&mut s, "Z", GffValue::Single(self.z));
176        upsert_field(&mut s, "ObjectId", GffValue::UInt32(self.object_id));
177        s
178    }
179}
180
181/// A placeable instance placed in the area (struct type 9).
182#[derive(Debug, Clone, PartialEq, Default)]
183pub struct GitPlaceable {
184    /// Template resref (`TemplateResRef`).
185    pub template_resref: ResRef,
186    /// Facing angle in radians (`Bearing`).
187    pub bearing: f32,
188    /// X position (`X`).
189    pub x: f32,
190    /// Y position (`Y`).
191    pub y: f32,
192    /// Z position (`Z`).
193    pub z: f32,
194    /// Runtime object ID (`ObjectId`). Save-game only.
195    pub object_id: u32,
196}
197
198impl GitPlaceable {
199    fn from_gff_struct(s: &GffStruct) -> Self {
200        Self {
201            template_resref: get_resref(s, "TemplateResRef").unwrap_or_default(),
202            bearing: get_f32(s, "Bearing").unwrap_or(0.0),
203            x: get_f32(s, "X").unwrap_or(0.0),
204            y: get_f32(s, "Y").unwrap_or(0.0),
205            z: get_f32(s, "Z").unwrap_or(0.0),
206            object_id: get_u32(s, "ObjectId").unwrap_or(0),
207        }
208    }
209
210    fn to_gff_struct(&self) -> GffStruct {
211        let mut s = GffStruct::new(9);
212        upsert_field(
213            &mut s,
214            "TemplateResRef",
215            GffValue::ResRef(self.template_resref),
216        );
217        upsert_field(&mut s, "Bearing", GffValue::Single(self.bearing));
218        upsert_field(&mut s, "X", GffValue::Single(self.x));
219        upsert_field(&mut s, "Y", GffValue::Single(self.y));
220        upsert_field(&mut s, "Z", GffValue::Single(self.z));
221        upsert_field(&mut s, "ObjectId", GffValue::UInt32(self.object_id));
222        s
223    }
224}
225
226/// A waypoint instance placed in the area (struct type 5).
227///
228/// Waypoints carry only position - orientation and template are resolved
229/// by the engine from the linked UTW resource.
230#[derive(Debug, Clone, PartialEq, Default)]
231pub struct GitWaypoint {
232    /// X position (`XPosition`).
233    pub x_position: f32,
234    /// Y position (`YPosition`).
235    pub y_position: f32,
236    /// Z position (`ZPosition`).
237    pub z_position: f32,
238    /// Runtime object ID (`ObjectId`). Save-game only.
239    pub object_id: u32,
240}
241
242impl GitWaypoint {
243    fn from_gff_struct(s: &GffStruct) -> Self {
244        Self {
245            x_position: get_f32(s, "XPosition").unwrap_or(0.0),
246            y_position: get_f32(s, "YPosition").unwrap_or(0.0),
247            z_position: get_f32(s, "ZPosition").unwrap_or(0.0),
248            object_id: get_u32(s, "ObjectId").unwrap_or(0),
249        }
250    }
251
252    fn to_gff_struct(&self) -> GffStruct {
253        let mut s = GffStruct::new(5);
254        upsert_field(&mut s, "XPosition", GffValue::Single(self.x_position));
255        upsert_field(&mut s, "YPosition", GffValue::Single(self.y_position));
256        upsert_field(&mut s, "ZPosition", GffValue::Single(self.z_position));
257        upsert_field(&mut s, "ObjectId", GffValue::UInt32(self.object_id));
258        s
259    }
260}
261
262/// A sound instance placed in the area (struct type 6).
263#[derive(Debug, Clone, PartialEq, Default)]
264pub struct GitSound {
265    /// Template resref (`TemplateResRef`).
266    pub template_resref: ResRef,
267    /// Generated type (`GeneratedType`).
268    pub generated_type: u32,
269    /// X position (`XPosition`).
270    pub x_position: f32,
271    /// Y position (`YPosition`).
272    pub y_position: f32,
273    /// Z position (`ZPosition`).
274    pub z_position: f32,
275    /// Runtime object ID (`ObjectId`). Save-game only.
276    pub object_id: u32,
277}
278
279impl GitSound {
280    fn from_gff_struct(s: &GffStruct) -> Self {
281        Self {
282            template_resref: get_resref(s, "TemplateResRef").unwrap_or_default(),
283            generated_type: get_u32(s, "GeneratedType").unwrap_or(0),
284            x_position: get_f32(s, "XPosition").unwrap_or(0.0),
285            y_position: get_f32(s, "YPosition").unwrap_or(0.0),
286            z_position: get_f32(s, "ZPosition").unwrap_or(0.0),
287            object_id: get_u32(s, "ObjectId").unwrap_or(0),
288        }
289    }
290
291    fn to_gff_struct(&self) -> GffStruct {
292        let mut s = GffStruct::new(6);
293        upsert_field(
294            &mut s,
295            "TemplateResRef",
296            GffValue::ResRef(self.template_resref),
297        );
298        upsert_field(
299            &mut s,
300            "GeneratedType",
301            GffValue::UInt32(self.generated_type),
302        );
303        upsert_field(&mut s, "XPosition", GffValue::Single(self.x_position));
304        upsert_field(&mut s, "YPosition", GffValue::Single(self.y_position));
305        upsert_field(&mut s, "ZPosition", GffValue::Single(self.z_position));
306        upsert_field(&mut s, "ObjectId", GffValue::UInt32(self.object_id));
307        s
308    }
309}
310
311/// A trigger instance placed in the area (struct type 1).
312///
313/// Triggers carry geometry (vertex polygon) and optional area-transition
314/// fields (`LinkedToModule`, `LinkedTo`, `LinkedToFlags`,
315/// `TransitionDestination`).
316#[derive(Debug, Clone, PartialEq)]
317pub struct GitTrigger {
318    /// Template resref (`TemplateResRef`).
319    pub template_resref: ResRef,
320    /// Linked module resref (`LinkedToModule`). Area transition target.
321    pub linked_to_module: ResRef,
322    /// Transition destination text (`TransitionDestin`, truncated from
323    /// `TransitionDestination` by the 16-byte GFF label limit).
324    pub transition_destination: GffLocalizedString,
325    /// Linked-to tag (`LinkedTo`). Waypoint tag in the target module.
326    pub linked_to: String,
327    /// Linked-to flags (`LinkedToFlags`).
328    pub linked_to_flags: u8,
329    /// X position (`XPosition`).
330    pub x_position: f32,
331    /// Y position (`YPosition`).
332    pub y_position: f32,
333    /// Z position (`ZPosition`).
334    pub z_position: f32,
335    /// Geometry polygon vertices (`Geometry`).
336    pub geometry: Vec<GitTriggerPoint>,
337    /// Runtime object ID (`ObjectId`). Save-game only.
338    pub object_id: u32,
339}
340
341impl Default for GitTrigger {
342    fn default() -> Self {
343        Self {
344            template_resref: ResRef::blank(),
345            linked_to_module: ResRef::blank(),
346            transition_destination: GffLocalizedString::new(StrRef::invalid()),
347            linked_to: String::new(),
348            linked_to_flags: 0,
349            x_position: 0.0,
350            y_position: 0.0,
351            z_position: 0.0,
352            geometry: Vec::new(),
353            object_id: 0,
354        }
355    }
356}
357
358impl GitTrigger {
359    fn from_gff_struct(s: &GffStruct) -> Self {
360        let geometry = match s.field("Geometry") {
361            Some(GffValue::List(elements)) => elements
362                .iter()
363                .map(GitTriggerPoint::from_gff_struct)
364                .collect(),
365            _ => Vec::new(),
366        };
367
368        Self {
369            template_resref: get_resref(s, "TemplateResRef").unwrap_or_default(),
370            linked_to_module: get_resref(s, "LinkedToModule").unwrap_or_default(),
371            transition_destination: get_locstring(s, "TransitionDestin")
372                .cloned()
373                .unwrap_or_else(|| GffLocalizedString::new(StrRef::invalid())),
374            linked_to: get_string(s, "LinkedTo").unwrap_or_default(),
375            linked_to_flags: get_u8(s, "LinkedToFlags").unwrap_or(0),
376            x_position: get_f32(s, "XPosition").unwrap_or(0.0),
377            y_position: get_f32(s, "YPosition").unwrap_or(0.0),
378            z_position: get_f32(s, "ZPosition").unwrap_or(0.0),
379            geometry,
380            object_id: get_u32(s, "ObjectId").unwrap_or(0),
381        }
382    }
383
384    fn to_gff_struct(&self) -> GffStruct {
385        let mut s = GffStruct::new(1);
386        upsert_field(
387            &mut s,
388            "TemplateResRef",
389            GffValue::ResRef(self.template_resref),
390        );
391        upsert_field(
392            &mut s,
393            "LinkedToModule",
394            GffValue::ResRef(self.linked_to_module),
395        );
396        upsert_field(
397            &mut s,
398            "TransitionDestin",
399            GffValue::LocalizedString(self.transition_destination.clone()),
400        );
401        upsert_field(&mut s, "LinkedTo", GffValue::String(self.linked_to.clone()));
402        upsert_field(
403            &mut s,
404            "LinkedToFlags",
405            GffValue::UInt8(self.linked_to_flags),
406        );
407        upsert_field(&mut s, "XPosition", GffValue::Single(self.x_position));
408        upsert_field(&mut s, "YPosition", GffValue::Single(self.y_position));
409        upsert_field(&mut s, "ZPosition", GffValue::Single(self.z_position));
410        let geom_structs: Vec<GffStruct> =
411            self.geometry.iter().map(|p| p.to_gff_struct()).collect();
412        upsert_field(&mut s, "Geometry", GffValue::List(geom_structs));
413        upsert_field(&mut s, "ObjectId", GffValue::UInt32(self.object_id));
414        s
415    }
416}
417
418/// A store instance placed in the area (struct type 0xb).
419///
420/// Stores use `ResRef` (not `TemplateResRef`) for the template reference.
421#[derive(Debug, Clone, PartialEq, Default)]
422pub struct GitStore {
423    /// Template resref (`ResRef`). Note the non-standard field name.
424    pub resref: ResRef,
425    /// X orientation (`XOrientation`).
426    pub x_orientation: f32,
427    /// Y orientation (`YOrientation`).
428    pub y_orientation: f32,
429    /// Z orientation (`ZOrientation`).
430    pub z_orientation: f32,
431    /// X position (`XPosition`).
432    pub x_position: f32,
433    /// Y position (`YPosition`).
434    pub y_position: f32,
435    /// Z position (`ZPosition`).
436    pub z_position: f32,
437    /// Runtime object ID (`ObjectId`). Save-game only.
438    pub object_id: u32,
439}
440
441impl GitStore {
442    fn from_gff_struct(s: &GffStruct) -> Self {
443        Self {
444            resref: get_resref(s, "ResRef").unwrap_or_default(),
445            x_orientation: get_f32(s, "XOrientation").unwrap_or(0.0),
446            y_orientation: get_f32(s, "YOrientation").unwrap_or(0.0),
447            z_orientation: get_f32(s, "ZOrientation").unwrap_or(0.0),
448            x_position: get_f32(s, "XPosition").unwrap_or(0.0),
449            y_position: get_f32(s, "YPosition").unwrap_or(0.0),
450            z_position: get_f32(s, "ZPosition").unwrap_or(0.0),
451            object_id: get_u32(s, "ObjectId").unwrap_or(0),
452        }
453    }
454
455    fn to_gff_struct(&self) -> GffStruct {
456        let mut s = GffStruct::new(11);
457        upsert_field(&mut s, "ResRef", GffValue::ResRef(self.resref));
458        upsert_field(&mut s, "XOrientation", GffValue::Single(self.x_orientation));
459        upsert_field(&mut s, "YOrientation", GffValue::Single(self.y_orientation));
460        upsert_field(&mut s, "ZOrientation", GffValue::Single(self.z_orientation));
461        upsert_field(&mut s, "XPosition", GffValue::Single(self.x_position));
462        upsert_field(&mut s, "YPosition", GffValue::Single(self.y_position));
463        upsert_field(&mut s, "ZPosition", GffValue::Single(self.z_position));
464        upsert_field(&mut s, "ObjectId", GffValue::UInt32(self.object_id));
465        s
466    }
467}
468
469/// A vertex in an encounter geometry polygon (`X`/`Y`/`Z`).
470#[derive(Debug, Clone, PartialEq, Default)]
471pub struct GitEncounterPoint {
472    /// X coordinate (`X`).
473    pub x: f32,
474    /// Y coordinate (`Y`).
475    pub y: f32,
476    /// Z coordinate (`Z`).
477    pub z: f32,
478}
479
480impl GitEncounterPoint {
481    fn from_gff_struct(s: &GffStruct) -> Self {
482        Self {
483            x: get_f32(s, "X").unwrap_or(0.0),
484            y: get_f32(s, "Y").unwrap_or(0.0),
485            z: get_f32(s, "Z").unwrap_or(0.0),
486        }
487    }
488
489    fn to_gff_struct(&self) -> GffStruct {
490        let mut s = GffStruct::new(0);
491        upsert_field(&mut s, "X", GffValue::Single(self.x));
492        upsert_field(&mut s, "Y", GffValue::Single(self.y));
493        upsert_field(&mut s, "Z", GffValue::Single(self.z));
494        s
495    }
496}
497
498/// A spawn point within an encounter (`X`/`Y`/`Z`/`Orientation`).
499#[derive(Debug, Clone, PartialEq, Default)]
500pub struct GitSpawnPoint {
501    /// X coordinate (`X`).
502    pub x: f32,
503    /// Y coordinate (`Y`).
504    pub y: f32,
505    /// Z coordinate (`Z`).
506    pub z: f32,
507    /// Facing angle (`Orientation`).
508    pub orientation: f32,
509}
510
511impl GitSpawnPoint {
512    fn from_gff_struct(s: &GffStruct) -> Self {
513        Self {
514            x: get_f32(s, "X").unwrap_or(0.0),
515            y: get_f32(s, "Y").unwrap_or(0.0),
516            z: get_f32(s, "Z").unwrap_or(0.0),
517            orientation: get_f32(s, "Orientation").unwrap_or(0.0),
518        }
519    }
520
521    fn to_gff_struct(&self) -> GffStruct {
522        let mut s = GffStruct::new(0);
523        upsert_field(&mut s, "X", GffValue::Single(self.x));
524        upsert_field(&mut s, "Y", GffValue::Single(self.y));
525        upsert_field(&mut s, "Z", GffValue::Single(self.z));
526        upsert_field(&mut s, "Orientation", GffValue::Single(self.orientation));
527        s
528    }
529}
530
531/// An encounter instance placed in the area (struct type 7).
532#[derive(Debug, Clone, PartialEq, Default)]
533pub struct GitEncounter {
534    /// Template resref (`TemplateResRef`).
535    pub template_resref: ResRef,
536    /// X position (`XPosition`).
537    pub x_position: f32,
538    /// Y position (`YPosition`).
539    pub y_position: f32,
540    /// Z position (`ZPosition`).
541    pub z_position: f32,
542    /// Geometry polygon vertices (`Geometry`).
543    pub geometry: Vec<GitEncounterPoint>,
544    /// Spawn point list (`SpawnPointList`).
545    pub spawn_points: Vec<GitSpawnPoint>,
546    /// Runtime object ID (`ObjectId`). Save-game only.
547    pub object_id: u32,
548}
549
550impl GitEncounter {
551    fn from_gff_struct(s: &GffStruct) -> Self {
552        let geometry = match s.field("Geometry") {
553            Some(GffValue::List(elements)) => elements
554                .iter()
555                .map(GitEncounterPoint::from_gff_struct)
556                .collect(),
557            _ => Vec::new(),
558        };
559
560        let spawn_points = match s.field("SpawnPointList") {
561            Some(GffValue::List(elements)) => elements
562                .iter()
563                .map(GitSpawnPoint::from_gff_struct)
564                .collect(),
565            _ => Vec::new(),
566        };
567
568        Self {
569            template_resref: get_resref(s, "TemplateResRef").unwrap_or_default(),
570            x_position: get_f32(s, "XPosition").unwrap_or(0.0),
571            y_position: get_f32(s, "YPosition").unwrap_or(0.0),
572            z_position: get_f32(s, "ZPosition").unwrap_or(0.0),
573            geometry,
574            spawn_points,
575            object_id: get_u32(s, "ObjectId").unwrap_or(0),
576        }
577    }
578
579    fn to_gff_struct(&self) -> GffStruct {
580        let mut s = GffStruct::new(7);
581        upsert_field(
582            &mut s,
583            "TemplateResRef",
584            GffValue::ResRef(self.template_resref),
585        );
586        upsert_field(&mut s, "XPosition", GffValue::Single(self.x_position));
587        upsert_field(&mut s, "YPosition", GffValue::Single(self.y_position));
588        upsert_field(&mut s, "ZPosition", GffValue::Single(self.z_position));
589        let geom_structs: Vec<GffStruct> =
590            self.geometry.iter().map(|p| p.to_gff_struct()).collect();
591        upsert_field(&mut s, "Geometry", GffValue::List(geom_structs));
592        let spawn_structs: Vec<GffStruct> = self
593            .spawn_points
594            .iter()
595            .map(|sp| sp.to_gff_struct())
596            .collect();
597        upsert_field(&mut s, "SpawnPointList", GffValue::List(spawn_structs));
598        upsert_field(&mut s, "ObjectId", GffValue::UInt32(self.object_id));
599        s
600    }
601}
602
603/// An area-of-effect instance in the area (struct type 0xd).
604///
605/// Area effects use `OrientationX`/`PositionX` naming (not `XOrientation`
606/// or `XPosition`).
607#[derive(Debug, Clone, PartialEq, Default)]
608pub struct GitAreaEffect {
609    /// X orientation (`OrientationX`).
610    pub orientation_x: f32,
611    /// Y orientation (`OrientationY`).
612    pub orientation_y: f32,
613    /// Z orientation (`OrientationZ`).
614    pub orientation_z: f32,
615    /// X position (`PositionX`).
616    pub position_x: f32,
617    /// Y position (`PositionY`).
618    pub position_y: f32,
619    /// Z position (`PositionZ`).
620    pub position_z: f32,
621    /// Runtime object ID (`ObjectId`). Save-game only.
622    pub object_id: u32,
623}
624
625impl GitAreaEffect {
626    fn from_gff_struct(s: &GffStruct) -> Self {
627        Self {
628            orientation_x: get_f32(s, "OrientationX").unwrap_or(0.0),
629            orientation_y: get_f32(s, "OrientationY").unwrap_or(0.0),
630            orientation_z: get_f32(s, "OrientationZ").unwrap_or(0.0),
631            position_x: get_f32(s, "PositionX").unwrap_or(0.0),
632            position_y: get_f32(s, "PositionY").unwrap_or(0.0),
633            position_z: get_f32(s, "PositionZ").unwrap_or(0.0),
634            object_id: get_u32(s, "ObjectId").unwrap_or(0),
635        }
636    }
637
638    fn to_gff_struct(&self) -> GffStruct {
639        let mut s = GffStruct::new(13);
640        upsert_field(&mut s, "OrientationX", GffValue::Single(self.orientation_x));
641        upsert_field(&mut s, "OrientationY", GffValue::Single(self.orientation_y));
642        upsert_field(&mut s, "OrientationZ", GffValue::Single(self.orientation_z));
643        upsert_field(&mut s, "PositionX", GffValue::Single(self.position_x));
644        upsert_field(&mut s, "PositionY", GffValue::Single(self.position_y));
645        upsert_field(&mut s, "PositionZ", GffValue::Single(self.position_z));
646        upsert_field(&mut s, "ObjectId", GffValue::UInt32(self.object_id));
647        s
648    }
649}
650
651// =========================================================================
652// Non-instance sub-types
653// =========================================================================
654
655/// Area-level properties (music, ambient sound, stealth XP, fog).
656///
657/// This is a single `Struct` in the GIT root (field label `AreaProperties`).
658#[derive(Debug, Clone, PartialEq, Default)]
659pub struct GitAreaProperties {
660    /// Unescapable flag (`Unescapable`).
661    pub unescapable: bool,
662    /// Stealth XP maximum (`StealthXPMax`).
663    pub stealth_xp_max: u32,
664    /// Current stealth XP count (`StealthXPCurrent`). Save-game state.
665    pub stealth_xp_current: u32,
666    /// Stealth XP loss per detection (`StealthXPLoss`).
667    pub stealth_xp_loss: u32,
668    /// Stealth XP enabled flag (`StealthXPEnabled`).
669    pub stealth_xp_enabled: bool,
670    /// Transition pending flag (`TransPending`). Save-game state.
671    pub trans_pending: bool,
672    /// Pending transition next ID (`TransPendNextID`). Save-game state.
673    pub trans_pend_next_id: u8,
674    /// Pending transition current ID (`TransPendCurrID`). Save-game state.
675    pub trans_pend_curr_id: u8,
676    /// Sun/fog color packed as RGBA (`SunFogColor`).
677    pub sun_fog_color: u32,
678    /// Music delay in ms (`MusicDelay`).
679    pub music_delay: i32,
680    /// Daytime music track ID (`MusicDay`). References ambientmusic.2da.
681    pub music_day: i32,
682    /// Night-time music track ID (`MusicNight`). References ambientmusic.2da.
683    pub music_night: i32,
684    /// Battle music track ID (`MusicBattle`). References ambientmusic.2da.
685    pub music_battle: i32,
686    /// Daytime ambient sound ID (`AmbientSndDay`). References ambientsound.2da.
687    pub ambient_snd_day: i32,
688    /// Night-time ambient sound ID (`AmbientSndNight`). References ambientsound.2da.
689    pub ambient_snd_night: i32,
690    /// Daytime ambient sound volume (`AmbientSndDayVol`).
691    pub ambient_snd_day_vol: i32,
692    /// Night-time ambient sound volume (`AmbientSndNitVol`).
693    pub ambient_snd_nit_vol: i32,
694}
695
696impl GitAreaProperties {
697    fn from_gff_struct(s: &GffStruct) -> Self {
698        Self {
699            unescapable: get_bool(s, "Unescapable").unwrap_or(false),
700            stealth_xp_max: get_u32(s, "StealthXPMax").unwrap_or(0),
701            stealth_xp_current: get_u32(s, "StealthXPCurrent").unwrap_or(0),
702            stealth_xp_loss: get_u32(s, "StealthXPLoss").unwrap_or(0),
703            stealth_xp_enabled: get_bool(s, "StealthXPEnabled").unwrap_or(false),
704            trans_pending: get_bool(s, "TransPending").unwrap_or(false),
705            trans_pend_next_id: get_u8(s, "TransPendNextID").unwrap_or(0),
706            trans_pend_curr_id: get_u8(s, "TransPendCurrID").unwrap_or(0),
707            sun_fog_color: get_u32(s, "SunFogColor").unwrap_or(0),
708            music_delay: get_i32(s, "MusicDelay").unwrap_or(0),
709            music_day: get_i32(s, "MusicDay").unwrap_or(0),
710            music_night: get_i32(s, "MusicNight").unwrap_or(0),
711            music_battle: get_i32(s, "MusicBattle").unwrap_or(0),
712            ambient_snd_day: get_i32(s, "AmbientSndDay").unwrap_or(0),
713            ambient_snd_night: get_i32(s, "AmbientSndNight").unwrap_or(0),
714            ambient_snd_day_vol: get_i32(s, "AmbientSndDayVol").unwrap_or(0),
715            ambient_snd_nit_vol: get_i32(s, "AmbientSndNitVol").unwrap_or(0),
716        }
717    }
718
719    fn to_gff_struct(&self) -> GffStruct {
720        let mut s = GffStruct::new(0);
721        upsert_field(
722            &mut s,
723            "Unescapable",
724            GffValue::UInt8(u8::from(self.unescapable)),
725        );
726        upsert_field(
727            &mut s,
728            "StealthXPMax",
729            GffValue::UInt32(self.stealth_xp_max),
730        );
731        upsert_field(
732            &mut s,
733            "StealthXPCurrent",
734            GffValue::UInt32(self.stealth_xp_current),
735        );
736        upsert_field(
737            &mut s,
738            "StealthXPLoss",
739            GffValue::UInt32(self.stealth_xp_loss),
740        );
741        upsert_field(
742            &mut s,
743            "StealthXPEnabled",
744            GffValue::UInt8(u8::from(self.stealth_xp_enabled)),
745        );
746        upsert_field(
747            &mut s,
748            "TransPending",
749            GffValue::UInt8(u8::from(self.trans_pending)),
750        );
751        upsert_field(
752            &mut s,
753            "TransPendNextID",
754            GffValue::UInt8(self.trans_pend_next_id),
755        );
756        upsert_field(
757            &mut s,
758            "TransPendCurrID",
759            GffValue::UInt8(self.trans_pend_curr_id),
760        );
761        upsert_field(&mut s, "SunFogColor", GffValue::UInt32(self.sun_fog_color));
762        upsert_field(&mut s, "MusicDelay", GffValue::Int32(self.music_delay));
763        upsert_field(&mut s, "MusicDay", GffValue::Int32(self.music_day));
764        upsert_field(&mut s, "MusicNight", GffValue::Int32(self.music_night));
765        upsert_field(&mut s, "MusicBattle", GffValue::Int32(self.music_battle));
766        upsert_field(
767            &mut s,
768            "AmbientSndDay",
769            GffValue::Int32(self.ambient_snd_day),
770        );
771        upsert_field(
772            &mut s,
773            "AmbientSndNight",
774            GffValue::Int32(self.ambient_snd_night),
775        );
776        upsert_field(
777            &mut s,
778            "AmbientSndDayVol",
779            GffValue::Int32(self.ambient_snd_day_vol),
780        );
781        upsert_field(
782            &mut s,
783            "AmbientSndNitVol",
784            GffValue::Int32(self.ambient_snd_nit_vol),
785        );
786        s
787    }
788}
789
790/// A static camera entry in the area.
791///
792/// `Position` is a GFF Vector3 and `Orientation` is a GFF Quaternion (Vector4).
793#[derive(Debug, Clone, PartialEq, Default)]
794pub struct GitCamera {
795    /// Camera ID (`CameraID`).
796    pub camera_id: i32,
797    /// Position as GFF Vector3 (`Position`).
798    pub position: [f32; 3],
799    /// Orientation as GFF Quaternion (`Orientation`).
800    pub orientation: [f32; 4],
801    /// Pitch angle (`Pitch`).
802    pub pitch: f32,
803    /// Height offset (`Height`).
804    pub height: f32,
805    /// Field of view (`FieldOfView`, engine default 55.0).
806    pub field_of_view: f32,
807    /// Microphone range (`MicRange`).
808    pub mic_range: f32,
809}
810
811impl GitCamera {
812    fn from_gff_struct(s: &GffStruct) -> Self {
813        let position = match s.field("Position") {
814            Some(GffValue::Vector3(v)) => *v,
815            _ => [0.0, 0.0, 0.0],
816        };
817        let orientation = match s.field("Orientation") {
818            Some(GffValue::Vector4(v)) => *v,
819            _ => [0.0, 0.0, 0.0, 1.0],
820        };
821
822        Self {
823            camera_id: get_i32(s, "CameraID").unwrap_or(-1),
824            position,
825            orientation,
826            pitch: get_f32(s, "Pitch").unwrap_or(0.0),
827            height: get_f32(s, "Height").unwrap_or(0.0),
828            field_of_view: get_f32(s, "FieldOfView").unwrap_or(55.0),
829            mic_range: get_f32(s, "MicRange").unwrap_or(0.0),
830        }
831    }
832
833    fn to_gff_struct(&self) -> GffStruct {
834        let mut s = GffStruct::new(0);
835        upsert_field(&mut s, "CameraID", GffValue::Int32(self.camera_id));
836        upsert_field(&mut s, "Position", GffValue::Vector3(self.position));
837        upsert_field(&mut s, "Orientation", GffValue::Vector4(self.orientation));
838        upsert_field(&mut s, "Pitch", GffValue::Single(self.pitch));
839        upsert_field(&mut s, "Height", GffValue::Single(self.height));
840        upsert_field(&mut s, "FieldOfView", GffValue::Single(self.field_of_view));
841        upsert_field(&mut s, "MicRange", GffValue::Single(self.mic_range));
842        s
843    }
844}
845
846// =========================================================================
847// Root GIT struct
848// =========================================================================
849
850/// Typed GIT model built from/to [`Gff`] data.
851///
852/// GIT is the area instance container. It places object instances by
853/// referencing templates and providing position/orientation data. All
854/// engine-read fields are typed; roundtrip is fully lossless for typed
855/// fields.
856#[derive(Debug, Clone, PartialEq, Default)]
857pub struct Git {
858    // --- Root scalars ---
859    /// Current weather type (`CurrentWeather`).
860    pub current_weather: u8,
861    /// Weather active flag (`WeatherStarted`).
862    pub weather_started: bool,
863    /// Use-templates flag (`UseTemplates`).
864    pub use_templates: bool,
865
866    // --- Object instance lists (10) ---
867    /// Creature instances (`Creature List`).
868    pub creatures: Vec<GitCreature>,
869    /// Item instances (`List`).
870    pub items: Vec<GitItem>,
871    /// Door instances (`Door List`).
872    pub doors: Vec<GitDoor>,
873    /// Placeable instances (`Placeable List`).
874    pub placeables: Vec<GitPlaceable>,
875    /// Waypoint instances (`WaypointList`).
876    pub waypoints: Vec<GitWaypoint>,
877    /// Sound instances (`SoundList`).
878    pub sounds: Vec<GitSound>,
879    /// Trigger instances (`TriggerList`).
880    pub triggers: Vec<GitTrigger>,
881    /// Store instances (`StoreList`).
882    pub stores: Vec<GitStore>,
883    /// Encounter instances (`Encounter List`).
884    pub encounters: Vec<GitEncounter>,
885    /// Area-of-effect instances (`AreaEffectList`).
886    pub area_effects: Vec<GitAreaEffect>,
887
888    // --- Nested structs ---
889    /// Area-level properties (`AreaProperties`).
890    pub area_properties: Option<GitAreaProperties>,
891
892    // --- Camera list ---
893    /// Static cameras (`CameraList`).
894    pub cameras: Vec<GitCamera>,
895}
896
897impl Git {
898    /// Creates an empty GIT value.
899    pub fn new() -> Self {
900        Self::default()
901    }
902
903    /// Builds typed GIT data from a parsed GFF container.
904    pub fn from_gff(gff: &Gff) -> Result<Self, GitError> {
905        if gff.file_type != *b"GIT " && gff.file_type != *b"GFF " {
906            return Err(GitError::UnsupportedFileType(gff.file_type));
907        }
908
909        let root = &gff.root;
910
911        fn read_list<T>(root: &GffStruct, label: &str, f: fn(&GffStruct) -> T) -> Vec<T> {
912            match root.field(label) {
913                Some(GffValue::List(elements)) => elements.iter().map(f).collect(),
914                _ => Vec::new(),
915            }
916        }
917
918        let area_properties = match root.field("AreaProperties") {
919            Some(GffValue::Struct(s)) => Some(GitAreaProperties::from_gff_struct(s)),
920            _ => None,
921        };
922
923        Ok(Self {
924            current_weather: get_u8(root, "CurrentWeather").unwrap_or(0),
925            weather_started: get_bool(root, "WeatherStarted").unwrap_or(false),
926            use_templates: get_bool(root, "UseTemplates").unwrap_or(false),
927
928            creatures: read_list(root, "Creature List", GitCreature::from_gff_struct),
929            items: read_list(root, "List", GitItem::from_gff_struct),
930            doors: read_list(root, "Door List", GitDoor::from_gff_struct),
931            placeables: read_list(root, "Placeable List", GitPlaceable::from_gff_struct),
932            waypoints: read_list(root, "WaypointList", GitWaypoint::from_gff_struct),
933            sounds: read_list(root, "SoundList", GitSound::from_gff_struct),
934            triggers: read_list(root, "TriggerList", GitTrigger::from_gff_struct),
935            stores: read_list(root, "StoreList", GitStore::from_gff_struct),
936            encounters: read_list(root, "Encounter List", GitEncounter::from_gff_struct),
937            area_effects: read_list(root, "AreaEffectList", GitAreaEffect::from_gff_struct),
938
939            area_properties,
940            cameras: read_list(root, "CameraList", GitCamera::from_gff_struct),
941        })
942    }
943
944    /// Converts this typed GIT value into a GFF container.
945    pub fn to_gff(&self) -> Gff {
946        let mut root = GffStruct::new(-1);
947
948        upsert_field(
949            &mut root,
950            "CurrentWeather",
951            GffValue::UInt8(self.current_weather),
952        );
953        upsert_field(
954            &mut root,
955            "WeatherStarted",
956            GffValue::UInt8(u8::from(self.weather_started)),
957        );
958        upsert_field(
959            &mut root,
960            "UseTemplates",
961            GffValue::UInt8(u8::from(self.use_templates)),
962        );
963
964        fn write_list<T>(root: &mut GffStruct, label: &str, items: &[T], f: fn(&T) -> GffStruct) {
965            let structs: Vec<GffStruct> = items.iter().map(f).collect();
966            upsert_field(root, label, GffValue::List(structs));
967        }
968
969        write_list(
970            &mut root,
971            "Creature List",
972            &self.creatures,
973            GitCreature::to_gff_struct,
974        );
975        write_list(&mut root, "List", &self.items, GitItem::to_gff_struct);
976        write_list(&mut root, "Door List", &self.doors, GitDoor::to_gff_struct);
977        write_list(
978            &mut root,
979            "Placeable List",
980            &self.placeables,
981            GitPlaceable::to_gff_struct,
982        );
983        write_list(
984            &mut root,
985            "WaypointList",
986            &self.waypoints,
987            GitWaypoint::to_gff_struct,
988        );
989        write_list(
990            &mut root,
991            "SoundList",
992            &self.sounds,
993            GitSound::to_gff_struct,
994        );
995        write_list(
996            &mut root,
997            "TriggerList",
998            &self.triggers,
999            GitTrigger::to_gff_struct,
1000        );
1001        write_list(
1002            &mut root,
1003            "StoreList",
1004            &self.stores,
1005            GitStore::to_gff_struct,
1006        );
1007        write_list(
1008            &mut root,
1009            "Encounter List",
1010            &self.encounters,
1011            GitEncounter::to_gff_struct,
1012        );
1013        write_list(
1014            &mut root,
1015            "AreaEffectList",
1016            &self.area_effects,
1017            GitAreaEffect::to_gff_struct,
1018        );
1019
1020        if let Some(ref ap) = self.area_properties {
1021            upsert_field(
1022                &mut root,
1023                "AreaProperties",
1024                GffValue::Struct(Box::new(ap.to_gff_struct())),
1025            );
1026        }
1027
1028        write_list(
1029            &mut root,
1030            "CameraList",
1031            &self.cameras,
1032            GitCamera::to_gff_struct,
1033        );
1034
1035        Gff::new(*b"GIT ", root)
1036    }
1037}
1038
1039/// Errors produced while reading or writing typed GIT data.
1040#[derive(Debug, Error)]
1041pub enum GitError {
1042    /// Source file type is not supported by this parser.
1043    #[error("unsupported GIT file type: {0:?}")]
1044    UnsupportedFileType([u8; 4]),
1045    /// Underlying GFF parser/writer error.
1046    #[error(transparent)]
1047    Gff(#[from] GffBinaryError),
1048}
1049
1050/// Reads typed GIT data from a reader at the current stream position.
1051#[cfg_attr(
1052    feature = "tracing",
1053    tracing::instrument(level = "debug", skip(reader))
1054)]
1055pub fn read_git<R: Read>(reader: &mut R) -> Result<Git, GitError> {
1056    let gff = read_gff(reader)?;
1057    Git::from_gff(&gff)
1058}
1059
1060/// Reads typed GIT data directly from bytes.
1061#[cfg_attr(
1062    feature = "tracing",
1063    tracing::instrument(level = "debug", skip(bytes), fields(bytes_len = bytes.len()))
1064)]
1065pub fn read_git_from_bytes(bytes: &[u8]) -> Result<Git, GitError> {
1066    let gff = read_gff_from_bytes(bytes)?;
1067    Git::from_gff(&gff)
1068}
1069
1070/// Writes typed GIT data to an output writer.
1071#[cfg_attr(
1072    feature = "tracing",
1073    tracing::instrument(level = "debug", skip(writer, git))
1074)]
1075pub fn write_git<W: Write>(writer: &mut W, git: &Git) -> Result<(), GitError> {
1076    let gff = git.to_gff();
1077    write_gff(writer, &gff)?;
1078    Ok(())
1079}
1080
1081/// Serializes typed GIT data into a byte vector.
1082#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(git)))]
1083pub fn write_git_to_vec(git: &Git) -> Result<Vec<u8>, GitError> {
1084    let mut cursor = Cursor::new(Vec::new());
1085    write_git(&mut cursor, git)?;
1086    Ok(cursor.into_inner())
1087}
1088
1089// =========================================================================
1090// Leaf sub-schemas (no nested children)
1091// =========================================================================
1092
1093/// GIT `Creature List` entry child schema (struct type 4).
1094static CREATURE_LIST_CHILDREN: &[FieldSchema] = &[
1095    FieldSchema {
1096        label: "ObjectId",
1097        expected_type: GffType::UInt32,
1098        required: false,
1099        children: None,
1100        constraint: None,
1101    },
1102    FieldSchema {
1103        label: "TemplateResRef",
1104        expected_type: GffType::ResRef,
1105        required: false,
1106        children: None,
1107        constraint: None,
1108    },
1109    FieldSchema {
1110        label: "XPosition",
1111        expected_type: GffType::Single,
1112        required: false,
1113        children: None,
1114        constraint: None,
1115    },
1116    FieldSchema {
1117        label: "YPosition",
1118        expected_type: GffType::Single,
1119        required: false,
1120        children: None,
1121        constraint: None,
1122    },
1123    FieldSchema {
1124        label: "ZPosition",
1125        expected_type: GffType::Single,
1126        required: false,
1127        children: None,
1128        constraint: None,
1129    },
1130    FieldSchema {
1131        label: "XOrientation",
1132        expected_type: GffType::Single,
1133        required: false,
1134        children: None,
1135        constraint: None,
1136    },
1137    FieldSchema {
1138        label: "YOrientation",
1139        expected_type: GffType::Single,
1140        required: false,
1141        children: None,
1142        constraint: None,
1143    },
1144    FieldSchema {
1145        label: "ZOrientation",
1146        expected_type: GffType::Single,
1147        required: false,
1148        children: None,
1149        constraint: None,
1150    },
1151];
1152
1153/// GIT `List` (item) entry child schema (struct type 0).
1154static ITEM_LIST_CHILDREN: &[FieldSchema] = &[
1155    FieldSchema {
1156        label: "ObjectId",
1157        expected_type: GffType::UInt32,
1158        required: false,
1159        children: None,
1160        constraint: None,
1161    },
1162    FieldSchema {
1163        label: "TemplateResRef",
1164        expected_type: GffType::ResRef,
1165        required: false,
1166        children: None,
1167        constraint: None,
1168    },
1169    FieldSchema {
1170        label: "XPosition",
1171        expected_type: GffType::Single,
1172        required: false,
1173        children: None,
1174        constraint: None,
1175    },
1176    FieldSchema {
1177        label: "YPosition",
1178        expected_type: GffType::Single,
1179        required: false,
1180        children: None,
1181        constraint: None,
1182    },
1183    FieldSchema {
1184        label: "ZPosition",
1185        expected_type: GffType::Single,
1186        required: false,
1187        children: None,
1188        constraint: None,
1189    },
1190    FieldSchema {
1191        label: "XOrientation",
1192        expected_type: GffType::Single,
1193        required: false,
1194        children: None,
1195        constraint: None,
1196    },
1197    FieldSchema {
1198        label: "YOrientation",
1199        expected_type: GffType::Single,
1200        required: false,
1201        children: None,
1202        constraint: None,
1203    },
1204    FieldSchema {
1205        label: "ZOrientation",
1206        expected_type: GffType::Single,
1207        required: false,
1208        children: None,
1209        constraint: None,
1210    },
1211];
1212
1213/// GIT `Door List` entry child schema (struct type 8).
1214static DOOR_LIST_CHILDREN: &[FieldSchema] = &[
1215    FieldSchema {
1216        label: "ObjectId",
1217        expected_type: GffType::UInt32,
1218        required: false,
1219        children: None,
1220        constraint: None,
1221    },
1222    FieldSchema {
1223        label: "Bearing",
1224        expected_type: GffType::Single,
1225        required: false,
1226        children: None,
1227        constraint: None,
1228    },
1229    FieldSchema {
1230        label: "X",
1231        expected_type: GffType::Single,
1232        required: false,
1233        children: None,
1234        constraint: None,
1235    },
1236    FieldSchema {
1237        label: "Y",
1238        expected_type: GffType::Single,
1239        required: false,
1240        children: None,
1241        constraint: None,
1242    },
1243    FieldSchema {
1244        label: "Z",
1245        expected_type: GffType::Single,
1246        required: false,
1247        children: None,
1248        constraint: None,
1249    },
1250];
1251
1252/// GIT `Placeable List` entry child schema (struct type 9).
1253static PLACEABLE_LIST_CHILDREN: &[FieldSchema] = &[
1254    FieldSchema {
1255        label: "ObjectId",
1256        expected_type: GffType::UInt32,
1257        required: false,
1258        children: None,
1259        constraint: None,
1260    },
1261    FieldSchema {
1262        label: "TemplateResRef",
1263        expected_type: GffType::ResRef,
1264        required: false,
1265        children: None,
1266        constraint: None,
1267    },
1268    FieldSchema {
1269        label: "Bearing",
1270        expected_type: GffType::Single,
1271        required: false,
1272        children: None,
1273        constraint: None,
1274    },
1275    FieldSchema {
1276        label: "X",
1277        expected_type: GffType::Single,
1278        required: false,
1279        children: None,
1280        constraint: None,
1281    },
1282    FieldSchema {
1283        label: "Y",
1284        expected_type: GffType::Single,
1285        required: false,
1286        children: None,
1287        constraint: None,
1288    },
1289    FieldSchema {
1290        label: "Z",
1291        expected_type: GffType::Single,
1292        required: false,
1293        children: None,
1294        constraint: None,
1295    },
1296];
1297
1298/// GIT `WaypointList` entry child schema (struct type 5).
1299static WAYPOINT_LIST_CHILDREN: &[FieldSchema] = &[
1300    FieldSchema {
1301        label: "ObjectId",
1302        expected_type: GffType::UInt32,
1303        required: false,
1304        children: None,
1305        constraint: None,
1306    },
1307    FieldSchema {
1308        label: "XPosition",
1309        expected_type: GffType::Single,
1310        required: false,
1311        children: None,
1312        constraint: None,
1313    },
1314    FieldSchema {
1315        label: "YPosition",
1316        expected_type: GffType::Single,
1317        required: false,
1318        children: None,
1319        constraint: None,
1320    },
1321    FieldSchema {
1322        label: "ZPosition",
1323        expected_type: GffType::Single,
1324        required: false,
1325        children: None,
1326        constraint: None,
1327    },
1328];
1329
1330/// GIT `SoundList` entry child schema (struct type 6).
1331static SOUND_LIST_CHILDREN: &[FieldSchema] = &[
1332    FieldSchema {
1333        label: "ObjectId",
1334        expected_type: GffType::UInt32,
1335        required: false,
1336        children: None,
1337        constraint: None,
1338    },
1339    FieldSchema {
1340        label: "TemplateResRef",
1341        expected_type: GffType::ResRef,
1342        required: false,
1343        children: None,
1344        constraint: None,
1345    },
1346    FieldSchema {
1347        label: "GeneratedType",
1348        expected_type: GffType::UInt32,
1349        required: false,
1350        children: None,
1351        constraint: None,
1352    },
1353    FieldSchema {
1354        label: "XPosition",
1355        expected_type: GffType::Single,
1356        required: false,
1357        children: None,
1358        constraint: None,
1359    },
1360    FieldSchema {
1361        label: "YPosition",
1362        expected_type: GffType::Single,
1363        required: false,
1364        children: None,
1365        constraint: None,
1366    },
1367    FieldSchema {
1368        label: "ZPosition",
1369        expected_type: GffType::Single,
1370        required: false,
1371        children: None,
1372        constraint: None,
1373    },
1374];
1375
1376/// GIT `TriggerList` geometry entry child schema (PointX/PointY/PointZ).
1377static TRIGGER_GEOMETRY_CHILDREN: &[FieldSchema] = &[
1378    FieldSchema {
1379        label: "PointX",
1380        expected_type: GffType::Single,
1381        required: false,
1382        children: None,
1383        constraint: None,
1384    },
1385    FieldSchema {
1386        label: "PointY",
1387        expected_type: GffType::Single,
1388        required: false,
1389        children: None,
1390        constraint: None,
1391    },
1392    FieldSchema {
1393        label: "PointZ",
1394        expected_type: GffType::Single,
1395        required: false,
1396        children: None,
1397        constraint: None,
1398    },
1399];
1400
1401/// GIT `TriggerList` entry child schema (struct type 1).
1402static TRIGGER_LIST_CHILDREN: &[FieldSchema] = &[
1403    FieldSchema {
1404        label: "ObjectId",
1405        expected_type: GffType::UInt32,
1406        required: false,
1407        children: None,
1408        constraint: None,
1409    },
1410    FieldSchema {
1411        label: "TemplateResRef",
1412        expected_type: GffType::ResRef,
1413        required: false,
1414        children: None,
1415        constraint: None,
1416    },
1417    FieldSchema {
1418        label: "LinkedToModule",
1419        expected_type: GffType::ResRef,
1420        required: false,
1421        children: None,
1422        constraint: None,
1423    },
1424    FieldSchema {
1425        label: "TransitionDestin",
1426        expected_type: GffType::LocalizedString,
1427        required: false,
1428        children: None,
1429        constraint: None,
1430    },
1431    FieldSchema {
1432        label: "LinkedTo",
1433        expected_type: GffType::String,
1434        required: false,
1435        children: None,
1436        constraint: None,
1437    },
1438    FieldSchema {
1439        label: "LinkedToFlags",
1440        expected_type: GffType::UInt8,
1441        required: false,
1442        children: None,
1443        constraint: None,
1444    },
1445    FieldSchema {
1446        label: "XPosition",
1447        expected_type: GffType::Single,
1448        required: false,
1449        children: None,
1450        constraint: None,
1451    },
1452    FieldSchema {
1453        label: "YPosition",
1454        expected_type: GffType::Single,
1455        required: false,
1456        children: None,
1457        constraint: None,
1458    },
1459    FieldSchema {
1460        label: "ZPosition",
1461        expected_type: GffType::Single,
1462        required: false,
1463        children: None,
1464        constraint: None,
1465    },
1466    FieldSchema {
1467        label: "Geometry",
1468        expected_type: GffType::List,
1469        required: false,
1470        children: Some(TRIGGER_GEOMETRY_CHILDREN),
1471        constraint: None,
1472    },
1473];
1474
1475/// GIT `StoreList` entry child schema (struct type 0xb).
1476static STORE_LIST_CHILDREN: &[FieldSchema] = &[
1477    FieldSchema {
1478        label: "ObjectId",
1479        expected_type: GffType::UInt32,
1480        required: false,
1481        children: None,
1482        constraint: None,
1483    },
1484    FieldSchema {
1485        label: "ResRef",
1486        expected_type: GffType::ResRef,
1487        required: false,
1488        children: None,
1489        constraint: None,
1490    },
1491    FieldSchema {
1492        label: "XOrientation",
1493        expected_type: GffType::Single,
1494        required: false,
1495        children: None,
1496        constraint: None,
1497    },
1498    FieldSchema {
1499        label: "YOrientation",
1500        expected_type: GffType::Single,
1501        required: false,
1502        children: None,
1503        constraint: None,
1504    },
1505    FieldSchema {
1506        label: "ZOrientation",
1507        expected_type: GffType::Single,
1508        required: false,
1509        children: None,
1510        constraint: None,
1511    },
1512    FieldSchema {
1513        label: "XPosition",
1514        expected_type: GffType::Single,
1515        required: false,
1516        children: None,
1517        constraint: None,
1518    },
1519    FieldSchema {
1520        label: "YPosition",
1521        expected_type: GffType::Single,
1522        required: false,
1523        children: None,
1524        constraint: None,
1525    },
1526    FieldSchema {
1527        label: "ZPosition",
1528        expected_type: GffType::Single,
1529        required: false,
1530        children: None,
1531        constraint: None,
1532    },
1533];
1534
1535/// GIT `Encounter List` geometry entry child schema (X/Y/Z).
1536static ENCOUNTER_GEOMETRY_CHILDREN: &[FieldSchema] = &[
1537    FieldSchema {
1538        label: "X",
1539        expected_type: GffType::Single,
1540        required: false,
1541        children: None,
1542        constraint: None,
1543    },
1544    FieldSchema {
1545        label: "Y",
1546        expected_type: GffType::Single,
1547        required: false,
1548        children: None,
1549        constraint: None,
1550    },
1551    FieldSchema {
1552        label: "Z",
1553        expected_type: GffType::Single,
1554        required: false,
1555        children: None,
1556        constraint: None,
1557    },
1558];
1559
1560/// GIT `Encounter List` spawn point entry child schema (X/Y/Z/Orientation).
1561static ENCOUNTER_SPAWN_POINT_CHILDREN: &[FieldSchema] = &[
1562    FieldSchema {
1563        label: "X",
1564        expected_type: GffType::Single,
1565        required: false,
1566        children: None,
1567        constraint: None,
1568    },
1569    FieldSchema {
1570        label: "Y",
1571        expected_type: GffType::Single,
1572        required: false,
1573        children: None,
1574        constraint: None,
1575    },
1576    FieldSchema {
1577        label: "Z",
1578        expected_type: GffType::Single,
1579        required: false,
1580        children: None,
1581        constraint: None,
1582    },
1583    FieldSchema {
1584        label: "Orientation",
1585        expected_type: GffType::Single,
1586        required: false,
1587        children: None,
1588        constraint: None,
1589    },
1590];
1591
1592/// GIT `Encounter List` entry child schema (struct type 7).
1593static ENCOUNTER_LIST_CHILDREN: &[FieldSchema] = &[
1594    FieldSchema {
1595        label: "ObjectId",
1596        expected_type: GffType::UInt32,
1597        required: false,
1598        children: None,
1599        constraint: None,
1600    },
1601    FieldSchema {
1602        label: "TemplateResRef",
1603        expected_type: GffType::ResRef,
1604        required: false,
1605        children: None,
1606        constraint: None,
1607    },
1608    FieldSchema {
1609        label: "XPosition",
1610        expected_type: GffType::Single,
1611        required: false,
1612        children: None,
1613        constraint: None,
1614    },
1615    FieldSchema {
1616        label: "YPosition",
1617        expected_type: GffType::Single,
1618        required: false,
1619        children: None,
1620        constraint: None,
1621    },
1622    FieldSchema {
1623        label: "ZPosition",
1624        expected_type: GffType::Single,
1625        required: false,
1626        children: None,
1627        constraint: None,
1628    },
1629    FieldSchema {
1630        label: "Geometry",
1631        expected_type: GffType::List,
1632        required: false,
1633        children: Some(ENCOUNTER_GEOMETRY_CHILDREN),
1634        constraint: None,
1635    },
1636    FieldSchema {
1637        label: "SpawnPointList",
1638        expected_type: GffType::List,
1639        required: false,
1640        children: Some(ENCOUNTER_SPAWN_POINT_CHILDREN),
1641        constraint: None,
1642    },
1643];
1644
1645/// GIT `AreaEffectList` entry child schema (struct type 0xd).
1646static AREA_EFFECT_LIST_CHILDREN: &[FieldSchema] = &[
1647    FieldSchema {
1648        label: "ObjectId",
1649        expected_type: GffType::UInt32,
1650        required: false,
1651        children: None,
1652        constraint: None,
1653    },
1654    FieldSchema {
1655        label: "OrientationX",
1656        expected_type: GffType::Single,
1657        required: false,
1658        children: None,
1659        constraint: None,
1660    },
1661    FieldSchema {
1662        label: "OrientationY",
1663        expected_type: GffType::Single,
1664        required: false,
1665        children: None,
1666        constraint: None,
1667    },
1668    FieldSchema {
1669        label: "OrientationZ",
1670        expected_type: GffType::Single,
1671        required: false,
1672        children: None,
1673        constraint: None,
1674    },
1675    FieldSchema {
1676        label: "PositionX",
1677        expected_type: GffType::Single,
1678        required: false,
1679        children: None,
1680        constraint: None,
1681    },
1682    FieldSchema {
1683        label: "PositionY",
1684        expected_type: GffType::Single,
1685        required: false,
1686        children: None,
1687        constraint: None,
1688    },
1689    FieldSchema {
1690        label: "PositionZ",
1691        expected_type: GffType::Single,
1692        required: false,
1693        children: None,
1694        constraint: None,
1695    },
1696];
1697
1698/// GIT `AreaProperties` struct child schema.
1699static AREA_PROPERTIES_CHILDREN: &[FieldSchema] = &[
1700    FieldSchema {
1701        label: "Unescapable",
1702        expected_type: GffType::UInt8,
1703        required: false,
1704        children: None,
1705        constraint: None,
1706    },
1707    FieldSchema {
1708        label: "StealthXPMax",
1709        expected_type: GffType::UInt32,
1710        required: false,
1711        children: None,
1712        constraint: None,
1713    },
1714    FieldSchema {
1715        label: "StealthXPCurrent",
1716        expected_type: GffType::UInt32,
1717        required: false,
1718        children: None,
1719        constraint: None,
1720    },
1721    FieldSchema {
1722        label: "StealthXPLoss",
1723        expected_type: GffType::UInt32,
1724        required: false,
1725        children: None,
1726        constraint: None,
1727    },
1728    FieldSchema {
1729        label: "StealthXPEnabled",
1730        expected_type: GffType::UInt8,
1731        required: false,
1732        children: None,
1733        constraint: None,
1734    },
1735    FieldSchema {
1736        label: "TransPending",
1737        expected_type: GffType::UInt8,
1738        required: false,
1739        children: None,
1740        constraint: None,
1741    },
1742    FieldSchema {
1743        label: "TransPendNextID",
1744        expected_type: GffType::UInt8,
1745        required: false,
1746        children: None,
1747        constraint: None,
1748    },
1749    FieldSchema {
1750        label: "TransPendCurrID",
1751        expected_type: GffType::UInt8,
1752        required: false,
1753        children: None,
1754        constraint: None,
1755    },
1756    FieldSchema {
1757        label: "SunFogColor",
1758        expected_type: GffType::UInt32,
1759        required: false,
1760        children: None,
1761        constraint: None,
1762    },
1763    // --- Ambient sound fields (CSWSAmbientSound::Load) ---
1764    FieldSchema {
1765        label: "MusicDelay",
1766        expected_type: GffType::Int32,
1767        required: false,
1768        children: None,
1769        constraint: None,
1770    },
1771    FieldSchema {
1772        label: "MusicDay",
1773        expected_type: GffType::Int32,
1774        required: false,
1775        children: None,
1776        constraint: None,
1777    },
1778    FieldSchema {
1779        label: "MusicNight",
1780        expected_type: GffType::Int32,
1781        required: false,
1782        children: None,
1783        constraint: None,
1784    },
1785    FieldSchema {
1786        label: "MusicBattle",
1787        expected_type: GffType::Int32,
1788        required: false,
1789        children: None,
1790        constraint: None,
1791    },
1792    FieldSchema {
1793        label: "AmbientSndDay",
1794        expected_type: GffType::Int32,
1795        required: false,
1796        children: None,
1797        constraint: None,
1798    },
1799    FieldSchema {
1800        label: "AmbientSndNight",
1801        expected_type: GffType::Int32,
1802        required: false,
1803        children: None,
1804        constraint: None,
1805    },
1806    FieldSchema {
1807        label: "AmbientSndDayVol",
1808        expected_type: GffType::Int32,
1809        required: false,
1810        children: None,
1811        constraint: None,
1812    },
1813    FieldSchema {
1814        label: "AmbientSndNitVol",
1815        expected_type: GffType::Int32,
1816        required: false,
1817        children: None,
1818        constraint: None,
1819    },
1820];
1821
1822/// GIT `AreaMap` struct child schema. Save-game only.
1823static AREA_MAP_CHILDREN: &[FieldSchema] = &[
1824    FieldSchema {
1825        label: "AreaMapResX",
1826        expected_type: GffType::Int32,
1827        required: false,
1828        children: None,
1829        constraint: None,
1830    },
1831    FieldSchema {
1832        label: "AreaMapResY",
1833        expected_type: GffType::Int32,
1834        required: false,
1835        children: None,
1836        constraint: None,
1837    },
1838    FieldSchema {
1839        label: "AreaMapDataSize",
1840        expected_type: GffType::UInt32,
1841        required: false,
1842        children: None,
1843        constraint: None,
1844    },
1845    FieldSchema {
1846        label: "AreaMapData",
1847        expected_type: GffType::Binary,
1848        required: false,
1849        children: None,
1850        constraint: None,
1851    },
1852];
1853
1854/// GIT `CameraList` entry child schema. Client-side only. Max 50 entries.
1855static CAMERA_LIST_CHILDREN: &[FieldSchema] = &[
1856    FieldSchema {
1857        label: "CameraID",
1858        expected_type: GffType::Int32,
1859        required: false,
1860        children: None,
1861        constraint: None,
1862    },
1863    FieldSchema {
1864        label: "Position",
1865        expected_type: GffType::Vector3,
1866        required: false,
1867        children: None,
1868        constraint: None,
1869    },
1870    FieldSchema {
1871        label: "Orientation",
1872        expected_type: GffType::Vector4,
1873        required: false,
1874        children: None,
1875        constraint: None,
1876    },
1877    FieldSchema {
1878        label: "Pitch",
1879        expected_type: GffType::Single,
1880        required: false,
1881        children: None,
1882        constraint: None,
1883    },
1884    FieldSchema {
1885        label: "Height",
1886        expected_type: GffType::Single,
1887        required: false,
1888        children: None,
1889        constraint: None,
1890    },
1891    FieldSchema {
1892        label: "FieldOfView",
1893        expected_type: GffType::Single,
1894        required: false,
1895        children: None,
1896        constraint: None,
1897    },
1898    FieldSchema {
1899        label: "MicRange",
1900        expected_type: GffType::Single,
1901        required: false,
1902        children: None,
1903        constraint: None,
1904    },
1905];
1906
1907impl GffSchema for Git {
1908    fn schema() -> &'static [FieldSchema] {
1909        static SCHEMA: &[FieldSchema] = &[
1910            // --- Root scalars ---
1911            FieldSchema {
1912                label: "CurrentWeather",
1913                expected_type: GffType::UInt8,
1914                required: false,
1915                children: None,
1916                constraint: None,
1917            },
1918            FieldSchema {
1919                label: "WeatherStarted",
1920                expected_type: GffType::UInt8,
1921                required: false,
1922                children: None,
1923                constraint: None,
1924            },
1925            FieldSchema {
1926                label: "UseTemplates",
1927                expected_type: GffType::UInt8,
1928                required: false,
1929                children: None,
1930                constraint: None,
1931            },
1932            // --- Object instance lists (10) ---
1933            FieldSchema {
1934                label: "Creature List",
1935                expected_type: GffType::List,
1936                required: false,
1937                children: Some(CREATURE_LIST_CHILDREN),
1938                constraint: None,
1939            },
1940            FieldSchema {
1941                label: "List",
1942                expected_type: GffType::List,
1943                required: false,
1944                children: Some(ITEM_LIST_CHILDREN),
1945                constraint: None,
1946            },
1947            FieldSchema {
1948                label: "Door List",
1949                expected_type: GffType::List,
1950                required: false,
1951                children: Some(DOOR_LIST_CHILDREN),
1952                constraint: None,
1953            },
1954            FieldSchema {
1955                label: "Placeable List",
1956                expected_type: GffType::List,
1957                required: false,
1958                children: Some(PLACEABLE_LIST_CHILDREN),
1959                constraint: None,
1960            },
1961            FieldSchema {
1962                label: "WaypointList",
1963                expected_type: GffType::List,
1964                required: false,
1965                children: Some(WAYPOINT_LIST_CHILDREN),
1966                constraint: None,
1967            },
1968            FieldSchema {
1969                label: "SoundList",
1970                expected_type: GffType::List,
1971                required: false,
1972                children: Some(SOUND_LIST_CHILDREN),
1973                constraint: None,
1974            },
1975            FieldSchema {
1976                label: "TriggerList",
1977                expected_type: GffType::List,
1978                required: false,
1979                children: Some(TRIGGER_LIST_CHILDREN),
1980                constraint: None,
1981            },
1982            FieldSchema {
1983                label: "StoreList",
1984                expected_type: GffType::List,
1985                required: false,
1986                children: Some(STORE_LIST_CHILDREN),
1987                constraint: None,
1988            },
1989            FieldSchema {
1990                label: "Encounter List",
1991                expected_type: GffType::List,
1992                required: false,
1993                children: Some(ENCOUNTER_LIST_CHILDREN),
1994                constraint: None,
1995            },
1996            FieldSchema {
1997                label: "AreaEffectList",
1998                expected_type: GffType::List,
1999                required: false,
2000                children: Some(AREA_EFFECT_LIST_CHILDREN),
2001                constraint: None,
2002            },
2003            // --- Nested structs (2) ---
2004            FieldSchema {
2005                label: "AreaProperties",
2006                expected_type: GffType::Struct,
2007                required: false,
2008                children: Some(AREA_PROPERTIES_CHILDREN),
2009                constraint: None,
2010            },
2011            FieldSchema {
2012                label: "AreaMap",
2013                expected_type: GffType::Struct,
2014                required: false,
2015                children: Some(AREA_MAP_CHILDREN),
2016                constraint: None,
2017            },
2018            // --- Camera list ---
2019            FieldSchema {
2020                label: "CameraList",
2021                expected_type: GffType::List,
2022                required: false,
2023                children: Some(CAMERA_LIST_CHILDREN),
2024                constraint: None,
2025            },
2026            // --- Variable table ---
2027            FieldSchema {
2028                label: "VarTable",
2029                expected_type: GffType::List,
2030                required: false,
2031                children: None,
2032                constraint: None,
2033            },
2034        ];
2035        SCHEMA
2036    }
2037}
2038
2039#[cfg(test)]
2040mod tests {
2041    use super::*;
2042
2043    /// Build a minimal GIT GFF for testing.
2044    fn make_test_git_gff() -> Gff {
2045        let mut root = GffStruct::new(-1);
2046        root.push_field("CurrentWeather", GffValue::UInt8(0));
2047        root.push_field("WeatherStarted", GffValue::UInt8(0));
2048        root.push_field("UseTemplates", GffValue::UInt8(1));
2049
2050        // One creature.
2051        let mut creature = GffStruct::new(4);
2052        creature.push_field("TemplateResRef", GffValue::resref_lit("n_darthbandon"));
2053        creature.push_field("XPosition", GffValue::Single(10.0));
2054        creature.push_field("YPosition", GffValue::Single(20.0));
2055        creature.push_field("ZPosition", GffValue::Single(0.5));
2056        creature.push_field("XOrientation", GffValue::Single(0.0));
2057        creature.push_field("YOrientation", GffValue::Single(1.0));
2058        creature.push_field("ZOrientation", GffValue::Single(0.0));
2059        creature.push_field("ObjectId", GffValue::UInt32(0));
2060        root.push_field("Creature List", GffValue::List(vec![creature]));
2061
2062        // One placeable.
2063        let mut placeable = GffStruct::new(9);
2064        placeable.push_field("TemplateResRef", GffValue::resref_lit("plc_footlkr"));
2065        placeable.push_field("Bearing", GffValue::Single(1.57));
2066        placeable.push_field("X", GffValue::Single(15.0));
2067        placeable.push_field("Y", GffValue::Single(25.0));
2068        placeable.push_field("Z", GffValue::Single(0.0));
2069        placeable.push_field("ObjectId", GffValue::UInt32(0));
2070        root.push_field("Placeable List", GffValue::List(vec![placeable]));
2071
2072        // One trigger with geometry.
2073        let mut trigger = GffStruct::new(1);
2074        trigger.push_field("TemplateResRef", GffValue::resref_lit("newtransition9"));
2075        trigger.push_field("LinkedToModule", GffValue::resref_lit("tar_m02aa"));
2076        trigger.push_field(
2077            "TransitionDestin",
2078            GffValue::LocalizedString(GffLocalizedString::new(StrRef::from_raw(12345))),
2079        );
2080        trigger.push_field("LinkedTo", GffValue::String("wp_target".into()));
2081        trigger.push_field("LinkedToFlags", GffValue::UInt8(2));
2082        trigger.push_field("XPosition", GffValue::Single(5.0));
2083        trigger.push_field("YPosition", GffValue::Single(5.0));
2084        trigger.push_field("ZPosition", GffValue::Single(0.0));
2085        let mut pt0 = GffStruct::new(0);
2086        pt0.push_field("PointX", GffValue::Single(0.0));
2087        pt0.push_field("PointY", GffValue::Single(0.0));
2088        pt0.push_field("PointZ", GffValue::Single(0.0));
2089        let mut pt1 = GffStruct::new(0);
2090        pt1.push_field("PointX", GffValue::Single(5.0));
2091        pt1.push_field("PointY", GffValue::Single(0.0));
2092        pt1.push_field("PointZ", GffValue::Single(0.0));
2093        let mut pt2 = GffStruct::new(0);
2094        pt2.push_field("PointX", GffValue::Single(5.0));
2095        pt2.push_field("PointY", GffValue::Single(5.0));
2096        pt2.push_field("PointZ", GffValue::Single(0.0));
2097        trigger.push_field("Geometry", GffValue::List(vec![pt0, pt1, pt2]));
2098        trigger.push_field("ObjectId", GffValue::UInt32(0));
2099        root.push_field("TriggerList", GffValue::List(vec![trigger]));
2100
2101        // AreaProperties.
2102        let mut props = GffStruct::new(0);
2103        props.push_field("Unescapable", GffValue::UInt8(0));
2104        props.push_field("StealthXPMax", GffValue::UInt32(100));
2105        props.push_field("StealthXPCurrent", GffValue::UInt32(0));
2106        props.push_field("StealthXPLoss", GffValue::UInt32(10));
2107        props.push_field("StealthXPEnabled", GffValue::UInt8(1));
2108        props.push_field("TransPending", GffValue::UInt8(0));
2109        props.push_field("TransPendNextID", GffValue::UInt8(0));
2110        props.push_field("TransPendCurrID", GffValue::UInt8(0));
2111        props.push_field("SunFogColor", GffValue::UInt32(0x00ABCDEF));
2112        props.push_field("MusicDelay", GffValue::Int32(6000));
2113        props.push_field("MusicDay", GffValue::Int32(48));
2114        props.push_field("MusicNight", GffValue::Int32(49));
2115        props.push_field("MusicBattle", GffValue::Int32(25));
2116        props.push_field("AmbientSndDay", GffValue::Int32(12));
2117        props.push_field("AmbientSndNight", GffValue::Int32(13));
2118        props.push_field("AmbientSndDayVol", GffValue::Int32(50));
2119        props.push_field("AmbientSndNitVol", GffValue::Int32(40));
2120        root.push_field("AreaProperties", GffValue::Struct(Box::new(props)));
2121
2122        // Empty lists for remaining types.
2123        root.push_field("List", GffValue::List(vec![]));
2124        root.push_field("Door List", GffValue::List(vec![]));
2125        root.push_field("WaypointList", GffValue::List(vec![]));
2126        root.push_field("SoundList", GffValue::List(vec![]));
2127        root.push_field("StoreList", GffValue::List(vec![]));
2128        root.push_field("Encounter List", GffValue::List(vec![]));
2129        root.push_field("AreaEffectList", GffValue::List(vec![]));
2130        root.push_field("CameraList", GffValue::List(vec![]));
2131
2132        Gff::new(*b"GIT ", root)
2133    }
2134
2135    #[test]
2136    fn reads_root_scalars() {
2137        let gff = make_test_git_gff();
2138        let git = Git::from_gff(&gff).expect("valid GIT GFF must parse");
2139
2140        assert_eq!(git.current_weather, 0);
2141        assert!(!git.weather_started);
2142        assert!(git.use_templates);
2143    }
2144
2145    #[test]
2146    fn reads_creature_list() {
2147        let gff = make_test_git_gff();
2148        let git = Git::from_gff(&gff).expect("valid GIT GFF must parse");
2149
2150        assert_eq!(git.creatures.len(), 1);
2151        let c = &git.creatures[0];
2152        assert_eq!(c.template_resref, "n_darthbandon");
2153        assert!((c.x_position - 10.0).abs() < f32::EPSILON);
2154        assert!((c.y_position - 20.0).abs() < f32::EPSILON);
2155        assert!((c.y_orientation - 1.0).abs() < f32::EPSILON);
2156    }
2157
2158    #[test]
2159    fn reads_placeable_list() {
2160        let gff = make_test_git_gff();
2161        let git = Git::from_gff(&gff).expect("valid GIT GFF must parse");
2162
2163        assert_eq!(git.placeables.len(), 1);
2164        let p = &git.placeables[0];
2165        assert_eq!(p.template_resref, "plc_footlkr");
2166        assert!((p.bearing - 1.57).abs() < 0.01);
2167        assert!((p.x - 15.0).abs() < f32::EPSILON);
2168    }
2169
2170    #[test]
2171    fn reads_trigger_with_geometry() {
2172        let gff = make_test_git_gff();
2173        let git = Git::from_gff(&gff).expect("valid GIT GFF must parse");
2174
2175        assert_eq!(git.triggers.len(), 1);
2176        let t = &git.triggers[0];
2177        assert_eq!(t.template_resref, "newtransition9");
2178        assert_eq!(t.linked_to_module, "tar_m02aa");
2179        assert_eq!(t.linked_to, "wp_target");
2180        assert_eq!(t.linked_to_flags, 2);
2181        assert_eq!(t.geometry.len(), 3);
2182        assert!((t.geometry[1].point_x - 5.0).abs() < f32::EPSILON);
2183    }
2184
2185    #[test]
2186    fn reads_area_properties() {
2187        let gff = make_test_git_gff();
2188        let git = Git::from_gff(&gff).expect("valid GIT GFF must parse");
2189
2190        let ap = git
2191            .area_properties
2192            .as_ref()
2193            .expect("test GFF includes AreaProperties");
2194        assert!(!ap.unescapable);
2195        assert_eq!(ap.stealth_xp_max, 100);
2196        assert!(ap.stealth_xp_enabled);
2197        assert_eq!(ap.music_day, 48);
2198        assert_eq!(ap.music_night, 49);
2199        assert_eq!(ap.music_battle, 25);
2200        assert_eq!(ap.ambient_snd_day_vol, 50);
2201        assert_eq!(ap.ambient_snd_nit_vol, 40);
2202    }
2203
2204    #[test]
2205    fn all_fields_survive_synthetic_roundtrip() {
2206        let gff = make_test_git_gff();
2207        let git = Git::from_gff(&gff).expect("valid GIT GFF must parse");
2208        let bytes = write_git_to_vec(&git).expect("write succeeds");
2209        let reparsed = read_git_from_bytes(&bytes).expect("reparse succeeds");
2210
2211        assert_eq!(reparsed, git);
2212    }
2213
2214    #[test]
2215    fn typed_edits_roundtrip_through_gff_writer() {
2216        let gff = make_test_git_gff();
2217        let mut git = Git::from_gff(&gff).expect("valid GIT GFF must parse");
2218        git.creatures[0].template_resref = ResRef::new("n_sithsoldier").expect("valid test resref");
2219        git.creatures[0].x_position = 100.0;
2220        git.area_properties
2221            .as_mut()
2222            .expect("test GFF includes AreaProperties")
2223            .music_day = 99;
2224
2225        let bytes = write_git_to_vec(&git).expect("write succeeds");
2226        let reparsed = read_git_from_bytes(&bytes).expect("reparse succeeds");
2227
2228        assert_eq!(reparsed.creatures[0].template_resref, "n_sithsoldier");
2229        assert!((reparsed.creatures[0].x_position - 100.0).abs() < f32::EPSILON);
2230        assert_eq!(
2231            reparsed
2232                .area_properties
2233                .as_ref()
2234                .expect("roundtripped GIT must have AreaProperties")
2235                .music_day,
2236            99
2237        );
2238    }
2239
2240    #[test]
2241    fn read_git_from_reader_matches_bytes_path() {
2242        let gff = make_test_git_gff();
2243        let bytes = {
2244            let mut c = Cursor::new(Vec::new());
2245            write_gff(&mut c, &gff).expect("GFF write to cursor always succeeds");
2246            c.into_inner()
2247        };
2248
2249        let mut cursor = Cursor::new(&bytes);
2250        let via_reader = read_git(&mut cursor).expect("reader parse succeeds");
2251        let via_bytes = read_git_from_bytes(&bytes).expect("bytes parse succeeds");
2252
2253        assert_eq!(via_reader, via_bytes);
2254    }
2255
2256    #[test]
2257    fn rejects_non_git_file_type() {
2258        let mut gff = make_test_git_gff();
2259        gff.file_type = *b"UTW ";
2260
2261        let err = Git::from_gff(&gff).expect_err("UTW must be rejected as GIT input");
2262        assert!(matches!(
2263            err,
2264            GitError::UnsupportedFileType(ft) if ft == *b"UTW "
2265        ));
2266    }
2267
2268    #[test]
2269    fn write_git_matches_direct_gff_writer() {
2270        let gff = make_test_git_gff();
2271        let git = Git::from_gff(&gff).expect("valid GIT GFF must parse");
2272
2273        let via_typed = write_git_to_vec(&git).expect("typed write succeeds");
2274
2275        let mut direct = Cursor::new(Vec::new());
2276        write_gff(&mut direct, &git.to_gff()).expect("direct write succeeds");
2277
2278        assert_eq!(via_typed, direct.into_inner());
2279    }
2280
2281    // --- Fixture tests ---
2282
2283    const TEST_GIT: &[u8] = include_bytes!(concat!(
2284        env!("CARGO_MANIFEST_DIR"),
2285        "/../../fixtures/test.git"
2286    ));
2287    const K1_GIT: &[u8] = include_bytes!(concat!(
2288        env!("CARGO_MANIFEST_DIR"),
2289        "/../../fixtures/k1_same_git_test.git"
2290    ));
2291
2292    #[test]
2293    fn parses_test_git_fixture() {
2294        let git = read_git_from_bytes(TEST_GIT).expect("test.git must parse");
2295        // Should have at least some instance data.
2296        let total = git.creatures.len()
2297            + git.items.len()
2298            + git.doors.len()
2299            + git.placeables.len()
2300            + git.waypoints.len()
2301            + git.sounds.len()
2302            + git.triggers.len()
2303            + git.stores.len()
2304            + git.encounters.len()
2305            + git.area_effects.len();
2306        assert!(total > 0, "fixture should have some instance data");
2307    }
2308
2309    #[test]
2310    fn parses_k1_git_fixture() {
2311        let git = read_git_from_bytes(K1_GIT).expect("k1 GIT must parse");
2312        let total = git.creatures.len()
2313            + git.items.len()
2314            + git.doors.len()
2315            + git.placeables.len()
2316            + git.waypoints.len()
2317            + git.sounds.len()
2318            + git.triggers.len()
2319            + git.stores.len()
2320            + git.encounters.len()
2321            + git.area_effects.len();
2322        assert!(total > 0, "K1 fixture should have some instance data");
2323    }
2324
2325    #[test]
2326    fn all_fields_survive_typed_roundtrip() {
2327        let git = read_git_from_bytes(TEST_GIT).expect("test.git must parse");
2328        let bytes = write_git_to_vec(&git).expect("write succeeds");
2329        let reparsed = read_git_from_bytes(&bytes).expect("reparse succeeds");
2330
2331        assert_eq!(reparsed, git);
2332    }
2333
2334    #[test]
2335    fn all_fields_survive_typed_roundtrip_k1() {
2336        let git = read_git_from_bytes(K1_GIT).expect("k1 GIT must parse");
2337        let bytes = write_git_to_vec(&git).expect("write succeeds");
2338        let reparsed = read_git_from_bytes(&bytes).expect("reparse succeeds");
2339
2340        assert_eq!(reparsed, git);
2341    }
2342
2343    // --- Schema tests ---
2344
2345    #[test]
2346    fn schema_field_count() {
2347        assert_eq!(Git::schema().len(), 17);
2348    }
2349
2350    #[test]
2351    fn schema_no_duplicate_labels() {
2352        let schema = Git::schema();
2353        let mut labels: Vec<&str> = schema.iter().map(|f| f.label).collect();
2354        labels.sort();
2355        let before = labels.len();
2356        labels.dedup();
2357        assert_eq!(before, labels.len(), "duplicate labels in GIT schema");
2358    }
2359
2360    #[test]
2361    fn schema_sub_schema_field_counts() {
2362        let schema = Git::schema();
2363
2364        let creature = schema
2365            .iter()
2366            .find(|f| f.label == "Creature List")
2367            .expect("schema must contain Creature List");
2368        assert_eq!(
2369            creature.children.expect("Creature List has children").len(),
2370            8
2371        );
2372
2373        let item = schema
2374            .iter()
2375            .find(|f| f.label == "List")
2376            .expect("schema must contain List");
2377        assert_eq!(item.children.expect("List has children").len(), 8);
2378
2379        let door = schema
2380            .iter()
2381            .find(|f| f.label == "Door List")
2382            .expect("schema must contain Door List");
2383        assert_eq!(door.children.expect("Door List has children").len(), 5);
2384
2385        let placeable = schema
2386            .iter()
2387            .find(|f| f.label == "Placeable List")
2388            .expect("schema must contain Placeable List");
2389        assert_eq!(
2390            placeable
2391                .children
2392                .expect("Placeable List has children")
2393                .len(),
2394            6
2395        );
2396
2397        let waypoint = schema
2398            .iter()
2399            .find(|f| f.label == "WaypointList")
2400            .expect("schema must contain WaypointList");
2401        assert_eq!(
2402            waypoint.children.expect("WaypointList has children").len(),
2403            4
2404        );
2405
2406        let sound = schema
2407            .iter()
2408            .find(|f| f.label == "SoundList")
2409            .expect("schema must contain SoundList");
2410        assert_eq!(sound.children.expect("SoundList has children").len(), 6);
2411
2412        let trigger = schema
2413            .iter()
2414            .find(|f| f.label == "TriggerList")
2415            .expect("schema must contain TriggerList");
2416        assert_eq!(
2417            trigger.children.expect("TriggerList has children").len(),
2418            10
2419        );
2420
2421        let store = schema
2422            .iter()
2423            .find(|f| f.label == "StoreList")
2424            .expect("schema must contain StoreList");
2425        assert_eq!(store.children.expect("StoreList has children").len(), 8);
2426
2427        let encounter = schema
2428            .iter()
2429            .find(|f| f.label == "Encounter List")
2430            .expect("schema must contain Encounter List");
2431        assert_eq!(
2432            encounter
2433                .children
2434                .expect("Encounter List has children")
2435                .len(),
2436            7
2437        );
2438
2439        let area_effect = schema
2440            .iter()
2441            .find(|f| f.label == "AreaEffectList")
2442            .expect("schema must contain AreaEffectList");
2443        assert_eq!(
2444            area_effect
2445                .children
2446                .expect("AreaEffectList has children")
2447                .len(),
2448            7
2449        );
2450
2451        let area_props = schema
2452            .iter()
2453            .find(|f| f.label == "AreaProperties")
2454            .expect("schema must contain AreaProperties");
2455        assert_eq!(
2456            area_props
2457                .children
2458                .expect("AreaProperties has children")
2459                .len(),
2460            17
2461        );
2462
2463        let area_map = schema
2464            .iter()
2465            .find(|f| f.label == "AreaMap")
2466            .expect("schema must contain AreaMap");
2467        assert_eq!(area_map.children.expect("AreaMap has children").len(), 4);
2468
2469        let camera = schema
2470            .iter()
2471            .find(|f| f.label == "CameraList")
2472            .expect("schema must contain CameraList");
2473        assert_eq!(camera.children.expect("CameraList has children").len(), 7);
2474    }
2475
2476    #[test]
2477    fn schema_trigger_geometry_has_children() {
2478        let schema = Git::schema();
2479        let trigger = schema
2480            .iter()
2481            .find(|f| f.label == "TriggerList")
2482            .expect("schema must contain TriggerList");
2483        let trigger_children = trigger.children.expect("TriggerList has children");
2484        let geometry = trigger_children
2485            .iter()
2486            .find(|f| f.label == "Geometry")
2487            .expect("TriggerList must contain Geometry");
2488        assert_eq!(geometry.children.expect("Geometry has children").len(), 3);
2489    }
2490
2491    #[test]
2492    fn schema_encounter_nested_lists_have_children() {
2493        let schema = Git::schema();
2494        let encounter = schema
2495            .iter()
2496            .find(|f| f.label == "Encounter List")
2497            .expect("schema must contain Encounter List");
2498        let encounter_children = encounter.children.expect("Encounter List has children");
2499
2500        let geometry = encounter_children
2501            .iter()
2502            .find(|f| f.label == "Geometry")
2503            .expect("Encounter List must contain Geometry");
2504        assert_eq!(geometry.children.expect("Geometry has children").len(), 3);
2505
2506        let spawn_points = encounter_children
2507            .iter()
2508            .find(|f| f.label == "SpawnPointList")
2509            .expect("Encounter List must contain SpawnPointList");
2510        assert_eq!(
2511            spawn_points
2512                .children
2513                .expect("SpawnPointList has children")
2514                .len(),
2515            4
2516        );
2517    }
2518
2519    #[test]
2520    fn schema_var_table_has_no_children() {
2521        let schema = Git::schema();
2522        let var_table = schema
2523            .iter()
2524            .find(|f| f.label == "VarTable")
2525            .expect("schema must contain VarTable");
2526        assert!(var_table.children.is_none());
2527    }
2528}