Skip to main content

nwnrs_git/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = "# nwnrs-git\n\nTyped parser for Neverwinter Nights area instance (`GIT`) resources.\n\n## Why This Crate Exists\n\n`GFF` is a general-purpose container; `GIT` is domain-specific. The raw GFF\nlayer has no knowledge of placed instances, instance types, or area geometry.\nThis crate lifts raw GFF structs into typed Rust collections so area tooling\ncan work with creatures, doors, placeables, and waypoints directly instead of\nnavigating untyped field maps.\n\n## Scope\n\n- parse `GIT` payloads into typed instance collections such as creatures,\n  doors, placeables, triggers, sounds, and waypoints\n- preserve the original raw `GFF` structures alongside the typed view\n- expose geometry and transform data in forms suitable for higher-level tools\n- rebuild and write typed `GIT` payloads back to `GFF`\n\nThe principal entry points are [`read_git`], [`build_git_root`], [`write_git`],\nand [`GitFile`].\n\n## Invariants\n\n- authored instance order is preserved within each typed collection\n- raw top-level and per-instance `GFF` data remain available through the typed\n  model\n- geometry points and transforms are represented explicitly rather than being\n  folded into ad hoc tuples\n- rebuilding a `GIT` payload preserves unknown per-entry raw fields while\n  rewriting the typed fields owned by this crate\n\n## See also\n\n- [`nwnrs-gff`](https://docs.rs/nwnrs-gff), the underlying typed GFF container\n  layer\n- [`nwnrs-set`](https://docs.rs/nwnrs-set), which describes tileset structure\n  rather than placed instances\n"include_str!("../README.md")]
3
4use std::{
5    fmt,
6    fs::File,
7    io::{self, Read, Seek, Write},
8    path::Path,
9};
10
11use nwnrs_gff::prelude::*;
12use nwnrs_resman::prelude::*;
13use nwnrs_resref::prelude::ResolvedResRef;
14use nwnrs_restype::prelude::*;
15use tracing::instrument;
16
17/// NWN resource type id for `git`.
18pub const GIT_RES_TYPE: ResType = ResType(2023);
19
20/// Errors returned while reading or parsing `GIT` payloads.
21///
22/// # Examples
23///
24/// ```rust,no_run
25/// let _ = std::mem::size_of::<nwnrs_git::GitError>();
26/// ```
27#[derive(#[automatically_derived]
impl ::core::fmt::Debug for GitError {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        match self {
            GitError::Io(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f, "Io",
                    &__self_0),
            GitError::Gff(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f, "Gff",
                    &__self_0),
            GitError::ResMan(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f, "ResMan",
                    &__self_0),
            GitError::Message(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f,
                    "Message", &__self_0),
        }
    }
}Debug)]
28pub enum GitError {
29    /// An underlying IO operation failed.
30    Io(io::Error),
31    /// GFF decoding failed.
32    Gff(GffError),
33    /// Resource-manager access failed.
34    ResMan(ResManError),
35    /// The payload was otherwise invalid or unsupported.
36    Message(String),
37}
38
39impl GitError {
40    /// Creates a free-form `GIT` error message.
41    ///
42    /// # Examples
43    ///
44    /// ```rust,no_run
45    /// let _ = nwnrs_git::GitError::msg;
46    /// ```
47    pub fn msg(message: impl Into<String>) -> Self {
48        Self::Message(message.into())
49    }
50}
51
52impl fmt::Display for GitError {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        match self {
55            Self::Io(error) => error.fmt(f),
56            Self::Gff(error) => error.fmt(f),
57            Self::ResMan(error) => error.fmt(f),
58            Self::Message(message) => f.write_str(message),
59        }
60    }
61}
62
63impl std::error::Error for GitError {}
64
65impl From<io::Error> for GitError {
66    fn from(value: io::Error) -> Self {
67        Self::Io(value)
68    }
69}
70
71impl From<GffError> for GitError {
72    fn from(value: GffError) -> Self {
73        Self::Gff(value)
74    }
75}
76
77impl From<ResManError> for GitError {
78    fn from(value: ResManError) -> Self {
79        Self::ResMan(value)
80    }
81}
82
83/// Result type for `GIT` operations.
84pub type GitResult<T> = Result<T, GitError>;
85
86/// Parsed area instance payload.
87///
88/// Each typed collection preserves the authored instance ordering for its
89/// category. Where typed coverage is incomplete, the underlying raw GFF
90/// structures remain available on the typed entries or through
91/// [`GitFile::legacy_list`].
92///
93/// # Examples
94///
95/// ```rust,no_run
96/// let git = nwnrs_git::GitFile::default();
97/// assert!(git.creatures.is_empty());
98/// ```
99#[derive(#[automatically_derived]
impl ::core::fmt::Debug for GitFile {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        let names: &'static _ =
            &["area_properties", "creatures", "doors", "encounters",
                        "legacy_list", "sounds", "stores", "triggers", "waypoints",
                        "placeables"];
        let values: &[&dyn ::core::fmt::Debug] =
            &[&self.area_properties, &self.creatures, &self.doors,
                        &self.encounters, &self.legacy_list, &self.sounds,
                        &self.stores, &self.triggers, &self.waypoints,
                        &&self.placeables];
        ::core::fmt::Formatter::debug_struct_fields_finish(f, "GitFile",
            names, values)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for GitFile {
    #[inline]
    fn clone(&self) -> GitFile {
        GitFile {
            area_properties: ::core::clone::Clone::clone(&self.area_properties),
            creatures: ::core::clone::Clone::clone(&self.creatures),
            doors: ::core::clone::Clone::clone(&self.doors),
            encounters: ::core::clone::Clone::clone(&self.encounters),
            legacy_list: ::core::clone::Clone::clone(&self.legacy_list),
            sounds: ::core::clone::Clone::clone(&self.sounds),
            stores: ::core::clone::Clone::clone(&self.stores),
            triggers: ::core::clone::Clone::clone(&self.triggers),
            waypoints: ::core::clone::Clone::clone(&self.waypoints),
            placeables: ::core::clone::Clone::clone(&self.placeables),
        }
    }
}Clone, #[automatically_derived]
impl ::core::default::Default for GitFile {
    #[inline]
    fn default() -> GitFile {
        GitFile {
            area_properties: ::core::default::Default::default(),
            creatures: ::core::default::Default::default(),
            doors: ::core::default::Default::default(),
            encounters: ::core::default::Default::default(),
            legacy_list: ::core::default::Default::default(),
            sounds: ::core::default::Default::default(),
            stores: ::core::default::Default::default(),
            triggers: ::core::default::Default::default(),
            waypoints: ::core::default::Default::default(),
            placeables: ::core::default::Default::default(),
        }
    }
}Default, #[automatically_derived]
impl ::core::cmp::PartialEq for GitFile {
    #[inline]
    fn eq(&self, other: &GitFile) -> bool {
        self.area_properties == other.area_properties &&
                                            self.creatures == other.creatures &&
                                        self.doors == other.doors &&
                                    self.encounters == other.encounters &&
                                self.legacy_list == other.legacy_list &&
                            self.sounds == other.sounds && self.stores == other.stores
                    && self.triggers == other.triggers &&
                self.waypoints == other.waypoints &&
            self.placeables == other.placeables
    }
}PartialEq)]
100pub struct GitFile {
101    /// Optional ambient/music settings for the area.
102    pub area_properties: Option<GitAreaProperties>,
103    /// Placed creatures.
104    pub creatures:       Vec<GitCreature>,
105    /// Placed doors.
106    pub doors:           Vec<GitDoor>,
107    /// Encounter volumes.
108    pub encounters:      Vec<GitEncounter>,
109    /// Raw legacy top-level `List` entries, preserved verbatim.
110    pub legacy_list:     Vec<GffStruct>,
111    /// Placed ambient or point sounds.
112    pub sounds:          Vec<GitSound>,
113    /// Placed stores.
114    pub stores:          Vec<GitStore>,
115    /// Trigger volumes.
116    pub triggers:        Vec<GitTrigger>,
117    /// Placed waypoints.
118    pub waypoints:       Vec<GitWaypoint>,
119    /// Placed placeables.
120    pub placeables:      Vec<GitPlaceable>,
121}
122
123impl GitFile {
124    /// Reads a typed `GIT` file from disk.
125    ///
126    /// # Errors
127    ///
128    /// Returns [`GitError`] if the file cannot be opened or parsed.
129    ///
130    /// # Examples
131    ///
132    /// ```rust,no_run
133    /// let _ = nwnrs_git::GitFile::from_file;
134    /// ```
135    pub fn from_file(path: impl AsRef<Path>) -> GitResult<Self> {
136        let mut file = File::open(path.as_ref())?;
137        read_git(&mut file)
138    }
139
140    /// Reads a typed `GIT` file from a [`Res`].
141    ///
142    /// # Errors
143    ///
144    /// Returns [`GitError`] if the resource is not a GIT type or the bytes
145    /// cannot be parsed.
146    ///
147    /// # Examples
148    ///
149    /// ```rust,no_run
150    /// let _ = nwnrs_git::GitFile::from_res;
151    /// ```
152    pub fn from_res(res: &Res, cache_policy: CachePolicy) -> GitResult<Self> {
153        if res.resref().res_type() != GIT_RES_TYPE {
154            return Err(GitError::msg(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("expected git resource, got {0}",
                res.resref()))
    })format!(
155                "expected git resource, got {}",
156                res.resref()
157            )));
158        }
159
160        let bytes = res.read_all(cache_policy)?;
161        let mut cursor = io::Cursor::new(bytes);
162        read_git(&mut cursor)
163    }
164
165    /// Reads a typed `GIT` file from a [`ResMan`] by area name.
166    ///
167    /// # Errors
168    ///
169    /// Returns [`GitError`] if the resource cannot be found or parsed.
170    ///
171    /// # Examples
172    ///
173    /// ```rust,no_run
174    /// let _ = nwnrs_git::GitFile::from_resman;
175    /// ```
176    pub fn from_resman(
177        resman: &mut ResMan,
178        area_name: &str,
179        cache_policy: CachePolicy,
180    ) -> GitResult<Self> {
181        let resolved = ResolvedResRef::from_filename(&::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}.git", area_name))
    })format!("{area_name}.git"))
182            .map_err(|error| GitError::msg(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("git resref: {0}", error))
    })format!("git resref: {error}")))?;
183        let res = resman
184            .get_resolved(&resolved)
185            .ok_or_else(|| GitError::msg(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("git not found in ResMan: {0}",
                resolved))
    })format!("git not found in ResMan: {resolved}")))?;
186        Self::from_res(&res, cache_policy)
187    }
188}
189
190/// Parsed `AreaProperties` block.
191///
192/// # Examples
193///
194/// ```rust,no_run
195/// let _ = std::mem::size_of::<nwnrs_git::GitAreaProperties>();
196/// ```
197#[derive(#[automatically_derived]
impl ::core::fmt::Debug for GitAreaProperties {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        let names: &'static _ =
            &["raw", "ambient_sound_day", "ambient_sound_night",
                        "ambient_sound_day_volume", "ambient_sound_night_volume",
                        "env_audio", "music_battle", "music_day", "music_night",
                        "music_delay"];
        let values: &[&dyn ::core::fmt::Debug] =
            &[&self.raw, &self.ambient_sound_day, &self.ambient_sound_night,
                        &self.ambient_sound_day_volume,
                        &self.ambient_sound_night_volume, &self.env_audio,
                        &self.music_battle, &self.music_day, &self.music_night,
                        &&self.music_delay];
        ::core::fmt::Formatter::debug_struct_fields_finish(f,
            "GitAreaProperties", names, values)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for GitAreaProperties {
    #[inline]
    fn clone(&self) -> GitAreaProperties {
        GitAreaProperties {
            raw: ::core::clone::Clone::clone(&self.raw),
            ambient_sound_day: ::core::clone::Clone::clone(&self.ambient_sound_day),
            ambient_sound_night: ::core::clone::Clone::clone(&self.ambient_sound_night),
            ambient_sound_day_volume: ::core::clone::Clone::clone(&self.ambient_sound_day_volume),
            ambient_sound_night_volume: ::core::clone::Clone::clone(&self.ambient_sound_night_volume),
            env_audio: ::core::clone::Clone::clone(&self.env_audio),
            music_battle: ::core::clone::Clone::clone(&self.music_battle),
            music_day: ::core::clone::Clone::clone(&self.music_day),
            music_night: ::core::clone::Clone::clone(&self.music_night),
            music_delay: ::core::clone::Clone::clone(&self.music_delay),
        }
    }
}Clone, #[automatically_derived]
impl ::core::cmp::PartialEq for GitAreaProperties {
    #[inline]
    fn eq(&self, other: &GitAreaProperties) -> bool {
        self.raw == other.raw &&
                                            self.ambient_sound_day == other.ambient_sound_day &&
                                        self.ambient_sound_night == other.ambient_sound_night &&
                                    self.ambient_sound_day_volume ==
                                        other.ambient_sound_day_volume &&
                                self.ambient_sound_night_volume ==
                                    other.ambient_sound_night_volume &&
                            self.env_audio == other.env_audio &&
                        self.music_battle == other.music_battle &&
                    self.music_day == other.music_day &&
                self.music_night == other.music_night &&
            self.music_delay == other.music_delay
    }
}PartialEq)]
198pub struct GitAreaProperties {
199    /// Original raw GFF structure.
200    pub raw: GffStruct,
201    /// Day ambient sound id.
202    pub ambient_sound_day: Option<i32>,
203    /// Night ambient sound id.
204    pub ambient_sound_night: Option<i32>,
205    /// Day ambient sound volume.
206    pub ambient_sound_day_volume: Option<i32>,
207    /// Night ambient sound volume.
208    pub ambient_sound_night_volume: Option<i32>,
209    /// Environment audio profile id.
210    pub env_audio: Option<i32>,
211    /// Combat music id.
212    pub music_battle: Option<i32>,
213    /// Day music id.
214    pub music_day: Option<i32>,
215    /// Night music id.
216    pub music_night: Option<i32>,
217    /// Music delay value.
218    pub music_delay: Option<i32>,
219}
220
221/// A world transform extracted from a GIT instance.
222///
223/// # Examples
224///
225/// ```rust,no_run
226/// let transform = nwnrs_git::GitTransform::default();
227/// assert!(transform.x.is_none());
228/// ```
229#[derive(#[automatically_derived]
impl ::core::fmt::Debug for GitTransform {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        let names: &'static _ =
            &["x", "y", "z", "bearing", "x_orientation", "y_orientation"];
        let values: &[&dyn ::core::fmt::Debug] =
            &[&self.x, &self.y, &self.z, &self.bearing, &self.x_orientation,
                        &&self.y_orientation];
        ::core::fmt::Formatter::debug_struct_fields_finish(f, "GitTransform",
            names, values)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for GitTransform {
    #[inline]
    fn clone(&self) -> GitTransform {
        GitTransform {
            x: ::core::clone::Clone::clone(&self.x),
            y: ::core::clone::Clone::clone(&self.y),
            z: ::core::clone::Clone::clone(&self.z),
            bearing: ::core::clone::Clone::clone(&self.bearing),
            x_orientation: ::core::clone::Clone::clone(&self.x_orientation),
            y_orientation: ::core::clone::Clone::clone(&self.y_orientation),
        }
    }
}Clone, #[automatically_derived]
impl ::core::default::Default for GitTransform {
    #[inline]
    fn default() -> GitTransform {
        GitTransform {
            x: ::core::default::Default::default(),
            y: ::core::default::Default::default(),
            z: ::core::default::Default::default(),
            bearing: ::core::default::Default::default(),
            x_orientation: ::core::default::Default::default(),
            y_orientation: ::core::default::Default::default(),
        }
    }
}Default, #[automatically_derived]
impl ::core::cmp::PartialEq for GitTransform {
    #[inline]
    fn eq(&self, other: &GitTransform) -> bool {
        self.x == other.x && self.y == other.y && self.z == other.z &&
                    self.bearing == other.bearing &&
                self.x_orientation == other.x_orientation &&
            self.y_orientation == other.y_orientation
    }
}PartialEq)]
230pub struct GitTransform {
231    /// World X position.
232    pub x:             Option<f32>,
233    /// World Y position.
234    pub y:             Option<f32>,
235    /// World Z position.
236    pub z:             Option<f32>,
237    /// Aurora planar bearing in radians for bearing-based instances.
238    pub bearing:       Option<f32>,
239    /// Orientation X component for vector-based instances.
240    pub x_orientation: Option<f32>,
241    /// Orientation Y component for vector-based instances.
242    pub y_orientation: Option<f32>,
243}
244
245/// A geometry point used by triggers or encounters.
246///
247/// # Examples
248///
249/// ```rust,no_run
250/// let point = nwnrs_git::GitPoint::default();
251/// assert!(point.x.is_none());
252/// ```
253#[derive(#[automatically_derived]
impl ::core::fmt::Debug for GitPoint {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field3_finish(f, "GitPoint", "x",
            &self.x, "y", &self.y, "z", &&self.z)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for GitPoint {
    #[inline]
    fn clone(&self) -> GitPoint {
        GitPoint {
            x: ::core::clone::Clone::clone(&self.x),
            y: ::core::clone::Clone::clone(&self.y),
            z: ::core::clone::Clone::clone(&self.z),
        }
    }
}Clone, #[automatically_derived]
impl ::core::default::Default for GitPoint {
    #[inline]
    fn default() -> GitPoint {
        GitPoint {
            x: ::core::default::Default::default(),
            y: ::core::default::Default::default(),
            z: ::core::default::Default::default(),
        }
    }
}Default, #[automatically_derived]
impl ::core::cmp::PartialEq for GitPoint {
    #[inline]
    fn eq(&self, other: &GitPoint) -> bool {
        self.x == other.x && self.y == other.y && self.z == other.z
    }
}PartialEq)]
254pub struct GitPoint {
255    /// Point X coordinate.
256    pub x: Option<f32>,
257    /// Point Y coordinate.
258    pub y: Option<f32>,
259    /// Point Z coordinate.
260    pub z: Option<f32>,
261}
262
263/// A placed creature entry.
264///
265/// # Examples
266///
267/// ```rust,no_run
268/// let _ = std::mem::size_of::<nwnrs_git::GitCreature>();
269/// ```
270#[derive(#[automatically_derived]
impl ::core::fmt::Debug for GitCreature {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        let names: &'static _ =
            &["raw", "tag", "template_resref", "localized_name",
                        "description", "transform"];
        let values: &[&dyn ::core::fmt::Debug] =
            &[&self.raw, &self.tag, &self.template_resref,
                        &self.localized_name, &self.description, &&self.transform];
        ::core::fmt::Formatter::debug_struct_fields_finish(f, "GitCreature",
            names, values)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for GitCreature {
    #[inline]
    fn clone(&self) -> GitCreature {
        GitCreature {
            raw: ::core::clone::Clone::clone(&self.raw),
            tag: ::core::clone::Clone::clone(&self.tag),
            template_resref: ::core::clone::Clone::clone(&self.template_resref),
            localized_name: ::core::clone::Clone::clone(&self.localized_name),
            description: ::core::clone::Clone::clone(&self.description),
            transform: ::core::clone::Clone::clone(&self.transform),
        }
    }
}Clone, #[automatically_derived]
impl ::core::cmp::PartialEq for GitCreature {
    #[inline]
    fn eq(&self, other: &GitCreature) -> bool {
        self.raw == other.raw && self.tag == other.tag &&
                        self.template_resref == other.template_resref &&
                    self.localized_name == other.localized_name &&
                self.description == other.description &&
            self.transform == other.transform
    }
}PartialEq)]
271pub struct GitCreature {
272    /// Original raw GFF structure.
273    pub raw:             GffStruct,
274    /// Instance tag.
275    pub tag:             Option<String>,
276    /// Blueprint resource reference.
277    pub template_resref: Option<String>,
278    /// Localized display name when present.
279    pub localized_name:  Option<GffCExoLocString>,
280    /// Description string when present.
281    pub description:     Option<GffCExoLocString>,
282    /// Spawn transform.
283    pub transform:       GitTransform,
284}
285
286/// A placed door entry.
287///
288/// # Examples
289///
290/// ```rust,no_run
291/// let _ = std::mem::size_of::<nwnrs_git::GitDoor>();
292/// ```
293#[derive(#[automatically_derived]
impl ::core::fmt::Debug for GitDoor {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        let names: &'static _ =
            &["raw", "tag", "localized_name", "description",
                        "template_resref", "appearance", "animation_state",
                        "linked_to", "transform"];
        let values: &[&dyn ::core::fmt::Debug] =
            &[&self.raw, &self.tag, &self.localized_name, &self.description,
                        &self.template_resref, &self.appearance,
                        &self.animation_state, &self.linked_to, &&self.transform];
        ::core::fmt::Formatter::debug_struct_fields_finish(f, "GitDoor",
            names, values)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for GitDoor {
    #[inline]
    fn clone(&self) -> GitDoor {
        GitDoor {
            raw: ::core::clone::Clone::clone(&self.raw),
            tag: ::core::clone::Clone::clone(&self.tag),
            localized_name: ::core::clone::Clone::clone(&self.localized_name),
            description: ::core::clone::Clone::clone(&self.description),
            template_resref: ::core::clone::Clone::clone(&self.template_resref),
            appearance: ::core::clone::Clone::clone(&self.appearance),
            animation_state: ::core::clone::Clone::clone(&self.animation_state),
            linked_to: ::core::clone::Clone::clone(&self.linked_to),
            transform: ::core::clone::Clone::clone(&self.transform),
        }
    }
}Clone, #[automatically_derived]
impl ::core::cmp::PartialEq for GitDoor {
    #[inline]
    fn eq(&self, other: &GitDoor) -> bool {
        self.raw == other.raw && self.tag == other.tag &&
                                    self.localized_name == other.localized_name &&
                                self.description == other.description &&
                            self.template_resref == other.template_resref &&
                        self.appearance == other.appearance &&
                    self.animation_state == other.animation_state &&
                self.linked_to == other.linked_to &&
            self.transform == other.transform
    }
}PartialEq)]
294pub struct GitDoor {
295    /// Original raw GFF structure.
296    pub raw:             GffStruct,
297    /// Instance tag.
298    pub tag:             Option<String>,
299    /// Localized display name.
300    pub localized_name:  Option<GffCExoLocString>,
301    /// Description string.
302    pub description:     Option<GffCExoLocString>,
303    /// Blueprint resource reference.
304    pub template_resref: Option<String>,
305    /// Door appearance id.
306    pub appearance:      Option<i32>,
307    /// Door animation state.
308    pub animation_state: Option<i32>,
309    /// Linked destination tag or waypoint.
310    pub linked_to:       Option<String>,
311    /// Placement transform.
312    pub transform:       GitTransform,
313}
314
315/// An encounter volume entry.
316///
317/// # Examples
318///
319/// ```rust,no_run
320/// let _ = std::mem::size_of::<nwnrs_git::GitEncounter>();
321/// ```
322#[derive(#[automatically_derived]
impl ::core::fmt::Debug for GitEncounter {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field5_finish(f, "GitEncounter",
            "raw", &self.raw, "tag", &self.tag, "localized_name",
            &self.localized_name, "transform", &self.transform, "geometry",
            &&self.geometry)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for GitEncounter {
    #[inline]
    fn clone(&self) -> GitEncounter {
        GitEncounter {
            raw: ::core::clone::Clone::clone(&self.raw),
            tag: ::core::clone::Clone::clone(&self.tag),
            localized_name: ::core::clone::Clone::clone(&self.localized_name),
            transform: ::core::clone::Clone::clone(&self.transform),
            geometry: ::core::clone::Clone::clone(&self.geometry),
        }
    }
}Clone, #[automatically_derived]
impl ::core::cmp::PartialEq for GitEncounter {
    #[inline]
    fn eq(&self, other: &GitEncounter) -> bool {
        self.raw == other.raw && self.tag == other.tag &&
                    self.localized_name == other.localized_name &&
                self.transform == other.transform &&
            self.geometry == other.geometry
    }
}PartialEq)]
323pub struct GitEncounter {
324    /// Original raw GFF structure.
325    pub raw:            GffStruct,
326    /// Instance tag.
327    pub tag:            Option<String>,
328    /// Localized display name.
329    pub localized_name: Option<GffCExoLocString>,
330    /// Encounter origin or anchor transform when present.
331    pub transform:      GitTransform,
332    /// Polygon geometry points.
333    pub geometry:       Vec<GitPoint>,
334}
335
336/// A single sound reference within a sound object.
337///
338/// # Examples
339///
340/// ```rust,no_run
341/// let sound_ref = nwnrs_git::GitSoundRef::default();
342/// assert!(sound_ref.sound.is_none());
343/// ```
344#[derive(#[automatically_derived]
impl ::core::fmt::Debug for GitSoundRef {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field1_finish(f, "GitSoundRef",
            "sound", &&self.sound)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for GitSoundRef {
    #[inline]
    fn clone(&self) -> GitSoundRef {
        GitSoundRef { sound: ::core::clone::Clone::clone(&self.sound) }
    }
}Clone, #[automatically_derived]
impl ::core::default::Default for GitSoundRef {
    #[inline]
    fn default() -> GitSoundRef {
        GitSoundRef { sound: ::core::default::Default::default() }
    }
}Default, #[automatically_derived]
impl ::core::cmp::PartialEq for GitSoundRef {
    #[inline]
    fn eq(&self, other: &GitSoundRef) -> bool { self.sound == other.sound }
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for GitSoundRef {
    #[inline]
    #[doc(hidden)]
    #[coverage(off)]
    fn assert_fields_are_eq(&self) {
        let _: ::core::cmp::AssertParamIsEq<Option<String>>;
    }
}Eq)]
345pub struct GitSoundRef {
346    /// Referenced sound resource name.
347    pub sound: Option<String>,
348}
349
350/// A sound emitter entry.
351///
352/// # Examples
353///
354/// ```rust,no_run
355/// let _ = std::mem::size_of::<nwnrs_git::GitSound>();
356/// ```
357#[derive(#[automatically_derived]
impl ::core::fmt::Debug for GitSound {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        let names: &'static _ =
            &["raw", "tag", "localized_name", "template_resref", "transform",
                        "positional", "min_distance", "max_distance", "volume",
                        "sounds"];
        let values: &[&dyn ::core::fmt::Debug] =
            &[&self.raw, &self.tag, &self.localized_name,
                        &self.template_resref, &self.transform, &self.positional,
                        &self.min_distance, &self.max_distance, &self.volume,
                        &&self.sounds];
        ::core::fmt::Formatter::debug_struct_fields_finish(f, "GitSound",
            names, values)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for GitSound {
    #[inline]
    fn clone(&self) -> GitSound {
        GitSound {
            raw: ::core::clone::Clone::clone(&self.raw),
            tag: ::core::clone::Clone::clone(&self.tag),
            localized_name: ::core::clone::Clone::clone(&self.localized_name),
            template_resref: ::core::clone::Clone::clone(&self.template_resref),
            transform: ::core::clone::Clone::clone(&self.transform),
            positional: ::core::clone::Clone::clone(&self.positional),
            min_distance: ::core::clone::Clone::clone(&self.min_distance),
            max_distance: ::core::clone::Clone::clone(&self.max_distance),
            volume: ::core::clone::Clone::clone(&self.volume),
            sounds: ::core::clone::Clone::clone(&self.sounds),
        }
    }
}Clone, #[automatically_derived]
impl ::core::cmp::PartialEq for GitSound {
    #[inline]
    fn eq(&self, other: &GitSound) -> bool {
        self.raw == other.raw && self.tag == other.tag &&
                                        self.localized_name == other.localized_name &&
                                    self.template_resref == other.template_resref &&
                                self.transform == other.transform &&
                            self.positional == other.positional &&
                        self.min_distance == other.min_distance &&
                    self.max_distance == other.max_distance &&
                self.volume == other.volume && self.sounds == other.sounds
    }
}PartialEq)]
358pub struct GitSound {
359    /// Original raw GFF structure.
360    pub raw:             GffStruct,
361    /// Instance tag.
362    pub tag:             Option<String>,
363    /// Localized display name.
364    pub localized_name:  Option<GffCExoLocString>,
365    /// Template resource reference.
366    pub template_resref: Option<String>,
367    /// World transform.
368    pub transform:       GitTransform,
369    /// Whether the sound is positional.
370    pub positional:      Option<bool>,
371    /// Minimum audible distance.
372    pub min_distance:    Option<f32>,
373    /// Maximum audible distance.
374    pub max_distance:    Option<f32>,
375    /// Base volume.
376    pub volume:          Option<i32>,
377    /// Referenced sound entries.
378    pub sounds:          Vec<GitSoundRef>,
379}
380
381/// A placed store entry.
382///
383/// # Examples
384///
385/// ```rust,no_run
386/// let _ = std::mem::size_of::<nwnrs_git::GitStore>();
387/// ```
388#[derive(#[automatically_derived]
impl ::core::fmt::Debug for GitStore {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field5_finish(f, "GitStore",
            "raw", &self.raw, "tag", &self.tag, "localized_name",
            &self.localized_name, "template_resref", &self.template_resref,
            "transform", &&self.transform)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for GitStore {
    #[inline]
    fn clone(&self) -> GitStore {
        GitStore {
            raw: ::core::clone::Clone::clone(&self.raw),
            tag: ::core::clone::Clone::clone(&self.tag),
            localized_name: ::core::clone::Clone::clone(&self.localized_name),
            template_resref: ::core::clone::Clone::clone(&self.template_resref),
            transform: ::core::clone::Clone::clone(&self.transform),
        }
    }
}Clone, #[automatically_derived]
impl ::core::cmp::PartialEq for GitStore {
    #[inline]
    fn eq(&self, other: &GitStore) -> bool {
        self.raw == other.raw && self.tag == other.tag &&
                    self.localized_name == other.localized_name &&
                self.template_resref == other.template_resref &&
            self.transform == other.transform
    }
}PartialEq)]
389pub struct GitStore {
390    /// Original raw GFF structure.
391    pub raw:             GffStruct,
392    /// Instance tag.
393    pub tag:             Option<String>,
394    /// Localized display name.
395    pub localized_name:  Option<GffCExoLocString>,
396    /// Blueprint resource reference.
397    pub template_resref: Option<String>,
398    /// Placement transform.
399    pub transform:       GitTransform,
400}
401
402/// A trigger volume entry.
403///
404/// # Examples
405///
406/// ```rust,no_run
407/// let _ = std::mem::size_of::<nwnrs_git::GitTrigger>();
408/// ```
409#[derive(#[automatically_derived]
impl ::core::fmt::Debug for GitTrigger {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field5_finish(f, "GitTrigger",
            "raw", &self.raw, "tag", &self.tag, "localized_name",
            &self.localized_name, "transform", &self.transform, "geometry",
            &&self.geometry)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for GitTrigger {
    #[inline]
    fn clone(&self) -> GitTrigger {
        GitTrigger {
            raw: ::core::clone::Clone::clone(&self.raw),
            tag: ::core::clone::Clone::clone(&self.tag),
            localized_name: ::core::clone::Clone::clone(&self.localized_name),
            transform: ::core::clone::Clone::clone(&self.transform),
            geometry: ::core::clone::Clone::clone(&self.geometry),
        }
    }
}Clone, #[automatically_derived]
impl ::core::cmp::PartialEq for GitTrigger {
    #[inline]
    fn eq(&self, other: &GitTrigger) -> bool {
        self.raw == other.raw && self.tag == other.tag &&
                    self.localized_name == other.localized_name &&
                self.transform == other.transform &&
            self.geometry == other.geometry
    }
}PartialEq)]
410pub struct GitTrigger {
411    /// Original raw GFF structure.
412    pub raw:            GffStruct,
413    /// Instance tag.
414    pub tag:            Option<String>,
415    /// Localized display name.
416    pub localized_name: Option<GffCExoLocString>,
417    /// Trigger origin or anchor transform when present.
418    pub transform:      GitTransform,
419    /// Polygon geometry points.
420    pub geometry:       Vec<GitPoint>,
421}
422
423/// A placed waypoint entry.
424///
425/// # Examples
426///
427/// ```rust,no_run
428/// let _ = std::mem::size_of::<nwnrs_git::GitWaypoint>();
429/// ```
430#[derive(#[automatically_derived]
impl ::core::fmt::Debug for GitWaypoint {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        let names: &'static _ =
            &["raw", "tag", "localized_name", "description",
                        "template_resref", "linked_to", "appearance", "transform"];
        let values: &[&dyn ::core::fmt::Debug] =
            &[&self.raw, &self.tag, &self.localized_name, &self.description,
                        &self.template_resref, &self.linked_to, &self.appearance,
                        &&self.transform];
        ::core::fmt::Formatter::debug_struct_fields_finish(f, "GitWaypoint",
            names, values)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for GitWaypoint {
    #[inline]
    fn clone(&self) -> GitWaypoint {
        GitWaypoint {
            raw: ::core::clone::Clone::clone(&self.raw),
            tag: ::core::clone::Clone::clone(&self.tag),
            localized_name: ::core::clone::Clone::clone(&self.localized_name),
            description: ::core::clone::Clone::clone(&self.description),
            template_resref: ::core::clone::Clone::clone(&self.template_resref),
            linked_to: ::core::clone::Clone::clone(&self.linked_to),
            appearance: ::core::clone::Clone::clone(&self.appearance),
            transform: ::core::clone::Clone::clone(&self.transform),
        }
    }
}Clone, #[automatically_derived]
impl ::core::cmp::PartialEq for GitWaypoint {
    #[inline]
    fn eq(&self, other: &GitWaypoint) -> bool {
        self.raw == other.raw && self.tag == other.tag &&
                                self.localized_name == other.localized_name &&
                            self.description == other.description &&
                        self.template_resref == other.template_resref &&
                    self.linked_to == other.linked_to &&
                self.appearance == other.appearance &&
            self.transform == other.transform
    }
}PartialEq)]
431pub struct GitWaypoint {
432    /// Original raw GFF structure.
433    pub raw:             GffStruct,
434    /// Instance tag.
435    pub tag:             Option<String>,
436    /// Localized display name.
437    pub localized_name:  Option<GffCExoLocString>,
438    /// Description string.
439    pub description:     Option<GffCExoLocString>,
440    /// Waypoint template resource reference.
441    pub template_resref: Option<String>,
442    /// Linked destination tag or waypoint.
443    pub linked_to:       Option<String>,
444    /// Waypoint appearance id.
445    pub appearance:      Option<i32>,
446    /// Placement transform.
447    pub transform:       GitTransform,
448}
449
450/// A placed placeable entry.
451///
452/// # Examples
453///
454/// ```rust,no_run
455/// let _ = std::mem::size_of::<nwnrs_git::GitPlaceable>();
456/// ```
457#[derive(#[automatically_derived]
impl ::core::fmt::Debug for GitPlaceable {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        let names: &'static _ =
            &["raw", "tag", "localized_name", "description",
                        "template_resref", "appearance", "static_object", "useable",
                        "has_inventory", "transform"];
        let values: &[&dyn ::core::fmt::Debug] =
            &[&self.raw, &self.tag, &self.localized_name, &self.description,
                        &self.template_resref, &self.appearance,
                        &self.static_object, &self.useable, &self.has_inventory,
                        &&self.transform];
        ::core::fmt::Formatter::debug_struct_fields_finish(f, "GitPlaceable",
            names, values)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for GitPlaceable {
    #[inline]
    fn clone(&self) -> GitPlaceable {
        GitPlaceable {
            raw: ::core::clone::Clone::clone(&self.raw),
            tag: ::core::clone::Clone::clone(&self.tag),
            localized_name: ::core::clone::Clone::clone(&self.localized_name),
            description: ::core::clone::Clone::clone(&self.description),
            template_resref: ::core::clone::Clone::clone(&self.template_resref),
            appearance: ::core::clone::Clone::clone(&self.appearance),
            static_object: ::core::clone::Clone::clone(&self.static_object),
            useable: ::core::clone::Clone::clone(&self.useable),
            has_inventory: ::core::clone::Clone::clone(&self.has_inventory),
            transform: ::core::clone::Clone::clone(&self.transform),
        }
    }
}Clone, #[automatically_derived]
impl ::core::cmp::PartialEq for GitPlaceable {
    #[inline]
    fn eq(&self, other: &GitPlaceable) -> bool {
        self.raw == other.raw && self.tag == other.tag &&
                                        self.localized_name == other.localized_name &&
                                    self.description == other.description &&
                                self.template_resref == other.template_resref &&
                            self.appearance == other.appearance &&
                        self.static_object == other.static_object &&
                    self.useable == other.useable &&
                self.has_inventory == other.has_inventory &&
            self.transform == other.transform
    }
}PartialEq)]
458pub struct GitPlaceable {
459    /// Original raw GFF structure.
460    pub raw:             GffStruct,
461    /// Instance tag.
462    pub tag:             Option<String>,
463    /// Localized display name.
464    pub localized_name:  Option<GffCExoLocString>,
465    /// Description string.
466    pub description:     Option<GffCExoLocString>,
467    /// Blueprint resource reference.
468    pub template_resref: Option<String>,
469    /// Placeable appearance id.
470    pub appearance:      Option<i32>,
471    /// Whether the placeable is static.
472    pub static_object:   Option<bool>,
473    /// Whether the placeable is useable.
474    pub useable:         Option<bool>,
475    /// Whether the placeable has inventory.
476    pub has_inventory:   Option<bool>,
477    /// Placement transform.
478    pub transform:       GitTransform,
479}
480
481/// Reads a typed `GIT` file from `reader`.
482///
483/// # Errors
484///
485/// Returns [`GitError`] if the data cannot be read or does not conform to the
486/// GIT format.
487///
488/// # Examples
489///
490/// ```rust,no_run
491/// let _ = nwnrs_git::read_git;
492/// ```
493#[allow(clippy :: redundant_closure_call)]
match (move ||
                {

                    #[allow(unknown_lints, unreachable_code, clippy ::
                    diverging_sub_expression, clippy :: empty_loop, clippy ::
                    let_unit_value, clippy :: let_with_type_underscore, clippy
                    :: needless_return, clippy :: unreachable)]
                    if false {
                        let __tracing_attr_fake_return: GitResult<GitFile> =
                            loop {};
                        return __tracing_attr_fake_return;
                    }
                    { let root = read_gff_root(reader)?; parse_git_root(&root) }
                })()
    {
        #[allow(clippy :: unit_arg)]
        Ok(x) => Ok(x),
    Err(e) => {
        {
            use ::tracing::__macro_support::Callsite as _;
            static __CALLSITE: ::tracing::callsite::DefaultCallsite =
                {
                    static META: ::tracing::Metadata<'static> =
                        {
                            ::tracing_core::metadata::Metadata::new("event src/lib.rs:493",
                                "nwnrs_git", ::tracing::Level::ERROR,
                                ::tracing_core::__macro_support::Option::Some("src/lib.rs"),
                                ::tracing_core::__macro_support::Option::Some(493u32),
                                ::tracing_core::__macro_support::Option::Some("nwnrs_git"),
                                ::tracing_core::field::FieldSet::new(&[{
                                                    const NAME:
                                                        ::tracing::__macro_support::FieldName<{
                                                            ::tracing::__macro_support::FieldName::len("error")
                                                        }> =
                                                        ::tracing::__macro_support::FieldName::new("error");
                                                    NAME.as_str()
                                                }], ::tracing_core::callsite::Identifier(&__CALLSITE)),
                                ::tracing::metadata::Kind::EVENT)
                        };
                    ::tracing::callsite::DefaultCallsite::new(&META)
                };
            let enabled =
                ::tracing::Level::ERROR <=
                            ::tracing::level_filters::STATIC_MAX_LEVEL &&
                        ::tracing::Level::ERROR <=
                            ::tracing::level_filters::LevelFilter::current() &&
                    {
                        let interest = __CALLSITE.interest();
                        !interest.is_never() &&
                            ::tracing::__macro_support::__is_enabled(__CALLSITE.metadata(),
                                interest)
                    };
            if enabled {
                (|value_set: ::tracing::field::ValueSet|
                            {
                                let meta = __CALLSITE.metadata();
                                ::tracing::Event::dispatch(meta, &value_set);
                                ;
                            })({
                        #[allow(unused_imports)]
                        use ::tracing::field::{debug, display, Value};
                        __CALLSITE.metadata().fields().value_set_all(&[(::tracing::__macro_support::Option::Some(&::tracing::field::display(&e)
                                                    as &dyn ::tracing::field::Value))])
                    });
            } else { ; }
        };
        Err(e)
    }
}#[instrument(level = "debug", skip_all, err)]
494pub fn read_git<R: Read + Seek>(reader: &mut R) -> GitResult<GitFile> {
495    let root = read_gff_root(reader)?;
496    parse_git_root(&root)
497}
498
499/// Parses a typed `GIT` file from a decoded [`GffRoot`].
500///
501/// # Errors
502///
503/// Returns [`GitError`] if the root file type is not `GIT `.
504///
505/// # Examples
506///
507/// ```rust,no_run
508/// let _ = nwnrs_git::parse_git_root;
509/// ```
510pub fn parse_git_root(root: &GffRoot) -> GitResult<GitFile> {
511    if root.file_type != "GIT " {
512        return Err(GitError::msg(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("expected GIT root, got {0:?}",
                root.file_type))
    })format!(
513            "expected GIT root, got {:?}",
514            root.file_type
515        )));
516    }
517
518    Ok(GitFile {
519        area_properties: gff_struct(&root.root, "AreaProperties").map(parse_area_properties),
520        creatures:       gff_list(&root.root, "Creature List")
521            .into_iter()
522            .flatten()
523            .map(parse_creature)
524            .collect(),
525        doors:           gff_list(&root.root, "Door List")
526            .into_iter()
527            .flatten()
528            .map(parse_door)
529            .collect(),
530        encounters:      gff_list(&root.root, "Encounter List")
531            .into_iter()
532            .flatten()
533            .map(parse_encounter)
534            .collect(),
535        legacy_list:     gff_list(&root.root, "List")
536            .map_or_else(Vec::new, <[nwnrs_gff::GffStruct]>::to_vec),
537        sounds:          gff_list(&root.root, "SoundList")
538            .into_iter()
539            .flatten()
540            .map(parse_sound)
541            .collect(),
542        stores:          gff_list(&root.root, "StoreList")
543            .into_iter()
544            .flatten()
545            .map(parse_store)
546            .collect(),
547        triggers:        gff_list(&root.root, "TriggerList")
548            .into_iter()
549            .flatten()
550            .map(parse_trigger)
551            .collect(),
552        waypoints:       gff_list(&root.root, "WaypointList")
553            .into_iter()
554            .flatten()
555            .map(parse_waypoint)
556            .collect(),
557        placeables:      gff_list(&root.root, "Placeable List")
558            .into_iter()
559            .flatten()
560            .map(parse_placeable)
561            .collect(),
562    })
563}
564
565/// Builds a typed [`GffRoot`] from a [`GitFile`].
566///
567/// Known typed fields are rewritten from the typed model. Unknown fields stored
568/// on per-entry raw structures are preserved.
569///
570/// # Errors
571///
572/// Returns [`GitError`] if any GFF field label is invalid.
573///
574/// # Examples
575///
576/// ```rust,no_run
577/// let _ = nwnrs_git::build_git_root;
578/// ```
579pub fn build_git_root(git: &GitFile) -> GitResult<GffRoot> {
580    let mut root = GffRoot::new("GIT ");
581
582    if let Some(area_properties) = &git.area_properties {
583        root.put_value(
584            "AreaProperties",
585            GffValue::Struct(build_area_properties(area_properties)?),
586        )?;
587    }
588
589    put_list(
590        &mut root.root,
591        "Creature List",
592        &git.creatures,
593        build_creature,
594    )?;
595    put_list(&mut root.root, "Door List", &git.doors, build_door)?;
596    put_list(
597        &mut root.root,
598        "Encounter List",
599        &git.encounters,
600        build_encounter,
601    )?;
602    put_list(&mut root.root, "List", &git.legacy_list, |value| {
603        Ok(value.clone())
604    })?;
605    put_list(&mut root.root, "SoundList", &git.sounds, build_sound)?;
606    put_list(&mut root.root, "StoreList", &git.stores, build_store)?;
607    put_list(&mut root.root, "TriggerList", &git.triggers, build_trigger)?;
608    put_list(
609        &mut root.root,
610        "WaypointList",
611        &git.waypoints,
612        build_waypoint,
613    )?;
614    put_list(
615        &mut root.root,
616        "Placeable List",
617        &git.placeables,
618        build_placeable,
619    )?;
620
621    Ok(root)
622}
623
624/// Writes a typed `GIT` file to `writer`.
625///
626/// This is equivalent to [`build_git_root`] followed by [`write_gff_root`].
627///
628/// # Errors
629///
630/// Returns [`GitError`] if building or writing the GFF root fails.
631///
632/// # Examples
633///
634/// ```rust,no_run
635/// let _ = nwnrs_git::write_git;
636/// ```
637#[allow(clippy :: redundant_closure_call)]
match (move ||
                {

                    #[allow(unknown_lints, unreachable_code, clippy ::
                    diverging_sub_expression, clippy :: empty_loop, clippy ::
                    let_unit_value, clippy :: let_with_type_underscore, clippy
                    :: needless_return, clippy :: unreachable)]
                    if false {
                        let __tracing_attr_fake_return: GitResult<()> = loop {};
                        return __tracing_attr_fake_return;
                    }
                    {
                        let root = build_git_root(git)?;
                        write_gff_root(writer, &root)?;
                        Ok(())
                    }
                })()
    {
        #[allow(clippy :: unit_arg)]
        Ok(x) => Ok(x),
    Err(e) => {
        {
            use ::tracing::__macro_support::Callsite as _;
            static __CALLSITE: ::tracing::callsite::DefaultCallsite =
                {
                    static META: ::tracing::Metadata<'static> =
                        {
                            ::tracing_core::metadata::Metadata::new("event src/lib.rs:637",
                                "nwnrs_git", ::tracing::Level::ERROR,
                                ::tracing_core::__macro_support::Option::Some("src/lib.rs"),
                                ::tracing_core::__macro_support::Option::Some(637u32),
                                ::tracing_core::__macro_support::Option::Some("nwnrs_git"),
                                ::tracing_core::field::FieldSet::new(&[{
                                                    const NAME:
                                                        ::tracing::__macro_support::FieldName<{
                                                            ::tracing::__macro_support::FieldName::len("error")
                                                        }> =
                                                        ::tracing::__macro_support::FieldName::new("error");
                                                    NAME.as_str()
                                                }], ::tracing_core::callsite::Identifier(&__CALLSITE)),
                                ::tracing::metadata::Kind::EVENT)
                        };
                    ::tracing::callsite::DefaultCallsite::new(&META)
                };
            let enabled =
                ::tracing::Level::ERROR <=
                            ::tracing::level_filters::STATIC_MAX_LEVEL &&
                        ::tracing::Level::ERROR <=
                            ::tracing::level_filters::LevelFilter::current() &&
                    {
                        let interest = __CALLSITE.interest();
                        !interest.is_never() &&
                            ::tracing::__macro_support::__is_enabled(__CALLSITE.metadata(),
                                interest)
                    };
            if enabled {
                (|value_set: ::tracing::field::ValueSet|
                            {
                                let meta = __CALLSITE.metadata();
                                ::tracing::Event::dispatch(meta, &value_set);
                                ;
                            })({
                        #[allow(unused_imports)]
                        use ::tracing::field::{debug, display, Value};
                        __CALLSITE.metadata().fields().value_set_all(&[(::tracing::__macro_support::Option::Some(&::tracing::field::display(&e)
                                                    as &dyn ::tracing::field::Value))])
                    });
            } else { ; }
        };
        Err(e)
    }
}#[instrument(level = "debug", skip_all, err)]
638pub fn write_git<W: Write + Seek>(writer: &mut W, git: &GitFile) -> GitResult<()> {
639    let root = build_git_root(git)?;
640    write_gff_root(writer, &root)?;
641    Ok(())
642}
643
644fn parse_area_properties(value: &GffStruct) -> GitAreaProperties {
645    GitAreaProperties {
646        raw: value.clone(),
647        ambient_sound_day: gff_i32(value, "AmbientSndDay"),
648        ambient_sound_night: gff_i32(value, "AmbientSndNight"),
649        ambient_sound_day_volume: gff_i32(value, "AmbientSndDayVol"),
650        ambient_sound_night_volume: gff_i32(value, "AmbientSndNitVol"),
651        env_audio: gff_i32(value, "EnvAudio"),
652        music_battle: gff_i32(value, "MusicBattle"),
653        music_day: gff_i32(value, "MusicDay"),
654        music_night: gff_i32(value, "MusicNight"),
655        music_delay: gff_i32(value, "MusicDelay"),
656    }
657}
658
659fn parse_creature(value: &GffStruct) -> GitCreature {
660    GitCreature {
661        raw:             value.clone(),
662        tag:             gff_string(value, "Tag"),
663        template_resref: gff_resref(value, "TemplateResRef"),
664        localized_name:  gff_loc_string_any(value, &["LocName", "LocalizedName"]),
665        description:     gff_loc_string(value, "Description"),
666        transform:       parse_transform(value),
667    }
668}
669
670fn parse_door(value: &GffStruct) -> GitDoor {
671    GitDoor {
672        raw:             value.clone(),
673        tag:             gff_string(value, "Tag"),
674        localized_name:  gff_loc_string(value, "LocName"),
675        description:     gff_loc_string(value, "Description"),
676        template_resref: gff_resref(value, "TemplateResRef"),
677        appearance:      gff_i32(value, "Appearance"),
678        animation_state: gff_i32(value, "AnimationState"),
679        linked_to:       gff_string(value, "LinkedTo"),
680        transform:       parse_transform(value),
681    }
682}
683
684fn parse_encounter(value: &GffStruct) -> GitEncounter {
685    GitEncounter {
686        raw:            value.clone(),
687        tag:            gff_string(value, "Tag"),
688        localized_name: gff_loc_string_any(value, &["LocName", "LocalizedName"]),
689        transform:      parse_transform(value),
690        geometry:       parse_geometry(value),
691    }
692}
693
694fn parse_sound(value: &GffStruct) -> GitSound {
695    let sounds = gff_list(value, "Sounds")
696        .into_iter()
697        .flatten()
698        .map(|entry| GitSoundRef {
699            sound: gff_string_any(entry, &["Sound", "SoundResRef"]),
700        })
701        .collect();
702
703    GitSound {
704        raw: value.clone(),
705        tag: gff_string(value, "Tag"),
706        localized_name: gff_loc_string(value, "LocName"),
707        template_resref: gff_resref(value, "TemplateResRef"),
708        transform: parse_transform(value),
709        positional: gff_bool(value, "Positional"),
710        min_distance: gff_f32(value, "MinDistance"),
711        max_distance: gff_f32(value, "MaxDistance"),
712        volume: gff_i32(value, "Volume"),
713        sounds,
714    }
715}
716
717fn parse_store(value: &GffStruct) -> GitStore {
718    GitStore {
719        raw:             value.clone(),
720        tag:             gff_string(value, "Tag"),
721        localized_name:  gff_loc_string_any(value, &["LocName", "LocalizedName"]),
722        template_resref: gff_string_any(value, &["ResRef", "TemplateResRef"]),
723        transform:       parse_transform(value),
724    }
725}
726
727fn parse_trigger(value: &GffStruct) -> GitTrigger {
728    GitTrigger {
729        raw:            value.clone(),
730        tag:            gff_string(value, "Tag"),
731        localized_name: gff_loc_string_any(value, &["LocName", "LocalizedName"]),
732        transform:      parse_transform(value),
733        geometry:       parse_geometry(value),
734    }
735}
736
737fn parse_waypoint(value: &GffStruct) -> GitWaypoint {
738    GitWaypoint {
739        raw:             value.clone(),
740        tag:             gff_string(value, "Tag"),
741        localized_name:  gff_loc_string_any(value, &["LocalizedName", "LocName"]),
742        description:     gff_loc_string(value, "Description"),
743        template_resref: gff_resref(value, "TemplateResRef"),
744        linked_to:       gff_string(value, "LinkedTo"),
745        appearance:      gff_i32(value, "Appearance"),
746        transform:       parse_transform(value),
747    }
748}
749
750fn parse_placeable(value: &GffStruct) -> GitPlaceable {
751    GitPlaceable {
752        raw:             value.clone(),
753        tag:             gff_string(value, "Tag"),
754        localized_name:  gff_loc_string(value, "LocName"),
755        description:     gff_loc_string(value, "Description"),
756        template_resref: gff_resref(value, "TemplateResRef"),
757        appearance:      gff_i32(value, "Appearance"),
758        static_object:   gff_bool(value, "Static"),
759        useable:         gff_bool(value, "Useable"),
760        has_inventory:   gff_bool(value, "HasInventory"),
761        transform:       parse_transform(value),
762    }
763}
764
765fn parse_transform(value: &GffStruct) -> GitTransform {
766    GitTransform {
767        x:             gff_f32_any(value, &["X", "XPosition"]),
768        y:             gff_f32_any(value, &["Y", "YPosition"]),
769        z:             gff_f32_any(value, &["Z", "ZPosition"]),
770        bearing:       gff_f32(value, "Bearing"),
771        x_orientation: gff_f32(value, "XOrientation"),
772        y_orientation: gff_f32(value, "YOrientation"),
773    }
774}
775
776fn parse_geometry(value: &GffStruct) -> Vec<GitPoint> {
777    gff_list(value, "Geometry")
778        .into_iter()
779        .flatten()
780        .map(|point| GitPoint {
781            x: gff_f32(point, "X"),
782            y: gff_f32(point, "Y"),
783            z: gff_f32(point, "Z"),
784        })
785        .collect()
786}
787
788fn gff_struct<'a>(value: &'a GffStruct, label: &str) -> Option<&'a GffStruct> {
789    match value.get_field(label)?.value() {
790        GffValue::Struct(child) => Some(child),
791        _ => None,
792    }
793}
794
795fn gff_list<'a>(value: &'a GffStruct, label: &str) -> Option<&'a [GffStruct]> {
796    match value.get_field(label)?.value() {
797        GffValue::List(items) => Some(items.as_slice()),
798        _ => None,
799    }
800}
801
802fn gff_bool(value: &GffStruct, label: &str) -> Option<bool> {
803    match value.get_field(label)?.value() {
804        GffValue::Byte(raw) => Some(*raw != 0),
805        GffValue::Char(raw) => Some(*raw != 0),
806        GffValue::Word(raw) => Some(*raw != 0),
807        GffValue::Short(raw) => Some(*raw != 0),
808        GffValue::Dword(raw) => Some(*raw != 0),
809        GffValue::Int(raw) => Some(*raw != 0),
810        _ => None,
811    }
812}
813
814fn gff_i32(value: &GffStruct, label: &str) -> Option<i32> {
815    match value.get_field(label)?.value() {
816        GffValue::Byte(raw) => Some(i32::from(*raw)),
817        GffValue::Char(raw) => Some(i32::from(*raw)),
818        GffValue::Word(raw) => Some(i32::from(*raw)),
819        GffValue::Short(raw) => Some(i32::from(*raw)),
820        GffValue::Dword(raw) => i32::try_from(*raw).ok(),
821        GffValue::Int(raw) => Some(*raw),
822        _ => None,
823    }
824}
825
826#[allow(clippy::cast_precision_loss)]
827fn gff_f32(value: &GffStruct, label: &str) -> Option<f32> {
828    match value.get_field(label)?.value() {
829        GffValue::Byte(raw) => Some(f32::from(*raw)),
830        GffValue::Char(raw) => Some(f32::from(*raw)),
831        GffValue::Word(raw) => Some(f32::from(*raw)),
832        GffValue::Short(raw) => Some(f32::from(*raw)),
833        GffValue::Dword(raw) => Some(*raw as f32),
834        GffValue::Int(raw) => Some(*raw as f32),
835        GffValue::Float(raw) => Some(*raw),
836        _ => None,
837    }
838}
839
840fn gff_string(value: &GffStruct, label: &str) -> Option<String> {
841    gff_string_any(value, &[label])
842}
843
844fn gff_string_any(value: &GffStruct, labels: &[&str]) -> Option<String> {
845    labels
846        .iter()
847        .find_map(|label| match value.get_field(label)?.value() {
848            GffValue::CExoString(raw) | GffValue::ResRef(raw) => {
849                let trimmed = raw.trim();
850                (!trimmed.is_empty()).then(|| trimmed.to_string())
851            }
852            _ => None,
853        })
854}
855
856fn gff_resref(value: &GffStruct, label: &str) -> Option<String> {
857    gff_string_any(value, &[label])
858}
859
860fn gff_loc_string(value: &GffStruct, label: &str) -> Option<GffCExoLocString> {
861    gff_loc_string_any(value, &[label])
862}
863
864fn gff_loc_string_any(value: &GffStruct, labels: &[&str]) -> Option<GffCExoLocString> {
865    labels
866        .iter()
867        .find_map(|label| match value.get_field(label)?.value() {
868            GffValue::CExoLocString(raw) => Some(raw.clone()),
869            _ => None,
870        })
871}
872
873fn gff_f32_any(value: &GffStruct, labels: &[&str]) -> Option<f32> {
874    labels.iter().find_map(|label| gff_f32(value, label))
875}
876
877fn build_area_properties(value: &GitAreaProperties) -> GitResult<GffStruct> {
878    let mut result = value.raw.clone();
879    clear_labels(
880        &mut result,
881        &[
882            "AmbientSndDay",
883            "AmbientSndNight",
884            "AmbientSndDayVol",
885            "AmbientSndNitVol",
886            "EnvAudio",
887            "MusicBattle",
888            "MusicDay",
889            "MusicNight",
890            "MusicDelay",
891        ],
892    );
893    put_i32(&mut result, "AmbientSndDay", value.ambient_sound_day)?;
894    put_i32(&mut result, "AmbientSndNight", value.ambient_sound_night)?;
895    put_i32(
896        &mut result,
897        "AmbientSndDayVol",
898        value.ambient_sound_day_volume,
899    )?;
900    put_i32(
901        &mut result,
902        "AmbientSndNitVol",
903        value.ambient_sound_night_volume,
904    )?;
905    put_i32(&mut result, "EnvAudio", value.env_audio)?;
906    put_i32(&mut result, "MusicBattle", value.music_battle)?;
907    put_i32(&mut result, "MusicDay", value.music_day)?;
908    put_i32(&mut result, "MusicNight", value.music_night)?;
909    put_i32(&mut result, "MusicDelay", value.music_delay)?;
910    Ok(result)
911}
912
913fn build_creature(value: &GitCreature) -> GitResult<GffStruct> {
914    let mut result = value.raw.clone();
915    clear_labels(
916        &mut result,
917        &[
918            "Tag",
919            "TemplateResRef",
920            "LocName",
921            "LocalizedName",
922            "Description",
923            "XPosition",
924            "YPosition",
925            "ZPosition",
926            "X",
927            "Y",
928            "Z",
929            "Bearing",
930            "XOrientation",
931            "YOrientation",
932        ],
933    );
934    put_string(&mut result, "Tag", value.tag.as_deref())?;
935    put_resref(
936        &mut result,
937        "TemplateResRef",
938        value.template_resref.as_deref(),
939    )?;
940    put_loc_string(
941        &mut result,
942        preferred_loc_label(&value.raw, &["LocName", "LocalizedName"], "LocName"),
943        value.localized_name.as_ref(),
944    )?;
945    put_loc_string(&mut result, "Description", value.description.as_ref())?;
946    put_transform(&mut result, &value.raw, &value.transform)?;
947    Ok(result)
948}
949
950fn build_door(value: &GitDoor) -> GitResult<GffStruct> {
951    let mut result = value.raw.clone();
952    clear_labels(
953        &mut result,
954        &[
955            "Tag",
956            "LocName",
957            "LocalizedName",
958            "Description",
959            "TemplateResRef",
960            "Appearance",
961            "AnimationState",
962            "LinkedTo",
963            "XPosition",
964            "YPosition",
965            "ZPosition",
966            "X",
967            "Y",
968            "Z",
969            "Bearing",
970            "XOrientation",
971            "YOrientation",
972        ],
973    );
974    put_string(&mut result, "Tag", value.tag.as_deref())?;
975    put_loc_string(
976        &mut result,
977        preferred_loc_label(&value.raw, &["LocName", "LocalizedName"], "LocName"),
978        value.localized_name.as_ref(),
979    )?;
980    put_loc_string(&mut result, "Description", value.description.as_ref())?;
981    put_resref(
982        &mut result,
983        "TemplateResRef",
984        value.template_resref.as_deref(),
985    )?;
986    put_i32(&mut result, "Appearance", value.appearance)?;
987    put_i32(&mut result, "AnimationState", value.animation_state)?;
988    put_string(&mut result, "LinkedTo", value.linked_to.as_deref())?;
989    put_transform(&mut result, &value.raw, &value.transform)?;
990    Ok(result)
991}
992
993fn build_encounter(value: &GitEncounter) -> GitResult<GffStruct> {
994    let mut result = value.raw.clone();
995    clear_labels(
996        &mut result,
997        &[
998            "Tag",
999            "LocName",
1000            "LocalizedName",
1001            "Geometry",
1002            "XPosition",
1003            "YPosition",
1004            "ZPosition",
1005            "X",
1006            "Y",
1007            "Z",
1008            "Bearing",
1009            "XOrientation",
1010            "YOrientation",
1011        ],
1012    );
1013    put_string(&mut result, "Tag", value.tag.as_deref())?;
1014    put_loc_string(
1015        &mut result,
1016        preferred_loc_label(&value.raw, &["LocName", "LocalizedName"], "LocName"),
1017        value.localized_name.as_ref(),
1018    )?;
1019    put_transform(&mut result, &value.raw, &value.transform)?;
1020    put_geometry(&mut result, &value.geometry)?;
1021    Ok(result)
1022}
1023
1024fn build_sound(value: &GitSound) -> GitResult<GffStruct> {
1025    let mut result = value.raw.clone();
1026    clear_labels(
1027        &mut result,
1028        &[
1029            "Tag",
1030            "LocName",
1031            "LocalizedName",
1032            "TemplateResRef",
1033            "Positional",
1034            "MinDistance",
1035            "MaxDistance",
1036            "Volume",
1037            "Sounds",
1038            "XPosition",
1039            "YPosition",
1040            "ZPosition",
1041            "X",
1042            "Y",
1043            "Z",
1044            "Bearing",
1045            "XOrientation",
1046            "YOrientation",
1047        ],
1048    );
1049    put_string(&mut result, "Tag", value.tag.as_deref())?;
1050    put_loc_string(
1051        &mut result,
1052        preferred_loc_label(&value.raw, &["LocName", "LocalizedName"], "LocName"),
1053        value.localized_name.as_ref(),
1054    )?;
1055    put_resref(
1056        &mut result,
1057        "TemplateResRef",
1058        value.template_resref.as_deref(),
1059    )?;
1060    put_transform(&mut result, &value.raw, &value.transform)?;
1061    put_bool(&mut result, "Positional", value.positional)?;
1062    put_f32(&mut result, "MinDistance", value.min_distance)?;
1063    put_f32(&mut result, "MaxDistance", value.max_distance)?;
1064    put_i32(&mut result, "Volume", value.volume)?;
1065
1066    let sounds = value
1067        .sounds
1068        .iter()
1069        .map(build_sound_ref)
1070        .collect::<GitResult<Vec<_>>>()?;
1071    put_list_value(&mut result, "Sounds", sounds)?;
1072    Ok(result)
1073}
1074
1075fn build_sound_ref(value: &GitSoundRef) -> GitResult<GffStruct> {
1076    let mut result = GffStruct::new(0);
1077    put_resref(&mut result, "Sound", value.sound.as_deref())?;
1078    Ok(result)
1079}
1080
1081fn build_store(value: &GitStore) -> GitResult<GffStruct> {
1082    let mut result = value.raw.clone();
1083    clear_labels(
1084        &mut result,
1085        &[
1086            "Tag",
1087            "LocName",
1088            "LocalizedName",
1089            "ResRef",
1090            "TemplateResRef",
1091            "XPosition",
1092            "YPosition",
1093            "ZPosition",
1094            "X",
1095            "Y",
1096            "Z",
1097            "Bearing",
1098            "XOrientation",
1099            "YOrientation",
1100        ],
1101    );
1102    put_string(&mut result, "Tag", value.tag.as_deref())?;
1103    put_loc_string(
1104        &mut result,
1105        preferred_loc_label(&value.raw, &["LocName", "LocalizedName"], "LocalizedName"),
1106        value.localized_name.as_ref(),
1107    )?;
1108    put_resref(
1109        &mut result,
1110        preferred_resref_label(&value.raw, &["ResRef", "TemplateResRef"], "ResRef"),
1111        value.template_resref.as_deref(),
1112    )?;
1113    put_transform(&mut result, &value.raw, &value.transform)?;
1114    Ok(result)
1115}
1116
1117fn build_trigger(value: &GitTrigger) -> GitResult<GffStruct> {
1118    let mut result = value.raw.clone();
1119    clear_labels(
1120        &mut result,
1121        &[
1122            "Tag",
1123            "LocName",
1124            "LocalizedName",
1125            "Geometry",
1126            "XPosition",
1127            "YPosition",
1128            "ZPosition",
1129            "X",
1130            "Y",
1131            "Z",
1132            "Bearing",
1133            "XOrientation",
1134            "YOrientation",
1135        ],
1136    );
1137    put_string(&mut result, "Tag", value.tag.as_deref())?;
1138    put_loc_string(
1139        &mut result,
1140        preferred_loc_label(&value.raw, &["LocName", "LocalizedName"], "LocName"),
1141        value.localized_name.as_ref(),
1142    )?;
1143    put_transform(&mut result, &value.raw, &value.transform)?;
1144    put_geometry(&mut result, &value.geometry)?;
1145    Ok(result)
1146}
1147
1148fn build_waypoint(value: &GitWaypoint) -> GitResult<GffStruct> {
1149    let mut result = value.raw.clone();
1150    clear_labels(
1151        &mut result,
1152        &[
1153            "Tag",
1154            "LocName",
1155            "LocalizedName",
1156            "Description",
1157            "TemplateResRef",
1158            "LinkedTo",
1159            "Appearance",
1160            "XPosition",
1161            "YPosition",
1162            "ZPosition",
1163            "X",
1164            "Y",
1165            "Z",
1166            "Bearing",
1167            "XOrientation",
1168            "YOrientation",
1169        ],
1170    );
1171    put_string(&mut result, "Tag", value.tag.as_deref())?;
1172    put_loc_string(
1173        &mut result,
1174        preferred_loc_label(&value.raw, &["LocalizedName", "LocName"], "LocalizedName"),
1175        value.localized_name.as_ref(),
1176    )?;
1177    put_loc_string(&mut result, "Description", value.description.as_ref())?;
1178    put_resref(
1179        &mut result,
1180        "TemplateResRef",
1181        value.template_resref.as_deref(),
1182    )?;
1183    put_string(&mut result, "LinkedTo", value.linked_to.as_deref())?;
1184    put_i32(&mut result, "Appearance", value.appearance)?;
1185    put_transform(&mut result, &value.raw, &value.transform)?;
1186    Ok(result)
1187}
1188
1189fn build_placeable(value: &GitPlaceable) -> GitResult<GffStruct> {
1190    let mut result = value.raw.clone();
1191    clear_labels(
1192        &mut result,
1193        &[
1194            "Tag",
1195            "LocName",
1196            "LocalizedName",
1197            "Description",
1198            "TemplateResRef",
1199            "Appearance",
1200            "Static",
1201            "Useable",
1202            "HasInventory",
1203            "XPosition",
1204            "YPosition",
1205            "ZPosition",
1206            "X",
1207            "Y",
1208            "Z",
1209            "Bearing",
1210            "XOrientation",
1211            "YOrientation",
1212        ],
1213    );
1214    put_string(&mut result, "Tag", value.tag.as_deref())?;
1215    put_loc_string(
1216        &mut result,
1217        preferred_loc_label(&value.raw, &["LocName", "LocalizedName"], "LocName"),
1218        value.localized_name.as_ref(),
1219    )?;
1220    put_loc_string(&mut result, "Description", value.description.as_ref())?;
1221    put_resref(
1222        &mut result,
1223        "TemplateResRef",
1224        value.template_resref.as_deref(),
1225    )?;
1226    put_i32(&mut result, "Appearance", value.appearance)?;
1227    put_bool(&mut result, "Static", value.static_object)?;
1228    put_bool(&mut result, "Useable", value.useable)?;
1229    put_bool(&mut result, "HasInventory", value.has_inventory)?;
1230    put_transform(&mut result, &value.raw, &value.transform)?;
1231    Ok(result)
1232}
1233
1234fn put_transform(target: &mut GffStruct, raw: &GffStruct, value: &GitTransform) -> GitResult<()> {
1235    let (x_label, y_label, z_label) = preferred_position_labels(raw);
1236    put_f32(target, x_label, value.x)?;
1237    put_f32(target, y_label, value.y)?;
1238    put_f32(target, z_label, value.z)?;
1239    put_f32(target, "Bearing", value.bearing)?;
1240    put_f32(target, "XOrientation", value.x_orientation)?;
1241    put_f32(target, "YOrientation", value.y_orientation)?;
1242    Ok(())
1243}
1244
1245fn put_geometry(target: &mut GffStruct, value: &[GitPoint]) -> GitResult<()> {
1246    let geometry = value
1247        .iter()
1248        .map(|point| {
1249            let mut result = GffStruct::new(0);
1250            put_f32(&mut result, "X", point.x)?;
1251            put_f32(&mut result, "Y", point.y)?;
1252            put_f32(&mut result, "Z", point.z)?;
1253            Ok(result)
1254        })
1255        .collect::<GitResult<Vec<_>>>()?;
1256    put_list_value(target, "Geometry", geometry)?;
1257    Ok(())
1258}
1259
1260fn preferred_position_labels(raw: &GffStruct) -> (&'static str, &'static str, &'static str) {
1261    if raw.get_field("XPosition").is_some()
1262        || raw.get_field("YPosition").is_some()
1263        || raw.get_field("ZPosition").is_some()
1264    {
1265        ("XPosition", "YPosition", "ZPosition")
1266    } else {
1267        ("X", "Y", "Z")
1268    }
1269}
1270
1271fn preferred_loc_label<'a>(raw: &GffStruct, labels: &[&'a str], fallback: &'a str) -> &'a str {
1272    labels
1273        .iter()
1274        .copied()
1275        .find(|label| raw.get_field(label).is_some())
1276        .unwrap_or(fallback)
1277}
1278
1279fn preferred_resref_label<'a>(raw: &GffStruct, labels: &[&'a str], fallback: &'a str) -> &'a str {
1280    labels
1281        .iter()
1282        .copied()
1283        .find(|label| raw.get_field(label).is_some())
1284        .unwrap_or(fallback)
1285}
1286
1287fn clear_labels(target: &mut GffStruct, labels: &[&str]) {
1288    for label in labels {
1289        let _ = target.remove(label);
1290    }
1291}
1292
1293fn put_list<T, F>(target: &mut GffStruct, label: &str, values: &[T], mut build: F) -> GitResult<()>
1294where
1295    F: FnMut(&T) -> GitResult<GffStruct>,
1296{
1297    let structs = values
1298        .iter()
1299        .map(&mut build)
1300        .collect::<GitResult<Vec<_>>>()?;
1301    put_list_value(target, label, structs)?;
1302    Ok(())
1303}
1304
1305fn put_list_value(target: &mut GffStruct, label: &str, values: Vec<GffStruct>) -> GitResult<()> {
1306    target.put_value(label, GffValue::List(values))?;
1307    Ok(())
1308}
1309
1310fn put_string(target: &mut GffStruct, label: &str, value: Option<&str>) -> GitResult<()> {
1311    if let Some(value) = value {
1312        target.put_value(label, GffValue::CExoString(value.to_string()))?;
1313    }
1314    Ok(())
1315}
1316
1317fn put_resref(target: &mut GffStruct, label: &str, value: Option<&str>) -> GitResult<()> {
1318    if let Some(value) = value {
1319        target.put_value(label, GffValue::ResRef(value.to_string()))?;
1320    }
1321    Ok(())
1322}
1323
1324fn put_loc_string(
1325    target: &mut GffStruct,
1326    label: &str,
1327    value: Option<&GffCExoLocString>,
1328) -> GitResult<()> {
1329    if let Some(value) = value {
1330        target.put_value(label, GffValue::CExoLocString(value.clone()))?;
1331    }
1332    Ok(())
1333}
1334
1335fn put_i32(target: &mut GffStruct, label: &str, value: Option<i32>) -> GitResult<()> {
1336    if let Some(value) = value {
1337        target.put_value(label, GffValue::Int(value))?;
1338    }
1339    Ok(())
1340}
1341
1342fn put_f32(target: &mut GffStruct, label: &str, value: Option<f32>) -> GitResult<()> {
1343    if let Some(value) = value {
1344        target.put_value(label, GffValue::Float(value))?;
1345    }
1346    Ok(())
1347}
1348
1349fn put_bool(target: &mut GffStruct, label: &str, value: Option<bool>) -> GitResult<()> {
1350    if let Some(value) = value {
1351        target.put_value(label, GffValue::Byte(u8::from(value)))?;
1352    }
1353    Ok(())
1354}
1355
1356/// Common imports for consumers of this crate.
1357pub mod prelude {
1358    pub use crate::{
1359        GIT_RES_TYPE, GitAreaProperties, GitCreature, GitDoor, GitEncounter, GitError, GitFile,
1360        GitPlaceable, GitPoint, GitResult, GitSound, GitSoundRef, GitStore, GitTransform,
1361        GitTrigger, GitWaypoint, build_git_root, parse_git_root, read_git, write_git,
1362    };
1363}
1364
1365#[allow(clippy::panic)]
1366#[cfg(test)]
1367mod tests {
1368    use std::{io::Cursor, sync::Arc};
1369
1370    use nwnrs_gff::prelude::{
1371        GffCExoLocString, GffRoot, GffStruct, GffValue, read_gff_root, write_gff_root,
1372    };
1373    use nwnrs_resman::{CachePolicy, ResContainer, ResMan};
1374    use nwnrs_resmemfile::prelude::read_resmemfile;
1375    use nwnrs_resref::ResRef;
1376
1377    use super::{GIT_RES_TYPE, GitFile, build_git_root, parse_git_root, read_git, write_git};
1378
1379    fn encode_root(root: &GffRoot) -> Vec<u8> {
1380        let mut output = Cursor::new(Vec::new());
1381        write_gff_root(&mut output, root).unwrap_or_else(|error| {
1382            panic!("encode gff: {error}");
1383        });
1384        output.into_inner()
1385    }
1386
1387    fn make_loc_string(text: &str) -> GffCExoLocString {
1388        let mut result = GffCExoLocString::default();
1389        result.entries.push((0, text.to_string()));
1390        result
1391    }
1392
1393    fn sample_git_root() -> GffRoot {
1394        let mut root = GffRoot::new("GIT ");
1395
1396        let mut area = GffStruct::new(100);
1397        area.put_value("AmbientSndDay", GffValue::Int(81))
1398            .unwrap_or_else(|error| panic!("area ambient day: {error}"));
1399        area.put_value("MusicDay", GffValue::Int(12))
1400            .unwrap_or_else(|error| panic!("area music day: {error}"));
1401        root.put_value("AreaProperties", GffValue::Struct(area))
1402            .unwrap_or_else(|error| panic!("root area properties: {error}"));
1403
1404        let mut creature = GffStruct::new(1);
1405        creature
1406            .put_value("Tag", GffValue::CExoString("orc_01".to_string()))
1407            .unwrap_or_else(|error| panic!("creature tag: {error}"));
1408        creature
1409            .put_value(
1410                "TemplateResRef",
1411                GffValue::ResRef("orcblueprint".to_string()),
1412            )
1413            .unwrap_or_else(|error| panic!("creature template: {error}"));
1414        creature
1415            .put_value("LocName", GffValue::CExoLocString(make_loc_string("Orc")))
1416            .unwrap_or_else(|error| panic!("creature loc name: {error}"));
1417        creature
1418            .put_value("XPosition", GffValue::Float(1.0))
1419            .unwrap_or_else(|error| panic!("creature x: {error}"));
1420        creature
1421            .put_value("YPosition", GffValue::Float(2.0))
1422            .unwrap_or_else(|error| panic!("creature y: {error}"));
1423        creature
1424            .put_value("ZPosition", GffValue::Float(3.0))
1425            .unwrap_or_else(|error| panic!("creature z: {error}"));
1426        root.put_value("Creature List", GffValue::List(vec![creature]))
1427            .unwrap_or_else(|error| panic!("root creature list: {error}"));
1428
1429        let mut door = GffStruct::new(2);
1430        door.put_value("Tag", GffValue::CExoString("gate".to_string()))
1431            .unwrap_or_else(|error| panic!("door tag: {error}"));
1432        door.put_value("TemplateResRef", GffValue::ResRef("door_gate".to_string()))
1433            .unwrap_or_else(|error| panic!("door template: {error}"));
1434        door.put_value("Appearance", GffValue::Int(4))
1435            .unwrap_or_else(|error| panic!("door appearance: {error}"));
1436        door.put_value("Bearing", GffValue::Float(1.57))
1437            .unwrap_or_else(|error| panic!("door bearing: {error}"));
1438        door.put_value("X", GffValue::Float(10.0))
1439            .unwrap_or_else(|error| panic!("door x: {error}"));
1440        door.put_value("Y", GffValue::Float(20.0))
1441            .unwrap_or_else(|error| panic!("door y: {error}"));
1442        door.put_value("Z", GffValue::Float(0.5))
1443            .unwrap_or_else(|error| panic!("door z: {error}"));
1444        root.put_value("Door List", GffValue::List(vec![door]))
1445            .unwrap_or_else(|error| panic!("root door list: {error}"));
1446
1447        let mut sound_ref = GffStruct::new(0);
1448        sound_ref
1449            .put_value("Sound", GffValue::ResRef("as_pl_creak1".to_string()))
1450            .unwrap_or_else(|error| panic!("sound ref: {error}"));
1451
1452        let mut sound = GffStruct::new(3);
1453        sound
1454            .put_value("Tag", GffValue::CExoString("creak".to_string()))
1455            .unwrap_or_else(|error| panic!("sound tag: {error}"));
1456        sound
1457            .put_value("Positional", GffValue::Byte(1))
1458            .unwrap_or_else(|error| panic!("sound positional: {error}"));
1459        sound
1460            .put_value("Volume", GffValue::Int(64))
1461            .unwrap_or_else(|error| panic!("sound volume: {error}"));
1462        sound
1463            .put_value("Sounds", GffValue::List(vec![sound_ref]))
1464            .unwrap_or_else(|error| panic!("sound list: {error}"));
1465        root.put_value("SoundList", GffValue::List(vec![sound]))
1466            .unwrap_or_else(|error| panic!("root sound list: {error}"));
1467
1468        let mut waypoint = GffStruct::new(4);
1469        waypoint
1470            .put_value("Tag", GffValue::CExoString("spawn0".to_string()))
1471            .unwrap_or_else(|error| panic!("waypoint tag: {error}"));
1472        waypoint
1473            .put_value(
1474                "LocalizedName",
1475                GffValue::CExoLocString(make_loc_string("Spawn")),
1476            )
1477            .unwrap_or_else(|error| panic!("waypoint loc name: {error}"));
1478        waypoint
1479            .put_value("TemplateResRef", GffValue::ResRef("spawn0".to_string()))
1480            .unwrap_or_else(|error| panic!("waypoint template: {error}"));
1481        waypoint
1482            .put_value("XPosition", GffValue::Float(5.0))
1483            .unwrap_or_else(|error| panic!("waypoint x: {error}"));
1484        waypoint
1485            .put_value("YPosition", GffValue::Float(6.0))
1486            .unwrap_or_else(|error| panic!("waypoint y: {error}"));
1487        waypoint
1488            .put_value("ZPosition", GffValue::Float(7.0))
1489            .unwrap_or_else(|error| panic!("waypoint z: {error}"));
1490        waypoint
1491            .put_value("XOrientation", GffValue::Float(0.0))
1492            .unwrap_or_else(|error| panic!("waypoint xo: {error}"));
1493        waypoint
1494            .put_value("YOrientation", GffValue::Float(1.0))
1495            .unwrap_or_else(|error| panic!("waypoint yo: {error}"));
1496        root.put_value("WaypointList", GffValue::List(vec![waypoint]))
1497            .unwrap_or_else(|error| panic!("root waypoint list: {error}"));
1498
1499        let mut placeable = GffStruct::new(5);
1500        placeable
1501            .put_value("Tag", GffValue::CExoString("chest_01".to_string()))
1502            .unwrap_or_else(|error| panic!("placeable tag: {error}"));
1503        placeable
1504            .put_value("LocName", GffValue::CExoLocString(make_loc_string("Chest")))
1505            .unwrap_or_else(|error| panic!("placeable loc name: {error}"));
1506        placeable
1507            .put_value("TemplateResRef", GffValue::ResRef("plc_chest".to_string()))
1508            .unwrap_or_else(|error| panic!("placeable template: {error}"));
1509        placeable
1510            .put_value("Appearance", GffValue::Int(99))
1511            .unwrap_or_else(|error| panic!("placeable appearance: {error}"));
1512        placeable
1513            .put_value("Static", GffValue::Byte(1))
1514            .unwrap_or_else(|error| panic!("placeable static: {error}"));
1515        placeable
1516            .put_value("Useable", GffValue::Byte(1))
1517            .unwrap_or_else(|error| panic!("placeable useable: {error}"));
1518        placeable
1519            .put_value("HasInventory", GffValue::Byte(1))
1520            .unwrap_or_else(|error| panic!("placeable inventory: {error}"));
1521        placeable
1522            .put_value("X", GffValue::Float(11.0))
1523            .unwrap_or_else(|error| panic!("placeable x: {error}"));
1524        placeable
1525            .put_value("Y", GffValue::Float(12.0))
1526            .unwrap_or_else(|error| panic!("placeable y: {error}"));
1527        placeable
1528            .put_value("Z", GffValue::Float(0.0))
1529            .unwrap_or_else(|error| panic!("placeable z: {error}"));
1530        placeable
1531            .put_value("Bearing", GffValue::Float(0.25))
1532            .unwrap_or_else(|error| panic!("placeable bearing: {error}"));
1533        root.put_value("Placeable List", GffValue::List(vec![placeable]))
1534            .unwrap_or_else(|error| panic!("root placeable list: {error}"));
1535
1536        root
1537    }
1538
1539    #[test]
1540    fn parses_typed_git_root() {
1541        let encoded = encode_root(&sample_git_root());
1542        let reparsed_root =
1543            read_gff_root(&mut Cursor::new(encoded.clone())).unwrap_or_else(|error| {
1544                panic!("re-read gff root: {error}");
1545            });
1546
1547        let parsed = parse_git_root(&reparsed_root).unwrap_or_else(|error| {
1548            panic!("parse git root: {error}");
1549        });
1550
1551        assert_eq!(
1552            parsed
1553                .area_properties
1554                .as_ref()
1555                .and_then(|value| value.ambient_sound_day),
1556            Some(81)
1557        );
1558        assert_eq!(parsed.creatures.len(), 1);
1559        assert_eq!(parsed.doors.len(), 1);
1560        assert_eq!(parsed.sounds.len(), 1);
1561        assert_eq!(parsed.waypoints.len(), 1);
1562        assert_eq!(parsed.placeables.len(), 1);
1563        assert_eq!(
1564            parsed
1565                .creatures
1566                .first()
1567                .and_then(|value| value.template_resref.as_deref()),
1568            Some("orcblueprint")
1569        );
1570        assert_eq!(
1571            parsed
1572                .doors
1573                .first()
1574                .and_then(|value| value.transform.bearing),
1575            Some(1.57)
1576        );
1577        assert_eq!(
1578            parsed
1579                .sounds
1580                .first()
1581                .and_then(|value| value.sounds.first())
1582                .and_then(|value| value.sound.as_deref()),
1583            Some("as_pl_creak1")
1584        );
1585        assert_eq!(
1586            parsed
1587                .waypoints
1588                .first()
1589                .and_then(|value| value.transform.y_orientation),
1590            Some(1.0)
1591        );
1592        assert_eq!(
1593            parsed
1594                .placeables
1595                .first()
1596                .and_then(|value| value.static_object),
1597            Some(true)
1598        );
1599
1600        let reparsed = read_git(&mut Cursor::new(encoded)).unwrap_or_else(|error| {
1601            panic!("read git: {error}");
1602        });
1603        assert_eq!(
1604            reparsed
1605                .placeables
1606                .first()
1607                .and_then(|value| value.transform.x),
1608            Some(11.0)
1609        );
1610    }
1611
1612    #[test]
1613    fn reads_git_from_resman() {
1614        let bytes = encode_root(&sample_git_root());
1615        let rr = ResRef::new("arena", GIT_RES_TYPE).unwrap_or_else(|error| {
1616            panic!("arena rr: {error}");
1617        });
1618        let resmem = read_resmemfile("arena.git", rr, bytes).unwrap_or_else(|error| {
1619            panic!("resmem file: {error}");
1620        });
1621
1622        let mut resman = ResMan::new(0);
1623        resman.add(Arc::new(resmem) as Arc<dyn ResContainer>);
1624
1625        let parsed = GitFile::from_resman(&mut resman, "arena", CachePolicy::Bypass)
1626            .unwrap_or_else(|error| {
1627                panic!("read git from resman: {error}");
1628            });
1629        assert_eq!(
1630            parsed
1631                .area_properties
1632                .as_ref()
1633                .and_then(|value| value.music_day),
1634            Some(12)
1635        );
1636        assert_eq!(
1637            parsed
1638                .placeables
1639                .first()
1640                .and_then(|value| value.template_resref.as_deref()),
1641            Some("plc_chest")
1642        );
1643    }
1644
1645    #[test]
1646    fn writes_git_round_trip_from_typed_model() {
1647        let original = read_git(&mut Cursor::new(encode_root(&sample_git_root())))
1648            .unwrap_or_else(|error| panic!("read original git: {error}"));
1649
1650        let mut encoded = Cursor::new(Vec::new());
1651        write_git(&mut encoded, &original).unwrap_or_else(|error| {
1652            panic!("write git: {error}");
1653        });
1654
1655        let reparsed = read_git(&mut Cursor::new(encoded.into_inner())).unwrap_or_else(|error| {
1656            panic!("re-read git: {error}");
1657        });
1658        assert_eq!(
1659            reparsed
1660                .area_properties
1661                .as_ref()
1662                .and_then(|value| value.music_day),
1663            original
1664                .area_properties
1665                .as_ref()
1666                .and_then(|value| value.music_day)
1667        );
1668        assert_eq!(reparsed.creatures.len(), original.creatures.len());
1669        assert_eq!(reparsed.doors.len(), original.doors.len());
1670        assert_eq!(reparsed.sounds.len(), original.sounds.len());
1671        assert_eq!(reparsed.waypoints.len(), original.waypoints.len());
1672        assert_eq!(reparsed.placeables.len(), original.placeables.len());
1673        assert_eq!(
1674            reparsed
1675                .creatures
1676                .first()
1677                .and_then(|value| value.template_resref.as_deref()),
1678            original
1679                .creatures
1680                .first()
1681                .and_then(|value| value.template_resref.as_deref())
1682        );
1683        assert_eq!(
1684            reparsed
1685                .doors
1686                .first()
1687                .and_then(|value| value.transform.bearing),
1688            original
1689                .doors
1690                .first()
1691                .and_then(|value| value.transform.bearing)
1692        );
1693        assert_eq!(
1694            reparsed
1695                .sounds
1696                .first()
1697                .and_then(|value| value.sounds.first())
1698                .and_then(|value| value.sound.as_deref()),
1699            original
1700                .sounds
1701                .first()
1702                .and_then(|value| value.sounds.first())
1703                .and_then(|value| value.sound.as_deref())
1704        );
1705        assert_eq!(
1706            reparsed
1707                .waypoints
1708                .first()
1709                .and_then(|value| value.transform.y_orientation),
1710            original
1711                .waypoints
1712                .first()
1713                .and_then(|value| value.transform.y_orientation)
1714        );
1715        assert_eq!(
1716            reparsed
1717                .placeables
1718                .first()
1719                .and_then(|value| value.static_object),
1720            original
1721                .placeables
1722                .first()
1723                .and_then(|value| value.static_object)
1724        );
1725    }
1726
1727    #[test]
1728    fn build_git_root_preserves_unknown_fields_from_raw_entries() {
1729        let mut parsed = read_git(&mut Cursor::new(encode_root(&sample_git_root())))
1730            .unwrap_or_else(|error| panic!("read original git: {error}"));
1731        parsed
1732            .creatures
1733            .first_mut()
1734            .unwrap_or_else(|| panic!("creature should exist"))
1735            .raw
1736            .put_value("CustomField", GffValue::Int(1234))
1737            .unwrap_or_else(|error| panic!("insert custom field: {error}"));
1738
1739        let root = build_git_root(&parsed).unwrap_or_else(|error| {
1740            panic!("build git root: {error}");
1741        });
1742
1743        let creature_list = match root
1744            .root
1745            .get_field("Creature List")
1746            .map(|field| field.value())
1747        {
1748            Some(GffValue::List(creatures)) => creatures,
1749            other => panic!("expected creature list, got {other:?}"),
1750        };
1751        assert_eq!(
1752            creature_list
1753                .first()
1754                .and_then(|creature| creature.get_field("CustomField"))
1755                .map(|field| field.value()),
1756            Some(&GffValue::Int(1234))
1757        );
1758    }
1759}